yihong0618 和朋友们的频道 头像

消息来源频道

yihong0618 和朋友们的频道

@hyi0618

频道8,612 位成员公开可见0 人在线

yihong0618 和朋友们的频道

成员规模8,612 位成员
在线情况0 人在线
消息总数10,129 条消息
浏览量总数3,273,532 次浏览

在这个频道里搜索消息……

t.me/hyi0618

上周在构造 tcp rst 的时候惊讶地发现下面的 scapy 代码居然无法和本地的 :8000 握手
from scapy.all import *
sr1(IP(dst="127.0.0.1")/TCP(dport=8000,flags="S"))
这个问题有个青春版,以下的 scapy ping localhost 也无法联通
from scapy.all import *
sr1(IP(dst="127.0.0.1")/ICMP())
稍加搜索可以发现 scapy 官方 troubleshooting 有解释:
The loopback interface is a very special interface. Packets going through it are not really assembled and disassembled. The kernel routes the packet to its destination while it is still stored an internal structure. What you see with tcpdump -i lo is only a fake to make you think everything is normal. The kernel is not aware of what Scapy is doing behind his back, so what you see on the loopback interface is also a fake. Except this one did not come from a local structure. Thus the kernel will never receive it.
On Linux, in order to speak to local IPv4 applications, you need to build your packets one layer upper, using a PF_INET/SOCK_RAW socket instead of a PF_PACKET/SOCK_RAW (or its equivalent on other systems than Linux):
https://scapy.readthedocs.io/en/latest/troubleshooting.html#i-can-t-ping-127-0-0-1-or-1-scapy-does-not-work-with-127-0-0-1-or-1-on-the-loopback-interface
问题解决了?以下才是正片。
TL;DR
scapy 文档在胡说八道,它根本不懂内核。
想要让 sr1(IP(dst="127.0.0.1")/ICMP()) 连通,我们只需要
sysctl net.ipv4.conf.lo.accept_local=1
想要让 sr1(IP(dst="127.0.0.1")/TCP(dport=8000,flags="S")) 可以和本地服务握手,我们需要
sysctl net.ipv4.conf.lo.route_localnet=1
sysctl net.ipv4.conf.lo.accept_local=1
Let's debug👩‍💻
用 ICMP 举例,我们轮流使用 pwru 和 ktcpdump。
1. pwru 观察这个 icmp skb 的生命期
# pwru --output-caller 'icmp and dst host 127.0.0.1'
lo:1 127.0.0.1:0->127.0.0.1:0(icmp) ip_rcv __netif_receive_skb_one_core
lo:1 127.0.0.1:0->127.0.0.1:0(icmp) ip_rcv_core ip_rcv
lo:1 127.0.0.1:0->127.0.0.1:0(icmp) nf_hook_slow ip_rcv
lo:1 127.0.0.1:0->127.0.0.1:0(icmp) nf_ip_checksum nf_conntrack_icmpv4_error[nf_conntrack]
lo:1 127.0.0.1:0->127.0.0.1:0(icmp) __skb_checksum_complete nf_ip_checksum
lo:1 127.0.0.1:0->127.0.0.1:0(icmp) ip_route_input_noref ip_rcv_finish_core.constprop.0
lo:1 127.0.0.1:0->127.0.0.1:0(icmp) ip_route_input_slow ip_route_input_noref
lo:1 127.0.0.1:0->127.0.0.1:0(icmp) fib_validate_source ip_route_input_slow
lo:1 127.0.0.1:0->127.0.0.1:0(icmp) __fib_validate_source fib_validate_source
lo:1 127.0.0.1:0->127.0.0.1:0(icmp) ip_handle_martian_source ip_route_input_slow
lo:1 127.0.0.1:0->127.0.0.1:0(icmp) kfree_skb_reason(SKB_DROP_REASON_NOT_SPECIFIED) ip_rcv_finish_core.constprop.0
pwru 说,这个包被 lo 接受了 (ip_rcv),但是之后在 ip_rcv_finish_core 这个内核函数里被扔掉了,DROP_REASON_NOT_SPECIFIED,谢谢你。
这时候我们可以根据 --output-caller 列的输出,大致画出这个 skb 转瞬即逝之前的内核函数调用图 (funcgraph):
ip_rcv_finish_core.constprop.0() {
ip_route_input_noref() {
ip_route_input_slow() {
fib_validate_source() {
__fib_validate_source()
}
ip_handle_martian_source()
}
}
kfree_skb_reason()
}
看一眼代码,最后一个函数 ip_handle_martian_source() 是这样被调用的
martian_source:
ip_handle_martian_source(dev, in_dev, skb, daddr, saddr);
goto out;
可知 ip_handle_martian_source 并不返回错误,而是走到这里的时候已经是死路了,所以我们看上一个函数 fib_validate_source
err = fib_validate_source(skb, saddr, daddr, tos,
0, dev, in_dev, &itag);
if (err < 0)
goto martian_source;
这个好像对了,fib_validate_source 返回了 err 所以 goto matian_source 然后被扔包。
但是 fib_validate_source 和 __fib_validate_source 的代码逻辑又该如何追踪呢?
2. ktcpdump 观测 __fib_validate_source 函数的运行