BPF 進階筆記(一):BPF 程序類型詳解:使用場景、函數簽名、執行位置及程序示例
關於本文
內核目前支持 30 來種 BPF 程序類型。對於主要的程序類型,本文將介紹其:
- 使用場景 :適合用來做什麼?
- Hook 位置 :在 何處(where)、何時(when) 會觸發執行?例如在內核協議棧的哪個位置,或是什麼事件觸發執行。
-
程序簽名
(程序
入口函數
簽名)
- 傳入參數:調用到 BPF 程序時,傳給它的上下文(context,也就是函數參數)是什麼?
- 返回值:返回值類型、含義、合法列表等。
- 加載方式 :如何將程序附着(attach)到執行點?
- 程序示例 :一些實際例子。
- 延伸閲讀 :其他高級主題,例如相關的內核設計與實現。
本文主要參考:
-
BPF: A Tour of Program Types
,內容略老,基於內核
4.14
關於 “BPF 進階筆記” 系列
平時學習使用 BPF 時所整理。由於是筆記而非教程,因此內容不會追求連貫,有基礎的 同學可作查漏補缺之用。
文中涉及的代碼,如無特殊説明,均基於內核 5.8/5.10 版本。
- 關於 “BPF 進階筆記” 系列
-
- BPF 程序類型:完整列表
- BPF attach 類型:完整列表
- ————————————————————————
- ————————————————————————
-
1
BPF_PROG_TYPE_SOCKET_FILTER
-
- 場景一:流量過濾/複製(只讀,相當於抓包)
- 場景二:可觀測性:流量統計
-
Hook 位置:
sock_queue_rcv_skb()
-
-
傳入參數:
struct __sk_buff *
-
傳入參數:
-
加載方式:
setsockopt()
-
-
1. 可觀測性:內核自帶
samples/bpf/sockex1
~samples/bpf/sockex3
- 2. 流量複製:每個包只保留前 N 個字節
-
1. 可觀測性:內核自帶
-
-
2
BPF_PROG_TYPE_SOCK_OPS
-
使用場景:動態跟蹤/修改 socket 操作
- 場景一:動態跟蹤:監聽 socket 事件
- 場景二:動態修改 socket(例如 tcp 建連)操作
-
場景三:socket redirection(需要
BPF_PROG_TYPE_SK_SKB
程序配合)
-
-
傳入參數:
struct bpf_sock_ops *
-
傳入參數:
- 加載方式:attach 到某個 cgroup(可使用 bpftool 等工具)
-
- 1. Customize TCP initial RTO (retransmission timeout) with BPF
- 2. Cracking kubernetes node proxy (aka kube-proxy),其中的第五種實現方式
- 3. (譯) 利用 ebpf sockmap/redirection 提升 socket 性能(2020)
-
使用場景:動態跟蹤/修改 socket 操作
-
3
BPF_PROG_TYPE_SK_SKB
-
- 場景一:修改 skb/socket 信息,socket 重定向
- 場景二:動態解析消息流(stream parsing)
-
- socket redirection 類型
-
strparser 類型:
smap_parse_func_strparser()
/smap_verdict_func()
-
-
傳入參數:
struct __sk_buff *
-
傳入參數:
- 加載方式:attach 到某個 sockmap(可使用 bpftool 等工具)
-
- 1. (譯) 利用 ebpf sockmap/redirection 提升 socket 性能(2020)
-
2.
strparser
框架:解析消息流
-
- ————————————————————————
- ————————————————————————
-
1
BPF_PROG_TYPE_SCHED_CLS
-
Hook 位置:
sch_handle_ingress()
/sch_handle_egress()
-
-
傳入參數:
struct __sk_buff *
-
傳入參數:
-
加載方式:
tc
命令(背後使用 netlink)
-
Hook 位置:
-
2
BPF_PROG_TYPE_SCHED_ACT
- ————————————————————————
- XDP(eXpress Data Path)程序
- ————————————————————————
-
-
- 場景一:防火牆、四層負載均衡等
-
-
傳入參數:
struct xdp_md *
-
返回值:
enum xdp_action
-
傳入參數:
- 加載方式:netlink socket
-
- 1. samples/bpf/bpf_load.c
-
- ————————————————————————
- ————————————————————————
-
1
BPF_PROG_TYPE_CGROUP_SKB
-
- 場景一:在 CGroup 級別:放行/丟棄數據包
-
Hook 位置:
sk_filter_trim_cap()
-
-
傳入參數:
struct sk_buff *skb
-
傳入參數:
- 加載方式:attach 到 cgroup 文件描述符
-
-
2
BPF_PROG_TYPE_CGROUP_SOCK
-
- 場景一:在 CGroup 級別:觸發 socket 操作時拒絕/放行網絡訪問
-
-
傳入參數:
struct sk_buff *skb
-
傳入參數:
-
觸發執行:
inet_create()
- 加載方式:attach 到 cgroup 文件描述符
-
- ————————————————————————
- kprobes、tracepoints、perf events
- ————————————————————————
-
1
BPF_PROG_TYPE_KPROBE
-
- 場景一:觀測內核函數(kprobe)和用户空間函數(uprobe)
-
Hook 位置:
k[ret]probe_perf_func()
/u[ret]probe_perf_func()
-
-
傳入參數:
struct pt_regs *ctx
-
傳入參數:
-
加載方式:
/sys/fs/debug/tracing/
目錄下的配置文件
-
-
2
BPF_PROG_TYPE_TRACEPOINT
-
- 場景一:Instrument 內核代碼中的 tracepoints
-
Hook 位置:
perf_trace_<event_class>()
-
- 傳入參數:因 tracepoint 而異
-
加載方式:
/sys/fs/debug/tracing/
目錄下的配置文件
-
-
3
BPF_PROG_TYPE_PERF_EVENT
-
- 場景一:Instrument 軟件/硬件 perf 事件
-
-
傳入參數:
struct bpf_perf_event_data *
-
傳入參數:
- 觸發執行:每個採樣間隔執行一次
-
- ————————————————————————
- ————————————————————————
-
1
BPF_PROG_TYPE_LWT_IN
-
- 場景一:檢查入向流量是否需要做解封裝(decap)
-
Hook 位置:
lwtunnel_input()
-
-
傳入參數:
struct sk_buff *
-
傳入參數:
-
加載方式:
ip route add
-
-
2
BPF_PROG_TYPE_LWT_OUT
- 場景一:對出向流量做封裝(encap)
-
加載方式:
ip route add
-
-
傳入參數:
struct __sk_buff *
-
傳入參數:
-
觸發執行:
lwtunnel_output()
-
3
BPF_PROG_TYPE_LWT_XMIT
-
- 場景一:實現輕量級隧道發送端的 encap/redir 方法
-
Hook 位置:
lwtunnel_xmit()
-
-
傳入參數:
struct __sk_buff *
-
傳入參數:
-
加載方式:
ip route add
-
基礎
BPF 程序類型:完整列表
Kernel 5.8 支持的 BPF 程序類型 :
// 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, // CLS: tc classifier,分類器 BPF_PROG_TYPE_SCHED_ACT, // ACT: tc action,動作 BPF_PROG_TYPE_TRACEPOINT, BPF_PROG_TYPE_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, BPF_PROG_TYPE_TRACING, BPF_PROG_TYPE_STRUCT_OPS, BPF_PROG_TYPE_EXT, BPF_PROG_TYPE_LSM, };
BPF attach 類型:完整列表
通過 socket()
系統調用將 BPF 程序 attach 到 hook 點時用到, 定義
:
// include/uapi/linux/bpf.h enum bpf_attach_type { BPF_CGROUP_INET_INGRESS, BPF_CGROUP_INET_EGRESS, BPF_CGROUP_INET_SOCK_CREATE, BPF_CGROUP_SOCK_OPS, BPF_SK_SKB_STREAM_PARSER, BPF_SK_SKB_STREAM_VERDICT, BPF_CGROUP_DEVICE, BPF_SK_MSG_VERDICT, BPF_CGROUP_INET4_BIND, BPF_CGROUP_INET6_BIND, BPF_CGROUP_INET4_CONNECT, BPF_CGROUP_INET6_CONNECT, BPF_CGROUP_INET4_POST_BIND, BPF_CGROUP_INET6_POST_BIND, BPF_CGROUP_UDP4_SENDMSG, BPF_CGROUP_UDP6_SENDMSG, BPF_LIRC_MODE2, BPF_FLOW_DISSECTOR, BPF_CGROUP_SYSCTL, BPF_CGROUP_UDP4_RECVMSG, BPF_CGROUP_UDP6_RECVMSG, BPF_CGROUP_GETSOCKOPT, BPF_CGROUP_SETSOCKOPT, BPF_TRACE_RAW_TP, BPF_TRACE_FENTRY, BPF_TRACE_FEXIT, BPF_MODIFY_RETURN, BPF_LSM_MAC, BPF_TRACE_ITER, BPF_CGROUP_INET4_GETPEERNAME, BPF_CGROUP_INET6_GETPEERNAME, BPF_CGROUP_INET4_GETSOCKNAME, BPF_CGROUP_INET6_GETSOCKNAME, BPF_XDP_DEVMAP, __MAX_BPF_ATTACH_TYPE };
————————————————————————
Socket 相關類型
————————————————————————
用於 過濾和重定向 socket 數據,或者監聽 socket 事件 。類型包括:
BPF_PROG_TYPE_SOCKET_FILTER BPF_PROG_TYPE_SOCK_OPS BPF_PROG_TYPE_SK_SKB BPF_PROG_TYPE_SK_MSG BPF_PROG_TYPE_SK_REUSEPORT
1 BPF_PROG_TYPE_SOCKET_FILTER
使用場景
場景一:流量過濾/複製(只讀,相當於抓包)
從名字 SOCKET_FILTER 可以看出,這種類型的 BPF 程序能對流量進行 過濾(filtering)。
場景二:可觀測性:流量統計
仍然是對流量進行過濾,但只統計流量信息,不要包本身。
Hook 位置: sock_queue_rcv_skb()
在
sock_queue_rcv_skb()
中觸發執行:
// net/core/sock.c // 處理 socket 入向流量,TCP/UDP/ICMP/raw-socket 等協議類型都會執行到這裏 int sock_queue_rcv_skb(struct sock *sk, struct sk_buff *skb) { err = sk_filter(sk, skb); // 執行 BPF 代碼,這裏返回的 err 表示對這個包保留前多少字節(trim) if (err) // 如果字節數大於 0 return err; // 跳過接下來的處理邏輯,直接返回到更上層 // 如果字節數等於 0,繼續執行內核正常的 socket receive 邏輯 return __sock_queue_rcv_skb(sk, skb); }
程序簽名
傳入參數: struct __sk_buff *
上面可以看到,hook 入口 sk_filter(sk, skb)
傳的是 struct sk_buff *skb
,
但經過層層傳遞,最終傳遞給 BPF 程序的其實是 struct __sk_buff *
。
這個結構體的
定義
見 include/uapi/linux/bpf.h
,
// include/uapi/linux/bpf.h // user accessible mirror of in-kernel sk_buff. struct __sk_buff { ... }
-
如註釋所説,它是對
struct sk_buff
的 用户可訪問字段 的鏡像。 -
BPF 程序中對
struct __sk_buff
字段的訪問,將會被 BPF 校驗器轉換成對相應的 struct sk_buff 字段的訪問 。 - 為什麼要多引入這一層封裝 ,見 bpf: allow extended BPF programs access skb fields 。
返回值
再來看一下 hook 前後的邏輯:
// net/core/sock.c // 處理 socket 入向流量,TCP/UDP/ICMP/raw-socket 等協議類型都會執行到這裏 int sock_queue_rcv_skb(struct sock *sk, struct sk_buff *skb) { err = sk_filter(sk, skb); // 執行 BPF 代碼,這裏返回的 err 表示對這個包保留前多少字節(trim) if (err) // 如果字節數大於 0 return err; // 跳過接下來的處理邏輯,直接返回到更上層 // 如果字節數等於 0,繼續執行內核正常的 socket receive 邏輯 return __sock_queue_rcv_skb(sk, skb); }
如果 sk_filter()
的返回值 err
-
err != 0
:直接return err
,返回到調用方, 不再繼續原來正常的內核處理邏輯__sock_queue_rcv_skb()
;所以效果就是: 將這個包過濾了出來 (符合過濾條件); -
err == 0
:接下來繼續執行正常的內核處理,也就是 這個包不符合過濾條件 ;
所以至此大概就知道要實現過濾和截斷功能,程序應該返回什麼了。要精確搞清楚,需要
看 sk_filter()
一直調用到 BPF 程序的代碼,看中間是否對 BPF 程序的返回值做了封
裝和轉換。
這裏給出結論:BPF 程序的 返回值 ,
-
n
(n < pkt_size
):返回一個 截斷的包 (副本),只保留前面n
個字節。 -
0
: 忽略 這個包;
需要説明:
- 這裏所謂的截斷並不是截斷原始包,而只是複製一份包的元數據,修改其中的包長字段;
- 程序本身不會截斷或丟棄原始流量,也就是説,對 原始流量是隻讀的 (read only);
加載方式: setsockopt()
通過 setsockopt(fd, SO_ATTACH_BPF, ...)
系統調用,其中 fd 是
BPF 程序的文件描述符
。
程序示例
1. 可觀測性:內核自帶 samples/bpf/sockex1
~ samples/bpf/sockex3
這三個例子都是用 BPF 程序 過濾網絡設備設備上的包 , 根據協議類型、IP、端口等信息統計流量。
源碼 samples/bpf/ 。
$ cd samples/bpf/ $ make $ ./sockex1
2. 流量複製:每個包只保留前 N 個字節
下面的例子根據包的協議類型對包進行截斷。簡單起見,不解析 IPv4 option 字段。
#include <uapi/linux/bpf.h> #include <uapi/linux/in.h> #include <uapi/linux/types.h> #include <uapi/linux/string.h> #include <uapi/linux/if_ether.h> #include <uapi/linux/if_packet.h> #include <uapi/linux/ip.h> #include <uapi/linux/tcp.h> #include <uapi/linux/udp.h> #include <bpf/bpf_helpers.h> #ifndef offsetof #define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER) #endif // We are only interested in TCP/UDP headers, so drop every other protocol // and trim packets after the TCP/UDP header by returning eth_len + ipv4_hdr + TCP/UDP header __section("socket") int bpf_trim_skb(struct __sk_buff *skb) { int proto = load_byte(skb, ETH_HLEN + offsetof(struct iphdr, protocol)); int size = ETH_HLEN + sizeof(struct iphdr); switch (proto) { case IPPROTO_TCP: size += sizeof(struct tcphdr); break; case IPPROTO_UDP: size += sizeof(struct udphdr); break; default: size = 0; break; // drop this packet } return size; } char _license[] __section("license") = "GPL";
編譯及測試:比較簡單的方法是將源文件放到內核源碼樹中 samples/bpf/
目錄下。
參考其中的 sockex1
來編譯、加載和測試。
延伸閲讀
相關實現見
sk_filter_trim_cap()
,
它會進一步調用
bpf_prog_run_save_cb()
。
2 BPF_PROG_TYPE_SOCK_OPS
使用場景:動態跟蹤/修改 socket 操作
這裏所説的 socket 事件包括建連(connection establishment)、重傳(retransmit)、超時(timeout)等等。
場景一:動態跟蹤:監聽 socket 事件
這種場景只會攔截和解析系統事件,不會修改任何東西。
場景二:動態修改 socket(例如 tcp 建連)操作
攔截到事件後,通過 bpf_setsockopt()
動態修改 socket 配置,
能夠實現 per-connection 的優化,提升性能。例如,
- 監聽到被動建連(passive establishment of a connection)事件時,如果 對端和本機不在同一個網段 ,就 動態修改這個 socket 的 MTU 。 這樣能避免包因為太大而被中間路由器分片(fragmenting)。
- 替換目的 IP 地址 ,實現高性能的 透明代理及負載均衡 。 Cilium 對 K8s 的 Service 就是這樣實現的,詳見 [1]。
場景三:socket redirection(需要 BPF_PROG_TYPE_SK_SKB
程序配合)
這個其實算是“動態修改”的特例。與 BPF_PROG_TYPE_SK_SKB
程序配合,通過
sockmap+redirection 實現 socket 重定向。這種情況下分為兩段 BPF 程序,
-
第一段是
BPF_PROG_TYPE_SOCK_OPS
程序,攔截 socket 事件,並從struct bpf_sock_ops
中提取 socket 信息存儲到 sockmap; -
第二段是
BPF_PROG_TYPE_SK_SKB
類型程序,從攔截到的 socket message 提取 socket 信息,然後去 sockmap 查找對端 socket,然後通過bpf_sk_redirect_map()
直接重定向過去。
Hook 位置:多個地方
其他類型的 BPF 程序都是在某個特定的代碼出執行的,但 SOCK_OPS 程序不同,它們
在多個地方執行,op 字段表示觸發執行的地方
。 op
字段是枚舉類型, 完整列表
:
// include/uapi/linux/bpf.h /* List of known BPF sock_ops operators. */ enum { BPF_SOCK_OPS_VOID, BPF_SOCK_OPS_TIMEOUT_INIT, // 初始化 TCP RTO 時調用 BPF 程序 // 程序應當返回希望使用的 SYN-RTO 值;-1 表示使用默認值 BPF_SOCK_OPS_RWND_INIT, // BPF 程序應當返回 initial advertized window (in packets);-1 表示使用默認值 BPF_SOCK_OPS_TCP_CONNECT_CB, // 主動建連 初始化之前 回調 BPF 程序 BPF_SOCK_OPS_ACTIVE_ESTABLISHED_CB, // 主動建連 成功之後 回調 BPF 程序 BPF_SOCK_OPS_PASSIVE_ESTABLISHED_CB,// 被動建連 成功之後 回調 BPF 程序 BPF_SOCK_OPS_NEEDS_ECN, // If connection's congestion control needs ECN */ BPF_SOCK_OPS_BASE_RTT, // 獲取 base RTT。The correct value is based on the path,可能還與擁塞控制 // 算法相關。In general it indicates // a congestion threshold. RTTs above this indicate congestion BPF_SOCK_OPS_RTO_CB, // 觸發 RTO(超時重傳)時回調 BPF 程序,三個參數: // Arg1: value of icsk_retransmits // Arg2: value of icsk_rto // Arg3: whether RTO has expired BPF_SOCK_OPS_RETRANS_CB, // skb 發生重傳之後,回調 BPF 程序,三個參數: // Arg1: sequence number of 1st byte // Arg2: # segments // Arg3: return value of tcp_transmit_skb (0 => success) BPF_SOCK_OPS_STATE_CB, // TCP 狀態發生變化時,回調 BPF 程序。參數: // Arg1: old_state // Arg2: new_state BPF_SOCK_OPS_TCP_LISTEN_CB, // 執行 listen(2) 系統調用,socket 進入 LISTEN 狀態之後,回調 BPF 程序 };
從以上註釋可以看到,這些 OPS 分為兩種類型:
-
通過 BPF 程序的返回值來動態修改配置 ,類型包括
BPF_SOCK_OPS_TIMEOUT_INIT BPF_SOCK_OPS_RWND_INIT BPF_SOCK_OPS_NEEDS_ECN BPF_SOCK_OPS_BASE_RTT
-
在 socket/tcp 狀態發生變化時, 回調(callback)BPF 程序 ,類型包括
BPF_SOCK_OPS_TCP_CONNECT_CB BPF_SOCK_OPS_ACTIVE_ESTABLISHED_CB BPF_SOCK_OPS_PASSIVE_ESTABLISHED_CB BPF_SOCK_OPS_RTO_CB BPF_SOCK_OPS_RETRANS_CB BPF_SOCK_OPS_STATE_CB BPF_SOCK_OPS_TCP_LISTEN_CB
引入該功能的內核 patch 見 bpf: Adding support for sock_ops ;
SOCK_OPS 類型的 BPF 程序 都是從 tcp_call_bpf() 調用過來的 ,這個文件中多個地方都會調用到該函數。
程序簽名
傳入參數: struct bpf_sock_ops *
結構體 定義 ,
// include/uapi/linux/bpf.h struct bpf_sock_ops { __u32 op; // socket 事件類型,就是上面的 BPF_SOCK_OPS_* union { __u32 args[4]; // Optionally passed to bpf program __u32 reply; // BPF 程序的返回值。例如,op==BPF_SOCK_OPS_TIMEOUT_INIT 時, // BPF 程序的返回值就表示希望為這個 TCP 連接設置的 RTO 值 __u32 replylong[4]; // Optionally returned by bpf prog }; __u32 family; __u32 remote_ip4; /* Stored in network byte order */ __u32 local_ip4; /* Stored in network byte order */ __u32 remote_ip6[4]; /* Stored in network byte order */ __u32 local_ip6[4]; /* Stored in network byte order */ __u32 remote_port; /* Stored in network byte order */ __u32 local_port; /* stored in host byte order */ ... };
返回值
如前面所述,ops 類型不同,返回值也不同。
加載方式:attach 到某個 cgroup(可使用 bpftool 等工具)
指定以 BPF_CGROUP_SOCK_OPS
類型,將 BPF 程序 attach 到某個 cgroup 文件描述符。
依賴 cgroupv2。
內核已經有了 BPF_PROG_TYPE_CGROUP_SOCK
類型的 BPF 程序,這裏為什麼又要引入一個 BPF_PROG_TYPE_SOCK_OPS
類型的程序呢?
-
BPF_PROG_TYPE_CGROUP_SOCK
類型的 BPF 程序:在一個連接(connection)的生命週期中 只執行一次 , -
BPF_PROG_TYPE_SOCK_OPS
類型的 BPF 程序:在一個連接的生命週期中,在 不同地方被多次調用 。
程序示例
1. Customize TCP initial RTO (retransmission timeout) with BPF
2. Cracking kubernetes node proxy (aka kube-proxy) ,其中的第五種實現方式
3. (譯) 利用 ebpf sockmap/redirection 提升 socket 性能(2020)
延伸閲讀
3 BPF_PROG_TYPE_SK_SKB
使用場景
場景一:修改 skb/socket 信息,socket 重定向
這個功能依賴 sockmap,後者是一種特殊類型的 BPF map,其中存儲的是 socket 引用(references)。
典型流程:
- 創建 sockmap
- 攔截 socket 操作,將 socket 信息存入 sockmap
- 攔截 socket sendmsg/recvmsg 等系統調用,從 msg 中提取信息(IP、port等),然後 在 sockmap 中查找對端 socket,然後重定向過去。
根據提取到的 socket 信息判斷接下來應該做什麼的過程稱為verdict(判決)。 verdict 類型可以是:
__SK_DROP __SK_PASS __SK_REDIRECT
場景二:動態解析消息流(stream parsing)
這種程序的一個應用是 strparser framework 。
它與上層應用配合, 在內核中提供應用層消息解析的支持 (provide kernel support for application layer messages)。兩個使用了 strparser 框架的例子:TLS 和 KCM( Kernel Connection Multiplexor)。
Hook 位置
socket redirection 類型
TODO
strparser 類型: smap_parse_func_strparser()
/ smap_verdict_func()
Socket receive 過程執行到
smap_parse_func_strparser()
時,觸發 STREAM_PARSER BPF 程序執行。
執行到 smap_verdict_func()
時,觸發 VERDICT BPF 程序執行。
程序簽名
傳入參數: struct __sk_buff *
見的介紹。
從中可以提取出 socket 信息(IP、port 等)。
返回值
TODO
加載方式:attach 到某個 sockmap(可使用 bpftool 等工具)
這種程序需要指定 BPF_SK_SKB_STREAM_*
類型,將 BPF 程序 attach 到 sockmap:
BPF_SK_SKB_STREAM_VERDICT BPF_SK_SKB_STREAM_PARSER
程序示例
1. (譯) 利用 ebpf sockmap/redirection 提升 socket 性能(2020)
2. strparser
框架:解析消息流
延伸閲讀
- 內核 patch 文檔: BPF: sockmap and sk redirect support
————————————————————————
TC 子系統相關類型
————————————————————————
將 BPF 程序用作 tc 分類器(classifiers)和執行器(actions)。
BPF_PROG_TYPE_SCHED_CLS BPF_PROG_TYPE_SCHED_ACT
TC 是 Linux 的 QoS 子系統。幫助信息(非常有用):
tc(8) tc-bpf(8)
1 BPF_PROG_TYPE_SCHED_CLS
使用場景
場景一:tc 分類器
tc(8)
命令支持 eBPF,因此能直接將 BPF 程序作為
classifiers 和 actions 加載到 ingress/egress hook 點。
如何使用 tc BPF 提供的能力,參考 man8: tc-bpf 。
Hook 位置: sch_handle_ingress()
/ sch_handle_egress()
sch_handle_ingress()/egress()
會調用到 tcf_classify()
,
- 對於 ingress,通過網絡設備的 receive 方法做 流量分類 ,這個處 理位置在 網卡驅動處理之後,在內核協議棧(IP 層)處理之前 。
- 對於 egress,將包交給設備隊列(device queue)發送之前,執行 BPF 程序。
程序簽名
傳入參數: struct __sk_buff *
見的介紹。
返回值
返回 TC verdict 結果。
加載方式: tc
命令(背後使用 netlink)
步驟:
- 為網絡設備添加分類器(classifier/qdisc):創建一個 “clsact” qdisc
- 為網絡設備添加過濾器(filter):需要指定方向(egress/ingress)、目標文件、ELF section 等選項
例如,
$ tc qdisc add dev eth0 clsact $ tc filter add dev eth0 egress bpf da obj toy-proxy-bpf.o sec egress
加載過程分為 tc 前端和內核 bpf 後端兩部分, 中間通過 netlink socket 通信,源碼分析見 Firewalling with BPF/XDP: Examples and Deep Dive
程序示例
延伸閲讀
- cls_bpf.c 實現 tc classifier 模塊
2 BPF_PROG_TYPE_SCHED_ACT
使用方式與 BPF_PROG_TYPE_SCHED_CLS
類似,但用作 TC action。
使用場景
場景一:tc action
Hook 位置
程序簽名
加載方式: tc
命令
延伸閲讀
- act_bpf.c 實現 tc action 模塊
————————————————————————
XDP(eXpress Data Path)程序
————————————————————————
XDP 位於設備驅動中(在創建 skb 之前),因此能最大化網絡處理性能, 而且可編程、通用(很多廠商的設備都支持)。
1 BPF_PROG_TYPE_XDP
使用場景
場景一:防火牆、四層負載均衡等
由於 XDP 程序執行時 skb 都還沒創建,開銷非常低,因此效率非常高。適用於 DDoS 防禦、四層負載均衡等場景。
XDP 就是通過 BPF hook 對內核進行 運行時編程 (run-time programming),但 基於內核而不是繞過(bypass)內核 。
Hook 位置:網絡驅動
XDP 是在 網絡驅動 中實現的, 有專門的 TX/RX queue (native 方式)。
對於沒有實現 XDP 的驅動,內核中實現了一個稱為 “generic XDP” 的 fallback 實現, 見 net/core/dev.c 。
- Native XDP:處理的階段非常早,在 skb 創建之前,因此性能非常高;
- Generic XDP: 在 skb 創建之後 ,因此性能比前者差,但功能是一樣的。
程序簽名
傳入參數: struct xdp_md *
定義 ,非常輕量級:
// include/uapi/linux/bpf.h /* user accessible metadata for XDP packet hook */ struct xdp_md { __u32 data; __u32 data_end; __u32 data_meta; /* Below access go through struct xdp_rxq_info */ __u32 ingress_ifindex; /* rxq->dev->ifindex */ __u32 rx_queue_index; /* rxq->queue_index */ __u32 egress_ifindex; /* txq->dev->ifindex */ };
返回值: enum xdp_action
// include/uapi/linux/bpf.h enum xdp_action { XDP_ABORTED = 0, XDP_DROP, XDP_PASS, XDP_TX, XDP_REDIRECT, };
加載方式:netlink socket
通過 netlink socket 消息 attach:
socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE) NLA_F_NESTED | 43
用 tc
attach BPF 程序,其實背後使用的也是 netlink socket。
程序示例
1. samples/bpf/bpf_load.c
延伸閲讀
TODO
————————————————————————
CGroups 相關的類型
————————————————————————
CGroups 用於 對一組進程 (a group of processes)進行控制,
- 處理資源分配,例如 CPU、網絡帶寬等。
- 系統資源權限控制(allowing or denying)。
CGroups 最典型的使用場景是容器 (containers)。
- 命名空間(namespace):控制資源視圖,即 能看到什麼,不能看到什麼 ,
- CGroups:控制的 能使用多少 ?
具體到 eBPF 方面,可以 用 CGroups 來控制訪問權限 (allow or deny),程序的返回結果只有兩種:
- 放行
- 禁止(導致隨後包被丟棄)
例如,很多 hook 會執行到宏 __cgroup_bpf_run_filter_skb()
,它負責執行 cgroup BPF 程序:
// include/linux/bpf-cgroup.h #define BPF_CGROUP_RUN_PROG_INET_INGRESS(sk, skb) \ ({ \ int __ret = 0; \ if (cgroup_bpf_enabled) \ __ret = __cgroup_bpf_run_filter_skb(sk, skb, \ BPF_CGROUP_INET_INGRESS); \ \ __ret; \ }) #define BPF_CGROUP_RUN_SK_PROG(sk, type) \ ({ \ int __ret = 0; \ if (cgroup_bpf_enabled) { \ __ret = __cgroup_bpf_run_filter_sk(sk, type); \ } \ __ret; \ })
完整的 cgroups hook 列表見 enum bpf_attach_type
列表,其中的 BPF_CGROUP_*
。
1 BPF_PROG_TYPE_CGROUP_SKB
使用場景
場景一:在 CGroup 級別:放行/丟棄數據包
在 IP egress/ingress 層禁止或允許網絡訪問。
Hook 位置: sk_filter_trim_cap()
對於 inet ingress, sk_filter_trim_cap()
會調用 BPF_CGROUP_RUN_PROG_INET_INGRESS(sk, skb)
;如果返回值非零,錯誤信息會傳遞給調
用方(例如, __sk_receive_skb()
),隨後包會被丟棄並釋放(discarded and freed)
。
egress 是類似的,但在 ip[6]_finish_output()
中。
程序簽名
傳入參數: struct sk_buff *skb
返回值
-
1
:放行; -
其他任何值:會使
__cgroup_bpf_run_filter_skb()
返回-EPERM
,這會進一步返 回給調用方,吿訴它們應該丟棄該包。
加載方式:attach 到 cgroup 文件描述符
根據 BPF attach 的 hook 位置,選擇合適的 attach 類型:
BPF_CGROUP_INET_INGRESS BPF_CGROUP_INET_EGRESS
2 BPF_PROG_TYPE_CGROUP_SOCK
使用場景
場景一:在 CGroup 級別:觸發 socket 操作時拒絕/放行網絡訪問
這裏的 socket 相關事件包括 BPF_CGROUP_INET_SOCK_CREATE、BPF_CGROUP_SOCK_OPS。
程序簽名
傳入參數: struct sk_buff *skb
返回值
跟前面一樣,程序返回 1 表示允許訪問。
返回其他值會導致 __cgroup_bpf_run_filter_sk()
返回 -EPERM
,調用方收到這個返回值會將包丟棄。
觸發執行: inet_create()
Socket 創建時會執行 inet_create()
,裏面會調用 BPF_CGROUP_RUN_PROG_INET_SOCK()
,如果該函數執行失敗,socket 就會被釋放。
加載方式:attach 到 cgroup 文件描述符
TODO
————————————————————————
kprobes、tracepoints、perf events
————————————————————————
三者都用於 kernel instrumentation。簡單對比:
數據源 | Type | Kernel/User space | |
---|---|---|---|
kprobes | Dynamic | Kernel | 觀測內核函數的運行時(進入和離開函數)參數值等信息 |
uprobes | Dynamic | Userspace | 同上,但觀測的是用户態函數 |
tracepoints | Static | Kernel | 將自定義 handler 編譯並加載到某些內核 hook,能拿到更多觀測信息 |
USDT | Static | Userspace |
更具體區別可參考 Linux tracing systems & how they fit together 。
-
kprobes :對 特定函數 進行 instrumentation。
-
進入
函數時觸發
kprobe
-
離開
函數時觸發
kretprobe
啟用後,會 將 probe 位置的一段空代碼替換為一個斷點指令 。 當程序執行到這個斷點時,會 觸發一條 trap 指令 ,然後保存寄存器狀態, 跳轉到指定的處理函數 (instrumentation handler)。
- kprobes 由 kprobe_dispatcher() 處理, 其中會獲取 kprobe 的地址和寄存器上下文信息。
- kretprobes 是通過 kprobes 實現的。
-
進入
函數時觸發
-
Tracepoints :內核中的輕量級 hook。
Tracepoints 與 kprobes 類似,但 前者是動態插入代碼來完成的,後者 顯式地(靜態地)寫在代碼中的 。 啟用之後,會 從這些地方收集 debug 信息 。
同一個 tracepoints 可能會在多個地方聲明;例如,
trace_drv_return_int()
在 net/mac80211/driver-ops.c 中的多個地方被調用。查看可用的 tracepoints 列表:
ls /sys/kernel/debug/tracing/events
。 -
Perf events :是這裏提到的幾種 eBPF 程序的基礎。
BPF 基於已有的基礎設施來完成事件採樣(event sampling),允許 attach 程序到 感興趣的 perf 事件,包括kprobes, uprobes, tracepoints 以及軟件和硬件事件。
這些 instrumentation points 使 BPF 成為了一個通用的跟蹤工具 , 超越了最初的網絡範疇。
1 BPF_PROG_TYPE_KPROBE
使用場景
場景一:觀測內核函數(kprobe)和用户空間函數(uprobe)
通過 kprobe
/ kretprobe
觀測內核函數。 k[ret]probe_perf_func()
會執行加載到 probe 點的 BPF 程序。
另外,這種程序也能 attach 到 u[ret]probes
,詳情見 uprobetracer.txt
。
Hook 位置: k[ret]probe_perf_func()
/ u[ret]probe_perf_func()
啟用某個 probe 並執行到斷點時, k[ret]probe_perf_func()
會通過 trace_call_bpf()
執行 attach 在這個 probe 位置的 BPF 程序。
u[ret]probe_perf_func()
也是類似的。
程序簽名
傳入參數: struct pt_regs *ctx
可以通過這個指針訪問寄存器。
這個變量內的很多字段是平台相關的,但也有一些通用函數,例如 regs_return_value(regs)
,返回的是存儲程序返回值的寄存器內的值(x86 上對應的是 ax
寄存器)。
返回值
加載方式: /sys/fs/debug/tracing/
目錄下的配置文件
/sys/kernel/debug/tracing/events/[uk]probe/<probename>/id /sys/kernel/debug/tracing/events/[uk]retprobe/<probename>/id
程序示例
Documentation/trace/kprobetrace.txt 有詳細的例子。例如,
# 創建一個名為 `myprobe` 的程序,attach 到進入函數 `tcp_retransmit_skb()` 的地方 $ echo 'p:myprobe tcp_retransmit_skb' > /sys/kernel/debug/tracing/kprobe_events # 獲取 probe id $ cat /sys/kernel/debug/tracing/events/kprobes/myprobe/id 2266
用以上 id 打開一個 perf event,啟用它,然後將這個 perf event 的 BPF 程序指定為我們的程序。 過程可參考 load_and_attach() :
// samples/bpf/bpf_load.c static int load_and_attach(const char *event, struct bpf_insn *prog, int size) { struct perf_event_attr attr; /* Load BPF program and assign programfd to it; and get probeid of probe from sysfs */ attr.type = PERF_TYPE_TRACEPOINT; attr.sample_type = PERF_SAMPLE_RAW; attr.sample_period = 1; attr.wakeup_events = 1; attr.config = probeid; // /sys/kernel/debug/tracing/events/kprobes/<probe>/id eventfd = sys_perf_event_open(&attr, -1, 0, programfd, 0); ioctl(eventfd, PERF_EVENT_IOC_ENABLE, 0); ioctl(eventfd, PERF_EVENT_IOC_SET_BPF, programfd); ... }
延伸閲讀
TODO
2 BPF_PROG_TYPE_TRACEPOINT
使用場景
場景一:Instrument 內核代碼中的 tracepoints
啟用方式和上面的 kprobe 類似:
$ echo 1 > /sys/kernel/xxx/enable
可跟蹤的事件都在 /sys/kernel/debug/tracing/events 目錄下面。
Hook 位置: perf_trace_<event_class>()
相應的 tracepoint 啟用並執行到之後, perf_trace_<event_class>()
(定義見 include/trace/perf.h)
調用 perf_trace_run_bpf_submit()
,後者通過 trace_call_bpf()
觸發 BPF 程序執行。
程序簽名
傳入參數:因 tracepoint 而異
傳入的參數和類型因 tracepoint 而異,見其定義。
見 /sys/kernel/debug/tracing/events/<tracepoint>/format
。例如,
$ sudo cat /sys/kernel/debug/tracing/events/net/netif_rx/format name: netif_rx ID: 1457 format: field:unsigned short common_type; offset:0; size:2; signed:0; field:unsigned char common_flags; offset:2; size:1; signed:0; field:unsigned char common_preempt_count; offset:3; size:1; signed:0; field:int common_pid; offset:4; size:4; signed:1; field:void * skbaddr; offset:8; size:8; signed:0; field:unsigned int len; offset:16; size:4; signed:0; field:__data_loc char[] name; offset:20; size:4; signed:1; print fmt: "dev=%s skbaddr=%p len=%u", __get_str(name), REC->skbaddr, REC->len
順便看一下這個 tracepoint 在內核中的 實現 ,
// net/core/dev.c static int netif_rx_internal(struct sk_buff *skb) { net_timestamp_check(netdev_tstamp_prequeue, skb); trace_netif_rx(skb); ... }
返回值
加載方式: /sys/fs/debug/tracing/
目錄下的配置文件
例如,
# 啟用 `net/net_dev_xmit` tracepoint as "myprobe2" $ echo 'p:myprobe2 trace:net/net_dev_xmit' > /sys/kernel/debug/tracing/kprobe_events # 獲取 probe ID $ cat /sys/kernel/debug/tracing/events/kprobes/myprobe2/id 2270
過程加載代碼可參考 load_and_attach() 。
3 BPF_PROG_TYPE_PERF_EVENT
使用場景
場景一:Instrument 軟件/硬件 perf 事件
包括系統調用事件、定時器超時事件、硬件採樣事件等。硬件事件包括 PMU(processor monitoring unit)事件,它吿訴我們已經執行了多少條指令之類的信息。
Perf 事件監控能具體到某個進程、組、處理器,也可以指定採樣頻率。
加載方式: ioctl()
- perf_event_open() ,帶一些採樣配置信息;
-
ioctl(fd, PERF_EVENT_IOC_SET_BPF)
設置 BPF程序, - 然後用 ioctl(fd, PERF_EVENT_IOC_ENABLE) 啟用事件,
程序簽名
傳入參數: struct bpf_perf_event_data *
定義 :
// include/uapi/linux/bpf_perf_event.h struct bpf_perf_event_data { bpf_user_pt_regs_t regs; __u64 sample_period; __u64 addr; };
觸發執行:每個採樣間隔執行一次
取決於 perf event firing 和選擇的採樣頻率。
————————————————————————
輕量級隧道類型
————————————————————————
Lightweight tunnels 提供了 對內核路 由子系統的編程能力 ,據此可以實現輕量級隧道。
舉個例子,下面是沒有 BPF 編程能力時,如何(為不同協議)添加路由:
# VXLAN: $ ip route add 40.1.1.1/32 encap vxlan id 10 dst 50.1.1.2 dev vxlan0 $ MPLS: $ ip route add 10.1.1.0/30 encap mpls 200 via inet 10.1.1.1 dev swp1
有了 BPF 可編程性之後,能為出向流量(入向是隻讀的)做封裝。 詳見 BPF for lightweight tunnel encapsulation 。
與 tc
類似, ip route
支持直接將 BPF 程序 attach 到網絡設備:
$ ip route add 192.168.253.2/32 \ encap bpf out obj lwt_len_hist_kern.o section len_hist \ dev veth0
1 BPF_PROG_TYPE_LWT_IN
使用場景
場景一:檢查入向流量是否需要做解封裝(decap)
Examine inbound packets for lightweight tunnel de-encapsulation.
Hook 位置: lwtunnel_input()
該函數支持多種封裝類型。 The BPF case runs bpf_input in net/core/lwt_bpf.c with redirection disallowed.
程序簽名
傳入參數: struct sk_buff *
返回值
加載方式: ip route add
$ ip route add <route+prefix> encap bpf in obj <bpf obj file.o> section <ELF section> dev <device>
延伸閲讀
TODO
2 BPF_PROG_TYPE_LWT_OUT
使用場景
場景一:對出向流量做封裝(encap)
加載方式: ip route add
$ ip route add <route+prefix> encap bpf out obj <bpf object file.o> section <ELF section> dev <device>
程序簽名
傳入參數: struct __sk_buff *
觸發執行: lwtunnel_output()
3 BPF_PROG_TYPE_LWT_XMIT
使用場景
場景一:實現輕量級隧道發送端的 encap/redir 方法
Hook 位置: lwtunnel_xmit()
程序簽名
傳入參數: struct __sk_buff *
定義見。
加載方式: ip route add
$ ip route add <route+prefix> encap bpf xmit obj <bpf obj file.o> section <ELF section> dev <device>
- [譯] 為 K8s workload 引入的一些 BPF datapath 擴展(LPC, 2021)
- [譯] [論文] 可虛擬化第三代(計算機)架構的規範化條件(ACM, 1974)
- [譯] NAT 穿透是如何工作的:技術原理及企業級實踐(Tailscale, 2020)
- [譯] 寫給工程師:關於證書(certificate)和公鑰基礎設施(PKI)的一切(SmallStep, 2018)
- [譯] 基於角色的訪問控制(RBAC):演進歷史、設計理念及簡潔實現(Tailscale, 2021)
- [譯] Control Group v2(cgroupv2 權威指南)(KernelDoc, 2021)
- [譯] Linux Socket Filtering (LSF, aka BPF)(KernelDoc,2021)
- [譯] LLVM eBPF 彙編編程(2020)
- [譯] Cilium:BPF 和 XDP 參考指南(2021)
- BPF 進階筆記(三):BPF Map 內核實現
- BPF 進階筆記(二):BPF Map 類型詳解:使用場景、程序示例
- BPF 進階筆記(一):BPF 程序類型詳解:使用場景、函數簽名、執行位置及程序示例
- 源碼解析:K8s 創建 pod 時,背後發生了什麼(四)(2021)
- 源碼解析:K8s 創建 pod 時,背後發生了什麼(三)(2021)
- [譯] 邁向完全可編程 tc 分類器(NetdevConf,2016)
- [譯] 雲原生世界中的數據包標記(packet mark)(LPC, 2020)
- [譯] 利用 eBPF 支撐大規模 K8s Service (LPC, 2019)
- 計算規模驅動下的網絡方案演進
- 邁入 Cilium BGP 的雲原生網絡時代
- [譯] BeyondProd:雲原生安全的一種新方法(Google, 2019)