Site cover image

Site icon imageYet Another Blog

Yet Another Blog

Post title iconTCP/IPをGoで実装する

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

経緯

大学の授業や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ができた...」と感動を覚えました。

Image in a image block

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

知見、苦労したところ

デバイスまわり

まず最初の関門がデバイス周りでした。デバイスとしてはLinuxのtapデバイスを用いています。tapデバイスについて書かれた本家のドキュメントはおそらhttps://www.kernel.org/doc/Documentation/networking/tuntap.txt です。これとhttps://linuxjm.osdn.jp/html/LDP_man-pages/man7/netdevice.7.html を参考にしつつ、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についての知識が得られました。一石三鳥くらいあると思うので面白そうだと思った方はぜひやってみてはどうでしょうか。