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