1use crate::models::{AsnLength, DissectionNode};
11use crate::parser::bgp::dissect::{dissect_bgp_message_base, read_u16};
12use crate::parser::mrt::RawMrtRecord;
13
14fn entry_type_name(entry_type: u16) -> &'static str {
15 match entry_type {
16 12 => "TABLE_DUMP",
17 13 => "TABLE_DUMP_V2",
18 16 => "BGP4MP",
19 17 => "BGP4MP_ET",
20 32 => "ISIS",
21 33 => "ISIS_ET",
22 48 => "OSPFv3",
23 49 => "OSPFv3_ET",
24 _ => "UNKNOWN",
25 }
26}
27
28pub fn dissect_mrt_record(raw: &RawMrtRecord) -> DissectionNode {
33 let mut buf = Vec::with_capacity(raw.total_bytes_len());
34 buf.extend_from_slice(&raw.header_bytes);
35 buf.extend_from_slice(&raw.message_bytes);
36 dissect_mrt_bytes(&buf)
37}
38
39pub fn dissect_mrt_bytes(data: &[u8]) -> DissectionNode {
44 let mut root = DissectionNode::new(
45 "mrt",
46 format!("MRT record ({} bytes)", data.len()),
47 0,
48 data.len() as u32,
49 );
50 if data.len() < 12 {
51 if !data.is_empty() {
52 root.children.push(DissectionNode::new(
53 "mrt.header",
54 format!("Common header (truncated, {} of 12 bytes)", data.len()),
55 0,
56 data.len() as u32,
57 ));
58 }
59 return root;
60 }
61
62 let entry_type = read_u16(data, 4).unwrap_or(0);
63 let sub_type = read_u16(data, 6).unwrap_or(0);
64
65 let is_et = matches!(entry_type, 17 | 33 | 49);
70 let header_len = if is_et { 16 } else { 12 };
71
72 let mut header = DissectionNode::new(
73 "mrt.header",
74 "Common header",
75 0,
76 header_len.min(data.len()) as u32,
77 );
78 let timestamp = u32::from_be_bytes([data[0], data[1], data[2], data[3]]);
79 header.children.push(DissectionNode::new(
80 "mrt.header.timestamp",
81 format!("Timestamp: {timestamp}"),
82 0,
83 4,
84 ));
85 header.children.push(DissectionNode::new(
86 "mrt.header.type",
87 format!("Type: {} ({entry_type})", entry_type_name(entry_type)),
88 4,
89 2,
90 ));
91 header.children.push(DissectionNode::new(
92 "mrt.header.subtype",
93 format!("Subtype: {sub_type}"),
94 6,
95 2,
96 ));
97 let length = u32::from_be_bytes([data[8], data[9], data[10], data[11]]);
98 header.children.push(DissectionNode::new(
99 "mrt.header.length",
100 format!("Length: {length}"),
101 8,
102 4,
103 ));
104 if is_et {
105 if data.len() >= 16 {
106 header.children.push(DissectionNode::new(
107 "mrt.header.microsecond",
108 format!(
109 "Microsecond timestamp: {}",
110 u32::from_be_bytes([data[12], data[13], data[14], data[15]])
111 ),
112 12,
113 4,
114 ));
115 } else {
116 header.children.push(DissectionNode::new(
117 "mrt.header.microsecond",
118 format!(
119 "Microsecond timestamp (truncated: {} of 4 bytes)",
120 data.len() - 12
121 ),
122 12,
123 (data.len() - 12) as u32,
124 ));
125 root.children.push(header);
126 return root;
127 }
128 }
129 root.children.push(header);
130
131 let body = &data[header_len..];
132 let body_base = header_len as u32;
133 if body.is_empty() {
134 return root;
135 }
136
137 let body_node = match entry_type {
138 16 | 17 => dissect_bgp4mp(body, body_base, sub_type),
139 _ => DissectionNode::new(
140 "mrt.body",
141 format!("Message body ({} bytes)", body.len()),
142 body_base,
143 body.len() as u32,
144 ),
145 };
146 root.children.push(body_node);
147 root
148}
149
150fn dissect_bgp4mp(data: &[u8], base: u32, sub_type: u16) -> DissectionNode {
159 let as4 = matches!(sub_type, 4 | 5 | 7 | 9 | 11);
160 let asn_size = if as4 { 4 } else { 2 };
161 let add_path = matches!(sub_type, 8..=11);
162 let asn_len = if as4 {
163 AsnLength::Bits32
164 } else {
165 AsnLength::Bits16
166 };
167 let is_message = !matches!(sub_type, 0 | 5);
168
169 let mut node = DissectionNode::new(
170 "mrt.bgp4mp",
171 format!(
172 "BGP4MP {} ({sub_type})",
173 if is_message {
174 "message"
175 } else {
176 "state change"
177 }
178 ),
179 base,
180 data.len() as u32,
181 );
182
183 let mut pos = 0usize;
184
185 if data.len() < 2 * asn_size {
186 node.children.push(DissectionNode::new(
187 "mrt.bgp4mp.truncated",
188 format!(
189 "Subheader (truncated: {} of at least {} bytes)",
190 data.len(),
191 2 * asn_size
192 ),
193 base,
194 data.len() as u32,
195 ));
196 return node;
197 }
198
199 let peer_asn = read_asn(data, 0, asn_size);
200 let local_asn = read_asn(data, asn_size, asn_size);
201 push_field(
202 &mut node,
203 "mrt.bgp4mp.peer_asn",
204 format!("Peer ASN: {peer_asn}"),
205 base,
206 asn_size,
207 );
208 pos += asn_size;
209 push_field(
210 &mut node,
211 "mrt.bgp4mp.local_asn",
212 format!("Local ASN: {local_asn}"),
213 base + pos as u32,
214 asn_size,
215 );
216 pos += asn_size;
217
218 let rest = &data[pos..];
221 if rest.len() >= 16 && rest[..16].iter().all(|b| *b == 0xFF) {
222 node.children.push(DissectionNode::new(
223 "mrt.bgp4mp.zebra_compat",
224 "Missing interface/AFI/address fields (old Zebra record)",
225 base + pos as u32,
226 0,
227 ));
228 node.children.push(dissect_bgp_message_base(
229 rest,
230 base + pos as u32,
231 &asn_len,
232 add_path,
233 ));
234 return node;
235 }
236
237 if !is_message && asn_size == 2 && data.len() == 8 {
241 let old_state = read_u16(data, pos).unwrap_or(0);
242 push_field(
243 &mut node,
244 "mrt.bgp4mp.old_state",
245 format!("Old state: {} ({})", old_state, bgp_state_name(old_state)),
246 base + pos as u32,
247 2,
248 );
249 let new_state = read_u16(data, pos + 2).unwrap_or(0);
250 push_field(
251 &mut node,
252 "mrt.bgp4mp.new_state",
253 format!("New state: {} ({})", new_state, bgp_state_name(new_state)),
254 base + pos as u32 + 2,
255 2,
256 );
257 return node;
258 }
259
260 let Some(pos) = walk_bgp4mp_addresses(&mut node, data, base, pos) else {
262 return node;
263 };
264
265 if is_message {
266 node.children.push(dissect_bgp_message_base(
267 &data[pos..],
268 base + pos as u32,
269 &asn_len,
270 add_path,
271 ));
272 } else {
273 if data.len() < pos + 4 {
275 node.children.push(DissectionNode::new(
276 "mrt.bgp4mp.truncated",
277 "Truncated old/new states",
278 base + pos as u32,
279 (data.len() - pos) as u32,
280 ));
281 return node;
282 }
283 let old_state = read_u16(data, pos).unwrap_or(0);
284 push_field(
285 &mut node,
286 "mrt.bgp4mp.old_state",
287 format!("Old state: {} ({})", old_state, bgp_state_name(old_state)),
288 base + pos as u32,
289 2,
290 );
291 let new_state = read_u16(data, pos + 2).unwrap_or(0);
292 push_field(
293 &mut node,
294 "mrt.bgp4mp.new_state",
295 format!("New state: {} ({})", new_state, bgp_state_name(new_state)),
296 base + pos as u32 + 2,
297 2,
298 );
299 }
300
301 node
302}
303
304fn walk_bgp4mp_addresses(
309 node: &mut DissectionNode,
310 data: &[u8],
311 base: u32,
312 mut pos: usize,
313) -> Option<usize> {
314 if data.len() < pos + 4 {
315 node.children.push(DissectionNode::new(
316 "mrt.bgp4mp.truncated",
317 "Truncated before interface index / AFI",
318 base + pos as u32,
319 (data.len() - pos) as u32,
320 ));
321 return None;
322 }
323 let afi = read_u16(data, pos + 2).unwrap_or(0);
324 push_field(
325 node,
326 "mrt.bgp4mp.interface_index",
327 format!("Interface index: {}", read_u16(data, pos).unwrap_or(0)),
328 base + pos as u32,
329 2,
330 );
331 pos += 2;
332 push_field(
333 node,
334 "mrt.bgp4mp.afi",
335 format!("Address family: {afi} ({})", afi_name(afi)),
336 base + pos as u32,
337 2,
338 );
339 pos += 2;
340 let addr_len = match afi {
341 1 => 4,
342 2 => 16,
343 _ => {
344 node.children.push(DissectionNode::new(
345 "mrt.bgp4mp.addresses",
346 format!("Unknown AFI {afi}; cannot walk further"),
347 base + pos as u32,
348 (data.len() - pos) as u32,
349 ));
350 return None;
351 }
352 };
353
354 if data.len() < pos + 2 * addr_len {
355 node.children.push(DissectionNode::new(
356 "mrt.bgp4mp.truncated",
357 "Truncated peer/local addresses",
358 base + pos as u32,
359 (data.len() - pos) as u32,
360 ));
361 return None;
362 }
363 push_field(
364 node,
365 "mrt.bgp4mp.peer_ip",
366 format!("Peer IP: {}", render_ip(&data[pos..pos + addr_len])),
367 base + pos as u32,
368 addr_len,
369 );
370 pos += addr_len;
371 push_field(
372 node,
373 "mrt.bgp4mp.local_ip",
374 format!("Local IP: {}", render_ip(&data[pos..pos + addr_len])),
375 base + pos as u32,
376 addr_len,
377 );
378 Some(pos + addr_len)
379}
380
381fn afi_name(afi: u16) -> &'static str {
382 match afi {
383 1 => "IPv4",
384 2 => "IPv6",
385 _ => "unknown",
386 }
387}
388
389fn push_field(node: &mut DissectionNode, field: &str, label: String, offset: u32, len: usize) {
390 node.children
391 .push(DissectionNode::new(field, label, offset, len as u32));
392}
393
394fn read_asn(data: &[u8], pos: usize, asn_size: usize) -> u32 {
395 if asn_size == 4 {
396 u32::from_be_bytes([
397 *data.get(pos).unwrap_or(&0),
398 *data.get(pos + 1).unwrap_or(&0),
399 *data.get(pos + 2).unwrap_or(&0),
400 *data.get(pos + 3).unwrap_or(&0),
401 ])
402 } else {
403 read_u16(data, pos).unwrap_or(0) as u32
404 }
405}
406
407fn render_ip(octets: &[u8]) -> String {
408 if octets.len() == 4 {
409 format!("{}.{}.{}.{}", octets[0], octets[1], octets[2], octets[3])
410 } else {
411 octets
412 .chunks(2)
413 .map(|c| format!("{:02x}{:02x}", c[0], c[1]))
414 .collect::<Vec<_>>()
415 .join(":")
416 }
417}
418
419fn bgp_state_name(state: u16) -> &'static str {
420 match state {
421 1 => "Idle",
422 2 => "Connect",
423 3 => "Active",
424 4 => "OpenSent",
425 5 => "OpenConfirm",
426 6 => "Established",
427 _ => "Unknown",
428 }
429}
430
431#[cfg(test)]
432mod tests {
433 use super::*;
434
435 fn bgp4mp_update_record() -> Vec<u8> {
438 let mut attrs = Vec::new();
440 attrs.extend_from_slice(&[0x40, 0x01, 0x01, 0x00]);
441 attrs.extend_from_slice(&[0x40, 0x02, 0x06, 0x02, 0x01]);
442 attrs.extend_from_slice(&65001u32.to_be_bytes());
443 attrs.extend_from_slice(&[0x40, 0x03, 0x04, 192, 0, 2, 254]);
444 let mut update = Vec::new();
445 update.extend_from_slice(&0u16.to_be_bytes());
446 update.extend_from_slice(&(attrs.len() as u16).to_be_bytes());
447 update.extend_from_slice(&attrs);
448 update.extend_from_slice(&[24, 203, 0, 113]);
449
450 let mut bgp = vec![0xFF; 16];
452 bgp.extend_from_slice(&((19 + update.len()) as u16).to_be_bytes());
453 bgp.push(2);
454 bgp.extend_from_slice(&update);
455
456 let mut body = Vec::new();
458 body.extend_from_slice(&64496u32.to_be_bytes());
459 body.extend_from_slice(&64497u32.to_be_bytes());
460 body.extend_from_slice(&0u16.to_be_bytes()); body.extend_from_slice(&1u16.to_be_bytes()); body.extend_from_slice(&[192, 0, 2, 1]); body.extend_from_slice(&[192, 0, 2, 2]); body.extend_from_slice(&bgp);
465
466 let mut wire = Vec::new();
468 wire.extend_from_slice(&1_700_000_000u32.to_be_bytes());
469 wire.extend_from_slice(&16u16.to_be_bytes());
470 wire.extend_from_slice(&4u16.to_be_bytes());
471 wire.extend_from_slice(&(body.len() as u32).to_be_bytes());
472 wire.extend_from_slice(&body);
473 wire
474 }
475
476 #[test]
477 fn dissect_mrt_record_layered_offsets() {
478 let wire = bgp4mp_update_record();
479 let tree = dissect_mrt_bytes(&wire);
480
481 assert_eq!(tree.field, "mrt");
482 assert_eq!(tree.length, wire.len() as u32);
483
484 let timestamp = tree.find("mrt.header.timestamp").unwrap();
485 assert_eq!((timestamp.offset, timestamp.length), (0, 4));
486 let entry_type = tree.find("mrt.header.type").unwrap();
487 assert_eq!(entry_type.label, "Type: BGP4MP (16)");
488 let subtype = tree.find("mrt.header.subtype").unwrap();
489 assert_eq!(subtype.label, "Subtype: 4");
490
491 let peer_asn = tree.find("mrt.bgp4mp.peer_asn").unwrap();
493 assert_eq!((peer_asn.offset, peer_asn.length), (12, 4));
494 assert_eq!(peer_asn.label, "Peer ASN: 64496");
495 let afi = tree.find("mrt.bgp4mp.afi").unwrap();
496 assert_eq!(afi.offset, 22);
497 assert_eq!(afi.label, "Address family: 1 (IPv4)");
498 let peer_ip = tree.find("mrt.bgp4mp.peer_ip").unwrap();
499 assert_eq!(peer_ip.label, "Peer IP: 192.0.2.1");
500
501 let marker = tree.find("bgp.header.marker").unwrap();
503 assert_eq!(marker.offset, 12 + 20);
504 let segment = tree.find("bgp.attr.as_path.segment").unwrap();
505 assert_eq!(segment.label, "AS_SEQUENCE: 65001");
506 let prefix = tree.find("bgp.update.nlri").unwrap();
507 assert_eq!(prefix.offset + prefix.length, wire.len() as u32);
508 }
509
510 #[test]
511 fn dissect_mrt_record_from_raw() {
512 let wire = bgp4mp_update_record();
514 let mut cursor = std::io::Cursor::new(wire.clone());
515 let raw = crate::parser::mrt::chunk_mrt_record(&mut cursor).unwrap();
516 let tree = dissect_mrt_record(&raw);
517 assert_eq!(tree.length, wire.len() as u32);
518 assert!(tree.find("bgp.header.type").is_some());
519 }
520
521 #[test]
522 fn dissect_mrt_state_change() {
523 let mut body = Vec::new();
524 body.extend_from_slice(&64496u16.to_be_bytes());
525 body.extend_from_slice(&64497u16.to_be_bytes());
526 body.extend_from_slice(&0u16.to_be_bytes());
527 body.extend_from_slice(&1u16.to_be_bytes());
528 body.extend_from_slice(&[192, 0, 2, 1]);
529 body.extend_from_slice(&[192, 0, 2, 2]);
530 body.extend_from_slice(&5u16.to_be_bytes()); body.extend_from_slice(&6u16.to_be_bytes()); let mut wire = Vec::new();
534 wire.extend_from_slice(&2u32.to_be_bytes());
535 wire.extend_from_slice(&16u16.to_be_bytes());
536 wire.extend_from_slice(&0u16.to_be_bytes()); wire.extend_from_slice(&(body.len() as u32).to_be_bytes());
538 wire.extend_from_slice(&body);
539
540 let tree = dissect_mrt_bytes(&wire);
541 let new_state = tree.find("mrt.bgp4mp.new_state").unwrap();
542 assert_eq!(new_state.label, "New state: 6 (Established)");
543 assert_eq!(new_state.offset, wire.len() as u32 - 2);
544 }
545
546 #[test]
547 fn dissect_mrt_truncated_header_and_unknown_type() {
548 let partial = dissect_mrt_bytes(&[0, 0, 0, 1, 0]);
549 let header = partial.find("mrt.header").unwrap();
550 assert!(header.label.contains("truncated"));
551
552 let mut wire = Vec::new();
554 wire.extend_from_slice(&1u32.to_be_bytes());
555 wire.extend_from_slice(&13u16.to_be_bytes());
556 wire.extend_from_slice(&1u16.to_be_bytes());
557 wire.extend_from_slice(&4u32.to_be_bytes());
558 wire.extend_from_slice(&[0xAA; 4]);
559 let tree = dissect_mrt_bytes(&wire);
560 let body = tree.find("mrt.body").unwrap();
561 assert_eq!(body.label, "Message body (4 bytes)");
562 }
563
564 #[test]
565 fn dissect_mrt_et_record_microsecond_field() {
566 let mut bgp = vec![0xFF; 16];
569 bgp.extend_from_slice(&19u16.to_be_bytes());
570 bgp.push(4); let mut body = Vec::new();
573 body.extend_from_slice(&64496u32.to_be_bytes()); body.extend_from_slice(&64497u32.to_be_bytes()); body.extend_from_slice(&0u16.to_be_bytes()); body.extend_from_slice(&1u16.to_be_bytes()); body.extend_from_slice(&[192, 0, 2, 1]);
578 body.extend_from_slice(&[192, 0, 2, 2]);
579 body.extend_from_slice(&bgp);
580
581 let mut wire = Vec::new();
582 wire.extend_from_slice(&1_700_000_000u32.to_be_bytes());
583 wire.extend_from_slice(&17u16.to_be_bytes());
584 wire.extend_from_slice(&4u16.to_be_bytes()); wire.extend_from_slice(&(body.len() as u32).to_be_bytes());
586 wire.extend_from_slice(&123_456u32.to_be_bytes()); wire.extend_from_slice(&body);
588
589 let tree = dissect_mrt_bytes(&wire);
590 let micro = tree.find("mrt.header.microsecond").unwrap();
591 assert_eq!((micro.offset, micro.length), (12, 4));
592 assert_eq!(micro.label, "Microsecond timestamp: 123456");
593 let peer_asn = tree.find("mrt.bgp4mp.peer_asn").unwrap();
595 assert_eq!(peer_asn.offset, 16);
596 assert!(tree.find("bgp.header.type").is_some());
597 }
598
599 #[test]
600 fn dissect_mrt_et_record_truncated_microsecond() {
601 let mut wire = Vec::new();
604 wire.extend_from_slice(&1u32.to_be_bytes());
605 wire.extend_from_slice(&17u16.to_be_bytes());
606 wire.extend_from_slice(&4u16.to_be_bytes());
607 wire.extend_from_slice(&8u32.to_be_bytes());
608 wire.extend_from_slice(&[0xAB, 0xCD]); let tree = dissect_mrt_bytes(&wire);
611 let micro = tree.find("mrt.header.microsecond").unwrap();
612 assert_eq!(
613 micro.label,
614 "Microsecond timestamp (truncated: 2 of 4 bytes)"
615 );
616 assert_eq!((micro.offset, micro.length), (12, 2));
617 assert!(
618 tree.find("mrt.bgp4mp").is_none(),
619 "no body walk on truncated ET header"
620 );
621 }
622
623 #[test]
624 fn dissect_mrt_zebra_short_state_change() {
625 let mut wire = Vec::new();
628 wire.extend_from_slice(&1u32.to_be_bytes());
629 wire.extend_from_slice(&16u16.to_be_bytes());
630 wire.extend_from_slice(&0u16.to_be_bytes()); wire.extend_from_slice(&8u32.to_be_bytes());
632 wire.extend_from_slice(&64496u16.to_be_bytes());
633 wire.extend_from_slice(&64497u16.to_be_bytes());
634 wire.extend_from_slice(&5u16.to_be_bytes()); wire.extend_from_slice(&6u16.to_be_bytes()); let tree = dissect_mrt_bytes(&wire);
638 let old_state = tree.find("mrt.bgp4mp.old_state").unwrap();
639 assert_eq!(old_state.label, "Old state: 5 (OpenConfirm)");
640 let new_state = tree.find("mrt.bgp4mp.new_state").unwrap();
641 assert_eq!(new_state.label, "New state: 6 (Established)");
642 assert_eq!(new_state.offset, wire.len() as u32 - 2);
643 }
644
645 #[test]
646 fn dissect_mrt_bgp4mp_ipv6_addresses() {
647 let mut bgp = vec![0xFF; 16];
648 bgp.extend_from_slice(&19u16.to_be_bytes());
649 bgp.push(4); let mut body = Vec::new();
652 body.extend_from_slice(&64496u32.to_be_bytes());
653 body.extend_from_slice(&64497u32.to_be_bytes());
654 body.extend_from_slice(&0u16.to_be_bytes());
655 body.extend_from_slice(&2u16.to_be_bytes()); body.extend_from_slice(&[0x20, 0x01, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]);
657 body.extend_from_slice(&[0x20, 0x01, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2]);
658 body.extend_from_slice(&bgp);
659
660 let mut wire = Vec::new();
661 wire.extend_from_slice(&1u32.to_be_bytes());
662 wire.extend_from_slice(&16u16.to_be_bytes());
663 wire.extend_from_slice(&4u16.to_be_bytes());
664 wire.extend_from_slice(&(body.len() as u32).to_be_bytes());
665 wire.extend_from_slice(&body);
666
667 let tree = dissect_mrt_bytes(&wire);
668 let afi = tree.find("mrt.bgp4mp.afi").unwrap();
669 assert_eq!(afi.label, "Address family: 2 (IPv6)");
670 let peer_ip = tree.find("mrt.bgp4mp.peer_ip").unwrap();
671 assert_eq!(peer_ip.length, 16);
672 assert_eq!(
673 peer_ip.label,
674 "Peer IP: 2001:0000:0000:0000:0000:0000:0000:0001"
675 );
676 let marker = tree.find("bgp.header.marker").unwrap();
678 assert_eq!(marker.offset, 12 + 4 + 4 + 2 + 2 + 32);
679 }
680
681 #[test]
682 fn dissect_mrt_zebra_compat_detected() {
683 let mut bgp = vec![0xFF; 16];
685 bgp.extend_from_slice(&19u16.to_be_bytes());
686 bgp.push(4); let mut body = Vec::new();
689 body.extend_from_slice(&64496u16.to_be_bytes());
690 body.extend_from_slice(&64497u16.to_be_bytes());
691 body.extend_from_slice(&bgp);
692
693 let mut wire = Vec::new();
694 wire.extend_from_slice(&1u32.to_be_bytes());
695 wire.extend_from_slice(&16u16.to_be_bytes());
696 wire.extend_from_slice(&1u16.to_be_bytes()); wire.extend_from_slice(&(body.len() as u32).to_be_bytes());
698 wire.extend_from_slice(&body);
699
700 let tree = dissect_mrt_bytes(&wire);
701 assert!(tree.find("mrt.bgp4mp.zebra_compat").is_some());
702 assert!(tree.find("bgp.header.type").is_some());
703 }
704}