@riceawa/dsh-lan-gateway 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +145 -55
- package/cordis.patch.yml +5 -2
- package/lib/client.js +78 -44
- package/lib/client.js.map +1 -1
- package/lib/index.d.ts +52 -5
- package/lib/index.js +470 -108
- package/package.json +2 -2
- package/skills/lan-gateway.md +47 -29
- package/src/auth.ts +39 -6
- package/src/client/index.ts +16 -2
- package/src/client/lan-gateway-card.tsx +56 -21
- package/src/gateway.ts +230 -86
- package/src/index.ts +196 -48
- package/src/login.ts +3 -0
- package/src/state.ts +27 -6
- package/src/tool.ts +12 -8
- package/src/upstream-session.ts +187 -0
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
8
|
<img src="https://img.shields.io/badge/DeepSeek%20Harness-4d6bfe?logo=deepseek&logoColor=fff&style=flat-square" alt="DeepSeek Harness" />
|
|
9
|
-
<img src="https://img.shields.io/badge/version-0.
|
|
9
|
+
<img src="https://img.shields.io/badge/version-0.5.0-2b7fff?style=flat-square" alt="version 0.5.0" />
|
|
10
10
|
<img src="https://img.shields.io/badge/TLS-8b5cf6?logo=lock&logoColor=fff&style=flat-square" alt="TLS" />
|
|
11
11
|
<img src="https://img.shields.io/github/license/rice-awa/dsh-lan-gateway?style=flat-square" alt="MIT license" />
|
|
12
12
|
</p>
|
|
@@ -20,26 +20,38 @@
|
|
|
20
20
|
|
|
21
21
|
`dsh` 的 web CLI 会硬拒绝 `--host 0.0.0.0`(避免把远程代码执行暴露到网络),所以本插件
|
|
22
22
|
让 dsh 继续只绑 `127.0.0.1`,自己另起一个 `0.0.0.0` 反向代理网关转发到 loopback 端口,
|
|
23
|
-
改写 `Host`/`Origin
|
|
24
|
-
|
|
23
|
+
改写 `Host`/`Origin`。**默认拒绝(fail-closed):所有来源——loopback、LAN、公网——都必须
|
|
24
|
+
先通过网关的登录页并出示 HMAC 会话 cookie**;“LAN 免密”改为显式 `lanPasswordless` opt-in
|
|
25
|
+
且默认关闭。对 dsh ≥ 0.1.2-rc.1(修复了 QVD-2026-57410 的浏览器会话认证底座),网关在进程内
|
|
26
|
+
中继**一条共享的上游会话**,上游自身的授权仍然把关每个请求——网关只决定谁能骑上这条
|
|
27
|
+
共享会话。
|
|
25
28
|
|
|
26
29
|
## 特性
|
|
27
30
|
|
|
28
|
-
- **双端一体**:host 端是反向代理网关(登录 / HMAC cookie /
|
|
29
|
-
|
|
30
|
-
|
|
31
|
+
- **双端一体**:host 端是反向代理网关(登录 / HMAC cookie / 会话撤销 / Origin 围栏 /
|
|
32
|
+
TLS / 上游会话中继);client 端是不安全源 UUID shim + **官方设置页卡片**(DSH Settings
|
|
33
|
+
→ Plugins → 可配置插件,网关的端口 / 网段 / 认证 / TLS 全部可视化调整,保存即热生效)。
|
|
34
|
+
- **默认全来源登录(QVD-2026-57410 加固)**:不再“只对公网来源认证”;loopback/LAN 同样
|
|
35
|
+
要求会话。唯一豁免是显式 `lanPasswordless: true`(仅豁免网关登录,上游会话中继仍生效)。
|
|
36
|
+
- **共享上游会话中继**:dsh 0.1.2 起不再信任 loopback Host,要求出示绑定权威来源的
|
|
37
|
+
`dsh-auth-*` cookie;插件在进程内用启动令牌走一遍浏览器等价换取,把这一条会话中继到
|
|
38
|
+
每个转发请求上(“单密码 = 单操作者”语义不变)。底座不支持时自动降级为无中继转发。
|
|
39
|
+
- **fail-closed 启动守卫**:未设密码拒启;`authRequired:false`(v0.4 及更早)拒启并给迁移
|
|
40
|
+
文案;明文监听需显式 `allowInsecurePlaintext:true`(或 TLS / 声明 `trustedTerminator`)。
|
|
31
41
|
- **TLS 双模式**:`self-signed` 自动生成自签名证书(首次启动生成并持久化到
|
|
32
42
|
`~/.dsh/lan-gateway/tls/`,重启复用;`lan_gateway tls-regenerate` 可换新证书),或
|
|
33
43
|
`custom` 直接挂载你自己的 PEM 证书与私钥(如 Let's Encrypt / 自建 CA 签发)。
|
|
44
|
+
- **会话撤销**:cookie 携带撤销 epoch;改密 / 清密 / `rotate-secret` 都会递增 epoch,使
|
|
45
|
+
所有已签发 cookie **与已建立的 WebSocket** 立即失效。清空密码会直接停止监听。
|
|
34
46
|
- **默认关闭(安全)**:bundle patch 里 `enabled: false`,只有运行 `lan_gateway enable`
|
|
35
47
|
后才监听网络端口。
|
|
36
48
|
- **密钥不进配置**:密码哈希、cookie secret 存 `~/.dsh/lan-gateway/state.json`。
|
|
37
49
|
|
|
38
50
|
## 快速安装(推荐)
|
|
39
51
|
|
|
40
|
-
|
|
52
|
+
已发布到 npm(预构建,安装无需 `allowBuilds` 授权)。请把下面这段话发送给你的 agent:
|
|
41
53
|
|
|
42
|
-
>
|
|
54
|
+
> 帮我安装 dsh 插件 `@riceawa/dsh-lan-gateway`,遵循
|
|
43
55
|
> `https://github.com/rice-awa/dsh-lan-gateway/blob/main/INSTALL.md`
|
|
44
56
|
|
|
45
57
|
## 配套 skill
|
|
@@ -62,18 +74,19 @@ dsh plugin --profile web add github:mexiaosqwq/dsh-web-mobile
|
|
|
62
74
|
|
|
63
75
|
## 手动安装
|
|
64
76
|
|
|
65
|
-
### 方式 A
|
|
77
|
+
### 方式 A:npm 包(推荐,免 allowBuilds)
|
|
78
|
+
|
|
79
|
+
已发布预构建产物到 npm,安装时**不需要**批准构建脚本:
|
|
66
80
|
|
|
67
81
|
```bash
|
|
68
82
|
# 官方装配(重启后由 bundles 列表接管,生产态)
|
|
69
|
-
dsh plugin --profile web add
|
|
83
|
+
dsh plugin --profile web add @riceawa/dsh-lan-gateway
|
|
70
84
|
```
|
|
71
85
|
|
|
72
|
-
>
|
|
73
|
-
>
|
|
74
|
-
> `
|
|
75
|
-
>
|
|
76
|
-
> [INSTALL.md](INSTALL.md#for-agents完整安装流程)。
|
|
86
|
+
> `dsh plugin ... add` 把剩余参数转发给 profile 目录里的 pnpm,npm 包自带预构建
|
|
87
|
+
> 的 `lib/`,不会触发 `ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED`。若仍提示,把报错
|
|
88
|
+
> 条目写进 `~/.dsh/profiles/web/pnpm-workspace.yaml` 的 `allowBuilds` 再重试,
|
|
89
|
+
> 完整步骤见 [INSTALL.md](INSTALL.md#for-agents完整安装流程)。
|
|
77
90
|
|
|
78
91
|
### 方式 B:从源码构建
|
|
79
92
|
|
|
@@ -83,21 +96,24 @@ cd dsh-lan-gateway
|
|
|
83
96
|
pnpm install
|
|
84
97
|
pnpm build # host(lib/index.js)
|
|
85
98
|
pnpm build:client # client(lib/client.js,window.__ModuleLoader__ 格式)
|
|
86
|
-
pnpm test #
|
|
99
|
+
pnpm test # 72 项(网关单元 27 + start-guard 12 + 集成 17 + UUID shim 3 + x509 6 + TLS 7)
|
|
87
100
|
```
|
|
88
101
|
|
|
89
102
|
## 使用
|
|
90
103
|
|
|
91
104
|
```bash
|
|
92
|
-
#
|
|
105
|
+
# 开启网关。先满足启动条件(已设密码 + 加密入口),否则 enable 会给出迁移文案
|
|
93
106
|
lan_gateway enable
|
|
94
107
|
|
|
95
|
-
#
|
|
108
|
+
# 查看状态(端口 / 目标 / 密码 / 会话 epoch / 中继状态 / 入口加密方式 / 上次错误)
|
|
96
109
|
lan_gateway status
|
|
97
110
|
|
|
98
|
-
#
|
|
111
|
+
# 设置登录密码(≥8 位;改动会让所有已签发会话立即失效)
|
|
99
112
|
lan_gateway set-password
|
|
100
113
|
|
|
114
|
+
# 轮换会话密钥(作废全部登录 cookie 与已建立的 WebSocket)
|
|
115
|
+
lan_gateway rotate-secret
|
|
116
|
+
|
|
101
117
|
# 换发自签名 TLS 证书(tlsMode=self-signed 时;换新密钥并热重启监听器)
|
|
102
118
|
lan_gateway tls-regenerate
|
|
103
119
|
|
|
@@ -121,8 +137,8 @@ lan_gateway disable
|
|
|
121
137
|
| `enabled` | `false` | 是否在启动时监听网络端口 |
|
|
122
138
|
| `gatewayPort` | `3081` | 网关监听端口(`0.0.0.0`) |
|
|
123
139
|
| `dshTargetPort` | 跟随 `ctx.webServer.port` | 转发到的 dsh loopback 端口 |
|
|
124
|
-
| `lanCidrs` | RFC1918 + link-local(见下) |
|
|
125
|
-
| `
|
|
140
|
+
| `lanCidrs` | RFC1918 + link-local(见下) | 视为 LAN 的网段;仅在 `lanPasswordless` 开启时用作豁免匹配集 |
|
|
141
|
+
| `lanPasswordless` | `false` | 显式 opt-in:LAN/loopback 来源跳过网关登录页(上游会话中继仍把关) |
|
|
126
142
|
| `cookieMaxAgeDays` | `7` | 会话 cookie 有效期(天) |
|
|
127
143
|
| `cookieName` | `dsh_gw_auth` | 会话 cookie 名(不进卡片) |
|
|
128
144
|
| `tlsEnabled` | `false` | 是否以 HTTPS(TLS)提供服务 |
|
|
@@ -131,21 +147,37 @@ lan_gateway disable
|
|
|
131
147
|
| `tlsCertPath` | — | `custom` 模式:PEM 证书(或证书链)绝对路径 |
|
|
132
148
|
| `tlsKeyPath` | — | `custom` 模式:PEM 私钥绝对路径 |
|
|
133
149
|
| `tlsCertMaxAgeDays` | `825` | 自签名证书有效期(天) |
|
|
150
|
+
| `allowInsecurePlaintext` | `false` | 显式 opt-in:允许明文 HTTP 监听(见下「入口加密」) |
|
|
151
|
+
| `trustedTerminator` | — | 声明一个受信 TLS 终止代理标识,视为加密入口(如 `nginx`) |
|
|
152
|
+
|
|
153
|
+
> v0.5.0 起 `authRequired` 被移除:认证恒为必需。若配置里残留 `authRequired: false`
|
|
154
|
+
> (v0.4 及更早的写法),启停守卫会拒绝并提示迁移——不会静默降级回“免密”。
|
|
155
|
+
|
|
156
|
+
### 入口加密(防明文)
|
|
157
|
+
|
|
158
|
+
网关默认**拒绝纯明文监听**,三种方式任选其一即可启动:
|
|
159
|
+
|
|
160
|
+
1. 启用 TLS:`tlsEnabled: true`(推荐,自签名或 custom 证书均可);
|
|
161
|
+
2. 声明由可信反向代理(nginx 等)终止 TLS:
|
|
162
|
+
```yaml
|
|
163
|
+
- id: dsh-lan-gateway
|
|
164
|
+
config:
|
|
165
|
+
enabled: true
|
|
166
|
+
gatewayPort: 8080
|
|
167
|
+
trustedTerminator: nginx # 由 nginx 以 HTTPS 对外,再转发回本端口
|
|
168
|
+
```
|
|
169
|
+
3. 显式接受明文(风险自担,密码与会话将明文在网内传输):
|
|
170
|
+
```yaml
|
|
171
|
+
- id: dsh-lan-gateway
|
|
172
|
+
config:
|
|
173
|
+
enabled: true
|
|
174
|
+
gatewayPort: 3081
|
|
175
|
+
allowInsecurePlaintext: true
|
|
176
|
+
```
|
|
134
177
|
|
|
135
178
|
自签名证书在**首次启用 TLS 时生成一次**,持久化于 `~/.dsh/lan-gateway/tls/`
|
|
136
179
|
(`selfsigned.crt` / `selfsigned.key`,0600),之后重启复用同一张证书;
|
|
137
180
|
`lan_gateway tls-regenerate` 可随时换发新证书(新密钥)并热重启监听器。
|
|
138
|
-
配置示例(`--patch`):
|
|
139
|
-
|
|
140
|
-
```yaml
|
|
141
|
-
- id: dsh-lan-gateway
|
|
142
|
-
config:
|
|
143
|
-
enabled: true
|
|
144
|
-
gatewayPort: 8443
|
|
145
|
-
tlsEnabled: true
|
|
146
|
-
tlsMode: self-signed
|
|
147
|
-
tlsSelfSignedHosts: localhost, 192.168.1.5
|
|
148
|
-
```
|
|
149
181
|
|
|
150
182
|
或用自己的证书(例如 `/etc/letsencrypt/live/example.com/` 下签发的 PEM):
|
|
151
183
|
|
|
@@ -158,35 +190,79 @@ lan_gateway disable
|
|
|
158
190
|
tlsKeyPath: /etc/letsencrypt/live/example.com/privkey.pem
|
|
159
191
|
```
|
|
160
192
|
|
|
161
|
-
启用 TLS
|
|
162
|
-
|
|
193
|
+
启用 TLS(或声明受信终止代理)后,登录 cookie 自动带 `Secure`;监听器自身是 HTTPS 时,
|
|
194
|
+
网关响应(登录页 / 重定向 / 拒绝)带 HSTS。自签名证书首次访问会看到浏览器警告,属预期行为。
|
|
163
195
|
|
|
164
|
-
默认 `lanCidrs`:`10.0.0.0/8`、`172.16.0.0/12`、`192.168.0.0/16`、`169.254.0.0/16
|
|
165
|
-
IPv6 的 `fe80::/10`(link-local
|
|
196
|
+
默认 `lanCidrs`:`10.0.0.0/8`、`172.16.0.0/12`、`192.168.0.0/16`、`169.254.0.0/16`;
|
|
197
|
+
IPv6 的 `fe80::/10`(link-local)与 `127.0.0.0/8` / `::1` 归类为 LAN/loopback。
|
|
166
198
|
|
|
167
199
|
## 安全模型
|
|
168
200
|
|
|
169
201
|
- **来源分级**:仅依据 `socket.remoteAddress`(IPv4-mapped IPv6 会先解包)把请求分为
|
|
170
|
-
loopback / lan / internet 三档,绝不信任 `X-Forwarded-For
|
|
171
|
-
|
|
172
|
-
|
|
202
|
+
loopback / lan / internet 三档,绝不信任 `X-Forwarded-For`。**分级本身不授予任何访问**
|
|
203
|
+
——默认每一档都必须出示有效网关会话,否则 302 到 `/__login`。
|
|
204
|
+
- **LAN 免密是显式 opt-in**:`lanPasswordless: true` 只让命中 `lanCidrs`(或 loopback)的
|
|
205
|
+
来源跳过**网关自己的登录页**;上游 dsh 若 ≥ 0.1.2-rc.1,插件在进程内中继一条共享上游
|
|
206
|
+
会话(见下),上游授权仍把关每个请求。底座没有浏览器会话认证时该开关**拒绝启用**——
|
|
207
|
+
否则就是把 QVD-2026-57410 原样装回去。
|
|
208
|
+
- **共享上游会话中继(dsh ≥ 0.1.2-rc.1)**:dsh 已不再信任“回环 Host”,要求请求出示
|
|
209
|
+
HMAC 签名的 `dsh-auth-*` cookie。插件经 `connection` 服务拿到启动令牌,在回环传输上
|
|
210
|
+
做浏览器等价的令牌换取,取得 cookie 后中继到每个转发请求;上游一旦以 401 拒绝就丢弃
|
|
211
|
+
这条会话并重新换取。**这仍是“单密码 = 单操作者”**:通过网关登录的人都骑同一条上游
|
|
212
|
+
会话,上游持钥,才是真正的授权主体。
|
|
173
213
|
- **登录页**:`/__login` 由网关独占、不转发;密码以 scrypt(每写一次重新加盐)校验,
|
|
174
214
|
登录尝试按来源限流(5 次 / 分钟)。
|
|
175
|
-
- **会话 cookie**:`payload.signature` 结构(HMAC-SHA256
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
-
|
|
184
|
-
|
|
185
|
-
|
|
215
|
+
- **会话 cookie**:`payload.signature` 结构(HMAC-SHA256),携带**撤销 epoch**;
|
|
216
|
+
`HttpOnly; SameSite=Strict`,过期即失效。改密 / 清密 / `rotate-secret` 递增 epoch,
|
|
217
|
+
作废全部已签发 cookie **并断开已建立的 WebSocket**,客户端重新登录。清空密码会停止
|
|
218
|
+
监听(网关必须有密码才能运行)。
|
|
219
|
+
- **管理面不外泄**:`/lan-gateway/*`(含配置路由)由网关独占、一律 403 不转发——远程
|
|
220
|
+
访问者无法借网关改写 Host 触及本机 loopback 的配置接口;原生 `/lan-gateway/config`
|
|
221
|
+
仅对回环 Host 且同源的请求应答(本地用户 / 本机能读 `~/.dsh` 的进程)。远程管理走
|
|
222
|
+
`lan_gateway` 工具。
|
|
223
|
+
- **CSRF / 跨站围栏(HTTP 与 WebSocket)**:因为网关把 Origin 改写回 loopback、会蒙蔽
|
|
224
|
+
dsh 自身的 CSRF 防线,网关在改写前对每个转发请求自检:`sec-fetch-site: cross-site`
|
|
225
|
+
直接拒绝;Origin 必须匹配访问者实际使用的网关权威来源;状态变更方法与 WebSocket
|
|
226
|
+
升级请求**必须携带同源 Origin**,否则 403。
|
|
227
|
+
- **密码未设置时拒启**:无论来源如何,未设密码一律拒绝监听——杜绝把远程代码执行门户
|
|
228
|
+
开放给任何非本机来源。
|
|
229
|
+
- **WebSocket**:`/api` 升级请求同样过登录校验、同源 Origin 校验,再拼接转发给 dsh,
|
|
230
|
+
并纳入会话撤销(epoch 变化即断开)。
|
|
231
|
+
|
|
232
|
+
## 从 v0.4(及更早)升级
|
|
233
|
+
|
|
234
|
+
1. **把底座升级到 dsh ≥ 0.1.2-rc.1**(含 QVD-2026-57410 上游修复)。低于该版本时本插件
|
|
235
|
+
仍可运行,但 `lanPasswordless` 会拒绝启用(fail-closed)。
|
|
236
|
+
2. 若配置里写过 `authRequired: false`:删除它。现在认证恒为必需;LAN 想免密改为显式
|
|
237
|
+
`lanPasswordless: true`。
|
|
238
|
+
3. 若以明文(无 TLS)运行且从未设置过密码:升级后 `enable` 会拒绝。请启用 TLS /
|
|
239
|
+
声明 `trustedTerminator` / 显式 `allowInsecurePlaintext: true` 之一,并先
|
|
240
|
+
`lan_gateway set-password`。
|
|
241
|
+
4. 曾以“免密 + 伪造 Host”形态暴露过受影响的 dsh 实例:按“可能已失陷”处置——轮换模型 /
|
|
242
|
+
系统凭据,并检查是否有未知登录会话。
|
|
243
|
+
|
|
244
|
+
一步到位迁移示例(HTTPS 自签名 + LAN 免密,本机已跑通):
|
|
245
|
+
|
|
246
|
+
```yaml
|
|
247
|
+
- id: dsh-lan-gateway
|
|
248
|
+
config:
|
|
249
|
+
enabled: true
|
|
250
|
+
tlsEnabled: true
|
|
251
|
+
tlsMode: self-signed
|
|
252
|
+
# 证书 SAN 覆盖所有接入方式:回环 / 主机名 / LAN IP / Tailscale IP。
|
|
253
|
+
# Tailscale 走 100.64.0.0/10(CGNAT),按默认 lanCidrs 归为 internet,
|
|
254
|
+
# 仍需密码登录,不会因 lanPasswordless 而豁免。
|
|
255
|
+
tlsSelfSignedHosts: localhost,my-host,192.168.1.20,100.99.1.2
|
|
256
|
+
lanPasswordless: true # LAN/loopback 免登录;需 dsh ≥ 0.1.2-rc.1 上游会话底座
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
首次以 `https://<主机名|LAN-IP|Tailscale-IP>:3081` 访问会看到自签名证书警告(预期)。
|
|
260
|
+
证书在首次启用 TLS 时生成一次并持久化到 `~/.dsh/lan-gateway/tls/`;若已有一张旧证书,
|
|
261
|
+
**必须用 `lan_gateway tls-regenerate` 重新签发**,新的 `tlsSelfSignedHosts` 才会进入 SAN。
|
|
186
262
|
|
|
187
263
|
## 登录页截图(预期)
|
|
188
264
|
|
|
189
|
-
|
|
265
|
+
远程来源打开 `http://<主机>:3081/` 时,先看到网关自带的登录表单(`/__login`),
|
|
190
266
|
输入正确密码后签发会话 cookie 并跳回 `/`。
|
|
191
267
|
|
|
192
268
|
<p align="center">
|
|
@@ -210,12 +286,26 @@ IPv6 的 `fe80::/10`(link-local)与回环地址始终免密。
|
|
|
210
286
|
|
|
211
287
|
```bash
|
|
212
288
|
pnpm test
|
|
213
|
-
# ✓ tests/gateway.test.ts
|
|
214
|
-
# ✓ tests/
|
|
215
|
-
# ✓ tests/
|
|
216
|
-
#
|
|
289
|
+
# ✓ tests/gateway.test.ts (27) 分类 / HMAC cookie / epoch / 密码状态 / 限流
|
|
290
|
+
# ✓ tests/start-guard.test.ts (12) fail-closed 启动守卫 / 配置路由回环围栏
|
|
291
|
+
# ✓ tests/integration/gateway.test.ts (17) 真实网关端到端:全来源登录 / LAN 豁免 /
|
|
292
|
+
# 跨站 403 / 升级拒绝 / cookie 属性 / epoch 撤销 / 会话中继
|
|
293
|
+
# ✓ tests/uuid-shim.test.ts ( 3) 不安全源补丁 / 安全源 no-op / v4 正确性
|
|
294
|
+
# ✓ tests/x509.test.ts ( 6) 自签名证书 DER/SAN/签名/TLS 握手
|
|
295
|
+
# ✓ tests/tls.test.ts ( 7) 证书持久化 / 重生成 / 自定义证书加载
|
|
217
296
|
```
|
|
218
297
|
|
|
298
|
+
## 安全评估与修复记录
|
|
299
|
+
|
|
300
|
+
0.5.0 的默认拒绝模型源自针对 QVD-2026-57410(DSH Web API 的 Host 信任缺陷)的加固,
|
|
301
|
+
相关文档收在 [docs/security/](docs/security/):
|
|
302
|
+
|
|
303
|
+
- [LAN 网关安全评估](docs/security/SECURITY-AUDIT.md)——0.4.0 时代 F1–F5 审计快照与 13 个
|
|
304
|
+
隔离观察(顶部标注 0.5.0 的修复状态)。
|
|
305
|
+
- [QVD-2026-57410 修复方案](docs/security/qvd-2026-57410-fix-plan.md)——方案全文 + §15
|
|
306
|
+
实施状态(0.5.0 落地差异)。
|
|
307
|
+
- [上游研究](docs/security/qvd-2026-57410-research.md)——公开通告 / 上游提交与版本核对。
|
|
308
|
+
|
|
219
309
|
## 许可
|
|
220
310
|
|
|
221
311
|
[MIT](./LICENSE)
|
package/cordis.patch.yml
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
# dsh-lan-gateway bundle patch: inserts the gateway plugin into a profile's layer stack.
|
|
2
2
|
# Disabled by default (safe): the gateway only opens a network port once `lan_gateway
|
|
3
|
-
# enable` is run through the tool
|
|
4
|
-
# (password hash, cookie secret) live in ~/.dsh/lan-gateway/
|
|
3
|
+
# enable` is run through the tool (or `enabled: true` is set here). Composition-level
|
|
4
|
+
# settings live here; secrets (password hash, cookie secret) live in ~/.dsh/lan-gateway/
|
|
5
|
+
# state.json, never in config. Secure defaults inherited from the plugin schema:
|
|
6
|
+
# auth is required for every source (lanPasswordless: false), the listener refuses
|
|
7
|
+
# plaintext (allowInsecurePlaintext: false) unless TLS or trustedTerminator is set.
|
|
5
8
|
- insert:
|
|
6
9
|
- id: dsh-lan-gateway
|
|
7
10
|
name: '@riceawa/dsh-lan-gateway'
|
package/lib/client.js
CHANGED
|
@@ -44,8 +44,12 @@ window.__ModuleLoader__.load({
|
|
|
44
44
|
"hint.dshTargetPort": "留空则自动跟随 dsh web 端口(默认 3080)",
|
|
45
45
|
"field.lanCidrs": "免密 LAN 网段",
|
|
46
46
|
"hint.lanCidrs": "逗号分隔的 CIDR,如 10.0.0.0/8, 192.168.0.0/16",
|
|
47
|
-
"field.
|
|
48
|
-
"hint.
|
|
47
|
+
"field.lanPasswordless": "LAN 免登录",
|
|
48
|
+
"hint.lanPasswordless": "LAN/回环来源跳过网关登录页,但仍共用同一上游会话(需 dsh ≥ 0.1.2)",
|
|
49
|
+
"field.allowInsecurePlaintext": "允许明文 HTTP",
|
|
50
|
+
"hint.allowInsecurePlaintext": "危险:关闭 TLS 或受信终止代理时仍启动监听,密码与会话将以明文传输",
|
|
51
|
+
"field.trustedTerminator": "受信 TLS 终止代理",
|
|
52
|
+
"hint.trustedTerminator": "可选:声明前置代理标识,视为加密入口(如 nginx)。留空 = 未声明",
|
|
49
53
|
"field.cookieMaxAgeDays": "会话有效期(天)",
|
|
50
54
|
"hint.cookieMaxAgeDays": "登录 cookie 的存活天数(默认 7)",
|
|
51
55
|
"field.tlsEnabled": "启用 TLS(HTTPS)",
|
|
@@ -86,8 +90,12 @@ window.__ModuleLoader__.load({
|
|
|
86
90
|
"hint.dshTargetPort": "Leave empty to follow the dsh web port (default 3080)",
|
|
87
91
|
"field.lanCidrs": "Password-free LAN CIDRs",
|
|
88
92
|
"hint.lanCidrs": "Comma separated CIDRs, e.g. 10.0.0.0/8, 192.168.0.0/16",
|
|
89
|
-
"field.
|
|
90
|
-
"hint.
|
|
93
|
+
"field.lanPasswordless": "LAN skip login",
|
|
94
|
+
"hint.lanPasswordless": "LAN/loopback sources skip the gateway login page but still ride one shared upstream session (needs dsh >= 0.1.2)",
|
|
95
|
+
"field.allowInsecurePlaintext": "Allow plaintext HTTP",
|
|
96
|
+
"hint.allowInsecurePlaintext": "Dangerous: start the listener even without TLS or a trusted terminator; passwords and sessions travel in clear",
|
|
97
|
+
"field.trustedTerminator": "Trusted TLS terminator",
|
|
98
|
+
"hint.trustedTerminator": "Optional identifier for a front proxy (e.g. nginx) treated as the encrypted ingress. Empty = none declared",
|
|
91
99
|
"field.cookieMaxAgeDays": "Session lifetime (days)",
|
|
92
100
|
"hint.cookieMaxAgeDays": "Login cookie lifetime (default 7)",
|
|
93
101
|
"field.tlsEnabled": "Enable TLS (HTTPS)",
|
|
@@ -126,7 +134,7 @@ window.__ModuleLoader__.load({
|
|
|
126
134
|
kind: "cidrs"
|
|
127
135
|
},
|
|
128
136
|
{
|
|
129
|
-
field: "
|
|
137
|
+
field: "lanPasswordless",
|
|
130
138
|
kind: "boolean"
|
|
131
139
|
},
|
|
132
140
|
{
|
|
@@ -159,6 +167,15 @@ window.__ModuleLoader__.load({
|
|
|
159
167
|
{
|
|
160
168
|
field: "tlsCertMaxAgeDays",
|
|
161
169
|
kind: "number"
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
field: "allowInsecurePlaintext",
|
|
173
|
+
kind: "boolean"
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
field: "trustedTerminator",
|
|
177
|
+
kind: "text",
|
|
178
|
+
optional: true
|
|
162
179
|
}
|
|
163
180
|
];
|
|
164
181
|
function formatValue(def, value) {
|
|
@@ -401,33 +418,34 @@ window.__ModuleLoader__.load({
|
|
|
401
418
|
onClick: () => {
|
|
402
419
|
setOpen(!open);
|
|
403
420
|
},
|
|
404
|
-
children: [
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
421
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
422
|
+
style: styles.headerTop,
|
|
423
|
+
children: [
|
|
424
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
408
425
|
style: styles.name,
|
|
409
426
|
children: t.title
|
|
410
|
-
}),
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
427
|
+
}),
|
|
428
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
429
|
+
style: styles.status,
|
|
430
|
+
title: statusLine,
|
|
431
|
+
children: statusLine
|
|
432
|
+
}),
|
|
433
|
+
dirty ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
434
|
+
style: styles.pending,
|
|
435
|
+
children: t.unsaved
|
|
436
|
+
}) : null,
|
|
437
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
438
|
+
style: open ? {
|
|
439
|
+
...styles.chevron,
|
|
440
|
+
...styles.chevronOpen
|
|
441
|
+
} : styles.chevron,
|
|
442
|
+
children: open ? "▾" : "▸"
|
|
443
|
+
})
|
|
444
|
+
]
|
|
445
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
446
|
+
style: styles.description,
|
|
447
|
+
children: t.description
|
|
448
|
+
})]
|
|
431
449
|
}), open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
432
450
|
style: styles.body,
|
|
433
451
|
children: [
|
|
@@ -502,8 +520,9 @@ window.__ModuleLoader__.load({
|
|
|
502
520
|
},
|
|
503
521
|
header: {
|
|
504
522
|
display: "flex",
|
|
505
|
-
|
|
506
|
-
|
|
523
|
+
flexDirection: "column",
|
|
524
|
+
alignItems: "stretch",
|
|
525
|
+
gap: "6px",
|
|
507
526
|
width: "100%",
|
|
508
527
|
padding: "14px 16px",
|
|
509
528
|
border: 0,
|
|
@@ -513,28 +532,39 @@ window.__ModuleLoader__.load({
|
|
|
513
532
|
textAlign: "left",
|
|
514
533
|
cursor: "pointer"
|
|
515
534
|
},
|
|
516
|
-
|
|
535
|
+
headerTop: {
|
|
517
536
|
display: "flex",
|
|
518
|
-
|
|
519
|
-
gap: "
|
|
520
|
-
|
|
521
|
-
minWidth: 0
|
|
537
|
+
alignItems: "center",
|
|
538
|
+
gap: "12px",
|
|
539
|
+
width: "100%"
|
|
522
540
|
},
|
|
523
541
|
name: {
|
|
542
|
+
flex: "1 1 auto",
|
|
543
|
+
minWidth: 0,
|
|
524
544
|
fontSize: "15px",
|
|
525
545
|
fontWeight: 600,
|
|
526
546
|
lineHeight: 1.4,
|
|
527
547
|
color: L.labelPrimary
|
|
528
548
|
},
|
|
549
|
+
status: {
|
|
550
|
+
flex: "0 1 auto",
|
|
551
|
+
minWidth: 0,
|
|
552
|
+
maxWidth: "60%",
|
|
553
|
+
fontSize: "11px",
|
|
554
|
+
lineHeight: 1.4,
|
|
555
|
+
color: L.labelTertiary,
|
|
556
|
+
whiteSpace: "nowrap",
|
|
557
|
+
overflow: "hidden",
|
|
558
|
+
textOverflow: "ellipsis"
|
|
559
|
+
},
|
|
529
560
|
description: {
|
|
561
|
+
display: "block",
|
|
530
562
|
fontSize: "13px",
|
|
531
563
|
lineHeight: 1.5,
|
|
532
|
-
color: L.labelTertiary
|
|
533
|
-
},
|
|
534
|
-
status: {
|
|
535
|
-
fontSize: "11px",
|
|
536
564
|
color: L.labelTertiary,
|
|
537
|
-
whiteSpace: "nowrap"
|
|
565
|
+
whiteSpace: "nowrap",
|
|
566
|
+
overflow: "hidden",
|
|
567
|
+
textOverflow: "ellipsis"
|
|
538
568
|
},
|
|
539
569
|
pending: {
|
|
540
570
|
flex: "none",
|
|
@@ -713,11 +743,15 @@ window.__ModuleLoader__.load({
|
|
|
713
743
|
*/
|
|
714
744
|
function apply(ctx) {
|
|
715
745
|
installRandomUuidShim();
|
|
746
|
+
const legacyListOptions = {
|
|
747
|
+
id: "lan-gateway",
|
|
748
|
+
order: 30
|
|
749
|
+
};
|
|
716
750
|
ctx.slots.inject("settings.plugin.item", function* () {
|
|
717
751
|
yield ctx.slots.register({
|
|
718
752
|
name: "settings.plugin.item",
|
|
719
|
-
|
|
720
|
-
|
|
753
|
+
key: "lan-gateway",
|
|
754
|
+
...legacyListOptions
|
|
721
755
|
}, LanGatewayCard);
|
|
722
756
|
});
|
|
723
757
|
}
|