UI Enhancements

dsh-cftunnel

weicy98/dsh-cftunnel · ★ 1

weicy98

dsh cf tunnel ui with token/email verify

License
MIT
Added
2026-08-16
Maintenance
active
Install kind
github
Manifest details
Manifest status
Declared
Manifest path
package.json#dsh
Manifest checked
2026-08-19
Lifecycle scripts
None found

Static manifest checks report declarations only; they are not compatibility or security review.

安装

dsh plugin --profile web add github:weicy98/dsh-cftunnel

GitHub 源码安装:首次需按提示配置 allowBuilds 构建授权后重试

For install and environment setup, plus plugin development tutorials, see DSH community docs ↗

README

Source README · body: en · Read Chinese README

Table of contents
  1. Quick start
  2. Modes
  3. How it works, end to end
  4. Install
  5. Configuration
  6. Security model (important)
  7. Access control (email OTP)
  8. API token permissions
  9. Prerequisites
  10. Development
  11. Architecture
  12. Limitations

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:

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

  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):

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 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

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, 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.