7.4. TCPコネクション

ここからのセクションでは、基本的プロトコルである TCP, UDP, ICMP それぞれについて、ステートと、それがどのように処理されるかをつぶさに見ていくことにする。さらに、デフォルト つまり、これら 3 つのプロトコルに類別されない場合どうなるのかについても掘り下げる。まずは TCP プロトコルから始めることにした。というのも、 TCP はそれ自体ステートフルなプロトコルであるし、 iptables のステート機構に関わる興味深い特性がたくさんあるからだ。

TCP コネクションは常に 3 ウェイハンドシェークによって開始される。このハンドシェークによって、その先実際のデータを乗せることになるコネクションを確立およびネゴシエート (交渉) する。セッションは、まず SYN パケット、次に SYN/ACK パケットが続き、最後に、セッション確立を承認する ACK パケットが送られて始まる。この時点でコネクションは確立し、データが送れる状態となる。問題は、コネクション追跡がこれにどう追従していくかだ。当然の成り行きといえるだろう。

ユーザの目から見る限り、コネクション追跡はどのコネクションタイプでも基本的には同じように動作する。下図は、コネクションの段階を追う毎にストリームのステートがどう変わってゆくかを端的に示している。ご覧の通り、ユーザの目から見た場合、コネクション追跡が示すコードは、 TCP コネクションの手続きを文字通りに反映したものとはなっていない。まずひとつのパケット (SYN パケット) を検出すると、コネクション追跡はこの接続を NEW と判定する。返答パケット (SYN/ACK) を検出すると ESTABLISHED と判定する。一瞬考えれば、理由は分かるだろう。こうした特別な実装方法を採ることによって、 NEWESTABLISHED がネットワークから出ていくのを許可しつつ、戻ってくることができるのは ESTABLISHED コネクションだけに絞ったままで、コネクションは完璧に成り立つのだ。では逆に考えてみよう。もしも、コネクション追跡機構がコネクションの確立過程をひとまとめに NEW と判断してしまったとしたら、返ってくる NEW ステートのパケットもすべて許可しない限り、我々のローカルネットワークへ向けた外からの接続を阻止することは事実上不可能だ。さらに物事をややこしくしているのは、カーネルの内部では、 TCP コネクションに特有のいくつものステートが存在するという点だ。ただし、それらのステートはユーザ空間では利用できない。内部ステートは概ね、 RFC 793 - Transmission Control Protocol の 21-23 ページで規定されたステートに倣っている。これについては、このセクションの後半で考察することにしよう。

見て分かるように、ユーザの視点から見た場合、ことは非常に単純だ。しかし、カーネル側に視点を移して全体の構造を見ると、やや難解さを増す。例を見てみよう。 /proc/net/ip_conntrack テーブルでコネクションのステートが実際にどう変わってゆくかを考察してみる。最初のステートは、コネクションで最初の SYN パケットを受け取った時点だ。

tcp      6 117 SYN_SENT src=192.168.1.5 dst=192.168.1.35 sport=1031 \
     dport=23 [UNREPLIED] src=192.168.1.35 dst=192.168.1.5 sport=23 \
     dport=1031 use=1
   

上記エントリから読み取れる通り、 SYN パケットは送信済み (SYN_SENT フラグがセットされている) で、それに対する返答はまだ受け取っていない ([UNREPLIED] フラグが裏付けている) という、文字通りそのままのステートになっている。次のステートは反対方向のパケットを検出した時に訪れる。

tcp      6 57 SYN_RECV src=192.168.1.5 dst=192.168.1.35 sport=1031 \
     dport=23 src=192.168.1.35 dst=192.168.1.5 sport=23 dport=1031 \
     use=1
   

今、こちらからの送信に呼応した SYN/ACK を受け取ったところだ。このパケットを受信した瞬間に、今度はステートが SYN_RECV に変わる。SYN_RECV が示すのは、冒頭の SYN の配信が正常に完了し、返答の SYN/ACK パケットもファイヤーウォールを無事に通った、という意味だ。これに加えて、このコネクション追跡エントリは既に双方向のトラフィックを検出済みなので、即ち返答受け取り済みとなる。ただしこれは明示されない。前述の [UNREPLIED] とは違って暗黙的だ。最後のステップは、3 ウェイハンドシェークの締めとなる ACK を検出した時点でやってくる。

tcp      6 431999 ESTABLISHED src=192.168.1.5 dst=192.168.1.35 \
     sport=1031 dport=23 src=192.168.1.35 dst=192.168.1.5 \
     sport=23 dport=1031 [ASSURED] use=1
   

最後の例の時点において、我々は 3 ウェイハンドシェークの最後の ACK を受け取り、コネクションは、少なくとも iptables 内部メカニズムの範疇では ESTABLISHED ステートに移行した。通常、ストリームはこの時すでに ASSURED になっている。

コネクションが [ASSURED] にならずに ESTABLISHED ステートに移行することもある。コネクションピックアップ (pickup) をオンにした場合だ (tcp-window-tracking パッチを適用して ip_conntrack_tcp_loose の値を 1 以上に設定する必要あり)。デフォルトの状態つまり tcp-window-tracking を適用していない場合には、前記のような振る舞いをするはずであり調整は不能だ。

TCP コネクションが閉じられる際には、以下のような流れを採り、ステートは以下のようになる。

ご覧の通り、本当にコネクションが閉じるのは、最後の ACK が送られてからだ。ただしこの図は、あくまでも、接続が正常にクローズされる場合に限った図式だ。例えば、接続が拒否された場合には RST (リセット) の送信によって閉じられたりする。その場合、接続は即座にクローズする。

TCP コネクションが閉じると、その接続は TIME_WAIT ステートに移行する。デフォルトの待ち時間は 2 分間だ。これは、何らかの障害に遭遇したパケットが、コネクションが既にクローズした後でもルールセットに入って来られるようにするための仕組みだ。これは一種の時間的緩衝域 (buffer time) として働き、パケットがどこかの中継ルータで渋滞に捕まってしまった場合でも、こちらのファイヤーウォールや相手へ到達できるようになっている。

RST パケットによってコネクションがリセットされた場合には、ステートは CLOSE に移行する。この意味するところは、接続が完全にクローズされるまで猶予 (デフォルトは 10 秒) を与えるというものだ。 RST パケットは、いかなる場合にも承認される (acknowledged) ことはない。ただ一方的にコネクションを切断するだけだ。ステートには、解説してきたもの以外にもいくつかの種類がある。 TCP ストリームが採り得るステートを、タイムアウトの値とともに紹介しておこう。

Table 7-2. 内部ステート

Stateタイムアウトの値
NONE30 分
ESTABLISHED5 日
SYN_SENT2 分
SYN_RECV60 秒
FIN_WAIT2 分
TIME_WAIT2 分
CLOSE10 秒
CLOSE_WAIT12 時間
LAST_ACK30 秒
LISTEN2 分

これらの値は、まったくもって普遍的とは言い難い。カーネルのリビジョンによっても変わるし、proc ファイルシステムの変数 /proc/sys/net/ipv4/netfilter/ip_ct_tcp_* の操作によって変更することもできる。とはいえ、デフォルトの値はかなり熟考されており、理にかなった値になっている。値は秒単位で記述される。初期のバージョンのパッチでは (バグにより) ジッフィ [:jiffy: 1/100 秒] が用いられていた。

Note

もうひとつ心に留めておかなくてはならないことがある。ユーザ空間でのステート機構は TCP パケットに記載された TCP フラグ (RST, ACK, SYN といったもの) を見ないという点だ。これは大抵の場合、悪い結果を招く。というのも、NEW ステートのパケットをファイヤーウォールを通過させたい場面があるだろうが、その際、 NEW フラグを指定しておいて SYN パケットのことを指しているつもりになっているケースが多々見受けられるのからだ。

これは現在のステート機構の実装にはできない相談だ。実際には、たとえどんな TCP フラグも立っていないパケットだとしても、あるいは ACK フラグの立ったパケットだとしても、 NEW の判定が下されてしまう。この仕組みは冗長化 (redundant) ファイヤーウォールなどに用いられるが、一般的に言って、ファイヤーウォールがひとつしかないようなホームネットワークにおいては非常にまずい。こうした挙動を手当てするには、付録 "よくある問題と質問" の "NEWステートでありながらSYNビットの立っていないパケット" で説明しているコマンドを使う。また別のやり方としては、patch-o-matictcp-window-tracking 拡張機能をインストールした上で /proc/sys/net/ipv4/netfilter/ip_conntrack_tcp_loose にゼロを設定する方法がある。こうすれば、ファイヤーウォールは NEW パケットのうち SYN フラグのセットされているもの以外は全てドロップするようになる。