BPF関連のテストはこれまでトレーシングが主でしたが、ここでは実用的なものとして、XDP(eXpress Data Path)を使ったパケットフィルタリングを試してみました。
BPF開発のライブラリはいろいろな形がありますが、今回は、Go言語でコードを生成するタイプのものになります。
参考)
https://github.com/cilium/ebpf/tree/master/examples/xdp
環境)
Ubuntu 22.04
n@n10:~$ uname -r
5.15.0-67-genericn@n10:~$ grep BPF /boot/config-5.15.0-67-generic
CONFIG_BPF=y
CONFIG_HAVE_EBPF_JIT=y
CONFIG_ARCH_WANT_DEFAULT_BPF_JIT=y
# BPF subsystem
CONFIG_BPF_SYSCALL=y
CONFIG_BPF_JIT=y
CONFIG_BPF_JIT_ALWAYS_ON=y
CONFIG_BPF_JIT_DEFAULT_ON=y
CONFIG_BPF_UNPRIV_DEFAULT_OFF=y
# CONFIG_BPF_PRELOAD is not set
CONFIG_BPF_LSM=y
# end of BPF subsystem
CONFIG_CGROUP_BPF=y
CONFIG_IPV6_SEG6_BPF=y
CONFIG_NETFILTER_XT_MATCH_BPF=m
CONFIG_BPFILTER=y
CONFIG_BPFILTER_UMH=m
CONFIG_NET_CLS_BPF=m
CONFIG_NET_ACT_BPF=m
CONFIG_BPF_STREAM_PARSER=y
CONFIG_LWTUNNEL_BPF=y
CONFIG_BPF_EVENTS=y
CONFIG_BPF_KPROBE_OVERRIDE=y
CONFIG_TEST_BPF=mn@n10:~$ grep XDP /boot/config-5.15.0-67-generic
CONFIG_XDP_SOCKETS=y
CONFIG_XDP_SOCKETS_DIAG=m
CONFIG_SENSORS_XDPE122=mn@n10:~$ go version
go version go1.18.1 linux/amd64
xdp.c(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 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 |
//go:build ignore #include "bpf_endian.h" #include "common.h" char __license[] SEC("license") = "Dual MIT/GPL"; #define MAX_MAP_ENTRIES 16 /* Define an LRU hash map for storing packet count by source IPv4 address */ struct { __uint(type, BPF_MAP_TYPE_LRU_HASH); __uint(max_entries, MAX_MAP_ENTRIES); __type(key, __u32); // source IPv4 address __type(value, __u32); // packet count } xdp_stats_map SEC(".maps"); /* Attempt to parse the IPv4 source address from the packet. Returns 0 if there is no IPv4 header field; otherwise returns non-zero. */ static __always_inline int parse_ip_src_addr(struct xdp_md *ctx, __u32 *ip_src_addr) { void *data_end = (void *)(long)ctx->data_end; void *data = (void *)(long)ctx->data; // First, parse the ethernet header. struct ethhdr *eth = data; if ((void *)(eth + 1) > data_end) { return 0; } if (eth->h_proto != bpf_htons(ETH_P_IP)) { // The protocol is not IPv4, so we can't parse an IPv4 source address. return 0; } // Then parse the IP header. struct iphdr *ip = (void *)(eth + 1); if ((void *)(ip + 1) > data_end) { return 0; } // if (ip->protocol == 1) { // return 2; // } // Return the source IP address in network byte order. *ip_src_addr = (__u32)(ip->saddr); return 1; } SEC("xdp") int xdp_prog_func(struct xdp_md *ctx) { __u32 ip; int ret = parse_ip_src_addr(ctx, &ip); if (ret == 0) { // Not an IPv4 packet, so don't count it. goto done; } // if (ret == 2) { // bpf_printk("Drop Ping"); // return XDP_DROP; // } __u32 *pkt_count = bpf_map_lookup_elem(&xdp_stats_map, &ip); if (!pkt_count) { // No entry in the map for this IP address yet, so set the initial value to 1. __u32 init_pkt_count = 1; bpf_map_update_elem(&xdp_stats_map, &ip, &init_pkt_count, BPF_ANY); } else { // Entry already exists for this IP address, // so increment it atomically using an LLVM built-in. __sync_fetch_and_add(pkt_count, 1); if (ip == 0xe401a8c0) { bpf_printk("host %x", ip); return XDP_PASS; } else { bpf_printk("others %x", ip); } } done: // Try changing this to XDP_DROP and see what happens! // return XDP_PASS; return XDP_DROP; } |
ビルド
n@n10:~/ebpf/examples$ sudo make -C ../
[sudo] password for n:
make: Entering directory ‘/home/n/ebpf’
docker run –rm –user “1000:1000” \
-v “/home/n/ebpf”:/ebpf -w /ebpf –env MAKEFLAGS \
–env CFLAGS=”-fdebug-prefix-map=/ebpf=.” \
–env HOME=”/tmp” \
“ghcr.io/cilium/ebpf-builder:1666886595” \
make all
make: Entering directory ‘/ebpf’
find . -type f -name “*.c” | xargs clang-format -i
go generate ./…
go: downloading golang.org/x/sys v0.2.0
Compiled /ebpf/cmd/bpf2go/test/test_bpfel.o
Stripped /ebpf/cmd/bpf2go/test/test_bpfel.o
Wrote /ebpf/cmd/bpf2go/test/test_bpfel.go
Compiled /ebpf/cmd/bpf2go/test/test_bpfeb.o
Stripped /ebpf/cmd/bpf2go/test/test_bpfeb.o
….
Compiled /ebpf/examples/xdp/bpf_bpfel.o
Stripped /ebpf/examples/xdp/bpf_bpfel.o
Wrote /ebpf/examples/xdp/bpf_bpfel.go
Compiled /ebpf/examples/xdp/bpf_bpfeb.o
Stripped /ebpf/examples/xdp/bpf_bpfeb.o
Wrote /ebpf/examples/xdp/bpf_bpfeb.go
enum AdjRoomMode
enum AttachType
enum Cmd
enum FunctionId
….
attr ProgGetNextId
attr ProgLoad
attr ProgQuery
attr ProgRun
attr RawTracepointOpen
ln -srf testdata/loader-clang-14-el.elf testdata/loader-el.elf
ln -srf testdata/loader-clang-14-eb.elf testdata/loader-eb.elf
make: Leaving directory ‘/ebpf’
make: Leaving directory ‘/home/n/ebpf’
docker imageを使ってビルドしています。
xdp.cと同じフォルダにあるmain.goには、下記の記述があり、generatorにより、bpf_bpfeb.go, bpf_bpfel.goが生成されます。
// $BPF_CLANG and $BPF_CFLAGS are set by the Makefile.
//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -cc $BPF_CLANG -cflags $BPF_CFLAGS bpf xdp.c — -I../headers
ebpf/cmd/bpf2go/
https://github.com/cilium/ebpf/tree/master/cmd/bpf2go
このような手の込んだことをして、カーネルモジュールを動かすしくみを作っています。
実行
n@n10:~/ebpf/examples$ go run -exec sudo ./xdp enp0s3
2023/03/06 06:06:13 Attached XDP program to iface “enp0s3” (index 2)
2023/03/06 06:06:13 Press Ctrl-C to exit and remove the program
2023/03/06 06:06:14 Map contents:
192.168.1.228 => 2
2023/03/06 06:06:15 Map contents:
192.168.1.228 => 3
2023/03/06 06:06:16 Map contents:
192.168.1.228 => 4
….
テスト内容
192.168.1.228 (host)
192.168.1.217
上二つからxdpプログラムを実行している下記にcurlコマンドを実行します。
192.168.1.172
上記出力からはわかりにくいですが、xdpプログラムを実行しているとき、ホスト以外からはcurlコマンドのレスポンスがないことを確認しています。コードのコメントをはずすとpingパケロットのフィルタリングも確認できます。
ただテストをホストからのSSH接続していて接続が不安定になったりしました。フィルタリングしていないとはいえ、デフォルトDropのロジックは、弊害があるかもしれません。要調査です。
カーネルプログラムでは、コードをコメントしただけでエラーが発生するなど、通常のCプログラムではないようなことが起きました。(未定義という単純なものではなく、invalid indirect read from stack offなどといったものなど)
このあたり関連する部分を注意深くコーディングする必要があるようです。