Skip to main content

dig_nat/method/
pcp.rs

1//! PCP method (RFC 6887) — the successor to NAT-PMP. Same goal (open an inbound pinhole on the
2//! gateway) with a richer, IPv6-capable datagram.
3//!
4//! PCP is spoken to the gateway on the same port as NAT-PMP (5351). We implement the MAP request /
5//! response directly (RFC 6887 §11.1 common header, §11.2 MAP opcode): a 24-byte common header plus
6//! a 36-byte MAP body. As with NAT-PMP, the fixed byte layout means encode/parse is fully
7//! unit-testable against the RFC with no network; the live `attempt` sends it and, absent a PCP
8//! gateway, times out so the strategy falls through.
9
10use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4};
11use std::time::Duration;
12
13use async_trait::async_trait;
14use tokio::net::UdpSocket;
15
16use crate::error::MethodError;
17use crate::method::natpmp::NATPMP_PORT;
18use crate::method::{MethodOutcome, TraversalKind, TraversalMethod};
19use crate::peer::PeerTarget;
20
21/// PCP version (RFC 6887 — version 2).
22pub const PCP_VERSION: u8 = 2;
23/// MAP opcode (RFC 6887 §11.2).
24pub const OP_MAP: u8 = 1;
25/// The `R` (response) bit in the opcode byte of a PCP response.
26pub const RESPONSE_BIT: u8 = 0x80;
27/// Result code SUCCESS (RFC 6887 §7.4).
28pub const RESULT_SUCCESS: u8 = 0;
29/// IANA protocol number for UDP (used in the MAP body's protocol field).
30pub const PROTO_UDP: u8 = 17;
31/// IANA protocol number for TCP.
32pub const PROTO_TCP: u8 = 6;
33
34/// A 96-bit MAP nonce (RFC 6887 §11.1) matching a response to a request.
35pub type MapNonce = [u8; 12];
36
37/// Encode a PCP **MAP request** (RFC 6887 §11.1 header + §11.2 body).
38///
39/// Header (24 bytes): `[version][opcode][reserved:2][lifetime:4][client_ip:16]`.
40/// MAP body (36 bytes): `[nonce:12][protocol][reserved:3][internal_port:2][suggested_ext_port:2]
41/// [suggested_ext_ip:16]`.
42///
43/// `client_ip` is this node's address as the gateway sees it, encoded as an IPv4-mapped IPv6 when
44/// IPv4 (RFC 6887 uses 128-bit address fields throughout).
45pub fn encode_map_request(
46    nonce: &MapNonce,
47    tcp: bool,
48    internal_port: u16,
49    suggested_external_port: u16,
50    lifetime_secs: u32,
51    client_ip: IpAddr,
52) -> Vec<u8> {
53    let mut buf = Vec::with_capacity(60);
54    buf.push(PCP_VERSION);
55    buf.push(OP_MAP); // request: R bit clear
56    buf.extend_from_slice(&[0, 0]); // reserved
57    buf.extend_from_slice(&lifetime_secs.to_be_bytes());
58    buf.extend_from_slice(&ip_to_16(client_ip));
59    // MAP body.
60    buf.extend_from_slice(nonce);
61    buf.push(if tcp { PROTO_TCP } else { PROTO_UDP });
62    buf.extend_from_slice(&[0, 0, 0]); // reserved
63    buf.extend_from_slice(&internal_port.to_be_bytes());
64    buf.extend_from_slice(&suggested_external_port.to_be_bytes());
65    buf.extend_from_slice(&ip_to_16(IpAddr::V4(Ipv4Addr::UNSPECIFIED))); // suggest any external ip
66    debug_assert_eq!(buf.len(), 60);
67    buf
68}
69
70/// Parsed PCP MAP response (the fields dig-nat needs).
71#[derive(Debug, Clone, PartialEq, Eq)]
72pub struct MapResponse {
73    /// The lifetime the gateway granted (seconds).
74    pub lifetime_secs: u32,
75    /// The MAP nonce echoed back (must match the request).
76    pub nonce: MapNonce,
77    /// The external port assigned.
78    pub external_port: u16,
79    /// The external IP assigned.
80    pub external_ip: IpAddr,
81}
82
83/// Parse a PCP MAP response, validating version, the MAP-response opcode, the result code, and the
84/// echoed nonce.
85///
86/// Response header (24 bytes): `[version][opcode|R][reserved][result_code][lifetime:4]
87/// [epoch:4][reserved:12]`. MAP body (36 bytes): `[nonce:12][protocol][reserved:3][internal_port:2]
88/// [assigned_ext_port:2][assigned_ext_ip:16]`.
89pub fn parse_map_response(msg: &[u8], expected_nonce: &MapNonce) -> Result<MapResponse, PcpError> {
90    if msg.len() < 60 {
91        return Err(PcpError::Truncated);
92    }
93    if msg[0] != PCP_VERSION {
94        return Err(PcpError::BadVersion(msg[0]));
95    }
96    if msg[1] != (OP_MAP | RESPONSE_BIT) {
97        return Err(PcpError::UnexpectedOpcode(msg[1]));
98    }
99    let result = msg[3];
100    if result != RESULT_SUCCESS {
101        return Err(PcpError::ResultCode(result));
102    }
103    let lifetime = u32::from_be_bytes([msg[4], msg[5], msg[6], msg[7]]);
104    // MAP body starts at byte 24.
105    let nonce: MapNonce = msg[24..36].try_into().map_err(|_| PcpError::Truncated)?;
106    if &nonce != expected_nonce {
107        return Err(PcpError::NonceMismatch);
108    }
109    let external_port = u16::from_be_bytes([msg[42], msg[43]]);
110    let ext_ip_bytes: [u8; 16] = msg[44..60].try_into().map_err(|_| PcpError::Truncated)?;
111    Ok(MapResponse {
112        lifetime_secs: lifetime,
113        nonce,
114        external_port,
115        external_ip: ip_from_16(ext_ip_bytes),
116    })
117}
118
119/// PCP protocol / transaction errors.
120#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
121pub enum PcpError {
122    /// The datagram was shorter than a valid PCP MAP response.
123    #[error("PCP response truncated")]
124    Truncated,
125    /// Unexpected PCP version byte.
126    #[error("unexpected PCP version: {0}")]
127    BadVersion(u8),
128    /// The response opcode was not the MAP response.
129    #[error("unexpected PCP opcode: {0}")]
130    UnexpectedOpcode(u8),
131    /// The gateway returned a non-success result code (RFC 6887 §7.4).
132    #[error("PCP result code {0}")]
133    ResultCode(u8),
134    /// The echoed nonce did not match the request (possible spoof / stale reply).
135    #[error("PCP MAP nonce mismatch")]
136    NonceMismatch,
137    /// Socket I/O error (stringified so the error stays `Clone`/`Eq`).
138    #[error("PCP io: {0}")]
139    Io(String),
140    /// No response within the deadline (likely no PCP gateway present).
141    #[error("PCP request timed out")]
142    Timeout,
143}
144
145/// The PCP traversal method — requests a MAP mapping from the gateway so inbound peer dials reach
146/// this node, then yields a dial address for the peer.
147#[derive(Debug, Clone)]
148pub struct PcpMethod {
149    /// The gateway address (usually the default route, port [`NATPMP_PORT`] = 5351).
150    pub gateway: SocketAddrV4,
151    /// The local port this node listens on and wants mapped.
152    pub local_port: u16,
153    /// This node's client IP as the gateway sees it (goes in the PCP header).
154    pub client_ip: IpAddr,
155    /// Requested mapping lifetime (seconds).
156    pub lifetime_secs: u32,
157    /// Per-request deadline.
158    pub timeout: Duration,
159}
160
161impl PcpMethod {
162    /// Build a PCP method for the given gateway + local port with sensible defaults.
163    pub fn new(gateway: Ipv4Addr, local_port: u16, client_ip: IpAddr) -> Self {
164        PcpMethod {
165            gateway: SocketAddrV4::new(gateway, NATPMP_PORT),
166            local_port,
167            client_ip,
168            lifetime_secs: 7200,
169            timeout: Duration::from_secs(1),
170        }
171    }
172
173    /// Send a PCP request and read one response datagram, bounded by [`Self::timeout`].
174    ///
175    /// Validates the response source == [`Self::gateway`] (#179 finding 3, defense-in-depth
176    /// alongside the CSPRNG MAP nonce): a datagram from any other address is discarded and the
177    /// receive loop continues within the deadline, so a single spoofed reply from an attacker on the
178    /// gateway path cannot defeat a transaction whose genuine reply is still in flight.
179    async fn transact(&self, payload: &[u8]) -> Result<Vec<u8>, PcpError> {
180        let socket = UdpSocket::bind((Ipv4Addr::UNSPECIFIED, 0))
181            .await
182            .map_err(|e| PcpError::Io(e.to_string()))?;
183        socket
184            .send_to(payload, self.gateway)
185            .await
186            .map_err(|e| PcpError::Io(e.to_string()))?;
187        let mut buf = [0u8; 128];
188        let deadline = tokio::time::Instant::now() + self.timeout;
189        loop {
190            let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
191            if remaining.is_zero() {
192                return Err(PcpError::Timeout);
193            }
194            let (n, from) = match tokio::time::timeout(remaining, socket.recv_from(&mut buf)).await
195            {
196                Ok(Ok(x)) => x,
197                Ok(Err(e)) => return Err(PcpError::Io(e.to_string())),
198                Err(_) => return Err(PcpError::Timeout),
199            };
200            if from != SocketAddr::V4(self.gateway) {
201                // Not from the gateway we queried — ignore and keep waiting for the genuine reply.
202                continue;
203            }
204            return Ok(buf[..n].to_vec());
205        }
206    }
207}
208
209#[async_trait]
210impl TraversalMethod for PcpMethod {
211    fn kind(&self) -> TraversalKind {
212        TraversalKind::Pcp
213    }
214
215    async fn attempt(&self, peer: &PeerTarget) -> Result<MethodOutcome, MethodError> {
216        // Carry the peer's whole candidate list so the post-mapping dial (dig-ip) keeps the fallback
217        // across families.
218        let dial_addrs = peer.direct_addrs();
219        if dial_addrs.is_empty() {
220            return Err(MethodError::failed(
221                TraversalKind::Pcp,
222                "peer has no address to dial after mapping",
223            ));
224        }
225        let nonce = new_nonce();
226        let req = encode_map_request(
227            &nonce,
228            false,
229            self.local_port,
230            self.local_port,
231            self.lifetime_secs,
232            self.client_ip,
233        );
234        let resp = self.transact(&req).await.map_err(|e| to_method_error(&e))?;
235        parse_map_response(&resp, &nonce).map_err(|e| to_method_error(&e))?;
236        Ok(MethodOutcome::candidates(
237            TraversalKind::Pcp,
238            dial_addrs.to_vec(),
239        ))
240    }
241}
242
243/// Map a [`PcpError`] to the shared [`MethodError`], preserving the timeout flag.
244fn to_method_error(e: &PcpError) -> MethodError {
245    match e {
246        PcpError::Timeout => MethodError::timeout(TraversalKind::Pcp),
247        other => MethodError::failed(TraversalKind::Pcp, other.to_string()),
248    }
249}
250
251/// Encode an [`IpAddr`] as a 16-byte field (IPv4 → IPv4-mapped IPv6, per RFC 6887).
252fn ip_to_16(ip: IpAddr) -> [u8; 16] {
253    match ip {
254        IpAddr::V4(v4) => v4.to_ipv6_mapped().octets(),
255        IpAddr::V6(v6) => v6.octets(),
256    }
257}
258
259/// Decode a 16-byte PCP address field back to an [`IpAddr`], unmapping IPv4-mapped IPv6.
260fn ip_from_16(bytes: [u8; 16]) -> IpAddr {
261    let v6 = Ipv6Addr::from(bytes);
262    match v6.to_ipv4_mapped() {
263        Some(v4) => IpAddr::V4(v4),
264        None => IpAddr::V6(v6),
265    }
266}
267
268/// Generate a 96-bit PCP MAP nonce from a CSPRNG (RFC 6887 §11.1 requires the nonce be
269/// unpredictable so a peer/attacker cannot forge or overwrite another client's mapping).
270///
271/// `parse_map_response`'s echoed-nonce check is the sole anti-spoof validator for a MAP response
272/// (see [`transact`](PcpMethod::transact) for the independent source-address check), so — exactly
273/// like the STUN transaction id ([`crate::stun::new_transaction_id`]) — the nonce MUST NOT be
274/// derived from wall-clock time or any other predictable input.
275///
276/// `pub` so tests can assert directly on the nonce's statistical properties (see `tests/pcp.rs`)
277/// without re-running a full network transaction.
278pub fn new_nonce() -> MapNonce {
279    use ring::rand::{SecureRandom, SystemRandom};
280
281    let mut n = [0u8; 12];
282    SystemRandom::new()
283        .fill(&mut n)
284        .expect("OS CSPRNG must be available to generate a PCP MAP nonce");
285    n
286}
287
288/// Turn a [`SocketAddr`] hint into an IPv4 gateway if possible.
289pub fn ipv4_gateway(addr: SocketAddr) -> Option<Ipv4Addr> {
290    match addr {
291        SocketAddr::V4(v4) => Some(*v4.ip()),
292        SocketAddr::V6(_) => None,
293    }
294}