#!/bin/bash # 看门狗第三版:两个信号都查。 # # 为什么还要改: # 第二版只看 cloudflared 的 ha_connections 指标。 # 但"僵死连接"会把指标维持在 1 —— 计数还在,实际早断了。 # 这种时候看门狗以为一切正常,一直不管。 # (今早就遇到一次:浏览器 1033,指标却说有 1 条连接) # # 这一版: # 信号 A:ha_connections == 0 -> 记一次"零连接" # 信号 B:公网 /v1/health 打不通 -> 记一次"公网不通" # 任一信号连续 2 次 -> 重启隧道 # 另外:隧道日志超过 10 分钟没新行 + 公网不通 -> 判定进程卡死,重启 set -u RELAY_DIR=/root/lanremote-relay URL_FILE="$RELAY_DIR/public_url.txt" STATE_FILE="$RELAY_DIR/watchdog.state" TUNNEL_LOG="$RELAY_DIR/tunnel.log" METRICS=http://127.0.0.1:20241/metrics LOG_TAG=lanremote-watchdog # 连续几次公网不通才重启。 # 设 3(约 60 秒)而不是 2:短抖动让 cloudflared 自己重连就好, # 频繁重启反而让它一直停在"正在重连"的状态。 THRESHOLD=3 STALE_MINUTES=10 # 日志多久没动静算卡死 # 两次重启之间至少隔这么久(秒)。 # 实测隧道重建只要 30~40 秒,原来设 120 秒太保守 —— # 真断的时候会干等(日志里出现过"距上次重启才 118 秒,先不重启", # 结果多断了 2 分钟)。 # 设 45 秒:既够 cloudflared 把连接建起来,又不会干等太久。 # 两次重启至少隔这么久。 # 150 秒不是拍脑袋:隧道在差的 4G 上重建 4 条连接要 30~60 秒, # 原来设 45 秒会导致**隧道还没建齐就被下一次重启打断**, # 永远停在只剩 1 条连接的状态(实测死循环,每 50 秒重启一次)。 # 给足宽限期,让它有机会稳定下来。 MIN_RESTART_GAP=150 GRACE=150 log() { logger -t "$LOG_TAG" "$1" echo "[$(date '+%m-%d %H:%M:%S')] $1" } read_state() { local key="$1" default="$2" value="" if [ -f "$STATE_FILE" ]; then value=$(grep "^$key=" "$STATE_FILE" 2>/dev/null | cut -d= -f2) fi [ -n "$value" ] && echo "$value" || echo "$default" } write_state() { printf 'zero=%s\nfail=%s\n' "$1" "$2" > "$STATE_FILE"; } LAST_RESTART_FILE="$RELAY_DIR/watchdog.last_restart" restart_tunnel() { local why="$1" local now last gap now=$(date +%s) last=0 [ -f "$LAST_RESTART_FILE" ] && last=$(cat "$LAST_RESTART_FILE" 2>/dev/null || echo 0) gap=$((now - last)) # 连接数归零 = 真的断了,不是抖动 —— 这种不受间隔限制 local conns_now conns_now=$(timeout 5 curl -s --max-time 4 "$METRICS" 2>/dev/null \ | grep '^cloudflared_tunnel_ha_connections' | awk '{print $2}') if [ "${conns_now:-1}" != "0" ] && [ "$gap" -lt "$MIN_RESTART_GAP" ]; then # 刚重启过还没到间隔:这次先不重启, # 让 cloudflared 自己把连接建起来(它本来就在重试)。 log "$why —— 但距上次重启才 ${gap} 秒(下限 ${MIN_RESTART_GAP} 秒),先不重启" write_state 0 0 exit 0 fi log "$why —— 重启隧道(距上次 ${gap} 秒)" echo "$now" > "$LAST_RESTART_FILE" systemctl restart lanremote-tunnel write_state 0 0 sleep 25 log "重启后地址: $(cat "$URL_FILE" 2>/dev/null || echo '(还没拿到)')" exit 0 } ZERO=$(read_state zero 0) FAIL=$(read_state fail 0) # ---------- 信号 A:cloudflared 自报的连接数 ---------- CONNS=$(timeout 5 curl -s --max-time 4 "$METRICS" 2>/dev/null \ | grep '^cloudflared_tunnel_ha_connections' | awk '{print $2}') [ -z "$CONNS" ] && CONNS="" # ---------- 信号 B:公网真的通吗 ---------- URL=$(cat "$URL_FILE" 2>/dev/null | tr -d '\r\n') CODE="000" if [ -n "$URL" ]; then CODE=$(timeout 20 curl -4 -s -o /dev/null -w '%{http_code}' \ -H 'User-Agent: LanRemote/1.4.0' "$URL/v1/health" 2>/dev/null) CODE=${CODE:-000} fi PUBLIC_OK=0 [ "$CODE" = "200" ] && PUBLIC_OK=1 # ---------- 信号 C:日志僵死了吗 ---------- LOG_STALE=0 if [ -f "$TUNNEL_LOG" ]; then LAST=$(stat -c %Y "$TUNNEL_LOG" 2>/dev/null || echo 0) NOW=$(date +%s) if [ "$LAST" -gt 0 ] && [ $((NOW - LAST)) -gt $((STALE_MINUTES * 60)) ]; then LOG_STALE=1 fi fi # ---------- 判断 ---------- if [ "$PUBLIC_OK" = "1" ]; then # 公网通就是真的通,计数器清零 if [ "$ZERO" != "0" ] || [ "$FAIL" != "0" ]; then log "公网恢复(HTTP 200),计数清零(之前 zero=$ZERO fail=$FAIL)" fi write_state 0 0 exit 0 fi # 公网不通 —— 看是哪种原因 DETAIL="HTTP $CODE" [ -n "$CONNS" ] && DETAIL="$DETAIL,ha_connections=$CONNS" if [ "$LOG_STALE" = "1" ] && [ "$FAIL" -ge 1 ]; then restart_tunnel "日志 ${STALE_MINUTES} 分钟没动静加上公网不通(进程卡死)" fi FAIL=$((FAIL + 1)) # 连接数 <= 1 也算"降级"。 # # 原来是 == 0 才算,但实测**只剩 1 条连接时跨机房回源就会失败** # (用户撞上的 530 就是这种:请求落 SEA,隧道只剩 LAX 一条)。 # 所以 1 和 0 一样危险,都计入降级计数。 DEGRADED=0 if [ -n "$CONNS" ] && [ "$CONNS" -le 1 ] 2>/dev/null; then ZERO=$((ZERO + 1)) DEGRADED=1 fi log "公网不通($DETAIL),连续第 $FAIL 次(降级 $ZERO 次)" # 公网不通 + 连接数已经降到危险区 = **确认的故障**,不是短抖动。 # 这种情况立刻重启,不等攒够 3 次 —— 每次检查间隔 20 秒, # 等 3 次就是白断 40 秒。 if [ "$DEGRADED" = "1" ]; then # 宽限期内不重启 —— 让上一次重启有机会把连接建齐。 # (不加这条会死循环:隧道还没建齐就被再次打断。) LAST_R=$(cat "$LAST_RESTART_FILE" 2>/dev/null || echo 0) SINCE=$(( $(date +%s) - LAST_R )) if [ "$SINCE" -lt "$GRACE" ]; then log "公网不通且只剩 ${CONNS} 条连接,但距上次重启才 ${SINCE} 秒(宽限 ${GRACE} 秒)—— 先让它建完" else restart_tunnel "公网不通且只剩 ${CONNS} 条隧道连接(确认故障,立即重启)" fi fi if [ "$FAIL" -ge "$THRESHOLD" ] || [ "$ZERO" -ge "$THRESHOLD" ]; then restart_tunnel "连续 $FAIL 次公网不通(含 $ZERO 次降级)" fi write_state "$ZERO" "$FAIL"