//! Best-effort UPnP port mapping for NAT traversal. //! Skipped entirely on mobile platforms where UPnP is unsupported. use std::net::SocketAddr; #[cfg(not(any(target_os = "android", target_os = "ios")))] use tracing::{info, debug}; /// Result of a successful UPnP port mapping. pub struct UpnpMapping { pub external_addr: SocketAddr, pub lease_secs: u32, pub local_port: u16, } /// Best-effort UPnP port mapping. /// 3s gateway discovery timeout, 1800s (30 min) lease, UDP protocol. /// Returns None on any failure (no router, unsupported, timeout, port conflict). #[cfg(not(any(target_os = "android", target_os = "ios")))] pub async fn try_upnp_mapping(local_port: u16) -> Option { use igd_next::SearchOptions; let search_opts = SearchOptions { timeout: Some(std::time::Duration::from_secs(3)), ..Default::default() }; let gateway = match igd_next::aio::tokio::search_gateway(search_opts).await { Ok(gw) => gw, Err(e) => { debug!("UPnP gateway discovery failed (expected behind non-UPnP router): {}", e); return None; } }; let external_ip = match gateway.get_external_ip().await { Ok(ip) => ip, Err(e) => { debug!("UPnP: could not get external IP: {}", e); return None; } }; // Local address for the mapping — bind to all interfaces let local_addr = SocketAddr::new(std::net::IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED), local_port); let lease_secs: u32 = 1800; // 30 minutes // Try mapping the same external port first let result = gateway.add_port( igd_next::PortMappingProtocol::UDP, local_port, local_addr, lease_secs, "itsgoin", ).await; let external_port = match result { Ok(()) => local_port, Err(_) => { // Port taken — try any available port match gateway.add_any_port( igd_next::PortMappingProtocol::UDP, local_addr, lease_secs, "itsgoin", ).await { Ok(port) => port, Err(e) => { debug!("UPnP: port mapping failed: {}", e); return None; } } } }; let external_addr = SocketAddr::new(external_ip, external_port); info!("UPnP: mapped {}:{} → :{}", external_ip, external_port, local_port); Some(UpnpMapping { external_addr, lease_secs, local_port, }) } #[cfg(any(target_os = "android", target_os = "ios"))] pub async fn try_upnp_mapping(_local_port: u16) -> Option { None } /// Renew an existing UPnP lease. Returns true on success. #[cfg(not(any(target_os = "android", target_os = "ios")))] pub async fn renew_upnp_mapping(local_port: u16, external_port: u16) -> bool { use igd_next::SearchOptions; let search_opts = SearchOptions { timeout: Some(std::time::Duration::from_secs(3)), ..Default::default() }; let gateway = match igd_next::aio::tokio::search_gateway(search_opts).await { Ok(gw) => gw, Err(_) => return false, }; let local_addr = SocketAddr::new(std::net::IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED), local_port); gateway.add_port( igd_next::PortMappingProtocol::UDP, external_port, local_addr, 1800, "itsgoin", ).await.is_ok() } #[cfg(any(target_os = "android", target_os = "ios"))] pub async fn renew_upnp_mapping(_local_port: u16, _external_port: u16) -> bool { false } /// Remove UPnP mapping on shutdown. Best-effort, errors are silently ignored. #[cfg(not(any(target_os = "android", target_os = "ios")))] pub async fn remove_upnp_mapping(external_port: u16) { use igd_next::SearchOptions; let search_opts = SearchOptions { timeout: Some(std::time::Duration::from_secs(3)), ..Default::default() }; if let Ok(gateway) = igd_next::aio::tokio::search_gateway(search_opts).await { let _ = gateway.remove_port(igd_next::PortMappingProtocol::UDP, external_port).await; info!("UPnP: removed port mapping for external port {}", external_port); } } #[cfg(any(target_os = "android", target_os = "ios"))] pub async fn remove_upnp_mapping(_external_port: u16) {} // --- TCP port mapping (for HTTP post delivery) --- /// Best-effort UPnP TCP port mapping on the same port as QUIC UDP. /// Returns true on success. Reuses the already-discovered gateway. #[cfg(not(any(target_os = "android", target_os = "ios")))] pub async fn try_upnp_tcp_mapping(local_port: u16, external_port: u16) -> bool { use igd_next::SearchOptions; let search_opts = SearchOptions { timeout: Some(std::time::Duration::from_secs(3)), ..Default::default() }; let gateway = match igd_next::aio::tokio::search_gateway(search_opts).await { Ok(gw) => gw, Err(_) => return false, }; let local_addr = SocketAddr::new( std::net::IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED), local_port, ); match gateway .add_port( igd_next::PortMappingProtocol::TCP, external_port, local_addr, 1800, "itsgoin-http", ) .await { Ok(()) => { info!("UPnP: TCP port {} mapped for HTTP serving", external_port); true } Err(e) => { debug!("UPnP: TCP port mapping failed (non-fatal): {}", e); false } } } #[cfg(any(target_os = "android", target_os = "ios"))] pub async fn try_upnp_tcp_mapping(_local_port: u16, _external_port: u16) -> bool { false } /// Renew an existing UPnP TCP lease. Returns true on success. #[cfg(not(any(target_os = "android", target_os = "ios")))] pub async fn renew_upnp_tcp_mapping(local_port: u16, external_port: u16) -> bool { use igd_next::SearchOptions; let search_opts = SearchOptions { timeout: Some(std::time::Duration::from_secs(3)), ..Default::default() }; let gateway = match igd_next::aio::tokio::search_gateway(search_opts).await { Ok(gw) => gw, Err(_) => return false, }; let local_addr = SocketAddr::new( std::net::IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED), local_port, ); gateway .add_port( igd_next::PortMappingProtocol::TCP, external_port, local_addr, 1800, "itsgoin-http", ) .await .is_ok() } #[cfg(any(target_os = "android", target_os = "ios"))] pub async fn renew_upnp_tcp_mapping(_local_port: u16, _external_port: u16) -> bool { false } /// Remove UPnP TCP mapping on shutdown. #[cfg(not(any(target_os = "android", target_os = "ios")))] pub async fn remove_upnp_tcp_mapping(external_port: u16) { use igd_next::SearchOptions; let search_opts = SearchOptions { timeout: Some(std::time::Duration::from_secs(3)), ..Default::default() }; if let Ok(gateway) = igd_next::aio::tokio::search_gateway(search_opts).await { let _ = gateway .remove_port(igd_next::PortMappingProtocol::TCP, external_port) .await; info!("UPnP: removed TCP port mapping for port {}", external_port); } } #[cfg(any(target_os = "android", target_os = "ios"))] pub async fn remove_upnp_tcp_mapping(_external_port: u16) {}