The server
The chat server is an Axum application. The
scion-h3-axum crate serves its router over HTTP/3 on a SCION
socket. The SCION code of the server is in one file,
chat-server/src/scion.rs.
From a minimal server to the chat server
The chat server is an HTTP/3 server. The SDK example
axum_h3.rs
is a minimal HTTP/3 server. It builds a stack, binds a socket, and serves a router on the socket:
// Build a SCION stack for the server in AS 2-ff00:0:212.
let server_stack: ScionStack = common::build_stack(&ps, IA212).await?;
// Bind a SCION UDP socket for the server to listen on.
let udp_socket: UdpScionSocket = server_stack.bind(None).await?;
// Convert to a trait object so we can pass it to the server.
let server_socket: Arc<dyn GenericScionUdpSocket> = Arc::new(udp_socket);
let server_addr = server_socket.local_addr();
let mut server_config = QuicConfig::builder()
.build()
.to_quiche_config()
.context("building quiche server config")?;
// `squiche` can only load certs/keys from files, so we write them to temporary files and
// keep them alive for the duration of the server.
server_config.load_cert_chain_from_pem_file(
cert_file.path().to_str().context("cert path not UTF-8")?,
)?;
server_config
.load_priv_key_from_pem_file(key_file.path().to_str().context("key path not UTF-8")?)?;
let shutdown = CancellationToken::new();
let server_task = tokio::spawn({
let shutdown = shutdown.clone();
async move {
// Create a normal axum app
let app = Router::new()
// Echoes the request body back verbatim.
.route("/echo", post(|body: String| async move { body }));
ScionH3AxumServer::serve_with_graceful_shutdown(
server_socket,
app,
server_config,
shutdown,
)
.await
}
});
The chat server does the same three steps:
- It builds a stack. The credential shows how.
- It binds a socket on the stack. The listen address shows how.
- It serves the Axum router on the socket. The next section shows how.
A real server adds three things to the example: a credential that does not expire, a certificate that clients can pin, and a graceful shutdown.
The credential
The server uses the SNAP underlay, so it proves to the SNAP that it may use the network. The server takes one of two flags:
--auth-token-filegives one SNAP token. The server stops working when the token expires.chat-devuses this flag.--auth-api-key-filegives an API key. The Anapaya AA exchanges the key for tokens, and the SDK renews the token while the server runs.
let mut builder = ScionStackBuilder::new().with_endhost_api(endhost_api);
if let Some(path) = &config.auth_token_file {
builder = builder.with_auth_token(read_secret(path, "--auth-token-file")?);
}
if let Some(path) = &config.auth_api_key_file {
let api_key = read_secret(path, "--auth-api-key-file")?;
// The first exchange runs here, so a refused key stops the server with the AA's reason.
let source = AnapayaAaTokenSource::start(api_key, &aa_url, DEVICE_ID).await?;
builder = builder.with_auth_token_source(source);
}
builder.build().await.map_err(|source| {
RunError::Scion {
action: "building the SCION stack",
detail: source.to_string(),
}
})
The ScionStack model explains the stack builder.
Serve the router
ScionH3AxumServer::serve_with_graceful_shutdown
takes the socket, the router, a QUIC configuration and a
CancellationToken:
ScionH3AxumServer::serve_with_graceful_shutdown(
Arc::clone(&listener.socket),
router,
quic,
shutdown,
)
.await
.map_err(|source| {
RunError::Scion {
action: "serving over scion",
detail: source.to_string(),
}
})
The function returns when the server stops or fails. The main function of the chat server
cancels the token on the first signal that asks the process to stop, and the server then stops
gracefully.
The router is the same Axum Router that the chat server serves over TCP with --transport tcp.
The routes, the store and the login of the chat are ordinary Axum code.
The certificate
HTTP/3 runs on QUIC, and QUIC uses TLS, so the server needs a certificate. The chat server makes a self-signed certificate on its first start and keeps it in its data directory. The server logs the fingerprint of the certificate. The operator gives that fingerprint to the clients that pin the certificate:
let cert = cert::ServerCert::load_or_create(&config.data_dir)?;
// The one line an operator has to pass on. Nothing else identifies the server.
tracing::info!(
fingerprint = %cert.fingerprint,
cert = %cert.cert_path.display(),
"pin this certificate"
);
The server then loads the certificate and its key into the QUIC configuration, with
load_cert_chain_from_pem_file and load_priv_key_from_pem_file. Both functions read a PEM file.
A client of the chat pins this certificate. A public server uses a publicly trusted certificate, and its clients trust it through the system roots. From a local SCION network to a real network explains the move from one to the other.
The listen address
The --listen flag of the server gives the IP address and the port of the socket. The endhost API
gives the AS: the server binds in the AS that stack.local_ases() returns.
// The endhost API decides which AS the host is in, so `--listen` contributes only its IP
// and port. Binding explicitly is what makes the port predictable.
let isd_asn = *stack.local_ases().first().ok_or_else(|| {
RunError::Scion {
action: "reading the local AS",
detail: "the endhost API reported no AS for this host".to_owned(),
}
})?;
let bind_addr = ScionSocketIpAddr::new(isd_asn, config.listen.ip(), config.listen.port());
let socket = stack.bind(Some(bind_addr)).await.map_err(|source| {
RunError::Scion {
action: "binding a SCION socket",
detail: source.to_string(),
}
})?;
Addressing explains SCION addresses.
A fixed port gives clients a fixed URL. For example, chat-dev starts the server with
--listen 127.0.0.1:8443, and its clients use the URL https://localhost:8443.
Where to go next
- The client: the other end of the connection.
- From a local SCION network to a real network: the credential, the certificate and the endhost API of a real deployment.
scion-h3-axumAPI reference: the complete server API.