portmapper/pcp/protocol/
request.rs

1//! A PCP request encoding and decoding.
2
3use std::net::{Ipv4Addr, Ipv6Addr};
4
5use super::{
6    opcode_data::{MapData, MapProtocol, OpcodeData},
7    Version,
8};
9
10/// A PCP Request.
11///
12/// See [RFC 6887 Request Header](https://datatracker.ietf.org/doc/html/rfc6887#section-7.1)
13// NOTE: PCP Options are optional, and currently not used in this code, thus not implemented
14#[derive(Debug, PartialEq, Eq)]
15pub struct Request {
16    /// [`Version`] to use in this request.
17    pub(super) version: Version,
18    /// Requested lifetime in seconds.
19    pub(super) lifetime_seconds: u32,
20    /// IP Address of the client.
21    ///
22    /// If the IP is an IpV4 address, is represented as a IpV4-mapped IpV6 address.
23    pub(super) client_addr: Ipv6Addr,
24    /// Data associated to the [`super::Opcode`] in this request.
25    pub(super) opcode_data: OpcodeData,
26}
27
28impl Request {
29    /// Size of a [`Request`] sent by this client, in bytes.
30    pub const MIN_SIZE: usize = // parts:
31        1 + // version
32        1 + // opcode
33        2 + // reserved
34        4 + // lifetime
35        16; // local ip
36
37    /// Encode this [`Request`].
38    pub fn encode(&self) -> Vec<u8> {
39        let Request {
40            version,
41            lifetime_seconds,
42            client_addr,
43            opcode_data,
44        } = self;
45        let mut buf = Vec::with_capacity(Self::MIN_SIZE + opcode_data.encoded_size());
46        // buf[0]
47        buf.push((*version).into());
48        // buf[1]
49        buf.push(opcode_data.opcode().into());
50        // buf[2] reserved
51        buf.push(0);
52        // buf[3] reserved
53        buf.push(0);
54        // buf[4..8]
55        buf.extend_from_slice(&lifetime_seconds.to_be_bytes());
56        // buf[8..24]
57        buf.extend_from_slice(&client_addr.octets());
58        // buf[24..]
59        opcode_data.encode_into(&mut buf);
60
61        buf
62    }
63
64    /// Create an announce request.
65    pub fn announce(client_addr: Ipv6Addr) -> Request {
66        Request {
67            version: Version::Pcp,
68            // opcode announce requires a lifetime of 0 and to ignore the lifetime on response
69            lifetime_seconds: 0,
70            client_addr,
71            // the pcp announce opcode requests and responses have no opcode-specific payload
72            opcode_data: OpcodeData::Announce,
73        }
74    }
75
76    /// Create a mapping request.
77    pub fn mapping(
78        nonce: [u8; 12],
79        local_port: u16,
80        local_ip: Ipv4Addr,
81        preferred_external_port: Option<u16>,
82        preferred_external_address: Option<Ipv4Addr>,
83        lifetime_seconds: u32,
84    ) -> Request {
85        Request {
86            version: Version::Pcp,
87            lifetime_seconds,
88            client_addr: local_ip.to_ipv6_mapped(),
89            opcode_data: OpcodeData::MapData(MapData {
90                nonce,
91                protocol: MapProtocol::Udp,
92                local_port,
93                // if the pcp client does not know the external port, or does not have a
94                // preference, it must use 0.
95                external_port: preferred_external_port.unwrap_or_default(),
96                external_address: preferred_external_address
97                    .unwrap_or(Ipv4Addr::UNSPECIFIED)
98                    .to_ipv6_mapped(),
99            }),
100        }
101    }
102
103    #[cfg(test)]
104    fn random<R: rand::Rng>(opcode: super::Opcode, rng: &mut R) -> Self {
105        let opcode_data = OpcodeData::random(opcode, rng);
106        let addr_octets: [u8; 16] = rng.gen();
107        Request {
108            version: Version::Pcp,
109            lifetime_seconds: rng.gen(),
110            client_addr: Ipv6Addr::from(addr_octets),
111            opcode_data,
112        }
113    }
114
115    #[cfg(test)]
116    #[track_caller]
117    fn decode(buf: &[u8]) -> Self {
118        let version: Version = buf[0].try_into().unwrap();
119        let opcode: super::Opcode = buf[1].try_into().unwrap();
120        // buf[2] reserved
121        // buf[3] reserved
122        let lifetime_bytes: [u8; 4] = buf[4..8].try_into().unwrap();
123        let lifetime_seconds = u32::from_be_bytes(lifetime_bytes);
124
125        let local_ip_bytes: [u8; 16] = buf[8..24].try_into().unwrap();
126        let client_addr: Ipv6Addr = local_ip_bytes.into();
127
128        let opcode_data = OpcodeData::decode(opcode, &buf[24..]).unwrap();
129        Self {
130            version,
131            lifetime_seconds,
132            client_addr,
133            opcode_data,
134        }
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use rand::SeedableRng;
141
142    use super::*;
143
144    #[test]
145    fn test_encode_decode_addr_request() {
146        let mut gen = rand_chacha::ChaCha8Rng::seed_from_u64(42);
147
148        let request = Request::random(super::super::Opcode::Announce, &mut gen);
149        let encoded = request.encode();
150        assert_eq!(request, Request::decode(&encoded));
151    }
152
153    #[test]
154    fn test_encode_decode_map_request() {
155        let mut gen = rand_chacha::ChaCha8Rng::seed_from_u64(42);
156
157        let request = Request::random(super::super::Opcode::Map, &mut gen);
158        let encoded = request.encode();
159        assert_eq!(request, Request::decode(&encoded));
160    }
161}