{
  "markdown": "# rustunnel\n\n[![CI](https://github.com/joaoh82/rustunnel/actions/workflows/ci.yml/badge.svg)](https://github.com/joaoh82/rustunnel/actions/workflows/ci.yml)\n[![License: AGPLv3](https://img.shields.io/badge/License-AGPLv3-blue.svg)](LICENSE)\n[![Rust](https://img.shields.io/badge/rust-1.76%2B-orange.svg)](https://www.rust-lang.org)\n[![Listed on Skillselion](https://skillselion.com/badge/mcp/tool/io.github.joaoh82/rustunnel.svg)](https://skillselion.com/mcp/tool/io.github.joaoh82/rustunnel)\n\n![rustunnel logo](images/rustunnel-logo-light.png)\n\nThe open-source tunnel that scales with you. Don't pay for idle time. Secure, Rust-fast, and Pay-as-you-go.\n\nExpose local services through a public server over encrypted WebSocket connections with TLS termination, HTTP/TCP proxying, a live dashboard, Prometheus metrics, and audit logging.\n\nYou can self-host or use our managed service.\n\n**Using an AI agent?** rustunnel ships an [MCP server](https://rustunnel.com/docs/guides/mcp-server) — one-click setup for Cursor (then add your [API token](https://rustunnel.com)):\n\n[![Add to Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/install-mcp?name=rustunnel&config=eyJjb21tYW5kIjoicnVzdHVubmVsLW1jcCIsImFyZ3MiOlsiLS1zZXJ2ZXIiLCJldS5lZGdlLnJ1c3R1bm5lbC5jb206NDA0MCIsIi0tYXBpIiwiaHR0cHM6Ly9ldS5lZGdlLnJ1c3R1bm5lbC5jb206ODQ0MyJdLCJlbnYiOnsiUlVTVFVOTkVMX1RPS0VOIjoiWU9VUl9UT0tFTiJ9fQ==)\n\nFor Claude Code, Claude Desktop, Windsurf, and others, see the [agent integration guide](https://rustunnel.com/docs/guides/agent-integration) or the agent manual at [rustunnel.com/agents.md](https://rustunnel.com/agents.md).\n\n---\n\n## Table of Contents\n\n- [Hosted service](#hosted-service)\n- [Architecture overview](#architecture-overview)\n- [Requirements](#requirements)\n- [Local development setup](#local-development-setup)\n  - [Build](#build)\n  - [Run tests](#run-tests)\n  - [Run the server locally](#run-the-server-locally)\n  - [Run the client locally](#run-the-client-locally)\n  - [Git hooks](#git-hooks)\n- [Production deployment (Ubuntu / systemd)](#production-deployment-ubuntu--systemd)\n  - [1 — Install dependencies](#1--install-dependencies)\n  - [2 — Build release binaries](#2--build-release-binaries)\n  - [3 — Create system user and directories](#3--create-system-user-and-directories)\n  - [4 — Install the server binary](#4--install-the-server-binary)\n  - [5 — Create the server config file](#5--create-the-server-config-file)\n  - [6 — TLS certificates (Let's Encrypt + Cloudflare)](#6--tls-certificates-lets-encrypt--cloudflare)\n  - [7 — Set up systemd service](#7--set-up-systemd-service)\n  - [8 — Open firewall ports](#8--open-firewall-ports)\n  - [9 — Verify the server is running](#9--verify-the-server-is-running)\n  - [Updating the server](#updating-the-server)\n- [Docker deployment](#docker-deployment) · [full guide](docs/docker-deployment.md)\n- [Client configuration](#client-configuration)\n  - [Installation](#installation)\n  - [Setup wizard](#setup-wizard)\n  - [Quick start (CLI flags)](#quick-start-cli-flags)\n  - [Machine-readable output (`--json`)](#machine-readable-output---json)\n  - [Config file](#config-file)\n  - [Token management](#token-management)\n- [Port reference](#port-reference)\n- [Config file reference (server)](#config-file-reference-server)\n- [REST API](#rest-api)\n- [AI agent integration (MCP server)](#ai-agent-integration-mcp-server)\n  - [OpenClaw skill](#openclaw-skill)\n- [Monitoring](#monitoring)\n- [Roadmap](#roadmap)\n- [Contributing](#contributing)\n- [License](#license)\n- [Contact](#contact)\n\n---\n\n## Hosted service\n\nYou can use rustunnel without running your own server. We operate a global fleet of public edge servers that you can connect to immediately.\n\n### Available regions\n\n| Region ID | Server | Location | Control plane | Status |\n|-----------|--------|----------|---------------|--------|\n| `eu` | `eu.edge.rustunnel.com` | Helsinki, FI | `:4040` | Live |\n| `us` | `us.edge.rustunnel.com` | Hillsboro, OR | `:4040` | Live |\n| `ap` | `ap.edge.rustunnel.com` | Singapore | `:4040` | Live |\n\nThe client auto-selects the nearest region by default. Use `--region <id>` to connect to a specific one. The legacy address `edge.rustunnel.com` is a CNAME to `eu.edge.rustunnel.com` and will continue to work for backward compatibility.\n\n### Getting an auth token\n\nSign up for a free account at **[rustunnel.com](https://rustunnel.com)** — no waiting list, no manual approval.\n\n1. Create an account at [rustunnel.com](https://rustunnel.com)\n2. Go to **Dashboard → API Keys** and create a token\n3. Copy the token — it is shown only once\n\n**Plans:**\n\n| Plan | Price | Tunnels | Custom subdomains | TLS/HTTPS |\n|------|-------|---------|-------------------|-----------|\n| Free | $0 | Up to 3 | — | ✓ |\n| Pay-as-you-go | $3/mo minimum + $0.10/GB | Unlimited | ✓ | ✓ |\n| Self-host | Free (run your own server) | Unlimited | ✓ | ✓ |\n\nThe free plan is a great way to get started. Upgrade to pay-as-you-go from your dashboard whenever you need custom subdomains or unlimited tunnels.\n\n### Quick start with the hosted server\n\nOnce you have a token, run the setup wizard:\n\n```bash\nrustunnel setup\n# Region [auto / eu / us / ap / self-hosted] (default: auto): (press Enter)\n#   Selecting nearest region… eu 12ms · us 143ms · ap 311ms · → eu (Helsinki, FI) 12ms\n#   Server set to: eu.edge.rustunnel.com:4040\n# Auth token: <paste your token>\n```\n\nThen expose a local service:\n\n```bash\n# HTTP tunnel — auto-selects the nearest region\nrustunnel http 3000\n\n# Connect to a specific region\nrustunnel http 3000 --region eu\n\n# Custom subdomain\nrustunnel http 3000 --subdomain myapp\n\n# TCP tunnel — e.g. expose a local database\nrustunnel tcp 5432\n\n# UDP tunnel — e.g. expose a game server\nrustunnel udp 27015\n\n# P2P tunnel — expose a service to another rustunnel client\nrustunnel p2p 27015 --name my-game --secret \"shared-secret\"\n\n# P2P tunnel — connect to a peer's service\nrustunnel p2p 8000 --target my-game --secret \"shared-secret\"\n```\n\nThe client prints the public URL as soon as the tunnel is established:\n\n```\n  Selecting nearest region… eu 12ms · us 143ms · ap 311ms → eu (Helsinki, FI) 12ms\n✓ tunnel open  https://abc123.eu.edge.rustunnel.com\n```\n\n---\n\n## Architecture overview\n\n![rustunnel architecture](images/rustunnel_architecture.png)\n\n```\n                        ┌──────────────────────────────────────────┐\n                        │           rustunnel-server               │\n                        │                                          │\nInternet ──── :80 ─────▶│  HTTP edge (301 → HTTPS)                 │\nInternet ──── :443 ────▶│  HTTPS edge  ──▶ yamux stream ──▶ client │\nClient ───── :4040 ────▶│  Control-plane WebSocket (TLS)           │\nBrowser ──── :8443 ────▶│  Dashboard UI + REST API                 │\nPrometheus ─ :9090 ────▶│  Metrics endpoint                        │\nInternet ── :20000+ ───▶│  TCP tunnel ports (one per TCP tunnel)   │\n                        └──────────────────────────────────────────┘\n                                          │ yamux multiplexed streams\n                                          ▼\n                              ┌─────────────────────┐\n                              │   rustunnel client   │\n                              │  (developer laptop)  │\n                              └──────────┬──────────┘\n                                         │ localhost\n                                         ▼\n                                ┌────────────────┐\n                                │  local service  │\n                                │  e.g. :3000    │\n                                └────────────────┘\n```\n\n---\n\n## Requirements\n\n### To build\n\n| Requirement | Version | Notes |\n|---|---|---|\n| Rust toolchain | 1.76+ | Install via [rustup](https://rustup.rs) |\n| `pkg-config` | any | Needed by `reqwest` (TLS) |\n| `libssl-dev` | any | On Debian/Ubuntu: `apt install libssl-dev` |\n| Node.js + npm | 18+ | Only needed to rebuild the dashboard UI |\n\n### To run the server in production\n\n| Requirement | Notes |\n|---|---|\n| Linux (Ubuntu 22.04+) | systemd service included |\n| TLS certificate + private key | PEM format (Let's Encrypt recommended) |\n| Public IP / DNS | Wildcard DNS `*.tunnel.yourdomain.com → server IP` required for HTTP tunnels |\n\n---\n\n## Local development setup\n\n### Build\n\n```bash\n# Clone the repository\ngit clone https://github.com/joaoh82/rustunnel.git\ncd rustunnel\n\n# Compile all workspace crates (debug mode)\ncargo build --workspace\n\n# Or use the Makefile shortcut\nmake build\n```\n\n### Run tests\n\nThe 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.\n\n```bash\n# Start the local PostgreSQL container (once per machine, persists across reboots)\nmake db-start\n\n# Full suite (unit + integration)\nmake test\n\n# With output visible\nTEST_DATABASE_URL=postgres://rustunnel:test@localhost:5432/rustunnel_test \\\n  cargo test --workspace -- --nocapture\n\n# Stop PostgreSQL when you no longer need it\nmake db-stop\n```\n\n`make db-start` runs `deploy/docker-compose.dev-deps.yml` which starts a Postgres 16 container on `localhost:5432`. The `make test` target injects `TEST_DATABASE_URL` automatically. If you run `cargo test` directly, export the variable first:\n\n```bash\nexport TEST_DATABASE_URL=postgres://rustunnel:test@localhost:5432/rustunnel_test\n```\n\n### Run the server locally\n\nGenerate a self-signed certificate for local testing:\n\n```bash\nmkdir -p /tmp/rustunnel-dev\n\nopenssl req -x509 -newkey rsa:2048 -keyout /tmp/rustunnel-dev/key.pem \\\n  -out /tmp/rustunnel-dev/cert.pem -days 365 -nodes \\\n  -subj \"/CN=localhost\"\n```\n\nA ready-made local config is checked into the repository at **`deploy/local/server.toml`**.\nIt points to the self-signed cert paths above and has auth disabled for convenience.\nStart the server with it directly:\n\n```bash\ncargo run -p rustunnel-server -- --config deploy/local/server.toml\n```\n\nKey settings in `deploy/local/server.toml`:\n\n| Setting | Value |\n|---|---|\n| Domain | `localhost` |\n| HTTP edge | `:8080` |\n| HTTPS edge | `:8443` |\n| Control plane | `:4040` |\n| Dashboard | `:4041` |\n| Auth token | `dev-secret-change-me` |\n| Auth required | `false` |\n| TLS cert | `/tmp/rustunnel-dev/cert.pem` |\n| TLS key | `/tmp/rustunnel-dev/key.pem` |\n| Database | `/tmp/rustunnel-dev/rustunnel.db` |\n\n### Run the client locally\n\nWith the server running, expose a local service (e.g. something on port 3000):\n\n```bash\n# HTTP tunnel\ncargo run -p rustunnel-client -- http 3000 \\\n  --server localhost:4040 \\\n  --token dev-secret-change-me \\\n  --insecure\n\n# TCP tunnel\ncargo run -p rustunnel-client -- tcp 5432 \\\n  --server localhost:4040 \\\n  --token dev-secret-change-me \\\n  --insecure\n```\n\n> `--insecure` skips TLS certificate verification. Required when using a\n> self-signed certificate locally. Never use this flag against a production server.\n\nThe client will print a public URL, for example:\n```\nhttp tunnel  →  http://abc123.localhost:8080\ntcp  tunnel  →  tcp://localhost:20000\n```\n\n### Testing the HTTP tunnel locally\n\nThe tunnel URL uses a subdomain (e.g. `http://abc123.localhost:8080`).\nBrowsers won't resolve `*.localhost` subdomains by default, so you have two options:\n\n**Option A — curl with a Host header (no setup required)**\n\n```bash\ncurl -v -H \"Host: abc123.localhost\" http://localhost:8080/\n```\n\n**Option B — wildcard DNS via dnsmasq (enables browser access)**\n\n```bash\n# Install and configure dnsmasq to resolve *.localhost → 127.0.0.1\nbrew install dnsmasq\necho \"address=/.localhost/127.0.0.1\" | sudo tee -a $(brew --prefix)/etc/dnsmasq.conf\nsudo brew services start dnsmasq\n\n# Tell macOS to use dnsmasq for .localhost queries\nsudo mkdir -p /etc/resolver\necho \"nameserver 127.0.0.1\" | sudo tee /etc/resolver/localhost\n```\n\nThen visit `http://abc123.localhost:8080` in the browser (include `:8080` since the\nlocal config uses port 8080, not port 80).\n\n### Git hooks\n\nA pre-push hook is included in `.githooks/` that mirrors the CI check step\n(format check + Clippy). Run this once after cloning to activate it:\n\n```bash\nmake install-hooks\n```\n\nFrom that point on, every `git push` will automatically run:\n\n```bash\ncargo fmt --all -- --check\ncargo clippy --workspace --all-targets -- -D warnings\n```\n\nIf either check fails the push is aborted, keeping the remote branch green.\n\n---\n\n## Production deployment (Ubuntu / systemd)\n\nThe steps below match a deployment where:\n- Domain: `edge.rustunnel.com`\n- Wildcard DNS: `*.edge.rustunnel.com → <server IP>`\n- TLS certs: Let's Encrypt via Certbot + Cloudflare DNS challenge\n\n### 1 — Install dependencies\n\n```bash\napt update && apt install -y \\\n  pkg-config libssl-dev curl git \\\n  certbot python3-certbot-dns-cloudflare\n```\n\nInstall Rust (as the build user, not root):\n\n```bash\ncurl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh\nsource \"$HOME/.cargo/env\"\n```\n\n### 2 — Build release binaries\n\n```bash\ngit clone https://github.com/joaoh82/rustunnel.git\ncd rustunnel\ncargo build --release -p rustunnel-server -p rustunnel-client\n```\n\nBinaries will be at:\n- `target/release/rustunnel-server`\n- `target/release/rustunnel`\n\n### 3 — Create system user and directories\n\n```bash\nuseradd --system --no-create-home --shell /usr/sbin/nologin rustunnel\n\nmkdir -p /etc/rustunnel /var/lib/rustunnel\nchown rustunnel:rustunnel /var/lib/rustunnel\nchmod 750 /var/lib/rustunnel\n```\n\n### 4 — Install the server binary\n\n```bash\ninstall -Dm755 target/release/rustunnel-server /usr/local/bin/rustunnel-server\n\n# Optionally install the client system-wide\ninstall -Dm755 target/release/rustunnel /usr/local/bin/rustunnel\n```\n\nOr use the Makefile target (runs build + install + systemd setup):\n\n```bash\nsudo make deploy\n```\n\n### 5 — Set up PostgreSQL\n\nrustunnel requires PostgreSQL for shared state (tokens, tunnel history, audit log).\n\n```bash\napt install -y postgresql postgresql-contrib\n\n# Start and enable the service\nsystemctl enable --now postgresql\n```\n\nCreate a dedicated database and user:\n\n```bash\nsudo -u postgres psql <<'SQL'\nCREATE USER rustunnel WITH PASSWORD 'CHANGE_ME';\nCREATE DATABASE rustunnel OWNER rustunnel;\nGRANT ALL PRIVILEGES ON DATABASE rustunnel TO rustunnel;\nSQL\n```\n\n> **Tip:** For managed PostgreSQL (e.g. AWS RDS, DigitalOcean Managed Database, Supabase) skip the\n> `apt install` step above and just note the connection URL for the config in the next step.\n\nSchema migrations run automatically when the server starts — no manual SQL needed.\n\n### 6 — Create the server config file\n\nCreate `/etc/rustunnel/server.toml` with the content below.\nReplace `your-admin-token-here` with a strong random secret (e.g. `openssl rand -hex 32`).\n\n```toml\n# /etc/rustunnel/server.toml\n\n[server]\n# Primary domain — must match your wildcard DNS record.\ndomain       = \"edge.rustunnel.com\"\n\n# Ports for incoming tunnel traffic (requires CAP_NET_BIND_SERVICE or root).\nhttp_port    = 80\nhttps_port   = 443\n\n# Control-plane WebSocket port — clients connect here.\ncontrol_port = 4040\n\n# Dashboard UI and REST API port.\ndashboard_port = 8443\n\n# Allowed CORS origin for the dashboard UI.\n# Set to the URL where you serve the dashboard-ui (e.g. http://localhost:3000 for local dev).\ndashboard_origin = \"http://localhost:3000\"\n\n# ── TLS ─────────────────────────────────────────────────────────────────────\n[tls]\n# Paths written by Certbot (see step 6).\ncert_path = \"/etc/letsencrypt/live/edge.rustunnel.com/fullchain.pem\"\nkey_path  = \"/etc/letsencrypt/live/edge.rustunnel.com/privkey.pem\"\n\n# Set acme_enabled = true only if you want rustunnel to manage certs itself\n# via the ACME protocol (requires Cloudflare credentials below).\n# When using Certbot (recommended), leave this false.\nacme_enabled = false\n\n# ── Auth ─────────────────────────────────────────────────────────────────────\n[auth]\n# Strong random secret — used both as the admin token and for client auth.\n# Generate: openssl rand -hex 32\nadmin_token  = \"your-admin-token-here\"\nrequire_auth = true\n# Failed auth attempts per source IP per minute before rejection (0 = off).\nmax_failed_auth_per_minute = 10\n\n# ── Database ─────────────────────────────────────────────────────────────────\n[database]\n# PostgreSQL connection URL — the database and user must exist before starting\n# the server (see the PostgreSQL setup step above).  Schema migrations run\n# automatically on first start.\nurl = \"postgresql://rustunnel:CHANGE_ME@localhost:5432/rustunnel\"\n\n# Per-region SQLite file for captured HTTP request bodies.\n# The directory must be writable by the rustunnel user.\ncaptured_path = \"/var/lib/rustunnel/captured.db\"\n\n# ── Logging ──────────────────────────────────────────────────────────────────\n[logging]\nlevel  = \"info\"\nformat = \"json\"\n\n# Optional: write an append-only audit log (JSON-lines) for auth attempts,\n# tunnel registrations, token creation/deletion, and admin actions.\n# Omit or comment out to disable.\naudit_log_path = \"/var/lib/rustunnel/audit.log\"\n\n# ── Limits ───────────────────────────────────────────────────────────────────\n[limits]\n# Maximum tunnels a single authenticated session may register.\nmax_tunnels_per_session = 10\n\n# Maximum simultaneous proxied connections per tunnel (semaphore).\nmax_connections_per_tunnel = 100\n\n# Per-tunnel request rate limit (requests/second).\nrate_limit_rps = 100\n\n# Per-source-IP rate limit (requests/second). Set to 0 to disable.\nip_rate_limit_rps = 100\n\n# Maximum size of a proxied HTTP request body (bytes). Default: 10 MB.\nrequest_body_max_bytes = 10485760\n\n# Inclusive port range reserved for TCP tunnels.\n# Each active TCP tunnel consumes one port from this range.\ntcp_port_range = [20000, 20099]\n\n# Inclusive port range reserved for UDP tunnels.\n# Each active UDP tunnel consumes one port from this range.\n# Must not overlap with tcp_port_range. Set to [0, 0] to disable UDP tunnels.\nudp_port_range = [20100, 20199]\n```\n\nSecure the file:\n\n```bash\nchown root:rustunnel /etc/rustunnel/server.toml\nchmod 640 /etc/rustunnel/server.toml\n```\n\n### 7 — TLS certificates (Let's Encrypt + Cloudflare)\n\nCreate the Cloudflare credentials file:\n\n```bash\ncat > /etc/letsencrypt/cloudflare.ini <<'EOF'\n# Cloudflare API token with DNS:Edit permission for the zone.\ndns_cloudflare_api_token = YOUR_CLOUDFLARE_API_TOKEN\nEOF\n\nchmod 600 /etc/letsencrypt/cloudflare.ini\n```\n\nRequest a certificate covering the bare domain and the wildcard (required for HTTP subdomain tunnels):\n\n```bash\ncertbot certonly \\\n  --dns-cloudflare \\\n  --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \\\n  -d \"edge.rustunnel.com\" \\\n  -d \"*.edge.rustunnel.com\" \\\n  --agree-tos \\\n  --email your@email.com\n```\n\nCertbot writes the certificate to:\n```\n/etc/letsencrypt/live/edge.rustunnel.com/fullchain.pem\n/etc/letsencrypt/live/edge.rustunnel.com/privkey.pem\n```\n\nThese paths are already set in the config above. Certbot sets up automatic renewal via a systemd timer.\n\nrustunnel reads TLS certificates from disk at startup, so it must be restarted after each renewal.\nAdd a Certbot deploy hook to do this automatically:\n\n```bash\ncat > /etc/letsencrypt/renewal-hooks/deploy/restart-rustunnel.sh <<'EOF'\n#!/bin/sh\nsystemctl restart rustunnel.service\nEOF\nchmod +x /etc/letsencrypt/renewal-hooks/deploy/restart-rustunnel.sh\n```\n\nAllow the `rustunnel` service user to read the certificates:\n\n```bash\n# Grant read access to the live/ and archive/ directories\nchmod 755 /etc/letsencrypt/{live,archive}\nchmod 640 /etc/letsencrypt/live/edge.rustunnel.com/*.pem\nchgrp rustunnel /etc/letsencrypt/live/edge.rustunnel.com/*.pem\nchgrp rustunnel /etc/letsencrypt/archive/edge.rustunnel.com/*.pem\nchmod 640 /etc/letsencrypt/archive/edge.rustunnel.com/*.pem\n```\n\n### 8 — Set up systemd service\n\n```bash\n# Copy the unit file from the repository\ninstall -Dm644 deploy/rustunnel.service /etc/systemd/system/rustunnel.service\n\nsystemctl daemon-reload\nsystemctl enable --now rustunnel.service\n\n# Check it started\nsystemctl status rustunnel.service\njournalctl -u rustunnel.service -f\n```\n\n### 9 — Open firewall ports\n\n```bash\nufw allow 80/tcp   comment \"rustunnel HTTP edge\"\nufw allow 443/tcp  comment \"rustunnel HTTPS edge\"\nufw allow 4040/tcp comment \"rustunnel control plane\"\nufw allow 8443/tcp comment \"rustunnel dashboard\"\nufw allow 9090/tcp comment \"rustunnel Prometheus metrics\"\n\n# TCP tunnel port range (must match tcp_port_range in server.toml)\nufw allow 20000:20099/tcp comment \"rustunnel TCP tunnels\"\n\n# UDP tunnel port range (must match udp_port_range in server.toml)\nufw allow 20100:20199/udp comment \"rustunnel UDP tunnels\"\n```\n\n### 10 — Verify the server is running\n\n```bash\n# Health check — use dashboard_port from server.toml (default 8443 in production)\ncurl http://localhost:8443/api/status\n\n# Confirm which ports the process is actually bound to\nss -tlnp | grep rustunnel-serve\n\n# Startup banner is visible in the logs\njournalctl -u rustunnel.service --no-pager | tail -30\n\n# Prometheus metrics\ncurl -s http://localhost:9090/metrics\n```\n\n> **Port reminder**: port 4040 is the control-plane WebSocket (clients connect here),\n> not the dashboard. Hitting it with plain HTTP returns `HTTP/0.9` which is expected.\n> The dashboard is on `dashboard_port` — check your `server.toml` if unsure.\n\n### Updating the server\n\nPull the latest code, rebuild, install, and restart in one command:\n\n```bash\ncd ~/rustunnel && sudo make update-server\n```\n\nThis runs `git pull` → `cargo build --release` → `install` → `systemctl restart` → `systemctl status`.\n\n---\n\n## Docker deployment\n\nA full Docker guide covering both local development (self-signed cert) and\nproduction VPS (Let's Encrypt) is available in\n[**docs/docker-deployment.md**](docs/docker-deployment.md).\n\n### Pull the published image\n\nMulti-arch images (`linux/amd64` + `linux/arm64`) are published to GitHub\nContainer Registry on every stable release — no build required:\n\n```bash\n# Latest stable, or pin a version (e.g. :0.8.1)\ndocker pull ghcr.io/joaoh82/rustunnel-server:latest\n\n# Run it with your server config mounted at /etc/rustunnel/server.toml\ndocker run --rm \\\n  -p 80:80 -p 443:443 -p 4040:4040 -p 8443:8443 \\\n  -v \"$PWD/deploy/server.toml:/etc/rustunnel/server.toml:ro\" \\\n  ghcr.io/joaoh82/rustunnel-server:latest\n```\n\n### Quick reference (build from source)\n\n```bash\n# Build the image locally (includes Next.js dashboard + Rust server)\nmake docker-build\n\n# Build and push a multi-arch image to GHCR (maintainers; needs buildx + ghcr login)\nmake docker-push\n\n# Local development (self-signed cert, no auth required)\ndocker compose -f deploy/docker-compose.local.yml up\n\n# Production VPS (requires deploy/server.toml to be configured first)\nmake docker-run\n\n# Production + Prometheus + Grafana monitoring stack\nmake docker-run-monitoring\n\n# Tail server logs\nmake docker-logs\n\n# Stop everything\nmake docker-stop\n```\n\n### Files\n\n| File | Purpose |\n|------|---------|\n| `deploy/Dockerfile` | Multi-stage build: Node.js UI → Rust server → slim runtime |\n| `deploy/docker-compose.yml` | Production compose file |\n| `deploy/docker-compose.local.yml` | Local development compose file |\n| `deploy/server.toml` | Production server config template |\n| `deploy/server.local.toml` | Local development server config |\n| `deploy/prometheus.yml` | Prometheus scrape config |\n\n---\n\n## Client configuration\n\n### Installation\n\n**Option 1 — Homebrew (macOS and Linux, recommended)**\n\n```bash\nbrew tap joaoh82/rustunnel\nbrew install rustunnel\n```\n\nHomebrew installs pre-built binaries — no Rust toolchain required.\nThe formula is updated automatically on every release. This installs\nboth `rustunnel` (the CLI client) and `rustunnel-mcp` (the MCP server\nfor AI agent integration).\n\n**Option 2 — Pre-built binary**\n\nDownload the archive for your platform from the\n[latest GitHub Release](https://github.com/joaoh82/rustunnel/releases/latest),\nextract it, and move the `rustunnel` binary to a directory on your `$PATH`:\n\n```bash\n# Example for macOS Apple Silicon\ncurl -L https://github.com/joaoh82/rustunnel/releases/latest/download/rustunnel-<version>-aarch64-apple-darwin.tar.gz \\\n  | tar xz\nsudo install -Dm755 rustunnel /usr/local/bin/rustunnel\n```\n\nAvailable targets:\n\n| Platform | Archive |\n|----------|---------|\n| macOS Apple Silicon | `rustunnel-<version>-aarch64-apple-darwin.tar.gz` |\n| macOS Intel | `rustunnel-<version>-x86_64-apple-darwin.tar.gz` |\n| Linux x86_64 (glibc) | `rustunnel-<version>-x86_64-unknown-linux-gnu.tar.gz` |\n| Linux x86_64 (musl, static) | `rustunnel-<version>-x86_64-unknown-linux-musl.tar.gz` |\n| Linux arm64 | `rustunnel-<version>-aarch64-unknown-linux-gnu.tar.gz` |\n| Windows x86_64 | `rustunnel-<version>-x86_64-pc-windows-msvc.zip` |\n\n**Option 3 — Build from source**\n\nRequires Rust 1.76+.\n\n```bash\ngit clone https://github.com/joaoh82/rustunnel.git\ncd rustunnel\ncargo build --release -p rustunnel-client\nsudo install -Dm755 target/release/rustunnel /usr/local/bin/rustunnel\n\n# Or via make\nmake deploy-client\n```\n\n### Setup wizard\n\nThe easiest way to create your config file is the interactive setup wizard:\n\n```bash\nrustunnel setup\n```\n\nIt prompts for your region and auth token, then writes `~/.rustunnel/config.yml` with the correct server address and a commented `tunnels:` example section.\n\n```\nrustunnel setup — create ~/.rustunnel/config.yml\n\nRegion [auto / eu / us / ap / self-hosted] (default: auto):\n  Selecting nearest region… eu 12ms · us 143ms · ap 311ms · → eu (Helsinki, FI) 12ms\n  Server set to: eu.edge.rustunnel.com:4040\n\nAuth token (leave blank to skip): rt_live_abc123...\n\nCreated: /Users/you/.rustunnel/config.yml\nRun `rustunnel start` to connect using this config.\n```\n\nPick a specific region (`eu`, `us`, `ap`) to connect directly, or `auto` (the default) to let the client probe all regions and pick the nearest. Choose `self-hosted` if you run your own server — the wizard will then prompt for your server address.\n\nAfter running setup, use `rustunnel start` to connect with all tunnels defined in the config, or use `rustunnel http <port>` / `rustunnel tcp <port>` for one-off tunnels.\n\n### Quick start (CLI flags)\n\n```bash\n# Expose a local HTTP service on port 3000 (auto-selects nearest region)\nrustunnel http 3000 \\\n  --token YOUR_AUTH_TOKEN\n\n# Connect to a specific region\nrustunnel http 3000 --region eu --token YOUR_AUTH_TOKEN\n\n# Use an explicit server address (bypasses region selection)\nrustunnel http 3000 \\\n  --server edge.rustunnel.com:4040 \\\n  --token YOUR_AUTH_TOKEN\n\n# Expose a local service with a custom subdomain\nrustunnel http 3000 \\\n  --token YOUR_AUTH_TOKEN \\\n  --subdomain myapp\n\n# Expose a local TCP service (e.g. a PostgreSQL database)\nrustunnel tcp 5432 \\\n  --token YOUR_AUTH_TOKEN\n\n# Disable automatic reconnection\nrustunnel http 3000 --no-reconnect\n```\n\n### Machine-readable output (`--json`)\n\nAll tunnel-running commands (`http`, `tcp`, `udp`, `p2p`, `start`) and\n`token create` accept a `--json` flag. With it, stdout emits NDJSON —\none JSON event object per line — instead of the human-readable startup\nbox, which makes the CLI easy to drive from scripts and AI agents:\n\n```bash\nrustunnel http 3000 --json --token YOUR_AUTH_TOKEN\n```\n\n```json\n{\"event\":\"tunnel_ready\",\"protocol\":\"http\",\"public_url\":\"https://myapp.eu.edge.rustunnel.com\",\"local_port\":3000,\"local_host\":\"localhost\",\"tunnel_id\":\"6f9a…\",\"name\":\"myapp\"}\n```\n\nEvents:\n\n| Event | When | Fields |\n|-------|------|--------|\n| `tunnel_ready` | Tunnel registered and accepting traffic | `protocol`, `public_url`, `public_addr` (host:port, tcp/udp only), `local_port`, `local_host`, `tunnel_id`, `name` |\n| `reconnecting` | Connection dropped, retry scheduled | `attempt`, `reason`, `delay_secs` |\n| `reconnected` | Reconnect succeeded (fresh `tunnel_ready` events follow) | — |\n| `error` | Fatal error; process exits with code 1 | `code` (`config`/`auth`/`tunnel`/`connection`/`protocol`/`io`), `message`, `hint` |\n| `token_created` | `token create --json` succeeded | `token`, `name`, `id` |\n\nDiagnostics still go to stderr, so stdout stays valid NDJSON. For the\n`http`, `tcp`, `udp`, and `p2p` commands the auth token can also be\nsupplied via the `RUSTUNNEL_TOKEN` environment variable instead of\n`--token` (`start` reads tokens from the config file).\n\n### Config file\n\nDefault location: `~/.rustunnel/config.yml`\n\n```yaml\n# ~/.rustunnel/config.yml\n\n# Tunnel server address (host:control_port)\nserver: edge.rustunnel.com:4040\n\n# Auth token (from server admin_token or a token created via the dashboard)\nauth_token: YOUR_AUTH_TOKEN\n\n# Region preference: auto (probe & pick nearest), or eu / us / ap.\n# Omit for self-hosted / single-server setups.\nregion: auto\n\n# Named tunnels started with `rustunnel start`\ntunnels:\n  web:\n    proto: http\n    local_port: 3000\n    subdomain: myapp      # optional — server assigns one if omitted\n\n  db:\n    proto: tcp\n    local_port: 5432\n```\n\nStart all tunnels from the config file:\n\n```bash\nrustunnel start\n# or with an explicit path\nrustunnel start --config /path/to/config.yml\n```\n\n### Token management\n\n**Hosted service:** manage tokens from the [rustunnel.com](https://rustunnel.com) dashboard under **Dashboard → API Keys**. You can create, label, and revoke tokens without any CLI commands.\n\n**Self-hosted:** create additional tokens via the dashboard API:\n\n```bash\nrustunnel token create \\\n  --name \"ci-deploy\" \\\n  --server your-server:8443 \\\n  --admin-token YOUR_ADMIN_TOKEN\n```\n\nOr via `curl`:\n\n```bash\ncurl -s -X POST https://your-server:8443/api/tokens \\\n  -H \"Authorization: Bearer YOUR_ADMIN_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"label\": \"ci-deploy\"}'\n```\n\n---\n\n## Port reference\n\n| Port | Protocol | Purpose |\n|------|----------|---------|\n| 80 | TCP | HTTP edge — redirects to HTTPS; also ACME HTTP-01 challenge |\n| 443 | TCP | HTTPS edge — TLS-terminated tunnel ingress |\n| 4040 | TCP | Control-plane WebSocket — clients connect here |\n| 8443 | TCP | Dashboard UI and REST API |\n| 9090 | TCP | Prometheus metrics (`/metrics`) |\n| 20000–20099 | TCP | TCP tunnel range (configurable via `tcp_port_range`) |\n| 20100–20199 | UDP | UDP tunnel range (configurable via `udp_port_range`) |\n\n---\n\n## Config file reference (server)\n\n| Key | Type | Default | Description |\n|-----|------|---------|-------------|\n| `server.domain` | string | — | Base domain for tunnel URLs |\n| `server.http_port` | u16 | — | HTTP edge port |\n| `server.https_port` | u16 | — | HTTPS edge port |\n| `server.control_port` | u16 | — | WebSocket control-plane port |\n| `server.dashboard_port` | u16 | `4040` | Dashboard port |\n| `tls.cert_path` | string | — | Path to TLS certificate (PEM) |\n| `tls.key_path` | string | — | Path to TLS private key (PEM) |\n| `tls.acme_enabled` | bool | `false` | Enable built-in ACME renewal |\n| `tls.acme_email` | string | `\"\"` | Contact email for ACME |\n| `tls.acme_staging` | bool | `false` | Use Let's Encrypt staging CA |\n| `tls.acme_account_dir` | string | `/var/lib/rustunnel` | ACME state directory |\n| `tls.cloudflare_api_token` | string | `\"\"` | Cloudflare DNS API token (prefer env var `CLOUDFLARE_API_TOKEN`) |\n| `tls.cloudflare_zone_id` | string | `\"\"` | Cloudflare Zone ID (prefer env var `CLOUDFLARE_ZONE_ID`) |\n| `auth.admin_token` | string | — | Master auth token |\n| `auth.require_auth` | bool | — | Reject unauthenticated clients |\n| `auth.max_failed_auth_per_minute` | int | `10` | Failed auth attempts allowed per source IP per minute before further attempts are rejected (control-plane handshake and dashboard API); `0` disables |\n| `database.url` | string | — | PostgreSQL connection URL (required) |\n| `database.captured_path` | string | `/var/lib/rustunnel/captured.db` | Per-region SQLite file for captured HTTP request bodies |\n| `server.dashboard_origin` | string | `\"\"` | Allowed CORS origin for the dashboard UI (e.g. `http://localhost:3000`) |\n| `logging.level` | string | — | `trace` / `debug` / `info` / `warn` / `error` |\n| `logging.format` | string | — | `json` or `pretty` |\n| `logging.audit_log_path` | string | `null` | Path for audit log (JSON-lines); omit to disable |\n| `limits.max_tunnels_per_session` | usize | — | Max tunnels per connected client |\n| `limits.max_connections_per_tunnel` | usize | — | Max concurrent connections per tunnel |\n| `limits.rate_limit_rps` | u32 | — | Per-tunnel request rate cap (req/s) |\n| `limits.ip_rate_limit_rps` | u32 | `100` | Per-source-IP rate cap (req/s); `0` = disabled |\n| `limits.request_body_max_bytes` | usize | — | Max proxied request body size (bytes) |\n| `limits.tcp_port_range` | [u16, u16] | — | Inclusive `[low, high]` TCP tunnel port range |\n| `limits.udp_port_range` | [u16, u16] | `[0, 0]` | Inclusive `[low, high]` UDP tunnel port range; `[0, 0]` disables UDP tunnels |\n| `region.id` | string | `\"default\"` | Region identifier recorded in tunnel history (e.g. `\"eu\"`, `\"us\"`, `\"ap\"`) |\n| `region.name` | string | `\"Default\"` | Human-readable region name shown in the dashboard |\n| `region.location` | string | `\"\"` | Physical location label (e.g. `\"Helsinki, FI\"`) |\n\n---\n\n## Monitoring\n\nA Prometheus metrics endpoint is available at `:9090/metrics`:\n\n```\nrustunnel_active_sessions      # gauge: connected clients\nrustunnel_active_tunnels_http  # gauge: active HTTP tunnels\nrustunnel_active_tunnels_tcp   # gauge: active TCP tunnels\n```\n\nStart with the full monitoring stack (Prometheus + Grafana):\n\n```bash\nmake docker-run-monitoring\n# Grafana:    http://localhost:3000  (admin / changeme)\n# Prometheus: http://localhost:9090\n```\n\n> **Production note:** The default Grafana password is `changeme`. Set the `GRAFANA_PASSWORD` environment variable before starting the stack in production:\n> ```bash\n> export GRAFANA_PASSWORD=your-strong-password\n> make docker-run-monitoring\n> ```\n\n---\n\n## REST API\n\nThe dashboard port exposes a REST API for programmatic access to tunnels, tokens, captured requests, and tunnel history. All endpoints (except the health check) require an `Authorization: Bearer <token>` header.\n\n`/api/tunnels`, `/api/groups`, and their per-tunnel sub-resources are scoped by caller: the admin token sees everything; a user-scoped API token sees only its own tunnels and groups; tunnels the caller can't see return `404`, never `403`. See [docs/api-reference.md § Visibility scope](docs/api-reference.md#visibility-scope) for the full rules.\n\n**Quick reference**\n\n| Method | Path | Description |\n|--------|------|-------------|\n| `GET` | `/api/status` | Health check (no auth) |\n| `GET` | `/api/tunnels` | List active tunnels |\n| `GET` | `/api/tunnels/:id` | Get a single tunnel |\n| `DELETE` | `/api/tunnels/:id` | Force-close a tunnel |\n| `GET` | `/api/tunnels/:id/requests` | Captured HTTP requests |\n| `POST` | `/api/tunnels/:id/replay/:req_id` | Fetch stored request for replay |\n| `GET` | `/api/tokens` | List API tokens |\n| `POST` | `/api/tokens` | Create an API token |\n| `DELETE` | `/api/tokens/:id` | Delete an API token |\n| `GET` | `/api/history` | Paginated tunnel history |\n\nFull request/response schemas, query parameters, and examples are in\n[**docs/api-reference.md**](docs/api-reference.md).\n\nA machine-readable OpenAPI 3.0 spec is served at `GET /api/openapi.json` (no auth required).\n\n---\n\n## AI agent integration (MCP server)\n\nrustunnel ships a `rustunnel-mcp` binary that implements the\n[Model Context Protocol](https://spec.modelcontextprotocol.io) over stdio,\nletting AI agents (Claude, GPT-4o, custom agents) open and manage tunnels\nwithout any manual intervention.\n\n**Works with any MCP harness.** For copy-paste config for Claude Code, Claude\nDesktop, Codex, Cursor, Windsurf, Cline, and custom agents — plus a one-command\ninstaller — see the **[Agent Integration Guide](docs/agent-integration.md)**:\n\n```bash\n./integrations/install.sh        # prompts for your token, writes the config\n```\n\n### Quick setup (Claude Desktop — hosted server)\n\n```json\n{\n  \"mcpServers\": {\n    \"rustunnel\": {\n      \"command\": \"rustunnel-mcp\",\n      \"args\": [\n        \"--server\", \"edge.rustunnel.com:4040\",\n        \"--api\",    \"https://edge.rustunnel.com:8443\"\n      ],\n      \"env\": { \"RUSTUNNEL_TOKEN\": \"<your-token>\" }\n    }\n  }\n}\n```\n\nSet `RUSTUNNEL_TOKEN` once and the agent never has to pass a token on a tool call.\n\n### Available tools\n\n| Tool | Description |\n|------|-------------|\n| `create_tunnel` | Open a tunnel and return the public URL — HTTP/TCP/UDP, P2P, and load-balanced pools with health checks |\n| `list_tunnels` | List all active tunnels |\n| `close_tunnel` | Force-close a tunnel by ID |\n| `list_regions` | List available server regions |\n| `get_connection_info` | Return the CLI command for cloud/sandbox agents |\n| `get_tunnel_history` | Retrieve past tunnel activity |\n\n### Installation\n\n**Homebrew (macOS and Linux)** — installs `rustunnel-mcp` alongside the CLI:\n\n```bash\nbrew tap joaoh82/rustunnel\nbrew install rustunnel\n```\n\n**Build from source:**\n\n```bash\nmake release-mcp\nsudo install -m755 target/release/rustunnel-mcp /usr/local/bin/rustunnel-mcp\n```\n\nFull setup guide, configuration options, and workflow examples are in\n[**docs/mcp-server.md**](docs/mcp-server.md).\n\n### Claude Code plugin\n\nThe easiest way to use rustunnel with [Claude Code](https://claude.com/claude-code).\nInstall the plugin and it handles all MCP configuration automatically — just enter\nyour token once and start asking Claude to expose ports.\n\n```\n/plugin install rustunnel\n```\n\nThe plugin prompts for your server address and API token at enable time, stores\nthem securely, and starts the MCP server in the background. No `.mcp.json` editing\nor manual setup needed.\n\n**Plugin directory:** [`plugins/claude-code/`](plugins/claude-code/)\n**Documentation:** [docs/claude-plugin.md](docs/claude-plugin.md)\n\n### OpenClaw skill\n\nrustunnel ships an [OpenClaw](https://openclaw.dev) skill that gives any\nOpenClaw-compatible AI agent first-class knowledge of rustunnel — config file\nformat, authentication, tool signatures, and common workflows — without you\nhaving to explain it.\n\n**Skill file:** [`skills/rustunnel/SKILL.md`](skills/rustunnel/SKILL.md)\n\n**What it covers:**\n\n| Topic | Details |\n|-------|---------|\n| Config file | Location (`~/.rustunnel/config.yml`), format, named tunnels |\n| First-time setup | `rustunnel setup` wizard or manual config creation |\n| MCP tools | `create_tunnel`, `list_tunnels`, `close_tunnel`, `get_connection_info`, `get_tunnel_history` |\n| Workflows | Webhook testing, demo sharing, cloud sandbox (no subprocess), named tunnels |\n| Security | Token handling, file permissions, HTTPS-only transport |\n\n**To load the skill in Claude Code:**\n\n```bash\n/skills load skills/rustunnel/SKILL.md\n```\n\nOnce loaded, you can ask the agent things like:\n\n> \"Expose my local port 3000 as an HTTPS tunnel using rustunnel.\"\n\n> \"List my active tunnels and close the one forwarding port 5432.\"\n\n> \"Set up my rustunnel config file with my token.\"\n\nThe skill instructs the agent to read credentials from `~/.rustunnel/config.yml`\nautomatically, so you won't be prompted for your token on every invocation.\n\n---\n\n## Roadmap\n\nA detailed list of shipped features and planned future work is maintained in\n[**docs/ROADMAP.md**](docs/ROADMAP.md).\n\n---\n\n## Contributing\n\nContributions are welcome! Please follow these steps:\n\n1. **Fork** the repository and create a feature branch from `main`.\n2. Run `make install-hooks` once after cloning to activate the pre-push quality gate.\n3. Make your changes. Ensure `make check` (fmt + Clippy) and `make test` pass locally.\n4. Open a **Pull Request** with a clear description of what changed and why.\n5. A maintainer will review and merge once CI is green.\n\n### Guidelines\n\n- Keep PRs focused — one logical change per PR.\n- Add or update tests for any new behaviour.\n- Follow the existing code style; `cargo fmt` is enforced by CI.\n- For larger changes or new features, open an issue first to discuss the approach.\n\n---\n\n## License\n\nThis project is licensed under the **GNU AGPLv3 License** — see the [LICENSE](LICENSE) file for details.\n\n---\n\n## Contact\n\n**João Henrique Machado Silva**\n\n- GitHub: [@joaoh82](https://github.com/joaoh82)\n- Project: [github.com/joaoh82/rustunnel](https://github.com/joaoh82/rustunnel)\n- Issues & feature requests: [GitHub Issues](https://github.com/joaoh82/rustunnel/issues)\n",
  "bytes": 40149,
  "sha": "2f7fd317ce1cbfe7bc68f831bf6a49a7fc40f5c577e21ed074bd595e7b024302",
  "repo_slug": "joaoh82/rustunnel",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/plg_joaoh82_rustunnel_rustunnel_8d8de14f/readme"
}