FRRはRFC5549な経路をLinuxカーネルのルーティングテーブルへどうやってインストールするのか
FRRouging (FRR)という、Quaggaからフォークしたルーティングデーモンがある。
FRRはRFC 5549に対応しており、bgpdで受信したIPv6ネクストホップを持つIPv4経路をzebra経由でLinuxカーネルのルーティングテーブルへインストールすることができる。しかし、Linuxカーネルのルーティングテーブルにそのような経路をインストールすることは通常できないはずである。正確に言うと最近のiproute2
ではip route
コマンドでそのような指定自体は可能だか、実際には反映されない。
$ ip -V ip utility, iproute2-ss170905 $ sudo ip route add 192.168.254.0/24 via inet6 fe80::a00:27ff:fe25:4c23 dev eth1 $ ip route 192.168.254.0/24 dev eth1
ではFRRはこの問題をどのように解決しているのか?気になったので少し調べてみた。
観察
FRR 2.0をインストールした2台のサーバ(server1
, server2
)のネットワークインタフェース(eth1
)どうしを直結した上でBGP unnumberedピアを設定し、server1
からserver2
に対し経路広報を行った。
動作を観察してみると、ネイバーテーブルにダミーの169.254.0.1
向けエントリを作成したのち、ルーティングテーブル上でネクストホップとして本来のIPv6リンクローカルアドレスの代わりに169.254.0.1
を指定することで問題を解決しているようである。
下記の例ではserver1
からRFC 5549で広報された172.16.254.0/24
の経路がserver2
のルーティングテーブルにインストールされた際の状態を示している。
server2$ ip neigh fe80::a00:27ff:fe25:4c23 dev eth1 lladdr 08:00:27:25:4c:23 router STALE 169.254.0.1 dev eth1 lladdr 08:00:27:25:4c:23 PERMANENT server2$ ip route 172.16.254.0/24 via 169.254.0.1 dev eth1 proto zebra metric 20 onlink
ネイバーテーブル・ルーティングテーブルのメンテナンス
気になるのが、FRRはどのようにネイバーテーブルやルーティングテーブルをメンテナンスしているのか、という点。ここではFRR 2.0のコードを元に、その方法を追ってみる。
- tag: frr-2.0 https://github.com/FRRouting/frr/tree/frr-2.0
- RFC5549サポートが実装された最初のコミット
まずはコード内を "5549" というキーワードでgrepしてみると、ルーティングテーブルにエントリをインストールしている箇所がzebra/rt_netlink.c
でヒットする。ルーティングテーブルのエントリ作成はこの部分が担当しているようである。
https://github.com/FRRouting/frr/blob/frr-2.0/zebra/rt_netlink.c#L654-L678
実際にzebra
の実行時のログを確認すると、この部分が使われている様子が観測できる。
ZEBRA: zebra message comes from socket [13] ZEBRA: 0:172.16.254.0/24: Inserting route rn 0x108a4c0, rib 0x108a3f0 (type 9) existing (nil) ZEBRA: 0:172.16.254.0/24: Adding route rn 0x108a4c0, rib 0x108a3f0 (type 9) ZEBRA: netlink_route_multipath() (single hop): RTM_NEWROUTE 172.16.254.0/24 vrf 0 type IPv6 nexthop with ifindex ZEBRA: 5549: _netlink_route_build_singlepath() (single hop): nexthop via 169.254.0.1 if 3 ZEBRA: netlink_talk: netlink-cmd (NS 0) type RTM_NEWROUTE(24), len=60 seq=7 flags 0x405 ZEBRA: netlink_parse_info: netlink-cmd (NS 0) ACK: type=RTM_NEWROUTE(24), seq=7, pid=0 ZEBRA: 0:172.16.254.0/24: Redist update rib 0x108a3f0 (type 9), old (nil) (type -1)
ではネイバーテーブルはどうか?コード内を "169.254.0.1" でgrepすると、先のzebra/rt_netlink.c
とは別にzebra/interface.c
のif_nbr_ipv6ll_to_ipv4ll_neigh_update()
がヒットする。
https://github.com/FRRouting/frr/blob/frr-2.0/zebra/interface.c#L752-L765
この部分を読むと、与えられたIPv6アドレスがEUI-64方式であることを前提としてそのMACアドレスを割り出したのち、それを用いてネイバーテーブルに169.254.0.1
のエントリを作成しているようである。
ではif_nbr_ipv6ll_to_ipv4ll_neigh_update()
を呼んでいるのは誰か?逆順に辿っていくと、
- zebra/zserv.c:nbr_connected_add_ipv6()
- zebra/rtadv.c:rtadv_process_advert()
- zebra/rtadv.c:rtadv_process_packet()
- zebra/rtadv.c:rtadv_read()
- ...
という具合に、IPv6 Router Advertisement (RA) の受信処理部分に辿り着く。また、上記パスとは別に、
- zebra/interface.c:if_nbr_ipv6ll_to_ipv4ll_neigh_add_all()
- zebra/interface.c:if_up()
- ...
からも呼ばれている。
これらより、zebra
はネットワークインターフェースがUPしたとき(zebra自身の起動時を含む)とIPv6 RAを受信したときにネイバーテーブルのエントリを作成していることが読み取れる。
まとめ
…といったことを挙動やコードから追っていたところ、Cumulus Linuxのドキュメントにしっかり記述されていた*1。
Border Gateway Protocol - BGP - Cumulus Linux 3.4.1 - Cumulus Networks
BGP and Extended Next-hop Encoding:
For link-local peerings enabled by dynamically learning the other end's link-local address using IPv6 neighbor discovery router advertisements, an IPv6 next-hop is converted into an IPv4 link-local address and a static neighbor entry is installed for this IPv4 link-local address with the MAC address derived from the link-local address of the other end.
It is assumed that the IPv6 implementation on the peering device will use the MAC address as the interface ID when assigning the IPv6 link-local address, as suggested by RFC 4291.
Managing Unnumbered Interfaces:
the IPv4 link-local address 169.254.0.1 is used to install the route and static neighbor entry to facilitate proper forwarding without having to install an IPv4 prefix with IPv6 next-hop in the kernel