Skip to main content

bgpkit_parser/models/mrt/
mod.rs

1//! MRT message and relevant structs.
2
3pub mod bgp4mp;
4pub mod legacy_bgp;
5pub mod table_dump;
6pub mod table_dump_v2;
7
8pub use bgp4mp::*;
9pub use legacy_bgp::*;
10use num_enum::{IntoPrimitive, TryFromPrimitive};
11use std::fmt::{Display, Formatter};
12pub use table_dump::*;
13pub use table_dump_v2::*;
14
15/// MrtRecord is a wrapper struct that contains a header and a message.
16///
17/// A MRT record is constructed as the following:
18/// ```text
19///  0                   1                   2                   3
20///  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
21/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
22/// |                      Header... (variable)                     |
23/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
24/// |                      Message... (variable)
25/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
26/// ```
27///
28/// See [CommonHeader] for the content in header, and [MrtMessage] for the
29/// message format.
30#[derive(Debug, PartialEq, Clone, Eq)]
31#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
32pub struct MrtRecord {
33    pub common_header: CommonHeader,
34    pub message: MrtMessage,
35}
36
37/// MRT common header.
38///
39/// A CommonHeader ([RFC6396 section 2][header-link]) is constructed as the following:
40/// ```text
41///  0                   1                   2                   3
42///  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
43/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
44/// |                           Timestamp                           |
45/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
46/// |             Type              |            Subtype            |
47/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
48/// |                             Length                            |
49/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
50/// ```
51///
52/// Or with extended timestamp:
53/// ```text
54///  0                   1                   2                   3
55///  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
56/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
57/// |                           Timestamp                           |
58/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
59/// |             Type              |            Subtype            |
60/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
61/// |                             Length                            |
62/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
63/// |                      Microsecond Timestamp                    |
64/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
65/// ```
66///
67/// The headers include the following:
68/// - timestamp: 32 bits
69/// - entry_type: [EntryType] enum
70/// - entry_subtype: entry subtype
71/// - length: length of the message in octets
72/// - (`ET` type only) microsecond_timestamp: microsecond part of the timestamp.
73///   only applicable to the MRT message type with `_ET` suffix, such as
74///   `BGP4MP_ET`
75///
76/// [header-link]: https://datatracker.ietf.org/doc/html/rfc6396#section-2
77#[derive(Debug, Copy, Clone, Eq)]
78#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
79pub struct CommonHeader {
80    pub timestamp: u32,
81    pub microsecond_timestamp: Option<u32>,
82    pub entry_type: EntryType,
83    pub entry_subtype: u16,
84    pub length: u32,
85}
86
87impl PartialEq for CommonHeader {
88    fn eq(&self, other: &Self) -> bool {
89        self.timestamp == other.timestamp
90            && self.microsecond_timestamp == other.microsecond_timestamp
91            && self.entry_type == other.entry_type
92            && self.entry_subtype == other.entry_subtype
93        // && self.length == other.length
94        // relax the length check as it might be different due to incorrect encoding
95    }
96}
97
98impl Display for CommonHeader {
99    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
100        let ts = match self.microsecond_timestamp {
101            Some(us) => format!("{}.{:06}", self.timestamp, us),
102            None => self.timestamp.to_string(),
103        };
104        write!(
105            f,
106            "MRT|{}|{:?}|{}|{}",
107            ts, self.entry_type, self.entry_subtype, self.length
108        )
109    }
110}
111
112impl Display for MrtRecord {
113    /// Formats the MRT record in a debug-friendly format.
114    ///
115    /// The format is: `MRT|<timestamp>|<type>|<subtype>|<message_summary>`
116    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
117        let ts = match self.common_header.microsecond_timestamp {
118            Some(us) => format!("{}.{:06}", self.common_header.timestamp, us),
119            None => self.common_header.timestamp.to_string(),
120        };
121        write!(
122            f,
123            "MRT|{}|{:?}|{}|{}",
124            ts, self.common_header.entry_type, self.common_header.entry_subtype, self.message
125        )
126    }
127}
128
129impl Display for MrtMessage {
130    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
131        match self {
132            MrtMessage::TableDumpMessage(msg) => {
133                write!(f, "TABLE_DUMP|{}|{}", msg.prefix, msg.peer_ip)
134            }
135            MrtMessage::TableDumpMessageBatch(messages) => {
136                write!(f, "TABLE_DUMP_BATCH|{} entries", messages.len())
137            }
138            MrtMessage::TableDumpV2Message(msg) => match msg {
139                TableDumpV2Message::PeerIndexTable(pit) => {
140                    write!(f, "PEER_INDEX_TABLE|{}", pit.id_peer_map.len())
141                }
142                TableDumpV2Message::RibAfi(rib) => {
143                    write!(
144                        f,
145                        "RIB|{:?}|{}|{} entries",
146                        rib.rib_type,
147                        rib.prefix,
148                        rib.rib_entries.len()
149                    )
150                }
151                TableDumpV2Message::RibGeneric(rib) => {
152                    write!(
153                        f,
154                        "RIB_GENERIC|AFI {:?}|SAFI {:?}|{} entries",
155                        rib.afi,
156                        rib.safi,
157                        rib.rib_entries.len()
158                    )
159                }
160                TableDumpV2Message::GeoPeerTable(gpt) => {
161                    write!(f, "GEO_PEER_TABLE|{} peers", gpt.geo_peers.len())
162                }
163            },
164            MrtMessage::Bgp4Mp(bgp4mp) => match bgp4mp {
165                Bgp4MpEnum::StateChange(sc) => {
166                    write!(
167                        f,
168                        "STATE_CHANGE|{}|{}|{:?}->{:?}",
169                        sc.peer_ip, sc.peer_asn, sc.old_state, sc.new_state
170                    )
171                }
172                Bgp4MpEnum::Message(msg) => {
173                    let msg_type = match &msg.bgp_message {
174                        crate::models::BgpMessage::Open(_) => "OPEN",
175                        crate::models::BgpMessage::Update(_) => "UPDATE",
176                        crate::models::BgpMessage::Notification(_) => "NOTIFICATION",
177                        crate::models::BgpMessage::KeepAlive => "KEEPALIVE",
178                        crate::models::BgpMessage::RouteRefresh(_) => "ROUTE_REFRESH",
179                    };
180                    write!(f, "BGP4MP|{}|{}|{}", msg.peer_ip, msg.peer_asn, msg_type)
181                }
182            },
183            MrtMessage::LegacyBgp(bgp) => match bgp {
184                LegacyBgp::StateChange(sc) => write!(
185                    f,
186                    "BGP|STATE_CHANGE|{}|{}|{:?}->{:?}",
187                    sc.peer_ip, sc.peer_asn, sc.old_state, sc.new_state
188                ),
189                LegacyBgp::Message(msg) => {
190                    let msg_type = match &msg.bgp_message {
191                        crate::models::BgpMessage::Update(_) => "UPDATE",
192                        crate::models::BgpMessage::KeepAlive => "KEEPALIVE",
193                        crate::models::BgpMessage::Open(_) => "OPEN",
194                        crate::models::BgpMessage::Notification(_) => "NOTIFICATION",
195                        crate::models::BgpMessage::RouteRefresh(_) => "ROUTE_REFRESH",
196                    };
197                    write!(f, "BGP|{}|{}|{}", msg.peer_ip, msg.peer_asn, msg_type)
198                }
199            },
200        }
201    }
202}
203
204#[derive(Debug, PartialEq, Clone, Eq)]
205#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
206pub enum MrtMessage {
207    TableDumpMessage(TableDumpMessage),
208    /// A historical physical TABLE_DUMP record containing multiple entries.
209    TableDumpMessageBatch(Vec<TableDumpMessage>),
210    TableDumpV2Message(TableDumpV2Message),
211    Bgp4Mp(Bgp4MpEnum),
212    /// Deprecated MRT Type 5 BGP message.
213    LegacyBgp(LegacyBgp),
214}
215
216/// MRT entry type.
217///
218/// EntryType indicates the type of the current MRT record. Type 0 to 10 are deprecated.
219///
220/// Excerpt from [RFC6396 section 4](https://datatracker.ietf.org/doc/html/rfc6396#section-4):
221/// ```text
222/// The following MRT Types are currently defined for the MRT format.
223/// The MRT Types that contain the "_ET" suffix in their names identify
224/// those types that use an Extended Timestamp MRT Header.  The Subtype
225/// and Message fields in these types remain as defined for the MRT Types
226/// of the same name without the "_ET" suffix.
227///
228///     11   OSPFv2
229///     12   TABLE_DUMP
230///     13   TABLE_DUMP_V2
231///     16   BGP4MP
232///     17   BGP4MP_ET
233///     32   ISIS
234///     33   ISIS_ET
235///     48   OSPFv3
236///     49   OSPFv3_ET
237/// ```
238#[derive(Debug, TryFromPrimitive, IntoPrimitive, Copy, Clone, PartialEq, Eq, Hash)]
239#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
240#[allow(non_camel_case_types)]
241#[repr(u16)]
242pub enum EntryType {
243    // START DEPRECATED
244    NULL = 0,
245    START = 1,
246    DIE = 2,
247    I_AM_DEAD = 3,
248    PEER_DOWN = 4,
249    BGP = 5,
250    RIP = 6,
251    IDRP = 7,
252    RIPNG = 8,
253    BGP4PLUS = 9,
254    BGP4PLUS_01 = 10,
255    // END DEPRECATED
256    OSPFv2 = 11,
257    TABLE_DUMP = 12,
258    TABLE_DUMP_V2 = 13,
259    BGP4MP = 16,
260    BGP4MP_ET = 17,
261    ISIS = 32,
262    ISIS_ET = 33,
263    OSPFv3 = 48,
264    OSPFv3_ET = 49,
265}
266
267#[cfg(test)]
268mod tests {
269
270    #[test]
271    fn test_mrt_record_display() {
272        use super::*;
273        use crate::models::Asn;
274        use std::net::IpAddr;
275        use std::str::FromStr;
276
277        let mrt_record = MrtRecord {
278            common_header: CommonHeader {
279                timestamp: 1609459200,
280                microsecond_timestamp: None,
281                entry_type: EntryType::BGP4MP,
282                entry_subtype: 0,
283                length: 0,
284            },
285            message: MrtMessage::Bgp4Mp(Bgp4MpEnum::StateChange(Bgp4MpStateChange {
286                msg_type: Bgp4MpType::StateChange,
287                peer_asn: Asn::new_32bit(65000),
288                local_asn: Asn::new_32bit(65001),
289                interface_index: 1,
290                peer_ip: IpAddr::from_str("10.0.0.1").unwrap(),
291                local_addr: IpAddr::from_str("10.0.0.2").unwrap(),
292                old_state: BgpState::Idle,
293                new_state: BgpState::Connect,
294            })),
295        };
296
297        let display = format!("{}", mrt_record);
298        assert!(display.contains("1609459200"));
299        assert!(display.contains("BGP4MP"));
300        assert!(display.contains("STATE_CHANGE"));
301        assert!(display.contains("10.0.0.1"));
302        assert!(display.contains("65000"));
303    }
304
305    #[test]
306    fn test_common_header_display() {
307        use super::*;
308
309        let header = CommonHeader {
310            timestamp: 1609459200,
311            microsecond_timestamp: Some(500000),
312            entry_type: EntryType::BGP4MP_ET,
313            entry_subtype: 4,
314            length: 128,
315        };
316
317        let display = format!("{}", header);
318        assert!(display.contains("1609459200.500000"));
319        assert!(display.contains("BGP4MP_ET"));
320        assert!(display.contains("128"));
321    }
322
323    #[test]
324    #[cfg(feature = "serde")]
325    fn test_entry_type_serialize_and_deserialize() {
326        use super::*;
327        let types = vec![
328            EntryType::NULL,
329            EntryType::START,
330            EntryType::DIE,
331            EntryType::I_AM_DEAD,
332            EntryType::PEER_DOWN,
333            EntryType::BGP,
334            EntryType::RIP,
335            EntryType::IDRP,
336            EntryType::RIPNG,
337            EntryType::BGP4PLUS,
338            EntryType::BGP4PLUS_01,
339            EntryType::OSPFv2,
340            EntryType::TABLE_DUMP,
341            EntryType::TABLE_DUMP_V2,
342            EntryType::BGP4MP,
343            EntryType::BGP4MP_ET,
344            EntryType::ISIS,
345            EntryType::ISIS_ET,
346            EntryType::OSPFv3,
347            EntryType::OSPFv3_ET,
348        ];
349
350        for entry_type in types {
351            let serialized = serde_json::to_string(&entry_type).unwrap();
352            let deserialized: EntryType = serde_json::from_str(&serialized).unwrap();
353
354            assert_eq!(entry_type, deserialized);
355        }
356    }
357
358    #[test]
359    #[cfg(feature = "serde")]
360    fn test_serialization() {
361        use super::*;
362        use serde_json;
363        use std::net::IpAddr;
364        use std::str::FromStr;
365
366        let mrt_record = MrtRecord {
367            common_header: CommonHeader {
368                timestamp: 0,
369                microsecond_timestamp: None,
370                entry_type: EntryType::BGP4MP,
371                entry_subtype: 0,
372                length: 0,
373            },
374            message: MrtMessage::Bgp4Mp(Bgp4MpEnum::StateChange(Bgp4MpStateChange {
375                msg_type: Bgp4MpType::StateChange,
376                peer_asn: crate::models::Asn::new_32bit(0),
377                local_asn: crate::models::Asn::new_32bit(0),
378                interface_index: 1,
379                peer_ip: IpAddr::from_str("10.0.0.0").unwrap(),
380                local_addr: IpAddr::from_str("10.0.0.0").unwrap(),
381                old_state: BgpState::Idle,
382                new_state: BgpState::Connect,
383            })),
384        };
385
386        let serialized = serde_json::to_string(&mrt_record).unwrap();
387        let deserialized: MrtRecord = serde_json::from_str(&serialized).unwrap();
388        assert_eq!(mrt_record, deserialized);
389    }
390}