eBPF
提问:以下哪个内核可以运行 ebpf?
- ZStack C74
- ZStack C76
- CentOS 7.7
- ZStack experimental repo 中的内核
(分别对应了 3.10.0-693 3.10.0-957 3.10.0-1062 4.18.0-240)
答案:有三个内核可以支持
C74 是不支持的,C76 有有限的支持,C77 支持程度更好一点,experimental kernel 里是 4.18.0,对 ebpf 支持已经很完善(当然了,ebpf 还处于快速发展阶段,因此还是版本越高越完善)
检查自己安装的内核:
grubby --info=ALL
在 ZStack 管理的计算节点上安装来自 experimental 的 4.18 内核:
yum --disablerepo=\* --enablerepo=zstack-experimental-mn install kernel-4.18.0
在 ZStack 管理节点上安装来自 experimental 的 4.18 内核:
yum --disablerepo=\* --enablerepo=zstack-experimental-mn install kernel-4.18.0
查看内核编译选项
[root@dev1-4 ~]# cat /boot/config-3.10.0-1062.18.1.el7.x86\_64 | grep -i bpf
CONFIG\_BPF=y
CONFIG\_BPF\_SYSCALL=y
CONFIG\_BPF\_JIT\_ALWAYS\_ON=y
CONFIG\_NETFILTER\_XT\_MATCH\_BPF=m
CONFIG\_NET\_CLS\_BPF=m
CONFIG\_BPF\_JIT=y
CONFIG\_HAVE\_EBPF\_JIT=y
CONFIG\_BPF\_EVENTS=y
CONFIG\_BPF\_KPROBE\_OVERRIDE=y
安装 bcc,跑一个 ebpf demo
参考:https://github.com/iovisor/bcc/blob/master/INSTALL.md#rhel—binary
yum install bcc-tools
下载 bcc 源码:
git clone <https://github.com/iovisor/bcc.git>
运行 hello world(跟踪系统调用 sys_clone):
python examples/hello\_world.py
在 hello world 上稍作修改,跟踪 ipt_do_table 调用
修改 hello world,改成调用 ipt_do_table(进入 iptables 的时候)的时候打印 hello world
[root@dev1-4 bcc]# cat examples/hello\_world.1.py
#!/usr/bin/python
from bcc import BPF
BPF(text='int kprobe\_\_ipt\_do\_table(void \*ctx) { bpf\_trace\_printk("Hello, World!\\n"); return 0; }').trace\_print()
此时会发现脚本会不断打印 hello world
下面将 iptables 模块移除:
# **保存一下之前的 iptables 内容:**
[root@dev1-4 ~]# iptables-save > ww.1.ipt
# **准备一个空的 iptables 配置**
[root@dev1-4 ~]# cat ww.empty.ipt
# Generated by iptables-save v1.4.21 on Thu Sep 23 10:24:35 2021
\*raw
:PREROUTING ACCEPT [0:0]
:OUTPUT ACCEPT [69467:48787226]
COMMIT
# Completed on Thu Sep 23 10:24:35 2021
# Generated by iptables-save v1.4.21 on Thu Sep 23 10:24:35 2021
\*filter
:INPUT ACCEPT [81:6753]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [69467:48787226]
COMMIT
# Completed on Thu Sep 23 10:24:35 2021
# Generated by iptables-save v1.4.21 on Thu Sep 23 10:24:35 2021
\*mangle
:PREROUTING ACCEPT [76482:29150585]
:INPUT ACCEPT [73927:27411897]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [69467:48787226]
:POSTROUTING ACCEPT [69467:48787226]
COMMIT
# Completed on Thu Sep 23 10:24:35 2021
# **停掉 zsn,将空配置导入**
[root@dev1-4 ~]# systemctl stop zstack-network-agent
[root@dev1-4 ~]# cat ww.empty.ipt | iptables-restore
[root@dev1-4 ~]# lsmod | grep ipt
ipt\_REJECT 16384 0
nf\_reject\_ipv4 16384 1 ipt\_REJECT
iptable\_raw 16384 0
iptable\_filter 16384 0
iptable\_mangle 16384 0
ip\_tables 28672 3 iptable\_filter,iptable\_raw,iptable\_mangle`
# **卸载模块**
[root@dev1-4 ~]# modprobe -r iptable\_raw
[root@dev1-4 ~]# modprobe -r iptable\_filter`
[root@dev1-4 ~]# modprobe -r iptable\_mangle
[root@dev1-4 ~]# modprobe -r ipt\_REJECT
[root@dev1-4 ~]# lsmod | grep ipt
[root@dev1-4 ~]#
发现不会再打印任何 hello world
类似的,可以跟踪其他函数,例如网络路径中的 iprcv、br_nf_forward 等等
复杂的例子:skbtracer
首先下载代码(原来的代码有一些问题,例如没有 KBUILD_MODNAME,dropstack 存在 bug 无法使用等,我做了一些修复)
git clone [https://github.com/MatheMatrix/skbtracer](https://github.com/MatheMatrix/skbtracer/blob/main/skbtracer.c)
可以简单看下命令介绍
python skbtracer.py --help
可以从 10.0.54.172 发个包文看下收包的路径,skbtracer 可以指定协议、IP 等信息做过滤
python skbtracer.py --proto=icmp -H 10.0.54.172
因为我的这个环境里,地址配在了网桥上,我们可以看下地址配置网卡的收包过程
ip link add link eth0 name eth0.999 type vlan id 999
ip link set dev eth0.999 up
ip a add 192.168.99.14/24 dev eth0.999
此外,我们可以在发包的虚拟机配一个路由,例如
ip r add 192.168.100.10 via 192.168.99.14
然后往这个假的 192.168.100.10 地址发包,让 192.168.99.14 起到一个路由的作用
skbtracer 能否同时 trace 函数和 netfilter?
此外,可以把 iptable 的 trace 打开:
如果我通过 iptables INPUT 将报文 drop,可以看到 request 最后一步会显示 INPUT drop:
如果我用 tc 来 drop 报文呢?
首先是准备工作
yum --enablerepo=zstack-experimental-mn install kernel-modules-extra
tc qdisc add dev eth0.999 root netem loss 50%
然后发四个报文,其中 seq=2 被丢掉了
查看 trace,发现相比正常 trace 痕迹,这里少了一个 __dev_queue_xmit 把报文从 eth0.999 送到 eth0 的过程,进而也少了 eth0 packet_rcv 的过程,因此可以确认是 eth0.999 把包丢掉了
但如果是在 eth0 上丢包呢?
[root@dev1-4 opensource]# tc qdisc add dev eth0 root netem loss 33%
[root@dev1-4 opensource]#
[root@dev1-4 opensource]# ping 10.0.54.172 -c 4
PING 10.0.54.172 (10.0.54.172) 56(84) bytes of data.
64 bytes from 10.0.54.172: icmp\_seq=1 ttl=64 time=0.346 ms
64 bytes from 10.0.54.172: icmp\_seq=2 ttl=64 time=0.445 ms
64 bytes from 10.0.54.172: icmp\_seq=4 ttl=64 time=0.470 ms
--- 10.0.54.172 ping statistics ---
4 packets transmitted, 3 received, 25% packet loss, time 3052ms
rtt min/avg/max/mdev = 0.346/0.420/0.470/0.056 ms
发送四个报文,seq=3被丢弃了,但是从 trace 上看确实发出去了,没有收到回包,说明我们的 trace 还不够细致,需要改造 skbtracer
如何改造 skbtracer 追踪 tc?
通过对 tc 的原理进行分析可以知道 tc 本质上是在 qdisc 上做文章,通过 perf 可以查到 qdisc 也有相应的 tracepoint,因此我们可以通过在 skbtracer 增加 qdisc 的 tracepoint 来达到目的
[root@dev1-4 skbtracer]# git diff
diff --git a/skbtracer.c b/skbtracer.c
index e62b542..e49c613 100644
--- a/skbtracer.c
+++ b/skbtracer.c
@@ -1,3 +1,5 @@
+#define KBUILD\_MODNAME "skbtracer"
+
#include <bcc/proto.h>
#include <uapi/linux/ip.h>
#include <uapi/linux/ipv6.h>
**@@ -589,6 +591,10 @@ int kprobe\_\_ip\_finish\_output(struct pt\_regs \*ctx, struct net \*net, struct sock \***
**return do\_trace(ctx, skb, \_\_func\_\_+8, NULL);**
**}**
**+TRACEPOINT\_PROBE(qdisc, qdisc\_dequeue) {**
**+ return do\_trace(args, (struct sk\_buff\*)args->skbaddr, \_\_func\_\_, NULL);**
**+}**
**+**
**#endif**
#if \_\_BCC\_iptable
@@ -671,7 +677,7 @@ int kprobe\_\_\_\_kfree\_skb(struct pt\_regs \*ctx, struct sk\_buff \*skb)
event.start\_ns = bpf\_ktime\_get\_ns();
bpf\_strncpy(event.func\_name, \_\_func\_\_+8, FUNCNAME\_MAX\_LEN);
get\_stack(ctx, &event);
- route\_event.perf\_submit(ctx, event, sizeof(\*event));
+ route\_event.perf\_submit(ctx, &event, sizeof(event));
return 0;
}
#endif
为了方便,并没有做什么开关之类的,而是直接加到了 trace 里,让我们来再试一下
[root@dev1-4 opensource]# ping 10.0.54.172 -c 4
PING 10.0.54.172 (10.0.54.172) 56(84) bytes of data.
64 bytes from 10.0.54.172: icmp\_seq=1 ttl=64 time=0.757 ms
64 bytes from 10.0.54.172: icmp\_seq=2 ttl=64 time=0.455 ms
64 bytes from 10.0.54.172: icmp\_seq=3 ttl=64 time=0.388 ms
--- 10.0.54.172 ping statistics ---
4 packets transmitted, 3 received, 25% packet loss, time 3098ms
rtt min/avg/max/mdev = 0.388/0.533/0.757/0.161 ms
这里就很容易看到了,之前的报文都会经过 tracepoint__qdisc__qdisc_dequeue,而最后一个报文却没有到达 tracepoint__qdisc__qdisc_dequeue,也就是在 qdisc dequeue 之前被丢弃了,因此我们的排查方向就可以往 qdisc 的方向排查
对于更新的 kernel 版本,还提供了 qdisc_enqueue 的 tracepoint,就可以更明确的展示出是不是 qdisc 这块搞鬼了
此外,如果你已经逐步的缩小了范围,那么可以通过检查 /proc/kallsyms 里的导出函数或者 perf list 中的 tracepoint 进一步 trace,缩小调查范围
思考题
目前 skbtracer 只做了 ip 层以下大部分和 iptables,如果想要跟踪 ebtables 该怎么改造?
拓展文章
iptables trace target
简介
iptables 有海量的现成的 target extension,参考:https://ipset.netfilter.org/iptables-extensions.man.html 有很多 extension 其实很小众但很有意思,过去的网络(设备)公司很多报文的修改等花活都是在 netfilter 这个框架下加 extension 实现的,当然随着设备公司转向用户态协议栈等方案,netfilter 插件技术也逐渐成文屠龙技了,但还是有很多很实用的可以多探索,例如 trace。
modprobe nf\_log\_ipv4
sysctl -w net.netfilter.nf\_log.2=nf\_log\_ipv4
iptables -t raw -I PREROUTING -p icmp -j TRACE # 根据需求修改过滤条件,这里是针对收包的,maybe 你需要针对入包的
思考题
还能结合什么其他 target extension 来实现 跟踪报文在 iptables 的“流转”过程吗?
dropwatch
简介
dropwatch 通过跟踪 skb_free 来跟踪丢包,不过功能相对有限,在高版本内核中,dropwatch 完全没有 ebpf 来的灵活好用,但好处是 3.10.X 内核你就可以通过 dropwatch 简单排查,有一个小 tip 是利用大量发报文定位到对应的 dropwatch。例如 iptables 丢包:
iptables -I INPUT -s 10.0.54.172 -j DROP
可以看到是 nf_hook_slow,其实根据函数名就能大致判断是 iptables 丢包,当然也可以进入到对应版本内核代码查看
用 perf 取代 dropwatch 获取更详细的信息
前面有提到 dropwatch 本质上是 trace skb_free 的函数,那么其实无论是 systemtap、ebpf、perf 等各种 trace 工具都可以做到类似的事情,下面我们演示用 perf 获取发生 skb free 的更详细的调用栈
[root@dev1-4 skbtracer]# perf record -g -a -e skb:kfree_skb
Lowering default frequency rate to 2000.
Please consider tweaking /proc/sys/kernel/perf_event_max_sample_rate.
^C[ perf record: Woken up 1 times to write data ]
[ perf record: Captured and wrote 0.404 MB perf.data (453 samples) ]
[root@dev1-4 skbtracer]# perf script
swapper 0 [000] 100201.435009: skb:kfree_skb: skbaddr=0xffff9b364e848b00 protocol=2048 location=0xffffffffb75ae9f7
ffffffffb7524bd3 kfree_skb+0x73 ([kernel.kallsyms])
ffffffffb7524bd3 kfree_skb+0x73 ([kernel.kallsyms])
ffffffffb75ae9f7 nf_hook_slow+0xa7 ([kernel.kallsyms])
ffffffffb75bb29c ip_local_deliver+0xcc ([kernel.kallsyms])
ffffffffc0b98191 ip_sabotage_in+0x41 ([kernel.kallsyms])
ffffffffb75ae994 nf_hook_slow+0x44 ([kernel.kallsyms])
ffffffffb75bb5bc ip_rcv+0x30c ([kernel.kallsyms])
ffffffffb753ebf7 __netif_receive_skb_core+0xb27 ([kernel.kallsyms])
ffffffffb753ed8d netif_receive_skb_internal+0x3d ([kernel.kallsyms])
ffffffffc0b6cf64 br_pass_frame_up+0xc4 ([kernel.kallsyms])
ffffffffc0b6d154 br_handle_frame_finish+0x164 ([kernel.kallsyms])
ffffffffc0b9907b br_nf_hook_thresh+0xdb ([kernel.kallsyms])
ffffffffc0b99a09 br_nf_pre_routing_finish+0x129 ([kernel.kallsyms])
ffffffffc0b99f91 br_nf_pre_routing+0x341 ([kernel.kallsyms])
ffffffffb75ae994 nf_hook_slow+0x44 ([kernel.kallsyms])
ffffffffc0b6d621 br_handle_frame+0x1f1 ([kernel.kallsyms])
ffffffffb753e767 __netif_receive_skb_core+0x697 ([kernel.kallsyms])
ffffffffb753ed8d netif_receive_skb_internal+0x3d ([kernel.kallsyms])
ffffffffb753f68a napi_gro_receive+0xba ([kernel.kallsyms])
ffffffffc031091e receive_buf+0x17e ([kernel.kallsyms])
ffffffffc0311413 virtnet_poll+0x153 ([kernel.kallsyms])
ffffffffb753fb89 net_rx_action+0x149 ([kernel.kallsyms])
ffffffffb7a000e4 __softirqentry_text_start+0xe4 ([kernel.kallsyms])
ffffffffb6ebc357 irq_exit+0xf7 ([kernel.kallsyms])
ffffffffb7801f9f do_IRQ+0x7f ([kernel.kallsyms])
ffffffffb7800a8f ret_from_intr+0x0 ([kernel.kallsyms])
ffffffffb76d832e native_safe_halt+0xe ([kernel.kallsyms])
ffffffffb76d7f9c __cpuidle_text_start+0x1c ([kernel.kallsyms])
ffffffffb6eebd37 do_idle+0x207 ([kernel.kallsyms])
ffffffffb6eebf8f cpu_startup_entry+0x6f ([kernel.kallsyms])
ffffffffb88a01e5 start_kernel+0x51e ([kernel.kallsyms])
ffffffffb6e000e7 secondary_startup_64+0xb7 ([kernel.kallsyms])
swapper 0 [000] 100201.438796: skb:kfree_skb: skbaddr=0xffff9b364e848b00 protocol=2048 location=0xffffffffb75bacad
ffffffffb7524bd3 kfree_skb+0x73 ([kernel.kallsyms])
ffffffffb7524bd3 kfree_skb+0x73 ([kernel.kallsyms])
ffffffffb75bacad ip_rcv_finish+0x20d ([kernel.kallsyms])
ffffffffc0b98191 ip_sabotage_in+0x41 ([kernel.kallsyms])
ffffffffb75ae994 nf_hook_slow+0x44 ([kernel.kallsyms])
ffffffffb75bb5bc ip_rcv+0x30c ([kernel.kallsyms])
ffffffffb753ebf7 __netif_receive_skb_core+0xb27 ([kernel.kallsyms])
ffffffffb753ed8d netif_receive_skb_internal+0x3d ([kernel.kallsyms])
ffffffffc0b6cf64 br_pass_frame_up+0xc4 ([kernel.kallsyms])
ffffffffc0b6d154 br_handle_frame_finish+0x164 ([kernel.kallsyms])
ffffffffc0b9907b br_nf_hook_thresh+0xdb ([kernel.kallsyms])
ffffffffc0b99a09 br_nf_pre_routing_finish+0x129 ([kernel.kallsyms])
ffffffffc0b99f91 br_nf_pre_routing+0x341 ([kernel.kallsyms])
ffffffffb75ae994 nf_hook_slow+0x44 ([kernel.kallsyms])
ffffffffc0b6d621 br_handle_frame+0x1f1 ([kernel.kallsyms])
ffffffffb753e767 __netif_receive_skb_core+0x697 ([kernel.kallsyms])
ffffffffb753ed8d netif_receive_skb_internal+0x3d ([kernel.kallsyms])
ffffffffb753f68a napi_gro_receive+0xba ([kernel.kallsyms])
ffffffffc031091e receive_buf+0x17e ([kernel.kallsyms])
ffffffffc0311413 virtnet_poll+0x153 ([kernel.kallsyms])
ffffffffb753fb89 net_rx_action+0x149 ([kernel.kallsyms])
ffffffffb7a000e4 __softirqentry_text_start+0xe4 ([kernel.kallsyms])
ffffffffb6ebc357 irq_exit+0xf7 ([kernel.kallsyms])
ffffffffb7801f9f do_IRQ+0x7f ([kernel.kallsyms])
ffffffffb7800a8f ret_from_intr+0x0 ([kernel.kallsyms])
ffffffffb76d832e native_safe_halt+0xe ([kernel.kallsyms])
ffffffffb76d7f9c __cpuidle_text_start+0x1c ([kernel.kallsyms])
ffffffffb6eebd37 do_idle+0x207 ([kernel.kallsyms])
ffffffffb6eebf8f cpu_startup_entry+0x6f ([kernel.kallsyms])
ffffffffb88a01e5 start_kernel+0x51e ([kernel.kallsyms])
ffffffffb6e000e7 secondary_startup_64+0xb7 ([kernel.kallsyms])
swapper 0 [000] 100201.441461: skb:kfree_skb: skbaddr=0xffff9b364e848b00 protocol=2048 location=0xffffffffb75bb348
ffffffffb7524bd3 kfree_skb+0x73 ([kernel.kallsyms])
ffffffffb7524bd3 kfree_skb+0x73 ([kernel.kallsyms])
ffffffffb75bb348 ip_rcv+0x98 ([kernel.kallsyms])
ffffffffb753ebf7 __netif_receive_skb_core+0xb27 ([kernel.kallsyms])
ffffffffb753ed8d netif_receive_skb_internal+0x3d ([kernel.kallsyms])
ffffffffc0b6cf64 br_pass_frame_up+0xc4 ([kernel.kallsyms])
ffffffffc0b6d154 br_handle_frame_finish+0x164 ([kernel.kallsyms])
ffffffffc0b9907b br_nf_hook_thresh+0xdb ([kernel.kallsyms])
ffffffffc0b99a09 br_nf_pre_routing_finish+0x129 ([kernel.kallsyms])
ffffffffc0b99f91 br_nf_pre_routing+0x341 ([kernel.kallsyms])
ffffffffb75ae994 nf_hook_slow+0x44 ([kernel.kallsyms])
ffffffffc0b6d621 br_handle_frame+0x1f1 ([kernel.kallsyms])
ffffffffb753e34b __netif_receive_skb_core+0x27b ([kernel.kallsyms])
ffffffffb753ed8d netif_receive_skb_internal+0x3d ([kernel.kallsyms])
ffffffffb753f68a napi_gro_receive+0xba ([kernel.kallsyms])
ffffffffc031091e receive_buf+0x17e ([kernel.kallsyms])
ffffffffc0311413 virtnet_poll+0x153 ([kernel.kallsyms])
ffffffffb753fb89 net_rx_action+0x149 ([kernel.kallsyms])
ffffffffb7a000e4 __softirqentry_text_start+0xe4 ([kernel.kallsyms])
ffffffffb6ebc357 irq_exit+0xf7 ([kernel.kallsyms])
ffffffffb7801f9f do_IRQ+0x7f ([kernel.kallsyms])
ffffffffb7800a8f ret_from_intr+0x0 ([kernel.kallsyms])
ffffffffb76d832e native_safe_halt+0xe ([kernel.kallsyms])
ffffffffb76d7f9c __cpuidle_text_start+0x1c ([kernel.kallsyms])
ffffffffb6eebd37 do_idle+0x207 ([kernel.kallsyms])
ffffffffb6eebf8f cpu_startup_entry+0x6f ([kernel.kallsyms])
ffffffffb88a01e5 start_kernel+0x51e ([kernel.kallsyms])
ffffffffb6e000e7 secondary_startup_64+0xb7 ([kernel.kallsyms])
swapper 0 [000] 100201.445009: skb:kfree_skb: skbaddr=0xffff9b364e848b00 protocol=2054 location=0xffffffffc0b6d143
ffffffffb7524bd3 kfree_skb+0x73 ([kernel.kallsyms])
ffffffffb7524bd3 kfree_skb+0x73 ([kernel.kallsyms])
ffffffffc0b6d143 br_handle_frame_finish+0x153 ([kernel.kallsyms])
ffffffffc0b6d584 br_handle_frame+0x154 ([kernel.kallsyms])
ffffffffb753e767 __netif_receive_skb_core+0x697 ([kernel.kallsyms])
ffffffffb753ed8d netif_receive_skb_internal+0x3d ([kernel.kallsyms])
ffffffffb753f68a napi_gro_receive+0xba ([kernel.kallsyms])
ffffffffc031091e receive_buf+0x17e ([kernel.kallsyms])
ffffffffc0311413 virtnet_poll+0x153 ([kernel.kallsyms])
ffffffffb753fb89 net_rx_action+0x149 ([kernel.kallsyms])
ffffffffb7a000e4 __softirqentry_text_start+0xe4 ([kernel.kallsyms])
ffffffffb6ebc357 irq_exit+0xf7 ([kernel.kallsyms])
ffffffffb7801f9f do_IRQ+0x7f ([kernel.kallsyms])
ffffffffb7800a8f ret_from_intr+0x0 ([kernel.kallsyms])
ffffffffb76d832e native_safe_halt+0xe ([kernel.kallsyms])
ffffffffb76d7f9c __cpuidle_text_start+0x1c ([kernel.kallsyms])
ffffffffb6eebd37 do_idle+0x207 ([kernel.kallsyms])
ffffffffb6eebf8f cpu_startup_entry+0x6f ([kernel.kallsyms])
ffffffffb88a01e5 start_kernel+0x51e ([kernel.kallsyms])
ffffffffb6e000e7 secondary_startup_64+0xb7 ([kernel.kallsyms])
这里是先用 perf record 记录了 kfree_skb 的事件,同时用 -g 记录了 call graph 方便我们追溯,最后用 perf script 自动处理记录的信息。
第一个报文,有一个 protocol 是 2048,可以在 https://www.iana.org/assignments/ieee-802-numbers/ieee-802-numbers.xhtml 查到就是 IP 报文,常用的,还有 2054 ARP
可以看到这个报文在 nf_hook_slow 之后就被调用了 kfree_skb,无疑就是我们上面分析的那种情况了
后面的几个报文,nf_hook_slow 已经过去了,分别在 ip_rcv_finish、ip_rcv、br_handle_frame_finish 被丢弃,都是些个大函数,而且这些函数往往是通过 goto DROP 的方式丢包,里面丢包的点很多,只能逐个分支分析情况了.
你可能能会想,这里不是给出了偏移量了吗?能否定位到某一行代码?这个留作思考题。
思考题
dropwatch 和 perf 给出来的偏移量能否定位都某一行代码?(提示:分两种情况,如果有对应版本的 debuginfo,和没有对应版本的 debuginfo)
拓展文章
https://blog.huoding.com/2016/12/15/574
https://cloud.tencent.com/developer/article/1631874