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 は複数のルーティングテーブルを持てる。うち、以下は最初から用意されている。
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
などあるが、我々の用途では基本的に ipv4
か ipv6
しか利用しない。
後述する 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 毎に作れるので、つまるところ ipv4
や ipv6
が 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
に入ることになる。
export
は master4
から 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 を受け取って最終的に reject
か accept
する関数。関数の中で 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 に登録される経路を扱う。例えば eth0
に 192.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 stats
の rejected
を注目する。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
典型的な発生シナリオは以下の通り。
あるルータが /31 のサブネットで繋がっている eBGP ピアから経路を受け取る
この経路の NEXT_HOP
は /31 に属するアドレスとなる。
そのルータが iBGP ピアに eBGP ピアから受け取った経路を伝える
iBGP は NEXT_HOP
を書き換えずに伝える。
受け取った 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、いいですよ!