# Linux 核心專題: 以 XDP 打造防火牆
> 執行人: jhin1228, D4nnyLee
> [專題講解影片](https://youtu.be/EWLRxGDtyPQ) (這是私人影片看不了)
:::success
:question: 提問清單
* ?
:::
## 任務簡述
研究 [XDP Firewall](https://github.com/gamemann/XDP-Firewall),解釋其運作原理,在其 GitHub Issue 找出可改進或提交內部實作的錯誤。
> Reference Resource: [A Beginners Guide to eBPF Programming for Networking](https://www.youtube.com/watch?v=l5l2EckwWME): 透過 eBPF hook kernel space的網路模組 (如 TCP/IP stack),並寫成條件判斷,當封包進到 NIC 時直接在核心被分類並動作。
## TODO: 解析 [XDP Firewall](https://github.com/gamemann/XDP-Firewall) 運作原理
### eXpress Data Path (XDP)
eXpress Data Path (XDP) 是 Extended Berkeley Packet Filter (eBPF) 的其中一種 BPF program type。不同的 program type 代表不同的 hook point 和 helper function,同時 ebpf program 的輸入和輸出格式也不同。
以下列舉 `bpf_prog_type` 種類 :
```c
/* /usr/src/linux-headers-5.4.0-148/include/uapi/linux/bpf.h */
enum bpf_prog_type {
BPF_PROG_TYPE_UNSPEC,
BPF_PROG_TYPE_SOCKET_FILTER,
BPF_PROG_TYPE_KPROBE,
BPF_PROG_TYPE_SCHED_CLS,
BPF_PROG_TYPE_SCHED_ACT,
BPF_PROG_TYPE_TRACEPOINT,
BPF_PROG_TYPE_XDP, // XDP
BPF_PROG_TYPE_PERF_EVENT,
BPF_PROG_TYPE_CGROUP_SKB,
BPF_PROG_TYPE_CGROUP_SOCK,
BPF_PROG_TYPE_LWT_IN,
BPF_PROG_TYPE_LWT_OUT,
BPF_PROG_TYPE_LWT_XMIT,
BPF_PROG_TYPE_SOCK_OPS,
BPF_PROG_TYPE_SK_SKB,
BPF_PROG_TYPE_CGROUP_DEVICE,
BPF_PROG_TYPE_SK_MSG,
BPF_PROG_TYPE_RAW_TRACEPOINT,
BPF_PROG_TYPE_CGROUP_SOCK_ADDR,
BPF_PROG_TYPE_LWT_SEG6LOCAL,
BPF_PROG_TYPE_LIRC_MODE2,
BPF_PROG_TYPE_SK_REUSEPORT,
BPF_PROG_TYPE_FLOW_DISSECTOR,
BPF_PROG_TYPE_CGROUP_SYSCTL,
BPF_PROG_TYPE_RAW_TRACEPOINT_WRITABLE,
BPF_PROG_TYPE_CGROUP_SOCKOPT,
};
```
XDP 可以提早處理從網路裝置進來的封包,根據 hook point 分為以下三種 :
![](https://hackmd.io/_uploads/ryDyvDRD3.png)
> [圖片出處](https://speakerdeck.com/johnlin/ebpf-based-container-networking?slide=34)
其中 Native/Offload XDP 需要[網路裝置](https://github.com/iovisor/bcc/blob/master/docs/kernel-versions.md#xdp)本身支援。
### eBPF Architecture
![](https://hackmd.io/_uploads/H1aR_wRw2.png)
上圖描述整體 eBPF 架構,可分成 BPF program 撰寫及編譯、BPF bytecode 載入到核心並完成 hook point 設定、藉 BPF maps 在 kernel space 和 user space 間傳遞資料三大部分。
### 載入 BPF 程式到 Linux 核心
![](https://hackmd.io/_uploads/HyeOIEXun.png)
以 [XDP Firewall](https://github.com/gamemann/XDP-Firewall) 為例,會將封包過濾規則寫在 BPF program 後,以 Clang/LLVM 編譯成 BPF bytecode (BPF instruction) 並交由 Loader (BCC、libbpf...) 透過系統呼叫把 BPF ELF object file 載入到核心內。
進到核心後,會先建立 `strcut bpf_prog`,這個結構體是 BPF bytecode 在核心的代表,之後將 BPF bytecode 從 user space 拷貝至 kernel space 並開始驗證此 bytecode 是否安全,最後分配一個 file descriptor 並傳回給 user space 的 process 作之後的處理。
> * BPF bytecode: 就是一種可以被虛擬機執行的 machine code,之所以稱其為 bytecode 是因為 BPF 指令集的 opcode 都是一個 byte 長度。
> * [BPF instruction (BPF bytecode)](https://docs.kernel.org/bpf/instruction-set.html#id1): BPF instruction 採用虛擬指令集,類似 assembly 在處理的指令集。
> * BPF 虛擬機: 可理解成直譯器 (Interpreter),架構圖如下。
> ![](https://hackmd.io/_uploads/HklZu1Nuh.png)
在 [XDP Firewall](https://github.com/gamemann/XDP-Firewall) 中以 `xdpfw.c` 作為 loader,透過以下流程將 BPF instruction 載入到核心:
![](https://hackmd.io/_uploads/BJRvK_Awh.png)
```c
// Open phase
int loadbpfobj()
--int bpf_prog_load_xattr() // libbpf.c
--static struct bpf_object *__bpf_object__open_xattr()
--static struct bpf_object *__bpf_object__open()
// Load phase
int loadbpfobj()
--int bpf_prog_load_xattr() // libbpf.c
--int bpf_object__load_xattr()
--static int bpf_object__load_progs()
--int bpf_program__load()
--static int load_program()
--int bpf_load_program_xattr() // bpf.c
--static inline int sys_bpf_prog_load()
--static inline int sys_bpf()
--SYSCALL_DEFINE3() // syscall.c
--static int bpf_prog_load()
```
> 關於 bpf skeleton 和 bpf app lifecycle 可參考這篇[文章](https://nakryiko.com/posts/bcc-to-libbpf-howto-guide/#bpf-skeleton-and-bpf-app-lifecycle)
### XDP and Hook
完成 BPF program 載入核心動作後,接著關心 BPF machine code 的 hook point 設定及存取到此 hook point 時的後續行為,這裡以 XDP 這個 BPF program type 接續探討。
[XDP Firewall](https://github.com/gamemann/XDP-Firewall) 中當 `xdpfw.c` 將 `xdpfw_kern.c` 載入完成後便進入 attachment 階段,流程如下 :
```c
// Attach phase
int attachxdp()
--int bpf_set_link_xdp_fd() // netlink.c
--static int __bpf_set_link_xdp_fd_replace()
nla->nla_type = NLA_F_NESTED | IFLA_XDP;
nla_xdp->nla_type = IFLA_XDP_FD;
--static int do_setlink() // /net/core/rtnetlink.c
--int dev_change_xdp_fd() // /net/core/dev.c
--static int dev_xdp_install()
```
在函式 `do_setlink()` 會根據先前設定好的 XDP flag 呼叫函示 `dev_change_xdp_fd()`
```c
static int do_setlink(const struct sk_buff *skb,
struct net_device *dev, struct ifinfomsg *ifm,
struct netlink_ext_ack *extack,
struct nlattr **tb, char *ifname, int status)
{
...
if (tb[IFLA_XDP]) {
...
if (xdp[IFLA_XDP_FD]) {
err = dev_change_xdp_fd(dev, extack,
nla_get_s32(xdp[IFLA_XDP_FD]),
xdp_flags);
...
}
}
...
}
```
`dev_change_xdp_fd()` 是將 XDP program fd (`struct bpf_prog`) 和指定的 NIC interface 作關聯。
在本次實驗 `dev->netdev_ops` 是 `ixgbe_netdev_ops`,`bpf_op` 則是 `ixgbe_xdp`。
> 從這裡可知 XDP program 是透過 [netlink](https://en.wikipedia.org/wiki/Netlink) 中的 [NETLINK_ROUTE](https://man7.org/linux/man-pages/man7/rtnetlink.7.html) 相關功能將其 hook 到指定的 interface 上。
```c
static const struct net_device_ops ixgbe_netdev_ops = {
...
.ndo_bpf = ixgbe_xdp,
.ndo_xdp_xmit = ixgbe_xdp_xmit,
...
};
int dev_change_xdp_fd(struct net_device *dev, struct netlink_ext_ack *extack,
int fd, u32 flags)
{
const struct net_device_ops *ops = dev->netdev_ops;
struct bpf_prog *prog = NULL;
bpf_op_t bpf_op, bpf_chk;
...
bpf_op = bpf_chk = ops->ndo_bpf;
...
if (fd >= 0) {
...
prog = bpf_prog_get_type_dev(fd, BPF_PROG_TYPE_XDP,
bpf_op == ops->ndo_bpf);
...
} else {
...
}
err = dev_xdp_install(dev, bpf_op, extack, flags, prog);
...
}
```
在 `dev_xdp_install` 就是針對各家網路裝置的驅動程式進行 XDP program 安裝,並根據 XDP 預計 hook 到的地方執行進一步的設定。
> 本實驗由於 hook 在 driver,所以 `xdp.command = XDP_SETUP_PROG;`
```c
static int dev_xdp_install(struct net_device *dev, bpf_op_t bpf_op,
struct netlink_ext_ack *extack, u32 flags,
struct bpf_prog *prog)
{
struct netdev_bpf xdp;
memset(&xdp, 0, sizeof(xdp));
if (flags & XDP_FLAGS_HW_MODE)
xdp.command = XDP_SETUP_PROG_HW;
else
xdp.command = XDP_SETUP_PROG;
xdp.extack = extack;
xdp.flags = flags;
xdp.prog = prog;
return bpf_op(dev, &xdp);
}
```
`ixgbe_xdp_setup` 就是將 XDP program (xdp_prog) 記錄到 `rx_ring`。
```c
// /tools/testing/selftests/powerpc/benchmarks/context_switch.c
static unsigned long xchg(unsigned long *p, unsigned long val)
{
return __atomic_exchange_n(p, val, __ATOMIC_SEQ_CST);
}
// /drivers/net/ethernet/intel/ixgbe/ixgbe_main.c
static int ixgbe_xdp_setup(struct net_device *dev, struct bpf_prog *prog)
{
struct ixgbe_adapter *adapter = netdev_priv(dev); // Get network device private data
...
/* verify ixgbe ring attributes are sufficient for XDP */
for (i = 0; i < adapter->num_rx_queues; i++) {
...
}
...
/* If transitioning XDP modes reconfigure rings */
if (need_reset) {
...
} else {
for (i = 0; i < adapter->num_rx_queues; i++)
// 將 XDP program (xdp_prog) 記錄到 rx_ring 上
(void)xchg(&adapter->rx_ring[i]->xdp_prog,
adapter->xdp_prog);
}
...
}
static int ixgbe_xdp(struct net_device *dev, struct netdev_bpf *xdp)
{
struct ixgbe_adapter *adapter = netdev_priv(dev);
switch (xdp->command) {
case XDP_SETUP_PROG:
return ixgbe_xdp_setup(dev, xdp->prog);
case XDP_QUERY_PROG:
xdp->prog_id = adapter->xdp_prog ?
adapter->xdp_prog->aux->id : 0;
return 0;
case XDP_SETUP_XSK_UMEM:
return ixgbe_xsk_umem_setup(adapter, xdp->xsk.umem,
xdp->xsk.queue_id);
default:
return -EINVAL;
}
}
```
> [__atomic_exchange_n](https://gcc.gnu.org/onlinedocs/gcc/_005f_005fatomic-Builtins.html)
設定好 XDP program 的 hook 點後,接著思考何時觸發此 hook 及觸發後的後續工作。
一般網路裝置 (不考慮 SmartNIC) 本身沒有網路處理器,當封包從網路裝置進來時沒有進程處理就會被丟棄,而常見的收包方式有以下兩種 :
* Busy-polling: 預留特定的 CPU core 和 process 給 網路裝置,100% 用於收包,如 [DPDK](https://www.dpdk.org/)。
* IRQ (硬中斷): 當網路裝置收到封包後,透過 IRQ 告知 CPU 需處理到來的封包。然而在高流量的情況下中斷所佔的開銷過大,這也是為何會有 [DPDK](https://www.dpdk.org/) 所採用 polling 機制。
針對 IRQ 在高流量的情形下的改進方式就是 [NAPI](https://en.wikipedia.org/wiki/New_API),它結合了 polling 和 interrupt :
* 當執行至 NAPI 的 `poll()` 時,會從 ring buffer 收取 batched 封包 (每次收取封包的量可以用 `budget` 決定),這段期間會接收所有到來的封包且不會觸發 IRQ。
* 當不在 `poll()` 時,收到封包時會觸發 IRQ,核心會呼叫 `poll()` 收包。
![](https://hackmd.io/_uploads/S1d8NqMun.png)
接著討論 DMA 將封包複製至 Rx ring buffer,產生一個 IRQ 後的流程 (以 `ixgbe` 驅動為例) :
```c
// /include/linux/interrupt.h
enum
{
...
NET_TX_SOFTIRQ,
NET_RX_SOFTIRQ,
...
};
// /drivers/net/ethernet/intel/ixgbe/ixgbe_main.c
static irqreturn_t ixgbe_msix_clean_rings() / static irqreturn_t ixgbe_intr()
--static inline void napi_schedule_irqoff() // /include/linux/netdevice.h
--void __napi_schedule_irqoff() // net/core/dev.c
--static inline void ____napi_schedule()
--__raise_softirq_irqoff(NET_RX_SOFTIRQ) // /kernel/softirq.c
```
`__raise_softirq_irqoff()` 會觸發 `NR_SOFTIRQS` 類型 soft-IRQ,最後會執行 `net_rx_action()`。
> [ksoftirqd](https://man.cx/ksoftirqd(9)?ref=bytelab.codes) 會執行 `net_rx_action()`
```c
// /net/core/dev.c
static __latent_entropy void net_rx_action()
--budget -= napi_poll(n, &repoll); // In this case, call ixgbe_poll()
--int ixgbe_poll() // /drivers/net/ethernet/intel/ixgbe/ixgbe_main.c
--static int ixgbe_clean_rx_irq()
--static struct sk_buff *ixgbe_run_xdp()
```
可觀察到當 XDP 的 hook 點在 driver 時,會進到以下程式碼的第 14 行,此時 `skb` 只是大小為 8 bytes 的指標,尚未將封包內容拷貝給它。
```c=
static int ixgbe_clean_rx_irq(struct ixgbe_q_vector *q_vector,
struct ixgbe_ring *rx_ring,
const int budget)
{
...
while (likely(total_rx_packets < budget)) {
...
struct sk_buff *skb;
...
/* retrieve a buffer from the ring */
if (!skb) {
...
skb = ixgbe_run_xdp(adapter, rx_ring, &xdp);
}
if (IS_ERR(skb)) {
unsigned int xdp_res = -PTR_ERR(skb);
if (xdp_res & (IXGBE_XDP_TX | IXGBE_XDP_REDIR)) {
xdp_xmit |= xdp_res;
ixgbe_rx_buffer_flip(rx_ring, rx_buffer, size);
} else {
rx_buffer->pagecnt_bias++;
}
total_rx_packets++;
total_rx_bytes += size;
} else if (skb) {
ixgbe_add_rx_frag(rx_ring, rx_buffer, skb, size);
} else if (ring_uses_build_skb(rx_ring)) {
skb = ixgbe_build_skb(rx_ring, rx_buffer,
&xdp, rx_desc);
} else {
skb = ixgbe_construct_skb(rx_ring, rx_buffer,
&xdp, rx_desc);
}
...
}
...
}
```
在 `ixgbe_run_xdp()` 中,根據 `bpf_prog_run_xdp()` 取得的 XDP action 決定封包該如何處理。
`XDP_PASS` : 正常處理封包,即封包交給 kernel networking stack 處理。
`XDP_TX` : 封包從原 interface 出去,適用於 proxy、load balance。
`XDP_REDIRECT` : 封包從其他 interface 出去、封包交由其他 CPU 處理、透過 `AF_XDP` 直接將封包導向 userspace 上的 process 處理。
`XDP_ABORTED` : 類似 `XDP_DROP`,只是 ebpf program 會在 tracepoint 上提供錯誤訊息的 log。
`XDP_DROP` : 在 XDP hook 階段將滿足過濾規則的封包丟棄,適用於 DDoS mitigation。
```c
#define IXGBE_XDP_PASS 0
#define IXGBE_XDP_CONSUMED BIT(0)
#define IXGBE_XDP_TX BIT(1)
#define IXGBE_XDP_REDIR BIT(2)
static struct sk_buff *ixgbe_run_xdp(struct ixgbe_adapter *adapter,
struct ixgbe_ring *rx_ring,
struct xdp_buff *xdp)
{
int err, result = IXGBE_XDP_PASS;
struct bpf_prog *xdp_prog;
struct xdp_frame *xdpf;
u32 act;
rcu_read_lock();
xdp_prog = READ_ONCE(rx_ring->xdp_prog);
if (!xdp_prog)
goto xdp_out;
prefetchw(xdp->data_hard_start); /* xdp_frame write */
act = bpf_prog_run_xdp(xdp_prog, xdp); // XDP hook point!
switch (act) {
case XDP_PASS:
break;
case XDP_TX:
xdpf = convert_to_xdp_frame(xdp);
if (unlikely(!xdpf)) {
result = IXGBE_XDP_CONSUMED;
break;
}
result = ixgbe_xmit_xdp_ring(adapter, xdpf);
break;
case XDP_REDIRECT:
err = xdp_do_redirect(adapter->netdev, xdp, xdp_prog);
if (!err)
result = IXGBE_XDP_REDIR;
else
result = IXGBE_XDP_CONSUMED;
break;
default:
bpf_warn_invalid_xdp_action(act);
/* fallthrough */
case XDP_ABORTED:
trace_xdp_exception(rx_ring->netdev, xdp_prog, act);
/* fallthrough -- handle aborts by dropping packet */
case XDP_DROP:
result = IXGBE_XDP_CONSUMED;
break;
}
xdp_out:
rcu_read_unlock();
return ERR_PTR(-result);
}
```
> `bpf_prog_run_xdp()` 正是觸發 XDP hook 後要執行的動作
### ebpf maps
![](https://hackmd.io/_uploads/Syyh9VQ_3.png)
以下是 loader 創建完 ebpf map 並回傳其 fd 給 userspace process 的過程 :
```c
// Create ebpf map and get map fd
int loadbpfobj()
--int bpf_prog_load_xattr() // libbpf.c
--static int bpf_object__create_maps() // Get map fd
--static int bpf_object__create_map()
--int bpf_create_map_xattr() // bpf.c
--sys_bpf(BPF_MAP_CREATE, &attr, sizeof(attr));
--static int map_create() // /kernel/bpf/syscall.c
// BPF program relocation
--static int bpf_object__relocate()
--static int bpf_program__relocate()
...
// BPF program load
--static int bpf_object__load_progs()
...
```
在 `map_create()` 中,首先會透過 `find_and_alloc_map()` 來為這個 map 分配空間,並且不同型態的 map 會使用不同的方法來分配,而最後統一都回傳 `struct bpf_map *`,不同型態的 map 都可以用此結構來表示。
為不同型態的 map 配置好空間之後,接下來就可以透過 `bpf_map_new_fd()` 來將 `struct bpf_map *` 對應到一個 file descriptor。
有一個事先定義好的陣列 `struct bpf_map_ops *bpf_map_types`,其中每個 `struct bpf_map_ops` 都對應到一種 map 型態的各種操作(例如分配空間、查找元素等),而 `find_and_alloc_map()` 就是透過 `bpf_map_types[type]` 來快速得到不同型態對應到的操作,而不是執行一連串的 `if-else` 來判斷。
```c
static int map_create(union bpf_attr *attr)
{
...
struct bpf_map *map;
int f_flags;
int err;
...
/* find map type and init map: hashtable vs rbtree vs bloom vs ... */
map = find_and_alloc_map(attr);
if (IS_ERR(map))
return PTR_ERR(map);
err = bpf_obj_name_cpy(map->name, attr->map_name);
if (err)
goto free_map;
...
err = bpf_map_new_fd(map, f_flags);
if (err < 0) {
/* failed to allocate fd.
* bpf_map_put_with_uref() is needed because the above
* bpf_map_alloc_id() has published the map
* to the userspace and the userspace may
* have refcnt-ed it through BPF_MAP_GET_FD_BY_ID.
*/
bpf_map_put_with_uref(map);
return err;
}
return err;
... // handle error and free space
}
```
`bpf_map_new_fd()` 做的事情其實很簡單,就只是在檔案系統上創建一個匿名檔案並且將前面分配好的 `map` 放到該檔案的 `private_data` 欄位,之後只要再將一個未使用的 fd 與此檔案關聯起來,我們就可以透過此 fd 存取到 `map`。
> 函式開頭的 anon 代表的意思是 anonymous,也就是匿名的,而程式碼中的 `"bpf-map"` 則是此檔案的類別,而不是檔案名稱。
>
> `private_data` 是一個可以讓開發者放自訂義結構的欄位。
```c
int bpf_map_new_fd(struct bpf_map *map, int flags)
{
...
return anon_inode_getfd("bpf-map", &bpf_map_fops, map,
flags | O_CLOEXEC);
}
int anon_inode_getfd(const char *name, const struct file_operations *fops,
void *priv, int flags)
{
int error, fd;
struct file *file;
error = get_unused_fd_flags(flags);
if (error < 0)
return error;
fd = error;
file = anon_inode_getfile(name, fops, priv, flags);
if (IS_ERR(file)) {
error = PTR_ERR(file);
goto err_put_unused_fd;
}
fd_install(fd, file);
return fd;
error:
... // handle error
}
```
要透過 fd 拿到對應的 map 的話,我們可以從 `map_lookup_elem()` 中看到其過程。
首先會透過 `fdget()` 從檔案系統中取得 fd 對應的檔案,之後 `__bpf_map_get()` 就會讀取檔案的 `private_data` 欄位,來得到前面 `map_create()` 中配置的 `struct bpf_map`,也就是 fd 對應的 map。
之後函式就會根據不同的 map 型別來查找 key 對應的 value,並且將其結果複製到 `value`(一個用 `kmalloc()` 得到的 buffer),最後再利用 `copy_to_user()` 把結果複製到 userspace 的指標(也就是 `uvalue`)。
> 以下變數中前綴的 `u` 都是指 userspace。
>
> 程式碼中的 `map->ops` 其實就是前面提到過的利用 `bpf_map_types[type]` 得到的每個型別對應的操作。
```c
// Lookup value from the map represented by fd
int bpf_map_lookup_elem()
--sys_bpf(BPF_MAP_LOOKUP_ELEM, &attr, sizeof(attr));
--static int map_lookup_elem(); // /kernel/bpf/syscall.c
```
```c
static int map_lookup_elem(union bpf_attr *attr)
{
void __user *ukey = u64_to_user_ptr(attr->key);
void __user *uvalue = u64_to_user_ptr(attr->value);
int ufd = attr->map_fd;
struct bpf_map *map;
void *key, *value, *ptr;
u32 value_size;
struct fd f;
int err;
...
f = fdget(ufd);
map = __bpf_map_get(f);
if (IS_ERR(map))
return PTR_ERR(map);
if (!(map_get_sys_perms(map, f) & FMODE_CAN_READ)) {
err = -EPERM;
goto err_put;
}
...
key = __bpf_copy_key(ukey, map->key_size); // Copy key from userspace
if (IS_ERR(key)) {
err = PTR_ERR(key);
goto err_put;
}
...
err = -ENOMEM;
value = kmalloc(value_size, GFP_USER | __GFP_NOWARN);
if (!value)
goto free_key;
...
preempt_disable();
this_cpu_inc(bpf_prog_active);
if (map->map_type == BPF_MAP_TYPE_PERCPU_HASH ||
map->map_type == BPF_MAP_TYPE_LRU_PERCPU_HASH) {
err = bpf_percpu_hash_copy(map, key, value);
} else if (map->map_type == BPF_MAP_TYPE_PERCPU_ARRAY) {
err = bpf_percpu_array_copy(map, key, value);
} ...
else {
rcu_read_lock();
if (map->ops->map_lookup_elem_sys_only)
ptr = map->ops->map_lookup_elem_sys_only(map, key);
else
ptr = map->ops->map_lookup_elem(map, key);
if (IS_ERR(ptr)) {
err = PTR_ERR(ptr);
}
...
rcu_read_unlock();
}
this_cpu_dec(bpf_prog_active);
preempt_enable();
done:
if (err)
goto free_value;
err = -EFAULT;
if (copy_to_user(uvalue, value, value_size) != 0)
goto free_value;
err = 0;
free_value:
kfree(value);
free_key:
kfree(key);
err_put:
fdput(f);
return err;
}
```
### `xdpfw_kern.c`
這個檔案定義要被載入核心執行的程式以及用來與 userspace 程式互相傳遞資料的 map,而為了方便與其他函式與變數做區分,XDP 程式與 map 都會用 `SEC()` 來表示他們要存放在不同的 section,如此一來透過解析 ELF 中的 section table 就可以快速找到要載入的程式以及 map。
#### `xdp_prog_main()`
`ctx` 這個參數讓我們有辦法存取到封包內容,封包的開頭與結尾分別是 `ctx->data` 和 `ctx->data_end`。
此函式做的事情主要可以分為四個部分,檢查完整性、確認黑名單、紀錄 pps/bps 以及過濾封包。
* 檢查完整性
函式一開始會先檢查封包是否完整,從下面的程式可以看到程式還會順便確認封包的 protocol(目前只支援 TCP、UDP、ICMP)。
```c!
int xdp_prog_main(struct xdp_md *ctx)
{
// Initialize data.
void *data_end = (void *)(long)ctx->data_end;
void *data = (void *)(long)ctx->data;
// Scan ethernet header.
struct ethhdr *eth = data;
...
// Initialize IP headers.
struct iphdr *iph = NULL;
struct ipv6hdr *iph6 = NULL;
__u128 srcip6 = 0;
// Set IPv4 and IPv6 common variables.
if (eth->h_proto == htons(ETH_P_IPV6))
{
iph6 = (data + sizeof(struct ethhdr));
...
}
else
{
iph = (data + sizeof(struct ethhdr));
...
}
// Check IP header protocols.
if ((iph6 && iph6->nexthdr != IPPROTO_UDP && iph6->nexthdr != IPPROTO_TCP && iph6->nexthdr != IPPROTO_ICMP) && (iph && iph->protocol != IPPROTO_UDP && iph->protocol != IPPROTO_TCP && iph->protocol != IPPROTO_ICMP))
{
return XDP_PASS;
}
```
* 確認黑名單
若是有封包在後面過濾封包的階段被判斷成惡意封包(回傳 `XDP_DROP`),並且過濾規則有設定 `blocktime` 時,接下來的 `blocktime` 秒內如果又收到同樣來源 IP 的封包的話就會在此階段直接丟棄封包。
時間紀錄的方式是將預計解除封鎖的時間點紀錄在 `ip_blacklist_map` 或 `ip6_blacklist_map` 中(依據 IP 為 IPv4 還是 IPv6 來決定),而每次這個階段則是檢查是否已經超過 IP 對應的時間點來決定是否繼續封鎖。
```c
__u64 now = bpf_ktime_get_ns();
// Check blacklist map.
__u64 *blocked = NULL;
if (iph6)
{
blocked = bpf_map_lookup_elem(&ip6_blacklist_map, &srcip6);
}
else if (iph)
{
blocked = bpf_map_lookup_elem(&ip_blacklist_map, &iph->saddr);
}
if (blocked != NULL && *blocked > 0)
{
if (now > *blocked)
{
// Remove element from map.
if (iph6)
{
bpf_map_delete_elem(&ip6_blacklist_map, &srcip6);
}
else if (iph)
{
bpf_map_delete_elem(&ip_blacklist_map, &iph->saddr);
}
}
else
{
...
// They're still blocked. Drop the packet.
return XDP_DROP;
}
}
...
matched:
if (action == 0)
{
// Before dropping, update the blacklist map.
if (blocktime > 0)
{
__u64 newTime = now + (blocktime * 1000000000);
if (iph6)
{
bpf_map_update_elem(&ip6_blacklist_map, &srcip6, &newTime, BPF_ANY);
}
else if (iph)
{
bpf_map_update_elem(&ip_blacklist_map, &iph->saddr, &newTime, BPF_ANY);
}
}
...
return XDP_DROP;
}
```
> `*blocked` 和 `newTime` 代表的都是解除封鎖的時間,單位為奈秒
* 紀錄 pps/bps
透過 `ip_stats_map` 和 `ip6_stats_map` 紀錄每個 IP 對應的 pps(packets per second)與 bps(bytes per second),計算的方法為每秒鐘都會將 pps/bps 歸零,並且紀錄接下來的一秒鐘處理的封包與 byte 數量。
```c
// Update IP stats (PPS/BPS).
__u64 pps = 0;
__u64 bps = 0;
struct ip_stats *ip_stats = NULL;
if (iph6)
{
ip_stats = bpf_map_lookup_elem(&ip6_stats_map, &srcip6);
}
else if (iph)
{
ip_stats = bpf_map_lookup_elem(&ip_stats_map, &iph->saddr);
}
if (ip_stats)
{
// Check for reset.
if ((now - ip_stats->tracking) > 1000000000)
{
ip_stats->pps = 0;
ip_stats->bps = 0;
ip_stats->tracking = now;
}
// Increment PPS and BPS using built-in functions.
__sync_fetch_and_add(&ip_stats->pps, 1);
__sync_fetch_and_add(&ip_stats->bps, ctx->data_end - ctx->data);
pps = ip_stats->pps;
bps = ip_stats->bps;
}
else
{
// Create new entry.
struct ip_stats new;
new.pps = 1;
new.bps = ctx->data_end - ctx->data;
new.tracking = now;
pps = new.pps;
bps = new.bps;
if (iph6)
{
bpf_map_update_elem(&ip6_stats_map, &srcip6, &new, BPF_ANY);
}
else if (iph)
{
bpf_map_update_elem(&ip_stats_map, &iph->saddr, &new, BPF_ANY);
}
}
```
注意到更新記錄在 map 的值的時候有使用到 `__sync_fetch_and_add()`,這是因為 `ip_stats` 為所有 BPF 程式共享的 map,並且可能會有多個 CPU 都在執行 `xdp_prog_main()`,因此需要使用 atomic operation 來避免可能的 race condition。
* 過濾封包
最後這個階段其實就只是根據過濾規則一一比對封包的內容,若是符合的話就會透過 `goto matched` 直接做後續的步驟,而不是繼續比對下一個規則。
```c!
for (__u8 i = 0; i < MAX_FILTERS; i++)
{
__u32 key = i;
struct filter *filter = bpf_map_lookup_elem(&filters_map, &key);
// Check if ID is above 0 (if 0, it's an invalid rule).
if (!filter || filter->id < 1)
{
break;
}
// Check if the rule is enabled.
if (!filter->enabled)
{
continue;
}
...
// Matched.
#ifdef DEBUG
bpf_printk("Matched rule ID #%d.\n", filter->id);
#endif
action = filter->action;
blocktime = filter->blocktime;
goto matched;
}
return XDP_PASS;
matched:
if (action == 0)
{
#ifdef DEBUG
//bpf_printk("Matched with protocol %d and sAddr %lu.\n", iph->protocol, iph->saddr);
#endif
...
if (stats)
{
stats->dropped++;
}
return XDP_DROP;
}
else
{
if (stats)
{
stats->allowed++;
}
}
return XDP_PASS;
```
而為了要讓 `xdp_prog_main()` 有辦法存取到使用者自訂的規則,loader(`xdpfw.c`)在將程式載入到核心之後,就會呼叫 `updateconfig()` 來解析規則並且存到 `cfg->filters` 這個陣列裡面,接著就會呼叫 `updatefilters()` 來將 `cfg->filters` 裡的所有規則一個一個透過 `bpf_map_update_elem()` 儲存到核心的 map 中。
## TODO: 找出 [XDP Firewall](https://github.com/gamemann/XDP-Firewall) 可改進之處並著手
### 簡化 TCP Flag 比對過程
以下為 `src/xdpfw_kern.c` 中與比對封包 TCP Flag 相關的程式碼:
> 其中 `tcph` 是封包中的 TCP header,而 `filter->tcpopts` 則是過濾規則中與 TCP 相關的部分
```c
// URG flag.
if (filter->tcpopts.do_urg && filter->tcpopts.urg != tcph->urg)
{
continue;
}
// ACK flag.
if (filter->tcpopts.do_ack && filter->tcpopts.ack != tcph->ack)
{
continue;
}
...
// CWR flag.
if (filter->tcpopts.do_cwr && filter->tcpopts.cwr != tcph->cwr)
{
continue;
}
```
可以發現對於每種 flag 都會用一次 `if` 來判斷是否符合過濾規則。
因為實際上每種 flag 都只占用一個位元,透過將整數中的不同位元對應到不同的 flag,並利用 bit-wise 操作,我們可以將比對全部 flag 的過程簡化成只需要一次 `if` 就可比對完成。
在 `<linux/tcp.h>` 裡面有定義好的巨集讓我們可以以 32 位元整數的形式取出 `struct tcphdr` 中所有的 flag,並且也有定義每種 flag 對應的 mask,方便我們讀取特定的 flag。
> `tcp_flag_word()` 以及 `TCP_FLAG_CWR`、`TCP_FLAG_ECE` 等
因此我們可以將 `struct tcpopts` 中的 flag 欄位改成兩個整數,其中 `enabled_flags` 是為了取代 `do_*` 欄位的 mask,而 `expected_flags` 則是取代各個 flag 的值。
```diff
diff --git a/src/xdpfw.h b/src/xdpfw.h
index 4be467b..8290204 100644
--- a/src/xdpfw.h
+++ b/src/xdpfw.h
@@ -41,29 +41,8 @@ struct tcpopts
__u16 dport;
// TCP flags.
- unsigned int do_urg : 1;
- unsigned int urg : 1;
-
- unsigned int do_ack : 1;
- unsigned int ack : 1;
-
- unsigned int do_rst : 1;
- unsigned int rst : 1;
-
- unsigned int do_psh : 1;
- unsigned int psh : 1;
-
- unsigned int do_syn : 1;
- unsigned int syn : 1;
-
- unsigned int do_fin : 1;
- unsigned int fin : 1;
-
- unsigned int do_ece : 1;
- unsigned int ece : 1;
-
- unsigned int do_cwr : 1;
- unsigned int cwr : 1;
+ __u32 enabled_flags;
+ __u32 expected_flags;
};
```
如此一來,我們只需要一次 `if` 敘述就可以比對所有的 TCP flag。
```cpp
if ((tcp_flag_word(tcph) & filter->tcpopts.enabled_flags) !=
filter->tcpopts.expected_flags)
{
continue;
}
```
:::warning
TODO: 提交 pull request 到原專案
:::
## 實驗
### 實驗拓樸
我們使用 `iperf3` 工具來測量防火牆對於封包吞吐量的影響。
實驗中我們將 192.168.1.100 做為 Server,而 192.168.2.200 則是 Client。
Server 會持續監聽 5201 port,而 Client 則透過傳送大量封包來測量吞吐量。
![](https://hackmd.io/_uploads/HyXBNszOh.png)
* Kernel Version: 中間主機的版本為 `5.4.0-152-generic`
* XDP-Firewall: 基於 [@b54c466](https://github.com/gamemann/XDP-Firewall/commit/b54c46638d32306ec27aecc69a830283aef17e61)
> 目前因為 kernel 版本過舊導致最新版的 XDP-Firewall 無法 build,所以先使用較舊版本的 XDP-Firewall。
* libbpf: 基於 [@7fc4d50](https://github.com/libbpf/libbpf/commit/7fc4d50)
規則比對有以下特性:
* 若是有比對到封包的某個數值不符合規則,則會直接跳過並比對下個規則
* 規則中沒有指定的欄位在防火牆中還是會執行一次 `if` 來確認是否要比對
因此為了盡量使得防火牆花費更多的時間比對,實驗中我們都只有比較 ICMP 的選項( ICMP 比對被寫在迴圈的最後)。
我們使用的規則為 90 個以下的 filter
```
{
enabled = true,
action = 1,
icmp_enabled = true,
icmp_type = 18
}
```
### 關閉防火牆
首先我們先測量不開啟防火牆的狀態下的數據。
```
Connecting to host 192.168.2.200, port 5201
[ 4] local 192.168.1.100 port 51468 connected to 192.168.2.200 port 5201
[ ID] Interval Transfer Bandwidth Retr Cwnd
[ 4] 0.00-1.00 sec 1.06 GBytes 1111979 KBytes/sec 43 1.13 MBytes
[ 4] 1.00-2.00 sec 1.10 GBytes 1149260 KBytes/sec 0 1.13 MBytes
[ 4] 2.00-3.00 sec 1.10 GBytes 1149279 KBytes/sec 0 1.13 MBytes
[ 4] 3.00-4.00 sec 1.10 GBytes 1149330 KBytes/sec 0 1.13 MBytes
[ 4] 4.00-5.00 sec 1.10 GBytes 1149215 KBytes/sec 0 1.13 MBytes
[ 4] 5.00-6.00 sec 1.10 GBytes 1149162 KBytes/sec 0 1.13 MBytes
[ 4] 6.00-7.00 sec 1.10 GBytes 1149248 KBytes/sec 0 1.13 MBytes
[ 4] 7.00-8.00 sec 1.10 GBytes 1149219 KBytes/sec 0 1.13 MBytes
[ 4] 8.00-9.00 sec 1.10 GBytes 1149207 KBytes/sec 0 1.13 MBytes
[ 4] 9.00-10.00 sec 1.10 GBytes 1149323 KBytes/sec 0 1.73 MBytes
- - - - - - - - - - - - - - - - - - - - - - - - -
[ ID] Interval Transfer Bandwidth Retr
[ 4] 0.00-10.00 sec 10.9 GBytes 1145522 KBytes/sec 43 sender
[ 4] 0.00-10.00 sec 10.9 GBytes 1145187 KBytes/sec receive
```
### 開啟防火牆 [@b54c466](https://github.com/D4nnyLee/XDP-Firewall/commit/b54c46638d32306ec27aecc69a830283aef17e61)
從下面數據可以看到開啟防火牆之後 Bandwidth 每秒下降了約 16.5 MB。
```
Connecting to host 192.168.2.200, port 5201
[ 4] local 192.168.1.100 port 34710 connected to 192.168.2.200 port 5201
[ ID] Interval Transfer Bandwidth Retr Cwnd
[ 4] 0.00-1.00 sec 1.06 GBytes 1116387 KBytes/sec 0 1.64 MBytes
[ 4] 1.00-2.00 sec 1.08 GBytes 1128982 KBytes/sec 0 1.64 MBytes
[ 4] 2.00-3.00 sec 1.09 GBytes 1138609 KBytes/sec 0 1.73 MBytes
[ 4] 3.00-4.00 sec 1.08 GBytes 1127468 KBytes/sec 0 1.81 MBytes
[ 4] 4.00-5.00 sec 1.05 GBytes 1105900 KBytes/sec 29 1.81 MBytes
[ 4] 5.00-6.00 sec 1.08 GBytes 1130410 KBytes/sec 0 1.81 MBytes
[ 4] 6.00-7.00 sec 1.09 GBytes 1139034 KBytes/sec 0 1.81 MBytes
[ 4] 7.00-8.00 sec 1.08 GBytes 1136199 KBytes/sec 0 1.81 MBytes
[ 4] 8.00-9.00 sec 1.08 GBytes 1137253 KBytes/sec 0 1.81 MBytes
[ 4] 9.00-10.00 sec 1.07 GBytes 1126335 KBytes/sec 0 1.81 MBytes
- - - - - - - - - - - - - - - - - - - - - - - - -
[ ID] Interval Transfer Bandwidth Retr
[ 4] 0.00-10.00 sec 10.8 GBytes 1128657 KBytes/sec 29 sender
[ 4] 0.00-10.00 sec 10.8 GBytes 1128394 KBytes/sec receiver
```
### 改進 TCP Flags 比對過程 [@2a0125c](https://github.com/D4nnyLee/XDP-Firewall/commit/2a0125c983c3bc0e6a0a561f86db371c3f228479)
經過簡化 TCP Flag 的比對後,Bandwidth 提升了約 1.4 MB。
```
Connecting to host 192.168.2.200, port 5201
[ 4] local 192.168.1.100 port 55482 connected to 192.168.2.200 port 5201
[ ID] Interval Transfer Bandwidth Retr Cwnd
[ 4] 0.00-1.00 sec 1.07 GBytes 1117284 KBytes/sec 0 1.75 MBytes
[ 4] 1.00-2.00 sec 1.09 GBytes 1146851 KBytes/sec 0 1.75 MBytes
[ 4] 2.00-3.00 sec 1.08 GBytes 1128799 KBytes/sec 26 1.75 MBytes
[ 4] 3.00-4.00 sec 1.06 GBytes 1110556 KBytes/sec 0 1.75 MBytes
[ 4] 4.00-5.00 sec 1.09 GBytes 1148022 KBytes/sec 0 1.92 MBytes
[ 4] 5.00-6.00 sec 1.08 GBytes 1130176 KBytes/sec 0 1.94 MBytes
[ 4] 6.00-7.00 sec 1.08 GBytes 1129429 KBytes/sec 0 1.94 MBytes
[ 4] 7.00-8.00 sec 1.09 GBytes 1138621 KBytes/sec 0 1.94 MBytes
[ 4] 8.00-9.00 sec 1.07 GBytes 1122506 KBytes/sec 0 1.94 MBytes
[ 4] 9.00-10.00 sec 1.08 GBytes 1128666 KBytes/sec 0 1.94 MBytes
- - - - - - - - - - - - - - - - - - - - - - - - -
[ ID] Interval Transfer Bandwidth Retr
[ 4] 0.00-10.00 sec 10.8 GBytes 1130091 KBytes/sec 26 sender
[ 4] 0.00-10.00 sec 10.8 GBytes 1129758 KBytes/sec receiver
```