Skip to content

jbingen/tnnl

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

16 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

🚇 tnnl

Expose localhost to the internet. Single binary, no account required.

demo

Quickstart

The public server at tnnl.run is free to use:

curl -fsSL https://tnnl.run/install.sh | sh
tnnl http 3000
# → https://abc12345.tnnl.run is live

Every request is logged as it comes in:

00:05  #1  GET    /                          200  16ms
00:09  #2  GET    /api/users                 200  20ms
00:14  #3  POST   /api/webhooks              201  14ms

Install

One-line (Linux / macOS):

curl -fsSL https://tnnl.run/install.sh | sh

Prebuilt binary: releases page

Via cargo:

cargo install tnnl-cli

From source:

cargo install --path .

Debugging webhooks

The main reason to reach for tnnl over a quick SSH tunnel. Pass --inspect to see full headers and body for every request and response:

tnnl http 3000 --inspect
00:14  #3  POST   /api/webhooks              201  14ms

  →  POST /api/webhooks  #3
     Content-Type: application/json
     Stripe-Signature: t=1234567890,v1=abc123...

     {
       "type": "charge.succeeded",
       "data": { "amount": 9900, "currency": "usd" }
     }

  ←  201 Created
     Content-Type: application/json

     {"received": true}

Every request is saved locally. Fix your handler and replay without touching the sender:

tnnl replay 3

IDs persist across restarts so you can replay old requests after a fresh start.

Config file

Persistent settings live in ~/.tnnl.toml:

server    = "tnnl.run"
token     = "your-secret"  # omit for open servers
subdomain = "myapp"        # same URL every time
auth      = "user:pass"    # HTTP basic auth on the tunnel

CLI flags always win over the config file.

Protecting your tunnel

Gate the exposed URL with HTTP basic auth. Unauthenticated requests are rejected before they touch your local service:

tnnl http 3000 --auth user:pass

How it works

One binary, two modes. tnnl server runs on a VPS, tnnl http runs on your machine.

  1. Client connects over TCP and authenticates with HMAC-SHA256 - the secret never crosses the wire
  2. Server assigns a subdomain (random, or pin one with --subdomain)
  3. Incoming HTTP traffic is routed by Host header to the right client
  4. Everything flows over a single multiplexed connection via yamux - no TCP handshake per request
  5. Client proxies to localhost and pipes the response back

502 if localhost isn't up. Exponential backoff reconnect if the connection drops.

Self-hosting

tnnl server --domain tunnel.example.com
# or with a shared secret
tnnl server --domain tunnel.example.com --token supersecret

Omitting --token makes it an open server. Either way, abuse protection is always on: per-IP rate limiting, per-IP tunnel caps, global tunnel limit.

Server flags

Flag Default Description
--domain required Base domain for tunnel subdomains
--token none Secret clients must know - omit for open server
--control-port 9443 Port for client connections
--http-port 8080 Port for public HTTP traffic

TLS

tnnl terminates plain HTTP on --http-port. Put Caddy or nginx in front for TLS with a wildcard cert.

Caddy:

*.tunnel.example.com {
    tls {
        dns cloudflare {env.CF_API_TOKEN}
    }
    reverse_proxy localhost:8080
}

nginx:

server {
    listen 443 ssl;
    server_name *.tunnel.example.com;

    ssl_certificate     /etc/letsencrypt/live/tunnel.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/tunnel.example.com/privkey.pem;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

DNS: wildcard A record pointing to your VPS:

*.tunnel.example.com.  A  <your-vps-ip>

CLI reference

tnnl http <PORT>

Flag Default Description
--to tnnl.run Server address
--token config / none Shared secret for authentication
--subdomain random Request a specific subdomain
--auth none Protect tunnel with HTTP basic auth (user:pass)
--control-port 9443 Server control port
--inspect off Print full request/response headers and body

tnnl replay <id>

Re-send a captured request to your local server. IDs are shown in the request log.

tnnl server

See Self-hosting above.

Why tnnl

tnnl ngrok bore frp
Self-hosted
No account required
Public shared server
Single binary
HTTP subdomain routing
Auth HMAC HMAC HMAC token
Config file required
Auto-reconnect
Request inspection
Replay
Tunnel basic auth paid
Free limited

License

MIT

About

Expose localhost to the internet. Single binary, no account required.

Topics

Resources

License

Stars

Watchers

Forks

Packages