自作IPv6ルータをgoで書き直してみた

2024/01/23に公開

Interfaceの2024年2月号で自作のIPv6専用ルータがc++で実装されており、これをgolangで再実装してみた。

事前学習

本編に入る前に、必要な前提知識を書いておく。
詳細はInterface2月号に丁寧な説明があるので(ぜひ購入してみてください)、ここではキーワードだけ記載する。

  • ネットワークの基礎知識
    • インターネットの階層構造
    • TCP/IPのパケットの構造
    • イーサネットフレーム
  • ネットワークコマンドの基礎
    • ipコマンド関連
    • tcpdump
  • IPv6
    • IPv4と比較
    • 近隣探索(NA、NDパケット)
    • ICMPv6
    • チェックサムと擬似ヘッダ
    • NDテーブル
    • パトリシアトライ木
  • linux関連
    • epoll
    • ファイルディスクリプタ

本編

実装はgithubで公開している。

宛先のNDテーブルとフォワーディングテーブルが存在する場合のフローチャート。
NDテーブルを探索して見つからない場合は、フォワーディングテーブル(パトリシアトライ木)を登録する処理などが加わるのでもう少し複雑になる。実装内では加味されているがフローチャートに乗せるとごちゃごちゃになるので省略。

ルータの起動

実行時の出力を見てみる。

make run
# router1のネットワークでバイイスの一覧。ip l コマンドを実行した結果と同じ。
# 今回は.IPv6を設定したrouter1-host1とrouter1-router2を扱う。
interfaces: [{Index:1 MTU:65536 Name:lo HardwareAddr: Flags:loopback} {Index:2 MTU:1480 Name:tunl0 HardwareAddr: Flags:0} {Index:3 MTU:1452 Name:ip6tnl0 HardwareAddr: Flags:0} {Index:4 MTU:1500 Name:router1-host1 HardwareAddr:b2:6a:c5:1e:c1:95 Flags:up|broadcast|multicast|running} {Index:7 MTU:1500 Name:router1-router2 HardwareAddr:a6:10:25:8e:cc:30 Flags:up|broadcast|multicast|running}]

# ip6tnl0という既存のNICをsocket APIにbindしているが、今回は扱わない。
socket file descriptor: 4
bind nic: {Index:3 MTU:1452 Name:ip6tnl0 HardwareAddr: Flags:0}

# router1-host1をsocket APIにbindして有効化する。
# 今回のプログラムで管理するネットワークデバイスの対象。
socket file descriptor: 5
bind nic: {Index:4 MTU:1500 Name:router1-host1 HardwareAddr:b2:6a:c5:1e:c1:95 Flags:up|broadcast|multicast|running}
effective netDevice, name is router1-host1, socketFd is 5

# router1-router2をsocket APIにbindして有効化する。
socket file descriptor: 6
bind nic: {Index:7 MTU:1500 Name:router1-router2 HardwareAddr:a6:10:25:8e:cc:30 Flags:up|broadcast|multicast|running}
effective netDevice, name is router1-router2, socketFd is 6

NICと紐付けたsocketAPIをオープンしてepollがイベントを管理してるが、linux上どうなっているのか見てみる。
goで動かしたプロセスのIDは974。/proc/<プロセス番号>/fd でファイルディスクリプタを確認できる。見ての通りファイルディスクリプタの実態はシンボリックリンクで、4と5と6番はsocketAPIと紐づいていることがわかる。

ファイルディスクリプタ
# ps aux | grep main
root       974  111  0.1 1598032 6944 pts/4    Rl+  02:44  27:48 ./cmd/main/main.exe
# pwd
/proc/974/fd
#  ls -la
total 0
dr-x------ 2 root root 10 Jan 23 02:44 .
dr-xr-xr-x 9 root root  0 Jan 23 02:44 ..
lrwx------ 1 root root 64 Jan 23 02:45 0 -> /dev/pts/4
lrwx------ 1 root root 64 Jan 23 02:45 1 -> /dev/pts/4
l-wx------ 1 root root 64 Jan 23 02:45 10 -> 'pipe:[27188]'
lrwx------ 1 root root 64 Jan 23 02:45 2 -> /dev/pts/4
lrwx------ 1 root root 64 Jan 23 02:45 3 -> 'anon_inode:[eventpoll]'
lrwx------ 1 root root 64 Jan 23 02:45 4 -> 'socket:[27182]'
lrwx------ 1 root root 64 Jan 23 02:45 5 -> 'socket:[27184]'
lrwx------ 1 root root 64 Jan 23 02:45 6 -> 'socket:[27186]'
lrwx------ 1 root root 64 Jan 23 02:45 8 -> 'anon_inode:[eventpoll]'
lr-x------ 1 root root 64 Jan 23 02:45 9 -> 'pipe:[27188]'

疎通

ICMPエコー応答を確認してみる。
router1に対してpingを実行し、同ネットワーク内で疎通ができることを確認する。

route1にping
# ping -c 1 2001:db8:0:1001::1

PING 2001:db8:0:1001::1(2001:db8:0:1001::1) 56 data bytes
64 bytes from 2001:db8:0:1001::1: icmp_seq=1 ttl=255 time=0.408 ms

router1の出力から、echo requestを正常に受信できていることがわかる。

router1の出力(一部抜粋)
received icmpv6 code=0, type=128
received echo request id=24538 seq=1

近隣要請パケットによるMACアドレス検索

router1に近隣要請(NS)パケットを送信してみるとMACアドレスを取得できる。

router1にndisc6
# ndisc6 2001:db8:0:1001::1 host1-router1

Soliciting 2001:db8:0:1001::1 (2001:db8:0:1001::1) on host1-router1...
Target link-layer address: B2:6A:C5:1E:C1:95 from 2001:db8:0:1001::1

router1の出力から、NSパケットを受信していることがわかる。デバックはしていないが応答として近隣広告(NA)パケットを作成して返信している。また、NDテーブルでIPとMACアドレスを紐づけて登録している。

router1の出力(一部抜粋)
multicast. ip is ff02::1:ff00:1
received icmpv6 code=0, type=135
icmpv6 NS packet. targetAddr is 2001:db8:0:1001::1
update ND table. macAddr is 4E:82:3C:5F:CD:5D, ipAddr is fe80::4c82:3cff:fe5f:cd5d

離れたネットワークとの疎通

host2にpingを実行してみる。

host2にping
# ping -c 1 2001:db8:0:1002::2
PING 2001:db8:0:1002::2(2001:db8:0:1002::2) 56 data bytes
64 bytes from 2001:db8:0:1002::2: icmp_seq=1 ttl=62 time=1.02 ms

--- 2001:db8:0:1002::2 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 1.022/1.022/1.022/0.000 ms

router1の出力を確認していく。
別のネットワークのIPアドレスであると判定されてフォワーディングされていることがわかる。

router1の出力
# host1送信されたパケット
Received 118 bytes from router1-host1: b26ac51ec1954e823c5fcd5d86dd6005e67a00403a4020010db800001001000000000000000220010db80000100200000000000000028000dd97e0a800016e33af6500000000655e040000000000101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f3031323334353637
# router1に対する通信ではないのでフォワーディングを行う
start forwarding!
# パトリシアトライ木からフォワーディング先のネットワーク情報がみつかる
find to next hop node. address id 2001:db8:0:1000::2
forwarding ipv6 packet to network
found nd entry to next hop!
sending ethernet frame type 86dd from A6:10:25:8E:CC:30 to B6:F0:2C:2F:AB:4A
# router2を経由してhost2から返却されたパケット
Received 118 bytes from router1-router2: a610258ecc30b6f02c2fab4a86dd6009b7d700403a3f20010db800001002000000000000000220010db80000100100000000000000028100f6c1a3dd0001e532af65000000001000050000000000101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f3031323334353637
ether type is ipv6, netDev is &{name:router1-router2 macAddr:[166 16 37 142 204 48] socketFd:6 sockAddr:{Protocol:768 Ifindex:7 Hatype:0 Pkttype:0 Halen:0 Addr:[0 0 0 0 0 0 0 0] raw:{Family:17 Protocol:768 Ifindex:7 Hatype:0 Pkttype:0 Halen:0 Addr:[0 0 0 0 0 0 0 0]}} ethHeader:0xc0000a2340 ipv6Dev:0xc00008e240}, dstMacAddr is A6:10:25:8E:CC:30
start forwarding!
# パトリシアトライ木からネットワークデバイス情報がみつかる
find to host node. address id 2001:db8:0:1001::1
forwarding ipv6 packet to host
trying ipv6 output to host, find nd record to 2001:db8:0:1001::2
sending ethernet frame type 86dd from B2:6A:C5:1E:C1:95 to 4E:82:3C:5F:CD:5D

まとめ

ネットワークの基礎を一から勉強したり、普段触れないlinuxのepollやsocketの実装をするいい機会になって大変良かったです。
疎通して通らないからtcpdumpしてここのbyte構成がおかしいみたいなのをちょっとずつ調べながら修正する時間が一番楽しかったかもしれません。本当はユニットテストを実装してTDDみたいな形で実装できれば一番いいと思うので次回やってみようと思います。

Discussion