diff --git a/src/network/mod.rs b/src/network/mod.rs new file mode 100644 index 0000000..78a1040 --- /dev/null +++ b/src/network/mod.rs @@ -0,0 +1,4 @@ +pub mod probe; +pub mod stun; + +pub use stun::IpFamily; diff --git a/src/network/probe.rs b/src/network/probe.rs new file mode 100644 index 0000000..2a220f5 --- /dev/null +++ b/src/network/probe.rs @@ -0,0 +1,225 @@ +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, UdpSocket}; + +use tracing::{info, warn}; + +use crate::config::NetworkConfig; +use crate::error::Result; +use crate::network::stun::{stun_probe_dual, DualStunResult, IpFamily}; + +#[derive(Debug, Clone, Default)] +pub struct NetworkProbe { + pub detected_ipv4: Option, + pub detected_ipv6: Option, + pub reflected_ipv4: Option, + pub reflected_ipv6: Option, + pub ipv4_is_bogon: bool, + pub ipv6_is_bogon: bool, + pub ipv4_nat_detected: bool, + pub ipv6_nat_detected: bool, + pub ipv4_usable: bool, + pub ipv6_usable: bool, +} + +#[derive(Debug, Clone, Default)] +pub struct NetworkDecision { + pub ipv4_dc: bool, + pub ipv6_dc: bool, + pub ipv4_me: bool, + pub ipv6_me: bool, + pub effective_prefer: u8, + pub effective_multipath: bool, +} + +impl NetworkDecision { + pub fn prefer_ipv6(&self) -> bool { + self.effective_prefer == 6 + } + + pub fn me_families(&self) -> Vec { + let mut res = Vec::new(); + if self.ipv4_me { + res.push(IpFamily::V4); + } + if self.ipv6_me { + res.push(IpFamily::V6); + } + res + } +} + +pub async fn run_probe(config: &NetworkConfig, stun_addr: Option, nat_probe: bool) -> Result { + let mut probe = NetworkProbe::default(); + + probe.detected_ipv4 = detect_local_ip_v4(); + probe.detected_ipv6 = detect_local_ip_v6(); + + probe.ipv4_is_bogon = probe.detected_ipv4.map(is_bogon_v4).unwrap_or(false); + probe.ipv6_is_bogon = probe.detected_ipv6.map(is_bogon_v6).unwrap_or(false); + + let stun_server = stun_addr.unwrap_or_else(|| "stun.l.google.com:19302".to_string()); + let stun_res = if nat_probe { + stun_probe_dual(&stun_server).await? + } else { + DualStunResult::default() + }; + probe.reflected_ipv4 = stun_res.v4.map(|r| r.reflected_addr); + probe.reflected_ipv6 = stun_res.v6.map(|r| r.reflected_addr); + + probe.ipv4_nat_detected = match (probe.detected_ipv4, probe.reflected_ipv4) { + (Some(det), Some(reflected)) => det != reflected.ip(), + _ => false, + }; + probe.ipv6_nat_detected = match (probe.detected_ipv6, probe.reflected_ipv6) { + (Some(det), Some(reflected)) => det != reflected.ip(), + _ => false, + }; + + probe.ipv4_usable = config.ipv4 + && probe.detected_ipv4.is_some() + && (!probe.ipv4_is_bogon || probe.reflected_ipv4.map(|r| !is_bogon(r.ip())).unwrap_or(false)); + + let ipv6_enabled = config.ipv6.unwrap_or(probe.detected_ipv6.is_some()); + probe.ipv6_usable = ipv6_enabled + && probe.detected_ipv6.is_some() + && (!probe.ipv6_is_bogon || probe.reflected_ipv6.map(|r| !is_bogon(r.ip())).unwrap_or(false)); + + Ok(probe) +} + +pub fn decide_network_capabilities(config: &NetworkConfig, probe: &NetworkProbe) -> NetworkDecision { + let mut decision = NetworkDecision::default(); + + decision.ipv4_dc = config.ipv4 && probe.detected_ipv4.is_some(); + decision.ipv6_dc = config.ipv6.unwrap_or(probe.detected_ipv6.is_some()) && probe.detected_ipv6.is_some(); + + decision.ipv4_me = config.ipv4 + && probe.detected_ipv4.is_some() + && (!probe.ipv4_is_bogon || probe.reflected_ipv4.is_some()); + + let ipv6_enabled = config.ipv6.unwrap_or(probe.detected_ipv6.is_some()); + decision.ipv6_me = ipv6_enabled + && probe.detected_ipv6.is_some() + && (!probe.ipv6_is_bogon || probe.reflected_ipv6.is_some()); + + decision.effective_prefer = match config.prefer { + 6 if decision.ipv6_me || decision.ipv6_dc => 6, + 4 if decision.ipv4_me || decision.ipv4_dc => 4, + 6 => { + warn!("prefer=6 requested but IPv6 unavailable; falling back to IPv4"); + 4 + } + _ => 4, + }; + + let me_families = decision.ipv4_me as u8 + decision.ipv6_me as u8; + decision.effective_multipath = config.multipath && me_families >= 2; + + decision +} + +fn detect_local_ip_v4() -> Option { + let socket = UdpSocket::bind("0.0.0.0:0").ok()?; + socket.connect("8.8.8.8:80").ok()?; + match socket.local_addr().ok()?.ip() { + IpAddr::V4(v4) => Some(v4), + _ => None, + } +} + +fn detect_local_ip_v6() -> Option { + let socket = UdpSocket::bind("[::]:0").ok()?; + socket.connect("[2001:4860:4860::8888]:80").ok()?; + match socket.local_addr().ok()?.ip() { + IpAddr::V6(v6) => Some(v6), + _ => None, + } +} + +pub fn is_bogon(ip: IpAddr) -> bool { + match ip { + IpAddr::V4(v4) => is_bogon_v4(v4), + IpAddr::V6(v6) => is_bogon_v6(v6), + } +} + +pub fn is_bogon_v4(ip: Ipv4Addr) -> bool { + let octets = ip.octets(); + if ip.is_private() || ip.is_loopback() || ip.is_link_local() { + return true; + } + if octets[0] == 0 { + return true; + } + if octets[0] == 100 && (octets[1] & 0xC0) == 64 { + return true; + } + if octets[0] == 192 && octets[1] == 0 && octets[2] == 0 { + return true; + } + if octets[0] == 192 && octets[1] == 0 && octets[2] == 2 { + return true; + } + if octets[0] == 198 && (octets[1] & 0xFE) == 18 { + return true; + } + if octets[0] == 198 && octets[1] == 51 && octets[2] == 100 { + return true; + } + if octets[0] == 203 && octets[1] == 0 && octets[2] == 113 { + return true; + } + if ip.is_multicast() { + return true; + } + if octets[0] >= 240 { + return true; + } + if ip.is_broadcast() { + return true; + } + false +} + +pub fn is_bogon_v6(ip: Ipv6Addr) -> bool { + if ip.is_unspecified() || ip.is_loopback() || ip.is_unique_local() { + return true; + } + let segs = ip.segments(); + if (segs[0] & 0xFFC0) == 0xFE80 { + return true; + } + if segs[0..5] == [0, 0, 0, 0, 0] && segs[5] == 0xFFFF { + return true; + } + if segs[0] == 0x0100 && segs[1..4] == [0, 0, 0] { + return true; + } + if segs[0] == 0x2001 && segs[1] == 0x0db8 { + return true; + } + if segs[0] == 0x2002 { + return true; + } + if ip.is_multicast() { + return true; + } + false +} + +pub fn log_probe_result(probe: &NetworkProbe, decision: &NetworkDecision) { + info!( + ipv4 = probe.detected_ipv4.as_ref().map(|v| v.to_string()).unwrap_or_else(|| "-".into()), + ipv6 = probe.detected_ipv6.as_ref().map(|v| v.to_string()).unwrap_or_else(|| "-".into()), + reflected_v4 = probe.reflected_ipv4.as_ref().map(|v| v.ip().to_string()).unwrap_or_else(|| "-".into()), + reflected_v6 = probe.reflected_ipv6.as_ref().map(|v| v.ip().to_string()).unwrap_or_else(|| "-".into()), + ipv4_bogon = probe.ipv4_is_bogon, + ipv6_bogon = probe.ipv6_is_bogon, + ipv4_me = decision.ipv4_me, + ipv6_me = decision.ipv6_me, + ipv4_dc = decision.ipv4_dc, + ipv6_dc = decision.ipv6_dc, + prefer = decision.effective_prefer, + multipath = decision.effective_multipath, + "Network capabilities resolved" + ); +} diff --git a/src/network/stun.rs b/src/network/stun.rs new file mode 100644 index 0000000..e1c811d --- /dev/null +++ b/src/network/stun.rs @@ -0,0 +1,186 @@ +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; + +use tokio::net::{lookup_host, UdpSocket}; + +use crate::error::{ProxyError, Result}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum IpFamily { + V4, + V6, +} + +#[derive(Debug, Clone, Copy)] +pub struct StunProbeResult { + pub local_addr: SocketAddr, + pub reflected_addr: SocketAddr, + pub family: IpFamily, +} + +#[derive(Debug, Default, Clone)] +pub struct DualStunResult { + pub v4: Option, + pub v6: Option, +} + +pub async fn stun_probe_dual(stun_addr: &str) -> Result { + let (v4, v6) = tokio::join!( + stun_probe_family(stun_addr, IpFamily::V4), + stun_probe_family(stun_addr, IpFamily::V6), + ); + + Ok(DualStunResult { + v4: v4?, + v6: v6?, + }) +} + +pub async fn stun_probe_family(stun_addr: &str, family: IpFamily) -> Result> { + use rand::RngCore; + + let bind_addr = match family { + IpFamily::V4 => "0.0.0.0:0", + IpFamily::V6 => "[::]:0", + }; + + let socket = UdpSocket::bind(bind_addr) + .await + .map_err(|e| ProxyError::Proxy(format!("STUN bind failed: {e}")))?; + + let target_addr = resolve_stun_addr(stun_addr, family).await?; + if let Some(addr) = target_addr { + socket + .connect(addr) + .await + .map_err(|e| ProxyError::Proxy(format!("STUN connect failed: {e}")))?; + } else { + return Ok(None); + } + + let mut req = [0u8; 20]; + req[0..2].copy_from_slice(&0x0001u16.to_be_bytes()); // Binding Request + req[2..4].copy_from_slice(&0u16.to_be_bytes()); // length + req[4..8].copy_from_slice(&0x2112A442u32.to_be_bytes()); // magic cookie + rand::rng().fill_bytes(&mut req[8..20]); // transaction ID + + socket + .send(&req) + .await + .map_err(|e| ProxyError::Proxy(format!("STUN send failed: {e}")))?; + + let mut buf = [0u8; 256]; + let n = socket + .recv(&mut buf) + .await + .map_err(|e| ProxyError::Proxy(format!("STUN recv failed: {e}")))?; + if n < 20 { + return Ok(None); + } + + let magic = 0x2112A442u32.to_be_bytes(); + let txid = &req[8..20]; + let mut idx = 20; + while idx + 4 <= n { + let atype = u16::from_be_bytes(buf[idx..idx + 2].try_into().unwrap()); + let alen = u16::from_be_bytes(buf[idx + 2..idx + 4].try_into().unwrap()) as usize; + idx += 4; + if idx + alen > n { + break; + } + + match atype { + 0x0020 /* XOR-MAPPED-ADDRESS */ | 0x0001 /* MAPPED-ADDRESS */ => { + if alen < 8 { + break; + } + let family_byte = buf[idx + 1]; + let port_bytes = [buf[idx + 2], buf[idx + 3]]; + let len_check = match family_byte { + 0x01 => 4, + 0x02 => 16, + _ => 0, + }; + if len_check == 0 || alen < 4 + len_check { + break; + } + + let raw_ip = &buf[idx + 4..idx + 4 + len_check]; + let mut port = u16::from_be_bytes(port_bytes); + + let reflected_ip = if atype == 0x0020 { + port ^= ((magic[0] as u16) << 8) | magic[1] as u16; + match family_byte { + 0x01 => { + let ip = [ + raw_ip[0] ^ magic[0], + raw_ip[1] ^ magic[1], + raw_ip[2] ^ magic[2], + raw_ip[3] ^ magic[3], + ]; + IpAddr::V4(Ipv4Addr::new(ip[0], ip[1], ip[2], ip[3])) + } + 0x02 => { + let mut ip = [0u8; 16]; + let xor_key = [magic.as_slice(), txid].concat(); + for (i, b) in raw_ip.iter().enumerate().take(16) { + ip[i] = *b ^ xor_key[i]; + } + IpAddr::V6(Ipv6Addr::from(ip)) + } + _ => { + idx += (alen + 3) & !3; + continue; + } + } + } else { + match family_byte { + 0x01 => IpAddr::V4(Ipv4Addr::new(raw_ip[0], raw_ip[1], raw_ip[2], raw_ip[3])), + 0x02 => IpAddr::V6(Ipv6Addr::from(<[u8; 16]>::try_from(raw_ip).unwrap())), + _ => { + idx += (alen + 3) & !3; + continue; + } + } + }; + + let reflected_addr = SocketAddr::new(reflected_ip, port); + let local_addr = socket + .local_addr() + .map_err(|e| ProxyError::Proxy(format!("STUN local_addr failed: {e}")))?; + + return Ok(Some(StunProbeResult { + local_addr, + reflected_addr, + family, + })); + } + _ => {} + } + + idx += (alen + 3) & !3; + } + + Ok(None) +} + +async fn resolve_stun_addr(stun_addr: &str, family: IpFamily) -> Result> { + if let Ok(addr) = stun_addr.parse::() { + return Ok(match (addr.is_ipv4(), family) { + (true, IpFamily::V4) | (false, IpFamily::V6) => Some(addr), + _ => None, + }); + } + + let addrs = lookup_host(stun_addr) + .await + .map_err(|e| ProxyError::Proxy(format!("STUN resolve failed: {e}")))?; + + let target = addrs + .filter(|a| match (a.is_ipv4(), family) { + (true, IpFamily::V4) => true, + (false, IpFamily::V6) => true, + _ => false, + }) + .next(); + Ok(target) +}