use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::time::Duration; use tokio::net::{UdpSocket, lookup_host}; use tokio::time::timeout; pub(super) async fn probe(url: &str, bind_ip: Ipv4Addr) -> anyhow::Result { let endpoint = url.strip_prefix("stun:").ok_or_else(|| anyhow::anyhow!("URL STUN non prise en charge"))?; let endpoint = endpoint.split('?').next().unwrap_or(endpoint); let mut addresses = lookup_host(endpoint).await?; let server = addresses.find(SocketAddr::is_ipv4).ok_or_else(|| anyhow::anyhow!("aucune adresse IPv4 pour le serveur STUN"))?; let socket = UdpSocket::bind((bind_ip, 0)).await?; let local = socket.local_addr()?; let transaction = *uuid::Uuid::new_v4().as_bytes(); let mut request = [0u8; 20]; request[..2].copy_from_slice(&0x0001u16.to_be_bytes()); request[4..8].copy_from_slice(&0x2112a442u32.to_be_bytes()); request[8..20].copy_from_slice(&transaction[..12]); tracing::info!(%server, %local, "Diagnostic STUN IPv4 : requête Binding envoyée (socket distinct d’ICE)"); socket.send_to(&request, server).await?; let mut response = [0u8; 1500]; let (length, sender) = timeout(Duration::from_secs(5), socket.recv_from(&mut response)).await??; anyhow::ensure!(sender == server, "réponse STUN d'une autre adresse : {sender}"); let mapped = mapped_address(&response[..length], &transaction[..12])?; tracing::info!(%server, %local, %mapped, "Diagnostic STUN IPv4 : réponse Binding reçue"); Ok(mapped) } fn mapped_address(response: &[u8], transaction: &[u8]) -> anyhow::Result { anyhow::ensure!(response.len() >= 20 && response[0..2] == [0x01, 0x01] && response[4..8] == 0x2112a442u32.to_be_bytes() && response[8..20] == *transaction, "réponse STUN invalide ou transaction différente"); let size = u16::from_be_bytes([response[2], response[3]]) as usize; anyhow::ensure!(response.len() >= 20 + size, "réponse STUN tronquée"); let mut offset = 20; while offset + 4 <= 20 + size { let kind = u16::from_be_bytes([response[offset], response[offset + 1]]); let len = u16::from_be_bytes([response[offset + 2], response[offset + 3]]) as usize; offset += 4; anyhow::ensure!(offset + len <= 20 + size, "attribut STUN tronqué"); if kind == 0x0020 && len >= 8 && response[offset + 1] == 1 { let port = u16::from_be_bytes([response[offset + 2], response[offset + 3]]) ^ 0x2112; let cookie = 0x2112a442u32.to_be_bytes(); let ip = Ipv4Addr::from(std::array::from_fn(|i| response[offset + 4 + i] ^ cookie[i])); return Ok(SocketAddr::new(IpAddr::V4(ip), port)); } offset += (len + 3) & !3; } anyhow::bail!("aucune adresse IPv4 XOR-MAPPED-ADDRESS dans la réponse STUN") } #[cfg(test)] mod tests { use super::mapped_address; #[test] fn decodes_ipv4_and_rejects_unrelated_response() { let transaction = [7; 12]; let mut response = vec![0x01, 0x01, 0, 12, 0x21, 0x12, 0xa4, 0x42]; response.extend(transaction); response.extend([0, 0x20, 0, 8, 0, 1, 0x3f, 0x52, 0x6a, 0x22, 0xa5, 0x43]); assert_eq!(mapped_address(&response, &transaction).unwrap().to_string(), "75.48.1.1:7744"); assert!(mapped_address(&response, &[8; 12]).is_err()); assert!(mapped_address(&response[..25], &transaction).is_err()); } }