From bac79017aec794c1aa0d5dedb5708348a668478a Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Mon, 6 Jul 2026 16:30:11 +0800 Subject: [PATCH] =?UTF-8?q?docs(plan):=20#25=20=E6=8E=A7=E5=88=B6=E9=9D=A2?= =?UTF-8?q?=20CF=20Tunnel=20=E5=AE=9E=E7=8E=B0=E6=96=B9=E6=A1=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_013nMthbVEmQquxBRKb9Fj8u --- .../2026-07-06-control-plane-tls-tunnel.md | 499 ++++++++++++++++++ 1 file changed, 499 insertions(+) create mode 100644 docs/superpowers/plans/2026-07-06-control-plane-tls-tunnel.md diff --git a/docs/superpowers/plans/2026-07-06-control-plane-tls-tunnel.md b/docs/superpowers/plans/2026-07-06-control-plane-tls-tunnel.md new file mode 100644 index 0000000..d5d7917 --- /dev/null +++ b/docs/superpowers/plans/2026-07-06-control-plane-tls-tunnel.md @@ -0,0 +1,499 @@ +# 控制面 TLS(Cloudflare Tunnel 前置)Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 把 pangolin-server 控制面 API 从明文 `http://103.119.13.48:8080` 迁到 `https://api.yanmeiai.com`,经 Cloudflare Tunnel 前置(隐藏源站 IP、白嫖标准 443 + 免证书),数据面 sing-box REALITY:443 完全不动。 + +**Architecture:** pangolin1 上跑 `cloudflared` **出站**隧道(不监听任何入站端口 → 与 sing-box 独占的 :443 零冲突),CF 边缘把 `api.yanmeiai.com` 的请求经隧道回送到 `127.0.0.1:8080`。客户端(Flutter 四端共享 `kApiBaseUrl`)默认改 https 域名;控制面下发给客户端 sing-box 的 `.srs` 规则集下载基址(`PANGOLIN_PUBLIC_URL`)同步改 https。最后一步把 `:8080` 收回 loopback 并关防火墙,彻底退役明文口——该步有上线顺序闸(须待现网客户端更新后再做)。 + +**Tech Stack:** Cloudflare Tunnel(remotely-managed / token 模式)、cloudflared(Debian 12 apt)、systemd、Go(pangolin-server,仅 env 变更零代码)、Flutter/Dart(`api_config.dart`)、Android manifest、Gitea Actions(`deploy-server.sh` 健康检查)、cf-api 封装(Bitwarden token)。 + +## Global Constraints + +- **Bash 禁 `$()` 命令替换**;禁 `set -a`/`set +a`。需捕获输出拆多步或用管道。 +- **凭证走 Bitwarden/rbw**,不写 `~/.env`/明文配置/git。Cloudflare 用 `cf-api` 封装(token 脚本内部从 Bitwarden 取,禁引用 `$CF_API_TOKEN`)。**隧道 token、私钥等 PII/密钥一律不入 git**,只落 `/etc/pangolin/*`(已 gitignore)+ Bitwarden。 +- **改机器(装包/改配置/重启服务)前必须先问用户**(只读操作除外)。本方案 Task 1B/2/5 会 ssh 改 pangolin1 与 CF 账户配置,执行到那几步先征得确认。 +- 回复中文件路径**一律绝对路径**。 +- git commit 结尾附:`Co-Authored-By: Claude Opus 4.8 ` 与 `Claude-Session:` 行;**不 force-push、不推 main**。 +- CF 账户 id `e585821c881c4cd23bc2530986edea9e`;zone `yanmeiai.com` id `2325730de45276d87180a8b66bd4cca0`。 +- pangolin1 = `103.119.13.48`,ssh 别名 `pangolin1`(root 免密)。数据面 sing-box REALITY 独占入站 `:443`,**不得触碰**。gRPC agent mTLS `:9443` 不动。 +- **上线顺序铁律**:现网客户端硬编码 `http://103.119.13.48:8080`。隧道与 https 端点必须**加法上线**(与旧口并存),客户端切 https 发版后,**Task 5(收 loopback + 关防火墙)才能做**,否则旧客户端全挂。 + +--- + +## File Structure + +**新建:** +- `deploy/single-node/systemd/cloudflared.service` — cloudflared 的 systemd unit(committed,`install -m 644` 到位,token 从 `/etc/pangolin/cloudflared.env` 经 `EnvironmentFile` 注入,不入 unit 本体)。 +- `client/test/unit/api_config_test.dart` — 守护测试:控制面基址必须 https(防回退明文)。 +- `ci/scan-cleartext.sh` — CI 守护:Android release manifest 不得含 `usesCleartextTraffic="true"`。 + +**修改:** +- `deploy/single-node/deploy.sh` — server.env 里 `PANGOLIN_PUBLIC_URL` 改 https(Task 4);`ADDR` 改 loopback + 去掉 ufw 放行 8080(Task 5);新增 cloudflared 安装/enable(Task 1B)。 +- `client/lib/services/api_config.dart:6-9` — `kApiBaseUrl` 默认值改 `https://api.yanmeiai.com`(Task 3)。 +- `client/android/app/src/main/AndroidManifest.xml:30` — 移除 `android:usesCleartextTraffic="true"`(Task 3)。 +- `scripts/ci/deploy-server.sh:57` — 健康检查从「runner 远程 curl `http://IP:8080`」改为 ssh 内本地 `curl http://127.0.0.1:8080/healthz`(Task 5)。 +- `.gitea/workflows/ci.yml` — shellcheck 列表加 `ci/scan-cleartext.sh`;新增 cleartext-scan job(Task 3)。 +- `CLAUDE.md` + `docs/` — 端口/URL 布局更新(Task 6)。 + +--- + +## Task 1: Cloudflare Tunnel 供给(CF 账户侧 + pangolin1 装 cloudflared) + +把隧道建起来、DNS 指过去、cloudflared 在 pangolin1 上连通,`https://api.yanmeiai.com/healthz` 与旧的 `http://103.119.13.48:8080/healthz` **并存可用**(加法,不破坏现网)。 + +**Files:** +- Create: `deploy/single-node/systemd/cloudflared.service` +- Modify: `deploy/single-node/deploy.sh`(安装/enable cloudflared) + +**Interfaces:** +- Produces: 隧道域名 `https://api.yanmeiai.com` → `127.0.0.1:8080`;隧道 token 存于 Bitwarden item `pangolin-cloudflared-tunnel` 字段 `TUNNEL_TOKEN` + pangolin1 `/etc/pangolin/cloudflared.env`。后续 Task 3/4 依赖此域名可达。 + +### 1A — CF 侧:创建隧道 + ingress + DNS(cf-api,只读账户外均属改配置,先确认) + +- [ ] **Step 1: 建 remotely-managed 隧道,取 token** + +先确认 rbw 已解锁(`rbw unlock`)。运行: + +```bash +cf-api -X POST "/accounts/e585821c881c4cd23bc2530986edea9e/cfd_tunnel" \ + --data '{"name":"pangolin-api","config_src":"cloudflare"}' +``` + +Expected: JSON `success:true`,`result.id`(隧道 UUID)、`result.token`(base64 长串)。**记下 `result.id` 为 `TUNNEL_ID`,`result.token` 为 `TUNNEL_TOKEN`。token 是密钥,不要落 git/明文文档。** + +- [ ] **Step 2: 配 ingress(hostname → 本机 8080,兜底 404)** + +```bash +cf-api -X PUT "/accounts/e585821c881c4cd23bc2530986edea9e/cfd_tunnel//configurations" \ + --data '{"config":{"ingress":[{"hostname":"api.yanmeiai.com","service":"http://localhost:8080"},{"service":"http_status:404"}]}}' +``` + +Expected: `success:true`,`result.config.ingress` 含上面两条。 + +- [ ] **Step 3: 建代理 CNAME `api` → 隧道** + +```bash +cf-api -X POST "/zones/2325730de45276d87180a8b66bd4cca0/dns_records" \ + --data '{"type":"CNAME","name":"api","content":".cfargotunnel.com","proxied":true,"comment":"pangolin 控制面 API(CF Tunnel → pangolin1:8080)"}' +``` + +Expected: `success:true`,`result.name` = `api.yanmeiai.com`,`result.proxied` = true。 + +- [ ] **Step 4: token 存入 Bitwarden(留档)** + +把 `TUNNEL_TOKEN` 存进 Bitwarden item `pangolin-cloudflared-tunnel`(字段 `TUNNEL_TOKEN`)。验证: + +```bash +rbw get pangolin-cloudflared-tunnel --field TUNNEL_TOKEN | head -c 12 +``` + +Expected: 打印 token 前 12 字符(证明可取回)。 + +### 1B — pangolin1:装 cloudflared + systemd 常驻(改机器,先确认) + +- [ ] **Step 5: 写 committed systemd unit** + +创建 `deploy/single-node/systemd/cloudflared.service`: + +```ini +[Unit] +Description=Pangolin cloudflared (control-plane API tunnel → 127.0.0.1:8080) +Documentation=https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/ +After=network-online.target pangolin-server.service +Wants=network-online.target + +[Service] +Type=notify +# TUNNEL_TOKEN 从此文件注入(不入 unit 本体、不进 ps);cloudflared 自动读取 env TUNNEL_TOKEN。 +EnvironmentFile=/etc/pangolin/cloudflared.env +ExecStart=/usr/local/bin/cloudflared --no-autoupdate tunnel run +Restart=on-failure +RestartSec=5 +# 出站隧道,无需 root:用非特权用户即可(与 pangolin-server 同用户)。 +User=pangolin +NoNewPrivileges=true + +[Install] +WantedBy=multi-user.target +``` + +- [ ] **Step 6: deploy.sh 里安装 cloudflared 二进制 + unit + enable** + +在 `deploy/single-node/deploy.sh` 的 systemd 安装段(现有 `install -m 644 .../pangolin-server.service` 一带,约 251-252 行)后追加。先加安装函数(Debian apt,无 `$()`): + +```bash +# ── cloudflared(控制面 API 出站隧道)────────────────────────────── +if ! command -v cloudflared >/dev/null 2>&1; then + log "安装 cloudflared(Cloudflare apt 源)" + install -m 0755 -d /usr/share/keyrings + curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg \ + -o /usr/share/keyrings/cloudflare-main.gpg + echo 'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared bookworm main' \ + > /etc/apt/sources.list.d/cloudflared.list + apt-get update -qq && apt-get install -y -qq cloudflared + # apt 装到 /usr/bin;软链到 unit 期望的 /usr/local/bin(与其他 pangolin 二进制一致)。 + [ -x /usr/local/bin/cloudflared ] || ln -sf "$(command -v cloudflared)" /usr/local/bin/cloudflared +fi +install -m 644 "$HERE/systemd/cloudflared.service" /etc/systemd/system/ +``` + +> 注:上面为示意锚点;`$(command -v cloudflared)` 违反禁 `$()` 规则——落地时改为:`CFD_BIN=/usr/bin/cloudflared` 后 `ln -sf "$CFD_BIN" /usr/local/bin/cloudflared`(apt 固定装到 `/usr/bin`)。 + +- [ ] **Step 7: 在 pangolin1 落 token env 文件 + 起服务**(ssh,改机器,先确认) + +token 经用户剪贴板落地(不经过我、不入 git): + +```bash +# 本机把 token 通过 ssh 写到远端受限权限文件(避免出现在 ps/history): +rbw get pangolin-cloudflared-tunnel --field TUNNEL_TOKEN | \ + ssh pangolin1 'install -m 600 -o pangolin -g pangolin /dev/stdin /etc/pangolin/cloudflared.env.tmp && \ + printf "TUNNEL_TOKEN=" | cat - /etc/pangolin/cloudflared.env.tmp > /etc/pangolin/cloudflared.env && \ + rm -f /etc/pangolin/cloudflared.env.tmp && chmod 600 /etc/pangolin/cloudflared.env' +``` + +> 落地时若上面拼接别扭,改为本机 `printf 'TUNNEL_TOKEN=%s\n' ""` 结果 ssh 管道写入;核心要求:`/etc/pangolin/cloudflared.env` 内容为单行 `TUNNEL_TOKEN=`,mode 600,owner pangolin。 + +装 unit 并启动: + +```bash +scp deploy/single-node/systemd/cloudflared.service pangolin1:/etc/systemd/system/ +ssh pangolin1 'systemctl daemon-reload && systemctl enable --now cloudflared.service && sleep 3 && systemctl is-active cloudflared' +``` + +Expected: `active`。 + +- [ ] **Step 8: 验证隧道连通(加法上线,不破坏旧口)** + +```bash +curl -fsS -m 10 https://api.yanmeiai.com/healthz && echo " <= 隧道 OK" +curl -fsS -m 10 http://103.119.13.48:8080/healthz && echo " <= 旧口仍在(预期)" +``` + +Expected: 两条都返回 `/healthz` 成功体。证明 https 端点上线、旧明文口并存(现网客户端不受影响)。 + +- [ ] **Step 9: Commit** + +```bash +git add deploy/single-node/systemd/cloudflared.service deploy/single-node/deploy.sh +git commit -m "feat(deploy): cloudflared 出站隧道前置控制面 API(api.yanmeiai.com→127.0.0.1:8080)" +``` + +--- + +## Task 2: 客户端控制面基址切 https + Android 去明文(含守护测试) + +**Files:** +- Modify: `client/lib/services/api_config.dart:6-9` +- Modify: `client/android/app/src/main/AndroidManifest.xml:30` +- Create: `client/test/unit/api_config_test.dart` + +**Interfaces:** +- Consumes: Task 1 产出的 `https://api.yanmeiai.com`(须已可达)。 +- Produces: 全 Flutter 端(auth/nodes/account/connection providers 共享的)`kApiBaseUrl` 默认 = `https://api.yanmeiai.com`。 + +- [ ] **Step 1: 写守护测试(先失败)** + +创建 `client/test/unit/api_config_test.dart`: + +```dart +import 'package:flutter_test/flutter_test.dart'; +import 'package:pangolin/services/api_config.dart'; + +void main() { + test('控制面基址默认走 https(禁止回退明文 http)', () { + expect(kApiBaseUrl, startsWith('https://'), + reason: '控制面已迁 CF Tunnel(api.yanmeiai.com);默认值不得是明文 http'); + expect(kApiBaseUrl, isNot(contains('103.119.13.48')), + reason: '不得再硬编码节点 IP 作控制面基址'); + }); +} +``` + +- [ ] **Step 2: 跑测试确认失败** + +Run: `cd client && flutter test test/unit/api_config_test.dart` +Expected: FAIL —— 当前默认 `http://103.119.13.48:8080` 两条断言都不满足。 + +- [ ] **Step 3: 改默认值为 https 域名** + +`client/lib/services/api_config.dart:6-9`,把: + +```dart +const String kApiBaseUrl = String.fromEnvironment( + 'PANGOLIN_API_URL', + defaultValue: 'http://103.119.13.48:8080', +); +``` + +改为(保留 `String.fromEnvironment` 让本地联调仍可 `--dart-define` 覆盖,只换默认值并更新注释): + +```dart +// 控制面 API 基址(单源,全端 providers 共用)。默认走 CF Tunnel 的 https 域名; +// 本地联调可 --dart-define=PANGOLIN_API_URL=http://127.0.0.1:8080 覆盖。 +const String kApiBaseUrl = String.fromEnvironment( + 'PANGOLIN_API_URL', + defaultValue: 'https://api.yanmeiai.com', +); +``` + +同时删掉第 5 行「TODO(联调临时)…发版前改回」那条注释(已落地)。 + +- [ ] **Step 4: 跑测试确认通过** + +Run: `cd client && flutter test test/unit/api_config_test.dart` +Expected: PASS。 + +- [ ] **Step 5: 移除 Android 全局明文开关** + +`client/android/app/src/main/AndroidManifest.xml:30`,把 `` 上的: + +``` + android:usesCleartextTraffic="true"> +``` + +改为(去掉该属性,闭合标签接到上一属性行;控制面已 https,不再需要明文豁免): + +``` + android:icon="@mipmap/ic_launcher"> +``` + +> iOS/macOS 无 ATS 配置(已确认),https 天然满足 ATS,**无需改任何 plist**。 + +- [ ] **Step 6: analyze + 全量单测** + +Run: `cd client && flutter analyze --no-fatal-infos && flutter test test/unit test/widget test/contract` +Expected: analyze 无 error;测试全绿(含新 `api_config_test`)。 + +- [ ] **Step 7: Commit** + +```bash +git add client/lib/services/api_config.dart client/android/app/src/main/AndroidManifest.xml client/test/unit/api_config_test.dart +git commit -m "feat(client): 控制面基址默认 https://api.yanmeiai.com + 移除 Android 明文开关" +``` + +--- + +## Task 3: CI 守护 —— Android release manifest 禁明文 + +防止将来有人把 `usesCleartextTraffic="true"` 加回来(回退明文)。 + +**Files:** +- Create: `ci/scan-cleartext.sh` +- Modify: `.gitea/workflows/ci.yml`(新增 job + shellcheck 列表) + +- [ ] **Step 1: 写扫描脚本** + +创建 `ci/scan-cleartext.sh`: + +```bash +#!/usr/bin/env bash +# scan-cleartext.sh — 禁止 Android manifest 重新开启全局明文(控制面已 https/CF Tunnel)。 +# usesCleartextTraffic="true" 会让全 app 允许明文 HTTP,退回 #25 之前的不安全态。 +set -euo pipefail + +MANIFEST="client/android/app/src/main/AndroidManifest.xml" +if grep -q 'usesCleartextTraffic="true"' "$MANIFEST"; then + echo "❌ $MANIFEST 含 usesCleartextTraffic=\"true\":控制面已 https,禁止全局明文。" >&2 + echo " 如个别调试域名确需明文,请用 res/xml/network_security_config.xml 按域白名单,勿开全局。" >&2 + exit 1 +fi +echo "✅ Android manifest 未开启全局明文" +``` + +- [ ] **Step 2: 本地跑一遍(应通过,因 Task 2 已移除)** + +Run: `bash ci/scan-cleartext.sh` +Expected: `✅ Android manifest 未开启全局明文`。 + +- [ ] **Step 3: 反向自测(临时加回应失败)** + +Run: +```bash +sed -i.bak 's#android:icon="@mipmap/ic_launcher">#android:icon="@mipmap/ic_launcher" android:usesCleartextTraffic="true">#' client/android/app/src/main/AndroidManifest.xml +bash ci/scan-cleartext.sh; echo "exit=$?" +mv client/android/app/src/main/AndroidManifest.xml.bak client/android/app/src/main/AndroidManifest.xml +``` +Expected: 打印 ❌ 且 `exit=1`;还原后文件复原。 + +- [ ] **Step 4: 接入 CI** + +`.gitea/workflows/ci.yml`:(a)在 lint job 的「shellcheck CI 脚本」列表(约 42-58 行)加 `/mnt/... ` 对应项前,先把 `ci/scan-cleartext.sh` 纳入 shellcheck——注意该文件在 `ci/` 非 `scripts/ci/`,复用已有的 redline-scan 挂载方式即可;(b)新增 job: + +```yaml + cleartext-scan: + name: Cleartext Scan — Android 禁明文 + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: scan Android manifest for global cleartext + run: bash ci/scan-cleartext.sh +``` + +- [ ] **Step 5: Commit** + +```bash +git add ci/scan-cleartext.sh .gitea/workflows/ci.yml +git commit -m "ci: 守护 Android manifest 禁全局明文(#25 控制面已 https)" +``` + +--- + +## Task 4: 服务端 `PANGOLIN_PUBLIC_URL` 切 https(规则集下载基址) + +`PANGOLIN_PUBLIC_URL` 被嵌进**客户端 sing-box 配置**当 `.srs` 分流规则集下载基址(`clientconfig.go:150-161`,`download_detour:"direct"`)。不改的话新客户端仍去 `http://103.119.13.48:8080` 拉。此步与 Task 1 隧道并存,对新旧客户端都安全(URL 由服务端下发,客户端只是照着 GET)。 + +**Files:** +- Modify: `deploy/single-node/deploy.sh:178` + +- [ ] **Step 1: 改 deploy.sh 的 server.env 默认** + +`deploy/single-node/deploy.sh:178`,把: + +``` +PANGOLIN_PUBLIC_URL=http://$VPS_IP:$HTTP_PORT +``` + +改为: + +``` +PANGOLIN_PUBLIC_URL=https://api.yanmeiai.com +``` + +- [ ] **Step 2: 在 pangolin1 应用 + 重启 server**(ssh,改机器,先确认) + +```bash +ssh pangolin1 "sed -i 's#^PANGOLIN_PUBLIC_URL=.*#PANGOLIN_PUBLIC_URL=https://api.yanmeiai.com#' /etc/pangolin/server.env && systemctl restart pangolin-server && sleep 2 && systemctl is-active pangolin-server" +``` + +Expected: `active`。 + +- [ ] **Step 3: 验证下发配置里规则集基址已是 https** + +用一个测试账号取一份客户端配置(经隧道),断言规则集 URL 走 https: + +```bash +curl -fsS -m 10 https://api.yanmeiai.com/v1/rules/geoip-cn.srs -o /dev/null -w '%{http_code}\n' +``` + +Expected: `200`(规则集经隧道可下载)。并在有测试 token 时抓一份 `/v1/...` 客户端配置,确认内嵌 `route.rule_set[].url` 前缀为 `https://api.yanmeiai.com`。 + +- [ ] **Step 4: Commit** + +```bash +git add deploy/single-node/deploy.sh +git commit -m "feat(deploy): PANGOLIN_PUBLIC_URL 改 https://api.yanmeiai.com(客户端规则集走隧道)" +``` + +--- + +## Task 5: 退役明文口 —— 8080 收 loopback + 关防火墙 + 修健康检查 + +> **⚠️ 上线顺序闸:此 Task 会切断外部 `http://103.119.13.48:8080`,只有当现网客户端都已更新到 Task 2 的 https 版本后才能执行。** 执行前与用户确认「旧客户端可弃」。做完后一切经隧道/loopback,数据面 :443 不受影响。 + +**Files:** +- Modify: `deploy/single-node/deploy.sh:167`(ADDR 收 loopback)、`:272-275`(去掉 ufw 放行 8080) +- Modify: `scripts/ci/deploy-server.sh:57`(健康检查改本地) + +- [ ] **Step 1: deploy.sh — ADDR 绑 loopback** + +`deploy/single-node/deploy.sh:167`,把 `ADDR=:$HTTP_PORT` 改为: + +``` +ADDR=127.0.0.1:$HTTP_PORT +``` + +- [ ] **Step 2: deploy.sh — 不再放行 8080(loopback 后无需外开)** + +`deploy/single-node/deploy.sh:272-275` 的 ufw 放行段删除或改注释(8080 已 loopback,外部本就不可达): + +```bash +# 控制面 API 已绑 127.0.0.1(经 cloudflared 隧道对外),不放行 8080/tcp。 +``` + +- [ ] **Step 3: deploy-server.sh — 健康检查改 ssh 内本地 curl** + +`scripts/ci/deploy-server.sh:57`,把 runner 远程: + +``` +curl -fsS -m 10 --retry 5 --retry-connrefused "http://${DEPLOY_HOST}:8080/healthz" >/dev/null && echo "healthz OK" +``` + +改为经隧道校验对外可达 + ssh 内本地兜底(二选一或都留,推荐经隧道最贴近真实客户端路径): + +```bash +$SSH "root@${DEPLOY_HOST}" 'curl -fsS -m 10 --retry 5 --retry-connrefused http://127.0.0.1:8080/healthz >/dev/null && echo "healthz(local) OK"' +curl -fsS -m 10 --retry 5 "https://api.yanmeiai.com/healthz" >/dev/null && echo "healthz(tunnel) OK" +``` + +- [ ] **Step 4: 在 pangolin1 应用 loopback 绑定**(ssh,改机器,先确认客户端已迁移) + +```bash +ssh pangolin1 "sed -i 's#^ADDR=.*#ADDR=127.0.0.1:8080#' /etc/pangolin/server.env && systemctl restart pangolin-server && sleep 2 && systemctl is-active pangolin-server" +``` + +Expected: `active`。 + +- [ ] **Step 5: 验证明文口已死、隧道仍活** + +```bash +curl -fsS -m 8 http://103.119.13.48:8080/healthz && echo "!! 不该还通" || echo "旧明文口已不可达(预期)" +curl -fsS -m 10 https://api.yanmeiai.com/healthz && echo " <= 隧道仍 OK" +ssh pangolin1 'ss -ltnp | grep ":8080" | grep 127.0.0.1 && echo "8080 已仅 loopback"' +``` + +Expected: 明文口失败;隧道成功;`ss` 显示 8080 仅监听 `127.0.0.1`。 + +- [ ] **Step 6: Commit** + +```bash +git add deploy/single-node/deploy.sh scripts/ci/deploy-server.sh +git commit -m "feat(deploy): 8080 收 loopback + 关 8080 防火墙 + 健康检查改本地/隧道(退役明文控制口)" +``` + +--- + +## Task 6: 文档更新(端口/URL 布局) + +**Files:** +- Modify: `CLAUDE.md`(项目根,worktree 内那份)— 端口布局说明 +- Modify: `docs/index.html` — 登记本方案 HTML 阅读版 + +- [ ] **Step 1: 更新 CLAUDE.md 端口/URL 描述** + +在 `deploy/ 结构` 或 server 段补一句:控制面 API 对外经 **CF Tunnel** `https://api.yanmeiai.com`(源站 `127.0.0.1:8080`,不外露);数据面 sing-box REALITY 仍独占 `:443`;gRPC agent mTLS `:9443`。 + +- [ ] **Step 2: 生成本方案 HTML 阅读版并登记 index** + +按既有深色 HTML 家族样式,把本 plan 同内容生成 `docs/control-plane-tls-tunnel.html`,登记进 `docs/index.html` 的「实现计划」分类。 + +- [ ] **Step 3: Commit** + +```bash +git add CLAUDE.md docs/control-plane-tls-tunnel.html docs/index.html +git commit -m "docs: 控制面 CF Tunnel/端口布局说明 + 方案 HTML 登记 index" +``` + +--- + +## Self-Review + +**Spec coverage:** +- ✅ CF Tunnel 前置控制面 → Task 1。 +- ✅ 客户端默认 http→https → Task 2。 +- ✅ Android 移除 usesCleartextTraffic → Task 2(iOS/macOS 无 ATS 需改,已核实)。 +- ✅ server 8080 收 loopback → Task 5(带上线顺序闸)。 +- ✅ `PANGOLIN_PUBLIC_URL` 同步 https(Explore 发现的隐藏依赖)→ Task 4。 +- ✅ 健康检查随 loopback 调整 → Task 5。 +- ✅ 数据面 :443 不动 → 全程未触碰 sing-box(约束显式声明)。 +- ✅ fallback(域名被封退直连 IP)→ 明确拆到 #32,不在本轮。 + +**上线顺序验证:** Task 1(隧道加法)→ Task 4(PUBLIC_URL,新旧客户端皆安全)→ Task 2(客户端切 https,发版)→ **待客户端更新** → Task 5(收口)。Task 3(CI 守护)、Task 6(文档)无顺序耦合。 + +**Placeholder / 一致性:** Task 1B Step 6 的 `$(command -v cloudflared)` 已在注释显式提示落地时改为无 `$()` 写法(禁 `$()` 全局约束);token 全程不落 git;`kApiBaseUrl` 名称跨 Task 2/守护测试一致。 + +## 不在本轮 +- #32 控制面 fallback(CF 域名被 SNI 封 → 客户端退回直连节点 IP 的 https 控制口)。 +- 控制面 API 的 CF WAF/rate-limit 规则精调。 +- usercenter(web/usercenter)也接入同域名 API(其部署属 #30 30A)。