CUBE SUGAR CONTAINER

技術系のこと書きます。

BIRD と Network Namespace で RIPv2 を使ったダイナミックルーティングを試す

BIRD 1 は The BIRD Internet Routing Daemon の略で、ルーティングプロトコルを実装した OSS のひとつ。 今回は、そんな BIRD を Network Namespace と組み合わせて RIPv2 を使ったダイナミックルーティングを設定をしてみる。 なお、現在 (2023-06-16) の BIRD はバージョン 1 系と 2 系が平行してメンテナンスされているが、今回使うのはバージョン 2 系である。

今回のエントリは、以下のエントリの RIPv2 を使ったバージョンになっている。 そのため、こちらに先に目を通しておくと内容を理解しやすい。

blog.amedama.jp

使った環境は次のとおり。

$ cat /etc/lsb-release 
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=22.04
DISTRIB_CODENAME=jammy
DISTRIB_DESCRIPTION="Ubuntu 22.04.2 LTS"
$ uname -srm
Linux 5.15.0-73-generic x86_64
$ bird --version
BIRD version 2.0.8

もくじ

下準備

まずは BIRD のバージョン 2 系をインストールする。

$ sudo apt-get update
$ sudo apt-get install bird2

インストールすると同時に BIRD のサービスが動作し始める。 しかし、今回は systemd 経由では BIRD を使わないため止めておく。

$ sudo systemctl stop bird
$ sudo systemctl disable bird

ネットワークを構築する

はじめに Network Namespace を使ってネットワークを構築する。 今回作るのは、以下のようなネットワークになる。 先のスタティックルーティングの構成と変わらない。

ネットワーク図 (論理構成)

上記で、router1198.51.100.0/24 と直接はつながっていないため経路を知らない。 同様に、router2192.0.2.0/24 と直接はつながっていないため経路を知らない。 そこで、BIRD の RIPv2 を使って経路を交換するのが今回の目的になる。

まずは Network Namespace を用意する。

$ sudo ip netns add ns1
$ sudo ip netns add router1
$ sudo ip netns add router2
$ sudo ip netns add ns2

Network Namespace 間をつなぐ Virtual Ethernet Device のインターフェイスを用意する。

$ sudo ip link add ns1-veth0 type veth peer name gw1-veth0
$ sudo ip link add gw1-veth1 type veth peer name gw2-veth0
$ sudo ip link add gw2-veth1 type veth peer name ns2-veth0

作成したインターフェイスを Network Namespace に所属させる。

$ sudo ip link set ns1-veth0 netns ns1
$ sudo ip link set gw1-veth0 netns router1
$ sudo ip link set gw1-veth1 netns router1
$ sudo ip link set gw2-veth0 netns router2
$ sudo ip link set gw2-veth1 netns router2
$ sudo ip link set ns2-veth0 netns ns2

それぞれのインターフェイスをリンクアップさせる。

$ sudo ip netns exec ns1 ip link set ns1-veth0 up
$ sudo ip netns exec router1 ip link set gw1-veth0 up
$ sudo ip netns exec router1 ip link set gw1-veth1 up
$ sudo ip netns exec router2 ip link set gw2-veth0 up
$ sudo ip netns exec router2 ip link set gw2-veth1 up
$ sudo ip netns exec ns2 ip link set ns2-veth0 up

それぞれのインターフェイスに IP アドレスを付与する。

$ sudo ip netns exec ns1 ip address add 192.0.2.1/24 dev ns1-veth0
$ sudo ip netns exec router1 ip address add 192.0.2.254/24 dev gw1-veth0
$ sudo ip netns exec router1 ip address add 203.0.113.1/24 dev gw1-veth1
$ sudo ip netns exec router2 ip address add 203.0.113.2/24 dev gw2-veth0
$ sudo ip netns exec router2 ip address add 198.51.100.254/24 dev gw2-veth1
$ sudo ip netns exec ns2 ip address add 198.51.100.1/24 dev ns2-veth0

ホストに相当する Network Namespace にはデフォルトルートを設定しておく。

$ sudo ip netns exec ns1 ip route add default via 192.0.2.254
$ sudo ip netns exec ns2 ip route add default via 198.51.100.254

そして、ルータになる Network Namespace は IPv4 のルーティングを有効にする。

$ sudo ip netns exec router1 sysctl net.ipv4.ip_forward=1
$ sudo ip netns exec router2 sysctl net.ipv4.ip_forward=1

この状態で、それぞれのルータは自身が直接つながっているセグメントへの経路は把握している。

$ sudo ip netns exec router1 ip route show
192.0.2.0/24 dev gw1-veth0 proto kernel scope link src 192.0.2.254 
203.0.113.0/24 dev gw1-veth1 proto kernel scope link src 203.0.113.1

しかし、直接つながっていないセグメントへの経路がないため ns1 から ns2 の間で ping(8) は通らない。

$ sudo ip netns exec ns1 ping -c 3 198.51.100.1 -I 192.0.2.1
PING 198.51.100.1 (198.51.100.1) from 192.0.2.1 : 56(84) bytes of data.
From 192.0.2.254 icmp_seq=1 Destination Net Unreachable
From 192.0.2.254 icmp_seq=2 Destination Net Unreachable
From 192.0.2.254 icmp_seq=3 Destination Net Unreachable

--- 198.51.100.1 ping statistics ---
3 packets transmitted, 0 received, +3 errors, 100% packet loss, time 2041ms

router1 の BIRD を設定する

まずは router1 の BIRD を設定する。 BIRD は 1 つの設定ファイルだけで動作する。 以下が、その設定ファイルになる。

$ cat << 'EOF' > router1.conf
log syslog all;
log stderr all;
log "/var/log/bird-router1.log" all;
debug protocols all;

router id 203.0.113.1;

protocol device {
}

protocol direct {
    ipv4;
}

protocol rip {
    ipv4 {
        import all;
        export all;
    };
    interface "gw1-veth1" {
        update time 10;
    };
}

protocol kernel {
    ipv4 {
        export filter {
            if proto = "direct1" then reject;
            accept;
        };
    };
}
EOF

スタティックルーティングの場合から変わっている部分として、まず direct プロトコルがある。 これは、自身が直接つながって知っているネットワークに関する情報を BIRD のルーティングテーブルに取り込む機能になっている。 デフォルトでは disabled; が設定されており、有効にするときは ipv4;ipv6; を指定して使う。 今回は直接つながっているネットワークの中で IPv4 の情報を取り込んで RIP で広告したいので ipv4; を有効にしている。

protocol direct {
    ipv4;
}

rip プロトコルに関しては、今回の主題となる部分。 ipv4import all は、RIP から得られた経路情報をすべて BIRD のルーティングテーブルに取り込む、という意味になる。 同様に export all は、BIRD の知っている経路情報をすべて RIP で伝達する、という意味になる。 interface "gw1-veth1" は RIP を使うインターフェイスを指定している。 RIP は定期的に経路情報を広告するプロトコルであり update time 10; で、その広告する間隔を変更している。 デフォルトは 30 秒だが 10 秒まで短くしているのは動作確認がやりやすくなるように。

protocol rip {
    ipv4 {
        import all;
        export all;
    };
    interface "gw1-veth1" {
        update time 10;
    };
}

あとは BIRD を起動する。 -d オプションを指定すると、フォアグラウンドでデバッグログを標準エラー出力に出すように起動する。 オプションをつけなければ、端末を切り離してデーモンとして動作する。

$ sudo ip netns exec router1 bird -d \
    -c router1.conf \
    -s router1.ctl \
    -P router1.pid

起動したら別のターミナルを開いてパケットをキャプチャしてみよう。 RIP はマルチキャストアドレス宛てに UDP の 520 番ポートで経路情報をやり取りする。

$ sudo ip netns exec router1 tcpdump -tnlvvv -i gw1-veth1 "udp port 520 and ip multicast"
tcpdump: listening on gw1-veth1, link-type EN10MB (Ethernet), snapshot length 262144 bytes
IP (tos 0xc0, ttl 1, id 35535, offset 0, flags [none], proto UDP (17), length 72)
    203.0.113.1.520 > 224.0.0.9.520: [bad udp cksum 0x1c51 -> 0xe15e!] 
    RIPv2, Response, length: 44, routes: 2 or less
      AFI IPv4,       192.0.2.0/24, tag 0x0000, metric: 1, next-hop: self
      AFI IPv4,     203.0.113.0/24, tag 0x0000, metric: 1, next-hop: self
    0x0000:  0202 0000 0002 0000 c000 0200 ffff ff00
    0x0010:  0000 0000 0000 0001 0002 0000 cb00 7100
    0x0020:  ffff ff00 0000 0000 0000 0001
^C
1 packet captured
1 packet received by filter
0 packets dropped by kernel

上記から、自身が直接つながって知っている経路を広告していることが確認できる。

この段階では、まだ他のルータからの経路情報を受け取っていないのでカーネルのルーティングテーブルに変化はない。

$ sudo ip netns exec router1 ip route show
192.0.2.0/24 dev gw1-veth0 proto kernel scope link src 192.0.2.254 
203.0.113.0/24 dev gw1-veth1 proto kernel scope link src 203.0.113.1 

router2 の BIRD を設定する

同じように router2 についても BIRD の設定ファイルを用意しよう。

$ cat << 'EOF' > router2.conf
log syslog all;
log stderr all;
log "/var/log/bird-router2.log" all;
debug protocols all;

router id 203.0.113.2;

protocol device {
}

protocol direct {
    ipv4;
}

protocol rip {
    ipv4 {
        import all;
        export all;
    };
    interface "gw2-veth0" {
        update time 10;
    };
}

protocol kernel {
    ipv4 {
        export filter {
            if proto = "direct1" then reject;
            accept;
        };
    };
}
EOF

そして、別のターミナルを開いて BIRD を起動する。

$ sudo ip netns exec router2 bird -d \
    -c router2.conf \
    -s router2.ctl \
    -P router2.pid

動作を確認する

少し待ってから動作を確認してみよう。 まずは router1 の BIRD のルーティングテーブルを確認してみる。

$ sudo birdc show route -s router1.ctl
BIRD 2.0.8 ready.
Table master4:
198.51.100.0/24      unicast [rip1 02:44:56.441] * (120/2)
    via 203.0.113.2 on gw1-veth1
192.0.2.0/24         unicast [direct1 02:44:35.774] * (240)
    dev gw1-veth0
203.0.113.0/24       unicast [direct1 02:44:35.774] * (240)
    dev gw1-veth1
                     unicast [rip1 02:44:56.441] (120/2)
    via 203.0.113.2 on gw1-veth1

router1 が直接つながっておらず、知らないはずの 198.51.100.0/24 がルーティングテーブルに登場している。 そして、このネットワークには 203.0.113.2 を経由して到達できるとあり、これは router2 の IP アドレスである。

カーネルのルーティングテーブルを確認しても、同様の経路がちゃんと追加されている。

$ sudo ip netns exec router1 ip route show
192.0.2.0/24 dev gw1-veth0 proto kernel scope link src 192.0.2.254 
198.51.100.0/24 via 203.0.113.2 dev gw1-veth1 proto bird metric 32 
203.0.113.0/24 dev gw1-veth1 proto kernel scope link src 203.0.113.1 

BIRD のプロトコルの稼働状況は次のとおり。 ちゃんとRIP を使った経路交換ができているようだ。

$ sudo birdc show protocols all -s router1.ctl
BIRD 2.0.8 ready.
Name       Proto      Table      State  Since         Info
device1    Device     ---        up     02:44:35.772  

direct1    Direct     ---        up     02:44:35.772  
  Channel ipv4
    State:          UP
    Table:          master4
    Preference:     240
    Input filter:   ACCEPT
    Output filter:  REJECT
    Routes:         2 imported, 0 exported, 2 preferred
    Route change stats:     received   rejected   filtered    ignored   accepted
      Import updates:              2          0          0          0          2
      Import withdraws:            0          0        ---          0          0
      Export updates:              0          0          0        ---          0
      Export withdraws:            0        ---        ---        ---          0

rip1       RIP        master4    up     02:44:35.772  
  Channel ipv4
    State:          UP
    Table:          master4
    Preference:     120
    Input filter:   ACCEPT
    Output filter:  ACCEPT
    Routes:         2 imported, 3 exported, 1 preferred
    Route change stats:     received   rejected   filtered    ignored   accepted
      Import updates:              2          0          0          0          2
      Import withdraws:            0          0        ---          0          0
      Export updates:              5          0          0        ---          5
      Export withdraws:            0        ---        ---        ---          0

kernel1    Kernel     master4    up     02:44:35.772  
  Channel ipv4
    State:          UP
    Table:          master4
    Preference:     10
    Input filter:   ACCEPT
    Output filter:  (unnamed)
    Routes:         0 imported, 1 exported, 0 preferred
    Route change stats:     received   rejected   filtered    ignored   accepted
      Import updates:              0          0          0          0          0
      Import withdraws:            0          0        ---          0          0
      Export updates:              5          0          4        ---          1
      Export withdraws:            0        ---        ---        ---          0

あらためてパケットをキャプチャすると、内容に変化がある。 まず、当然ながら router2 も RIPv2 で経路を広告するようになっている。 そして、router1router2 が広告する内容には、今回登場するすべてのセグメントが含まれている。 たとえば router1198.51.100.0/24 の経路を metric: 16 として広告しているようだ。 RIP において、メトリックが 16 というのはデフォルトでは無限遠や到達不能といった意味を持っている。 このような経路が広告されているのは、経路のループを防ぐスプリットホライズンとポイズンリバースが BIRD ではデフォルトで有効になっているため。 まず、スプリットホライズンは経路のループを防ぐために、あるインターフェイスから受け取った経路を、そのインターフェイスには広告しない機能である。 ポイズンリバースはスプリットホライズンのオプションで、あるインターフェイスから受け取った経路を、そのインターフェイスから到達不能という形で広告する。

$ sudo ip netns exec router1 tcpdump -tnlvvv -i gw1-veth1 "udp port 520 and ip multicast"
tcpdump: listening on gw1-veth1, link-type EN10MB (Ethernet), snapshot length 262144 bytes
IP (tos 0xc0, ttl 1, id 26403, offset 0, flags [none], proto UDP (17), length 92)
    203.0.113.1.520 > 224.0.0.9.520: [bad udp cksum 0x1c65 -> 0xb7ef!] 
    RIPv2, Response, length: 64, routes: 3 or less
      AFI IPv4,    198.51.100.0/24, tag 0x0000, metric: 16, next-hop: self
      AFI IPv4,       192.0.2.0/24, tag 0x0000, metric: 1, next-hop: self
      AFI IPv4,     203.0.113.0/24, tag 0x0000, metric: 1, next-hop: self
    0x0000:  0202 0000 0002 0000 c633 6400 ffff ff00
    0x0010:  0000 0000 0000 0010 0002 0000 c000 0200
    0x0020:  ffff ff00 0000 0000 0000 0001 0002 0000
    0x0030:  cb00 7100 ffff ff00 0000 0000 0000 0001
IP (tos 0xc0, ttl 1, id 11531, offset 0, flags [none], proto UDP (17), length 92)
    203.0.113.2.520 > 224.0.0.9.520: [bad udp cksum 0x1c66 -> 0xb7ee!] 
    RIPv2, Response, length: 64, routes: 3 or less
      AFI IPv4,    198.51.100.0/24, tag 0x0000, metric: 1, next-hop: self
      AFI IPv4,       192.0.2.0/24, tag 0x0000, metric: 16, next-hop: self
      AFI IPv4,     203.0.113.0/24, tag 0x0000, metric: 1, next-hop: self
    0x0000:  0202 0000 0002 0000 c633 6400 ffff ff00
    0x0010:  0000 0000 0000 0001 0002 0000 c000 0200
    0x0020:  ffff ff00 0000 0000 0000 0010 0002 0000
    0x0030:  cb00 7100 ffff ff00 0000 0000 0000 0001

どうやら経路の交換がうまくいっているようなので ns1 から ns2 へ ping を打ってみよう。

$ sudo ip netns exec ns1 ping -c 3 198.51.100.1 -I 192.0.2.1
PING 198.51.100.1 (198.51.100.1) from 192.0.2.1 : 56(84) bytes of data.
64 bytes from 198.51.100.1: icmp_seq=1 ttl=62 time=0.129 ms
64 bytes from 198.51.100.1: icmp_seq=2 ttl=62 time=0.097 ms
64 bytes from 198.51.100.1: icmp_seq=3 ttl=62 time=0.112 ms

--- 198.51.100.1 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2045ms
rtt min/avg/max/mdev = 0.097/0.112/0.129/0.013 ms

ちゃんと疎通があることが確認できた。

まとめ

今回は Network Namespace で構築したネットワーク上で BIRD を動かし RIPv2 を使ったダイナミックルーティングを試してみた。