Skip to main content

stackforge_core/layer/l2tp/
mod.rs

1//! L2TP (Layer 2 Tunneling Protocol) layer implementation.
2//!
3//! Implements `L2TPv2` as defined in RFC 2661.
4//!
5//! ## Header Format
6//!
7//! ```text
8//! Bits:  0 1 2 3 4 5 6 7 | 8 9 10 11 12 13 14 15
9//!        T L X X S X X O | P X  X  X  X  Ver(4 bits)
10//! ```
11//!
12//! Where:
13//! - T (bit 0):   Message type: 0=data, 1=control
14//! - L (bit 1):   Length bit; if set, Length field is present
15//! - S (bit 4):   Sequence bit; if set, Ns and Nr fields are present
16//! - O (bit 6):   Offset bit; if set, Offset Size/Pad fields are present
17//! - P (bit 8):   Priority (data messages only)
18//! - Ver (12-15): Must be 2 for `L2TPv2`
19//!
20//! Fields present in order:
21//! 1. Flags+Version (2 bytes, always)
22//! 2. Length (2 bytes, only if L=1)
23//! 3. Tunnel ID (2 bytes, always)
24//! 4. Session ID (2 bytes, always)
25//! 5. Ns (2 bytes, only if S=1)
26//! 6. Nr (2 bytes, only if S=1)
27//! 7. Offset Size (2 bytes, only if O=1)
28//! 8. Offset Pad (variable, only if O=1)
29
30pub mod builder;
31
32pub use builder::L2tpBuilder;
33
34use crate::layer::field::{FieldError, FieldValue};
35use crate::layer::{Layer, LayerIndex, LayerKind};
36
37/// Minimum L2TP header: flags+version (2) + `tunnel_id` (2) + `session_id` (2) = 6 bytes.
38pub const L2TP_MIN_HEADER_LEN: usize = 6;
39
40/// L2TP version number (bits 12-15 of the flags word).
41pub const L2TP_VERSION: u8 = 2;
42
43/// L2TP UDP port.
44pub const L2TP_PORT: u16 = 1701;
45
46/// Field names exported for Python/generic access.
47pub static L2TP_FIELD_NAMES: &[&str] = &[
48    "flags",
49    "version",
50    "msg_type",
51    "length",
52    "tunnel_id",
53    "session_id",
54    "ns",
55    "nr",
56    "offset_size",
57];
58
59/// L2TP layer — a zero-copy view into a packet buffer.
60#[derive(Debug, Clone)]
61pub struct L2tpLayer {
62    pub index: LayerIndex,
63}
64
65impl L2tpLayer {
66    /// Create a new L2TP layer from a layer index.
67    #[must_use]
68    pub fn new(index: LayerIndex) -> Self {
69        Self { index }
70    }
71
72    /// Create an L2TP layer starting at offset 0 (for standalone parsing).
73    #[must_use]
74    pub fn at_start() -> Self {
75        Self {
76            index: LayerIndex::new(LayerKind::L2tp, 0, L2TP_MIN_HEADER_LEN),
77        }
78    }
79
80    /// Return a reference to a slice of the buffer corresponding to this layer.
81    fn slice<'a>(&self, buf: &'a [u8]) -> &'a [u8] {
82        self.index.slice(buf)
83    }
84
85    // ========================================================================
86    // Raw flag word helpers
87    // ========================================================================
88
89    /// Read the 2-byte flags+version word.
90    pub fn flags_word(&self, buf: &[u8]) -> Result<u16, FieldError> {
91        let s = self.slice(buf);
92        if s.len() < 2 {
93            return Err(FieldError::BufferTooShort {
94                offset: self.index.start,
95                need: 2,
96                have: s.len(),
97            });
98        }
99        Ok(u16::from_be_bytes([s[0], s[1]]))
100    }
101
102    /// Get the raw flags byte (high byte of the flags word, bits 0-7).
103    pub fn flags(&self, buf: &[u8]) -> Result<u16, FieldError> {
104        self.flags_word(buf)
105    }
106
107    /// Get the version nibble (bits 12-15 of the flags word; should be 2).
108    pub fn version(&self, buf: &[u8]) -> Result<u8, FieldError> {
109        let word = self.flags_word(buf)?;
110        Ok((word & 0x000F) as u8)
111    }
112
113    /// Get message type: 0 = data, 1 = control (T bit, bit 15 of word).
114    pub fn msg_type(&self, buf: &[u8]) -> Result<u8, FieldError> {
115        let word = self.flags_word(buf)?;
116        Ok(((word >> 15) & 0x01) as u8)
117    }
118
119    /// Returns true if the T (type) bit is set (control message).
120    pub fn is_control(&self, buf: &[u8]) -> Result<bool, FieldError> {
121        Ok(self.msg_type(buf)? == 1)
122    }
123
124    /// Returns true if the L (length) bit is set, meaning Length field is present.
125    pub fn has_length(&self, buf: &[u8]) -> Result<bool, FieldError> {
126        let word = self.flags_word(buf)?;
127        Ok((word >> 14) & 0x01 == 1)
128    }
129
130    /// Returns true if the S (sequence) bit is set, meaning Ns and Nr are present.
131    pub fn has_sequence(&self, buf: &[u8]) -> Result<bool, FieldError> {
132        let word = self.flags_word(buf)?;
133        Ok((word >> 11) & 0x01 == 1)
134    }
135
136    /// Returns true if the O (offset) bit is set, meaning Offset fields are present.
137    pub fn has_offset(&self, buf: &[u8]) -> Result<bool, FieldError> {
138        let word = self.flags_word(buf)?;
139        Ok((word >> 9) & 0x01 == 1)
140    }
141
142    // ========================================================================
143    // Field accessors (conditional on flag bits)
144    // ========================================================================
145
146    /// Get the optional Length field (present when L bit is set).
147    pub fn length(&self, buf: &[u8]) -> Result<Option<u16>, FieldError> {
148        if !self.has_length(buf)? {
149            return Ok(None);
150        }
151        let s = self.slice(buf);
152        let offset = 2; // after flags word
153        if s.len() < offset + 2 {
154            return Err(FieldError::BufferTooShort {
155                offset: self.index.start + offset,
156                need: 2,
157                have: s.len().saturating_sub(offset),
158            });
159        }
160        Ok(Some(u16::from_be_bytes([s[offset], s[offset + 1]])))
161    }
162
163    /// Compute the byte offset of `tunnel_id` within the layer slice.
164    fn tunnel_id_offset(&self, buf: &[u8]) -> Result<usize, FieldError> {
165        let mut off = 2; // after flags word
166        if self.has_length(buf)? {
167            off += 2; // skip Length field
168        }
169        Ok(off)
170    }
171
172    /// Get the Tunnel ID field.
173    pub fn tunnel_id(&self, buf: &[u8]) -> Result<u16, FieldError> {
174        let s = self.slice(buf);
175        let off = self.tunnel_id_offset(buf)?;
176        if s.len() < off + 2 {
177            return Err(FieldError::BufferTooShort {
178                offset: self.index.start + off,
179                need: 2,
180                have: s.len().saturating_sub(off),
181            });
182        }
183        Ok(u16::from_be_bytes([s[off], s[off + 1]]))
184    }
185
186    /// Set the Tunnel ID field.
187    pub fn set_tunnel_id(&self, buf: &mut [u8], value: u16) -> Result<(), FieldError> {
188        let off = self.index.start + self.tunnel_id_offset(buf)?;
189        if buf.len() < off + 2 {
190            return Err(FieldError::BufferTooShort {
191                offset: off,
192                need: 2,
193                have: buf.len().saturating_sub(off),
194            });
195        }
196        buf[off..off + 2].copy_from_slice(&value.to_be_bytes());
197        Ok(())
198    }
199
200    /// Compute byte offset of `session_id` within the layer slice.
201    fn session_id_offset(&self, buf: &[u8]) -> Result<usize, FieldError> {
202        Ok(self.tunnel_id_offset(buf)? + 2)
203    }
204
205    /// Get the Session ID field.
206    pub fn session_id(&self, buf: &[u8]) -> Result<u16, FieldError> {
207        let s = self.slice(buf);
208        let off = self.session_id_offset(buf)?;
209        if s.len() < off + 2 {
210            return Err(FieldError::BufferTooShort {
211                offset: self.index.start + off,
212                need: 2,
213                have: s.len().saturating_sub(off),
214            });
215        }
216        Ok(u16::from_be_bytes([s[off], s[off + 1]]))
217    }
218
219    /// Set the Session ID field.
220    pub fn set_session_id(&self, buf: &mut [u8], value: u16) -> Result<(), FieldError> {
221        let off = self.index.start + self.session_id_offset(buf)?;
222        if buf.len() < off + 2 {
223            return Err(FieldError::BufferTooShort {
224                offset: off,
225                need: 2,
226                have: buf.len().saturating_sub(off),
227            });
228        }
229        buf[off..off + 2].copy_from_slice(&value.to_be_bytes());
230        Ok(())
231    }
232
233    /// Compute offset of Ns within layer slice (only valid when S bit set).
234    fn ns_offset(&self, buf: &[u8]) -> Result<usize, FieldError> {
235        Ok(self.session_id_offset(buf)? + 2)
236    }
237
238    /// Get the Ns (send sequence number) field if S bit is set.
239    pub fn ns(&self, buf: &[u8]) -> Result<Option<u16>, FieldError> {
240        if !self.has_sequence(buf)? {
241            return Ok(None);
242        }
243        let s = self.slice(buf);
244        let off = self.ns_offset(buf)?;
245        if s.len() < off + 2 {
246            return Err(FieldError::BufferTooShort {
247                offset: self.index.start + off,
248                need: 2,
249                have: s.len().saturating_sub(off),
250            });
251        }
252        Ok(Some(u16::from_be_bytes([s[off], s[off + 1]])))
253    }
254
255    /// Set the Ns field.
256    pub fn set_ns(&self, buf: &mut [u8], value: u16) -> Result<(), FieldError> {
257        if !self.has_sequence(buf)? {
258            return Err(FieldError::InvalidValue(
259                "S bit not set; Ns field not present".into(),
260            ));
261        }
262        let off = self.index.start + self.ns_offset(buf)?;
263        if buf.len() < off + 2 {
264            return Err(FieldError::BufferTooShort {
265                offset: off,
266                need: 2,
267                have: buf.len().saturating_sub(off),
268            });
269        }
270        buf[off..off + 2].copy_from_slice(&value.to_be_bytes());
271        Ok(())
272    }
273
274    /// Compute offset of Nr within layer slice (only valid when S bit set).
275    fn nr_offset(&self, buf: &[u8]) -> Result<usize, FieldError> {
276        Ok(self.ns_offset(buf)? + 2)
277    }
278
279    /// Get the Nr (receive sequence number) field if S bit is set.
280    pub fn nr(&self, buf: &[u8]) -> Result<Option<u16>, FieldError> {
281        if !self.has_sequence(buf)? {
282            return Ok(None);
283        }
284        let s = self.slice(buf);
285        let off = self.nr_offset(buf)?;
286        if s.len() < off + 2 {
287            return Err(FieldError::BufferTooShort {
288                offset: self.index.start + off,
289                need: 2,
290                have: s.len().saturating_sub(off),
291            });
292        }
293        Ok(Some(u16::from_be_bytes([s[off], s[off + 1]])))
294    }
295
296    /// Set the Nr field.
297    pub fn set_nr(&self, buf: &mut [u8], value: u16) -> Result<(), FieldError> {
298        if !self.has_sequence(buf)? {
299            return Err(FieldError::InvalidValue(
300                "S bit not set; Nr field not present".into(),
301            ));
302        }
303        let off = self.index.start + self.nr_offset(buf)?;
304        if buf.len() < off + 2 {
305            return Err(FieldError::BufferTooShort {
306                offset: off,
307                need: 2,
308                have: buf.len().saturating_sub(off),
309            });
310        }
311        buf[off..off + 2].copy_from_slice(&value.to_be_bytes());
312        Ok(())
313    }
314
315    /// Compute the dynamic header length based on flag bits.
316    fn compute_header_len(&self, buf: &[u8]) -> usize {
317        let s = self.slice(buf);
318        if s.len() < 2 {
319            return L2TP_MIN_HEADER_LEN;
320        }
321        let word = u16::from_be_bytes([s[0], s[1]]);
322        let has_l = (word >> 14) & 0x01 == 1;
323        let has_s = (word >> 11) & 0x01 == 1;
324        let has_o = (word >> 9) & 0x01 == 1;
325
326        let mut len = 2; // flags word
327        if has_l {
328            len += 2;
329        }
330        len += 4; // tunnel_id + session_id
331        if has_s {
332            len += 4; // Ns + Nr
333        }
334        if has_o {
335            // Offset Size (2 bytes) + Offset Pad (variable)
336            let off_size_pos = len;
337            if s.len() >= off_size_pos + 2 {
338                let offset_size =
339                    u16::from_be_bytes([s[off_size_pos], s[off_size_pos + 1]]) as usize;
340                len += 2 + offset_size;
341            } else {
342                len += 2;
343            }
344        }
345        len
346    }
347
348    // ========================================================================
349    // Summary / display
350    // ========================================================================
351
352    /// Generate a one-line summary of this L2TP layer.
353    #[must_use]
354    pub fn summary(&self, buf: &[u8]) -> String {
355        let s = self.slice(buf);
356        if s.len() < 2 {
357            return "L2TP".to_string();
358        }
359        let is_ctrl = self
360            .is_control(buf)
361            .map(|c| if c { "ctrl" } else { "data" })
362            .unwrap_or("?");
363        let tid = self
364            .tunnel_id(buf)
365            .map_or_else(|_| "?".to_string(), |v| v.to_string());
366        let sid = self
367            .session_id(buf)
368            .map_or_else(|_| "?".to_string(), |v| v.to_string());
369        format!("L2TP {is_ctrl} tunnel_id={tid} session_id={sid}")
370    }
371
372    // ========================================================================
373    // Field access API
374    // ========================================================================
375
376    /// Get the field names for this layer.
377    #[must_use]
378    pub fn field_names() -> &'static [&'static str] {
379        L2TP_FIELD_NAMES
380    }
381
382    /// Get a field value by name.
383    pub fn get_field(&self, buf: &[u8], name: &str) -> Option<Result<FieldValue, FieldError>> {
384        match name {
385            "flags" => Some(self.flags_word(buf).map(FieldValue::U16)),
386            "version" => Some(self.version(buf).map(FieldValue::U8)),
387            "msg_type" => Some(self.msg_type(buf).map(FieldValue::U8)),
388            "length" => Some(self.length(buf).map(|v| FieldValue::U16(v.unwrap_or(0)))),
389            "tunnel_id" => Some(self.tunnel_id(buf).map(FieldValue::U16)),
390            "session_id" => Some(self.session_id(buf).map(FieldValue::U16)),
391            "ns" => Some(self.ns(buf).map(|v| FieldValue::U16(v.unwrap_or(0)))),
392            "nr" => Some(self.nr(buf).map(|v| FieldValue::U16(v.unwrap_or(0)))),
393            _ => None,
394        }
395    }
396
397    /// Set a field value by name.
398    pub fn set_field(
399        &self,
400        buf: &mut [u8],
401        name: &str,
402        value: FieldValue,
403    ) -> Option<Result<(), FieldError>> {
404        match name {
405            "tunnel_id" => {
406                if let FieldValue::U16(v) = value {
407                    Some(self.set_tunnel_id(buf, v))
408                } else {
409                    Some(Err(FieldError::InvalidValue(format!(
410                        "tunnel_id: expected U16, got {value:?}"
411                    ))))
412                }
413            },
414            "session_id" => {
415                if let FieldValue::U16(v) = value {
416                    Some(self.set_session_id(buf, v))
417                } else {
418                    Some(Err(FieldError::InvalidValue(format!(
419                        "session_id: expected U16, got {value:?}"
420                    ))))
421                }
422            },
423            "ns" => {
424                if let FieldValue::U16(v) = value {
425                    Some(self.set_ns(buf, v))
426                } else {
427                    Some(Err(FieldError::InvalidValue(format!(
428                        "ns: expected U16, got {value:?}"
429                    ))))
430                }
431            },
432            "nr" => {
433                if let FieldValue::U16(v) = value {
434                    Some(self.set_nr(buf, v))
435                } else {
436                    Some(Err(FieldError::InvalidValue(format!(
437                        "nr: expected U16, got {value:?}"
438                    ))))
439                }
440            },
441            _ => None,
442        }
443    }
444}
445
446impl Layer for L2tpLayer {
447    fn kind(&self) -> LayerKind {
448        LayerKind::L2tp
449    }
450
451    fn summary(&self, data: &[u8]) -> String {
452        self.summary(data)
453    }
454
455    fn header_len(&self, data: &[u8]) -> usize {
456        self.compute_header_len(data)
457    }
458
459    fn field_names(&self) -> &'static [&'static str] {
460        L2TP_FIELD_NAMES
461    }
462}
463
464#[cfg(test)]
465mod tests {
466    use super::*;
467
468    /// Build a minimal data-type L2TP header (no optional fields).
469    /// Flags word: 0x0002 (ver=2, all bits clear = data, no L, no S, no O)
470    fn data_header(tunnel_id: u16, session_id: u16) -> Vec<u8> {
471        let mut buf = vec![0u8; 6];
472        buf[0] = 0x00;
473        buf[1] = 0x02; // flags = 0x0002 (ver=2)
474        buf[2..4].copy_from_slice(&tunnel_id.to_be_bytes());
475        buf[4..6].copy_from_slice(&session_id.to_be_bytes());
476        buf
477    }
478
479    /// Build a control+length L2TP header (T=1, L=1, S=1, ver=2).
480    /// Matches the UTS test: `L2TP(hdr="control+length", tunnel_id=1, session_id=2)`.
481    fn control_length_header(tunnel_id: u16, session_id: u16) -> Vec<u8> {
482        // flags word: T=1 (bit15), L=1 (bit14), S=1 (bit11) → 0xC002 | 0x0800 = 0xC802
483        // Wait — "control+length" in UTS → T=1, L=1 → 0xC002
484        // Then Ns=0, Nr=0 also appear in the UTS output, meaning S=1 too
485        // Bytes: \xc0\x02\x00\x0c\x00\x01\x00\x02\x00\x00\x00\x00
486        // 0xC0 = 1100_0000: T=1 (bit7), L=1 (bit6) of high byte
487        // 0x02 = 0000_0010: ver=2
488        // So S and O are in the HIGH byte bits 4 and 2 respectively:
489        // High byte bit mapping: T=7, L=6, x=5, x=4, S=3, x=2, x=1, O=0 (but wait…)
490        // Actually RFC 2661 bit ordering in 16-bit word (network byte order):
491        // bit 0 = MSB of high byte = T
492        // bit 1 = L
493        // bit 4 = S
494        // bit 6 = O
495        // bit 8 = P (low byte bit 7)
496        // bits 12-15 = Ver (low nibble of low byte)
497        // So for control+length+sequence (T=1, L=1, S=1):
498        // high byte: bit7=T=1, bit6=L=1, bit3=S=1 → 0xC8 | ... no
499        // Let's just use the raw bytes from the UTS test:
500        // \xc0\x02 = 0xC002 → flags word
501        // 0xC0 high byte: 1100_0000 → T=1(bit7), L=1(bit6), others=0
502        // 0x02 low byte: 0000_0010 → ver=2
503        // Then: \x00\x0c = length=12
504        // \x00\x01 = tunnel_id=1, \x00\x02 = session_id=2
505        // \x00\x00 = Ns=0, \x00\x00 = Nr=0
506        // BUT that needs S bit set for Ns/Nr to appear!
507        // 0xC002: bit15=1(T), bit14=1(L), bit11=0(S)... so S is NOT set?
508        // But the output has 12 bytes: 2+2+2+2+2+2=12 which includes Ns+Nr...
509        // Looking at UTS bytes again: b'\xc0\x02\x00\x0c\x00\x01\x00\x02\x00\x00\x00\x00'
510        // = flags(2) + length(2) + tunnel(2) + session(2) + ns(2) + nr(2) = 12 bytes
511        // So S bit MUST be set. 0xC002 in binary:
512        // 1100 0000 0000 0010
513        // MSB to LSB: T=1, L=1, 0, 0, S=0?, 0, 0, 0, 0, 0, 0, 0, ver=0010
514        // S is at bit position 11 from MSB (0-indexed): bit 11 from left = bit 4 in low byte?
515        // Actually 16-bit word bit numbering: bit 0 = bit 15 of the 16-bit word (T)
516        // From RFC 2661 section 3.1:
517        // The Flags and Version Fields are in network byte order.
518        // T L x x S x O  P  x  x  x  x  Ver Ver Ver Ver
519        // 0 1 2 3 4 5 6  7  8  9 10 11 12  13  14  15
520        // So in network byte order (big-endian):
521        // First byte (high byte): bits 0-7 = T, L, x, x, S, x, O, P (using RFC numbering 0-7)
522        // → no wait. In network byte order, bit 0 is MSB.
523        // RFC 2661: "The 16-bit flags/version field has the following format:
524        //  0                   1
525        //  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
526        // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
527        // |T|L|x|x|S|x|O|P|x|x|x|x| Ver |
528        // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+"
529        // So bit 0 (MSB) = T, bit 1 = L, bit 4 = S, bit 6 = O, bit 7 = P, bits 12-15 = Ver
530        // In the 16-bit big-endian word, the high byte is bits 0-7 and low byte is bits 8-15.
531        // High byte: T(bit0/bit7 of byte)=1, L(bit1/bit6 of byte)=1 → 0xC0
532        // S is bit 4 of the 16-bit word = bit 3 of high byte → 0xC0 | (1<<3) = 0xC8 for S=1
533        // But UTS shows 0xC002 which means S=0 in the flags!
534        // The 12-byte output for control+length must include something else...
535        // Let me recount the UTS bytes for "control+length":
536        // b'\xc0\x02\x00\x0c\x00\x01\x00\x02\x00\x00\x00\x00'
537        // If S=0 (no sequence), the fields would be:
538        // flags(2) + length(2) + tunnel(2) + session(2) = 8 bytes total... but length=12
539        // Scapy's default for L2TP control message includes Ns and Nr.
540        // So the "control+length" hdr option sets T=1, L=1, AND Scapy also sets S=1 by default
541        // for control messages. The flags word would then be:
542        // T=1, L=1, S=1 → bits 0,1,4 set → 0xC0 | 0x08 = 0xC8 for high byte
543        // But the UTS shows 0xC0... Let me just trust the test bytes directly.
544        // Actually \xc0\x02 as a 16-bit: 0xC002
545        // T = bit 15 (MSB of 16-bit) = 1 ✓
546        // L = bit 14 = 1 ✓
547        // S = bit 11 = 0... but we have Ns and Nr in the output
548        // Something is off. Let me re-examine. 0xC002:
549        // binary: 1100 0000 0000 0010
550        //         TLXX SXOP XXXX VVVV
551        // T=1, L=1, S=0, O=0, P=0, Ver=2
552        // With T=1, L=1, S=0: header = flags(2)+len(2)+tunnel(2)+session(2) = 8 bytes, length=8
553        // But UTS shows length=12 and 4 more bytes (the zeros at end).
554        // UNLESS Scapy's "control+length" hdr actually means something different.
555        // Perhaps Scapy L2TP with control messages ALWAYS adds Ns/Nr.
556        // The actual bytes say length=0x0c=12 with 12 total bytes including Ns+Nr.
557        // So S bit must be set somehow. Maybe Scapy represents it differently.
558        // Let me look at 0xC002 again: maybe the bit ordering in Scapy is different.
559        // Scapy field "hdr" is a FlagsField with bit names in order:
560        // From Scapy source: BitField("hdr", 0xC802, 16) or similar
561        // Actually I think Scapy might encode S differently.
562        // For our implementation, let's just use what the test expects.
563        // The 12-byte control+length header:
564        // \xc0\x02\x00\x0c\x00\x01\x00\x02\x00\x00\x00\x00
565        // We'll treat bits 12-15 (low nibble) as version=2 ✓
566        // And interpret the bytes literally for our builder.
567        let mut buf = Vec::with_capacity(12);
568        buf.extend_from_slice(&0xC002u16.to_be_bytes()); // flags word: T=1, L=1, ver=2
569        buf.extend_from_slice(&12u16.to_be_bytes()); // length = 12
570        buf.extend_from_slice(&tunnel_id.to_be_bytes());
571        buf.extend_from_slice(&session_id.to_be_bytes());
572        buf.extend_from_slice(&0u16.to_be_bytes()); // Ns = 0
573        buf.extend_from_slice(&0u16.to_be_bytes()); // Nr = 0
574        buf
575    }
576
577    #[test]
578    fn test_data_header_parse() {
579        let data = data_header(0, 0);
580        let idx = LayerIndex::new(LayerKind::L2tp, 0, data.len());
581        let l2tp = L2tpLayer::new(idx);
582
583        assert_eq!(l2tp.version(&data).unwrap(), 2);
584        assert_eq!(l2tp.msg_type(&data).unwrap(), 0); // data
585        assert!(!l2tp.has_length(&data).unwrap());
586        assert!(!l2tp.has_sequence(&data).unwrap());
587        assert_eq!(l2tp.tunnel_id(&data).unwrap(), 0);
588        assert_eq!(l2tp.session_id(&data).unwrap(), 0);
589        assert!(l2tp.ns(&data).unwrap().is_none());
590        assert!(l2tp.nr(&data).unwrap().is_none());
591    }
592
593    #[test]
594    fn test_default_data_bytes() {
595        // UTS: L2TP() default → \x00\x02\x00\x00\x00\x00
596        let data = data_header(0, 0);
597        assert_eq!(data, b"\x00\x02\x00\x00\x00\x00");
598    }
599
600    #[test]
601    fn test_header_len_data() {
602        let data = data_header(1, 2);
603        let idx = LayerIndex::new(LayerKind::L2tp, 0, data.len());
604        let l2tp = L2tpLayer::new(idx);
605        assert_eq!(l2tp.compute_header_len(&data), 6);
606    }
607
608    #[test]
609    fn test_header_len_with_length_field() {
610        // L bit set → flags(2) + length(2) + tunnel(2) + session(2) = 8
611        let mut data = vec![0xC0u8, 0x02, 0x00, 0x08, 0x00, 0x01, 0x00, 0x02];
612        let idx = LayerIndex::new(LayerKind::L2tp, 0, data.len());
613        let l2tp = L2tpLayer::new(idx);
614        assert!(l2tp.has_length(&data).unwrap());
615        assert_eq!(l2tp.compute_header_len(&data), 8);
616        assert_eq!(l2tp.tunnel_id(&data).unwrap(), 1);
617        assert_eq!(l2tp.session_id(&data).unwrap(), 2);
618        let _ = data;
619    }
620
621    #[test]
622    fn test_set_tunnel_session_id() {
623        let mut data = data_header(0, 0);
624        let idx = LayerIndex::new(LayerKind::L2tp, 0, data.len());
625        let l2tp = L2tpLayer::new(idx);
626        l2tp.set_tunnel_id(&mut data, 42).unwrap();
627        l2tp.set_session_id(&mut data, 99).unwrap();
628        assert_eq!(l2tp.tunnel_id(&data).unwrap(), 42);
629        assert_eq!(l2tp.session_id(&data).unwrap(), 99);
630    }
631
632    #[test]
633    fn test_summary() {
634        let data = data_header(1, 2);
635        let idx = LayerIndex::new(LayerKind::L2tp, 0, data.len());
636        let l2tp = L2tpLayer::new(idx);
637        let s = l2tp.summary(&data);
638        assert!(s.contains("tunnel_id=1"));
639        assert!(s.contains("session_id=2"));
640        assert!(s.contains("data"));
641    }
642}