weicy98
dsh-cftunnel
dsh cf tunnel ui with token/email verify
- Stars
- 1
- Language
- JavaScript
- Created
- Aug 16, 2026
- Updated
- Aug 16, 2026
Introduction
dsh-cftunnel
A DeepSeek Harness bundle plugin that runs a Cloudflare Tunnel to expose the local web GUI (http://127.0.0.1:3080) on the public internet.
Install the plugin, open Settings, type your Cloudflare info, click Connect. cloudflared is downloaded and managed by the plugin itself — no manual install, no package manager, no restart.
Quick start
-
Install — one command:
dsh plugin --profile web add git+https://github.com/weicy98/dsh-cftunnel -
Restart
dsh web, open Settings → Cloudflare Tunnel. -
Pick a mode, fill in your Cloudflare info, click Connect. Copy the access URL (it embeds a token on first use) and share it.
The first Connect downloads
cloudflared(~40 MB) once — on slow networks this can take several minutes; the Settings panel shows a progress bar while it downloads.
Modes
Pick the mode that matches what you have:
| Mode | Inputs | URL | Use when |
|---|---|---|---|
| Quick tunnel | none | random https://<name>.trycloudflare.com | you want a URL right now, no Cloudflare account setup |
| Tunnel token | tunnel token + hostname | https://<hostname> | you already have a tunnel from the Zero Trust dashboard |
| API token | API token + tunnel name + hostname | https://<hostname> | you want the plugin to create the tunnel, DNS, and (optionally) email OTP for you |
How it works, end to end
- The Settings section shows a mode picker (Quick / Tunnel token / API token) and a Connect button.
- On Connect the plugin first self-provisions
cloudflared— if$DSH_HOME/cloudflared/cloudflaredis missing it downloads the official GitHub release asset for your platform, extracts it, and caches it. - It starts the tunnel:
- Quick:
cloudflared tunnel --url http://127.0.0.1:<proxyPort>→ randomhttps://<name>.trycloudflare.com. - Token:
cloudflared tunnel runwith a remotely-managed token (routing/ingress configured in the Zero Trust dashboard). - API: create the tunnel + ingress + DNS CNAME through the Cloudflare HTTP API, then
cloudflared tunnel runwith the returned token (nocert.pem, no local config.yml).
- Quick:
- All tunnel traffic flows through a loopback reverse proxy (
127.0.0.1:<proxyPort>) that rewrites the Host header to loopback and drops Origin, so the harness trust fence accepts every request — privileged methods included. Access-token verification (default on) and optional Cloudflare Access email OTP gate the proxy. - Secrets (API token, tunnel token, proxy access token) live in the harness credential store (
~/.dsh/.credentials.yaml); non-secret preferences (mode, hostname, tunnel name, account/zone ids, email-OTP settings) live in acloudflare-tunnelsettings namespace (~/.dsh/settings.yaml) and prefill the form on the next open.
Install
From this checkout's directory:
dsh plugin --profile web add file:.
Or from a git / npm source:
dsh plugin --profile web add git+https://github.com/weicy98/dsh-cftunnel
dsh plugin --profile web add dsh-cftunnel
dsh plugin runs pnpm in the profile directory, then reconciles dsh.profile.bundles — because this package declares dsh.bundle.patch, it is appended to the bundle stack automatically. Restart dsh web and open Settings → Cloudflare Tunnel.
Reinstalling after editing the source:
dsh plugin --profile web remove dsh-cftunnel && dsh plugin --profile web add file:.— pnpm treatsfile:dependencies as immutable, so remove + add (or a version bump) is the reliable refresh path.
Configuration
Deployment-level options are set through the profile's own cordis.patch.yml (~/.dsh/profiles/web/cordis.patch.yml) or a --patch overlay. Everything else is a Settings-panel field and persists in the settings namespace.
- id: cloudflare-tunnel
config:
cloudflaredPath: 'cloudflared' # explicit binary path (default: auto-download to $DSH_HOME/cloudflared)
port: 3080 # local harness port to proxy
downloadMirror: '' # optional ghproxy-style prefix, e.g. 'https://ghproxy.example/'
proxyPort: 3091 # loopback port of the reverse proxy
proxyAuth: 'token' # 'token' (default) | 'none'
proxyToken: '' # optional fixed access token; empty = auto-generate and persist
accessPolicy: 'none' # 'none' | 'emailOtp'
accessEmailDomains: [] # e.g. ['yourcompany.com'] — restrict email OTP delivery
accessAccountId: '' # optional explicit Cloudflare account id
accessZoneId: '' # optional explicit Cloudflare zone id
Notes:
downloadMirrorcontrols how the one-timecloudflareddownload happens. Empty (the default) downloads through the GitHub API → CDN — authoritative, but slow on some networks (measured ~80 KB/s, so ~8 minutes for the 40 MB binary). Set a mirror prefix to speed it up; the plugin then downloads from<mirror>https://github.com/cloudflare/cloudflared/releases/latest/download/<asset>. A mirror is a third party you are trusting to serve an unmodified binary.accessAccountId/accessZoneIdare also fillable in the Settings panel; setting them there avoids needingAccount Settings:Read/Zone:ReadAPI-token permissions.proxyAuth: 'none'disables the access-token gate (the UI confirms before turning it off).
Security model (important)
All tunnel traffic goes through a loopback reverse proxy (127.0.0.1:<proxyPort>) that rewrites requests to look loopback (Host → 127.0.0.1:<port>, Origin dropped). The harness trust fence therefore accepts them fully — privileged methods included — so a remote visitor gets the complete local experience.
The proxy binds 127.0.0.1 only; its sole entry point is cloudflared (Cloudflare edge). Two independent gates sit in front of it:
- Access token (default ON,
proxyAuth: 'token'): every request must carry the token — sent once as?token=…(the proxy then plants anHttpOnly; Securecookie), or as the cookie afterwards. The UI shows the ready-to-share access URL and can toggle the gate live. - Cloudflare Access email OTP (optional,
accessPolicy: 'emailOtp'): edge-level email verification before the request even reaches the proxy.
Recommendation: expose the public URL with both gates on. Disabling both means anyone holding the URL has full control of the harness (settings, credentials, directory picker) — only acceptable on a trusted network.
Local use (http://127.0.0.1:3080) bypasses the proxy entirely and is unaffected.
Access control (email OTP)
Set accessPolicy: 'emailOtp' (or tick the checkbox in the panel) to gate the tunnel behind Cloudflare Access One-time PIN. On connect, the plugin uses the same API token to create a self-hosted Access application bound to the hostname plus an allow policy:
- No whitelist: any visitor enters an email, receives a 6-digit PIN, and only gets through after entering it.
- Whitelist with full emails (
user@example.com): exact-email rules (Access "Emails"). - Whitelist with domains (
yourcompany.com): email-domain rules (Access "Emails ending in").
Requirements:
- API-token mode only (quick and token tunnels cannot be gated automatically — their hostname isn't programmatically managed by this plugin).
- The API token additionally needs
Access: Organizations, Identity Providers, and Groups:Edit, and (unlessaccessAccountIdis set)Account Settings:Readto resolve the account id.
API token permissions
API-token mode needs (see also the hint inside the Settings panel):
| Scope | Permission | Used for |
|---|---|---|
| Account | Cloudflare Tunnel — Write | create tunnel, set ingress |
| Zone | DNS — Edit | create the CNAME record |
| Account | Account Settings — Read | resolve account id (skip by filling Account ID) |
| Zone | Zone — Read | resolve zone id (skip by filling Zone ID) |
| Account | Access: Organizations, Identity Providers, and Groups — Write | email OTP only |
Prerequisites
- A profile with the web surface (
dsh web/--profile web). - Tunnel token mode: a tunnel token from the Zero Trust dashboard, with a public hostname routing to
http://127.0.0.1:3091(the loopback proxy port — routing to3080bypasses the access-token gate). - API token mode: an API token with the permissions above, plus a hostname whose DNS zone is on that account.
- Outbound network access to
github.comon first connect (only to fetchcloudflared; it is then cached).
Development
pnpm install # dsh-settings (dev/peer), schemastery
pnpm test # node --test (cfapi, cloudflared, host lifecycle, and proxy suites)
Architecture
| Concern | Where | Mechanism |
|---|---|---|
Provision cloudflared | lib/cloudflared.js | GitHub release download + tar extract + cache under $DSH_HOME/cloudflared |
| Run/manage tunnel | lib/index.js | node:child_process spawn + ctx.effect teardown |
| Cloudflare API | lib/cfapi.js | cfd_tunnel create + configurations ingress + dns_records CNAME + Access app/policy |
| Loopback proxy | lib/proxy.js | Host rewrite to loopback + Origin drop + access-token gate (cookie / ?token=) + WS forward — zero dependencies (node:http + node:net, no http-proxy / DEP0060) |
| Dependencies | package.json | schemastery as a leaf dependency; host seams (dsh-settings) as peerDependencies; engines.node >= 20.3 |
| Secrets | lib/index.js | ctx.credentials (CLOUDFLARE_API_TOKEN, CLOUDFLARE_TUNNEL_TOKEN, CLOUDFLARE_TUNNEL_PROXY_TOKEN) |
| Preferences | lib/index.js | ctx.settings namespace cloudflare-tunnel |
| Client→Host RPC | both halves | ctx.connection.rpc.handle('/cloudflare-tunnel', …) / ctx.get('connection').rpc.call(…) |
| Settings UI | lib/client.js | ctx.slots.inject('settings.section', …) + locale dictionaries |
| Bundle wiring | cordis.patch.yml + package.json dsh | one insert row, auto-added to dsh.profile.bundles |
RPC endpoints on /cloudflare-tunnel: status, start, stop, setProxyAuth. start accepts an AbortSignal, so cancelling from the UI aborts in-flight downloads and API setup.
Limitations
- Web-profile only (the node half waits for
connection,credentials,settings, andwebServer; in headless/TUI it stays dormant). - Process-local: the running tunnel does not survive a harness restart, but the cached binary, the stored secrets and preferences do. The tunnel, ingress, and DNS records persist on Cloudflare.
- The API token is used for Cloudflare API setup calls (tunnel/ingress/DNS/Access); the running
cloudflaredauthenticates with the tunnel token, not the API token.