use anyhow::{Context, Result};
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::time::Duration;
const GATEWAY_PORT: u16 = 5351;
const MAPPING_LIFETIME: u32 = 3600;
const TIMEOUT: Duration = Duration::from_secs(2);
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
pub struct Mapping {
pub internal_port: u16,
pub external_port: u16,
pub lifetime: u32,
}
pub async fn map_port(internal_port: u16) -> Result<Option<Mapping>> {
let gateway = default_gateway().await?;
let Some(gateway) = gateway else {
return Ok(None);
};
let sock = tokio::net::UdpSocket::bind("0.0.0.0:0")
.await
.context("could not open a UDP socket for NAT-PMP")?;
sock.connect(SocketAddr::from((gateway, GATEWAY_PORT)))
.await
.with_context(|| format!("could not reach the NAT-PMP responder at {gateway}"))?;
let external_request = build_external_request(internal_port);
let reply = exchange(&sock, &external_request, "external address").await?;
if reply.is_empty() {
return Ok(None);
}
let port_request = build_port_request(internal_port, MAPPING_LIFETIME);
let reply = exchange(&sock, &port_request, "port mapping").await?;
let Some((result, internal_port, lifetime, external_port)) = parse_port_reply(&reply) else {
return Ok(None);
};
if result != 0 {
anyhow::bail!("NAT-PMP refused the mapping: result {result}");
}
Ok(Some(Mapping {
internal_port,
external_port,
lifetime,
}))
}
pub async fn unmap_port(internal_port: u16) -> Result<()> {
let Some(gateway) = default_gateway().await? else {
return Ok(());
};
let Ok(sock) = tokio::net::UdpSocket::bind("0.0.0.0:0").await else {
return Ok(());
};
if sock
.connect(SocketAddr::from((gateway, GATEWAY_PORT)))
.await
.is_err()
{
return Ok(());
}
let mut request = build_port_request(internal_port, 0);
request[2] = 0;
request[3] = 2;
let _ = exchange(&sock, &request, "port delete").await;
Ok(())
}
async fn exchange(sock: &tokio::net::UdpSocket, request: &[u8], what: &str) -> Result<Vec<u8>> {
sock.send(request)
.await
.with_context(|| format!("could not send the NAT-PMP {what} request"))?;
let mut buf = [0u8; 32];
let n = tokio::time::timeout(TIMEOUT, sock.recv(&mut buf))
.await
.map_err(|_| anyhow::anyhow!("no answer to the NAT-PMP {what} request within {TIMEOUT:?}"))?
.context("NAT-PMP read failed")?;
Ok(buf[..n].to_vec())
}
pub(crate) async fn default_gateway() -> Result<Option<Ipv4Addr>> {
let probe: SocketAddr = "198.51.100.1:9".parse().expect("literal");
let Ok(sock) = tokio::net::UdpSocket::bind("0.0.0.0:0").await else {
return Ok(None);
};
if sock.connect(probe).await.is_err() {
return Ok(None);
}
match sock.local_addr().ok().map(|a| a.ip()) {
Some(IpAddr::V4(v4)) => Ok(Some(v4)),
_ => Ok(None),
}
}
fn build_external_request(internal_port: u16) -> Vec<u8> {
let mut v = Vec::with_capacity(12);
v.extend_from_slice(&0u16.to_be_bytes()); v.extend_from_slice(&0u16.to_be_bytes()); v.extend_from_slice(&[0u8; 4]); v.extend_from_slice(&internal_port.to_be_bytes());
v.extend_from_slice(&0u16.to_be_bytes()); v
}
fn build_port_request(internal_port: u16, lifetime: u32) -> Vec<u8> {
let mut v = Vec::with_capacity(16);
v.extend_from_slice(&0u16.to_be_bytes());
v.extend_from_slice(&1u16.to_be_bytes()); v.extend_from_slice(&[0u8; 4]); v.extend_from_slice(&internal_port.to_be_bytes());
v.extend_from_slice(&internal_port.to_be_bytes());
v.extend_from_slice(&lifetime.to_be_bytes());
v
}
fn parse_port_reply(reply: &[u8]) -> Option<(u16, u16, u32, u16)> {
if reply.len() < 18 {
return None;
}
Some((
u16::from_be_bytes([reply[4], reply[5]]), u16::from_be_bytes([reply[10], reply[11]]), u32::from_be_bytes([reply[14], reply[15], reply[16], reply[17]]), u16::from_be_bytes([reply[12], reply[13]]), ))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_external_request_is_twelve_bytes_and_well_formed() {
let r = build_external_request(5800);
assert_eq!(r.len(), 12);
assert_eq!(&r[0..2], &[0, 0], "version 0");
assert_eq!(&r[2..4], &[0, 0], "external address request");
assert_eq!(&r[4..8], &[0, 0, 0, 0], "reserved");
assert_eq!(&r[8..10], &5800u16.to_be_bytes(), "internal port");
assert_eq!(&r[10..12], &[0, 0], "suggested port");
}
#[test]
fn the_port_request_carries_the_lifetime_in_seconds() {
let r = build_port_request(5800, 3600);
assert_eq!(r.len(), 16, "RFC 6886 section 3.2");
assert_eq!(&r[0..2], &[0, 0], "version 0");
assert_eq!(&r[2..4], &[0, 1], "op 1 = map UDP");
assert_eq!(&r[4..8], &[0, 0, 0, 0], "reserved");
assert_eq!(&r[8..10], &5800u16.to_be_bytes(), "internal port");
assert_eq!(&r[10..12], &5800u16.to_be_bytes(), "external port");
assert_eq!(&r[12..16], &3600u32.to_be_bytes(), "lifetime in seconds");
}
#[test]
fn a_delete_request_is_the_map_request_with_op_two() {
let mut r = build_port_request(5800, 0);
r[2] = 0;
r[3] = 2;
assert_eq!(&r[2..4], &[0, 2], "op 2 = delete");
assert_eq!(&r[8..10], &5800u16.to_be_bytes());
assert_eq!(&r[12..16], &[0, 0, 0, 0], "lifetime 0 releases it");
}
#[test]
fn a_port_reply_is_read_at_the_right_offsets() {
let mut reply = vec![0u8; 18];
reply[0..2].copy_from_slice(&0u16.to_be_bytes()); reply[2..4].copy_from_slice(&129u16.to_be_bytes()); reply[4..6].copy_from_slice(&0u16.to_be_bytes()); reply[6..10].copy_from_slice(&7u32.to_be_bytes()); reply[10..12].copy_from_slice(&5800u16.to_be_bytes());
reply[12..14].copy_from_slice(&5801u16.to_be_bytes());
reply[14..18].copy_from_slice(&3600u32.to_be_bytes());
let (result, internal, lifetime, external) = parse_port_reply(&reply).unwrap();
assert_eq!(
(result, internal, lifetime, external),
(0, 5800, 3600, 5801)
);
}
#[test]
fn a_short_port_reply_is_refused() {
assert!(parse_port_reply(&[0u8; 12]).is_none());
}
}