Post content
re-review https://dlee-libo.github.io/2020/05/31/bpftrace-kernel-journey/🤬 这是五年前我人生第一次排查内核网络栈连通性问题,完全不会,请教最爱的同事波波,他用一个周末钻木取火纯肉眼反汇编 + kprobe 找到了问题写下了这篇荡气回肠的青词。这五年里我每次进步一点都要来回头看看这个最初的问题,正好昨天评论区又提到 rp filter,这就是一个 rp_filter=2 造成的 arp 丢包问题没有丢,压根就没回。 在 ubuntu desktop 2404 kernel 6.14.0-29 依然可以按照博客脚本直接复现,症状是 ping 不通,pwru/tcpdump/ip-neigh 一下发现 arp 没回应,开始检查 arp。 arp 和一般的连通性问题不同,arp 的问题可能根本不是丢包。比如这一题尝试用 pwru 检查 arp 请求和返回最多只能看到: $ pwru --filter-track-skb --output-caller 'arp and dst host 169.254.0.1' arp_rcv __netif_receive_skb_one_core arp_process arp_rcv ip_route_input_noref arp_process ip_route_input_slow ip_route_input_noref __mkroute_input ip_route_input_slow fib_validate_source __mkroute_input __fib_validate_source fib_validate_source ip_handle_martian_source __mkroute_input consume_skb arp_process 完全没有丢包,看起来就像是正确无误地处理了 arp skb 一样。 用 retsnoop 摸奖也毫无结果,下面的命令检查所有包含 arp 的内核函数的返回 errno: retsnoop -a '*arp*' -e '*arp*' -x any 完全没有任何 errno。 这时候只能根据 pwru 的 skb lifetime tracing 输出随缘读一下内核源代码,发现最可疑的似乎是 arp_process() 这个内核函数里的 if (...) // 一大堆嵌套 if,巨型 code block arp_send_dst(ARPOP_REPLY, ETH_P_ARP, sip, dev, tip, sha, dev->dev_addr, sha, reply_dst); 没有执行。 我在 PyCon CN 25 说过,查“某事为何没有发生”比“某事为何发生”更难,后者找到发生怪事的 hook 一波 bt 带走,前者连 attach point 都不知道从哪里下手。 波巨魔在五年前直接肉翻汇编找到 offset 打 kprobe,但现在我们有了 bpfsnoop lbr,来试试看。 注意 arp_send_dst() 所在的巨型 code block 之后是一个 neigh_lookup if (...) { // 一大堆嵌套 if,巨型 code block arp_send_dst(ARPOP_REPLY, ETH_P_ARP, sip, dev, tip, sha, dev->dev_addr, sha, reply_dst); } n = __neigh_lookup(&arp_tbl, &sip, dev, 0); 思路就来了,在 neigh_lookup 的入口检查 lbr 不就知道肛才运行的谜语代码是怎么运行了的吗 $ bpfsnoop -k neigh_lookup --output-lbr --filter-arg 'dev->ifindex == 9' -m entry --filter-pid $(pidof ping) ip_route_input_noref+0x6e (net/ipv4/route.c:2489) -> __rcu_read_unlock+0x0 (kernel/rcu/tree_plugin.h:431) arp_process+0x4a4 (net/ipv4/arp.c:839) -> arp_process+0x1a5 (include/net/neighbour.h:545) arp_process+0x1be (include/net/neighbour.h:545) -> neigh_lookup+0x0 (net/core/neighbour.c:579) 最后三行输出已经很清楚了,看一眼 net/ipv4/arp.c:839 的代码 if (arp->ar_op == htons(ARPOP_REQUEST) && ip_route_input_noref(skb, tip, sip, 0, dev) == 0) { 是 ip_route_input_noref(skb, tip, sip, 0, dev) == 0 这个条件失败了。 检查 ip_route_input_noref 的 retval 也很简单 $ sudo bpfsnoop -k ip_route_input_noref --output-arg retval --filter-pkt 'arp and dst host 169.254.0.1' ← retval=(enum skb_drop_reason)SKB_DROP_REASON_IP_RPFILTER 已经出现了 SKB_DROP_REASON_IP_RPFILTER,结束。最终原因是 rp_filter 是 max(netdev, all) 的 sysctl,在复现脚本里只修改了 netdev.rp_filter=0 不足够,还要再改 all.rp_filter=0。 非常感谢 Leon Hwang (tg ch: https://t.me/eBPFTalker) 的工具 bpfsnoop,我已经安利了五六次,自己也撸起袖子打算做点微小的贡献。 这些微小的星光推动计算机工程的边界,make a better world.