WSL2和podman
shixudong163.com在《关于PREROUTING处理自发自收包的新认识》一文中提到Linux主机在使用127.0.0.1访问桥模式docker时需通过OUTPUT链实现DNAT同时还应启用docker0的route_localnet,并对源IP127.0.0.1进行MASQUERADE。这一系列动作实际上是docker自动完成的Docker还针对WSL2mirrored进行了适配确保开箱即用无需人工干预。Podman也是一款容器引擎以root权限运行时桥模式下数据包转发行为和docker基本一致。对于nat或bridged模式下的WSL2使用127.0.0.1访问podman也是开箱即用和普通Linux主机没有区别。然而对于mirrored模式却并不是如此需要做一些额外定制否则WSL2就无法使用127.0.0.1访问podman。目前影响WSL2mirrored上podman的开箱即用除了mirrored模式本身的问题还和WSL2使用的内核版本有关。对于内核版本不高于6.6的WSL2症状详见github 13868。简而言之Win11主机能使用127.0.0.1访问podmanWSL2自身反而不行原因在于WSL2访问podman的数据包没有被MASQUERADE。Podman的MASQUERADE分两步实现先在DNAT处对需要MASQUERADE的数据包打mark而后在POSTROUTING处再对已打mark的数据包进行MASQUERADE这对于普通Linux完全没有问题。然而WSL2mirrored比较特殊需在filter表OUTPUT链对WSL2自身发出的数据包打mark这一动作发生在OUTPUT/DNAT之后POSTROUTING/MASQUERADE之前刚好覆盖掉了podman先前在OUTPUT/DNAT处打的mark导致后续无法对WSL2访问127.0.0.1的数据包进行MASQUERADE。而Win11主机访问127.0.0.1的数据包在PREROUTING/DNAT处打mark且全程无需经过OUTPUT链能顺利进行MASQUERADE所以反而不受影响。找到原因就能对症下药解决办法很简单。在POSTROUTING处强制对来自127.0.0.1的数据包进行MASQUERADE执行sudo iptables -t nat -A POSTROUTING -s 127.0.0.1 -o podman0 -j MASQUERADE即可实现WSL2自身使用127.0.0.1访问podman。至于docker也是直接在POSTROUTING处对来自127.0.0.1的数据包进行MASQUERADE所以不存在podman的上述问题。由于我在用的WSL2是最新版本使用6.18内核在解决前述问题时竟然有了意外发现。在执行上述命令前WSL2上执行nc -w 5 -v 127.0.0.1 8080结果总是TIMEOUT执行上述命令后WSL2上nc就能成功连接到这里一切都正常也证明了解决办法的有效性。然而如在WSL2上执行curl http://127.0.0.1:8080居然无法成功。对比测试发现使用6.6内核的WSL2执行上述命令后nc能成功连接curl也能成功连接。通过conntrack、tcpdump、pwru、bpftrace以及chatgpt等多管齐下并对比内核源码终于找到了问题的根本原因。6.7内核对nf_nat_ipv4_local_in函数做了较大修改详见该函数注释针对WSL2mirrored这种特殊情形如果继续通过OUTPUT链实现DNATWSL2自身就再也无法使用127.0.0.1访问podman实际上也包括docker详细原因分析如下。在同一台linux机器上连接跟踪功能保证了数据包不管是否需要NAT转换都会在conntrack表留下跟踪记录。具体过程为首先在进入PREROUTING/OUTPUT时调用nf_conntrack_in创建记录然后在离开POSTROUTING/INPUT时调用nf_conntrack_confirm确认记录并插入conntrack表。对于PREROUTING/OUTPUT处的DNAT如DNAT到不同的机器同一个数据包需要遍历该台机器的POSTROUTING/SNAT如DNATREDIRECT到同一台机器同一个数据包需要遍历该台机器的INPUT/SNAT。至于相反方向的数据包则直接快速匹配先前的conntrack记录并进行相应逆转换操作。WSL2mirrored的特殊之处在于WSL2访问127.0.0.1的数据包在OUTPUT处进行DNAT转换然后直接到达podman并在conntrack表留痕。其返回包则快速匹配先前的记录并进行NAT逆转换此时返回包的目标IP为127.0.0.1然而该127.0.0.1实际上对应Win11主机的127.0.0.1而非WSL2自身因此总是要到Win11绕一圈才能返回WSL2。当该数据包从Win11再次返回WSL2进入PREROUTING链调用nf_conntrack_in时就conntrack状态来说事实上已经是新数据包并且无法匹配先前的conntrack记录因此将重新生成新的conntrack记录。随后INPUT/SNAT为了避免和先前WSL2访问127.0.0.1的那条记录发生冲突随机修改了新conntrack记录的源端口并将返回数据包的源端口变更为修改后源端口因此后续tcp_v4_rcv既无法关联到先前WSL2访问127.0.0.1的socket也关联不到侦听socket只能发出RST包导致连接失败。使用6.6内核的WSL2其实也存在同样的问题不过由于tcp_early_demux开关默认为1数据包经过PREROUTING/DNAT之后将强制调用tcp_v4_early_demux提前查找socket。由于此时返回数据包尚未经过INPUT/SNAT仍使用初始源端口故能关联到先前WSL2访问127.0.0.1的socket。后续tcp_v4_rcv无需重新查找而是直接使用这个socket因此curl http://127.0.0.1:8080能成功连接。6.7内核修改了INPUT/SNAT对应的nf_nat_ipv4_local_in函数数据包执行完SNAT后立即判断源IP和源端口是否发生改变如改变则调用skb_orphan作废先前tcp_v4_early_demux获取的socket后续导致tcp_v4_rcv需要重新查找socket并触发RST后果便是curl http://127.0.0.1:8080报错“连接被对方重置”。6.6内核的nf_nat_ipv4_local_in函数只在源IP发生改变后才去调用skb_orphan作废先前tcp_v4_early_demux获取的socket因此不受源端口改变带来的影响curl能成功连接。但如果临时禁用tcp_early_demux开关后续同样会导致tcp_v4_rcv关联不到socket并触发RST结果便是curl也会出现同样报错。至于nc -w 5 -v 127.0.0.1 8080命令只涉及握手包当从podman返回的SYNACK包从Win11再次返回WSL2时由于conntrack状态已经重置属于新数据包。然而内核连接跟踪模块一旦发现新数据包带SYNACK标记就认定为异常情形并立即作废刚生成的conntrack记录导致SYNACK包后续不会被INPUT/SNAT处理也就不存在源端口转换问题。因此无论内核是否禁用tcp_early_demux开关该SYNACK包都能被tcp_v4_rcv正确处理并且也不受内核版本影响只要MASQUERADE到位nc总是能成功连接。根据以上分析其实前面已经有了结论如果通过OUTPUT链实现DNAT6.18内核的WSL2mirrored再也无法使用127.0.0.1访问podman。然而天无绝人之路正是由于WSL2访问127.0.0.1的数据包总是要到Win11绕一圈当数据包从Win11再次返回WSL2时已经不属于自发自收包可以通过PREROUTING链实现DNAT。针对WSL2mirrored的这种特殊情形可在WSL2的raw表PREROUTING链添加一条规则sudo iptables -t raw -I PREROUTING -p tcp --dport 8080 -j CT --zone-orig 1详见《关于PREROUTING处理自发自收包的新认识》同时必须作废podman自带的OUTPUT链DNAT规则否则WSL2访问127.0.0.1的数据包仍然优先去匹配OUTPUT链DNAT规则。如podman使用iptables可使用sudo iptables -t nat -I OUTPUT -p tcp --dport 8080 -j ACCEPT作废OUTPUT链DNAT规则如podman使用nftables则需要采用相应的nft命令。也可以直接使用sudo iptables -t raw -I OUTPUT -p tcp --dport 8080 -j CT --notrack而无需考虑podman使用何种后端。另外既然OUTPUT链DNAT规则已经作废先前那条针对6.6内核WSL2的sudo iptables -t nat -A POSTROUTING -s 127.0.0.1 -o podman0 -j MASQUERADE也就失去了存在意义。现在再回过头来看其实conntrack -L已经给出了蛛丝马迹。WSL2mirrored通过OUTPUT链实现DNAT在使用6.6内核时curl http://127.0.0.1:8080能成功连接并生成两条conntrack记录。一条为带DNAT信息的[ASSURED]记录对应WSL2通过OUTPUT/DNAT发往podman的双向数据流另一条为不带DNAT信息的[UNREPLIED]记录对应podman返回包绕行win11主机后发往WSL2的单向数据流这条[UNREPLIED]记录也能体现返回包源端口的变化。而在使用6.18内核时虽然curl同样也生成了两条conntrack记录可用conntrack -E验证,但TCP随后发出的RST又立即删除了那条[UNREPLIED]记录导致conntrack -L只能显示一条带DNAT信息的[ASSURED]记录这也意味着podman返回包绕行win11主机后无法发往WSL2所以curl不能成功连接。改用PREROUTING链实现DNAT后无论哪个内核版本curl http://127.0.0.1:8080都能成功连接并产生两条[ASSURED]记录。一条带zone-orig和DNAT信息对应WSL2绕行Win11主机后通过PREROUTING/DNAT发往podman的双向数据流另一条不带DNAT信息对应WSL2发往win11主机的双向数据流。至于nc -w 5 -v 127.0.0.1 8080如使用PREROUTING/DNATpodman返回的SYNACK包从Win11再次返回WSL2时由于能匹配到先前外出记录不再被认定为新数据包所以和curl一样无论哪个内核版本都能成功连接也能产生两条 [ASSURED]记录。如使用OUTPUT/DNATpodman返回的SYNACK包从Win11再次返回WSL2时无法匹配先前记录属于带SYNACK标记的新数据包被认定为异常因此该新数据包不会产生conntrack记录但并不影响后续tcp_v4_rcv。同时由于nc只涉及握手包不再有另外的返回包因此无论哪个内核版本nc也都能成功连接并且只能产生一条带DNAT信息的[ASSURED]记录。WSL2高版本内核的以上分析同样适用于docker。使用6.18内核的WSL2mirrored今后也只能采用在raw表PREROUTING链添加规则同时作废OUTPUT/DNAT规则并通过PREROUTING/DNAT实现WSL2自身使用127.0.0.1访问Docker。