ルーター自作本を試す環境をnetnsの仮想ネットワークで実現する

ルーター自作でわかるパケットの流れ/小俣 光之を読みました。

本文にほとんどがソースコードのためひたすら写経しました。 ちなみに後半に差し掛かったあたりで気づいたのですが、サポートページソースコードは公開されています。

「せっかく書いたのだから動かしたい!」という欲求が生まれたものの、本書に書かれているような物理機器が手元になかったので、netnsで仮想ネットワークを構築しました。

構成

ネットワーク構成図は以下のとおりです。

f:id:cipepser:20180603143611p:plain

環境

手元にあった環境のため古いですが、vagrantで立てたlinux環境を使いました。

$ cat /etc/redhat-release
CentOS release 6.6 (Final)

netnsを使ってネットワークを構築する

基本的にroot権限が必要です。

namespaceを区切る

hostRTNextRouterをnamespaceを区切ることで実現します。

sudo ip netns add host
sudo ip netns add RT
sudo ip netns add NextRouter

以下で作成されたことが確認できます。

$ ip netns show
RT
host
NextRouter

ケーブルをつなぐ

Linux Network Namespaceでは、vethを物理ケーブルだと思うとわかりやすいです。 構成図の通り繋いでいきます。
命名規則<ホスト名>_<I/F名>としました。NextRouterだけ文字制限なのか設定できなかったのでNRと略しています。 I/Fは、本書中でルータのLAN側をeth0、WAN側をeth1と指定されていたので、その他の機器でも統一しています。

sudo ip link add host_veth1 type veth peer name RT_veth0
sudo ip link add RT_veth1 type veth peer name NR_veth0
sudo ip link add NR_veth1 type veth peer name Linux_veth0

上記でケーブルは繋いだので、各機器に所属させます。

sudo ip link set host_veth1 netns host
sudo ip link set RT_veth0 netns RT
sudo ip link set RT_veth1 netns RT
sudo ip link set NR_veth0 netns NextRouter
sudo ip link set NR_veth1 netns NextRouter

上記のip link setを実行する前は、ホストOS(CentOS)上でip link showとすると見えていたリンクが以下のようになっていると思います。

$ ip link show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
    link/ether 08:00:27:2d:82:3a brd ff:ff:ff:ff:ff:ff
10: Linux_veth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state DOWN qlen 1000
    link/ether 1a:dd:55:e7:f4:bd brd ff:ff:ff:ff:ff:ff

ifconfigでもOKですが、この時点ではifconfig -aとしないとDOWNしているI/Fは表示されないはずです。 loeth0vagrantが割り当てているのもので今回の対象でないので、Linux_veth0だけになっていることに注目してください。 言い換えると、host_veth1RT_veth0RT_veth1NR_veth0NR_veth1がそれぞれのnamespaceに所属したので、ホストOS(CentOS)から見えなくなっています。

IPアドレスを採番する

/24単位でセグメントを分ける設計としました。また、hostは拡張性を見据え(今は関係ないですが。。。)、若番から採番します。 next hopになる機器は老番からの採番としています。

sudo ip netns exec host ip addr add 192.168.0.1/24 dev host_veth1
sudo ip netns exec RT ip addr add 192.168.0.254/24 dev RT_veth0
sudo ip netns exec RT ip addr add 192.168.1.1/24 dev RT_veth1
sudo ip netns exec NextRouter ip addr add 192.168.1.254/24 dev NR_veth0
sudo ip netns exec NextRouter ip addr add 192.168.2.1/24 dev NR_veth1
sudo ip addr add 192.168.2.254/24 dev Linux_veth0

確認コマンドのため、ここでは省略しますが、ip addr showで採番は確認できます。

I/Fを立ち上げる

各機器のI/Fをupさせます。loも忘れずに。

sudo ip netns exec host ip link set lo up
sudo ip netns exec RT ip link set lo up
sudo ip netns exec NextRouter ip link set lo up

sudo ip netns exec host ip link set host_veth1 up
sudo ip netns exec RT ip link set RT_veth0 up
sudo ip netns exec RT ip link set RT_veth1 up
sudo ip netns exec NextRouter ip link set NR_veth0 up
sudo ip netns exec NextRouter ip link set NR_veth1 up
sudo ip link set Linux_veth0 up

確認コマンドのため、ここでは省略しますが、ip link showでstateがUPになっていることが確認でき、ifconfigでも見えるようになります。

ルーティングを設定する

各機器のWAN側にデフォルトゲートウェイを設定します。
ホストOS(CentOS)とNextRouterには、LAN側へstatic routeを設定します。

  • ホストOS(CentOS): 192.168.0.0/24, 192.168.1.0/24
  • NextRouter: 192.168.0.0/24

Direct Connectedになるセグメントはルーティング不要です。

sudo ip netns exec host ip route add default via 192.168.0.254
sudo ip netns exec RT ip route add default via 192.168.1.254
sudo ip netns exec NextRouter ip route add default via 192.168.2.254
sudo ip netns exec NextRouter ip route add 192.168.0.0/24 via 192.168.1.1
sudo ip route add 192.168.0.0/24 via 192.168.2.1
sudo ip route add 192.168.1.0/24 via 192.168.2.1

ip_forwardを有効化する

パケット転送を行えるようにip_forwardを有効化します。 ただし、RTは自作ルータのため転送機能があるためLinuxと機能が重複しないよう無効化します。 つまり、hostとNextRouterがip_forwardを有効化する対象です。

しかしnetnsでは、ネットワークまわりの設定だけがnamespaceごとに区切られていますが、ファイルシステムは共通のため、 sudo sysctl -w net.ipv4.ip_forward=1で設定するとRTも含めて全機器で有効化されてしまいます。

netnsでは、/etc/netns/<namespace名>が優先的に読み込まれるので、 RTについては、net.ipv4.ip_forward=0とした/etc/sysctl.conf/etc/netns/RT/sysctl.confとしてコピーしておけばOKです。 デフォルト値は、0のはずなので、念の為sysctl net.ipv4.ip_forwardnet.ipv4.ip_forward = 0になっていることを確認したあと、以下のようにコピーしましょう。

sudo cp /etc/sysctl.conf /etc/netns/RT/

RT以外は共通で有効化するので以下を実行しましょう。

sudo sysctl -w net.ipv4.ip_forward=1

sysctl net.ipv4.ip_forwardnet.ipv4.ip_forward = 1になっていればOKです。

自作ルータ

自作ルータのコードは、サポートページで公開されています。 もしくは本書を見ていただくのがよいと思います。

パラメータ

main.c中のパラメータは以下のように設定します。

PARAM Param = {"RT_veth0", "RT_veth1", 1, "192.168.0.254"};

実行してみる

hostからホストOS(CentOS)のveth0(192.168.2.254)pingを打ってみましょう。

先に自作ルータを 起動せずpingを打ってみましょう。

sudo ip netns exec RT bash #hostにログイン
$ ping 192.168.2.254 -c 1
PING 192.168.2.254 (192.168.2.254) 56(84) bytes of data.

--- 192.168.2.254 ping statistics ---
1 packets transmitted, 0 received, 100% packet loss, time 10000ms

予想通り、タイムアウトします。

では、いよいよ自作ルータを試します。 sudo ip netns exec RT bashでRTにログインしてからmakeした./routerを実行します。 (ログインしないとI/Fがないと怒られるはずです)

ping 192.168.2.254 -c 1
PING 192.168.2.254 (192.168.2.254) 56(84) bytes of data.
64 bytes from 192.168.2.254: icmp_seq=1 ttl=62 time=0.304 ms

--- 192.168.2.254 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.304/0.304/0.304/0.000 ms

ちゃんとping応答がありました!

ルータ側のログ

おまけです。ルータ側のログを見てみましょう。 本書に記載のものに加えていくつか自分で見やすいようにデバッグログを追加しています。 <INFO>となっているものが追加したものです(デバッグしながらだったので他にもあるかも)。

$ ./router
NextRouter=192.168.1.254
RT_veth0 OK
addr=192.168.0.254
subnet=192.168.0.0
netmask=255.255.255.0
RT_veth1 OK
addr=192.168.1.1
subnet=192.168.1.0
netmask=255.255.255.0
router start

ここまでがルータ起動処理です。RT_veth0RT_veth1を自作ルータに割り当て、NextRouterのIPを設定しています。

--------AnalyzePacket-------
<INFO> dst IP: 192.168.2.254
<INFO> subnet: 255.255.255.0
[0]:192.168.2.254 to NextRouter
----Ip2MacSearch----
Ip2Mac ADD [1] 192.168.1.254 = 0
<INFO> dst MAC: 00:00:00:00:00:00
Ip2Mac(192.168.1.254):NG
Ip2Mac(192.168.1.254):Send Arp Request
[0]:Ip2Mac:error or sending
AppendSendData:[1] 192.168.1.254 98bytes(Total=1:98bytes)

上記が192.168.2.254宛にpingを打ったログです。 192.168.2.0/24は自作ルータのI/Fが持っているセグメントではないので、NextRouterへパケットを転送しようとしています。 しかし、NextRouterのIP192.168.1.254に紐づくMACアドレスを知らないので、Arp Requestを投げ、pingパケットはバッファに積んでいます。

--------AnalyzePacket-------
[1]recv: ARP REPLY: 42bytes
----Ip2MacSearch----
AppendSendReqData:[1] 0
Ip2Mac EXIST [1] 192.168.1.254 = 0
Ip2Mac(192.168.1.254):OK
dst MAC(Ip2Mac): 7e:85:60:2f:76:fb
GetSendReqData:[1] 0
GetSendData:[1] 192.168.1.254 98bytes
iphdr.ttl 64->63
write:BufferSendOne:[1] 98bytes

Arp Replyが返ってきました。これでNextRouterのMACアドレスを知ることができたのでバッファにあったpingパケットを[1](RT_veth1)から送出しています。 L3ホップになるのでttlも減算しています。

--------AnalyzePacket-------
<INFO> dst IP: 192.168.0.1
<INFO> subnet: 255.255.255.0
[1]:192.168.0.1 to TargetSegment
----Ip2MacSearch----
Ip2Mac ADD [0] 192.168.0.1 = 0
<INFO> dst MAC: 00:00:00:00:00:00
Ip2Mac(192.168.0.1):NG
Ip2Mac(192.168.0.1):Send Arp Request
[1]:Ip2Mac:error or sending
AppendSendData:[1] 192.168.0.1 98bytes(Total=1:98bytes)

宛先が192.168.0.1となっていることがからもわかるように、pingの戻りパケットです。 先程と同じようにArp RequestでIP-MACアドレス解決を図っています。

--------AnalyzePacket-------
[0]recv: ARP REPLY: 42bytes
----Ip2MacSearch----
AppendSendReqData:[0] 0
Ip2Mac EXIST [0] 192.168.0.1 = 0
Ip2Mac(192.168.0.1):OK
dst MAC(Ip2Mac): 02:01:1d:da:db:04
GetSendReqData:[0] 0
GetSendData:[0] 192.168.0.1 98bytes
iphdr.ttl 63->62
write:BufferSendOne:[0] 98bytes

Arp Reply192.168.0.1に紐づくMACアドレスが学習できたので、パケットを送出します。 ここでも、L3ホップになるのでttlも減算しています。

これらの処理が行われた結果、hostでもping応答を確認できたわけです。

最後に

Cでechoサーバやチャットサーバを書いたことがあったものの、写経時にtypoが多すぎてデバッグに時間取られました。 奇しくも動作をより理解する結果に繋がったので悪くはなかったかなと思っています。 本記事のnetnsも普段dockerがいい感じにやってくれる部分を知ることができたので、いい勉強になりました。

ちなみに、本記事タイトルは、本書タイトルそのままだと長くなりすぎなので、略しましたが通称あるんでしょうか。

ルーター自作でわかるパケットの流れ

ルーター自作でわかるパケットの流れ

References