- DredTuner: maps live network metrics (loss/RTT/jitter) to continuous DRED duration every ~500ms instead of discrete tier-locked values. Includes jitter-spike detection for pre-emptive Starlink-style boost. - Opus6k DRED extended from 500ms to 1040ms (max libopus 1.5 supports) - PMTUD: quinn MtuDiscoveryConfig with upper_bound=1452, 300s interval - TrunkedForwarder respects discovered MTU (was hard-coded 1200) - QuinnPathSnapshot exposes quinn internal stats + discovered MTU - AudioEncoder trait: set_expected_loss() + set_dred_duration() methods - PathMonitor: sliding-window jitter variance for spike detection - Integrated into both Android and desktop send tasks in engine.rs - 14 new tests (10 tuner unit + 4 encoder integration) - Updated ARCHITECTURE.md, PROGRESS.md, PRD-dred-integration, PRD-mtu Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
229 lines
8.9 KiB
Rust
229 lines
8.9 KiB
Rust
//! QUIC configuration tuned for lossy VoIP links.
|
|
|
|
use std::sync::Arc;
|
|
use std::time::Duration;
|
|
|
|
use quinn::crypto::rustls::QuicClientConfig;
|
|
use quinn::crypto::rustls::QuicServerConfig;
|
|
|
|
/// Create a server configuration with a self-signed certificate (random keypair).
|
|
///
|
|
/// The certificate changes on every call. Use `server_config_from_seed` for
|
|
/// a deterministic certificate that survives relay restarts.
|
|
pub fn server_config() -> (quinn::ServerConfig, Vec<u8>) {
|
|
let cert_key = rcgen::generate_simple_self_signed(vec!["localhost".to_string()])
|
|
.expect("failed to generate self-signed cert");
|
|
let cert_der = rustls::pki_types::CertificateDer::from(cert_key.cert);
|
|
let key_der =
|
|
rustls::pki_types::PrivateKeyDer::try_from(cert_key.key_pair.serialize_der()).unwrap();
|
|
build_server_config(cert_der, key_der)
|
|
}
|
|
|
|
/// Create a server configuration with a deterministic self-signed certificate
|
|
/// derived from a 32-byte seed. Same seed = same cert = same TLS fingerprint.
|
|
pub fn server_config_from_seed(seed: &[u8; 32]) -> (quinn::ServerConfig, Vec<u8>) {
|
|
use ed25519_dalek::pkcs8::EncodePrivateKey;
|
|
use ed25519_dalek::SigningKey;
|
|
use hkdf::Hkdf;
|
|
use sha2::Sha256;
|
|
|
|
// Derive Ed25519 key bytes from seed via HKDF
|
|
let hk = Hkdf::<Sha256>::new(None, seed);
|
|
let mut ed_bytes = [0u8; 32];
|
|
hk.expand(b"wzp-tls-ed25519", &mut ed_bytes)
|
|
.expect("HKDF expand failed");
|
|
|
|
// Create Ed25519 signing key and export as PKCS8 DER
|
|
let signing_key = SigningKey::from_bytes(&ed_bytes);
|
|
let pkcs8_doc = signing_key.to_pkcs8_der()
|
|
.expect("failed to encode Ed25519 key as PKCS8");
|
|
let key_der_for_rcgen = rustls::pki_types::PrivateKeyDer::try_from(pkcs8_doc.as_bytes().to_vec())
|
|
.expect("failed to wrap PKCS8 DER");
|
|
|
|
// Create rcgen KeyPair from DER
|
|
let key_pair = rcgen::KeyPair::from_der_and_sign_algo(
|
|
&key_der_for_rcgen,
|
|
&rcgen::PKCS_ED25519,
|
|
)
|
|
.expect("failed to create KeyPair from seed-derived Ed25519 key");
|
|
|
|
// Build self-signed cert with this deterministic keypair
|
|
let params = rcgen::CertificateParams::new(vec!["localhost".to_string()])
|
|
.expect("failed to create CertificateParams");
|
|
let cert = params.self_signed(&key_pair).expect("failed to self-sign cert");
|
|
let cert_der = rustls::pki_types::CertificateDer::from(cert.der().to_vec());
|
|
let key_der = rustls::pki_types::PrivateKeyDer::try_from(key_pair.serialize_der())
|
|
.expect("failed to serialize key DER");
|
|
|
|
build_server_config(cert_der, key_der)
|
|
}
|
|
|
|
/// Compute a hex-formatted SHA-256 fingerprint of a DER-encoded certificate.
|
|
///
|
|
/// Format: `xx:xx:xx:xx:...` (32 bytes = 64 hex chars with colons).
|
|
pub fn tls_fingerprint(cert_der: &[u8]) -> String {
|
|
use sha2::{Sha256, Digest};
|
|
let hash = Sha256::digest(cert_der);
|
|
hash.iter()
|
|
.map(|b| format!("{b:02x}"))
|
|
.collect::<Vec<_>>()
|
|
.join(":")
|
|
}
|
|
|
|
fn build_server_config(
|
|
cert_der: rustls::pki_types::CertificateDer<'static>,
|
|
key_der: rustls::pki_types::PrivateKeyDer<'static>,
|
|
) -> (quinn::ServerConfig, Vec<u8>) {
|
|
let mut server_crypto = rustls::ServerConfig::builder()
|
|
.with_no_client_auth()
|
|
.with_single_cert(vec![cert_der.clone()], key_der)
|
|
.expect("bad server cert/key");
|
|
server_crypto.alpn_protocols = vec![b"wzp".to_vec()];
|
|
|
|
let quic_server_config =
|
|
QuicServerConfig::try_from(server_crypto).expect("failed to create QuicServerConfig");
|
|
|
|
let mut server_config = quinn::ServerConfig::with_crypto(Arc::new(quic_server_config));
|
|
let transport = transport_config();
|
|
server_config.transport_config(Arc::new(transport));
|
|
|
|
(server_config, cert_der.to_vec())
|
|
}
|
|
|
|
/// Create a client configuration that trusts any certificate (for testing).
|
|
///
|
|
/// Uses the same VoIP-tuned transport parameters as the server.
|
|
pub fn client_config() -> quinn::ClientConfig {
|
|
let mut client_crypto = rustls::ClientConfig::builder()
|
|
.dangerous()
|
|
.with_custom_certificate_verifier(Arc::new(SkipServerVerification))
|
|
.with_no_client_auth();
|
|
client_crypto.alpn_protocols = vec![b"wzp".to_vec()];
|
|
|
|
let quic_client_config =
|
|
QuicClientConfig::try_from(client_crypto).expect("failed to create QuicClientConfig");
|
|
|
|
let mut client_config = quinn::ClientConfig::new(Arc::new(quic_client_config));
|
|
let transport = transport_config();
|
|
client_config.transport_config(Arc::new(transport));
|
|
|
|
client_config
|
|
}
|
|
|
|
/// Shared transport configuration tuned for lossy VoIP.
|
|
fn transport_config() -> quinn::TransportConfig {
|
|
let mut config = quinn::TransportConfig::default();
|
|
|
|
// 30 second idle timeout
|
|
config.max_idle_timeout(Some(
|
|
quinn::IdleTimeout::try_from(Duration::from_secs(30)).unwrap(),
|
|
));
|
|
|
|
// 5 second keep-alive interval
|
|
config.keep_alive_interval(Some(Duration::from_secs(5)));
|
|
|
|
// Enable DATAGRAM extension for unreliable media packets.
|
|
config.datagram_receive_buffer_size(Some(65536));
|
|
|
|
// Conservative flow control for bandwidth-constrained links
|
|
config.receive_window(quinn::VarInt::from_u32(256 * 1024)); // 256KB
|
|
config.send_window(128 * 1024); // 128KB
|
|
config.stream_receive_window(quinn::VarInt::from_u32(64 * 1024)); // 64KB per stream
|
|
|
|
// Aggressive initial RTT estimate for high-latency links
|
|
config.initial_rtt(Duration::from_millis(300));
|
|
|
|
// PMTUD (Path MTU Discovery) — quinn 0.11 enables this by default but
|
|
// with conservative bounds (initial 1200, upper 1452). We keep the safe
|
|
// initial_mtu of 1200 so the first packets always get through, but raise
|
|
// upper_bound so the binary search can discover larger MTUs on paths that
|
|
// support them. Typical results:
|
|
// - Ethernet/fiber: discovers ~1452 (Ethernet MTU minus IP/UDP/QUIC)
|
|
// - WireGuard/VPN: discovers ~1380-1420
|
|
// - Starlink: discovers ~1400-1452
|
|
// - Cellular: stays at 1200-1300
|
|
// Black hole detection automatically falls back to 1200 if probes fail.
|
|
// This matters for future video frames which can be 1-50 KB and benefit
|
|
// from fewer application-layer fragments per frame.
|
|
let mut mtu_config = quinn::MtuDiscoveryConfig::default();
|
|
mtu_config
|
|
.upper_bound(1452)
|
|
.interval(Duration::from_secs(300)) // re-probe every 5 min
|
|
.black_hole_cooldown(Duration::from_secs(30)); // retry faster on lossy links
|
|
config.mtu_discovery_config(Some(mtu_config));
|
|
config.initial_mtu(1200); // safe starting point
|
|
|
|
config
|
|
}
|
|
|
|
/// Certificate verifier that accepts any server certificate (testing only).
|
|
#[derive(Debug)]
|
|
struct SkipServerVerification;
|
|
|
|
impl rustls::client::danger::ServerCertVerifier for SkipServerVerification {
|
|
fn verify_server_cert(
|
|
&self,
|
|
_end_entity: &rustls::pki_types::CertificateDer<'_>,
|
|
_intermediates: &[rustls::pki_types::CertificateDer<'_>],
|
|
_server_name: &rustls::pki_types::ServerName<'_>,
|
|
_ocsp_response: &[u8],
|
|
_now: rustls::pki_types::UnixTime,
|
|
) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
|
|
Ok(rustls::client::danger::ServerCertVerified::assertion())
|
|
}
|
|
|
|
fn verify_tls12_signature(
|
|
&self,
|
|
_message: &[u8],
|
|
_cert: &rustls::pki_types::CertificateDer<'_>,
|
|
_dss: &rustls::DigitallySignedStruct,
|
|
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
|
|
Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
|
|
}
|
|
|
|
fn verify_tls13_signature(
|
|
&self,
|
|
_message: &[u8],
|
|
_cert: &rustls::pki_types::CertificateDer<'_>,
|
|
_dss: &rustls::DigitallySignedStruct,
|
|
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
|
|
Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
|
|
}
|
|
|
|
fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
|
|
// Support the schemes that rustls typically uses
|
|
vec![
|
|
rustls::SignatureScheme::ECDSA_NISTP256_SHA256,
|
|
rustls::SignatureScheme::ECDSA_NISTP384_SHA384,
|
|
rustls::SignatureScheme::ED25519,
|
|
rustls::SignatureScheme::RSA_PSS_SHA256,
|
|
rustls::SignatureScheme::RSA_PSS_SHA384,
|
|
rustls::SignatureScheme::RSA_PSS_SHA512,
|
|
rustls::SignatureScheme::RSA_PKCS1_SHA256,
|
|
rustls::SignatureScheme::RSA_PKCS1_SHA384,
|
|
rustls::SignatureScheme::RSA_PKCS1_SHA512,
|
|
]
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn server_config_creates_without_error() {
|
|
let _ = rustls::crypto::ring::default_provider().install_default();
|
|
let (cfg, cert_der) = server_config();
|
|
assert!(!cert_der.is_empty());
|
|
// Verify the config was created (no panic)
|
|
drop(cfg);
|
|
}
|
|
|
|
#[test]
|
|
fn client_config_creates_without_error() {
|
|
let _ = rustls::crypto::ring::default_provider().install_default();
|
|
let cfg = client_config();
|
|
drop(cfg);
|
|
}
|
|
}
|