BPF 進階筆記(一):BPF 程序類型詳解:使用場景、函數簽名、執行位置及程序示例

語言: CN / TW / HK

關於本文

內核目前支持 30 來種 BPF 程序類型。對於主要的程序類型,本文將介紹其:

  1. 使用場景 :適合用來做什麼?
  2. Hook 位置 :在 何處(where)、何時(when) 會觸發執行?例如在內核協議棧的哪個位置,或是什麼事件觸發執行。
  3. 程序簽名 (程序 入口函數 簽名)
    1. 傳入參數:調用到 BPF 程序時,傳給它的上下文(context,也就是函數參數)是什麼?
    2. 返回值:返回值類型、含義、合法列表等。
  4. 加載方式 :如何將程序附着(attach)到執行點?
  5. 程序示例 :一些實際例子。
  6. 延伸閲讀 :其他高級主題,例如相關的內核設計與實現。

本文主要參考:

  1. 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 個字節
  • 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)
  • 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)
  • 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

  1. err != 0 :直接 return err ,返回到調用方, 不再繼續原來正常的內核處理邏輯 __sock_queue_rcv_skb() ;所以效果就是: 將這個包過濾了出來 (符合過濾條件);
  2. err == 0 :接下來繼續執行正常的內核處理,也就是 這個包不符合過濾條件

所以至此大概就知道要實現過濾和截斷功能,程序應該返回什麼了。要精確搞清楚,需要 看 sk_filter() 一直調用到 BPF 程序的代碼,看中間是否對 BPF 程序的返回值做了封 裝和轉換。

這裏給出結論:BPF 程序的 返回值

  • nn < pkt_size ):返回一個 截斷的包 (副本),只保留前面 n 個字節。
  • 0忽略 這個包;

需要説明:

  1. 這裏所謂的截斷並不是截斷原始包,而只是複製一份包的元數據,修改其中的包長字段;
  2. 程序本身不會截斷或丟棄原始流量,也就是説,對 原始流量是隻讀的 (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 的優化,提升性能。例如,

  1. 監聽到被動建連(passive establishment of a connection)事件時,如果 對端和本機不在同一個網段 ,就 動態修改這個 socket 的 MTU 。 這樣能避免包因為太大而被中間路由器分片(fragmenting)。
  2. 替換目的 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 分為兩種類型:

  1. 通過 BPF 程序的返回值來動態修改配置 ,類型包括

    BPF_SOCK_OPS_TIMEOUT_INIT
    BPF_SOCK_OPS_RWND_INIT
    BPF_SOCK_OPS_NEEDS_ECN
    BPF_SOCK_OPS_BASE_RTT
    
  2. 在 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 類型的程序呢?

  1. BPF_PROG_TYPE_CGROUP_SOCK 類型的 BPF 程序:在一個連接(connection)的生命週期中 只執行一次
  2. 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)

延伸閲讀

  1. bpf: Adding support for sock_ops

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 框架:解析消息流

延伸閲讀

  1. 內核 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)

步驟:

  1. 為網絡設備添加分類器(classifier/qdisc):創建一個 “clsact” qdisc
  2. 為網絡設備添加過濾器(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

程序示例

  1. Firewalling with BPF/XDP: Examples and Deep Dive
  2. Cracking Kubernetes Node Proxy (aka kube-proxy)

延伸閲讀

  • 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),程序的返回結果只有兩種:

  1. 放行
  2. 禁止(導致隨後包被丟棄)

例如,很多 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()

  1. perf_event_open() ,帶一些採樣配置信息;
  2. ioctl(fd, PERF_EVENT_IOC_SET_BPF) 設置 BPF程序,
  3. 然後用 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>

« Firewalling with BPF/XDP: Examples and Deep Dive