DSH LAN Guard
Use the official DeepSeek Harness Web UI from your phone — a gated reverse proxy on your LAN that never touches DSH's own loopback binding.
English | 中文
Features · Install · Usage · Compatibility · Configuration · Troubleshooting · Security · Uninstall · Development
DSH LAN Guard is a community plugin for DeepSeek Harness. It does not modify DSH core, does not change DSH's own listening address, and reuses the official Web UI unmodified.
DSH serves its Web UI on 127.0.0.1 only, so phones and tablets cannot reach it, and DSH deliberately refuses to bind 0.0.0.0. This plugin leaves that binding alone and runs a second, gated port that proxies the official UI to your LAN: password gate, self-signed HTTPS by default, a QR code to open it on a phone, and per-device pairing you can revoke one by one.
Features
- Gated reverse proxy — full HTTP and WebSocket forwarding (the official UI's
/api/remote.muxmux included),Host/Originrewriting, hop-by-hop header stripping,502when the upstream is unreachable. - Password gate — PBKDF2-SHA256 (600,000 iterations), a separate access password (phones) and admin password (this console), a
dsh_passwordless link, persistent visitor sessions, per-IP lockout and CSRF checks. - Self-signed HTTPS by default — generates its own
DSH LAN Guard CA, issues a leaf certificate for the selected NIC address, and keeps the CA identity stable across restarts so devices only trust it once. - Your own machine is never locked — direct
127.0.0.1access is physically unlocked. Remote access followsauth.adminPolicy: read-only (default), password-unlocked, or open. - Per-device pairing — a phone that passes the gate names itself once, receives an HttpOnly device cookie, appears in the settings page (name / created / last used / source IP) and can be revoked individually. A revoked device is refused with
403immediately. - Settings inside the official page — a "局域网访问" section with four tabs: QR access, authentication, authorised devices, connection & certificates. All typography and colours use the official design tokens; the official layout is never replaced.
- Configurable port — defaults to
3081(DSH's port + 1), walks up to ten ports when that one is taken, editable in the settings page with an availability check. - Optional mDNS — off by default; advertises
_dsh-lan-guard._tcpwhen enabled.
Install
dsh plugin --profile web add dsh-lan-guard
Then restart DSH once (the plugin's server half is loaded at startup) and open Settings → 局域网访问.
Usage
- In Settings → 局域网访问 → 安全认证, set an access password (at least 8 characters). Until you do, the gate refuses every device.
- In 连接与证书, pick the NIC to publish on.
0.0.0.0is the default for a configured plugin; setlistenHost: 127.0.0.1in the config to keep it local-only while you try it out. - Open the 扫码访问 tab and scan the QR code with your phone.
- On the phone: trust the
DSH LAN Guard CAcertificate (the SHA-256 fingerprint is shown in the settings page), enter the access password once, then name the device on the pairing page. - The phone now runs the official DSH UI. It appears under 已授权设备, where you can revoke it at any time.
Remote devices are read-only by default (
adminPolicy: local_only): they can use DSH but cannot change plugin settings. Switch the policy on the desktop if you want a phone to manage them.
Compatibility
Current release: plugin 0.3.1 is verified against DeepSeek Harness 0.1.7-rc.2 (the latest release candidate).
Which plugin version goes with which DeepSeek Harness version
| Plugin | Verified DeepSeek Harness | On npm | What that version is |
|---|---|---|---|
0.3.1 |
0.1.7-rc.2, 0.1.7-rc.1 |
latest |
Verification release for DSH 0.1.7-rc.2: no code change — compatibility metadata and this table updated |
0.3.0 |
0.1.7-rc.1 |
published | Device approval and permanent ban (F9); fixed the blank page when opening a shared ?auth= link |
0.2.0 |
0.1.7-rc.1 |
published | Update detection; removed the bottom-right status pill; spacing fix |
0.1.1 |
0.1.7-rc.1 |
published | Documentation release: bilingual user README |
0.1.0 |
0.1.7-rc.1 |
published | First release: gated reverse proxy, self-signed HTTPS, device pairing, settings UI, QR access |
The declared range is
>=0.1.7-rc.1 <0.2.0(dsh.engines.dsh), anddsh.compatibility.dshReleasesrecords0.1.7-rc.2: compatibleand0.1.7-rc.1: compatible.How
0.3.1was verified on0.1.7-rc.2: every host/client surface this plugin uses is present and unchanged —webServer.register/indexTaps,connection.requestRejection,connection.authenticatedUrl, the additivesettings.sectionandshell.overlayseats, and@deepseek-ai/schemastery— and the plugin runs end-to-end (settings page, QR access, gate, proxy) on that release. No source change was required.A DSH release that is not listed is unverified — test it before trusting it.
Install a specific version when it matters:
dsh plugin --profile web add dsh-lan-guard@0.3.1
Configuration
The plugin reads its config from its Cordis entry (profile patch or dsh plugin config). Defaults are conservative: nothing is published until you say so.
enabled: true # master switch
listenHost: 0.0.0.0 # default 127.0.0.1 (loopback only); set to face the LAN
listenPort: 3081 # DSH port + 1; auto-walks up to 10 ports when taken
upstreamOrigin: http://127.0.0.1:3080
dataDir: ~/.dsh/profiles/web/data/dsh-lan-guard
networkInterface: en0 # optional: publish on one NIC (empty = automatic)
tls:
mode: self-signed # 'self-signed' (default) | 'provided' | 'off'
allowInsecureLan: false # required acknowledgement for LAN plain HTTP
mdns:
enabled: false # advertise _dsh-lan-guard._tcp
auth:
mode: token_and_password # 'token_and_password' | 'password' | 'token'
adminPolicy: local_only # 'local_only' (default) | 'password_unlock' | 'open'
adminProtection: true # admin console needs the admin password
allowLoopback: true # 127.0.0.1 visitors skip the gate (physically unlocked)
requirePairing: true # new remote devices must name themselves once
Every key above can also be changed from the settings page (the non-sensitive ones are declared as volatile config fields).
Device approval and permanent ban
Paired devices are listed under Settings → 局域网访问 → 已授权设备 with three groups: pending, approved and blocked.
- Turn on 新设备需要管理员批准 to require an explicit approval before a newly paired device is let in (off by default). A pending phone sees a "waiting for approval" page until you press 批准.
- 吊销并拉黑 cuts a device off permanently: its identity is blocked, and re-pairing with the access password from another browser is refused too. 解除拉黑 is the only way back.
- This deliberately does not use device fingerprinting (which breaks whenever the browser or OS changes) — the operator decides, and the decision sticks.
Updates
The settings page shows the running version next to the newest one on npm, with a copyable upgrade command:
dsh plugin --profile web add dsh-lan-guard@latest
The plugin never installs anything by itself and never restarts DSH — run the command yourself and restart DSH once. The check only asks the public npm registry, is cached for six hours, and a failure is reported in the UI instead of breaking the gate.
Troubleshooting
The phone shows a certificate warning. The CA is self-signed: install/trust DSH LAN Guard CA once per device. Compare the fingerprint shown in 连接与证书 before trusting it.
The phone cannot reach the address at all. Check that the phone is on the same network, that the address matches the QR code, and that no VPN or "private relay" feature is intercepting traffic. The settings page shows the address the listener is actually bound to.
"配置的端口 X 已被占用,已自动改用 Y". Something else holds the port; the plugin moved on by itself. Set a different port in 连接与证书 (it has an availability check) or free the port.
"此设备已被移除访问权限" (403). The device was revoked under 已授权设备. Delete that record to let it pair again.
I forgot the access password. On the machine that runs DSH, open http://127.0.0.1:3080 (direct loopback access is physically unlocked) and set a new one. On a headless server, delete secrets.json in dataDir and set a new password — until then the gate refuses every device.
Every device asks for the password again after I changed it. That is intentional: changing the access password or the auth mode revokes every existing visitor session.
Plain HTTP on the LAN is refused. listenHost + tls.mode: 'off' is rejected unless you set tls.allowInsecureLan: true — the gate password would otherwise travel in clear text.
Security boundary
- DSH's own listener is untouched; this plugin never edits DSH config or the official UI.
- Secrets (
secrets.json,devices.json, sessions) live indataDirwith mode600; the plaintext device token is returned once and only its SHA-256 hash is stored. - The gate applies before the listener is useful, and the proxy stamps every forwarded request with an unforgeable origin marker so the host can tell the machine's own operator from a proxied visitor.
- Loopback direct access is physically unlocked by design — anyone who can already use that machine can change these settings.
- The access password is shared: revoking a device invalidates that device's identity cookie immediately, but re-pairing with the password from another browser is still possible. A permanent per-machine ban would need device fingerprinting or per-device tokens.
- LAN-only by design: no public tunnels, no IM bots, no port forwarding.
Uninstall
dsh plugin --profile web remove dsh-lan-guard
rm -rf ~/.dsh/profiles/web/data/dsh-lan-guard # optional: removes secrets, devices and the CA
Development
pnpm install
pnpm test # unit + integration tests (typecheck included)
pnpm run build # bundles lib/index.js and lib/client.js
pnpm run verify # typecheck + tests + build + pack dry-run
The design and verification record lives in docs/ — SPEC.md (what it must do), PLAN.md (phase gates and what was verified when), RESEARCH.md (verified DSH facts), GUARDRAILS.md (red lines), RELEASE.md (release flow).
No comments yet. Be the first to write one.