Expose I/O — A high-performance, self-hosted tunneling protocol for exposing local services to the internet.
Exio creates secure tunnels from your local machine to a public server, allowing you to expose local HTTP and TCP services to the internet. It's designed to work seamlessly behind Cloudflare Tunnels for DDoS protection and SSL termination.
- HTTP & TCP tunneling - Expose web apps, databases, SSH, game servers, and more
- WebSocket-based transport - Reliably traverses NATs, firewalls, and proxies
- Yamux multiplexing - Multiple concurrent requests over a single connection
- Basic Auth protection - Protect tunnel endpoints with HTTP Basic Authentication
- Rate limiting - Server-side per-tunnel rate limiting to prevent abuse
- QR code display - Quick mobile access with
--qrflag - Clipboard copy - Auto-copy public URL with
--copyflag - Interactive TUI - Real-time request inspection with
--tuiflag - Automatic Host rewriting - Works with development servers that have DNS rebinding protection
- PSK authentication - Simple shared-secret authentication model
- Cloudflare-ready - Designed to sit behind Cloudflare Tunnel for production deployments
- Flexible routing - Path-based (
tunnel.example.com/id/) or subdomain-based (id.tunnel.example.com) routing
Linux / macOS:
curl -fsSL https://raw.githubusercontent.com/SonnyTaylor/exio/main/install.sh | shWindows (PowerShell):
irm https://raw.githubusercontent.com/SonnyTaylor/exio/main/install.ps1 | iexgit clone https://github.com/SonnyTaylor/exio.git
cd exio
make buildDownload from the Releases page.
exio initThis interactive wizard will prompt you for your server URL and authentication token, saving the configuration to ~/.exio.yaml.
HTTP Tunnels:
# Expose local port 3000
exio http 3000
# Request a specific tunnel ID
exio http 3000 --subdomain my-app
# With real-time request viewer
exio http 3000 --tui
# Protect with Basic Auth
exio http 3000 --auth user:password
# Show QR code and copy URL to clipboard
exio http 3000 --qr --copyTCP Tunnels:
# Expose a local database (e.g., PostgreSQL)
exio tcp 5432
# Expose SSH
exio tcp 22 --subdomain my-sshYour service will be available at a URL like:
- HTTP (path mode):
https://tunnel.example.com/my-app/ - HTTP (subdomain mode):
https://my-app.tunnel.example.com - TCP:
tcp://tunnel.example.com:10001(server assigns port)
Alternatively, configure via environment variables:
# Client configuration
export EXIO_SERVER=https://tunnel.example.com
export EXIO_TOKEN=your-secret-token
# Server configuration
export EXIO_PORT=8080
export EXIO_TOKEN=your-secret-token
export EXIO_BASE_DOMAIN=dev.example.com┌─────────────────────────────────────────────────────────────────┐
│ Public Internet │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Cloudflare Edge Network │
│ (SSL Termination, DDoS Protection, WAF) │
└─────────────────────────────────────────────────────────────────┘
│
▼ (cloudflared tunnel)
┌─────────────────────────────────────────────────────────────────┐
│ Exio Server (exiod) │
│ │
│ ┌──────────────┐ ┌──────────────────┐ ┌──────────────────┐ │
│ │ Control Plane│ │ Session Registry │ │ Data Plane │ │
│ │ /_connect │ │ map[subdomain] │ │ HTTP Router │ │
│ │ WebSocket │──│ *Session │──│ Host→Tunnel │ │
│ └──────────────┘ └──────────────────┘ └──────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│
│ WebSocket + Yamux
▼
┌─────────────────────────────────────────────────────────────────┐
│ Exio Client (exio) │
│ │
│ ┌──────────────┐ ┌──────────────────┐ ┌──────────────────┐ │
│ │ Connection │ │ Stream Handler │ │ Local Proxy │ │
│ │ Manager │──│ Accept & Bridge │──│ Host Rewrite │ │
│ └──────────────┘ └──────────────────┘ └──────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Local Development Server │
│ (localhost:3000, etc.) │
└─────────────────────────────────────────────────────────────────┘
Linux / macOS:
curl -fsSL https://raw.githubusercontent.com/SonnyTaylor/exio/main/install-server.sh | sudo shThis will download the server binary and launch the interactive setup wizard.
If you already have the binary installed, run the setup wizard:
sudo exiod initThe wizard will:
- Generate a secure authentication token
- Configure your domain, port, and routing mode
- Create the configuration file (
/etc/exio/exiod.env) - Install and enable the systemd service
- Display the client connection info to share with users
# Create exio user
sudo useradd -r -s /bin/false exio
# Install binary
sudo cp exiod /usr/local/bin/
sudo chmod +x /usr/local/bin/exiod
# Create config directory
sudo mkdir -p /etc/exio
sudo cp deploy/exiod.env.example /etc/exio/exiod.env
sudo chmod 600 /etc/exio/exiod.env
# Edit configuration
sudo nano /etc/exio/exiod.env
# Install and start service
sudo cp deploy/exiod.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable exiod
sudo systemctl start exiodFor path-based routing (recommended):
- Create a Cloudflare Tunnel in your Zero Trust dashboard
- Configure public hostname:
tunnel.example.com - Set service:
http://localhost:8080 - Add a single DNS CNAME record for
tunnelpointing to your tunnel
For subdomain-based routing:
- Create a Cloudflare Tunnel in your Zero Trust dashboard
- Configure public hostname:
*.tunnel.example.com - Set service:
http://localhost:8080 - Add a wildcard DNS CNAME record for
*.tunnel - Requires Cloudflare Advanced Certificate Manager for SSL on
*.tunnel.example.com
The tunnel handles SSL termination; Exio receives plain HTTP.
The client establishes a WebSocket connection to /_connect with:
Authorization: Bearer <token>header for authentication?subdomain=<name>query parameter to request a subdomain
Path-based routing (default):
- Server receives HTTP request for
tunnel.example.com/my-app/api/users - Server extracts tunnel ID (
my-app) from the first path segment - Server rewrites path to
/api/users(strips tunnel ID prefix) - Server looks up session in registry
- Server opens new Yamux stream to client
- Server writes modified HTTP request to stream
- Client reads request, forwards to local service
- Client writes response back to stream
- Server copies response to original HTTP response writer
Subdomain-based routing:
- Server receives HTTP request for
my-app.tunnel.example.com/api/users - Server extracts subdomain (
my-app) from Host header - Server looks up session in registry
- Server opens new Yamux stream to client
- Server writes raw HTTP request to stream
- Client reads request, forwards to local service
- Client writes response back to stream
- Server copies response to original HTTP response writer
By default, the client rewrites the Host header to 127.0.0.1:<port> before forwarding to the local service. This is necessary because many development frameworks (Next.js, Django, Rails) reject requests with unrecognized Host headers as a DNS rebinding protection.
Use --no-rewrite-host to disable this behavior if your local service requires the original Host header.
| Flag | Environment | Description |
|---|---|---|
--server, -s |
EXIO_SERVER |
Server URL (required) |
--token, -t |
EXIO_TOKEN |
Authentication token (required) |
--subdomain |
- | Requested subdomain |
--host |
- | Local host to forward to (default: 127.0.0.1) |
--no-rewrite-host |
- | Don't rewrite Host header (HTTP only) |
--tui |
- | Enable interactive request viewer (HTTP only) |
--auth |
- | Protect with HTTP Basic Auth (user:pass) (HTTP only) |
--qr |
- | Display QR code for the public URL |
--copy |
- | Copy public URL to clipboard |
| Command | Description |
|---|---|
exio init |
Interactive setup wizard |
exio http <port> |
Expose local HTTP service |
exio tcp <port> |
Expose local TCP service (database, SSH, etc.) |
exio version |
Show version information |
| Command | Description |
|---|---|
exiod init |
Interactive server setup wizard (generates token, configures systemd) |
exiod |
Start the server |
exiod version |
Show version information |
| Flag | Environment | Description |
|---|---|---|
--port, -p |
EXIO_PORT |
Listening port (default: 8080) |
--token, -t |
EXIO_TOKEN |
Authentication token (required) |
--domain, -d |
EXIO_BASE_DOMAIN |
Base domain for tunnel URLs (required) |
--routing-mode, -r |
EXIO_ROUTING_MODE |
Routing mode: path (default) or subdomain |
--tcp-port-start |
EXIO_TCP_PORT_START |
Start of TCP port range (default: 10000) |
--tcp-port-end |
EXIO_TCP_PORT_END |
End of TCP port range (default: 20000) |
--rate-limit |
EXIO_RATE_LIMIT |
Rate limit per tunnel (requests/min, 0 = unlimited) |
| Mode | URL Format | SSL Requirements |
|---|---|---|
path |
https://tunnel.example.com/my-app/ |
Standard SSL (free Cloudflare) |
subdomain |
https://my-app.tunnel.example.com |
Wildcard SSL (Advanced Certificate Manager) |
MIT License - see LICENSE for details.