💡

Golangで作るソフトウェアルータ をやってみた

2023/12/03に公開

はじめに

Golangで作るソフトウェアルータ
という面白そうな記事が販売されていたのでやってみた。

自作ソフトウェアルータを作ってみようという記事です。
各ネットワークレイヤーのプロトコルでどんな処理をしているか?
どんか感じで上位から下位にパケットを渡しているのか?
どうやってインターネットへパケットを送っているのか、また受信しているのか?

なんて部分を実装しながら学べました。

0.準備の準備

まず、開発をするにあたってある程度のネットワークの知識があることが必要とされるので、マスタリングTCP/IP―入門編は一読した方がいいと思います。
新人なら読んでおけとよく言われる本ですが、正直すんなり理解できる人は少ないのではないでしょうか?
というのも、実際にTCP/IPと触れるような機会がある人は世間一般に行ったら少なく、当時の私も「目は通したけどそれで何???」みたいな感想を持ちました。実際、L2とL3スイッチでネットワークを構成してみた、TCPダンプで取得したパケットをワイヤーシャークで解析してみたなどの経験を持つ人は少人数で、現実とリンクしないがために理解ができなかった人は多かったのではないでしょうか?
今回手を動かしたことで、イーサネット、ACP、TCP/IPなどもHTTP以外の通信プロトコルも多少親近感が湧いてきたように思えるので、同じような感想を持った方がいれば是非開発に挑戦してみてください。

ネットワーク系のコマンドを覚えておくと便利。資格でいととCCNAもしくはネットワークスペシャリスト。私はどちらも持っていないですが、中途半端に取得を目指していたこともあってので雰囲気だけわかりました。

1.開発準備

Linuxのシステムコールを環境作成のスクリプトやgoで使っているので、LinuxもしくはWSLの環境で開発することを筆者は勧めています。
私はmacで開発したので、dockerにて当該の環境を構築しました。

ネットワーク環境

Linuxのnetwork namespace機能を利用して擬似的にhostやrouterを設置していきます。
この記事にまとめられていました。

namespaceを作成する
ip netns add <namespace>

LAN接続された仮想Ethernetインターフェースのペアを作成する
ip link add name <インターフェイス名> type veth peer name <インターフェイス名>

インターフェイスを特定のnamespaceに割り当てる
ip link set <インターフェイス名> netns host1 <namespace>

インターフェイスにIPアドレスを設定
ip netns exec <namespace> ip addr add <IPアドレス> dev <インターフェイス>
インターフェイスを起動する
ip netns exec <namespace> ip link set <インターフェイス> up
NICのパラメータを設定
ip netns exec <namespace> ethtool -K <インターフェイス> rx off tx off
デフォルトの経路の設定
ip netns exec <namespace> ip route add default via <IPアドレス>

ルータを自作したい場合は、Ethernetのレベルでパケットを取り扱わなければいけないので、syscallを利用する必要がある。

複数のインタフェースに対して、ソケット作成してbindさせる。
ソケット作成
syscall.Socket()
ソケットにインターフェイスwobind
syscall.Bind()

さらにソケットをepollの監視対象として、epollへイベントが通知されたら、パケットを取得する
epollの監視対象とする
syscall.EpollCtl()
epollがイベントを受信する
syscall.EpollWait(()
ソケットからメッセージを取得する
syscall.Recvfrom()

epollとは、プロセスとファイルディスクリプタ(fd)の中間に位置して、I/Oの準備ができたらfdを通知する役割を持つ。こちらに綺麗にまとめられていました。

2.Ethernet/IP/ARP

Ethernetのフレームワークの構成を覚えておくことと、データ部分は上位プロトコルのフレームで構成されるので、今回はARPもしくはIPのデータで構成されます。
下位のプロトコルから当該のフレームフォーマットに沿ったbyte単位でデータを分解することを上位まで繰り返します。今回はEthernetヘッダのタイプでARPかIPを判定して、それぞれの仕様に沿ってbyte単位で分解します。また、結果は上位のプロトコルから下位のプロトコルに向かうように作成します。

IPパケットの送信

IPヘッダーとペイロードを作成してからbyte変換し、下位プロトコルのEthernetでラップして送信する。送信するにはMACアドレスが必要なので、送信元のIPアドレスをキーにARPテーブルから検索する。見つからなかった場合はARPリクエストを送信して取得後に送信する。

3.ICMP

Ethernet -> IP -> ICMPの順で受信する。送信は逆順。

4.IP ルーティングとフォワーディング

IPアドレスの登録と検索について

RadixTreeを使って,0と1のtree構造で登録して、nexthop(別ルータのIPなど)を検索・抽出する。

ルーティングについて

NICインターフェイスが持っている(要するに自分)アドレスでないか調べる。
ルーティングテーブルからnexthopのアドレスを抽出する。
ローカルホスト内の他のIPであれば、当該のhostに直接送信。
ローカルホスト外であれば、ネットーワークへ送信する。

5.NAT

インターネットに出る場合に、NATで送信元のIPアドレスをルータのIPアドレスを変換する。
インターネットから来る場合は、送信先のIPをルータのIPからローカルIPに変換する。

チェックサム

IPヘッダのアドレスやポートが壊れると他の通信にも影響が出るため、パケットに破損がないか調べる方法。
1.各ヘッダ情報を16bitごとに分割して加算
2.オーバーフローした分を加算
3.1の補数をとる
送信時には3で得た値がチェックサムを設定する。
受信時は同じ手順を行なって生成したチェックサムと一致するか比較しする

Discussion