{"id":47201319,"url":"https://github.com/joaoh82/rustunnel","last_synced_at":"2026-05-01T23:04:47.379Z","repository":{"id":344016366,"uuid":"1178265503","full_name":"joaoh82/rustunnel","owner":"joaoh82","description":"**Rustunnel** is a open-source tunnel service written in Rust that replicates the core functionality of ngrok. It exposes local services running behind NAT/firewalls to the public internet through a relay server self-hosted or our managed service.","archived":false,"fork":false,"pushed_at":"2026-03-28T23:07:34.000Z","size":8962,"stargazers_count":593,"open_issues_count":0,"forks_count":40,"subscribers_count":2,"default_branch":"main","last_synced_at":"2026-03-29T00:55:41.386Z","etag":null,"topics":["api-gateway","reverse-proxy","rust","rustunnel"],"latest_commit_sha":null,"homepage":"https://www.rustunnel.com","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"agpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/joaoh82.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":"docs/ROADMAP.md","authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":"MAINTAINERS","copyright":"COPYRIGHT","agents":null,"dco":null,"cla":null}},"created_at":"2026-03-10T21:25:25.000Z","updated_at":"2026-03-28T23:07:33.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/joaoh82/rustunnel","commit_stats":null,"previous_names":["joaoh82/rustunnel"],"tags_count":17,"template":false,"template_full_name":null,"purl":"pkg:github/joaoh82/rustunnel","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joaoh82%2Frustunnel","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joaoh82%2Frustunnel/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joaoh82%2Frustunnel/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joaoh82%2Frustunnel/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/joaoh82","download_url":"https://codeload.github.com/joaoh82/rustunnel/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joaoh82%2Frustunnel/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31291678,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-01T13:12:26.723Z","status":"ssl_error","status_checked_at":"2026-04-01T13:12:25.102Z","response_time":53,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["api-gateway","reverse-proxy","rust","rustunnel"],"created_at":"2026-03-13T13:03:40.317Z","updated_at":"2026-05-01T23:04:47.352Z","avatar_url":"https://github.com/joaoh82.png","language":"Rust","funding_links":[],"categories":["Rust"],"sub_categories":[],"readme":"# 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\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---\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  - [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 \u003cid\u003e` 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: \u003cpaste your token\u003e\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\u003e `--insecure` skips TLS certificate verification. Required when using a\n\u003e 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 → \u003cserver IP\u003e`\n- TLS certs: Let's Encrypt via Certbot + Cloudflare DNS challenge\n\n### 1 — Install dependencies\n\n```bash\napt update \u0026\u0026 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 \u003c\u003c'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\u003e **Tip:** For managed PostgreSQL (e.g. AWS RDS, DigitalOcean Managed Database, Supabase) skip the\n\u003e `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\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 \u003e /etc/letsencrypt/cloudflare.ini \u003c\u003c'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 \u003e /etc/letsencrypt/renewal-hooks/deploy/restart-rustunnel.sh \u003c\u003c'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\u003e **Port reminder**: port 4040 is the control-plane WebSocket (clients connect here),\n\u003e not the dashboard. Hitting it with plain HTTP returns `HTTP/0.9` which is expected.\n\u003e 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 \u0026\u0026 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### Quick reference\n\n```bash\n# Build the image (includes Next.js dashboard + Rust server)\nmake docker-build\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-\u003cversion\u003e-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-\u003cversion\u003e-aarch64-apple-darwin.tar.gz` |\n| macOS Intel | `rustunnel-\u003cversion\u003e-x86_64-apple-darwin.tar.gz` |\n| Linux x86_64 (glibc) | `rustunnel-\u003cversion\u003e-x86_64-unknown-linux-gnu.tar.gz` |\n| Linux x86_64 (musl, static) | `rustunnel-\u003cversion\u003e-x86_64-unknown-linux-musl.tar.gz` |\n| Linux arm64 | `rustunnel-\u003cversion\u003e-aarch64-unknown-linux-gnu.tar.gz` |\n| Windows x86_64 | `rustunnel-\u003cversion\u003e-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 \u003cport\u003e` / `rustunnel tcp \u003cport\u003e` 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### 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 \u0026 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| `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\u003e **Production note:** The default Grafana password is `changeme`. Set the `GRAFANA_PASSWORD` environment variable before starting the stack in production:\n\u003e ```bash\n\u003e export GRAFANA_PASSWORD=your-strong-password\n\u003e make docker-run-monitoring\n\u003e ```\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 \u003ctoken\u003e` header.\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### 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    }\n  }\n}\n```\n\n### Available tools\n\n| Tool | Description |\n|------|-------------|\n| `create_tunnel` | Spawn a tunnel and return the public URL |\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\u003e \"Expose my local port 3000 as an HTTPS tunnel using rustunnel.\"\n\n\u003e \"List my active tunnels and close the one forwarding port 5432.\"\n\n\u003e \"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 \u0026 feature requests: [GitHub Issues](https://github.com/joaoh82/rustunnel/issues)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjoaoh82%2Frustunnel","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjoaoh82%2Frustunnel","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjoaoh82%2Frustunnel/lists"}