pcp_client/
lib.rs

1//! Definitions and utilities to interact with a PCP server.
2
3use std::{net::Ipv4Addr, num::NonZeroU16, time::Duration};
4
5use rand::RngCore;
6use tracing::{debug, trace};
7
8mod protocol;
9
10/// Timeout to receive a response from a PCP server.
11const RECV_TIMEOUT: Duration = Duration::from_millis(500);
12
13/// Use the recommended port mapping lifetime for PMP, which is 2 hours. See
14/// <https://datatracker.ietf.org/doc/html/rfc6886#section-3.3>
15const MAPPING_REQUESTED_LIFETIME_SECONDS: u32 = 60 * 60;
16
17/// A mapping sucessfully registered with a PCP server.
18#[derive(Debug)]
19pub struct Mapping {
20    /// Local ip used to create this mapping.
21    pub local_ip: Ipv4Addr,
22    /// Local port used to create this mapping.
23    pub local_port: NonZeroU16,
24    /// Gateway address used to registed this mapping.
25    pub gateway: Ipv4Addr,
26    /// External port of the mapping.
27    pub external_port: NonZeroU16,
28    /// External address of the mapping.
29    pub external_address: Ipv4Addr,
30    /// Allowed time for this mapping as informed by the server.
31    pub lifetime_seconds: u32,
32    /// The nonce of the mapping, used for modifications with the PCP server, for example releasing
33    /// the mapping.
34    pub nonce: [u8; 12],
35}
36
37impl Mapping {
38    /// Attempt to registed a new mapping with the PCP server on the provided gateway.
39    pub async fn new(
40        local_ip: Ipv4Addr,
41        local_port: NonZeroU16,
42        gateway: Ipv4Addr,
43        preferred_external_address: Option<(Ipv4Addr, NonZeroU16)>,
44    ) -> anyhow::Result<Self> {
45        // create the socket and send the request
46        let socket = tokio::net::UdpSocket::bind((local_ip, 0)).await?;
47        socket.connect((gateway, protocol::SERVER_PORT)).await?;
48
49        let mut nonce = [0u8; 12];
50        rand::thread_rng().fill_bytes(&mut nonce);
51
52        let (requested_address, requested_port) = match preferred_external_address {
53            Some((ip, port)) => (Some(ip), Some(port.into())),
54            None => (None, None),
55        };
56
57        let req = protocol::Request::mapping(
58            nonce,
59            local_port.into(),
60            local_ip,
61            requested_port,
62            requested_address,
63            MAPPING_REQUESTED_LIFETIME_SECONDS,
64        );
65
66        socket.send(&req.encode()).await?;
67
68        // wait for the response and decode it
69        let mut buffer = vec![0; protocol::Response::MAX_SIZE];
70        let read = tokio::time::timeout(RECV_TIMEOUT, socket.recv(&mut buffer)).await??;
71        let response = protocol::Response::decode(&buffer[..read])?;
72
73        // verify that the response is correct and matches the request
74        let protocol::Response {
75            lifetime_seconds,
76            epoch_time: _,
77            data,
78        } = response;
79
80        match data {
81            protocol::OpcodeData::MapData(map_data) => {
82                let protocol::MapData {
83                    nonce: received_nonce,
84                    protocol,
85                    local_port: received_local_port,
86                    external_port,
87                    external_address,
88                } = map_data;
89
90                if nonce != received_nonce {
91                    anyhow::bail!("received nonce does not match sent request");
92                }
93
94                if protocol != protocol::MapProtocol::Udp {
95                    anyhow::bail!("received mapping is not for UDP");
96                }
97
98                let sent_port: u16 = local_port.into();
99                if received_local_port != sent_port {
100                    anyhow::bail!("received mapping is for a local port that does not match the requested one");
101                }
102                let external_port = external_port
103                    .try_into()
104                    .map_err(|_| anyhow::anyhow!("received 0 external port for mapping"))?;
105
106                let external_address = external_address
107                    .to_ipv4_mapped()
108                    .ok_or(anyhow::anyhow!("received external address is not ipv4"))?;
109
110                Ok(Mapping {
111                    external_port,
112                    external_address,
113                    lifetime_seconds,
114                    nonce,
115                    local_ip,
116                    local_port,
117                    gateway,
118                })
119            }
120            protocol::OpcodeData::Announce => {
121                anyhow::bail!("received an announce response for a map request")
122            }
123        }
124    }
125
126    pub async fn release(self) -> anyhow::Result<()> {
127        let Mapping {
128            nonce,
129            local_ip,
130            local_port,
131            gateway,
132            ..
133        } = self;
134
135        // create the socket and send the request
136        let socket = tokio::net::UdpSocket::bind((local_ip, 0)).await?;
137        socket.connect((gateway, protocol::SERVER_PORT)).await?;
138
139        let local_port = local_port.into();
140        let req = protocol::Request::mapping(nonce, local_port, local_ip, None, None, 0);
141
142        socket.send(&req.encode()).await?;
143
144        // mapping deletion is a notification, no point in waiting for the response
145        Ok(())
146    }
147}
148
149/// Probes the local gateway for PCP support.
150pub async fn probe_available(local_ip: Ipv4Addr, gateway: Ipv4Addr) -> bool {
151    match probe_available_fallible(local_ip, gateway).await {
152        Ok(response) => {
153            trace!("probe response: {response:?}");
154            let protocol::Response {
155                lifetime_seconds: _,
156                epoch_time: _,
157                data,
158            } = response;
159            match data {
160                protocol::OpcodeData::Announce => true,
161                _ => {
162                    debug!("server returned an unexpected response type for probe");
163                    // missbehaving server is not useful
164                    false
165                }
166            }
167        }
168        Err(e) => {
169            debug!("probe failed: {e}");
170            false
171        }
172    }
173}
174
175async fn probe_available_fallible(
176    local_ip: Ipv4Addr,
177    gateway: Ipv4Addr,
178) -> anyhow::Result<protocol::Response> {
179    // create the socket and send the request
180    let socket = tokio::net::UdpSocket::bind((local_ip, 0)).await?;
181    socket.connect((gateway, protocol::SERVER_PORT)).await?;
182    let req = protocol::Request::annouce(local_ip.to_ipv6_mapped());
183    socket.send(&req.encode()).await?;
184
185    // wait for the response and decode it
186    let mut buffer = vec![0; protocol::Response::MAX_SIZE];
187    let read = tokio::time::timeout(RECV_TIMEOUT, socket.recv(&mut buffer)).await??;
188    let response = protocol::Response::decode(&buffer[..read])?;
189
190    Ok(response)
191}