Skip to main content

rustbgpd_wire/
update.rs

1use bytes::{Buf, BufMut, Bytes};
2
3use crate::attribute::PathAttribute;
4use crate::constants::{HEADER_LEN, MAX_MESSAGE_LEN};
5use crate::error::{DecodeError, EncodeError};
6use crate::header::{BgpHeader, MessageType};
7use crate::nlri::{Ipv4NlriEntry, Ipv4Prefix};
8use crate::{Afi, Safi};
9
10/// How IPv4 unicast NLRI should be encoded in an outbound UPDATE.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum Ipv4UnicastMode {
13    /// Encode IPv4 announcements/withdrawals in the legacy body NLRI fields.
14    Body,
15    /// Encode IPv4 announcements/withdrawals in `MP_REACH_NLRI` /
16    /// `MP_UNREACH_NLRI` attributes instead of the body fields.
17    MpReach,
18}
19
20/// A decoded BGP UPDATE message (RFC 4271 §4.3).
21///
22/// Stores the three variable-length sections as raw `Bytes`.
23/// Call [`parse()`](Self::parse) to decode NLRI and path attributes.
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct UpdateMessage {
26    /// Raw withdrawn routes (NLRI encoding).
27    pub withdrawn_routes: Bytes,
28    /// Raw path attributes.
29    pub path_attributes: Bytes,
30    /// Raw Network Layer Reachability Information.
31    pub nlri: Bytes,
32}
33
34/// A fully parsed UPDATE message with decoded prefixes and attributes.
35///
36/// Uses [`Ipv4NlriEntry`] to carry Add-Path path IDs alongside each prefix.
37/// For non-Add-Path peers, `path_id` is always 0.
38#[derive(Debug, Clone, PartialEq, Eq)]
39pub struct ParsedUpdate {
40    /// Withdrawn IPv4 NLRI entries.
41    pub withdrawn: Vec<Ipv4NlriEntry>,
42    /// Decoded path attributes.
43    pub attributes: Vec<PathAttribute>,
44    /// Announced IPv4 NLRI entries.
45    pub announced: Vec<Ipv4NlriEntry>,
46}
47
48impl UpdateMessage {
49    /// Decode an UPDATE message body from a buffer.
50    /// The header must already be consumed; `body_len` is
51    /// `header.length - HEADER_LEN`.
52    ///
53    /// # Errors
54    ///
55    /// Returns [`DecodeError::UpdateLengthMismatch`] if the body is too short
56    /// or length fields are inconsistent, or [`DecodeError::Incomplete`] if
57    /// the buffer has fewer bytes than `body_len`.
58    pub fn decode(buf: &mut impl Buf, body_len: usize) -> Result<Self, DecodeError> {
59        // Minimum body: withdrawn_len(2) + attrs_len(2) = 4
60        if body_len < 4 {
61            return Err(DecodeError::UpdateLengthMismatch {
62                detail: format!("body too short: {body_len} bytes (need at least 4)"),
63            });
64        }
65
66        if buf.remaining() < body_len {
67            return Err(DecodeError::Incomplete {
68                needed: body_len,
69                available: buf.remaining(),
70            });
71        }
72
73        let withdrawn_routes_len = buf.get_u16();
74
75        // Validate withdrawn routes fit in remaining body
76        // body_len = 2 (withdrawn_len) + withdrawn_routes + 2 (attrs_len) + attrs + nlri
77        let after_withdrawn = body_len
78            .checked_sub(2)
79            .and_then(|v| v.checked_sub(usize::from(withdrawn_routes_len)))
80            .ok_or_else(|| DecodeError::UpdateLengthMismatch {
81                detail: format!("withdrawn routes length {withdrawn_routes_len} exceeds body"),
82            })?;
83
84        if after_withdrawn < 2 {
85            return Err(DecodeError::UpdateLengthMismatch {
86                detail: format!(
87                    "no room for path attributes length after {withdrawn_routes_len} \
88                     bytes of withdrawn routes"
89                ),
90            });
91        }
92
93        let withdrawn_routes = buf.copy_to_bytes(usize::from(withdrawn_routes_len));
94
95        let path_attributes_len = buf.get_u16();
96
97        let nlri_len = after_withdrawn
98            .checked_sub(2)
99            .and_then(|v| v.checked_sub(usize::from(path_attributes_len)))
100            .ok_or_else(|| DecodeError::UpdateLengthMismatch {
101                detail: format!(
102                    "path attributes length {path_attributes_len} exceeds remaining body"
103                ),
104            })?;
105
106        let path_attributes = buf.copy_to_bytes(usize::from(path_attributes_len));
107        let nlri = buf.copy_to_bytes(nlri_len);
108
109        Ok(Self {
110            withdrawn_routes,
111            path_attributes,
112            nlri,
113        })
114    }
115
116    /// Parse the raw UPDATE into decoded prefixes and path attributes.
117    ///
118    /// `four_octet_as` controls whether AS numbers in `AS_PATH` are 2 or 4 bytes
119    /// wide (determined by capability negotiation).
120    ///
121    /// `add_path_ipv4` indicates whether the peer is sending Add-Path path IDs
122    /// for IPv4 body NLRI (RFC 7911). When false, decoded entries have `path_id = 0`.
123    ///
124    /// # Errors
125    ///
126    /// Returns `DecodeError` if NLRI or attribute data is malformed.
127    pub fn parse(
128        &self,
129        four_octet_as: bool,
130        add_path_ipv4: bool,
131        add_path_families: &[(Afi, Safi)],
132    ) -> Result<ParsedUpdate, DecodeError> {
133        let withdrawn = if add_path_ipv4 {
134            crate::nlri::decode_nlri_addpath(&self.withdrawn_routes)?
135        } else {
136            crate::nlri::decode_nlri(&self.withdrawn_routes)?
137                .into_iter()
138                .map(|prefix| Ipv4NlriEntry { path_id: 0, prefix })
139                .collect()
140        };
141        let attributes = crate::attribute::decode_path_attributes(
142            &self.path_attributes,
143            four_octet_as,
144            add_path_families,
145        )?;
146        let announced = if add_path_ipv4 {
147            crate::nlri::decode_nlri_addpath(&self.nlri)?
148        } else {
149            crate::nlri::decode_nlri(&self.nlri)?
150                .into_iter()
151                .map(|prefix| Ipv4NlriEntry { path_id: 0, prefix })
152                .collect()
153        };
154
155        Ok(ParsedUpdate {
156            withdrawn,
157            attributes,
158            announced,
159        })
160    }
161
162    /// Encode a complete UPDATE message (header + body) into a buffer.
163    ///
164    /// `max_message_len` is the negotiated maximum: 4096 normally, or 65535
165    /// when Extended Messages (RFC 8654) has been negotiated.
166    ///
167    /// # Errors
168    ///
169    /// Returns [`EncodeError::MessageTooLong`] if the encoded message exceeds
170    /// the negotiated maximum message size.
171    pub fn encode_with_limit(
172        &self,
173        buf: &mut impl BufMut,
174        max_message_len: u16,
175    ) -> Result<(), EncodeError> {
176        let body_len =
177            2 + self.withdrawn_routes.len() + 2 + self.path_attributes.len() + self.nlri.len();
178        let total_len = HEADER_LEN + body_len;
179
180        if total_len > usize::from(max_message_len) {
181            return Err(EncodeError::MessageTooLong { size: total_len });
182        }
183
184        let header = BgpHeader {
185            #[expect(clippy::cast_possible_truncation)]
186            length: total_len as u16,
187            message_type: MessageType::Update,
188        };
189        header.encode(buf);
190
191        #[expect(clippy::cast_possible_truncation)]
192        buf.put_u16(self.withdrawn_routes.len() as u16);
193        buf.put_slice(&self.withdrawn_routes);
194
195        #[expect(clippy::cast_possible_truncation)]
196        buf.put_u16(self.path_attributes.len() as u16);
197        buf.put_slice(&self.path_attributes);
198
199        buf.put_slice(&self.nlri);
200
201        Ok(())
202    }
203
204    /// Encode using the standard 4096-byte limit.
205    ///
206    /// # Errors
207    ///
208    /// Returns [`EncodeError::MessageTooLong`] if the encoded message exceeds
209    /// the standard 4096-byte maximum.
210    pub fn encode(&self, buf: &mut impl BufMut) -> Result<(), EncodeError> {
211        self.encode_with_limit(buf, MAX_MESSAGE_LEN)
212    }
213
214    /// Build an `UpdateMessage` from structured data.
215    ///
216    /// Encodes NLRI, withdrawn routes, and path attributes into the raw
217    /// `Bytes` fields that `encode()` expects.
218    ///
219    /// When `add_path` is true, path IDs are included in the wire encoding.
220    /// When false, only the prefix is encoded (path IDs are ignored).
221    #[must_use]
222    pub fn build(
223        announced: &[Ipv4NlriEntry],
224        withdrawn: &[Ipv4NlriEntry],
225        attributes: &[PathAttribute],
226        four_octet_as: bool,
227        add_path: bool,
228        ipv4_unicast_mode: Ipv4UnicastMode,
229    ) -> Self {
230        let mut withdrawn_buf = Vec::new();
231        if matches!(ipv4_unicast_mode, Ipv4UnicastMode::Body) {
232            if add_path {
233                crate::nlri::encode_nlri_addpath(withdrawn, &mut withdrawn_buf);
234            } else {
235                let prefixes: Vec<Ipv4Prefix> = withdrawn.iter().map(|e| e.prefix).collect();
236                crate::nlri::encode_nlri(&prefixes, &mut withdrawn_buf);
237            }
238        }
239
240        let mut attrs_buf = Vec::new();
241        if !attributes.is_empty() {
242            crate::attribute::encode_path_attributes(
243                attributes,
244                &mut attrs_buf,
245                four_octet_as,
246                add_path,
247            );
248        }
249
250        let mut nlri_buf = Vec::new();
251        if matches!(ipv4_unicast_mode, Ipv4UnicastMode::Body) {
252            if add_path {
253                crate::nlri::encode_nlri_addpath(announced, &mut nlri_buf);
254            } else {
255                let prefixes: Vec<Ipv4Prefix> = announced.iter().map(|e| e.prefix).collect();
256                crate::nlri::encode_nlri(&prefixes, &mut nlri_buf);
257            }
258        }
259
260        Self {
261            withdrawn_routes: Bytes::from(withdrawn_buf),
262            path_attributes: Bytes::from(attrs_buf),
263            nlri: Bytes::from(nlri_buf),
264        }
265    }
266
267    /// Total encoded size in bytes.
268    #[must_use]
269    pub fn encoded_len(&self) -> usize {
270        HEADER_LEN
271            + 2
272            + self.withdrawn_routes.len()
273            + 2
274            + self.path_attributes.len()
275            + self.nlri.len()
276    }
277}
278
279#[cfg(test)]
280mod tests {
281    use bytes::BytesMut;
282
283    use super::*;
284    use crate::constants::MAX_MESSAGE_LEN;
285    use crate::{NlriEntry, Prefix};
286
287    #[test]
288    fn decode_minimal_update() {
289        // withdrawn_len=0, attrs_len=0, no NLRI
290        let body: &[u8] = &[0, 0, 0, 0];
291        let mut buf = Bytes::copy_from_slice(body);
292        let msg = UpdateMessage::decode(&mut buf, 4).unwrap();
293        assert!(msg.withdrawn_routes.is_empty());
294        assert!(msg.path_attributes.is_empty());
295        assert!(msg.nlri.is_empty());
296    }
297
298    #[test]
299    fn decode_with_withdrawn_routes() {
300        // withdrawn_len=3, withdrawn=[0x18, 0x0A, 0x00] (10.0.0.0/24), attrs_len=0
301        let body: &[u8] = &[0, 3, 0x18, 0x0A, 0x00, 0, 0];
302        let mut buf = Bytes::copy_from_slice(body);
303        let msg = UpdateMessage::decode(&mut buf, 7).unwrap();
304        assert_eq!(msg.withdrawn_routes.as_ref(), &[0x18, 0x0A, 0x00]);
305        assert!(msg.path_attributes.is_empty());
306        assert!(msg.nlri.is_empty());
307    }
308
309    #[test]
310    fn decode_with_all_sections() {
311        let mut body = BytesMut::new();
312        body.put_u16(2); // withdrawn_len
313        body.put_slice(&[0x10, 0x0A]); // withdrawn
314        body.put_u16(3); // attrs_len
315        body.put_slice(&[0x40, 0x01, 0x00]); // attrs (fake)
316        body.put_slice(&[0x18, 0xC0, 0xA8]); // NLRI (fake)
317
318        let total = body.len();
319        let mut buf = body.freeze();
320        let msg = UpdateMessage::decode(&mut buf, total).unwrap();
321        assert_eq!(msg.withdrawn_routes.len(), 2);
322        assert_eq!(msg.path_attributes.len(), 3);
323        assert_eq!(msg.nlri.len(), 3);
324    }
325
326    #[test]
327    fn reject_withdrawn_overflow() {
328        // withdrawn_len=100, but body is only 6 bytes
329        let body: &[u8] = &[0, 100, 0, 0, 0, 0];
330        let mut buf = Bytes::copy_from_slice(body);
331        assert!(matches!(
332            UpdateMessage::decode(&mut buf, 6),
333            Err(DecodeError::UpdateLengthMismatch { .. })
334        ));
335    }
336
337    #[test]
338    fn reject_attrs_overflow() {
339        // withdrawn_len=0, attrs_len=100, but body is only 4 bytes
340        let body: &[u8] = &[0, 0, 0, 100];
341        let mut buf = Bytes::copy_from_slice(body);
342        assert!(matches!(
343            UpdateMessage::decode(&mut buf, 4),
344            Err(DecodeError::UpdateLengthMismatch { .. })
345        ));
346    }
347
348    #[test]
349    fn encode_decode_roundtrip() {
350        let original = UpdateMessage {
351            withdrawn_routes: Bytes::from_static(&[0x18, 0x0A, 0x00]),
352            path_attributes: Bytes::from_static(&[0x40, 0x01, 0x00]),
353            nlri: Bytes::from_static(&[0x18, 0xC0, 0xA8]),
354        };
355
356        let mut encoded = BytesMut::with_capacity(original.encoded_len());
357        original.encode(&mut encoded).unwrap();
358
359        let mut bytes = encoded.freeze();
360        let header = BgpHeader::decode(&mut bytes, MAX_MESSAGE_LEN).unwrap();
361        assert_eq!(header.message_type, MessageType::Update);
362
363        let body_len = usize::from(header.length) - HEADER_LEN;
364        let decoded = UpdateMessage::decode(&mut bytes, body_len).unwrap();
365        assert_eq!(original, decoded);
366    }
367
368    /// Helper to create an `Ipv4NlriEntry` with `path_id=0`.
369    fn entry(prefix: Ipv4Prefix) -> Ipv4NlriEntry {
370        Ipv4NlriEntry { path_id: 0, prefix }
371    }
372
373    #[test]
374    fn build_roundtrip() {
375        use crate::attribute::{AsPath, AsPathSegment, Origin};
376
377        let announced = vec![
378            entry(Ipv4Prefix::new(std::net::Ipv4Addr::new(10, 0, 0, 0), 24)),
379            entry(Ipv4Prefix::new(std::net::Ipv4Addr::new(192, 168, 1, 0), 24)),
380        ];
381        let attrs = vec![
382            PathAttribute::Origin(Origin::Igp),
383            PathAttribute::AsPath(AsPath {
384                segments: vec![AsPathSegment::AsSequence(vec![65001])],
385            }),
386            PathAttribute::NextHop(std::net::Ipv4Addr::new(10, 0, 0, 1)),
387        ];
388
389        let msg = UpdateMessage::build(&announced, &[], &attrs, true, false, Ipv4UnicastMode::Body);
390        let parsed = msg.parse(true, false, &[]).unwrap();
391        assert_eq!(parsed.announced, announced);
392        assert!(parsed.withdrawn.is_empty());
393        assert_eq!(parsed.attributes, attrs);
394    }
395
396    #[test]
397    fn build_ipv4_mp_mode_omits_body_nlri() {
398        use std::net::{IpAddr, Ipv6Addr};
399
400        use crate::attribute::{AsPath, AsPathSegment, MpReachNlri, Origin};
401
402        let announced = vec![entry(Ipv4Prefix::new(
403            std::net::Ipv4Addr::new(10, 0, 0, 0),
404            24,
405        ))];
406        let attrs = vec![
407            PathAttribute::Origin(Origin::Igp),
408            PathAttribute::AsPath(AsPath {
409                segments: vec![AsPathSegment::AsSequence(vec![65001])],
410            }),
411            PathAttribute::MpReachNlri(MpReachNlri {
412                afi: Afi::Ipv4,
413                safi: Safi::Unicast,
414                next_hop: IpAddr::V6(Ipv6Addr::LOCALHOST),
415                link_local_next_hop: None,
416                announced: vec![NlriEntry {
417                    path_id: 0,
418                    prefix: Prefix::V4(announced[0].prefix),
419                }],
420                flowspec_announced: vec![],
421                evpn_announced: vec![],
422            }),
423        ];
424
425        let msg = UpdateMessage::build(
426            &announced,
427            &[],
428            &attrs,
429            true,
430            false,
431            Ipv4UnicastMode::MpReach,
432        );
433        assert!(msg.withdrawn_routes.is_empty());
434        assert!(msg.nlri.is_empty());
435
436        let parsed = msg.parse(true, false, &[]).unwrap();
437        assert!(parsed.announced.is_empty());
438        let mp = parsed
439            .attributes
440            .iter()
441            .find_map(|attr| match attr {
442                PathAttribute::MpReachNlri(mp) => Some(mp),
443                _ => None,
444            })
445            .unwrap();
446        assert_eq!(mp.afi, Afi::Ipv4);
447        assert_eq!(mp.safi, Safi::Unicast);
448        assert_eq!(mp.announced.len(), 1);
449        assert_eq!(mp.announced[0].prefix, Prefix::V4(announced[0].prefix));
450        assert_eq!(mp.next_hop, IpAddr::V6(Ipv6Addr::LOCALHOST));
451    }
452
453    #[test]
454    fn build_withdrawal_only() {
455        let withdrawn = vec![entry(Ipv4Prefix::new(
456            std::net::Ipv4Addr::new(10, 0, 0, 0),
457            24,
458        ))];
459        let msg = UpdateMessage::build(&[], &withdrawn, &[], true, false, Ipv4UnicastMode::Body);
460        let parsed = msg.parse(true, false, &[]).unwrap();
461        assert!(parsed.announced.is_empty());
462        assert_eq!(parsed.withdrawn, withdrawn);
463        assert!(parsed.attributes.is_empty());
464    }
465
466    #[test]
467    fn build_announce_only() {
468        use crate::attribute::Origin;
469
470        let announced = vec![entry(Ipv4Prefix::new(
471            std::net::Ipv4Addr::new(10, 1, 0, 0),
472            16,
473        ))];
474        let attrs = vec![
475            PathAttribute::Origin(Origin::Igp),
476            PathAttribute::NextHop(std::net::Ipv4Addr::new(10, 0, 0, 1)),
477        ];
478        let msg = UpdateMessage::build(&announced, &[], &attrs, true, false, Ipv4UnicastMode::Body);
479
480        // Verify it encodes and decodes properly
481        let mut encoded = BytesMut::with_capacity(msg.encoded_len());
482        msg.encode(&mut encoded).unwrap();
483
484        let mut bytes = encoded.freeze();
485        let header = BgpHeader::decode(&mut bytes, MAX_MESSAGE_LEN).unwrap();
486        let body_len = usize::from(header.length) - HEADER_LEN;
487        let decoded = UpdateMessage::decode(&mut bytes, body_len).unwrap();
488        let parsed = decoded.parse(true, false, &[]).unwrap();
489        assert_eq!(parsed.announced, announced);
490        assert_eq!(parsed.attributes, attrs);
491    }
492
493    #[test]
494    fn build_mixed() {
495        use crate::attribute::Origin;
496
497        let announced = vec![entry(Ipv4Prefix::new(
498            std::net::Ipv4Addr::new(10, 0, 0, 0),
499            24,
500        ))];
501        let withdrawn = vec![entry(Ipv4Prefix::new(
502            std::net::Ipv4Addr::new(172, 16, 0, 0),
503            16,
504        ))];
505        let attrs = vec![
506            PathAttribute::Origin(Origin::Igp),
507            PathAttribute::NextHop(std::net::Ipv4Addr::new(10, 0, 0, 1)),
508        ];
509
510        let msg = UpdateMessage::build(
511            &announced,
512            &withdrawn,
513            &attrs,
514            true,
515            false,
516            Ipv4UnicastMode::Body,
517        );
518        let parsed = msg.parse(true, false, &[]).unwrap();
519        assert_eq!(parsed.announced, announced);
520        assert_eq!(parsed.withdrawn, withdrawn);
521        assert_eq!(parsed.attributes, attrs);
522    }
523
524    #[test]
525    fn build_roundtrip_with_add_path() {
526        use crate::attribute::{AsPath, AsPathSegment, Origin};
527
528        let announced = vec![
529            Ipv4NlriEntry {
530                path_id: 1,
531                prefix: Ipv4Prefix::new(std::net::Ipv4Addr::new(10, 0, 0, 0), 24),
532            },
533            Ipv4NlriEntry {
534                path_id: 2,
535                prefix: Ipv4Prefix::new(std::net::Ipv4Addr::new(10, 0, 0, 0), 24),
536            },
537        ];
538        let withdrawn = vec![Ipv4NlriEntry {
539            path_id: 3,
540            prefix: Ipv4Prefix::new(std::net::Ipv4Addr::new(192, 168, 0, 0), 16),
541        }];
542        let attrs = vec![
543            PathAttribute::Origin(Origin::Igp),
544            PathAttribute::AsPath(AsPath {
545                segments: vec![AsPathSegment::AsSequence(vec![65001])],
546            }),
547            PathAttribute::NextHop(std::net::Ipv4Addr::new(10, 0, 0, 1)),
548        ];
549
550        let msg = UpdateMessage::build(
551            &announced,
552            &withdrawn,
553            &attrs,
554            true,
555            true,
556            Ipv4UnicastMode::Body,
557        );
558        let parsed = msg.parse(true, true, &[]).unwrap();
559        assert_eq!(parsed.announced, announced);
560        assert_eq!(parsed.withdrawn, withdrawn);
561        assert_eq!(parsed.attributes, attrs);
562    }
563
564    #[test]
565    fn reject_message_too_long() {
566        let msg = UpdateMessage {
567            withdrawn_routes: Bytes::new(),
568            path_attributes: Bytes::from(vec![0u8; 4096]),
569            nlri: Bytes::new(),
570        };
571        let mut buf = BytesMut::with_capacity(5000);
572        assert!(matches!(
573            msg.encode(&mut buf),
574            Err(EncodeError::MessageTooLong { .. })
575        ));
576    }
577}