// Created by: Claude // Date: 2026-01-04 // Purpose: QUIC endpoint management with P2P handshake // Refs: AGENT.md, signaling_v_2.md use anyhow::Result; use quinn::{Endpoint, Connection, RecvStream, SendStream}; use std::sync::Arc; use std::net::SocketAddr; use tokio::sync::Mutex; use std::collections::HashMap; use tracing::{info, warn, error}; use super::{tls, protocol::*}; pub struct QuicEndpoint { endpoint: Endpoint, local_port: u16, active_sessions: Arc>>, // Cache local pour validation des session_tokens valid_tokens: Arc>>, } struct ActiveSession { pub session_id: String, pub connection: Connection, } struct SessionTokenCache { session_id: String, session_token: String, expires_at: std::time::SystemTime, } impl QuicEndpoint { pub async fn new(port: u16) -> Result { let rustls_server_config = tls::make_server_config()?; let server_config = quinn::ServerConfig::with_crypto(Arc::new(rustls_server_config)); let addr: SocketAddr = format!("0.0.0.0:{}", port).parse()?; let endpoint = Endpoint::server(server_config, addr)?; let local_port = endpoint.local_addr()?.port(); info!("QUIC endpoint listening on port {}", local_port); Ok(Self { endpoint, local_port, active_sessions: Arc::new(Mutex::new(HashMap::new())), valid_tokens: Arc::new(Mutex::new(HashMap::new())), }) } pub fn local_port(&self) -> u16 { self.local_port } /// Ajouter un token au cache (appelé par P2PHandler lors de p2p.session.created) pub async fn add_valid_token(&self, session_id: String, session_token: String, ttl_secs: u64) { let expires_at = std::time::SystemTime::now() + std::time::Duration::from_secs(ttl_secs); let cache_entry = SessionTokenCache { session_id: session_id.clone(), session_token, expires_at, }; self.valid_tokens.lock().await.insert(session_id.clone(), cache_entry); info!("Token cached for session: {} (TTL: {}s)", session_id, ttl_secs); } /// Valider un token depuis le cache local async fn validate_token(&self, session_id: &str, session_token: &str) -> Result<()> { let tokens = self.valid_tokens.lock().await; if let Some(cached) = tokens.get(session_id) { if cached.session_token != session_token { anyhow::bail!("Token mismatch"); } if std::time::SystemTime::now() > cached.expires_at { anyhow::bail!("Token expired"); } Ok(()) } else { anyhow::bail!("Session not found in cache") } } /// Accept loop (spawn dans main) pub async fn accept_loop(self: Arc) -> Result<()> { info!("Starting QUIC accept loop"); loop { let incoming = match self.endpoint.accept().await { Some(incoming) => incoming, None => { info!("QUIC endpoint closed"); return Ok(()); } }; let endpoint_clone = Arc::clone(&self); tokio::spawn(async move { if let Err(e) = endpoint_clone.handle_incoming(incoming).await { error!("Failed to handle incoming connection: {}", e); } }); } } pub fn close(&self) { self.endpoint.close(0u32.into(), b"shutdown"); } async fn handle_incoming(&self, incoming: quinn::Connecting) -> Result<()> { let connection = incoming.await?; info!("Incoming QUIC connection from {}", connection.remote_address()); // Wait for P2P_HELLO let (send, recv) = connection.accept_bi().await?; let hello = self.receive_hello(recv).await?; info!("P2P_HELLO received: session_id={}", hello.session_id); // Valider session_token via cache local if let Err(e) = self.validate_token(&hello.session_id, &hello.session_token).await { warn!("Token validation failed: {}", e); self.send_response(send, &P2PResponse::Deny { reason: format!("Invalid token: {}", e), }).await?; return Ok(()); } // Send P2P_OK self.send_response(send, &P2PResponse::Ok).await?; info!("P2P handshake successful for session: {}", hello.session_id); // Store session let session = ActiveSession { session_id: hello.session_id.clone(), connection, }; self.active_sessions.lock().await.insert(hello.session_id, session); Ok(()) } /// Connect to remote peer pub async fn connect_to_peer( &self, remote_addr: SocketAddr, session_id: String, session_token: String, device_id: String, ) -> Result { let rustls_client_config = tls::make_client_config()?; let mut client_config = quinn::ClientConfig::new(Arc::new(rustls_client_config)); // Configurer transport parameters si nécessaire let mut transport_config = quinn::TransportConfig::default(); transport_config.max_idle_timeout(Some(std::time::Duration::from_secs(30).try_into()?)); client_config.transport_config(Arc::new(transport_config)); info!("Connecting to peer at {}", remote_addr); let connection = self.endpoint.connect_with( client_config, remote_addr, "mesh-peer", )?.await?; info!("QUIC connection established to {}", remote_addr); // Send P2P_HELLO let (mut send, recv) = connection.open_bi().await?; self.send_hello(&mut send, session_id.clone(), session_token, device_id).await?; // Wait for P2P_OK let response = self.receive_response(recv).await?; match response { P2PResponse::Ok => { info!("P2P handshake successful for session: {}", session_id); Ok(connection) } P2PResponse::Deny { reason } => { anyhow::bail!("P2P handshake denied: {}", reason) } } } async fn send_hello( &self, stream: &mut SendStream, session_id: String, session_token: String, device_id: String, ) -> Result<()> { let hello = P2PHello { t: "P2P_HELLO".to_string(), session_id, session_token, from_device_id: device_id, }; let json = serde_json::to_vec(&hello)?; stream.write_all(&json).await?; stream.finish().await?; info!("Sent P2P_HELLO"); Ok(()) } async fn receive_hello(&self, mut stream: RecvStream) -> Result { let data = stream.read_to_end(4096).await?; let hello: P2PHello = serde_json::from_slice(&data)?; if hello.t != "P2P_HELLO" { anyhow::bail!("Expected P2P_HELLO, got {}", hello.t); } Ok(hello) } async fn send_response(&self, mut stream: SendStream, response: &P2PResponse) -> Result<()> { let json = serde_json::to_vec(response)?; stream.write_all(&json).await?; stream.finish().await?; Ok(()) } async fn receive_response(&self, mut stream: RecvStream) -> Result { let data = stream.read_to_end(4096).await?; Ok(serde_json::from_slice(&data)?) } /// Get active session by ID pub async fn get_session(&self, session_id: &str) -> Option { self.active_sessions.lock().await.get(session_id).map(|s| s.connection.clone()) } }