0.3.5-alpha.2
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
# file: Cargo.toml
|
||||
# version: 95
|
||||
# version: 96
|
||||
|
||||
[workspace]
|
||||
resolver = "3"
|
||||
@@ -16,6 +16,7 @@ members = [
|
||||
"crates/common/game-logging-lib",
|
||||
"crates/common/game-realtime-transport-lib",
|
||||
"crates/common/game-realtime-websocket-lib",
|
||||
"crates/common/game-realtime-webtransport-lib",
|
||||
"crates/apps/game-android-entrypoint",
|
||||
"crates/apps/game-reflex-poc-tauri",
|
||||
"crates/apps/game-reflex-poc-wasm",
|
||||
@@ -24,7 +25,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.3.5-alpha.1"
|
||||
version = "0.3.5-alpha.2"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
repository = "https://git.sasedev.com/Sasedev/games"
|
||||
@@ -33,6 +34,7 @@ publish = false
|
||||
|
||||
[workspace.dependencies]
|
||||
futures-util = { version = "0.3.34", default-features = false }
|
||||
rcgen = { version = "0.14.10", default-features = false }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
sdl3 = "^0.20"
|
||||
tracing = "0.1.44"
|
||||
@@ -44,7 +46,9 @@ tauri-build = "2"
|
||||
tauri-plugin-tracing = "^0.3"
|
||||
tokio = "1.53.1"
|
||||
tokio-tungstenite = { version = "0.30.0", default-features = false }
|
||||
url = "2.5.8"
|
||||
wasm-bindgen = "0.2"
|
||||
web-transport-quinn = { version = "0.12.1", default-features = false }
|
||||
|
||||
[workspace.lints.rust]
|
||||
missing_docs = "warn"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: README.md -->
|
||||
<!-- version: 63 -->
|
||||
<!-- version: 64 -->
|
||||
|
||||
# games.sasedev
|
||||
|
||||
@@ -27,9 +27,9 @@ Workspace expérimental puis productif pour des jeux multiplateformes principale
|
||||
|
||||
Version stable de référence : `0.3.4`.
|
||||
|
||||
Version active : `0.3.5-alpha.1`. `0.3.2` reste différée.
|
||||
Version active : `0.3.5-alpha.2`. `0.3.2` reste différée.
|
||||
|
||||
La stable `0.3.4` livre la première baseline realtime : contrat binaire transport-neutral, backend WebSocket Tokio/tokio-tungstenite, limites et deadlines, tests loopback/robustesse, smoke runtime localhost public et frontières de dépendances empêchant moteurs et gameplay de dépendre d'un backend concret. `0.3.5-alpha.1` ouvre le POC WebTransport/QUIC par un cadrage sans backend nouveau : audit de la stable, étude de l'écosystème actuel, choix de la famille `web-transport`, stratégie TLS/fallback, smoke tests et découpage fin avant implémentation.
|
||||
La stable `0.3.4` livre la première baseline realtime : contrat binaire transport-neutral, backend WebSocket Tokio/tokio-tungstenite, limites et deadlines, tests loopback/robustesse, smoke runtime localhost public et frontières de dépendances empêchant moteurs et gameplay de dépendre d'un backend concret. `0.3.5-alpha.2` introduit le backend WebTransport natif minimal : Quinn/HTTP/3, identité TLS locale ECDSA P-256 courte ou injectée, pin SHA-256 et établissement client/server loopback. Streams applicatifs, framing fiable et adaptation `RealtimeConnection` restent réservés à la tranche suivante.
|
||||
|
||||
Les deux premiers jeux sont des POC structurels : `game-reflex-poc` et `game-snake-poc`. Ils existent d'abord pour valider les frontières du workspace, le moteur, les assets et le packaging multiplateforme.
|
||||
|
||||
@@ -45,4 +45,4 @@ Les deux premiers jeux sont des POC structurels : `game-reflex-poc` et `game-sna
|
||||
|
||||
## Diagnostics et tests
|
||||
|
||||
Les socles transverses `crates/common/game-assets-lib` et `crates/common/game-logging-lib` fournissent respectivement la résolution logique des assets et le tracing commun. Le realtime est séparé entre `game-realtime-transport-lib`, contrat binaire transport-neutral, et `game-realtime-websocket-lib`, backend Tokio/tokio-tungstenite ; leurs responsabilités et leur consommation sont documentées dans leurs README/USAGE locaux. `game-realtime-websocket-smoke` fournit la preuve runtime localhost hors harness de test. Les tests unitaires résident hors `src/` sous `unit_tests/`; les tests d’intégration/environnement résident sous `tests/`.
|
||||
Les socles transverses `crates/common/game-assets-lib` et `crates/common/game-logging-lib` fournissent respectivement la résolution logique des assets et le tracing commun. Le realtime est séparé entre `game-realtime-transport-lib`, contrat binaire transport-neutral, `game-realtime-websocket-lib`, backend Tokio/tokio-tungstenite, et `game-realtime-webtransport-lib`, backend WebTransport/QUIC candidat dont la première frontière native couvre TLS/pinning et établissement de session. Leurs responsabilités sont documentées dans leurs README/USAGE locaux lorsqu'un guide d'usage est justifié. `game-realtime-websocket-smoke` fournit la preuve runtime localhost hors harness de test ; le smoke WebTransport public est planifié après le chemin fiable. Les tests unitaires résident hors `src/` sous `unit_tests/`; les tests d’intégration/environnement résident sous `tests/`.
|
||||
|
||||
24
crates/common/game-realtime-webtransport-lib/Cargo.toml
Normal file
24
crates/common/game-realtime-webtransport-lib/Cargo.toml
Normal file
@@ -0,0 +1,24 @@
|
||||
# file: crates/common/game-realtime-webtransport-lib/Cargo.toml
|
||||
# version: 1
|
||||
|
||||
[package]
|
||||
name = "game-realtime-webtransport-lib"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
authors.workspace = true
|
||||
publish.workspace = true
|
||||
|
||||
[dependencies]
|
||||
game-realtime-transport-lib = { path = "../game-realtime-transport-lib" }
|
||||
rcgen = { workspace = true, features = ["ring"] }
|
||||
tracing.workspace = true
|
||||
url.workspace = true
|
||||
web-transport-quinn = { workspace = true, features = ["ring"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { workspace = true, features = ["macros", "rt", "time"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
35
crates/common/game-realtime-webtransport-lib/README.md
Normal file
35
crates/common/game-realtime-webtransport-lib/README.md
Normal file
@@ -0,0 +1,35 @@
|
||||
<!-- file: crates/common/game-realtime-webtransport-lib/README.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# game-realtime-webtransport-lib
|
||||
|
||||
Backend WebTransport/QUIC candidat pour le realtime de `games.sasedev`.
|
||||
|
||||
## Responsabilité
|
||||
|
||||
La crate possède le transport WebTransport concret sans introduire de sémantique gameplay, room, joueur, tick ou snapshot. Son chemin natif repose sur `web-transport-quinn` et conserve les erreurs publiques dans `game-realtime-transport-lib`.
|
||||
|
||||
La première frontière disponible couvre :
|
||||
|
||||
- configuration client HTTPS avec pin SHA-256 exact ;
|
||||
- identité serveur X.509 DER + clé privée PKCS#8 DER injectables ;
|
||||
- génération locale d'une identité self-signed ECDSA P-256 à validité courte pour `localhost`, IPv4 loopback et IPv6 loopback ;
|
||||
- bind UDP/QUIC sur adresse explicite ou port éphémère ;
|
||||
- établissement HTTP/3 WebTransport client/server natif ;
|
||||
- tracing sous `games::realtime::webtransport`.
|
||||
|
||||
## TLS de développement
|
||||
|
||||
`WebTransportServerIdentity::generate_loopback()` crée une identité en mémoire. La clé privée n'est ni écrite ni versionnée. Le certificat est valide sept jours, avec une petite marge de clock skew, et son SHA-256 est exposé à travers `WebTransportCertificateHash` afin que le client puisse utiliser le pinning fourni par `web-transport-quinn`.
|
||||
|
||||
Une identité préexistante peut être injectée en DER avec `WebTransportServerIdentity::from_pkcs8_der(...)`. La compatibilité certificat/clé est alors vérifiée par le builder TLS au bind du serveur.
|
||||
|
||||
Aucune option de désactivation globale de la vérification TLS n'est exposée.
|
||||
|
||||
## Frontières actuelles
|
||||
|
||||
Cette crate n'adapte pas encore une session vers `RealtimeConnection`. Elle n'ouvre pas encore le stream bidirectionnel applicatif principal et ne définit donc ni framing message, ni limites de payload, ni deadlines applicatives, ni datagram API commune.
|
||||
|
||||
Ces responsabilités doivent rester séparées de la seule preuve d'établissement natif afin que l'introduction de QUIC/TLS demeure testable indépendamment du futur framing fiable.
|
||||
|
||||
Le chemin natif s’exécute sous un runtime Tokio fourni par le consommateur ; la crate ne crée ni runtime ni thread privé. Le chemin navigateur/WASM est distinct : aucun `cfg` WASM ni dépendance navigateur n'est requis par le backend natif actuel.
|
||||
25
crates/common/game-realtime-webtransport-lib/src/lib.rs
Normal file
25
crates/common/game-realtime-webtransport-lib/src/lib.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
// file: crates/common/game-realtime-webtransport-lib/src/lib.rs
|
||||
// version: 1
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
//! Native Quinn-backed WebTransport establishment for the games.sasedev realtime transport POC.
|
||||
|
||||
mod webtransport;
|
||||
|
||||
/// Re-export of one SHA-256 certificate fingerprint accepted by the native WebTransport client.
|
||||
pub use self::webtransport::WebTransportCertificateHash;
|
||||
/// Re-export of native client establishment configuration.
|
||||
pub use self::webtransport::WebTransportClientConfig;
|
||||
/// Re-export of a bound native WebTransport server listener.
|
||||
pub use self::webtransport::WebTransportListener;
|
||||
/// Re-export of native WebTransport server establishment configuration.
|
||||
pub use self::webtransport::WebTransportServerConfig;
|
||||
/// Re-export of a generated or injected WebTransport server identity.
|
||||
pub use self::webtransport::WebTransportServerIdentity;
|
||||
/// Re-export of an established native WebTransport session.
|
||||
pub use self::webtransport::WebTransportSession;
|
||||
/// Re-export of the native WebTransport client session constructor.
|
||||
pub use self::webtransport::connect;
|
||||
283
crates/common/game-realtime-webtransport-lib/src/webtransport.rs
Normal file
283
crates/common/game-realtime-webtransport-lib/src/webtransport.rs
Normal 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;
|
||||
@@ -0,0 +1,67 @@
|
||||
// file: crates/common/game-realtime-webtransport-lib/tests/establishment.rs
|
||||
// version: 1
|
||||
|
||||
//! Deterministic native loopback proof for WebTransport session establishment and SHA-256 pinning.
|
||||
|
||||
const TEST_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn pinned_client_and_server_establish_a_loopback_session() {
|
||||
let identity = match game_realtime_webtransport_lib::WebTransportServerIdentity::generate_loopback() {
|
||||
Ok(value) => value,
|
||||
Err(error) => panic!("loopback identity generation failed: {error}"),
|
||||
};
|
||||
let certificate_hash = identity.certificate_hash().clone();
|
||||
let server_config = game_realtime_webtransport_lib::WebTransportServerConfig::new(std::net::SocketAddr::from(([127, 0, 0, 1], 0)), identity);
|
||||
let mut listener = match game_realtime_webtransport_lib::WebTransportListener::bind(server_config) {
|
||||
Ok(value) => value,
|
||||
Err(error) => panic!("WebTransport listener bind failed: {error}"),
|
||||
};
|
||||
let endpoint = format!("https://{}/establishment", listener.local_addr());
|
||||
let client_config = match game_realtime_webtransport_lib::WebTransportClientConfig::new(endpoint.as_str(), certificate_hash) {
|
||||
Ok(value) => value,
|
||||
Err(error) => panic!("WebTransport client configuration failed: {error}"),
|
||||
};
|
||||
let pair = tokio::time::timeout(TEST_TIMEOUT, async {
|
||||
return tokio::join!(listener.accept(), game_realtime_webtransport_lib::connect(&client_config));
|
||||
})
|
||||
.await;
|
||||
let (server_session, client_session) = match pair {
|
||||
Ok((Ok(server), Ok(client))) => (server, client),
|
||||
Ok((Err(error), _)) => panic!("WebTransport server establishment failed: {error}"),
|
||||
Ok((_, Err(error))) => panic!("WebTransport client establishment failed: {error}"),
|
||||
Err(_) => panic!("WebTransport loopback establishment timed out"),
|
||||
};
|
||||
assert_eq!(client_session.request_url(), Some(endpoint.as_str()));
|
||||
assert_eq!(server_session.request_url(), Some(endpoint.as_str()));
|
||||
assert_eq!(client_session.remote_addr(), listener.local_addr());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn incorrect_certificate_pin_rejects_establishment() {
|
||||
let identity = match game_realtime_webtransport_lib::WebTransportServerIdentity::generate_loopback() {
|
||||
Ok(value) => value,
|
||||
Err(error) => panic!("loopback identity generation failed: {error}"),
|
||||
};
|
||||
let server_config = game_realtime_webtransport_lib::WebTransportServerConfig::new(std::net::SocketAddr::from(([127, 0, 0, 1], 0)), identity);
|
||||
let mut listener = match game_realtime_webtransport_lib::WebTransportListener::bind(server_config) {
|
||||
Ok(value) => value,
|
||||
Err(error) => panic!("WebTransport listener bind failed: {error}"),
|
||||
};
|
||||
let endpoint = format!("https://{}/wrong-pin", listener.local_addr());
|
||||
let wrong_hash = game_realtime_webtransport_lib::WebTransportCertificateHash::from_sha256([0_u8; 32]);
|
||||
let client_config = match game_realtime_webtransport_lib::WebTransportClientConfig::new(endpoint.as_str(), wrong_hash) {
|
||||
Ok(value) => value,
|
||||
Err(error) => panic!("WebTransport client configuration failed: {error}"),
|
||||
};
|
||||
let (server_result, client_result) = tokio::join!(
|
||||
tokio::time::timeout(TEST_TIMEOUT, listener.accept()),
|
||||
tokio::time::timeout(TEST_TIMEOUT, game_realtime_webtransport_lib::connect(&client_config)),
|
||||
);
|
||||
match client_result {
|
||||
Ok(Ok(_)) => panic!("WebTransport establishment unexpectedly accepted an incorrect certificate pin"),
|
||||
Ok(Err(error)) => assert_eq!(error.kind(), game_realtime_transport_lib::TransportErrorKind::Connect),
|
||||
Err(_) => panic!("incorrect-pin connection attempt timed out"),
|
||||
}
|
||||
assert!(matches!(server_result, Err(_) | Ok(Err(_))));
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
// file: crates/common/game-realtime-webtransport-lib/unit_tests/webtransport.rs
|
||||
// version: 1
|
||||
|
||||
#[test]
|
||||
fn certificate_hash_preserves_exact_sha256_bytes() {
|
||||
let bytes = [7_u8; super::CERTIFICATE_HASH_SIZE];
|
||||
let hash = super::WebTransportCertificateHash::from_sha256(bytes);
|
||||
assert_eq!(hash.as_bytes(), &bytes);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn client_config_accepts_https_and_rejects_non_secure_schemes() {
|
||||
let hash = super::WebTransportCertificateHash::from_sha256([1_u8; super::CERTIFICATE_HASH_SIZE]);
|
||||
let secure = super::WebTransportClientConfig::new("https://127.0.0.1:4433/game", hash.clone());
|
||||
assert!(secure.is_ok());
|
||||
let insecure_http = super::WebTransportClientConfig::new("http://127.0.0.1:4433/game", hash.clone());
|
||||
match insecure_http {
|
||||
Ok(_) => panic!("HTTP endpoint unexpectedly accepted"),
|
||||
Err(error) => assert_eq!(error.kind(), game_realtime_transport_lib::TransportErrorKind::InvalidConfiguration),
|
||||
}
|
||||
let websocket = super::WebTransportClientConfig::new("ws://127.0.0.1:4433/game", hash);
|
||||
match websocket {
|
||||
Ok(_) => panic!("WebSocket endpoint unexpectedly accepted"),
|
||||
Err(error) => assert_eq!(error.kind(), game_realtime_transport_lib::TransportErrorKind::InvalidConfiguration),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn injected_identity_rejects_empty_certificate_or_key() {
|
||||
let missing_certificate = super::WebTransportServerIdentity::from_pkcs8_der(Vec::new(), vec![1]);
|
||||
match missing_certificate {
|
||||
Ok(_) => panic!("empty certificate unexpectedly accepted"),
|
||||
Err(error) => assert_eq!(error.kind(), game_realtime_transport_lib::TransportErrorKind::InvalidConfiguration),
|
||||
}
|
||||
let missing_key = super::WebTransportServerIdentity::from_pkcs8_der(vec![1], Vec::new());
|
||||
match missing_key {
|
||||
Ok(_) => panic!("empty private key unexpectedly accepted"),
|
||||
Err(error) => assert_eq!(error.kind(), game_realtime_transport_lib::TransportErrorKind::InvalidConfiguration),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generated_loopback_identity_has_sha256_fingerprint() {
|
||||
let identity = match super::WebTransportServerIdentity::generate_loopback() {
|
||||
Ok(value) => value,
|
||||
Err(error) => panic!("loopback identity generation failed: {error}"),
|
||||
};
|
||||
assert_eq!(identity.certificate_hash().as_bytes().len(), super::CERTIFICATE_HASH_SIZE);
|
||||
}
|
||||
203
deltas/0.3.5/alpha.2.md
Normal file
203
deltas/0.3.5/alpha.2.md
Normal file
@@ -0,0 +1,203 @@
|
||||
<!-- file: deltas/0.3.5/alpha.2.md -->
|
||||
<!-- version: 2 -->
|
||||
|
||||
# Delta 0.3.5-alpha.2
|
||||
|
||||
## Base
|
||||
|
||||
Base : `0.3.5-alpha.1` validée par l'utilisateur le 2026-09-21.
|
||||
|
||||
Cette tranche reste limitée à la fondation WebTransport native prévue par le plan : dépendances minimales, identité TLS, pin SHA-256, bind QUIC/HTTP3 et établissement d'une session client/server. Elle ne contient encore ni stream applicatif principal, ni framing `u32 + payload`, ni adaptation `RealtimeConnection`, ni datagram, ni fallback.
|
||||
|
||||
## Historique fermé
|
||||
|
||||
Ajout de :
|
||||
|
||||
```text
|
||||
history/0.3.5/alpha.1.md
|
||||
```
|
||||
|
||||
L'entrée enregistre exactement la gate utilisateur reçue : fmt check, trois audits propres et `cargo check --workspace` propre sur `0.3.5-alpha.1`. Le plan révisé a été explicitement accepté avant le passage à cette tranche.
|
||||
|
||||
## Version
|
||||
|
||||
La version workspace passe de :
|
||||
|
||||
```text
|
||||
0.3.5-alpha.1
|
||||
```
|
||||
|
||||
à :
|
||||
|
||||
```text
|
||||
0.3.5-alpha.2
|
||||
```
|
||||
|
||||
Aucune version Android, npm ou Tauri indépendante n'est modifiée.
|
||||
|
||||
## Nouvelle crate WebTransport
|
||||
|
||||
Ajout de :
|
||||
|
||||
```text
|
||||
crates/common/game-realtime-webtransport-lib/Cargo.toml
|
||||
crates/common/game-realtime-webtransport-lib/README.md
|
||||
crates/common/game-realtime-webtransport-lib/src/lib.rs
|
||||
crates/common/game-realtime-webtransport-lib/src/webtransport.rs
|
||||
crates/common/game-realtime-webtransport-lib/unit_tests/webtransport.rs
|
||||
crates/common/game-realtime-webtransport-lib/tests/establishment.rs
|
||||
```
|
||||
|
||||
La crate est placée sous `crates/common/` au même niveau que le contrat transport-neutral et le backend WebSocket. Elle ne contient aucune sémantique de jeu.
|
||||
|
||||
## Dépendances et features
|
||||
|
||||
Les contraintes nouvelles sont centralisées dans `[workspace.dependencies]` :
|
||||
|
||||
```text
|
||||
rcgen = 0.14.10, default-features = false
|
||||
url = 2.5.8
|
||||
web-transport-quinn = 0.12.1, default-features = false
|
||||
```
|
||||
|
||||
La crate consommatrice active localement uniquement `ring` sur `rcgen` et `web-transport-quinn`.
|
||||
|
||||
`web-transport-quinn` est utilisé directement dans cette tranche native. La façade multiplateforme `web-transport` reste différée jusqu'au chemin WASM, afin de ne pas introduire une dépendance sans consommateur réel.
|
||||
|
||||
Le backend crypto par défaut `aws-lc-rs` de `web-transport-quinn` est donc désactivé. `ring` devient le provider unique du POC natif initial.
|
||||
|
||||
## Identité TLS et pinning
|
||||
|
||||
`WebTransportServerIdentity` accepte deux chemins :
|
||||
|
||||
```text
|
||||
generate_loopback()
|
||||
from_pkcs8_der(certificate_der, private_key_pkcs8_der)
|
||||
```
|
||||
|
||||
La génération loopback produit en mémoire :
|
||||
|
||||
- une clé ECDSA P-256 ;
|
||||
- un certificat self-signed avec SHA-256 ;
|
||||
- les SAN `localhost`, `127.0.0.1` et `::1` ;
|
||||
- une validité de sept jours avec 60 secondes de marge avant l'heure courante ;
|
||||
- aucun PEM ni fichier de clé versionné.
|
||||
|
||||
`WebTransportCertificateHash` porte exactement les 32 octets SHA-256 du certificat. Le client configure `ClientBuilder::with_server_certificate_hashes(...)` ; aucune option de TLS permissif n'est exposée.
|
||||
|
||||
L'injection DER ne prétend pas parser ou certifier la cohérence clé/certificat avant le bind : le builder TLS natif reste l'autorité qui rejette une paire incompatible.
|
||||
|
||||
## Configuration et établissement natif
|
||||
|
||||
`WebTransportClientConfig` impose un endpoint `https://` valide et un hash épinglé.
|
||||
|
||||
`WebTransportServerConfig` possède l'adresse UDP et l'identité TLS.
|
||||
|
||||
`WebTransportListener::bind(...)` :
|
||||
|
||||
- construit le serveur Quinn/WebTransport ;
|
||||
- supporte le port `0` pour une allocation éphémère ;
|
||||
- expose l'adresse effectivement bindée ;
|
||||
- mappe les erreurs vers `TransportErrorKind::Bind`.
|
||||
|
||||
`WebTransportListener::accept(...)` accepte le CONNECT HTTP/3 et retourne une `WebTransportSession`.
|
||||
|
||||
`connect(...)` construit un client pinned et retourne également une `WebTransportSession`. Les erreurs d'établissement client sont mappées vers `Connect`; les erreurs serveur vers `Accept`.
|
||||
|
||||
La session expose uniquement des diagnostics d'établissement (`remote_addr`, URL CONNECT). Le stream fiable applicatif appartient explicitement à `alpha.3`.
|
||||
|
||||
## Tests ajoutés
|
||||
|
||||
Les tests unitaires couvrent :
|
||||
|
||||
- conservation exacte d'un SHA-256 de 32 octets ;
|
||||
- acceptation d'un endpoint HTTPS ;
|
||||
- rejet HTTP/WebSocket ;
|
||||
- rejet d'une identité injectée sans certificat ou sans clé ;
|
||||
- génération d'une identité loopback avec fingerprint SHA-256.
|
||||
|
||||
Le test d'intégration `establishment.rs` couvre :
|
||||
|
||||
- bind sur `127.0.0.1:0` ;
|
||||
- génération d'identité éphémère ;
|
||||
- pin SHA-256 transmis au client ;
|
||||
- établissement client/server concurrent borné par timeout ;
|
||||
- URL CONNECT observée des deux côtés ;
|
||||
- rejet d'un mauvais pin côté client.
|
||||
|
||||
Il ne transmet volontairement aucun payload : ce serait anticiper `alpha.3`.
|
||||
|
||||
## Tracing
|
||||
|
||||
Le nouveau backend utilise :
|
||||
|
||||
```text
|
||||
games::realtime::webtransport
|
||||
```
|
||||
|
||||
pour bind, connexion, accept et diagnostics d'échec d'établissement.
|
||||
|
||||
## Documentation et plan
|
||||
|
||||
`README.md` racine annonce `0.3.5-alpha.2` et la nouvelle frontière native.
|
||||
|
||||
Le README local documente la responsabilité de la crate, le TLS de développement et les frontières encore exclues.
|
||||
|
||||
Le plan `005` est réconcilié avec les choix réellement fermés : dépendances natives directes, provider `ring`, identité sept jours et absence volontaire de façade `web-transport` avant le chemin WASM.
|
||||
|
||||
`ROADMAP.md` et `CHANGELOG.md` restent inchangés : le scope macro de `0.3.5` ne change pas et cette alpha n'est pas un jalon de changelog.
|
||||
|
||||
## Validation exécutée dans l'environnement de génération
|
||||
|
||||
L'environnement de génération ne possède pas de toolchain Rust. Il ne doit donc attribuer aucun `cargo fmt`, `cargo check`, Clippy, test ou `cargo tree` à cette livraison.
|
||||
|
||||
Les audits Python et contrôles statiques sont exécutés après constitution du delta. Sur la reconstruction locale issue du ZIP taggé `v0.3.4` puis du delta `alpha.1`, ils donnent :
|
||||
|
||||
```text
|
||||
General Rust rule audit: clean
|
||||
Rust export completeness audit: 0 candidate(s)
|
||||
games.sasedev workspace audit: clean
|
||||
Markdown table audit: clean (5 table(s), 269 file(s))
|
||||
Distribution layout audit: clean (50 required path(s), 8 forbidden path(s) absent)
|
||||
TOML/workspace consistency: clean
|
||||
```
|
||||
|
||||
Le compteur de fichiers Markdown de cette reconstruction n'est pas utilisé comme référence pour le checkout utilisateur : la gate `alpha.1` de l'utilisateur comptait déjà davantage de fichiers (`282`) que la reconstruction autoritaire ZIP + delta. Seul le statut clean est comparé. Ces contrôles restent distincts de la gate Cargo utilisateur.
|
||||
|
||||
## Validation utilisateur demandée
|
||||
|
||||
Cette tranche modifie Rust et le graphe de dépendances ; la gate ciblée est donc :
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
cargo fmt --all -- --check
|
||||
|
||||
python3 scripts/audit_rust_workspace_rules.py
|
||||
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates Android Web deltas history
|
||||
python3 scripts/audit_distribution_layout.py
|
||||
|
||||
cargo check --workspace
|
||||
cargo clippy --workspace --all-targets --all-features -- -D warnings
|
||||
|
||||
cargo test -p game-realtime-webtransport-lib --all-targets --all-features
|
||||
|
||||
cargo tree -p game-realtime-webtransport-lib --edges normal
|
||||
cargo tree -p game-realtime-webtransport-lib --edges features
|
||||
cargo tree -i web-transport-quinn --workspace --edges normal
|
||||
```
|
||||
|
||||
Le full `cargo test --workspace --all-targets --all-features` reste réservé au jalon large `beta.1` conformément au plan.
|
||||
|
||||
Aucun smoke executable n'est requis dans `alpha.2` : le test d'intégration prouve l'établissement natif sous harness, tandis que le smoke WebTransport public reste la responsabilité explicite de `alpha.5` après le framing fiable et la robustesse.
|
||||
|
||||
## Suite après validation
|
||||
|
||||
Si la gate est propre, `0.3.5-alpha.3` peut :
|
||||
|
||||
- ouvrir/ accepter le stream bidirectionnel principal ;
|
||||
- ajouter le framing privé borné `u32 big-endian + payload` ;
|
||||
- implémenter `RealtimeConnection`, sender et receiver ;
|
||||
- prouver le round-trip binaire ordonné et plusieurs messages ;
|
||||
- conserver datagrams, robustesse avancée et smoke public hors de cette tranche.
|
||||
|
||||
Un défaut fermé de cette tranche produit d'abord `0.3.5-alpha.2.fix.N` au lieu d'ouvrir `alpha.3`.
|
||||
@@ -1,11 +1,11 @@
|
||||
<!-- file: docs/plans/005-V0_3_5_WEBTRANSPORT_QUIC_POC_PLAN.md -->
|
||||
<!-- version: 1 -->
|
||||
<!-- version: 2 -->
|
||||
|
||||
# Plan 0.3.5 — POC WebTransport/QUIC et fallback WebSocket
|
||||
|
||||
## Statut
|
||||
|
||||
Plan actif créé pendant `0.3.5-alpha.1` à partir de l'archive taggée `v0.3.4`.
|
||||
Plan actif créé pendant `0.3.5-alpha.1` à partir de l'archive taggée `v0.3.4`, puis réconcilié pour l'implémentation native de `0.3.5-alpha.2`.
|
||||
|
||||
Le cadrage détaillé et la comparaison des stacks actuelles sont conservés dans `docs/studies/026-V0_3_5_WEBTRANSPORT_QUIC_STACK_AUDIT.md`. Le présent document porte les décisions opérationnelles, le scope, les gates et le forecast vivant de la version.
|
||||
|
||||
@@ -57,6 +57,22 @@ La version exacte résolue par Cargo sera enregistrée dans le delta qui introdu
|
||||
|
||||
`wtransport 0.7.x` reste le candidat de repli prioritaire si une exigence concrète bloque la famille primaire. Quinn/H3 brut n'est pas le premier choix.
|
||||
|
||||
### Fermeture de dépendances en alpha.2
|
||||
|
||||
La première tranche native utilise directement :
|
||||
|
||||
```text
|
||||
web-transport-quinn 0.12.1, default-features = false, feature ring
|
||||
rcgen 0.14.10, default-features = false, feature ring
|
||||
url 2.5.8
|
||||
```
|
||||
|
||||
La façade `web-transport` n'est pas ajoutée avant le chemin WASM : `alpha.2` ne possède qu'un backend natif et n'a pas besoin d'une abstraction multiplateforme encore inutilisée. `ring` est choisi explicitement et seul pour éviter le backend crypto par défaut `aws-lc-rs` de `web-transport-quinn` et garder le graphe POC plus petit et déterministe.
|
||||
|
||||
L'identité locale générée est ECDSA P-256/SHA-256, contient les SAN `localhost`, `127.0.0.1` et `::1`, vit sept jours avec une petite marge de clock skew et reste uniquement en mémoire. Une identité X.509 DER + PKCS#8 DER peut aussi être injectée. Le client accepte uniquement le hash SHA-256 explicitement configuré ; aucune API de désactivation de validation TLS n'est exposée.
|
||||
|
||||
`alpha.2` retourne une session WebTransport établie mais n'ouvre encore aucun stream applicatif et n'implémente pas `RealtimeConnection`. Cette séparation ferme la preuve QUIC/TLS avant le framing de `alpha.3`.
|
||||
|
||||
### Ownership physique
|
||||
|
||||
Nouveau backend durable candidat :
|
||||
|
||||
39
history/0.3.5/alpha.1.md
Normal file
39
history/0.3.5/alpha.1.md
Normal file
@@ -0,0 +1,39 @@
|
||||
<!-- file: history/0.3.5/alpha.1.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Historique 0.3.5-alpha.1
|
||||
|
||||
## Statut
|
||||
|
||||
`0.3.5-alpha.1` a été validée par l'utilisateur le 2026-09-21. Le cadrage WebTransport/QUIC, le plan actif et la synchronisation de version sont acceptés sans fix.
|
||||
|
||||
La suite peut ouvrir `0.3.5-alpha.2` pour introduire le backend natif minimal et prouver l'établissement TLS/WebTransport avec pinning de certificat.
|
||||
|
||||
## Gates fournies
|
||||
|
||||
Les commandes exécutées par l'utilisateur :
|
||||
|
||||
```text
|
||||
cargo fmt --all -- --check
|
||||
python3 scripts/audit_rust_workspace_rules.py
|
||||
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates Android Web deltas history
|
||||
python3 scripts/audit_distribution_layout.py
|
||||
cargo check --workspace
|
||||
```
|
||||
|
||||
ont terminé proprement avec :
|
||||
|
||||
```text
|
||||
General Rust rule audit: clean
|
||||
Rust export completeness audit: 0 candidate(s)
|
||||
games.sasedev workspace audit: clean
|
||||
Markdown table audit: clean (5 table(s), 282 file(s))
|
||||
Distribution layout audit: clean (50 required path(s), 8 forbidden path(s) absent)
|
||||
cargo check --workspace: clean
|
||||
```
|
||||
|
||||
Le workspace compilé porte `0.3.5-alpha.1`, y compris les deux POC Tauri affichés dans la sortie finale.
|
||||
|
||||
## Conséquence
|
||||
|
||||
Le forecast révisé de `docs/plans/005-V0_3_5_WEBTRANSPORT_QUIC_POC_PLAN.md` est retenu. `alpha.2` reste limitée à la crate backend, aux dépendances/features minimales, à l'identité TLS/hash pinning et à l'établissement natif ; streams fiables, framing et contrat commun restent réservés à `alpha.3`.
|
||||
Reference in New Issue
Block a user