Back to home

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

English | 中文

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

  1. Install — one command:

    dsh plugin --profile web add git+https://github.com/weicy98/dsh-cftunnel
    
  2. Restart dsh web, open Settings → Cloudflare Tunnel.

  3. 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:

ModeInputsURLUse when
Quick tunnelnonerandom https://<name>.trycloudflare.comyou want a URL right now, no Cloudflare account setup
Tunnel tokentunnel token + hostnamehttps://<hostname>you already have a tunnel from the Zero Trust dashboard
API tokenAPI token + tunnel name + hostnamehttps://<hostname>you want the plugin to create the tunnel, DNS, and (optionally) email OTP for you

How it works, end to end

  1. The Settings section shows a mode picker (Quick / Tunnel token / API token) and a Connect button.
  2. On Connect the plugin first self-provisions cloudflared — if $DSH_HOME/cloudflared/cloudflared is missing it downloads the official GitHub release asset for your platform, extracts it, and caches it.
  3. It starts the tunnel:
    • Quick: cloudflared tunnel --url http://127.0.0.1:<proxyPort> → random https://<name>.trycloudflare.com.
    • Token: cloudflared tunnel run with 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 run with the returned token (no cert.pem, no local config.yml).
  4. 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.
  5. 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 a cloudflare-tunnel settings 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 treats file: 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:

  • downloadMirror controls how the one-time cloudflared download 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 / accessZoneId are also fillable in the Settings panel; setting them there avoids needing Account Settings:Read / Zone:Read API-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 an HttpOnly; Secure cookie), 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 (unless accessAccountId is set) Account Settings:Read to resolve the account id.

API token permissions

API-token mode needs (see also the hint inside the Settings panel):

ScopePermissionUsed for
AccountCloudflare Tunnel — Writecreate tunnel, set ingress
ZoneDNS — Editcreate the CNAME record
AccountAccount Settings — Readresolve account id (skip by filling Account ID)
ZoneZone — Readresolve zone id (skip by filling Zone ID)
AccountAccess: Organizations, Identity Providers, and Groups — Writeemail 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 to 3080 bypasses 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.com on first connect (only to fetch cloudflared; it is then cached).

Development

pnpm install   # dsh-settings (dev/peer), schemastery
pnpm test      # node --test (cfapi, cloudflared, host lifecycle, and proxy suites)

Architecture

ConcernWhereMechanism
Provision cloudflaredlib/cloudflared.jsGitHub release download + tar extract + cache under $DSH_HOME/cloudflared
Run/manage tunnellib/index.jsnode:child_process spawn + ctx.effect teardown
Cloudflare APIlib/cfapi.jscfd_tunnel create + configurations ingress + dns_records CNAME + Access app/policy
Loopback proxylib/proxy.jsHost rewrite to loopback + Origin drop + access-token gate (cookie / ?token=) + WS forward — zero dependencies (node:http + node:net, no http-proxy / DEP0060)
Dependenciespackage.jsonschemastery as a leaf dependency; host seams (dsh-settings) as peerDependencies; engines.node >= 20.3
Secretslib/index.jsctx.credentials (CLOUDFLARE_API_TOKEN, CLOUDFLARE_TUNNEL_TOKEN, CLOUDFLARE_TUNNEL_PROXY_TOKEN)
Preferenceslib/index.jsctx.settings namespace cloudflare-tunnel
Client→Host RPCboth halvesctx.connection.rpc.handle('/cloudflare-tunnel', …) / ctx.get('connection').rpc.call(…)
Settings UIlib/client.jsctx.slots.inject('settings.section', …) + locale dictionaries
Bundle wiringcordis.patch.yml + package.json dshone 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, and webServer; 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 cloudflared authenticates with the tunnel token, not the API token.