所有过去的假期!
我们决定在假期后将第一篇文章专门介绍Linux,即我们精彩的
Linux Administrator课程,该课程属于最动态的课程,即具有最相关的材料和实践。 好吧,因此,我们提供了有趣的文章和
公开课 。
发表者Matteo Croce
原始标题: 使用eBPF(RHEL 8 Beta)进行网络调试引言联网是令人兴奋的体验,但并非总是能避免问题。 故障排除可能很棘手,就像试图重现“现场”发生的错误行为一样。
幸运的是,有一些工具可以帮助解决此问题:网络名称空间,虚拟机,
tc
和
netfilter
。 可以使用网络名称空间和veth设备来复制简单的网络设置,而更复杂的设置则需要将虚拟机与软件桥连接,并使用标准的网络工具(例如
iptables
或
tc
)来模拟不正确的行为。 如果SSH服务器
iptables -A INPUT -p tcp --dport 22 -j REJECT --reject-with icmp-host-unreachable
时生成的ICMP响应出现问题,则在正确的名称空间中
iptables -A INPUT -p tcp --dport 22 -j REJECT --reject-with icmp-host-unreachable
可以帮助解决问题。
本文介绍如何使用
eBPF(扩展的BPF)(伯克利数据包过滤器的高级版本
)解决复杂的网络问题。 eBPF是一项相对较新的技术,该项目尚处于初期阶段,因此文档和SDK尚未准备就绪。 但是,让我们希望有所改进,特别是因为XDP(eXpress数据路径)是随
Red Hat Enterprise Linux 8 Beta一起提供的 ,您可以立即下载并运行它。
eBPF不能解决所有问题,但是它仍然是一个功能强大的网络调试工具,值得关注。 我相信它将在网络的未来中扮演非常重要的角色。
问题我调试了
开放vSwitch(OVS)网络问题,该问题涉及非常复杂的安装:一些TCP数据包被分散并以错误的顺序传递,虚拟机的带宽从稳定的6 Gb / s下降到波动的2-4 Gb / s。 分析表明,每个带有PSH标志的连接的第一个TCP数据包的发送顺序都错误:每个连接仅第一个,并且只有一个。
我尝试在两个虚拟机上重现此设置,并且在许多帮助文章和搜索查询之后,我发现
iptables
和
nftables
都不能操纵TCP标志,而
tc
可以操纵,但是只能重写标志并中断新连接和TCP一般而言。
结合使用
iptables
,
conntrack
和
tc
可能可以解决问题,但是我认为这对于eBPF来说是一项出色的工作。
什么是eBPF?eBPF是伯克利分组过滤器的增强版本。 她为BPF带来了很多改进。 特别是,它允许您在内存中写入数据,而不仅仅是读取数据,因此不仅可以过滤软件包,还可以对其进行编辑。
通常将eBPF简称为BPF,并将BPF本身称为cBPF(经典(经典)BPF),因此根据上下文,单词“ BPF”可以用来表示两个版本:在本文中,我总是谈论扩展版本。
“内幕” eBPF有一个非常简单的虚拟机,可以执行字节码的小片段并编辑一些内存缓冲区。 eBPF中存在一些限制,可以保护其免受恶意使用:
- 禁止循环,以使程序始终在特定时间终止。
- 它只能通过堆栈和暂存缓冲区访问内存;
- 只能调用允许的内核函数。
可以使用
调试和跟踪以各种方式将程序加载到内核中。 在我们的案例中,eBPF对使用网络子系统感兴趣。 使用eBPF程序有两种方法:
- 通过XDP连接到物理或虚拟网卡的RX路径的起点;
- 通过
tc
连接到输入或输出中的qdisc。
要创建用于连接的eBPF程序,只需编写C代码并将其转换为字节码即可。 以下是使用XDP的简单示例:
SEC("prog") int xdp_main(struct xdp_md *ctx) { void *data_end = (void *)(uintptr_t)ctx->data_end; void *data = (void *)(uintptr_t)ctx->data; struct ethhdr *eth = data; struct iphdr *iph = (struct iphdr *)(eth + 1); struct icmphdr *icmph = (struct icmphdr *)(iph + 1); if (icmph + 1 > data_end) return XDP_PASS; if (eth->h_proto != ntohs(ETH_P_IP) || iph->protocol != IPPROTO_ICMP || icmph->type != ICMP_ECHOREPLY) return XDP_PASS; if (iph->ttl) { uint16_t *ttlproto = (uint16_t *)&iph->ttl; uint16_t old_ttlproto = *ttlproto; iph->ttl = bpf_get_prandom_u32() % iph->ttl + 1; csum_replace2(&iph->check, old_ttlproto, *ttlproto); } return XDP_PASS; } char _license[] SEC("license") = "GPL";
上面的代码片段,其中不
include
表达式,帮助程序和可选代码,是一个XDP程序,可以将接收到的ICMP回声回复(即pong)的TTL更改为随机数。 主要功能获得
xdp_md
结构,其中包含指向包的开始和结尾的两个指针。
要将我们的代码编译为eBPF字节码,需要具有适当支持的编译器。 Clang支持它,并通过在编译时将bpf指定为目标来创建eBPF字节码:
$ clang -O2 -target bpf -c xdp_manglepong.c -o xdp_manglepong.o
上面的命令创建的文件乍一看似乎是一个普通的目标文件,但是仔细检查后发现,指定的计算机类型是Linux eBPF,而不是操作系统的本机类型:
$ readelf -h xdp_manglepong.o ELF Header: Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 Class: ELF64 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: REL (Relocatable file) Machine: Linux BPF <--- HERE [...]
收到常规目标文件的包装后,eBPF程序即可下载并通过XDP连接到设备。 可以使用
iproute2
包中的
ip
使用以下语法来完成此操作:
该命令指定目标wlan0接口,并且由于使用了-force选项,该命令将覆盖已经加载的任何现有eBPF代码。 加载eBPF字节码后,系统的行为如下:
$ ping -c10 192.168.85.1 PING 192.168.85.1 (192.168.85.1) 56(84) bytes of data. 64 bytes from 192.168.85.1: icmp_seq=1 ttl=41 time=0.929 ms 64 bytes from 192.168.85.1: icmp_seq=2 ttl=7 time=0.954 ms 64 bytes from 192.168.85.1: icmp_seq=3 ttl=17 time=0.944 ms 64 bytes from 192.168.85.1: icmp_seq=4 ttl=64 time=0.948 ms 64 bytes from 192.168.85.1: icmp_seq=5 ttl=9 time=0.803 ms 64 bytes from 192.168.85.1: icmp_seq=6 ttl=22 time=0.780 ms 64 bytes from 192.168.85.1: icmp_seq=7 ttl=32 time=0.847 ms 64 bytes from 192.168.85.1: icmp_seq=8 ttl=50 time=0.750 ms 64 bytes from 192.168.85.1: icmp_seq=9 ttl=24 time=0.744 ms 64 bytes from 192.168.85.1: icmp_seq=10 ttl=42 time=0.791 ms --- 192.168.85.1 ping statistics --- 10 packets transmitted, 10 received, 0% packet loss, time 125ms rtt min/avg/max/mdev = 0.744/0.849/0.954/0.082 ms
每个数据包都会通过eBPF,而eBPF最终会进行一些更改并决定是丢弃数据包还是跳过数据包。
eBPF如何提供帮助回到最初的网络问题,我们记得有必要标记几个TCP标志,每个连接一个,并且
iptables
和
tc
都不能这样做。 编写此方案的代码一点也不难:配置通过OVS桥连接的两个虚拟机,然后将eBPF连接到其中一个虚拟VM设备。
听起来这是一个很好的解决方案,但是请记住XDP仅支持处理接收到的数据包,并且将eBPF连接到接收虚拟机的
rx
路径不会对交换机产生任何影响。
要解决此问题,必须使用
tc
加载eBPF并将其连接到VM的输出路径,因为
tc
可以加载eBPF程序并将其连接到qdisk。 要标记离开主机的数据包,必须将eBPF连接到输出qdisk。
加载eBPF程序时,
XDP
和
tc
API之间存在一些差异:默认情况下,节名称不同,主函数自变量的结构类型,返回值不同。 但这不是问题。 以下是加入tc动作时标记TCP的程序片段:
#define RATIO 10 SEC("action") int bpf_main(struct __sk_buff *skb) { void *data = (void *)(uintptr_t)skb->data; void *data_end = (void *)(uintptr_t)skb->data_end; struct ethhdr *eth = data; struct iphdr *iph = (struct iphdr *)(eth + 1); struct tcphdr *tcphdr = (struct tcphdr *)(iph + 1); if ((void *)(tcphdr + 1) > data_end) return TC_ACT_OK; if (eth->h_proto != __constant_htons(ETH_P_IP) || iph->protocol != IPPROTO_TCP) return TC_ACT_OK; if (tcphdr->syn || tcphdr->fin || tcphdr->rst || tcphdr->psh) return TC_ACT_OK; if (bpf_get_prandom_u32() % RATIO == 0) tcphdr->psh = 1; return TC_ACT_OK; } char _license[] SEC("license") = "GPL";
如上XDP示例所示,使用以下代码将代码编译为字节码:
clang -O2 -target bpf -c tcp_psh.c -o tcp_psh.o
但是下载是不同的:
现在,eBPF已加载到正确的位置,并且标记了离开VM的数据包。 检查第二个虚拟机中收到的数据包后,我们将看到以下内容:

tcpdump
确认新的eBPF代码正在运行,并且每10个TCP数据包中大约有1个设置了PSH标志。 只需要20行C代码即可选择性地标记离开虚拟机的TCP数据包,重现“战斗中”发生的错误,并且所有这些都无需重新编译甚至重新启动! 这大大简化了
Open vSwitch修复程序的验证,而其他工具则无法实现。
结论eBPF是一项相当新的技术,社区对此实施有明确的看法。 还值得注意的是,基于eBPF的项目(例如
bpfilter )正变得越来越流行,结果,许多设备供应商开始直接在网卡中实现eBPF支持。
eBPF不能解决所有问题,因此请勿滥用它,但它仍然是用于网络调试的非常强大的工具,值得关注。 我相信它将在网络的未来中发挥重要作用。
结束我们在这里等待您的评论,我们还邀请您访问我们的
公开课程 ,如果您也可以提出问题。