Skip to content

harn tunnel

harn tunnel up | down | reload | status | logs | allow [...]

harn tunnel puts a local reverse-proxy gate in front of a provider tunnel, so you can reach a local service (the web UI, a dev server, etc.) from your phone or another machine without opening a firewall port or deploying. The gate rewrites the upstream Host header and forwards the request. Cloudflare mode uses a Cloudflare quick tunnel plus an IP allowlist checked against CF-Connecting-IP; Tailscale mode uses Tailscale Serve/Funnel and lets Tailscale own the access boundary.

Terminal window
harn tunnel up # start the default instance
harn tunnel up --target 127.0.0.1:9000 --vhost localhost:9000 # custom upstream + Host
harn tunnel up --name web # a second, independently-managed instance
harn tunnel up --provider cloudflare # default: random *.trycloudflare.com URL + IP allowlist
harn tunnel up --provider tailscale # tailnet-only Tailscale Serve URL
harn tunnel up --provider tailscale --visibility public # public Tailscale Funnel URL
harn tunnel up --provider tailscale --name web --path /web # named instance at a path
harn tunnel status # table of running instances + their URLs
harn tunnel down [--name <id>|--all] # stop one / all
harn tunnel reload [--name <id>|--all] # pick up allowlist edits, keeping the URL
harn tunnel logs [-f] # tail the gate log (--provider for the provider log)
Provider Command URL shape Access model
Cloudflare harn tunnel up --provider cloudflare random https://*.trycloudflare.com gate requires Cloudflare’s CF-Connecting-IP to match harn tunnel allow
Tailscale Serve harn tunnel up --provider tailscale https://<machine>.<tailnet>.ts.net/ tailnet members only
Tailscale Funnel harn tunnel up --provider tailscale --visibility public https://<machine>.<tailnet>.ts.net/ public internet, if Funnel is enabled in the tailnet policy

Cloudflare gives each instance a separate random hostname. Tailscale gives the machine one MagicDNS hostname, so named instances default to path mounts: the default instance uses /, and --name web uses /web unless you pass --path.

The Cloudflare provider denies every request whose origin IP isn’t on the allowlist (shared across Cloudflare instances):

Terminal window
harn tunnel allow # list allowed IPs
harn tunnel allow add 1.2.3.4 # add, then `harn tunnel reload --all` to apply
harn tunnel allow rm 1.2.3.4 # remove

Each gate reads the allowlist once, when it starts, so editing the config alone changes nothing for a tunnel that is already running.

harn tunnel reload restarts an instance’s gate in place and leaves the provider process alone:

Terminal window
harn tunnel reload # the default instance
harn tunnel reload --name web # one named instance
harn tunnel reload --all # every live instance

The reason to reach for this instead of down then up is the URL. A Cloudflare quick tunnel’s hostname is minted by cloudflared when it starts, so a full restart hands back a different *.trycloudflare.com address and invalidates every link you have already shared. cloudflared only ever forwards to the gate’s local port, so swapping the gate underneath it keeps the hostname intact and the edge reconnects on its own.

Denied requests are logged with the client IP, so the usual sequence after someone hits a 403 is to read the IP out of the gate log, add it, and reload:

Terminal window
harn tunnel logs # find the `deny: ... ip=<addr>` line
harn tunnel allow add <addr>
harn tunnel reload --all

--all covers live instances only. Stale state files from earlier runs are counted and skipped rather than reported as failures. Reloading a single stale instance refuses instead, because a dead provider means the public URL is already gone and only harn tunnel up can mint a new one.

  • Quick tunnels reroll their hostname on every restart. Use harn tunnel reload for allowlist changes so the URL survives. A hostname that persists across a full down/up would need a named Cloudflare tunnel tied to a domain, which isn’t built today.
  • cloudflared auto-installs to ~/.local/bin/ on first run (Linux); on macOS, brew install cloudflared.
  • Tailscale mode requires the tailscale CLI to be installed, signed in, and reporting a MagicDNS name from tailscale status --json.
  • Tailscale Funnel is public internet exposure. Use Tailscale Serve when the reviewing device is in the tailnet.
  • State lives under .cache/tunnel/.