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

Analysis

Q. NLRIに :: (ゼロ埋め) が含まれるのは正しいか?

  • A. 分からん
  • とりあえず RFC2545Constructing 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.

  • そもそも「IPv6 Link-Local address (LLA) のみを利用したBGPピアリング」を定義するRFCってどれだ?

BIRDの実装におけるNext-Hop Addressの選択

BIRDのソースコードを覗いたところ、広報するNLRIに採用する Next-Hop は bgp.cL1737 - L1836 で設定されている。 このロジックを追ったところ、以下のような内容になっていた。

⚠ NOTE
BIRDのソースコードは GNU GPL でライセンスされています。 以降の文章にはソースコードの解説などが含まれます

  • 「IPv6 インターフェイスのIPアドレスは GUA + LLA がセット」という前提で書かれている
    • 各セッションの構造体に以下の2つの変数が定義されている
      • next_hop_addr : ピアリングインターフェイスのGUAを格納する
      • link_addr : ピアリングインターフェイスのLLAを格納する
  • LLAのみが付与されたインターフェイスでのBGP接続を行う場合、以下のような動作になる
    1. ピアリングインターフェイスに設定されたIPv6アドレスを拾ってきて next_hop_addr に格納する
      • もしインターフェイスにGUAが設定されている場合
        next_hop_addr にGUAがセットされる
      • もしインターフェイスにGUAが設定されていない(LLAしか存在しない)場合 (※今回の例)
        next_hop_addr にLLAがセットされる
    2. ピアリングインターフェイスに設定されたIPv6 LLAアドレスを拾ってきて link_addr に格納する
    3. もし next_hop_addr の内容がLLAなら next_hop_addrを IPA_ZERO(IPv6アドレス表記で :: )で埋める
    4. BIRDはBGPのNLRIを next_hop_addr + link_addr (if exist) という形で作成する
      → 送信するNLRIのNext-Hopが「 :: LLA 」になる

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は分からない。

mp_reach_nlri.png

(このNLRIはARISTAが送信したもので、両方にIPv6 LLAがセットされている)