Skip to content

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 matches
tls_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:

Terminal window
RWS_CONFIG_VIRTUAL_HOST_0_DOMAIN=example.com
RWS_CONFIG_VIRTUAL_HOST_0_CERT_FILE=/etc/ssl/example.pem
RWS_CONFIG_VIRTUAL_HOST_0_KEY_FILE=/etc/ssl/example.key
RWS_CONFIG_VIRTUAL_HOST_1_DOMAIN=api.example.com
RWS_CONFIG_VIRTUAL_HOST_1_CERT_FILE=/etc/ssl/api-example.pem
RWS_CONFIG_VIRTUAL_HOST_1_KEY_FILE=/etc/ssl/api-example.key

How 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:

Terminal window
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.