Back to Omniroute

Tunnels Guide

docs/ops/TUNNELS_GUIDE.md

3.8.4912.7 KB
Original Source

Tunnels Guide

Source of truth: src/lib/{cloudflaredTunnel,ngrokTunnel,tailscaleTunnel}.ts, src/app/api/tunnels/ Last updated: 2026-06-28 — v3.8.40

OmniRoute can expose its local server (http://localhost:20128) to the public internet via three tunnel backends. This is useful for:

  • OAuth callbacks from cloud providers (Antigravity, Gemini, Cursor) that need a publicly reachable redirect URL.
  • Sharing your local instance with teammates without deploying a VM.
  • Mobile, remote, or cross-network testing.

All three backends are managed in-process — OmniRoute starts/stops the underlying binary or SDK from the dashboard or REST API. No reverse-proxy or systemd setup is required.

Backends at a glance

BackendPersistenceCostSetup
Cloudflare Quick TunnelEphemeral (URL changes each restart)FreeZero — auto-installs cloudflared
ngrokStable while a paid plan or fixed domain is configuredFree tier + paidRequires ngrok account + authtoken
Tailscale FunnelStable per node within your tailnetFree for personalRequires Tailscale install + login + Funnel ACL

The implementations live in src/lib/cloudflaredTunnel.ts, src/lib/ngrokTunnel.ts, and src/lib/tailscaleTunnel.ts. All three return a common-shaped status object with phase, running, publicUrl, apiUrl, targetUrl, and lastError fields, so the dashboard can render them uniformly.

1. Cloudflare Tunnel (Quick Tunnel)

src/lib/cloudflaredTunnel.ts runs cloudflared tunnel --url http://localhost:<apiPort> as a child process and parses the assigned *.trycloudflare.com URL from stdout.

Key behaviors:

  • Auto-install. On first use, OmniRoute downloads the latest cloudflared binary from the official GitHub releases (managed install lives under DATA_DIR/cloudflared/). SHA256 of the downloaded asset is verified against the release manifest before execution.
  • Quick-tunnel only. The current implementation runs only the --url-style quick tunnel. Named/persistent tunnels (cloudflared tunnel login + cloudflared tunnel route dns ...) are not orchestrated by OmniRoute. URLs are ephemeral and will change every restart.
  • Process supervision. The cloudflared PID and resolved URL are persisted to cloudflared-state.json so the dashboard can resume status across reloads.

Enable / disable via REST

The endpoint uses an {action: "enable" | "disable"} body, not separate start/stop paths. Management auth (admin session or admin API key) is required.

bash
# Enable
curl -X POST http://localhost:20128/api/tunnels/cloudflared \
  -H "Content-Type: application/json" \
  -H "Cookie: auth_token=..." \
  -d '{"action":"enable"}'

# Status
curl http://localhost:20128/api/tunnels/cloudflared \
  -H "Cookie: auth_token=..."

# Disable
curl -X POST http://localhost:20128/api/tunnels/cloudflared \
  -H "Content-Type: application/json" \
  -H "Cookie: auth_token=..." \
  -d '{"action":"disable"}'

Or via dashboard: Settings → Tunnels → Cloudflare.

Optional env vars

VariablePurpose
CLOUDFLARED_BINOverride the binary path. If set and valid, OmniRoute uses it instead of downloading.
CLOUDFLARED_PROTOCOL / TUNNEL_TRANSPORT_PROTOCOLTransport protocol (default http2).

2. ngrok

src/lib/ngrokTunnel.ts uses the @ngrok/ngrok SDK (in-process, no CLI subprocess). The native module is imported lazily on first start so platforms without prebuilt binaries do not break the app at boot.

Prerequisites

  1. Sign up at https://ngrok.com.
  2. Copy your authtoken from the ngrok dashboard.
  3. Provide it either via:
    • .env: NGROK_AUTHTOKEN=<token>, or
    • Dashboard: Settings → Tunnels → ngrok, or
    • REST body (one-shot): {"action":"enable","authToken":"<token>"}.

If neither is configured, status returns phase: "needs_auth".

Enable / disable via REST

bash
# Enable (uses NGROK_AUTHTOKEN from env)
curl -X POST http://localhost:20128/api/tunnels/ngrok \
  -H "Content-Type: application/json" \
  -H "Cookie: auth_token=..." \
  -d '{"action":"enable"}'

# Enable with inline token
curl -X POST http://localhost:20128/api/tunnels/ngrok \
  -H "Content-Type: application/json" \
  -H "Cookie: auth_token=..." \
  -d '{"action":"enable","authToken":"2abc..."}'

# Status
curl http://localhost:20128/api/tunnels/ngrok \
  -H "Cookie: auth_token=..."

# Disable
curl -X POST http://localhost:20128/api/tunnels/ngrok \
  -H "Content-Type: application/json" \
  -H "Cookie: auth_token=..." \
  -d '{"action":"disable"}'

The response includes the assigned publicUrl (e.g. https://abcd-1234.ngrok-free.app). Custom domains, regions, and policy rules must be configured in the ngrok dashboard — OmniRoute itself only forwards the local target URL to the SDK.

3. Tailscale Funnel

src/lib/tailscaleTunnel.ts orchestrates the system tailscale CLI to expose the local API port via Funnel (Tailscale's public-internet egress for serve). It supports the full lifecycle: install, login, daemon start, enable, disable.

The implementation invokes tailscale funnel --bg <port> (background mode). The public URL has the shape https://<machine>.<tailnet>.ts.net/.

Prerequisites

  1. Install Tailscale (or let OmniRoute do it — see install endpoint below).
  2. Sign in (tailscale login or via OmniRoute's login endpoint).
  3. Enable Funnel for your tailnet in the Tailscale admin console: https://login.tailscale.com/admin/settings/features.

On Linux and macOS the daemon (tailscaled) requires sudo to control. The POST endpoints accept an optional sudoPassword field which is forwarded to OmniRoute's MITM password cache (getCachedPassword / setCachedPassword) for the duration of the call. Windows uses the default service install at C:\Program Files\Tailscale\tailscale.exe.

REST endpoints

Tailscale has a richer surface than the other backends because installation, login, daemon, and tunnel are separate concerns.

EndpointMethodPurpose
/api/tunnels/tailscaleGETAggregated tunnel status (phase, tunnelUrl, apiUrl, etc.)
/api/tunnels/tailscale/checkGETLower-level check: installed? logged in? daemon running?
/api/tunnels/tailscale/installPOSTInstall Tailscale (SSE-streamed progress events) — Linux/macOS
/api/tunnels/tailscale/start-daemonPOSTStart tailscaled on Linux/macOS
/api/tunnels/tailscale/loginPOSTBegin login flow; returns authUrl to open in a browser
/api/tunnels/tailscale/enablePOSTStart the Funnel for the API port
/api/tunnels/tailscale/disablePOSTStop the Funnel

All Tailscale endpoints require management auth (see routeUtils.ts :: requireTailscaleAuth).

Example enable:

bash
curl -X POST http://localhost:20128/api/tunnels/tailscale/enable \
  -H "Content-Type: application/json" \
  -H "Cookie: auth_token=..." \
  -d '{"sudoPassword":"<linux-pwd>","port":20128}'

If Funnel is not enabled in the admin console, the response includes funnelNotEnabled: true plus an enableUrl to open in a browser.

Optional env vars

VariablePurpose
TAILSCALE_BINOverride the tailscale binary path

Endpoint summary

EndpointMethodBodyAuth
/api/tunnels/cloudflaredGETmanagement
/api/tunnels/cloudflaredPOST{action: "enable" | "disable"}management
/api/tunnels/ngrokGETmanagement
/api/tunnels/ngrokPOST{action, authToken?}management
/api/tunnels/tailscaleGETmanagement
/api/tunnels/tailscale/checkGETmanagement
/api/tunnels/tailscale/installPOST{sudoPassword?} (SSE)management
/api/tunnels/tailscale/start-daemonPOST{sudoPassword?}management
/api/tunnels/tailscale/loginPOST{hostname?}management
/api/tunnels/tailscale/enablePOST{sudoPassword?, hostname?, port?}management
/api/tunnels/tailscale/disablePOST{sudoPassword?}management

There is no central /api/settings/tunnels endpoint — each backend is independent.

OAuth callback considerations

When you expose OmniRoute through a tunnel, the dashboard and OAuth flows must build callback URLs against the public hostname, not localhost. Otherwise the OAuth provider redirects the user back to a URL its servers cannot reach, and the handshake fails.

Dashboard edits and settings saves do not require pinning the tunnel hostname in NEXT_PUBLIC_BASE_URL. The authenticated dashboard sends same-origin unsafe requests with a session-bound CSRF token, so ephemeral Cloudflare Quick Tunnel hosts can still be used for normal UI management after logging in.

Set:

bash
NEXT_PUBLIC_BASE_URL=https://<your-tunnel-host>

and restart OmniRoute before initiating OAuth. For ephemeral Cloudflare Quick Tunnels the URL changes after every restart, so prefer ngrok with a reserved domain or Tailscale Funnel for production OAuth use.

Health and monitoring

The dashboard surfaces tunnel state under Settings → Tunnels:

  • Active backend(s) and current phase (stopped, starting, running, needs_auth, error).
  • The current public URL and the derived API URL (<publicUrl>/v1).
  • The local target URL the tunnel is forwarding to.
  • Last error message, if any.

For programmatic monitoring poll the per-backend GET endpoints. Running more than one backend simultaneously is allowed; OmniRoute will track each independently.

Troubleshooting

"cloudflared binary not found"

OmniRoute attempts to auto-install on first use. If the install is blocked (restricted network, no GitHub access), download cloudflared manually from https://github.com/cloudflare/cloudflared/releases and set CLOUDFLARED_BIN=/path/to/cloudflared.

"ngrok: authtoken required"

phase: "needs_auth" means no authtoken was found. Set NGROK_AUTHTOKEN in .env, configure it via the dashboard, or pass authToken in the enable POST body.

"tailscale: funnel not enabled"

When the enable response includes funnelNotEnabled: true, Funnel is disabled for your tailnet. Open the returned enableUrl (or the admin console feature page) and toggle Funnel on.

Tunnel URL changes break OAuth

Use ngrok with a reserved domain or Tailscale Funnel (both stable per-node). Cloudflare Quick Tunnels are ephemeral by design and not recommended for long-lived OAuth callbacks.

Permission denied on Linux/macOS for Tailscale

tailscaled needs root. Provide sudoPassword to the relevant POST endpoint, or run the daemon yourself (sudo systemctl start tailscaled).

See also