Cloudflare Tunnel 保活看门狗设计:从能连边缘到端到端可达的实战总结

举报
yd_225649965 发表于 2026/10/23 01:28:48 2026/10/23
【摘要】 Cloudflare Tunnel 保活看门狗设计:从"能连边缘"到"端到端可达"的实战总结背景:一台 Windows 云电脑(以下简称被控端)通过 Cloudflare Tunnel 对外暴露 SSH, 控制端经 cloudflared access ssh 建连。隧道在边缘侧显示 Connected,但控制端 频繁报 Connection timed out during banner ...

Cloudflare Tunnel 保活看门狗设计:从"能连边缘"到"端到端可达"的实战总结

背景:一台 Windows 云电脑(以下简称被控端)通过 Cloudflare Tunnel 对外暴露 SSH, 控制端经 cloudflared access ssh 建连。隧道在边缘侧显示 Connected,但控制端 频繁报 Connection timed out during banner exchange。本文记录我们为这条隧道 设计的保活看门狗:它不止盯进程存活,还盯端到端建连延迟,并把 DNS 解析、 二进制自愈、降级通道都收进同一个闭环。

一、为什么"进程在跑"不等于"隧道可用"

最开始的看门狗很朴素:pgrep cloudflared 找不到就重启。结果隧道照样三天两头 "假死":

  1. 被控端 cloudflared 进程 Running,且 cloudflared 边缘连接数正常;
  2. 控制端 ssh 报 banner 交换超时;
  3. 重启 cloudflared,"好像"好了——但 20 秒后大概率又不行。

事后复盘,真正的原因有两层,而且都不在被控端:

  • 控制端 DNS 解析抖动:控制端走沙箱 Fake-IP DNS,*.cfargotunnel.com 的 真实解析依赖一个本地 DoH 存根(doh-stub)。上游抖动时它回 SERVFAIL 空包, cloudflared access 拿不到正确 IP 就建连失败。cloudflared 本体没死, 是解析链路先死了。
  • 连接前置延迟吃掉超时预算:为了绕开 flaky DNS,我们在 ProxyCommand 里加了"连接前同步解析 hosts"的步骤。hosts 缓存过期后,每次 ssh 都要同步 走 doh-stub 重解析 5 次重试,烧掉约 20 秒,而 ssh 的 ConnectTimeout 只有 10 秒——还没轮到 cloudflared 启动,ssh 自己先超时了。

教训:看门狗必须做端到端探活(真的发起一次 ssh 建连并拿到 banner), 只查"进程在、边缘连着"会有盲区。以及:重启后恢复 ≠ 重启修好的, 先排除测量路径的差异再下结论。

二、看门狗的三层设计

最终落地的是一套三层结构,各管一段链路:

┌──────────────┐     ┌──────────────────┐     ┌─────────────────────┐
│ L1 本机自查   │────▶│ L2 端到端探活     │────▶│ L3 降级与自愈        │
│ 组件存活      │     │ 真实 ssh 建连     │     │ 换通道/重建/告警     │
└──────────────┘     └──────────────────┘     └─────────────────────┘

L1:本机自查(快、便宜,每分钟可跑)

检查清单(每项都是"存在性"断言,脚本退出码按 0 健康 / 1 需恢复 / 2 环境异常 三段约定):

  • cloudflared 二进制存在且可执行(必须用持久化路径,血的教训见下);
  • 隧道配置文件存在、隧道 ID 与凭证对得上;
  • doh-stub 存根进程在、hosts 预解析缓存文件新鲜度 < 1 小时;
  • cloudflared-access-wrap.sh 语法可通过 bash -n。

L2:端到端探活(慢、准,5 分钟一轮)

用生产完全相同的命令发起一次真实连接:

timeout 15 ssh -n -o BatchMode=yes -o StrictHostKeyChecking=accept-new \
  -o ConnectTimeout=10 ydy "echo alive" >/dev/null 2>&1

注意三个细节:

  1. -n 是保命参数:无 TTY 环境下 stdin 不送 EOF,OpenSSH 会在 banner 阶段假死,看起来像"连接超时",其实是本地 stdin 的问题;
  2. ConnectTimeout=10 与 timeout 15 双保险,探活本身绝不能 hang 住 看门狗;
  3. 探活走 ssh ydy 这个真实别名(含 ProxyCommand),不走裸 cloudflared access——要测的就是用户实际走的那条路。

连续 2 次探活失败才判隧道故障,单次失败只记抖动(doh-stub 上游抖是常态)。

L3:降级与自愈

按顺序执行,每步做完都回 L2 验证,通了就停:

  1. 刷新 DNS 预解析缓存:后台异步重拉 hosts(flock 防并发,成功才原子 覆盖,失败保留旧文件——缓存旧一点也能用,空文件会直接断连);
  2. 重启被控端 cloudflared(经备用通道下发,保持最后手段);
  3. 切备用通道:主通道(CFT)与备用通道(FRP STCP / Tailscale 直连) 交替,ssh 配置里三个 Host 别名平时就都保持可用;
  4. 告警:三次自愈无效,发邮件给机主,附上最近一次 ssh -v 的真实报错 (定位时永远先看 -v 输出,不要靠猜)。

三、四个实战踩过的坑

坑 1:ProxyCommand 里不要放同步重解析

早期 cloudflared-access-wrap.sh 在每次建连前同步刷新 hosts。缓存过期后 单次建连前置延迟 20 秒 > ConnectTimeout 10 秒,全灭。

修复:hosts 有可用 IP 直接放行,刷新丢到后台异步做:

# 有缓存直接用,不阻塞本次连接
if [ -s "$HOSTS_CACHE" ] && [ $(($(date +%s) - $(stat -c %Y "$HOSTS_CACHE"))) -lt 3600 ]; then
  exec cloudflared access ssh --hostname "$TUNNEL_HOST"
fi
# 缓存过期:后台刷新,本次先用旧 IP 顶着
(
  flock -n 200 || exit 0
  resolve_and_update_hosts && mv "$HOSTS_TMP" "$HOSTS_CACHE"
) 200>"$HOSTS_LOCK" &
exec cloudflared access ssh --hostname "$TUNNEL_HOST"

冷启动(无缓存)才同步解析一次。

坑 2:脚本里写死 /usr/local/bin 路径

一次 VM 重建清空了 /usr/local/bin/cloudflared(当时是个软链), wrapper 里写死的绝对路径导致 env: No such file or directory, ssh 全天失败;而 L1 自查只验了"wrapper 语法对",没验"它引用的二进制存在", 于是自查天天报全绿、实际全红,还连发了 4 封误向升级邮件。

铁律:脚本引用的二进制一律用家目录下持久路径;自查清单必须覆盖 "探活链路上每个外部命令的二进制存在性"——语法对 ≠ 能跑。

坑 3:doh-stub 上游抖动

doh-stub 的上游 DoH 经代理出去,延迟 5~9 秒是常态,超 8 秒就回 SERVFAIL。 对策是解析器侧加 rcode 检查 + 截断包防护 + 有限重试(5 次), 以及上面说的"失败保留旧缓存"。DNS 这种基础设施抖动, 设计时就当它一定会抖,而不是当异常处理。

坑 4:自愈脚本的并发双跑

看门狗(cron)和平台开机钩子可能同时触发恢复脚本。mkdir 抢锁的经典写法 有个窗口期:A 抢到锁还没写完标记,B 看到没标记就 rm -rf 抢走, 两个恢复脚本并发双跑。解法是换成 noclobber 原子建文件锁 (O_EXCL,时间戳与锁文件同时出现,无窗口期),stale 20 分钟自动接管:

set -o noclobber
if ! ( : > "$LOCK_FILE" ) 2>/dev/null; then
  # 锁存在:检查是否 stale(20 分钟)
  if [ $(($(date +%s) - $(stat -c %Y "$LOCK_FILE"))) -gt 1200 ] \
     && pgrep -af 'recover\.sh' | grep -qv "$$"; then
    rm -f "$LOCK_FILE"   # 确认真无进程在跑才删
  else
    exit 0
  fi
fi

注意删锁前必须 pgrep 确认真无进程在跑,别只看锁文件。

四、现在这套看门狗长什么样

  • health-check.sh:L1 自查,退出码 0/1/2 契约,cron 每 10 分钟跑;
  • tunnel-probe.sh:L2 端到端探活,5 分钟一轮,连续 2 次失败触发 L3;
  • recover.sh:L3 自愈,幂等、可重入,开机钩子与 cron 双触发, 原子锁防双跑;
  • 降级通道 FRP STCP 与 Tailscale 的探活同样进 L2,主备交替, 批量命令走 ControlMaster 长连接复用。

上线两个月,隧道"假死"误报从每周数次降到零(剩下的都是真故障: 被控端断电、运营商劫持之类,看门狗修不了、但能 5 分钟内准确定位并发邮件)。

五、一句话总结

隧道保活看门狗的三条设计原则:

  1. 探活必须端到端:用生产相同的命令真的建连一次,进程存活只是必要条件;
  2. 前置步骤计入超时预算:ProxyCommand 里的同步动作(DNS、鉴权)有多慢, ConnectTimeout 就要留多大余量,或者干脆异步化;
  3. 自查清单要闭环到二进制:语法对、配置对、进程对都不够, 链路上每个外部命令存在且可执行,才算"自查通过"。

希望这套"从误报里长出来"的设计,能帮你少踩几遍我们踩过的坑。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。