dshn — DeepSeek Harness Network
English · 中文
Expose a locally-running DeepSeek Harness (dsh) web UI to the public
internet under a *.ds.hn subdomain, gated by a login. Install the plugin, open
dsh locally, and a form in Settings asks for a subdomain prefix and a
password — those two are the credential. No tokens, no env vars, no
provisioning. An optional end-to-end password encrypts the traffic so even
the relay operator sees only ciphertext.
⚠️ dsh ships bash and filesystem tools. A publicly reachable dsh UI is a
remote shell. The relay's login gate is not optional; do not disable it. Use
a strong password, and prefer end-to-end encryption for anything sensitive.
Features
- Zero-config credential.
(subdomain, password) set once in dsh's Settings
→ the plugin claims the subdomain and connects. Persists to dsh's own
~/.dsh/settings.yaml and reconnects on restart.
- Trust-on-first-use claim. The first agent to present a free subdomain sets
its password (scrypt-hashed on the relay). Later connects and every browser
login must match it — squatting-protected.
- Multi-device. Several machines can bind ONE subdomain with the same
credential — each shows up as a named device. With ≥2 online, opening the URL
offers a device picker, and a switcher appears in the page's sidebar footer;
the choice sticks per browser (a routing cookie), and switching is a clean
reload against the other machine. One device online behaves exactly as before.
- Optional end-to-end encryption (off by default). A separate e2e password,
never sent to the relay, encrypts
/api bodies and the event stream:
PBKDF2-SHA256 (210k) → AES-256-GCM. Visitors enter it once in the browser; it
can be remembered per-device in localStorage (never transmitted).
- Native UI. Config lives in dsh's own Settings ("公网转发" / Public
forwarding); a footer row shows live latency and links to it.
- Compressed uplink. Response bodies are gzipped before they enter the
tunnel, because the agent→relay hop is the narrow one and dsh's own web server
compresses nothing by default. A cold app load drops from ~12.5 MB to ~5.0 MB
on the wire (2.5×); a response dsh already encoded is passed through untouched.
- Self-hosted data plane. Traffic rides Cloudflare's edge to your server —
no per-user Cloudflare account, no NS delegation.
dsh compatibility
Tested against dsh 0.1.5-rc.1 (and built to keep working on older ones).
From dsh 0.1.2-rc.1 onward, dsh fences its app shell and every /api call
behind a signed, authority-bound browser-session cookie: a browser earns one by
opening the launch URL dsh prints, and everything else gets
401 dsh web authentication required. A public visitor can never hold that
cookie — their browser's cookies belong to the public authority, while every
request the agent replays has its Host rewritten to loopback, and dsh checks the
cookie against the Host it arrives on. So the agent asks dsh's own connection
service for this process's launch token, spends it over loopback once, and stamps
the resulting cookie on every replayed request and tunnelled upgrade. The cookie
never leaves the machine: neither the relay nor the visitor's browser sees it,
and access stays gated by the relay's login. On a dsh without that fence the
agent detects it and behaves exactly as it did before.
Agent 0.4.0 is required for dsh ≥ 0.1.2. Earlier agents forward a 401 page
and nothing else. No relay change is needed — the fix is entirely in the agent,
so a deployed relay keeps serving both.
Architecture
browser alice.ds.hn
│ HTTPS
▼
Cloudflare edge (*.ds.hn proxied / orange-cloud) free DDoS, WAF, TLS,
│ origin pull anycast, hidden origin
▼
relay (your server, @dshn/relay) login gate + subdomain
│ one WSS per device (multiplexed) claim store; moves bytes
▼
dshn (the dsh plugin, on the user's machine) replays HTTP + WS to dsh,
│ http://127.0.0.1:<dsh port> Host/Origin rewritten to loopback
▼
dsh (local web server) fence sees a loopback request
- No trustedHosts patch. The agent rewrites each forwarded request's
Host/Origin to loopback, so dsh's
/api browser-trust fence accepts it as a
local same-origin request for any runtime-chosen subdomain — which is what
lets the subdomain come from a form instead of the composition. Access is gated
by the relay login, not the fence.
- End-to-end mode seals request/response bodies at the agent and opens them
in the browser; the relay stays a blind byte-mover. The app shell and plugin
bundles are left in the clear so the browser can bootstrap and show the unlock
gate. It defeats a passive/curious relay and a data-at-rest breach — not an
actively malicious relay that tampers with the served JS.
Packages
| package | what it is | runs where |
|---|
@dshn/protocol | the WSS frame contract both ends compile against | shared |
@dshn/agent | the dsh plugin: setup form + outbound tunnel + status widget + e2e | user's machine, inside dsh |
@dshn/relay | login gate + claim store + subdomain router + HTTP/WS bridge | your server, behind Cloudflare |
The claim store (packages/relay/src/claims.ts) is trust-on-first-use for now;
an account-backed control plane replaces it later.
Install the agent (user's machine)
From npm (recommended — one command, fully self-contained):
dsh plugin --profile web add @dshn/agent
dsh --profile web
Or a prebuilt tarball from the latest GitHub release:
curl -L -o dshn.tgz \
https://github.com/jsdvjx/dshn/releases/latest/download/dshn.tgz
dsh plugin --profile web add ./dshn.tgz
Or build from source:
pnpm install && node scripts/build-dist.mjs
dsh plugin --profile web add ./dist/dshn
dsh --profile web
Then open dsh locally, go to Settings → 公网转发 (Public forwarding), pick a
subdomain prefix and a password (optionally an end-to-end password), and click
Connect. Use the same access password to log in from a phone. Run at most
one agent per subdomain — two agents with the same credential fight over it.
Agent environment (all optional; sensible defaults):
| var | default | purpose |
|---|
DSHN_ENABLED | 1 | set 0 to load the plugin inert |
DSHN_RELAY_HOST | relay.ds.hn | relay authority; wss://origin.ds.hn:8787 for a direct off-Cloudflare path |
DSHN_ORIGIN_CA | — | PEM to pin a self-signed direct-origin cert |
DSHN_STATE | ~/.dsh/dshn-agent.json | legacy state file (creds now live in settings.yaml) |
DSH_HOME | ~/.dsh | dsh home directory |
Self-host your own network
You don't have to use ds.hn — run the whole thing on your own domain. The relay
ships as @dshn/relay (npm) and a Docker image; point your agents at it in
the setup form (pick 自托管 / Self-hosted and paste the relay URL) or with
DSHN_RELAY_HOST. Full guide, including DNS + TLS options: SELF-HOSTING.md.
# your server — the only thing you set is your apex; the cookie secret is
# auto-generated and persisted, claims + secret live in --data-dir
npx @dshn/relay --apex tunnel.example.com --data-dir /var/lib/dshn
# your dsh — or just set it in Settings → 公网转发 → 自托管
DSHN_RELAY_HOST=wss://tunnel.example.com dsh --profile web
Or from source:
pnpm install && pnpm build
node packages/relay/lib/index.js --apex ds.hn --data-dir ./dshn-data
The only setting you need is --apex. The cookie secret is auto-generated and
persisted under --data-dir (no openssl rand), reused across restarts; every
flag also has an env var (DSHN_APEX, …). --help lists them all:
| flag | env | default | purpose |
|---|
--apex | DSHN_APEX | ds.hn | apex the wildcard hangs off |
--data-dir | DSHN_DATA_DIR | ./dshn-data | holds claims.json + the auto-generated cookie-secret |
--port | DSHN_RELAY_PORT | 8787 | listen port |
--secret | DSHN_COOKIE_SECRET | (auto) | cookie HMAC secret; set only to pin it |
--tls-cert / --tls-key | DSHN_TLS_CERT / DSHN_TLS_KEY | — | PEM paths to serve HTTPS directly (else plain HTTP behind CF) |
--site | DSHN_SITE | — | apex landing-page HTML |
Cloudflare: proxy *.ds.hn (orange cloud) to the relay's origin. Harden the
origin to accept only Cloudflare — firewall to the
Cloudflare IP ranges and enable Authenticated
Origin Pulls (mTLS). Because Cloudflare closes a proxied WebSocket after ~100s
idle, both ends heartbeat every 25s — already built in. For a direct
(off-Cloudflare) tunnel that survives sustained heavy throughput, add a
grey-cloud (DNS-only) origin.ds.hn A record and point agents at it with
DSHN_RELAY_HOST + DSHN_ORIGIN_CA.
Status
Working end-to-end against dsh 0.1.5-rc.1. Known gaps: an occasional
tunnel-socket drop fails that connection's in-flight requests (no request replay
yet); Cloudflare can reset the tunnel under sustained heavy throughput (use the
direct-origin option); the CF free-plan 100 MB request cap can clip large dsh
image uploads; a single tunnel frame is capped at 128 MB, below dsh's own 300 MB
request-body limit, so an end-to-end-sealed response larger than that would fail;
end-to-end-sealed /api bodies are not compressed (ciphertext does not compress);
the claim store is trust-on-first-use with no account layer; and the relay origin
should be locked to Cloudflare IPs + Authenticated Origin Pulls in production.
License
MIT