feat: dsh-lan-access — 局域网访问 dsh Web UI
用独立反向代理把 dsh Web UI 发布到局域网。官方 CLI 主动拒绝 `dsh web --host 0.0.0.0`(会把宿主机远程代码执行暴露到网络), 因此本插件让官方服务器保持只绑回环,另开监听并转发。 宿主端(index.js) - 在 0.0.0.0:3082 监听,把 HTTP 与 WebSocket 转发到 127.0.0.1:<webServer 端口> - 上游 Host/Origin 重写为回环,使官方 /api 信任围栏「通过」而不是被绕过 - 不改动 Set-Cookie:浏览器按请求 URL 建立 host-only cookie,转发层无需干预 - 监听端 DNS 重绑定防护;targetHost 仅接受回环地址(不会变成开放代理) - /api/dsh-lan-access/summary 复用官方 connection.admit() 做围栏与浏览器认证 - 启动横幅打印带 launch token 的局域网链接 - 端口被占用时只告警不抛出,并在面板上报 listening:false,不展示不可用的二维码 - ownsHostCompat(默认关):仅为非回环页面声明 ownsHost,恢复官方设置界面 客户端(src/ 经 scripts/build-client.mjs 生成 client.js) - Settings → 局域网访问:局域网地址列表、复制/打开、选中地址的二维码 - 零依赖二维码编码器(byte 模式 / ECC M / version 1–10) 组合层 - 只 insert 一行,不覆盖任何 shipped row:停用本插件只会移除局域网监听, 回环 Web UI 不受影响 验证 - 宿主转发契约 28/28;客户端契约 25/25 - 二维码编码器与 npm qrcode 参考实现在全版本 × 全 8 掩码下逐模块比对 240/240 一致 - 端到端:局域网 token 首访 303 并铸 cookie → 应用 200;无凭证 401; WebSocket 升级 101,且与直连回环逐项行为一致
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
# @sutong/dsh-lan-access
|
||||
|
||||
局域网访问 dsh Web UI:一个**独立反向代理**,在 `0.0.0.0:<port>` 监听,把 HTTP 与 WebSocket 转发到回环 web 服务器,并**保留官方 launch token 登录**。
|
||||
|
||||
**简体中文** | [English](README.en.md)
|
||||
|
||||
## 为什么需要插件
|
||||
|
||||
`dsh web --host 0.0.0.0` 被官方 CLI 主动拒绝(`@deepseek-ai/dsh-web-app/lib/startup.js`:会把这台机器的远程代码执行能力暴露到网络)。所以官方 web 服务器始终只绑回环,局域网访问必须由插件在**另一个端口**上另开监听并转发。
|
||||
|
||||
## 与 dsh-lan-proxy 的区别
|
||||
|
||||
社区插件 `@wingsky-1/dsh-lan-proxy` 也做转发,但设计取向不同:
|
||||
|
||||
| | 本插件 | dsh-lan-proxy |
|
||||
|---|---|---|
|
||||
| 默认登录 | **必须持有 launch token**(横幅/二维码里的链接) | `injectToken: true`,局域网内任何设备免 token 直入 |
|
||||
| 上游 `Host` | 重写为回环(官方围栏通过) | 重写为回环 |
|
||||
| `Set-Cookie` | 不改动(浏览器按**请求 URL** 建立 host-only cookie,转发侧无需干预) | 需配合 token 注入与 401 重放 |
|
||||
| WebSocket | 握手按字节转发,`permessage-deflate` 端到端协商,**代理不解压任何帧** | 压缩桥接(存在解压放大面) |
|
||||
| 官方行覆盖 | **不覆盖任何 shipped row**,失败/禁用不会影响回环 UI | 插入自身行 + 兼容注入 |
|
||||
|
||||
> **端口冲突**:dsh-lan-proxy 默认占用 `3081`,因此本插件默认用 `3082`。两者可共存,但同时启用等于把 UI 暴露两次;建议只留一个。
|
||||
|
||||
## 安装
|
||||
|
||||
```sh
|
||||
dsh plugin --profile web add /path/to/dsh-lan-access
|
||||
```
|
||||
|
||||
安装后重启一次 `dsh web`:
|
||||
|
||||
```sh
|
||||
# 重启后终端会打印带 token 的局域网链接
|
||||
```
|
||||
|
||||
或在本仓库内用本地目录安装(Profile → Plugins → 本地安装)。
|
||||
|
||||
## 配置
|
||||
|
||||
配置写在插件行的 `config` 里(Host Profile → Plugins → 本插件 → Configure,或直接改 `cordis.patch.yml`)。
|
||||
|
||||
| 键 | 默认 | 说明 |
|
||||
|---|---|---|
|
||||
| `enabled` | `true` | 总开关;关闭后不再监听 |
|
||||
| `host` | `0.0.0.0` | 监听地址 |
|
||||
| `port` | `3082` | 监听端口(避开 dsh-lan-proxy 的 3081) |
|
||||
| `targetHost` | `127.0.0.1` | 上游主机。**仅接受回环地址**,否则插件拒绝启动(防止变成开放转发/SSRF) |
|
||||
| `targetPort` | `0` | 上游端口;`0` = 跟随 `webServer` 实际端口 |
|
||||
| `printBanner` | `true` | 启动时打印局域网链接 |
|
||||
|
||||
## 使用
|
||||
|
||||
1. **终端横幅**(启动时):
|
||||
|
||||
```
|
||||
dsh-lan-access: 0.0.0.0:3082 → http://127.0.0.1:3080 (LAN listener)
|
||||
dsh-lan-access: http://192.168.1.5:3082/?token=XXXXXXXX
|
||||
```
|
||||
|
||||
2. **Web UI 面板**:Settings → **局域网访问**(插件自带的设置页)。列出全部局域网地址、可复制、可直接打开,并为你选中的地址渲染**二维码**,手机扫一下即可进入。
|
||||
|
||||
链接里的 `token` 只在首次访问用到:服务器校验后会铸一个 30 天有效的 cookie 并 302 跳到干净的 `./`,**token 不会留在地址栏或历史记录里**。之后该设备靠 cookie 直接进入。
|
||||
|
||||
## 安全模型
|
||||
|
||||
- **认证不放松**:不注入 token、不绕过围栏。局域网设备必须拿到横幅或二维码里的链接。这比"信任整个局域网"更保守。
|
||||
- **官方围栏照旧生效**:`/api` 只接受回环或已声明的 authority,且 `Origin` 必须与 `Host` 同源;本插件把上游 `Host`/`Origin` 重写为回环,因此这些检查全部通过而不是被绕过。
|
||||
- **DNS 重绑定防护**:监听器只接受 `localhost`、回环地址、本插件公布的局域网 IP 字面量,或它自己的监听地址;域名一律断开。
|
||||
- **上游锁定回环**:`targetHost` 非回环直接拒绝启动,避免开放代理。
|
||||
- **无额外解压面**:中间不做响应/帧压缩,也不解压 WebSocket 帧。
|
||||
- **面板数据路由**:`/api/dsh-lan-access/summary` 挂在官方 web 服务器上,并**复用官方 `connection.admit()`**(同一套围栏 + 浏览器认证)——只有已登录页面能读到 token。
|
||||
- **⚠️ 风险面**:拿到该链接的设备拥有**完整 dsh 控制权,包括在宿主机执行命令**。请只在可信局域网启用;不使用时卸载或关闭插件。列表里的每一条链接都是一把钥匙。
|
||||
|
||||
## 局域网页面上的设置界面(ownsHostCompat)
|
||||
|
||||
DSH 官方把「持久化设置」限制在回环页面:客户端由 `location.hostname` 推导 `isLoopback`
|
||||
(`dsh-client-connection/lib/client.js`),非回环页面得到 `persistence = "memory"`
|
||||
(`dsh-client-ui-settings/lib/client.js`),于是设置镜像永远没有 `view`,模型/提供商页会报
|
||||
**settings are unavailable in this browser**,General 页的「打开配置文件」也会消失。
|
||||
|
||||
**这与转发是否正确无关**:任何局域网暴露方式都会遇到,用 `127.0.0.1:3080` 打开则一切正常。
|
||||
服务端也**没有**对应的回环门禁(`/api` 围栏 + token/cookie 才是授权面),所以本插件提供一个开关:
|
||||
|
||||
| 键 | 默认 | 说明 |
|
||||
|---|---|---|
|
||||
| `ownsHostCompat` | `false` | 为**非回环页面**声明 `ownsHost`,恢复官方设置界面(回环页面不受影响) |
|
||||
|
||||
本 profile 已设为 `true`。取舍:该页面因此可读写服务器设置(含凭据)。考虑到持有带 token
|
||||
链接的设备本就拥有完整控制权(含在宿主机执行命令),这不算新增一类能力,但确实绕过了官方的
|
||||
回环隔离 —— 设为 `false` 即回到官方姿态,设置请在本机通过 `127.0.0.1:3080`(或
|
||||
`ssh -L 3080:127.0.0.1:3080 <主机>` 隧道)修改。
|
||||
|
||||
## 排障
|
||||
|
||||
| 现象 | 原因与处理 |
|
||||
|---|---|
|
||||
| 终端出现 `LAN listener unavailable ... EADDRINUSE` | 端口被占用(常见:dsh-lan-proxy 占着 3081)。改 `port`,或停用另一个插件。面板会同时显示该错误,不会展示不可用的二维码。 |
|
||||
| 局域网设备打开后 401 | 用的是不带 token 的裸地址,且该设备还没有 cookie。改用横幅/面板里带 `?token=` 的链接。 |
|
||||
| 重启后旧书签失效 | launch token 每次启动都会变。已登录过的设备靠 cookie 仍然有效;否则重新用新链接进入。 |
|
||||
| 面板显示"未找到非内部 IPv4 地址" | 这台机器当前没有可用的局域网 IPv4(只有回环)。 |
|
||||
| 扫码无反应 | 部分机型对深色背景上的二维码识别差;面板里的二维码固定渲染在白底卡片上。适当调高屏幕亮度。 |
|
||||
|
||||
## 开发
|
||||
|
||||
```sh
|
||||
node scripts/build-client.mjs # 由 src/panel.js + src/qrcode.js 生成 client.js
|
||||
node scripts/smoke.mjs # 宿主端转发契约(HTTP/WS/Host/Origin/cookie/绑定失败)
|
||||
node scripts/test-qr.mjs # 与 npm qrcode 参考实现逐模块比对
|
||||
node scripts/test-client.mjs # 浏览器端契约(槽位注册、渲染、路由一致)
|
||||
```
|
||||
|
||||
`client.js` 是生成产物,请改 `src/` 后重新构建。
|
||||
|
||||
二维码编码器(`src/qrcode.js`)是零依赖自实现(byte 模式 / ECC M / version 1–10),与 npm `qrcode` 参考实现做了全版本 × 全掩码的逐模块比对(240/240 一致)。
|
||||
|
||||
## 回退
|
||||
|
||||
在 Plugins 页停用或卸载本插件即可;因为本插件**不覆盖任何官方行**,停用只会让局域网监听消失,回环 `dsh web` 不受影响。
|
||||
|
||||
## 许可
|
||||
|
||||
MIT
|
||||
Reference in New Issue
Block a user