Virtual Hosting
Serve multiple domains from a single server instance, each with its own TLS certificate, using SNI-based routing.
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:
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.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>HTTP/3 (QUIC) uses the same SniCertResolver via
create_quinn_server_config_from_vhosts(), 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 api_router = Router::new()
.with_host("api.example.com")
.get("/v1/users", |_req, _params, _conn| { /* ... */ });
let www_router = Router::new()
.with_host("example.com")
.get("/", |_req, _params, _conn| { /* ... */ });router.handle(request, connection) returns None when the host or path
doesn't match, so an Application can try each domain's router in turn and
fall through to the built-in App (static files, /healthz, …):
use rust_web_server::app::App;
use rust_web_server::application::Application;
use rust_web_server::core::New;
use rust_web_server::request::Request;
use rust_web_server::response::{Response, STATUS_CODE_REASON_PHRASE};
use rust_web_server::router::Router;
use rust_web_server::server::ConnectionInfo;
pub struct VhostApp {
example: Router,
other: Router,
fallback: App,
}
impl VhostApp {
pub fn new() -> Self {
VhostApp {
example: Router::new().with_host("example.com").get("/", |_req, _p, _c| {
Response::text(STATUS_CODE_REASON_PHRASE.n200_ok, "example.com home")
}),
other: Router::new().with_host("other.com").get("/", |_req, _p, _c| {
Response::text(STATUS_CODE_REASON_PHRASE.n200_ok, "other.com home")
}),
fallback: App::new(),
}
}
}
impl Application for VhostApp {
fn execute(&self, request: &Request, connection: &ConnectionInfo) -> Result<Response, String> {
if let Some(r) = self.example.handle(request, connection) { return Ok(r); }
if let Some(r) = self.other.handle(request, connection) { return Ok(r); }
self.fallback.execute(request, connection)
}
}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.