TCP Relay
Expose local TCP services like databases, SSH servers, and custom TCP applications through LocalUp's TCP relay with automatic port allocation and port-based routing.
Overview
The TCP relay enables you to expose any TCP-based service running on your local machine to the internet. Perfect for:
- Databases: PostgreSQL, MySQL, MongoDB, Redis
- SSH Access: Remote access to development machines
- Custom TCP Services: Game servers, IoT devices, custom protocols
Key Features:
- Port-based routing with automatic port allocation
- Configurable port ranges (e.g., 10000-20000)
- Request specific ports or let the relay auto-allocate
- JWT authentication for secure tunnel access
- No TLS termination - pure TCP passthrough
How It Works
Local Service → LocalUp Client → TCP Relay → External Client
:5432 QUIC :16432 connects
tunnel to :16432
Flow:
- TCP relay starts with a configured port range (e.g., 10000-20000)
- Client requests a tunnel (auto-allocate or specific port)
- Relay allocates a port from the range and binds it
- External connections to that port are forwarded through the QUIC tunnel to your local service
- Port is released when tunnel closes
Setup Guide
Step 1: Start the TCP Relay
localup relay tcp \
--localup-addr "0.0.0.0:14443" \
--tcp-port-range "10000-20000" \
--jwt-secret "my-jwt-secret"
Options:
--localup-addr: Control plane address for QUIC connections [default: 0.0.0.0:4443]--tcp-port-range: Port range for TCP tunnels [default: 10000-20000]--jwt-secret: JWT secret for authenticating clients (REQUIRED)--domain: Public domain name [default: localhost]--database-url: Database URL for persistence (optional)
Step 2: Generate Authentication Token
export TOKEN=$(localup generate-token --secret "my-jwt-secret" --sub "mydb" --token-only)
Note: The --sub value is just an identifier for the token. TCP tunnels don't require special JWT claims.
Step 3: Create the Tunnel
Option A: Auto-allocate port (recommended)
localup --port 5432 --protocol tcp --relay localhost:14443 --token "$TOKEN"
# Output: ✅ TCP tunnel created: localhost:16234
The relay automatically selects an available port from the configured range.
Option B: Request specific port
localup --port 5432 --protocol tcp --relay localhost:14443 --remote-port 15432 --token "$TOKEN"
# Output: ✅ TCP tunnel created: localhost:15432
The requested port must be:
- Within the relay's
--tcp-port-range(e.g., 10000-20000) - Not in use by the OS or another tunnel
- Available (check with
lsof -i :PORT)
Port Allocation
Auto-Allocation (Recommended)
Let the relay choose an available port from the configured range:
localup --port <LOCAL_PORT> --protocol tcp --relay <RELAY_ADDR> --token "$TOKEN"
Advantages:
- No port conflicts
- Automatic retry if port is unavailable
- Simpler client command
Manual Port Selection
Request a specific port within the relay's range:
localup --port <LOCAL_PORT> --protocol tcp --relay <RELAY_ADDR> --remote-port <SPECIFIC_PORT> --token "$TOKEN"
Requirements:
- Port must be within
--tcp-port-range(e.g., 10000-20000) - Port must not be in use by OS or other tunnels
- Check availability:
lsof -i :PORT
When to use:
- You need a consistent port for external clients
- You have firewall rules configured for specific ports
- You're documenting connection instructions
Complete Example
Here's a full example exposing a PostgreSQL database:
Terminal 1: Start Relay
localup relay tcp \
--localup-addr "0.0.0.0:14443" \
--tcp-port-range "10000-20000" \
--jwt-secret "my-jwt-secret"
Terminal 2: Generate Token
export TOKEN=$(localup generate-token --secret "my-jwt-secret" --sub "mydb" --token-only)
echo "Token: $TOKEN"
Terminal 3: Expose Local PostgreSQL
Assuming PostgreSQL is running on localhost:5432:
# Auto-allocate port
localup --port 5432 --protocol tcp --relay localhost:14443 --token "$TOKEN" --remote-port=16432
# Wait for: ✅ TCP tunnel created: localhost:16432
Terminal 4: Connect from Anywhere
# Connect using the allocated port
psql -h localhost -p 16432 -U postgres
# Or with connection string
psql "postgresql://postgres@localhost:16432/mydb"
What's happening:
- TCP relay listens on port 14443 for QUIC control connections
- Client authenticates with JWT and requests tunnel for local port 5432
- Relay allocates port 16432 and binds it
- External clients connect to
localhost:16432 - Relay forwards traffic through QUIC tunnel to client
- Client forwards traffic to local PostgreSQL at
localhost:5432
Troubleshooting
"Address already in use" or "Failed to bind to port"
Cause: The requested port is in use by another process or tunnel.
Solution:
# Check what's using the port
lsof -i :16432
# If it's a lingering process, kill it
kill -9 <PID>
# Or use auto-allocation instead of --remote-port
localup --port 5432 --protocol tcp --relay localhost:14443 --token "$TOKEN"
Note: TCP ports stay in TIME_WAIT for 60 seconds after closing. The relay automatically retries binding up to 3 times with 1-second delays.
"Connection refused" when connecting to relay
Cause: Relay is not running or firewall is blocking QUIC.
Solution:
# Verify relay is running
lsof -i :14443
# Check firewall allows UDP (QUIC uses UDP)
# On macOS: System Preferences → Security & Privacy → Firewall
# On Linux: sudo ufw allow 14443/udp
"Authentication failed"
Cause: JWT token doesn't match relay's --jwt-secret.
Solution:
# Regenerate token with correct secret
export TOKEN=$(localup generate-token --secret "my-jwt-secret" --sub "mydb" --token-only)
# Verify secret matches relay's --jwt-secret flag
"Requested port outside allowed range"
Cause: --remote-port is outside the relay's --tcp-port-range.
Solution:
# Check relay's configured range (e.g., 10000-20000)
# Request a port within that range, or use auto-allocation
localup --port 5432 --protocol tcp --relay localhost:14443 --token "$TOKEN"
Tunnel hangs on startup
Cause: Relay server is not running or address is incorrect.
Solution:
# Verify relay is listening
lsof -i :14443
# Ensure relay address matches client's --relay flag
# Client: --relay localhost:14443
# Relay: --localup-addr 0.0.0.0:14443
Next Steps: