香菜的博客
全部项目

PROJECT · 2026

DSH 手机访问:反向隧道 + 一键开关插件

家里 CGNAT 没有公网 IPv4,也能用手机随时随地打开 DSH Web UI:反向隧道 + 服务器 nginx 反代 + 输入框下方两个一键开关。

运行中 DeepSeek HarnessDSH 插件内网穿透SSHnginxPowerShell

要解决的问题

家宽 WAN IP 是 100.64.x.x(RFC 6598 CGNAT),没有公网 IPv4;路由器里翻遍了也没找到 IPv6 入站放行在哪;腾讯云安全组也不想再开新端口。但我想在地铁上、在公司、在任何网络下,都能用手机打开家里的 DeepSeek Harness。

最后做成的结构:

手机(4G / 任意 WiFi)
   │  https://dsh.example.com      ← 全链路 TLS
   ▼
云服务器 :443
   │  nginx proxy_pass → 127.0.0.1:18081(只绑 loopback)
   ▼
SSH 反向隧道(家里主动拨出,断线 10s 重连,登录自启)
   ▼
家里的 DSH Pocket 代理 :3081(PIN 验证)
   ▼
dsh web :3080

核心思路一句话:既然外面进不来,就让家里主动连出去,服务器只做一层 nginx 反代。腾讯云安全组一条规则都不用改(只用已经开着的 22/80/443)。

三条被否掉的路线

想法为什么不行
路由器端口映射到公网 IPv4CGNAT 段内的地址互联网不可路由,映射了也白映射
直接用原生公网 IPv6机器上确实有 240e: 地址,但 dsh-pocket 的代理 server.listen(port,'0.0.0.0') 是纯 IPv4,v6 连不上;还得在路由器(可能还有光猫)两层放行入站
DDNS 更新 AAAAWindows 同时有两条 IPv6:SuffixOrigin=Link 稳定地址与 SuffixOrigin=Random 临时地址(约一天轮换、出站优先用它)。「查我的 IP」问回来的是临时地址,写进 DNS 几小时就失效

还有个隐藏前提:DSH 前端多处调用 crypto.randomUUID(),该 API 只在 secure context(https 或 localhost)存在,所以用 http://192.168.x.x 裸开是会白屏的——必须走 HTTPS,或者用插件注入 polyfill。

做出来的三块

1. DSH 插件 @local/dsh-net-toggle —— 聊天输入框下面多一行:

手机访问   ● 局域网 已开   ● 互联网 已开   [复制链接]
  • 宿主侧注册 /dsh-net 前缀路由,认证直接复用 DSH 自己的围栏 ctx.connection.requestRejection(req),没带浏览器 cookie 一律 401
  • 局域网开关改 dsh-pocket 的 settings.json(它的代理每次请求重读配置,即时生效、不用重启)
  • 互联网开关调隧道脚本 -InstallTask / -Remove,不自己实现隧道,保证隧道只有一个 owner
  • 浏览器侧用 ctx.slots.inject('conversation.composer.dock', …) 注册组件,5 秒轮询状态

2. dsh-tunnel.ps1 —— Windows 隧道守护:ssh -N -R 循环重连、启动文件夹 VBS 自启、pid 文件供插件读状态。用 WMI Win32_Process.Create 启动,让守护的父进程变成 WmiPrvSE.exe,脱离 DSH 进程树。

3. 服务器 nginx + certbot —— 443 反代到隧道端口,必须透传 Upgrade(DSH 的 RPC 是 WebSocket),proxy_read_timeout 3600s;80 只做 ACME 校验和跳转。

踩过的坑(都修了)

  1. VBS 的引号规则和命令行不一样:Run ""C:\app.exe" -arg" 在 VBScript 里是语法错误("" 是空字符串)。正确做法是整条命令做成一个字符串字面量,路径不含空格时干脆不加引号。另外我用 -Encoding ASCII 写 VBS,中文目录名被写成 ????,启动项静默失效——开机自启从来没成功过,之前的隧道全是我手动起的。
  2. Copy-Item 自我复制:安装逻辑「把脚本复制到稳定路径」,从稳定路径运行时源=目标,直接 IOException,守护还没启动就中止。
  3. taskkill 死 PID + ErrorActionPreference='Stop':pid 里的进程早死了,taskkill 往 stderr 写一行,被当成终止错误,后果同上。
  4. 测的路径 ≠ 用户走的路径:我一直用工作区那份脚本测试(源≠目标),而按钮和启动项走 %LOCALAPPDATA% 那份(源=目标),所以 2、3 两个 bug 一直没暴露。教训:按用户实际触发的入口测试。
  5. 进程树:Start-Process 起的守护挂在 dsh 进程树里,dsh 一重启就被带走 → 公网 502。改用 WMI 创建。

安全上做的几件事

  • 只用到已经开放的 22/80/443,安全组零新增规则
  • 服务器上的隧道端口只绑 127.0.0.1,外网碰不到
  • 入口 PIN(8–64 位字母数字)+ 同 IP 5 次/60 秒限速
  • 实测「关掉局域网不会把自己锁在门外」:局域网关闭后公网入口仍可访问

已知限制

  • 家里电脑要开着、dsh web 在跑、隧道守护在跑;休眠会断,唤醒后约 10 秒自动重连
  • dsh web 重启后手机要重新输一次 PIN(cookie 绑定进程 sessionKey)
  • 开机自启是「登录 Windows 后」触发,停登录界面不会起

附件是脱敏后的完整工程(域名、IP、密码都换成了占位符):插件源码、隧道脚本、nginx 配置、签发脚本和一份 README。