Virtual Hosting
Overview
A single rust-web-server instance can host multiple domains simultaneously.
At the TLS handshake, the client sends its target hostname via SNI (Server Name
Indication). SniCertResolver reads that hostname and selects the matching
certificate before any HTTP traffic is exchanged. The negotiated hostname is
then available in every request handler as ConnectionInfo::sni_hostname.
Virtual hosting requires the http2 or http3 (default) feature for TLS
support.
Configuring virtual hosts in rws.config.toml
Add one [[virtual_host]] block per domain. The top-level tls_cert_file /
tls_key_file pair is used as a fallback when no SNI hostname matches (or when
the client sends no SNI):
# Default certificate — used when no virtual host matchestls_cert_file = "/etc/ssl/default.pem"tls_key_file = "/etc/ssl/default.key"
[[virtual_host]]domain = "example.com"cert_file = "/etc/ssl/example.pem"key_file = "/etc/ssl/example.key"
[[virtual_host]]domain = "api.example.com"cert_file = "/etc/ssl/api-example.pem"key_file = "/etc/ssl/api-example.key"Configuring virtual hosts via environment variables
The same configuration is available through numbered environment variables, which is convenient in container environments:
RWS_CONFIG_VIRTUAL_HOST_0_DOMAIN=example.comRWS_CONFIG_VIRTUAL_HOST_0_CERT_FILE=/etc/ssl/example.pemRWS_CONFIG_VIRTUAL_HOST_0_KEY_FILE=/etc/ssl/example.key
RWS_CONFIG_VIRTUAL_HOST_1_DOMAIN=api.example.comRWS_CONFIG_VIRTUAL_HOST_1_CERT_FILE=/etc/ssl/api-example.pemRWS_CONFIG_VIRTUAL_HOST_1_KEY_FILE=/etc/ssl/api-example.keyHow SNI resolution works
SniCertResolver implements rustls::server::ResolvesServerCert. It holds a
HashMap<String, Arc<CertifiedKey>> keyed by the exact SNI hostname, plus an
optional default. The resolver is built once at startup (or after SIGHUP) by
create_tls_acceptor_from_vhosts():
// src/tls/mod.rs (simplified)pub fn create_tls_acceptor_from_vhosts( vhosts: &[VirtualHostConfig], default_cert: &str, default_key: &str,) -> Result<TlsAcceptor, String>The same function is used for both HTTP/2 (TCP/TLS) and HTTP/3 (QUIC) listeners, so virtual hosting works transparently across all protocols.
Reading the SNI hostname in handlers
After the TLS handshake, ConnectionInfo::sni_hostname carries the negotiated
hostname as Option<String>. For plain HTTP/1.1 connections (no TLS) this
field is None and the Host header should be used instead.
fn process(&self, request: &Request, response: Response, connection: &ConnectionInfo) -> Response { match &connection.sni_hostname { Some(host) => println!("Serving request for: {}", host), None => println!("No SNI — plain HTTP or no matching vhost"), } response}Host-restricted routing with Router
Call .with_host("hostname") before registering routes to restrict a Router
to requests whose SNI hostname (TLS) or Host header (plain HTTP) matches:
use rust_web_server::router::Router;
let mut api_router = Router::new();api_router.with_host("api.example.com") .get("/v1/users", |_req, _params, _conn| { /* ... */ });
let mut www_router = Router::new();www_router.with_host("example.com") .get("/", |_req, _params, _conn| { /* ... */ });Hot reload
Send SIGHUP (or POST /admin/config/reload) to hot-reload all virtual host
certificates from disk without restarting the server:
kill -HUP $(pidof rws)Server::run_tls rebuilds TlsAcceptor with updated certificates for all
virtual hosts on every SIGHUP. New connections immediately use the refreshed
certificates; existing connections are not interrupted.