TCP/IPをGoで自作しました。

タイトルにある通りですが、TCP/IPプロトコルスタックをGoで実装しました。デバイス, IP, ARP ...などと実装してとりあえずTCPの最低限の機能までは実装したため、 ここに書いておこうと思います。つくったものは以下になります。

github.com

経緯

大学の授業やhttpあたりを勉強しているとTCP/IPという単語が登場します。マスタリングTCP/IPを一通り読みましたが、実装することでわかることは多いだろうということで実装してみました。実装に当たっては

を参考にさせていただきました。Cで実装するかGoで実装するか迷いましたが、Goにもっと慣れたいというのとchannel,goroutineを使っていい感じに並行処理してみたいということ でGoで書くことにしました。

ちなみにまったく5日では終わらず、約2週間ほどかけて実装しました。実装していく中で知らなかったGoの便利なライブラリや、Linuxのデバイス周りのこと、デバイスプロトコルの抽象化の方法などネットワークとは直接関係ない部分についても知見が得られました。

実装したもの

バイスからEthernet, IPv4,ARP,ICMP,UDP,ICMPなどの主要なプロトコルの機能の一部を実装しました。デバイスなどの下のレイヤから実装していきました。最終的にTCPでthree-way hand shakeができたときは「あのthree-way hand shakeができた...」と感動を覚えました。

f:id:hedwig1001:20220220151954p:plain
three-way hand shake

しかし、現状では「TCPはヘッダのオプションを無視している、IPフラグメンテーションに対応していない」などの問題があり、実装すべきところは まだあるかなという感じです。

知見、苦労したところ

バイスまわり

まず最初の関門がデバイス周りでした。デバイスとしてはLinuxのtapデバイスを用いています。tapデバイスについて書かれた本家のドキュメントはおそらく https://www.kernel.org/doc/Documentation/networking/tuntap.txtです。これと Man page of NETDEVICEを参考にしつつ、Goのsyscallパッケージを使ってlinuxシステムコールをよぶことで実装します。

より具体的には ifreq 構造体をつくって適切なシステムコールを読んで、その情報を取り出すということを行います。この辺のデバイスシステムコールを触ったことがなかったため苦労しました。tapデバイスを理解するためには Linux bridge、Tapインタフェースとは - passacaglia自作プロトコルスタック(全体像の理解〜ARPリプライ) - おしぼりの日常を読みました。 とくに前者の記事でtapデバイスとはそもそもなにか、後者の記事でデバイスの具体的な動作について理解が深まったような気がします。

binaryパッケージ

ネットワークを勉強したことある方はご存知だと思いますが、ネットワークを流れるパケットはビッグエンディアン、多くのハードウェアはリトルエンディアンで動きます。これは32bitや16bitの数をどのような順で並べるかという話です。このバイトオーダーの変換にGoの binary パッケージが有用でした。下位プロトコルペイロードをヘッダにパースする際に 「ビッグエンディアン -> リトルエンディアン」と変換する必要があるのですが、TCPヘッダ構造体のフィールドをヘッダの上から順に並ぶようにしておき、

var hdr TCPHeader
r := bytes.NewReader(payload)
err := binary.Read(r, binary.BigEndian, &hdr)

とすることで実現できます。逆にヘッダ->バイト列の変換も簡単です。「ヘッダ <-> データ」の変換はよくでてくるので便利でした。

TCP

TCPの実装が大変でした。TCP/IP プロトコルスタックを自作した - kawasin73のブログ のブログを見て、僕もRFC793を読んでみる ことにしました。RFC793は91ページあったのですが、21ページ以降のFUNCTIONAL SPECIFICATION以降を中心に読み、58ページ以降のEvent Processingを実装していくという感じでした。

TCPはデータが送られたことを確認するためにシーケンス番号を持ちます。シーケンス番号はTCPの送ったペイロードのバイトサイズ分だけ大きくなります。実はそれだけでなく、SYNとFINを送った時にもシーケンス番号が1だけ増えます。これはSYNとFINが再送されたり、きちんとACKが返ってくることを保証するためです。これは26ページに書いてあります。

FINがあったときにシーケンス番号を増やすというこの仕様をきちんと読めていなかったためうまく動かず、micropsの方を見てバグに気づきました。 仕様は隅から隅まで読まなければ正しく実装できないのだということを実感できました。

おわりに

TCP/IPをGoで実装して、デバイス、ネットワーク、Goについての知識が得られました。一石三鳥くらいあると思うので面白そうだと思った方はぜひやってみてはどうでしょうか。