ふつうの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ハードウェアオフロードの組み合わせにより、高パフォーマンスを維持しつつより多くの問題に適用範囲が広がることを期待したい。

PythonでNICの統計情報を取得する

Pythonなコードから ethtool -S 相当のNIC統計情報を取得する必要に迫られたときどうするか。ただし ethtool コマンドの出力結果をパースする方法は使用しないものとする。

$ python -V
Python 3.6.5
$ uname -a
Linux laptop 4.16.12-200.fc27.x86_64 #1 SMP Fri May 25 21:10:16 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux

gist.github.com

$ python ethtool.py wlp4s0
rx_packets: 421793
rx_bytes: 423497644
rx_duplicates: 4
rx_fragments: 421402
rx_dropped: 2008
tx_packets: 218510
tx_bytes: 60880710
tx_filtered: 0
tx_retry_failed: 0
tx_retries: 15806
sta_state: 4
txrate: 866700000
rxrate: 780000000
signal: 203
channel: 0

ざっくりした方針として ethtool.cのdo_gstats()を移植している。本来は ctypes.Structure を使うのが正しいのだとは思うが、可変長の要素 (たとえば struct ethtool_gstrings ) を綺麗に扱う方法が思い付かなかったので array.array で。

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

Generic XDPを使えばXDP動作環境がお手軽に構築できるようになった

XDP (eXpress Data Path) ネタ。

パフォーマンス上の理由から、XDPのパケット処理はskbが割り当てられる前にNICドライバから直接呼び出させる。そのため、XDPプログラムの動作にはNICドライバ側でのサポートが必要となる。

5月時点での対応ドライバリストは下記エントリ末尾に記載した。Linux kernel 4.12からixgbeでXDPがサポートされる予定とは言え、ハードウェアの準備も必要であり、従来XDPの動作を気軽に試すことは少し難しかった。

yunazuno.hatenablog.com

そこでkernel 4.12から登場するのが Generic XDP と呼ばれる機能である。これはskbが割り当てられた後にXDPのパケット処理を適用するものである*1。これによりNICドライバのサポートが無くともXDPプログラムを実行可能になった。パフォーマンスを犠牲にする代わりに、XDPプログラムの開発環境として使用したりXDP自体の間口を広げることを目的としている。

動作環境

前述した通り、Generic XDPはkernel 4.12でリリース予定である。4.12 rc3時点で既に最初のコミットに加えいくつかの微修正やサンプルコードの更新もマージされている。

プログラムのアタッチ時の動作

ネットワークインタフェースにXDPプログラムをアタッチする際*2、ドライバ側でのXDPサポート状況やフラグの値により、通常のNIC-level XDPを使用するか、あるいはGeneric XDPを使用するかが決定される。

  • デフォルト: まずNIC-level XDPでのアタッチを試み、失敗したら (=NICドライバがサポート外) Generic XDPを使用する
  • XDP_FLAGS_SKB_MODE を指定: Generic XDPのみを使用する
  • XDP_FLAGS_DRV_MODE を指定: NIC-level XDPでのアタッチのみを試みる。Generic XDPは使用しない

また、iproute2 (ipコマンド) はバージョン4.10からXDPプログラムローダとしての機能を持っている。更に次回リリースではGeneric XDPに特化したロード (上記 XDP_FLAGS_SKB_MODE に該当) もサポートされる予定である。

実際に試してみる

簡単なXDPプログラムを用意した上でそれをXDP非対応NICにアタッチし、Generic XDPが期待通り動作することを確認する。 また、XDPプログラムローダとして、過去のエントリで使用したiovisor/bccではなくiproute2を使用する。

  • Kernel: 4.12 rc3 (Fedora rawhide 4.12.0-0.rc3.git0.2.fc27.x86_64)
  • iproute2: 4.11+ (5a3ec4b)
  • NIC: 8139cp (qemu-kvm RTL8139, ens4)
    • NIC-level XDPは非サポート

まず、全ての受信パケットをドロップするXDPプログラムを用意し、コンパイルする:

#include <linux/bpf.h>

#ifndef __section
# define __section(NAME)                  \
   __attribute__((section(NAME), used))
#endif

__section("prog")
int xdp_drop(struct xdp_md *ctx)
{
    return XDP_DROP;
}

char __license[] __section("license") = "GPL";
$ clang -O2 -Wall -target bpf -c xdp_drop.c -o xdp_drop.o

続いて、これをipコマンドでens4にアタッチする:

# ip link set dev ens4 xdp obj xdp_drop.o

ip link showの出力結果にxdpgenericという文字列が出現し、Generic XDPでXDPプログラムがアタッチされていることが確認できる*3

# ip link show dev ens4
2: ens4: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 xdpgeneric qdisc fq_codel state UP mode DEFAULT group default qlen 1000
    link/ether 52:54:00:71:fc:9b brd ff:ff:ff:ff:ff:ff

デタッチは同様にipコマンドで:

# ip link set dev ens4 xdp off

このとき別ノードからens4宛にpingを打ってみると、XDPプログラムをアタッチしていた間、pingのレスポンスが無くなっていたことが確認できる。

$ ping 192.168.11.239
PING 192.168.11.239 (192.168.11.239) 56(84) bytes of data.
64 bytes from 192.168.11.239: icmp_seq=1 ttl=64 time=0.560 ms
64 bytes from 192.168.11.239: icmp_seq=2 ttl=64 time=0.318 ms
64 bytes from 192.168.11.239: icmp_seq=3 ttl=64 time=0.385 ms
64 bytes from 192.168.11.239: icmp_seq=8 ttl=64 time=0.594 ms
64 bytes from 192.168.11.239: icmp_seq=9 ttl=64 time=0.508 ms
64 bytes from 192.168.11.239: icmp_seq=10 ttl=64 time=0.624 ms
^C
--- 192.168.11.239 ping statistics ---
10 packets transmitted, 6 received, 40% packet loss, time 9205ms
rtt min/avg/max/mdev = 0.318/0.498/0.624/0.111 ms

References

*1:具体的に言うとnet/core/dev.cのnetif_receive_skb_internal

*2:netlink経由で実行する; サンプル: http://elixir.free-electrons.com/linux/v4.12-rc3/source/samples/bpf/bpf_load.c#L674

*3:通常のNIC-level XDPの場合は単に xdp と表示される