//! Minimal raw STUN client for NAT type detection. //! Sends STUN Binding Requests to two servers and compares mapped ports. use std::net::SocketAddr; use tokio::net::UdpSocket; use tracing::{debug, warn}; use crate::types::{NatMapping, NatType}; const STUN_SERVERS: &[&str] = &[ "stun.l.google.com:19302", "stun.cloudflare.com:3478", ]; const STUN_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(3); /// STUN Binding Request (RFC 5389): 20 bytes /// Type: 0x0001 (Binding Request), Length: 0, Magic: 0x2112A442, Transaction ID: 12 random bytes fn build_binding_request() -> [u8; 20] { let mut buf = [0u8; 20]; // Message type: Binding Request (0x0001) buf[0] = 0x00; buf[1] = 0x01; // Message length: 0 buf[2] = 0x00; buf[3] = 0x00; // Magic cookie: 0x2112A442 buf[4] = 0x21; buf[5] = 0x12; buf[6] = 0xA4; buf[7] = 0x42; // Transaction ID: 12 random bytes use rand::Rng; let mut rng = rand::rng(); rng.fill(&mut buf[8..20]); buf } /// Parse XOR-MAPPED-ADDRESS from a STUN Binding Response. /// Returns the mapped SocketAddr or None if not found/parseable. fn parse_xor_mapped_address(resp: &[u8], txn_id: &[u8; 12]) -> Option { if resp.len() < 20 { return None; } // Verify it's a Binding Response (0x0101) if resp[0] != 0x01 || resp[1] != 0x01 { return None; } let magic: [u8; 4] = [0x21, 0x12, 0xA4, 0x42]; // Walk attributes let msg_len = u16::from_be_bytes([resp[2], resp[3]]) as usize; let end = std::cmp::min(20 + msg_len, resp.len()); let mut pos = 20; while pos + 4 <= end { let attr_type = u16::from_be_bytes([resp[pos], resp[pos + 1]]); let attr_len = u16::from_be_bytes([resp[pos + 2], resp[pos + 3]]) as usize; pos += 4; if pos + attr_len > end { break; } // XOR-MAPPED-ADDRESS = 0x0020, MAPPED-ADDRESS = 0x0001 if attr_type == 0x0020 && attr_len >= 8 { // byte 0: reserved, byte 1: family (0x01=IPv4, 0x02=IPv6) let family = resp[pos + 1]; if family == 0x01 { // IPv4 let xport = u16::from_be_bytes([resp[pos + 2], resp[pos + 3]]) ^ u16::from_be_bytes([magic[0], magic[1]]); let xip = [ resp[pos + 4] ^ magic[0], resp[pos + 5] ^ magic[1], resp[pos + 6] ^ magic[2], resp[pos + 7] ^ magic[3], ]; let addr = SocketAddr::new( std::net::IpAddr::V4(std::net::Ipv4Addr::new(xip[0], xip[1], xip[2], xip[3])), xport, ); return Some(addr); } else if family == 0x02 && attr_len >= 20 { // IPv6: XOR with magic + txn_id let xport = u16::from_be_bytes([resp[pos + 2], resp[pos + 3]]) ^ u16::from_be_bytes([magic[0], magic[1]]); let mut ip6 = [0u8; 16]; let xor_key: Vec = magic.iter().chain(txn_id.iter()).copied().collect(); for i in 0..16 { ip6[i] = resp[pos + 4 + i] ^ xor_key[i]; } let addr = SocketAddr::new( std::net::IpAddr::V6(std::net::Ipv6Addr::from(ip6)), xport, ); return Some(addr); } } // Pad to 4-byte boundary pos += (attr_len + 3) & !3; } None } /// Query a single STUN server and return the mapped address. async fn stun_query(sock: &UdpSocket, server: &str) -> Option { use std::net::ToSocketAddrs; let server_addr = match server.to_socket_addrs() { Ok(mut addrs) => addrs.next()?, Err(e) => { debug!(server, error = %e, "STUN DNS resolution failed"); return None; } }; let request = build_binding_request(); let txn_id: [u8; 12] = request[8..20].try_into().unwrap(); if let Err(e) = sock.send_to(&request, server_addr).await { debug!(server, error = %e, "STUN send failed"); return None; } let mut buf = [0u8; 256]; match tokio::time::timeout(STUN_TIMEOUT, sock.recv_from(&mut buf)).await { Ok(Ok((len, _))) => parse_xor_mapped_address(&buf[..len], &txn_id), Ok(Err(e)) => { debug!(server, error = %e, "STUN recv failed"); None } Err(_) => { debug!(server, "STUN query timed out (3s)"); None } } } /// Detect NAT type by comparing mapped addresses from two STUN servers. /// Must be called with the local port we're interested in (for Public detection). /// Also returns the NatMapping classification for the advanced NAT profile. pub async fn detect_nat_type(local_port: u16) -> (NatType, NatMapping) { let sock = match UdpSocket::bind("0.0.0.0:0").await { Ok(s) => s, Err(e) => { warn!(error = %e, "Failed to bind UDP socket for STUN"); return (NatType::Unknown, NatMapping::Unknown); } }; let local_addr = sock.local_addr().ok(); // Query both servers from the same socket let result1 = stun_query(&sock, STUN_SERVERS[0]).await; let result2 = stun_query(&sock, STUN_SERVERS[1]).await; match (result1, result2) { (Some(addr1), Some(addr2)) => { debug!( server1 = STUN_SERVERS[0], mapped1 = %addr1, server2 = STUN_SERVERS[1], mapped2 = %addr2, local_port, "STUN results" ); // If mapped port matches our local port, we might be public/no-NAT if let Some(local) = local_addr { if addr1.port() == local.port() && addr2.port() == local.port() { return (NatType::Public, NatMapping::EndpointIndependent); } } // Same mapped port from both = cone NAT (Easy / EIM) // Different ports = symmetric NAT (Hard / EDM) if addr1.port() == addr2.port() { (NatType::Easy, NatMapping::EndpointIndependent) } else { (NatType::Hard, NatMapping::EndpointDependent) } } (Some(addr), None) | (None, Some(addr)) => { debug!(mapped = %addr, "Only one STUN server responded, assuming Easy"); (NatType::Easy, NatMapping::EndpointIndependent) } (None, None) => { warn!("Both STUN servers unreachable, NAT type unknown"); (NatType::Unknown, NatMapping::Unknown) } } }