use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4};
use std::time::Duration;
use async_trait::async_trait;
use tokio::net::UdpSocket;
use crate::error::MethodError;
use crate::method::{MethodOutcome, TraversalKind, TraversalMethod};
use crate::peer::PeerTarget;
pub const NATPMP_PORT: u16 = 5351;
pub const NATPMP_VERSION: u8 = 0;
pub const OP_EXTERNAL_ADDRESS: u8 = 0;
pub const OP_MAP_UDP: u8 = 1;
pub const OP_MAP_TCP: u8 = 2;
pub const RESPONSE_FLAG: u8 = 0x80;
pub const RESULT_SUCCESS: u16 = 0;
pub fn encode_external_address_request() -> [u8; 2] {
[NATPMP_VERSION, OP_EXTERNAL_ADDRESS]
}
pub fn encode_map_request(
tcp: bool,
internal_port: u16,
suggested_external_port: u16,
lifetime_secs: u32,
) -> [u8; 12] {
let opcode = if tcp { OP_MAP_TCP } else { OP_MAP_UDP };
let mut buf = [0u8; 12];
buf[0] = NATPMP_VERSION;
buf[1] = opcode;
buf[4..6].copy_from_slice(&internal_port.to_be_bytes());
buf[6..8].copy_from_slice(&suggested_external_port.to_be_bytes());
buf[8..12].copy_from_slice(&lifetime_secs.to_be_bytes());
buf
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExternalAddressResponse {
pub seconds_since_epoch: u32,
pub external_ip: Ipv4Addr,
}
pub fn parse_external_address_response(msg: &[u8]) -> Result<ExternalAddressResponse, NatPmpError> {
if msg.len() < 12 {
return Err(NatPmpError::Truncated);
}
if msg[1] != OP_EXTERNAL_ADDRESS + RESPONSE_FLAG {
return Err(NatPmpError::UnexpectedOpcode(msg[1]));
}
let result = u16::from_be_bytes([msg[2], msg[3]]);
if result != RESULT_SUCCESS {
return Err(NatPmpError::ResultCode(result));
}
let seconds = u32::from_be_bytes([msg[4], msg[5], msg[6], msg[7]]);
let ip = Ipv4Addr::new(msg[8], msg[9], msg[10], msg[11]);
Ok(ExternalAddressResponse {
seconds_since_epoch: seconds,
external_ip: ip,
})
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MapResponse {
pub internal_port: u16,
pub external_port: u16,
pub lifetime_secs: u32,
}
pub fn parse_map_response(msg: &[u8], tcp: bool) -> Result<MapResponse, NatPmpError> {
if msg.len() < 16 {
return Err(NatPmpError::Truncated);
}
let expected_op = if tcp { OP_MAP_TCP } else { OP_MAP_UDP } + RESPONSE_FLAG;
if msg[1] != expected_op {
return Err(NatPmpError::UnexpectedOpcode(msg[1]));
}
let result = u16::from_be_bytes([msg[2], msg[3]]);
if result != RESULT_SUCCESS {
return Err(NatPmpError::ResultCode(result));
}
let internal_port = u16::from_be_bytes([msg[8], msg[9]]);
let external_port = u16::from_be_bytes([msg[10], msg[11]]);
let lifetime = u32::from_be_bytes([msg[12], msg[13], msg[14], msg[15]]);
Ok(MapResponse {
internal_port,
external_port,
lifetime_secs: lifetime,
})
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum NatPmpError {
#[error("NAT-PMP response truncated")]
Truncated,
#[error("unexpected NAT-PMP opcode: {0}")]
UnexpectedOpcode(u8),
#[error("NAT-PMP result code {0}")]
ResultCode(u16),
#[error("NAT-PMP io: {0}")]
Io(String),
#[error("NAT-PMP request timed out")]
Timeout,
}
#[derive(Debug, Clone)]
pub struct NatPmpMethod {
pub gateway: SocketAddrV4,
pub local_port: u16,
pub lifetime_secs: u32,
pub timeout: Duration,
}
impl NatPmpMethod {
pub fn new(gateway: Ipv4Addr, local_port: u16) -> Self {
NatPmpMethod {
gateway: SocketAddrV4::new(gateway, NATPMP_PORT),
local_port,
lifetime_secs: 7200,
timeout: Duration::from_secs(1),
}
}
async fn transact(&self, payload: &[u8]) -> Result<Vec<u8>, NatPmpError> {
let socket = UdpSocket::bind((Ipv4Addr::UNSPECIFIED, 0))
.await
.map_err(|e| NatPmpError::Io(e.to_string()))?;
socket
.send_to(payload, self.gateway)
.await
.map_err(|e| NatPmpError::Io(e.to_string()))?;
let mut buf = [0u8; 32];
match tokio::time::timeout(self.timeout, socket.recv_from(&mut buf)).await {
Ok(Ok((n, _))) => Ok(buf[..n].to_vec()),
Ok(Err(e)) => Err(NatPmpError::Io(e.to_string())),
Err(_) => Err(NatPmpError::Timeout),
}
}
}
#[async_trait]
impl TraversalMethod for NatPmpMethod {
fn kind(&self) -> TraversalKind {
TraversalKind::NatPmp
}
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::NatPmp,
"peer has no address to dial after mapping",
));
}
let resp = self
.transact(&encode_external_address_request())
.await
.map_err(|e| to_method_error(&e))?;
parse_external_address_response(&resp).map_err(|e| to_method_error(&e))?;
let map = encode_map_request(false, self.local_port, self.local_port, self.lifetime_secs);
let resp = self.transact(&map).await.map_err(|e| to_method_error(&e))?;
parse_map_response(&resp, false).map_err(|e| to_method_error(&e))?;
Ok(MethodOutcome::candidates(
TraversalKind::NatPmp,
dial_addrs.to_vec(),
))
}
}
fn to_method_error(e: &NatPmpError) -> MethodError {
match e {
NatPmpError::Timeout => MethodError::timeout(TraversalKind::NatPmp),
other => MethodError::failed(TraversalKind::NatPmp, other.to_string()),
}
}
pub fn ipv4_gateway(addr: SocketAddr) -> Option<Ipv4Addr> {
match addr {
SocketAddr::V4(v4) => Some(*v4.ip()),
SocketAddr::V6(_) => None,
}
}