Skip to main content

dig_nat/method/
natpmp.rs

1//! NAT-PMP method (RFC 6886) — ask the local NAT gateway for a port mapping so inbound peer dials
2//! reach this node, and learn the gateway's external IP.
3//!
4//! NAT-PMP is a tiny fixed-layout UDP protocol spoken to the default gateway on port 5351. We
5//! implement the two datagrams we need directly (RFC 6886 §3.2 external-address request, §3.3
6//! map-port request): the packets are a handful of big-endian fields, so encode/parse is fully
7//! unit-testable against the RFC byte layout with NO real network. The live `attempt` sends them to
8//! the gateway; when there is no NAT-PMP gateway the request times out and the strategy falls
9//! through to the next method.
10
11use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4};
12use std::time::Duration;
13
14use async_trait::async_trait;
15use tokio::net::UdpSocket;
16
17use crate::error::MethodError;
18use crate::method::{MethodOutcome, TraversalKind, TraversalMethod};
19use crate::peer::PeerTarget;
20
21/// The well-known NAT-PMP / PCP server port on the gateway (RFC 6886 §3).
22pub const NATPMP_PORT: u16 = 5351;
23/// NAT-PMP version byte (RFC 6886 — version 0).
24pub const NATPMP_VERSION: u8 = 0;
25/// Opcode: request external address (RFC 6886 §3.2).
26pub const OP_EXTERNAL_ADDRESS: u8 = 0;
27/// Opcode: map UDP port (RFC 6886 §3.3). (TCP would be opcode 2.)
28pub const OP_MAP_UDP: u8 = 1;
29/// Opcode: map TCP port (RFC 6886 §3.3).
30pub const OP_MAP_TCP: u8 = 2;
31/// Response opcodes have the high bit set (opcode + 128, RFC 6886 §3.2/§3.3).
32pub const RESPONSE_FLAG: u8 = 0x80;
33
34/// Result code 0 = Success (RFC 6886 §3.5).
35pub const RESULT_SUCCESS: u16 = 0;
36
37/// Encode a NAT-PMP **external-address request** (RFC 6886 §3.2): `[version=0][opcode=0]`.
38pub fn encode_external_address_request() -> [u8; 2] {
39    [NATPMP_VERSION, OP_EXTERNAL_ADDRESS]
40}
41
42/// Encode a NAT-PMP **map-port request** (RFC 6886 §3.3):
43/// `[version][opcode][reserved:2][internal_port:2][suggested_external_port:2][lifetime_secs:4]`.
44pub fn encode_map_request(
45    tcp: bool,
46    internal_port: u16,
47    suggested_external_port: u16,
48    lifetime_secs: u32,
49) -> [u8; 12] {
50    let opcode = if tcp { OP_MAP_TCP } else { OP_MAP_UDP };
51    let mut buf = [0u8; 12];
52    buf[0] = NATPMP_VERSION;
53    buf[1] = opcode;
54    // buf[2..4] reserved = 0
55    buf[4..6].copy_from_slice(&internal_port.to_be_bytes());
56    buf[6..8].copy_from_slice(&suggested_external_port.to_be_bytes());
57    buf[8..12].copy_from_slice(&lifetime_secs.to_be_bytes());
58    buf
59}
60
61/// Parsed NAT-PMP external-address response (RFC 6886 §3.2).
62#[derive(Debug, Clone, PartialEq, Eq)]
63pub struct ExternalAddressResponse {
64    /// Seconds since the gateway's port-mapping table was initialised.
65    pub seconds_since_epoch: u32,
66    /// The gateway's external IPv4 address.
67    pub external_ip: Ipv4Addr,
68}
69
70/// Parse a NAT-PMP external-address response:
71/// `[version][opcode=128][result_code:2][seconds:4][external_ipv4:4]`.
72pub fn parse_external_address_response(msg: &[u8]) -> Result<ExternalAddressResponse, NatPmpError> {
73    if msg.len() < 12 {
74        return Err(NatPmpError::Truncated);
75    }
76    if msg[1] != OP_EXTERNAL_ADDRESS + RESPONSE_FLAG {
77        return Err(NatPmpError::UnexpectedOpcode(msg[1]));
78    }
79    let result = u16::from_be_bytes([msg[2], msg[3]]);
80    if result != RESULT_SUCCESS {
81        return Err(NatPmpError::ResultCode(result));
82    }
83    let seconds = u32::from_be_bytes([msg[4], msg[5], msg[6], msg[7]]);
84    let ip = Ipv4Addr::new(msg[8], msg[9], msg[10], msg[11]);
85    Ok(ExternalAddressResponse {
86        seconds_since_epoch: seconds,
87        external_ip: ip,
88    })
89}
90
91/// Parsed NAT-PMP map-port response (RFC 6886 §3.3).
92#[derive(Debug, Clone, PartialEq, Eq)]
93pub struct MapResponse {
94    /// The internal port that was mapped.
95    pub internal_port: u16,
96    /// The external port the gateway assigned (may differ from the suggestion).
97    pub external_port: u16,
98    /// The lifetime the gateway granted, in seconds.
99    pub lifetime_secs: u32,
100}
101
102/// Parse a NAT-PMP map-port response:
103/// `[version][opcode=128+op][result:2][seconds:4][internal_port:2][external_port:2][lifetime:4]`.
104pub fn parse_map_response(msg: &[u8], tcp: bool) -> Result<MapResponse, NatPmpError> {
105    if msg.len() < 16 {
106        return Err(NatPmpError::Truncated);
107    }
108    let expected_op = if tcp { OP_MAP_TCP } else { OP_MAP_UDP } + RESPONSE_FLAG;
109    if msg[1] != expected_op {
110        return Err(NatPmpError::UnexpectedOpcode(msg[1]));
111    }
112    let result = u16::from_be_bytes([msg[2], msg[3]]);
113    if result != RESULT_SUCCESS {
114        return Err(NatPmpError::ResultCode(result));
115    }
116    let internal_port = u16::from_be_bytes([msg[8], msg[9]]);
117    let external_port = u16::from_be_bytes([msg[10], msg[11]]);
118    let lifetime = u32::from_be_bytes([msg[12], msg[13], msg[14], msg[15]]);
119    Ok(MapResponse {
120        internal_port,
121        external_port,
122        lifetime_secs: lifetime,
123    })
124}
125
126/// NAT-PMP protocol / transaction errors.
127#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
128pub enum NatPmpError {
129    /// The datagram was shorter than a valid NAT-PMP response.
130    #[error("NAT-PMP response truncated")]
131    Truncated,
132    /// The response opcode did not match the request.
133    #[error("unexpected NAT-PMP opcode: {0}")]
134    UnexpectedOpcode(u8),
135    /// The gateway returned a non-success result code (RFC 6886 §3.5).
136    #[error("NAT-PMP result code {0}")]
137    ResultCode(u16),
138    /// Socket I/O error (stringified so the error stays `Clone`/`Eq`).
139    #[error("NAT-PMP io: {0}")]
140    Io(String),
141    /// No response within the deadline (likely no NAT-PMP gateway present).
142    #[error("NAT-PMP request timed out")]
143    Timeout,
144}
145
146/// The NAT-PMP traversal method.
147///
148/// Discovers the gateway's external IPv4, then requests a UDP mapping from `local_port` so the peer
149/// can reach this node, and yields a dial address for the peer. When no NAT-PMP gateway is present,
150/// the request times out and the method fails (strategy falls through).
151#[derive(Debug, Clone)]
152pub struct NatPmpMethod {
153    /// The gateway address (usually the default route, port [`NATPMP_PORT`]).
154    pub gateway: SocketAddrV4,
155    /// The local UDP port this node listens on and wants mapped.
156    pub local_port: u16,
157    /// Requested mapping lifetime (seconds).
158    pub lifetime_secs: u32,
159    /// Per-request deadline.
160    pub timeout: Duration,
161}
162
163impl NatPmpMethod {
164    /// Build a NAT-PMP method for the given gateway + local port with sensible defaults
165    /// (2h lifetime, 1s timeout — a present gateway answers in milliseconds).
166    pub fn new(gateway: Ipv4Addr, local_port: u16) -> Self {
167        NatPmpMethod {
168            gateway: SocketAddrV4::new(gateway, NATPMP_PORT),
169            local_port,
170            lifetime_secs: 7200,
171            timeout: Duration::from_secs(1),
172        }
173    }
174
175    /// Send `payload` to the gateway and read one response datagram, bounded by [`Self::timeout`].
176    async fn transact(&self, payload: &[u8]) -> Result<Vec<u8>, NatPmpError> {
177        let socket = UdpSocket::bind((Ipv4Addr::UNSPECIFIED, 0))
178            .await
179            .map_err(|e| NatPmpError::Io(e.to_string()))?;
180        socket
181            .send_to(payload, self.gateway)
182            .await
183            .map_err(|e| NatPmpError::Io(e.to_string()))?;
184        let mut buf = [0u8; 32];
185        match tokio::time::timeout(self.timeout, socket.recv_from(&mut buf)).await {
186            Ok(Ok((n, _))) => Ok(buf[..n].to_vec()),
187            Ok(Err(e)) => Err(NatPmpError::Io(e.to_string())),
188            Err(_) => Err(NatPmpError::Timeout),
189        }
190    }
191}
192
193#[async_trait]
194impl TraversalMethod for NatPmpMethod {
195    fn kind(&self) -> TraversalKind {
196        TraversalKind::NatPmp
197    }
198
199    async fn attempt(&self, peer: &PeerTarget) -> Result<MethodOutcome, MethodError> {
200        // NAT-PMP opens OUR pinhole; we still need the peer's address(es) to dial afterwards. Carry
201        // the peer's whole IPv6-first candidate list so the post-mapping dial keeps the fallback.
202        let dial_addrs = peer.direct_addrs();
203        if dial_addrs.is_empty() {
204            return Err(MethodError::failed(
205                TraversalKind::NatPmp,
206                "peer has no address to dial after mapping",
207            ));
208        }
209
210        // 1) Confirm a NAT-PMP gateway exists (external-address request).
211        let resp = self
212            .transact(&encode_external_address_request())
213            .await
214            .map_err(|e| to_method_error(&e))?;
215        parse_external_address_response(&resp).map_err(|e| to_method_error(&e))?;
216
217        // 2) Request a UDP mapping so inbound reaches us.
218        let map = encode_map_request(false, self.local_port, self.local_port, self.lifetime_secs);
219        let resp = self.transact(&map).await.map_err(|e| to_method_error(&e))?;
220        parse_map_response(&resp, false).map_err(|e| to_method_error(&e))?;
221
222        Ok(MethodOutcome::candidates(
223            TraversalKind::NatPmp,
224            dial_addrs.to_vec(),
225        ))
226    }
227}
228
229/// Map a [`NatPmpError`] to the shared [`MethodError`], preserving the timeout flag.
230fn to_method_error(e: &NatPmpError) -> MethodError {
231    match e {
232        NatPmpError::Timeout => MethodError::timeout(TraversalKind::NatPmp),
233        other => MethodError::failed(TraversalKind::NatPmp, other.to_string()),
234    }
235}
236
237/// Turn a [`SocketAddr`] hint into an IPv4 gateway if possible (NAT-PMP is IPv4-only).
238pub fn ipv4_gateway(addr: SocketAddr) -> Option<Ipv4Addr> {
239    match addr {
240        SocketAddr::V4(v4) => Some(*v4.ip()),
241        SocketAddr::V6(_) => None,
242    }
243}