phone-tunnel-pool — Cloudflare quick-tunnel pool for the dsh web GUI
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.
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.
No comments yet. Be the first to write one.