0.3.5-alpha.2

This commit is contained in:
2026-09-21 22:36:38 +02:00
parent 3e24b7840c
commit 0077ea8b4d
11 changed files with 753 additions and 8 deletions

View File

@@ -0,0 +1,283 @@
// file: crates/common/game-realtime-webtransport-lib/src/webtransport.rs
// version: 1
const CERTIFICATE_HASH_SIZE: usize = 32;
const LOCAL_CERTIFICATE_CLOCK_SKEW: std::time::Duration = std::time::Duration::from_secs(60);
const LOCAL_CERTIFICATE_VALIDITY: std::time::Duration = std::time::Duration::from_secs(7 * 24 * 60 * 60);
const TRACING_TARGET: &str = "games::realtime::webtransport";
/// SHA-256 fingerprint of one certificate accepted by the native WebTransport client.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct WebTransportCertificateHash {
bytes: [u8; CERTIFICATE_HASH_SIZE],
}
impl WebTransportCertificateHash {
/// Creates a fingerprint from an already-computed SHA-256 digest.
#[must_use]
pub fn from_sha256(bytes: [u8; CERTIFICATE_HASH_SIZE]) -> Self {
return Self { bytes };
}
/// Returns the exact 32-byte SHA-256 digest.
#[must_use]
pub fn as_bytes(&self) -> &[u8; CERTIFICATE_HASH_SIZE] {
return &self.bytes;
}
}
/// Self-contained certificate/private-key identity used by a native WebTransport server.
pub struct WebTransportServerIdentity {
certificate_der: Vec<u8>,
private_key_pkcs8_der: Vec<u8>,
certificate_hash: WebTransportCertificateHash,
}
impl WebTransportServerIdentity {
/// Generates a short-lived self-signed ECDSA P-256 identity for localhost and loopback addresses.
pub fn generate_loopback() -> Result<Self, game_realtime_transport_lib::TransportError> {
let now = std::time::SystemTime::now();
let not_before = match now.checked_sub(LOCAL_CERTIFICATE_CLOCK_SKEW) {
Some(value) => value,
None => return Err(invalid_configuration("failed to compute local certificate not-before time")),
};
let not_after = match now.checked_add(LOCAL_CERTIFICATE_VALIDITY) {
Some(value) => value,
None => return Err(invalid_configuration("failed to compute local certificate not-after time")),
};
let subject_alt_names = vec!["localhost".to_owned(), "127.0.0.1".to_owned(), "::1".to_owned()];
let mut params = match rcgen::CertificateParams::new(subject_alt_names) {
Ok(value) => value,
Err(error) => return Err(invalid_configuration(error.to_string())),
};
params.not_before = not_before.into();
params.not_after = not_after.into();
let key_pair = match rcgen::KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256) {
Ok(value) => value,
Err(error) => return Err(invalid_configuration(error.to_string())),
};
let certificate = match params.self_signed(&key_pair) {
Ok(value) => value,
Err(error) => return Err(invalid_configuration(error.to_string())),
};
let certificate_der = certificate.der().to_vec();
let private_key_pkcs8_der = key_pair.serialize_der();
return Self::from_pkcs8_der(certificate_der, private_key_pkcs8_der);
}
/// Builds an identity from an X.509 certificate DER blob and its PKCS#8 private key DER blob.
///
/// Certificate/key compatibility is validated by the native TLS server builder when the listener is bound.
pub fn from_pkcs8_der(certificate_der: Vec<u8>, private_key_pkcs8_der: Vec<u8>) -> Result<Self, game_realtime_transport_lib::TransportError> {
if certificate_der.is_empty() {
return Err(invalid_configuration("certificate DER must not be empty"));
}
if private_key_pkcs8_der.is_empty() {
return Err(invalid_configuration("PKCS#8 private-key DER must not be empty"));
}
let certificate_hash = match certificate_hash(certificate_der.as_slice()) {
Ok(value) => value,
Err(error) => return Err(error),
};
return Ok(Self { certificate_der, private_key_pkcs8_der, certificate_hash });
}
/// Returns the SHA-256 certificate fingerprint used for native hash pinning.
#[must_use]
pub fn certificate_hash(&self) -> &WebTransportCertificateHash {
return &self.certificate_hash;
}
}
/// Native WebTransport client endpoint and pinned server-certificate fingerprint.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct WebTransportClientConfig {
endpoint: url::Url,
certificate_hash: WebTransportCertificateHash,
}
impl WebTransportClientConfig {
/// Parses and validates a secure WebTransport endpoint with one pinned SHA-256 certificate fingerprint.
pub fn new(endpoint: &str, certificate_hash: WebTransportCertificateHash) -> Result<Self, game_realtime_transport_lib::TransportError> {
let parsed = match url::Url::parse(endpoint) {
Ok(value) => value,
Err(error) => return Err(invalid_configuration(error.to_string())),
};
if parsed.scheme() != "https" {
return Err(invalid_configuration("WebTransport endpoint scheme must be https"));
}
if parsed.host().is_none() {
return Err(invalid_configuration("WebTransport endpoint must contain a host"));
}
return Ok(Self { endpoint: parsed, certificate_hash });
}
/// Returns the validated WebTransport endpoint URL.
#[must_use]
pub fn endpoint(&self) -> &str {
return self.endpoint.as_str();
}
/// Returns the pinned SHA-256 server-certificate fingerprint.
#[must_use]
pub fn certificate_hash(&self) -> &WebTransportCertificateHash {
return &self.certificate_hash;
}
}
/// Native WebTransport server bind address and TLS identity.
pub struct WebTransportServerConfig {
bind_address: std::net::SocketAddr,
identity: WebTransportServerIdentity,
}
impl WebTransportServerConfig {
/// Creates native server configuration for the requested bind address and TLS identity.
#[must_use]
pub fn new(bind_address: std::net::SocketAddr, identity: WebTransportServerIdentity) -> Self {
return Self { bind_address, identity };
}
/// Returns the requested UDP bind address.
#[must_use]
pub fn bind_address(&self) -> std::net::SocketAddr {
return self.bind_address;
}
/// Returns the server certificate fingerprint that clients must pin for this identity.
#[must_use]
pub fn certificate_hash(&self) -> &WebTransportCertificateHash {
return self.identity.certificate_hash();
}
}
/// Established native WebTransport session before application-stream adaptation.
pub struct WebTransportSession {
inner: web_transport_quinn::Session,
}
impl WebTransportSession {
fn new(inner: web_transport_quinn::Session) -> Self {
return Self { inner };
}
/// Returns the remote UDP socket address backing the established QUIC connection.
#[must_use]
pub fn remote_addr(&self) -> std::net::SocketAddr {
return self.inner.remote_address();
}
/// Returns the HTTP/3 CONNECT URL used to establish this session when available.
#[must_use]
pub fn request_url(&self) -> Option<&str> {
return match self.inner.request() {
Some(request) => Some(request.url.as_str()),
None => None,
};
}
}
/// Bound native WebTransport server endpoint that accepts HTTP/3 WebTransport sessions.
pub struct WebTransportListener {
server: web_transport_quinn::Server,
local_addr: std::net::SocketAddr,
}
impl WebTransportListener {
/// Binds a native WebTransport server using TLS 1.3 and the configured certificate identity.
pub fn bind(config: WebTransportServerConfig) -> Result<Self, game_realtime_transport_lib::TransportError> {
let certificate = web_transport_quinn::quinn::rustls::pki_types::CertificateDer::from(config.identity.certificate_der);
let private_key = web_transport_quinn::quinn::rustls::pki_types::PrivatePkcs8KeyDer::from(config.identity.private_key_pkcs8_der);
let private_key = web_transport_quinn::quinn::rustls::pki_types::PrivateKeyDer::Pkcs8(private_key);
let server = match web_transport_quinn::ServerBuilder::new().with_addr(config.bind_address).with_certificate(vec![certificate], private_key) {
Ok(value) => value,
Err(error) => {
let mapped = transport_error(game_realtime_transport_lib::TransportErrorKind::Bind, error.to_string());
tracing::warn!(target: TRACING_TARGET, address = %config.bind_address, detail = mapped.detail(), "WebTransport listener bind failed");
return Err(mapped);
},
};
let local_addr = match server.local_addr() {
Ok(value) => value,
Err(error) => {
let mapped = transport_error(game_realtime_transport_lib::TransportErrorKind::Bind, error.to_string());
tracing::warn!(target: TRACING_TARGET, detail = mapped.detail(), "bound WebTransport listener address lookup failed");
return Err(mapped);
},
};
tracing::info!(target: TRACING_TARGET, address = %local_addr, "WebTransport listener bound");
return Ok(Self { server, local_addr });
}
/// Returns the concrete UDP socket address, including an ephemeral port selected by the OS.
#[must_use]
pub fn local_addr(&self) -> std::net::SocketAddr {
return self.local_addr;
}
/// Accepts one native WebTransport CONNECT request and returns the established session.
pub async fn accept(&mut self) -> Result<WebTransportSession, game_realtime_transport_lib::TransportError> {
let request = match self.server.accept().await {
Some(value) => value,
None => {
let error = transport_error(game_realtime_transport_lib::TransportErrorKind::Accept, "WebTransport server stopped accepting sessions");
tracing::warn!(target: TRACING_TARGET, detail = error.detail(), "WebTransport accept ended");
return Err(error);
},
};
let peer = request.conn().remote_address();
let session = match request.ok().await {
Ok(value) => value,
Err(error) => {
let mapped = transport_error(game_realtime_transport_lib::TransportErrorKind::Accept, error.to_string());
tracing::warn!(target: TRACING_TARGET, peer = %peer, detail = mapped.detail(), "WebTransport server handshake failed");
return Err(mapped);
},
};
tracing::info!(target: TRACING_TARGET, peer = %peer, "WebTransport peer accepted");
return Ok(WebTransportSession::new(session));
}
}
/// Establishes one native WebTransport session using an exact SHA-256 certificate pin.
pub async fn connect(config: &WebTransportClientConfig) -> Result<WebTransportSession, game_realtime_transport_lib::TransportError> {
let client = match web_transport_quinn::ClientBuilder::new().with_server_certificate_hashes(vec![config.certificate_hash.as_bytes().to_vec()]) {
Ok(value) => value,
Err(error) => return Err(invalid_configuration(error.to_string())),
};
let session = match client.connect(config.endpoint.clone()).await {
Ok(value) => value,
Err(error) => {
let mapped = transport_error(game_realtime_transport_lib::TransportErrorKind::Connect, error.to_string());
tracing::warn!(target: TRACING_TARGET, endpoint = config.endpoint.as_str(), detail = mapped.detail(), "WebTransport client connection failed");
return Err(mapped);
},
};
tracing::info!(target: TRACING_TARGET, endpoint = config.endpoint.as_str(), peer = %session.remote_address(), "WebTransport client connected");
return Ok(WebTransportSession::new(session));
}
fn certificate_hash(certificate_der: &[u8]) -> Result<WebTransportCertificateHash, game_realtime_transport_lib::TransportError> {
let certificate = web_transport_quinn::quinn::rustls::pki_types::CertificateDer::from(certificate_der.to_vec());
let provider = web_transport_quinn::crypto::default_provider();
let digest = web_transport_quinn::crypto::sha256(&provider, &certificate);
let digest_bytes = digest.as_ref();
if digest_bytes.len() != CERTIFICATE_HASH_SIZE {
return Err(invalid_configuration("WebTransport certificate SHA-256 digest has an unexpected length"));
}
let mut bytes = [0_u8; CERTIFICATE_HASH_SIZE];
bytes.copy_from_slice(digest_bytes);
return Ok(WebTransportCertificateHash::from_sha256(bytes));
}
fn invalid_configuration(detail: impl Into<String>) -> game_realtime_transport_lib::TransportError {
return transport_error(game_realtime_transport_lib::TransportErrorKind::InvalidConfiguration, detail);
}
fn transport_error(kind: game_realtime_transport_lib::TransportErrorKind, detail: impl Into<String>) -> game_realtime_transport_lib::TransportError {
return game_realtime_transport_lib::TransportError::new(kind, detail);
}
#[cfg(test)]
#[path = "../unit_tests/webtransport.rs"]
mod tests;