Architecture
How funnel tunnels work under the hood
Overview
Funnel is a client-server system where the client maintains a persistent QUIC connection to the server. When a public request arrives at the server, it opens a new QUIC stream to the client, which forwards the request to the local service and sends the response back.
QUIC connection
┌────────┐ (persistent, muxed) ┌────────┐
│ Client │ ◄──────────────────────────────────► │ Server │
└───┬────┘ └───┬────┘
│ │
│ HTTP HTTP │
▼ ▼
┌────────┐ ┌──────────┐
│ Local │ │ Public │
│ Service│ │ Internet │
└────────┘ └──────────┘Why QUIC
Funnel uses QUIC instead of WebSocket or plain TCP for tunnel transport:
- Per-request streams: each HTTP request gets its own QUIC stream. No head-of-line blocking. A slow response on one request doesn't delay others.
- Built-in multiplexing: multiple concurrent requests share a single connection without framing overhead.
- Connection migration: QUIC connections survive network changes (e.g. switching from WiFi to cellular).
- 0-RTT reconnection: after the initial handshake, reconnections can send data immediately.
Connection flow
- Client connects to the server's QUIC port (default
4433) - Client opens an initial stream and sends a registration message with the tunnel ID and auth token
- Server validates the token, checks for ID conflicts, and registers the tunnel
- Server assigns a subdomain:
<tunnel_id>.<base_domain> - The QUIC connection stays open. The server opens new streams as requests arrive.
Request proxying
When a request arrives at <tunnel_id>.<base_domain>:
- Server matches the subdomain to a registered tunnel
- Server opens a new QUIC stream to the client
- Server serializes the HTTP request (method, path, headers, body) and sends it over the stream
- Client deserializes the request and forwards it to the local service via HTTP
- Client reads the local response, serializes it, and sends it back over the same stream
- Server reconstructs the HTTP response and sends it to the original caller
Each request uses its own stream, so the server can handle many concurrent requests to the same tunnel without coordination.
WebSocket forwarding
WebSocket upgrade requests are detected and handled specially. The tunnel transparently forwards WebSocket frames between the public client and the local service, maintaining the bidirectional connection.
Reconnection
If the QUIC connection drops, the client reconnects with exponential backoff. During reconnection, the tunnel subdomain is unavailable. Once reconnected, the same tunnel ID is re-registered (if still available).
Subdomain routing
The server uses a middleware layer that inspects the Host header of incoming requests. If the subdomain matches a registered tunnel, the request is routed to the tunnel proxy. Otherwise, it falls through to the API routes.
Proxy headers
The server adds standard proxy headers to forwarded requests:
X-Forwarded-For: client IP addressX-Forwarded-Host: original host headerX-Forwarded-Proto:httporhttps