Self-healing Cloudflare quick-tunnel pool for the dsh web GUI: floating widget with QR phone access, generational rotation, chase service worker and prompt-free credential handoff.
Install
# from GitHub (first run asks for allowBuilds approval — follow the hint, retry)
dsh plugin --profile web add github:iimaguest/phone-tunnel-pool
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
English | 简体中文
Enable/disable a self-healing Cloudflare quick-tunnel pool for
http://127.0.0.1:3080 (the DeepSeek Harness web GUI) from a floating widget
with a scannable QR code. One scan from your phone, and the pool keeps
itself alive:
- Generational rotation (12h): a new pair of tunnels spawns on schedule; older generations stay alive while anything is still on them.
- Chase service worker: every origin your browser touches registers a service worker. Dead or rotated hostnames redirect to a live sibling or the newest primary — the same open tab survives generation changes as long as it stays connected.
- Prompt-free migrations: the proxy injects credentials only into pages it has already authenticated; before any redirect the watchdog pre-authenticates the target hostname (minting its auth cookie), so migrations land authenticated — no "Authentication required" popups.
- Usage-based retirement: generations retire only when idle (no tabs / websockets / recent traffic) or at a hard age cap.
- Respawn with backoff: dead tunnels are replaced with new hostnames; quick-tunnel mint quota (Cloudflare 429) is respected via exponential backoff + a 2-probe dead-grace (DNS propagation).
Extra: the daemon runs detached and adopts across dsh web restarts, so
the same URL, password and QR stay valid until you click Disable — no
re-scan (an OS reboot still costs one fresh scan; a named tunnel removes even
that — see PLAN.md §7).
Install / Uninstall
# install (from this public repo)
dsh plugin --profile web add github:iimaguest/phone-tunnel-pool
dsh web # the GUI shows a floating 📱 widget (bottom-right)
# uninstall (one command — removes the dependency AND the dsh.profile.bundles layer)
dsh plugin --profile web remove phone-tunnel-pool
dsh web
After install: open the widget → Enable → scan the QR with your phone
camera. Install/remove reconcile dsh.profile.bundles against the installed
state automatically — never edit ~/.dsh/profiles/web/package.json by hand;
a stray bundle entry with no matching dependency is exactly the kind of state
that fails profile boot ("cannot resolve profile bundle").
Prerequisites (all of them)
| What | Needed? | Who provides it |
|---|---|---|
dsh web running on its default port 3080 (DSH_TARGET_PORT to override) |
required | you (the plugin tunnels to it) |
cloudflared binary on PATH |
required | you — brew install cloudflared (or apt/dnf/Chocolatey, or set DSH_CLOUDFLARED to the existing binary) |
| Node.js runtime | required | dsh itself — no separate install (the daemon reuses dsh's node) |
python3 + qrcode package |
optional | you — pip install qrcode; without it the widget shows URL + login instead of a scannable QR |
caffeinate |
optional | macOS ships it; skipped elsewhere |
| PowerShell | optional | Windows ships it — used only for process cleanup on Windows (no pkill there) |
| Outbound network | required | cloudflared → Cloudflare edge on 443/7844 (no inbound port needed) |
The widget preflights these on dsh web start and shows a yellow warning
line (with the exact fix, e.g. brew install cloudflared) before you even
click Enable; the daemon also fails fast with a readable error if cloudflared
is missing at Enable time, and refresh in the popup re-checks everything —
a stale error clears once the prereqs pass.
The feature flag set is version-gated on cloudflared --version:
2024.6+ enables the opt-in post-quantum handshake (DSH_PQ=1), 2024.8+ adds
--management-diagnostics=false; older builds (apt/dnf packages) get a
reduced, compatible flag set.
Platforms. macOS, Linux and Windows (Windows uses PowerShell for process
cleanup; caffeinate is macOS-only and silently skipped elsewhere). The
daemon's state file and log live in the per-OS temp directory
(os.tmpdir()); the widget settings file (iptunnel-settings.json) lives in
~/.dsh.
Screenshots
Live hostnames, credentials and the QR are blurred out in these shots.
How it's wired
dsh web GUI <-- /iptunnel routes -- auth proxy (127.0.0.1:3090)
│ Basic + session cookie,
│ Host rewrite to 127.0.0.1:3080
│ (the GUI's browser-trust fence)
▼
cloudflared A ─ to ─ auth proxy ───────────────────────────────────┐
cloudflared B ─ to ─ auth proxy ───────────────────────────────────┤ (tunnel daemon
... new generations ... ────────────────────────────────── │ manages all)
Files: lib/index.js (host API: enable/disable/adopt, state + QR SVG routes),
lib/daemon.mjs (detached pool brain: spawn, probe, rotate, retire, respawn),
cf-auth-proxy.mjs (public /iptunnel/* service paths + Basic auth +
watchdog injection + credential handoff), iptunnel-sw.js (chase service
worker), iptunnel-watchdog.js (open-tab watchdog), lib/client.js
(widget), verify.sh (end-to-end audit). PLAN.md = full spec + edge cases;
NOTES.md = engineering history.
The dsh 0.2 launch token (why the tunnel 401'd)
dsh 0.2 moved web auth into the connection service: the desktop URL carries a
per-process launch token (http://127.0.0.1:3080/?token=<secret>), a valid
token exchange mints a signed browser-session cookie, and every request with
neither gets 401. The tunnel path is phone → trycloudflare → this proxy →
the GUI, and a phone can never learn a token that only exists in the desktop
process output — so on 0.2 the tunnel answered 401 all the way down.
The fix (0.3.35): the plugin reads the token through the connection service's
public authenticatedUrl() API and hands it to the proxy — via the daemon
environment on a fresh enable, and via POST /iptunnel/__ctl/launch-token
whenever a surviving daemon outlives a dsh restart (the token is
per-process, the daemon is deliberately not). The proxy exchanges the token
against the GUI once, holds the resulting session cookie, and stamps it onto
every forwarded request and websocket handshake — re-minting on an upstream
401 and on every token push. A phone therefore presents only this proxy's
Basic auth, exactly as before, and the launch token never reaches cloudflared's
environment, the QR, or the published URL. On a dsh without the gate the token
is empty, every new code path no-ops, and forwarding is byte-identical to the
pre-0.2 behaviour (test/launch-token.test.mjs pins all of this against a
mock origin, including rotation).
Resource footprint (minimal by default)
- Disabled = zero processes (just the floating pill in the GUI).
- Enabled = 1 node daemon + 1 auth proxy + 2
cloudflaredper live generation. Default ceiling: 4 generations × 2 = 8 tunnels (a busy pool runs all of them; idle generations retire on their own after 60 min). - Knobs to shrink further:
DSH_MAX_GENS=2(≤4 tunnels),DSH_IDLE_MS=1200000(retire after 20 min idle),DSH_PQ— post-quantum handshake is opt-in (DSH_PQ=1) because it costs CPU per connection; without it the tunnel uses the classic handshake. - Phone battery: the watchdog backs off 30s → 300s (5 min) while nothing changes.
- Keep-awake is opt-in:
caffeinate(macOS) is off by default; turn it on in the widget ("Keep machine awake while enabled") or viaDSH_CAFFEINATE=1— it applies on the next Enable (and lets the display sleep —-ionly, no screen-on drain). Without it, an idle MacBook may sleep and the pool goes quiet until it wakes. - Daemon log is capped at 512 KB (keeps the last 128 KB); probes run at 30s.
Security model
- The password is generated per Enable, held in memory, shown in the
widget and embedded in the QR; nothing is committed or published. (The
daemon keeps the current credentials in a
0600state file under the OS temp dir so the tunnel survives a dsh web restart; that file is deleted on disable.) /iptunnel/*service paths (health, sw-config, sw.js, entry, watchdog.js, telemetry, preauth) are public by necessity — browsers fetch service workers without credentials; they carry hostnames and pool liveness only. The credential handoff (/iptunnel/preauth) mints a cookie only for a caller presenting the valid password; it never echoes anything.window.__ptAuthis injected only into HTML the proxy has authenticated.- The proxy listens on 127.0.0.1; public network exposure happens only through the tunnel hostnames — the QR/hostname is a bearer secret (anyone who gets it can open the tunnel while enabled): disable when done.
- Quick tunnels are testing-grade (no uptime SLA, per-IP mint quota). The repo-agnostic sibling package carries the same pattern for any local port; a named tunnel is the lifetime endgame (one stable hostname → no re-scans, no prompts, no quota).
License
Apache-2.0 — see LICENSE. Third-party code:
cloudflared (distributed by
Cloudflare), the Python qrcode library — used at runtime, not vendored.
Links
More in this category
zhu1090093659/dsh-web#packages/dsh-remote-web-ui★ 8670
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.
zhu1090093659/dsh-web#packages/dsh-ssh★ 8670
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.
saya-ch/dsh-mobile★ 441
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.
ZSeven-W/dsh-ios★ 320
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.
liguobao/ds-harness-remote★ 318
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★ 195
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.