Skip to main content

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:
All subsystems share a single 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.
Each /_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:
  1. Extracts the Host header subdomain (e.g. myapp from myapp.tunnel.example.com).
  2. Looks up the subdomain in TunnelCore.http_routes.
  3. If found, allocates a conn_id, stores a oneshot sender in TunnelCore.pending_conns, and sends a NewConnection frame to the owning session.
  4. Waits for the yamux stream to arrive (delivered by the session’s yamux driver).
  5. Copies bytes bidirectionally between the incoming HTTP connection and the yamux stream.
Rate limiting (per-IP sliding window and per-tunnel token bucket) is enforced before step 2.

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-based tcp_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 at http://<server>:9090/metrics:

f) ACME Certificate Renewal

If acme_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 Ping every 30 seconds.
  • Server must respond with Pong within 10 seconds.
  • If no Pong arrives 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 requires futures::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 yamux Connection 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 share Arc<TunnelCore> which uses lock-free interior mutability:

TLS and Security

Certificate management

Two modes are supported:

Authentication

  1. The client sends an Auth frame with a bearer token.
  2. The server validates the token against its PostgreSQL database.
  3. A failed auth returns AuthError and closes the connection. Auth errors are fatal on the client — reconnect is not attempted.

--insecure flag

When --insecure is set, the client installs a custom ServerCertVerifier that accepts any certificate. This is intended for local development with self-signed certs only. Never use in production.

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 use tracing 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.