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::capability::Safi;
6use crate::constants::{attr_flags, attr_type};
7use crate::notification::update_subcode;
8
9/// Error produced by UPDATE attribute validation.
10///
11/// Contains the NOTIFICATION subcode and data bytes per RFC 4271 §6.3.
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct UpdateError {
14    /// NOTIFICATION subcode for this validation error.
15    pub subcode: u8,
16    /// Raw bytes for the NOTIFICATION data field.
17    pub data: Vec<u8>,
18}
19
20/// Context-dependent UPDATE validation knobs.
21#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
22pub struct UpdateValidationOptions {
23    /// Permit an IPv6 link-local primary next-hop only for IPv4 unicast
24    /// `MP_REACH_NLRI`. This is intentionally opt-in: ordinary IPv6 next-hop
25    /// validation remains strict, and callers must have already established the
26    /// session scope/interface needed to resolve the link-local address.
27    pub allow_ipv4_link_local_mp_reach_next_hop: bool,
28}
29
30/// Well-known attribute type codes that MUST be present when NLRI is advertised.
31const MANDATORY_ATTRS: &[u8] = &[attr_type::ORIGIN, attr_type::AS_PATH];
32
33/// Validate the semantic correctness of a set of path attributes.
34///
35/// This is separate from decode (which is structural — "can I read these bytes?").
36/// Validation checks whether the attribute set is RFC-compliant for this UPDATE.
37///
38/// `has_nlri` — true if the UPDATE carries announced prefixes (body or MP).
39/// `has_body_nlri` — true if the UPDATE carries IPv4 NLRI in the body fields.
40/// `is_ebgp` — true if the session is external BGP.
41///
42/// # Errors
43///
44/// Returns an `UpdateError` with the appropriate subcode and data.
45pub fn validate_update_attributes(
46    attrs: &[PathAttribute],
47    has_nlri: bool,
48    has_body_nlri: bool,
49    is_ebgp: bool,
50) -> Result<(), UpdateError> {
51    validate_update_attributes_with_options(
52        attrs,
53        has_nlri,
54        has_body_nlri,
55        is_ebgp,
56        UpdateValidationOptions::default(),
57    )
58}
59
60/// Validate UPDATE attributes with context-dependent validation options.
61///
62/// Use [`validate_update_attributes`] unless the caller has session context
63/// that legitimately relaxes one of the default checks.
64///
65/// # Errors
66///
67/// Returns an `UpdateError` with the appropriate subcode and data.
68pub fn validate_update_attributes_with_options(
69    attrs: &[PathAttribute],
70    has_nlri: bool,
71    has_body_nlri: bool,
72    is_ebgp: bool,
73    options: UpdateValidationOptions,
74) -> Result<(), UpdateError> {
75    check_duplicate_types(attrs)?;
76    check_unrecognized_wellknown(attrs)?;
77
78    if has_nlri {
79        check_mandatory_present(attrs, has_body_nlri, is_ebgp)?;
80    }
81
82    for attr in attrs {
83        match attr {
84            PathAttribute::NextHop(addr) => check_next_hop(*addr)?,
85            PathAttribute::AsPath(path) => check_as_path(path)?,
86            // RFC 8955 §6.1: for FlowSpec (SAFI 133), the NEXT_HOP
87            // attribute value is "irrelevant" and is recommended
88            // to be 0 when advertising. The on-wire NH-Len for
89            // FlowSpec is 0 and the decoder fills `mp.next_hop`
90            // with 0.0.0.0; running the standard NEXT_HOP validator
91            // (which rejects 0.0.0.0) on a FlowSpec MP_REACH causes
92            // us to send NOTIFICATION 3/8 and tear the session
93            // against any RFC-compliant peer. Skip validation for
94            // FlowSpec; the rule contents travel in
95            // `flowspec_announced`, not next_hop.
96            PathAttribute::MpReachNlri(mp) if mp.safi != Safi::FlowSpec => {
97                let allow_link_local_primary = options.allow_ipv4_link_local_mp_reach_next_hop
98                    && mp.afi == crate::capability::Afi::Ipv4
99                    && mp.safi == Safi::Unicast;
100                check_mp_reach_next_hop(
101                    mp.next_hop,
102                    mp.link_local_next_hop,
103                    allow_link_local_primary,
104                )?;
105            }
106            _ => {}
107        }
108    }
109
110    Ok(())
111}
112
113/// (3,1) Duplicate attribute type codes.
114fn check_duplicate_types(attrs: &[PathAttribute]) -> Result<(), UpdateError> {
115    let mut seen = HashSet::new();
116    for attr in attrs {
117        let tc = attr.type_code();
118        if !seen.insert(tc) {
119            return Err(UpdateError {
120                subcode: update_subcode::MALFORMED_ATTRIBUTE_LIST,
121                data: vec![],
122            });
123        }
124    }
125    Ok(())
126}
127
128/// (3,2) Unrecognized well-known attribute: Optional=0 and type code unknown.
129fn check_unrecognized_wellknown(attrs: &[PathAttribute]) -> Result<(), UpdateError> {
130    for attr in attrs {
131        if let PathAttribute::Unknown(raw) = attr {
132            // If Optional bit is NOT set, it claims to be well-known
133            if (raw.flags & attr_flags::OPTIONAL) == 0 {
134                return Err(UpdateError {
135                    subcode: update_subcode::UNRECOGNIZED_WELLKNOWN,
136                    data: attr_error_data(raw.flags, raw.type_code, &raw.data),
137                });
138            }
139        }
140    }
141    Ok(())
142}
143
144/// (3,3) Missing mandatory well-known attributes.
145///
146/// `has_body_nlri` — true if the UPDATE carries IPv4 NLRI in the body fields.
147/// When only `MP_REACH_NLRI` is present (no body NLRI), `NEXT_HOP` is carried
148/// inside the MP attribute (RFC 4760 §3) and not required as a separate attribute.
149/// Mixed UPDATEs (body NLRI + `MP_REACH_NLRI`) still require body `NEXT_HOP`.
150fn check_mandatory_present(
151    attrs: &[PathAttribute],
152    has_body_nlri: bool,
153    is_ebgp: bool,
154) -> Result<(), UpdateError> {
155    let present: HashSet<u8> = attrs.iter().map(PathAttribute::type_code).collect();
156
157    for &tc in MANDATORY_ATTRS {
158        if !present.contains(&tc) {
159            return Err(UpdateError {
160                subcode: update_subcode::MISSING_WELLKNOWN,
161                data: vec![tc],
162            });
163        }
164    }
165
166    // NEXT_HOP mandatory for eBGP when body NLRI is present. When only MP_REACH
167    // carries NLRI, the next-hop is inside the MP attribute (RFC 4760 §3).
168    if is_ebgp && has_body_nlri && !present.contains(&attr_type::NEXT_HOP) {
169        return Err(UpdateError {
170            subcode: update_subcode::MISSING_WELLKNOWN,
171            data: vec![attr_type::NEXT_HOP],
172        });
173    }
174
175    Ok(())
176}
177
178/// (3,8) Invalid `NEXT_HOP` address.
179fn check_next_hop(addr: std::net::Ipv4Addr) -> Result<(), UpdateError> {
180    let octets = addr.octets();
181
182    // 0.0.0.0
183    if addr.is_unspecified() {
184        return Err(UpdateError {
185            subcode: update_subcode::INVALID_NEXT_HOP,
186            data: octets.to_vec(),
187        });
188    }
189
190    // 127.0.0.0/8
191    if addr.is_loopback() {
192        return Err(UpdateError {
193            subcode: update_subcode::INVALID_NEXT_HOP,
194            data: octets.to_vec(),
195        });
196    }
197
198    // 224.0.0.0/4 (multicast)
199    if addr.is_multicast() {
200        return Err(UpdateError {
201            subcode: update_subcode::INVALID_NEXT_HOP,
202            data: octets.to_vec(),
203        });
204    }
205
206    // 255.255.255.255
207    if addr.is_broadcast() {
208        return Err(UpdateError {
209            subcode: update_subcode::INVALID_NEXT_HOP,
210            data: octets.to_vec(),
211        });
212    }
213
214    Ok(())
215}
216
217/// Validate `MP_REACH_NLRI` next-hop address(es).
218///
219/// `addr` is the global next-hop (always present in any non-FlowSpec
220/// `MP_REACH`). `link_local` is the optional second-16-byte
221/// component carried only when the on-wire NH-Len is 32 (RFC 4760
222/// §3 / RFC 2545 §3 — the IPv6-with-link-local form). When present,
223/// it MUST be in `fe80::/10`; otherwise the peer is sending a
224/// malformed `MP_REACH` and we reject with subcode 8 rather than
225/// accepting a non-link-local second segment into the receive path
226/// where downstream consumers may treat it as if it were
227/// link-local.
228fn check_mp_reach_next_hop(
229    addr: IpAddr,
230    link_local: Option<std::net::Ipv6Addr>,
231    allow_link_local_primary: bool,
232) -> Result<(), UpdateError> {
233    match addr {
234        IpAddr::V4(v4) => check_next_hop(v4)?,
235        IpAddr::V6(v6) => {
236            let valid = if allow_link_local_primary && is_ipv6_link_local(&v6) {
237                link_local.is_some_and(|ll| ll == v6)
238            } else {
239                is_valid_ipv6_nexthop(&v6)
240            };
241            if !valid {
242                return Err(UpdateError {
243                    subcode: update_subcode::INVALID_NEXT_HOP,
244                    data: v6.octets().to_vec(),
245                });
246            }
247        }
248    }
249    if let Some(ll) = link_local
250        && !is_ipv6_link_local(&ll)
251    {
252        return Err(UpdateError {
253            subcode: update_subcode::INVALID_NEXT_HOP,
254            data: ll.octets().to_vec(),
255        });
256    }
257    Ok(())
258}
259
260/// Check if an IPv6 address is link-local (`fe80::/10`).
261fn is_ipv6_link_local(addr: &std::net::Ipv6Addr) -> bool {
262    (addr.segments()[0] & 0xffc0) == 0xfe80
263}
264
265/// Returns `true` if `addr` is a valid IPv6 next-hop for BGP advertisements.
266///
267/// Rejects unspecified (`::`), loopback (`::1`), multicast (`ff00::/8`),
268/// and link-local (`fe80::/10`) addresses.
269#[must_use]
270pub fn is_valid_ipv6_nexthop(addr: &std::net::Ipv6Addr) -> bool {
271    !addr.is_unspecified()
272        && !addr.is_loopback()
273        && !addr.is_multicast()
274        && !is_ipv6_link_local(addr)
275}
276
277/// (3,11) Malformed `AS_PATH`.
278fn check_as_path(path: &AsPath) -> Result<(), UpdateError> {
279    for segment in &path.segments {
280        let asns = match segment {
281            AsPathSegment::AsSet(asns) | AsPathSegment::AsSequence(asns) => asns,
282        };
283        if asns.is_empty() {
284            return Err(UpdateError {
285                subcode: update_subcode::MALFORMED_AS_PATH,
286                data: vec![],
287            });
288        }
289    }
290    Ok(())
291}
292
293#[cfg(test)]
294mod tests {
295    use std::net::Ipv4Addr;
296
297    use bytes::Bytes;
298
299    use super::*;
300    use crate::attribute::{Origin, RawAttribute};
301
302    fn basic_attrs(next_hop: Ipv4Addr) -> Vec<PathAttribute> {
303        vec![
304            PathAttribute::Origin(Origin::Igp),
305            PathAttribute::AsPath(AsPath {
306                segments: vec![AsPathSegment::AsSequence(vec![65001])],
307            }),
308            PathAttribute::NextHop(next_hop),
309        ]
310    }
311
312    #[test]
313    fn valid_ebgp_update() {
314        let attrs = basic_attrs(Ipv4Addr::new(10, 0, 0, 1));
315        assert!(validate_update_attributes(&attrs, true, true, true).is_ok());
316    }
317
318    #[test]
319    fn valid_ibgp_update_no_next_hop() {
320        // iBGP doesn't require NEXT_HOP (it's optional based on the peer)
321        let attrs = vec![
322            PathAttribute::Origin(Origin::Igp),
323            PathAttribute::AsPath(AsPath {
324                segments: vec![AsPathSegment::AsSequence(vec![65001])],
325            }),
326        ];
327        assert!(validate_update_attributes(&attrs, true, true, false).is_ok());
328    }
329
330    #[test]
331    fn withdrawal_only_no_attrs_ok() {
332        // No NLRI → no mandatory attributes required
333        assert!(validate_update_attributes(&[], false, false, true).is_ok());
334    }
335
336    #[test]
337    fn reject_duplicate_type() {
338        let attrs = vec![
339            PathAttribute::Origin(Origin::Igp),
340            PathAttribute::Origin(Origin::Egp),
341        ];
342        let err = validate_update_attributes(&attrs, false, false, true).unwrap_err();
343        assert_eq!(err.subcode, update_subcode::MALFORMED_ATTRIBUTE_LIST);
344    }
345
346    #[test]
347    fn reject_missing_origin() {
348        let attrs = vec![
349            PathAttribute::AsPath(AsPath {
350                segments: vec![AsPathSegment::AsSequence(vec![65001])],
351            }),
352            PathAttribute::NextHop(Ipv4Addr::new(10, 0, 0, 1)),
353        ];
354        let err = validate_update_attributes(&attrs, true, true, true).unwrap_err();
355        assert_eq!(err.subcode, update_subcode::MISSING_WELLKNOWN);
356    }
357
358    #[test]
359    fn reject_missing_as_path() {
360        let attrs = vec![
361            PathAttribute::Origin(Origin::Igp),
362            PathAttribute::NextHop(Ipv4Addr::new(10, 0, 0, 1)),
363        ];
364        let err = validate_update_attributes(&attrs, true, true, true).unwrap_err();
365        assert_eq!(err.subcode, update_subcode::MISSING_WELLKNOWN);
366    }
367
368    #[test]
369    fn reject_missing_next_hop_ebgp() {
370        let attrs = vec![
371            PathAttribute::Origin(Origin::Igp),
372            PathAttribute::AsPath(AsPath {
373                segments: vec![AsPathSegment::AsSequence(vec![65001])],
374            }),
375        ];
376        let err = validate_update_attributes(&attrs, true, true, true).unwrap_err();
377        assert_eq!(err.subcode, update_subcode::MISSING_WELLKNOWN);
378        assert_eq!(err.data, vec![attr_type::NEXT_HOP]);
379    }
380
381    #[test]
382    fn reject_next_hop_unspecified() {
383        let attrs = basic_attrs(Ipv4Addr::UNSPECIFIED);
384        let err = validate_update_attributes(&attrs, true, true, true).unwrap_err();
385        assert_eq!(err.subcode, update_subcode::INVALID_NEXT_HOP);
386    }
387
388    #[test]
389    fn reject_next_hop_loopback() {
390        let attrs = basic_attrs(Ipv4Addr::LOCALHOST);
391        let err = validate_update_attributes(&attrs, true, true, true).unwrap_err();
392        assert_eq!(err.subcode, update_subcode::INVALID_NEXT_HOP);
393    }
394
395    #[test]
396    fn reject_next_hop_multicast() {
397        let attrs = basic_attrs(Ipv4Addr::new(224, 0, 0, 1));
398        let err = validate_update_attributes(&attrs, true, true, true).unwrap_err();
399        assert_eq!(err.subcode, update_subcode::INVALID_NEXT_HOP);
400    }
401
402    #[test]
403    fn reject_next_hop_broadcast() {
404        let attrs = basic_attrs(Ipv4Addr::BROADCAST);
405        let err = validate_update_attributes(&attrs, true, true, true).unwrap_err();
406        assert_eq!(err.subcode, update_subcode::INVALID_NEXT_HOP);
407    }
408
409    #[test]
410    fn reject_empty_as_path_segment() {
411        let attrs = vec![
412            PathAttribute::Origin(Origin::Igp),
413            PathAttribute::AsPath(AsPath {
414                segments: vec![AsPathSegment::AsSequence(vec![])],
415            }),
416            PathAttribute::NextHop(Ipv4Addr::new(10, 0, 0, 1)),
417        ];
418        let err = validate_update_attributes(&attrs, true, true, true).unwrap_err();
419        assert_eq!(err.subcode, update_subcode::MALFORMED_AS_PATH);
420    }
421
422    #[test]
423    fn reject_unrecognized_wellknown() {
424        let attrs = vec![PathAttribute::Unknown(RawAttribute {
425            flags: attr_flags::TRANSITIVE, // Optional=0 → claims well-known
426            type_code: 99,
427            data: Bytes::from_static(&[1, 2, 3]),
428        })];
429        let err = validate_update_attributes(&attrs, false, false, true).unwrap_err();
430        assert_eq!(err.subcode, update_subcode::UNRECOGNIZED_WELLKNOWN);
431    }
432
433    #[test]
434    fn optional_unknown_attribute_ok() {
435        let attrs = vec![PathAttribute::Unknown(RawAttribute {
436            flags: attr_flags::OPTIONAL | attr_flags::TRANSITIVE,
437            type_code: 99,
438            data: Bytes::from_static(&[1, 2, 3]),
439        })];
440        assert!(validate_update_attributes(&attrs, false, false, true).is_ok());
441    }
442
443    // --- MP_REACH_NLRI validation tests ---
444
445    #[test]
446    fn mp_reach_nlri_no_body_next_hop_required_for_ebgp() {
447        use crate::attribute::MpReachNlri;
448        use crate::capability::{Afi, Safi};
449        use crate::nlri::{Ipv6Prefix, NlriEntry, Prefix};
450
451        // eBGP UPDATE with MP_REACH_NLRI only (no body NLRI): NEXT_HOP not required
452        let attrs = vec![
453            PathAttribute::Origin(Origin::Igp),
454            PathAttribute::AsPath(AsPath {
455                segments: vec![AsPathSegment::AsSequence(vec![65001])],
456            }),
457            PathAttribute::MpReachNlri(MpReachNlri {
458                afi: Afi::Ipv6,
459                safi: Safi::Unicast,
460                next_hop: std::net::IpAddr::V6("2001:db8::1".parse().unwrap()),
461                link_local_next_hop: None,
462                announced: vec![NlriEntry {
463                    path_id: 0,
464                    prefix: Prefix::V6(Ipv6Prefix::new("2001:db8::".parse().unwrap(), 32)),
465                }],
466                flowspec_announced: vec![],
467                evpn_announced: vec![],
468            }),
469        ];
470        // has_nlri=true, has_body_nlri=false (only MP NLRI), is_ebgp=true
471        assert!(validate_update_attributes(&attrs, true, false, true).is_ok());
472    }
473
474    #[test]
475    fn mixed_update_requires_body_next_hop_for_ebgp() {
476        use crate::attribute::MpReachNlri;
477        use crate::capability::{Afi, Safi};
478        use crate::nlri::{Ipv6Prefix, NlriEntry, Prefix};
479
480        // eBGP UPDATE with BOTH body NLRI and MP_REACH_NLRI but no NEXT_HOP attr
481        let attrs = vec![
482            PathAttribute::Origin(Origin::Igp),
483            PathAttribute::AsPath(AsPath {
484                segments: vec![AsPathSegment::AsSequence(vec![65001])],
485            }),
486            PathAttribute::MpReachNlri(MpReachNlri {
487                afi: Afi::Ipv6,
488                safi: Safi::Unicast,
489                next_hop: std::net::IpAddr::V6("2001:db8::1".parse().unwrap()),
490                link_local_next_hop: None,
491                announced: vec![NlriEntry {
492                    path_id: 0,
493                    prefix: Prefix::V6(Ipv6Prefix::new("2001:db8::".parse().unwrap(), 32)),
494                }],
495                flowspec_announced: vec![],
496                evpn_announced: vec![],
497            }),
498        ];
499        // has_nlri=true, has_body_nlri=true (body IPv4 NLRI present), is_ebgp=true
500        // → should require NEXT_HOP for the body NLRI
501        let err = validate_update_attributes(&attrs, true, true, true).unwrap_err();
502        assert_eq!(err.subcode, update_subcode::MISSING_WELLKNOWN);
503        assert_eq!(err.data, vec![attr_type::NEXT_HOP]);
504    }
505
506    #[test]
507    fn mp_reach_nlri_reject_unspecified_v6_next_hop() {
508        use crate::attribute::MpReachNlri;
509        use crate::capability::{Afi, Safi};
510
511        let attrs = vec![
512            PathAttribute::Origin(Origin::Igp),
513            PathAttribute::AsPath(AsPath {
514                segments: vec![AsPathSegment::AsSequence(vec![65001])],
515            }),
516            PathAttribute::MpReachNlri(MpReachNlri {
517                afi: Afi::Ipv6,
518                safi: Safi::Unicast,
519                next_hop: std::net::IpAddr::V6(std::net::Ipv6Addr::UNSPECIFIED),
520                link_local_next_hop: None,
521                announced: vec![],
522                flowspec_announced: vec![],
523                evpn_announced: vec![],
524            }),
525        ];
526        let err = validate_update_attributes(&attrs, true, false, true).unwrap_err();
527        assert_eq!(err.subcode, update_subcode::INVALID_NEXT_HOP);
528    }
529
530    #[test]
531    fn mp_reach_nlri_reject_link_local_v6_next_hop() {
532        use crate::attribute::MpReachNlri;
533        use crate::capability::{Afi, Safi};
534
535        let attrs = vec![
536            PathAttribute::Origin(Origin::Igp),
537            PathAttribute::AsPath(AsPath {
538                segments: vec![AsPathSegment::AsSequence(vec![65001])],
539            }),
540            PathAttribute::MpReachNlri(MpReachNlri {
541                afi: Afi::Ipv6,
542                safi: Safi::Unicast,
543                next_hop: std::net::IpAddr::V6("fe80::1".parse().unwrap()),
544                link_local_next_hop: None,
545                announced: vec![],
546                flowspec_announced: vec![],
547                evpn_announced: vec![],
548            }),
549        ];
550        let err = validate_update_attributes(&attrs, true, false, true).unwrap_err();
551        assert_eq!(err.subcode, update_subcode::INVALID_NEXT_HOP);
552    }
553
554    #[test]
555    fn mp_reach_nlri_allows_link_local_primary_only_for_opted_in_ipv4() {
556        use crate::attribute::MpReachNlri;
557        use crate::capability::{Afi, Safi};
558
559        let attrs = vec![
560            PathAttribute::Origin(Origin::Igp),
561            PathAttribute::AsPath(AsPath {
562                segments: vec![AsPathSegment::AsSequence(vec![65001])],
563            }),
564            PathAttribute::MpReachNlri(MpReachNlri {
565                afi: Afi::Ipv4,
566                safi: Safi::Unicast,
567                next_hop: std::net::IpAddr::V6("fe80::1".parse().unwrap()),
568                link_local_next_hop: Some("fe80::1".parse().unwrap()),
569                announced: vec![],
570                flowspec_announced: vec![],
571                evpn_announced: vec![],
572            }),
573        ];
574
575        assert!(validate_update_attributes(&attrs, true, false, true).is_err());
576        assert!(
577            validate_update_attributes_with_options(
578                &attrs,
579                true,
580                false,
581                true,
582                UpdateValidationOptions {
583                    allow_ipv4_link_local_mp_reach_next_hop: true,
584                },
585            )
586            .is_ok()
587        );
588
589        let mut attrs_without_companion = attrs.clone();
590        if let PathAttribute::MpReachNlri(mp) = &mut attrs_without_companion[2] {
591            mp.link_local_next_hop = None;
592        }
593        assert!(
594            validate_update_attributes_with_options(
595                &attrs_without_companion,
596                true,
597                false,
598                true,
599                UpdateValidationOptions {
600                    allow_ipv4_link_local_mp_reach_next_hop: true,
601                },
602            )
603            .is_err()
604        );
605    }
606
607    #[test]
608    fn mp_reach_nlri_reject_loopback_v6_next_hop() {
609        use crate::attribute::MpReachNlri;
610        use crate::capability::{Afi, Safi};
611
612        let attrs = vec![
613            PathAttribute::Origin(Origin::Igp),
614            PathAttribute::AsPath(AsPath {
615                segments: vec![AsPathSegment::AsSequence(vec![65001])],
616            }),
617            PathAttribute::MpReachNlri(MpReachNlri {
618                afi: Afi::Ipv6,
619                safi: Safi::Unicast,
620                next_hop: std::net::IpAddr::V6(std::net::Ipv6Addr::LOCALHOST),
621                link_local_next_hop: None,
622                announced: vec![],
623                flowspec_announced: vec![],
624                evpn_announced: vec![],
625            }),
626        ];
627        let err = validate_update_attributes(&attrs, true, false, true).unwrap_err();
628        assert_eq!(err.subcode, update_subcode::INVALID_NEXT_HOP);
629    }
630
631    #[test]
632    fn is_valid_ipv6_nexthop_accepts_global() {
633        assert!(super::is_valid_ipv6_nexthop(
634            &"2001:db8::1".parse().unwrap()
635        ));
636    }
637
638    #[test]
639    fn is_valid_ipv6_nexthop_rejects_unspecified() {
640        assert!(!super::is_valid_ipv6_nexthop(
641            &std::net::Ipv6Addr::UNSPECIFIED
642        ));
643    }
644
645    #[test]
646    fn is_valid_ipv6_nexthop_rejects_loopback() {
647        assert!(!super::is_valid_ipv6_nexthop(
648            &std::net::Ipv6Addr::LOCALHOST
649        ));
650    }
651
652    #[test]
653    fn is_valid_ipv6_nexthop_rejects_link_local() {
654        assert!(!super::is_valid_ipv6_nexthop(&"fe80::1".parse().unwrap()));
655    }
656
657    #[test]
658    fn is_valid_ipv6_nexthop_rejects_multicast() {
659        assert!(!super::is_valid_ipv6_nexthop(&"ff02::1".parse().unwrap()));
660    }
661
662    #[test]
663    fn mp_reach_nlri_reject_multicast_v6_next_hop() {
664        use crate::attribute::MpReachNlri;
665        use crate::capability::{Afi, Safi};
666
667        let attrs = vec![
668            PathAttribute::Origin(Origin::Igp),
669            PathAttribute::AsPath(AsPath {
670                segments: vec![AsPathSegment::AsSequence(vec![65001])],
671            }),
672            PathAttribute::MpReachNlri(MpReachNlri {
673                afi: Afi::Ipv6,
674                safi: Safi::Unicast,
675                // ff02::1 is multicast
676                next_hop: std::net::IpAddr::V6("ff02::1".parse().unwrap()),
677                link_local_next_hop: None,
678                announced: vec![],
679                flowspec_announced: vec![],
680                evpn_announced: vec![],
681            }),
682        ];
683        let err = validate_update_attributes(&attrs, true, false, true).unwrap_err();
684        assert_eq!(err.subcode, update_subcode::INVALID_NEXT_HOP);
685    }
686
687    /// Regression: an `MP_REACH` for `FlowSpec` (SAFI 133) with
688    /// the recommended-by-RFC-8955-§6.1 next-hop value of 0.0.0.0
689    /// must NOT trip `NEXT_HOP` validation. Before the `FlowSpec`
690    /// guard, validate ran the standard `check_next_hop` against
691    /// every `MP_REACH` next-hop and rejected 0.0.0.0 with subcode
692    /// 8 (Invalid `NEXT_HOP`), causing rustbgpd to send
693    /// `NOTIFICATION` 3/8 and tear the session against any
694    /// RFC-compliant `FlowSpec` peer (FRR, `GoBGP`). M22 was
695    /// masking this with long display-path waits that hid the
696    /// resulting flap-and-recover cycle.
697    #[test]
698    fn mp_reach_flowspec_unspecified_next_hop_is_valid() {
699        use crate::attribute::MpReachNlri;
700        use crate::capability::{Afi, Safi};
701
702        let attrs = vec![
703            PathAttribute::Origin(Origin::Igp),
704            PathAttribute::AsPath(AsPath {
705                segments: vec![AsPathSegment::AsSequence(vec![65001])],
706            }),
707            PathAttribute::MpReachNlri(MpReachNlri {
708                afi: Afi::Ipv4,
709                safi: Safi::FlowSpec,
710                // RFC 8955 §6.1 recommends 0.0.0.0 for FlowSpec
711                // advertisements; on the wire NH-Len is 0 and the
712                // decoder defaults this to 0.0.0.0.
713                next_hop: std::net::IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED),
714                link_local_next_hop: None,
715                announced: vec![],
716                flowspec_announced: vec![],
717                evpn_announced: vec![],
718            }),
719        ];
720        // Empty announced + empty body — FlowSpec EoR-equivalent
721        // shape FRR sends post-handshake. Must pass validation.
722        assert!(
723            validate_update_attributes(&attrs, false, false, true).is_ok(),
724            "FlowSpec MP_REACH with 0.0.0.0 next-hop must pass — RFC 8955 §6.1 \
725             specifies the next-hop value is irrelevant for FlowSpec and \
726             recommends 0. The pre-fix path tore sessions against every \
727             RFC-compliant FlowSpec peer."
728        );
729    }
730
731    /// Audit follow-up: an IPv6 `MP_REACH` with NH-Len=32 (the
732    /// global-and-link-local form, RFC 4760 §3 / RFC 2545 §3)
733    /// where the second 16 bytes are NOT in `fe80::/10` is a
734    /// malformed advertisement. The pre-audit validator only
735    /// inspected `mp.next_hop` and silently accepted any value at
736    /// `mp.link_local_next_hop`, letting a non-link-local second
737    /// segment land in the receive path where downstream consumers
738    /// may treat it as if it were link-local. Reject with subcode
739    /// 8 (Invalid `NEXT_HOP`).
740    #[test]
741    fn mp_reach_ipv6_invalid_link_local_segment_rejected() {
742        use crate::attribute::MpReachNlri;
743        use crate::capability::{Afi, Safi};
744
745        let attrs = vec![
746            PathAttribute::Origin(Origin::Igp),
747            PathAttribute::AsPath(AsPath {
748                segments: vec![AsPathSegment::AsSequence(vec![65001])],
749            }),
750            PathAttribute::MpReachNlri(MpReachNlri {
751                afi: Afi::Ipv6,
752                safi: Safi::Unicast,
753                next_hop: std::net::IpAddr::V6("2001:db8::1".parse().unwrap()),
754                // Second 16 bytes purport to be link-local but are a
755                // global address. Should be rejected.
756                link_local_next_hop: Some("2001:db8::2".parse().unwrap()),
757                announced: vec![],
758                flowspec_announced: vec![],
759                evpn_announced: vec![],
760            }),
761        ];
762        let err = validate_update_attributes(&attrs, false, false, true).unwrap_err();
763        assert_eq!(err.subcode, update_subcode::INVALID_NEXT_HOP);
764    }
765
766    /// Audit follow-up complement to the above: a properly-formed
767    /// 32-byte next-hop (global + actual link-local) must pass.
768    /// Pins that the new validation didn't over-reject the legal form.
769    #[test]
770    fn mp_reach_ipv6_global_plus_link_local_accepted() {
771        use crate::attribute::MpReachNlri;
772        use crate::capability::{Afi, Safi};
773
774        let attrs = vec![
775            PathAttribute::Origin(Origin::Igp),
776            PathAttribute::AsPath(AsPath {
777                segments: vec![AsPathSegment::AsSequence(vec![65001])],
778            }),
779            PathAttribute::MpReachNlri(MpReachNlri {
780                afi: Afi::Ipv6,
781                safi: Safi::Unicast,
782                next_hop: std::net::IpAddr::V6("2001:db8::1".parse().unwrap()),
783                link_local_next_hop: Some("fe80::1".parse().unwrap()),
784                announced: vec![],
785                flowspec_announced: vec![],
786                evpn_announced: vec![],
787            }),
788        ];
789        assert!(validate_update_attributes(&attrs, false, false, true).is_ok());
790    }
791}