# 計算機網路 - XDP
[TOC]

([圖片來源](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)
```