A TLS Reverse Proxy for SNI and ALPN routing.
This is v2 - a clean-room rewrite with:
- Modern Go patterns (Go 1.26+, context propagation, atomic config)
- Comprehensive test coverage (17 test suites)
- Thread-safe dynamic configuration (ready for API)
- Certmagic integration for ACME certificates
- Clean separation of concerns
# Build
go build -o tlsrouter ./cmd/tlsrouter-v2
# Run with static routes (CSV)
./tlsrouter --addr :443 routes.csv
# Run with environment variables
IP_DOMAINS="vm.example.com,a.bnna.net" \
NETWORKS="192.168.1.0/24,10.0.0.0/8" \
./tlsrouter --addr :443domain,alpn,backend,action
example.com,http/1.1,127.0.0.1:8080,terminate
passthrough.com,h2,127.0.0.1:443,passthrough
*.example.com,*,127.0.0.1:8081,terminateURLs like tls-192-168-1-100.vm.example.com automatically route to 192.168.1.100:
tls-prefix: Terminate TLS, proxy to port 3080tcp-prefix: Raw passthrough to port 443
Handles three ACME scenarios:
- Certmagic internal: Let's Encrypt certs via certmagic
- Per-domain backend: Route challenges to specific backends
- Global backend: Route all challenges to one backend
v2/
├── router.go # Core interfaces (Router, CertProvider, Dialer)
├── handler.go # TLS handshake with routing callback
├── server.go # TCP server with graceful shutdown
├── config.go # Atomic config for dynamic updates
├── cert_provider.go # Mock and static cert providers
├── certmagic_provider.go # Real ACME integration
└── static_router.go # Static and dynamic routing logic
type Router interface {
Route(sni string, alpns []string) (Decision, error)
}
type CertProvider interface {
GetCertificate(domain string) (Certificate, error)
}Thread-safe configuration swaps for dynamic API:
newCfg := cfg.AddStaticRoute("example.com>http/1.1", route)
handler.SetConfig(newCfg) // Atomic swap# Terminate TLS to 3080
CNAME site-a.example.com tls-192-168-1-100.vm.example.net
# Raw passthrough to 443
CNAME site-a.example.com tcp-192-168-1-100.vm.example.net
A example.com 192.168.1.100
SRV _http._tcp.example.com 10 3080 tls-192-168-1-100.vm.example.net
SRV _ssh._tcp.example.com 10 22 tls-192-168-1-100.vm.example.net
| ALPN | Raw Port | Decrypted Port | Notes |
|---|---|---|---|
| http/1.1 | 443 | 3080 | Non-standard to avoid conflicts |
| ssh | 44322 | 22 | SSH over TLS needs special port |
| h2 | 443 | - | HTTP/2 requires passthrough |
| acme-tls/1 | 443 | - | ACME TLS-ALPN challenges |
| postgresql | 5432 | 5432 | PostgreSQL native TLS |
See Full Port Table below for all protocols.
- Go 1.26+
- certmagic v0.25.1
cd v2
go test -v
# PASS: 17 test suites, 41 subtestsgo build -o tlsrouter ./cmd/tlsrouter-v2| Variable | Description |
|---|---|
IP_DOMAINS |
Comma-separated domains for dynamic routing |
NETWORKS |
Comma-separated CIDR networks (e.g., 192.168.1.0/24) |
ACME_BACKEND |
Global ACME challenge backend |
ACME_BACKENDS |
Per-domain: domain1=backend1,domain2=backend2 |
-addr string
Address to listen on (default ":443")
-bind string
Address to listen on (alias for addr)
-acme-email string
Email for ACME registration
-acme-dir string
ACME directory URL (default: Let's Encrypt)
-acme-agree
Agree to ACME terms
- Dynamic config API (view/add/remove routes)
- HTTP/80 redirect handler
- PROXY protocol support (v1/v2)
- Prometheus metrics endpoint
- Connection pooling for backends
- Rate limiting
MPL-2.0
| ALPN | Raw Port | Decrypted Port | Comment |
|---|---|---|---|
| http/1.1 | 443 | 3080 | 3080 to be familiar, but non-default like 3000, 8080, and 80 |
| ssh | 44322 | 22 | sshd can't handle sclient tls directly, hence 44322 for tls |
| --- | --- | --- | special protocols |
| acme-tls/1 | 443 | - | for ACME / Let's Encrypt TLS SNI ALPN challenges |
| h2 | 443 | - | proper HTTP/2 requires raw passthrough and has no plain port |
| h2c | - | 3080 | plain HTTP/2, for testing/debugging |
| --- | --- | --- | 10,000 is added to the default ports below |
| coap | 5684 | 15683 | IoT, plain port is 5683 |
| dicom | 2762 | 10104 | biomedical imaging, plain port is 104 |
| dot | 853 | 10053 | dns-over-tls, normal plain port is 53 (udp and tcp) |
| ftp | 990 | 10021 | normal plain port is 21, but it's more complicated than that |
| imap | 993 | 10143 | normal plain port is 143 |
| irc | 6697 | 16667 | normal plain port is 6667 |
| managesieve | 4190 | 14190 | for mail filtering, plain is also 4190 |
| mqtt | 8883 | 11883 | normal plain port is 1883 |
| nntp | 563 | 10119 | for News Servers, plain port is 119 |
| ntske/1 | 4460 | 10123 | for NTP, normal plain port is 123 |
| pop3 | 995 | 10110 | normal plain port is 110 |
| postgresql | 5432 | 15432 | Postgres 17+ supports direct TLS |
| tds/8.0 | 1433 | 11433 | MS SQL 2025+ supports direct TLS |
| radius/1.0 | 2083 | 12083 | legacy TLS optional |
| radius/1.1 | 2083 | 12083 | direct TLS required |
| sip | 5061 | 15060 | normal plain port is 5060 (or 5080) |
| smb | 10445 | 10445 | Either use requires tunneling (native SMB TLS requires QUIC) |
| webrtc | 443 | 10080 | 10080 to be familiar, but not 18080, 8080, 8081, or 9000 |
| c-webrtc | 443 | 10080 | " |
| xmpp-client | 5223 | 15222 | client-to-server communication, default 5222 (plain) |
| xmpp-server | 5270 | 15269 | server-to-server communication, default 5269 (plain) |
For all registered ALPNs, see https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml.
Excluded:
cois UDP-onlydoqDNS over QUIC is UDP-onlyhttp/0.9,http/1.0superseded byhttp/1.1h3HTTP over QUIC is UDP-onlynnsphas no port designationspdy/*superseded byh2stun.turnhas complex implicationsstun.nat-discovery(same)sunrpcprobably not relevant