Tailscale remote-access operations panel for the dsh web GUI: health checks, HTTPS entry toggle via tailscale serve, macOS proxy bypass, relay server ops, and ACL snippet generation.
Install
# from npm (prebuilt)
dsh plugin --profile web add dsh-tailscale-console
# from GitHub (first run asks for allowBuilds approval — follow the hint, retry)
dsh plugin --profile web add github:evanfang0054/dsh-tailscale-console
Any plugin you install runs third-party code with your own permissions — it can read your files, use your credentials, and reach the network, and tool approvals don’t sandbox it. GitHub-sourced plugins also run build scripts at install time — pnpm blocks those until you allow them, so an install can stop with ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED or ERR_PNPM_IGNORED_BUILDS; dsh prints the exact key to add under allowBuilds in your profile’s pnpm-workspace.yaml, and the install works on the next run. Allowing a build is a trust decision: only install sources you trust, and pin a commit (github:owner/repo#sha).
README
A control panel for the DeepSeek Harness web GUI that operates secure remote access over Tailscale: one-click health checks, HTTPS entry (Tailscale Serve) toggle, macOS proxy bypass repair, relay-server ops, and an ACL snippet generator.
UI language: Chinese. Docs: English · 中文
Features
| Card | Description |
|---|---|
| ① Devices & online alerts | Per-device online/offline state; auto-alerts when an offline device comes online |
| ② Health check | One-click verification of HTTPS entry, page, /api session list, remote /api RPC over HTTPS, server direct path, proxy bypass |
| ③ HTTPS entry | Tailscale Serve status / URL + on/off toggle (with verification & retry) |
| ④ Proxy bypass | Checks *.ts.net / 100.64.0.0/10 in the macOS system proxy bypass list; one-click re-apply (macOS only) |
| ⑤ Relay server | Tailnet status of your relay server + enable Peer Relay + ping verify |
| ⑥ Access | Remote HTTPS URL, local URL, and the exact dsh web start command |
| ⑦ ACL snippet | Generates the tailscale.com/cap/relay grants JSON from live device IPs |
| ⑧ Device pairing | One-click login link/QR for remote devices — re-pair from a device bookmark after restarts, with Add-to-Home-Screen (PWA) |
Full Setup Guide (server → local machine → DSH)
Target architecture: the server joins the tailnet as a Peer Relay; the local machine exposes HTTPS via Tailscale Serve; DSH keeps listening on 127.0.0.1 only.
0. Prerequisites
- Tailscale account (enable 2FA), MagicDNS enabled
- A Linux server with a public IP (Ubuntu 22.04 in this guide)
- Local macOS with the
tailscaleCLI installed and logged in - A dsh web profile (
~/.dsh/profiles/web) — requires dsh ≥ 0.1.0-rc.6 (for--trusted-hostand thewebRuntimeservice) - Node 20.x + pnpm ≥ 9 for the profile (
pnpm-lock.yamlis v9; the default pnpm 8 under Node 24 will fail. Verified with Node 20.19.2 + pnpm 10.27.0) - A phone with the Tailscale app installed and logged into the same account (for remote access testing)
How to find your tailnet values (the
<...>placeholders below):
- Local tailnet IP:
tailscale ip -4- All devices:
tailscale status- MagicDNS name (
<hostname>.<tailnet>.ts.net):tailscale status --json | python3 -c "import json,sys; print(json.load(sys.stdin)['Self']['DNSName'].rstrip('.'))", or read it from the Tailscale admin console → Machines, or from the output oftailscale serve statusonce serve is up (section 2.1)- Server tailnet IP:
tailscale statuson the Mac (the Linux peer)Tip: you can bring the panel up locally first with
dsh web(loopback default) — card ⑥ then shows the real remote URL, start command and ACL JSON, which you can copy back into the steps below.
1. Server side
1.1 Install & authenticate Tailscale
ssh <server> 'curl -fsSL https://tailscale.com/install.sh | sh'
ssh <server> 'nohup tailscale up >/tmp/ts-up.log 2>&1 & sleep 4; cat /tmp/ts-up.log'
# Open the https://login.tailscale.com/a/xxxx link printed, authorize with your account.
# The server comes online automatically; `tailscale status` on the Mac should list it.
1.2 Enable the Peer Relay port + firewall
ssh <server> 'tailscale set --relay-server-port=40000 && ufw allow 40000/udp && ufw allow 41641/udp'
# 41641 = WireGuard direct port; 40000 = Peer Relay port
⚠️ Your cloud security group must also allow inbound UDP 40000 and 41641 (source 0.0.0.0/0) — the guest ufw cannot protect the cloud edge.
1.3 Verify the direct path (run on the Mac)
tailscale ping -c 3 <server-tailnet-ip> # expect "via <public-ip>:41641" (tens of ms); DERP means 41641 is blocked
2. Local machine
2.1 HTTPS entry: Tailscale Serve (the core)
tailscale serve --bg 3080 # https://<hostname>.<tailnet>.ts.net → 127.0.0.1:3080, tailnet-only
tailscale serve status # inspect; `tailscale serve reset` to disable
Why HTTPS is mandatory: browsers expose
crypto.randomUUIDonly in secure contexts (HTTPS or localhost), and every DSH client RPC depends on it. The GUI athttp://<tailnet-ip>:3080will never work — debug only.
2.2 Proxy bypass (required if you run Clash-family proxies on macOS)
A local Clash proxy hijacks the private ts.net domain and breaks HTTPS in the browser. Add *.ts.net and 100.64.0.0/10 to the bypass list in Clash Verge → Settings → System proxy; or apply immediately:
networksetup -getproxybypassdomains "Wi-Fi" # first: back up the current list
networksetup -setproxybypassdomains "Wi-Fi" "*.ts.net" "100.64.0.0/10" "*.local" "<local>" "localhost" "127.0.0.1" "192.168.0.0/16" "10.0.0.0/8" "172.16.0.0/12"
⚠️
-setproxybypassdomainsreplaces the whole exception list of that service — back it up first with-getproxybypassdomains. Non-macOS: the panel hides card ④ automatically; for Linux/Windows set the equivalent bypass, e.g.export NO_PROXY="*.ts.net,100.64.0.0/10"in your proxy tool or environment.
2.3 (Optional) socat debug forward
brew install socat
socat TCP-LISTEN:3080,bind=<local-tailnet-ip>,reuseaddr,fork TCP:127.0.0.1:3080
# curl/API debugging only; the GUI is unusable over plain HTTP (non-secure context)
3. DSH configuration
3.1 Start command (use this every time)
dsh web --trusted-host <local-tailnet-ip>:3080 --trusted-host <hostname>.<tailnet>.ts.net
⚠️ Every change below (cordis.patch.yml, plugin code, better-sidebar patch) requires restarting
dsh web, which interrupts the currently running session — save/finish your work first. The web profile has HMR disabled.
3.2 Static trustedHosts (belt & braces)
Append to ~/.dsh/profiles/web/cordis.patch.yml:
- id: connection
config:
trustedHosts: ['<local-tailnet-ip>:3080', '<hostname>.<tailnet>.ts.net']
3.3 Known third-party issue: dsh-better-sidebar
If your profile installs dsh-better-sidebar, its /sidebar/api fence has a bug in version 0.10.3 (the latest published): trustedHostsOf() matches entry.options.name against "connection", but name is the package name, so the trust list is always empty and /sidebar/api/* is loopback-only (403 remotely — the sidebar breaks). No fixed upstream version exists yet.
Patch trustedHostsOf() in node_modules/dsh-better-sidebar/lib/index.js (back up the file first) to read the webRuntime service:
function trustedHostsOf(ctx) {
const hosts = []
const runtime = ctx.get("webRuntime")
if (runtime !== void 0 && Array.isArray(runtime.trustedHosts)) hosts.push(...runtime.trustedHosts)
for (const entry of ctx.loader.entries()) if (entry.options.id === "connection") {
const cfg = entry.options.config?.trustedHosts
if (Array.isArray(cfg)) hosts.push(...cfg)
}
return [...new Set(hosts)]
}
⚠️ The patch lives in
node_modulesand is lost on the nextpnpm install— re-apply after every reinstall (check withgrep -c 'options.id === "connection"').
4. ACL grant (required for phone relaying)
console.tailscale.com/admin/acls → JSON editor, add to grants:
{
"grants": [
{
"src": ["<device-A-ip>", "<device-B-ip>"],
"dst": ["<server-tailnet-ip>"],
"app": { "tailscale.com/cap/relay": [] }
}
]
}
(src = devices allowed to relay, dst = the relay server. Without it, phone traffic falls back to official DERP, 400ms+. Card ⑦ generates this for you.)
5. Install this plugin
The package declares dsh.bundle, so the official CLI installs and auto-mounts it into the profile's bundle stack — one command, no profile file edits:
# npm source (published): auto-appends to dsh.profile.bundles + mounts
dsh plugin --profile web add dsh-tailscale-console
# or install straight from the repo (also auto-mounts — the repo ships cordis.patch.yml at its root)
dsh plugin --profile web add github:evanfang0054/dsh-tailscale-console
Restart dsh web → Settings → Tailscale Console.
Optional per-user options (like sshAlias) are machine-specific and are not part of the bundled patch. Override them from the profile's own cordis.patch.yml by targeting the same entry id:
cd ~/.dsh/profiles/web
pnpm add "dsh-tailscale-console@file:./packages/dsh-tailscale-console" # local dev: file: reference
# append to ~/.dsh/profiles/web/cordis.patch.yml (optional overrides)
- id: tailscale-console
config:
sshAlias: my-server # optional: ssh alias for server-side mutations (install/relay)
⚠️ If you previously mounted this plugin the manual way (an
- insert:line incordis.patch.yml), remove that line before switching to thedsh plugin addbundle channel — keeping both double-mounts the plugin (two host halves, two panels).
6. Verification checklist
# HTTPS entry
curl -s -o /dev/null -w "%{http_code}\n" --noproxy '*' https://<hostname>.<tailnet>.ts.net/ # 200
# /api fence
curl -s --noproxy '*' -X POST https://<hostname>.<tailnet>.ts.net/api/session.list \
-H 'content-type: application/json' \
-d '{"type":"client-request","rpcId":"t-1","method":"session.list","payload":{}}' | head -c 120 # ok:true
# panel host route
curl -s -o /dev/null -w "%{http_code}\n" --noproxy '*' -X POST \
https://<hostname>.<tailnet>.ts.net/tsctl/api/config -H 'content-type: application/json' -d '{}' # 200
# direct path
tailscale ping -c 3 <server-tailnet-ip>
# Final: open https://<hostname>.<tailnet>.ts.net on any tailnet device — the sidebar lists all sessions
Configuration
All values come from the plugin Config; the code contains no personal identifiers. MagicDNS URLs and tailnet IPs are auto-derived from tailscale status --json when not configured.
| Key | Type | Default | Meaning |
|---|---|---|---|
dshPort |
number | 3080 |
DSH web GUI port |
relayPort |
number | 40000 |
Relay server's Peer Relay UDP port |
tailnetUrl |
string | auto | HTTPS base, e.g. https://myhost.tail1234.ts.net |
sshAlias |
string | — | SSH alias for server-side mutations (install / relay) |
serverPeerPattern |
string | first Linux peer | Regex to identify the relay server peer |
proxyServices |
string[] | macOS set | Network services for the bypass apply |
Security model
- Fixed command allowlist: every button maps to a hard-coded command; the client cannot pass free-form input.
- Sandbox: commands run with an explicit
danger-full-accesssandbox policy (same as the model's bash tool session policy). The defaultworkspace-writeconfinement kills the detached child oftailscale serve --bg, breaking the HTTPS "on" toggle. Blast radius stays bounded by the allowlist. - Browser-trust fence: the
/tsctl/apiprefix accepts only loopback Hosts or hosts in dsh's--trusted-hostlist (same fence as dsh's own/api). - Platform: proxy bypass cards are macOS-only; other platforms degrade gracefully.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Panel missing after restart, host route 200 | exports lacks "./package.json" — client scan (require.resolve('<pkg>/package.json')) is blocked by Node exports encapsulation |
add "./package.json": "./package.json" to exports; restart |
| Stale code served after editing sources | pnpm file: deps are hard-linked copies |
re-symlink: ln -s ../packages/dsh-tailscale-console node_modules/dsh-tailscale-console |
| HTTPS "on" fails silently | sandbox kills serve --bg's daemon |
fixed in code (explicit policy + verify/retry) |
| "On" button dead after HTTPS entry is off | chicken-and-egg: the HTTPS page itself is unreachable once serve is off (connection refused) | open the panel at http://127.0.0.1:<port> on the local machine and toggle on (remote page shows a hint) |
| Settings/credentials page 403 remotely | dsh PRIVILEGED_METHODS design limit |
operate locally; do not relax |
Health check "/api session list" always ✗, detail shows a mid-body fragment (e.g. 2144},"contextBreakdown"...) |
dsh's shell service truncates long stdout keeping only the tail (~63KB cap); the session.list response far exceeds it, so curl-based stdout loses its head and JSON.parse always fails |
fixed built-in (0.2.3+): Host requests 127.0.0.1 directly via node:http, bypassing the shell |
| Phone access is slow | ACL grants not saved | see section 4 |
tailscale ping (TSMP) works, but real TCP/ICMP all time out |
control-plane ACL lost the default allow rule (only custom rules like the Peer Relay grant remain); tailscaled logs show Drop: ... no rules matched |
re-add the default rule at the top of grants (below) — propagates in ~30s, no restart |
Default ACL rule (new grants format — three requirements)
When configuring a Peer Relay, the default "allow all connections" rule is easily overwritten. Once lost, every real packet is dropped by the ACL filter with no rules matched, while TSMP ping keeps working (internal path, bypasses ACL) — the classic "ping works, data doesn't" illusion. Always keep the default rule first:
{
"grants": [
{ "src": ["*"], "dst": ["*"], "ip": ["*"] },
{ "src": ["<device IP>"], "dst": ["<relay server IP>"],
"app": { "tailscale.com/cap/relay": [] } }
]
}
New grants requirements: dst takes no port; an explicit ip field is mandatory; app and ip cannot both be empty.
Inspect commands:
- Server:
journalctl -u tailscaled | grep "no rules matched" - macOS:
log show --last 30m --predicate 'eventMessage CONTAINS "no rules matched"'
License
MIT. The browser-trust fence mirrors dsh's api-request-trust logic (see @deepseek-ai/dsh-client-connection).
Links
More in this category
zhu1090093659/dsh-web-ui#packages/dsh-ssh★ 7488
SSH ops panel for DSH: web terminal, SFTP transfer with progress, local port forwarding, and one-command cluster execution across hosts; agents share the same host config.
zhu1090093659/dsh-web#packages/dsh-remote-web-ui★ 7488
Remote control of a dsh web workspace from phone or PC: QR-code pairing through a token-gated channel, SSE real-time sync, and separate mobile and full desktop GUI modes.
ZSeven-W/dsh-ios★ 288
A live iOS Simulator or USB-connected iPhone inside the conversation: 22 agent tools for booting, building, driving the UI by accessibility identity or OCR text, list-row actions and SwiftUI preview hot reload, plus a streaming sidebar panel you can tap and drag on.
saya-ch/dsh-mobile★ 260
Access DeepSeek Harness from the Android app or a mobile browser with secure LAN and remote connections, persistent device pairing, and a customizable mobile interface.
liguobao/deepseek-harness-remote★ 183
Multi-device remote access for DeepSeek Harness: continue an active session from your phone, tablet, browser, or another computer over an end-to-end encrypted channel (Noise IK + adaptive relay/WebRTC transport), with device authorization, ApiProxy-only remote capabilities, and read-only file preview via dsh-file-viewer — no shell, remote desktop, or write access.
wenbin-wb/dsh-bridge★ 158
Remote and mobile access for DeepSeek Harness: provides LAN QR code connection, Cloudflare/custom tunnels, WeChat, QQ, Feishu, Telegram bot integration, and security authentication.
Community comments
Comments are public GitHub Discussions. Loading them connects to GitHub and Giscus; a GitHub account is required to post.