VPS 上如何访问 DSH Web UI
DSH Web UI 默认绑定 127.0.0.1:3080,VPS 输出的 URL 无法直接在笔记本打开。本文介绍 3 种安全访问方式,并说明为何不应改为公网监听。
为什么从笔记本电脑无法打开 DSH Web UI
DSH Web UI 绑定到回环接口,因此终端输出的 URL 只能在输出该 URL 的计算机上使用。npx @deepseek-ai/dsh web 报告 http://127.0.0.1:3080,而 127.0.0.1 对读取它的计算机来说,表示“这台计算机”。您的笔记本电脑读取该地址后,会查看自身的回环接口,但那里没有任何程序监听。如果问题在于该地址本身,请参阅dsh 为什么会首先输出 http://127.0.0.1:3080,其中有更详细的说明。不要更改 DSH 的绑定地址。请从笔记本电脑建立一条经过身份验证的路径,连接到 VPS 的回环地址。
默认使用回环接口是正确的,本页中的所有方法都会保留这一设置。3080 端口后面运行的是一个可在服务器上执行 shell 命令的代理,而 Web UI 不提供登录页面。回环接口是 DSH 在这里唯一的访问控制:要访问该套接字,您必须已经拥有该服务器上的 shell。
DSH 绑定到什么地址,以及如何检查
截至 17 August 2026,DeepSeek Harness README 写明,npx @deepseek-ai/dsh web“默认在 http://127.0.0.1:3080 启动 Web UI”。同一仓库中的 CLI 参考文档将 --port <num> 记录为覆盖项,其默认值为 3080;--host <addr> 是一个“有意拒绝 0.0.0.0”的覆盖项。DSH 仍处于开发者预览阶段,README 以大写字母警告后续会有破坏兼容性的更改。请在自己的安装中确认这两个值,不要依赖任何页面,包括本文。
dsh --profile web --dump-config
ss -ltnp | grep 3080--dump-config 会在不启动 agent 的情况下输出组合后的配置树,因此可以显示经过所有补丁层后最终生效的主机和端口。ss 会显示当前实际处于监听状态的地址。正常输出类似这样:
LISTEN 0 511 127.0.0.1:3080 0.0.0.0:* users:(("node",pid=8123,fd=24))只读取冒号前的地址。127.0.0.1:3080 仅绑定 loopback,这正是所需的配置。0.0.0.0:3080 表示绑定服务器上的所有网络接口,包括公网接口。如果 ss 完全没有输出,说明 DSH 未运行;在它启动前,任何隧道都无法解决问题。在 VPS 上安装并运行 DeepSeek Harness介绍了如何完成这一步。
标志只对单次运行生效。若要永久保存某个值,请改用 profile 补丁层。dsh --profile <name> 会在 $DSH_HOME/profiles/<name> 启动 profile,补丁层按以下顺序叠加:bundle 补丁、profile 自身的 cordis.patch.yml、用户主目录级别的 $DSH_HOME/cordis.patch.yml,以及任何 --patch overlay。监听设置位于 @deepseek-ai/dsh-host-webserver 插件下,分别对应 host 和 port。编辑前,请先使用 --dump-config 读取自己的组合配置树,因为 Web profile 首次启动时会根据随软件发布的模板自动写入自身配置,而预览版本仍在持续改变该模板的结构。
第二道关卡:/api 信任边界
让数据包到达 3080 端口只是问题的一半。DSH 还有第二项检查。它会导致一种容易混淆的故障:页面可以加载,布局也会显示,但随后所有功能都无法使用。
@deepseek-ai/dsh-client-connection 插件包含一个 trustedHosts 设置项,文档中的说明是“此部署除 loopback 外提供服务的权威:精确的 host:port,或不带端口、可匹配任意端口的 host”。如果 Host 请求的 /api 请求头既不是 loopback,也没有列在该设置中,信任边界就会拒绝它。浏览器会这样报告拒绝:
transport failure for /api/host.describe: HTTP 403因此,放在 DSH 前面的任何代理都必须声明浏览器地址栏中输入的名称。CLI 接受 --trusted-host <authority>,并且该参数可以重复指定:
dsh web --trusted-host dsh.example.com --trusted-host dsh.example.com:8443信任边界会将 Host 请求头作为普通字符串进行比较,因此上面会出现两种写法。对它而言,dsh.example.com 和 dsh.example.com:8443 是两个不同的权威,localhost:3080 和 127.0.0.1:3080 也是如此。无法解释的 403 几乎总是因为地址栏中的 URL 与信任列表中的写法不同。
信任边界执行的是 origin 检查,不是身份验证。浏览器本身禁止网页设置 Host 请求头,因此该检查确实可以阻止其他网站上的页面控制您的代理。任何非浏览器客户端都可以自由写入该请求头。应将 trustedHosts 视为代理的兼容性设置,绝不能将其作为安全控制。
方案 1:SSH 本地转发
优先使用此方案,因为它完全不会改动服务器。
ssh -N -L 127.0.0.1:3080:127.0.0.1:3080 you@your-vps-L 会在笔记本电脑上监听,并将每个连接转发到 VPS 解析到的 127.0.0.1:3080。-N 表示“不运行远程命令”,因此会话只承载转发,不执行其他操作。将本地端写成 127.0.0.1:3080 而不是简单的 3080 是有意为之:它会将笔记本电脑一侧的监听器绑定到 loopback,这样 SSH 客户端配置中的 GatewayPorts 设置就不会悄然将代理重新发布到当前所在的网络。
现在在浏览器中打开 http://localhost:3080。这里有两点会自动生效。Host 标头表示 loopback 权限源,因此 /api 限制无需任何配置即可通过。浏览器还会将 http://localhost 视为安全上下文。这一点很重要,因为 DSH Web 应用启动时会调用 crypto.randomUUID(),而浏览器只会通过 HTTPS 或 loopback 来源开放该功能。
添加 -f,让 ssh 在转发建立后自动转入后台:
ssh -f -N -L 127.0.0.1:3080:127.0.0.1:3080 you@your-vps需要明确的是,这种方案不会在公网地址上新增监听器,也不会修改防火墙规则,因此暴露面增幅最小。全部访问控制都依赖 SSH 配置,所以必须使用仅密钥认证并禁用密码登录的 SSH,而不能将其视为可选增强措施。代价是隧道只属于一个客户端会话。笔记本电脑进入睡眠状态时隧道会断开,需要手动重新启动,而且手机无法使用该隧道。
选项 2:在 VPS 上使用 Tailscale serve
Tailscale 会在您自己的设备之间建立专用网络。在 VPS 上安装 Tailscale 后,该计算机会获得一个只有您的设备可以路由到的 100.x.y.z 地址。
加入该网络还不够,这也是许多人遇到问题的地方。DSH 没有监听 100.x.y.z 地址,因为它监听的是回环地址。浏览器访问 http://100.x.y.z:3080 时会收到连接被拒绝,因为没有套接字绑定到该地址。
tailscale serve 负责连接这两端。它运行在 VPS 上,接收来自专用网络的流量,并将其代理到本地地址:
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up
sudo tailscale serve --bg --https=443 localhost:3080
tailscale serve statustailscale serve status 会输出 URL,格式为 https://<machine>.<tailnet>.ts.net/。使用该名称作为可信名称启动 DSH,否则 /api 会返回 403,具体如上所述:
dsh web --trusted-host your-vps.your-tailnet.ts.net这也解决了安全上下文问题。Tailscale 会使用为该名称签发的证书终止真正的 HTTPS,因此 crypto.randomUUID() 可用,UI 可以在手机浏览器中启动。流量不会到达公网,因为 serve 只会在您的专用网络内部发布。
不要在这里替换为 tailscale funnel。它属于同一命令系列,但面向公网,会在任何人都能解析的主机名上暴露一个具有 shell 访问权限的代理。输入任一命令前,请先阅读了解 serve 如何将流量限制在您的 tailnet 内,而 funnel 会将其发布到公网。如需查看适合手机使用的完整配置,请参阅从手机访问自托管代理。
代价是增加了一项依赖。所有需要使用 UI 的设备都必须加入该网络,并且由您不负责运行的协调服务器决定哪些设备可以加入。如果这不可接受,请使用自托管 Headscale 控制服务器;它在您拥有的硬件上使用相同的协议。
选项 3:经过身份验证的 TLS 反向代理
当浏览器无法加入私有网络时使用此方案,例如访问者使用的是您不管理的设备。此时,您会将一个主机名发布到互联网,因此身份验证必须真实有效,并且必须由代理执行。DSH 不提供身份验证。
TLS 表示传输层安全,即 https:// 背后的加密机制。让 nginx 指向回环端口,并在前面配置密码。map 块应放在 http 上下文中,因此为它在 /etc/nginx/conf.d/ 下创建单独的文件:
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}然后配置站点本身:
server {
listen 443 ssl;
server_name dsh.example.com;
ssl_certificate /etc/letsencrypt/live/dsh.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/dsh.example.com/privkey.pem;
auth_basic "dsh";
auth_basic_user_file /etc/nginx/dsh.htpasswd;
location / {
proxy_pass http://127.0.0.1:3080;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 3600s;
proxy_buffering off;
}
}创建密码文件、测试配置并重新加载:
sudo apt install -y apache2-utils
sudo htpasswd -c /etc/nginx/dsh.htpasswd you
sudo nginx -t && sudo systemctl reload nginxnginx -t 应输出 syntax is ok 和 test is successful。如果输出错误,nginx 会继续使用旧配置,因此当前服务尚未中断。然后以信任公网名称的方式启动 DSH,因为 proxy_set_header Host $host 会转发 dsh.example.com,否则防护机制会拒绝该请求:
dsh web --trusted-host dsh.example.com其中有两个指令不可缺少。Upgrade 和 Connection 请求头用于保持界面的长连接;没有它们,nginx 会关闭连接,导致界面在显示过期状态的同时反复重连。proxy_read_timeout 3600s 可防止 nginx 中断运行时间超过默认 60 秒的代理任务。其余配置的原因请参阅逐条解释 nginx 反向代理配置,代理选择本身请参阅nginx 与 Caddy 和 Traefik 的对比。
请明确基本身份验证能提供什么保护。它可以阻止随机扫描,并且远胜于直接暴露端口。但它只是在 shell 前设置一个共享密码,没有第二因素,也无法撤销某一个人的访问权限。任何获得该密码的人,都可以以 DSH 运行所用的用户身份执行命令。如果多人需要访问,应改用真正的身份代理,并在进一步扩大访问范围前阅读在 VPS 上运行编码代理的安全规则。
绑定到 0.0.0.0 并开放端口会怎样
最直接的做法是重新绑定并开放防火墙端口。DSH 会阻止前半步。--host <addr> 会有意拒绝 0.0.0.0,而 @deepseek-ai/dsh-host-webserver 插件文档说明,host 只接受 127.0.0.1 或 0.0.0.0,因此只有一个受支持的值,另一个值则会被 CLI 拒绝。社区中存在可以移除这项检查的插件。这些插件附带警告,而且警告内容准确。将插件层用于相反的方向更有价值:通过 支出上限和工具权限规则 限制代理可以执行的操作,而不是扩大其监听范围。
强制这样配置的实际结果如下。端口 3080 会在您的公网 IP 地址上响应。这里没有登录页面。/api 防护会检查 Host 标头,而任何非浏览器客户端都可以自行写入该标头,因此将您的地址列入 trustedHosts 对持有 curl 的攻击者不起任何作用。您公开的是一个可以以您的用户身份执行 shell 命令的代理,以及 $DSH_HOME 中保存的凭据;DSH 会在其中保存配置文件和 API 密钥。这相当于让服务器暴露远程代码执行能力,同时还会产生您的服务商账单。扫描器会持续扫描整个地址空间,因此未列出的 IP 不是有效的控制措施。代理可以读取的每个密钥 都会随 shell 命令发送出去。
上面的每种方法都旨在避免您必须这样做。对于一台笔记本电脑上的单个用户,SSH 转发是正确的默认方案。涉及手机,或需要真正的证书时,改用 tailscale serve;只有在必须让您无法管理的浏览器访问 UI 时,才使用公网反向代理,并在其前面配置真正的身份验证。
FAQ
为什么从我的笔记本电脑无法打开 http://127.0.0.1:3080?
因为 127.0.0.1 表示“读取此地址的计算机”。DSH 在 VPS 上输出该地址,因此在 VPS 上是正确的。您的笔记本电脑读取同一个字符串后,会访问自身的 loopback 接口,而该接口没有任何进程监听。请在 VPS 上使用 ss -ltnp | grep 3080 检查服务器端。显示 127.0.0.1:3080 的行表示 DSH 正在运行,并且按设计绑定到 loopback。您需要的是隧道或代理,而不是更换绑定地址。
可以使用 --host 0.0.0.0 运行 dsh web 吗?
不可以。仓库 CLI 参考文档于 17 August 2026 检查确认,--host <addr> 是一个会有意拒绝 0.0.0.0 的覆盖选项。原因是 DSH 会执行 shell 命令,但不提供登录页面。因此,绑定到所有接口会在您的公网 IP 地址上发布未经身份验证的 shell。社区补丁会移除这一检查。如果您应用此类补丁,防火墙以及 DSH 未提供的身份验证将由您负责。
为什么反向代理后的所有 /api 请求都返回 HTTP 403?
/api 信任边界会拒绝任何 Host 标头既不是 loopback 地址、也未列在 trustedHosts 中的请求。在代理后,该标头包含您的公网名称,因此信任边界会拒绝请求,浏览器会记录 transport failure for /api/host.describe: HTTP 403。使用 --trusted-host <your name> 启动 DSH,并确保拼写完全一致。如果 URL 中包含端口,也必须包含端口,因为比较采用字面字符串匹配。
为什么 DSH UI 可以加载,但通过普通 HTTP 启动时始终无法完成?
Web 应用启动时会调用 crypto.randomUUID()。浏览器仅在安全上下文中提供此功能,例如 HTTPS,或 http://localhost 之类的 loopback 来源。通过普通 HTTP 从裸 IP 地址提供服务时,该功能未定义,依赖它的调用会抛出错误,因此界面始终无法填充。使用 tailscale serve 通过 HTTPS 提供服务,或使用 TLS 反向代理,可以解决此问题。通过 http://localhost 使用 SSH 转发访问也可以解决此问题。
单个开发者应使用哪种方法?
使用 SSH 本地转发。它不会向服务器添加任何内容,也不会修改防火墙规则,并且可以复用您已经信任的 SSH 密钥。运行 ssh -f -N -L 127.0.0.1:3080:127.0.0.1:3080 you@your-vps,然后打开 http://localhost:3080。如果您希望在手机上使用 UI,或希望连接在笔记本电脑休眠后仍然保持,请改用 tailscale serve。