dsh-tailnet-gateway:经 Tailscale 私网访问本机 DSH,不把 Web 面暴露到公网

前言

DeepSeek Harness(DSH)的 Web 界面默认只监听本机回环地址,这是有意为之:dsh web --host 0.0.0.0 会被直接拒绝,错误信息会说明这样做会把远程代码执行暴露到网络上。即便配合 --trusted-host 放宽 Host 校验,聊天可用,Settings、Models、Plugins 等仍会在浏览器里显示「unavailable in this browser」——客户端在非 loopback 场景下不会发起对应 RPC。

若你已经在用 Tailscale 组网,希望用手机或其它 tailnet 设备操作家里或办公电脑上正在跑的 DSH,又不想把 dsh、局域网或公网端口敞开,就需要在「不拆掉 dsh 两道安全栅栏」的前提下,增加一条受控的访问路径。下面介绍社区插件 dsh-tailnet-gateway(维护者 OutLawZhangSan-liii,仓库 OutLawZhangSan-liii/dsh-tailnet-gateway)的设计与用法。插件以 MIT 许可发布,当前 package 版本为 0.1.0;README 写明在 dsh 0.1.5-rc.1、Web profile、Windows 上开发并验证过。

DSH 生态里能力多以插件形式挂载;插件目录站点 SkillHub 上的 dsh-tailnet-gateway 页 与 DeepSeek / 幻方无官方从属关系,安装前请自行阅读源码与许可证。

这是什么

dsh-tailnet-gateway 是一个无额外运行时依赖的 ES 模块 DSH 插件(入口 lib/index.js),在本机 127.0.0.1 上起一个带身份校验的反向代理,把仅绑定 loopback 的 dsh web 经 Tailscale Serve 发布到你的 tailnet。配套脚本 grant-access.mjs 用于给无法手动塞 Cookie 的浏览器(例如手机)发放一次性授权链接。

插件不扩大 dsh 的「请求信任」与「特权 RPC」两道栅栏,而是在它们前面加第二道门:通过改写 Host / Origin / Referer 为 loopback 权威,让上游 dsh 看到的是来自本机的合法 loopback 调用,而不是绕过校验。

访问链路

经过上面的步骤,典型路径如下(README 原文链路):

   phone (Tailscale app)
      │
      │  1. WireGuard, end-to-end encrypted, tailnet members only
      ▼
   tailscaled  ──  2. terminates TLS for the *.ts.net name
      │            3. stamps Tailscale-User-Login + X-Forwarded-For,
      │               OVERWRITING whatever the client sent
      ▼
   tailscale serve :443  ──  4. published with: tailscale serve --bg 7242
      │
      ▼
   gateway  127.0.0.1:7242   ──  5. this plugin. Two gates:
      │                             - the stamped login must be allowed
      │                             - optional per-device tailnet IP allowlist
      │                         6. rewrites Host/Origin/Referer to the
      │                            loopback authority, then forwards
      ▼
   dsh web  127.0.0.1:3080   ──  7. sees a genuine loopback call from this
                                     machine, so the privileged fence is
                                     satisfied honestly — not bypassed

要点有三:身份来自 tailscaled 覆写的头字段,tailnet 对端不能自报身份;网关与 dsh 监听都保持在 127.0.0.1;拒绝发生在连上 dsh 之前,失败请求不会打到上游。

核心功能与远程侧能力

能力 经 tailnet 访问
聊天、运行 agent、tools、jobs、workflows 可用
文件浏览/上传、session 与 goal 视图 可用
Settings、Models、Plugins、Agent presets 浏览器 UI 中不可用

最后一项是刻意设计:插件只实现访问侧网关,不 patch 客户端 bundle;相关页面会只读降级,服务端特权栅栏仍然生效。

安全相关行为在 test/gateway.test.js 中有测试覆盖,包括 loopback 绑定钳制、缺身份头即拒绝、X-Forwarded-For 取第一项、WebSocket 升级路径同样门禁等。

安装与启用

本插件不是通过 dsh plugin add github:... 安装,而是在 Web profile 的 patch 层挂载。需要先取得仓库副本(路径指向 文件 lib/index.js,不能是目录,否则 Node ESM 会报 ERR_UNSUPPORTED_DIR_IMPORT)。运行环境要求 Node >=20(见 package.json)。

1. 挂载插件

编辑 Web profile 的 %USERPROFILE%\.dsh\profiles\web\cordis.patch.yml,增加:

- insert:
    - id: tailnet-gateway
      name: '<absolute path to this repo>/lib/index.js'
      config:
        upstreamPort: 3080

若 profile 配置了 patchReload: live,运行中的 dsh 可能热加载该行,但仍建议用下面命令核实监听地址,而不是假设已生效:

Get-NetTCPConnection -State Listen | Where-Object { $_.LocalPort -in 3080, 7242 } |
  Select-Object LocalAddress, LocalPort, OwningProcess

3080 与 7242 都应是 127.0.0.1,且 OwningProcess 为同一 dsh PID;若出现 0.0.0.0 应视为异常。

2. 配置 Tailscale Serve

需要两次无法在脚本里完成的浏览器授权:

  1. 对 tailnet 启用 Serve:首次执行 tailscale serve --bg 7242 会提示 Serve is not enabled on your tailnet 并给出授权链接。
  2. 在 https://login.tailscale.com/admin/dns 的 HTTPS Certificates 中启用 HTTPS 证书;未开启时 tailscale cert 与 tailscale serve 会因同一根因而失败。

然后发布网关:

tailscale serve --bg 7242
tailscale serve status

3. 配置项(可选)

Key 默认值 含义
enabled true 是否启动监听
host 127.0.0.1 绑定地址,钳制为 loopback,非 loopback 会拒绝
port 7242 与 tailscale serve 发布的端口一致;0 表示由 OS 分配
requireLogin true 是否要求 Tailscale 身份;关闭后等价于开放的 loopback 代理
allowedLogins [] 允许的 Tailscale 登录名;空表示 tailnet 内任意账号
allowedPeers [] 可选按设备 tailnet IP 白名单(与 tailscale status 一致)
upstreamPort 3080 被代理的 dsh web 端口

进一步收紧示例:

      config:
        upstreamPort: 3080
        allowedLogins: ['you@example.com']
        allowedPeers: ['100.64.0.7']

allowedPeers 填错会把 tailnet 入口锁在外面;本机 127.0.0.1:3080 仍可用,需在本地改 patch。

典型用法:授权手机浏览器

先做环境变量,再运行 grant 工具:

$env:DSH_TAILNET_ORIGIN = 'https://<machine>.<tailnet>.ts.net'
node grant-access.mjs

脚本会打印一次性链接并生成 QR;若需要,再发布 grant 路由:

tailscale serve --bg --set-path=/grant http://127.0.0.1:7245

在目标设备的浏览器中打开该链接。之后日常使用 bare origin 即可:

https://<machine>.<tailnet>.ts.net/

Session Cookie 默认 30 天绝对有效期(非滑动窗口),dsh web 重启后仍有效,因为签名密钥持久化。到期后浏览器会再次出现 401 页(文案含 dsh web authentication required; reopen the URL printed by dsh web),服务端数据不受影响;在同一设备浏览器中重新执行 grant-access.mjs 并打开新链接即可。可选 --days 90 或 --days 7 调整寿命。

撤销所有设备:停止 dsh,从 %USERPROFILE%\.dsh\.credentials.yaml 删除 client-connection/browser-session 记录,再启动 dsh 使新密钥生效,然后对各设备重新 grant。

验证与运维

仓库内验证命令:

node --test test/gateway.test.js     # 15 unit + real-socket integration tests
node verify.mjs all                  # live gateway + full TLS chain
node verify.mjs cookie "<pair>"      # a specific cookie authenticates dsh

verify.mjs 的 chain 环节把 / 返回 401 视为成功,表示 dsh 会话校验已响应,说明前置链路通畅。

运维注意:

  • dsh 进程必须保持运行;关闭启动 dsh web 的终端会带走 tailnet 入口。
  • Windows 上 tailscale-ipn.exe 需在运行,否则 BackendState: NoState 会导致 Serve 等命令失败。
  • tailscale cert 可能在当前目录留下私钥文件,Serve 不依赖这些副本,用后可删除。

回滚:从 cordis.patch.yml 移除 tailnet-gateway 插入项并重启 dsh,再执行 tailscale serve --https=443 off。插件不写额外文件、无需 npm 安装包。

适用场景与注意

适合谁: 已有 Tailscale tailnet、需要在手机或其它私网设备上操作本机 DSH 日常能力(聊天、agent、文件等),且接受 Settings/Models/Plugins 等在远程浏览器中不可用。

务必知晓:

  • 插件与 dsh 同一进程权限运行;网关若被本机其它进程直接访问 127.0.0.1:7242 并伪造身份头,可能获得 dsh 本机特权 RPC 所拒绝的能力——README 明确这不构成「对抗已在机器上运行的代码」的边界。
  • tailnet 设备被攻破、Tailscale 账号泄露、或 Cookie 在 30 天内被窃取,都在威胁模型内;allowedLogins / allowedPeers 与 Tailscale ACL 用于收窄范围。
  • 文档与示例以 Windows / PowerShell 为主;Linux 用户需自行映射 %USERPROFILE% 与 Tailscale 客户端行为。

安装前请阅读 GitHub 仓库 全文(含 Troubleshooting 表)并核对 LICENSE(MIT)。

结尾

dsh-tailnet-gateway 在不动 dsh loopback 安全模型的前提下,用 Tailscale 身份与 Serve 把 Web UI 安全地延伸到 tailnet,并用 grant 流程解决手机浏览器的会话问题。目录页:https://www.skillhub.cn/plugins/OutLawZhangSan-liii/dsh-tailnet-gateway;源码与完整说明:https://github.com/OutLawZhangSan-liii/dsh-tailnet-gateway。

羽毛球分组比赛记分
小程序二维码

欢迎使用《羽毛球分组比赛记分》微信小程序

Xiaoye