问题现象
需求是将服务的UDP流量从机器A切换到机器B. 路由器操作前机器A和机器B相关服务已准备就绪. 切换期间上游会一直有流量过来.
路由器设置端口转发, UDP协议, 外部端口保持不变的情况下改变内部IP, 保存后不生效, UDP包仍然会发送到原来的内网IP.
在机器A上运行tcpdump
:
1 | 06:13:05.130930 IP (tos 0x28, ttl 61, id 60828, offset 0, flags [none], proto UDP (17), length 176) |
注意此时机器A上已没有服务监听在目标端口, 已通过iptables
DROP来源包, 否则会有ICMP不可达报文
1 | 05:48:51.052956 IP (tos 0xc8, ttl 64, id 24675, offset 0, flags [none], proto ICMP (1), length 204) |
在机器B(新的目标机)运行tcpdump
接收不到包.
问题排查
机器A和B本地排查无果, 登录路由器进行排查.
运行conntrack -L
:
1 | udp 17 47 src=[远端机器的IP] dst=[路由器公网侧IP] sport=51820 dport=53820 src=[机器A的IP] dst=[远端机器的IP] sport=51820 dport=51820 [ASSURED] mark=0 use=1 |
运行iptables -t nat -vnL
1 | 0 0 DNAT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:53820 to:192.168.50.3:53820 |
可以看到端口转发配置是生效的, 但是因为有conntrack规则的存在所以后续来的包并没有被当成”新的链接”走iptables, 而是继续按照conntrack中的规则进行转发.
这里比较怀疑这个特定版本的conntrack可能经过魔改, 查到的标准是180秒内如果没有回包, conntrack规则就应该被移除. 但实际上在路由器上可以看到:
1 | udp 17 1 src=##.##.##.## dst=##.##.##.## sport=51820 dport=53820 src=##.##.##.## dst=##.##.##.## sport=51820 dport=51820 [ASSURED] mark=0 use=1 |
很明显, 规则定时归0时没有被删除而是重置了倒计时.
解决方案
在路由器上, 使用命令删除这条规则: conntrack -D -p udp --dport 53820
删除后马上在机器B的tcpdump上就可以看到来自远端机器的UDP流量了.
参考
portmap: delete UDP conntrack entries on teardown · Issue #123 · containernetworking/plugins
在CNI github issue里找到了一个类似的问题, 带udp端口转发的pod移除的时候需要手动调conntrack删除掉NAT规则, 否则流量将无法分配到新的pod上. 但这种场景里路由器(上层NAT设备)一般是不在控制范围内的, 可能除了在远端发起换端口之外没有任何办法了…
netfilter: Kill unreplied conntracks by ICMP errors
这里有一个patch提议说可以用ICMP错误回包来剔除掉netfilter conntrack里无效的规则, 但应该没有被merge到linux kernel里.
Linux Packet Filtering and iptables - 7.5. UDP connections
Iptables Tutorial 1.2.1 - 7.5. UDP connections
The conntrack-tools user manual
Conntrack tales - one thousand and one flows
Connection Tracking (conntrack): Design and Implementation Inside Linux Kernel