dsh-lan-proxy
English | 中文
A LAN/public reverse proxy plugin for dsh. Runs inside the dsh process: listens on HTTP/HTTPS and forwards to dsh's loopback server, with IP-whitelist trust, optional Basic Auth, and access logging.
dsh refuses --host 0.0.0.0; this plugin is the sanctioned "proxy in front" shape living in-process. Zero-build plain ESM; cleanup on unload via ctx.effect().
> Compatible with the dsh developer preview (web profile). dsh is pre-1.0 with breaking changes — pin by version (#v0.3.1), not by expecting API stability.
Install
# local checkout — use file:, NOT link: (link: won't install the bundle's deps)
dsh plugin --profile web add file:/path/to/dsh-lan-proxy
# or GitHub
dsh plugin --profile web add github:mariGoIds/dsh-lan-proxy#v0.3.1
# restart dsh afterwardsServes localhost only until configured — see Configuration below.
Features
- Dual-stack binding (
::= IPv4 + IPv6): public access over your global IPv6, no NAT tricks - IP whitelist (prefix/exact): password-less trust for known networks
- Basic Auth + cookie session that covers WebSocket; optional
authSecretsalt against offline cracking - Access log: one line per request, 403/401 included, optional file output
- TLS on
:3443: missing cert or taken port only skips that listener, never kills dsh
Configuration
Schemastery schema. Secure-by-default: empty whitelist, auth off, log off. Override in your profile's cordis.patch.yml:
- id: lan-proxy
config:
listenHost: '::'
listenPort: 3080
tlsPort: 3443
backendHost: 127.0.0.1
backendPort: 3081
allowedPrefixes: ['192.0.2.'] # password-less trusted networks
allowedIps: [198.51.100.25]
authUsername: dsh # empty + empty password = auth off
authPassword: '<long-random-password>'
authSecret: '<random-salt>' # optional: salts the session token
authRealm: dsh
authCookieName: dsh_auth
accessLog: true
accessLogFile: '' # '' = dsh logger only; non-empty = append to file
certDir: '' # '' = $DSH_HOME/certs or ~/.dsh/certs| Key | Default | Description |
|---|---|---|
listenHost | :: | Bind host; :: is dual-stack (v4+v6) |
listenPort / tlsPort | 3080 / 3443 | HTTP / HTTPS listener |
backendHost / backendPort | 127.0.0.1 / 3081 | The dsh backend |
allowedPrefixes | [] | Prefixes allowed without password (IPv4/IPv6) |
allowedIps | [] | Exact addresses allowed without password |
authUsername / authPassword | '' | Basic Auth credential (both set = auth on) |
authRealm | dsh | String shown in the browser login dialog |
authCookieName | dsh_auth | Session cookie name |
authSecret | '' | Private salt for the session token. Set a long random value on public deployments. Empty stays backwards-compatible with v0.2 tokens |
accessLog | false | Log every request |
accessLogFile | '' | Append log lines to this file (else only dsh logger) |
certDir | '' | TLS cert dir; files must be key.pem + cert.pem |
Security model
Access order: ① trusted IP (localhost / allowedIps / allowedPrefixes) → pass, no password; ② otherwise + auth on → Basic credentials or valid cookie, else 401; ③ otherwise + auth off → 403.
Why a cookie: dsh's frontend streams events over WebSocket and browsers can't attach an Authorization header to it. After a successful Basic login the proxy issues Set-Cookie (HttpOnly; SameSite=Lax, Secure on TLS), which same-origin fetch/SSE/WebSocket all carry automatically. Stateless; credentials survive restarts.
Known limitations & boundaries
- Whitelist skips the password — don't put untrusted networks (e.g. mobile WAN) in it
- No rate limiting / lockout yet — weak passwords invite brute force; use a long random password and set
authSecret - Session token never expires — it's derived from the credentials; changing them invalidates every session immediately
- Credentials are only safe over TLS —
:3080logs a warning on plaintext Basic and is debug-only
Certificates & public IPv6
:: already listens on v6 — browse to https://<your-ipv6>:3443. The cert SAN must include that IPv6 (or your DNS name) or browsers flag a mismatch; prefer a DNS/DDNS name over a bare v6 literal (SLAAC addresses change). Without IPv6, fall back to listenHost: '0.0.0.0' + router port-forwarding.
Generate the cert (files must be certDir/key.pem + certDir/cert.pem):
openssl req -x509 -newkey rsa:2048 -nodes -keyout key.pem -out cert.pem -days 3650 \
-subj "/CN=DeepSeek-Harness-LAN" \
-addext "subjectAltName=DNS:example.com,IP:192.0.2.10,IP:127.0.0.1,DNS:localhost"Missing certs only disable HTTPS; HTTP keeps working (logged as a warning).
Access log
With accessLog: true each request writes one line (403/401 included):
2026-08-17T04:00:00.000Z https ip=2001:db8::99 "GET /" 200 12ms 48213bDevelopment
npm test # node:test, fake-request driven — no running dsh neededLicense
MIT