Documentation

Everything you need to know.

Install, configure, and use tunnels.io to expose your local services to the internet securely. From your first tunnel to advanced configurations.

Getting started

What is tunnels.io?

tunnels.io is a secure tunneling service that exposes your local development servers, APIs, and services to the public internet. Whether you are testing webhooks from Stripe or GitHub, demoing an application to a client, or debugging a mobile app that needs to reach your local backend, tunnels.io creates an encrypted tunnel from a public URL directly to a port on your machine.

When you start a tunnel, you get a public URL such as https://myapp.tunnels.host. Any HTTP or TCP traffic sent to that URL is forwarded through an encrypted TLS connection to your local service. No firewall config, no router port-forwarding, no remote deploy.

System requirements

The client is a single static binary with no external dependencies.

PlatformArchitectureMinimum OS
Linuxamd64, arm64, 386, armKernel 3.10+
macOSamd64 (Intel), arm64 (Apple Silicon)macOS 11 (Big Sur)
Windowsamd64, 386, arm64Windows 10
FreeBSDamd64, arm64FreeBSD 12+

Quick install (recommended)

One command. The installer detects your OS and architecture, downloads the correct binary, verifies the checksum, and installs it.

# Linux, macOS, FreeBSD
curl -fsSL https://tunnels.io/install.sh | bash

# Or with wget
wget -qO- https://tunnels.io/install.sh | bash

# Windows (PowerShell)
Invoke-WebRequest -Uri https://download.tunnels.io/0.0.70/windows/amd64/tunnels.exe -OutFile tunnels.exe

On Linux, macOS, and FreeBSD the installer places the binary in /usr/local/bin/tunnels (or ~/.local/bin/tunnels without sudo).

Manual installation

Prefer to install by hand? Download from the Downloads page or:

# Linux (amd64)
curl -Lo tunnels https://download.tunnels.io/0.0.70/linux/amd64/tunnels
# macOS (Apple Silicon)
curl -Lo tunnels https://download.tunnels.io/0.0.70/darwin/arm64/tunnels
chmod +x tunnels
sudo mv tunnels /usr/local/bin/

Verify the installation:

tunnels version
# 0.0.70

Creating your first tunnel

Have a local server on port 8000? Expose it:

tunnels http 8000

tunnels.io connects, allocates a public URL, and forwards traffic:

Tunnel Status: online
Version:       0.0.70
Web Interface: http://127.0.0.1:4040
Forwarding:    https://a1b2c3d4.tunnels.host -> http://localhost:8000
Web Interface: a local inspection UI at http://localhost:4040 shows every request through the tunnel with its method, path, status, duration, and full request and response headers. Invaluable for debugging webhooks.

Signing up and getting an auth token

An account unlocks static subdomains, multiple tunnels, TCP, and higher limits:

  1. Visit tunnels.io/signup and create your account.
  2. Verify your email via the link sent to your inbox.
  3. Your initial CLI token (tnl_…) is shown in your dashboard. Copy it.
  4. Create additional tokens from Account > API Tokens.

Using your auth token

tunnels config add-authtoken tnl_xxxxxxxxxxxxxxxxxx
tunnels http 8000

Installation

tunnels.io ships as a single static binary, no dependencies, runtimes, or package managers. Fastest path:

# Automatic install (detects OS + architecture)
curl -fsSL https://tunnels.io/install.sh | bash

Linux

amd64, arm64, 386, and arm.

curl -fsSL https://tunnels.io/install.sh | bash
# Or manual (amd64)
curl -Lo tunnels https://download.tunnels.io/0.0.70/linux/amd64/tunnels
chmod +x tunnels && sudo mv tunnels /usr/local/bin/ && tunnels version

macOS

Intel (amd64) and Apple Silicon (arm64).

# Apple Silicon
curl -Lo tunnels https://download.tunnels.io/0.0.70/darwin/arm64/tunnels
chmod +x tunnels
macOS Gatekeeper: the macOS binary is signed with a Developer ID certificate and notarized by Apple, so it runs without a Gatekeeper prompt. If you moved or rewrote the file after downloading and macOS still blocks it, clear the quarantine flag once with xattr -d com.apple.quarantine ./tunnels.

Windows

# PowerShell
Invoke-WebRequest -Uri https://download.tunnels.io/0.0.70/windows/amd64/tunnels.exe -OutFile tunnels.exe
.\tunnels.exe version

FreeBSD

curl -Lo tunnels https://download.tunnels.io/0.0.70/freebsd/amd64/tunnels
chmod +x tunnels && sudo mv tunnels /usr/local/bin/

Verifying with SHA-256 checksums

Every release includes checksums; the install script verifies automatically. Manually:

curl -fsSL https://download.tunnels.io/checksums.json
sha256sum tunnels          # Linux
shasum -a 256 tunnels      # macOS

Adding to PATH

PlatformLocationCommand
Linux/usr/local/bin/sudo mv tunnels /usr/local/bin/
macOS/usr/local/bin/sudo mv tunnels /usr/local/bin/
WindowsC:\Program Files\Tunnels\Add directory to PATH
FreeBSD/usr/local/bin/sudo mv tunnels /usr/local/bin/

CLI reference

Basic syntax

tunnels [options] <local-address>

The <local-address> is the local service to expose: a port (8080), host:port (localhost:8080), or IP:port. A bare port assumes localhost.

Command-line flags

FlagDefaultDescription
--authtoken TOKENnoneYour auth token (begins tnl_). Required for static subdomains, multiple tunnels, and paid features. Also settable via config or tunnels config add-authtoken.
--subdomain NAMErandomRequest a specific subdomain (Pro+). 4-63 chars, lowercase alphanumeric and hyphens.
--proto PROTOCOLhttpshttp or tcp. TCP requires Pro or higher.
--config FILE~/.tunnelsPath to the YAML config file. Auto-loaded from the current directory if present.
--log DESTnonestdout, stderr, none, or a file path.
--log-level LEVELinfodebug, info, warn, error.
start-allN/AStart every tunnel defined in your config file.

Usage examples

# Basic HTTP tunnel
tunnels localhost:3000

# Authenticated, static subdomain (Pro+)
tunnels --authtoken tnl_abc123 --subdomain myapi localhost:8080
# -> https://myapi.tunnels.host

# TCP tunnel for a database
tunnels --proto tcp --authtoken tnl_abc123 localhost:5432


# Debug logging to a file
tunnels --log /tmp/tunnels-debug.log --log-level debug localhost:8000

# Start all tunnels from config
tunnels --config ./my-tunnels.yaml start-all

Configuration

Settings live in ~/.tunnels, a YAML file the client reads on every run. Point at a different file with --config path/to/file. The easiest way to create it is tunnels config add-authtoken TOKEN, which writes the file for you.

File format

Note the shape of proto: it is a map of protocol to local address, not a string. There is no separate addr field.

# ~/.tunnels
server_addr: tunnels.host:443
auth_token: tnl_xxxxxxxxxxxxxxxxxxxxxxxx
inspect_addr: 127.0.0.1:4040

tunnels:
  webapp:
    subdomain: myapp
    proto:
      http: localhost:3000
  database:
    proto:
      tcp: localhost:5432

Start them with tunnels start webapp, or tunnels start-all.

Configuration fields

FieldTypeDescription
server_addrstringTunnel server address. Default tunnels.host:443.
auth_tokenstringAuth token (begins tnl_). Same as --authtoken. Set it with tunnels config add-authtoken.
inspect_addrstringAddress for the local request inspector. Default 127.0.0.1:4040. Set it to disabled to turn the inspector off. Left at the default, a second client automatically takes the next free port (4041, 4042, and so on) and prints the one it used. Set it explicitly and the client binds that address or reports that it could not.
http_proxystringOutbound proxy for the connection to the tunnel server.
trust_host_root_certsboolUse the operating system trust store for the server connection.
tunnelsmapNamed tunnel definitions. Each entry takes proto (a map of protocol to local address), and optionally subdomain, domain, hostname, auth and remote_port.

Custom domains

On Gold and Premium plans, use your own domain instead of a *.tunnels.host subdomain:

  1. Add a CNAME pointing your domain to tunnels.host:
    myapp.example.com    CNAME    tunnels.host.
  2. Wait for DNS propagation (5-30 min typical).
  3. Start with the -hostname flag:
    tunnels --hostname myapp.example.com --authtoken tnl_xxx localhost:3000

TLS certificates for custom domains are provisioned and renewed automatically via Let's Encrypt.

Environment variables

The client reads the variables below. Note the prefix is TUNELS_ with a single N, which does not match the product name. There is no environment override for the auth token: set it in the config file with tunnels config add-authtoken, or pass --authtoken.

Environment VariableEffect
TUNELS_NO_UPDATE_CHECKSet to 1 to skip the once-a-day update check and its banner. Recommended in CI.
TUNELS_NO_TELEMETRYSet to 1 to disable telemetry.
TUNELS_API_BASE_URLOverride the platform API base the client talks to. Rarely needed.
http_proxyStandard outbound proxy for the connection to the tunnel server.
Priority (highest to lowest): command-line flags, then the config file, then built-in defaults.

API reference

The REST API manages your account, tokens, subscriptions, and usage metrics. All responses are JSON.

Base URL

https://tunnels.io/api/v1

Authentication

Authenticate with a JWT Bearer token (from login) or a CLI token, in the Authorization header. JWT access tokens expire after 15 minutes; CLI tokens don't expire unless revoked.

Authorization: Bearer eyJhbGciOiJSUzI1NiIs...     # JWT
Authorization: Bearer tnl_xxxxxxxxxxxxxxxx     # CLI token

Response format

// Success
{ "data": { ... }, "error": null }
// Error
{ "data": null, "error": "Description of what went wrong" }

Endpoints

POST/api/v1/auth/register

Create a new account. No auth required.

{ "email": "[email protected]", "password": "MySecureP@ss1", "first_name": "Jane" }

Password: 8-72 chars; ≥1 upper, lower, digit, special.

POST/api/v1/auth/login

Authenticate and receive a JWT token pair.

{ "access_token": "eyJ…", "refresh_token": "eyJ…", "token_type": "Bearer" }
GET/api/v1/auth/meAuth required

The current user's profile (email, plan, status, verified).

GET/api/v1/plans

List all subscription plans (code, name, price, limits). No auth.

GET/api/v1/subscriptionAuth required

Active subscription, plan, billing cycle, and bandwidth used.

POST/api/v1/tokensAuth required

Create a CLI token. The full tnl_… value is returned only once.

GET/api/v1/tokensAuth required

List CLI tokens (values masked to last 4 chars).

DELETE/api/v1/tokens/{id}Auth required

Revoke a token. Active tunnels using it are terminated immediately.

GET/api/v1/metrics/currentAuth required

Current period usage: bandwidth, active tunnels, total requests.

HTTP status codes

200Request succeeded
201Resource created
400Bad request (validation error)
401Unauthorized
403Forbidden (plan/permission)
404Not found
409Conflict (e.g. email already registered)
429Rate limited
500Internal server error
Rate limiting: the API limits per IP. Exceeding it returns 429 Too Many Requests with a Retry-After header.

Protocols

Two transport protocols: HTTP/HTTPS and TCP. TCP requires a paid plan.

HTTP/HTTPS tunneling

The default. You get a public HTTPS URL (e.g. https://myapp.tunnels.host); the server terminates TLS and forwards plain HTTP to your local service.

  • Automatic HTTPS — every tunnel gets a valid TLS cert, no config.
  • Request inspection — the local UI at localhost:4040.
  • Host header rewriting — for virtual-host local setups.
  • WebSocket supportwss:// works automatically.

TCP tunneling

Forward raw TCP for databases, SSH, game servers (Pro+). You get a public host:port like 0.tcp.tunnels.host:12345.

# PostgreSQL
tunnels --proto tcp --authtoken tnl_xxx localhost:5432
# psql -h 0.tcp.tunnels.host -p 12345 -U user db

# SSH
tunnels --proto tcp --authtoken tnl_xxx localhost:22
Security warning: TCP tunnels expose raw access. Ensure your service has authentication (DB passwords, SSH keys) and use IP restrictions on Gold/Premium.

Protocol availability by plan

ProtocolBasicStudentProGoldPremium
HTTP/HTTPS
TCP
WebSocket

Security

TLS encryption

All traffic between the internet and the server is TLS 1.2+. The control channel between client and server uses a dedicated TLS connection. HTTP tunnels automatically receive a valid HTTPS certificate; the data channel is encrypted for TCP tunnels too.

Authentication methods

  • Basic Auth (Pro+)tunnels -auth="user:password" localhost:8000
  • OAuth / Google (Pro+) — restrict access to Google-authenticated users.
  • IP allowlisting (Gold/Premium)tunnels -cidr-allow="203.0.113.0/24" localhost:8000

Token security

CLI tokens (tnl_) are 64+ chars of cryptographically random data, the keys to your account.

Best practices

Do

  • Store tokens in env vars or chmod 600 config files
  • Use separate tokens per environment
  • Rotate tokens periodically
  • Use IP restrictions for sensitive services
  • Add config files containing tokens to .gitignore

Do not

  • Commit auth tokens to version control
  • Share tokens in chat, email, or forums
  • Expose production databases without IP restrictions
  • Leave tunnels running unauthenticated
  • Reuse one token across team members
If a token is compromised: revoke it immediately from Account > API Tokens or DELETE /api/v1/tokens/{id}. Active tunnels are terminated instantly. Then create a new one.

Plans & limits

Plan comparison

FeatureBasicStudentProGoldPremium
ProtocolsHTTP/HTTPSHTTP/HTTPS+ TCP+ TCP+ TCP
SubdomainRandomRandomStaticCustom domainCustom domain
SupportCommunityForumEmailPriority emailPriority + Slack

Current prices, bandwidth allowances, request-rate limits and tunnel counts are on the pricing page, which reads them live from the plan catalog.

The Student plan requires verification with a valid university email address.

Rate limiting (RPS)

Each plan has a max requests-per-second. A token-bucket algorithm with a burst multiplier allows short spikes at 2-5x. Requests over the limit get a 429 from the server, your local service never sees them.

Bandwidth enforcement

  • 80% usage — email notification.
  • 100% usage — a 24-hour grace period; traffic keeps flowing.
  • Grace expired — new connections denied until next period or upgrade.
Need more? Upgrade anytime from the Pricing page. Changes take effect immediately and are prorated.

Troubleshooting

"Connection refused" or "502 Bad Gateway"
Cause: the tunnel is up but your local service isn't listening on that address/port. Fix: verify with curl http://localhost:8000; match the port; try 127.0.0.1:8000 if your service binds there specifically.
"Authentication failed" / "Invalid auth token"
Cause: token invalid, revoked, or account unverified. Fix: confirm it starts with tnl_ and isn't truncated; check it isn't revoked; verify your email; create a new token.
"Tunnel limit exceeded"
Cause: at your plan's concurrent-tunnel cap. Fix: close unused tunnels (Ctrl+C); orphaned ones auto-clean within minutes; upgrade for more.
macOS: "developer cannot be verified"
The macOS binary is Developer ID signed and notarized, so this should not happen on a fresh download. It can occur if the file was moved, rewritten or unpacked by a tool that dropped the signature. Fix: re-download it, or clear the quarantine flag with xattr -d com.apple.quarantine ./tunnels
Firewall / corporate proxy blocking connection
Fix: allow outbound TCP to tunnels.host:443; behind an HTTP proxy set HTTPS_PROXY; test from a mobile hotspot to confirm it's the network.
Slow tunnel performance
Fix: throughput can't exceed your upload bandwidth; check for 429s in the inspector; disable inspection for high throughput (inspect_addr: disabled); a slow backend looks like a slow tunnel.
Inspector is on a different port, or shows another tunnel's traffic
Each running client needs its own inspector port. When you start a second client, it takes the next free port after 4040 and prints the address it actually bound, so read the Web Interface line rather than assuming 4040. Any inspector page also lists the other clients running on your machine, so you can move between them. Fix for older clients: versions before this behaviour shipped kept printing 127.0.0.1:4040 even when the port was already taken, which sent you to the first client's traffic; upgrade with tunnels update. To put several tunnels in one inspector instead, run them from one process with tunnels start-all.

Enabling debug logs

tunnels --log stdout --log-level debug localhost:8000
# Or to a file for support
tunnels --log /tmp/tunnels-debug.log --log-level debug localhost:8000

Deployment

Integrate tunnels.io into CI/CD pipelines, Docker workflows, and team setups for staging and deploy previews.

GitHub Actions example

# .github/workflows/e2e-tests.yml
name: E2E Tests
on: [pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - name: Install tunnels
        run: curl -fsSL https://tunnels.io/install.sh | bash
      - name: Start tunnel
        env: { TUNELS_NO_UPDATE_CHECK: "1" }
        run: |
          tunnels config add-authtoken ${{ secrets.TUNNELS_TOKEN }}
          tunnels --subdomain pr-${{ github.event.number }} http 3000 &

Deploy previews for pull requests

Give every PR its own public URL using the PR number as the subdomain: https://pr-42.tunnels.host. Post the URL as a bot comment so reviewers click once.

Docker integration

# docker-compose.yml
services:
  app:
    build: .
    ports: ["3000:3000"]
  tunnel:
    image: alpine:latest
    depends_on: [app]
    environment: ["TUNNELS_TOKEN=${TUNNELS_TOKEN}"]
    command: sh -c "wget -qO- https://tunnels.io/install.sh | bash && tunnels config add-authtoken $TUNNELS_TOKEN && tunnels --subdomain myapp http app:3000"

Team collaboration

  1. Each developer has a personal account + token.
  2. CI/CD uses a dedicated service-account token stored as a repo secret.
  3. Subdomain convention: {project}-{developer} (e.g. api-jane).
  4. Shared staging: a team service account for always-on tunnels.
Pro tip: run tunnels config add-authtoken TOKEN once. It saves the token to ~/.tunnels so every later command picks it up automatically.