---
# System prepended metadata

title: 計算機網路 - XDP

---

# 計算機網路 - 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)
```