Files
dsh-lan-access/README.md
T
sutong ced05e06a3 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,且与直连回环逐项行为一致
2026-09-27 03:49:12 +08:00

124 lines
7.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# @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