在看 iptables-nfqueue 源代码的时候,发现 iptables 有 bpf 特性,于是查了下 iptables-bpf 的资料。

iptables-bpf 资料甚少,bpf 资料 ebpf.ioebpf.top。看了 dog250 大神的玩法,心疼大神一两秒,扣 bpf 字节码不是小弟能够企及的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
bpf

Match using Linux Socket Filter. Expects a path to an eBPF object or a cBPF program in decimal format.

--object-pinned path
		Pass a path to a pinned eBPF object.

Applications load eBPF programs into the kernel with the bpf() system call and BPF_PROG_LOAD command and can pin them in a virtual filesystem with BPF_OBJ_PIN. To use a pinned object in iptables, mount the bpf filesystem using

		mount -t bpf bpffs ${BPF_MOUNT}

then insert the filter in iptables by path:

		iptables -A OUTPUT -m bpf --object-pinned ${BPF_MOUNT}/{PINNED_PATH} -j ACCEPT

这才是我想要的。编写一个 socket bpf 程序才是我在行的。

源代码:github.com/Asphaltt/iptables-bpf

bpf 程序怎么能少了 bpf map

不能将 IP 地址写死在 bpf 程序里,那就将 IP 地址放到 bpf map 里。源代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
struct {
	__uint(type, BPF_MAP_TYPE_HASH);
	__uint(max_entries, 16);
	__type(key, u32);
	__type(value, u8);
} filter_daddrs SEC(".maps");

SEC("socket")
int filter_iptables(void *skb) {
	struct iphdr iph;
	u8 *filtered;

	if (bpf_skb_load_bytes_relative(skb, 0, &iph, sizeof(iph), BPF_HDR_START_NET) < 0)
		return BPF_OK;

	filtered = bpf_map_lookup_elem(&filter_daddrs, &iph.daddr);
	if (filtered != NULL && *filtered == 1)
		return BPF_DROP;

	return BPF_OK;
}

iptables -I OUTPUT -m bpf --object-pinned $(EBPF_PINNED) -j DROP 里使用这个 bpf 程序,将 bpf map 里匹配到的目的地址的网络包都 drop 掉。效果如下:

 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
# ping -c4 223.5.5.5
PING 223.5.5.5 (223.5.5.5) 56(84) bytes of data.
64 bytes from 223.5.5.5: icmp_seq=1 ttl=63 time=167 ms
64 bytes from 223.5.5.5: icmp_seq=2 ttl=63 time=159 ms
64 bytes from 223.5.5.5: icmp_seq=3 ttl=63 time=1047 ms

--- 223.5.5.5 ping statistics ---
4 packets transmitted, 3 received, 25% packet loss, time 3081ms
rtt min/avg/max/mdev = 158.744/457.539/1047.019/416.838 ms, pipe 2

# make
clang -I./bpf/headers -O2 -g -target bpf -c bpf/iptables-bpf.c  -o iptables-bpf.elf
go build -v -o iptables-bpf main.go

# make setup
bpftool prog load iptables-bpf.elf /sys/fs/bpf/iptbpf
iptables -I OUTPUT -m bpf --object-pinned /sys/fs/bpf/iptbpf -j DROP

# make mapid
986

#./iptables-bpf -m 986 -d 223.5.5.5
2021/12/16 15:18:49 223.5.5.5 can't be pinged

# ping -c4 223.5.5.5
PING 223.5.5.5 (223.5.5.5) 56(84) bytes of data.

--- 223.5.5.5 ping statistics ---
4 packets transmitted, 0 received, 100% packet loss, time 3065ms

# make clean
rm -f iptables-bpf.elf
rm -f iptables-bpf
iptables -D OUTPUT -m bpf --object-pinned /sys/fs/bpf/iptbpf -j DROP
rm -f /sys/fs/bpf/iptbpf

# ping -c4 223.5.5.5
PING 223.5.5.5 (223.5.5.5) 56(84) bytes of data.
64 bytes from 223.5.5.5: icmp_seq=1 ttl=63 time=139 ms
64 bytes from 223.5.5.5: icmp_seq=2 ttl=63 time=157 ms
64 bytes from 223.5.5.5: icmp_seq=3 ttl=63 time=169 ms
64 bytes from 223.5.5.5: icmp_seq=4 ttl=63 time=121 ms

--- 223.5.5.5 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3087ms
rtt min/avg/max/mdev = 120.720/146.495/168.600/18.163 ms

管理 bpf map

困难山头已经跨过,剩下个小山头。

使用 bpftool 工具人肉来管理 bpf map?不可能的,用 Go 来管理吧。

 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
func main() {

	var daddr string
	var bpfMap int
	flag.StringVar(&daddr, "d", "", "ip addresses to drop, separated by ','")
	flag.IntVar(&bpfMap, "m", 0, "the id of the bpf map(filter_daddrs)")
	flag.Parse()

	var ips []netaddr.IP
	addrs := strings.FieldsFunc(daddr, func(r rune) bool { return r == ',' })
	for _, addr := range addrs {
		ip, err := netaddr.ParseIP(addr)
		if err != nil {
			log.Fatalf("%s is not a valid IPv4 address", ip)
		}

		ips = append(ips, ip)
	}
	if len(ips) == 0 {
		log.Fatalf("no ip address(es) to be dropped")
	}

	m, err := ebpf.NewMapFromID(ebpf.MapID(bpfMap))
	if err != nil {
		log.Fatalf("bpf map(%d) not found, err: %v", bpfMap, err)
	}

	val := uint8(1)
	for _, ip := range ips {
		_ip := ip.As4()
		ipval := binary.LittleEndian.Uint32(_ip[:])
		if err := m.Update(ipval, val, ebpf.UpdateAny); err != nil {
			log.Fatalf("failed to upsert data to bpf map(%d), err: %v", bpfMap, err)
		}
	}

	log.Printf("%s can't be pinged", daddr)
}

使用 Go 来管理 bpf map,将 iptables-bpf 的易用性提升了一个台阶。相比于单纯的 iptables 规则,iptables-bpf 给 iptables 带来了无与伦比的可编程性。

实验环境

1
2
3
4
5
6
7
8
9
# lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 21.04
Release:        21.04
Codename:       hirsute

# uname -a
Linux pagani 5.11.0-31-generic #33-Ubuntu SMP Wed Aug 11 13:19:04 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux

iptables

有的 iptables 默认带有 bpf 特性,有的没带。如果没带,则在执行 iptables -I OUTPUT -m bpf --object-pinned /sys/fs/bpf/iptbpf -j DROP 的时候,提示 iptables v1.6.1: No bpf header, kernel headers too old?。需要重新编译 iptables。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
git clone git://git.netfilter.org/iptables.git
cd iptables
bash autogen.sh
apt install -y libpcap-dev # bpf 依赖 libpcap
./configure --enable-bpf-compiler --disable-nftables # disable nftables 是为了快速安装一个能用 bpf 的 iptables
# 留意结果
Iptables Configuration:
  ...
  BPF utils support:                    yes

make -j4
make install
# 此时 iptables 已开启 bpf 特性

总结

bpf 带给了 iptables 不少想象力,只是目前还没有释放出来。

如果使用 iptables-bpf 实现 iptables 的匹配能力,该如何在 bpf 程序里实现一个高性能的匹配算法呢?