Skip to main content

pim_protocol/
control_frame.rs

1//! Control-plane messages carried inside transport frames.
2
3use bytes::{Buf, BufMut, BytesMut};
4
5use pim_core::{FrameCodec, NodeId, PimError};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8#[repr(u8)]
9/// Discriminator for [`ControlFrame`] payloads.
10pub enum ControlType {
11    /// Client requests a mesh IPv4 assignment from a gateway.
12    IpRequest = 0x01,
13    /// Gateway assigns a mesh IPv4 configuration lease to a client.
14    IpAssign = 0x02,
15    /// Peer is leaving and wants its state cleaned up promptly.
16    Goodbye = 0x03,
17    /// Session keys should be renegotiated.
18    Rekey = 0x04,
19    /// RTT probe request.
20    Ping = 0x05,
21    /// RTT probe response.
22    Pong = 0x06,
23    /// One-shot exchange of node identity metadata after handshake.
24    PeerInfo = 0x07,
25    /// User-to-user encrypted message payload.
26    Message = 0x08,
27    /// Acknowledgement for a previously delivered message.
28    MessageAck = 0x09,
29}
30
31impl ControlType {
32    /// Decode a raw control-type tag from the wire.
33    pub fn from_u8(v: u8) -> Result<Self, PimError> {
34        match v {
35            0x01 => Ok(Self::IpRequest),
36            0x02 => Ok(Self::IpAssign),
37            0x03 => Ok(Self::Goodbye),
38            0x04 => Ok(Self::Rekey),
39            0x05 => Ok(Self::Ping),
40            0x06 => Ok(Self::Pong),
41            0x07 => Ok(Self::PeerInfo),
42            0x08 => Ok(Self::Message),
43            0x09 => Ok(Self::MessageAck),
44            other => Err(PimError::Protocol(format!(
45                "unknown control type: 0x{other:02x}"
46            ))),
47        }
48    }
49}
50
51/// Multiplexed control message.
52///
53/// Layout: control_type(1) + body (variable, depends on type)
54#[derive(Debug, Clone, PartialEq, Eq)]
55pub enum ControlFrame {
56    /// Request an address lease from a gateway.
57    IpRequest {
58        /// Node requesting a mesh IP allocation.
59        requester_id: NodeId,
60    },
61    /// Lease configuration returned by a gateway.
62    IpAssign {
63        /// Assigned mesh IPv4 address.
64        assigned_ip: [u8; 4],
65        /// CIDR prefix length for the assigned subnet.
66        subnet_mask: u8,
67        /// Mesh IPv4 address of the serving gateway.
68        gateway_ip: [u8; 4],
69        /// Lease duration in seconds.
70        lease_seconds: u32,
71    },
72    /// Graceful disconnect notification.
73    Goodbye {
74        /// Node that is departing.
75        departing_id: NodeId,
76        /// Implementation-defined reason code.
77        reason: u8,
78    },
79    /// Request that the session be rekeyed.
80    Rekey,
81    /// Ping carrying an opaque nonce.
82    Ping {
83        /// Opaque value echoed by the corresponding pong.
84        nonce: u64,
85    },
86    /// Pong echoing a ping nonce.
87    Pong {
88        /// Opaque value copied from the ping request.
89        nonce: u64,
90    },
91    /// Identity metadata exchanged once after the session is established
92    /// so that peers can address each other by `NodeId` and end-to-end
93    /// encrypt to the recipient's static X25519 key.
94    PeerInfo {
95        /// X25519 public key derived from the sender's Ed25519 seed.
96        x25519_pub: [u8; 32],
97        /// Sender's friendly node name as configured locally (UTF-8,
98        /// length-prefixed by `u8` — capped at 255 bytes by the codec).
99        friendly_name: String,
100    },
101    /// End-to-end encrypted user message.
102    ///
103    /// Carried inside a [`MeshDataFrame`](crate::MeshDataFrame) with both
104    /// [`DataFlags::IS_CONTROL`](crate::DataFlags::IS_CONTROL) and
105    /// [`DataFlags::IS_E2E`](crate::DataFlags::IS_E2E) set. `ciphertext`
106    /// is the literal output of `pim_crypto::e2e_encrypt`.
107    Message {
108        /// 16-byte stable identifier (UUIDv4 bytes).
109        message_id: [u8; 16],
110        /// Sender-stamped wall-clock time in milliseconds since epoch.
111        timestamp_ms: u64,
112        /// ECIES-encrypted UTF-8 plaintext.
113        ciphertext: Vec<u8>,
114    },
115    /// Receipt for a previously sent [`ControlFrame::Message`].
116    MessageAck {
117        /// Identifier of the original message being acknowledged.
118        message_id: [u8; 16],
119        /// Acknowledgement category: `1 = delivered`, `2 = read`.
120        ack_kind: u8,
121    },
122}
123
124impl FrameCodec for ControlFrame {
125    fn encode(&self, buf: &mut BytesMut) {
126        match self {
127            ControlFrame::IpRequest { requester_id } => {
128                buf.put_u8(ControlType::IpRequest as u8);
129                buf.put_slice(requester_id.as_bytes());
130            }
131            ControlFrame::IpAssign {
132                assigned_ip,
133                subnet_mask,
134                gateway_ip,
135                lease_seconds,
136            } => {
137                buf.put_u8(ControlType::IpAssign as u8);
138                buf.put_slice(assigned_ip);
139                buf.put_u8(*subnet_mask);
140                buf.put_slice(gateway_ip);
141                buf.put_u32(*lease_seconds);
142            }
143            ControlFrame::Goodbye {
144                departing_id,
145                reason,
146            } => {
147                buf.put_u8(ControlType::Goodbye as u8);
148                buf.put_slice(departing_id.as_bytes());
149                buf.put_u8(*reason);
150            }
151            ControlFrame::Rekey => {
152                buf.put_u8(ControlType::Rekey as u8);
153            }
154            ControlFrame::Ping { nonce } => {
155                buf.put_u8(ControlType::Ping as u8);
156                buf.put_u64(*nonce);
157            }
158            ControlFrame::Pong { nonce } => {
159                buf.put_u8(ControlType::Pong as u8);
160                buf.put_u64(*nonce);
161            }
162            ControlFrame::PeerInfo {
163                x25519_pub,
164                friendly_name,
165            } => {
166                buf.put_u8(ControlType::PeerInfo as u8);
167                buf.put_slice(x25519_pub);
168                let name_bytes = friendly_name.as_bytes();
169                let name_len = name_bytes.len().min(255) as u8;
170                buf.put_u8(name_len);
171                buf.put_slice(&name_bytes[..name_len as usize]);
172            }
173            ControlFrame::Message {
174                message_id,
175                timestamp_ms,
176                ciphertext,
177            } => {
178                buf.put_u8(ControlType::Message as u8);
179                buf.put_slice(message_id);
180                buf.put_u64(*timestamp_ms);
181                let ciphertext_len = ciphertext.len().min(u16::MAX as usize) as u16;
182                buf.put_u16(ciphertext_len);
183                buf.put_slice(&ciphertext[..ciphertext_len as usize]);
184            }
185            ControlFrame::MessageAck {
186                message_id,
187                ack_kind,
188            } => {
189                buf.put_u8(ControlType::MessageAck as u8);
190                buf.put_slice(message_id);
191                buf.put_u8(*ack_kind);
192            }
193        }
194    }
195
196    fn decode(buf: &mut BytesMut) -> Result<Self, PimError> {
197        if buf.is_empty() {
198            return Err(PimError::Protocol("control frame empty".into()));
199        }
200
201        let control_type = ControlType::from_u8(buf[0])?;
202
203        match control_type {
204            ControlType::IpRequest => {
205                if buf.len() < 17 {
206                    return Err(PimError::Protocol("IpRequest too short".into()));
207                }
208                let mut id = [0u8; 16];
209                id.copy_from_slice(&buf[1..17]);
210                buf.advance(17);
211                Ok(ControlFrame::IpRequest {
212                    requester_id: NodeId::from_bytes(id),
213                })
214            }
215            ControlType::IpAssign => {
216                if buf.len() < 14 {
217                    // 1 + 4 + 1 + 4 + 4
218                    return Err(PimError::Protocol("IpAssign too short".into()));
219                }
220                let mut assigned_ip = [0u8; 4];
221                assigned_ip.copy_from_slice(&buf[1..5]);
222                let subnet_mask = buf[5];
223                let mut gateway_ip = [0u8; 4];
224                gateway_ip.copy_from_slice(&buf[6..10]);
225                let lease_seconds = (&buf[10..14]).get_u32();
226                buf.advance(14);
227                Ok(ControlFrame::IpAssign {
228                    assigned_ip,
229                    subnet_mask,
230                    gateway_ip,
231                    lease_seconds,
232                })
233            }
234            ControlType::Goodbye => {
235                if buf.len() < 18 {
236                    // 1 + 16 + 1
237                    return Err(PimError::Protocol("Goodbye too short".into()));
238                }
239                let mut id = [0u8; 16];
240                id.copy_from_slice(&buf[1..17]);
241                let reason = buf[17];
242                buf.advance(18);
243                Ok(ControlFrame::Goodbye {
244                    departing_id: NodeId::from_bytes(id),
245                    reason,
246                })
247            }
248            ControlType::Rekey => {
249                buf.advance(1);
250                Ok(ControlFrame::Rekey)
251            }
252            ControlType::Ping => {
253                if buf.len() < 9 {
254                    return Err(PimError::Protocol("Ping too short".into()));
255                }
256                let nonce = (&buf[1..9]).get_u64();
257                buf.advance(9);
258                Ok(ControlFrame::Ping { nonce })
259            }
260            ControlType::Pong => {
261                if buf.len() < 9 {
262                    return Err(PimError::Protocol("Pong too short".into()));
263                }
264                let nonce = (&buf[1..9]).get_u64();
265                buf.advance(9);
266                Ok(ControlFrame::Pong { nonce })
267            }
268            ControlType::PeerInfo => {
269                // 1 (tag) + 32 (x25519) + 1 (name_len) + N (name)
270                if buf.len() < 34 {
271                    return Err(PimError::Protocol("PeerInfo too short".into()));
272                }
273                let mut x25519_pub = [0u8; 32];
274                x25519_pub.copy_from_slice(&buf[1..33]);
275                let name_len = buf[33] as usize;
276                let total = 34 + name_len;
277                if buf.len() < total {
278                    return Err(PimError::Protocol(format!(
279                        "PeerInfo truncated: need {total}, have {}",
280                        buf.len()
281                    )));
282                }
283                let friendly_name = match std::str::from_utf8(&buf[34..total]) {
284                    Ok(s) => s.to_owned(),
285                    Err(_) => {
286                        return Err(PimError::Protocol(
287                            "PeerInfo friendly_name not valid UTF-8".into(),
288                        ))
289                    }
290                };
291                buf.advance(total);
292                Ok(ControlFrame::PeerInfo {
293                    x25519_pub,
294                    friendly_name,
295                })
296            }
297            ControlType::Message => {
298                // 1 (tag) + 16 (id) + 8 (ts) + 2 (len) + N (ciphertext)
299                if buf.len() < 27 {
300                    return Err(PimError::Protocol("Message too short".into()));
301                }
302                let mut message_id = [0u8; 16];
303                message_id.copy_from_slice(&buf[1..17]);
304                let timestamp_ms = (&buf[17..25]).get_u64();
305                let ciphertext_len = (&buf[25..27]).get_u16() as usize;
306                let total = 27 + ciphertext_len;
307                if buf.len() < total {
308                    return Err(PimError::Protocol(format!(
309                        "Message truncated: need {total}, have {}",
310                        buf.len()
311                    )));
312                }
313                let ciphertext = buf[27..total].to_vec();
314                buf.advance(total);
315                Ok(ControlFrame::Message {
316                    message_id,
317                    timestamp_ms,
318                    ciphertext,
319                })
320            }
321            ControlType::MessageAck => {
322                // 1 (tag) + 16 (id) + 1 (ack_kind)
323                if buf.len() < 18 {
324                    return Err(PimError::Protocol("MessageAck too short".into()));
325                }
326                let mut message_id = [0u8; 16];
327                message_id.copy_from_slice(&buf[1..17]);
328                let ack_kind = buf[17];
329                buf.advance(18);
330                Ok(ControlFrame::MessageAck {
331                    message_id,
332                    ack_kind,
333                })
334            }
335        }
336    }
337}
338
339#[cfg(test)]
340mod tests;