ルーティングソフトウェア BIRD の使いかた

Neco プロジェクトの ymmt です。本記事では Neco のネットワークの実装を理解するために、ルーティングソフトウェアである BIRD の仕組みと設定方法を解説します。

公式文書がすこしとっつきにくいので、こちらを読んでから公式文書にあたるとスムーズに理解ができると思います。見所は、Invalid NEXT_HOP への工夫をこらした各種対処方法です。

以下、社内向けの解説文書からコピペしているので文体が変わります。悪しからずご了承ください。

BIRD とは

BIRD は Linux 等で動作する BGP や RIP などのルーティングプロトコルを実装したプログラムである。2018 年 10 月時点の最新版は 2.0.2 で、2.0 と 1.6 の両系列がメンテナンスされている。

本記事の内容はバージョン 2.0.2 を対象とする。

アーキテクチャ

BIRD は単独のプロセスとして動作する。

内部に複数のルーティングテーブルを持ち、それらを protocol と呼ばれる仕組みで外部のルータや Linux カーネル(が持つルーティングテーブル)とつなげることができる。

以下で理解が必要なものについて解説する。

Routing tables

BIRD は複数のルーティングテーブルを持てる。うち、以下は最初から用意されている。

  • master4

    ipv4 nettype でデフォルトで使用されるルーティングテーブル

  • master6

    ipv6 nettype でデフォルトで使用されるルーティングテーブル

IPv4 用のテーブルを別に作るには、以下のように bird.conf に書く。

ipv4 table another_table;

これらは Linux カーネルが持つルーティングテーブルとは関係がない。

BIRD のルーティングテーブルは BGP であれば AS の経路情報(aspath)など様々な属性を持つ route を登録するもの。Linux カーネルのルーティングテーブルにはそのような属性はなく、単純に prefix match でパケットの転送先を決めるFIB (Forwarding Information Base)としての役割しか持たない。

以下、ルーティングテーブルと言えば BIRD のルーティングテーブルを指す。カーネルのルーティングテーブルは「フォワーディングテーブル」もしくは「FIB」と呼ぶ。

Routes and nettypes

Route は文字通り経路だが、IPv4/IPv6 で言えば対象となるアドレスレンジ(サブネット)と、宛先(nexthop)、それに加えこの Route を連絡してきたネタ元(source protocol instance)や優先度(preference)などなどの属性が付属しているもののこと。

nexthop は複数あることもあり、その場合どの経路をどの程度の割り合いで選ぶかの重み(weight)を持つ。全ての nexthop が同じ重みを持つ場合は Equal Cost Multipath (ECMP) となる。

nettypes には先述の ipv4, ipv6 以外に flow4 などあるが、我々の用途では基本的に ipv4ipv6 しか利用しない。

後述する route filter は route を受け取る関数となっており、route が持つ各種属性を関数内で変数として参照できるようになっている。

Protocols and channels

Protocol はルーティングテーブルと「何か」をつなぐもの。「何か」が他のルータである場合、protocol は BGP や OSPF といったルーティングプロトコルを指す。「何か」はルータ以外のこともあり、例えば kernel プロトコルは Linux kernel が持つフォワーディングテーブル(FIB)と BIRD のルーティングテーブルとの経路交換を実現する。

Protocol は接続先に応じてインスタンスを複数持てる。例えば、BGP であればピア先のルータ毎にインスタンスを作る。これらを区別するため、protocol のインスタンスは名前を付けられる。以下は ToR 2 台と eBGP 構成を取るルータの例。インスタンスに tor1, tor2 と名前を付けている。

protocol bgp tor1 {
    local AS 65000;
    neighbor IP_OF_TOR1 AS 65001;
    ...
}
protocol bgp tor2 {
    local AS 65000;
    neighbor IP_OF_TOR2 AS 65001;
    ...
}

Protocol の中には channel が書ける。channel は nettype 毎に作れるので、つまるところ ipv4ipv6 が channel 名となる。BGP protocol では ipv4, ipv6 両方の channel を持てるが、RIP などは一つしか持てない。この辺りはプロトコルの特性による。後述する bfd などの特殊な protocol は channel を持つことができない。

channel は、ルーティングテーブルとその protocol インスタンスの接続先で経路を交換する方法を示す。ipv4 channel では、指定をしないと master4 ルーティングテーブルと経路交換をすることになる。

protocol bgp tor1 {
    local AS 65000;
    neighbor IP_OF_TOR1 AS 65001;

    ipv4 {
        import all;
        export filter {
            if proto = "static1" then reject;
            accept;
        };
    };
}

上の例は tor1 BGP インスタンスが IPv4 の経路を master4 ルーティングテーブルにどう入れるかを設定している。import は protocol の接続先からルーティングテーブルに経路を入れる設定で、all であるので neighbor AS から受け取った経路は全て master4 に入ることになる。

exportmaster4 から neighbor AS に伝える経路の設定で、後述する経路フィルタで一部の経路を広報しないようにしている。

channel の設定が空の場合(デフォルトのままでいい場合)は、{, } ごと省略できる。ipv4 channel であれば単に ipv4; と書くことになる。ただし BGP protocol のうち eBGP となる場合は export, import の明示が必要であり、この記法は使えない。

重要な機能

protocol template

BGP のピア毎に protocol を書いていくと、共通点が非常に多い設定が並ぶ。このような作業を効率的に行えるよう、BIRD は protocol template を提供している。以下のように使う。

template bgp tor {
    local AS 65000;
    rr client;

    ipv4 {
        import all;
        export filter {
            if proto = "static1" then reject;
            accept;
        };
    };
}

protocol bgp tor1 from tor {
    neighbor IP_OF_TOR1 AS 65001;
}
protocol bgp tor2 from tor {
    neighbor IP_OF_TOR2 AS 65001;
}

経路フィルタ

経路フィルタは、route を受け取って最終的に rejectaccept する関数。関数の中で if などの制御構文や集合を含む各種のデータ型を利用できるため、非常に柔軟な経路制御を可能としている。

フィルタの中では、route は出現せず、route が持つ属性が変数に代入されて利用できる。例えば proto という変数には、その route をルーティングテーブルに登録した protocol インスタンスの名前が入っている。

一部の属性はフィルタの中で書き換えが可能で、例えば dest 属性を書き換えることで ICMP UNREACHABLE を返すように経路設定をすることができる。

関数

経路フィルタは関数だが、別途純粋な関数も定義できる。

function with_parameters (int parameter)
int local_variable;
{
    local_variable = 5;
    return parameter + local_variable;
}

プロトコル

以下、Neco で利用するものに限定して解説する。

Device

ルーティングプロトコルではなく、ルーティングテーブルとも結びつかない。BIRD が OS のネットワークインターフェイス(link)情報をスキャンする設定。

このプロトコルのインスタンスはほぼ確実に一つ作る必要がある。

scan time は Linux の場合 OS が notify する仕組みになっているので、それほど短い値にしなくても良い。

protocol device {
    scan time 10;
}

BFD

BFD はルーティングプロトコルではなく、ルーティングテーブルとも結びつかない。ルータ機器同士で非常に短い間隔で keepalive メッセージを相互に送ることで、経路障害を迅速に検出するプロトコルである。

BFD プロトコルは必要に応じて BGP 他のプロトコルが利用する。また BFD のセッションは BGP のピア接続情報から自動的に作ることができる。そのため BFD プロトコルは有効にしておけば他に設定する必要はないことが多い。

protocol bfd {
}

Direct

Direct はカーネルが持つ L2 リンクに割り当てられた IP アドレスから自動的に FIB に登録される経路を扱う。例えば eth0192.168.16.3/24 というアドレスを割り当てると、FIB には 192.168.16.0/24 via eth0 という経路が自動登録される。

Direct から BIRD のルーティングテーブルに経路を import するのは、通常は必要がない。必要になるケースとしては、例えば以下のように dummy デバイスを作り /32 の代表アドレスを持たせる場合などである。

$ sudo ip link add node0 type dummy
$ sudo ip address add 192.168.16.3/32 dev node0

Direct から BIRD ルーティングテーブルに import した経路は、次に述べる Kernel プロトコルであらためて FIB に登録はしたくないはずである。以下のように、export filter で落とす。

protocol direct direct1 {
    ipv4;
    interface "node0";
}

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

Kernel

Kernel プロトコルはルーティングプロトコルではない。ルーティングテーブルの情報を OS のフォワーディングテーブル(FIB)に登録するものである。逆に、FIB に手動で登録された経路をルーティングテーブルに取り込むこともできる。

ルーティングテーブルに対して、kernel protocol は一つしかインスタンスを持てない。ルーティングテーブルを分ければ複数のインスタンスを作れるが、接続先の FIB は異なるものにしなければならない。

ipv4 table alt_v4tab;

# alt_v4tab の経路を全て FIB 8 番に登録
protocol kernel {
    kernel table 8;
    ipv4 {
        table alt_v4tab;
        export all;
    };
}

# master4 の経路を全て main FIB に登録
# main FIB の経路も learn して master4 に登録
protocol kernel {
    learn;
    persist;
    ipv4 {
        import all;
        export all;
    };
}

FIB 8 番を実際に Linux カーネルがルーティングに利用するには以下が必要。

$ sudo ip rule add priority 100 from all lookup 8

Pipe

前節で複数のルーティングテーブルを使う場面がでてきたが、ルーティングテーブル間で経路を交換するために使うのが pipe protocol である。pipe は 2 つのルーティングテーブルを繋ぐ channel というべき存在であり、その設定には export, import 等の channel の設定を直に書く。

# BGP 用に経路を集約するテーブル
ipv4 table bgp_v4tab;

# master4 -> bgp_v4tab
protocol pipe {
    table bgp_v4tab;
    peer table master4;
}

# alt_v4tab -> bgp_v4tab
protocol pipe {
    table bgp_v4tab;
    peer table alt_v4tab;
}

Static

いわゆるスタティックルートを定義できる。パケットを捨てたり ICMP UNREACHABLE を返すような設定もできる。

protocol static {
    ipv4;
    route 0.0.0.0/0 via 192.168.0.1;  # default gateway
}

BGP

BGP のインスタンスは、neighbor (ピア)毎に設定をする必要がある。同一 AS 番号の neighbor なら iBGP, 異なる AS 番号なら eBGP となるが、iBGP か eBGP かで各種設定のデフォルト値が変わるので注意をすること。

また、ピアが多々ある場合はほぼ同じ設定が並ぶので、前述の protocol template を利用すると良い。

  • bfd: BFD を有効にしたければ、こう書くだけで有効になる。
  • passive: もしかすると存在しないピアである場合、自分から接続にはいかないようにする。
  • rr client: ピアに対してルートリフレクターとして振る舞うならこれ。
  • add paths: iBGP で一つの宛先に複数の経路を neighbor に伝えたり受け取ったりする。ECMP に必要。
  • direct: iBGP のピアが同一 L2 ドメインにある場合は指定する必要がある。
  • multihop: eBGP のピアが同一 L2 ドメインにない場合は指定する必要がある。

トラブルシューティング

状態の確認

birdc を使うと、ルーティングテーブルやプロトコルインスタンスの状況を確認できる。

ルーティングテーブルの表示:

bird> show route
Table master4:
0.0.0.0/0            unicast [tor1 18:55:10.191] * (100) [AS65000i]
        via 10.69.64.1 on eth0
                     unicast [tor2 18:55:09.946] (100) [AS65000i]
        via 10.69.128.1 on eth1
10.69.0.5/32         unicast [tor1 18:55:07.808] * (100) [i]
        via 10.69.64.1 on eth0
                     unicast [tor2 18:55:07.609] (100) [i]
        via 10.69.128.1 on eth1
10.69.0.4/32         unicast [tor1 18:55:10.903] * (100) [i]
        via 10.69.64.1 on eth0
                     unicast [tor2 18:55:11.059] (100) [i]
        via 10.69.128.1 on eth1
10.69.0.3/32         unicast [direct1 18:55:03.687] * (240)
        dev node0

プロトコルインスタンスの一覧:

bird> show protocols
Name       Proto      Table      State  Since         Info
device1    Device     ---        up     18:16:10.826
bfd1       BFD        ---        up     18:16:10.826
defaultgw  Static     master4    up     18:16:10.826
kernel1    Kernel     master4    up     18:16:10.826
rack0-tor1 BGP        ---        up     18:16:14.081  Established
rack0-tor2 BGP        ---        up     18:16:14.686  Established

BGP インスタンスの詳細を表示:

bird> show protocols all 'rack0-tor1'
Name       Proto      Table      State  Since         Info
rack0-tor1 BGP        ---        up     18:16:14.081  Established
  BGP state:          Established
    Neighbor address: 10.0.1.1
    Neighbor AS:      64600
    Neighbor ID:      10.0.1.1
    Local capabilities
      Multiprotocol
        AF announced: ipv4
      Route refresh
      Graceful restart
      4-octet AS numbers
      Enhanced refresh
    Neighbor capabilities
      Multiprotocol
        AF announced: ipv4
      Route refresh
      Graceful restart
      4-octet AS numbers
      Enhanced refresh
    Session:          external AS4
    Source address:   10.0.1.0
    Hold timer:       177.403/240
    Keepalive timer:  8.268/80
  Channel ipv4
    State:          UP
    Table:          master4
    Preference:     100
    Input filter:   ACCEPT
    Output filter:  ACCEPT
    Routes:         4 imported, 2 exported
    Route change stats:     received   rejected   filtered    ignored   accepted
      Import updates:              4          0          0          0          4
      Import withdraws:            0          0        ---          0          0
      Export updates:              7          4          0        ---          3
      Export withdraws:            0        ---        ---        ---          1
    BGP Next hop:   10.0.1.0

ここでは特に Channel ipv4 -> Route change statsrejected を注目する。Export updates が先方に reject される理由は様々あり、多いのはすでにより良い経路を持っているからなどだが、next hop アドレスが到達不能アドレスになっているように修正を要するケースもある。

ログ出力

journalctl -u bird.service でエラーが記録されていないか確認をする。

詳細にプロトコルインスタンスの動きを知りたい場合、以下のように birdc で指示すると、ログに出力されるようになる。

bird> debug 'rack0-tor1' all

出力例:

2018-04-20 19:14:38.143 <TRACE> rack0-tor1: BGP session established
2018-04-20 19:14:38.143 <TRACE> rack0-tor1: State changed to up
2018-04-20 19:14:38.143 <TRACE> rack0-tor1: Sending KEEPALIVE
2018-04-20 19:14:38.143 <TRACE> rack0-tor1 < added 0.0.0.0/0 unicast
2018-04-20 19:14:38.143 <TRACE> rack0-tor1 < added 10.69.0.5/32 unicast
2018-04-20 19:14:38.143 <TRACE> rack0-tor1 < added 10.69.0.4/32 unicast
2018-04-20 19:14:38.143 <TRACE> rack0-tor1 < added 10.69.128.0/26 unicast
2018-04-20 19:14:38.143 <TRACE> rack0-tor1 < added 10.69.0.3/32 unicast
2018-04-20 19:14:38.144 <TRACE> rack0-tor1: Sending UPDATE
2018-04-20 19:14:38.144 <TRACE> rack0-tor1: Sending UPDATE
2018-04-20 19:14:38.144 <TRACE> rack0-tor1: Sending END-OF-RIB
2018-04-20 19:14:38.144 <TRACE> rack0-tor1: Got UPDATE
2018-04-20 19:14:38.144 <TRACE> rack0-tor1 > added [best] 10.69.64.0/26 unicast
2018-04-20 19:14:38.144 <TRACE> rack0-tor1 < rejected by protocol 10.69.64.0/26 unicast

Invalid NEXT_HOP への対応

以下は NEXT_HOP アドレスが到達不能な経路を受け取ったときに出力されるログである。

2018-04-20 19:14:38.253 <RMT> tor2: Invalid NEXT_HOP attribute

典型的な発生シナリオは以下の通り。

  1. あるルータが /31 のサブネットで繋がっている eBGP ピアから経路を受け取る

    この経路の NEXT_HOP は /31 に属するアドレスとなる。

  2. そのルータが iBGP ピアに eBGP ピアから受け取った経路を伝える

    iBGP は NEXT_HOP を書き換えずに伝える。

  3. 受け取った iBGP ピアは、1 の /31 サブネットを知らないので Invalid として破棄する

    NEXT_HOP 記載アドレスに到達ができない場合、BIRD はすぐに経路を捨ててしまう

これを回避するには以下のいずれかの対応が必要になる。

  • 事前に eBGP の IP アドレスに到達できるよう、iBGP ルータに経路を入れておく

    事前に入れる方法としては RIP などの IGP かスタティックルートを設定するかとなる。BGP で同時に広報すると、タイミング次第で先に到達不能な NEXT_HOP が弾かれてしまい、うまくいかない。

  • 経路を広報するルータが next hop self を設定する

    BIRD や他の BGP ルータは、iBGP でよくあるこの問題に対処するため、広告する経路の NEXT_HOP を全て自ルータのアドレスにする機能を持つ。これを有効にすることで、eBGP ルータの IP アドレスを隠すことができる。ただし、ルートリフレクターでこれを設定すると eBGP ピアから受け取った経路だけでなく、 iBGP ピアから受け取った経路の NEXT_HOP も書き換えてしまう。

  • 経路を広報するルータが export filter で NEXT_HOP を手動設定する

    eBGP ピアから受け取った経路の NEXT_HOP を書き換えるといったことが条件文で書くことができる。以下に例を示す。

      protocol bgp {
          ...
    
          ipv4 {
              import all;
              export filter {
                  # eBGP からの経路のみ NEXT_HOP を書き換える
                  if proto = "eBGP" then bgp_next_hop = 10.16.0.1;
                  accept;
              };
          };
      }
    
  • 経路を受け取る側が import filter でゲートウェイを上書きする

    BIRD では igp table TABLE を指定すれば、NEXT_HOP の経路の有無はそちらのテーブルを参照する。これを利用してひとまず受け取れるようにし、 import filter で経路のゲートウェイを上書きする。

      ipv4 table dummytab;
      protocol static dummystatic {
          ipv4 { table dummytab; };
          route 0.0.0.0/0 via "lo";
      }
    
      protocol bgp {
          ...
    
          ipv4 {
              igp table dummytab;
              gateway recursive;   # direct 接続時も IGP テーブルを参照させる
              import filter {
                  # ラック内 AS (iBGP ピア)からの経路なら bgp_next_hop を維持
                  # 以下の式は送信元ルータのアドレスを 26bit サブネットに含まれるかの判定
                  if bgp_next_hop.mask(26) = from.mask(26) then {
                      gw = bgp_next_hop;
                      accept;
                  }
    
                  # そうでないなら from をゲートウェイに(next hop self 相当)
                  gw = from;
                  accept;
              };
          };
      }
    

print デバッグ

BIRD がどのような経路を広報しているか詳しく知りたい場合、以下のように export filter 中で print することで経路の情報をログに出力できる。

    ipv4 {
        export filter {
            print "route: ", net, ", ", from, ", ", proto, ", ", bgp_next_hop;
            accept;
        };
    };

備考

代表 IP アドレスの実装

Neco のネットワーク設計 では、各サーバーは NIC の IP アドレスとは別に代表 IP アドレスを持つことにしている。

NIC とは別に仮想的なインターフェイスを作るには、Linux では dummy link を使う。

$ sudo ip link add node0 type dummy
$ sudo ip address add ADDRESS/32 dev node0

dummy インターフェイスは自ホスト宛てに決まっているので、FIB への登録は不要。BIRD で経路広報するには Direct プロトコルで node0 インターフェイスをルーティングテーブルに import する。

protocol direct direct1 {
    ipv4;
    interface "node0";
}

protocol kernel {
    persist;
    merge paths;   # ECMP

    # direct1 の経路を FIB に取り込まないようにする
    ipv4 {
        export filter {
            if proto = "direct1" then reject;
            accept;
        };
    };
}

疑似経路集約

内部でラック等に利用している AS 番号はクラスタ間で共通なので、クラスタ外への経路広報では aspath に含めないようにする必要がある。これは Cisco 等では経路集約して summary-only 指定すれば良いが、BIRD には経路集約機能がない。

代わりに、複数のルーティングテーブルを自由に利用できるため、クラスタ外の BGP ピアに広報したい経路を static protocol で登録したルーティングテーブルを用意すれば良い。

ipv4 table outertab;

protocol static myroutes {
    ipv4 { table outertab; }
    route ...;
}

protocol bgp outerpeer {
    local as ...;
    neighbor ADDRESS as ...;

    ipv4 {
        table outertab;
        import all;
        export all;
        next hop self;  # next hop self の正しい使いかた
    };
}

# BGP で受け取った経路を master4 に登録
protocol pipe outerroutes {
    table master4;
    peer table outertab;
    import filter {
        if proto = "myroutes" then reject;
        accept;
    };
    export none;
}

まとめ

ルーティングソフトウェア BIRD の仕組みと、各種設定方法やトラブルシューティングを解説しました。

複数の内部ルーティングテーブルとそれをつなぐ protocol という BIRD のアーキテクチャは、ソフトウェアの柔軟性を活かした拡張性の高い優れた設計だと思います。一度理解すれば、できないことがほとんどない便利なルーティングソフトウェアです。

BIRD、いいですよ!