Reference

How It Works

Internals of SNI Router — entrypoint, config generation, priority sorting, and HAProxy startup.

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

  1. The container starts and /entrypoint.sh runs before HAProxy.
  2. The script validates that SNI_DEFAULT is set (the only required variable). If it is absent, the container exits immediately with an error.
  3. The script reads SNI_ROUTE_N, SNI_ROUTES, TCP_ROUTE_N, HTTP_ROUTE_N, and HTTP_ROUTES and generates a haproxy.cfg in memory.
  4. 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.
  5. 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 — collects use_backend lines for exact hostnames
  • SNI_ACL_WILDCARD — collects use_backend lines 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 haproxy user (set in the base image)
  • The container needs NET_BIND_SERVICE capability to bind to port 443 (Docker grants this by default for ports below 1024 when ports: 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
Copyright © 2026