ふつうのNICでハードウェアL3スイッチング; あるいはSR-IOV switchdev modeとTC hardware offloadの使用例

最近のNICの中にはTCハードウェアオフロード機能を持っているものがあり、これを使えばNICハードウェア上でパケット転送処理に手を加えられることは以前のエントリで簡単に紹介した。

yunazuno.hatenablog.com

今回はこれを応用し、NICハードウェア上でL3スイッチングを実現できないか検討してみる。

前置き: tcを用いたレイヤ3パケットスイッチング

L3スイッチの動作はおおまかに、 1. 受信したパケットの宛先IPアドレスをスキャンし、 2. 宛先MACアドレスを書き換えた上で、 3. 出口ポートからパケットを送信する、 というステップから構成される。これらの動作はそれぞれ tcflower フィルタ、pedit アクション、mirred アクションで表現することができる。

tcを用いたL3スイッチ動作表現の具体例

ここでは2個のNetwork Namespace ns0, ns1 がL3 Master Device vrf-tc 経由で接続されている状況を考える。

yunazuno.hatenablog.com

        +--------------------+
        |       vrf-tc       |
        +-----+--------+-----+
        | p0a |        | p1a |
        +--+--+        +--+--+
           |.1          .1|
10.0.0.0/30|              |10.0.1.0/30
           |.2          .2|
        +--+--+        +--+--+
        | p0z |        | p1z |
        +-----+        +-----+
          ns0            ns1

上図の環境は以下のコマンドで作成できる:

sudo ip link add vrf-tc type vrf table 10
sudo ip link set vrf-tc up

for i in $(seq 0 1); do
  sudo ip netns add ns${i}
  sudo ip link add p${i}a type veth peer name p${i}z
  sudo ip link set p${i}z netns ns${i}
  sudo ip link set p${i}a master vrf-tc

  sudo ip link set p${i}a up
  sudo ip -n ns${i} link set p${i}z up

  sudo ip addr add 10.0.${i}.1/30 dev p${i}a
  sudo ip -n ns${i} addr add 10.0.${i}.2/30 dev p${i}z
  sudo ip -n ns${i} route add 0.0.0.0/0 via 10.0.${i}.1
done

このとき、ns0からns110.0.1.2宛通信フローが存在するとして、これをtcで処理するならば以下のようになる:

sudo tc qdisc add dev p0a ingress

sudo tc filter add dev p0a ingress protocol ip \
    flower dst_ip 10.0.1.2/32 \
    action pedit ex munge eth dst set $(sudo ip -n ns1 -j link show dev p1z | jq -r '.[0].address') \
    pipe mirred egress redirect dev p1a

この状態でns0からns110.0.1.2宛にpingを打ったのちtc -sコマンドの出力を確認すると、この通信がどうやらtcによって処理されたことが確認できる:

$ sudo ip netns exec ns0 ping -c 10 10.0.1.2
$ tc -s filter show dev p0a ingress
filter protocol ip pref 49152 flower chain 0 
filter protocol ip pref 49152 flower chain 0 handle 0x1 
  eth_type ipv4
  dst_ip 10.0.1.2
  not_in_hw
        action order 1:  pedit action pipe keys 2
         index 1 ref 1 bind 1 installed 68 sec used 18 sec
         key #0  at eth+0: val ce884422 mask 00000000
         key #1  at eth+4: val a5220000 mask 0000ffff
        Action statistics:
        Sent 840 bytes 10 pkt (dropped 0, overlimits 0 requeues 0)
        backlog 0b 0p requeues 0

        action order 2: mirred (Egress Redirect to device p1a) stolen
        index 1 ref 1 bind 1 installed 68 sec used 18 sec
        Action statistics:
        Sent 840 bytes 10 pkt (dropped 0, overlimits 0 requeues 0)
        backlog 0b 0p requeues 0

このように、tcを使用するとL3スイッチの挙動を模擬することができる。

本題: SR-IOV switchdev mode + TCハードウェアオフロードによる、NICハードウェア上でのL3スイッチング

ここからは、SR-IOV switchdev modeとTCハードウェアオフロードを組み合わせることにより、NICハードウェア上でパケット転送の挙動を制御できることを確認する。その具体例として、前述したTCによるL3スイッチングをNICハードウェア上で実現してみることにする。

SR-IOV switchdev modeでNICハードウェアの挙動を制御する

VM環境で高パフォーマンスなネットワーク環境を実現する手段として、SR-IOVによるNIC仮想化が従来より用いられている。SR-IOV VFをVMにアタッチすればそのVMNICのハードウェア性能を直接享受できる反面、VMのネットワークが完全にハイパーバイザのソフトウェアスタックをバイパスされるため、ハイパーバイザ側からVMのネットワークの挙動を制御することは難しかった。

この問題を解決するのがSR-IOV switchdev modeである。SR-IOV switchdev modeにおいては、通常のVFとは別にVF representorと呼ばれるインタフェースが作成される。ハイパーバイザ上からこのVF reporesentorに設定を行うと、内容に応じてそのrepresentorに対応するVFに設定が反映される。たとえば下図でVF0 repに対してtcの設定を行ったとすると、その設定がハードウェアオフロード可能であればVF0に適用される。

         +---+ +---+ +---+
         |PF0| |VF0| |VF1|
         |rep| |rep| |rep|
         +-+-+ +-+-+ +-+-+            VM0   VM1
           ^     ^     ^             +---+ +---+
           |     |     |             |VF0| |VF1|
        +--+-----+-----+--+          +-+-+ +-+-+
        |    NIC Driver   |            ^     ^
        +------+----------+            |     |
Kernel         |                       |     |
+-------------------------------+      |     |
NIC            |                       |     |
        +------+-----------------------+-----+----+
        |             Embeded Switch              |
        +-----+-----------------------------------+
              |
              |
              v
             PF0

なお、SR-IOV switchdev modeではデフォルトで全てのトラフィックがソフトウェア処理される点に注意が必要である。上図VM0から送信されたポケットは、何も設定しなればVF0 repを経由してハイパーバイザに流入する。一方、ハイパーバイザからVF0 repに送信したパケットはVM0のVF0で受信される。 この特性を利用すれば、ARPトラフィックフローの初期段階はハイパーバイザのソフトウェアパスで処理し、ハードウェア処理の準備が整ったらハードウェアパスでのパケット転送に移行する、という動作が実現できる。実例としてOpen vSwitchのハードウェアオフロード機能はこの挙動を利用している。

SR-IOV switchdev modeの設定方法

まずSR-IOV switchdev modeの設定方法を確認する。通常のSR-IOV動作モード(legacy modeと呼ばれる)からswitchdev modeに移行するにはdevlinkコマンドを使用する。ここでは2個のSR-IOV VFを作成し、switchdev modeで使用する。 PCI Bus IDやインタフェース名、ドライバ名は各環境に応じて。

なお今回の環境は以下の通り:

  • Fedora 28 4.17.3-200.fc28.x86_64
  • MCX512A-ACAT fw 16.23.1000
# enp7s0f0 = PF, 0000:07:00.0
echo 2 | sudo tee /sys/class/net/enp7s0f0/device/sriov_numvfs

for i in $(seq 2 3); do
  echo 0000:07:00.$i | sudo tee /sys/bus/pci/drivers/mlx5_core/unbind
done

sudo devlink dev eswitch set pci/0000:07:00.0 mode switchdev

2個のVF representorが作成されていることが確認できる。

$ ip link
(snip)
17: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
    link/ether f6:60:41:6a:4d:6e brd ff:ff:ff:ff:ff:ff
18: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
    link/ether 9e:a5:d8:0b:b4:a5 brd ff:ff:ff:ff:ff:ff
$ ethtool -i eth0
driver: mlx5e_rep
(snip)

これらのVF representatorをL3 Master Device vrf-tchw経由で接続する。また、簡単のためVFをそれぞれNetwork Namespacens2, ns3にアタッチする。

sudo ip link add vrf-tchw type vrf table 20
sudo ip link set vrf-tchw up

for i in $(seq 2 3); do
  sudo ip netns add ns${i}
  sudo ip link set eth$(echo ${i}-2|bc) master vrf-tchw

  echo 0000:07:00.$i | sudo tee /sys/bus/pci/drivers/mlx5_core/bind
  sudo ip link set enp7s0f${i} netns ns${i}

  sudo ip link set eth$(echo ${i}-2|bc) up
  sudo ip -n ns${i} link set enp7s0f${i} up

  sudo ip addr add 10.0.${i}.1/30 dev eth$(echo ${i}-2|bc)
  sudo ip -n ns${i} addr add 10.0.${i}.2/30 dev enp7s0f${i}
  sudo ip -n ns${i} route add 0.0.0.0/0 via 10.0.${i}.1
done

図で表すとこのようになる:

         +---------------+
         |   vrf-tchw    |
         +--------+------+
         | eth0 | | eth1 |
         +------+ +------+              ns2          ns3
             |.2   .2|             +----------+ +----------+
  10.0.2.0/30|       |10.0.3.0/30  | enp7s0f2 | | enp7s0f3 |
             |.1   .1|             +-----+----+ +-----+----+
        +-----------------+              ^            ^
        |    NIC Driver   |              |            |
        +------+----------+              |            |
Kernel         |                         |            |
+-------------------------------+        |            |
NIC            |                         |            |
        +------+-------------------------+------------+----+
        |                  Embeded Switch                  |
        +--------------------------------------------------+

この状態でns2からns310.0.3.2宛にpingが通るようになっている。また、tcpdump -i eth0を実行すると、この通信がどうやらVF representorを経由していることが分かる:

$ sudo ip netns exec ns2 ping 10.0.3.2
$ sudo tcpdump -i eth0 -nn
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
14:35:11.750957 ARP, Request who-has 10.0.2.1 tell 10.0.2.2, length 46
14:35:11.751018 ARP, Reply 10.0.2.1 is-at f6:60:41:6a:4d:6e, length 28
14:35:11.751119 IP 10.0.2.2 > 10.0.3.2: ICMP echo request, id 5965, seq 1, length 64
14:35:11.751503 IP 10.0.3.2 > 10.0.2.2: ICMP echo reply, id 5965, seq 1, length 64

TCでFIBをハードウェアにオフロード

ここまで準備が整えば、あとは最初の例と同様にVF representor経由でTCの設定を行えばよい。 ここではLinuxカーネルのFIBとネイバーテーブルからtcフィルタを生成して適用する簡易的なpythonスクリプトを用意した。

gist.github.com

上記スクリプトを実行した状態でns2からns310.0.3.2宛にpingを打ちつつtc filter showコマンドを実行すると、tcフィルタが適用されており、かつin_hw表示がある通りハードウェアオフロードされている様子が確認できる。

$ tc filter show block 1
filter protocol ip pref 1 flower chain 0 
filter protocol ip pref 1 flower chain 0 handle 0x1 
  eth_type ipv4
  dst_ip 10.0.3.2
  in_hw
        action order 1:  pedit action pipe keys 2
         index 1 ref 1 bind 1
         key #0  at eth+0: val 92ef51a2 mask 00000000
         key #1  at eth+4: val 33e80000 mask 0000ffff
 
        action order 2: mirred (Egress Redirect to device eth1) stolen
        index 1 ref 1 bind 1
 
filter protocol ip pref 2 flower chain 0 
filter protocol ip pref 2 flower chain 0 handle 0x1 
  eth_type ipv4
  dst_ip 10.0.2.2
  in_hw
        action order 1:  pedit action pipe keys 2
         index 2 ref 1 bind 1
         key #0  at eth+0: val 4e6279b1 mask 00000000
         key #1  at eth+4: val 159f0000 mask 0000ffff
 
        action order 2: mirred (Egress Redirect to device eth0) stolen
        index 2 ref 1 bind 1

なお、今回は簡単のためフィルタを1秒毎に更新する実装になっているが、本来はnelinkをリッスンしてイベントドリブンにフィルタを更新すべきだろう。また、ネイバーテーブル上でREACHABLEなネイバー宛経路のみオフロードするよう実装しているが、エントリがSTELE状態に遷移するとすぐにオフロードが中止されてしまう、という問題もある。

まとめ

SR-IOV switchdev modeとTCハードウェアオフロードを組み合わせることで、一般的なNICのハードウェア上でレイヤ3スイッチと同様の処理を実現できることを確認した。今回は簡易的なpythonエージェントでオフロード機構を実装したため実用性は低いが、もしこれらの処理がLinuxカーネルやルーティングデーモン等によって透過的に実行されるようになれば面白いかもしれない。

また、今回はL3スイッチングを例としたが、他のパケット処理、たとえばACLやトンネリング(カプセリング)処理にももちろん適用可能である。従来のSR-IOV legacy modeは柔軟性の低さがひとつのネックとなっていたが、switchdev modeとTCハードウェアオフロードの組み合わせにより、高パフォーマンスを維持しつつより多くの問題に適用範囲が広がることを期待したい。

tc flowerオフロードを利用してNICハードウェア上でパケットフィルタを実施する

Linuxtcには、指定した条件にマッチしたパケットをドロップしたりヘッダを書き換えたりするためのflowerフィルタが存在する。

tc-flower(8) - Linux manual page

最近のNICの中にはこの処理をハードウェアにオフロードできるものがあり、通常のソフトウェア処理と比較して高スループットを実現しつつCPU使用率低減が期待できる。ここではtc flowerのNICハードウェアオフロード機能の動作を試してみる。

対応するNICドライバ

Linuxカーネル4.16.7のソースコード上で確認する限り、以下のNICドライバでtc flowerハードウェアオフロードに対応しているようである。ちなみに対応状況はTC_SETUP_CLSFLOWERないしTC_SETUP_BLOCK*1といったキーワードで検索するとおおよそ掴める。

  • Broadcom bnxt (BCM573xx; NetXtreme C-series/E-series)
  • Chelsio cxgb4 (T4/T5/T6)
  • Intel i40e (710 series)
  • Mellanox mlx5 (ConnectX-4/5)
  • Netronme nfp (Agilio)
  • (Mellanox mlxsw*2 )

ただし、オフロード可能なマッチング条件 (ヘッダのフィールド) およびアクションはNICドライバおよびハードウェアによって異なる。ドライバが対応していてもハードウェアの世代やファームウェアバージョンによっては動作しない場合もある。たとえばi40eはtc flowerハードウェアオフロード自体には対応しているがaction dropのオフロードに対応していない*3ため、これから述べるパケットフィルタの例は動作しない*4

設定方法

ethtoolコマンドでtc offloadを有効にしたのち、tcコマンドでフィルタを設定してやればよい。

$ uname -a
Linux server01 4.16.5-300.fc28.x86_64 #1 SMP Fri Apr 27 17:38:36 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux
$ tc -V
tc utility, iproute2-ss180129
$ ethtool -i enp7s0f0
driver: mlx5_core
version: 5.0-0
firmware-version: 14.22.1002 (MT_2420110004)
(snip)

ethtool -K IFACE hw-tc-offload onでtc offloadを有効にしたのち、対象インタフェースのqdiscingressを追加する。qdiscingressではなくclsactを使用してもよい。clsactingressをegress方向への操作にも適用できるように一般化したもの (net, sched: add clsact qdisc)。ただしegress方向へのflower適用に対応しているNICは現状無さそう*5なので、特に差異は無いはず。また詳しく追えていないがFedora 28でNetworkManagerが動作していると、追加したingressがしばらく経過すると削除される事象がみられた。

$ sudo ethtool -K enp7s0f0 hw-tc-offload on
$ sudo tc qdisc add dev enp7s0f0 ingress
$ tc qdisc show dev enp7s0f0
qdisc mq 0: root 
qdisc fq_codel 0: parent :8 limit 10240p flows 1024 quantum 1514 target 5.0ms interval 100.0ms memory_limit 32Mb ecn 
qdisc fq_codel 0: parent :7 limit 10240p flows 1024 quantum 1514 target 5.0ms interval 100.0ms memory_limit 32Mb ecn 
qdisc fq_codel 0: parent :6 limit 10240p flows 1024 quantum 1514 target 5.0ms interval 100.0ms memory_limit 32Mb ecn 
qdisc fq_codel 0: parent :5 limit 10240p flows 1024 quantum 1514 target 5.0ms interval 100.0ms memory_limit 32Mb ecn 
qdisc fq_codel 0: parent :4 limit 10240p flows 1024 quantum 1514 target 5.0ms interval 100.0ms memory_limit 32Mb ecn 
qdisc fq_codel 0: parent :3 limit 10240p flows 1024 quantum 1514 target 5.0ms interval 100.0ms memory_limit 32Mb ecn 
qdisc fq_codel 0: parent :2 limit 10240p flows 1024 quantum 1514 target 5.0ms interval 100.0ms memory_limit 32Mb ecn 
qdisc fq_codel 0: parent :1 limit 10240p flows 1024 quantum 1514 target 5.0ms interval 100.0ms memory_limit 32Mb ecn 
qdisc ingress ffff: parent ffff:fff1 ----------------

tc filter add ... flower ... action ...の形式でマッチ条件と動作を指定する。ここでは8080/tcp宛のパケットをドロップするよう設定している。skip_swを指定するとハードウェアオフロードのみを試行し、オフロードできない場合はエラーとなる。skip_hwを指定するとソフトウェア処理のみを試行する。何も指定しない場合はまずハードウェアオフロードを試行し、オフロードできない場合はソフトウェア処理にフォールバックする。また、実際にオフロードされていればin_hwが、そうでなければnot_in_hwが表示される。

$ sudo tc filter add dev enp7s0f0 ingress protocol ip \
    flower skip_sw ip_proto tcp dst_port 8080 action drop
$ tc filter show dev enp7s0f0 ingress
filter protocol ip pref 49152 flower chain 0 
filter protocol ip pref 49152 flower chain 0 handle 0x1 
  eth_type ipv4
  ip_proto tcp
  dst_port 8080
  skip_sw
  in_hw
        action order 1: gact action drop
         random type none pass val 0
         index 1 ref 1 bind 1

tcコマンドに-sオプションを付ければ統計情報も出力される。ここでは別ホストからhping -S -p 8080 --flood8080/tcp宛にパケットを送ってみたが、CPU使用率の上昇は特にみられなかった。

$ tc -s filter show dev enp7s0f0 ingress
filter protocol ip pref 49152 flower chain 0 
filter protocol ip pref 49152 flower chain 0 handle 0x1 
  eth_type ipv4
  ip_proto tcp
  dst_port 8080
  skip_sw
  in_hw
        action order 1: gact action drop
         random type none pass val 0
         index 1 ref 1 bind 1 installed 947 sec used 0 sec
        Action statistics:
        Sent 1002736860 bytes 16712281 pkt (dropped 16712281, overlimits 0 requeues 0) 
        backlog 0b 0p requeues 0

削除はtc filter delコマンドで。

$ sudo tc filter del dev enp7s0f0 ingress protocol ip pref 49152 handle 1 flower

tc-flower offloadのその他の利用例

ここまでtc flower offloadを使って特定のパケットをハードウェア上でドロップする例を見てきたが、実のところ同様のことはntupleフィルタをethtool経由で設定することで以前から実現可能である:

software.intel.com

fastnetmon.com

ethtoolを用いたntupleフィルタと比較したtc flower offloadの利点は、drop以外のアクションをオフロードすることでNICハードウェアに多彩な動作を行わせることが(仕組み上)可能な点にある。

たとえばpeditアクションとcsumアクションをオフロードすれば、簡易的なstatic NATを実装できる*6:

github.com

また、mirredアクションをオフロードすれば、NICをL2スイッチや簡易的なL3スイッチのように振る舞わせることができる。これをSR-IOV switchdev modeと組み合わせれば、SR-IOVでVM向けに高パフォーマンスなネットワーク環境を提供しつつ、ハイパーバイザ側からVM間通信の挙動を制御することができる。実際にOpen vSwitch 2.8以降はこれらを利用してVM間のL2通信をNICハードウェアにオフロードする機能が実装されている。

SR-IOV switchdev modeの概要:

Introduction to switchdev SR-IOV offloads - Netdev 1.2

OpenStackとの組み合わせ例:

OpenStack Docs: Open vSwitch hardware offloading

Getting started with Mellanox ASAP^2 | Mellanox Interconnect Community

参考資料

*1:https://github.com/torvalds/linux/commit/6529eaba33f0465fc6d228e1d05b1745f7d0e8c9

*2:NICドライバではなく、MellanoxのイーサネットスイッチASIC (spectrum) をネイティブなLinuxネットワークAPIで操作できるようにするドライバ

*3:https://github.com/torvalds/linux/commit/2f4b411a3d6766e6362ffbf00e0495a2dfe92507 https://patchwork.ozlabs.org/cover/824130/ あたりを参照

*4:正確に言うとtc filter add ... skip_sw ... action dropコマンドは通るが動作している気配が無い@Intel X710

*5:対応しているのは恐らくswitch ASICであるところのmlxswのみ https://github.com/Mellanox/mlxsw/wiki/ACLs

*6:ConnectX-4 Lxでは mlx5: parsed 0 pedit actions, can't do more と言われて設定できなかった; https://github.com/torvalds/linux/blob/v4.16/drivers/net/ethernet/mellanox/mlx5/core/en_tc.c#L1653-L1656

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を採用している

Linuxにおける Equal Cost Multipath (ECMP) の設定方法と挙動に関するメモ

Linuxのネットワークスタックでmultipathを使用する場合の設定方法と挙動に関するメモ。

設定方法

設定自体は ip route add コマンドで nexthop を複数指定するだけ。src の指定は必要に応じて。

# ip route add 10.1.1.0/24 src 10.0.0.3 \
    nexthop via 192.168.11.1 weight 1 \
    nexthop via 192.168.12.1 weight 1
$ ip route
(snip)
10.1.1.0/24  src 10.0.0.3 
        nexthop via 192.168.11.1  dev eth1 weight 1
        nexthop via 192.168.12.1  dev eth2 weight 1

なお、カーネルCONFIG_IP_ROUTE_MULTIPATH=yコンパイルされていることが必要。

カーネルバージョンごとの挙動の差異

カーネルバージョン毎にトラフィックバランシング方法が異なるため、注意が必要*1

  • ~ 3.5: レイヤ3情報 (src/dst IP address) に基づく per-flow ECMP
  • 3.6 ~ 4.3: per-packet ECMP
  • 4.4 ~ 4.11: レイヤ3情報に基づく per-flow ECMP
  • 4.12 ~: レイヤ3情報 or 5-tuple (src/dst IP addr, src/dst port, L4 proto) に基づく per-flow ECMP

カーネル3.6から4.4の間の状況の概要はredditのこのスレッドに纏まっている:

www.reddit.com

カーネル3.6で一旦消滅した per-flow ECMP は、カーネル4.4で復活した:

github.com

ちなみに RHEL 7 (CentOS 7) のカーネルは3.10ベースであるため、当初の挙動は per-packet ECMP だった。ただし、3.10.0-514 (=7.3の初期カーネルパッケージ) で上記パッチがバックポートされたため、これ以降は動作が per-flow ECMP に変わっている。

https://git.centos.org/blob/rpms!kernel.git/f91430943f84931525936ea22f3cb2f24577f23a/SPECS!kernel.spec#L17805

レイヤ4情報も含めた per-flow ECMP はカーネル4.12でマージされた。net.ipv4.fib_multipath_hash_policy で 1 を指定すれば使用可能。(デフォルトは0 = L3のみ)

github.com

*1:IPv4の場合。IPv6に関しては未調査

Linux VRF with L3 Master Device

Linux kernel 4.4から登場した、L3 Master Device (l3mdev) によるVirtual Routing and Forwading (VRF) を軽く触ってみたメモ。

そもそも何をするためのものなのか

Linuxのネットワーク廻りを触っていると、たまに「特定のネットワークインタフェースから入ってきた通信に、特定のルーティングルールを適用したい」といった場面がある。こうしたケースでは、以前より Routing Policy Database (RPDB) を利用して Policy Based Routing (PBR) を行う方法が知られている。

d.hatena.ne.jp

一方、ネットワーク機器の世界では一般的に、L3ドメインを分割する手段として Virtual Routing and Forwarding (VRF) という機能が PBR とは別に存在している。VRFは、あるネットワークインタフェース群に適用されるルーティングテーブルを分離させるためのものである。このような概念をLinux上で実現するために登場したのが、L3 Master Device である。

net: L3 master device [LWN.net]

kernel/git/davem/net-next.git - David Miller's -next networking tree

使用方法

前提として、カーネルが4.4以降かつ NET_L3_MASTER_DEV=yコンパイルされている必要がある。たとえばFedoraの場合、4.11以前のパッケージではこのオプションが無効であるため注意が必要。また、iproute2 のバージョンが古いと、後述するl3mdevが暗黙的に追加するポリシーが ip rule コマンドで正しく表示されないので同様に注意する必要がある。

Bug 1428530 – Set NET_L3_MASTER_DEV=y to enable ipvlan module

設定のおおまかな流れとしては次の3ステップ。

  1. L3 Master Device (l3mdev) のネットワークインタフェースを作成する
  2. 既存のネットワークインタフェースのmasterとしてl3madevを指定する
  3. VRF毎にルーティングを設定する

流れ自体はbridge interfaceを作成する場合と似ている。l3mdev は通常のネットワークインタフェース (net_device) と同様に振る舞うので、l3mdev自体にIPアドレスを振ることも可能。ネットワーク機器で言うところのloopback address的な使い方が可能。

使用例

まずはl3mdevを作成する。ここでは vrf-xvrf-y の2個のVRFを作成する。

# ip link add dev vrf-x type vrf table 10
# ip link set dev vrf-x up
# ip link add dev vrf-y type vrf table 20
# ip link set dev vrf-y up

ip link コマンドを実行すると、2個のインタフェースが作成されていることが分かる。

$ ip link
2: ens4: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
    link/ether 52:54:00:15:7f:1c brd ff:ff:ff:ff:ff:ff
3: ens5: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
    link/ether 52:54:00:4e:69:5d brd ff:ff:ff:ff:ff:ff
4: ens6: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
    link/ether 52:54:00:cb:ef:bd brd ff:ff:ff:ff:ff:ff
5: ens7: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
    link/ether 52:54:00:f1:2e:11 brd ff:ff:ff:ff:ff:ff
6: ens3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
    link/ether 52:54:00:83:4c:ca brd ff:ff:ff:ff:ff:ff
9: vrf-x: <NOARP,MASTER,UP,LOWER_UP> mtu 65536 qdisc noqueue state UP mode DEFAULT group default qlen 1000
    link/ether 5a:b6:1d:4a:84:21 brd ff:ff:ff:ff:ff:ff
11: vrf-y: <NOARP,MASTER,UP,LOWER_UP> mtu 65536 qdisc noqueue state UP mode DEFAULT group default qlen 1000
    link/ether 76:c3:e8:67:00:01 brd ff:ff:ff:ff:ff:ff

ここでは ens4, ens5vrf-x に、ens6, ens7vrf-y に所属させることにする。

# ip link set dev ens4 master vrf-x
# ip link set dev ens5 master vrf-x
# ip link set dev ens6 master vrf-y
# ip link set dev ens7 master vrf-y

再度 ip link コマンドを実行すると、ens[4-7] のmasterが設定されていることが分かる。

$ ip link
2: ens4: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel master vrf-x state UP mode DEFAULT group default qlen 1000
    link/ether 52:54:00:15:7f:1c brd ff:ff:ff:ff:ff:ff
3: ens5: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel master vrf-x state UP mode DEFAULT group default qlen 1000
    link/ether 52:54:00:4e:69:5d brd ff:ff:ff:ff:ff:ff
4: ens6: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel master vrf-y state UP mode DEFAULT group default qlen 1000
    link/ether 52:54:00:cb:ef:bd brd ff:ff:ff:ff:ff:ff
5: ens7: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel master vrf-y state UP mode DEFAULT group default qlen 1000
    link/ether 52:54:00:f1:2e:11 brd ff:ff:ff:ff:ff:ff
6: ens3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
    link/ether 52:54:00:83:4c:ca brd ff:ff:ff:ff:ff:ff
9: vrf-x: <NOARP,MASTER,UP,LOWER_UP> mtu 65536 qdisc noqueue state UP mode DEFAULT group default qlen 1000
    link/ether 5a:b6:1d:4a:84:21 brd ff:ff:ff:ff:ff:ff
11: vrf-y: <NOARP,MASTER,UP,LOWER_UP> mtu 65536 qdisc noqueue state UP mode DEFAULT group default qlen 1000
    link/ether 76:c3:e8:67:00:01 brd ff:ff:ff:ff:ff:ff

また、ip rule コマンドを実行すると、priority 1000に l3mdev-table というポリシーが自動的に追加されていることが分かる。なお、この機能はkernel 4.8から実装されたもの*1なので、4.4~4.7のカーネルでl3mdevを使用する場合、手動でルールを追加する必要がある。

$ ip rule
0:      from all lookup local 
1000:   from all lookup [l3mdev-table] 
32766:  from all lookup main 
32767:  from all lookup default

ここで、例として 172.16.0.1/32 の経路を各VRFに追加する。

$ ip addr
2: ens4: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel master vrf-x state UP group default qlen 1000
    link/ether 52:54:00:15:7f:1c brd ff:ff:ff:ff:ff:ff
    inet 192.168.1.1/24 scope global ens4
       valid_lft forever preferred_lft forever
    inet6 fe80::5054:ff:fe15:7f1c/64 scope link 
       valid_lft forever preferred_lft forever
3: ens5: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel master vrf-x state UP group default qlen 1000
    link/ether 52:54:00:4e:69:5d brd ff:ff:ff:ff:ff:ff
    inet 192.168.2.1/24 scope global ens5
       valid_lft forever preferred_lft forever
    inet6 fe80::5054:ff:fe4e:695d/64 scope link 
       valid_lft forever preferred_lft forever
4: ens6: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel master vrf-y state UP group default qlen 1000
    link/ether 52:54:00:cb:ef:bd brd ff:ff:ff:ff:ff:ff
    inet 10.0.1.1/24 scope global ens6
       valid_lft forever preferred_lft forever
    inet6 fe80::5054:ff:fecb:efbd/64 scope link 
       valid_lft forever preferred_lft forever
5: ens7: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel master vrf-y state UP group default qlen 1000
    link/ether 52:54:00:f1:2e:11 brd ff:ff:ff:ff:ff:ff
    inet 10.0.2.1/24 scope global ens7
       valid_lft forever preferred_lft forever
    inet6 fe80::5054:ff:fef1:2e11/64 scope link 
       valid_lft forever preferred_lft forever
6: ens3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether 52:54:00:83:4c:ca brd ff:ff:ff:ff:ff:ff
    inet 10.168.20.201/24 brd 10.168.20.255 scope global ens3
       valid_lft forever preferred_lft forever
    inet6 fe80::24fd:811e:6432:bd36/64 scope link 
       valid_lft forever preferred_lft forever
9: vrf-x: <NOARP,MASTER,UP,LOWER_UP> mtu 65536 qdisc noqueue state UP group default qlen 1000
    link/ether 5a:b6:1d:4a:84:21 brd ff:ff:ff:ff:ff:ff
11: vrf-y: <NOARP,MASTER,UP,LOWER_UP> mtu 65536 qdisc noqueue state UP group default qlen 1000
    link/ether 76:c3:e8:67:00:01 brd ff:ff:ff:ff:ff:ff
# ip route add 172.16.0.1/32 via 192.168.1.2 table 10 
# ip route add 172.16.0.1/32 via 10.0.1.2 table 20

ip route list を見ると、デフォルトおよび各VRF毎に独立したルーティングテーブルが設定されていることが確認できる。

$ ip route list
default via 10.168.20.1 dev ens3 proto static metric 100 
10.168.20.0/24 dev ens3 proto kernel scope link src 10.168.20.201 metric 100
$ ip route list vrf vrf-x
172.16.0.1 via 192.168.1.2 dev ens4 
192.168.1.0/24 dev ens4 proto kernel scope link src 192.168.1.1 
192.168.2.0/24 dev ens5 proto kernel scope link src 192.168.2.1 
$ ip route list vrf vrf-y
10.0.1.0/24 dev ens6 proto kernel scope link src 10.0.1.1 
10.0.2.0/24 dev ens7 proto kernel scope link src 10.0.2.1 
172.16.0.1 via 10.0.1.2 dev ens6 

Network Namespaceとの比較

Linuxのネットワークリソースを分離・独立させる手段としては、最近はNetwork Namespaceがポピュラーである。Network Namespaceはネットワークスタック全体を分離するのに対し、l3mdevはL3のルーティングのみを分離する。l3mdevのモデルは、L3ドメインをまたいでプロセスを実行したい場合、具体的にはルーティングデーモンを実行する場合に都合が良い。このあたりの背景は、L3 Master Deviceの提案者であるDavid Ahernが書いた記事が詳しい。

cumulusnetworks.com

アプリケーション側の挙動に関しては、カーネル付随ドキュメントの Applications セクションに記載がある通り sysctlnet.ipv4.tcp_l3mdev_accept / net.ipv4.udp_l3mdev_accept によって変化するようである。このあたりはまた別途触ってみる予定。

Reference

自宅のルータをAT-AR2050Vに入れ替えた & 回線をv6プラス化した

自宅マンションで光配線方式のフレッツが使えるようになったので、これを機にルータの入れ替えとv6プラス(IPv6 IPoE)化を実施したメモ。 結論から言うと、IPv4 PPPoE時代に発生していた夜間帯の輻輳が解消され、かなり快適になった。

構成変更前の環境

現住居に引っ越した時点ではフレッツのVDSL方式しか選択肢が無かったため、一旦考えるのをやめて最低限インターネットに接続できる環境だけを構築して使っていた。

ひかり電話契約は無し。VDSLモデムの下にルータが居て、そこでIPv4 PPPoEを終端するだけの単純な構成。

構成変更後の構成

回線が光配線方式に。また、Biglobe経由でJPNEのv6プラスも適用。

AT-AR2050Vなのは昨年末までキャンペーンで3万円弱で買えたため。昨年末、似たような価格でJuniper SRX300がeBayで大量に出回っていて気になったものの見送り。

v6プラスの効果

今回の構成変更で一番効果が有ったのは、v6プラス化したことにより夜間帯の輻輳が解消されたこと。

特にDNS名前解決が速くなったのが体感速度向上に貢献しているように思う。帯域幅に関してはそれほど気にしていないが、試しに ftp.kddilabs.jp から適当なisoイメージをダウンロードすると400Mbps弱出ている。

ネットワーク構成

それほど複雑なことはしていない。HGW (PR-500MI) - ルータ (AT-AR2050V) - L2 (GS908M) というシンプルな構成。 AR2050Vでrouted VLAN interfaceを2個作成し、それぞれを日常生活用と各種実験用として使っている。VLANはGS908Mまでtag VLANで運び、そこでポート毎に解いている。

IPv4インターネット接続に関しはHGWでNAT(MAP-E)されて出ていく。AR2050Vの下に居るサブネット宛のstatic routeをHGWに設定する必要があるので注意。 IPv6インターネット接続に関しては、HGWからAR2050VにDHCPv6-PDで/60が振ってくるので、これをクライアントに振っている。

IPv6まわりに関しての設定例がアライドテレシスのサイトにあるので、それほど苦労せずに設定できた。

AT-AR2050V/AT-AR3050S/AT-AR4050S コマンドリファレンス 5.4.7: 設定例集#44: フレッツ 光ネクスト IPv6インターネット接続(IPv6 IPoE、DHCPv6 PD方式)

機器周辺はこんな感じ。横向きなのがHGWで、隙間に居るのはDNSフルリゾルバ兼DHCPサーバのRaspberry Pi. 上の段に居るのは各種実験用に使っているQuanta LB6M. 無線LANはHGW内蔵APは使用せず、Aterm WG1200HPをブリッジモードで使用。

f:id:yunazuno:20170513161528j:plain

Facebookはレイヤ4ロードバランサをIPVS(LVS)からXDPベースのものに乗り換えつつある

4月に開催されたnetdev 2.1で面白いセッションがあったのでメモ。

Facebookが使用しているレイヤ4のロードバランサに関する発表で、従来はIPVS (LVS) を使用していたが、XDPベースで自ら開発したものに移行しつつある、という内容。

XDP Production Usage: DDoS Protection and L4LB (slide)

www.youtube.com

XDP (eXpress Data Path) については以前のエントリで簡単に紹介した。

yunazuno.hatenablog.com

XDPを改めて簡単に紹介すると、Linuxカーネルのネットワークスタックの最下部 (NICに一番近い場所) でパケット処理を行う仕組みのことで、オーバーヘッドが非常に小さい高速パケット処理の実現を目的としている。XDPはeBPFを用いており、eBPFが提供するmapやhelper functionといった機能を用いて柔軟なプログラムが書けることに加え、JITによる高速化やカーネル自身の堅牢性をそのまま享受することができるとされている。

XDPがリリースされたのは2016年10月のカーネル4.8なので、半年程で実際の事例が出てきたことになる。

FacebookにおけるXDPのユースケース: L4LB & DDoS防御

FacebookはIPVS (LVS) ベースのレイヤ4ロードバランサ (L4LB, 通称Shiv) を利用していることが知られている。

blog.stanaka.org

yunazuno.hatenablog.com

Shivの役割はConsistent Hashによる負荷分散先の決定とIP encapsulation (IPIP) によるパケット転送である。また、ローカルにセッションキャッシュを持つことにより、Consistent Hashの弱点であるリマップ時のセッション切断問題をカバーしている。

今回の発表では、これらの機能をXDPの基盤上で実装したのちIPVSベースの既存環境を置き換えていることが明かされた。スライドでは下のグラフが示され、XDPで実装された新しいShivがIPVSベースのそれと比較して高いスループットを発揮しつつもCPU使用率が低く抑えられている、と述べられている*1

f:id:yunazuno:20170507222416p:plain (スライドp3より; それぞれ1本だけラインが離れているものがXDP, 固まっているものがIPVS)

また、DDoS防御のための仕組み (Dropletと呼ばれる) も併せて紹介されている。

DDoS防御にあたっては、攻撃パケットを可能な限り早い段階で検出・遮断することが望ましい。加えて、攻撃パケット検出のシグネチャプログラマブルに構成・変更可能であることが望ましい。彼らはこれもXDP上に実装している。L4LBのプログラムの前にDDoS検出・遮断のプログラムが実行されるようにすることで、L4LBが余分なリソースを消費することを防いでいる。

f:id:yunazuno:20170507222600p:plain (スライドp10より; #0 XDP Dumpはデバッグ用のもの)

ちなみに、XDPによるDDoS防御についてはCloudflareのエンジニアも同様の発表を行っている。こちらはよりBPFにフォーカスした内容。

XDP in practice: integrating XDP in our DDoS mitigation pipeline (slide)

www.youtube.com

実装の詳細

冒頭のビデオで発表部分が12分弱で収まっていることからも分かる通り、やっていること自体は非常にシンプルである。

ただその裏では、(発表内で直接は触れられていないものの) XDPやその基礎となるeBPFの機能がふんだんに使われていることが見て取れる。これらの機能はFacebookのエンジニア自身によって実装されたのちLinuxカーネルにマージされたものも少なくない。

BPF_MAP_TYPE_LRU_HASH: セッション維持のためのLRU cache

Consistent Hashを用いるL4LBではその特性上、分散先のリアルサーバが増減したタイミングで幾許かのTCPセッションが意図せず切断されてしまう。この弱点を補うため、いくつかのL4LB実装ではセッションテーブルを併用している。TCP SYNパケットがL4LBに到達すると同時にセッションテーブルにセッション情報を記録すれば、仮にConsistent Hashのリマップが発生したとしてもセッションテーブルに基づいてTCPセッションを維持できる、という仕組みである。ただセッションテーブルのサイズは有限であるため、何らかの方法でセッションテーブルのメンテナンスを行う必要がある。

そこで登場するのがeBPF map objectのひとつ、BPF_MAP_TYPE_LRU_HASHである。その名前が示す通り、Least Recently Used (LRU) なハッシュテーブルである。これを利用すれば、先に述べた要件を満たすセッションテーブルを容易に実現できる。 この機能は冒頭で紹介したセッションのスピーカーであるMartin Lauによって開発され、kernel 4.10以降で利用可能である。

xdp_adjust_head(): IP encapsulation

ShivはL3DSRによるリアルサーバへのパケット転送にIPIP方式を採用している。これはリアルサーバ宛てIPヘッダで元のVIP宛てパケットをカプセル化することでパケット転送を実現するものである*2。 これをXDPで実現するため、メモリページ上であらかじめ空間 (headroom) を確保しておき、そこにデータ(ここではリアルサーバ宛てのIPヘッダ)を追加するインタフェースが用意されている。このインタフェースがeBPF helper functionのひとつ、bpf_xdp_adjust_head()である。この機能も同じくMartin Lauによって開発され、kernel 4.10以降で利用可能である。

なお、NICドライバには通常のXDPサポートとは別にheadroom確保のサポートが要求される。

BPF_MAP_TYPE_PROG_ARRAY & tail call: 複数のeBPFプログラムを連携させる

XDPはその仕様上、1個のネットワークインタフェースに対して1個のeBPFプログラムのみアタッチすることができる。これは1個のネットワークインタフェースに対して複数の独立した処理を適用したい場合に都合が悪い。 これを解決するのがBPF_MAP_TYPE_PROG_ARRAYbpf_tail_call() である。

yunazuno.hatenablog.com

上記エントリで紹介した通り、BPF_MAP_TYPE_PROG_ARRAYbpf_tail_call()を組み合わせることで、複数のeBPFプログラムを連結して呼び出したり実行中のeBPFプログラムを動的に入れ替えたりできる。ちなみにこの機能を実装したAlexei StarovoitovFacebookに在籍している*3

余談: XDPの近況

2016年10月にXDPがリリースされてから約半年が経過したが、その間に様々なアップデートが行われた。

まず大きなトピックとしてNICドライバのサポート拡充が挙げられる。当初はMellanox mlx4のみであったものが徐々に拡充され、先日遂にIntel ixgbeサポートがnet-nextにマージされた。恐らくカーネル4.12でリリースされると思われ、XDPが利用可能な環境が大幅に広がることになる。また変わり種として、CaviumのARM SoCであるThunderXでのサポートもnet-nextにマージされている

現時点では、下記のNICドライバで利用可能あるいは近日中に利用可能となる:

  • mlx4: Mellanox ConnectX-3
  • mlx5: Mellanox ConnectX-4 or later
  • nfp: Netronome Agilio
  • qede: QLogic FastlinQ
  • virtio_net: Virtio
  • bnxt_en: Broadcom NetXtreme C-Series
  • ixgbe: Intel X5xx (82598, 82599) (from kernel 4.12?)
  • thunderx: Cavium ThunderX (from kernel 4.12?)

また、NICドライバのサポートが無くてもXDPプログラムを実行できるようにする仕組み (“Generic XDP”) も登場した。

kernel/git/torvalds/linux.git - Linux kernel source tree - net: Generic XDP

これは主に開発用やお試し用を意図しており、パフォーマンスに関しては考慮されていない。これまでXDPプログラムを実行するには、対応NICが挿さったハードウェアを用意するか、あるいはe1000, virtio_netの環境を用意する必要があった。Generic XDPの登場により、より手軽に開発環境を整えられるようになる。

なお、各カーネルバージョン毎にサポートされるeBPF/XDP関連の機能やドライバの情報はiovisor/bccのドキュメントに纏まっている:

bcc/kernel-versions.md at master · iovisor/bcc · GitHub

XDPリリース直後の2016年12月にPLUMgrid*4VMWareに買収される出来事があったものの、特に大きな影響も無くXDP周辺の発展は続いている様子である。eBPF自体を含め機能がどんどん拡充されており、かつXDPのユースケースも徐々に出始めてきたことで、今後の動向がますます楽しみ。

*1:正直なところ、このグラフは読み方や環境、条件がよく分からないので何とも言えないところ。もちろん、XDPがIPVSと比較して同一条件下で高スループットが得られる、という結果自体は疑いようが無いものの・・・。

*2:対してIPヘッダのDSCPフィールドを使うのがDSCP方式 https://www.nanog.org/meetings/nanog51/presentations/Monday/NANOG51.Talk45.nanog51-Schaumann.pdf

*3:当時はPLUMgrid

*4:XDPを推進していた陣営のひとつ