TUN设备在容器环境下的调试要点

TUN调试 / 0人浏览

凌晨三点十七分,我的钉钉群像被捅了马蜂窝。运维老张连发十二条语音,每条都带着交易所机房特有的风扇轰鸣声:“哥,充值到账延迟四分钟了!链上交易广播出去,但容器里的TUN设备跟死了一样,数据包全堵在虚拟网卡里!”

我灌下最后一口冷掉的浓缩咖啡,指尖在键盘上敲出火星。这不是普通的网络故障——我们这套基于Kubernetes的虚拟币充值系统,每个Pod里都跑着一个TUN设备,专门用来劫持并转发链上节点与外部网络的流量。四分钟延迟,意味着用户充的USDT迟迟无法被节点确认,而行情每秒钟都在跳。

第一现场:TUN设备在容器里的“幽灵状态”

我跳上跳板机,kubectl get pods -n wallet-core 刷出一排Running,但其中三个Pod的RESTARTS列赫然写着“2”。用kubectl exec钻进去,先看最基础的:

bash $ ip addr show tun0 3: tun0: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UNKNOWN qlen 500 link/none inet 10.244.1.2/32 scope global tun0

状态是UP,LOWER_UP也在,但ethtool tun0直接报错——TUN设备根本不吃ethtool这套。我立刻切换思路,用cat /proc/net/dev对比流量计数:

bash $ cat /proc/net/dev | grep tun0 tun0: 123456789 987654321 0 0 0 0 0 0 0 0 0 0 0 0 0 0

收包计数在涨,但发包计数纹丝不动。这就是典型的“半死”状态:内核把包收进来了,但用户态程序(我们那个跑在Pod里的tun2socks代理)没把它们读走。老张说的“充值到账延迟”,本质就是TUN设备里的数据包堆积成了堰塞湖。

第二幕:抓包时踩中的“命名空间陷阱”

我第一反应是tcpdump -i tun0,结果在Pod里一分钟没出任何包。但宿主机上用tcpdump -i any port 8545却能抓到节点发往公网的JSON-RPC请求。这里有个致命认知差——TUN设备是三层虚拟网卡,它收发的是IP包,而tcpdump默认抓二层帧。在容器里必须加-e参数看链路层,或者直接抓ip协议:

bash $ tcpdump -i tun0 -e -nn -v tcpdump: listening on tun0, link-type EN10MB (Ethernet)

等等,link-type显示EN10MB?TUN设备明明应该是LINKTYPE_RAW或者LINKTYPE_IPV4。我立刻意识到,这个Pod里的TUN设备不是标准的/dev/net/tun创建的,而是被某个CNI插件(比如我们用的Cilium)用BPF伪装出来的。这就解释了为什么tcpdump行为诡异——它抓到的“以太网帧”其实是BPF程序重新封装的数据。

我改用tcpdump -i any -nn 'ip[9] == 6'按协议过滤,终于看到真相:大量TCP RST包从节点IP发往公网IP,源端口是8545。这不对劲——节点服务明明在监听,怎么会主动RST?

第三幕:MTU黑洞,还是路由黑洞?

我查了节点日志,发现它一直在重试连接某个外部基础设施服务(用于同步区块头)。每次连接建立后三秒内就被RST。用ping -M do -s 1472 10.244.1.1测试容器到宿主机的PMTU,发现能通;但ping -M do -s 1473直接报Frag needed。

问题来了:TUN设备的MTU是1500,但容器网络底层是VXLAN叠加网,实际有效载荷MTU只有1450。当节点发出一个1460字节的TCP段(含IP头20字节+TCP头20字节),TUN设备照单全收,但到了宿主机VXLAN隧道接口时,因为封装VXLAN头(50字节)导致总包超过1500,直接触发ICMP Fragmentation Needed。但容器里的tun2socks代理没有正确处理ICMP错误,它不会动态降低TCP MSS,导致重传风暴。

我立刻验证:在Pod里ip route show table all | grep tun0,发现有一条10.244.1.0/24 dev tun0 proto kernel scope link src 10.244.1.2,但没有任何针对外部网络的默认路由。再看iptables -t mangle -L OUTPUT,果然有个规则把TCP SYN的MSS值固定为1460,完全没考虑VXLAN开销。

第四幕:修复时刻——绕开“伪TUN”的坑

我决定不跟BPF较劲,直接改tun2socks的配置。这个代理支持--mtu参数,我把它从1500改成1400,并开启--tcp-moderate选项,让它主动协商MSS。同时,在Pod里加一条路由规则:

bash $ ip route add default dev tun0 table 100 $ ip rule add from 10.244.1.2 lookup 100 priority 500

这样所有从Pod发出的流量都会强制走TUN设备,而不是被宿主机的策略路由截胡。但更关键的是,我发现tun2socks的缓冲区设置有问题——它默认的--sndbuf和--rcvbuf只有64KB,在币圈这种高频交易场景下,节点每秒要推送几百条交易广播,缓冲区瞬间被打满。

我把两个参数都提到1MB,并加上--udp-timeout 30(避免DNS查询的UDP包在TUN里滞留)。重启Pod后,观察/proc/net/dev,发包计数开始以每秒几千个的速度跳动。

第五幕:从“能用”到“可观测”——调试的终极武器

光修好不行,得能持续监控。我写了个简单的Go程序,用golang.org/x/sys/unix直接读取TUN设备的TUNGETSTATS ioctl,拿到每个Pod的丢包数、队列深度。然后通过Prometheus exporter暴露出来,在Grafana上拉了张图。

但真正让我后背发凉的,是抓到一个隐藏的“僵尸连接”。用ss -tnp | grep tun0,发现有个连接状态是SYN-SENT,但源端口是随机高位端口,持续了整整两小时。查进程PID,发现是节点里一个旧版geth进程,它没走tun2socks,而是直接尝试用原始socket连外部。这个进程的流量绕过了TUN设备,但内核路由又把它送进了tun0——于是包在TUN设备里转了一圈又回来,形成死循环。

我果断kill -9那个僵尸进程,并更新了节点的启动脚本,强制设置--ipcdisable和--nat=extip:10.244.1.2,确保所有外部流量必须经过TUN设备。同时,在K8s的Pod安全策略里加了readOnlyRootFilesystem: true,防止节点运行时往/dev/net/tun写奇怪的东西。

第六幕:复盘时刻——容器TUN调试的“三板斧”

凌晨五点四十分,充值延迟降到800毫秒,老张在群里发了三个磕头表情。我关掉终端,把这次踩坑经验浓缩成三句话:

第一板斧:永远先确认TUN设备是“真TUN”还是“BPF仿冒品”。 用ip link show看link-type,如果是EN10MB,恭喜你,所有基于AF_PACKET的抓包工具都会骗你。必须用tcpdump -i any或者直接读/proc/net/dev。

第二板斧:MTU不是玄学,是数学。 容器网络叠加层(VXLAN/Geneve/IPIP)每层都会吃掉几十字节。TUN设备的MTU必须等于“物理网卡MTU - 所有隧道头长度 - 20字节IP头 - 20字节TCP头”的余量。宁可设小10%,不要设大1%。

第三板斧:用户态代理的缓冲区决定生死。 虚拟币节点每秒产生的交易广播量是普通Web服务的百倍。tun2socks这类工具默认的64KB缓冲区在币圈场景就是个笑话。至少设到512KB,同时要监控/proc/net/dev的tx_queue和rx_queue,一旦非零持续超过5秒,立刻查代理进程的CPU和内存。

窗外天已经亮了,我盯着Grafana上那条平滑的延迟曲线,突然想起老张最开始说的“四分钟延迟”。其实那四分钟里,TUN设备里的数据包堆积了大概2.3GB——如果当时没修好,再过半小时,Pod的内存就会被这些待处理的skb撑爆,然后K8s OOMKill,整个充值服务直接雪崩。

现在,我终于可以安心地把这个案例写进我们的故障复盘文档了。但我知道,币圈的行情不会等任何人,下一个凌晨三点,可能还有新的坑在等着我。

版权声明:

作者: 最新鸿蒙OS VPN免费节点分享

链接: https://harmonyosvpn.com/tun-debug/tun-debug-container-env.htm

来源: harmonyosvpn.com

文章版权归作者所有,未经允许请勿转载。

最新文章

归档

标签