Day 55 — SNI Routing
Learning Objectives
- Route TLS connections by hostname before decrypting them (SNI routing)
- Serve multiple domains from one IP/port with different backends
- Understand how CDNs and multi-tenant proxies work
Key Concepts
SNI (Server Name Indication):
Extension to TLS ClientHello — client announces which hostname it's connecting to
Server uses this to select the right certificate BEFORE the TLS handshake completes
Without SNI: one IP can only serve one TLS certificate
How CDNs use SNI:
1. All traffic hits the same IP (anycast)
2. TLS ClientHello contains SNI (e.g., "acme.corp.com")
3. CDN routes to the right customer's origin based on SNI
4. CDN terminates TLS with the customer's cert
Exercise: SNI Router
package main
import (
"crypto/tls"
"fmt"
"net"
"strings"
)
type SNIRouter struct {
// map from hostname → backend address
routes map[string]string
reloader *CertReloader // from Day 54
}
func NewSNIRouter() *SNIRouter {
return &SNIRouter{routes: make(map[string]string)}
}
func (r *SNIRouter) AddRoute(hostname, backendAddr string) {
r.routes[hostname] = backendAddr
}
// GetCertificate picks the right cert based on SNI hostname
func (r *SNIRouter) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
// In production: per-hostname cert store
// Simplified: one cert that covers all hostnames via SANs
return r.reloader.GetCertificate(hello)
}
// GetConfigForClient is called during TLS handshake — access to SNI before conn is established
func (r *SNIRouter) GetConfigForClient(hello *tls.ClientHelloInfo) (*tls.Config, error) {
fmt.Printf("[SNI] hostname=%q from %s\n", hello.ServerName, hello.Conn.RemoteAddr())
// Return per-hostname config (cert, min TLS version, etc.)
return &tls.Config{
GetCertificate: r.GetCertificate,
MinVersion: tls.VersionTLS12,
}, nil
}
// Route returns the backend address for a given hostname
func (r *SNIRouter) Route(hostname string) (string, bool) {
// Exact match
if backend, ok := r.routes[hostname]; ok {
return backend, true
}
// Wildcard match (*.example.com)
parts := strings.SplitN(hostname, ".", 2)
if len(parts) == 2 {
wildcard := "*." + parts[1]
if backend, ok := r.routes[wildcard]; ok {
return backend, true
}
}
return "", false
}
func handleSNIConn(conn net.Conn, router *SNIRouter) {
defer conn.Close()
// At this point, TLS handshake has completed
// We know the SNI hostname from GetConfigForClient
tlsConn, ok := conn.(*tls.Conn)
if !ok { return }
sni := tlsConn.ConnectionState().ServerName
backend, found := router.Route(sni)
if !found {
fmt.Printf("[SNI] no route for %q\n", sni)
return
}
// Connect to the appropriate backend and proxy
upstream, err := net.Dial("tcp", backend)
if err != nil {
fmt.Printf("[SNI] backend %s unavailable: %v\n", backend, err)
return
}
defer upstream.Close()
fmt.Printf("[SNI] routing %q → %s\n", sni, backend)
var wg sync.WaitGroup
wg.Add(2)
go func() { defer wg.Done(); io.Copy(upstream, conn) }()
go func() { defer wg.Done(); io.Copy(conn, upstream) }()
wg.Wait()
}
func main() {
router := NewSNIRouter()
router.AddRoute("api.example.com", "127.0.0.1:8081")
router.AddRoute("admin.example.com", "127.0.0.1:8082")
router.AddRoute("*.staging.example.com", "127.0.0.1:8083")
// Reloader from Day 54
reloader, _ := NewCertReloader("server.crt", "server.key")
router.reloader = reloader
tlsConfig := &tls.Config{
GetConfigForClient: router.GetConfigForClient,
}
ln, err := tls.Listen("tcp", ":443", tlsConfig)
if err != nil { fmt.Println(err); return }
defer ln.Close()
fmt.Println("SNI router on :443")
for {
conn, err := ln.Accept()
if err != nil { return }
go handleSNIConn(conn, router)
}
}
Test:
# Connect with specific hostname (SNI)
curl --cacert ca.crt --resolve api.example.com:443:127.0.0.1 https://api.example.com/
curl --cacert ca.crt --resolve admin.example.com:443:127.0.0.1 https://admin.example.com/
Real-World Context
Cloudflare, Fastly, and every multi-tenant CDN uses SNI routing. When your company hosts multiple products on the same IP, SNI routing lets each one have its own TLS cert and backend. This is also how nginx ssl_server_name and AWS ALB host-based routing work.