Skip to main content

netget/server/openvpn/
packet.rs

1//! OpenVPN wire format: control-channel and data-channel framing.
2//!
3//! # Layout
4//!
5//! Every OpenVPN packet starts with one byte carrying the opcode in the upper
6//! five bits and the key id in the lower three.
7//!
8//! Control and ACK packets (`P_CONTROL_*`, `P_ACK_V1`) are laid out as:
9//!
10//! ```text
11//! u8       opcode << 3 | key_id
12//! u64      session id of the *sender*
13//! u8       number of acknowledged packet ids (may be 0)
14//! u32 * n  acknowledged packet ids
15//! u64      session id of the *peer*   -- present only when n > 0
16//! u32      message packet id          -- absent for P_ACK_V1
17//! ...      payload (TLS ciphertext for P_CONTROL_V1)
18//! ```
19//!
20//! Two details in that layout are easy to get wrong and both break
21//! interoperability with a real client:
22//!
23//! 1. The **message packet id comes after the ACK array and the peer session
24//!    id**, not before them.
25//! 2. The **peer session id is present only when the ACK array is non-empty**.
26//!    It is not an optional trailer to be sniffed for by length.
27//!
28//! Data packets carry no reliability fields:
29//!
30//! ```text
31//! P_DATA_V1: u8 opcode/key_id | ciphertext
32//! P_DATA_V2: u8 opcode/key_id | u24 peer id | ciphertext
33//! ```
34//!
35//! # Verification
36//!
37//! The layout above is not inferred from this codebase. It was read off the
38//! wire from OpenVPN 2.7.4 (`aarch64-apple-darwin`, OpenSSL 3.6.2); the captured
39//! frames are pinned as literal byte vectors in
40//! `tests/server/openvpn/codec_test.rs`, and the frames this module emits were
41//! confirmed to be accepted by that client (it logged `TLS: Initial packet
42//! from ...` for our reset reply and `P_ACK_V1 kid=0 [ 1 ] DATA len=0` for our
43//! ACK, then stopped retransmitting).
44//!
45//! # Robustness
46//!
47//! Every field is length-checked before it is read. Parsing is fed straight
48//! from a UDP socket, so it is fully attacker-controlled: it must return `Err`
49//! rather than panic on any input, including empty, truncated and
50//! deliberately-inconsistent frames.
51
52use anyhow::{bail, Result};
53use bytes::{BufMut, BytesMut};
54
55/// Largest UDP payload we will look at.
56pub const MAX_PACKET_SIZE: usize = 65535;
57
58/// OpenVPN opcodes, as carried in the upper five bits of the first byte.
59#[repr(u8)]
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum Opcode {
62    ControlHardResetClientV1 = 1,
63    ControlHardResetServerV1 = 2,
64    ControlSoftResetV1 = 3,
65    ControlV1 = 4,
66    AckV1 = 5,
67    DataV1 = 6,
68    ControlHardResetClientV2 = 7,
69    ControlHardResetServerV2 = 8,
70    DataV2 = 9,
71    /// tls-crypt-v2 client reset. Everything after the session id is encrypted,
72    /// so it must never be parsed with the plaintext layout above.
73    ControlHardResetClientV3 = 10,
74    /// tls-crypt-v2 wrapped client key. Same caveat as V3.
75    ControlWkcV1 = 11,
76}
77
78impl Opcode {
79    pub fn from_u8(v: u8) -> Option<Self> {
80        match v {
81            1 => Some(Opcode::ControlHardResetClientV1),
82            2 => Some(Opcode::ControlHardResetServerV1),
83            3 => Some(Opcode::ControlSoftResetV1),
84            4 => Some(Opcode::ControlV1),
85            5 => Some(Opcode::AckV1),
86            6 => Some(Opcode::DataV1),
87            7 => Some(Opcode::ControlHardResetClientV2),
88            8 => Some(Opcode::ControlHardResetServerV2),
89            9 => Some(Opcode::DataV2),
90            10 => Some(Opcode::ControlHardResetClientV3),
91            11 => Some(Opcode::ControlWkcV1),
92            _ => None,
93        }
94    }
95
96    /// True for opcodes that use the control-channel reliability layout.
97    ///
98    /// `ControlHardResetClientV3` and `ControlWkcV1` are deliberately excluded:
99    /// they are tls-crypt-v2 frames whose reliability fields are encrypted, so
100    /// applying the plaintext layout to them yields garbage. See
101    /// [`Opcode::is_tls_crypt_v2`].
102    pub fn is_control(&self) -> bool {
103        matches!(
104            self,
105            Opcode::ControlHardResetClientV1
106                | Opcode::ControlHardResetServerV1
107                | Opcode::ControlHardResetClientV2
108                | Opcode::ControlHardResetServerV2
109                | Opcode::ControlSoftResetV1
110                | Opcode::ControlV1
111        )
112    }
113
114    pub fn is_data(&self) -> bool {
115        matches!(self, Opcode::DataV1 | Opcode::DataV2)
116    }
117
118    pub fn is_ack(&self) -> bool {
119        matches!(self, Opcode::AckV1)
120    }
121
122    /// True for the tls-crypt-v2 opcodes this server cannot decode.
123    pub fn is_tls_crypt_v2(&self) -> bool {
124        matches!(
125            self,
126            Opcode::ControlHardResetClientV3 | Opcode::ControlWkcV1
127        )
128    }
129
130    /// True for a client-initiated session reset.
131    pub fn is_client_reset(&self) -> bool {
132        matches!(
133            self,
134            Opcode::ControlHardResetClientV1 | Opcode::ControlHardResetClientV2
135        )
136    }
137}
138
139/// Split the leading byte into `(opcode, key_id)` without consuming the frame.
140pub fn parse_opcode_byte(data: &[u8]) -> Result<(Opcode, u8)> {
141    let first = *data
142        .first()
143        .ok_or_else(|| anyhow::anyhow!("Empty packet"))?;
144    let raw = (first >> 3) & 0x1F;
145    let opcode =
146        Opcode::from_u8(raw).ok_or_else(|| anyhow::anyhow!("Unknown OpenVPN opcode: {}", raw))?;
147    Ok((opcode, first & 0x07))
148}
149
150/// Little cursor that refuses to read past the end of the buffer.
151struct Reader<'a> {
152    buf: &'a [u8],
153    pos: usize,
154}
155
156impl<'a> Reader<'a> {
157    fn new(buf: &'a [u8]) -> Self {
158        Reader { buf, pos: 0 }
159    }
160
161    fn take(&mut self, n: usize, what: &str) -> Result<&'a [u8]> {
162        let end = self
163            .pos
164            .checked_add(n)
165            .ok_or_else(|| anyhow::anyhow!("Length overflow reading {}", what))?;
166        if end > self.buf.len() {
167            bail!(
168                "Packet too short for {}: need {} more bytes at offset {}, have {}",
169                what,
170                n,
171                self.pos,
172                self.buf.len().saturating_sub(self.pos)
173            );
174        }
175        let out = &self.buf[self.pos..end];
176        self.pos = end;
177        Ok(out)
178    }
179
180    fn u8(&mut self, what: &str) -> Result<u8> {
181        Ok(self.take(1, what)?[0])
182    }
183
184    fn u32(&mut self, what: &str) -> Result<u32> {
185        let b = self.take(4, what)?;
186        Ok(u32::from_be_bytes([b[0], b[1], b[2], b[3]]))
187    }
188
189    fn u64(&mut self, what: &str) -> Result<u64> {
190        let b = self.take(8, what)?;
191        Ok(u64::from_be_bytes([
192            b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7],
193        ]))
194    }
195
196    fn rest(&mut self) -> &'a [u8] {
197        let out = &self.buf[self.pos..];
198        self.pos = self.buf.len();
199        out
200    }
201}
202
203/// A control-channel or ACK frame.
204#[derive(Debug, Clone, PartialEq, Eq)]
205pub struct ControlFrame {
206    pub opcode: Opcode,
207    pub key_id: u8,
208    /// Session id of whoever sent this frame.
209    pub session_id: u64,
210    /// Packet ids this frame acknowledges. May be empty.
211    pub ack_packet_ids: Vec<u32>,
212    /// Session id of the receiving side. Present on the wire only when
213    /// `ack_packet_ids` is non-empty; serialization enforces the same rule.
214    pub remote_session_id: Option<u64>,
215    /// This frame's own packet id. `None` for `P_ACK_V1`, which carries none.
216    pub packet_id: Option<u32>,
217    /// TLS ciphertext for `P_CONTROL_V1`; empty for a plain reset or ACK.
218    pub payload: Vec<u8>,
219}
220
221impl ControlFrame {
222    /// Build a `P_CONTROL_HARD_RESET_SERVER_V2` answering a client reset.
223    pub fn hard_reset_server_v2(
224        key_id: u8,
225        server_session_id: u64,
226        client_session_id: u64,
227        client_packet_id: u32,
228        our_packet_id: u32,
229    ) -> Self {
230        ControlFrame {
231            opcode: Opcode::ControlHardResetServerV2,
232            key_id,
233            session_id: server_session_id,
234            ack_packet_ids: vec![client_packet_id],
235            remote_session_id: Some(client_session_id),
236            packet_id: Some(our_packet_id),
237            payload: Vec::new(),
238        }
239    }
240
241    /// Build a standalone `P_ACK_V1` acknowledging one or more packet ids.
242    pub fn ack(
243        key_id: u8,
244        server_session_id: u64,
245        client_session_id: u64,
246        ack_packet_ids: Vec<u32>,
247    ) -> Self {
248        ControlFrame {
249            opcode: Opcode::AckV1,
250            key_id,
251            session_id: server_session_id,
252            ack_packet_ids,
253            remote_session_id: Some(client_session_id),
254            packet_id: None,
255            payload: Vec::new(),
256        }
257    }
258
259    /// Parse a control or ACK frame.
260    ///
261    /// Returns `Err` — never panics — for empty, truncated, unknown-opcode,
262    /// wrong-category and tls-crypt-v2 frames.
263    pub fn parse(data: &[u8]) -> Result<Self> {
264        let (opcode, key_id) = parse_opcode_byte(data)?;
265
266        if opcode.is_tls_crypt_v2() {
267            bail!(
268                "{:?} is a tls-crypt-v2 frame: its reliability fields are encrypted and \
269                 this server has no tls-crypt-v2 key, so it cannot be parsed",
270                opcode
271            );
272        }
273        if !opcode.is_control() && !opcode.is_ack() {
274            bail!("Not a control or ACK packet: {:?}", opcode);
275        }
276
277        let mut r = Reader::new(data);
278        r.take(1, "opcode byte")?;
279
280        let session_id = r.u64("session id")?;
281
282        let ack_count = r.u8("ACK array length")? as usize;
283        let mut ack_packet_ids = Vec::with_capacity(ack_count.min(64));
284        for i in 0..ack_count {
285            ack_packet_ids.push(r.u32(&format!("ACK id #{}", i))?);
286        }
287
288        // Present only when at least one packet id is acknowledged.
289        let remote_session_id = if ack_count > 0 {
290            Some(r.u64("remote session id")?)
291        } else {
292            None
293        };
294
295        // P_ACK_V1 is pure acknowledgement and carries no packet id of its own.
296        let packet_id = if opcode.is_ack() {
297            None
298        } else {
299            Some(r.u32("message packet id")?)
300        };
301
302        Ok(ControlFrame {
303            opcode,
304            key_id,
305            session_id,
306            ack_packet_ids,
307            remote_session_id,
308            packet_id,
309            payload: r.rest().to_vec(),
310        })
311    }
312
313    /// Serialize this frame.
314    ///
315    /// The peer session id is written only when the ACK array is non-empty, and
316    /// the message packet id only for non-ACK opcodes, so a round trip through
317    /// [`ControlFrame::parse`] is lossless for any frame that is valid on the
318    /// wire.
319    pub fn serialize(&self) -> BytesMut {
320        let mut buf = BytesMut::with_capacity(32 + self.payload.len());
321
322        buf.put_u8(((self.opcode as u8) << 3) | (self.key_id & 0x07));
323        buf.put_u64(self.session_id);
324
325        // The count is a u8 on the wire; truncate rather than emit a length that
326        // disagrees with the array that follows.
327        let acks = &self.ack_packet_ids[..self.ack_packet_ids.len().min(u8::MAX as usize)];
328        buf.put_u8(acks.len() as u8);
329        for id in acks {
330            buf.put_u32(*id);
331        }
332
333        if !acks.is_empty() {
334            buf.put_u64(self.remote_session_id.unwrap_or(0));
335        }
336
337        if !self.opcode.is_ack() {
338            buf.put_u32(self.packet_id.unwrap_or(0));
339        }
340
341        buf.put_slice(&self.payload);
342        buf
343    }
344
345    /// True when this looks like an unprotected reset, i.e. no `--tls-auth` or
346    /// `--tls-crypt` wrapper.
347    ///
348    /// A plain `P_CONTROL_HARD_RESET_CLIENT_V*` carries no payload. With
349    /// `--tls-auth` an HMAC, a replay packet id and a timestamp sit between the
350    /// session id and the ACK array; the fields we read are then not the fields
351    /// the client wrote, and the leftover bytes show up here as payload. Callers
352    /// use this to refuse a frame they would otherwise silently mis-parse.
353    pub fn is_plain_reset(&self) -> bool {
354        self.opcode.is_client_reset() && self.payload.is_empty() && self.ack_packet_ids.is_empty()
355    }
356}
357
358/// A data-channel frame. The payload is opaque ciphertext.
359#[derive(Debug, Clone, PartialEq, Eq)]
360pub struct DataFrame {
361    pub opcode: Opcode,
362    pub key_id: u8,
363    /// 24-bit peer id, present on `P_DATA_V2` only.
364    pub peer_id: Option<u32>,
365    pub payload: Vec<u8>,
366}
367
368impl DataFrame {
369    pub fn parse(data: &[u8]) -> Result<Self> {
370        let (opcode, key_id) = parse_opcode_byte(data)?;
371        if !opcode.is_data() {
372            bail!("Not a data packet: {:?}", opcode);
373        }
374
375        let mut r = Reader::new(data);
376        r.take(1, "opcode byte")?;
377
378        let peer_id = if opcode == Opcode::DataV2 {
379            let b = r.take(3, "peer id")?;
380            Some(u32::from_be_bytes([0, b[0], b[1], b[2]]))
381        } else {
382            None
383        };
384
385        Ok(DataFrame {
386            opcode,
387            key_id,
388            peer_id,
389            payload: r.rest().to_vec(),
390        })
391    }
392
393    pub fn serialize(&self) -> BytesMut {
394        let mut buf = BytesMut::with_capacity(4 + self.payload.len());
395        buf.put_u8(((self.opcode as u8) << 3) | (self.key_id & 0x07));
396        if self.opcode == Opcode::DataV2 {
397            let id = self.peer_id.unwrap_or(0);
398            buf.put_slice(&[(id >> 16) as u8, (id >> 8) as u8, id as u8]);
399        }
400        buf.put_slice(&self.payload);
401        buf
402    }
403}