How It Works
How It Works
SNI Router is a thin shell entrypoint wrapped around HAProxy. No routing logic runs at request time — all routing decisions are expressed in haproxy.cfg, which is generated once at container start.
Startup sequence
- The container starts and
/entrypoint.shruns before HAProxy. - The script validates that
SNI_DEFAULTis set (the only required variable). If it is absent, the container exits immediately with an error. - The script reads
SNI_ROUTE_N,SNI_ROUTES,TCP_ROUTE_N,HTTP_ROUTE_N, andHTTP_ROUTESand generates ahaproxy.cfgin memory. - The generated config is validated with
haproxy -c -f /usr/local/etc/haproxy/haproxy.cfg. If validation fails, the container exits with the HAProxy error output. - If validation passes, the script
execs into HAProxy:haproxy -W -db -f /usr/local/etc/haproxy/haproxy.cfg.
Config generation — exact vs wildcard sorting
HAProxy uses first-match semantics in use_backend rules. A wildcard ACL (req.ssl_sni -m end .example.com) declared before an exact ACL (req.ssl_sni -i docs.example.com) would shadow the exact rule.
To prevent this, entrypoint.sh maintains two separate buffers during rule processing:
SNI_ACL_EXACT— collectsuse_backendlines for exact hostnamesSNI_ACL_WILDCARD— collectsuse_backendlines for wildcard rules
When the HAProxy frontend is written, exact ACL lines are always emitted first, followed by wildcard lines. The SNI_DEFAULT backend is written last as the implicit catch-all.
This means declaration order in SNI_ROUTE_N or SNI_ROUTES is irrelevant — routing priority is always: exact → wildcard → default.
HAProxy TLS passthrough
For TLS connections, HAProxy operates in TCP mode with tcp-request inspect-delay and tcp-request content accept if { req.ssl_hello_type 1 }. It reads only the TLS ClientHello to extract the SNI field, then forwards the entire TCP byte stream — including the ClientHello it already read — to the matching backend.
The backend receives a normal TLS connection and performs the TLS handshake with the client. SNI Router is transparent to both sides.
Plain TCP routing
Each TCP_ROUTE_N produces an independent HAProxy frontend block listening on listen_port in TCP mode with no inspection. All traffic arriving on that port is forwarded to the configured backend.
Config validation
The haproxy -c -f flag causes HAProxy to parse and validate the configuration without starting. This catches syntax errors, duplicate backend names, invalid ACLs, and other problems before HAProxy attempts to bind any port.
If validation fails, the error output from HAProxy is printed to stderr and the container exits with a non-zero code, making the problem immediately visible in docker compose logs.
Security posture
- HAProxy runs as the unprivileged
haproxyuser (set in the base image) - The container needs
NET_BIND_SERVICEcapability to bind to port 443 (Docker grants this by default for ports below 1024 whenports:is used) - No secrets or credentials are stored inside the image — all configuration is injected via environment variables at runtime
- TLS certificates are never present in the sni-router container