https://bpf.plus/tcpdump
在上一篇文章中,我们介绍了 LPM Trie 。今天,我们将介绍对它的应用:实现一个类似 tcpdump 的小工具。
tcpdump 大家都不陌生,它也是 BPF 最早期的应用,参见 1992 年的那篇著名论文:The BSD Packet Filter: A New Architecture for User-level Packet Capture
BPF is now about two years old and has been put to work in several applications. The most widely used is tcpdump
https://www.tcpdump.org/papers/bpf-usenix93.pdf
在本文中,我们将实现以下过滤规则:
tcpdump tcp and host 220.181.38.148
即过滤出协议类型为 TCP 并且源地址或目的地址为 220.181.38.148 的网络包。
我们看看 tcpdump 是怎么处理的:
$ tcpdump tcp and host 220.181.38.148 -d
(000) ldh [12]
(001) jeq #0x86dd jt 10 jf 2
(002) jeq #0x800 jt 3 jf 10
(003) ldb [23]
(004) jeq #0x6 jt 5 jf 10
(005) ld [26]
(006) jeq #0xdcb52694 jt 9 jf 7
(007) ld [30]
(008) jeq #0xdcb52694 jt 9 jf 10
(009) ret #262144
(010) ret #0
这是一段非常简单的汇编代码,在解读它之前,我们需要了解一些背景知识。
首先,我们看到的网络包的视图是这样的(读过前面文章的读者应该知道,这和我们在 XDP 程序中看到的是一样的):
具体到 Linux 内核代码中,Ethernet Header 和 IP Header 是这样的:
struct ethhdr {
unsigned char h_dest[6];
unsigned char h_source[6];
__be16 h_proto;
};
struct iphdr {
__u8 ihl: 4;
__u8 version: 4;
__u8 tos;
__be16 tot_len;
__be16 id;
__be16 frag_off;
__u8 ttl;
__u8 protocol;
__sum16 check;
__be32 saddr;
__be32 daddr;
};
IPv4 和 IPv6 协议类型的数值为:
TCP 则为:
IPPROTO_TCP = 6
而 0xdcb52694 则等于 inet_pton(220.181.38.148)
有了上述背景知识后,我们便能得知上面的汇编代码的具体含义:
(000): 读取网络包中偏移量为 12 的两个字节,即 struct ethhdr 的 h_proto 字段
(001): 如果是 IPv6 (0x86DD),则丢弃(因为目标地址 220.181.38.148 是 IPv4 的)
(002): 如果是 IPv4 (0x800),则继续执行;否则,也丢弃
(003): 读取网络包中偏移量为 23 的一个字节,即 struct iphdr 的 protocol 字段
(004): 如果是 TCP (0x6),则继续执行;否则丢弃
(005): 读取网络包中偏移量为 26 的四个字节,即 struct iphdr 的 saddr 字段
(006): 如果是 220.181.38.148 (0xdcb52694),则是目标网络包,返回;否则继续执行
(007): 读取网络包中偏移量为 30 的四个字节,即 struct iphdr 的 daddr 字段
(008): 如果是 220.181.38.148 (0xdcb52694),则是目标网络包,返回;否则丢弃
(009): 返回网络包
(010): 丢弃网络包
当然,我们也可以用 C 语言来实现它:
$ tcpdump tcp and host 220.181.38.148 -dd
{ 0x28, 0, 0, 0x0000000c },
{ 0x15, 8, 0, 0x000086dd },
{ 0x15, 0, 7, 0x00000800 },
{ 0x30, 0, 0, 0x00000017 },
{ 0x15, 0, 5, 0x00000006 },
{ 0x20, 0, 0, 0x0000001a },
{ 0x15, 2, 0, 0xdcb52694 },
{ 0x20, 0, 0, 0x0000001e },
{ 0x15, 0, 1, 0xdcb52694 },
{ 0x6, 0, 0, 0x00040000 },
{ 0x6, 0, 0, 0x00000000 },
每一条指令对应的是结构体:
struct sock_filter { /* Filter block */
__u16 code; /* Actual filter code */
__u8 jt; /* Jump true */
__u8 jf; /* Jump false */
__u32 k; /* Generic multiuse field */
};
感兴趣的读者可以自行尝试。
下面让我们来用 LPM Trie 和 BPF 程序来实现它:
定义一个 LPM Trie ,用来存放要过滤的目标地址
struct {
__uint(type, BPF_MAP_TYPE_LPM_TRIE);
__uint(max_entries, 1);
__type(key, struct cidr);
__type(value, int);
__uint(map_flags, BPF_F_NO_PREALLOC);
} target SEC(".maps");
在 BPF 程序中执行过滤:
/* IPv4 */
bpf_skb_load_bytes(skb, ETH_PROTO_OFF, ð_proto, sizeof(eth_proto));
if (eth_proto != bpf_htons(ETH_P_IP))
return 0;
/* TCP */
bpf_skb_load_bytes(skb, IP_PROTO_OFF, &ip_proto, sizeof(ip_proto));
if (ip_proto != IPPROTO_TCP)
return 0;
/* do filter */
cidr.prefix_len = network_prefix_bits;
bpf_skb_load_bytes(skb, IP_DADDR_OFF, &cidr.ip, sizeof(cidr.ip));
if (bpf_map_lookup_elem(&target, &cidr))
return skb->len;
cidr.prefix_len = network_prefix_bits;
bpf_skb_load_bytes(skb, IP_SADDR_OFF, &cidr.ip, sizeof(cidr.ip));
if (bpf_map_lookup_elem(&target, &cidr))
return skb->len;
核心代码就是这 20 行,我们可以看到,它和上面 tcpdump 中的汇编代码是对应的。
看看效果,访问 220.181.38.148 三次握手的结果:
代码
头文件
// SPDX-License-Identifier: GPL-2.0
/* Copyright (c) 2022 Hengqi Chen */
struct cidr {
__u32 prefix_len;
__u32 ip;
};
BPF 程序代码