Skip to content

Latest commit

 

History

5 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

dsh-loopback-serve

English · 简体中文

Minimal, upgrade-safe remote access to the DeepSeek Harness (dsh) Web UI from your phone over Tailscale Serve — dsh never leaves loopback.


TL;DR for agents and humans

dsh web binds 127.0.0.1:3080 by design and refuses --host 0.0.0.0. To use the UI from a phone over Tailscale, keep dsh on loopback and let tailscale serve terminate HTTPS from your private tailnet and proxy to that port. Only official flags are used (--trusted-host, --no-open, --port); no dsh source or node_modules is touched, so rc upgrades are far less likely to break.

Key facts (machine-greppable)

Fact Value
What it serves DeepSeek Harness (dsh) Web UI
dsh bind 127.0.0.1:3080 (loopback only, by design)
--host 0.0.0.0 rejected by dsh (do not use)
Remote access Tailscale Serve (HTTPS on your tailnet)
Public internet never (never tailscale funnel)
Required dsh flag --trusted-host <node>.tailXXXX.ts.net
Launcher bash start-dsh-web.sh
Hostname config local.conf (gitignored) or $DSH_TS_HOST
First-use auth visit ?token=... URL once → 30-day signed cookie
Verified against dsh 0.1.2-rc.1, Tailscale 1.102.3, Ubuntu 26.04

Repo layout

start-dsh-web.sh        one-shot launcher (stop → start with trust flag → print URLs)
README.md               this file
README.zh.md            简体中文
docs/TROUBLESHOOTING.md auth/token internals, 401 vs 403, persistence, alternatives, boot autostart
local.conf              generated at --configure; gitignored (holds hostname)
LICENSE, SECURITY.md

Why this architecture

DeepSeek Harness is a plugin-first agent harness whose Web UI can read files, run shell commands, and write disk — a remote-code-execution surface. For that reason dsh binds to loopback and refuses --host 0.0.0.0. You therefore cannot use a phone browser straight at http://<ip>:3080; you need a tunnel.

Tailscale Serve is the least-moving-parts, tailnet-only option: it terminates HTTPS, exposes only to your own devices, and proxies back to the loopback port. The UI stays on loopback (smallest dsh attack surface) and dsh internals stay untouched (rc upgrades don't break you). Cleaner/faster alternatives exist (Cloudflare Tunnel + Access, zero-trust gateway plugin) but are heavier; binding 0.0.0.0 + a password wall is explicitly less safe.

Architecture

phone/desktop browser (tailnet)
      │  https://<node>.tailXXXX.ts.net/      (TLS by Serve)
      ▼
Tailscale Serve  (terminates HTTPS; no header injection here)
      │  plain HTTP proxy
      ▼
dsh web 127.0.0.1:3080   (the ONLY listener that can touch dsh)

Prerequisites

  • dsh on PATH, with a Web profile (~/.dsh present)
  • Tailscale: logged in, node on your tailnet, MagicDNS enabled
  • A serve route https://<node>.tailXXXX.ts.net/ → 127.0.0.1:3080
  • Phone / other device on the same tailnet

Quick start

# 0. Configure tailnet hostname once (find it via `tailscale status` → self node)
bash start-dsh-web.sh --configure '<node>.tailXXXX.ts.net'

# 1. Serve maps 3080 (needs sudo once, 443)
sudo tailscale serve --bg 3080
tailscale serve status          # verify

# 2. Launch dsh web for remote use
bash start-dsh-web.sh
#   prints:
#     Local access : http://127.0.0.1:3080/?token=...
#     Phone FIRST visit (exchanges token for a 30-day cookie):
#       https://<node>.tailXXXX.ts.net/?token=...

Phone (browser, tailnet): open the ?token=... URL once → dsh mints a signed, 30-day cookie (persisted across dsh restarts). After that, https://<node>.…/ just works.

First visit applies to the LOCAL browser too

The browser-auth applies to any origin, including 127.0.0.1:3080 on the machine itself. If you open 127.0.0.1:3080 and get dsh web authentication required; reopen the URL printed by dsh web, that's normal — you must first visit once with the token:

# one-off token exchange (localhost)
http://127.0.0.1:3080/?token=<CURRENT_TOKEN>

Then plain http://127.0.0.1:3080/ works for 30 days on that browser.

Getting the current token (incl. systemd autostart)

When dsh web runs under the systemd user service (dsh-web.service) the URL is not printed to your terminal — it goes to the journal. Read it from there:

journalctl --user -u dsh-web.service --no-pager | grep -oE 'token=[A-Za-z0-9_-]*' | tail -1

(The start-dsh-web.sh launcher prints it directly instead.) Every dsh web start mints a new token, so re-read it after any restart. See docs/TROUBLESHOOTING.md for the full auth model.

Daily operations (start / stop / restart)

Both ways to run dsh web — the launcher script and the systemd autostart service — and how to control each. Pick the one you use.

Option A: systemd user service (autostart at boot)

If you set up the boot service (dsh-web.service), this is the everyday path:

systemctl --user status  dsh-web.service   # is it running? (→ active)
systemctl --user restart dsh-web.service   # restart (mints a NEW token)
systemctl --user stop    dsh-web.service   # stop (phone fails until start)
systemctl --user start   dsh-web.service   # start again
systemctl --user disable --now dsh-web.service   # turn OFF boot autostart
journalctl --user -u dsh-web.service --no-pager | grep -oE 'token=[A-Za-z0-9_-]*' | tail -1   # current token

Restart=on-failure relaunches it automatically on a crash; use the manual commands only when you want control or a fresh token.

Option B: launcher script (manual, no autostart)

bash start-dsh-web.sh            # start (prints local + phone URLs with token)
pkill -f "dsh web --trusted-host <node>.tailXXXX.ts.net"   # stop

After any restart

The token changes. Re-read it (journalctl ... above, or the script's output) and have the phone/browser do the ?token= exchange again only if its 30-day cookie has expired or it's a new browser.

Non-privileged port (no sudo)

tailscale serve --bg --https 8443 http://127.0.0.1:3080   # URL uses :8443

Security

  • Serve exposes only to your own tailnet — not the public internet.
  • Never tailscale funnel — that exposes a shell-capable UI to the world.
  • After first visit, the 30-day cookie is the access boundary; revoke = delete cookie.
  • dsh's /api fence is anti-DNS-rebinding / cross-origin, not auth. Serve's private tailnet is your network-level auth. For per-identity auth see docs/TROUBLESHOOTING.md → Alternatives (dsh-one-gateway, dsh-auth-tailscale, read Serve's Tailscale-User-Login header).
  • Before pushing a change, scan for real identifiers per SECURITY.md.

Requirements

  • Linux (verified Ubuntu 26.04 Desktop); pattern is cross-platform (macOS/WSL2).
  • Node.js (dsh usually supplies its runtime).

License

MIT — see LICENSE.

Disclaimer

DeepSeek Harness is a developer preview. Everything here is verified against a specific dsh rc version and dsh APIs change fast. After an upgrade, re-check the checklist in docs/TROUBLESHOOTING.md.

Contributing

Open an issue or PR. Keep change-scans per SECURITY.md.

About

Tailscale Serve remote access for DeepSeek Harness (dsh) - loopback-only, upgrade-safe, self-hosted.

Topics

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages