TCP・UDP・ICMP — 「つながらない」の三つの顔
一言でいうと
TCPはSYN・SYN-ACK・ACKの3回で接続を開き、FINで閉じ、閉じたポートはRSTで答えます。UDPは接続がなく、閉じたポートはICMP port unreachableで知らせます。MTUを超えるパケットは、フラグメント化されるか(DFなし)、破棄され(DFあり)、その事実を知らせるのもICMPです。
なぜ必要なのか
「接続できない」という1つの文に、原因の異なる3つが混ざっています。SYNを送ったのにRSTが返ってくれば、相手は生きていて、そのポートには誰もいません。何も返ってこなければ、ファイアウォールが捨てたか、経路がありません。SYN-ACKまで来たのにデータが来なければ、アプリケーションが止まっています。ユーザーには3つとも「つながらない」ですが、直す人はまったく違います。フラグを読めるようになれば、最初の30秒でその3つが分かれます。
どう動くのか
TCPのハンドシェイクが3回なのは、双方が、お互いの開始シーケンス番号を確認したという確認まで受け取る必要があるからです。
先に閉じた側は、TIME-WAITに60秒とどまります。最後のACKが失われたときに再送する責任と、遅れて届く古いセグメントが新しい接続に混ざらないように4タプルを封印しておく責任があるからです。短い接続を毎秒数千回開くクライアントは、これによりエフェメラルポート(
ip_local_port_range、通常は32768–60999)が枯渇します。クライアントのポートはカーネルがこの範囲から選び、サーバーのポートだけが取り決めです。
UDPは、送ったら終わりです。相手が聞いているかも、受け取ったかもわかりません。閉じたポートに送ると、カーネルがICMP port unreachableを返しますが、ファイアウォールがICMPをまるごとブロックしていると、その答えも消えて「応答なし」だけが残ります。DNSが再試行を自分で行う理由です。
MTUは、リンクが運べるIPパケットの最大サイズです。イーサネットの1500からIPヘッダーの20とICMPヘッダーの8を引くと、pingのペイロードは1472までです。-M doでDFをオンにすると、超えた瞬間にmessage too longになり、オフにするとフラグメント化されます。フラグメント化はできますが、1つ失っただけで全体を再送する必要があり、ファイアウォールは後ろのフラグメントのポートを見られません。そのため最近はDFをオンにして経路MTUを探しますが(PMTUD)、その信号であるICMPのfragmentation neededをファイアウォールがブロックすると、小さなパケットだけが通り、大きなパケットだけが消えるという障害になります。
現場での姿
SSHは通るのにファイル転送だけ止まる。VPNを新しく導入してからです。トンネルヘッダーが付いて実際のMTUが1500より小さくなったのに、DFパケットがそれを超え、ICMPはブロックされています。プロンプトのような小さなパケットは通り、本体だけが消えます。ping -M do -s 1472でどこで壊れるかを探せば、わかります。
エフェメラルポートの枯渇。プロキシサーバーが、毎秒数千個の短い接続を後段へ開きました。ss -sにTIME-WAITが2万個あり、新しい接続がCannot assign requested addressで失敗します。答えは、keep-aliveで接続を再利用することです。
接続が切れる3つの方式
「接続が切れた」にも原因が複数あり、それぞれ別の場所で直します。
FINによる正常終了。片側が「もう送るものがない」と知らせたものです。アプリケーションがソケットを閉じたか、プロセスが正常終了したという意味なので、ネットワークではなく、そちらのコードや再起動を見る必要があります。
RSTによる強制終了。相手が「この接続はない」と答えたものです。プロセスが突然死んだとき、途中の機器がセッションテーブルからその接続を消したとき、アプリケーションが読んでいないデータが残ったままソケットを閉じたときに出ます。
何の信号もなく静かに止まる。最も厄介です。双方とも接続が生きていると信じているのに、実際には途中で切れています。よくある原因が、NATやファイアウォールのセッションの期限切れです。こうした機器は、しばらくトラフィックがない接続をテーブルから消しますが、消した事実を双方に知らせません。そのため、数分間静かだったデータベース接続が、次のクエリでいつまでも答えを返さなくなります。
この静かな切断を防ぐ方法が、定期的に何かを送ることです。TCP自体のkeepaliveはデフォルトが2時間で、ほとんどのセッションの期限切れよりずっと遅いので、値を短くするか、アプリケーション層で独自の信号を送ります。コネクションプールが「貸し出す前に生きているか確認する」機能を持っているのも、同じ問題への答えです。
症状で3つを見分ける方法は簡単です。即座にエラーが出るならFINかRSTで、何も起きないまま長く待たされてからタイムアウトが出るなら、静かな切断です。そして静かな切断は、アイドル時間が長かったあとの最初のリクエストでだけ現れるという特徴があるため、「朝の最初のリクエストだけ失敗する」といった報告として上がってきます。
次のラボですること
lo上でncのサーバーとクライアントを立て、tcpdumpでハンドシェイク・終了・RSTを捕まえ、TIME-WAITとエフェメラルポートをssで見ます。そのあと2つのネームスペースの間でICMP echoを捕まえ、ping -M doでMTUの境界を探し、3000バイトのpingがフラグメント化される様子と、閉じたUDPポートのunreachableをキャプチャします。