Overview
rustunnel is a self-hosted reverse tunnel built in Rust: it lets a client behind NAT or a firewall expose local TCP services to the internet via a central server that has a public IP address. It is architecturally similar to ngrok or Cloudflare Tunnel — see the ngrok-vs-rustunnel breakdown for a feature-level comparison — but designed to be simple, auditable, and self-hosted on any VPS. Key design choices:- WebSocket transport — works through any HTTP proxy or firewall that allows HTTPS.
- yamux multiplexing — a single data WebSocket carries streams for all proxied connections simultaneously.
- Two-connection model — control plane (JSON frames) and data plane (binary yamux frames) are separated.
- Tokio async runtime — all I/O is non-blocking; each session runs in a handful of lightweight tasks.
- TLS everywhere — all external connections are encrypted via rustls (ACME or static PEM).
High-Level Topology
Server Subsystems
The server is a single binary (rustunnel-server) composed of six concurrently running subsystems:
Arc<TunnelCore> routing table.
a) Control-Plane WebSocket Server
Handles two routes:/_control— One persistent WebSocket per client. Manages authentication, tunnel registration, and heartbeats./_data/<session_id>— One persistent WebSocket per client session. Carries raw yamux frames for all proxied connections belonging to that session.
/_control connection spawns a session task that runs the control loop for that client.
b) HTTP / HTTPS Edge Proxy
Listens on ports 80 and 443. For each incoming HTTP request:- Extracts the
Hostheader subdomain (e.g.myappfrommyapp.tunnel.example.com). - Looks up the subdomain in
TunnelCore.http_routes. - If found, allocates a
conn_id, stores a oneshot sender inTunnelCore.pending_conns, and sends aNewConnectionframe to the owning session. - Waits for the yamux stream to arrive (delivered by the session’s yamux driver).
- Copies bytes bidirectionally between the incoming HTTP connection and the yamux stream.
c) TCP Edge Proxy
For each registered TCP tunnel, the server allocates a port from a configured range and spawns a listener. The per-connection flow is identical to HTTP but uses the port-basedtcp_routes lookup instead of the subdomain-based http_routes lookup.
d) Dashboard
Serves the dashboard UI and a REST API for:- Listing active sessions and tunnels
- Creating and revoking API tokens (stored in PostgreSQL)
- Viewing a live request capture feed (via Server-Sent Events)
- Viewing audit logs
e) Prometheus Metrics
Exposes three gauges athttp://<server>:9090/metrics:
f) ACME Certificate Renewal
Ifacme_enabled = true in the server config, a background task periodically checks whether the TLS certificate needs renewal and triggers an ACME challenge. The certificate is hot-swapped into all TLS listeners without restart.
Client Architecture
The client is a single binary (rustunnel) with three main concurrent pieces:
State machines
The main loop maintains two buffering maps to handle the race between two asynchronous events that must be correlated:
When both halves arrive (in either order), a proxy task is spawned and both entries are removed.
Control Protocol
Control frames are JSON objects sent as binary WebSocket messages. They use serde’s{ "type": "...", ...fields } envelope.
Frame types
Handshake sequence
Heartbeat
- Client sends
Pingevery 30 seconds. - Server must respond with
Pongwithin 10 seconds. - If no
Pongarrives within the deadline, the client disconnects with"heartbeat timeout"and reconnects.
Data Plane — yamux over WebSocket
The data plane uses yamux (a stream multiplexer similar to HTTP/2 framing) over the data WebSocket. This allows a single WebSocket connection to carry streams for all proxied connections simultaneously.WsCompat adapter
Because yamux requiresfutures::io::{AsyncRead, AsyncWrite} but WebSocket is message-oriented, both server and client use a WsCompat<S> wrapper:
Mode assignment
yamux uses stream IDs to multiplex; “client” mode uses odd IDs, “server” mode uses even IDs:This assignment is intentional. yamux 0.13 uses lazy SYN: a new stream does not send a SYN frame until the first write. By making the server the yamux client (opener+writer), it forces the SYN+DATA immediately, unblocking the actual client’s
poll_next_inbound and avoiding a deadlock.16-byte conn_id prefix
When the server opens a yamux stream for a new proxied connection, it immediately writes the 16-byte raw UUID (conn_id.as_bytes()) into the stream before any proxy data. This allows the client’s yamux driver to correlate the stream with the NewConnection control frame that named the same conn_id.
Server-side duplex pipe
The server does not connect the yamuxConnection directly to the data WebSocket socket in the session task. Instead, it uses an in-process loopback pipe:
copy_bidirectional bridges pipe_client ↔ the data WebSocket transport. The yamux Connection reads/writes its internal framing through server_side. This decouples session lifecycle from WebSocket I/O.
Per-Connection Flow
Full end-to-end trace for a single HTTP request through the tunnel:Concurrency Model
rustunnel uses Tokio’s multi-threaded async runtime. All I/O is non-blocking. The key concurrency units are:Server-side tasks
Client-side tasks
Shared state (server)
All server tasks shareArc<TunnelCore> which uses lock-free interior mutability:
TLS and Security
Certificate management
Two modes are supported:Authentication
- The client sends an
Authframe with a bearer token. - The server validates the token against its PostgreSQL database.
- A failed auth returns
AuthErrorand closes the connection. Auth errors are fatal on the client — reconnect is not attempted.
--insecure flag
Rate limiting
Two independent rate limiters run on the server:
Both are enforced in the HTTP edge before the request is forwarded.
Metrics and Observability
Prometheus
The server exposes metrics at:9090/metrics in the standard text format:
Structured logging
Both server and client usetracing with configurable output. The server supports two formats:
Log level is controlled by
RUST_LOG (client) or the logging.level config key (server).
Audit log
The server writes append-only JSON audit events to a configurable file:- Token creation / revocation
- Session connect / disconnect
- Tunnel register / unregister
Component Dependency Graph
Crate Structure
See also
Self-Hosting
Production deployment of the rustunnel server on Ubuntu with systemd, TLS, and PostgreSQL.
Load Balancing & Health Checks
How the data plane dispatches across grouped backends with health probes.
P2P Tunnels
Direct peer-to-peer connections, NAT classification, hole punching, and relay fallback.
Client Guide
Every CLI flag and config option, including reconnect and region-selection behavior.

