设备层抓包?不就是 tcpdump 嘛。缺点是,-i any 时无法抓到各个 netns 内的网络包。

netif_receive_skbnet_dev_queue 这 2 个 tracepoint 呢?好,能抓到所有 netns 内的网络包了。但, tracepoint bpf prog 不支持 bpf_skb_output helper,就无法将网络包传到用户态保存到 pcap 文件了。

bpf_skb_output helper 只能用在 BPF_PROG_TYPE_TRACING 类型的 bpf prog 上,因此 kprobe 也不行。

fentry 的 bpf prog 类型就是 BPF_PROG_TYPE_TRACING,而且 __dev_queue_xmit() 函数的第一个参数就是 struct sk_buff *skb;但是, __netif_receive_skb_core() 函数的第一个参数是 struct sk_buff **pskb,这就无法使用 fentry 了。

经过一番研究,tp_btf bpf prog 的类型是 BPF_PROG_TYPE_TRACING,如此便能使用上述的那两个 tracepoint 了;尽管要求 v5.7+ 内核。

设备层抓包的需求

在原有的对 XDP 和 tc-bpf 抓包的基础上,实现设备层抓包的需求。

即,该抓包工具在设备层能够:

  1. 使用 pcap-filter(7) 语法过滤网络包。
  2. 将网络包通过 bpf_skb_output helper 传到用户态,保存到 pcap 文件。
  3. 无视 netns 隔离,抓到所有 netns 内的网络包。

设备层抓包之 tcpdump

tcpdump -i any 抓当前 netns 所有网络设备的网络包。

其工作在:

tcpdump on netdev layer

更多实现细节,请参考 用户态 tcpdump 如何实现抓到内核网络包的?

设备层抓包之 tracepoint

先看一下网络 tracepoint

1
2
3
4
# ls /sys/kernel/debug/tracing/events/net/
enable                napi_gro_frags_exit     net_dev_queue       net_dev_xmit_timeout     netif_receive_skb_exit        netif_rx
filter                napi_gro_receive_entry  net_dev_start_xmit  netif_receive_skb        netif_receive_skb_list_entry  netif_rx_entry
napi_gro_frags_entry  napi_gro_receive_exit   net_dev_xmit        netif_receive_skb_entry  netif_receive_skb_list_exit   netif_rx_exit

关注其中关键的 2 个 tracepoint, netif_receive_skbnet_dev_queue

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// ${KERNEL}/net/core/dev.c

static int __netif_receive_skb_core(struct sk_buff **pskb, bool pfmemalloc,
                                    struct packet_type **ppt_prev)
{
    // ...

    trace_netif_receive_skb(skb);

    // ...

    if (static_branch_unlikely(&generic_xdp_needed_key)) {
        // ...
        ret2 = do_xdp_generic(rcu_dereference(skb->dev->xdp_prog),
                              &skb);
        // ...
    }

    if (eth_type_vlan(skb->protocol)) {
        skb = skb_vlan_untag(skb);
        // ...
    }

    // ...

    list_for_each_entry_rcu(ptype, &skb->dev->ptype_all, list) {
        if (pt_prev)
            ret = deliver_skb(skb, pt_prev, orig_dev);
        pt_prev = ptype;
    }

    // ...
    if (static_branch_unlikely(&ingress_needed_key)) {
        // ...
        skb = sch_handle_ingress(skb, &pt_prev, &ret, orig_dev,
                                 &another);
        // ...
    }

    // ...
}

int __dev_queue_xmit(struct sk_buff *skb, struct net_device *sb_dev)
{
    // ...
    if (static_branch_unlikely(&egress_needed_key)) {
        // ...
        skb = sch_handle_egress(skb, &rc, dev);
        // ...
    }
    // ...

    trace_net_dev_queue(skb);
    if (q->enqueue) {
        rc = __dev_xmit_skb(skb, q, dev, txq);
        goto out;
    }

    // ...
    skb = dev_hard_start_xmit(skb, dev, txq, &rc);
    // ...
}

因为 __netif_receive_skb_core() 函数和 __dev_queue_xmit() 函数不会被跳过,所以 netif_receive_skbnet_dev_queue 两个 tracepoint 可以用来 trace 网络包。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
struct netif_receive_skb_args {
    __u64 unused;

    void * skbaddr;
    unsigned int len;
};

SEC("tp/net/netif_receive_skb")
int tp_netif_receive_skb(struct netif_receive_skb_args *args)
{
    struct sk_buff *skb = (typeof(skb)) args->skbaddr;

    // Deal with skb ...
}

struct net_dev_queue_args {
    __u64 unused;

    void * skbaddr;
    unsigned int len;
};

SEC("tp/net/net_dev_queue")
int tp_net_dev_queue(struct net_dev_queue_args *args)
{
    struct sk_buff *skb = (typeof(skb)) args->skbaddr;

    // Deal with skb ...
}

正如前面所言,BPF_PROG_TYPE_TRACEPOINT 类型的 bpf prog 无法使用 bpf_skb_output helper,因此无法将网络包传到用户态保存到 pcap 文件。

设备层抓包之 kprobe

类似 tracepoint 的实现方式,使用 kprobe 也能用来 trace 网络包。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
SEC("kprobe/__netif_receive_skb_core")
int BPF_KPROBE(kprobe___netif_receive_skb_core, struct sk_buff **pskb)
{
    struct sk_buff *skb;

    bpf_probe_read_kernel(&skb, sizeof(skb), pskb);

    // Deal with skb ...
}

SEC("kprobe/__dev_queue_xmit")
int BPF_KPROBE(kprobe___dev_queue_xmit, struct sk_buff *skb, struct net_device *sb_dev)
{
    // Deal with skb ...
}

BPF_PROG_TYPE_KPROBE 类型的 bpf prog 也无法使用 bpf_skb_output helper,因此也无法将网络包传到用户态保存到 pcap 文件。

而且,__netif_receive_skb_core() 函数并不一定能够被 kprobe 到,因为它大概率被内联了;比如 Ubuntu 24.04 6.8.0-35-generic 内核。

设备层抓包之 fentry

类似 kprobe 的实现方式,使用 fentry 也能用来 trace 网络包。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
SEC("fentry/__netif_receive_skb_core")
int BPF_PROG(fentry___netif_receive_skb_core, struct sk_buff **pskb)
{
    struct sk_buff *skb;

    bpf_probe_read_kernel(&skb, sizeof(skb), pskb);

    // Deal with skb ...
}

SEC("fentry/__dev_queue_xmit")
int BPF_PROG(fentry___dev_queue_xmit, struct sk_buff *skb, struct net_device *sb_dev)
{
    // Deal with skb ...
}

使用 fentry trace __netif_receive_skb_core() 函数的另一个问题是:其第一个参数是 struct sk_buff **pskb,而不是 struct sk_buff *skb,导致不能使用 bpf_skb_output helper。

如果另寻函数,如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// ${KERNEL}/net/core/dev.c

int netif_receive_skb_core(struct sk_buff *skb)
{
    int ret;

    rcu_read_lock();
    ret = __netif_receive_skb_one_core(skb, false);
    rcu_read_unlock();

    return ret;
}
EXPORT_SYMBOL(netif_receive_skb_core);

则存在个问题:收包时可能不经过 netif_receive_skb_core() 函数,而是 netif_receive_skb_list() 函数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// ${KERNEL}/net/core/dev.c

void netif_receive_skb_list(struct list_head *head)
{
    struct sk_buff *skb;

    if (list_empty(head))
        return;
    if (trace_netif_receive_skb_list_entry_enabled()) {
        list_for_each_entry(skb, head, list)
            trace_netif_receive_skb_list_entry(skb);
    }
    netif_receive_skb_list_internal(head);
    trace_netif_receive_skb_list_exit(0);
}
EXPORT_SYMBOL(netif_receive_skb_list);

分析至此,tracepoint, kprobefentry 都无法满足设备层抓包的需求。

当了解到 tp_btf bpf prog 的类型是 BPF_PROG_TYPE_TRACING,便将目光转向了它。

1
2
3
4
// https://github.com/cilium/ebpf/blob/fd1fe1b9ac2532d2aec6cc70e05e24e305f78f19/elf_sections.go#L45C1-L46C80

    {"tp_btf+", sys.BPF_PROG_TYPE_TRACING, sys.BPF_TRACE_RAW_TP, _SEC_ATTACH_BTF},
    {"fentry+", sys.BPF_PROG_TYPE_TRACING, sys.BPF_TRACE_FENTRY, _SEC_ATTACH_BTF},

设备层抓包之 tp_btf

类似 tracepoint 的实现方式,使用 tp_btf 来 trace 网络包。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
struct {
    __uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
} events SEC(".maps");

SEC("tp_btf/netif_receive_skb")
int BPF_PROG(tp_btf_netif_receive_skb, struct sk_buff *skb)
{
    struct ethhdr *eth = (typeof(eth)) (BPF_CORE_READ(skb, head) +
                                        BPF_CORE_READ(skb, mac_header));
    struct iphdr *ip = (typeof(ip)) (eth + 1);
    struct udphdr *udp = (typeof(udp)) (ip + 1);
    __u64 skbaddr = (unsigned long)skb;
    __u64 flags;

    if (BPF_CORE_READ(eth, h_proto) == bpf_htons(ETH_P_IP) &&
        BPF_CORE_READ(ip, protocol) == IPPROTO_ICMP)
        bpf_printk("tp_btf_netif_receive_skb: %d\n", BPF_CORE_READ(ip, protocol));

    flags = ((__u64) (14 + 20 + 8)) << 32 | BPF_F_CURRENT_CPU;
    if (BPF_CORE_READ(eth, h_proto) == bpf_htons(ETH_P_IP) &&
        BPF_CORE_READ(ip, protocol) == IPPROTO_UDP && BPF_CORE_READ(udp, dest) == bpf_htons(65535))
        bpf_skb_output(skb, &events, flags, &skbaddr, sizeof(skbaddr));

    return BPF_OK;
}

同时,能够使用 bpf_skb_output helper 将网络包传到用户态保存到 pcap 文件。

该 demo 的源代码:learn-by-example tp_btf

总结

tracepoint, kprobefentry 都无法满足设备层抓包的需求:

  1. tracepoint bpf prog 类型是 BPF_PROG_TYPE_TRACEPOINT,无法使用 bpf_skb_output helper。
  2. kprobe bpf prog 类型是 BPF_PROG_TYPE_KPROBE,也无法使用 bpf_skb_output helper。
  3. fentry bpf prog 类型是 BPF_PROG_TYPE_TRACING,可以使用 bpf_skb_output helper,但 __netif_receive_skb_core() 函数可能被内联。

tp_btf bpf prog 类型是 BPF_PROG_TYPE_TRACING,能够使用 bpf_skb_output helper,因此能够在 netif_receive_skbnet_dev_queue 两个 tracepoint 上 trace 网络包。