use std::net::{IpAddr, SocketAddr};
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::RwLock;
const STUN_MAGIC_COOKIE: u32 = 0x2112A442;
const BINDING_REQUEST: u16 = 0x0001;
const BINDING_SUCCESS_RESPONSE: u16 = 0x0101;
const XOR_MAPPED_ADDRESS: u16 = 0x0020;
const MAPPED_ADDRESS: u16 = 0x0001;
pub const STUN_SERVERS: &[&str] = &[
"stun.l.google.com:19302", "stun1.l.google.com:19302", "stun.cloudflare.com:3478", ];
#[derive(Debug, thiserror::Error)]
pub enum StunError {
#[error("IO error: {0}")]
IoError(#[from] std::io::Error),
#[error("Invalid STUN response")]
InvalidResponse,
#[error("No mapped address in response")]
NoMappedAddress,
#[error("Timeout waiting for response")]
Timeout,
}
pub async fn get_public_ip_stun_with_cache(
server: &str,
timeout: Duration,
cache: &Arc<RwLock<crate::public_ip::stun_cache::StunCache>>,
) -> Result<IpAddr, StunError> {
let verbose = std::env::var("FTR_VERBOSE")
.ok()
.and_then(|v| v.parse::<u8>().ok())
.unwrap_or(0);
if verbose >= 2 {
eprintln!("[STUN] Attempting to contact STUN server: {}", server);
}
let cache_read = cache.read().await;
let server_addrs = cache_read
.get_stun_server_addrs(server)
.await
.map_err(|e| {
if verbose >= 2 {
eprintln!("[STUN] Failed to resolve {}: {}", server, e);
}
StunError::IoError(e)
})?;
drop(cache_read);
for server_addr in server_addrs {
if verbose >= 2 {
eprintln!("[STUN] Trying {} (resolved from {})", server_addr, server);
}
match get_public_ip_stun_addr(server_addr, timeout).await {
Ok(ip) => {
if verbose >= 2 {
eprintln!(
"[STUN] Successfully obtained public IP {} from {}",
ip, server
);
}
return Ok(ip);
}
Err(e) => {
if verbose >= 2 {
eprintln!("[STUN] Failed to get IP from {}: {:?}", server_addr, e);
}
continue; }
}
}
if verbose >= 2 {
eprintln!("[STUN] All addresses for {} failed", server);
}
Err(StunError::Timeout)
}
async fn get_public_ip_stun_addr(
server_addr: SocketAddr,
timeout: Duration,
) -> Result<IpAddr, StunError> {
let socket = tokio::net::UdpSocket::bind("0.0.0.0:0").await?;
let request = build_binding_request();
socket.send_to(&request, server_addr).await?;
let mut buf = vec![0u8; 1024];
let result = tokio::time::timeout(timeout, socket.recv_from(&mut buf)).await;
let (size, _) = match result {
Ok(Ok(data)) => data,
Ok(Err(e)) => return Err(StunError::IoError(e)),
Err(_) => return Err(StunError::Timeout),
};
parse_stun_response(&buf[..size])
}
pub async fn get_public_ip_stun_with_fallback_and_cache(
timeout: Duration,
cache: &Arc<RwLock<crate::public_ip::stun_cache::StunCache>>,
) -> Result<IpAddr, StunError> {
let _ = crate::public_ip::stun_cache::prewarm_stun_cache_with_cache(cache).await;
if let Ok(custom_server) = std::env::var("FTR_STUN_SERVER") {
if let Ok(ip) = get_public_ip_stun_with_cache(&custom_server, timeout, cache).await {
return Ok(ip);
}
}
if let Ok(ip) = get_public_ip_stun_with_cache(STUN_SERVERS[0], timeout, cache).await {
return Ok(ip);
}
for server in &STUN_SERVERS[1..] {
match get_public_ip_stun_with_cache(server, timeout, cache).await {
Ok(ip) => return Ok(ip),
Err(_) => continue, }
}
Err(StunError::Timeout)
}
fn build_binding_request() -> Vec<u8> {
let mut request = Vec::with_capacity(20);
request.extend_from_slice(&BINDING_REQUEST.to_be_bytes());
request.extend_from_slice(&0u16.to_be_bytes());
request.extend_from_slice(&STUN_MAGIC_COOKIE.to_be_bytes());
let mut transaction_id = [0u8; 12];
getrandom::fill(&mut transaction_id).unwrap_or_else(|_| {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default();
let nanos = now.as_nanos() as u64;
transaction_id[..8].copy_from_slice(&nanos.to_be_bytes());
});
request.extend_from_slice(&transaction_id);
request
}
fn parse_stun_response(data: &[u8]) -> Result<IpAddr, StunError> {
if data.len() < 20 {
return Err(StunError::InvalidResponse);
}
let msg_type = u16::from_be_bytes([data[0], data[1]]);
if msg_type != BINDING_SUCCESS_RESPONSE {
return Err(StunError::InvalidResponse);
}
let magic = u32::from_be_bytes([data[4], data[5], data[6], data[7]]);
if magic != STUN_MAGIC_COOKIE {
return Err(StunError::InvalidResponse);
}
let msg_length = u16::from_be_bytes([data[2], data[3]]) as usize;
let mut offset = 20;
while offset + 4 <= 20 + msg_length && offset + 4 <= data.len() {
let attr_type = u16::from_be_bytes([data[offset], data[offset + 1]]);
let attr_length = u16::from_be_bytes([data[offset + 2], data[offset + 3]]) as usize;
if offset + 4 + attr_length > data.len() {
break;
}
match attr_type {
XOR_MAPPED_ADDRESS => {
if attr_length >= 8 {
return parse_xor_mapped_address(&data[offset + 4..offset + 4 + attr_length]);
}
}
MAPPED_ADDRESS => {
if attr_length >= 8 {
return parse_mapped_address(&data[offset + 4..offset + 4 + attr_length]);
}
}
_ => {}
}
offset += 4 + ((attr_length + 3) & !3);
}
Err(StunError::NoMappedAddress)
}
fn parse_xor_mapped_address(data: &[u8]) -> Result<IpAddr, StunError> {
if data.len() < 8 {
return Err(StunError::InvalidResponse);
}
let family = data[1];
let _port = u16::from_be_bytes([data[2], data[3]]) ^ (STUN_MAGIC_COOKIE >> 16) as u16;
match family {
0x01 => {
if data.len() < 8 {
return Err(StunError::InvalidResponse);
}
let addr_bytes = [
data[4] ^ (STUN_MAGIC_COOKIE >> 24) as u8,
data[5] ^ (STUN_MAGIC_COOKIE >> 16) as u8,
data[6] ^ (STUN_MAGIC_COOKIE >> 8) as u8,
data[7] ^ STUN_MAGIC_COOKIE as u8,
];
Ok(IpAddr::V4(std::net::Ipv4Addr::from(addr_bytes)))
}
0x02 => {
Err(StunError::InvalidResponse)
}
_ => Err(StunError::InvalidResponse),
}
}
fn parse_mapped_address(data: &[u8]) -> Result<IpAddr, StunError> {
if data.len() < 8 {
return Err(StunError::InvalidResponse);
}
let family = data[1];
match family {
0x01 => {
let addr_bytes = [data[4], data[5], data[6], data[7]];
Ok(IpAddr::V4(std::net::Ipv4Addr::from(addr_bytes)))
}
_ => Err(StunError::InvalidResponse),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_build_binding_request() {
let request = build_binding_request();
assert_eq!(request.len(), 20);
assert_eq!(
u16::from_be_bytes([request[0], request[1]]),
BINDING_REQUEST
);
assert_eq!(
u32::from_be_bytes([request[4], request[5], request[6], request[7]]),
STUN_MAGIC_COOKIE
);
}
#[tokio::test]
async fn test_stun_google() {
let cache = Arc::new(RwLock::new(crate::public_ip::stun_cache::StunCache::new()));
let result = get_public_ip_stun_with_cache(
"stun.l.google.com:19302",
Duration::from_secs(2),
&cache,
)
.await;
match result {
Ok(ip) => {
println!("Detected public IP via STUN: {}", ip);
match ip {
IpAddr::V4(v4) => {
assert!(!v4.is_private());
assert!(!v4.is_loopback());
}
IpAddr::V6(_) => {
}
}
}
Err(e) => {
eprintln!("STUN test failed (may be offline): {}", e);
}
}
}
}