openclaw-gateway 的 Telegram 和微信通道同时挂了。日志里全是 TypeError: fetch failed 和 UND_ERR_CONNECT_TIMEOUT,每隔 30 秒循环一次。两个完全不同的 API 端点同时挂,第一反应就是网络层出了问题。
症状
gateway 跑在一台 Intel MacBook Pro 上(以下简称 mbp),Node.js 25.5.0。错误日志长这样:
[openclaw-weixin] weixin getUpdates error (3/3): TypeError: fetch failed[openclaw-weixin] weixin getUpdates: 3 consecutive failures, backing off 30s[telegram] fetch fallback: enabling sticky IPv4-only dispatcher (codes=UND_ERR_CONNECT_TIMEOUT)[telegram] fetch fallback: DNS-resolved IP unreachable; trying alternative Telegram API IPTelegram 的错误更详细——能看到 undici 在尝试各种 fallback:先切 IPv4-only,再试备用 IP,全部超时。微信那边更简单粗暴,直接 fetch failed。
第一轮排查:IPv4 vs IPv6
先用 curl 分别测 IPv4 和 IPv6:
curl -4 -sv --connect-timeout 10 https://api.telegram.org 2>&1 | head -5* Trying 149.154.166.110:443...* Connected to api.telegram.org (149.154.166.110) port 443* SSL connection timeoutIPv4:TCP 能连上,但 TLS 握手超时。ClientHello 发出去了,服务器不回 ServerHello。
curl -6 -sv --connect-timeout 10 https://api.telegram.org 2>&1 | head -5* Trying [2001:67c:4e8:f004::9]:443...* Connected to api.telegram.org (2001:67c:4e8:f004::9) port 443* (304) (IN), TLS handshake, Server hello (2):IPv6:秒通。
再验证 Node.js:
node -e "const dns = require('dns');dns.setDefaultResultOrder('verbatim');fetch('https://api.telegram.org').then(r => console.log('OK', r.status)) .catch(e => console.log('FAIL', e.message));"OK 200把 DNS 结果顺序设为 verbatim(系统返回什么顺序就用什么),Node.js 就能连了。因为系统 DNS 返回 IPv6 在前,verbatim 保持了这个顺序,所以走了 IPv6。
到这里看起来像是 Node 25.x 的 undici 双栈处理 bug——默认先尝试 IPv4,卡在那里不回退。但这只是表象,真正的问题是:为什么 IPv4 不通?
第二轮:这不是某个站的问题
测更多站点:
curl -4 --noproxy '*' -s --connect-timeout 5 -o /dev/null -w '%{http_code}' https://www.google.com# 000curl -4 --noproxy '*' -s --connect-timeout 5 -o /dev/null -w '%{http_code}' https://www.apple.com# 000curl -4 --noproxy '*' -s --connect-timeout 5 -o /dev/null -w '%{http_code}' https://www.cloudflare.com# 000所有 IPv4 HTTPS 都不通。 不只是 Telegram 和微信。
再测 ping:
ping -c 2 8.8.8.8# 64 bytes from 8.8.8.8: icmp_seq=0 ttl=119 time=18.019 msPing 正常。所以 ICMP 通、IPv6 通、IPv4 TCP connect 能完成三次握手、但数据传输被吞。
第三轮:同一 LAN,不同设备
同一局域网的 Mac Mini(以下简称 mini):
# 从 mini 测试curl -4 -s --connect-timeout 5 https://www.google.com -o /dev/null -w '%{http_code}'# 200mini 完全正常。同一个路由器、同一条宽带,mbp 不通 mini 通。
再加上一台 Raspberry Pi(以下简称 rp),它通过 OpenWrt 做 AP 桥接回 Fios 路由器:
# 从 rp 测试curl -4 -s --connect-timeout 5 https://www.google.com -o /dev/null -w '%{http_code}'# 000rp 也不通。两台不通,一台通。
第四轮:发现 Tailscale 配置异常
三台设备都跑了 Tailscale。对比配置:
tailscale debug prefs | grep -E "RouteAll|AdvertiseRoutes|ExitNodeID"| 设备 | RouteAll | AdvertiseRoutes | ExitNodeID |
|---|---|---|---|
| mbp (不通) | true | ['0.0.0.0/0', '::/0'] | "" |
| rp (不通) | true | ['0.0.0.0/0', '::/0'] | "" |
| mini (正常) | true | null | "" |
关键区别:mbp 和 rp 都在广播自己为 exit node(AdvertiseRoutes: 0.0.0.0/0),同时 RouteAll: true 但 ExitNodeID 为空。
这是 Tailscale GitHub #18923 记录的一个已知 bug:macOS GUI 关闭 exit node 后没有清除 RouteAll 标志。RouteAll: true + 空 ExitNodeID = Tailscale 会安装 blackhole 路由,把 IPv4 流量吞掉。
修复方式:
tailscale up --exit-node="" --reset注意必须用 --reset,单独 tailscale set --exit-node= 不会清除 RouteAll。
第五轮:修了 Tailscale,但问题没解决
在 rp 上执行修复:
sudo tailscale up --exit-node="" --resettailscale debug prefs | grep RouteAll# "RouteAll": false,配置确认清除了。重启 tailscaled:
sudo systemctl restart tailscaled再测:
curl -4 -s --connect-timeout 8 -o /dev/null -w '%{http_code}' https://www.google.com# 000还是不通。
更激进——直接停掉 Tailscale:
sudo systemctl stop tailscaledsystemctl is-active tailscaled# inactive检查 iptables:
Chain INPUT (policy ACCEPT)Chain FORWARD (policy ACCEPT)Chain OUTPUT (policy ACCEPT)干干净净,只有 Docker 的默认规则。没有 Tailscale 残留。
再测:
curl -4 -s --max-time 5 -o /dev/null -w '%{http_code}' http://httpbin.org/ip# 000Tailscale 完全停了,iptables 干净,IPv4 仍然不通。
第六轮:排除 Fios 路由器
通过 SSH tunnel 连进 Fios 路由器管理界面,查看所有日志:
- Security Log:只有 Web 登录记录
- Firewall Log:只有入站拦截(外部 ICMPv6 探测被拒),零出站阻断
- Access Control:空的,没有规则
- Advanced Log:WiFi association/steering 事件
Fios 没有封锁任何出站流量。
重启 Fios 路由器后再测——mbp 和 rp 仍然不通,mini 仍然正常。
第七轮:用 strace 看清真相
在 rp 上用 strace 抓 curl 的系统调用:
sudo strace -e trace=network curl -4 --noproxy '*' -sv --max-time 5 http://example.com 2>&1connect(5, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("104.18.26.120")}, 16) = -1 EINPROGRESSgetsockopt(5, SOL_SOCKET, SO_ERROR, [0], [4]) = 0sendto(5, "GET / HTTP/1.1\r\nHost: example.co"..., 75, MSG_NOSIGNAL, NULL, 0) = 75# ... 然后什么都没有,5 秒后超时TCP 三次握手完成(connect 成功),HTTP 请求 75 字节成功写入 socket(sendto 返回 75),然后……内核从未收到任何响应数据。
用 Python raw socket 在 mini 和 rp 上做对比:
import sockets = socket.socket(socket.AF_INET, socket.SOCK_STREAM)s.settimeout(5)s.connect(("104.18.26.120", 80))s.send(b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n")data = s.recv(200)print("RECV:", data[:100])mini:
RECV: b'HTTP/1.1 200 OK\r\nDate: Tue, 31 Mar 2026 21:49:39 GMT...'rp:
TimeoutError: timed out同样的代码、同样的目标 IP、同一个路由器出去——mini 收到响应,rp 什么都收不到。
第八轮:关掉 mbp 的 Tailscale
mbp 的 SSH 走的是 Tailscale,直接 tailscale down 会断开连接。用 ProxyJump 绕过:
# 从 mini 跳到 mbp 的 LAN IPssh -o ProxyJump=mini lishuyu@192.168.1.183 \ "tailscale down --accept-risk=lose-ssh"然后测试:
ssh -o ProxyJump=mini lishuyu@192.168.1.183 \ "curl -4 --noproxy '*' -s --max-time 8 -o /dev/null -w '%{http_code}' http://httpbin.org/ip"# 000mbp 关掉 Tailscale 后,IPv4 仍然不通。
排除清单
到这里排除了几乎所有常见嫌疑:
| 假设 | 排除方式 |
|---|---|
| Tailscale blackhole 路由 | 停掉 tailscaled + 检查 iptables/路由表 |
| Tailscale Network Extension | macOS 上完全 tailscale down |
| Fios 防火墙 | 日志无出站阻断 |
| Fios NAT 状态 | Fios 重启后问题依旧 |
| CGNAT | 同一公网 IP 出去,mini 通 mbp 不通,CGNAT 无法区分 |
| ClashX 代理 | rp 上没有 ClashX 也不通 |
| iptables/pf 残留 | rp 干净的 iptables,mbp 关了 Tailscale |
| DNS 问题 | 三台设备解析结果一致 |
排了 8 轮,一度以为走进了死胡同。直到翻出两周前的排查记录——同一台 rp,同样的症状,已经找到过根因了。
真凶:ICMP Redirect 触发 Fios MAC 封锁
3 月 17 日在 rp 上排查过完全相同的问题。当时的发现:
现象链
- Docker 和 Tailscale 都需要
ip_forward=1 - Linux 默认
net.ipv4.conf.all.send_redirects=1 - 当
ip_forward=1+send_redirects=1时,设备会向网关发送 ICMP Type 5 (Redirect) 消息——告诉路由器”这个包可以直接发给目标,不用经过我” - Verizon Fios 路由器收到 ICMP Redirect 后,将发送方的 MAC 地址静默封锁
- 封锁方式:允许 TCP SYN/SYN-ACK(三次握手正常),但丢弃后续的 TCP 数据包
- ICMP ping 不受影响,IPv6 不受影响——只针对 IPv4 TCP 数据传输
这就解释了所有观察到的症状:connect 成功、sendto 成功、但 recv 永远超时。
验证:MAC 轮换立即恢复
上次在 rp 上的验证:
# 查看当前 MACcat /sys/class/net/wlan0/address# 2c:cf:67:46:7f:ba
# 换一个 MACsudo ip link set wlan0 downsudo ip link set dev wlan0 address 2c:cf:67:46:7f:bbsudo ip link set wlan0 up
# 等待重新关联和 DHCPsleep 5
# 立即恢复curl -4 -s http://1.1.1.1 -o /dev/null -w '%{http_code}'# 301换了 MAC 的最后一个字节,IPv4 立刻恢复。确认是 Fios 路由器按 MAC 地址做的封锁。
长期修复
禁止设备发送 ICMP Redirect:
net.ipv4.conf.all.send_redirects = 0net.ipv4.conf.default.send_redirects = 0sudo sysctl -p /etc/sysctl.d/99-no-redirects.conf这样即使 ip_forward=1,设备也不会发 ICMP Redirect,Fios 路由器就不会触发 MAC 封锁。
为什么这次又中招了
rp 在这两周内重启过(Tailscale 掉线、Docker 更新等),99-no-redirects.conf 可能在某次系统更新或配置重置中丢失了。ip_forward 被 Docker 和 Tailscale 自动开启,send_redirects 回到了默认的 1,ICMP Redirect 重新触发,Fios 再次封锁 MAC。
mbp 的情况类似——Tailscale exit node 广播 0.0.0.0/0 加上 Parallels Desktop 的网桥(bridge100),都需要 IP forwarding。虽然 macOS 的 net.inet.ip.forwarding 检查结果是 0,但 Tailscale 的 Network Extension 和 Parallels 的虚拟网络可能通过其他机制转发了包含 ICMP Redirect 的流量。
为什么 Fios 路由器对 ICMP Redirect 反应这么极端?
这其实不是 bug,而是一种安全防护。ICMP Redirect 消息可以被用来做中间人攻击——恶意设备发送伪造的 Redirect 告诉路由器”把流量发给我”。Fios 路由器检测到 LAN 内设备发送 Redirect 后,判定该设备可能在进行 ARP/ICMP 劫持,于是对其实施 MAC 级别的流量过滤。
这个防护逻辑本身没问题,问题在于:
- 完全不记日志——Firewall Log、Security Log 里都没有任何记录
- 不通知用户——设备列表里也没有标记
- 只断数据不断连接——TCP 握手正常,ping 正常,让你觉得网络是通的
- 重启路由器不清除——MAC 黑名单持久化存储
这种”隐形封锁”让排查极其困难。如果不是上次偶然试了 MAC 轮换,可能到现在还在怀疑 Tailscale 或 CGNAT。
修复清单
rp (Linux)
# 1. 确认 sysctl 配置存在cat /etc/sysctl.d/99-no-redirects.conf# net.ipv4.conf.all.send_redirects = 0# net.ipv4.conf.default.send_redirects = 0
# 2. 如果不存在,创建它echo -e "net.ipv4.conf.all.send_redirects = 0\nnet.ipv4.conf.default.send_redirects = 0" | \ sudo tee /etc/sysctl.d/99-no-redirects.conf
# 3. 应用sudo sysctl -p /etc/sysctl.d/99-no-redirects.conf
# 4. 轮换 MAC 解除封锁sudo ip link set wlan0 downsudo ip link set dev wlan0 address $(python3 -c "import random; print(':'.join(['%02x' % (random.randint(0,255) if i else random.randint(0,255) & 0xfe | 0x02) for i in range(6)]))")sudo ip link set wlan0 upmbp (macOS)
macOS 上禁用 ICMP Redirect:
sudo sysctl -w net.inet.ip.redirect=0持久化需要创建 Launch Daemon。轮换 MAC:
# macOS 使用 Private Wi-Fi Address 功能可以自动轮换# 或手动:sudo ifconfig en0 ether $(openssl rand -hex 6 | sed 's/\(..\)/\1:/g; s/.$//')Tailscale exit node 清理
虽然不是直接原因,但 RouteAll: true + 空 ExitNodeID 的配置残留也应该清理:
tailscale up --exit-node="" --reset这是 Tailscale #18923 记录的 bug——macOS GUI 关闭 exit node 后不清除 RouteAll。用 --reset 可以正确重置。
经验总结
-
Docker + Tailscale 在家用路由器后面是高危组合。两者都开
ip_forward,而家用路由器(至少 Verizon Fios G3100)会把 ICMP Redirect 视为攻击行为。任何跑 Docker 的 Linux 设备连到 Fios WiFi 上,都应该预防性地关闭send_redirects。 -
“隐形封锁”是最难排查的问题类型。TCP 握手正常让你以为连接没问题,ping 正常让你排除了网络层,而路由器日志里什么都没有。当排除了所有软件层因素后,该考虑”网络设备本身在做什么”。
-
跨会话记忆很重要。这个问题两周前已经解决过一次。如果不是翻出了上次的排查记录,可能又要花好几个小时走完同样的弯路。
-
排查方法论回顾:
curl -4vscurl -6分离 IPv4/IPv6 问题- 同一 LAN 不同设备对比(控制变量)
strace -e trace=network定位到系统调用层ssh -o ProxyJump=绕过 Tailscale 依赖做独立测试- MAC 轮换作为”是否被路由器封锁”的终极验证
关联文章
这台 rp 和这个网络环境之前也出过不少幺蛾子:
- Tailscale DNS 死锁 — Tailscale 接管 DNS 后自身掉线形成死锁
- Raspberry Pi OpenWrt WiFi 排查 — WPA 握手超时
- 斐讯 N1 OpenWrt 更新 Tailscale — 静态二进制更新踩坑