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:

  1. External client initiates TLS connection to relay (e.g., relay.example.com:18443)
  2. Client sends TLS ClientHello with SNI: api.example.com
  3. Relay extracts SNI hostname without decrypting
  4. Relay routes connection to tunnel with matching subdomain
  5. Traffic passes through QUIC tunnel to LocalUp client
  6. Client forwards to local TLS service (e.g., localhost:3443)
  7. 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

  1. Client initiates TLS: ClientHello includes SNI extension with hostname
  2. Relay extracts SNI: Relay reads SNI from ClientHello (unencrypted part of handshake)
  3. Route to tunnel: Relay matches SNI to tunnel's subdomain
  4. Forward connection: Relay forwards entire TLS stream to client
  5. 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:

  1. TLS relay listens on port 14443 (QUIC control) and 18443 (TLS connections)
  2. Local TLS service runs on port 3443 with self-signed certificates
  3. LocalUp client creates tunnel with subdomain api.example.com
  4. External clients connect to relay:18443 with SNI api.example.com
  5. Relay routes to tunnel, client forwards to localhost:3443
  6. 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:

Was this page helpful?