# Contributing Source: https://rustunnel.com/docs/contribute How to contribute to rustunnel — an open-source, self-hosted tunnel server written in Rust. Contributions are welcome! rustunnel is open-source under the [AGPLv3 license](https://github.com/joaoh82/rustunnel/blob/main/LICENSE). Whether it's a bug fix, a new feature, or improved documentation, we appreciate your help. ## Before you start For larger changes or new features, **open an issue first** to discuss the approach. This avoids duplicate effort and ensures the change is aligned with the project direction. For small fixes (typos, docs, minor bugs) you can go straight to a pull request. *** ## Getting started Fork the repository on GitHub, then clone your fork: ```bash theme={null} git clone https://github.com//rustunnel.git cd rustunnel ``` You need: | Tool | Version | Notes | | -------------- | ------- | ------------------------------------------ | | Rust toolchain | 1.76+ | Install via [rustup](https://rustup.rs) | | `pkg-config` | any | Required by `reqwest` (TLS) | | `libssl-dev` | any | On Debian/Ubuntu: `apt install libssl-dev` | | Node.js + npm | 18+ | Only needed to rebuild the dashboard UI | | Docker | any | Required to run the test database | Run this once after cloning to activate the pre-push quality gate: ```bash theme={null} make install-hooks ``` Every `git push` will automatically run `cargo fmt` checks and `cargo clippy` before the push is allowed. Branch off `main` with a descriptive name: ```bash theme={null} git checkout -b fix/heartbeat-timeout # or git checkout -b feat/tcp-region-selection ``` *** ## Local development ### Build ```bash theme={null} # Debug build (fast compile) cargo build --workspace # Or via Make make build ``` ### Run the server locally Generate a self-signed certificate for local testing: ```bash theme={null} mkdir -p /tmp/rustunnel-dev openssl req -x509 -newkey rsa:2048 \ -keyout /tmp/rustunnel-dev/key.pem \ -out /tmp/rustunnel-dev/cert.pem \ -days 365 -nodes \ -subj "/CN=localhost" ``` Start the server with the checked-in local config: ```bash theme={null} cargo run -p rustunnel-server -- --config deploy/local/server.toml ``` ### Run the client locally With the server running, expose a local service: ```bash theme={null} # HTTP tunnel cargo run -p rustunnel-client -- http 3000 \ --server localhost:4040 \ --token dev-secret-change-me \ --insecure # TCP tunnel cargo run -p rustunnel-client -- tcp 5432 \ --server localhost:4040 \ --token dev-secret-change-me \ --insecure ``` `--insecure` skips TLS verification — required when using the self-signed cert. Never use it against a production server. *** ## Running tests The integration test suite spins up a real server on random ports and exercises auth, HTTP tunnels, TCP tunnels, and reconnection logic. It requires a running PostgreSQL instance. ```bash theme={null} # Start the local PostgreSQL container (persists across reboots) make db-start # Full suite: unit + integration make test # With output visible TEST_DATABASE_URL=postgres://rustunnel:test@localhost:5432/rustunnel_test \ cargo test --workspace -- --nocapture # Run a single test TEST_DATABASE_URL=postgres://rustunnel:test@localhost:5432/rustunnel_test \ cargo test -p # Stop PostgreSQL when done make db-stop ``` *** ## Code quality Before opening a PR, make sure these pass: ```bash theme={null} # Format check cargo fmt --all -- --check # Lint (zero warnings policy) cargo clippy --workspace --all-targets -- -D warnings # Both at once make check ``` CI runs the same checks on every push and pull request. *** ## Opening a pull request 1. Push your branch to your fork and open a PR against `main`. 2. Write a clear description of **what changed and why**. 3. Make sure CI is green — format, Clippy, and all tests. 4. A maintainer will review and merge. ### PR guidelines * **Keep PRs focused** — one logical change per PR. Avoid bundling unrelated fixes. * **Add or update tests** for any new behaviour. The integration test suite in `tests/integration/` is the right place for end-to-end scenarios. * **Follow existing code style** — `cargo fmt` is enforced by CI. * **Update documentation** if you change user-facing behaviour (flags, config keys, API endpoints). *** ## Project structure | Path | Purpose | | ---------------------------- | ---------------------------------------------------------- | | `crates/rustunnel-protocol/` | Shared `ControlFrame` types and serialization | | `crates/rustunnel-server/` | Server: control plane, HTTP/TCP edges, dashboard, TLS | | `crates/rustunnel-client/` | CLI client with auto-reconnect and local port forwarding | | `crates/rustunnel-mcp/` | MCP server for AI agent integration | | `dashboard-ui/` | Next.js dashboard frontend | | `deploy/` | Dockerfiles, compose files, systemd unit, config templates | | `tests/integration/` | End-to-end tests (real server + real PostgreSQL) | | `docs/` | Public documentation source | *** ## Useful make targets | Target | Description | | -------------------- | ------------------------------------------ | | `make build` | Debug build of all workspace crates | | `make release` | Optimized release build | | `make check` | `cargo fmt` check + `cargo clippy` | | `make test` | Full test suite (requires `make db-start`) | | `make db-start` | Start the local PostgreSQL container | | `make db-stop` | Stop the local PostgreSQL container | | `make install-hooks` | Install the pre-push git hook | | `make docker-build` | Build the server Docker image | *** ## License By contributing you agree that your work will be licensed under the [GNU AGPLv3 License](https://github.com/joaoh82/rustunnel/blob/main/LICENSE). *** ## Contact **João Henrique Machado Silva** * GitHub: [@joaoh82](https://github.com/joaoh82) * Issues & feature requests: [GitHub Issues](https://github.com/joaoh82/rustunnel/issues) *** ## Related pages Understand the control protocol, yamux data plane, and crate structure before diving into the code. Install the client and open your first tunnel in three steps. Every command, flag, and config option for the rustunnel CLI client. Run your own rustunnel server on a VPS with systemd and Let's Encrypt. # Using rustunnel from an AI agent or LLM harness Source: https://rustunnel.com/docs/guides/agent-integration Copy-paste MCP config for Claude Code, Claude Desktop, Codex, Cursor, Windsurf, Cline, and any custom MCP client — plus a one-command installer and a programmatic JSON-RPC example. Let your agent create, list, and close tunnels in plain language. rustunnel ships an **MCP server** (`rustunnel-mcp`) that exposes tunnel management as tools any [Model Context Protocol](https://modelcontextprotocol.io) client can call. Once it's wired into your harness, you can just say: > "Create an HTTP tunnel to my service on port 3000." > "Expose port 5432 over TCP so I can reach my database." > "Spin up a load-balanced pool across ports 3000 and 3001 with a health check." This guide is the one-stop reference for adding rustunnel to **Claude Code, Claude Desktop, Codex, Cursor, Windsurf, Cline, or any custom MCP agent**. For the deeper server and tool reference, see the [MCP server guide](/docs/guides/mcp-server). *** ## 1. Prerequisites You need two binaries on your `PATH`: * `rustunnel` — the client (the MCP server spawns it to open tunnels) * `rustunnel-mcp` — the MCP server itself ```bash theme={null} # Homebrew (macOS / Linux) brew tap joaoh82/rustunnel && brew install rustunnel # Or build from source git clone https://github.com/joaoh82/rustunnel.git && cd rustunnel make release # builds rustunnel, rustunnel-server, rustunnel-mcp sudo install -m755 target/release/rustunnel /usr/local/bin/rustunnel sudo install -m755 target/release/rustunnel-mcp /usr/local/bin/rustunnel-mcp ``` ## 2. Get an API token (one-time) This is the **one required user step**. Everything else is config you can copy-paste. rustunnel requires an API token. **You only need to do this once.** * **Hosted (rustunnel.com):** sign up free at [rustunnel.com](https://rustunnel.com), then go to **Dashboard → API Keys** and create a key. No waiting list. * **Self-hosted:** run `rustunnel token create --name agent --server :4040 --admin_token `, or create one from your dashboard. Set it as `RUSTUNNEL_TOKEN` in the MCP config below. When that variable is set, the agent never has to pass the token on individual calls. ## 3. The two server settings | Setting | Hosted example | Self-hosted example | Local dev | | ---------------------------- | ------------------------------------ | --------------------------------- | ----------------------- | | `--server` (control plane) | `eu.edge.rustunnel.com:4040` | `tunnel.example.com:4040` | `localhost:4040` | | `--api` (dashboard REST API) | `https://eu.edge.rustunnel.com:8443` | `https://tunnel.example.com:8443` | `http://localhost:4041` | Add `--insecure` only for local dev with self-signed certs. *** ## 4. Quick install (any harness) The [installer](https://github.com/joaoh82/rustunnel/blob/main/integrations/install.sh) prompts for your token and writes the right config for the harness you pick: ```bash theme={null} curl -fsSL https://raw.githubusercontent.com/joaoh82/rustunnel/main/integrations/install.sh | bash # or, from a clone: ./integrations/install.sh ``` It supports `claude-code`, `claude-desktop`, `codex`, `cursor`, `windsurf`, `cline`, and `generic` (prints a snippet). Run `./integrations/install.sh --help` for non-interactive flags. *** ## 5. Per-harness configuration Ready-to-edit templates live in the [`integrations/`](https://github.com/joaoh82/rustunnel/tree/main/integrations) folder. In every template, replace the placeholder token with your own, and swap the `--server` / `--api` values if you self-host. ### Claude Code **Plugin (recommended — zero config):** ```text theme={null} /plugin install rustunnel /plugin configure rustunnel # prompts for server, API URL, token ``` See the [Claude Code plugin guide](/docs/guides/claude-plugin). **Manual `.mcp.json`** in your project root — see the [template](https://github.com/joaoh82/rustunnel/blob/main/integrations/claude-code/.mcp.json): ```json theme={null} { "mcpServers": { "rustunnel": { "command": "rustunnel-mcp", "args": ["--server", "eu.edge.rustunnel.com:4040", "--api", "https://eu.edge.rustunnel.com:8443"], "env": { "RUSTUNNEL_TOKEN": "" } } } } ``` ### Claude Desktop Edit `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows). Use the same `mcpServers` shape as above — [template](https://github.com/joaoh82/rustunnel/blob/main/integrations/claude-desktop/config.json). ### Codex (OpenAI) Codex reads MCP servers from `~/.codex/config.toml` — [template](https://github.com/joaoh82/rustunnel/blob/main/integrations/codex/config.toml): ```toml theme={null} [mcp_servers.rustunnel] command = "rustunnel-mcp" args = ["--server", "eu.edge.rustunnel.com:4040", "--api", "https://eu.edge.rustunnel.com:8443"] env = { RUSTUNNEL_TOKEN = "" } ``` Codex also reads an `AGENTS.md` at the repo root for guidance. ### Cursor Project: `.cursor/mcp.json`. Global: `~/.cursor/mcp.json`. Same `mcpServers` shape — [template](https://github.com/joaoh82/rustunnel/blob/main/integrations/cursor/mcp.json). ### Windsurf `~/.codeium/windsurf/mcp_config.json`. Same shape — [template](https://github.com/joaoh82/rustunnel/blob/main/integrations/windsurf/mcp_config.json). ### Cline (VS Code) `cline_mcp_settings.json` (via the Cline MCP settings UI). Same shape — [template](https://github.com/joaoh82/rustunnel/blob/main/integrations/cline/cline_mcp_settings.json). ### Generic / custom agent Any MCP client uses the same `command` + `args` + `env`. Use the [generic template](https://github.com/joaoh82/rustunnel/blob/main/integrations/generic/mcp.json). For a programmatic agent, spawn `rustunnel-mcp` and speak newline-delimited JSON-RPC 2.0 over stdin/stdout: ```python theme={null} import subprocess, json, os proc = subprocess.Popen( ["rustunnel-mcp", "--server", "eu.edge.rustunnel.com:4040", "--api", "https://eu.edge.rustunnel.com:8443"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, env={**os.environ, "RUSTUNNEL_TOKEN": ""}, ) def call(method, params=None, _id=[0]): _id[0] += 1 msg = {"jsonrpc": "2.0", "id": _id[0], "method": method} if params: msg["params"] = params proc.stdin.write(json.dumps(msg).encode() + b"\n"); proc.stdin.flush() return json.loads(proc.stdout.readline()) call("initialize", {"protocolVersion": "2024-11-05"}) print(call("tools/call", {"name": "create_tunnel", "arguments": {"local_port": 3000, "protocol": "http"}})) ``` *** ## 6. Available tools Every authenticated tool accepts an optional `token` argument. Omit it when `RUSTUNNEL_TOKEN` is set in the MCP config. | Tool | Purpose | | --------------------- | ------------------------------------------------------------------------------ | | `create_tunnel` | Open a tunnel and get a public URL. HTTP/TCP/UDP, P2P, and load-balanced pools | | `close_tunnel` | Close a tunnel by ID | | `list_tunnels` | List active tunnels | | `get_tunnel_history` | Past tunnel activity (audit) | | `list_regions` | Available server regions (no auth) | | `get_connection_info` | CLI command / config for manual runs (cloud sandboxes) | ### `create_tunnel` arguments | Argument | When | Description | | --------------------- | -------------- | ----------------------------------------------------------------------------------------------- | | `local_port` | always | Local port the service listens on | | `protocol` | always | `http`, `tcp`, `udp`, or `p2p` | | `token` | optional | Falls back to `RUSTUNNEL_TOKEN` | | `subdomain` | http | Custom subdomain | | `region` | optional | `eu` / `us` / `ap`; omit to auto-select | | `local_host` | optional | Forward to another host (default `localhost`) | | `secret` | p2p | Shared secret (publisher and subscriber match) | | `peer_name` | p2p publish | Publish under this name | | `peer_target` | p2p connect | Connect to this published name | | `group` + `group_key` | load balancing | Join a pool (http/tcp); members share the key | | `health_check` | optional | `{ type: tcp\|http, path, interval_secs, timeout_secs, max_failed, expect_2xx, alert_webhook }` | See the [MCP server guide](/docs/guides/mcp-server) for the full tool reference, [load balancing](/docs/reference/load-balancing) for pools and health checks, and [P2P tunnels](/docs/reference/p2p-tunnels) for peer-to-peer details. *** ## 7. Security notes * Tokens travel to the server over HTTPS. Use `--insecure` only in local dev. * The `RUSTUNNEL_TOKEN` env var keeps the token out of tool-call payloads and chat history — prefer it over passing `token` inline. * Child `rustunnel` processes spawned by `create_tunnel` are killed when the MCP server exits; temp config files for load-balanced tunnels are cleaned up too. * Protect any config file you save: `chmod 600 ~/.rustunnel/config.yml`. *** ## Related pages Full reference for rustunnel-mcp: every tool, parameter, and response shape. Skip the manual config — install the plugin with one command in Claude Code. How load-balanced pools and health checks work for tunnels your agent creates. Direct peer-to-peer tunnels with shared secrets and named peers. Per-harness setup pages — Claude Code, Cursor, Windsurf, generic MCP — with agent-ready prompts. Copy-paste recipes and an FAQ for AI agents, served as plain markdown. # rustunnel Architecture — control plane, data plane, and yamux multiplexing Source: https://rustunnel.com/docs/guides/architecture Internal architecture of rustunnel: control protocol (JSON over WebSocket), data plane (yamux multiplexed binary), concurrency model (Tokio), and the crate structure of the open-source tunnel server. ## 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](https://rustunnel.com/blog/ngrok-alternative) for a feature-level comparison — but designed to be simple, auditable, and [self-hosted on any VPS](/docs/guides/self-hosting). 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 ```text theme={null} INTERNET │ ┌─────────────▼──────────────┐ │ rustunnel-server │ │ │ │ ┌──────────┐ ┌──────────┐ │ Browser ───►│ │HTTP Edge │ │TCP Edge │ │ / Client │ │:80/:443 │ │:xxxxx │ │ │ └────┬─────┘ └────┬─────┘ │ │ │ │ │ │ ┌────▼─────────────▼─────┐ │ │ │ TunnelCore │ │ │ │ (routing table + │ │ │ │ pending conn map) │ │ │ └────────────┬───────────┘ │ │ │ │ │ ┌────────────▼───────────┐ │ │ │ Control-Plane WS │ │ │ │ :4040 /_control │ │ │ │ :4040 /_data/ │ │ │ └────────────────────────┘ │ └──────────────┬──────────────┘ │ WebSocket (TLS) ┌────────▼──────────┐ │ rustunnel client │ │ │ │ ┌──────────────┐ │ │ │ Control loop │ │ │ └──────────────┘ │ │ ┌──────────────┐ │ │ │ yamux driver │ │ │ └──────────────┘ │ │ ┌──────────────┐ │ │ │ Proxy tasks │ │ │ └──────────────┘ │ └────────┬──────────┘ │ TCP ┌──────────────▼───────────────┐ │ Local Service │ │ (web server, SSH, DB, etc.) │ └───────────────────────────────┘ ``` *** ## Server Subsystems The server is a single binary (`rustunnel-server`) composed of six concurrently running subsystems: ```text theme={null} rustunnel-server ├── a) Control-plane WebSocket server (:4040) │ /_control — JSON control frames │ /_data/ — binary yamux frames ├── b) HTTP + HTTPS edge proxy (:80, :443) ├── c) TCP edge proxy (dynamic ports) ├── d) Dashboard REST API + SPA (:4041) ├── e) Prometheus metrics endpoint (:9090) └── f) ACME certificate renewal task (background) ``` All subsystems share a single `Arc` routing table. ### a) Control-Plane WebSocket Server Handles two routes: * **`/_control`** — One persistent WebSocket per client. Manages authentication, tunnel registration, and heartbeats. * **`/_data/`** — 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://:9090/metrics`: | Metric | Description | | ------------------------------- | ----------------------------- | | `rustunnel_active_sessions` | Number of connected clients | | `rustunnel_active_tunnels_http` | Number of active HTTP tunnels | | `rustunnel_active_tunnels_tcp` | Number of active TCP tunnels | ### 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: ```text theme={null} rustunnel process │ ├── main_loop task (async) │ select! { │ Ctrl-C → clean shutdown │ ping_interval → send Ping frame │ stream_rx.recv → match stream with pending NewConnection │ ctrl_ws.next → handle NewConnection / Pong / etc. │ } │ ├── drive_client_mux task (spawned) │ loop { │ poll_next_inbound(yamux conn) │ → read 16-byte conn_id prefix │ → send (conn_id, stream) to stream_rx channel │ } │ └── proxy tasks (one per connection, spawned) tokio::io::copy_bidirectional( yamux_stream ↔ TcpStream to local service ) ``` ### State machines The main loop maintains two buffering maps to handle the race between two asynchronous events that must be correlated: | Map | Key | Value | Purpose | | ----------------- | --------- | ------------- | ----------------------------------------------- | | `pending_conns` | `conn_id` | `local_addr` | `NewConnection` arrived before the yamux stream | | `pending_streams` | `conn_id` | `YamuxStream` | yamux stream arrived before `NewConnection` | 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 ```text theme={null} Client → Server Server → Client ───────────────────────────────── ───────────────────────────────── Auth AuthOk token: string session_id: uuid client_version: string server_version: string AuthError message: string RegisterTunnel TunnelRegistered request_id: string request_id: string protocol: http|tcp tunnel_id: uuid subdomain?: string public_url: string local_addr: string assigned_port?: u16 TunnelError request_id: string message: string Ping NewConnection timestamp: u64 (ms) conn_id: uuid client_addr: string Pong protocol: http|tcp timestamp: u64 (ms) Ping / Pong (same as client→server) ``` ### Handshake sequence ```text theme={null} Client Server │ │ │──── WebSocket upgrade ────────────►│ wss:///_control │ │ │──── Auth ─────────────────────────►│ │ token, client_version │ │ │ validate token against DB │◄─── AuthOk ───────────────────────│ │ session_id, server_version │ │ │ │──── RegisterTunnel ───────────────►│ (one per tunnel) │ request_id, protocol, … │ │ │ allocate subdomain/port │◄─── TunnelRegistered ─────────────│ │ public_url, assigned_port │ │ │ │──── WebSocket upgrade ────────────►│ wss:///_data/ │ │ (data plane, runs in parallel) │ │ │◄══════════════ normal operation ══════════════►│ │ │ │ every 30 s: │ │──── Ping ─────────────────────────►│ │◄─── Pong ─────────────────────────│ │ │ │ on incoming external connection: │ │◄─── NewConnection ────────────────│ │ conn_id, client_addr │ │ │ │ (yamux stream arrives separately │ │ on the data WebSocket) │ ``` ### 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` wrapper: ```text theme={null} WebSocket (message-oriented) │ WsCompat ┌────────────────────────────────────────┐ │ Read: dequeue binary WS frames, │ │ present as a byte stream │ │ Write: wrap byte slices in binary │ │ WS frames and send │ └────────────────────────────────────────┘ │ yamux Connection (stream-oriented) ``` ### Mode assignment yamux uses stream IDs to multiplex; "client" mode uses odd IDs, "server" mode uses even IDs: | Side | yamux Mode | Role | | ------ | -------------- | ----------------------------------------------- | | Server | `Mode::Client` | Opens streams, writes first (forces SYN frame) | | Client | `Mode::Server` | Accepts inbound streams via `poll_next_inbound` | 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`. ```text theme={null} Server yamux driver Client yamux driver │ │ │ open stream (yamux SYN) │ │──────────────────────────────────►│ │ │ │ write conn_id (16 bytes) │ │──────────────────────────────────►│ │ │ read exactly 16 bytes │ │ conn_id = Uuid::from_bytes(id_bytes) │ │ send (conn_id, stream) → main loop │ │ ├──── proxy data flows both ways ───┤ ``` ### 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**: ```text theme={null} Session task Yamux driver task │ │ │ tokio::io::duplex(64 KiB) │ │ server_side ↔ client_side │ │ │ │ yamux::Connection(server_side) │ ◄── drives IO │ │ │ pipe_client (taken by bridge) │ │ │ │ │ ┌───────▼────────────────────┐ │ │ │ copy_bidirectional │ │ │ │ pipe_client ↔ data WS │ │ │ └────────────────────────────┘ │ ``` `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: ```text theme={null} Browser Server (HTTP edge) TunnelCore Session task Yamux driver Client (main loop) Client (proxy task) Local service │ │ │ │ │ │ │ │ │──── GET /api ──────►│ │ │ │ │ │ │ │ Host: myapp… │ │ │ │ │ │ │ │ │──── lookup ───────►│ │ │ │ │ │ │ │◄─── TunnelInfo ────│ │ │ │ │ │ │ │──── alloc conn_id ─►│ │ │ │ │ │ │ │──── NewConnection ──────────────►│ │ │ │ │ │ │ │ │──── open stream ──────────────►│ │ │ │ │ │ │ │──── write conn_id (16 bytes) ──►│ │ │ │ │ │ │ │ │ read conn_id │ │ │ │ │ │ │ │ │ send (conn_id, stream) ────────►│ │ │ │ │ │ │ │ │──── spawn proxy task ──────────────►│ │ │ │◄══ yamux stream ═══════════════════════════════════════════════════════════════════════════════════════│ │ │◄──── 200 OK ────────│◄══════════════════════ bytes flow ═══════════════════════════════════════════════════════════════════►│ │ ``` *** ## Concurrency Model rustunnel uses **Tokio**'s multi-threaded async runtime. All I/O is non-blocking. The key concurrency units are: ### Server-side tasks | Task | Lifetime | Purpose | | ---------------------- | ---------------------- | -------------------------------------------------- | | Control-plane listener | Server lifetime | Accepts new `/_control` WebSocket connections | | Session task | Per client session | Runs the control loop for one client | | Yamux driver task | Per client session | Drives yamux IO, opens streams for new connections | | Data WebSocket bridge | Per client session | `copy_bidirectional(pipe_client ↔ data_ws)` | | Edge proxy connection | Per proxied connection | `copy_bidirectional(edge_socket ↔ yamux_stream)` | | HTTP edge | Server lifetime | Accepts HTTP/HTTPS connections | | TCP edge listener | Per TCP tunnel | Accepts TCP connections | | Dashboard | Server lifetime | Serves REST API and SPA | | Metrics | Server lifetime | Serves Prometheus endpoint | | ACME renewal | Server lifetime | Background cert renewal | ### Client-side tasks | Task | Lifetime | Purpose | | ------------ | ---------------------- | ---------------------------------------------- | | Main loop | Session lifetime | Control protocol, signal handling, heartbeat | | Yamux driver | Session lifetime | Accepts inbound yamux streams, reads conn\_id | | Proxy task | Per proxied connection | `copy_bidirectional(yamux_stream ↔ local_tcp)` | ### Shared state (server) All server tasks share `Arc` which uses lock-free interior mutability: | Field | Type | Purpose | | --------------------- | --------------------------------------------- | ---------------------------- | | `http_routes` | `DashMap` | subdomain → tunnel | | `tcp_routes` | `DashMap` | port → tunnel | | `sessions` | `DashMap` | session\_id → session | | `pending_conns` | `DashMap>` | conn\_id → stream rendezvous | | `available_tcp_ports` | `Mutex>` | port pool | *** ## TLS and Security ### Certificate management Two modes are supported: | Mode | Config | Description | | ---------- | ------------------------ | ------------------------------------------------------------------------------------------------------ | | Static PEM | `cert_path` + `key_path` | Pre-existing certificate. Loaded at startup. No auto-renewal. | | ACME | `acme_enabled = true` | Automatic certificate issuance and renewal via Let's Encrypt. Certificate hot-swapped without restart. | ### 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: | Limiter | Scope | Algorithm | | ------------------- | ------------- | ------------------------------------ | | IP rate limiter | Per source IP | Sliding window (requests per second) | | Tunnel rate limiter | Per tunnel | Token bucket | 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: ```text theme={null} # HELP rustunnel_active_sessions Number of active client sessions # TYPE rustunnel_active_sessions gauge rustunnel_active_sessions 3 # HELP rustunnel_active_tunnels_http Number of active HTTP tunnels # TYPE rustunnel_active_tunnels_http gauge rustunnel_active_tunnels_http 5 # HELP rustunnel_active_tunnels_tcp Number of active TCP tunnels # TYPE rustunnel_active_tunnels_tcp gauge rustunnel_active_tunnels_tcp 2 ``` ### Structured logging Both server and client use `tracing` with configurable output. The server supports two formats: | Format | Config | Use case | | ------ | ----------------- | -------------------------------------------- | | pretty | `format = "text"` | Human-readable terminal output | | JSON | `format = "json"` | Machine-readable log aggregation (e.g. Loki) | 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 ```text theme={null} rustunnel-client ├── rustunnel-protocol (control frame types) ├── tokio (async runtime) ├── tokio-tungstenite (WebSocket client) ├── yamux 0.13 (stream multiplexer) ├── rustls (TLS — ring provider) ├── clap (CLI) ├── serde-json (frame serialization) ├── indicatif (spinner / progress bar) ├── console (terminal colors) └── tracing (structured logging) rustunnel-server ├── rustunnel-protocol (control frame types) ├── tokio (async runtime, multi-thread) ├── axum (HTTP edge + dashboard API) ├── tokio-tungstenite (WebSocket server) ├── yamux 0.13 (stream multiplexer) ├── rustls (TLS — ring provider + ACME) ├── sqlx + PostgreSQL (token storage + tunnel log) ├── sqlx + SQLite (captured request storage) ├── dashmap (lock-free routing table) ├── parking_lot (port pool mutex) ├── clap (CLI) ├── serde-json (frame + REST serialization) └── tracing (structured logging) rustunnel-mcp (MCP server) ├── rustunnel-protocol (control frame types) ├── tokio (async runtime) ├── reqwest (REST API client) ├── serde-json (JSON-RPC serialization) └── clap (CLI) rustunnel-protocol (shared library crate) ├── serde + serde-json (frame serialization) └── uuid (conn_id / session_id / tunnel_id) ``` *** ## Crate Structure ```text theme={null} rustunnel/ ├── Cargo.toml (workspace) ├── Makefile ├── deploy/ │ ├── Dockerfile │ ├── docker-compose.yml (production) │ ├── docker-compose.local.yml (local dev) │ ├── server.toml (production config template) │ └── rustunnel.service (systemd unit) ├── dashboard-ui/ (Next.js dashboard UI) ├── docs/ │ ├── client-guide.md │ ├── api-reference.md │ ├── architecture.md │ ├── database.md │ ├── docker-deployment.md │ └── mcp-server.md ├── tests/ │ ├── common/mod.rs (test helpers) │ └── integration/ │ ├── http_tunnel.rs │ ├── tcp_tunnel.rs │ └── reconnect.rs └── crates/ ├── rustunnel-protocol/ │ └── src/ │ ├── frame.rs (ControlFrame enum, encode/decode) │ └── error.rs ├── rustunnel-client/ │ └── src/ │ ├── main.rs │ ├── config.rs │ ├── control.rs │ ├── reconnect.rs │ ├── proxy.rs │ ├── regions.rs │ ├── display.rs │ └── error.rs ├── rustunnel-mcp/ │ └── src/ │ └── main.rs (JSON-RPC over stdio, tool dispatch) └── rustunnel-server/ └── src/ ├── main.rs ├── config.rs ├── control/ │ ├── server.rs │ ├── session.rs │ └── mux.rs (WsCompat, MuxSession) ├── core/ │ ├── router.rs (TunnelCore) │ ├── tunnel.rs │ ├── limiter.rs │ └── ip_limiter.rs ├── edge/ │ ├── http.rs │ ├── tcp.rs │ └── capture.rs ├── dashboard/ ├── db/ ├── migrations/ └── tls/ ├── mod.rs (CertManager) └── acme.rs ``` *** ## See also Production deployment of the rustunnel server on Ubuntu with systemd, TLS, and PostgreSQL. How the data plane dispatches across grouped backends with health probes. Direct peer-to-peer connections, NAT classification, hole punching, and relay fallback. Every CLI flag and config option, including reconnect and region-selection behavior. # Claude Code Tunnel Plugin — one-command MCP install for rustunnel Source: https://rustunnel.com/docs/guides/claude-plugin Install the rustunnel Claude Code tunnel plugin in one command. Claude Code gets six tunnel-management tools via MCP, your token is stored securely, and tunnels are cleaned up automatically on session exit. The rustunnel **Claude Code tunnel plugin** gives Claude Code full tunnel management without manual MCP configuration. Install the plugin, enter your token, and start asking Claude to expose local services. The plugin is a thin wrapper over the same [MCP server](/docs/guides/mcp-server) any other MCP-compatible agent (Cursor, Windsurf, Claude Desktop, OpenClaw) can use. ## How it works The plugin bundles a skill definition (agent instructions) and an MCP server configuration. When you enable it, Claude Code: 1. Prompts for your server address and API token (stored securely, entered once) 2. Starts the `rustunnel-mcp` MCP server in the background 3. Makes 6 tunnel management tools available to the agent *** ## Installation ### From the plugin marketplace ```text theme={null} /plugin install rustunnel ``` ### Manual install from source Clone the repository and point Claude Code at the plugin directory: ```bash theme={null} git clone https://github.com/joaoh82/rustunnel.git claude --plugin-dir rustunnel/plugins/claude-code/ ``` To load it permanently, add it to your Claude Code settings: ```bash theme={null} claude plugin install --path rustunnel/plugins/claude-code/ ``` During development, use `/reload-plugins` inside Claude Code to pick up file changes without restarting. *** ## Setup When you enable the plugin, you'll be prompted for three values: | Prompt | Hosted example | Self-hosted example | | -------------- | ------------------------------------ | ----------------------- | | Server address | `eu.edge.rustunnel.com:4040` | `localhost:4040` | | API URL | `https://eu.edge.rustunnel.com:8443` | `http://localhost:4041` | | API token | `rt_live_abc123...` | your admin token | Get a token at [rustunnel.com](https://rustunnel.com) (hosted) or generate one with `openssl rand -hex 32` for self-hosted servers. These values are persisted by Claude Code — you won't be asked again until you reconfigure. ### Reconfiguring To change your configuration after installation: ```text theme={null} /plugin configure rustunnel ``` Run `/reload-plugins` after reconfiguring for changes to take effect. The MCP server will not start until all three values are set. If you see a "Missing required user configuration value" error, run `/plugin configure rustunnel` to provide the missing values. *** ## Prerequisites The `rustunnel` CLI must be installed on your machine. The MCP server spawns it as a subprocess when `create_tunnel` is called. ```bash theme={null} # Homebrew (macOS/Linux) brew tap joaoh82/rustunnel brew install rustunnel # Or download from GitHub releases # https://github.com/joaoh82/rustunnel/releases/latest ``` *** ## Usage Once installed, just ask Claude: > "Expose my local server on port 3000." > "Open an HTTP tunnel to port 8080 with subdomain myapp." > "Create a TCP tunnel to my PostgreSQL on port 5432." > "List my active tunnels and close the one on port 3000." *** ## Available tools | Tool | Auth | Description | | --------------------- | ---- | --------------------------------------------------- | | `create_tunnel` | yes | Open a tunnel and get a public URL | | `close_tunnel` | yes | Close a tunnel by UUID | | `list_tunnels` | yes | List all active tunnels | | `list_regions` | no | Show available server regions | | `get_tunnel_history` | yes | View past tunnel activity | | `get_connection_info` | yes | Get the CLI command string (cloud sandbox fallback) | See the [MCP Server guide](/docs/guides/mcp-server) for full parameter tables and example responses. *** ## Plugin vs manual MCP setup | Aspect | Plugin | Manual `.mcp.json` | | -------------- | --------------------------- | ------------------------------ | | Setup | `/plugin install rustunnel` | Edit config by hand | | Token storage | Secure, entered once | Hardcoded or passed every call | | Updates | `/plugin update rustunnel` | Manual | | Agent guidance | Built-in skill | Must add instructions yourself | *** ## Self-hosted servers The plugin works with self-hosted rustunnel instances. When prompted for the server address and API URL, enter your own server's values instead of the hosted defaults. See the [Self-Hosting guide](/docs/guides/self-hosting) for server setup. *** ## Security * The API token is stored securely by Claude Code (`sensitive: true`) * Tokens are transmitted over HTTPS to the rustunnel server * Tunnel subprocesses are cleaned up when the MCP server exits * Use `--insecure` only in local development with self-signed certificates *** ## Related pages Full reference for the rustunnel-mcp server the plugin wraps, including all tool parameters. Wire rustunnel into Cursor, Windsurf, Cline, Codex, and other MCP clients. Stand up your own rustunnel server to use with the plugin. Quick-start version of this page with install one-liners and an agent-ready prompt. # rustunnel CLI Client Guide — every flag, command, and config option Source: https://rustunnel.com/docs/guides/client-guide Complete reference for the rustunnel CLI client: installation, authentication, HTTP/TCP/UDP/P2P tunnel commands, live terminal UI, local request inspector, region selection, config file format, and troubleshooting. The companion to the Quickstart and Self-Hosting guides. This is the complete reference for the rustunnel CLI — every flag, command, and config option, including the live [terminal UI](#terminal-ui) and [request inspector](#request-inspector) shipping in 0.8.3. New to rustunnel? Start with the [Quickstart](/docs/quickstart) for a three-step walkthrough, or the [self-hosting guide](/docs/guides/self-hosting) if you want to run your own server. ## Installation ### From source (recommended) Requires [Rust](https://rustup.rs/) 1.75 or later. ```bash theme={null} git clone https://github.com/joaoh82/rustunnel cd rustunnel cargo build --release -p rustunnel-client sudo install -Dm755 target/release/rustunnel /usr/local/bin/rustunnel ``` Or use the Makefile shortcut: ```bash theme={null} make deploy-client ``` ### Verify ```bash theme={null} rustunnel --version ``` *** ## Quick Start ```bash theme={null} # 1. Get an auth token # → Sign up at https://rustunnel.com, then go to Dashboard → API Keys → Create token # 2. Create a config file interactively rustunnel setup # → prompts for region and auth token, writes ~/.rustunnel/config.yml # 3. Expose a local web server running on port 3000 rustunnel http 3000 # 4. Expose a raw TCP service (e.g. SSH on port 22) rustunnel tcp 22 ``` After connecting, the terminal displays the public URL: ```text theme={null} ╭────────────────────────────────────────────────────────────╮ │ rustunnel │ ├────────────────────────────────────────────────────────────┤ │ HTTP [myapp] → localhost:3000 │ │ https://myapp.tunnel.example.com │ ╰────────────────────────────────────────────────────────────╯ ✓ Tunnels active. Press Ctrl-C to quit. ``` *** ## Configuration File The client reads `~/.rustunnel/config.yml` automatically. CLI flags always override file values. ### Full example ```yaml theme={null} # Tunnel server address (required — choose a region) server: eu.edge.rustunnel.com:4040 # Authentication token (required) auth_token: rt_live_abc123... # Skip TLS certificate verification — local dev ONLY, never use in production insecure: false # Region preference: auto (probe & pick nearest), or eu / us / ap. # Omit for self-hosted / single-server setups. region: auto # Named tunnel definitions used by `rustunnel start` tunnels: web: proto: http local_port: 3000 local_host: localhost # optional, defaults to localhost subdomain: myapp # optional, requests a specific subdomain api: proto: http local_port: 8080 subdomain: myapi database: proto: tcp local_port: 5432 ``` ### Field reference | Field | Type | Default | Description | | --------------------------- | ------- | ------------- | ---------------------------------------------------------------------- | | `server` | string | — | Tunnel server host:port (e.g. `eu.edge.rustunnel.com:4040`) | | `auth_token` | string | — | Authentication token issued by the server | | `insecure` | bool | `false` | Skip TLS certificate verification (dev only) | | `region` | string | omit | `auto` (probe nearest), `eu`, `us`, `ap`. Omit for self-hosted setups. | | `tunnels` | map | `{}` | Named tunnel definitions (used by `rustunnel start`) | | `tunnels..proto` | string | — | `http` or `tcp` | | `tunnels..local_port` | integer | — | Local port to forward | | `tunnels..local_host` | string | `localhost` | Local hostname to connect to | | `tunnels..subdomain` | string | auto-assigned | Requested HTTP subdomain | *** ## Commands ### `setup` — Interactive config wizard Create (or overwrite) `~/.rustunnel/config.yml` through a guided prompt sequence. ```bash theme={null} rustunnel setup ``` **Prompts:** | Prompt | Default | Description | | -------------- | ----------------- | ---------------------------------------------------------- | | Region | `auto` | `auto` (probe nearest), `eu`, `us`, `ap`, or `self-hosted` | | Auth token | *(blank)* | Token issued by the server; leave empty to fill in later | | Server address | *(auto-resolved)* | Only prompted when `self-hosted` is selected | **Region behavior:** | Choice | Server resolution | | ------------------ | ------------------------------------------------ | | `eu` / `us` / `ap` | Auto-set to `.edge.rustunnel.com:4040` | | `auto` | Probes all regions, picks the nearest by latency | | `self-hosted` | Prompts for your server address | **Behaviour:** * Creates `~/.rustunnel/` if the directory doesn't exist. * If a config file already exists it is overwritten — a backup is not kept, so copy the old file first if you want to preserve it. * Writes a commented `tunnels:` block with HTTP and TCP examples so you can see the structure right away. * Prints `Created:` or `Updated:` with the full path when done. **Example session (auto region):** ```text theme={null} rustunnel setup — create ~/.rustunnel/config.yml Region [auto / eu / us / ap / self-hosted] (default: auto): Selecting nearest region… eu 12ms · us 143ms · ap 311ms · → eu (Helsinki, FI) 12ms Server set to: eu.edge.rustunnel.com:4040 Auth token (leave blank to skip): rt_live_abc123xyz Created: /Users/alice/.rustunnel/config.yml Run `rustunnel start` to connect using this config. ``` **Example session (self-hosted):** ```text theme={null} rustunnel setup — create ~/.rustunnel/config.yml Region [auto / eu / us / ap / self-hosted] (default: auto): self-hosted Tunnel server address: tunnel.internal.corp:4040 Auth token (leave blank to skip): my-token Created: /Users/alice/.rustunnel/config.yml Run `rustunnel start` to connect using this config. ``` **Generated file (managed):** ```yaml theme={null} # rustunnel configuration # Documentation: https://github.com/joaoh82/rustunnel server: eu.edge.rustunnel.com:4040 auth_token: rt_live_abc123xyz region: auto # tunnels: # web: # proto: http # local_port: 3000 # api: # proto: http # local_port: 8080 # subdomain: myapi # database: # proto: tcp # local_port: 5432 ``` **Generated file (self-hosted):** ```yaml theme={null} # rustunnel configuration # Documentation: https://github.com/joaoh82/rustunnel server: tunnel.internal.corp:4040 auth_token: my-token # region: not applicable (self-hosted) # tunnels: # ... ``` After running `setup`, uncomment and fill in the `tunnels:` section then run `rustunnel start`, or use `rustunnel http ` / `rustunnel tcp ` directly. *** ### `http` — HTTP tunnel Expose a local HTTP/HTTPS service through the tunnel server. ```bash theme={null} rustunnel http [options] ``` **Arguments:** | Argument | Description | | -------- | --------------------------------------- | | `` | Local TCP port to forward (e.g. `3000`) | **Options:** | Flag | Default | Description | | ---------------------- | ------------- | -------------------------------------------------------------------------------- | | `--subdomain ` | auto-assigned | Request a specific subdomain (e.g. `myapp` → `myapp.eu.edge.rustunnel.com`) | | `--server ` | from config | Override the server address (bypasses region selection) | | `--token ` | from config | Override the auth token | | `--local-host ` | `localhost` | Local hostname to forward to | | `--region ` | from config | Region to connect to: `eu`, `us`, `ap`, or `auto`. Ignored if `--server` is set. | | `--no-reconnect` | off | Exit instead of reconnecting on failure | | `--insecure` | off | Skip TLS verification (dev only) | **Examples:** ```bash theme={null} # Expose port 3000 — auto-selects the nearest region rustunnel http 3000 # Connect to a specific region rustunnel http 3000 --region eu # Request a specific subdomain rustunnel http 3000 --subdomain myapp # Forward to a non-localhost service rustunnel http 8080 --local-host 192.168.1.10 # One-shot connection (exit on disconnect instead of reconnecting) rustunnel http 3000 --no-reconnect # Use an explicit server address (bypasses region selection) rustunnel http 3000 --server tunnel.example.com:9000 --token rt_live_abc123 ``` *** ### `tcp` — TCP tunnel Expose any raw TCP service (database, SSH, game server, etc.). ```bash theme={null} rustunnel tcp [options] ``` **Arguments:** | Argument | Description | | -------- | ------------------------- | | `` | Local TCP port to forward | **Options:** Same as `http` except `--subdomain` has no effect for TCP tunnels. `--region` still works. **Examples:** ```bash theme={null} # Expose a local PostgreSQL instance rustunnel tcp 5432 # Expose SSH on a non-standard port rustunnel tcp 2222 --local-host 10.0.0.5 ``` The server assigns a random public port from its configured TCP port range. The public address is displayed in the startup box. *** ### `start` — Multi-tunnel mode Start all tunnels defined in a config file simultaneously. ```bash theme={null} rustunnel start [--config ] ``` **Options:** | Flag | Default | Description | | --------------------- | ------------------------- | --------------------- | | `-c, --config ` | `~/.rustunnel/config.yml` | Path to a config file | **Example:** ```bash theme={null} # Use default config file rustunnel start # Use a custom config file rustunnel start --config /etc/rustunnel/production.yml ``` `start` always reconnects automatically (equivalent to running each tunnel without `--no-reconnect`). At least one tunnel must be defined in the config file or the command exits with an error. *** ### Multiple backends (load balancing) When `start` is given two or more tunnels with the same `(group, group_key)`, the server treats them as one logical pool — inbound traffic is dispatched to a healthy member at random. This is the path for running a hot-spare backend, scaling out a service horizontally behind a single subdomain, or doing zero-downtime rollouts. Add the load-balancing fields to each pool member's tunnel definition: ```yaml theme={null} server: tunnel.example.com:4040 auth_token: "your-token" tunnels: a: proto: http local_port: 3000 subdomain: pool group: web group_key: shared-secret-for-this-pool health_check: type: tcp # Optional: when this group goes 0/N healthy, the server POSTs a # group_zero_healthy alert to this URL. Per-tenant — independent # of any operator-side webhook configured on the edge. alert_webhook: "https://hooks.slack.com/services/my-team/..." ``` Run a second client (on the same or a different host) with `local_port: 3001`, the same `subdomain`, and the same `(group, group_key)`. Both clients register as members of the same pool; public connections to `pool.tunnel.example.com` are dispatched at random across them. Load balancing requires `[load_balancing] enabled = true` in the server's `server.toml`. When the flag is `false` (the default), the client's `group` / `group_key` / `health_check` fields are accepted but ignored — both clients try to register as solo tunnels and the second one fails with `subdomain '...' is already in use`. #### Quick smoke test End-to-end validation with two backends sharing one subdomain. ```bash theme={null} git clone https://github.com/joaoh82/rustunnel cd rustunnel cargo build --release -p rustunnel-client ``` ```bash theme={null} cat > /tmp/lb-test.yml <<'EOF' server: tunnel.example.com:4040 auth_token: "your-token" tunnels: a: proto: http local_port: 3000 subdomain: lbtest group: web group_key: shared-secret-for-lb-test health_check: type: tcp EOF ``` ```bash theme={null} # Terminal 1 — backend A python3 -m http.server 3000 # Terminal 2 — client A ./target/release/rustunnel start --config /tmp/lb-test.yml ``` Use a second config file that's identical except for `local_port: 3001`. ```bash theme={null} # Terminal 3 — backend B python3 -m http.server 3001 # Terminal 4 — client B ./target/release/rustunnel start --config /tmp/lb-test-b.yml ``` ```bash theme={null} for i in $(seq 1 50); do curl -fsS https://lbtest.tunnel.example.com/ -o /dev/null -w "%{http_code}\n" done ``` Both backends should see roughly half of the requests. Kill one of the local backends (Ctrl-C on its `python3` process). The probe loop on that client marks it unhealthy after `max_failed × interval_secs` seconds; subsequent requests all land on the survivor. Restart the backend — the probe re-registers it as healthy and dispatch distributes again. For the full reference (config field meanings, behaviour rules, observability series, limitations) see [Load Balancing & Health Checks](/docs/reference/load-balancing). *** ### `token create` — API token management Create a new API token via the server's dashboard REST API. Requires admin credentials. ```bash theme={null} rustunnel token create --name