# 計算機網路 - XDP [TOC] ![](https://i.imgur.com/pFwo1HB.png) ([圖片來源](https://elinux.org/images/9/96/Elce-af_xdp-topel-v3.pdf)) ## 參考影片 在這之前,如果需要看一下 eBPF 的簡介,那可以去 YouTube 上找。比如說 [*"eBPF 101" - Muhammad Falak R Wani (LCA 2022 Online)*](https://www.youtube.com/watch?v=n6SdBmgLFmA&list=LL7n1RsbJfolU3-2KfaI8QYg)。2016 年開始每年各大研討會都會有,會放這個是因為年份看起來最新。 ### Get start with XDP: write a network filter in 10m - DevConf.CZ 2021 {%youtube ZfpAAyfeiwA %} ### eCHO episode 13: XDP Hands-on Tutorial {%youtube YUI78vC4qSQ %} 這個教學的原始版本是 2019 年 netdev 中的 [*XDP Hands On Tutorial*](https://youtu.be/XQOPAemSj88)。不過這個工作坊當中比較像是參與者自己看文件做一遍,有問題自己發問,幾乎沒有講解。 ### Netdev 0x13 - Veth XDP: XDP for containers {%youtube q3gjNe6LKDI %} 上一個影片是使用 network namespace 搭配 `veth` 做實驗。更多 XDP 在 container 中的應用可以參考這個影片。 ### Hello [World] XDP_DROP! Writing Your First XDP/eBPF Program {%youtube kFC8Bfk3FuI %} ### Netdev 0x14 - Tutorial: XDP and the cloud : Using XDP on hosts and VMs {%youtube l9C-ANkN1-Q %} ### Low-Latency, Deterministic Networking with Standard Linux using XDP Sockets Magnus Karlsson & Björn Töpel, Intel 是 `AF_XDP` 的介紹, {%youtube p61PlC9y62k %} ## 環境設置 這裡使用的是 [`xdp-project/xdp-tutorial`](https://github.com/xdp-project/xdp-tutorial) 中的例子。除了該文件上提到的套件外,如果是在 Raspberry Pi 4B 上使用 Ubuntu 21.10,那麼要多安裝以下套件: ``` $ sudo apt-get install libbpf-dev ``` 否則會出現下面的警告: ``` xdp_pass_kern.c:3:10: fatal error: 'bpf/bpf_helpers.h' file not found #include <bpf/bpf_helpers.h> ^~~~~~~~~~~~~~~~~~~ 1 error generated. ``` 另外,如果是使用 [`xdp-project/xdp-tutorial`](https://github.com/xdp-project/xdp-tutorial) 中的 Makefile,而且要在 Raspberry Pi 4B 上編譯,那要在 `common/common.mk` 中的 `BPF_CFLAGS` 後面多加上 `BPF_CFLAGS += -I/usr/include/aarch64-linux-gnu` 選項 (參考 [*Update setup_dependencies.org #201*](https://github.com/xdp-project/xdp-tutorial/pull/201/commits/c09c1cf8493254457428bc8272761ec0be44f273) 這個 PR): ```diff ... BPF_CFLAGS ?= -I$(LIBBPF_DIR)/build/usr/include/ -I../headers/ +BPF_CFLAGS += -I/usr/include/aarch64-linux-gnu ... ``` 而如果是在命令列直接用 `clang` 編譯,那要加上 `-I/usr/include/aarch64-linux-gnu` 選項。 ## basic01 ### 不用 Makefile 編譯與載入 如果是在 Raspberry Pi 4B 上編譯,但是不想使用它的 Makefile,那麼可以使用以下命令: ```shell $ clang -O2 -g -Wall \ -target bpf -c xdp_pass_kern.c \ -I/usr/include/aarch64-linux-gnu \ -o xdp_kern_pass.o ``` 然後使用以下命令來載入: ```shell $ sudo ip link set dev lo xdpgeneric obj xdp_pass_kern.o sec xdp ``` 這裡的 `xdpgeneric` 選項有一些細節,不過用下一個例子說明會比較清楚。 ## basic02 例子裡面提到會藉由兩個 network namespace 之間的溝通作為例子,使用當中的 `testenv.sh` 會建立具有以下的 namespace: ``` +-----------------------------+ +-----------------------------+ | Root namespace | | namespace 'veth-basic02' | | | From 'veth-basic02' | | | +--------------+ TX-> RX-> +--------+ | | | veth-basic02 +--------------------------+ veth0 | | | +--------------+ <-RX <-TX +--------+ | | | From 'veth0' | | +-----------------------------+ +-----------------------------+ ``` ### 手動設置 Network Namespace 如果不想用那個 `testenv.sh` 來建立 namespace,也可以用以下方法建立類似的實驗環境。首先,新增一個名稱為 `test01` 的 network namespace: ```shell $ sudo ip netns add veth-basic02 ``` 然後新增一對 veth-pair。其中,令這個 veth-pair 中的兩個 interface 的名稱各自為 `veth-basic02` 與 `veth0`: ```shell $ sudo ip link add veth0 type veth peer name veth-basic02 ``` 把 `veth0` 那個 interface 加入 `veth-basic02` 這個 network namespace 中: ```shell $ sudo ip link set veth0 netns veth-basic02 ``` 接著幫這兩個 interface 各自設定 IPv6 位址: ```shell $ sudo ip addr add fc00:dead:cafe:1::1/64 dev veth-basic02 scope global $ sudo ip addr add fe80::cc4c:c0ff:fe33:bad6/64 dev veth-basic02 scope link $ sudo ip -n veth-basic02 addr add fc00:dead:cafe:1::2/64 dev veth0 scope global $ # use default local unicast addree for veth-basic02 ``` 最後,啟動這兩個 interface: ```shell $ sudo ip link set veth-basic02 up $ sudo ip -n veth-basic02 link set veth0 up ``` 就可以在 `veth-basic02` 這個 namespace 中載入剛剛的 XDP 程式。比如說如果載入 `xdp_drop` 這個 section: ```shell $ sudo ip -n veth-basic02 link set dev veth0 xdpgeneric obj xdp_prog_kern.o sec xdp_drop ``` 這時候,如果在 Root namespace 中: ```shell $ ip a ``` 會發現 `veth-basic02` 這個 interface: ```clike 13: veth-basic02@if14: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000 link/ether ce:4c:c0:33:ba:d6 brd ff:ff:ff:ff:ff:ff link-netns veth-basic02 inet6 fc00:dead:cafe:1::1/64 scope global valid_lft forever preferred_lft forever inet6 fe80::cc4c:c0ff:fe33:bad6/64 scope link valid_lft forever preferred_lft forever ``` 而如果是在 `veth-basic02` 裡面: ```shell $ sudo ip -n veth-basic02 a ``` 則會看到以下的: ```clike 14: veth0@if13: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 xdpgeneric/id:24 qdisc noqueue state UP group default qlen 1000 link/ether 2a:8e:bc:e7:dd:ad brd ff:ff:ff:ff:ff:ff link-netnsid 0 inet6 fc00:dead:cafe:1::2/64 scope global valid_lft forever preferred_lft forever inet6 fe80::288e:bcff:fee7:ddad/64 scope link valid_lft forever preferred_lft forever ``` 如果之後要在 `veth-basic02` 這個 network namespace 中執行命令,可以使用: ```shell $ ip netns exec veth-basic02 <COMMAND> ``` 比如說如果要從 `veth-basic02` 這個 namespace `ping` 到 Root namespace,那麼就: ```shell $ sudo ip netns exec veth-basic02 ping fc00:dead:cafe:1::1 ``` ### 其他工具 Hook 住 Tracepoint --- 以 ftrace 為例 另外,原先的文章中提到使用 `XDP_ABORTED` 會觸發 `xdp:xdp_exception` 這個 tracepoint,然後有使用 `perf` 去 hook 這個 tracepoint。事實上也可以用 `trace-cmd` (ftrace) 去 hook。比如說在背景 `ping` 之後,使用以下命令: ```shell $ sudo trace-cmd record -e "xdp:xdp_exception" ``` 一段時間後終止它,再生成報告: ```shell $ trace-cmd report -l ``` 就會看到被觸發的事件: ```clike <idle>-0 [002] 5034.925574: xdp_exception: prog_id=36 action=ABORTED ifindex=14 ksoftirqd/1-21 [001] 5035.021677: xdp_exception: prog_id=36 action=ABORTED ifindex=14 ksoftirqd/2-27 [002] 5035.949502: xdp_exception: prog_id=36 action=ABORTED ifindex=14 ksoftirqd/1-21 [001] 5036.045543: xdp_exception: prog_id=36 action=ABORTED ifindex=14 ping-3522 [002] 5037.069633: xdp_exception: prog_id=36 action=ABORTED ifindex=14 ``` 順帶一提,如果使用 `-T` 選項: ```shell $ sudo trace-cmd record -e "xdp:xdp_exception" -T ``` 那麼在 `trace-cmd report` 的時候,可以產生 stack trace。比如說: ``` ksoftirq-21 1..s1 5185.541837: xdp_exception: prog_id=36 action=ABORTED ifindex=14 ksoftirq-21 1..s1 5185.541845: kernel_stack: <stack trace > => __traceiter_xdp_exception (ffffb8170f80cd08) => netif_receive_generic_xdp (ffffb817100d9ea0) => do_xdp_generic.part.0 (ffffb817100e2204) => __netif_receive_skb_core.constprop.0 (ffffb817100e245c) => __netif_receive_skb_one_core (ffffb817100e4e84) => __netif_receive_skb (ffffb817100e4f40) => process_backlog (ffffb817100e5280) => __napi_poll.constprop.0 (ffffb817100e4490) => net_rx_action (ffffb817100e4964) => __do_softirq (ffffb8170f6104f0) => run_ksoftirqd (ffffb8170f698e3c) => smpboot_thread_fn (ffffb8170f6c794c) => kthread (ffffb8170f6bfb6c) => ret_from_fork (ffffb8170f616604) ``` 或是像這個: ``` ping-3522 3..s2 5181.446105: xdp_exception: prog_id=36 action=ABORTED ifindex=14 ping-3522 3..s2 5181.446111: kernel_stack: <stack trace > => __traceiter_xdp_exception (ffffb8170f80cd08) => netif_receive_generic_xdp (ffffb817100d9ea0) => do_xdp_generic.part.0 (ffffb817100e2204) => __netif_receive_skb_core.constprop.0 (ffffb817100e245c) => __netif_receive_skb_one_core (ffffb817100e4e84) => __netif_receive_skb (ffffb817100e4f40) => process_backlog (ffffb817100e5280) => __napi_poll.constprop.0 (ffffb817100e4490) => net_rx_action (ffffb817100e4964) => __do_softirq (ffffb8170f6104f0) => do_softirq (ffffb8170f698c90) => __local_bh_enable_ip (ffffb8170f698db0) => ip6_finish_output2 (ffffb81710247994) => __ip6_finish_output (ffffb81710249bf4) => ip6_finish_output (ffffb81710249d60) => ip6_output (ffffb81710249e80) => ip6_local_out (ffffb817102a8974) => ip6_send_skb (ffffb8171024a724) => ip6_push_pending_frames (ffffb8171024a874) => rawv6_push_pending_frames (ffffb81710279a00) => rawv6_sendmsg (ffffb8171027ba84) => inet_sendmsg (ffffb817101ee430) => sock_sendmsg (ffffb817100ae140) => __sys_sendto (ffffb817100b1be4) => __arm64_sys_sendto (ffffb817100b1c44) => invoke_syscall (ffffb8170f62a8f0) => el0_svc_common.constprop.0 (ffffb8170f62aa2c) => do_el0_svc (ffffb8170f62ab94) => el0_svc (ffffb81710300dbc) => el0_sync_handler (ffffb81710301664) => el0_sync (ffffb8170f61269c) ``` 上面這兩個狀況都是在 softirq 中觸發到注入的 XDP 程式:前者是給 `ksoftirqd` 做,後者則是在重新啟動 softirq 時被執行。但不管是哪一個,都可以發現發生在處理封包的 softirq 中觸發了這個 XDP 程式。 另外,這個 XDP 程式被執行到的時候,已經是 `sk_buff` 處理到一半的時候。這跟上面某些演講提到「若驅動程式中有支援 XDP,則可以在建立 `sk_buuff` 之前就過濾封包」似乎不同。這個理由是:前面的例子中使用的是 *generic XDP*,也就是剛剛使用 `ip` 命令載入時使用的 `xdpgeneric` 選項,它觸發的時機是出現在 `sk_buff` 建立之後,所以會發現過濾並非在驅動程式中發生,而是在 network stack 中。[`ip-link(8)`](https://www.man7.org/linux/man-pages/man8/ip-link.8.html) 的文件中的 **ip link set - change device attributes** 中則有提到: > **xdp object | pinned | off** > *...If the driver does not have native XDP support, the kernel will fall back to a slower, driver-independent "generic" XDP variant. The ip link output will in that case indicate xdpgeneric instead of xdp only.* 除此之外,可以參考 LWN 中的 [*Generic XDP*](https://lwn.net/Articles/721019/) 的說明。 ### Generic XDP vs. Driver XDP 如果希望在驅動程式中就能使用 XDP,則需要該驅動程式有支援,並且在載入時使用 `xdpdrv` 選項。在有些上面的演講裡面,這會叫作 driver XDP 或 native XDP。而 `veth` 對此也有支援。想做到這件事,首先把原來的 XDP 程式卸載: ``` $ sudo ip -n veth-basic02 link set dev veth0 xdpgeneric off ``` 並且將改為: ``` $ sudo ip -n veth-basic02 link set dev veth0 xdpdrv obj xdp_prog_kern.o sec xdp_aborted ``` 然後重複剛剛的 `ping` 與 `trace-cmd`。可以用 `tmux` 開多個分割,或是把 hook 住 tracepoint 的程式先用 `&` 放到背景執行,`ping` 完之後再把它用 `fg` 恢復到前景並終止: ```shell $ sudo trace-cmd record -e "xdp:xdp_exception" -T & $ sudo ip netns exec veth-basic02 ping fc00:dead:cafe:1::1 PING fc00:dead:cafe:1::1(fc00:dead:cafe:1::1) 56 data bytes ^C --- fc00:dead:cafe:1::1 ping statistics --- 12 packets transmitted, 0 received, 100% packet loss, time 11244ms $ fg $ sudo trace-cmd record -e "xdp:xdp_exception" -T ^C ``` 結束後產生報告 (使用 `trace-cmd report -l`) 會發現:這個 XDP 程式被觸發的地方並不是在處理 `sk_buff` 的 `__netif_receive_skb_core` 中,而是在那之前,在 `__napi_poll` 裡面就觸發了: ``` ksoftirq-21 1..s. 894.522772: xdp_exception: prog_id=102 action=ABORTED ifindex=5 ksoftirq-21 1..s. 894.522779: kernel_stack: <stack trace > => __traceiter_xdp_exception (ffffbc6c96c0cd08) => veth_xdp_rcv_skb (ffffbc6c79953610) => veth_xdp_rcv (ffffbc6c79953cc4) => veth_poll (ffffbc6c79953e70) => __napi_poll.constprop.0 (ffffbc6c974e4490) => net_rx_action (ffffbc6c974e4964) => __do_softirq (ffffbc6c96a104f0) => run_ksoftirqd (ffffbc6c96a98e3c) => smpboot_thread_fn (ffffbc6c96ac794c) => kthread (ffffbc6c96abfb6c) => ret_from_fork (ffffbc6c96a16604) ``` 另外一部分對應的 stack trace 如下,可以觀察到相同的結論。只是為了完整而附上。 ``` ping-2820 1..s1 893.518789: xdp_exception: prog_id=102 action=ABORTED ifindex=5 ping-2820 1..s1 893.518795: kernel_stack: <stack trace > => __traceiter_xdp_exception (ffffbc6c96c0cd08) => veth_xdp_rcv_skb (ffffbc6c79953610) => veth_xdp_rcv (ffffbc6c79953cc4) => veth_poll (ffffbc6c79953e70) => __napi_poll.constprop.0 (ffffbc6c974e4490) => net_rx_action (ffffbc6c974e4964) => __do_softirq (ffffbc6c96a104f0) => do_softirq (ffffbc6c96a98c90) => __local_bh_enable_ip (ffffbc6c96a98db0) => ip6_finish_output2 (ffffbc6c97647994) => __ip6_finish_output (ffffbc6c97649bf4) => ip6_finish_output (ffffbc6c97649d60) => ip6_output (ffffbc6c97649e80) => ip6_local_out (ffffbc6c976a8974) => ip6_send_skb (ffffbc6c9764a724) => ip6_push_pending_frames (ffffbc6c9764a874) => rawv6_push_pending_frames (ffffbc6c97679a00) => rawv6_sendmsg (ffffbc6c9767ba84) => inet_sendmsg (ffffbc6c975ee430) => sock_sendmsg (ffffbc6c974ae140) => __sys_sendto (ffffbc6c974b1be4) => __arm64_sys_sendto (ffffbc6c974b1c44) => invoke_syscall (ffffbc6c96a2a8f0) => el0_svc_common.constprop.0 (ffffbc6c96a2aa2c) => do_el0_svc (ffffbc6c96a2ab94) => el0_svc (ffffbc6c97700dbc) => el0_sync_handler (ffffbc6c97701664) => el0_sync (ffffbc6c96a1269c) ```