dshn
jsdvjx/dshn
Forward a local dsh web service to the public internet over ds.hn (bundled).
Installation
dsh plugin --profile web add github:jsdvjx/dshnREADME
dshn — DeepSeek Harness Network
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.yamland 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
/apibodies and the event stream: PBKDF2-SHA256 (210k) → AES-256-GCM. Visitors enter it once in the browser; it can be remembered per-device inlocalStorage(never transmitted). - Native UI. Config lives in dsh's own Settings ("公网转发" / Public forwarding); a footer row shows live latency and links to it.
- Self-hosted data plane. Traffic rides Cloudflare's edge to your server — no per-user Cloudflare account, no NS delegation.
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
/apibrowser-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 | 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
DSHN_COOKIE_SECRET=$(openssl rand -hex 32) \
DSHN_APEX=ds.hn \
DSHN_RELAY_PORT=8787 \
DSHN_CLAIMS=./claims.json \
DSHN_TLS_CERT=./cert.pem DSHN_TLS_KEY=./key.pem \
node packages/relay/lib/index.js
| var | required | purpose |
|---|---|---|
DSHN_COOKIE_SECRET | ✅ | HMAC secret for session cookies (rotating it logs everyone out) |
DSHN_APEX | — (ds.hn) | apex domain the wildcard hangs off |
DSHN_RELAY_PORT | — (8787) | listen port |
DSHN_CLAIMS | — | JSON file the relay creates/maintains (subdomain → scrypt hash) |
DSHN_TLS_CERT / DSHN_TLS_KEY | — | PEM paths to serve HTTPS directly (else plain HTTP behind CF) |
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. 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; 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.