TLS/SNI Relay
Expose TLS-encrypted services with end-to-end encryption using SNI-based routing. The relay performs TLS passthrough without terminating the connection, preserving your end-to-end security.
Overview
The TLS/SNI relay enables you to expose TLS-encrypted services while maintaining end-to-end encryption. The relay uses Server Name Indication (SNI) from the TLS ClientHello to route connections without decrypting traffic.
Perfect for:
- Secure APIs: TLS-based APIs that require end-to-end encryption
- Custom TLS Services: Any service using TLS/SSL
- Multi-tenant Applications: Host-based routing without certificate management on relay
Key Features:
- SNI-based routing from TLS ClientHello
- TLS passthrough (no termination at relay)
- End-to-end encryption preserved
- No certificates needed on relay server
- JWT authentication for tunnel access
Why use TLS relay instead of TCP relay?
- Host-based routing (multiple services on one port)
- Automatic SNI extraction and routing
- Better for web services and APIs
How It Works
External Client → TLS Relay (SNI routing) → QUIC Tunnel → LocalUp Client → Local TLS Service
connects to extracts SNI encrypted forwards to
relay:18443 (api.example.com) tunnel localhost:3443
Flow:
- External client initiates TLS connection to relay (e.g.,
relay.example.com:18443) - Client sends TLS ClientHello with SNI:
api.example.com - Relay extracts SNI hostname without decrypting
- Relay routes connection to tunnel with matching subdomain
- Traffic passes through QUIC tunnel to LocalUp client
- Client forwards to local TLS service (e.g.,
localhost:3443) - End-to-end TLS encryption maintained throughout
Important: The relay NEVER sees your plaintext traffic. It only inspects the SNI field in the TLS handshake to route connections.
Setup Guide
Step 1: Start the TLS/SNI Relay
localup relay tls \
--localup-addr "0.0.0.0:14443" \
--tls-addr "0.0.0.0:18443" \
--jwt-secret "my-jwt-secret"
Options:
--localup-addr: Control plane address for QUIC connections [default: 0.0.0.0:4443]--tls-addr: TLS/SNI server address for external clients [default: 0.0.0.0:4443]--jwt-secret: JWT secret for authenticating clients (REQUIRED)--domain: Public domain name [default: localhost]--database-url: Database URL for persistence (optional)
Note: The relay does NOT need TLS certificates. It performs passthrough only.
Step 2: Generate Authentication Token
export TOKEN=$(localup generate-token --secret "my-jwt-secret" --sub "api" --token-only)
Step 3: Start Your Local TLS Service
For testing, you can use OpenSSL's built-in TLS server:
# Generate self-signed certificates for your local service (one-time)
openssl req -x509 -newkey rsa:2048 -keyout tls-service-key.pem -out tls-service-cert.pem \
-days 365 -nodes -subj "/CN=localhost"
# Start OpenSSL TLS server on port 3443
openssl s_server -cert tls-service-cert.pem -key tls-service-key.pem \
-accept 3443 -www
Or use any TLS-enabled service (HTTPS server, TLS-enabled database, etc.).
Step 4: Create the Tunnel
localup --port 3443 --protocol tls --relay localhost:14443 --subdomain api.example.com --token "$TOKEN"
# Output: ✅ TLS tunnel created: api.example.com via relay:18443
Important: The --subdomain must match the SNI hostname that clients will use to connect.
Step 5: Test the Connection
# Connect using OpenSSL client
openssl s_client -connect localhost:18443 -servername api.example.com
# Or use curl for HTTPS
curl -k https://api.example.com:18443
# Or directly to local service (bypassing tunnel)
openssl s_client -connect localhost:3443 -servername api.example.com
SNI Routing
SNI (Server Name Indication) is a TLS extension that allows multiple hostnames to be served on a single IP address and port.
How SNI Works
- Client initiates TLS:
ClientHelloincludes SNI extension with hostname - Relay extracts SNI: Relay reads SNI from ClientHello (unencrypted part of handshake)
- Route to tunnel: Relay matches SNI to tunnel's subdomain
- Forward connection: Relay forwards entire TLS stream to client
- Client decrypts: Local TLS service completes handshake and decrypts
Example SNI Routing
# Tunnel 1: api.example.com
localup --port 3443 --protocol tls --relay relay.example.com:14443 --subdomain api.example.com --token "$TOKEN1"
# Tunnel 2: admin.example.com
localup --port 4443 --protocol tls --relay relay.example.com:14443 --subdomain admin.example.com --token "$TOKEN2"
# Client connections are routed by SNI:
curl -k https://api.example.com:18443 # → Tunnel 1 → localhost:3443
curl -k https://admin.example.com:18443 # → Tunnel 2 → localhost:4443
Note: All tunnels share the same relay TLS port (18443), routing is based on SNI hostname.
Complete Example
Here's a complete example with a local TLS service:
Terminal 1: Start TLS Relay
localup relay tls \
--localup-addr "0.0.0.0:14443" \
--tls-addr "0.0.0.0:18443" \
--jwt-secret "my-jwt-secret"
Terminal 2: Start Local TLS Service
# Generate self-signed certificates (one-time)
openssl req -x509 -newkey rsa:2048 -keyout tls-service-key.pem -out tls-service-cert.pem \
-days 365 -nodes -subj "/CN=localhost"
# Start OpenSSL TLS server
openssl s_server -cert tls-service-cert.pem -key tls-service-key.pem \
-accept 3443 -www
Terminal 3: Generate Token and Create Tunnel
export TOKEN=$(localup generate-token --secret "my-jwt-secret" --sub "api" --token-only)
localup --port 3443 --protocol tls --relay localhost:14443 --subdomain api.example.com --token "$TOKEN"
Terminal 4: Test the Tunnel
# Test via relay (with SNI routing)
openssl s_client -connect localhost:18443 -servername api.example.com
# Test direct connection to local service (bypass tunnel)
openssl s_client -connect localhost:3443 -servername api.example.com
What's happening:
- TLS relay listens on port 14443 (QUIC control) and 18443 (TLS connections)
- Local TLS service runs on port 3443 with self-signed certificates
- LocalUp client creates tunnel with subdomain
api.example.com - External clients connect to
relay:18443with SNIapi.example.com - Relay routes to tunnel, client forwards to
localhost:3443 - Local TLS service completes handshake and serves content
Troubleshooting
"No tunnel found for SNI hostname"
Cause: The SNI hostname in the client's request doesn't match any active tunnel's subdomain.
Solution:
# Verify tunnel subdomain matches SNI
# Client: -servername api.example.com
# Tunnel: --subdomain api.example.com
# These must match exactly (case-sensitive)
"Connection refused" when connecting to relay
Cause: TLS relay is not running or firewall is blocking.
Solution:
# Verify TLS relay is running
lsof -i :18443
# Check QUIC control plane is listening
lsof -i :14443
# Ensure firewall allows traffic
# On macOS: System Preferences → Security & Privacy → Firewall
# On Linux: sudo ufw allow 18443/tcp && sudo ufw allow 14443/udp
"TLS handshake failed"
Cause: Local TLS service has certificate issues or is not running.
Solution:
# Verify local TLS service is running
lsof -i :3443
# Test local service directly (bypass tunnel)
openssl s_client -connect localhost:3443 -servername localhost
# Regenerate self-signed certificates if expired
openssl req -x509 -newkey rsa:2048 -keyout tls-service-key.pem -out tls-service-cert.pem \
-days 365 -nodes -subj "/CN=localhost"
"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 "api" --token-only)
# Verify secret matches relay's --jwt-secret flag
SNI not sent by client
Cause: Some older TLS clients don't send SNI by default.
Solution:
# With curl, SNI is automatic from URL hostname
curl -k https://api.example.com:18443
# With openssl, use -servername flag
openssl s_client -connect localhost:18443 -servername api.example.com
# With programming languages, ensure TLS library sends SNI
# Most modern libraries (Go, Python, Node.js) send SNI by default
Next Steps: