mcpField
C
cloudflare-tunnel-publish

cloudflare-tunnel-publish

github.com/starchild-ai-agent云与 DevOps3093 次安装6 天前更新
代码仓库

关于 cloudflare-tunnel-publish

一个通过 Cloudflare Tunnel 将本地 HTTP 服务发布到自定义域名的工作流技能,无需公网 IP 或开放入站端口。它引导初学者准备域名和令牌,并自动配置隧道、DNS、启动与验证。

主要功能

  1. 01将本地端口映射到公开域名
  2. 02配置 Cloudflare Tunnel 和 DNS
  3. 03复用健康隧道或创建新隧道
  4. 04启动并验证公开 URL
  5. 05支持多个子域名共用隧道

适用场景

  1. 01发布本地运行的 Web 服务
  2. 02公开 Starchild 预览
  3. 03为多个子域名配置同一隧道

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/starchild-ai-agent/official-skills --skill cloudflare-tunnel-publish

安装后在 Claude Code 中输入/cloudflare-tunnel-publish确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

scripts/
cf_api.py
check_status.py
keepalive.sh
run_tunnel.sh
setup.py
teardown.py
verify.py

详细介绍

这个技能的作用

使用 Cloudflare Tunnel,将运行在本地端口上的服务(默认为 Starchild 预览,但适用于任何 HTTP 服务)变成外界可访问的服务,网址为 app.userdomain.com。无需公网 IP,无需开放入站端口,且提供免费 SSL。

流程中的两个角色:

  • 用户手动完成(必须手动操作,无法自动完成):创建 Cloudflare 账户、在 Cloudflare 上购买或转移域名、创建 API Token。
  • Agent 自动完成(此技能):验证 Token、选择区域、创建隧道、配置入口规则、创建 DNS、安装并启动 cloudflared,验证公网 URL 是否可用。

面向的用户

请将用户视为初学者。他们可能从未使用过 Cloudflare。每次只引导他们完成一个微步骤,等待确认后再继续。不要把完整的 10 步计划一次性发给用户后就不管了。

工作流

阶段 0 — 说明流程(1 条消息)

用通俗的语言告诉用户接下来会分 4 个阶段:

  1. 他们注册 Cloudflare 账户并添加域名(手动操作,约 5 分钟)
  2. 他们创建 API Token 并安全地交给你(手动操作,约 2 分钟)
  3. 你创建隧道和 DNS 并启动服务(自动操作,约 1 分钟)
  4. 你们一起测试 URL(自动操作)

询问:“你已经在 Cloudflare 上有域名了吗?还是需要从头开始?” 根据回答进入相应流程。

阶段 1 — 在 Cloudflare 上获取域名

如果用户还没有域名:

可以告诉初学者:“当你的 Cloudflare 控制面板中显示域名状态为 Active 时,就可以继续了。”

如果用户已经有域名:跳到阶段 2。

阶段 2 — 创建 API Token

将此确切链接发给他们(该链接会在可能的情况下预填正确的权限,用户也可以手动创建):

https://dash.cloudflare.com/profile/api-tokens → Create Token → Create Custom Token

必需权限(请告诉他们添加以下三项):

  • Account → Cloudflare Tunnel → Edit
  • Zone → DNS → Edit
  • Zone → Zone → Read

Account Resources:选择他们的账户。Zone Resources:选择 Include All zones(或指定该域名)。TTL:保留默认值。

他们点击 Continue to summary → Create Token 后,Cloudflare 会仅显示一次 Token。请告诉他们:不要把 Token 粘贴到聊天中。

阶段 3 — 安全接收 Token

调用 request_env_input,并提供:

env_vars=[{"key": "CLOUDFLARE_API_TOKEN", "label": "Cloudflare API Token", "required": true}]
reason="Used to create the tunnel and DNS record on your domain. Stored locally in workspace/.env, never echoed in chat."

等待用户通过安全弹窗提交。如果他们没有立即提交,不要反复重试,只需等待。

阶段 4 — 验证 Token 并选择区域

运行 python3 skills/cloudflare-tunnel-publish/scripts/verify.py。该脚本会显示:

  • Token 是否有效
  • 用户的 account_id(保存到 workspace/.cf_state.json)
  • 账户中的所有区域(域名)

如果有多个区域,询问用户要使用哪个域名。将 zone_id 和 zone_name 保存到状态文件中。

阶段 5 — 决定要发布的内容

向用户询问两项信息:

  1. 子域名(例如 app、demo、www)→ 最终主机名将为 <sub>.<zone_name>。也允许使用根域名(@)。
  2. 本地端口(例如 8080、3000)。如果他们说“我的 Starchild 预览”,运行 cat /data/previews.json 2>/dev/null,查找现有预览的端口;否则明确询问端口。

默认服务 URL:http://localhost:<port>。

阶段 6 — 创建隧道(自动化)

运行 python3 skills/cloudflare-tunnel-publish/scripts/setup.py --hostname <full_hostname> --port <port>。

默认复用隧道。 如果账户中已有正常运行的隧道,setup.py 会将新主机名添加到该隧道的 ingress,并将 DNS CNAME 指向它,而不是创建单独的隧道。一个隧道 = 一个 cloudflared 进程 = 一个保活监控程序。每个站点都创建新隧道,会导致你需要看管 N 个进程,也有 N 种可能在不知情的情况下失联(这正是促使我们修复此问题的那个 bug:第二个隧道的 cloudflared 进程退出后,无人监控,因为保活监控程序只知道第一个隧道)。

仅在有理由隔离流量时才传入 --new-tunnel(例如,某个站点的流量极高,可能会耗尽共享的边缘连接)。

脚本按以下顺序执行:

  1. 如果存在正常运行的隧道,则复用它(如果不存在,则创建名为 starchild-<hostname> 的隧道;也可使用 --new-tunnel 强制创建新隧道)
  2. 获取隧道的 运行令牌(用于启动 cloudflared 的长 base64 字符串)
  3. PUT ingress 配置:将 <hostname> → http://localhost:<port> 合并到现有 ingress 规则中(保留其他主机名),并以 404 作为回退
  4. 创建 CNAME DNS 记录:<hostname> → <tunnel_id>.cfargotunnel.com,已启用代理
  5. 将站点添加到 workspace/.cf_state.json 中的 sites 数组(支持多个站点,见下方“多个站点”)

如果存在同名隧道,则复用它,而不是报错。

阶段 7 — 启动 cloudflared

运行 bash skills/cloudflare-tunnel-publish/scripts/keepalive.sh——这是启动站点的标准方式。keepalive.sh 是统一的启动与修复程序(见下方“保持运行”):它会读取 .cf_state.json,启动应用(如果你记录了 --app-cmd)和隧道,并验证公开 URL。run_tunnel.sh 是更底层的仅启动隧道的启动器,由 keepalive.sh 调用——如果 workspace/bin/ 中没有 cloudflared,它会下载该程序,从 .cf_state.json 中读取 run_token,并运行 cloudflared tunnel run。

告知用户站点已上线。为了确保稳定运行,你还会将同一个 keepalive.sh 配置为开机启动并按计划运行——不要另行手写启动脚本。

阶段 8 — 验证

⚠️ 不要直接使用容器的 curl https://<hostname>——容器的解析器会缓存新域名的旧 NXDOMAIN 结果,给出错误信息。请始终通过 DoH 验证:

curl -sS "https://dns.google/resolve?name=<hostname>&type=A" | python3 -m json.tool

可能有以下三种结果:

  1. Status: 0 且 Answer 中有 IP → 已上线。现在运行 curl -I https://<hostname> 应返回 200/301/302。向用户展示其 URL。🎉
  2. Status: 3(NXDOMAIN)且 Authority = TLD 注册局 NS(例如 ns.trs-dns.com)→ TLD 注册局尚未传播新域名。 告诉用户:配置已 100% 完成,请等待 30–60 分钟(新注册的域名可能需要最多 24 小时),然后重试。不要持续轮询——让他们在自己的设备上检查。
  3. 隧道日志显示错误(检查 bash_process(action='log', session_id=...))→ 存在实际配置错误。常见原因:ingress 未指向正确端口、本地服务未运行、CNAME 目标错误。

决策规则

  • 用户说“我的服务在自己的笔记本电脑上,不在 Starchild 中” → 流程完全相同,但第 7 阶段必须在他们的笔记本电脑上运行,而不是在此容器中。提供适用于其操作系统的对应安装命令:

    • macOS:brew install cloudflared && cloudflared tunnel run --token <TOKEN>
    • Linux/Windows:链接到 https://github.com/cloudflare/cloudflared/releases/latest 如果需要,通过 request_env_input 发送 run_token;或者只打印一次,并告诉他们复制该令牌(可以安全地分享给自己的设备,但绝不要粘贴回聊天中)。
  • 用户想要多个子域名 → 重用同一条隧道;PUT 一个列出所有主机名的新 ingress 配置;为每个主机名创建一个 CNAME。这现在是默认行为——只需为每个子域名运行 setup.py,它就会合并到现有隧道的 ingress 中。

  • 用户想移除它 → 运行 python3 skills/cloudflare-tunnel-publish/scripts/teardown.py(删除 DNS 和隧道,并终止本地 cloudflared 进程)。

注意事项(⚠️ 均已在实际运行中确认)

Token / API
  • Cloudflare-Tunnel:Edit 不会授予 /accounts 列表权限。 即使使用有效令牌,调用 GET /accounts 也会返回空列表。解决方案: 从任意 zone 内嵌的 account.id 字段中获取 account_id——verify.py 已经这样做了。不要仅为了解决此问题而添加 Account:Account Settings:Read;使用 zone 的方法更简单。
  • GET /accounts/{id}/cfd_tunnel/{tunnel_id}/token 返回的“run token”是 cloudflared tunnel run --token 使用的令牌。不要与以下内容混淆:
    • tunnel secret — 仅与旧版本地管理的隧道有关(我们不使用)
    • API Token — 用于调用 api.cloudflare.com
隧道 / Ingress
  • CNAME 的目标必须是 <tunnel_id>.cfargotunnel.com,而不是隧道名称。
  • 远程管理的隧道(config_src: "cloudflare")通过 API 配置端点进行路由,而不是使用本地 config.yml。不要生成该文件。
  • 即使 config_src=cloudflare,通过 API 创建隧道也需要 tunnel_secret 字段(32 个随机字节,经过 base64 编码)。setup.py 会自动生成。
Universal SSL 证书签发延迟 — 另一个大陷阱

DNS 传播后,用户仍可能在浏览器中遇到 ERR_SSL_VERSION_OR_CIPHER_MISMATCH。这不是 bug——Cloudflare 尚未为新主机名签发 Universal SSL 证书。

诊断(在容器中运行——无需身份验证):

echo | timeout 10 openssl s_client -connect <hostname>:443 -servername <hostname> 2>&1 | grep -E "(handshake|peer certificate|Cipher is)"
  • no peer certificate available + handshake failure → 证书尚未签发 ⏳
  • 返回真实证书 → 正常运行 ✅

时间:

  • 已有先前证书的现有 zones:通常少于 5 min
  • 全新域名:15 min ~ 24 h(DNS 验证 + CA 签名 + 边缘节点传播)

告知用户:

  1. 打开 dash.cloudflare.com → <domain> → SSL/TLS → Edge Certificates
  2. 找到类似 *.<domain>, <domain> 的行并查看状态:
    • Active → 已完成,刷新浏览器
    • Pending Validation / Initializing → 请等待
  3. 确认 SSL/TLS → Overview → Encryption mode 设置为 Full(而不是 Flexible 或 Full Strict)。隧道始终通过 HTTPS 连接到源站,因此 Full 是合适的设置。

不要: 告诉用户添加 Advanced Certificate($$$)或更改 DNS——这两种方法都无济于事。只需等待。

DNS 传播 — 大陷阱

新注册的域名需要 30 min ~ 2 h(有时长达 24 h)才能在全球 TLD 注册表中传播完成,即使 Cloudflare 仪表板立即显示“Active”也是如此。症状:

  • dig @1.1.1.1 yourdomain.com NS 返回 NXDOMAIN(Status=3)
  • Authority 部分显示 TLD 注册表的 NS(例如,经由 Tucows 注册的 .fun 域名显示 ns.trs-dns.com),而不是 Cloudflare 的 NS
  • cloudflared 隧道已连接且运行正常,但 https://yourdomain.com 返回 DNS 解析失败

这不是该 skill 的 bug——而是 TLD 注册表同步延迟。 使用下面的诊断片段来区分这种情况与实际问题。告诉用户:“配置已完成。请等待 30–60 分钟后重试。我们这边无需再做任何操作。”

容器 DNS — 误报

在 Starchild 容器内部进行测试时,容器的本地解析器可能数小时都看不到新域名。务必使用公共 DoH 进行交叉验证:

curl -sS "https://dns.google/resolve?name=hello.example.com&type=A" | python3 -m json.tool
  • Status: 0,且 Answer 数组包含 IP → 正常 ✅
  • Status: 3(NXDOMAIN),且 Authority: ns.trs-dns.com(或类似的注册局 NS)→ TLD 传播仍在等待中 ⏳
  • Status: 0,但没有 Answer → CNAME 已存在,但 Cloudflare 橙云尚未开始路由 → 等待 30 秒

保持服务运行 — 一个脚本,两个触发器

这是 Agent 容易出错的部分。“发布”很容易;让隧道站点持续运行数周才是真正的任务。Starchild 容器会在没有警告的情况下重启(平台更新、OOM、迁移、用户重启),而 cloudflared 也会在运行期间自行退出(网络短暂中断、边缘节点重置、QUIC 故障),但容器仍在运行。无论哪种情况,都会导致 https://yourdomain.com 返回 502 / 521 / 530 / 1033,直到进程被重启。DNS 和 Cloudflare 侧的隧道配置会保留(它们存储在 Cloudflare 服务器上)——只需重新启动本地进程。

设计:一个幂等的恢复核心(scripts/keepalive.sh),由两个触发器调用。 不要为每个项目编写单独的启动或修复脚本——这样会导致两份代码逐渐不一致。keepalive.sh 随此技能提供,且是通用脚本:它从 .cf_state.json 读取 hostname、port、app_cmd、app_dir,因此同一个文件可用于任何域名。调用它的 Agent 不需要编写任何项目专用的 shell 脚本。

keepalive.sh 每次运行时会:

  1. 检查公共 URL(检查可访问性,而不只是 PID——cloudflared 进程可能仍在运行,却已断开连接)。
  2. 状态正常 → 记录一行日志,静默退出。
  3. 服务不可用 → 排查原因:本地应用端口未打开 → 重启应用(通过 app_cmd)和隧道;只有隧道已退出 → 仅重启隧道。随后通过几次重试重新验证(覆盖冷启动预热时间)。
  4. 仅在状态转换时报告(记录于 run/keepalive.state):刚恢复或刚发生故障时输出一行;状态稳定时(正常,或已知不可用)静默。因此,定时任务只在有情况需要通知时发送信号,不会刷屏。
第 1 步 — 记录如何启动应用(在设置时)

向 setup.py 传入 --app-cmd / --app-dir,以便 keepalive 能重启应用,而不只是隧道:

python3 setup.py --hostname app.example.com --port 8765 \
    --app-cmd "python3 server.py" --app-dir projects/myapp

如果省略 --app-cmd,keepalive 只会守护隧道,无法恢复已崩溃的应用。除非应用由其他服务进行监控,否则务必记录此参数。

第 2 步 — 启动站点
bash skills/cloudflare-tunnel-publish/scripts/keepalive.sh

幂等:启动所有已停止的服务,状态正常时不执行任何操作。

第 3 步 — 在容器重启后继续运行(启动触发器)

将 keepalive 添加到 workspace/setup.sh(容器每次启动时都会运行):

# Bring the tunnel site back after a restart. keepalive.sh only READS
# .cf_state.json — no Cloudflare API call, no token needed at boot.
if [ -f /data/workspace/.cf_state.json ]; then
  bash /data/workspace/skills/cloudflare-tunnel-publish/scripts/keepalive.sh &
  disown
fi

🚫 绝不要将 setup.py 放入 setup.sh。 setup.py 用于配置时运行:它会调用 Cloudflare API,可能轮换 run_token,并覆盖 .cf_state.json。每次启动都运行它不仅浪费资源,还可能触及速率限制;如果 API 令牌已被移除,运行会失败,也可能更改原本正常的配置。启动时只能读取状态——这正是 keepalive.sh 的作用。名称“setup”容易让人想把它放进“setup.sh”;请克制这种冲动。

第 4 步 — 在运行过程中进程退出后继续运行(看门狗触发器)

将同一个脚本安排为低开销的 command 模式任务。一个定时任务即可守护所有站点——keepalive.sh 会在一次运行中遍历 .cf_state.json 中的所有站点,因此无需为每个站点单独设置看门狗:

scheduled_task(action="schedule",
  schedule="every 2 minutes",
  command="cd /data/workspace && bash skills/cloudflare-tunnel-publish/scripts/keepalive.sh",
  title="cloudflare tunnel keepalive (all sites)")
  • 使用相对命令,并以 cd /data/workspace && 开头。绝对路径 /data/workspace/... 可能会被调度器规范化为不存在的 /data/skills/...(workspace/ 这一段会被删掉),导致每次运行都静默失败。使用 cd 加相对路径即可避免此问题。(已在实际运行中确认。)
  • 保留默认的 deliver 值,这样仅在状态转换时发出的提醒才能实际送达用户。keepalive 在状态正常时本来就不会发出消息,因此无需抑制刷屏——真正发生故障时应当通知你。
  • 时间间隔可能会被规范化(例如,“每 2 分钟”→ 3 分钟)——这没问题。
验证持久性(在声称“稳定”之前,以下事项都要完成)
# 1. start + idempotency
bash skills/cloudflare-tunnel-publish/scripts/keepalive.sh   # brings up
bash skills/cloudflare-tunnel-publish/scripts/keepalive.sh   # silent no-op
tail -5 logs/keepalive.log                                   # ok https://...
# 2. boot wired
grep keepalive setup.sh
# 3. watchdog registered + command stored correctly (relative path!)
#    scheduled_task(action="list")

不要只编辑 setup.sh 就声称“长期稳定”。 这只能应对重启,无法应对运行期间的进程退出。确认启动触发器和定时触发器都指向 keepalive.sh,并且首次运行记录了 ok https://...。

端口冲突会导致无声故障。 其他工作区项目可能已经占用了常见端口(8000/8080/8765)。如果应用的 bind() 因 Address already in use 失败,就会退出,但 curl localhost:<port> 仍会返回 200——响应来自别人的应用。请使用较高且项目专属的端口,并验证页面内容确实是你的(curl https://yourdomain.com | head),而不只是返回 200。

明确告知用户:

🔔 你的站点运行在容器中。容器可能因更新/内存/迁移而随时重启,隧道进程偶尔也可能自行掉线(域名变成 502/521/530/1033)。我已经把一个自愈脚本写入 workspace/setup.sh(开机自动启动),并设置了每隔几分钟一次的巡检(掉线时自动重启,仅在恢复/失败时通知你)。两层都指向同一个脚本,你基本不用管。真打不开时,让我看一眼 logs/keepalive.log 和 logs/cloudflared.log 就能定位。

计划限制
  • 免费计划足够了。 无需推销升级。
  • 免费计划只公开代理 80/443 端口——这对我们无关紧要,因为隧道始终会向全网开放 443;localhost:<port> 可以是任意端口。

状态文件格式

workspace/.cf_state.json 支持多个站点。规范存储位置是 sites 数组;顶层的平铺字段为了向后兼容而保留,并且始终反映最后配置的站点:

{
  "account_id": "...",
  "zone_id": "...",
  "zone_name": "example.com",
  "sites": [
    {
      "hostname": "app.example.com",
      "port": 8080,
      "tunnel_id": "...",
      "tunnel_name": "starchild-app-example-com",
      "run_token": "...",
      "app_cmd": "python3 server.py",
      "app_dir": "/data/workspace/projects/myapp"
    },
    {
      "hostname": "blog.example.com",
      "port": 3000,
      "tunnel_id": "...",
      "run_token": "...",
      "app_cmd": "",
      "app_dir": ""
    }
  ],
  "hostname": "blog.example.com",
  "port": 3000,
  "tunnel_id": "...",
  "run_token": "...",
  "app_cmd": "",
  "app_dir": ""
}

keepalive.sh 会读取 sites[],并在一次巡检中守护每个站点——一个看门狗,所有主机名。这解决了“每个站点都有自己的隧道

  • 自己的 keepalive,但只有一个受到监控”的问题:现在恰好只有一个 keepalive 进程负责所有已配置的站点。
多站点——设计方案

一条隧道、多个主机名、一个看门狗。 这是唯一合理的拓扑结构:

  • setup.py 默认复用在账户中找到的第一个健康隧道。它会将新主机名合并到该隧道现有的 ingress 规则中(保留其他主机名),并将 DNS CNAME 指向该隧道。
  • .cf_state.json 包含一个 sites[] 数组。每次运行 setup.py 都会追加(或替换)一个条目。teardown.py 会移除一个条目。
  • keepalive.sh 会遍历 sites[] 并守护每个站点。一个进程、一个看门狗、所有主机名。如果某个站点与另一个站点共用隧道,同一个 cloudflared 进程就会为两者提供服务——keepalive 不会再启动第二个进程。

Agent 弄错时的情况(促成本节的那个错误): 每个站点都创建一条新隧道,就意味着有 N 个 cloudflared 进程、N 个 PID 文件、N 个需要配置的看门狗——而实际上最终只会设置一个看门狗。其他隧道会悄无声息地停止运行,几周都没人发现。复用隧道,将条目追加到 sites[],让一个 keepalive 守护所有站点。

评论

支持 Markdown