使用eBPF(RHEL 8 Beta)调试网络

所有过去的假期!

我们决定在假期后将第一篇文章专门介绍Linux,即我们精彩的Linux Administrator课程,该课程属于最动态的课程,即具有最相关的材料和实践。 好吧,因此,我们提供了有趣的文章和公开课

发表者Matteo Croce
原始标题: 使用eBPF(RHEL 8 Beta)进行网络调试

引言

联网是令人兴奋的体验,但并非总是能避免问题。 故障排除可能很棘手,就像试图重现“现场”发生的错误行为一样。

幸运的是,有一些工具可以帮助解决此问题:网络名称空间,虚拟机, tcnetfilter 。 可以使用网络名称空间和veth设备来复制简单的网络设置,而更复杂的设置则需要将虚拟机与软件桥连接,并使用标准的网络工具(例如iptablestc )来模拟不正确的行为。 如果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数据包的发送顺序都错误:每个连接仅第一个,并且只有一个。

我尝试在两个虚拟机上重现此设置,并且在许多帮助文章和搜索查询之后,我发现iptablesnftables都不能操纵TCP标志,而tc可以操纵,但是只能重写标志并中断新连接和TCP一般而言。

结合使用iptablesconntracktc可能可以解决问题,但是我认为这对于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); /* sanity check needed by the eBPF verifier */ if (icmph + 1 > data_end) return XDP_PASS; /* matched a pong packet */ if (eth->h_proto != ntohs(ETH_P_IP) || iph->protocol != IPPROTO_ICMP || icmph->type != ICMP_ECHOREPLY) return XDP_PASS; if (iph->ttl) { /* save the old TTL to recalculate the checksum */ uint16_t *ttlproto = (uint16_t *)&iph->ttl; uint16_t old_ttlproto = *ttlproto; /* set the TTL to a pseudorandom number 1 < x < TTL */ iph->ttl = bpf_get_prandom_u32() % iph->ttl + 1; /* recalculate the checksum; otherwise, the IP stack will drop it */ 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使用以下语法来完成此操作:

 # ip -force link set dev wlan0 xdp object xdp_manglepong.o verbose 

该命令指定目标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标志,每个连接一个,并且iptablestc都不能这样做。 编写此方案的代码一点也不难:配置通过OVS桥连接的两个虚拟机,然后将eBPF连接到其中一个虚拟VM设备。

听起来这是一个很好的解决方案,但是请记住XDP仅支持处理接收到的数据包,并且将eBPF连接到接收虚拟机的rx路径不会对交换机产生任何影响。

要解决此问题,必须使用tc加载eBPF并将其连接到VM的输出路径,因为tc可以加载eBPF程序并将其连接到qdisk。 要标记离开主机的数据包,必须将eBPF连接到输出qdisk。

加载eBPF程序时, XDPtc 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); /* sanity check needed by the eBPF verifier */ if ((void *)(tcphdr + 1) > data_end) return TC_ACT_OK; /* skip non-TCP packets */ if (eth->h_proto != __constant_htons(ETH_P_IP) || iph->protocol != IPPROTO_TCP) return TC_ACT_OK; /* incompatible flags, or PSH already set */ 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 

但是下载是不同的:

 # tc qdisc add dev eth0 clsact # tc filter add dev eth0 egress matchall action bpf object-file tcp_psh.o 

现在,eBPF已加载到正确的位置,并且标记了离开VM的数据包。 检查第二个虚拟机中收到的数据包后,我们将看到以下内容:



tcpdump确认新的eBPF代码正在运行,并且每10个TCP数据包中大约有1个设置了PSH标志。 只需要20行C代码即可选择性地标记离开虚拟机的TCP数据包,重现“战斗中”发生的错误,并且所有这些都无需重新编译甚至重新启动! 这大大简化了Open vSwitch修复程序的验证,而其他工具则无法实现。

结论

eBPF是一项相当新的技术,社区对此实施有明确的看法。 还值得注意的是,基于eBPF的项目(例如bpfilter )正变得越来越流行,结果,许多设备供应商开始直接在网卡中实现eBPF支持。

eBPF不能解决所有问题,因此请勿滥用它,但它仍然是用于网络调试的非常强大的工具,值得关注。 我相信它将在网络的未来中发挥重要作用。

结束

我们在这里等待您的评论,我们还邀请您访问我们的公开课程 ,如果您也可以提出问题。

Source: https://habr.com/ru/post/zh-CN436528/


All Articles