Skip to main content

bgpkit_parser/parser/mrt/messages/
table_dump.rs

1use crate::encoder::sink::with_u16_len;
2use crate::error::*;
3use crate::models::*;
4use crate::parser::bgp::attributes::parse_attributes;
5use crate::parser::ReadUtils;
6use bytes::{BufMut, Bytes, BytesMut};
7use ipnet::IpNet;
8use std::net::IpAddr;
9
10/// Parse MRT TABLE_DUMP type message.
11///
12/// <https://www.rfc-editor.org/rfc/rfc6396#section-4.2>
13///
14/// ```text
15/// The TABLE_DUMP Type does not permit 4-byte Peer AS numbers, nor does
16//  it allow the AFI of the peer IP to differ from the AFI of the Prefix
17//  field.  The TABLE_DUMP_V2 Type MUST be used in these situations.
18/// ```
19///
20/// ```text
21///  0                   1                   2                   3
22///  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
23/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
24/// |         View Number           |       Sequence Number         |
25/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
26/// |                        Prefix (variable)                      |
27/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
28/// | Prefix Length |    Status     |
29/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
30/// |                         Originated Time                       |
31/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
32/// |                    Peer IP Address (variable)                 |
33/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
34/// |           Peer AS             |       Attribute Length        |
35/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
36/// |                   BGP Attribute... (variable)
37/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
38/// ```
39pub fn parse_table_dump_message(
40    sub_type: u16,
41    data: Bytes,
42) -> Result<TableDumpMessage, ParserError> {
43    let mut messages = parse_table_dump_messages(sub_type, data)?;
44    if messages.len() != 1 {
45        return Err(ParserError::ParseError(format!(
46            "expected one TABLE_DUMP entry, found {}",
47            messages.len()
48        )));
49    }
50    Ok(messages.remove(0))
51}
52
53/// Parse all entries carried by one physical TABLE_DUMP record.
54///
55/// RFC 6396 records contain one entry. Early MRT implementations batched
56/// multiple entries behind one view/sequence header.
57pub fn parse_table_dump_messages(
58    sub_type: u16,
59    mut data: Bytes,
60) -> Result<Vec<TableDumpMessage>, ParserError> {
61    // ####
62    // Step 0. prepare
63    //   - define AS number length
64    //   - determine address family
65    //   - create data slice reader cursor
66
67    // determine address family based on the sub_type value defined in the MRT [CommonHeader].
68    let afi = match sub_type {
69        1 => Afi::Ipv4,
70        2 => Afi::Ipv6,
71        _ => {
72            return Err(ParserError::ParseError(format!(
73                "Invalid subtype found for TABLE_DUMP (V1) message: {sub_type}"
74            )))
75        }
76    };
77
78    // ####
79    // Step 1. read simple fields
80    //   - view number
81    //   - sequence number
82    //   - prefix
83    //   - prefix-length
84    //   - status
85    //   - originated time
86    //   - peer IP address
87    //   - peer ASN
88    //   - attribute length
89
90    let view_number = data.read_u16()?;
91    let sequence_number = data.read_u16()?;
92    let mut messages = Vec::new();
93
94    while !data.is_empty() {
95        messages.push(parse_table_dump_entry(
96            &mut data,
97            &afi,
98            view_number,
99            sequence_number,
100        )?);
101    }
102
103    if messages.is_empty() {
104        return Err(ParserError::TruncatedMsg(
105            "TABLE_DUMP record contains no entries".to_string(),
106        ));
107    }
108
109    Ok(messages)
110}
111
112fn parse_table_dump_entry(
113    data: &mut Bytes,
114    afi: &Afi,
115    view_number: u16,
116    sequence_number: u16,
117) -> Result<TableDumpMessage, ParserError> {
118    let prefix = match afi {
119        Afi::Ipv4 => data.read_ipv4_prefix().map(ipnet::IpNet::V4),
120        Afi::Ipv6 => data.read_ipv6_prefix().map(ipnet::IpNet::V6),
121        Afi::LinkState => {
122            // Link-State doesn't use traditional prefixes, but we need a placeholder
123            // Use 0.0.0.0/0 as a placeholder for now
124            Ok(ipnet::IpNet::V4(
125                ipnet::Ipv4Net::new(std::net::Ipv4Addr::new(0, 0, 0, 0), 0).unwrap(),
126            ))
127        }
128    }?;
129
130    let status = data.read_u8()?;
131    let time = data.read_u32()? as u64;
132
133    let peer_ip: IpAddr = data.read_address(afi)?;
134    let peer_asn = Asn::new_16bit(data.read_u16()?);
135
136    let attribute_length = data.read_u16()? as usize;
137
138    // ####
139    // Step 2. read the attributes
140    //   - create subslice based on the cursor's current position
141    //   - pass the data into the parser function
142
143    data.has_n_remaining(attribute_length)?;
144    let attr_data_slice = data.split_to(attribute_length);
145
146    // for TABLE_DUMP type, the AS number length is always 2-byte. The element type already
147    // fixed the family: IPv4 (subtype 1) or IPv6 (subtype 2), both unicast.
148    let mut attributes = parse_attributes(
149        attr_data_slice,
150        &AsnLength::Bits16,
151        false,
152        Some(*afi),
153        Some(Safi::Unicast),
154        None,
155    )?;
156
157    // validate mandatory attributes (TABLE_DUMP is always an announcement)
158    attributes.check_mandatory_attributes(true, *afi == Afi::Ipv4);
159
160    Ok(TableDumpMessage {
161        view_number,
162        sequence_number,
163        prefix: NetworkPrefix::new(prefix, None),
164        status,
165        originated_time: time,
166        peer_ip,
167        peer_asn,
168        attributes,
169    })
170}
171
172impl TableDumpMessage {
173    pub fn encode(&self) -> Result<Bytes, EncodingError> {
174        let mut bytes = BytesMut::new();
175        bytes.put_u16(self.view_number);
176        bytes.put_u16(self.sequence_number);
177        self.encode_entry_to(&mut bytes)?;
178        Ok(bytes.freeze())
179    }
180
181    fn encode_entry_to(&self, bytes: &mut BytesMut) -> Result<(), EncodingError> {
182        match &self.prefix.prefix {
183            IpNet::V4(p) => {
184                bytes.put_u32(p.addr().into());
185                bytes.put_u8(p.prefix_len());
186            }
187            IpNet::V6(p) => {
188                bytes.put_u128(p.addr().into());
189                bytes.put_u8(p.prefix_len());
190            }
191        }
192        bytes.put_u8(self.status);
193        bytes.put_u32(self.originated_time as u32);
194
195        // peer address and peer asn
196        match self.peer_ip {
197            IpAddr::V4(a) => {
198                bytes.put_u32(a.into());
199            }
200            IpAddr::V6(a) => {
201                bytes.put_u128(a.into());
202            }
203        }
204        bytes.put_u16(self.peer_asn.into());
205
206        // encode attributes; asn_len is always 16-bit for TABLE_DUMP
207        with_u16_len(bytes, "TABLE_DUMP attribute length", |b| {
208            self.attributes.encode_to(AsnLength::Bits16, b)
209        })?;
210        Ok(())
211    }
212}
213
214pub(crate) fn encode_table_dump_batch(
215    messages: &[TableDumpMessage],
216    sub_type: u16,
217) -> Result<Bytes, EncodingError> {
218    let Some(first) = messages.first() else {
219        return Err(EncodingError::unencodable(
220            "TABLE_DUMP batch",
221            "batch is empty",
222        ));
223    };
224
225    let expected_ipv4 = match sub_type {
226        1 => true,
227        2 => false,
228        _ => {
229            return Err(EncodingError::unencodable(
230                "TABLE_DUMP batch",
231                format!("invalid subtype {sub_type}"),
232            ));
233        }
234    };
235
236    let mut bytes = BytesMut::new();
237    bytes.put_u16(first.view_number);
238    bytes.put_u16(first.sequence_number);
239    for message in messages {
240        if message.view_number != first.view_number
241            || message.sequence_number != first.sequence_number
242        {
243            return Err(EncodingError::unencodable(
244                "TABLE_DUMP batch",
245                "all entries must have the same view and sequence numbers",
246            ));
247        }
248        if matches!(message.prefix.prefix, IpNet::V4(_)) != expected_ipv4
249            || matches!(message.peer_ip, IpAddr::V4(_)) != expected_ipv4
250        {
251            return Err(EncodingError::unencodable(
252                "TABLE_DUMP batch",
253                "entry address family does not match the MRT subtype",
254            ));
255        }
256        message.encode_entry_to(&mut bytes)?;
257    }
258    Ok(bytes.freeze())
259}
260
261/// Return true when a historical batched TABLE_DUMP body is structurally
262/// complete except for the final four attribute bytes.
263///
264/// This recovers files written by an old MRT writer, likely MRT Toolkit. The
265/// surviving MRT Toolkit source already has the fix: `bgp_table_dump_write`
266/// includes the 4-byte View Number and Sequence Number in the declared length.
267/// The fixture `bview.20000111.0032.gz` omits those four bytes from the length.
268///
269/// [original MRT writer]: https://fossies.org/linux/misc/old/mrt-2.2.2a-src.tar.gz/mrt-2.2.2a/src/lib/bgp_proto/bgp_dump2.c
270pub(crate) fn needs_legacy_length_correction(sub_type: u16, data: &[u8]) -> bool {
271    let address_len = match sub_type {
272        1 => 4usize,
273        2 => 16usize,
274        _ => return false,
275    };
276    if data.len() < 4 {
277        return false;
278    }
279
280    // Prefix, prefix length, status, originated time, peer IP, peer ASN,
281    // and the attribute-length field.
282    let fixed_entry_len = address_len * 2 + 10;
283    let mut offset = 4usize;
284    while offset < data.len() {
285        let Some(attr_len_offset) = offset.checked_add(fixed_entry_len - 2) else {
286            return false;
287        };
288        if attr_len_offset + 2 > data.len() {
289            return false;
290        }
291        let attr_len =
292            u16::from_be_bytes([data[attr_len_offset], data[attr_len_offset + 1]]) as usize;
293        let Some(entry_end) = offset
294            .checked_add(fixed_entry_len)
295            .and_then(|value| value.checked_add(attr_len))
296        else {
297            return false;
298        };
299        if entry_end == data.len() + 4 {
300            return true;
301        }
302        if entry_end <= offset || entry_end > data.len() {
303            return false;
304        }
305        offset = entry_end;
306    }
307    false
308}
309
310#[cfg(test)]
311mod tests {
312    use super::*;
313    use bytes::{BufMut, BytesMut};
314    use std::net::{Ipv4Addr, Ipv6Addr};
315
316    const VIEW_NUMBER: u16 = 0;
317    const SEQUENCE_NUMBER: u16 = 0;
318    const IPV4_PREFIX: Ipv4Addr = Ipv4Addr::new(0, 0, 0, 0);
319    const IPV6_PREFIX: Ipv6Addr = Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0);
320    const PREFIX_LEN: u8 = 0;
321    const STATUS: u8 = 0;
322    const TIME: u64 = 0;
323    const PEER_IPV4: Ipv4Addr = Ipv4Addr::new(0, 0, 0, 0);
324    const PEER_IPV6: Ipv6Addr = Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0);
325    const PEER_ASN_16BIT: u16 = 0;
326    const ATTRIBUTE_LENGTH: usize = 0;
327    const DUMMY_ATTRIBUTES: &[u8] = &[];
328
329    #[test]
330    fn test_parse_table_dump_message_ipv4() {
331        let mut bytes_mut = BytesMut::new();
332        // Populate the bytes_mut with the same sequence that parse_table_dump_message() expects to parse
333        bytes_mut.put_u16(VIEW_NUMBER);
334        bytes_mut.put_u16(SEQUENCE_NUMBER);
335        bytes_mut.put_u32(IPV4_PREFIX.into());
336        bytes_mut.put_u8(PREFIX_LEN);
337        bytes_mut.put_u8(STATUS);
338        bytes_mut.put_u32(TIME as u32);
339        bytes_mut.put_u32(PEER_IPV4.into());
340        bytes_mut.put_u16(PEER_ASN_16BIT);
341        bytes_mut.put_u16(ATTRIBUTE_LENGTH as u16);
342        bytes_mut.put_slice(DUMMY_ATTRIBUTES);
343
344        // Convert from BytesMut to Bytes
345        let bytes = bytes_mut.freeze();
346
347        let table_dump_message_res = parse_table_dump_message(1, bytes.clone());
348        assert!(
349            table_dump_message_res.is_ok(),
350            "Failed to parse TABLE_DUMP_V1 message"
351        );
352
353        let table_dump_message = table_dump_message_res.unwrap();
354        assert_eq!(
355            table_dump_message.view_number, VIEW_NUMBER,
356            "VIEW_NUMBER mismatch"
357        );
358        assert_eq!(
359            table_dump_message.sequence_number, SEQUENCE_NUMBER,
360            "SEQUENCE_NUMBER mismatch"
361        );
362        // Add more assertions here as per your actual requirements
363        let encoded = table_dump_message.encode().unwrap();
364        assert_eq!(encoded, bytes);
365    }
366    #[test]
367    fn test_parse_table_dump_message_ipv6() {
368        let mut bytes_mut = BytesMut::new();
369        // Populate the bytes_mut with the same sequence that parse_table_dump_message() expects to parse
370        bytes_mut.put_u16(VIEW_NUMBER);
371        bytes_mut.put_u16(SEQUENCE_NUMBER);
372        bytes_mut.put_u128(IPV6_PREFIX.into());
373        bytes_mut.put_u8(PREFIX_LEN);
374        bytes_mut.put_u8(STATUS);
375        bytes_mut.put_u32(TIME as u32);
376        bytes_mut.put_u128(PEER_IPV6.into());
377        bytes_mut.put_u16(PEER_ASN_16BIT);
378        bytes_mut.put_u16(ATTRIBUTE_LENGTH as u16);
379        bytes_mut.put_slice(DUMMY_ATTRIBUTES);
380
381        // Convert from BytesMut to Bytes
382        let bytes = bytes_mut.freeze();
383
384        let table_dump_message_res = parse_table_dump_message(2, bytes.clone());
385        assert!(
386            table_dump_message_res.is_ok(),
387            "Failed to parse TABLE_DUMP_V1 message"
388        );
389
390        let table_dump_message = table_dump_message_res.unwrap();
391        assert_eq!(
392            table_dump_message.view_number, VIEW_NUMBER,
393            "VIEW_NUMBER mismatch"
394        );
395        assert_eq!(
396            table_dump_message.sequence_number, SEQUENCE_NUMBER,
397            "SEQUENCE_NUMBER mismatch"
398        );
399        // Add more assertions here as per your actual requirements
400
401        // test encoding
402        let encoded = table_dump_message.encode().unwrap();
403        assert_eq!(encoded, bytes);
404    }
405
406    #[test]
407    fn test_parse_table_dump_message_invalid_subtype() {
408        // Create a simple byte array for testing
409        let mut bytes_mut = BytesMut::new();
410        bytes_mut.put_u16(VIEW_NUMBER);
411        bytes_mut.put_u16(SEQUENCE_NUMBER);
412        let bytes = bytes_mut.freeze();
413
414        // Test with an invalid sub_type (not 1 or 2)
415        let result = parse_table_dump_message(0, bytes.clone());
416        assert!(result.is_err(), "Expected error for invalid sub_type");
417
418        if let Err(ParserError::ParseError(msg)) = result {
419            assert!(
420                msg.contains("Invalid subtype"),
421                "Expected error message to mention invalid subtype"
422            );
423        } else {
424            panic!("Expected ParseError for invalid sub_type");
425        }
426
427        // Test with another invalid sub_type
428        let result = parse_table_dump_message(3, bytes);
429        assert!(result.is_err(), "Expected error for invalid sub_type");
430
431        if let Err(ParserError::ParseError(msg)) = result {
432            assert!(
433                msg.contains("Invalid subtype"),
434                "Expected error message to mention invalid subtype"
435            );
436        } else {
437            panic!("Expected ParseError for invalid sub_type");
438        }
439    }
440
441    #[test]
442    fn test_table_dump_message_encode_with_attributes() {
443        use crate::models::{Asn, AttributeValue, Attributes, Origin};
444        use std::str::FromStr;
445
446        let prefix = IpNet::from_str("192.168.0.0/24").unwrap();
447        let mut attributes = Attributes::default();
448        attributes.add_attr(AttributeValue::Origin(Origin::IGP).into());
449
450        let table_dump = TableDumpMessage {
451            view_number: 1,
452            sequence_number: 2,
453            prefix: NetworkPrefix::new(prefix, None),
454            status: 1,
455            originated_time: 12345,
456            peer_ip: IpAddr::V4("10.0.0.1".parse().unwrap()),
457            peer_asn: Asn::from(65000),
458            attributes,
459        };
460
461        // This should exercise the attr.encode(AsnLength::Bits16).unwrap() line
462        let _encoded = table_dump.encode().unwrap();
463    }
464
465    #[test]
466    fn parses_and_encodes_batched_table_dump_messages() {
467        use crate::models::{AttributeValue, Origin};
468        use std::str::FromStr;
469
470        let mut attributes = Attributes::default();
471        attributes.add_attr(AttributeValue::Origin(Origin::IGP).into());
472        let first = TableDumpMessage {
473            view_number: 7,
474            sequence_number: 9,
475            prefix: NetworkPrefix::from_str("192.0.2.0/24").unwrap(),
476            status: 1,
477            originated_time: 12345,
478            peer_ip: IpAddr::V4(Ipv4Addr::new(198, 51, 100, 1)),
479            peer_asn: Asn::new_16bit(64512),
480            attributes: attributes.clone(),
481        };
482        let second = TableDumpMessage {
483            prefix: NetworkPrefix::from_str("198.51.100.0/24").unwrap(),
484            ..first.clone()
485        };
486
487        let wire = encode_table_dump_batch(&[first.clone(), second.clone()], 1).unwrap();
488        let parsed = parse_table_dump_messages(1, wire.clone()).unwrap();
489        assert_eq!(parsed.len(), 2);
490        assert_eq!(parsed[0].prefix, first.prefix);
491        assert_eq!(parsed[1].prefix, second.prefix);
492        assert!(!needs_legacy_length_correction(1, &wire));
493        assert!(needs_legacy_length_correction(1, &wire[..wire.len() - 4]));
494        assert!(!needs_legacy_length_correction(1, &wire[..wire.len() - 3]));
495    }
496    #[test]
497    fn test_table_dump_entry_domain_path_is_validated_against_its_family() {
498        // TABLE_DUMP v1 entries are IPv4 or IPv6 unicast, so a D-PATH on one is invalid
499        let mut data = Vec::new();
500        data.extend_from_slice(&0u16.to_be_bytes()); // view number
501        data.extend_from_slice(&0u16.to_be_bytes()); // sequence number
502        data.extend_from_slice(&[192, 0, 2, 0]); // prefix address 192.0.2.0
503        data.push(24); // prefix length
504        data.push(1); // status
505        data.extend_from_slice(&0u32.to_be_bytes()); // originated time
506        data.extend_from_slice(&[10, 0, 0, 1]); // peer address
507        data.extend_from_slice(&65001u16.to_be_bytes()); // peer ASN
508        let attributes = [
509            0xc0, 0x24, 0x08, 0x01, 0x00, 0x00, 0xfd, 0xe8, 0x00, 0x01, 0x46,
510        ];
511        data.extend_from_slice(&(attributes.len() as u16).to_be_bytes());
512        data.extend_from_slice(&attributes);
513
514        let message = parse_table_dump_message(1, Bytes::from(data)).unwrap();
515
516        assert!(message
517            .attributes
518            .validation_warnings()
519            .iter()
520            .any(|warning| {
521                matches!(
522                    warning,
523                    crate::error::BgpValidationWarning::OptionalAttributeError { attr_type, .. }
524                        if *attr_type == crate::models::AttrType::BGP_DOMAIN_PATH
525                )
526            }));
527    }
528}