FRRはRFC5549な経路をLinuxカーネルのルーティングテーブルへどうやってインストールするのか

FRRouging (FRR)という、Quaggaからフォークしたルーティングデーモンがある。

github.com

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のコードを元に、その方法を追ってみる。

まずはコード内を "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.cif_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

*1:CumulusはFRR開発の中心的存在で、最近のCumulus LinuxではルーティングデーモンとしてFRRを採用している