BIRD の IPv6 Link-Local BGP 実装のインターオペ問題 (Invalid GUA Next-Hop)
BIRDは *nix 系システム上で動作するIPルーティングデーモンのOSS実装だ。古くから存在し、よく安定している。
BIRD (Version 2)には、「IPv6 Link-Local address (LLA) のみを利用したBGPピアリング」が実装されている。しかし実際に利用したところ、BIRDが生成するNLRIは他実装とのインターオペに問題があった。
Symptom
以下の内容は BIRD 2.13.1 で確認した。
show protocols all でセッションのNext-Hop情報を確認すると、
fe80::...
の手前に::
というNext-Hopが含まれているbird> show proto all Name Proto Table State Since Info rt01 BGP --- up 22:42:09.189 Established BGP state: Established Neighbor address: fe80::1%enp23s0f0 : BGP Next hop: :: fe80::42a6:b7ff:fea7:f0
実際にBIRDから広報されるNLRIにも
::
fe80::...
という2つのNext-Hopが含まれる。
この形式のNLRI Next-Hopは、他実装で問題が生じる- Arista EOS:
::
というNext-Hopを不正と見なし経路をRIBにインストールしない(設定で回避可能、後述)
Aug 3 17:53:53 rt01 Rib: mpbgp_recv_mpreach: peer fe80::42a6:b7ff:fea7:f0%Et1 next hop :: improper, will ignore routes in this update unless set by route-map
- FRR: 当初はこのようなNLRIに対応していなかったが、後にこれをケアするよう修正された
https://github.com/FRRouting/frr/issues/6259
- Arista EOS:
Analysis
Q. NLRIに ::
(ゼロ埋め) が含まれるのは正しいか?
- A. 分からん
- とりあえず RFC2545 の
Constructing the Next Hop field
を確認した- Global Unique Address (GUA) のみ or GUA+LLAの両Next-HopをNLRIに入れる場合の定義はある。LLAのみのケースについては読み取れない。
- この場合、一つ目のNext-HopはGUA + 二つ目のNext-HopはLLAと記載されている (shall)
- 上記のFRRのIssueを読むと、「BIRDはNLRIの一つ目のNext-HopはGUA、二つ目のNext-HopはLLAと決め打ちしている」との投稿がある
- かつてのBIRDはそのような仕様だったようだが、以下のコミットで修正されたようだ
https://gitlab.nic.cz/labs/bird/-/commit/17de3a023f7bde293892b41bfafe5740c8553fc8 - その際のCommit messageから、BIRDにとって これらのNLRIに関する仕様は意図したものらしい ことが分かる
BGP: Fix handling of strange IPv6 link-local-only next hops
There are three common ways how to encode IPv6 link-local-only next hops: (:: ll), (ll), and (ll ll). We use the first one but we should accept all three. The patch fixes handling of the last one.
- かつてのBIRDはそのような仕様だったようだが、以下のコミットで修正されたようだ
- そもそも「IPv6 Link-Local address (LLA) のみを利用したBGPピアリング」を定義するRFCってどれだ?
BIRDの実装におけるNext-Hop Addressの選択
BIRDのソースコードを覗いたところ、広報するNLRIに採用する Next-Hop はbgp.c
の
L1737 - L1836 で設定されている。
このロジックを追ったところ、以下のような内容になっていた。
⚠ NOTE
BIRDのソースコードは GNU GPL でライセンスされています。 以降の文章にはソースコードの解説などが含まれます
- 「IPv6 インターフェイスのIPアドレスは GUA + LLA がセット」という前提で書かれている
- 各セッションの構造体に以下の2つの変数が定義されている
- next_hop_addr : ピアリングインターフェイスのGUAを格納する
- link_addr : ピアリングインターフェイスのLLAを格納する
- 各セッションの構造体に以下の2つの変数が定義されている
- LLAのみが付与されたインターフェイスでのBGP接続を行う場合、以下のような動作になる
- ピアリングインターフェイスに設定されたIPv6アドレスを拾ってきて next_hop_addr に格納する
- もしインターフェイスにGUAが設定されている場合
→ next_hop_addr にGUAがセットされる - もしインターフェイスにGUAが設定されていない(LLAしか存在しない)場合 (※今回の例)
→ next_hop_addr にLLAがセットされる
- もしインターフェイスにGUAが設定されている場合
- ピアリングインターフェイスに設定されたIPv6 LLAアドレスを拾ってきて link_addr に格納する
- もし next_hop_addr の内容がLLAなら next_hop_addrを
IPA_ZERO
(IPv6アドレス表記で::
)で埋める - BIRDはBGPのNLRIを next_hop_addr + link_addr (if exist) という形で作成する
→ 送信するNLRIのNext-Hopが「::
LLA
」になる
- ピアリングインターフェイスに設定されたIPv6アドレスを拾ってきて next_hop_addr に格納する
Resolution
方法1. Aristaサイドでのワークアラウンド
AristaはRoute-mapによる受信経路のNext-Hopの書き換えをサポートしているので、これで peer-address
を指定して書き換えれればBIRD側の修正不要で経路交換できる
route-map DOWNLINK-IN permit 10 set ipv6 next-hop peer-address : router bgp 1234 neighbor DOWNLINK-v6 peer group neighbor DOWNLINK-v6 route-map DOWNLINK-IN in
方法2. BIRDへのパッチ
BIRDがNLRIに ::
を埋めるのを止めさせるというアプローチ
解決方法A
単に next_hop_addr のゼロ埋めを止める
- そもそも next_hop_addr がLLAであったとして ゼロ埋めする必要はないと考える
- この場合NLRIに同じNext-Hopが2個入ることになる (next_hop_addr, link_addr ともに同じLLAが入るため) が、これが現実的に問題を引き起こす気はしない
- 試しにこの形に修正したところ、特に問題なくAristaと経路交換できていた。
- ARISTAのNLRIはこの形式を取っているように見える
修正パッチ
- 上記で解説している bgp.c のうち
if (ipa_is_link_local(c->next_hop_addr))
ブロックをコメントアウトすればいい
解決方法B
next_hop_addr の内容がLLAになっていてもゼロ埋めせず、 link_addr に内容を詰める処理をスキップする
- コード的には next_hop_addr がメインであって、 link_addr は「もし内容が入っているならばそれを NLRI や show proto の表示に付け足す」という存在
→ next_hop_addr にLLAが入っているなら、そのまま next_hop_addr だけを使えばいい (link_addr は空のままにしておけばいい) - この修正で
next hop prefer global
などのオプションに影響が出る可能性はあるが、利用予定がないので検証していない。
修正パッチ (⚠Licensed under GNU GPL)
--- proto/bgp/bgp.c.orig 2023-08-04 16:28:46.547319954 +0900 +++ proto/bgp/bgp.c 2023-08-04 16:50:17.860400016 +0900 @@ -1820,8 +1820,13 @@ return 0; } - /* Set link-local address for IPv6 single-hop BGP */ - if (ipa_is_ip6(c->next_hop_addr) && p->neigh) + /* Set link-local address for IPv6 single-hop BGP. */ + /* but if next_hop_addr already contains LLA, + there is no GUA for this Interface. + so we keep next_hop_addr as is and stop using link_addr. + Otherwize, it's an IPv6 interface having GUA & LLA. + set link-local address into link_addr normally. */ + if (ipa_is_ip6(c->next_hop_addr) && !ipa_is_link_local(c->next_hop_addr) && p->neigh) { c->link_addr = p->link_addr; @@ -1829,10 +1834,6 @@ log(L_WARN "%s: Missing link-local address", p->p.name); } - /* Link local address is already in c->link_addr */ - if (ipa_is_link_local(c->next_hop_addr)) - c->next_hop_addr = IPA_NONE; - return 0; /* XXXX: Currently undefined */ }
Appendix
むしろ上記の点を除けば、 BIRDのIPv6ピアリングは極めてマトモに動作している。RFC 5549を用いたIPv6ピア上でのIPv4経路伝搬自体は正常で、カーネルへの経路インストールにも問題はない。枯れたデーモンでありながら新しめの技術にも追随しているし、今回試したところでは設定も動作も簡潔で、個人的にはかなり印象が良い。
2023-08-10 追記
NLRIをWiresharkで眺めていたところ、Wireshark 4.0.7 の BGP dissector では 以下のように「一つ目のNext-HopはGUA、二つ目のNext-HopはLLA」と表示していた。 やはりこの順番自体には根拠があるのだが、未だに「Next-HopがLLAしかないケース」のRFCは分からない。
(このNLRIはARISTAが送信したもので、両方にIPv6 LLAがセットされている)