Skip to main content

rustbgpd_wire/
validate.rs

1use std::collections::HashSet;
2use std::net::IpAddr;
3
4use crate::attribute::{AsPath, AsPathSegment, PathAttribute, attr_error_data};
5use crate::constants::{attr_flags, attr_type};
6use crate::notification::update_subcode;
7
8/// Error produced by UPDATE attribute validation.
9///
10/// Contains the NOTIFICATION subcode and data bytes per RFC 4271 §6.3.
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct UpdateError {
13    /// NOTIFICATION subcode for this validation error.
14    pub subcode: u8,
15    /// Raw bytes for the NOTIFICATION data field.
16    pub data: Vec<u8>,
17}
18
19/// Well-known attribute type codes that MUST be present when NLRI is advertised.
20const MANDATORY_ATTRS: &[u8] = &[attr_type::ORIGIN, attr_type::AS_PATH];
21
22/// Validate the semantic correctness of a set of path attributes.
23///
24/// This is separate from decode (which is structural — "can I read these bytes?").
25/// Validation checks whether the attribute set is RFC-compliant for this UPDATE.
26///
27/// `has_nlri` — true if the UPDATE carries announced prefixes (body or MP).
28/// `has_body_nlri` — true if the UPDATE carries IPv4 NLRI in the body fields.
29/// `is_ebgp` — true if the session is external BGP.
30///
31/// # Errors
32///
33/// Returns an `UpdateError` with the appropriate subcode and data.
34pub fn validate_update_attributes(
35    attrs: &[PathAttribute],
36    has_nlri: bool,
37    has_body_nlri: bool,
38    is_ebgp: bool,
39) -> Result<(), UpdateError> {
40    check_duplicate_types(attrs)?;
41    check_unrecognized_wellknown(attrs)?;
42
43    if has_nlri {
44        check_mandatory_present(attrs, has_body_nlri, is_ebgp)?;
45    }
46
47    for attr in attrs {
48        match attr {
49            PathAttribute::NextHop(addr) => check_next_hop(*addr)?,
50            PathAttribute::AsPath(path) => check_as_path(path)?,
51            PathAttribute::MpReachNlri(mp) => check_mp_reach_next_hop(mp.next_hop)?,
52            _ => {}
53        }
54    }
55
56    Ok(())
57}
58
59/// (3,1) Duplicate attribute type codes.
60fn check_duplicate_types(attrs: &[PathAttribute]) -> Result<(), UpdateError> {
61    let mut seen = HashSet::new();
62    for attr in attrs {
63        let tc = attr.type_code();
64        if !seen.insert(tc) {
65            return Err(UpdateError {
66                subcode: update_subcode::MALFORMED_ATTRIBUTE_LIST,
67                data: vec![],
68            });
69        }
70    }
71    Ok(())
72}
73
74/// (3,2) Unrecognized well-known attribute: Optional=0 and type code unknown.
75fn check_unrecognized_wellknown(attrs: &[PathAttribute]) -> Result<(), UpdateError> {
76    for attr in attrs {
77        if let PathAttribute::Unknown(raw) = attr {
78            // If Optional bit is NOT set, it claims to be well-known
79            if (raw.flags & attr_flags::OPTIONAL) == 0 {
80                return Err(UpdateError {
81                    subcode: update_subcode::UNRECOGNIZED_WELLKNOWN,
82                    data: attr_error_data(raw.flags, raw.type_code, &raw.data),
83                });
84            }
85        }
86    }
87    Ok(())
88}
89
90/// (3,3) Missing mandatory well-known attributes.
91///
92/// `has_body_nlri` — true if the UPDATE carries IPv4 NLRI in the body fields.
93/// When only `MP_REACH_NLRI` is present (no body NLRI), `NEXT_HOP` is carried
94/// inside the MP attribute (RFC 4760 §3) and not required as a separate attribute.
95/// Mixed UPDATEs (body NLRI + `MP_REACH_NLRI`) still require body `NEXT_HOP`.
96fn check_mandatory_present(
97    attrs: &[PathAttribute],
98    has_body_nlri: bool,
99    is_ebgp: bool,
100) -> Result<(), UpdateError> {
101    let present: HashSet<u8> = attrs.iter().map(PathAttribute::type_code).collect();
102
103    for &tc in MANDATORY_ATTRS {
104        if !present.contains(&tc) {
105            return Err(UpdateError {
106                subcode: update_subcode::MISSING_WELLKNOWN,
107                data: vec![tc],
108            });
109        }
110    }
111
112    // NEXT_HOP mandatory for eBGP when body NLRI is present. When only MP_REACH
113    // carries NLRI, the next-hop is inside the MP attribute (RFC 4760 §3).
114    if is_ebgp && has_body_nlri && !present.contains(&attr_type::NEXT_HOP) {
115        return Err(UpdateError {
116            subcode: update_subcode::MISSING_WELLKNOWN,
117            data: vec![attr_type::NEXT_HOP],
118        });
119    }
120
121    Ok(())
122}
123
124/// (3,8) Invalid `NEXT_HOP` address.
125fn check_next_hop(addr: std::net::Ipv4Addr) -> Result<(), UpdateError> {
126    let octets = addr.octets();
127
128    // 0.0.0.0
129    if addr.is_unspecified() {
130        return Err(UpdateError {
131            subcode: update_subcode::INVALID_NEXT_HOP,
132            data: octets.to_vec(),
133        });
134    }
135
136    // 127.0.0.0/8
137    if addr.is_loopback() {
138        return Err(UpdateError {
139            subcode: update_subcode::INVALID_NEXT_HOP,
140            data: octets.to_vec(),
141        });
142    }
143
144    // 224.0.0.0/4 (multicast)
145    if addr.is_multicast() {
146        return Err(UpdateError {
147            subcode: update_subcode::INVALID_NEXT_HOP,
148            data: octets.to_vec(),
149        });
150    }
151
152    // 255.255.255.255
153    if addr.is_broadcast() {
154        return Err(UpdateError {
155            subcode: update_subcode::INVALID_NEXT_HOP,
156            data: octets.to_vec(),
157        });
158    }
159
160    Ok(())
161}
162
163/// Validate `MP_REACH_NLRI` next-hop address.
164fn check_mp_reach_next_hop(addr: IpAddr) -> Result<(), UpdateError> {
165    match addr {
166        IpAddr::V4(v4) => check_next_hop(v4)?,
167        IpAddr::V6(v6) => {
168            if !is_valid_ipv6_nexthop(&v6) {
169                return Err(UpdateError {
170                    subcode: update_subcode::INVALID_NEXT_HOP,
171                    data: v6.octets().to_vec(),
172                });
173            }
174        }
175    }
176    Ok(())
177}
178
179/// Check if an IPv6 address is link-local (`fe80::/10`).
180fn is_ipv6_link_local(addr: &std::net::Ipv6Addr) -> bool {
181    (addr.segments()[0] & 0xffc0) == 0xfe80
182}
183
184/// Returns `true` if `addr` is a valid IPv6 next-hop for BGP advertisements.
185///
186/// Rejects unspecified (`::`), loopback (`::1`), multicast (`ff00::/8`),
187/// and link-local (`fe80::/10`) addresses.
188#[must_use]
189pub fn is_valid_ipv6_nexthop(addr: &std::net::Ipv6Addr) -> bool {
190    !addr.is_unspecified()
191        && !addr.is_loopback()
192        && !addr.is_multicast()
193        && !is_ipv6_link_local(addr)
194}
195
196/// (3,11) Malformed `AS_PATH`.
197fn check_as_path(path: &AsPath) -> Result<(), UpdateError> {
198    for segment in &path.segments {
199        let asns = match segment {
200            AsPathSegment::AsSet(asns) | AsPathSegment::AsSequence(asns) => asns,
201        };
202        if asns.is_empty() {
203            return Err(UpdateError {
204                subcode: update_subcode::MALFORMED_AS_PATH,
205                data: vec![],
206            });
207        }
208    }
209    Ok(())
210}
211
212#[cfg(test)]
213mod tests {
214    use std::net::Ipv4Addr;
215
216    use bytes::Bytes;
217
218    use super::*;
219    use crate::attribute::{Origin, RawAttribute};
220
221    fn basic_attrs(next_hop: Ipv4Addr) -> Vec<PathAttribute> {
222        vec![
223            PathAttribute::Origin(Origin::Igp),
224            PathAttribute::AsPath(AsPath {
225                segments: vec![AsPathSegment::AsSequence(vec![65001])],
226            }),
227            PathAttribute::NextHop(next_hop),
228        ]
229    }
230
231    #[test]
232    fn valid_ebgp_update() {
233        let attrs = basic_attrs(Ipv4Addr::new(10, 0, 0, 1));
234        assert!(validate_update_attributes(&attrs, true, true, true).is_ok());
235    }
236
237    #[test]
238    fn valid_ibgp_update_no_next_hop() {
239        // iBGP doesn't require NEXT_HOP (it's optional based on the peer)
240        let attrs = vec![
241            PathAttribute::Origin(Origin::Igp),
242            PathAttribute::AsPath(AsPath {
243                segments: vec![AsPathSegment::AsSequence(vec![65001])],
244            }),
245        ];
246        assert!(validate_update_attributes(&attrs, true, true, false).is_ok());
247    }
248
249    #[test]
250    fn withdrawal_only_no_attrs_ok() {
251        // No NLRI → no mandatory attributes required
252        assert!(validate_update_attributes(&[], false, false, true).is_ok());
253    }
254
255    #[test]
256    fn reject_duplicate_type() {
257        let attrs = vec![
258            PathAttribute::Origin(Origin::Igp),
259            PathAttribute::Origin(Origin::Egp),
260        ];
261        let err = validate_update_attributes(&attrs, false, false, true).unwrap_err();
262        assert_eq!(err.subcode, update_subcode::MALFORMED_ATTRIBUTE_LIST);
263    }
264
265    #[test]
266    fn reject_missing_origin() {
267        let attrs = vec![
268            PathAttribute::AsPath(AsPath {
269                segments: vec![AsPathSegment::AsSequence(vec![65001])],
270            }),
271            PathAttribute::NextHop(Ipv4Addr::new(10, 0, 0, 1)),
272        ];
273        let err = validate_update_attributes(&attrs, true, true, true).unwrap_err();
274        assert_eq!(err.subcode, update_subcode::MISSING_WELLKNOWN);
275    }
276
277    #[test]
278    fn reject_missing_as_path() {
279        let attrs = vec![
280            PathAttribute::Origin(Origin::Igp),
281            PathAttribute::NextHop(Ipv4Addr::new(10, 0, 0, 1)),
282        ];
283        let err = validate_update_attributes(&attrs, true, true, true).unwrap_err();
284        assert_eq!(err.subcode, update_subcode::MISSING_WELLKNOWN);
285    }
286
287    #[test]
288    fn reject_missing_next_hop_ebgp() {
289        let attrs = vec![
290            PathAttribute::Origin(Origin::Igp),
291            PathAttribute::AsPath(AsPath {
292                segments: vec![AsPathSegment::AsSequence(vec![65001])],
293            }),
294        ];
295        let err = validate_update_attributes(&attrs, true, true, true).unwrap_err();
296        assert_eq!(err.subcode, update_subcode::MISSING_WELLKNOWN);
297        assert_eq!(err.data, vec![attr_type::NEXT_HOP]);
298    }
299
300    #[test]
301    fn reject_next_hop_unspecified() {
302        let attrs = basic_attrs(Ipv4Addr::UNSPECIFIED);
303        let err = validate_update_attributes(&attrs, true, true, true).unwrap_err();
304        assert_eq!(err.subcode, update_subcode::INVALID_NEXT_HOP);
305    }
306
307    #[test]
308    fn reject_next_hop_loopback() {
309        let attrs = basic_attrs(Ipv4Addr::LOCALHOST);
310        let err = validate_update_attributes(&attrs, true, true, true).unwrap_err();
311        assert_eq!(err.subcode, update_subcode::INVALID_NEXT_HOP);
312    }
313
314    #[test]
315    fn reject_next_hop_multicast() {
316        let attrs = basic_attrs(Ipv4Addr::new(224, 0, 0, 1));
317        let err = validate_update_attributes(&attrs, true, true, true).unwrap_err();
318        assert_eq!(err.subcode, update_subcode::INVALID_NEXT_HOP);
319    }
320
321    #[test]
322    fn reject_next_hop_broadcast() {
323        let attrs = basic_attrs(Ipv4Addr::BROADCAST);
324        let err = validate_update_attributes(&attrs, true, true, true).unwrap_err();
325        assert_eq!(err.subcode, update_subcode::INVALID_NEXT_HOP);
326    }
327
328    #[test]
329    fn reject_empty_as_path_segment() {
330        let attrs = vec![
331            PathAttribute::Origin(Origin::Igp),
332            PathAttribute::AsPath(AsPath {
333                segments: vec![AsPathSegment::AsSequence(vec![])],
334            }),
335            PathAttribute::NextHop(Ipv4Addr::new(10, 0, 0, 1)),
336        ];
337        let err = validate_update_attributes(&attrs, true, true, true).unwrap_err();
338        assert_eq!(err.subcode, update_subcode::MALFORMED_AS_PATH);
339    }
340
341    #[test]
342    fn reject_unrecognized_wellknown() {
343        let attrs = vec![PathAttribute::Unknown(RawAttribute {
344            flags: attr_flags::TRANSITIVE, // Optional=0 → claims well-known
345            type_code: 99,
346            data: Bytes::from_static(&[1, 2, 3]),
347        })];
348        let err = validate_update_attributes(&attrs, false, false, true).unwrap_err();
349        assert_eq!(err.subcode, update_subcode::UNRECOGNIZED_WELLKNOWN);
350    }
351
352    #[test]
353    fn optional_unknown_attribute_ok() {
354        let attrs = vec![PathAttribute::Unknown(RawAttribute {
355            flags: attr_flags::OPTIONAL | attr_flags::TRANSITIVE,
356            type_code: 99,
357            data: Bytes::from_static(&[1, 2, 3]),
358        })];
359        assert!(validate_update_attributes(&attrs, false, false, true).is_ok());
360    }
361
362    // --- MP_REACH_NLRI validation tests ---
363
364    #[test]
365    fn mp_reach_nlri_no_body_next_hop_required_for_ebgp() {
366        use crate::attribute::MpReachNlri;
367        use crate::capability::{Afi, Safi};
368        use crate::nlri::{Ipv6Prefix, NlriEntry, Prefix};
369
370        // eBGP UPDATE with MP_REACH_NLRI only (no body NLRI): NEXT_HOP not required
371        let attrs = vec![
372            PathAttribute::Origin(Origin::Igp),
373            PathAttribute::AsPath(AsPath {
374                segments: vec![AsPathSegment::AsSequence(vec![65001])],
375            }),
376            PathAttribute::MpReachNlri(MpReachNlri {
377                afi: Afi::Ipv6,
378                safi: Safi::Unicast,
379                next_hop: std::net::IpAddr::V6("2001:db8::1".parse().unwrap()),
380                announced: vec![NlriEntry {
381                    path_id: 0,
382                    prefix: Prefix::V6(Ipv6Prefix::new("2001:db8::".parse().unwrap(), 32)),
383                }],
384                flowspec_announced: vec![],
385            }),
386        ];
387        // has_nlri=true, has_body_nlri=false (only MP NLRI), is_ebgp=true
388        assert!(validate_update_attributes(&attrs, true, false, true).is_ok());
389    }
390
391    #[test]
392    fn mixed_update_requires_body_next_hop_for_ebgp() {
393        use crate::attribute::MpReachNlri;
394        use crate::capability::{Afi, Safi};
395        use crate::nlri::{Ipv6Prefix, NlriEntry, Prefix};
396
397        // eBGP UPDATE with BOTH body NLRI and MP_REACH_NLRI but no NEXT_HOP attr
398        let attrs = vec![
399            PathAttribute::Origin(Origin::Igp),
400            PathAttribute::AsPath(AsPath {
401                segments: vec![AsPathSegment::AsSequence(vec![65001])],
402            }),
403            PathAttribute::MpReachNlri(MpReachNlri {
404                afi: Afi::Ipv6,
405                safi: Safi::Unicast,
406                next_hop: std::net::IpAddr::V6("2001:db8::1".parse().unwrap()),
407                announced: vec![NlriEntry {
408                    path_id: 0,
409                    prefix: Prefix::V6(Ipv6Prefix::new("2001:db8::".parse().unwrap(), 32)),
410                }],
411                flowspec_announced: vec![],
412            }),
413        ];
414        // has_nlri=true, has_body_nlri=true (body IPv4 NLRI present), is_ebgp=true
415        // → should require NEXT_HOP for the body NLRI
416        let err = validate_update_attributes(&attrs, true, true, true).unwrap_err();
417        assert_eq!(err.subcode, update_subcode::MISSING_WELLKNOWN);
418        assert_eq!(err.data, vec![attr_type::NEXT_HOP]);
419    }
420
421    #[test]
422    fn mp_reach_nlri_reject_unspecified_v6_next_hop() {
423        use crate::attribute::MpReachNlri;
424        use crate::capability::{Afi, Safi};
425
426        let attrs = vec![
427            PathAttribute::Origin(Origin::Igp),
428            PathAttribute::AsPath(AsPath {
429                segments: vec![AsPathSegment::AsSequence(vec![65001])],
430            }),
431            PathAttribute::MpReachNlri(MpReachNlri {
432                afi: Afi::Ipv6,
433                safi: Safi::Unicast,
434                next_hop: std::net::IpAddr::V6(std::net::Ipv6Addr::UNSPECIFIED),
435                announced: vec![],
436                flowspec_announced: vec![],
437            }),
438        ];
439        let err = validate_update_attributes(&attrs, true, false, true).unwrap_err();
440        assert_eq!(err.subcode, update_subcode::INVALID_NEXT_HOP);
441    }
442
443    #[test]
444    fn mp_reach_nlri_reject_link_local_v6_next_hop() {
445        use crate::attribute::MpReachNlri;
446        use crate::capability::{Afi, Safi};
447
448        let attrs = vec![
449            PathAttribute::Origin(Origin::Igp),
450            PathAttribute::AsPath(AsPath {
451                segments: vec![AsPathSegment::AsSequence(vec![65001])],
452            }),
453            PathAttribute::MpReachNlri(MpReachNlri {
454                afi: Afi::Ipv6,
455                safi: Safi::Unicast,
456                next_hop: std::net::IpAddr::V6("fe80::1".parse().unwrap()),
457                announced: vec![],
458                flowspec_announced: vec![],
459            }),
460        ];
461        let err = validate_update_attributes(&attrs, true, false, true).unwrap_err();
462        assert_eq!(err.subcode, update_subcode::INVALID_NEXT_HOP);
463    }
464
465    #[test]
466    fn mp_reach_nlri_reject_loopback_v6_next_hop() {
467        use crate::attribute::MpReachNlri;
468        use crate::capability::{Afi, Safi};
469
470        let attrs = vec![
471            PathAttribute::Origin(Origin::Igp),
472            PathAttribute::AsPath(AsPath {
473                segments: vec![AsPathSegment::AsSequence(vec![65001])],
474            }),
475            PathAttribute::MpReachNlri(MpReachNlri {
476                afi: Afi::Ipv6,
477                safi: Safi::Unicast,
478                next_hop: std::net::IpAddr::V6(std::net::Ipv6Addr::LOCALHOST),
479                announced: vec![],
480                flowspec_announced: vec![],
481            }),
482        ];
483        let err = validate_update_attributes(&attrs, true, false, true).unwrap_err();
484        assert_eq!(err.subcode, update_subcode::INVALID_NEXT_HOP);
485    }
486
487    #[test]
488    fn is_valid_ipv6_nexthop_accepts_global() {
489        assert!(super::is_valid_ipv6_nexthop(
490            &"2001:db8::1".parse().unwrap()
491        ));
492    }
493
494    #[test]
495    fn is_valid_ipv6_nexthop_rejects_unspecified() {
496        assert!(!super::is_valid_ipv6_nexthop(
497            &std::net::Ipv6Addr::UNSPECIFIED
498        ));
499    }
500
501    #[test]
502    fn is_valid_ipv6_nexthop_rejects_loopback() {
503        assert!(!super::is_valid_ipv6_nexthop(
504            &std::net::Ipv6Addr::LOCALHOST
505        ));
506    }
507
508    #[test]
509    fn is_valid_ipv6_nexthop_rejects_link_local() {
510        assert!(!super::is_valid_ipv6_nexthop(&"fe80::1".parse().unwrap()));
511    }
512
513    #[test]
514    fn is_valid_ipv6_nexthop_rejects_multicast() {
515        assert!(!super::is_valid_ipv6_nexthop(&"ff02::1".parse().unwrap()));
516    }
517
518    #[test]
519    fn mp_reach_nlri_reject_multicast_v6_next_hop() {
520        use crate::attribute::MpReachNlri;
521        use crate::capability::{Afi, Safi};
522
523        let attrs = vec![
524            PathAttribute::Origin(Origin::Igp),
525            PathAttribute::AsPath(AsPath {
526                segments: vec![AsPathSegment::AsSequence(vec![65001])],
527            }),
528            PathAttribute::MpReachNlri(MpReachNlri {
529                afi: Afi::Ipv6,
530                safi: Safi::Unicast,
531                // ff02::1 is multicast
532                next_hop: std::net::IpAddr::V6("ff02::1".parse().unwrap()),
533                announced: vec![],
534                flowspec_announced: vec![],
535            }),
536        ];
537        let err = validate_update_attributes(&attrs, true, false, true).unwrap_err();
538        assert_eq!(err.subcode, update_subcode::INVALID_NEXT_HOP);
539    }
540}