//! Best-effort UPnP port mapping so firewalled peers can reach our //! listener directly. Failure is non-fatal — Soulseek's indirect //! (server-relayed) connection path still works without it. use std::net::SocketAddrV4; use std::time::Duration; /// Try to map `port` (TCP) on the local gateway to ourselves. Returns the /// external address on success. Runs on a blocking thread. pub(crate) async fn try_map_port(port: u16) { let result = tokio::task::spawn_blocking(move || map_blocking(port)).await; match result { Ok(Ok(external)) => log::info!("UPnP: mapped port {port} (external {external})"), Ok(Err(e)) => log::info!("UPnP unavailable ({e}); relying on indirect connections"), Err(_) => log::debug!("UPnP task panicked"), } } fn map_blocking(port: u16) -> Result { use igd_next::{search_gateway, PortMappingProtocol, SearchOptions}; let gateway = search_gateway(SearchOptions { timeout: Some(Duration::from_secs(5)), ..Default::default() }) .map_err(|e| e.to_string())?; // Find our LAN address facing the gateway. let local_ip = local_ipv4().ok_or_else(|| "no local IPv4".to_string())?; let local = SocketAddrV4::new(local_ip, port); gateway .add_port( PortMappingProtocol::TCP, port, std::net::SocketAddr::V4(local), 0, // lease 0 = indefinite (until router reboots) "SoulStream", ) .map_err(|e| e.to_string())?; match gateway.get_external_ip() { Ok(std::net::IpAddr::V4(ip)) => Ok(SocketAddrV4::new(ip, port)), _ => Ok(local), } } /// Discover our LAN IPv4 by opening a UDP socket toward a public address /// (no packets are actually sent). fn local_ipv4() -> Option { let sock = std::net::UdpSocket::bind("0.0.0.0:0").ok()?; sock.connect("8.8.8.8:80").ok()?; match sock.local_addr().ok()?.ip() { std::net::IpAddr::V4(ip) => Some(ip), _ => None, } }