Skip to main content

packet_dissector_bfd/
lib.rs

1//! BFD (Bidirectional Forwarding Detection) dissector.
2//!
3//! ## References
4//! - RFC 5880 (BFD base specification): <https://www.rfc-editor.org/rfc/rfc5880>
5//! - RFC 5881 (BFD for IPv4/IPv6 single hop): <https://www.rfc-editor.org/rfc/rfc5881>
6//! - RFC 5883 (BFD Multihop): <https://www.rfc-editor.org/rfc/rfc5883>
7//! - RFC 7419 (BFD Common Interval Support; updates RFC 5880):
8//!   <https://www.rfc-editor.org/rfc/rfc7419>
9//! - RFC 7880 (Seamless BFD; updates RFC 5880):
10//!   <https://www.rfc-editor.org/rfc/rfc7880>
11//! - RFC 8562 (BFD for Multipoint Networks; updates RFC 5880, redefines the
12//!   Multipoint (M) bit): <https://www.rfc-editor.org/rfc/rfc8562>
13//! - RFC 9747 (BFD Echo function clarifications; updates RFC 5880):
14//!   <https://www.rfc-editor.org/rfc/rfc9747>
15
16#![deny(missing_docs)]
17
18use packet_dissector_core::dissector::{DispatchHint, DissectResult, Dissector};
19use packet_dissector_core::error::PacketError;
20use packet_dissector_core::field::{FieldDescriptor, FieldType, FieldValue};
21use packet_dissector_core::packet::DissectBuffer;
22use packet_dissector_core::util::read_be_u32;
23
24/// BFD protocol version defined by RFC 5880, Section 4.1 —
25/// <https://www.rfc-editor.org/rfc/rfc5880#section-4.1>
26const BFD_VERSION: u8 = 1;
27
28/// Minimum BFD Control packet size without authentication.
29/// RFC 5880, Section 6.8.6 —
30/// <https://www.rfc-editor.org/rfc/rfc5880#section-6.8.6>
31const MIN_HEADER_SIZE: usize = 24;
32
33/// Minimum BFD Control packet size with authentication.
34/// RFC 5880, Section 6.8.6 —
35/// <https://www.rfc-editor.org/rfc/rfc5880#section-6.8.6>
36const MIN_HEADER_SIZE_WITH_AUTH: usize = 26;
37
38/// Fixed Auth Len for Keyed MD5 / Meticulous Keyed MD5.
39/// RFC 5880, Section 4.3 —
40/// <https://www.rfc-editor.org/rfc/rfc5880#section-4.3>
41const AUTH_LEN_MD5: usize = 24;
42
43/// Fixed Auth Len for Keyed SHA1 / Meticulous Keyed SHA1.
44/// RFC 5880, Section 4.4 —
45/// <https://www.rfc-editor.org/rfc/rfc5880#section-4.4>
46const AUTH_LEN_SHA1: usize = 28;
47
48/// Minimum Auth Len for Simple Password (Type + Len + Key ID + 1-byte
49/// password). RFC 5880, Section 4.2 —
50/// <https://www.rfc-editor.org/rfc/rfc5880#section-4.2>
51const MIN_AUTH_LEN_SIMPLE_PASSWORD: usize = 4;
52
53/// Minimum Auth Len for an unknown authentication type (Type + Len + at
54/// least one byte of authentication data).
55const MIN_AUTH_LEN_UNKNOWN: usize = 3;
56
57/// Returns a human-readable name for the Diagnostic (Diag) field value.
58///
59/// RFC 5880, Section 4.1 —
60/// <https://www.rfc-editor.org/rfc/rfc5880#section-4.1> — Diagnostic values:
61///   "0 -- No Diagnostic
62///    1 -- Control Detection Time Expired
63///    2 -- Echo Function Failed
64///    3 -- Neighbor Signaled Session Down
65///    4 -- Forwarding Plane Reset
66///    5 -- Path Down
67///    6 -- Concatenated Path Down
68///    7 -- Administratively Down
69///    8 -- Reverse Concatenated Path Down
70///    9-31 -- Reserved for future use"
71fn diagnostic_name(diag: u8) -> &'static str {
72    match diag {
73        0 => "No Diagnostic",
74        1 => "Control Detection Time Expired",
75        2 => "Echo Function Failed",
76        3 => "Neighbor Signaled Session Down",
77        4 => "Forwarding Plane Reset",
78        5 => "Path Down",
79        6 => "Concatenated Path Down",
80        7 => "Administratively Down",
81        8 => "Reverse Concatenated Path Down",
82        _ => "Reserved",
83    }
84}
85
86/// Returns a human-readable name for the State (Sta) field value.
87///
88/// RFC 5880, Section 4.1 —
89/// <https://www.rfc-editor.org/rfc/rfc5880#section-4.1> — State values:
90///   "0 -- AdminDown
91///    1 -- Down
92///    2 -- Init
93///    3 -- Up"
94fn state_name(state: u8) -> &'static str {
95    match state {
96        0 => "AdminDown",
97        1 => "Down",
98        2 => "Init",
99        3 => "Up",
100        // State is a 2-bit field so only 0-3 are possible.
101        _ => unreachable!(),
102    }
103}
104
105/// Returns a human-readable name for the Authentication Type value.
106///
107/// RFC 5880, Section 4.2 —
108/// <https://www.rfc-editor.org/rfc/rfc5880#section-4.2> — Authentication Type
109/// values:
110///   "0 - Reserved
111///    1 - Simple Password
112///    2 - Keyed MD5
113///    3 - Meticulous Keyed MD5
114///    4 - Keyed SHA1
115///    5 - Meticulous Keyed SHA1"
116fn auth_type_name(auth_type: u8) -> &'static str {
117    match auth_type {
118        0 => "Reserved",
119        1 => "Simple Password",
120        2 => "Keyed MD5",
121        3 => "Meticulous Keyed MD5",
122        4 => "Keyed SHA1",
123        5 => "Meticulous Keyed SHA1",
124        _ => "Reserved",
125    }
126}
127
128/// BFD dissector.
129pub struct BfdDissector;
130
131/// Field descriptors for the BFD dissector.
132static FIELD_DESCRIPTORS: &[FieldDescriptor] = &[
133    FieldDescriptor::new("version", "Version", FieldType::U8),
134    FieldDescriptor {
135        name: "diagnostic",
136        display_name: "Diagnostic",
137        field_type: FieldType::U8,
138        optional: false,
139        children: None,
140        display_fn: Some(|v, _siblings| match v {
141            FieldValue::U8(d) => Some(diagnostic_name(*d)),
142            _ => None,
143        }),
144        format_fn: None,
145    },
146    FieldDescriptor {
147        name: "state",
148        display_name: "State",
149        field_type: FieldType::U8,
150        optional: false,
151        children: None,
152        display_fn: Some(|v, _siblings| match v {
153            FieldValue::U8(s) => Some(state_name(*s)),
154            _ => None,
155        }),
156        format_fn: None,
157    },
158    FieldDescriptor::new("poll", "Poll", FieldType::U8),
159    FieldDescriptor::new("final", "Final", FieldType::U8),
160    FieldDescriptor::new(
161        "control_plane_independent",
162        "Control Plane Independent",
163        FieldType::U8,
164    ),
165    FieldDescriptor::new("auth_present", "Authentication Present", FieldType::U8),
166    FieldDescriptor::new("demand", "Demand", FieldType::U8),
167    FieldDescriptor::new("multipoint", "Multipoint", FieldType::U8),
168    FieldDescriptor::new("detect_mult", "Detect Multiplier", FieldType::U8),
169    FieldDescriptor::new("length", "Length", FieldType::U8),
170    FieldDescriptor::new("my_discriminator", "My Discriminator", FieldType::U32),
171    FieldDescriptor::new("your_discriminator", "Your Discriminator", FieldType::U32),
172    FieldDescriptor::new(
173        "desired_min_tx_interval",
174        "Desired Min TX Interval",
175        FieldType::U32,
176    ),
177    FieldDescriptor::new(
178        "required_min_rx_interval",
179        "Required Min RX Interval",
180        FieldType::U32,
181    ),
182    FieldDescriptor::new(
183        "required_min_echo_rx_interval",
184        "Required Min Echo RX Interval",
185        FieldType::U32,
186    ),
187    // Authentication fields (optional — only present when A bit is set)
188    FieldDescriptor {
189        name: "auth_type",
190        display_name: "Auth Type",
191        field_type: FieldType::U8,
192        optional: true,
193        children: None,
194        display_fn: Some(|v, _siblings| match v {
195            FieldValue::U8(a) => Some(auth_type_name(*a)),
196            _ => None,
197        }),
198        format_fn: None,
199    },
200    FieldDescriptor::new("auth_data", "Auth Data", FieldType::Bytes).optional(),
201];
202
203/// Index constants for `FIELD_DESCRIPTORS`.
204const FD_VERSION: usize = 0;
205const FD_DIAGNOSTIC: usize = 1;
206const FD_STATE: usize = 2;
207const FD_POLL: usize = 3;
208const FD_FINAL: usize = 4;
209const FD_CONTROL_PLANE_INDEPENDENT: usize = 5;
210const FD_AUTH_PRESENT: usize = 6;
211const FD_DEMAND: usize = 7;
212const FD_MULTIPOINT: usize = 8;
213const FD_DETECT_MULT: usize = 9;
214const FD_LENGTH: usize = 10;
215const FD_MY_DISCRIMINATOR: usize = 11;
216const FD_YOUR_DISCRIMINATOR: usize = 12;
217const FD_DESIRED_MIN_TX_INTERVAL: usize = 13;
218const FD_REQUIRED_MIN_RX_INTERVAL: usize = 14;
219const FD_REQUIRED_MIN_ECHO_RX_INTERVAL: usize = 15;
220// Authentication fields (optional — only present when A bit is set)
221const FD_AUTH_TYPE: usize = 16;
222const FD_AUTH_DATA: usize = 17;
223
224impl Dissector for BfdDissector {
225    fn name(&self) -> &'static str {
226        "Bidirectional Forwarding Detection"
227    }
228
229    fn short_name(&self) -> &'static str {
230        "BFD"
231    }
232
233    fn field_descriptors(&self) -> &'static [FieldDescriptor] {
234        FIELD_DESCRIPTORS
235    }
236
237    fn dissect<'pkt>(
238        &self,
239        data: &'pkt [u8],
240        buf: &mut DissectBuffer<'pkt>,
241        offset: usize,
242    ) -> Result<DissectResult, PacketError> {
243        if data.len() < MIN_HEADER_SIZE {
244            return Err(PacketError::Truncated {
245                expected: MIN_HEADER_SIZE,
246                actual: data.len(),
247            });
248        }
249
250        // RFC 5880, Section 4.1 —
251        // <https://www.rfc-editor.org/rfc/rfc5880#section-4.1> — first octet:
252        // Vers (3 bits) | Diag (5 bits).
253        let byte0 = data[0];
254        let version = (byte0 >> 5) & 0x07;
255        let diagnostic = byte0 & 0x1F;
256
257        // RFC 5880, Section 6.8.6 #1 —
258        // <https://www.rfc-editor.org/rfc/rfc5880#section-6.8.6> — "If the
259        // version number is not correct (1), the packet MUST be discarded."
260        if version != BFD_VERSION {
261            return Err(PacketError::InvalidFieldValue {
262                field: "version",
263                value: u32::from(version),
264            });
265        }
266
267        // RFC 5880, Section 4.1 —
268        // <https://www.rfc-editor.org/rfc/rfc5880#section-4.1> — second octet:
269        // Sta (2) | P | F | C | A | D | M.
270        let byte1 = data[1];
271        let state = (byte1 >> 6) & 0x03;
272        let poll = (byte1 >> 5) & 0x01;
273        let final_flag = (byte1 >> 4) & 0x01;
274        let control_plane_independent = (byte1 >> 3) & 0x01;
275        let auth_present = (byte1 >> 2) & 0x01;
276        let demand = (byte1 >> 1) & 0x01;
277        // RFC 5880 originally reserved the M bit as zero; RFC 8562, Section
278        // 4.2 — <https://www.rfc-editor.org/rfc/rfc8562#section-4.2> — updates
279        // RFC 5880 to set M=1 on MultipointHead sessions, so either value is
280        // accepted here.
281        let multipoint = byte1 & 0x01;
282
283        let detect_mult = data[2];
284        // RFC 5880, Section 6.8.6 #4 —
285        // <https://www.rfc-editor.org/rfc/rfc5880#section-6.8.6> — "If the
286        // Detect Mult field is zero, the packet MUST be discarded."
287        if detect_mult == 0 {
288            return Err(PacketError::InvalidFieldValue {
289                field: "detect_mult",
290                value: 0,
291            });
292        }
293
294        // RFC 5880, Section 4.1 —
295        // <https://www.rfc-editor.org/rfc/rfc5880#section-4.1> — "The length
296        // of the BFD Control packet, in bytes."
297        let length_u8 = data[3];
298        let length = length_u8 as usize;
299        // RFC 5880, Section 6.8.6 #2 —
300        // <https://www.rfc-editor.org/rfc/rfc5880#section-6.8.6> — Length
301        // below the minimum (24 without auth, 26 with auth) MUST be
302        // discarded. The auth variant is enforced below once the A bit is
303        // known.
304        if length < MIN_HEADER_SIZE {
305            return Err(PacketError::InvalidFieldValue {
306                field: "length",
307                value: length_u8 as u32,
308            });
309        }
310        // RFC 5880, Section 6.8.6 #3 —
311        // <https://www.rfc-editor.org/rfc/rfc5880#section-6.8.6> — "If the
312        // Length field is greater than the payload of the encapsulating
313        // protocol, the packet MUST be discarded."
314        if data.len() < length {
315            return Err(PacketError::Truncated {
316                expected: length,
317                actual: data.len(),
318            });
319        }
320
321        let my_discriminator = read_be_u32(data, 4)?;
322        // RFC 5880, Section 6.8.6 #6 —
323        // <https://www.rfc-editor.org/rfc/rfc5880#section-6.8.6> — "If the My
324        // Discriminator field is zero, the packet MUST be discarded."
325        if my_discriminator == 0 {
326            return Err(PacketError::InvalidFieldValue {
327                field: "my_discriminator",
328                value: 0,
329            });
330        }
331        let your_discriminator = read_be_u32(data, 8)?;
332        let desired_min_tx = read_be_u32(data, 12)?;
333        let required_min_rx = read_be_u32(data, 16)?;
334        let required_min_echo_rx = read_be_u32(data, 20)?;
335
336        buf.begin_layer(
337            self.short_name(),
338            None,
339            FIELD_DESCRIPTORS,
340            offset..offset + length,
341        );
342        buf.push_field(
343            &FIELD_DESCRIPTORS[FD_VERSION],
344            FieldValue::U8(version),
345            offset..offset + 1,
346        );
347        buf.push_field(
348            &FIELD_DESCRIPTORS[FD_DIAGNOSTIC],
349            FieldValue::U8(diagnostic),
350            offset..offset + 1,
351        );
352        buf.push_field(
353            &FIELD_DESCRIPTORS[FD_STATE],
354            FieldValue::U8(state),
355            offset + 1..offset + 2,
356        );
357        buf.push_field(
358            &FIELD_DESCRIPTORS[FD_POLL],
359            FieldValue::U8(poll),
360            offset + 1..offset + 2,
361        );
362        buf.push_field(
363            &FIELD_DESCRIPTORS[FD_FINAL],
364            FieldValue::U8(final_flag),
365            offset + 1..offset + 2,
366        );
367        buf.push_field(
368            &FIELD_DESCRIPTORS[FD_CONTROL_PLANE_INDEPENDENT],
369            FieldValue::U8(control_plane_independent),
370            offset + 1..offset + 2,
371        );
372        buf.push_field(
373            &FIELD_DESCRIPTORS[FD_AUTH_PRESENT],
374            FieldValue::U8(auth_present),
375            offset + 1..offset + 2,
376        );
377        buf.push_field(
378            &FIELD_DESCRIPTORS[FD_DEMAND],
379            FieldValue::U8(demand),
380            offset + 1..offset + 2,
381        );
382        buf.push_field(
383            &FIELD_DESCRIPTORS[FD_MULTIPOINT],
384            FieldValue::U8(multipoint),
385            offset + 1..offset + 2,
386        );
387        buf.push_field(
388            &FIELD_DESCRIPTORS[FD_DETECT_MULT],
389            FieldValue::U8(detect_mult),
390            offset + 2..offset + 3,
391        );
392        buf.push_field(
393            &FIELD_DESCRIPTORS[FD_LENGTH],
394            FieldValue::U8(length_u8),
395            offset + 3..offset + 4,
396        );
397        buf.push_field(
398            &FIELD_DESCRIPTORS[FD_MY_DISCRIMINATOR],
399            FieldValue::U32(my_discriminator),
400            offset + 4..offset + 8,
401        );
402        buf.push_field(
403            &FIELD_DESCRIPTORS[FD_YOUR_DISCRIMINATOR],
404            FieldValue::U32(your_discriminator),
405            offset + 8..offset + 12,
406        );
407        buf.push_field(
408            &FIELD_DESCRIPTORS[FD_DESIRED_MIN_TX_INTERVAL],
409            FieldValue::U32(desired_min_tx),
410            offset + 12..offset + 16,
411        );
412        buf.push_field(
413            &FIELD_DESCRIPTORS[FD_REQUIRED_MIN_RX_INTERVAL],
414            FieldValue::U32(required_min_rx),
415            offset + 16..offset + 20,
416        );
417        buf.push_field(
418            &FIELD_DESCRIPTORS[FD_REQUIRED_MIN_ECHO_RX_INTERVAL],
419            FieldValue::U32(required_min_echo_rx),
420            offset + 20..offset + 24,
421        );
422
423        // RFC 5880, Section 4.2 —
424        // <https://www.rfc-editor.org/rfc/rfc5880#section-4.2> — Optional
425        // Authentication Section.
426        if auth_present == 1 {
427            // RFC 5880, Section 6.8.6 #2 —
428            // <https://www.rfc-editor.org/rfc/rfc5880#section-6.8.6> — when
429            // the A bit is set, the minimum correct Length is 26.
430            if length < MIN_HEADER_SIZE_WITH_AUTH {
431                return Err(PacketError::InvalidHeader(
432                    "BFD auth present but length is less than minimum with auth",
433                ));
434            }
435            let auth_type = data[24];
436            let auth_len = data[25] as usize;
437
438            // RFC 5880, Sections 4.2–4.4 — each authentication type has a
439            // fixed or minimum Auth Len including the Type and Length bytes
440            // themselves. Enforce these to reject malformed auth sections
441            // with missing fields such as the Key ID or fixed MD5/SHA1
442            // digest/hash.
443            //   - Simple Password (Section 4.2 —
444            //     <https://www.rfc-editor.org/rfc/rfc5880#section-4.2>):
445            //     Type(1) + Len(1) + Key ID(1) + 1..=16 byte password ⇒ 4..=19
446            //   - Keyed / Meticulous Keyed MD5 (Section 4.3 —
447            //     <https://www.rfc-editor.org/rfc/rfc5880#section-4.3>):
448            //     Auth Len is fixed at 24.
449            //   - Keyed / Meticulous Keyed SHA1 (Section 4.4 —
450            //     <https://www.rfc-editor.org/rfc/rfc5880#section-4.4>):
451            //     Auth Len is fixed at 28.
452            let min_auth_len = match auth_type {
453                1 => MIN_AUTH_LEN_SIMPLE_PASSWORD,
454                2 | 3 => AUTH_LEN_MD5,
455                4 | 5 => AUTH_LEN_SHA1,
456                _ => MIN_AUTH_LEN_UNKNOWN,
457            };
458
459            if auth_len < min_auth_len {
460                return Err(PacketError::InvalidHeader(
461                    "BFD auth length is less than minimum for auth type",
462                ));
463            }
464            let auth_end = 24 + auth_len;
465            if auth_end > length {
466                return Err(PacketError::InvalidHeader(
467                    "BFD auth section exceeds packet length",
468                ));
469            }
470
471            buf.push_field(
472                &FIELD_DESCRIPTORS[FD_AUTH_TYPE],
473                FieldValue::U8(auth_type),
474                offset + 24..offset + 25,
475            );
476            if auth_len > 2 {
477                buf.push_field(
478                    &FIELD_DESCRIPTORS[FD_AUTH_DATA],
479                    FieldValue::Bytes(&data[26..auth_end]),
480                    offset + 26..offset + auth_end,
481                );
482            }
483        }
484
485        buf.end_layer();
486
487        Ok(DissectResult::new(length, DispatchHint::End))
488    }
489}
490
491#[cfg(test)]
492mod tests {
493    use super::*;
494
495    // # RFC 5880 Coverage
496    //
497    // | RFC Section | Description                                | Test                              |
498    // |-------------|--------------------------------------------|-----------------------------------|
499    // | 4.1         | Header: Version, Diagnostic                | test_parse_basic_up               |
500    // | 4.1         | Header: State, flags                       | test_parse_all_flags_set          |
501    // | 4.1         | Header: Detect Mult, Length                | test_parse_basic_up               |
502    // | 4.1         | Header: Discriminators                     | test_parse_basic_up               |
503    // | 4.1         | Header: Interval fields                    | test_parse_basic_up               |
504    // | 4.1         | All diagnostic codes                       | test_diagnostic_codes             |
505    // | 4.1         | All state values                           | test_state_values                 |
506    // | 4.1         | Multipoint (M) bit set (RFC 8562 update)   | test_parse_multipoint_bit_set     |
507    // | 4.2         | Auth section (Simple Password)             | test_parse_with_auth_simple       |
508    // | 4.2         | Auth section (Keyed MD5)                   | test_parse_with_auth_md5          |
509    // | 4.2         | Auth section (Keyed SHA1)                  | test_parse_with_auth_sha1         |
510    // | 4.2         | Simple Password too short (< 4) rejected   | test_simple_password_too_short    |
511    // | 4.2         | Keyed MD5 wrong length rejected            | test_md5_wrong_length             |
512    // | 4.2         | Keyed SHA1 wrong length rejected           | test_sha1_wrong_length            |
513    // | 6.8.6 #1    | Version != 1 rejected                      | test_invalid_version              |
514    // | 6.8.6 #2    | Invalid length (< 24)                      | test_invalid_length_field         |
515    // | 6.8.6 #3    | Length > payload                           | test_length_exceeds_data          |
516    // | 6.8.6 #2    | Auth present but length < 26               | test_auth_present_but_truncated   |
517    // | 6.8.6 #4    | Detect Mult == 0 rejected                  | test_detect_mult_zero             |
518    // | 6.8.6 #6    | My Discriminator == 0 rejected             | test_my_discriminator_zero        |
519    // | ---         | Truncated header (< 24 bytes)              | test_truncated_packet             |
520    // | ---         | Auth section exceeds length                | test_auth_section_exceeds_length  |
521    // | ---         | Offset handling                            | test_dissect_with_offset          |
522    // | ---         | Field descriptors                          | test_field_descriptors            |
523
524    /// Build a minimal BFD Control packet.
525    #[allow(clippy::too_many_arguments)]
526    fn build_bfd(
527        version: u8,
528        diagnostic: u8,
529        state: u8,
530        poll: u8,
531        final_f: u8,
532        cpi: u8,
533        auth: u8,
534        demand: u8,
535        multipoint: u8,
536        detect_mult: u8,
537        length: u8,
538        my_disc: u32,
539        your_disc: u32,
540        desired_min_tx: u32,
541        required_min_rx: u32,
542        required_min_echo_rx: u32,
543    ) -> Vec<u8> {
544        let byte0 = (version << 5) | (diagnostic & 0x1F);
545        let byte1 = (state << 6)
546            | (poll << 5)
547            | (final_f << 4)
548            | (cpi << 3)
549            | (auth << 2)
550            | (demand << 1)
551            | multipoint;
552        let mut pkt = Vec::with_capacity(length as usize);
553        pkt.push(byte0);
554        pkt.push(byte1);
555        pkt.push(detect_mult);
556        pkt.push(length);
557        pkt.extend_from_slice(&my_disc.to_be_bytes());
558        pkt.extend_from_slice(&your_disc.to_be_bytes());
559        pkt.extend_from_slice(&desired_min_tx.to_be_bytes());
560        pkt.extend_from_slice(&required_min_rx.to_be_bytes());
561        pkt.extend_from_slice(&required_min_echo_rx.to_be_bytes());
562        pkt
563    }
564
565    /// Build a BFD Control packet with an authentication section.
566    #[allow(clippy::too_many_arguments)]
567    fn build_bfd_with_auth(
568        version: u8,
569        diagnostic: u8,
570        state: u8,
571        detect_mult: u8,
572        my_disc: u32,
573        your_disc: u32,
574        auth_type: u8,
575        auth_data: &[u8],
576    ) -> Vec<u8> {
577        let auth_len = 2 + auth_data.len();
578        let total_len = 24 + auth_len;
579        let mut pkt = build_bfd(
580            version,
581            diagnostic,
582            state,
583            0,
584            0,
585            0,
586            1, // auth present
587            0,
588            0,
589            detect_mult,
590            total_len as u8,
591            my_disc,
592            your_disc,
593            1_000_000,
594            1_000_000,
595            0,
596        );
597        pkt.push(auth_type);
598        pkt.push(auth_len as u8);
599        pkt.extend_from_slice(auth_data);
600        pkt
601    }
602
603    #[test]
604    fn test_parse_basic_up() {
605        // BFD v1, State=Up, Diag=No Diagnostic, Detect Mult=3, Length=24
606        let data = build_bfd(
607            1,         // version
608            0,         // diagnostic: No Diagnostic
609            3,         // state: Up
610            0,         // poll
611            0,         // final
612            0,         // cpi
613            0,         // auth
614            0,         // demand
615            0,         // multipoint
616            3,         // detect mult
617            24,        // length
618            0x0001,    // my discriminator
619            0x0002,    // your discriminator
620            1_000_000, // desired min tx (1s)
621            1_000_000, // required min rx (1s)
622            0,         // required min echo rx
623        );
624        let mut buf = DissectBuffer::new();
625        BfdDissector.dissect(&data, &mut buf, 0).unwrap();
626
627        assert_eq!(buf.layers().len(), 1);
628        let layer = &buf.layers()[0];
629        assert_eq!(layer.name, "BFD");
630
631        assert_eq!(
632            buf.field_by_name(layer, "version").unwrap().value,
633            FieldValue::U8(1)
634        );
635        assert_eq!(
636            buf.field_by_name(layer, "diagnostic").unwrap().value,
637            FieldValue::U8(0)
638        );
639        assert_eq!(
640            buf.resolve_display_name(layer, "diagnostic_name"),
641            Some("No Diagnostic")
642        );
643        assert_eq!(
644            buf.field_by_name(layer, "state").unwrap().value,
645            FieldValue::U8(3)
646        );
647        assert_eq!(buf.resolve_display_name(layer, "state_name"), Some("Up"));
648        assert_eq!(
649            buf.field_by_name(layer, "poll").unwrap().value,
650            FieldValue::U8(0)
651        );
652        assert_eq!(
653            buf.field_by_name(layer, "final").unwrap().value,
654            FieldValue::U8(0)
655        );
656        assert_eq!(
657            buf.field_by_name(layer, "control_plane_independent")
658                .unwrap()
659                .value,
660            FieldValue::U8(0)
661        );
662        assert_eq!(
663            buf.field_by_name(layer, "auth_present").unwrap().value,
664            FieldValue::U8(0)
665        );
666        assert_eq!(
667            buf.field_by_name(layer, "demand").unwrap().value,
668            FieldValue::U8(0)
669        );
670        assert_eq!(
671            buf.field_by_name(layer, "multipoint").unwrap().value,
672            FieldValue::U8(0)
673        );
674        assert_eq!(
675            buf.field_by_name(layer, "detect_mult").unwrap().value,
676            FieldValue::U8(3)
677        );
678        assert_eq!(
679            buf.field_by_name(layer, "length").unwrap().value,
680            FieldValue::U8(24)
681        );
682        assert_eq!(
683            buf.field_by_name(layer, "my_discriminator").unwrap().value,
684            FieldValue::U32(0x0001)
685        );
686        assert_eq!(
687            buf.field_by_name(layer, "your_discriminator")
688                .unwrap()
689                .value,
690            FieldValue::U32(0x0002)
691        );
692        assert_eq!(
693            buf.field_by_name(layer, "desired_min_tx_interval")
694                .unwrap()
695                .value,
696            FieldValue::U32(1_000_000)
697        );
698        assert_eq!(
699            buf.field_by_name(layer, "required_min_rx_interval")
700                .unwrap()
701                .value,
702            FieldValue::U32(1_000_000)
703        );
704        assert_eq!(
705            buf.field_by_name(layer, "required_min_echo_rx_interval")
706                .unwrap()
707                .value,
708            FieldValue::U32(0)
709        );
710    }
711
712    #[test]
713    fn test_parse_all_flags_set() {
714        // All flag bits set: P=1, F=1, C=1, A=1, D=1, M=1
715        // Auth present requires auth section, so include a minimal one.
716        let mut data = build_bfd(
717            1, 7, // Administratively Down
718            0, // AdminDown
719            1, // poll
720            1, // final
721            1, // cpi
722            1, // auth present
723            1, // demand
724            1, // multipoint
725            5, // detect mult
726            28, 0xAABBCCDD, 0x11223344, 500_000, 500_000, 100_000,
727        );
728        // Append minimal auth: type=1 (Simple Password), len=4, 2 bytes data
729        data.push(1); // auth type
730        data.push(4); // auth len (2 header + 2 data)
731        data.push(0x41); // 'A'
732        data.push(0x42); // 'B'
733
734        let mut buf = DissectBuffer::new();
735        BfdDissector.dissect(&data, &mut buf, 0).unwrap();
736
737        let layer = &buf.layers()[0];
738        assert_eq!(
739            buf.field_by_name(layer, "poll").unwrap().value,
740            FieldValue::U8(1)
741        );
742        assert_eq!(
743            buf.field_by_name(layer, "final").unwrap().value,
744            FieldValue::U8(1)
745        );
746        assert_eq!(
747            buf.field_by_name(layer, "control_plane_independent")
748                .unwrap()
749                .value,
750            FieldValue::U8(1)
751        );
752        assert_eq!(
753            buf.field_by_name(layer, "auth_present").unwrap().value,
754            FieldValue::U8(1)
755        );
756        assert_eq!(
757            buf.field_by_name(layer, "demand").unwrap().value,
758            FieldValue::U8(1)
759        );
760        assert_eq!(
761            buf.field_by_name(layer, "multipoint").unwrap().value,
762            FieldValue::U8(1)
763        );
764        assert_eq!(
765            buf.field_by_name(layer, "auth_type").unwrap().value,
766            FieldValue::U8(1)
767        );
768        assert_eq!(
769            buf.resolve_display_name(layer, "auth_type_name"),
770            Some("Simple Password")
771        );
772        assert_eq!(
773            buf.field_by_name(layer, "auth_data").unwrap().value,
774            FieldValue::Bytes(&[0x41, 0x42])
775        );
776    }
777
778    #[test]
779    fn test_diagnostic_codes() {
780        for diag in 0..=8 {
781            let data = build_bfd(
782                1, diag, 3, 0, 0, 0, 0, 0, 0, 3, 24, 1, 0, 1_000_000, 1_000_000, 0,
783            );
784            let mut buf = DissectBuffer::new();
785            BfdDissector.dissect(&data, &mut buf, 0).unwrap();
786            let layer = &buf.layers()[0];
787            assert_eq!(
788                buf.field_by_name(layer, "diagnostic").unwrap().value,
789                FieldValue::U8(diag)
790            );
791            if let Some(name) = buf.resolve_display_name(layer, "diagnostic_name") {
792                assert!(!name.is_empty());
793                assert_ne!(name, "Reserved");
794            } else {
795                panic!("diagnostic_name should be Str");
796            }
797        }
798        // Reserved value
799        let data = build_bfd(
800            1, 9, 3, 0, 0, 0, 0, 0, 0, 3, 24, 1, 0, 1_000_000, 1_000_000, 0,
801        );
802        let mut buf = DissectBuffer::new();
803        BfdDissector.dissect(&data, &mut buf, 0).unwrap();
804        assert_eq!(
805            buf.resolve_display_name(&buf.layers()[0], "diagnostic_name"),
806            Some("Reserved")
807        );
808    }
809
810    #[test]
811    fn test_state_values() {
812        let names = ["AdminDown", "Down", "Init", "Up"];
813        for state in 0..=3u8 {
814            let data = build_bfd(
815                1, 0, state, 0, 0, 0, 0, 0, 0, 3, 24, 1, 0, 1_000_000, 1_000_000, 0,
816            );
817            let mut buf = DissectBuffer::new();
818            BfdDissector.dissect(&data, &mut buf, 0).unwrap();
819            let layer = &buf.layers()[0];
820            assert_eq!(
821                buf.field_by_name(layer, "state").unwrap().value,
822                FieldValue::U8(state)
823            );
824            assert_eq!(
825                buf.resolve_display_name(layer, "state_name"),
826                Some(names[state as usize])
827            );
828        }
829    }
830
831    #[test]
832    fn test_parse_with_auth_simple() {
833        // Simple Password authentication: type=1, key_id=1, password="secret"
834        let auth_data = [1, b's', b'e', b'c', b'r', b'e', b't']; // key_id + password
835        let data = build_bfd_with_auth(1, 0, 3, 3, 0x1000, 0x2000, 1, &auth_data);
836        let mut buf = DissectBuffer::new();
837        BfdDissector.dissect(&data, &mut buf, 0).unwrap();
838
839        let layer = &buf.layers()[0];
840        assert_eq!(
841            buf.field_by_name(layer, "auth_present").unwrap().value,
842            FieldValue::U8(1)
843        );
844        assert_eq!(
845            buf.field_by_name(layer, "auth_type").unwrap().value,
846            FieldValue::U8(1)
847        );
848        assert_eq!(
849            buf.resolve_display_name(layer, "auth_type_name"),
850            Some("Simple Password")
851        );
852        assert_eq!(
853            buf.field_by_name(layer, "auth_data").unwrap().value,
854            FieldValue::Bytes(&auth_data)
855        );
856    }
857
858    #[test]
859    fn test_parse_with_auth_sha1() {
860        // Keyed SHA1 authentication: type=4, key_id=1, reserved=0, seq=1, hash=20 bytes
861        let mut auth_data = vec![1, 0, 0, 0]; // key_id, reserved, reserved, reserved
862        auth_data.extend_from_slice(&1u32.to_be_bytes()); // sequence number
863        auth_data.extend_from_slice(&[0xAA; 20]); // SHA1 hash (20 bytes)
864        let data = build_bfd_with_auth(1, 0, 3, 3, 0x1000, 0x2000, 4, &auth_data);
865        let mut buf = DissectBuffer::new();
866        BfdDissector.dissect(&data, &mut buf, 0).unwrap();
867
868        let layer = &buf.layers()[0];
869        assert_eq!(
870            buf.field_by_name(layer, "auth_type").unwrap().value,
871            FieldValue::U8(4)
872        );
873        assert_eq!(
874            buf.resolve_display_name(layer, "auth_type_name"),
875            Some("Keyed SHA1")
876        );
877    }
878
879    #[test]
880    fn test_truncated_packet() {
881        let data = [0u8; 23]; // 23 < 24
882        let mut buf = DissectBuffer::new();
883        let result = BfdDissector.dissect(&data, &mut buf, 0);
884        assert!(result.is_err());
885        match result.unwrap_err() {
886            PacketError::Truncated { expected, actual } => {
887                assert_eq!(expected, 24);
888                assert_eq!(actual, 23);
889            }
890            other => panic!("Expected Truncated, got {other:?}"),
891        }
892    }
893
894    #[test]
895    fn test_invalid_length_field() {
896        // Length field set to 20, which is < MIN_HEADER_SIZE (24)
897        let data = build_bfd(
898            1, 0, 3, 0, 0, 0, 0, 0, 0, 3, 20, 1, 0, 1_000_000, 1_000_000, 0,
899        );
900        let mut buf = DissectBuffer::new();
901        let result = BfdDissector.dissect(&data, &mut buf, 0);
902        assert!(result.is_err());
903        match result.unwrap_err() {
904            PacketError::InvalidFieldValue { field, value } => {
905                assert_eq!(field, "length");
906                assert_eq!(value, 20);
907            }
908            other => panic!("Expected InvalidFieldValue, got {other:?}"),
909        }
910    }
911
912    #[test]
913    fn test_auth_present_but_truncated() {
914        // Auth bit set but length is only 24 (needs at least 26)
915        let data = build_bfd(
916            1, 0, 3, 0, 0, 0, 1, 0, 0, 3, 24, 1, 0, 1_000_000, 1_000_000, 0,
917        );
918        let mut buf = DissectBuffer::new();
919        let result = BfdDissector.dissect(&data, &mut buf, 0);
920        assert!(result.is_err());
921        match result.unwrap_err() {
922            PacketError::InvalidHeader(msg) => {
923                assert!(msg.contains("auth present"));
924            }
925            other => panic!("Expected InvalidHeader, got {other:?}"),
926        }
927    }
928
929    #[test]
930    fn test_dissect_with_offset() {
931        let data = build_bfd(
932            1, 0, 3, 0, 0, 0, 0, 0, 0, 3, 24, 1, 0, 1_000_000, 1_000_000, 0,
933        );
934        let offset = 42; // simulate preceding headers
935        let mut buf = DissectBuffer::new();
936        BfdDissector.dissect(&data, &mut buf, offset).unwrap();
937
938        let layer = &buf.layers()[0];
939        assert_eq!(layer.range, offset..offset + 24);
940        assert_eq!(
941            buf.field_by_name(layer, "required_min_echo_rx_interval")
942                .unwrap()
943                .range,
944            offset + 20..offset + 24
945        );
946    }
947
948    #[test]
949    fn test_field_descriptors() {
950        let descriptors = BfdDissector.field_descriptors();
951        assert_eq!(descriptors.len(), 18);
952        assert_eq!(descriptors[0].name, "version");
953        assert_eq!(descriptors[descriptors.len() - 1].name, "auth_data");
954        // Check optional fields
955        assert!(!descriptors[0].optional); // version
956        assert!(descriptors[16].optional); // auth_type
957        assert!(descriptors[17].optional); // auth_data
958    }
959
960    #[test]
961    fn test_length_exceeds_data() {
962        // Length field says 30 but only 24 bytes of data
963        let data = build_bfd(
964            1, 0, 3, 0, 0, 0, 0, 0, 0, 3, 30, 1, 0, 1_000_000, 1_000_000, 0,
965        );
966        let mut buf = DissectBuffer::new();
967        let result = BfdDissector.dissect(&data, &mut buf, 0);
968        assert!(result.is_err());
969        match result.unwrap_err() {
970            PacketError::Truncated { expected, actual } => {
971                assert_eq!(expected, 30);
972                assert_eq!(actual, 24);
973            }
974            other => panic!("Expected Truncated, got {other:?}"),
975        }
976    }
977
978    #[test]
979    fn test_invalid_version() {
980        // RFC 5880, Section 6.8.6 #1 — "If the version number is not correct
981        // (1), the packet MUST be discarded."
982        let data = build_bfd(
983            0, 0, 3, 0, 0, 0, 0, 0, 0, 3, 24, 1, 0, 1_000_000, 1_000_000, 0,
984        );
985        let mut buf = DissectBuffer::new();
986        let result = BfdDissector.dissect(&data, &mut buf, 0);
987        match result.unwrap_err() {
988            PacketError::InvalidFieldValue { field, value } => {
989                assert_eq!(field, "version");
990                assert_eq!(value, 0);
991            }
992            other => panic!("Expected InvalidFieldValue, got {other:?}"),
993        }
994
995        // Version 2 must also be rejected.
996        let data = build_bfd(
997            2, 0, 3, 0, 0, 0, 0, 0, 0, 3, 24, 1, 0, 1_000_000, 1_000_000, 0,
998        );
999        let mut buf = DissectBuffer::new();
1000        let result = BfdDissector.dissect(&data, &mut buf, 0);
1001        match result.unwrap_err() {
1002            PacketError::InvalidFieldValue { field, value } => {
1003                assert_eq!(field, "version");
1004                assert_eq!(value, 2);
1005            }
1006            other => panic!("Expected InvalidFieldValue, got {other:?}"),
1007        }
1008    }
1009
1010    #[test]
1011    fn test_detect_mult_zero() {
1012        // RFC 5880, Section 6.8.6 #4 — "If the Detect Mult field is zero, the
1013        // packet MUST be discarded."
1014        let data = build_bfd(
1015            1, 0, 3, 0, 0, 0, 0, 0, 0, 0, 24, 1, 0, 1_000_000, 1_000_000, 0,
1016        );
1017        let mut buf = DissectBuffer::new();
1018        let result = BfdDissector.dissect(&data, &mut buf, 0);
1019        match result.unwrap_err() {
1020            PacketError::InvalidFieldValue { field, value } => {
1021                assert_eq!(field, "detect_mult");
1022                assert_eq!(value, 0);
1023            }
1024            other => panic!("Expected InvalidFieldValue, got {other:?}"),
1025        }
1026    }
1027
1028    #[test]
1029    fn test_my_discriminator_zero() {
1030        // RFC 5880, Section 6.8.6 #6 — "If the My Discriminator field is zero,
1031        // the packet MUST be discarded."
1032        let data = build_bfd(
1033            1, 0, 3, 0, 0, 0, 0, 0, 0, 3, 24, 0, 0, 1_000_000, 1_000_000, 0,
1034        );
1035        let mut buf = DissectBuffer::new();
1036        let result = BfdDissector.dissect(&data, &mut buf, 0);
1037        match result.unwrap_err() {
1038            PacketError::InvalidFieldValue { field, value } => {
1039                assert_eq!(field, "my_discriminator");
1040                assert_eq!(value, 0);
1041            }
1042            other => panic!("Expected InvalidFieldValue, got {other:?}"),
1043        }
1044    }
1045
1046    #[test]
1047    fn test_parse_multipoint_bit_set() {
1048        // RFC 5880 originally required M to be zero; RFC 8562, Section 4.2 —
1049        // <https://www.rfc-editor.org/rfc/rfc8562#section-4.2> — redefines M=1
1050        // for MultipointHead sessions. The dissector therefore accepts M=1.
1051        let data = build_bfd(
1052            1, 0, 3, 0, 0, 0, 0, 0, 1, 3, 24, 1, 0, 1_000_000, 1_000_000, 0,
1053        );
1054        let mut buf = DissectBuffer::new();
1055        BfdDissector.dissect(&data, &mut buf, 0).unwrap();
1056        let layer = &buf.layers()[0];
1057        assert_eq!(
1058            buf.field_by_name(layer, "multipoint").unwrap().value,
1059            FieldValue::U8(1)
1060        );
1061    }
1062
1063    #[test]
1064    fn test_simple_password_too_short() {
1065        // RFC 5880, Section 4.2 — Simple Password Auth Len range is 4-19
1066        // (Type + Len + Key ID + 1-16 password bytes). Auth Len == 3 is
1067        // malformed (no password bytes).
1068        let mut pkt = build_bfd(
1069            1, 0, 3, 0, 0, 0, 1, 0, 0, 3, 27, 1, 0, 1_000_000, 1_000_000, 0,
1070        );
1071        pkt.push(1); // auth type: Simple Password
1072        pkt.push(3); // auth len: illegal, below minimum 4
1073        pkt.push(1); // key id
1074        let mut buf = DissectBuffer::new();
1075        let result = BfdDissector.dissect(&pkt, &mut buf, 0);
1076        match result.unwrap_err() {
1077            PacketError::InvalidHeader(msg) => {
1078                assert!(msg.contains("auth length"), "unexpected message: {msg}");
1079            }
1080            other => panic!("Expected InvalidHeader, got {other:?}"),
1081        }
1082    }
1083
1084    #[test]
1085    fn test_parse_with_auth_md5() {
1086        // Keyed MD5: type=2, auth_len=24, key_id=1, reserved=0, 4-byte seq,
1087        // 16-byte digest.
1088        let mut auth_data = vec![1, 0]; // key_id, reserved
1089        auth_data.extend_from_slice(&42u32.to_be_bytes()); // sequence number
1090        auth_data.extend_from_slice(&[0xBB; 16]); // MD5 digest
1091        let data = build_bfd_with_auth(1, 0, 3, 3, 0x1000, 0x2000, 2, &auth_data);
1092        let mut buf = DissectBuffer::new();
1093        BfdDissector.dissect(&data, &mut buf, 0).unwrap();
1094
1095        let layer = &buf.layers()[0];
1096        assert_eq!(
1097            buf.field_by_name(layer, "auth_type").unwrap().value,
1098            FieldValue::U8(2)
1099        );
1100        assert_eq!(
1101            buf.resolve_display_name(layer, "auth_type_name"),
1102            Some("Keyed MD5")
1103        );
1104    }
1105
1106    #[test]
1107    fn test_md5_wrong_length() {
1108        // Keyed MD5 requires Auth Len == 24. Using 20 must be rejected.
1109        let mut pkt = build_bfd(
1110            1, 0, 3, 0, 0, 0, 1, 0, 0, 3, 44, 1, 0, 1_000_000, 1_000_000, 0,
1111        );
1112        pkt.push(2); // auth type: Keyed MD5
1113        pkt.push(20); // illegal: below required 24
1114        pkt.extend_from_slice(&[0u8; 18]); // fill remaining bytes
1115        let mut buf = DissectBuffer::new();
1116        let result = BfdDissector.dissect(&pkt, &mut buf, 0);
1117        match result.unwrap_err() {
1118            PacketError::InvalidHeader(msg) => {
1119                assert!(msg.contains("auth length"), "unexpected message: {msg}");
1120            }
1121            other => panic!("Expected InvalidHeader, got {other:?}"),
1122        }
1123    }
1124
1125    #[test]
1126    fn test_sha1_wrong_length() {
1127        // Keyed SHA1 requires Auth Len == 28. Using 24 must be rejected.
1128        let mut pkt = build_bfd(
1129            1, 0, 3, 0, 0, 0, 1, 0, 0, 3, 48, 1, 0, 1_000_000, 1_000_000, 0,
1130        );
1131        pkt.push(4); // auth type: Keyed SHA1
1132        pkt.push(24); // illegal: below required 28
1133        pkt.extend_from_slice(&[0u8; 22]); // fill remaining bytes
1134        let mut buf = DissectBuffer::new();
1135        let result = BfdDissector.dissect(&pkt, &mut buf, 0);
1136        match result.unwrap_err() {
1137            PacketError::InvalidHeader(msg) => {
1138                assert!(msg.contains("auth length"), "unexpected message: {msg}");
1139            }
1140            other => panic!("Expected InvalidHeader, got {other:?}"),
1141        }
1142    }
1143
1144    #[test]
1145    fn test_auth_section_exceeds_length() {
1146        // Auth Len extends beyond the packet length field.
1147        let mut pkt = build_bfd(
1148            1, 0, 3, 0, 0, 0, 1, 0, 0, 3, 28, 1, 0, 1_000_000, 1_000_000, 0,
1149        );
1150        pkt.push(1); // auth type: Simple Password
1151        pkt.push(10); // auth len: 10, but only 4 bytes available (24..28)
1152        pkt.push(1);
1153        pkt.push(b'x');
1154        let mut buf = DissectBuffer::new();
1155        let result = BfdDissector.dissect(&pkt, &mut buf, 0);
1156        match result.unwrap_err() {
1157            PacketError::InvalidHeader(msg) => {
1158                assert!(msg.contains("exceeds"), "unexpected message: {msg}");
1159            }
1160            other => panic!("Expected InvalidHeader, got {other:?}"),
1161        }
1162    }
1163}