Bundle
phone-tunnel-pool
One-click Cloudflare quick-tunnel pool for the dsh web GUI — access from your phone via QR, self-healing generations, chase service worker.
- Source
- iimaguest
- stars
- 4 stars
- License
- Apache-2.0
- Updated
- Updated 7 days ago
Readme
# phone-tunnel-pool — Cloudflare quick-tunnel pool for the dsh web GUI
**English** | [简体中文](./README.zh-CN.md)
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`](./PLAN.md) §7).
## Install / Uninstall
```bash
# 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
<p align="center">
<img src="docs/phone-on-tunnel.jpg" alt="dsh web on a phone, reached through the tunnel pool" width="280">
</p>
<p align="center">
<img src="docs/screenshot-widget.png" alt="Tunnel pool widget: a phone tunnel enabled with a live pool of generations" width="380">
</p>
*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 `cloudflared` per 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 via
`DSH_CAFFEINATE=1` — it applies on the next Enable (and lets the display
sleep — `-i` only, 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 `0600` state 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.__ptAuth` is 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](https://github.com/iimaguest/port-tunnel-pool)
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](./LICENSE). Third-party code:
[cloudflared](https://github.com/cloudflare/cloudflared) (distributed by
Cloudflare), the Python `qrcode` library — used at runtime, not vendored.
Install
dsh plugin --profile web add github:iimaguest/phone-tunnel-pool#bff870f6aa4f47c94063b84397660eb289c2a685
Profile: web
With the hub plugin installed, ask your agent to install it by name — it resolves the same plan shown here.
dsh plugin --profile web add github:stvlynn/dsh.fish#path:packages/dsh-plugin-hub
install phone-tunnel-pool from the hub