Expose any local port as a public URL in seconds.
tunnel 8080
# https://8080.yourdomain.com
tunnel runs a local reverse proxy and routes traffic through a Cloudflare Tunnel. Any subdomain matching <port>.yourdomain.com is forwarded to localhost:<port> — no port forwarding, no firewall rules.
curl -fsSL https://raw.githubusercontent.com/tunnel-ops/tunnel/main/install.sh | bashInstalls tunnel and requests-proxy to /usr/local/bin. macOS and Linux (amd64/arm64) are supported.
To install a specific version:
VERSION=v1.0.0 bash <(curl -fsSL https://raw.githubusercontent.com/tunnel-ops/tunnel/main/install.sh)tunnel setupThe TUI wizard handles everything in one go:
- Installs
cloudflaredif missing (via Homebrew on macOS) - Authenticates with Cloudflare
- Creates a named tunnel
- Configures the wildcard DNS record (
*.yourdomain.com) - Writes the cloudflared config
- Optionally installs both services to auto-start on login
Supported DNS providers: Cloudflare, GoDaddy, Namecheap, Manual.
API credentials for GoDaddy and Namecheap are stored in the macOS Keychain — never written to disk as plaintext.
# Your dev server
npm run dev # running on :5173
# Register and print the public URL
tunnel 5173
# https://5173.yourdomain.comtunnel warns if nothing is listening on the port yet and auto-registers it so it shows up in tunnel list.
tunnel welcome Show welcome screen and quick-start info
tunnel setup First-time configuration wizard
tunnel <port> Print the public URL for a numeric port
tunnel <port> --open Print and open in browser
tunnel <p1> <p2> ... Register and print multiple ports at once
tunnel --name <name> <port> Register a named subdomain and print URL
tunnel --name <name> <port> --open Register, print, and open in browser
tunnel close <port|name> Remove a registered tunnel
tunnel close <p1> <p2> ... Remove multiple tunnels at once
tunnel rm <name> Alias for close
tunnel list List active tunnels
tunnel list -a List all registered tunnels (including inactive and blocked)
tunnel watch Live request monitor — all ports
tunnel watch <port> Live request monitor — one port
tunnel block <port> Block a port from being exposed
tunnel unblock <port> Remove a port block
tunnel update Check for a newer release and apply it
tunnel update --enable Enable automatic updates
tunnel update --disable Disable automatic updates
tunnel help Show this help
# Numeric port
tunnel 3000
# https://3000.yourdomain.com
# Multiple ports at once
tunnel 3000 4000 5173
tunnel close 3000 4000 5173
# Named subdomain
tunnel --name api 8080
# https://api.yourdomain.com
# Open in browser immediately
tunnel --name app 5173 --open
# See everything running
tunnel list
# See all registered (including inactive and blocked)
tunnel list -a
# Monitor live traffic
tunnel watch
tunnel watch 3000
# Block/unblock a port
tunnel block 5432
tunnel unblock 5432
# Remove when done
tunnel close api
tunnel close 3000
# Updates
tunnel update
tunnel update --enablebrowser → Cloudflare edge → cloudflared tunnel → requests-proxy (:7999) → localhost:<port>
**requests-proxy**is a reverse proxy that listens on a single port (default7999). It reads theHostheader, extracts the subdomain, resolves it to a local port, and forwards the request to127.0.0.1:<port>.**cloudflared**maintains the tunnel from Cloudflare's edge torequests-proxy, so inbound traffic never requires an open firewall port.- Named subdomains (e.g.
api.yourdomain.com) are registered in~/.config/requests/names.jsonand resolved by the proxy at request time. - WebSocket connections are proxied transparently (HMR works out of the box).
| Concern | Mitigation |
|---|---|
| SSRF | Proxy always forwards to 127.0.0.1 — never follows arbitrary targets |
| Sensitive ports | SSH (22), SMTP (25), MySQL (3306), PostgreSQL (5432), Redis (6379), MongoDB (27017+) and others are blocked by default |
| Port blocking | tunnel block <port> kills the process and prevents future registration; CLI rejects blocked ports before they are saved |
| Subdomain validation | Only single-level subdomains matching the configured domain are accepted; nested subdomains return 403 |
| Body size | Requests over 10 MB are rejected (configurable via MAX_BODY_MB) |
| API credentials | Stored in macOS Keychain via the security CLI; never written to disk |
After tunnel setup, the domain is saved to ~/.config/requests/config.json and no environment variables are needed for daily use.
You can override any setting with environment variables:
| Variable | Default | Description |
|---|---|---|
DOMAIN |
from config | Your base domain (e.g. yourdomain.com) |
PROXY_PORT |
7999 |
Port requests-proxy listens on |
MAX_BODY_MB |
10 |
Maximum request body size in MB |
BLOCKED_PORTS |
(built-in list) | Comma-separated list of blocked ports |
READ_TIMEOUT |
30s |
HTTP read timeout |
WRITE_TIMEOUT |
30s |
HTTP write timeout |
IDLE_TIMEOUT |
120s |
HTTP idle timeout |
User-blocked ports are stored in ~/.config/requests/blocked.json. Auto-update preference is stored in ~/.config/requests/config.json (autoUpdate field) and can be toggled with tunnel update --enable / --disable.
git clone https://github.com/tunnel-ops/tunnel
cd tunnel
make build
# → bin/tunnel
# → bin/requests-proxyRequires Go 1.21+.
# Run tests
go test ./...
# Install as background services (after setup)
make install DOMAIN=yourdomain.comMIT — see LICENSE.