Skip to main content

bgpkit_parser/parser/iters/
diagnostic.rs

1//! Record-level diagnostic iterator for malformed MRT data investigation.
2
3use crate::error::{BgpValidationWarning, ParserError};
4use crate::models::*;
5use crate::parser::mrt::mrt_record::{chunk_mrt_record_with_context, raw_record_uses_zebra_compat};
6use crate::parser::mrt::RawMrtRecord;
7use crate::parser::BgpkitParser;
8use std::io::Read;
9
10/// A record-level parsing outcome for malformed-data investigation.
11#[derive(Debug)]
12#[non_exhaustive]
13pub enum DiagnosticEvent {
14    /// A fully parsed record with no RFC 7606 validation findings.
15    Record(MrtRecord),
16    /// A parsed record with one or more recoverable validation findings.
17    Validation {
18        /// The parsed MRT record.
19        record: MrtRecord,
20        /// RFC 7606 validation findings in record traversal order.
21        warnings: Vec<BgpValidationWarning>,
22        /// The original MRT record bytes and header.
23        raw_record: RawMrtRecord,
24    },
25    /// A record that could not be fully parsed.
26    ParseError {
27        /// The parsing failure.
28        error: ParserError,
29        /// The MRT common header when framing completed successfully.
30        common_header: Option<CommonHeader>,
31        /// All bytes consumed for this record, when available.
32        raw_bytes: Option<Vec<u8>>,
33    },
34}
35
36/// Iterator over record-level parsing diagnostics.
37pub struct DiagnosticIterator<R> {
38    parser: BgpkitParser<R>,
39    terminated: bool,
40}
41
42impl<R> DiagnosticIterator<R> {
43    pub(crate) fn new(parser: BgpkitParser<R>) -> Self {
44        Self {
45            parser,
46            terminated: false,
47        }
48    }
49}
50
51impl<R: Read> Iterator for DiagnosticIterator<R> {
52    type Item = DiagnosticEvent;
53
54    fn next(&mut self) -> Option<Self::Item> {
55        // Text dumps have no MRT record boundary or raw MRT representation.
56        if self.terminated || self.parser.text_dump_iter.is_some() {
57            return None;
58        }
59
60        let raw_record = match chunk_mrt_record_with_context(&mut self.parser.reader) {
61            Ok(raw_record) => raw_record,
62            Err(error) if matches!(error.error, ParserError::EofExpected) => {
63                self.terminated = true;
64                return None;
65            }
66            Err(error) => {
67                // A framing error may leave the reader between record boundaries. Do not
68                // attempt to reinterpret the remaining bytes as another MRT header.
69                self.terminated = true;
70                return Some(DiagnosticEvent::ParseError {
71                    error: error.error,
72                    common_header: error.common_header,
73                    raw_bytes: error.bytes,
74                });
75            }
76        };
77
78        let used_zebra_compat = raw_record_uses_zebra_compat(&raw_record);
79        let record = match raw_record.clone().parse() {
80            Ok(record) => record,
81            Err(error) => {
82                return Some(DiagnosticEvent::ParseError {
83                    error,
84                    common_header: Some(raw_record.common_header),
85                    raw_bytes: Some(raw_record.raw_bytes().to_vec()),
86                });
87            }
88        };
89        if used_zebra_compat {
90            self.parser.warn_zebra_compat_once();
91        }
92
93        let warnings = record_validation_warnings(&record);
94        if warnings.is_empty() {
95            Some(DiagnosticEvent::Record(record))
96        } else {
97            Some(DiagnosticEvent::Validation {
98                record,
99                warnings,
100                raw_record,
101            })
102        }
103    }
104}
105
106fn record_validation_warnings(record: &MrtRecord) -> Vec<BgpValidationWarning> {
107    match &record.message {
108        MrtMessage::Bgp4Mp(Bgp4MpEnum::Message(message)) => update_warnings(&message.bgp_message),
109        MrtMessage::LegacyBgp(LegacyBgp::Message(message)) => update_warnings(&message.bgp_message),
110        MrtMessage::TableDumpMessage(message) => message.attributes.validation_warnings().to_vec(),
111        MrtMessage::TableDumpMessageBatch(messages) => messages
112            .iter()
113            .flat_map(|message| message.attributes.validation_warnings().iter().cloned())
114            .collect(),
115        MrtMessage::TableDumpV2Message(TableDumpV2Message::RibAfi(rib)) => rib
116            .rib_entries
117            .iter()
118            .flat_map(|entry| entry.attributes.validation_warnings().iter().cloned())
119            .collect(),
120        _ => Vec::new(),
121    }
122}
123
124fn update_warnings(message: &BgpMessage) -> Vec<BgpValidationWarning> {
125    match message {
126        BgpMessage::Update(update) => update.attributes.validation_warnings().to_vec(),
127        BgpMessage::RouteRefresh(refresh) => refresh.validation_warnings(),
128        _ => Vec::new(),
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135    use std::io::Cursor;
136    use std::net::IpAddr;
137    use std::str::FromStr;
138
139    fn update_record(attributes: Attributes) -> MrtRecord {
140        MrtRecord {
141            common_header: CommonHeader {
142                timestamp: 1_700_000_000,
143                microsecond_timestamp: None,
144                entry_type: EntryType::BGP4MP,
145                entry_subtype: Bgp4MpType::MessageAs4 as u16,
146                length: 0,
147            },
148            message: MrtMessage::Bgp4Mp(Bgp4MpEnum::Message(Bgp4MpMessage {
149                msg_type: Bgp4MpType::MessageAs4,
150                peer_asn: Asn::new_32bit(64496),
151                local_asn: Asn::new_32bit(64497),
152                interface_index: 0,
153                peer_ip: IpAddr::from_str("192.0.2.1").unwrap(),
154                local_ip: IpAddr::from_str("192.0.2.2").unwrap(),
155                bgp_message: BgpMessage::Update(BgpUpdateMessage {
156                    withdrawn_prefixes: vec![],
157                    attributes,
158                    announced_prefixes: vec![NetworkPrefix::from_str("203.0.113.0/24").unwrap()],
159                }),
160            })),
161        }
162    }
163
164    fn valid_attributes() -> Attributes {
165        let mut attributes = Attributes::default();
166        attributes.add_attr(AttributeValue::Origin(Origin::IGP).into());
167        attributes.add_attr(
168            AttributeValue::AsPath {
169                path: AsPath::from_sequence([64500]),
170                is_as4: false,
171            }
172            .into(),
173        );
174        attributes
175            .add_attr(AttributeValue::NextHop(IpAddr::from_str("192.0.2.254").unwrap()).into());
176        attributes
177    }
178
179    fn update_wire_body(withdrawn: &[u8], attributes: &[u8], announced: &[u8]) -> Vec<u8> {
180        let mut body = Vec::new();
181        body.extend_from_slice(&(withdrawn.len() as u16).to_be_bytes());
182        body.extend_from_slice(withdrawn);
183        body.extend_from_slice(&(attributes.len() as u16).to_be_bytes());
184        body.extend_from_slice(attributes);
185        body.extend_from_slice(announced);
186        body
187    }
188
189    fn bgp4mp_update_wire(withdrawn: &[u8], attributes: &[u8], announced: &[u8]) -> Vec<u8> {
190        let update_body = update_wire_body(withdrawn, attributes, announced);
191        bgp4mp_message_wire(BgpMessageType::UPDATE, &update_body)
192    }
193
194    fn bgp4mp_route_refresh_wire(subtype: u8, orf_data: &[u8]) -> Vec<u8> {
195        let mut refresh_body = vec![0x00, 0x01, subtype, 0x01];
196        refresh_body.extend_from_slice(orf_data);
197        bgp4mp_message_wire(BgpMessageType::ROUTE_REFRESH, &refresh_body)
198    }
199
200    fn bgp4mp_message_wire(msg_type: BgpMessageType, msg_body: &[u8]) -> Vec<u8> {
201        let mut bgp_message = vec![0xff; 16];
202        bgp_message.extend_from_slice(&((19 + msg_body.len()) as u16).to_be_bytes());
203        bgp_message.push(msg_type as u8);
204        bgp_message.extend_from_slice(msg_body);
205
206        let mut body = Vec::new();
207        body.extend_from_slice(&64496u32.to_be_bytes());
208        body.extend_from_slice(&64497u32.to_be_bytes());
209        body.extend_from_slice(&0u16.to_be_bytes());
210        body.extend_from_slice(&1u16.to_be_bytes());
211        body.extend_from_slice(&[192, 0, 2, 1]);
212        body.extend_from_slice(&[192, 0, 2, 2]);
213        body.extend_from_slice(&bgp_message);
214
215        let header = CommonHeader {
216            timestamp: 1_700_000_000,
217            microsecond_timestamp: None,
218            entry_type: EntryType::BGP4MP,
219            entry_subtype: Bgp4MpType::MessageAs4 as u16,
220            length: body.len() as u32,
221        };
222        let mut wire = header.encode().to_vec();
223        wire.extend_from_slice(&body);
224        wire
225    }
226
227    fn valid_update_attributes_wire() -> Vec<u8> {
228        vec![
229            0x40, 0x01, 0x01, 0x00, // ORIGIN = IGP
230            0x40, 0x02, 0x00, // AS_PATH = empty
231            0x40, 0x03, 0x04, 192, 0, 2, 254, // NEXT_HOP
232        ]
233    }
234
235    fn assert_wire_validation(
236        wire: Vec<u8>,
237        expected_warning: impl Fn(&BgpValidationWarning) -> bool,
238    ) {
239        let mut iter = BgpkitParser::from_reader(Cursor::new(wire.clone())).into_diagnostic_iter();
240        match iter.next().unwrap() {
241            DiagnosticEvent::Validation {
242                warnings,
243                raw_record,
244                ..
245            } => {
246                assert!(
247                    warnings.iter().any(expected_warning),
248                    "expected validation warning, got {warnings:?}"
249                );
250                assert_eq!(raw_record.raw_bytes().as_ref(), wire.as_slice());
251            }
252            event => panic!("expected validation event, got {event:?}"),
253        }
254        assert!(iter.next().is_none());
255    }
256
257    #[test]
258    fn diagnostic_iterator_flags_unknown_route_refresh_subtype() {
259        // RFC 7313 Section 5: subtype other than 0-2 is ignored on the wire
260        let wire = bgp4mp_route_refresh_wire(3, &[]);
261        assert_wire_validation(wire, |warning| {
262            matches!(
263                warning,
264                BgpValidationWarning::UnknownRouteRefreshSubtype { subtype: 3 }
265            )
266        });
267    }
268
269    #[test]
270    fn diagnostic_iterator_flags_borr_with_trailing_data() {
271        // RFC 7313 Section 5: a BoRR/EoRR body must be exactly 4 bytes
272        let wire = bgp4mp_route_refresh_wire(1, &[0xDE, 0xAD]);
273        assert_wire_validation(wire, |warning| {
274            matches!(
275                warning,
276                BgpValidationWarning::InvalidRouteRefreshLength {
277                    subtype: 1,
278                    length: 6
279                }
280            )
281        });
282    }
283
284    #[test]
285    fn diagnostic_iterator_passes_normal_route_refresh_with_orf_data_clean() {
286        // A normal (subtype 0) refresh may carry ORF data (RFC 5291)
287        let wire = bgp4mp_route_refresh_wire(0, &[0x01, 0x80, 0x00, 0x00]);
288        let mut iter = BgpkitParser::from_reader(Cursor::new(wire)).into_diagnostic_iter();
289        match iter.next().unwrap() {
290            DiagnosticEvent::Record(record) => {
291                assert!(matches!(
292                    record.message,
293                    MrtMessage::Bgp4Mp(Bgp4MpEnum::Message(Bgp4MpMessage {
294                        bgp_message: BgpMessage::RouteRefresh(_),
295                        ..
296                    }))
297                ));
298            }
299            event => panic!("expected clean record event, got {event:?}"),
300        }
301        assert!(iter.next().is_none());
302    }
303
304    #[test]
305    fn diagnostic_iterator_yields_clean_record() {
306        let wire = update_record(valid_attributes()).encode().unwrap().to_vec();
307        let mut iter = BgpkitParser::from_reader(Cursor::new(wire)).into_diagnostic_iter();
308
309        assert!(matches!(iter.next(), Some(DiagnosticEvent::Record(_))));
310        assert!(iter.next().is_none());
311    }
312
313    #[test]
314    fn diagnostic_iterator_yields_validation_with_original_bytes() {
315        let wire = update_record(Attributes::default())
316            .encode()
317            .unwrap()
318            .to_vec();
319        let mut iter = BgpkitParser::from_reader(Cursor::new(wire.clone())).into_diagnostic_iter();
320
321        match iter.next().unwrap() {
322            DiagnosticEvent::Validation {
323                record,
324                warnings,
325                raw_record,
326            } => {
327                assert!(matches!(record.message, MrtMessage::Bgp4Mp(_)));
328                assert!(warnings.iter().any(|warning| {
329                    matches!(
330                        warning,
331                        BgpValidationWarning::MissingWellKnownAttribute {
332                            attr_type: AttrType::ORIGIN
333                        }
334                    )
335                }));
336                assert_eq!(raw_record.raw_bytes().as_ref(), wire.as_slice());
337            }
338            event => panic!("expected validation event, got {event:?}"),
339        }
340    }
341
342    #[test]
343    fn diagnostic_iterator_preserves_body_error_and_continues() {
344        let header = CommonHeader {
345            timestamp: 2,
346            microsecond_timestamp: None,
347            entry_type: EntryType::TABLE_DUMP,
348            entry_subtype: 0,
349            length: 4,
350        };
351        let mut input = header.encode().to_vec();
352        input.extend_from_slice(&[0xff; 4]);
353        let invalid_record = input.clone();
354        input.extend_from_slice(&update_record(valid_attributes()).encode().unwrap());
355
356        let mut iter = BgpkitParser::from_reader(Cursor::new(input)).into_diagnostic_iter();
357        match iter.next().unwrap() {
358            DiagnosticEvent::ParseError {
359                common_header,
360                raw_bytes,
361                ..
362            } => {
363                assert_eq!(common_header, Some(header));
364                assert_eq!(raw_bytes.as_deref(), Some(invalid_record.as_slice()));
365            }
366            event => panic!("expected parse error event, got {event:?}"),
367        }
368        assert!(matches!(iter.next(), Some(DiagnosticEvent::Record(_))));
369        assert!(iter.next().is_none());
370    }
371
372    #[test]
373    fn diagnostic_iterator_preserves_partial_body_context() {
374        let header = CommonHeader {
375            timestamp: 3,
376            microsecond_timestamp: None,
377            entry_type: EntryType::BGP4MP,
378            entry_subtype: 0,
379            length: 4,
380        };
381        let mut input = header.encode().to_vec();
382        input.extend_from_slice(&[0, 1]);
383
384        match BgpkitParser::from_reader(Cursor::new(input.clone()))
385            .into_diagnostic_iter()
386            .next()
387            .unwrap()
388        {
389            DiagnosticEvent::ParseError {
390                common_header,
391                raw_bytes,
392                ..
393            } => {
394                assert_eq!(common_header, Some(header));
395                assert_eq!(raw_bytes.as_deref(), Some(input.as_slice()));
396            }
397            event => panic!("expected parse error event, got {event:?}"),
398        }
399    }
400
401    #[test]
402    fn diagnostic_iterator_terminates_after_framing_error() {
403        let header = CommonHeader {
404            timestamp: 3,
405            microsecond_timestamp: None,
406            entry_type: EntryType::BGP4MP,
407            entry_subtype: Bgp4MpType::MessageAs4 as u16,
408            length: 16 * 1024 * 1024 + 1,
409        };
410        let mut input = header.encode().to_vec();
411        input.extend_from_slice(&update_record(valid_attributes()).encode().unwrap());
412
413        let mut iter = BgpkitParser::from_reader(Cursor::new(input)).into_diagnostic_iter();
414        match iter.next().unwrap() {
415            DiagnosticEvent::ParseError {
416                common_header,
417                raw_bytes,
418                ..
419            } => {
420                assert_eq!(common_header, Some(header));
421                assert_eq!(raw_bytes.as_deref(), Some(header.encode().as_ref()));
422            }
423            event => panic!("expected parse error event, got {event:?}"),
424        }
425        assert!(iter.next().is_none());
426    }
427
428    #[test]
429    fn diagnostic_iterator_preserves_invalid_header_bytes() {
430        let invalid_header = vec![
431            0, 0, 0, 1, // timestamp
432            0xff, 0xff, // unknown entry type
433            0, 0, // subtype
434            0, 0, 0, 0, // length
435        ];
436
437        match BgpkitParser::from_reader(Cursor::new(invalid_header.clone()))
438            .into_diagnostic_iter()
439            .next()
440            .unwrap()
441        {
442            DiagnosticEvent::ParseError {
443                common_header,
444                raw_bytes,
445                ..
446            } => {
447                assert!(common_header.is_none());
448                assert_eq!(raw_bytes.as_deref(), Some(invalid_header.as_slice()));
449            }
450            event => panic!("expected parse error event, got {event:?}"),
451        }
452    }
453
454    #[test]
455    fn diagnostic_iterator_ignores_filters() {
456        let wire = update_record(valid_attributes()).encode().unwrap().to_vec();
457        let parser = BgpkitParser::from_reader(Cursor::new(wire))
458            .add_filter("prefix", "198.51.100.0/24")
459            .unwrap();
460
461        assert!(matches!(
462            parser.into_diagnostic_iter().next(),
463            Some(DiagnosticEvent::Record(_))
464        ));
465    }
466
467    #[test]
468    fn diagnostic_iterator_reports_wire_level_nlri_warnings_and_continues() {
469        let malformed_nlri = [0xc8, 0x01];
470        let invalid_wire = bgp4mp_update_wire(
471            &malformed_nlri,
472            &valid_update_attributes_wire(),
473            &malformed_nlri,
474        );
475        let mut input = invalid_wire.clone();
476        input.extend_from_slice(&update_record(valid_attributes()).encode().unwrap());
477
478        let mut iter = BgpkitParser::from_reader(Cursor::new(input)).into_diagnostic_iter();
479        match iter.next().unwrap() {
480            DiagnosticEvent::Validation {
481                warnings,
482                raw_record,
483                ..
484            } => {
485                assert!(warnings.iter().any(|warning| {
486                    matches!(
487                        warning,
488                        BgpValidationWarning::MalformedNlri {
489                            nlri_type: "withdrawn",
490                            raw_bytes,
491                            ..
492                        } if raw_bytes == &malformed_nlri
493                    )
494                }));
495                assert!(warnings.iter().any(|warning| {
496                    matches!(
497                        warning,
498                        BgpValidationWarning::MalformedNlri {
499                            nlri_type: "announced",
500                            raw_bytes,
501                            ..
502                        } if raw_bytes == &malformed_nlri
503                    )
504                }));
505                assert_eq!(raw_record.raw_bytes().as_ref(), invalid_wire.as_slice());
506            }
507            event => panic!("expected validation event, got {event:?}"),
508        }
509        assert!(matches!(iter.next(), Some(DiagnosticEvent::Record(_))));
510        assert!(iter.next().is_none());
511    }
512
513    #[test]
514    fn diagnostic_iterator_reports_wire_level_attribute_warnings() {
515        let announced = [0x18, 10, 0, 0];
516        let mut flags_and_duplicate = valid_update_attributes_wire();
517        flags_and_duplicate[0] = 0x80;
518        flags_and_duplicate.extend_from_slice(&[0x40, 0x01, 0x01, 0x00]);
519        assert_wire_validation(
520            bgp4mp_update_wire(&[], &flags_and_duplicate, &announced),
521            |warning| {
522                matches!(
523                    warning,
524                    BgpValidationWarning::AttributeFlagsError {
525                        attr_type: AttrType::ORIGIN,
526                        ..
527                    }
528                )
529            },
530        );
531        assert_wire_validation(
532            bgp4mp_update_wire(&[], &flags_and_duplicate, &announced),
533            |warning| {
534                matches!(
535                    warning,
536                    BgpValidationWarning::DuplicateAttribute {
537                        attr_type: AttrType::ORIGIN
538                    }
539                )
540            },
541        );
542
543        let mut invalid_origin = valid_update_attributes_wire();
544        invalid_origin[3] = 3;
545        assert_wire_validation(
546            bgp4mp_update_wire(&[], &invalid_origin, &announced),
547            |warning| matches!(warning, BgpValidationWarning::MalformedAttributeList { .. }),
548        );
549
550        let malformed_as_path = vec![
551            0x40, 0x01, 0x01, 0x00, // ORIGIN = IGP
552            0x40, 0x02, 0x02, 0x02, 0x01, // AS_PATH segment lacks its ASN
553            0x40, 0x03, 0x04, 192, 0, 2, 254, // NEXT_HOP
554        ];
555        assert_wire_validation(
556            bgp4mp_update_wire(&[], &malformed_as_path, &announced),
557            |warning| matches!(warning, BgpValidationWarning::MalformedAttributeList { .. }),
558        );
559
560        let invalid_next_hop = vec![
561            0x40, 0x01, 0x01, 0x00, // ORIGIN = IGP
562            0x40, 0x02, 0x00, // AS_PATH = empty
563            0x40, 0x03, 0x03, 192, 0, 2, // NEXT_HOP has the wrong length
564        ];
565        assert_wire_validation(
566            bgp4mp_update_wire(&[], &invalid_next_hop, &announced),
567            |warning| {
568                matches!(
569                    warning,
570                    BgpValidationWarning::AttributeLengthError {
571                        attr_type: AttrType::NEXT_HOP,
572                        ..
573                    }
574                )
575            },
576        );
577    }
578
579    #[test]
580    fn diagnostic_iterator_collects_table_dump_batch_and_rib_warnings() {
581        let table_dump_messages = vec![
582            TableDumpMessage {
583                view_number: 0,
584                sequence_number: 1,
585                prefix: NetworkPrefix::from_str("192.0.2.0/24").unwrap(),
586                status: 1,
587                originated_time: 1,
588                peer_ip: IpAddr::from_str("192.0.2.1").unwrap(),
589                peer_asn: Asn::new_16bit(64512),
590                attributes: Attributes::default(),
591            },
592            TableDumpMessage {
593                view_number: 0,
594                sequence_number: 1,
595                prefix: NetworkPrefix::from_str("198.51.100.0/24").unwrap(),
596                status: 1,
597                originated_time: 2,
598                peer_ip: IpAddr::from_str("192.0.2.2").unwrap(),
599                peer_asn: Asn::new_16bit(64513),
600                attributes: Attributes::default(),
601            },
602        ];
603        let table_dump_record = MrtRecord {
604            common_header: CommonHeader {
605                timestamp: 1,
606                microsecond_timestamp: None,
607                entry_type: EntryType::TABLE_DUMP,
608                entry_subtype: 1,
609                length: 0,
610            },
611            message: MrtMessage::TableDumpMessageBatch(table_dump_messages),
612        };
613        let table_dump_wire = table_dump_record.encode().unwrap().to_vec();
614        match BgpkitParser::from_reader(Cursor::new(table_dump_wire.clone()))
615            .into_diagnostic_iter()
616            .next()
617            .unwrap()
618        {
619            DiagnosticEvent::Validation {
620                record,
621                warnings,
622                raw_record,
623            } => {
624                assert!(matches!(
625                    record.message,
626                    MrtMessage::TableDumpMessageBatch(_)
627                ));
628                assert_eq!(warnings.len(), 6);
629                assert_eq!(raw_record.raw_bytes().as_ref(), table_dump_wire.as_slice());
630            }
631            event => panic!("expected validation event, got {event:?}"),
632        }
633
634        let rib_record = MrtRecord {
635            common_header: CommonHeader {
636                timestamp: 2,
637                microsecond_timestamp: None,
638                entry_type: EntryType::TABLE_DUMP_V2,
639                entry_subtype: TableDumpV2Type::RibIpv4Unicast as u16,
640                length: 0,
641            },
642            message: MrtMessage::TableDumpV2Message(TableDumpV2Message::RibAfi(RibAfiEntries {
643                rib_type: TableDumpV2Type::RibIpv4Unicast,
644                sequence_number: 1,
645                prefix: NetworkPrefix::from_str("203.0.113.0/24").unwrap(),
646                rib_entries: vec![RibEntry {
647                    peer_index: 0,
648                    originated_time: 1,
649                    path_id: None,
650                    attributes: Attributes::default(),
651                }],
652            })),
653        };
654        let rib_wire = rib_record.encode().unwrap().to_vec();
655        match BgpkitParser::from_reader(Cursor::new(rib_wire.clone()))
656            .into_diagnostic_iter()
657            .next()
658            .unwrap()
659        {
660            DiagnosticEvent::Validation {
661                record,
662                warnings,
663                raw_record,
664            } => {
665                assert!(matches!(
666                    record.message,
667                    MrtMessage::TableDumpV2Message(TableDumpV2Message::RibAfi(_))
668                ));
669                assert_eq!(warnings.len(), 3);
670                assert_eq!(raw_record.raw_bytes().as_ref(), rib_wire.as_slice());
671            }
672            event => panic!("expected validation event, got {event:?}"),
673        }
674    }
675}