Skip to main content

pim_protocol/
control_frame.rs

1//! Control-plane messages carried inside transport frames.
2
3use bytes::{Buf, BufMut, Bytes, 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    /// Generic plugin-defined payload — see [`ControlFrame::PluginPayload`].
26    PluginPayload = 0x08,
27}
28
29impl ControlType {
30    /// Decode a raw control-type tag from the wire.
31    pub fn from_u8(v: u8) -> Result<Self, PimError> {
32        match v {
33            0x01 => Ok(Self::IpRequest),
34            0x02 => Ok(Self::IpAssign),
35            0x03 => Ok(Self::Goodbye),
36            0x04 => Ok(Self::Rekey),
37            0x05 => Ok(Self::Ping),
38            0x06 => Ok(Self::Pong),
39            0x07 => Ok(Self::PeerInfo),
40            0x08 => Ok(Self::PluginPayload),
41            other => Err(PimError::Protocol(format!(
42                "unknown control type: 0x{other:02x}"
43            ))),
44        }
45    }
46}
47
48/// Multiplexed control message.
49///
50/// Layout: control_type(1) + body (variable, depends on type).
51///
52/// Mesh-essential variants (IP lease, routing, liveness, identity)
53/// live here directly. Optional features such as user messaging are
54/// carried inside [`ControlFrame::PluginPayload`] so the daemon can
55/// be built without those plugins compiled in.
56#[derive(Debug, Clone, PartialEq, Eq)]
57pub enum ControlFrame {
58    /// Request an address lease from a gateway.
59    IpRequest {
60        /// Node requesting a mesh IP allocation.
61        requester_id: NodeId,
62    },
63    /// Lease configuration returned by a gateway.
64    IpAssign {
65        /// Assigned mesh IPv4 address.
66        assigned_ip: [u8; 4],
67        /// CIDR prefix length for the assigned subnet.
68        subnet_mask: u8,
69        /// Mesh IPv4 address of the serving gateway.
70        gateway_ip: [u8; 4],
71        /// Lease duration in seconds.
72        lease_seconds: u32,
73    },
74    /// Graceful disconnect notification.
75    Goodbye {
76        /// Node that is departing.
77        departing_id: NodeId,
78        /// Implementation-defined reason code.
79        reason: u8,
80    },
81    /// Request that the session be rekeyed.
82    Rekey,
83    /// Ping carrying an opaque nonce.
84    Ping {
85        /// Opaque value echoed by the corresponding pong.
86        nonce: u64,
87    },
88    /// Pong echoing a ping nonce.
89    Pong {
90        /// Opaque value copied from the ping request.
91        nonce: u64,
92    },
93    /// Identity metadata exchanged once after the session is established
94    /// so that peers can address each other by `NodeId` and end-to-end
95    /// encrypt to the recipient's static X25519 key.
96    PeerInfo {
97        /// X25519 public key derived from the sender's Ed25519 seed.
98        x25519_pub: [u8; 32],
99        /// Sender's friendly node name as configured locally (UTF-8,
100        /// length-prefixed by `u8` — capped at 255 bytes by the codec).
101        friendly_name: String,
102    },
103    /// Generic plugin-defined payload.
104    ///
105    /// `kind` is an ASCII identifier (≤ 255 bytes) registered by a
106    /// [`pim-plugin`](https://crates.io/crates/pim-plugin)-style
107    /// plugin (e.g. `"messaging.msg"`). `body` is plugin-private and
108    /// opaque to the daemon — typically further encrypted/serialized
109    /// according to the plugin's own scheme.
110    ///
111    /// Wire layout:
112    /// ```text
113    /// 0x08
114    /// kind_len (u8, 1..=255)
115    /// kind     (kind_len bytes, ASCII)
116    /// body_len (u16 BE)
117    /// body     (body_len bytes)
118    /// ```
119    PluginPayload {
120        /// Plugin-namespaced kind identifier.
121        kind: String,
122        /// Opaque payload bytes — interpretation is up to the plugin
123        /// claiming `kind`.
124        body: Bytes,
125    },
126}
127
128impl FrameCodec for ControlFrame {
129    fn encode(&self, buf: &mut BytesMut) {
130        match self {
131            ControlFrame::IpRequest { requester_id } => {
132                buf.put_u8(ControlType::IpRequest as u8);
133                buf.put_slice(requester_id.as_bytes());
134            }
135            ControlFrame::IpAssign {
136                assigned_ip,
137                subnet_mask,
138                gateway_ip,
139                lease_seconds,
140            } => {
141                buf.put_u8(ControlType::IpAssign as u8);
142                buf.put_slice(assigned_ip);
143                buf.put_u8(*subnet_mask);
144                buf.put_slice(gateway_ip);
145                buf.put_u32(*lease_seconds);
146            }
147            ControlFrame::Goodbye {
148                departing_id,
149                reason,
150            } => {
151                buf.put_u8(ControlType::Goodbye as u8);
152                buf.put_slice(departing_id.as_bytes());
153                buf.put_u8(*reason);
154            }
155            ControlFrame::Rekey => {
156                buf.put_u8(ControlType::Rekey as u8);
157            }
158            ControlFrame::Ping { nonce } => {
159                buf.put_u8(ControlType::Ping as u8);
160                buf.put_u64(*nonce);
161            }
162            ControlFrame::Pong { nonce } => {
163                buf.put_u8(ControlType::Pong as u8);
164                buf.put_u64(*nonce);
165            }
166            ControlFrame::PeerInfo {
167                x25519_pub,
168                friendly_name,
169            } => {
170                buf.put_u8(ControlType::PeerInfo as u8);
171                buf.put_slice(x25519_pub);
172                let name_bytes = friendly_name.as_bytes();
173                let name_len = name_bytes.len().min(255) as u8;
174                buf.put_u8(name_len);
175                buf.put_slice(&name_bytes[..name_len as usize]);
176            }
177            ControlFrame::PluginPayload { kind, body } => {
178                buf.put_u8(ControlType::PluginPayload as u8);
179                let kind_bytes = kind.as_bytes();
180                let kind_len = kind_bytes.len().min(255) as u8;
181                buf.put_u8(kind_len);
182                buf.put_slice(&kind_bytes[..kind_len as usize]);
183                let body_len = body.len().min(u16::MAX as usize) as u16;
184                buf.put_u16(body_len);
185                buf.put_slice(&body[..body_len as usize]);
186            }
187        }
188    }
189
190    fn decode(buf: &mut BytesMut) -> Result<Self, PimError> {
191        if buf.is_empty() {
192            return Err(PimError::Protocol("control frame empty".into()));
193        }
194
195        let control_type = ControlType::from_u8(buf[0])?;
196
197        match control_type {
198            ControlType::IpRequest => {
199                if buf.len() < 17 {
200                    return Err(PimError::Protocol("IpRequest too short".into()));
201                }
202                let mut id = [0u8; 16];
203                id.copy_from_slice(&buf[1..17]);
204                buf.advance(17);
205                Ok(ControlFrame::IpRequest {
206                    requester_id: NodeId::from_bytes(id),
207                })
208            }
209            ControlType::IpAssign => {
210                if buf.len() < 14 {
211                    // 1 + 4 + 1 + 4 + 4
212                    return Err(PimError::Protocol("IpAssign too short".into()));
213                }
214                let mut assigned_ip = [0u8; 4];
215                assigned_ip.copy_from_slice(&buf[1..5]);
216                let subnet_mask = buf[5];
217                let mut gateway_ip = [0u8; 4];
218                gateway_ip.copy_from_slice(&buf[6..10]);
219                let lease_seconds = (&buf[10..14]).get_u32();
220                buf.advance(14);
221                Ok(ControlFrame::IpAssign {
222                    assigned_ip,
223                    subnet_mask,
224                    gateway_ip,
225                    lease_seconds,
226                })
227            }
228            ControlType::Goodbye => {
229                if buf.len() < 18 {
230                    // 1 + 16 + 1
231                    return Err(PimError::Protocol("Goodbye too short".into()));
232                }
233                let mut id = [0u8; 16];
234                id.copy_from_slice(&buf[1..17]);
235                let reason = buf[17];
236                buf.advance(18);
237                Ok(ControlFrame::Goodbye {
238                    departing_id: NodeId::from_bytes(id),
239                    reason,
240                })
241            }
242            ControlType::Rekey => {
243                buf.advance(1);
244                Ok(ControlFrame::Rekey)
245            }
246            ControlType::Ping => {
247                if buf.len() < 9 {
248                    return Err(PimError::Protocol("Ping too short".into()));
249                }
250                let nonce = (&buf[1..9]).get_u64();
251                buf.advance(9);
252                Ok(ControlFrame::Ping { nonce })
253            }
254            ControlType::Pong => {
255                if buf.len() < 9 {
256                    return Err(PimError::Protocol("Pong too short".into()));
257                }
258                let nonce = (&buf[1..9]).get_u64();
259                buf.advance(9);
260                Ok(ControlFrame::Pong { nonce })
261            }
262            ControlType::PeerInfo => {
263                // 1 (tag) + 32 (x25519) + 1 (name_len) + N (name)
264                if buf.len() < 34 {
265                    return Err(PimError::Protocol("PeerInfo too short".into()));
266                }
267                let mut x25519_pub = [0u8; 32];
268                x25519_pub.copy_from_slice(&buf[1..33]);
269                let name_len = buf[33] as usize;
270                let total = 34 + name_len;
271                if buf.len() < total {
272                    return Err(PimError::Protocol(format!(
273                        "PeerInfo truncated: need {total}, have {}",
274                        buf.len()
275                    )));
276                }
277                let friendly_name = match std::str::from_utf8(&buf[34..total]) {
278                    Ok(s) => s.to_owned(),
279                    Err(_) => {
280                        return Err(PimError::Protocol(
281                            "PeerInfo friendly_name not valid UTF-8".into(),
282                        ))
283                    }
284                };
285                buf.advance(total);
286                Ok(ControlFrame::PeerInfo {
287                    x25519_pub,
288                    friendly_name,
289                })
290            }
291            ControlType::PluginPayload => {
292                // 1 (tag) + 1 (kind_len) + N (kind) + 2 (body_len) + M (body)
293                if buf.len() < 4 {
294                    return Err(PimError::Protocol("PluginPayload too short".into()));
295                }
296                let kind_len = buf[1] as usize;
297                let body_len_off = 2 + kind_len;
298                if buf.len() < body_len_off + 2 {
299                    return Err(PimError::Protocol(format!(
300                        "PluginPayload header truncated: need {}, have {}",
301                        body_len_off + 2,
302                        buf.len()
303                    )));
304                }
305                let kind = match std::str::from_utf8(&buf[2..body_len_off]) {
306                    Ok(s) => s.to_owned(),
307                    Err(_) => {
308                        return Err(PimError::Protocol(
309                            "PluginPayload kind not valid UTF-8".into(),
310                        ))
311                    }
312                };
313                let body_len = (&buf[body_len_off..body_len_off + 2]).get_u16() as usize;
314                let total = body_len_off + 2 + body_len;
315                if buf.len() < total {
316                    return Err(PimError::Protocol(format!(
317                        "PluginPayload truncated: need {total}, have {}",
318                        buf.len()
319                    )));
320                }
321                let body = Bytes::copy_from_slice(&buf[body_len_off + 2..total]);
322                buf.advance(total);
323                Ok(ControlFrame::PluginPayload { kind, body })
324            }
325        }
326    }
327}
328
329#[cfg(test)]
330mod tests;