Skip to main content

packet_dissector_ppp/
lib.rs

1//! PPP (Point-to-Point Protocol) frame dissector and sub-protocol parsers.
2//!
3//! Provides a [`PppDissector`] for PPP frame dissection (RFC 1661 Section 2)
4//! as well as sub-protocol parsers commonly carried inside 3GPP Protocol
5//! Configuration Options (PCO) and PPPoE sessions:
6//!
7//! - **IPCP** -- Internet Protocol Control Protocol ([RFC 1332], [RFC 1877])
8//! - **LCP** -- Link Control Protocol ([RFC 1661], [RFC 2153])
9//! - **PAP** -- Password Authentication Protocol ([RFC 1334])
10//! - **CHAP** -- Challenge Handshake Authentication Protocol ([RFC 1994])
11//!
12//! The sub-protocol parsers accept raw PPP packet bytes (starting with the Code
13//! field) and push fields directly into a [`DissectBuffer`].
14//!
15//! ## References
16//! - RFC 1661 (PPP): <https://www.rfc-editor.org/rfc/rfc1661>
17//! - RFC 2153 (PPP Vendor Extensions, updates RFC 1661): <https://www.rfc-editor.org/rfc/rfc2153>
18//! - RFC 1332 (IPCP): <https://www.rfc-editor.org/rfc/rfc1332>
19//! - RFC 1334 (PAP): <https://www.rfc-editor.org/rfc/rfc1334>
20//! - RFC 1877 (DNS/NBNS extensions for IPCP): <https://www.rfc-editor.org/rfc/rfc1877>
21//! - RFC 1994 (CHAP, obsoletes RFC 1334 CHAP): <https://www.rfc-editor.org/rfc/rfc1994>
22//!
23//! [RFC 1332]: https://www.rfc-editor.org/rfc/rfc1332
24//! [RFC 1334]: https://www.rfc-editor.org/rfc/rfc1334
25//! [RFC 1661]: https://www.rfc-editor.org/rfc/rfc1661
26//! [RFC 1877]: https://www.rfc-editor.org/rfc/rfc1877
27//! [RFC 1994]: https://www.rfc-editor.org/rfc/rfc1994
28//! [RFC 2153]: https://www.rfc-editor.org/rfc/rfc2153
29
30#![deny(missing_docs)]
31
32use packet_dissector_core::packet::DissectBuffer;
33
34macro_rules! ppp_header_descriptors {
35    ($dfn:expr) => {
36        &[
37            FieldDescriptor::new("code", "Code", FieldType::U8).with_display_fn($dfn),
38            FieldDescriptor::new("identifier", "Identifier", FieldType::U8),
39            FieldDescriptor::new("length", "Length", FieldType::U16),
40        ]
41    };
42}
43
44macro_rules! ppp_option_descriptors {
45    ($dfn:expr) => {
46        &[
47            FieldDescriptor::new("type", "Type", FieldType::U8).with_display_fn($dfn),
48            FieldDescriptor::new("length", "Length", FieldType::U8),
49            FieldDescriptor::new("value", "Value", FieldType::Bytes),
50        ]
51    };
52}
53
54pub mod chap;
55pub mod ipcp;
56pub mod lcp;
57pub mod pap;
58
59use packet_dissector_core::dissector::{DispatchHint, DissectResult, Dissector};
60use packet_dissector_core::error::PacketError;
61use packet_dissector_core::field::{FieldDescriptor, FieldType, FieldValue};
62use packet_dissector_core::util::read_be_u16;
63
64/// Minimum PPP packet header size (Code + Identifier + Length).
65///
66/// RFC 1661, Section 5 — <https://www.rfc-editor.org/rfc/rfc1661#section-5>
67/// RFC 1332, Section 2 — <https://www.rfc-editor.org/rfc/rfc1332#section-2>
68/// RFC 1334, Section 2.2 — <https://www.rfc-editor.org/rfc/rfc1334#section-2.2>
69/// RFC 1994, Section 4 — <https://www.rfc-editor.org/rfc/rfc1994#section-4>
70pub const PPP_HEADER_SIZE: usize = 4;
71
72const FD_HDR_CODE: usize = 0;
73const FD_HDR_IDENTIFIER: usize = 1;
74const FD_HDR_LENGTH: usize = 2;
75
76// IPCP shares LCP codes 1–7 (RFC 1332, Section 2 —
77// https://www.rfc-editor.org/rfc/rfc1332#section-2); LCP additionally defines
78// codes 8–11 (RFC 1661, Section 5 — https://www.rfc-editor.org/rfc/rfc1661#section-5).
79pub(crate) static HEADER_DESCRIPTORS: &[FieldDescriptor] =
80    ppp_header_descriptors!(|v, _| match v {
81        FieldValue::U8(c) => Some(code_name(*c)),
82        _ => None,
83    });
84pub(crate) static LCP_HEADER_DESCRIPTORS: &[FieldDescriptor] =
85    ppp_header_descriptors!(|v, _| match v {
86        FieldValue::U8(c) => Some(lcp_code_name(*c)),
87        _ => None,
88    });
89pub(crate) static PAP_HEADER_DESCRIPTORS: &[FieldDescriptor] =
90    ppp_header_descriptors!(|v, _| match v {
91        FieldValue::U8(c) => Some(pap_code_name(*c)),
92        _ => None,
93    });
94pub(crate) static CHAP_HEADER_DESCRIPTORS: &[FieldDescriptor] =
95    ppp_header_descriptors!(|v, _| match v {
96        FieldValue::U8(c) => Some(chap_code_name(*c)),
97        _ => None,
98    });
99
100const FD_OPT_TYPE: usize = 0;
101const FD_OPT_LENGTH: usize = 1;
102const FD_OPT_VALUE: usize = 2;
103
104/// Parse the common PPP packet header (Code, Identifier, Length) into a DissectBuffer.
105///
106/// RFC 1661, Section 5 -- <https://www.rfc-editor.org/rfc/rfc1661#section-5>
107///
108/// Returns `Some((code, length))` or `None` if the data is too short.
109pub fn parse_header(
110    data: &[u8],
111    offset: usize,
112    descriptors: &'static [FieldDescriptor],
113    buf: &mut DissectBuffer<'_>,
114) -> Option<(u8, u16)> {
115    if data.len() < PPP_HEADER_SIZE {
116        return None;
117    }
118    let code = data[0];
119    let length = read_be_u16(data, 2).unwrap_or_default();
120    buf.push_field(
121        &descriptors[FD_HDR_CODE],
122        FieldValue::U8(code),
123        offset..offset + 1,
124    );
125    buf.push_field(
126        &descriptors[FD_HDR_IDENTIFIER],
127        FieldValue::U8(data[1]),
128        offset + 1..offset + 2,
129    );
130    buf.push_field(
131        &descriptors[FD_HDR_LENGTH],
132        FieldValue::U16(length),
133        offset + 2..offset + 4,
134    );
135    Some((code, length))
136}
137
138/// Returns the human-readable name for a PPP Code value common to LCP, IPCP
139/// and other NCPs (codes 1..=7).
140///
141/// RFC 1661, Section 5 — <https://www.rfc-editor.org/rfc/rfc1661#section-5>
142/// RFC 1332, Section 2 — <https://www.rfc-editor.org/rfc/rfc1332#section-2>
143/// (IPCP uses only codes 1..=7.)
144pub fn code_name(code: u8) -> &'static str {
145    match code {
146        1 => "Configure-Request",
147        2 => "Configure-Ack",
148        3 => "Configure-Nak",
149        4 => "Configure-Reject",
150        5 => "Terminate-Request",
151        6 => "Terminate-Ack",
152        7 => "Code-Reject",
153        _ => "Unknown",
154    }
155}
156
157/// Returns the human-readable name for an LCP Code value (codes 1..=11).
158///
159/// RFC 1661, Section 5 — <https://www.rfc-editor.org/rfc/rfc1661#section-5>
160/// Codes 8..=11 are LCP-only and are not used by NCPs (RFC 1332, Section 2 —
161/// <https://www.rfc-editor.org/rfc/rfc1332#section-2>).
162pub fn lcp_code_name(code: u8) -> &'static str {
163    match code {
164        1 => "Configure-Request",
165        2 => "Configure-Ack",
166        3 => "Configure-Nak",
167        4 => "Configure-Reject",
168        5 => "Terminate-Request",
169        6 => "Terminate-Ack",
170        7 => "Code-Reject",
171        8 => "Protocol-Reject",
172        9 => "Echo-Request",
173        10 => "Echo-Reply",
174        11 => "Discard-Request",
175        _ => "Unknown",
176    }
177}
178
179/// Returns the human-readable name for a PAP Code value.
180///
181/// RFC 1334, Section 2.2 — <https://www.rfc-editor.org/rfc/rfc1334#section-2.2>
182pub fn pap_code_name(code: u8) -> &'static str {
183    match code {
184        1 => "Authenticate-Request",
185        2 => "Authenticate-Ack",
186        3 => "Authenticate-Nak",
187        _ => "Unknown",
188    }
189}
190
191/// Returns the human-readable name for a CHAP Code value.
192///
193/// RFC 1994, Section 4 — <https://www.rfc-editor.org/rfc/rfc1994#section-4>
194pub fn chap_code_name(code: u8) -> &'static str {
195    match code {
196        1 => "Challenge",
197        2 => "Response",
198        3 => "Success",
199        4 => "Failure",
200        _ => "Unknown",
201    }
202}
203
204/// Dispatch a PPP sub-protocol payload to the appropriate parser.
205///
206/// Selector is the PPP Protocol field (RFC 1661, Section 2 —
207/// <https://www.rfc-editor.org/rfc/rfc1661#section-2>).
208pub fn parse_protocol<'pkt>(
209    protocol_id: u16,
210    data: &'pkt [u8],
211    offset: usize,
212    buf: &mut DissectBuffer<'pkt>,
213) {
214    match protocol_id {
215        PPP_PROTO_IPCP => ipcp::parse(data, offset, buf),
216        PPP_PROTO_LCP => lcp::parse(data, offset, buf),
217        PPP_PROTO_PAP => pap::parse(data, offset, buf),
218        PPP_PROTO_CHAP => chap::parse(data, offset, buf),
219        _ => {
220            static FD_RAW: FieldDescriptor = FieldDescriptor::new("data", "Data", FieldType::Bytes);
221            buf.push_field(
222                &FD_RAW,
223                FieldValue::Bytes(data),
224                offset..offset + data.len(),
225            );
226        }
227    }
228}
229
230/// Parse TLV-encoded configuration options into a DissectBuffer. Returns `true` if any parsed.
231///
232/// Each option is Type(1) + Length(1) + Data(Length-2) per RFC 1661, Section 6 —
233/// <https://www.rfc-editor.org/rfc/rfc1661#section-6>.
234pub(crate) fn parse_options<'pkt>(
235    options_data: &'pkt [u8],
236    base_offset: usize,
237    container_descriptor: &'static FieldDescriptor,
238    descriptors: &'static [FieldDescriptor],
239    value_parser: fn(u8, &[u8]) -> FieldValue,
240    buf: &mut DissectBuffer<'pkt>,
241) -> bool {
242    let mut pos: usize = 0;
243    let mut count = 0;
244    while pos + 2 <= options_data.len() {
245        let opt_type = options_data[pos];
246        let opt_len = options_data[pos + 1] as usize;
247        if opt_len < 2 {
248            break;
249        }
250        if pos
251            .checked_add(opt_len)
252            .is_none_or(|end| end > options_data.len())
253        {
254            break;
255        }
256        let opt_start = base_offset + pos;
257        let opt_value = value_parser(opt_type, &options_data[pos + 2..pos + opt_len]);
258        let obj_idx = buf.begin_container(
259            container_descriptor,
260            FieldValue::Object(0..0),
261            opt_start..opt_start + opt_len,
262        );
263        buf.push_field(
264            &descriptors[FD_OPT_TYPE],
265            FieldValue::U8(opt_type),
266            opt_start..opt_start + 1,
267        );
268        buf.push_field(
269            &descriptors[FD_OPT_LENGTH],
270            FieldValue::U8(opt_len as u8),
271            opt_start + 1..opt_start + 2,
272        );
273        buf.push_field(
274            &descriptors[FD_OPT_VALUE],
275            opt_value,
276            opt_start + 2..opt_start + opt_len,
277        );
278        buf.end_container(obj_idx);
279        count += 1;
280        pos += opt_len;
281    }
282    count > 0
283}
284
285// HDLC-like framing constants.
286// RFC 1662, Section 3.1 — <https://www.rfc-editor.org/rfc/rfc1662#section-3.1>
287const HDLC_ADDRESS: u8 = 0xFF;
288const HDLC_CONTROL: u8 = 0x03;
289// Minimum PPP frame: 2 octets of Protocol field (without HDLC framing).
290// RFC 1661, Section 2 — <https://www.rfc-editor.org/rfc/rfc1661#section-2>
291const MIN_FRAME_SIZE: usize = 2;
292// PPP DLL Protocol numbers.
293// RFC 1661, Section 2 — <https://www.rfc-editor.org/rfc/rfc1661#section-2>
294// IANA registry: <https://www.iana.org/assignments/ppp-numbers>
295const PPP_PROTO_IPV4: u16 = 0x0021;
296const PPP_PROTO_IPV6: u16 = 0x0057;
297const PPP_PROTO_IPCP: u16 = 0x8021;
298const PPP_PROTO_LCP: u16 = 0xC021;
299const PPP_PROTO_PAP: u16 = 0xC023;
300const PPP_PROTO_CHAP: u16 = 0xC223;
301
302const FD_ADDRESS: usize = 0;
303const FD_CONTROL: usize = 1;
304const FD_PROTOCOL: usize = 2;
305const FD_PAYLOAD: usize = 3;
306
307static FIELD_DESCRIPTORS: &[FieldDescriptor] = &[
308    FieldDescriptor::new("address", "Address", FieldType::U8).optional(),
309    FieldDescriptor::new("control", "Control", FieldType::U8).optional(),
310    FieldDescriptor {
311        name: "protocol",
312        display_name: "Protocol",
313        field_type: FieldType::U16,
314        optional: false,
315        children: None,
316        display_fn: Some(|v, _| match v {
317            FieldValue::U16(p) => protocol_name(*p),
318            _ => None,
319        }),
320        format_fn: None,
321    },
322    FieldDescriptor::new("payload", "Payload", FieldType::Object).optional(),
323];
324
325fn protocol_name(proto: u16) -> Option<&'static str> {
326    match proto {
327        0x0021 => Some("IPv4"),
328        0x0057 => Some("IPv6"),
329        0x8021 => Some("IPCP"),
330        0x8057 => Some("IPv6CP"),
331        0xC021 => Some("LCP"),
332        0xC023 => Some("PAP"),
333        0xC223 => Some("CHAP"),
334        0x0031 => Some("Bridging PDU"),
335        0x003D => Some("Multi-Link"),
336        0x00FD => Some("MPPC/MPPE"),
337        0x8031 => Some("Bridging NCP"),
338        0x80FD => Some("CCP"),
339        _ => None,
340    }
341}
342
343/// PPP frame dissector.
344pub struct PppDissector;
345
346impl Dissector for PppDissector {
347    fn name(&self) -> &'static str {
348        "Point-to-Point Protocol"
349    }
350    fn short_name(&self) -> &'static str {
351        "PPP"
352    }
353    fn field_descriptors(&self) -> &'static [FieldDescriptor] {
354        FIELD_DESCRIPTORS
355    }
356
357    fn dissect<'pkt>(
358        &self,
359        data: &'pkt [u8],
360        buf: &mut DissectBuffer<'pkt>,
361        offset: usize,
362    ) -> Result<DissectResult, PacketError> {
363        if data.len() < MIN_FRAME_SIZE {
364            return Err(PacketError::Truncated {
365                expected: MIN_FRAME_SIZE,
366                actual: data.len(),
367            });
368        }
369        let mut pos = 0;
370        let has_hdlc = data.len() >= 4 && data[0] == HDLC_ADDRESS && data[1] == HDLC_CONTROL;
371        if has_hdlc {
372            pos = 2;
373        }
374        if data.len() < pos + 2 {
375            return Err(PacketError::Truncated {
376                expected: pos + 2,
377                actual: data.len(),
378            });
379        }
380        let proto = read_be_u16(data, pos)?;
381        pos += 2;
382        let header_len = pos;
383        let payload = &data[pos..];
384        let dispatch = match proto {
385            PPP_PROTO_IPCP | PPP_PROTO_LCP | PPP_PROTO_PAP | PPP_PROTO_CHAP => DispatchHint::End,
386            PPP_PROTO_IPV4 => DispatchHint::ByEtherType(0x0800),
387            PPP_PROTO_IPV6 => DispatchHint::ByEtherType(0x86DD),
388            _ => DispatchHint::End,
389        };
390        buf.begin_layer("PPP", None, FIELD_DESCRIPTORS, offset..offset + header_len);
391        if has_hdlc {
392            buf.push_field(
393                &FIELD_DESCRIPTORS[FD_ADDRESS],
394                FieldValue::U8(HDLC_ADDRESS),
395                offset..offset + 1,
396            );
397            buf.push_field(
398                &FIELD_DESCRIPTORS[FD_CONTROL],
399                FieldValue::U8(HDLC_CONTROL),
400                offset + 1..offset + 2,
401            );
402        }
403        buf.push_field(
404            &FIELD_DESCRIPTORS[FD_PROTOCOL],
405            FieldValue::U16(proto),
406            offset + header_len - 2..offset + header_len,
407        );
408        match proto {
409            PPP_PROTO_IPCP | PPP_PROTO_LCP | PPP_PROTO_PAP | PPP_PROTO_CHAP
410                if !payload.is_empty() =>
411            {
412                let obj_idx = buf.begin_container(
413                    &FIELD_DESCRIPTORS[FD_PAYLOAD],
414                    FieldValue::Object(0..0),
415                    offset + pos..offset + data.len(),
416                );
417                parse_protocol(proto, payload, offset + pos, buf);
418                buf.end_container(obj_idx);
419            }
420            _ => {}
421        }
422        buf.end_layer();
423        Ok(DissectResult::new(header_len, dispatch))
424    }
425}
426
427#[cfg(test)]
428mod tests {
429    //! # RFC 1661 (PPP Frame) Coverage
430    //!
431    //! | RFC Section | Description                             | Test                          |
432    //! |-------------|-----------------------------------------|-------------------------------|
433    //! | 2           | Protocol field (no HDLC)                | dissect_no_hdlc_ipv4          |
434    //! | 2           | Protocol field + HDLC Address/Control   | dissect_hdlc_ipv4             |
435    //! | 2           | IPv4 dispatch (0x0021)                  | dissect_hdlc_ipv4             |
436    //! | 2           | IPv6 dispatch (0x0057)                  | dissect_ipv6                  |
437    //! | 2           | LCP inline parsing (0xC021)             | dissect_lcp_inline            |
438    //! | 2           | IPCP inline parsing (0x8021)            | dispatch_ipcp                 |
439    //! | 2           | Unknown protocol -> raw Data            | dispatch_unknown              |
440    //! | 2           | Unknown protocol -> End dispatch        | dissect_unknown_protocol      |
441    //! | 5           | Shared packet header parser             | parse_header_valid            |
442    //! | —           | Header truncated                        | parse_header_truncated        |
443    //! | —           | Frame truncated                         | dissect_truncated             |
444
445    use super::*;
446
447    #[test]
448    fn parse_header_valid() {
449        let data = [0x01, 0x42, 0x00, 0x04];
450        let mut buf = DissectBuffer::new();
451        buf.begin_layer("test", None, &[], 0..4);
452        let result = parse_header(&data, 10, HEADER_DESCRIPTORS, &mut buf);
453        buf.end_layer();
454        let (code, length) = result.unwrap();
455        assert_eq!(code, 1);
456        assert_eq!(length, 4);
457        let layer = &buf.layers()[0];
458        let fields = buf.layer_fields(layer);
459        assert_eq!(fields.len(), 3);
460        assert_eq!(fields[0].value, FieldValue::U8(1));
461        assert_eq!(fields[0].range, 10..11);
462        assert_eq!(
463            fields[0].descriptor.display_fn.unwrap()(&fields[0].value, &[]),
464            Some("Configure-Request")
465        );
466        assert_eq!(fields[1].value, FieldValue::U8(0x42));
467        assert_eq!(fields[2].value, FieldValue::U16(4));
468    }
469
470    #[test]
471    fn parse_header_truncated() {
472        let mut buf = DissectBuffer::new();
473        assert!(parse_header(&[0x01, 0x02], 0, HEADER_DESCRIPTORS, &mut buf).is_none());
474    }
475
476    #[test]
477    fn dispatch_ipcp() {
478        let data = [0x01, 0x01, 0x00, 0x04];
479        let mut buf = DissectBuffer::new();
480        buf.begin_layer("test", None, &[], 0..4);
481        let idx = buf.begin_container(
482            &FIELD_DESCRIPTORS[FD_PAYLOAD],
483            FieldValue::Object(0..0),
484            0..4,
485        );
486        parse_protocol(0x8021, &data, 0, &mut buf);
487        buf.end_container(idx);
488        buf.end_layer();
489        let layer = &buf.layers()[0];
490        let fields = buf.layer_fields(layer);
491        assert!(matches!(fields[0].value, FieldValue::Object(_)));
492    }
493
494    #[test]
495    fn dispatch_unknown() {
496        let data = [0x01, 0x02, 0x03];
497        let mut buf = DissectBuffer::new();
498        buf.begin_layer("test", None, &[], 0..3);
499        parse_protocol(0xFFFF, &data, 0, &mut buf);
500        buf.end_layer();
501        let layer = &buf.layers()[0];
502        let fields = buf.layer_fields(layer);
503        assert_eq!(fields[0].value, FieldValue::Bytes(&[0x01, 0x02, 0x03]));
504    }
505
506    #[test]
507    fn dissect_hdlc_ipv4() {
508        let data = [0xFF, 0x03, 0x00, 0x21, 0x45, 0x00, 0x00];
509        let mut buf = DissectBuffer::new();
510        let result = PppDissector.dissect(&data, &mut buf, 0).unwrap();
511        assert_eq!(result.bytes_consumed, 4);
512        assert_eq!(result.next, DispatchHint::ByEtherType(0x0800));
513        let layer = buf.layer_by_name("PPP").unwrap();
514        let fields = buf.layer_fields(layer);
515        assert_eq!(fields.len(), 3);
516        assert_eq!(fields[0].value, FieldValue::U8(0xFF));
517        assert_eq!(fields[1].value, FieldValue::U8(0x03));
518        assert_eq!(fields[2].value, FieldValue::U16(0x0021));
519        let display = fields[2].descriptor.display_fn.unwrap()(&fields[2].value, fields);
520        assert_eq!(display, Some("IPv4"));
521    }
522
523    #[test]
524    fn dissect_no_hdlc_ipv4() {
525        let data = [0x00, 0x21, 0x45, 0x00, 0x00];
526        let mut buf = DissectBuffer::new();
527        let result = PppDissector.dissect(&data, &mut buf, 10).unwrap();
528        assert_eq!(result.bytes_consumed, 2);
529        let layer = buf.layer_by_name("PPP").unwrap();
530        let fields = buf.layer_fields(layer);
531        assert_eq!(fields.len(), 1);
532        assert_eq!(fields[0].value, FieldValue::U16(0x0021));
533        assert_eq!(fields[0].range, 10..12);
534    }
535
536    #[test]
537    fn dissect_ipv6() {
538        let data = [0x00, 0x57, 0x60, 0x00];
539        let mut buf = DissectBuffer::new();
540        let result = PppDissector.dissect(&data, &mut buf, 0).unwrap();
541        assert_eq!(result.next, DispatchHint::ByEtherType(0x86DD));
542        let layer = buf.layer_by_name("PPP").unwrap();
543        let fields = buf.layer_fields(layer);
544        let display = fields[0].descriptor.display_fn.unwrap()(&fields[0].value, fields);
545        assert_eq!(display, Some("IPv6"));
546    }
547
548    #[test]
549    fn dissect_lcp_inline() {
550        let data = [0xC0, 0x21, 0x01, 0x01, 0x00, 0x08, 1, 4, 0x05, 0xDC];
551        let mut buf = DissectBuffer::new();
552        let result = PppDissector.dissect(&data, &mut buf, 0).unwrap();
553        assert_eq!(result.bytes_consumed, 2);
554        assert_eq!(result.next, DispatchHint::End);
555        let layer = buf.layer_by_name("PPP").unwrap();
556        let fields = buf.layer_fields(layer);
557        // protocol is first field, payload Object is second
558        assert!(fields.len() >= 2);
559        let display = fields[0].descriptor.display_fn.unwrap()(&fields[0].value, fields);
560        assert_eq!(display, Some("LCP"));
561        assert!(matches!(fields[1].value, FieldValue::Object(_)));
562    }
563
564    #[test]
565    fn dissect_unknown_protocol() {
566        let data = [0x00, 0x99, 0xAA, 0xBB];
567        let mut buf = DissectBuffer::new();
568        let result = PppDissector.dissect(&data, &mut buf, 0).unwrap();
569        assert_eq!(result.next, DispatchHint::End);
570        let layer = buf.layer_by_name("PPP").unwrap();
571        let fields = buf.layer_fields(layer);
572        assert_eq!(fields.len(), 1);
573    }
574
575    #[test]
576    fn dissect_truncated() {
577        let mut buf = DissectBuffer::new();
578        let result = PppDissector.dissect(&[0x00], &mut buf, 0);
579        assert!(matches!(result, Err(PacketError::Truncated { .. })));
580    }
581}