File size: 12,743 Bytes
cd8bd0a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
---
title: "Tunnels Guide"
version: 3.8.2
lastUpdated: 2026-05-13
---

# Tunnels Guide

> **Source of truth:** `src/lib/{cloudflaredTunnel,ngrokTunnel,tailscaleTunnel}.ts`, `src/app/api/tunnels/`
> **Last updated:** 2026-05-13 β€” v3.8.0

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

| Backend                     | Persistence                                            | Cost              | Setup                                           |
| --------------------------- | ------------------------------------------------------ | ----------------- | ----------------------------------------------- |
| **Cloudflare Quick Tunnel** | Ephemeral (URL changes each restart)                   | Free              | Zero β€” auto-installs `cloudflared`              |
| **ngrok**                   | Stable while a paid plan or fixed domain is configured | Free tier + paid  | Requires ngrok account + authtoken              |
| **Tailscale Funnel**        | Stable per node within your tailnet                    | Free for personal | Requires 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

| Variable                                             | Purpose                                                                               |
| ---------------------------------------------------- | ------------------------------------------------------------------------------------- |
| `CLOUDFLARED_BIN`                                    | Override the binary path. If set and valid, OmniRoute uses it instead of downloading. |
| `CLOUDFLARED_PROTOCOL` / `TUNNEL_TRANSPORT_PROTOCOL` | Transport 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.

| Endpoint                              | Method | Purpose                                                         |
| ------------------------------------- | ------ | --------------------------------------------------------------- |
| `/api/tunnels/tailscale`              | `GET`  | Aggregated tunnel status (`phase`, `tunnelUrl`, `apiUrl`, etc.) |
| `/api/tunnels/tailscale/check`        | `GET`  | Lower-level check: installed? logged in? daemon running?        |
| `/api/tunnels/tailscale/install`      | `POST` | Install Tailscale (SSE-streamed progress events) β€” Linux/macOS  |
| `/api/tunnels/tailscale/start-daemon` | `POST` | Start `tailscaled` on Linux/macOS                               |
| `/api/tunnels/tailscale/login`        | `POST` | Begin login flow; returns `authUrl` to open in a browser        |
| `/api/tunnels/tailscale/enable`       | `POST` | Start the Funnel for the API port                               |
| `/api/tunnels/tailscale/disable`      | `POST` | Stop 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

| Variable        | Purpose                              |
| --------------- | ------------------------------------ |
| `TAILSCALE_BIN` | Override the `tailscale` binary path |

## Endpoint summary

| Endpoint                              | Method | Body                                | Auth       |
| ------------------------------------- | ------ | ----------------------------------- | ---------- |
| `/api/tunnels/cloudflared`            | `GET`  | β€”                                   | management |
| `/api/tunnels/cloudflared`            | `POST` | `{action: "enable" \| "disable"}`   | management |
| `/api/tunnels/ngrok`                  | `GET`  | β€”                                   | management |
| `/api/tunnels/ngrok`                  | `POST` | `{action, authToken?}`              | management |
| `/api/tunnels/tailscale`              | `GET`  | β€”                                   | management |
| `/api/tunnels/tailscale/check`        | `GET`  | β€”                                   | management |
| `/api/tunnels/tailscale/install`      | `POST` | `{sudoPassword?}` (SSE)             | management |
| `/api/tunnels/tailscale/start-daemon` | `POST` | `{sudoPassword?}`                   | management |
| `/api/tunnels/tailscale/login`        | `POST` | `{hostname?}`                       | management |
| `/api/tunnels/tailscale/enable`       | `POST` | `{sudoPassword?, hostname?, port?}` | management |
| `/api/tunnels/tailscale/disable`      | `POST` | `{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.

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

- [PROXY_GUIDE.md](./PROXY_GUIDE.md) β€” outbound proxy (1proxy, SOCKS5, HTTP) for
  egress traffic.
- [ENVIRONMENT.md](../reference/ENVIRONMENT.md) β€” full list of env vars including
  `NEXT_PUBLIC_BASE_URL`.
- [FLY_IO_DEPLOYMENT_GUIDE.md](./FLY_IO_DEPLOYMENT_GUIDE.md),
  [DOCKER_GUIDE.md](../guides/DOCKER_GUIDE.md) β€” alternatives to tunneling for stable
  public hosting.
- Source: `src/lib/{cloudflaredTunnel,ngrokTunnel,tailscaleTunnel}.ts`,
  `src/app/api/tunnels/`.