#!/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"