TCP/IPをGoで自作しました。
タイトルにある通りですが、TCP/IPのプロトコルスタックをGoで実装しました。デバイス, IP, ARP ...などと実装してとりあえずTCPの最低限の機能までは実装したため、 ここに書いておこうと思います。つくったものは以下になります。
経緯
大学の授業やhttpあたりを勉強しているとTCP/IPという単語が登場します。マスタリングTCP/IPを一通り読みましたが、実装することでわかることは多いだろうということで実装してみました。実装に当たっては
3月に開催したプロトコルスタック自作キャンプの講義資料を公開しました。1週間でTCP/IPのプロトコルスタックを自作してUDPやTCPで通信するアプリケーションを動かすという内容で300ページくらいのスライドです。これがあれば一人で自作できますよ! #KLabExpertCamphttps://t.co/4sUTh2MAk6
— YAMAMOTO Masaya (@pandax381) 2021年4月20日
を参考にさせていただきました。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ができた...」と感動を覚えました。
しかし、現状では「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についての知識が得られました。一石三鳥くらいあると思うので面白そうだと思った方はぜひやってみてはどうでしょうか。