BPF_PROG_TEST_RUNでXDPプログラムの挙動をテストする
eBPFには BPF_PROG_TEST_RUN
と呼ばれる機能がある。これを活用すると、XDPやtc-bpf(8)
向けに実装したパケット処理プログラムの挙動をテストすることができる。
BPF_PROG_TEST_RUN?
BPF_PROG_TEST_RUN
はbpf
syscall経由で使用できる1機能。テストしたいeBPFプログラムとパケットのバイト列を入力として与えると、実行結果の返り値と処理後のパケットバイト列、および処理にかかった時間が出力として得られる。このとき、テスト対象のプログラムは実際にネットワークインタフェースにアタッチされるわけではないので、実行環境に影響を与えることなくテストを遂行できる。
libbpfに bpf_prog_test_run()
というラッパ関数が用意されているので、実用上はこれを経由して使用するのが便利。
libbpf/bpf.c at d5b146fec50d7aa126fe98323aeaee688d4af289 · libbpf/libbpf · GitHub
int bpf_prog_test_run(int prog_fd, int repeat, void *data, __u32 size, void *data_out, __u32 *size_out, __u32 *retval, __u32 *duration)
入力のバイト列 (XDPで言うところのstruct xdp_md *
のdata
) を *data
で与えると、返り値と処理後のバイト列がそれぞれ *retval
および*data_out
経由で得られる。
使用例
ここではPythonからBPF_PROG_TEST_RUN
を実行してみる。
BCCのこのPRがマージされると、Pythonからlibbpfの関数を呼び出せるようになる。今回はこのブランチをビルドしてテストに使用した。
$ uname -a Linux xdp 4.19.15-300.fc29.x86_64 #1 SMP Mon Jan 14 16:32:35 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux $ cat /etc/fedora-release Fedora release 29 (Twenty Nine) $ python3 -V Python 3.7.2
テスト対象として、IPv4の80/tcp宛パケットをドロップするXDPプログラムを仮定する。
#include <uapi/linux/if_ether.h> #include <uapi/linux/in.h> #include <uapi/linux/ip.h> #include <uapi/linux/tcp.h> int drop_ipv4_tcp_80(struct xdp_md *ctx) { void *data = (void *)(long)ctx->data; void *data_end = (void *)(long)ctx->data_end; struct ethhdr *eth; struct iphdr *iph; struct tcphdr *th; eth = data; if ((void *)(eth + 1) > data_end) return XDP_DROP; if (eth->h_proto != htons(ETH_P_IP)) return XDP_PASS; iph = (struct iphdr *)(eth + 1); if ((void *)(iph + 1) > data_end) return XDP_DROP; if (iph->ihl != 5 || iph->frag_off & htons(0x2000 | 0x1FFF)) return XDP_PASS; if (iph->protocol != IPPROTO_TCP) { return XDP_PASS; } th = (struct tcphdr *)(iph + 1); if ((void *)(th + 1) > data_end) return XDP_DROP; if (th->dest == htons(80)) return XDP_DROP; return XDP_PASS; }
次に、このプログラムを bpf_prog_test_run
でテストするコードをPythonで用意する。今回はテストケースを unittest
で記述した。
import unittest from bcc import BPF, libbcc import ctypes from scapy.all import * class PacketDropTestCase(unittest.TestCase): bpf = None func = None DATA_OUT_LEN = 1514 def _run_test(self, data, data_out_expect, retval_expect, repeat=1): size = len(data) data = ctypes.create_string_buffer(raw(data), size) data_out = ctypes.create_string_buffer(self.DATA_OUT_LEN) size_out = ctypes.c_uint32() retval = ctypes.c_uint32() duration = ctypes.c_uint32() ret = libbcc.lib.bpf_prog_test_run(self.func.fd, repeat, ctypes.byref(data), size, ctypes.byref(data_out), ctypes.byref(size_out), ctypes.byref(retval), ctypes.byref(duration)) self.assertEqual(ret, 0) self.assertEqual(retval.value, retval_expect) if data_out_expect: self.assertEqual(data_out[:size_out.value], raw(data_out_expect)) def setUp(self): self.bpf = BPF(src_file=b"drop_ipv4_tcp_80.c") self.func = self.bpf.load_func(b"drop_ipv4_tcp_80", BPF.XDP) def test_ipv4_tcp_80(self): packet_in = Ether() / IP() / TCP(dport=80) self._run_test(packet_in, None, BPF.XDP_DROP) def test_ipv4_udp_80(self): packet_in = Ether() / IP() / UDP(dport=80) self._run_test(packet_in, packet_in, BPF.XDP_PASS) def test_ipv4_tcp_443(self): packet_in = Ether() / IP() / TCP(dport=443) self._run_test(packet_in, packet_in, BPF.XDP_PASS) def test_ipv6_tcp_80(self): packet_in = Ether() / IPv6() / TCP(dport=80) self._run_test(packet_in, packet_in, BPF.XDP_PASS) if __name__ == '__main__': unittest.main()
これを実行すると、テスト対象のプログラムがどうやら正しく実装されていそうなことが分かる。
$ sudo python3 test_xdp_prog.py .... ---------------------------------------------------------------------- Ran 4 tests in 1.538s OK
続いてテスト対象のプログラムに意図的にバグを仕込んだ上でテストを再実行する。
$ diff -u drop_ipv4_tcp_80.c{.orig,} --- drop_ipv4_tcp_80.c.orig 2019-01-19 08:32:41.388966124 +0000 +++ drop_ipv4_tcp_80.c 2019-01-19 08:33:08.758135901 +0000 @@ -26,6 +26,7 @@ return XDP_PASS; if (iph->protocol != IPPROTO_TCP) { + iph->ttl -= 1; return XDP_PASS; } @@ -33,7 +34,7 @@ if ((void *)(th + 1) > data_end) return XDP_DROP; - if (th->dest == htons(80)) + if (th->dest == 80) return XDP_DROP; return XDP_PASS;
$ sudo python3 test_xdp_prog.py .FF. ====================================================================== FAIL: test_ipv4_tcp_80 (__main__.PacketDropTestCase) ---------------------------------------------------------------------- Traceback (most recent call last): File "test_xdp_prog.py", line 40, in test_ipv4_tcp_80 self._run_test(packet_in, None, BPF.XDP_DROP) File "test_xdp_prog.py", line 30, in _run_test self.assertEqual(retval.value, retval_expect) AssertionError: 2 != 1 ====================================================================== FAIL: test_ipv4_udp_80 (__main__.PacketDropTestCase) ---------------------------------------------------------------------- Traceback (most recent call last): File "test_xdp_prog.py", line 44, in test_ipv4_udp_80 self._run_test(packet_in, packet_in, BPF.XDP_PASS) File "test_xdp_prog.py", line 32, in _run_test self.assertEqual(data_out[:size_out.value], raw(data_out_expect)) AssertionError: b'\xf[77 chars]0\x00?\x11|\xce\x7f\x00\x00\x01\x7f\x00\x00\x0[20 chars]x01W' != b'\xf[77 chars]0\x00@\x11|\xce\x7f\x00\x00\x01\x7f\x00\x00\x0[20 chars]x01W' ---------------------------------------------------------------------- Ran 4 tests in 1.510s FAILED (failures=2)
先程までパスしていたテストケースが通らなくなっており、無事問題を発見することができた。
ふつうのNICでハードウェアL3スイッチング; あるいはSR-IOV switchdev modeとTC hardware offloadの使用例
最近のNICの中にはTCハードウェアオフロード機能を持っているものがあり、これを使えばNICハードウェア上でパケット転送処理に手を加えられることは以前のエントリで簡単に紹介した。
今回はこれを応用し、NICハードウェア上でL3スイッチングを実現できないか検討してみる。
前置き: tcを用いたレイヤ3パケットスイッチング
L3スイッチの動作はおおまかに、 1. 受信したパケットの宛先IPアドレスをスキャンし、 2. 宛先MACアドレスを書き換えた上で、 3. 出口ポートからパケットを送信する、 というステップから構成される。これらの動作はそれぞれ tc
の flower
フィルタ、pedit
アクション、mirred
アクションで表現することができる。
tcを用いたL3スイッチ動作表現の具体例
ここでは2個のNetwork Namespace ns0
, ns1
がL3 Master Device vrf-tc
経由で接続されている状況を考える。
+--------------------+ | 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
からns1
の10.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
からns1
の10.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にアタッチすればそのVMはNICのハードウェア性能を直接享受できる反面、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やインタフェース名、ドライバ名は各環境に応じて。
なお今回の環境は以下の通り:
# 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
からns3
の10.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スクリプトを用意した。
上記スクリプトを実行した状態でns2
からns3
の10.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
$ 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ハードウェア上でパケットフィルタを実施する
Linuxのtc
には、指定した条件にマッチしたパケットをドロップしたりヘッダを書き換えたりするための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を有効にしたのち、対象インタフェースのqdisc
にingress
を追加する。qdisc
はingress
ではなくclsact
を使用してもよい。clsact
はingress
を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 --flood
で8080/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
経由で設定することで以前から実現可能である:
ethtool
を用いたntupleフィルタと比較したtc flower offloadの利点は、drop
以外のアクションをオフロードすることでNICハードウェアに多彩な動作を行わせることが(仕組み上)可能な点にある。
たとえばpedit
アクションとcsum
アクションをオフロードすれば、簡易的なstatic NATを実装できる*6:
また、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
参考資料
- TC Flower Offload - Netdev 2.2
- tc flower offloadのイントロダクション的なセッション。スライド、ビデオあり
- TC Workshop - Netdev 2.2
- 各NICベンダの対応状況など。スライド、ビデオあり
- tc flower offload関連のman pages
*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からフォークしたルーティングデーモンがある。
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のコードを元に、その方法を追ってみる。
- tag: frr-2.0 https://github.com/FRRouting/frr/tree/frr-2.0
- RFC5549サポートが実装された最初のコミット
まずはコード内を "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.c
のif_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
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のこのスレッドに纏まっている:
カーネル3.6で一旦消滅した per-flow ECMP は、カーネル4.4で復活した:
ちなみに RHEL 7 (CentOS 7) のカーネルは3.10ベースであるため、当初の挙動は per-packet ECMP だった。ただし、3.10.0-514 (=7.3の初期カーネルパッケージ) で上記パッチがバックポートされたため、これ以降は動作が per-flow ECMP に変わっている。
レイヤ4情報も含めた per-flow ECMP はカーネル4.12でマージされた。net.ipv4.fib_multipath_hash_policy
で 1 を指定すれば使用可能。(デフォルトは0 = L3のみ)
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) を行う方法が知られている。
一方、ネットワーク機器の世界では一般的に、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ステップ。
- L3 Master Device (l3mdev) のネットワークインタフェースを作成する
- 既存のネットワークインタフェースのmasterとしてl3madevを指定する
- VRF毎にルーティングを設定する
流れ自体はbridge interfaceを作成する場合と似ている。l3mdev は通常のネットワークインタフェース (net_device) と同様に振る舞うので、l3mdev自体にIPアドレスを振ることも可能。ネットワーク機器で言うところのloopback address的な使い方が可能。
使用例
まずはl3mdevを作成する。ここでは vrf-x
と vrf-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
, ens5
を vrf-x
に、ens6
, ens7
を vrf-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が書いた記事が詳しい。
アプリケーション側の挙動に関しては、カーネル付随ドキュメントの Applications セクションに記載がある通り sysctl
で net.ipv4.tcp_l3mdev_accept
/ net.ipv4.udp_l3mdev_accept
によって変化するようである。このあたりはまた別途触ってみる予定。
Reference
- https://www.kernel.org/doc/Documentation/networking/vrf.txt
- カーネルに付随するドキュメント。
- VRF for Linux — a contribution to the Linux Kernel - Cumulus Networks Blog
- l3mdevの提案者、David Ahernによるエントリ。VRF実装が必要とされるコンテキストが詳細に述べられている。
- What is an L3 Master Device?
- 同じくDavid Ahernによるnetdev 1.2での発表資料。実装面やパフォーマンスについても述べられている。