use std::net::SocketAddr;
use std::time::Duration;
use async_trait::async_trait;
use crate::error::MethodError;
use crate::method::{MethodOutcome, TraversalKind, TraversalMethod};
use crate::peer::PeerTarget;
#[async_trait]
pub trait IgdGateway: Send + Sync {
async fn add_port_mapping(&self, internal_port: u16, lifetime_secs: u32)
-> Result<u16, String>;
}
#[derive(Debug, Clone)]
pub struct RealIgd {
pub discovery_timeout: Duration,
}
impl Default for RealIgd {
fn default() -> Self {
RealIgd {
discovery_timeout: Duration::from_secs(2),
}
}
}
#[async_trait]
impl IgdGateway for RealIgd {
async fn add_port_mapping(
&self,
internal_port: u16,
lifetime_secs: u32,
) -> Result<u16, String> {
use igd_next::aio::tokio as igd_tokio;
use igd_next::{PortMappingProtocol, SearchOptions};
let opts = SearchOptions {
timeout: Some(self.discovery_timeout),
..Default::default()
};
let gateway = igd_tokio::search_gateway(opts)
.await
.map_err(|e| format!("igd discovery: {e}"))?;
let local_ip = local_ipv4().ok_or_else(|| "no local IPv4 to map".to_string())?;
let local = SocketAddr::new(local_ip, internal_port);
gateway
.add_port(
PortMappingProtocol::UDP,
internal_port,
local,
lifetime_secs,
"dig-nat",
)
.await
.map_err(|e| format!("igd add_port: {e}"))?;
Ok(internal_port)
}
}
pub struct UpnpMethod<G: IgdGateway> {
gateway: G,
pub local_port: u16,
pub lifetime_secs: u32,
}
impl<G: IgdGateway> UpnpMethod<G> {
pub fn new(gateway: G, local_port: u16) -> Self {
UpnpMethod {
gateway,
local_port,
lifetime_secs: 7200,
}
}
}
pub type RealUpnpMethod = UpnpMethod<RealIgd>;
impl RealUpnpMethod {
pub fn real(local_port: u16) -> Self {
UpnpMethod::new(RealIgd::default(), local_port)
}
}
#[async_trait]
impl<G: IgdGateway> TraversalMethod for UpnpMethod<G> {
fn kind(&self) -> TraversalKind {
TraversalKind::Upnp
}
async fn attempt(&self, peer: &PeerTarget) -> Result<MethodOutcome, MethodError> {
let dial_addrs = peer.direct_addrs();
if dial_addrs.is_empty() {
return Err(MethodError::failed(
TraversalKind::Upnp,
"peer has no address to dial after mapping",
));
}
self.gateway
.add_port_mapping(self.local_port, self.lifetime_secs)
.await
.map_err(|e| MethodError::failed(TraversalKind::Upnp, e))?;
Ok(MethodOutcome::candidates(
TraversalKind::Upnp,
dial_addrs.to_vec(),
))
}
}
fn local_ipv4() -> Option<std::net::IpAddr> {
let sock = std::net::UdpSocket::bind((std::net::Ipv4Addr::UNSPECIFIED, 0)).ok()?;
sock.connect((std::net::Ipv4Addr::new(1, 1, 1, 1), 80))
.ok()?;
sock.local_addr().ok().map(|a| a.ip())
}
pub fn local_ipv6() -> Option<std::net::IpAddr> {
let sock = std::net::UdpSocket::bind((std::net::Ipv6Addr::UNSPECIFIED, 0)).ok()?;
sock.connect((
std::net::Ipv6Addr::new(0x2606, 0x4700, 0x4700, 0, 0, 0, 0, 0x1111),
80,
))
.ok()?;
let ip = sock.local_addr().ok()?.ip();
select_global_ipv6(&[ip])
}
pub fn select_global_ipv6(candidates: &[std::net::IpAddr]) -> Option<std::net::IpAddr> {
candidates
.iter()
.copied()
.find(|ip| matches!(ip, std::net::IpAddr::V6(v6) if is_global_unicast_v6(v6)))
}
fn is_global_unicast_v6(v6: &std::net::Ipv6Addr) -> bool {
if v6.is_loopback() || v6.is_unspecified() || v6.is_multicast() {
return false;
}
let seg0 = v6.segments()[0];
let is_link_local = (seg0 & 0xffc0) == 0xfe80; let is_unique_local = (seg0 & 0xfe00) == 0xfc00; !is_link_local && !is_unique_local
}