您的位置 首页 科技

舍 bpftrace 而取 systemtap 的代价和思考

作者 | dog250 责编 | 张文 头图 | CSDN 下载自视觉中国 出品 | CSDN(ID:CSDNnews)

作者 |dog250责编 | 张文

头图 | CSDN 下载自视觉中国

出品 | CSDN(ID:CSDNnews)

舍 bpftrace 而取 systemtap 的代价和思考

上个礼拜我就想喷 eBPF 了,由于周末时间实在太紧,就准备拖延一周,但还是立了个 flag,先发了个朋友圈:

ebpf 就像皮藓一样,已经遍布在 linux 内核的各个角落,每个调用点都看上去很随意,毫无规划,让人觉得好像自己觉得哪里需要这么一个调用点并不很难…

但实际上如果你真的去尝试在某处加一个 ebpf 调用点时,就会觉得这件事和清除牛皮藓的过程非常类似,修改散落在各个目录的多个文件,还得重新编译,大概率失败,还要重新做一次,很难一次做干净,当你好不容易成功了,会有一种“不过如此”的嗟叹…

我曾将 ebpf 比做扩散的癌细胞,这个比喻没有给人密集恐惧的效果,所以我换成了牛皮癣。该存在 ebpf 调用点的地方一个也没有,没必要 ebpf 的地方到处都是,这些点还在持续增加,迄至 5.11 内核,ebpf 已经有大三十个点了,依然在毫无规划地疯长着…

eBPF 是个创新,但人们明显狂热过度了,ebpf 增加调用点也过于随意,太业务导向了,损坏了内核的内聚性,远远比不上当初 netfilter 的五个 hook 点以及 qdisc 这种经过良好设计的机制。

另外还有一个问题,netfilter 的五个 hook 点上如果部署了 ebpf 点,其实就能解决大部分性能问题,然而直到现在都没有,感觉是社区矫枉过正了,真的彻底把 netfilter 当成了旧时代的象征,把马杀掉的同时,轮子也不要了。

人们都像虫子一样在这里你争我抢,吃的都是良心,拉的全是思想。

终于到了周末,我终于还是不能说话,我甚至已经不知道该说些什么了。

昨天,我把自己血祭了,这种自我牺牲在古罗马共和国是一种美德,执政官会把自己献给神,以换取战争的胜利。

为了喷 eBPF,在平时的工作和学习中,我积累了很多素材,eBPF 的领地分为两个部分:

  • 网络协议栈功能

  • trace 跟踪

在网络方面,我用牛皮癣的比喻来说明 eBPF 不断疯长的画面是多么糟糕。而在 trace 跟踪方面,我想拿性能和功能说事。

我本希望用这些素材来佐证自己的一些怪异的观点,昨天,我准备用我的实际工作来作为我最后一个素材,结果它恰恰验证了我的认知错误,它恰恰说明了 eBPF 作为 trace 跟踪工具是多么的好用!

我的故事是这样的。

在大流量的背景下,特别是如果你的代码使用类似 Bonding,tun/tap,GRE,IPIP 等虚拟网卡,排查 skb 在哪里被 drop 一直都是一个很麻烦的事情。

即便是你已经知道了一个特定的五元组,这件事也不会因此变得简单。

抓包?抓包永远是第一步的操作,但也只是第一步,它只能告诉你 skb 收到了或者没有收到,如果没有收到,进一步就需要确认 skb 到底在哪里丢了。当然了,如果最后实在是定位不了,一般会把锅甩给运营商这个黑洞。

类似 wireguard 这种,其所有复杂的操作,包括加密,分发等逻辑在内均在 wireguard 虚拟网卡的 xmit 函数中完成,跟踪 wg_xmit 的细节除了要求你对 wireguard 的代码非常熟悉之外,还需要各种奇技淫巧的手艺。

以下面这个场景为例:

舍 bpftrace 而取 systemtap 的代价和思考

我要实锤 skb 在从 1 到 11 的哪一步被 drop 的,要怎么做?

这不正是 stap 的地盘吗?

哦,我不能说 stap,在有了 bpftrace 可供选择的时候,再说 stap 有点不正确,如果我坚持要用 stap,会有一群人建议我用 bpftrace,并说 stap 是多么的糟糕,过时,不友好。

那好,那就用 bpftrace,下面的脚本可以对 skb 进行完美的全程 trace:

#!/usr/local/bin/bpftrace #include <linux/skbuff.h> #include <linux/udp.h> #include <net/sock.h> k:encrypt_packet { $skb = (struct sk_buff *)arg0; // 这个skb的mark需要iptables来为特定的五元组标记上,但是encrypt_packet这里是可以使用mark的最后的地方。 // 在encrypt_packet成功返回后,skb的几乎所有附属标记都会被reset,包括skb->mark。 // 因此这里必须用另一个标记,以确保在encrypt_packet之后还能用此特征跟踪到特定的skb。 // 由于bpftrace只能读不能写,这里我选择直接用skb的地址! if($skb->mark == 1234) { printf( “encrypt got %p/n”, $skb); @addr = $skb; } } //k:send4 k:udp_tunnel_xmit_skb { $daddr = arg4; $saddr = arg3; $skb = (struct sk_buff *)arg2; // 这里除了match地址之外,是不是也要match一下其它字段呢?毕竟slub中的skb是可以重用的。 // 如果mark 1234的skb在这个之前被drop & free了,它被重新alloc后依然会到这里,这就错了! // 然而由于流量可控,且我是一个函数一个函数trace,上述概率极低。手艺人不求完美! if($skb == @addr) { printf( “—- skb:%p daddr:%08x saddr:%08x /n”, @addr, $daddr, $saddr); } } k:iptunnel_xmit //k:dev_queue_xmit //k:dev_hard_start_xmit //k:dev_queue_xmit_nit { $skb = (struct sk_buff *)arg2; // 从裸包中取外层协议头的内容。 $udph = (struct udphdr *)($skb->head + $skb->transport_header); $sport = $udph->source; $dport = $udph->dest; if($skb == @addr) { $port = (($sport & 0xff00) >> 8) | (($sport & 0xff) << 8); $port2 = (($dport & 0xff00) >> 8) | (($dport & 0xff) << 8); printf( “sport:%d dport:%d/n”, $port, $port2); // trace结束,重置全局变量。 @addr = (struct sk_buff *) 0; } }

啊哈,我觉得这是一个让人感觉很顺畅的脚本,skb 在进入 wg_xmit 前打上 mark,在 wg_xmit 的过程中清除 skb 的 mark 之前将其地址保存,此后跟踪该地址的 skb。

然而悲哀的是,skb 顺利发送出去了,我一无所获,悲哀的是,内层的报文在对端 wireguard 的 wg 网卡上也没有抓到。

去对端反着来一遍且 OK? 思路是一回事,落地是另一回事。

怎么才能在对端继续 trace 这个 skb 呢?

如果没有办法 trace 这个 skb,你怎么区分这个报文是被中间网络设备 drop 了还是被对端 wireguard 接收过程 drop 了呢?

由于发送端已经可以获取内层和外层的特定五元组,在接收端用外层五元组去 match 外层协议头当然是一个正确的思路,问题是 如果外层隧道的五元组被大流量复用,你又将如何在 skb 解密前去匹配内层五元组,流量实在太大了,就像很多抓包由于流量大无法进行一样,你想要的信息几乎会被瞬间淹没!

我想知道的是,bpftrace 怎么来做这件事。如果不能方便快捷地解决这个问题,我就有充分的理由使用旧时代的旧事物了。

问题是 bpftrace 不允许我修改 skb 啊!现在,我决定扔掉 bpftrace,用 stap 来做正确的事。

我需要做的仅仅是,为特定的数据包打上一个标记,该标记必须在对端可以被识别。我决定使用无伤大雅的 IP 头 TTL 字段,使用 stap 完成这件事非常简单。顺便地,我将为 skb 打 mark,这件事也用 stap 来做,于是我也删掉了 iptables 规则:

#!/usr/local/bin/stap -g

%{#include <linux/skbuff.h>#include <linux/ip.h>#include <linux/tcp.h>#include <net/addrconf.h>

structsk_buff *tmp = NULL; %}

function getinfo(pskb: long) %{structsk_buff *skb = ( structsk_buff *)STAP_ARG_pskb; structiphdr *hdr = ip_hdr(skb); conststructskb_shared_info *shinfo = skb_shinfo(skb); structdst_entry *dst = skb_dst(skb);

if(dst && !strcmp( “wg0”, dst->dev->name)) { structtcphdr *tp = ( structtcphdr *)(( char*)hdr + sizeof( structiphdr)); if(ntohs(tp->source) == 443&& ntohs(tp->dest) == 3663) { STAP_PRINTF( “sport:%d dport:%d addr:%p/n”, ntohs(tp->source), ntohs(tp->dest), skb); skb->mark = 1234; tmp = skb;}}%}

probe kernel.function( “ip_forward”) {getinfo($skb)}

function setttl(pskb: long) %{structsk_buff *skb = ( structsk_buff *)STAP_ARG_pskb; structiphdr *hdr;

if(skb == NULL) return;

hdr = ip_hdr(skb);if(skb == tmp) { hdr->ttl = 120; tmp = NULL; }

%}

// 这个是在确认了skb顺利通过了wg server后,为了在wg client端跟踪所使用的。// 由于数据包没有什么好的字段可利用来打标,就随手选了ttl,作为一个特殊值,不一定是120,180更好。。。probe kernel.function( “ip_local_out”) {setttl($skb)}⁣

如此一个脚本,在对端直接 match 外层五元组和 TTL 值就行了,我们只需要在匹配外层五元组的同时,匹配 TTL 值大于 70 的 skb 即可。

bpftrace 只能读不能写,为了让这件事成为可能,我只能用 stap。

以上就是我的态度,我不是不接受 bpftrace,我更不是不接受新事物,我只是想说不能在接受新事物的时候把旧事物一棍子打死!不能因为 eBPF 的流行就选择 bpftrace 而把 stap 丢进垃圾桶。…

我正准备吐槽,然而我正准备喷 bpftrace 做不到某某事情的时候,我血祭了!

我明明可以在 wireguard 的数据接收端像数据发送端一样用 bpftrace 来 trace 这个特定的 skb,毕竟我只是想知道它在哪里被 drop 了,这完全没有写操作的必要。

然而我为了一种假装成格调的态度,死活非要用 stap 来完成这件事而放弃 bpftrace。

虽然我自以为自己是 stap 的熟练工,但我却几乎都是在用-g 的 guru 模式,不是因为我自信,而是因为我搞不清楚又记不住 stap 的语法。我几乎只会 C 和汇编,我几乎总是记不住任何其它语言的语法,包括 Bash 在内…

我在使用 stap 来 trace 内核或者模块的函数前,我总会看下它的参数解析情况:

stap-L ‘module( “wireguard”).function( “wg_allowedips_lookup_src”)’⁣

很不幸,无法使用任何参数,原因未知。

因此当我希望使用它的参数的时候,我只能裸取寄存器了,像下面这样:

probe module( “wireguard”). function( “wg_allowedips_lookup_src”) {// 由于stap -L无法解析参数,只能用x86_64的调用规则直接取寄存器if(cmpskb(register( “rsi”))) { a = 1;}}

probe module( “wireguard”). function( “wg_allowedips_lookup_src”). return{if(a == 1) { printf( “peer returned::%p/n”, register( “rax”)); a = 0;}}⁣

既然都用 stap 了,为何不让事情简单一些呢?

于是我就开始了自信满满的写操作。改 skb 内存,手工修改 skb 的 data,以期望能 bypass 掉很多不必要的流程。…

在我持续这么玩了大概几个小时后,大概就是开着飞机修引擎的感觉,我有些疲劳,大概在某个精确的时间点,crash or soft lockup,完美的完成了血祭!

这是一次线上作业,锅显然是我的。

这不能说明我的手艺不精湛,在这种硬着陆之前,我毕竟还放飞了几个小时呢,但这说明一个问题, bpftrace 就是比 stap 好,至少安全

这恰恰是我要反驳的观点,却被我证明了

在稳定性方面,eBPF 的两个不允许就够了:

  • eBPF 不允许你写任何有潜在风险的代码。

  • eBPF 不允许你写任何复杂的代码。

stap 里你可以随便一个 while(true) 而把系统锁死,bpftrace 中却不行。

处理类似的问题时,其实我是带有偏见的。

我不喜欢使用工具特别是不喜欢使用新工具的原因背后更多的是因为我比较懒,我不喜欢面对和驾驭一大坨不相关的东西。比方说我明明知道有个 dropwatch 却没有使用,就是因为它太复杂了,还要去了解那么复杂的命令行。与其这样,还不如我直接 stap probe kfree_skb 然后 dump_stack 呢。

一开始我对 stap 也是抵触的,因为它也是足够复杂,我宁愿裸写 ftrace 函数,比方说手工把一个函数的头 5 个字节替换成 call stub_handler 这种。

如今即便我对 stap 已经轻车熟路了,我依然还是坚持只写 guru 模式的脚本,我依然还是懒得去学习 stap 的语法。

使用工具提高效率那是针对熟悉这种工具的人来讲的,对于不熟悉该工具的来讲,比如我,花在学习这种工具的使用方法上的时间将让我延迟对真正问题的处理。

同样,磨刀不误砍柴工,工欲善其事必先利其器,我并不赞同这是普适的。这种话术是针对频繁解决同类问题的人来讲的,他们需要的是总结出一种范式,锤子能钉钉子,如果你需要频繁钉钉子,你当然需要买一把锤子,但如果你只需要钉一次钉子,随手拿起边上的一块砖或许比去买一把锤子更方便。

如果你每次玩的都是新花样,当然不需要工具了。

此外 “在路上” 的价值观在我针对工具的观点也起到了推波助澜的作用。我一向觉得自己是在路上的人,因此我讨厌任何累赘,我不会去携带,背负,记忆那些随处可以得到的东西。杭州往上海搬家,既然上海可以买到被子,我何必要把被子寄回来,直接扔掉不是更好吗?

在石器时代,人们就已经拥有了类别丰富的工具,但对于一个母系时代净身出户的男人而言,唯一可以带走并且他们自愿选择的工具就是弓箭,弹弓这种类似的远程攻击工具,可能就连刀,斧之类的,都属于累赘,抓住问题的本质本身,这就足够了。

突发!Intel CEO 换帅,VMware CEO 将走马上任

微信封杀 QQ 音乐、拼多多等 App 外链;蠕虫病毒在国内肆虐;Dropbox 宣布裁员 |极客头条

“火星人”马斯克推论: 世界或是被编码而成,上帝可能是个程序员!

他曾一举击败英伟达,却因坚持研发背负骂名 教育行业 A 股 IPO 第一股,如何做成程序员培训这门生意 | 极客头条

13 年 29 款手机,从激进到求稳,iPhone 都经历什么?

45 年编程经验告诉我的技术真相

舍 bpftrace 而取 systemtap 的代价和思考

舍 bpftrace 而取 systemtap 的代价和思考

舍 bpftrace 而取 systemtap 的代价和思考

在看

本文来自网络,不代表聚合资讯立场,转载请注明出处:https://www.jiongx.com/82194.html

作者: jiongx

为您推荐

发表评论

邮箱地址不会被公开。 必填项已用*标注

返回顶部