DSH LAN Guard

把桌面 DSH 的官方 Web 界面安全地开放到局域网:带门禁的反向代理 + 默认自签 HTTPS + 手机扫码入口。DSH 自身的回环监听与官方 UI 零改动。

CI npm 版本 MIT DSH 0.1.7-rc.2

English | 中文

能做什么 · 快速开始 · 设置 · 兼容性 · 安全边界 · 排障 · 更新记录

> DSH LAN Guard 是 DeepSeek Harness 社区插件。它只注册官方设置页里的一个区块(`settings.section`),不替换任何官方布局,也不修改 DSH 源码或 DSH 自身的监听绑定。 DSH 的 Web 界面只监听 `127.0.0.1`,而官方明确拒绝绑定 `0.0.0.0`。本插件不碰那个绑定,而是在另一个端口上再开一个**带门禁的**监听,把官方界面原样代理出去:密码门禁、默认自签 HTTPS、手机扫码入口,以及可逐个批准与拉黑的设备配对。**装完重启一次即可用**——不需要手写任何配置。

设置 → 局域网访问 → 扫码访问:运行中状态、免密链接与可扫描二维码

## 能做什么 - **门禁反向代理**:完整转发 HTTP 与 WebSocket(含官方界面的 `/api/remote.mux` 长连接),改写 `Host`/`Origin`,剥离逐跳头,上游不可达时返回 `502`。DSH 自身的绑定与配置一动不动。 - **默认对外可用,但对外可达 ≠ 可以进入**:默认监听 `0.0.0.0`,所以安装后**只需重启一次**就能用;门禁默认开启,**未设访问密码时拒绝所有设备**,且默认自签 HTTPS——不存在明文传输。想只在本机使用,在设置页切到「仅本机」即可。 - **双密码门禁**:PBKDF2-SHA256(600,000 次迭代)。**访问密码**给手机等访客设备登录用,**管理密码**用于解锁本设置页的管理台(未设置时退回访问密码);另有 `dsh_` 免密链接、持久访客会话、按 IP 锁定与 CSRF 校验。 - **默认自签 HTTPS,CA 身份跨重启不变**:自动生成 `DSH LAN Guard CA`,按所选网卡地址签发叶证书。换 IP 只重签叶证书,所以每台设备只需信任一次。 - **本机永不锁定**:直连 `127.0.0.1` 享有物理免锁特权(能用这台电脑的人本就能改这些设置);远程访问按 `adminPolicy` 处理——只读(默认)、需密码解锁、或不锁。 - **设备配对与永久拉黑**:手机首次通过门禁时自己命名一次,获得 HttpOnly 设备身份 cookie,出现在 **已授权设备** 里(名称 / 创建时间 / 最近使用 / 来源 IP),可逐个「吊销并拉黑」。拉黑不依赖设备指纹——换浏览器用密码重新配对也会被拒,「解除拉黑」是唯一的恢复方式。 - **设置挂在官方设置页内**:「局域网访问」分区含四个 tab——扫码访问、安全认证、已授权设备、连接与证书。排版与配色全部使用官方设计 token,**不替换任何官方布局**。 - **端口与监听范围可配**:默认 `3081`(DSH 端口 + 1),被占用时自动往后顺延(最多试 10 个);设置页可改端口并带可用性检查,也可在「局域网(默认)/ 仅本机」之间切换(两者都需重启 dsh 生效)。 - **可选 mDNS**:默认关闭;开启后广播 `_dsh-lan-guard._tcp`。 - **升级检测**:设置页显示「当前版本 → npm 上最新版本」并给出可复制的升级命令;插件**不会自己安装或重启任何东西**。 ## 快速开始 环境要求: - 带 Web profile 的 DeepSeek Harness - Node.js 20 或更新 - 已验证的 DeepSeek Harness:`0.1.7-rc.2` 从 npm 安装: ```sh dsh plugin --profile web add dsh-lan-guard@latest ``` 从 GitHub 安装: ```sh dsh plugin --profile web add "github:idoall/dsh-lan-guard" ``` 从本地克隆安装: ```sh git clone https://github.com/idoall/dsh-lan-guard.git cd dsh-lan-guard pnpm install && pnpm run build dsh plugin --profile web add "link:$(pwd)" ``` 然后**重启一次 DSH**,打开 **设置 → 局域网访问**。重启后插件已经在对外监听(默认 `0.0.0.0:3081`,自签 HTTPS + 门禁),你只需要: 1. 在 **安全认证** 里设置**访问密码**(至少 8 位)。没设之前,门禁拒绝所有设备。 2. 在 **连接与证书** 确认**监听范围**(默认「局域网」),并选择对外公布的网卡——网卡决定二维码/访问地址用哪个 IP、自签证书签哪些地址。 3. 在 **扫码访问** 扫码,手机上信任一次 `DSH LAN Guard CA`、输入访问密码、给设备命名。之后手机运行的就是官方 DSH 界面。 > 远程设备默认是**只读**的(`adminPolicy: local_only`):能用 DSH,但不能改插件设置。想让手机也能管理,在桌面把策略改掉。 ## 设置 设置项集中在 **设置 → 局域网访问** 的四个 tab 里。非敏感开关(`enabled`、`listenPort`、`listenHost`、`networkInterface`、`auth.mode`、`auth.adminPolicy`、`auth.adminProtection`、`auth.allowLoopback`、`auth.requirePairing`、`auth.requireApproval`)可直接改;`listenPort` 与 `listenHost` 需重启 dsh 生效;`dataDir` 与 `tls.*` 属启动期字段,需在 profile patch 里改。 | 设置项 | 默认 | 作用 | | --- | --- | --- | | 监听范围 | **局域网 `0.0.0.0`** | 决定局域网能否访问这个端口。切到「仅本机 `127.0.0.1`」更保守,需重启 dsh。两种选择都保留门禁与自签 HTTPS。 | | 代理端口 | **3081** | DSH 端口 + 1;被占用时自动往后顺延(最多试 10 个),带可用性检查。需重启 dsh。 | | 对外公布的网卡 | 自动选择 | 决定二维码/访问地址用哪个 IP、自签证书签哪些地址;虚拟网卡会被降权标注。 | | 验证模式 | **扫码免密 + 密码** | 也可选「仅密码」或「仅安全 Token」。切换会**吊销所有已有访客会话**。 | | 访问密码 | 未设置 | 手机等访客设备的登录密码。**未设置时门禁拒绝所有设备**。 | | 管理密码 | 未设置 | 解锁本设置页的管理台;未设置时退回访问密码。 | | 本机回环访问免密 | **开** | `127.0.0.1` 直连跳过门禁(物理免锁)。 | | 新设备需要命名确认 | **开** | 新设备首次通过门禁时要自己命名一次,之后才出现在设备列表里。 | | 新设备需要管理员批准 | 关 | 开启后,命名完还要你在设备列表点「批准」才能进入。 | | TLS | **自签 HTTPS** | 关闭会明文传输门禁密码;非回环 + 关闭 TLS 必须显式设置 `tls.allowInsecureLan: true`,否则**拒绝启动**。 |

连接与证书:监听范围「局域网(默认)/ 仅本机」开关、代理端口与可用性检查、网卡选择

插件从它的 Cordis 条目读取配置。**所有字段都有可用默认值,装完即可用**: ```yaml # ~/.dsh/profiles/web/cordis.patch.yml(可选:只写你想改的字段) - id: dsh-lan-guard config: listenHost: 0.0.0.0 # 默认面向局域网;'127.0.0.1' = 仅本机,或写具体网卡 IP listenPort: 3081 # DSH 端口 + 1;被占用时自动顺延 networkInterface: en0 # 可选:只在一个网卡上公布(留空 = 自动) dataDir: ~/.dsh/profiles/web/data/dsh-lan-guard # 可选;缺省即用这个推导路径 tls: mode: self-signed # 'self-signed'(默认)| 'provided' | 'off' allowInsecureLan: false # 局域网明文 HTTP 的显式风险确认 mdns: enabled: false # 广播 _dsh-lan-guard._tcp auth: mode: token_and_password # 'token_and_password' | 'password' | 'token' adminPolicy: local_only # 'local_only'(默认)| 'password_unlock' | 'open' adminProtection: true # 管理台需要管理密码 allowLoopback: true # 127.0.0.1 访客跳过门禁(物理免锁) requirePairing: true # 新的远程设备必须先命名一次 ``` `dataDir` 是唯一需要解释的一项:**不写就用** `<当前 profile>/data/dsh-lan-guard`(例如 `~/.dsh/profiles/web/data/dsh-lan-guard`),**写了就优先用你写的**(支持 `~` 前缀)。它只影响插件私有数据(密码哈希、设备令牌哈希、会话、自签 CA)的落点,不影响可用性。

安全认证:验证模式三选一、访问密码与管理密码设置、本机回环免密开关

已授权设备:命名确认与管理员批准开关,以及可吊销并拉黑的设备列表

## 兼容性 当前版本:插件 **`0.3.2`** 已在 DeepSeek Harness **`0.1.7-rc.2`** 上验证。 | 插件 | 已验证的 DeepSeek Harness | 这个版本是什么 | | --- | --- | --- | | **`0.3.2`** | **`0.1.7-rc.2`**、`0.1.7-rc.1` | 装完即用:默认对外监听 + `dataDir` 自动推导;设置页液体玻璃与官方尺寸;访问地址改为单行滚动 | | **`0.3.1`** | **`0.1.7-rc.2`**、`0.1.7-rc.1` | 针对 `0.1.7-rc.2` 的验证版:代码零改动,只更新兼容元数据 | | `0.3.0` | `0.1.7-rc.1` | 设备批准与永久拉黑;修掉打开分享的 `?auth=` 链接时的空白页 | | `0.2.0` | `0.1.7-rc.1` | 升级检测;移除右下角状态胶囊;间距修复 | | `0.1.1` | `0.1.7-rc.1` | 文档版:中英双语用户 README | | `0.1.0` | `0.1.7-rc.1` | 首个版本:门禁反向代理、自签 HTTPS、设备配对、设置页、扫码访问 | - 声明范围 `>=0.1.7-rc.1 <0.2.0`(`dsh.engines.dsh`);未列入的 DSH 版本属**未验证**,请自行验证后再使用。 - 本插件用到的宿主/客户端接口:`webServer.register` / `indexTaps`、`connection.requestRejection`、`connection.authenticatedUrl`、追加型 `settings.section` seat、`@deepseek-ai/schemastery`,以及 `profileContext`(用于推导默认数据目录)。 - **破坏性默认值变更(`0.3.2` 起)**:`listenHost` 默认由 `127.0.0.1` 改为 `0.0.0.0`,装完重启一次即可用;`0.3.1` 及更早默认仅回环。门禁与自签 TLS 的默认值未变(未设密码仍拒绝所有设备)。详见 [CHANGELOG](CHANGELOG.md)。 官方 UI 零改动复用,手机视口下自动适配:

390px 手机视口下的官方 DSH 界面:插件只做代理,界面由官方原样提供

## 安全边界 - DSH 自身的监听地址不被改动;本插件从不修改 DSH 配置、会话数据或官方 UI。 - **门禁先于监听**:监听器只在门禁对象构造完成之后才打开。默认对外监听之所以可接受,是因为 `auth.enabled` 默认为真、**未设访问密码时门禁拒绝所有非回环设备**、且 TLS 默认自签——三者必须同时成立。 - 密钥(`secrets.json`、`devices.json`、会话)存放在 `dataDir`,权限 `600`;密码只存 PBKDF2-SHA256 哈希,设备令牌明文只返回一次,落盘只存 SHA-256 哈希,**免密链接的 token 不写日志**。 - 代理给每个转发请求打上不可伪造的来源标记,宿主据此区分「本机操作者」与「经代理的访客」。 - 回环直连**按设计物理免锁**——能使用这台电脑的人本就能改这些设置。 - 访问密码是**共享**的:吊销设备会立即让该设备的身份 cookie 失效,但换个浏览器用密码仍可重新配对。要「同一台机器永久拉黑」需要设备指纹或每设备独立令牌,本项目刻意不用指纹。 - 只面向局域网:不做公网隧道、不做 IM Bot、不做端口转发。 - 插件自己**不安装、不重启、不推送**任何东西:升级命令由你复制执行。 ## 排障 **手机提示证书不受信任。** CA 是自签的:每台设备安装/信任一次 `DSH LAN Guard CA`。信任前先比对 **连接与证书** 里显示的 SHA-256 指纹。 **手机完全连不上。** 确认手机在同一网络、地址与二维码一致,并检查是否有 VPN 或「专用代理/中继」类功能拦截流量;也确认 **监听范围** 没有被切成「仅本机」。 **设置页显示「端口已对局域网开放,但尚未设置访问密码」。** 这是预期的中间状态:端口可达,但门禁拒绝所有设备,不会泄露数据。去 **安全认证** 设一个访问密码即可。 **「配置的端口 X 已被占用,已自动改用 Y」。** 端口被别的程序占着,插件已自行顺延。可在 **连接与证书** 换端口(带可用性检查),或释放该端口。 **「此设备已被移除访问权限」(403)。** 该设备已在 **已授权设备** 中被吊销或拉黑。删除那条记录即可让它重新配对(被拉黑的需先「解除拉黑」)。 **我忘了访问密码。** 在运行 DSH 的电脑上直连 `http://127.0.0.1:3080`(本机直连物理免锁)重设。无头服务器则删除 `dataDir` 下的 `secrets.json` 后重设——在此之前门禁会拒绝所有设备。 **改过密码后所有设备都要重新输密码。** 这是有意的:更换访问密码或切换验证模式会**吊销所有已有访客会话**。 **局域网明文 HTTP 被拒绝。** 非回环 `listenHost` + `tls.mode: 'off'` 会被拒绝,除非显式设置 `tls.allowInsecureLan: true`——否则门禁密码将明文传输。 ## 升级 设置页会显示「当前版本 → npm 上最新版本」,并给出**可复制**的升级命令: ```sh dsh plugin --profile web add dsh-lan-guard@latest ``` 插件**不会自己安装任何东西,也不会重启 DSH**——命令由你执行,执行后手动重启一次 dsh。检测只访问公开的 npm registry,结果缓存 6 小时;连不上时只在界面上提示,不影响门禁与代理。 ## 卸载 ```sh dsh plugin --profile web remove dsh-lan-guard rm -rf ~/.dsh/profiles/web/data/dsh-lan-guard # 可选:删除密钥、设备记录与 CA ``` ## 开发 ```sh pnpm install pnpm test # 单元 + 集成测试(含类型检查) pnpm run build # 打包 lib/index.js 与 lib/client.js pnpm run verify # 类型检查 + 测试 + 构建 + pack dry-run ``` 客户端半边注册到官方追加型 seat `settings.section`,宿主半边通过包内 `cordis.patch.yml` 挂载。 ## 发版 发版由 tag 驱动。更新 `package.json`、把对应 CHANGELOG 段落移出 `Unreleased`、编写带中英双锚点的 `release-notes/v<版本>.md` 后,推送发版提交与 tag: ```sh git tag v0.3.1 git push origin v0.3.1 ``` 发版工作流会校验 tag 与 `package.json` 版本一致、要求发布说明含中英双锚点,然后运行 `pnpm run verify`、打包插件、通过 npm trusted publishing(OIDC)发布,并创建附带 tarball 的 GitHub Release。 ## 许可 [MIT](LICENSE)