LocalSync2026-08-24
从 FRP 翻车到 Cloudflared 固定域名:DSH 外网访问完整实战教程
起因:想把本机的 DSH(DeepSeek Harness)带出家门,用手机 4G 随时看会话、发消息。中间踩了一路坑:FRP 跨洋太慢、随机隧道域名天天变、证书报错、下载卡死……最后用 Cloudflared 命名隧道 + 固定域名 +「秘密路径免密」一步到位。本文记录完整起因、方案演进、踩坑与最终解法。
一、需求与背景
- 本机运行 DSH Desktop,Web 界面监听本机回环地址(默认 43120 端口),只有本机浏览器能访问;
- 想在外网(手机 4G / 公司网络)打开 同一个界面,看到和电脑完全一致的会话列表、实时消息;
- 两个硬要求:
1. 地址固定,不想每次变来变去;
2. 根路径要密码,但想留一条「只有我知道的秘密路径」可以免密直达(个人自用场景)。
二、方案一:自建 FRP 中转(翻车)
做了什么
- 台 VPS 上起 FRP 服务端(frps),绑定 vhostHTTPPort;
- 本机跑 frpc 客户端,把 DSH 端口穿透出去;
- 域名解析到 VPS,nginx 做反代 + Basic Auth + 秘密前缀路由。
踩的坑(按时间线)
-
端口 43120 会不会变?
查源码确认:DSH 的 Web 端口是持久化配置(默认 43120),重启不变,只有被占用才换端口。→ frpc 的 localPort 写死没问题。 -
页面白屏(静态资源 404)
SPA 的资源(/assets/、/plugins/)是绝对路径,秘密路径前缀一提就 404。→ 被迫改为全路径透传 + HTTP Basic Auth。 -
/api 全部 403(browser-trust fence)
DSH 有「浏览器信任围栏」:dynamicCordisRunner等敏感端点即使配了 trusted-host 也强制只认 loopback。前端加载插件时调这些 API 直接 403 → 页面卡在 Loading / Failed to load plugins。
试过:加connection.trustedHosts白名单(对普通端点有效,但对 loopback-only 端点无效);nginx 伪装 Host/Origin 为 127.0.0.1(对部分端点有效,但导致浏览器端 Origin 与页面地址不一致,恢复页)。 -
WebSocket 长连接被掐
代理用 HTTP/1.0 + 清空 Connection 头时,前端「connection lost, retry #N」刷屏。→ 换 HTTP/1.1 + Upgrade 支持后解决。 -
FRP 跨洋线路不稳定
手机 → VPS(境外)→ 国内电脑,跨洋 TCP 频繁重连(日志里大量 client exit / login 循环),加载慢、会话列表经常拉不出来。→ 这是 FRP 方案的根本死穴,物理距离决定论,优化只能缓解不能根治。
结论
FRP 自建不是不能用,但对「人在国内 + 服务器在境外 + 手机访问 DSH 这种实时界面」的场景,慢、不稳、配置复杂。弃用。
三、方案二:Cloudflared 命名隧道(最终方案)
为什么它快
- 手机 → 就近的 Cloudflare 边缘节点(全球节点,国内也有)→ 骨干网 → 本机 cloudflared,不再跨洋绕回;
- 相比 FRP 直连 VPS,路径短、线路稳。
最终架构
手机浏览器
│ https://你的域名/ ← 根路径:要求 8 位访问密码
│ https://你的域名/秘密路径/ ← 秘密路径:免密直达(自定义 patch)
▼
Cloudflare 边缘(DNS: CNAME → 隧道,橙云代理)
▼
命名隧道(cloudflared,随系统开机自启)
▼
本机 dsh-pocket 代理(默认 3081,负责 Host/Origin 改写 + 密码校验)
▼
本机 DSH Web(127.0.0.1:43120)
关键步骤
- 用 Cloudflare API 创建命名隧道,拿到隧道 Token(
cloudflared tunnel run --token …即可跑,无需本地凭据文件); - DNS 把域名改成 CNAME → 隧道ID.cfargotunnel.com,开橙云代理(不能是灰云裸 A 记录);
- 本机 cloudflared 指向 dsh-pocket 代理:
cloudflared tunnel run --token … --url http://127.0.0.1:3081; - 创建系统计划任务,开机自启。
踩坑补充(方案二阶段)
-
证书报错 ERR_CERT_COMMON_NAME_INVALID
Cloudflare 边缘证书本身合法(SAN 含泛域名),但手机 DNS 缓存旧记录(旧 FRP 的 A 记录),连到旧服务器 → 报证书错。解决:清 DNS 缓存 / 等运营商缓存过期。 -
cloudflared 下载卡死(“已等待 67 秒…”)
下载源 GitHub releases 国内慢。解决:直接把已装插件 bundle 里的 cloudflared.exe(约 54MB)复制到$DSH_HOME/dsh-pocket/bin/cloudflared-windows-amd64.exe,插件启动时检测到就直接用,不再下载。 -
临时隧道域名天天变
*.trycloudflare.com是 quick tunnel,每次重启都换。→ 正是因为要固定域名,才用命名隧道 + 自定义域名。
四、「秘密路径免密」的实现(核心技巧)
需求
- 根路径
https://你的域名/:要密码; - 秘密路径
https://你的域名/一段随机路径/:免密直达。
原理
dsh-pocket 代理(proxy.mjs)的认证逻辑是:
function authCheck(req, token) {
if (!token) return true; // 没配密码 → 直接放行
const cookieTok = parseCookies(req.headers.cookie)['dsh_pocket_token'];
if (cookieTok === token) return true; // cookie 带正确 token → 放行
const qTok = new URL(req.url, 'http://x').searchParams.get('token');
return qTok === token; // URL ?token= 正确 → 放行
}
即:只要浏览器带了一个有效的会话 Cookie,就放行所有请求(含 SPA 的资源 / API)。
补丁做法(在代理主处理块之前插入)
// 秘密路径免密入口
if (req.url.startsWith('/你的秘密路径/') || req.url === '/你的秘密路径') {
// 1) 剥前缀:秘密路径 → 根路径(DSH SPA 只认根路径)
req.url = req.url === 前缀 ? '/' : req.url.slice(前缀长度) || '/';
// 2) 若浏览器还没有有效 token cookie,301 到目标并种下 cookie(等价自动登录)
if (cookie['dsh_pocket_token'] !== theToken) {
res.writeHead(302, {
location: req.url,
'set-cookie': 'dsh_pocket_token=' + theToken + '; HttpOnly; SameSite=Lax; Path=/',
});
res.end();
return;
}
}
效果
- 第一次打开秘密路径 → 302 跳转到根路径并种 cookie → 浏览器自动带上 cookie → 之后所有请求(页面/资源/API)都免密;
- 直接打开根路径(无 cookie)→ 走正常认证 → 仍要求输入密码;
- 浏览器关闭后 cookie 失效(HttpOnly 会话 cookie)→ 下次再访问秘密路径又自动「登录」一次。
注意:这是「路径即钥匙」的轻量门禁,只适合个人单用户、域名不公开的场景。生产级多用户请用正式认证(如 Basic Auth / Cloudflare Access)。
五、最终效果与维护
- 固定域名 + 命名隧道:地址永不变化;
- 根路径密码:固定 8 位数字(写在
$DSH_HOME/dsh-pocket/token-lan,不点「刷新」就不会变); - 秘密路径:免密直达,方便日常手机快速进入;
- 开机自启:cloudflared 命名隧道注册为系统计划任务,DSH 重启自动恢复;
- 升级提醒:dsh-pocket 插件升级会还原 proxy.mjs 的补丁,升级后需重新打补丁(先备份原文件
proxy.mjs.bak)。
六、心得
- 先想清楚「物理链路」再动手:自建 VPS 跨洋,路由再优化也快不过 Cloudflare 边缘就近;
- DSH 的安全设计很严格:browser-trust fence 对 loopback-only 端点不会因为配了信任域名就放松,别在对外代理层硬扛;
- 复用现成生态:dsh-pocket 这类插件把「代理改写 + 隧道 + 密码」全包了,比自己手搓 frp+nginx 干净得多;
- 补丁要留备份 + 记录:改 node_modules 里的源码,一定留
.bak,升级后能快速恢复。

评论