use std::{net::Ipv4Addr, num::NonZeroU16, time::Duration};
use n0_error::{e, stack_error};
use netwatch::UdpSocket;
use tracing::{debug, trace};
use self::protocol::{MapProtocol, Request, Response};
use crate::{Protocol, defaults::NAT_PMP_RECV_TIMEOUT as RECV_TIMEOUT};
mod protocol;
const MAPPING_REQUESTED_LIFETIME_SECONDS: u32 = 60 * 60 * 2;
#[derive(Debug)]
pub struct Mapping {
local_ip: Ipv4Addr,
local_port: NonZeroU16,
gateway: Ipv4Addr,
external_port: NonZeroU16,
external_addr: Ipv4Addr,
lifetime_seconds: u32,
}
#[allow(missing_docs)]
#[stack_error(derive, add_meta, from_sources)]
#[non_exhaustive]
pub enum Error {
#[error("server returned unexpected response for mapping request")]
UnexpectedServerResponse {},
#[error("received 0 port from server as external port")]
ZeroExternalPort {},
#[error(transparent)]
Io {
#[error(std_err)]
source: std::io::Error,
},
#[error(transparent)]
Protocol { source: protocol::Error },
}
impl super::mapping::PortMapped for Mapping {
fn external(&self) -> (Ipv4Addr, NonZeroU16) {
(self.external_addr, self.external_port)
}
fn half_lifetime(&self) -> Duration {
Duration::from_secs((self.lifetime_seconds / 2).into())
}
}
impl Mapping {
pub async fn new(
protocol: Protocol,
local_ip: Ipv4Addr,
local_port: NonZeroU16,
gateway: Ipv4Addr,
external_port: Option<NonZeroU16>,
) -> Result<Self, Error> {
let socket = UdpSocket::bind_full((local_ip, 0))?;
socket.connect((gateway, protocol::SERVER_PORT).into())?;
let proto = match protocol {
Protocol::Udp => MapProtocol::Udp,
Protocol::Tcp => MapProtocol::Tcp,
};
let req = Request::Mapping {
proto,
local_port: local_port.into(),
external_port: external_port.map(Into::into).unwrap_or_default(),
lifetime_seconds: MAPPING_REQUESTED_LIFETIME_SECONDS,
};
socket.send(&req.encode()).await?;
let mut buffer = vec![0; Response::MAX_SIZE];
let read = tokio::time::timeout(RECV_TIMEOUT, socket.recv(&mut buffer))
.await
.map_err(|_| {
std::io::Error::new(std::io::ErrorKind::TimedOut, "read timeout".to_string())
})??;
let response = Response::decode(&buffer[..read])?;
let (external_port, lifetime_seconds) = match response {
Response::PortMap {
proto: proto_rcvd,
epoch_time: _,
private_port,
external_port,
lifetime_seconds,
} if private_port == Into::<u16>::into(local_port) && proto == proto_rcvd => {
(external_port, lifetime_seconds)
}
_ => return Err(e!(Error::UnexpectedServerResponse)),
};
let external_port = external_port
.try_into()
.map_err(|_| e!(Error::ZeroExternalPort))?;
let req = Request::ExternalAddress;
socket.send(&req.encode()).await?;
let mut buffer = vec![0; Response::MAX_SIZE];
let read = tokio::time::timeout(RECV_TIMEOUT, socket.recv(&mut buffer))
.await
.map_err(|_| {
std::io::Error::new(std::io::ErrorKind::TimedOut, "read timeout".to_string())
})??;
let response = Response::decode(&buffer[..read])?;
let external_addr = match response {
Response::PublicAddress {
epoch_time: _,
public_ip,
} => public_ip,
_ => return Err(e!(Error::UnexpectedServerResponse)),
};
Ok(Mapping {
external_port,
external_addr,
lifetime_seconds,
local_ip,
local_port,
gateway,
})
}
pub(crate) async fn release(self) -> Result<(), Error> {
let Mapping {
local_ip,
local_port,
gateway,
..
} = self;
let socket = UdpSocket::bind_full((local_ip, 0))?;
socket.connect((gateway, protocol::SERVER_PORT).into())?;
let req = Request::Mapping {
proto: MapProtocol::Udp,
local_port: local_port.into(),
external_port: 0,
lifetime_seconds: 0,
};
socket.send(&req.encode()).await?;
Ok(())
}
}
pub async fn probe_available(local_ip: Ipv4Addr, gateway: Ipv4Addr) -> bool {
match probe_available_fallible(local_ip, gateway).await {
Ok(response) => {
trace!("probe response: {response:?}");
match response {
Response::PublicAddress { .. } => true,
_ => {
debug!("server returned an unexpected response type for probe");
false
}
}
}
Err(e) => {
debug!("probe failed: {e}");
false
}
}
}
async fn probe_available_fallible(
local_ip: Ipv4Addr,
gateway: Ipv4Addr,
) -> Result<Response, Error> {
let socket = UdpSocket::bind_full((local_ip, 0))?;
socket.connect((gateway, protocol::SERVER_PORT).into())?;
let req = Request::ExternalAddress;
socket.send(&req.encode()).await?;
let mut buffer = vec![0; Response::MAX_SIZE];
let read = tokio::time::timeout(RECV_TIMEOUT, socket.recv(&mut buffer))
.await
.map_err(|_| {
std::io::Error::new(std::io::ErrorKind::TimedOut, "read timeout".to_string())
})??;
let response = Response::decode(&buffer[..read])?;
Ok(response)
}