nvpn 4.0.21

CLI and daemon for Nostr VPN private mesh networks
use std::net::{IpAddr, SocketAddr, SocketAddrV4};
use std::num::NonZeroU16;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};

use anyhow::{Result, anyhow};
use crab_nat::{
    InternetProtocol, PortMapping as CrabPortMapping, PortMappingOptions, PortMappingType,
    TimeoutConfig,
};
use igd_next::PortMappingProtocol;
use igd_next::aio::Gateway as UpnpGateway;
use igd_next::aio::tokio::{Tokio as UpnpProvider, search_gateway};
use nostr_vpn_core::diagnostics::{PortMappingStatus, ProbeState, ProbeStatus};

use super::NetworkSnapshot;
use super::probes::{
    NAT_PMP_DEFAULT_PORT, PCP_DEFAULT_PORT, SSDP_DISCOVERY_ADDR, probe_nat_pmp_server,
    probe_pcp_server, probe_upnp_ssdp_server,
};

const PORT_MAPPING_LEASE_SECS: u32 = 3_600;
const UPNP_DESCRIPTION: &str = "nostr-vpn";

#[derive(Debug, Clone)]
enum ActivePortMappingLease {
    Crab(CrabPortMapping),
    Upnp(UpnpLease),
}

#[derive(Debug, Clone)]
struct UpnpLease {
    gateway: UpnpGateway<UpnpProvider>,
    external_endpoint: SocketAddr,
    good_until: Instant,
}

#[derive(Debug, Default)]
pub(crate) struct PortMappingRuntime {
    lease: Option<ActivePortMappingLease>,
    status: PortMappingStatus,
}

impl PortMappingRuntime {
    #[must_use]
    pub(crate) fn status(&self) -> PortMappingStatus {
        self.status.clone()
    }

    #[must_use]
    pub(crate) fn advertised_endpoint(&self) -> Option<String> {
        self.status.external_endpoint.clone()
    }

    pub(crate) async fn refresh(
        &mut self,
        snapshot: &NetworkSnapshot,
        listen_port: u16,
        timeout: Duration,
    ) -> Result<bool> {
        let previous_endpoint = self.advertised_endpoint();
        self.stop().await;

        let (gateway, local_ip) = match (snapshot.gateway_ipv4, snapshot.primary_ipv4) {
            (Some(gateway), Some(local_ip)) => (IpAddr::V4(gateway), IpAddr::V4(local_ip)),
            _ => {
                self.status = PortMappingStatus {
                    upnp: ProbeStatus::new(
                        ProbeState::Unsupported,
                        "default gateway or primary IPv4 unavailable",
                    ),
                    nat_pmp: ProbeStatus::new(
                        ProbeState::Unsupported,
                        "default gateway or primary IPv4 unavailable",
                    ),
                    pcp: ProbeStatus::new(
                        ProbeState::Unsupported,
                        "default gateway or primary IPv4 unavailable",
                    ),
                    ..PortMappingStatus::default()
                };
                return Ok(previous_endpoint != self.advertised_endpoint());
            }
        };

        let timeout_config = TimeoutConfig {
            initial_timeout: timeout.min(Duration::from_millis(500)),
            max_retries: 1,
            max_retry_timeout: Some(timeout),
        };
        let mapping_options = PortMappingOptions {
            external_port: NonZeroU16::new(listen_port),
            lifetime_seconds: Some(PORT_MAPPING_LEASE_SECS),
            timeout_config: Some(timeout_config),
        };

        match CrabPortMapping::new(
            gateway,
            local_ip,
            InternetProtocol::Udp,
            NonZeroU16::new(listen_port).ok_or_else(|| anyhow!("listen port must be non-zero"))?,
            mapping_options,
        )
        .await
        {
            Ok(mapping) => {
                let (protocol, external_ip) = match mapping.mapping_type() {
                    PortMappingType::NatPmp => (
                        "nat_pmp".to_string(),
                        crab_nat::natpmp::external_address(gateway, Some(timeout_config))
                            .await
                            .ok()
                            .map(IpAddr::V4),
                    ),
                    PortMappingType::Pcp { external_ip, .. } => {
                        ("pcp".to_string(), Some(external_ip))
                    }
                };
                let endpoint = external_ip
                    .map(|ip| SocketAddr::new(ip, mapping.external_port().get()).to_string());
                self.status = PortMappingStatus {
                    upnp: ProbeStatus::default(),
                    nat_pmp: ProbeStatus::new(
                        if protocol == "nat_pmp" {
                            ProbeState::Available
                        } else {
                            ProbeState::Unknown
                        },
                        if protocol == "nat_pmp" {
                            "mapped UDP listen port"
                        } else {
                            ""
                        },
                    ),
                    pcp: ProbeStatus::new(
                        if protocol == "pcp" {
                            ProbeState::Available
                        } else {
                            ProbeState::Unknown
                        },
                        if protocol == "pcp" {
                            "mapped UDP listen port"
                        } else {
                            ""
                        },
                    ),
                    active_protocol: Some(protocol),
                    external_endpoint: endpoint,
                    gateway: Some(gateway.to_string()),
                    good_until: Some(instant_to_unix(mapping.expiration())),
                };
                self.lease = Some(ActivePortMappingLease::Crab(mapping));
                return Ok(previous_endpoint != self.advertised_endpoint());
            }
            Err(error) => {
                self.status.nat_pmp = ProbeStatus::new(ProbeState::Error, error.to_string());
                self.status.pcp = ProbeStatus::new(ProbeState::Error, error.to_string());
            }
        }

        let local_addr = SocketAddr::new(local_ip, listen_port);
        match search_gateway(Default::default()).await {
            Ok(gateway) => {
                let endpoint = match gateway
                    .add_port(
                        PortMappingProtocol::UDP,
                        listen_port,
                        local_addr,
                        PORT_MAPPING_LEASE_SECS,
                        UPNP_DESCRIPTION,
                    )
                    .await
                {
                    Ok(()) => {
                        let external_ip = gateway.get_external_ip().await.ok();
                        external_ip.map(|ip| SocketAddr::new(ip, listen_port))
                    }
                    Err(_) => gateway
                        .get_any_address(
                            PortMappingProtocol::UDP,
                            local_addr,
                            PORT_MAPPING_LEASE_SECS,
                            UPNP_DESCRIPTION,
                        )
                        .await
                        .ok(),
                };

                if let Some(endpoint) = endpoint {
                    self.status.upnp =
                        ProbeStatus::new(ProbeState::Available, "mapped UDP listen port");
                    self.status.active_protocol = Some("upnp".to_string());
                    self.status.external_endpoint = Some(endpoint.to_string());
                    self.status.gateway = Some(gateway.addr.ip().to_string());
                    self.status.good_until = Some(system_time_to_unix(
                        SystemTime::now()
                            .checked_add(Duration::from_secs(u64::from(PORT_MAPPING_LEASE_SECS)))
                            .unwrap_or(SystemTime::now()),
                    ));
                    self.lease = Some(ActivePortMappingLease::Upnp(UpnpLease {
                        gateway,
                        external_endpoint: endpoint,
                        good_until: Instant::now()
                            + Duration::from_secs(u64::from(PORT_MAPPING_LEASE_SECS)),
                    }));
                } else {
                    self.status.upnp = ProbeStatus::new(
                        ProbeState::Unavailable,
                        "gateway responded but port mapping failed",
                    );
                }
            }
            Err(error) => {
                self.status.upnp = ProbeStatus::new(ProbeState::Unavailable, error.to_string());
            }
        }

        Ok(previous_endpoint != self.advertised_endpoint())
    }

    pub(crate) async fn renew_if_due(
        &mut self,
        snapshot: &NetworkSnapshot,
        listen_port: u16,
        timeout: Duration,
    ) -> Result<bool> {
        let Some(lease) = &mut self.lease else {
            return Ok(false);
        };

        let needs_renew = match lease {
            ActivePortMappingLease::Crab(mapping) => {
                mapping
                    .expiration()
                    .saturating_duration_since(Instant::now())
                    <= Duration::from_secs(300)
            }
            ActivePortMappingLease::Upnp(lease) => {
                lease.good_until.saturating_duration_since(Instant::now())
                    <= Duration::from_secs(300)
            }
        };

        if !needs_renew {
            return Ok(false);
        }

        self.refresh(snapshot, listen_port, timeout).await
    }

    pub(crate) async fn stop(&mut self) {
        let Some(lease) = self.lease.take() else {
            return;
        };

        match lease {
            ActivePortMappingLease::Crab(mapping) => {
                let _ = mapping.try_drop().await;
            }
            ActivePortMappingLease::Upnp(lease) => {
                let _ = lease
                    .gateway
                    .remove_port(PortMappingProtocol::UDP, lease.external_endpoint.port())
                    .await;
            }
        }
    }
}

pub(super) async fn probe_port_mapping_services(
    snapshot: &NetworkSnapshot,
    timeout: Duration,
) -> PortMappingStatus {
    let mut status = PortMappingStatus::default();
    let Some(gateway) = snapshot.gateway_ipv4 else {
        status.upnp = ProbeStatus::new(ProbeState::Unsupported, "default IPv4 gateway unavailable");
        status.nat_pmp =
            ProbeStatus::new(ProbeState::Unsupported, "default IPv4 gateway unavailable");
        status.pcp = ProbeStatus::new(ProbeState::Unsupported, "default IPv4 gateway unavailable");
        return status;
    };

    status.nat_pmp = probe_nat_pmp_server(
        SocketAddr::V4(SocketAddrV4::new(gateway, NAT_PMP_DEFAULT_PORT)),
        timeout,
    );
    status.pcp = probe_pcp_server(
        SocketAddr::V4(SocketAddrV4::new(gateway, PCP_DEFAULT_PORT)),
        snapshot
            .primary_ipv4
            .map(IpAddr::V4)
            .or_else(|| snapshot.primary_ipv6.map(IpAddr::V6)),
        timeout,
    );
    status.upnp = probe_upnp_ssdp_server(
        SSDP_DISCOVERY_ADDR.parse().expect("valid ssdp addr"),
        timeout,
    );
    status
}

fn system_time_to_unix(value: SystemTime) -> u64 {
    value
        .duration_since(UNIX_EPOCH)
        .map(|duration| duration.as_secs())
        .unwrap_or(0)
}

fn instant_to_unix(value: Instant) -> u64 {
    let remaining = value.saturating_duration_since(Instant::now());
    system_time_to_unix(SystemTime::now() + remaining)
}