rustunnel Client

July 22, 2026 · View on GitHub

rustunnel exposes local services to the internet through a secure, self-hosted tunnel server.


Table of Contents

  1. Installation
  2. Quick Start
  3. Configuration File
  4. Commands
  5. Flags Reference
  6. Region Selection
  7. Reconnection Behavior
  8. Terminal Output
  9. Request Inspector
  10. Environment Variables
  11. Error Reference
  12. Troubleshooting

Installation

Requires Rust 1.75 or later.

git clone https://github.com/your-org/rustunnel
cd rustunnel
cargo build --release -p rustunnel-client
sudo install -Dm755 target/release/rustunnel /usr/local/bin/rustunnel

Or use the Makefile shortcut:

make deploy-client

Verify

rustunnel --version

Quick Start

# 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

# 5. Expose a UDP service (e.g. game server)
rustunnel udp 27015

# 6. P2P tunnel — expose a service to another client
rustunnel p2p 27015 --name my-game --secret "shared-secret"

After connecting, the terminal displays the public URL:

╭────────────────────────────────────────────────────────────╮
│                         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

# Tunnel server address (required)
server: 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

  gameserver:
    proto: udp
    local_port: 27015

  p2p-publish:
    proto: p2p
    local_port: 27015
    p2p_name: my-game
    p2p_secret: shared-secret-123

Field reference

FieldTypeDefaultDescription
serverstringTunnel server host:port (e.g. edge.rustunnel.com:4040)
auth_tokenstringAuthentication token issued by the server
insecureboolfalseSkip TLS certificate verification (dev only)
regionstringomitauto (probe nearest), eu, us, ap. Omit for self-hosted setups.
tunnelsmap{}Named tunnel definitions (used by rustunnel start)
tunnels.<name>.protostringhttp, tcp, udp, or p2p
tunnels.<name>.local_portintegerLocal port to forward
tunnels.<name>.local_hoststringlocalhostLocal hostname to connect to
tunnels.<name>.subdomainstringauto-assignedRequested HTTP subdomain
tunnels.<name>.p2p_namestringP2P publisher tunnel name (P2P only)
tunnels.<name>.p2p_targetstringP2P subscriber target name (P2P only)
tunnels.<name>.p2p_secretstringShared secret for P2P authentication

Commands

setup — Interactive config wizard

Create (or overwrite) ~/.rustunnel/config.yml through a guided prompt sequence.

rustunnel setup

Prompts:

PromptDefaultDescription
Regionautoauto (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:

ChoiceServer resolution
eu / us / apAuto-set to <region>.edge.rustunnel.com:4040
autoProbes all regions, picks the nearest by latency
self-hostedPrompts 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):

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):

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):

# 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):

# 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 <port> / rustunnel tcp <port> directly.


http — HTTP tunnel

Expose a local HTTP/HTTPS service through the tunnel server.

rustunnel http <port> [options]

Arguments:

ArgumentDescription
<port>Local TCP port to forward (e.g. 3000)

Options:

FlagDefaultDescription
--subdomain <name>auto-assignedRequest a specific subdomain (e.g. myappmyapp.eu.edge.rustunnel.com)
--server <host:port>from configOverride the server address (bypasses region selection)
--token <token>from configOverride the auth token
--local-host <host>localhostLocal hostname to forward to
--region <id>from configRegion to connect to: eu, us, ap, or auto. Ignored if --server is set.
--no-reconnectoffExit instead of reconnecting on failure
--insecureoffSkip TLS verification (dev only)

Examples:

# 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

Receiving webhooks (Twilio, Stripe, GitHub, …):

HTTP tunnels are safe for HMAC-signed webhooks:

  • The request body is forwarded byte-for-byte — the proxy never parses or re-serializes it, so signatures computed over the raw payload stay valid.
  • Every proxied request carries X-Forwarded-For (caller IP, appended to any existing chain), X-Forwarded-Proto (http or https) and X-Forwarded-Host (the public tunnel host). Frameworks use these to reconstruct the public URL, which signed-webhook validation depends on — no manual base-URL override needed. X-Forwarded-Proto and X-Forwarded-Host are set authoritatively by the edge; for X-Forwarded-For, only the rightmost entry is edge-verified — earlier entries are supplied by the caller and must not be trusted for IP allowlists or logging.
  • Prefer configuring providers with the https:// tunnel URL. http:// URLs also work when the server runs with plain_http_mode = "proxy" (the default on rustunnel.com edges); on redirect servers an http:// webhook URL gets a 308 redirect, which many providers either refuse to follow or re-sign against the target URL — breaking signature validation.

tcp — TCP tunnel

Expose any raw TCP service (database, SSH, game server, etc.).

rustunnel tcp <port> [options]

Arguments:

ArgumentDescription
<port>Local TCP port to forward

Options: Same as http except --subdomain has no effect for TCP tunnels. --region still works.

Examples:

# 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.


udp — UDP tunnel

Expose a local UDP service (game server, DNS, VoIP, etc.).

rustunnel udp <port> [options]

Arguments:

ArgumentDescription
<port>Local UDP port to forward

Options: Same as http except --subdomain has no effect. --region still works.

Examples:

# Expose a game server
rustunnel udp 27015

# Expose a DNS service
rustunnel udp 53 --local-host 10.0.0.1

The server assigns a random public UDP port from its configured UDP port range. Incoming datagrams are forwarded to the local service; responses are sent back to the original sender. Sessions are tracked by remote address and expire after 60 seconds of inactivity.


p2p — Peer-to-peer tunnel

Connect two rustunnel clients directly. One client acts as a publisher (exposes a service), the other as a subscriber (connects to it). Data is relayed through the server.

rustunnel p2p <port> --name <name> --secret <secret>     # publisher
rustunnel p2p <port> --target <name> --secret <secret>    # subscriber

Arguments:

ArgumentDescription
<port>Local port — the service port (publisher) or the listener port (subscriber)

Options:

FlagDescription
--name <name>Publish a service under this name (publisher mode)
--target <name>Connect to a published tunnel by name (subscriber mode)
--secret <secret>Shared secret for authentication (both sides must match)
--server, --token, --region, --insecure, --no-reconnectSame as other tunnel types

--name and --target are mutually exclusive. One of them is required.

Examples:

# Publisher: expose a game server under the name "my-game"
rustunnel p2p 27015 --name my-game --secret "shared-secret-123"

# Subscriber: connect to "my-game" and listen on local port 8000
rustunnel p2p 8000 --target my-game --secret "shared-secret-123"

# Any app connecting to localhost:8000 on the subscriber's machine
# will be forwarded to localhost:27015 on the publisher's machine.

See the P2P Tunnels reference for details on relay mode, direct mode, NAT classification, and hole punching.


start — Multi-tunnel mode

Start all tunnels defined in a config file simultaneously.

rustunnel start [--config <path>]

Options:

FlagDefaultDescription
-c, --config <path>~/.rustunnel/config.ymlPath to a config file

Example:

# 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.


token create — API token management

Create a new API token via the server's dashboard REST API. Requires admin credentials.

rustunnel token create --name <label> [options]

Options:

FlagDefaultDescription
--name <label>Human-readable label for the token (required)
--server <host:port>localhost:4040Dashboard server address
--admin-token <token>Admin token for authentication

Example:

rustunnel token create \
  --name "production-server" \
  --server tunnel.example.com:4040 \
  --admin-token admin_secret_here

Output:

Token created:
  id:    f47ac10b-58cc-4372-a567-0e02b2c3d479
  token: rt_live_abc123xyz...
  label: production-server

Copy the token value — it is shown only once. Add it to your config file as auth_token.


Flags Reference

This table summarises all flags across all commands:

FlagCommandsDescription
--server <host:port>http, tcpTunnel server address (bypasses region selection)
--token <token>http, tcpAuth token (overrides config; also read from RUSTUNNEL_TOKEN)
--subdomain <name>httpRequested HTTP subdomain
--local-host <host>http, tcpLocal hostname (default: localhost)
--region <id>http, tcpRegion: eu, us, ap, or auto. Ignored if --server is set.
--no-reconnecthttp, tcpExit on failure instead of reconnecting
--insecurehttp, tcpSkip TLS certificate verification
--jsonhttp, tcp, udp, p2p, start, token createEmit machine-readable NDJSON events on stdout instead of human output
--no-tuihttp, tcp, udp, p2p, startDisable the full-screen terminal UI; print one line per request instead
--inspect-port <port>http, tcp, udp, p2p, startPort for the local web inspector (default 4040; 0 picks any free port)
--no-inspecthttp, tcp, udp, p2p, startDisable the local web inspector
-c, --config <path>startConfig file path
--name <label>token createToken label (required)
--admin-token <token>token createAdmin token for dashboard API
--versionallPrint version and exit
--helpallPrint help and exit

setup takes no flags — all input is collected interactively.


Region Selection

rustunnel can connect to multiple edge servers in different geographic regions. The region selection logic follows this priority order:

  1. --server <host:port> — explicit server address always wins; region logic is skipped entirely.
  2. --region <id> — connect directly to the named region without probing.
  3. region: auto (config file or --region auto) — probe all regions in parallel and pick the nearest.
  4. No region preference — use server: from config as-is (backward compatible with self-hosted setups).

Available regions (hosted service)

Region IDLocationServer
euHelsinki, FIeu.edge.rustunnel.com:4040
usHillsboro, ORus.edge.rustunnel.com:4040
apSingaporeap.edge.rustunnel.com:4040

Auto-select output

When region: auto is active, the client probes all regions by TCP connect time and prints:

  Selecting nearest region… eu 12ms · us 143ms · ap 311ms → eu (Helsinki, FI) 12ms

Unreachable regions time out after 3 seconds and are assigned a 10-second penalty so they never win the selection.

Region list refresh

The region list is cached at ~/.rustunnel/regions.json for 24 hours. On expiry the client fetches a fresh list from GET https://<host>:8443/api/regions; if that fails it falls back to the hardcoded list compiled into the binary.


Reconnection Behavior

By default, rustunnel reconnects automatically when the connection drops. The retry delay follows an exponential backoff schedule:

AttemptDelay
1~1 s
2~2 s
3~4 s
4~8 s
n≥6~60 s (max)

Each delay has ±20% random jitter to prevent thundering-herd reconnects when a server restarts.

  Reconnecting in 2.3s (attempt 2)…
  Reconnecting in 5.1s (attempt 3)…

Fatal errors (no reconnect)

The following errors cause an immediate exit — retrying would not help:

  • Auth failed — invalid or revoked token. Fix: create a new token at rustunnel.com (Dashboard → API Keys) and update your config.
  • Tunnel error — the server rejected the registration (subdomain already taken, tunnel limit reached). Fix: pick a different --subdomain or close an existing tunnel.
  • Config error — missing required fields. Fix: check your ~/.rustunnel/config.yml.

Disabling reconnect

Use --no-reconnect for scripting, CI, or when you want manual control:

rustunnel http 3000 --no-reconnect || echo "Tunnel exited"

Terminal Output

When stdout is a terminal, the client takes over the screen with a live UI. Otherwise — piped output, CI, --no-tui, or --json — it falls back to the line-based output described further down.

Terminal UI

┌ rustunnel ───────────────────────────────────────────────────────────────────┐
│Session  ● online                    Server   eu.edge.rustunnel.com:4040      │
│Uptime   00:12:43                    Region   eu · 57ms                       │
│Version  0.8.2                       Inspect  http://127.0.0.1:4040           │
└──────────────────────────────────────────────────────────────────────────────┘
┌ Tunnels ─────────────────────────────────────────────────────────────────────┐
│HTTP   web    http://bb176fb6.eu.edge.rustunnel.com  → localhost:3000  ● healthy│
└──────────────────────────────────────────────────────────────────────────────┘
┌ Traffic ─────────────────────────────────────────────────────────────────────┐
│Conns    2 open · 27 total    Requests 1204                        req/s       │
│Traffic  ↑ 1.2 MB  ↓ 830.0 kB    Latency  p50 6ms · p90 41ms      ▁▂▃▅▂▁▃▂    │
└──────────────────────────────────────────────────────────────────────────────┘
┌ Requests ────────────────────────────────────────────────────────────────────┐
│23:21:54 GET     /style.css                    200   3 ms    95.99.57.76:51224│
│23:21:53 POST    /api/items                    201  18 ms    95.99.57.76:51223│
│23:21:53 GET     /missing                      404   2 ms    95.99.57.76:51222│
└──────────────────────────────────────────────────────────────────────────────┘
q quit  ↑↓ scroll  f pause  c clear  l logs
  • Session — connection state, uptime, client version, edge server, region and live control-plane latency (measured from the keepalive round-trip), and the local inspector URL.
  • Tunnels — one row per tunnel with its public URL, local target, and health when a health_check is configured.
  • Traffic — open/total connections, bytes each way, p50/p90 request duration, and a requests-per-second sparkline.
  • Requests — live log of every HTTP request: time, method, path, status, duration, and the public client address. Replayed requests are marked .

Keys:

KeyAction
q, Esc, Ctrl-CQuit (closes the tunnel)
/ k jScroll the request log
PgUp / PgDnScroll by page
g / GJump to newest / oldest
fPause or resume following new requests
cClear the captured requests
lToggle the log pane (client diagnostics)

Only HTTP tunnels produce request rows. TCP and UDP tunnels are opaque byte streams, so they show connection and traffic counters only.

Line mode

With --no-tui, a non-terminal stdout, or --json, output stays line-based.

While establishing the connection a spinner is shown:

⠙ Connecting to tunnel server…
⠹ Authenticating…
⠸ Registering tunnels…

Once all tunnels are registered, a bordered box appears:

╭────────────────────────────────────────────────────────────╮
│                         rustunnel                          │
├────────────────────────────────────────────────────────────┤
│   HTTP [myapp] → localhost:3000                            │
│   https://myapp.tunnel.example.com                        │
│   TCP  [ssh]   → localhost:22                             │
│   tcp://tunnel.example.com:34521                          │
╰────────────────────────────────────────────────────────────╯

  ✓ Tunnels active. Press Ctrl-C to quit.

  Inspect http://127.0.0.1:4040

Color coding:

  • Protocol label — bold yellow
  • Tunnel name — dim
  • Public URL — bold green
  • Border — cyan

Requests then stream one per line as they arrive:

23:21:54    GET /style.css                     200 3ms
23:21:53   POST /api/items                     201 18ms
23:21:53    GET /missing                       404 2ms

JSON output (--json)

With --json, the spinner and startup box are suppressed and stdout instead carries NDJSON — one JSON event object per line — for scripts and AI agents:

{"event":"tunnel_ready","protocol":"http","public_url":"https://myapp.tunnel.example.com","local_port":3000,"local_host":"localhost","tunnel_id":"6f9a…","name":"myapp"}

Events: inspector_ready (url; emitted once at startup when the request inspector is enabled), tunnel_ready (includes public_addr host:port for tcp/udp), reconnecting (attempt, reason, delay_secs), reconnected, error (code, message, hint; exit code 1 follows), and token_created (for token create --json). Diagnostics still go to stderr.

A --json session that also runs the inspector emits it first, so a script can pick the port up before any traffic arrives:

{"event":"inspector_ready","url":"http://127.0.0.1:4040"}
{"event":"tunnel_ready","protocol":"http","public_url":"https://myapp.tunnel.example.com","local_port":3000,"local_host":"localhost"}

Graceful shutdown

Press Ctrl-C to cleanly close the tunnel and exit. The control WebSocket is closed before the process exits. In the terminal UI, q and Esc do the same.


Request Inspector

Every tunnel session also starts a small web inspector on loopback, printed at startup and shown in the terminal UI:

Inspect http://127.0.0.1:4040

Open it to browse everything that flowed through the tunnel:

  • Request list — live-updating, with method, path, status, and duration. Filter by method, path, or status.
  • Detail view — Summary (bodies), Headers (full request and response headers), and Raw (the reconstructed HTTP messages).
  • Replay — re-issue any captured request against your local service without the original caller doing anything. Handy for webhooks: trigger once, then iterate against the same payload. Replayed requests appear in the list marked replay.

Scope and limits

  • Captures HTTP tunnels only. TCP and UDP tunnels are raw byte streams; they contribute connection and traffic counters but no request entries.
  • Keeps the last 500 requests in memory, per process. Nothing is written to disk and nothing survives a restart.
  • Bodies are captured up to 64 KB each; larger ones are marked truncated (the reported size is still exact). A truncated request body cannot be replayed byte-for-byte.
  • WebSocket and other upgraded connections are recorded as their handshake (101); the frames afterwards are not parsed.

Configuration

FlagEffect
--inspect-port <port>Bind a specific port (default 4040). If it is taken, the next free port is used and the real URL is displayed.
--no-inspectDisable the inspector entirely.

Because the port can shift when the preferred one is taken, always read the actual URL rather than assuming 4040 — from the terminal UI header, the line under the startup box, or the inspector_ready event in --json mode.

The inspector binds 127.0.0.1 only and has no authentication, so treat it as local-only — captured payloads may contain credentials, tokens, and personal data. Use --no-inspect on shared or multi-user machines.


Environment Variables

VariableDescription
RUST_LOGLog level filter (e.g. debug, info, warn, rustunnel=debug). Default: warn.
RUSTUNNEL_TOKENAuth token for the http, tcp, udp, and p2p commands, used when --token is not passed (takes precedence over the config file; empty values are ignored). start reads tokens from the config file only, and token create authenticates with --admin-token.

Examples:

# Enable debug logging for all crates
RUST_LOG=debug rustunnel http 3000

# Enable debug only for rustunnel internals
RUST_LOG=rustunnel=debug rustunnel http 3000

# Quiet mode (errors only)
RUST_LOG=error rustunnel http 3000

Log output and human-readable reconnect notices go to stderr. Normal tunnel output (startup box, or NDJSON events with --json) goes to stdout.


Error Reference

ErrorCauseFix
config error: server address is requiredNo --server flag and no config fileAdd server: to ~/.rustunnel/config.yml or pass --server
auth failed: <message>Token invalid or revokedCreate a new token at rustunnel.com (Dashboard → API Keys) or with rustunnel token create for self-hosted setups
tunnel error: <message>Subdomain already in use or server limit reachedUse a different --subdomain or wait
connection error: cannot reach <server> (…)Can't reach the serverCheck network, firewall, and server address; pass --server <host:port> or --region <id>
connection error: heartbeat timeoutServer stopped responding to pingsTransient — reconnect loop will retry
connection error: timeout waiting for server responseAuth/registration timed out (10 s)Check server health; may be overloaded
no tunnels defined in config filerustunnel start with an empty tunnels: mapAdd at least one tunnel to the config

Troubleshooting

Tunnel connects but requests don't arrive

  • Verify your local service is running and listening: curl http://localhost:<port>
  • Check --local-host if forwarding to a non-localhost address

Certificate verification failed

If your server uses a self-signed certificate (common for local/staging environments), use --insecure:

rustunnel http 3000 --insecure

Never use --insecure in production — it disables all TLS certificate checks.

Subdomain already taken

The server returns tunnel error: subdomain already in use. Either:

  • Omit --subdomain to get an auto-assigned subdomain, or
  • Choose a different name: --subdomain myapp-dev

Debugging connection issues

Enable verbose logging to see full protocol traces:

RUST_LOG=debug rustunnel http 3000 2>&1 | tee rustunnel.log

Key log messages to look for:

MessageMeaning
authenticated session_id=...Auth succeeded
tunnel registered public_url=...Tunnel is active
data WebSocket connectedData plane is ready
new connection from server conn_id=...Incoming proxied request
yamux data conn errorData-plane transport error
heartbeat timeoutServer stopped responding — will reconnect

Multiple tunnels on the same server

Use rustunnel start with a config file to open all tunnels over a single control connection:

server: tunnel.example.com:9000
auth_token: rt_live_abc123

tunnels:
  frontend:
    proto: http
    local_port: 3000
    subdomain: app
  backend:
    proto: http
    local_port: 8080
    subdomain: api
  metrics:
    proto: tcp
    local_port: 9090
rustunnel start