上个月底调一个网络不通的问题,查到最后发现是路由配置写错了,跟内核一点关系都没有。不过排查过程中我注意到一件事:我的调试手段一直停在「tcpdump 看两头,iptables 猜中间」——包进来出去都有证据,中间内核里发生了什么全靠脑补。前阵在 DEV 上翻到一篇讲 pwru 的笔记(Packet, Where are you?,cilium 项目出的 eBPF 跟踪工具),正好补的就是这块空白。读了两遍,觉得最值得记下来的不是命令用法,而是它怎么保证自己跟住的始终是同一个包。
先把问题拆开看看。网络包进内核之后,并不是像文件句柄那样从头到尾持有同一个对象。NAT 改写、clone、copy、穿过 veth、桥接转发,都会让代表这个包的 sk_buff 指针发生变化,甚至整个换掉。如果跟踪逻辑只认“当前地址”,跟到一半就看到对象变了,然后在半个内核里丢失这个包的行踪——你看到的分明是同一个包的下一站,但工具已经认不出它了。
pwru 的做法是分两个阶段。先用过滤条件选出要跟的包,例如:
pwru 'host 1.1.1.1 and tcp'
包第一次匹配这个条件时,把当时的 skb 指针存进 BPF hash map;之后每一次 kprobe 命中,都先查一下这个 map,确认当前这个 skb 是不是我正在跟的那个包。匹配只做一次→后续跟踪完全靠身份,不依赖再次命中过滤条件。
这一段我第一次读的时候卡住了。既然 trace 挂着这么多 kprobe,为什么不把所有 skb 都收进 map、一路比对下去就行了?后来才绕明白:如果收的是“所有经过的包”,map 里的条目之间没有任何关系,你看到的是一堆孤立的指针快照——指针变了,你没法判断哪两个快照属于同一次旅程。pwru 把“按数据匹配”和“按身份跟踪”分成两个阶段,匹配负责选出起点,身份链则交给变换点上的探针显式维护。我觉得这是整篇里很干净的一层抽象。
身份链具体怎么维护,有两个细节值得记。skb_clone 和 skb_copy 这类函数上挂了 fexit 探针,克隆或复制出来的新 skb 会继承“被跟踪”标记,所以 NAT 改写指针之后还认得出来;kfree_skbmem 被调用时负责清掉对应条目,hash map 不会越涨越大。另外它还 hook 了 kfree_skb_reason,把内核的 skb_drop_reason 枚举解码成 SKB_DROP_REASON_NETFILTER_DROP 这种可读文本——丢包那一刻,从“trace 在这里断了”变成“trace 在这里断了,断的原因是……”,信息量完全不同。
还有一个模式,顺手记一下:--filter-track-skb-by-stackid 不按指针认包,而是按调用栈指纹认。
pwru --filter-track-skb-by-stackid 'host 1.1.1.1 and tcp'
桥接这类场景里原始指针是真的会丢的,这个模式能继续跟住同一个逻辑包。
划重点:第一,内核里“一个包”是流动的概念,skb 指针不能当身份证用;第二,pwru 把过滤匹配和身份跟踪拆成两个阶段,匹配只做一次,身份链由变换点上的探针显式维护;第三,丢包原因可以靠 hook 解码出来,别满足于“跟到这就断了”。这篇就记到这里,你可以挑一个手头的网络问题跑一遍 pwru,哪怕最后查出来还是配置写错了,亲眼看到一次内核里的跟踪过程也值。
