RustでL2からTCPプロトコルを自作する

マスタリングTCP/IPは何度か読んではいるけれど、レイヤーとかいわれても面白くないし、ネットワークあんまりわかってない気がしていたので、もうちょっと理解するために、L2レイヤであるEthernetからL4のTCPまでをRustでだいたい自作してみた。だいたいというのはRustのpnetを使ってさぼった部分があるから。パケットにデータ詰める部分は別に自作する必要もないと思ったし。

やっぱり手を動かすと座学だけではなかなか気づきにくいことに気づけた気がするので、よかったとは思う。ただし、一人でやると時間は結構溶けると思う。

とりあえず、やってみての気づきなど。

各ヘッダのサイズに従った配列を生成しておく必要があること。

これはpnetの問題かもしれないが、イーサネット、IP,TCP,UDP、ICMPなどのプロトコル毎にヘッダがあるが、そのヘッダの長さ分のバッファを用意する必要がある。

パケットのサイズ

ACKなどはpayloadのサイズ分通りseqを進めるのに対して、SYNとFINのみpayloadが0でもseqを1進める必要があること。再送対象のパケットもこのサイズが0じゃないものなので、一緒に考えるとよい。 逆に言えば、サイズ0のACKはロストしても基本的に再送されない。

再送時のACK

再送するパケット、ACK差し替えた方が効率よさそうだけど、最初おくったときのものをそのままでもよさそう。

ACK of FIN

FINというのは、これ以上送信しないことを示すフラグ。FINのACKというのは最後に送ったFINまですべてACKされたことが確認できるということ。ACK | FINフラグではない。

フラグ

SYNフラグは接続要求のときに送信するフラグだけれども、SEQの初期値を同期するためのフラグである。 ACKフラグはACKフィールドが有効であることを示す。そのため、コネクションが確立された後はFINなどもすべてACKも立てる。

ウィンドウプローブ

受信済みのACKを再送してもパケットは破棄される?ので、Windowサイズの変更が通知されることを期待して1オクテット?のパケットを送る。

パケットのロス検出

  1. 同じACKを4回連続で受け取ったとき。ただし最新のSEQまでACKを受け取っているときはその限りではないはず。
  2. 再送時間が経過したとき。これは結構輻輳していると判断される。ただし、最後のパケットがロスした場合も同じ扱いになってしまう。

リロード

通常のクライアント側のポートはランダムに決定するので、リロードするたびに別のポートでコネクションが張られる。そのため、前に接続したコネクションがしばらくTIME-WAITのまま残っていてもほとんど影響は出ない。

3ハンドシェイクしようとすると勝手にKernelがRSTを送ってしまう。

Kernelからすれば、自分が管理してないところでACK受け取ったら拒否するのは当然なのだけれど、なんとかしないと通信できない。 Network Namespaceで新しい仮想のノードを作成し、そのノードではRSTフラグをフィルタリングするようにiptablesを設定することで、KernelからのRSTのみフィルタリングできるようにした。iptablesの設定はL3だが、自作TCPが送信するパケットはL2なので、自作TCPは影響を受けないので都合がよい。