修真凶:4G 链路静默断连导致隧道"假活"(用户撞上 Error 1033)

现象:用户早上打开 relay.xxmxxs.top 看到 Cloudflare Error 1033。
      watchdog 日志把过程记下来了:
        公网不通(HTTP 000,ha_connections=2)  <- 隧道显示连着,公网不通
        公网不通(HTTP 502,ha_connections=2)
        公网恢复(HTTP 200)                     <- 自己又好了
      也就是说:**间歇性假活**,短样本测不出来。
      我之前只测 15~25 次就说"可用率 100%",太草率了。

根因:4G 链路会**静默断开** TCP 连接 —— 不发 FIN/RST,双方都以为还连着。
      Cloudflare 把请求发到这条僵尸连接上,就回 502/530/1033。
      Linux 默认 tcp_keepalive_time = **7200 秒(2 小时)**才发现连接死了,
      所以中间这段时间服务一直"看着在线、实际打不开"。

      tunnel.log 最近 3000 行的佐证:
        i/o timeout              212 次
        Retrying connection      127 次
        Lost connection           22 次

修法:
  1. TCP keepalive 调到 30 秒 + 每 10 秒探测 3 次
     => 最多约 60 秒发现僵尸连接,cloudflared 立刻重连
     (/etc/sysctl.d/97-ykxmz-keepalive.conf)
  2. 隧道连接数 2 -> 4,多几条冗余
  3. 修 relay 从不清理设备记录的问题:
     self.devices 只增不减,被控端每换端口/重装就多一条,
     实测一天攒了 75 条僵尸记录,/v1/health 从几 KB 涨到 20KB。
     加 DEVICE_TTL(6 小时)在 snapshot() 里清掉。

实测(修完立刻验证):
    设备侧  公网 15/15 HTTP 200
    Windows 公网 25/25,中位 520 ms,最快 463 ms
    health  20931 字节 / 75 台  ->  152 字节 / 0 台
This commit is contained in:
xmz7891
2026-10-07 09:15:27 +08:00
parent 2b40968d23
commit 8c4ee6df08
9 changed files with 603 additions and 50 deletions
+137
View File
@@ -0,0 +1,137 @@
#!/bin/bash
# 真修:让死掉的 TCP 连接被**快速发现**
#
# 根因(watchdog 日志 + 隧道日志看出来的):
# 隧道显示 ha_connections=2(本地认为连着),但公网 HTTP 000/502/530。
# 隧道日志最近 3000 行里 i/o timeout 212 次、Retrying 127 次。
#
# => 4G 链路会把 TCP 连接**静默断开**:连接实际死了,
# 但双方都没有收到 FIN/RST,于是都以为还连着。
# Cloudflare 把请求发到这条"僵尸连接"上,就回 502/530/1033。
#
# Linux 默认参数:
# tcp_keepalive_time = 7200 秒(2 小时!)才发现连接死了
# tcp_keepalive_intvl = 75 秒
# tcp_keepalive_probes = 9 次
# => 最坏要 2 小时才知道连接没了。中间这段时间服务就是"假活"。
#
# 修法:
# 1. 把 keepalive 调到 30 秒 + 10 秒探测 3 次
# => 最多 ~60 秒发现僵尸连接,cloudflared 立刻重连
# 2. ha-connections 从 2 提到 4,多几条冗余,一条死了还有别的
# 3. 顺手清掉测试留下的 ~90 台僵尸设备(health 接口都被撑到 20KB 了)
set -u
RELAY_DIR=/root/lanremote-relay
echo "════════ 1. 改 TCP keepalive(核心修复)════════"
cat > /etc/sysctl.d/97-ykxmz-keepalive.conf <<'EOF'
# ykxmz:快速发现"静默断开"的 TCP 连接
#
# 背景:这台设备走 4G,运营商链路会静默丢连接(不发 FIN/RST)。
# Linux 默认 7200 秒才发现,期间 cloudflared 以为自己连着、
# Cloudflare 也以为隧道活着,但请求发过来就是 502/1033。
# 把 keepalive 调到 30 秒,最多一分钟就能发现并重连。
net.ipv4.tcp_keepalive_time = 30
net.ipv4.tcp_keepalive_intvl = 10
net.ipv4.tcp_keepalive_probes = 3
EOF
sysctl -p /etc/sysctl.d/97-ykxmz-keepalive.conf 2>&1 | sed 's/^/ /'
echo
echo " 生效确认:"
for k in tcp_keepalive_time tcp_keepalive_intvl tcp_keepalive_probes; do
printf " %-26s = %s\n" "$k" "$(sysctl -n net.ipv4.$k)"
done
echo
echo "════════ 2. 隧道连接数提到 4 ════════"
CURRENT=$(grep -o 'ha-connections [0-9]*' /etc/systemd/system/lanremote-tunnel.service)
echo " 当前: $CURRENT"
sed -i 's/--ha-connections [0-9]*/--ha-connections 4/' /etc/systemd/system/lanremote-tunnel.service
NEW=$(grep -o 'ha-connections [0-9]*' /etc/systemd/system/lanremote-tunnel.service)
echo " 改为: $NEW"
echo
echo "════════ 3. 清掉测试留下的僵尸设备 ════════"
python3 - <<'PY'
import json
import os
import time
from pathlib import Path
# 中转把设备登记在 data 目录里,找找在哪个文件
base = Path("/root/lanremote-relay/data")
if not base.is_dir():
print(" 没有 data 目录")
raise SystemExit(0)
for path in sorted(base.glob("*.json")):
try:
data = json.loads(path.read_text(encoding="utf-8"))
except Exception as exc:
print(" %s 读不了: %s" % (path.name, exc))
continue
if not isinstance(data, dict):
print(" %s 不是字典,跳过" % path.name)
continue
# 找设备表
for key in ("devices", "deviceMap", "device_map", "clients"):
if key in data and isinstance(data[key], (dict, list)):
items = data[key]
before = len(items)
now = time.time()
if isinstance(items, dict):
keep = {k: v for k, v in items.items()
if isinstance(v, dict)
and now - float(v.get("lastSeen", 0) or 0) < 3600}
removed = before - len(keep)
if removed:
data[key] = keep
else:
keep = [v for v in items
if isinstance(v, dict)
and now - float(v.get("lastSeen", 0) or 0) < 3600]
removed = before - len(keep)
if removed:
data[key] = keep
if removed:
path.write_text(json.dumps(data, ensure_ascii=False),
encoding="utf-8")
print(" %s: %s 从 %d 清到 %d(删了 %d)"
% (path.name, key, before, len(keep), removed))
else:
print(" %s: %s 共 %d 条,都是新的" % (path.name, key, before))
PY
echo
echo " 清理后 health 大小:"
curl -s --max-time 10 http://127.0.0.1:8080/v1/health -o /tmp/h.json
echo " $(wc -c < /tmp/h.json) 字节"
python3 -c "
import json
d = json.load(open('/tmp/h.json'))
print(' 设备数:', len(d.get('devices', [])))
" 2>/dev/null | sed 's/^/ /'
echo
echo "════════ 4. 重启隧道让新参数生效 ════════"
systemctl daemon-reload
systemctl restart lanremote-tunnel
sleep 30
echo " 连接数: $(curl -s --max-time 5 http://127.0.0.1:20241/metrics 2>/dev/null | grep '^cloudflared_tunnel_ha_connections' | awk '{print $2}')"
echo " 落点:"
grep -E 'Registered tunnel connection' "$RELAY_DIR/tunnel.log" 2>/dev/null | tail -4 \
| grep -oE 'location=[a-z0-9]+ protocol=[a-z0-9]+' | sed 's/^/ /'
echo
echo "════════ 5. 立刻测公网 ════════"
ok=0
for i in $(seq 1 10); do
c=$(curl -s -o /dev/null -w '%{http_code}' --max-time 15 \
-H 'User-Agent: LanRemote/1.4.0' https://relay.xxmxxs.top/v1/health)
[ "$c" = "200" ] && ok=$((ok+1))
printf " %2d: %s\n" "$i" "$c"
sleep 1
done
echo " 可用 $ok/10"