diff --git a/crates/quicprochat-p2p/src/transport.rs b/crates/quicprochat-p2p/src/transport.rs index b1509c8..7c8a0da 100644 --- a/crates/quicprochat-p2p/src/transport.rs +++ b/crates/quicprochat-p2p/src/transport.rs @@ -35,6 +35,77 @@ impl fmt::Display for TransportAddr { } } +/// Transport capability level for crypto mode selection. +/// +/// Ordered from worst to best so max_by_key picks the best transport. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub enum TransportCapability { + /// Very low bandwidth, severely duty-cycled (LoRa SF11-SF12, serial). + /// MLS-Lite without signature preferred. + SeverelyConstrained = 0, + /// Low bandwidth, duty-cycled (LoRa SF7-SF10). + /// Classical MLS marginal, prefer MLS-Lite with sig. + Constrained = 1, + /// Medium bandwidth (BLE, slower WiFi). + /// Supports full MLS with classical crypto. + Medium = 2, + /// High-bandwidth, low-latency (QUIC, TCP, WiFi). + /// Supports full MLS with PQ-KEM, large KeyPackages. + Unconstrained = 3, +} + +impl TransportCapability { + /// Determine capability from bitrate and MTU. + pub fn from_metrics(bitrate_bps: u64, mtu: usize) -> Self { + match (bitrate_bps, mtu) { + (b, _) if b >= 1_000_000 => Self::Unconstrained, // ≥1 Mbps + (b, m) if b >= 10_000 && m >= 200 => Self::Medium, // ≥10 kbps, decent MTU + (b, m) if b >= 1_000 || m >= 100 => Self::Constrained, // ≥1 kbps + _ => Self::SeverelyConstrained, + } + } + + /// Recommended crypto mode for this capability level. + pub fn recommended_crypto(&self) -> CryptoMode { + match self { + Self::Unconstrained => CryptoMode::MlsHybrid, + Self::Medium => CryptoMode::MlsClassical, + Self::Constrained => CryptoMode::MlsLiteSigned, + Self::SeverelyConstrained => CryptoMode::MlsLiteUnsigned, + } + } + + /// Whether full MLS is viable on this transport. + pub fn supports_mls(&self) -> bool { + matches!(self, Self::Unconstrained | Self::Medium) + } +} + +/// Crypto mode for mesh messaging. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum CryptoMode { + /// Full MLS with X25519 + ML-KEM-768 hybrid. + MlsHybrid, + /// Full MLS with classical X25519 only. + MlsClassical, + /// MLS-Lite with Ed25519 signature. + MlsLiteSigned, + /// MLS-Lite without signature (smallest overhead). + MlsLiteUnsigned, +} + +impl CryptoMode { + /// Approximate overhead in bytes for this mode. + pub fn overhead_bytes(&self) -> usize { + match self { + Self::MlsHybrid => 2700, // PQ KeyPackage alone + Self::MlsClassical => 400, // Classical KeyPackage + message + Self::MlsLiteSigned => 262, // MLS-Lite with sig + Self::MlsLiteUnsigned => 129, // MLS-Lite minimal + } + } +} + /// Metadata about a transport's capabilities. #[derive(Clone, Debug)] pub struct TransportInfo { @@ -48,6 +119,18 @@ pub struct TransportInfo { pub bidirectional: bool, } +impl TransportInfo { + /// Compute capability level from this transport's metrics. + pub fn capability(&self) -> TransportCapability { + TransportCapability::from_metrics(self.bitrate, self.mtu) + } + + /// Recommended crypto mode for this transport. + pub fn recommended_crypto(&self) -> CryptoMode { + self.capability().recommended_crypto() + } +} + /// Received packet from a transport. #[derive(Clone, Debug)] pub struct TransportPacket { @@ -137,4 +220,70 @@ mod tests { assert_eq!(a, b); assert_ne!(a, c); } + + #[test] + fn capability_ordering() { + // Higher value = better capability + assert!(TransportCapability::Unconstrained > TransportCapability::Medium); + assert!(TransportCapability::Medium > TransportCapability::Constrained); + assert!(TransportCapability::Constrained > TransportCapability::SeverelyConstrained); + + // max_by_key should pick the best + let caps = vec![ + TransportCapability::Constrained, + TransportCapability::Unconstrained, + TransportCapability::Medium, + ]; + let best = caps.into_iter().max().unwrap(); + assert_eq!(best, TransportCapability::Unconstrained); + } + + #[test] + fn capability_recommended_crypto() { + assert_eq!( + TransportCapability::Unconstrained.recommended_crypto(), + CryptoMode::MlsHybrid + ); + assert_eq!( + TransportCapability::Medium.recommended_crypto(), + CryptoMode::MlsClassical + ); + assert_eq!( + TransportCapability::Constrained.recommended_crypto(), + CryptoMode::MlsLiteSigned + ); + assert_eq!( + TransportCapability::SeverelyConstrained.recommended_crypto(), + CryptoMode::MlsLiteUnsigned + ); + } + + #[test] + fn transport_info_capability() { + let tcp_info = TransportInfo { + name: "tcp".to_string(), + mtu: 1500, + bitrate: 100_000_000, // 100 Mbps + bidirectional: true, + }; + assert_eq!(tcp_info.capability(), TransportCapability::Unconstrained); + assert_eq!(tcp_info.recommended_crypto(), CryptoMode::MlsHybrid); + + let lora_info = TransportInfo { + name: "lora".to_string(), + mtu: 51, + bitrate: 300, + bidirectional: true, + }; + assert_eq!(lora_info.capability(), TransportCapability::SeverelyConstrained); + assert_eq!(lora_info.recommended_crypto(), CryptoMode::MlsLiteUnsigned); + } + + #[test] + fn crypto_mode_overhead() { + assert!(CryptoMode::MlsHybrid.overhead_bytes() > 2000); + assert!(CryptoMode::MlsClassical.overhead_bytes() < 500); + assert!(CryptoMode::MlsLiteSigned.overhead_bytes() < 300); + assert!(CryptoMode::MlsLiteUnsigned.overhead_bytes() < 150); + } } diff --git a/crates/quicprochat-p2p/src/transport_manager.rs b/crates/quicprochat-p2p/src/transport_manager.rs index cdaacfe..c55882b 100644 --- a/crates/quicprochat-p2p/src/transport_manager.rs +++ b/crates/quicprochat-p2p/src/transport_manager.rs @@ -8,7 +8,7 @@ use anyhow::{bail, Result}; -use crate::transport::{MeshTransport, TransportAddr, TransportInfo}; +use crate::transport::{CryptoMode, MeshTransport, TransportAddr, TransportCapability, TransportInfo}; /// Manages multiple mesh transports and routes packets to the best available one. pub struct TransportManager { @@ -81,6 +81,63 @@ impl TransportManager { } Ok(()) } + + /// Get the best (highest capability) transport available. + pub fn best_transport(&self) -> Option<&dyn MeshTransport> { + self.transports + .iter() + .max_by_key(|t| t.info().capability()) + .map(|t| t.as_ref()) + } + + /// Get the capability level of the best available transport. + pub fn best_capability(&self) -> Option { + self.best_transport().map(|t| t.info().capability()) + } + + /// Get the recommended crypto mode based on best available transport. + pub fn recommended_crypto(&self) -> CryptoMode { + self.best_capability() + .map(|c| c.recommended_crypto()) + .unwrap_or(CryptoMode::MlsLiteUnsigned) + } + + /// Check if any transport supports full MLS. + pub fn supports_mls(&self) -> bool { + self.transports.iter().any(|t| t.info().capability().supports_mls()) + } + + /// Get the capability level for a specific transport name. + pub fn capability_for(&self, name: &str) -> Option { + self.transports + .iter() + .find(|t| t.info().name == name) + .map(|t| t.info().capability()) + } + + /// Select the best transport for a given data size. + /// + /// Prefers transports where the data fits in one MTU. + /// Falls back to highest-capability transport if fragmentation is needed. + pub fn select_for_size(&self, data_size: usize) -> Option<&dyn MeshTransport> { + // First, try transports where data fits in MTU + let fits: Vec<_> = self + .transports + .iter() + .filter(|t| t.info().mtu >= data_size) + .collect(); + + if !fits.is_empty() { + // Among those that fit, prefer highest capability + return fits + .into_iter() + .max_by_key(|t| t.info().capability()) + .map(|t| t.as_ref()); + } + + // Nothing fits — return highest capability (will need fragmentation) + self.best_transport() + } } impl Default for TransportManager { @@ -178,4 +235,105 @@ mod tests { let result = mgr.close_all().await; assert!(result.is_ok()); } + + struct MockLoRaTransport; + + #[async_trait::async_trait] + impl MeshTransport for MockLoRaTransport { + fn info(&self) -> TransportInfo { + TransportInfo { + name: "lora".to_string(), + mtu: 51, // SF12 LoRa + bitrate: 300, // ~300 bps + bidirectional: true, + } + } + + async fn send(&self, _dest: &TransportAddr, _data: &[u8]) -> Result<()> { + Ok(()) + } + + async fn recv(&self) -> Result { + bail!("mock") + } + } + + #[test] + fn capability_classification() { + use crate::transport::TransportCapability; + + // High bandwidth = Unconstrained + assert_eq!( + TransportCapability::from_metrics(10_000_000, 1500), + TransportCapability::Unconstrained + ); + + // Medium bandwidth = Medium + assert_eq!( + TransportCapability::from_metrics(50_000, 500), + TransportCapability::Medium + ); + + // LoRa-like = Constrained + assert_eq!( + TransportCapability::from_metrics(1200, 200), + TransportCapability::Constrained + ); + + // Very slow = SeverelyConstrained + assert_eq!( + TransportCapability::from_metrics(300, 51), + TransportCapability::SeverelyConstrained + ); + } + + #[test] + fn best_transport_selection() { + let mut mgr = TransportManager::new(); + mgr.add(Box::new(MockLoRaTransport)); + mgr.add(Box::new(MockTransport::new("tcp"))); + + // TCP should be best (higher capability) + let best = mgr.best_transport().expect("should have transport"); + assert_eq!(best.info().name, "tcp"); + assert!(mgr.supports_mls()); + } + + #[test] + fn recommended_crypto_based_on_transports() { + use crate::transport::CryptoMode; + + // With TCP available → MLS Hybrid + let mut mgr = TransportManager::new(); + mgr.add(Box::new(MockTransport::new("tcp"))); + assert_eq!(mgr.recommended_crypto(), CryptoMode::MlsHybrid); + + // With only LoRa → MLS-Lite unsigned + let mut mgr_lora = TransportManager::new(); + mgr_lora.add(Box::new(MockLoRaTransport)); + assert_eq!(mgr_lora.recommended_crypto(), CryptoMode::MlsLiteUnsigned); + + // Empty → default to MLS-Lite unsigned + let empty = TransportManager::new(); + assert_eq!(empty.recommended_crypto(), CryptoMode::MlsLiteUnsigned); + } + + #[test] + fn select_for_size_prefers_fitting() { + let mut mgr = TransportManager::new(); + mgr.add(Box::new(MockLoRaTransport)); // MTU 51 + mgr.add(Box::new(MockTransport::new("tcp"))); // MTU 1500 + + // Small data should prefer TCP (fits and higher capability) + let small = mgr.select_for_size(100).expect("transport"); + assert_eq!(small.info().name, "tcp"); + + // Data larger than LoRa MTU but smaller than TCP should use TCP + let medium = mgr.select_for_size(500).expect("transport"); + assert_eq!(medium.info().name, "tcp"); + + // Huge data still uses TCP (highest capability) + let huge = mgr.select_for_size(10000).expect("transport"); + assert_eq!(huge.info().name, "tcp"); + } }