设备层抓包?不就是 tcpdump
嘛。缺点是,-i any
时无法抓到各个 netns 内的网络包。
netif_receive_skb
和 net_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 抓包的基础上,实现设备层抓包的需求。
即,该抓包工具在设备层能够:
- 使用 pcap-filter(7) 语法过滤网络包。
- 将网络包通过
bpf_skb_output
helper 传到用户态,保存到 pcap 文件。
- 无视 netns 隔离,抓到所有 netns 内的网络包。
设备层抓包之 tcpdump
tcpdump -i any
抓当前 netns 所有网络设备的网络包。
其工作在:

更多实现细节,请参考 用户态 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_skb
和 net_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_skb
和 net_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
, kprobe
和 fentry
都无法满足设备层抓包的需求。
当了解到 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
, kprobe
和 fentry
都无法满足设备层抓包的需求:
tracepoint
bpf prog 类型是 BPF_PROG_TYPE_TRACEPOINT
,无法使用 bpf_skb_output
helper。
kprobe
bpf prog 类型是 BPF_PROG_TYPE_KPROBE
,也无法使用 bpf_skb_output
helper。
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_skb
和 net_dev_queue
两个 tracepoint
上 trace 网络包。