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                announced: vec![NlriEntry {
416                    path_id: 0,
417                    prefix: Prefix::V4(announced[0].prefix),
418                }],
419                flowspec_announced: vec![],
420            }),
421        ];
422
423        let msg = UpdateMessage::build(
424            &announced,
425            &[],
426            &attrs,
427            true,
428            false,
429            Ipv4UnicastMode::MpReach,
430        );
431        assert!(msg.withdrawn_routes.is_empty());
432        assert!(msg.nlri.is_empty());
433
434        let parsed = msg.parse(true, false, &[]).unwrap();
435        assert!(parsed.announced.is_empty());
436        let mp = parsed
437            .attributes
438            .iter()
439            .find_map(|attr| match attr {
440                PathAttribute::MpReachNlri(mp) => Some(mp),
441                _ => None,
442            })
443            .unwrap();
444        assert_eq!(mp.afi, Afi::Ipv4);
445        assert_eq!(mp.safi, Safi::Unicast);
446        assert_eq!(mp.announced.len(), 1);
447        assert_eq!(mp.announced[0].prefix, Prefix::V4(announced[0].prefix));
448        assert_eq!(mp.next_hop, IpAddr::V6(Ipv6Addr::LOCALHOST));
449    }
450
451    #[test]
452    fn build_withdrawal_only() {
453        let withdrawn = vec![entry(Ipv4Prefix::new(
454            std::net::Ipv4Addr::new(10, 0, 0, 0),
455            24,
456        ))];
457        let msg = UpdateMessage::build(&[], &withdrawn, &[], true, false, Ipv4UnicastMode::Body);
458        let parsed = msg.parse(true, false, &[]).unwrap();
459        assert!(parsed.announced.is_empty());
460        assert_eq!(parsed.withdrawn, withdrawn);
461        assert!(parsed.attributes.is_empty());
462    }
463
464    #[test]
465    fn build_announce_only() {
466        use crate::attribute::Origin;
467
468        let announced = vec![entry(Ipv4Prefix::new(
469            std::net::Ipv4Addr::new(10, 1, 0, 0),
470            16,
471        ))];
472        let attrs = vec![
473            PathAttribute::Origin(Origin::Igp),
474            PathAttribute::NextHop(std::net::Ipv4Addr::new(10, 0, 0, 1)),
475        ];
476        let msg = UpdateMessage::build(&announced, &[], &attrs, true, false, Ipv4UnicastMode::Body);
477
478        // Verify it encodes and decodes properly
479        let mut encoded = BytesMut::with_capacity(msg.encoded_len());
480        msg.encode(&mut encoded).unwrap();
481
482        let mut bytes = encoded.freeze();
483        let header = BgpHeader::decode(&mut bytes, MAX_MESSAGE_LEN).unwrap();
484        let body_len = usize::from(header.length) - HEADER_LEN;
485        let decoded = UpdateMessage::decode(&mut bytes, body_len).unwrap();
486        let parsed = decoded.parse(true, false, &[]).unwrap();
487        assert_eq!(parsed.announced, announced);
488        assert_eq!(parsed.attributes, attrs);
489    }
490
491    #[test]
492    fn build_mixed() {
493        use crate::attribute::Origin;
494
495        let announced = vec![entry(Ipv4Prefix::new(
496            std::net::Ipv4Addr::new(10, 0, 0, 0),
497            24,
498        ))];
499        let withdrawn = vec![entry(Ipv4Prefix::new(
500            std::net::Ipv4Addr::new(172, 16, 0, 0),
501            16,
502        ))];
503        let attrs = vec![
504            PathAttribute::Origin(Origin::Igp),
505            PathAttribute::NextHop(std::net::Ipv4Addr::new(10, 0, 0, 1)),
506        ];
507
508        let msg = UpdateMessage::build(
509            &announced,
510            &withdrawn,
511            &attrs,
512            true,
513            false,
514            Ipv4UnicastMode::Body,
515        );
516        let parsed = msg.parse(true, false, &[]).unwrap();
517        assert_eq!(parsed.announced, announced);
518        assert_eq!(parsed.withdrawn, withdrawn);
519        assert_eq!(parsed.attributes, attrs);
520    }
521
522    #[test]
523    fn build_roundtrip_with_add_path() {
524        use crate::attribute::{AsPath, AsPathSegment, Origin};
525
526        let announced = vec![
527            Ipv4NlriEntry {
528                path_id: 1,
529                prefix: Ipv4Prefix::new(std::net::Ipv4Addr::new(10, 0, 0, 0), 24),
530            },
531            Ipv4NlriEntry {
532                path_id: 2,
533                prefix: Ipv4Prefix::new(std::net::Ipv4Addr::new(10, 0, 0, 0), 24),
534            },
535        ];
536        let withdrawn = vec![Ipv4NlriEntry {
537            path_id: 3,
538            prefix: Ipv4Prefix::new(std::net::Ipv4Addr::new(192, 168, 0, 0), 16),
539        }];
540        let attrs = vec![
541            PathAttribute::Origin(Origin::Igp),
542            PathAttribute::AsPath(AsPath {
543                segments: vec![AsPathSegment::AsSequence(vec![65001])],
544            }),
545            PathAttribute::NextHop(std::net::Ipv4Addr::new(10, 0, 0, 1)),
546        ];
547
548        let msg = UpdateMessage::build(
549            &announced,
550            &withdrawn,
551            &attrs,
552            true,
553            true,
554            Ipv4UnicastMode::Body,
555        );
556        let parsed = msg.parse(true, true, &[]).unwrap();
557        assert_eq!(parsed.announced, announced);
558        assert_eq!(parsed.withdrawn, withdrawn);
559        assert_eq!(parsed.attributes, attrs);
560    }
561
562    #[test]
563    fn reject_message_too_long() {
564        let msg = UpdateMessage {
565            withdrawn_routes: Bytes::new(),
566            path_attributes: Bytes::from(vec![0u8; 4096]),
567            nlri: Bytes::new(),
568        };
569        let mut buf = BytesMut::with_capacity(5000);
570        assert!(matches!(
571            msg.encode(&mut buf),
572            Err(EncodeError::MessageTooLong { .. })
573        ));
574    }
575}