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                evpn_announced: vec![],
386            }),
387        ];
388        // has_nlri=true, has_body_nlri=false (only MP NLRI), is_ebgp=true
389        assert!(validate_update_attributes(&attrs, true, false, true).is_ok());
390    }
391
392    #[test]
393    fn mixed_update_requires_body_next_hop_for_ebgp() {
394        use crate::attribute::MpReachNlri;
395        use crate::capability::{Afi, Safi};
396        use crate::nlri::{Ipv6Prefix, NlriEntry, Prefix};
397
398        // eBGP UPDATE with BOTH body NLRI and MP_REACH_NLRI but no NEXT_HOP attr
399        let attrs = vec![
400            PathAttribute::Origin(Origin::Igp),
401            PathAttribute::AsPath(AsPath {
402                segments: vec![AsPathSegment::AsSequence(vec![65001])],
403            }),
404            PathAttribute::MpReachNlri(MpReachNlri {
405                afi: Afi::Ipv6,
406                safi: Safi::Unicast,
407                next_hop: std::net::IpAddr::V6("2001:db8::1".parse().unwrap()),
408                announced: vec![NlriEntry {
409                    path_id: 0,
410                    prefix: Prefix::V6(Ipv6Prefix::new("2001:db8::".parse().unwrap(), 32)),
411                }],
412                flowspec_announced: vec![],
413                evpn_announced: vec![],
414            }),
415        ];
416        // has_nlri=true, has_body_nlri=true (body IPv4 NLRI present), is_ebgp=true
417        // → should require NEXT_HOP for the body NLRI
418        let err = validate_update_attributes(&attrs, true, true, true).unwrap_err();
419        assert_eq!(err.subcode, update_subcode::MISSING_WELLKNOWN);
420        assert_eq!(err.data, vec![attr_type::NEXT_HOP]);
421    }
422
423    #[test]
424    fn mp_reach_nlri_reject_unspecified_v6_next_hop() {
425        use crate::attribute::MpReachNlri;
426        use crate::capability::{Afi, Safi};
427
428        let attrs = vec![
429            PathAttribute::Origin(Origin::Igp),
430            PathAttribute::AsPath(AsPath {
431                segments: vec![AsPathSegment::AsSequence(vec![65001])],
432            }),
433            PathAttribute::MpReachNlri(MpReachNlri {
434                afi: Afi::Ipv6,
435                safi: Safi::Unicast,
436                next_hop: std::net::IpAddr::V6(std::net::Ipv6Addr::UNSPECIFIED),
437                announced: vec![],
438                flowspec_announced: vec![],
439                evpn_announced: vec![],
440            }),
441        ];
442        let err = validate_update_attributes(&attrs, true, false, true).unwrap_err();
443        assert_eq!(err.subcode, update_subcode::INVALID_NEXT_HOP);
444    }
445
446    #[test]
447    fn mp_reach_nlri_reject_link_local_v6_next_hop() {
448        use crate::attribute::MpReachNlri;
449        use crate::capability::{Afi, Safi};
450
451        let attrs = vec![
452            PathAttribute::Origin(Origin::Igp),
453            PathAttribute::AsPath(AsPath {
454                segments: vec![AsPathSegment::AsSequence(vec![65001])],
455            }),
456            PathAttribute::MpReachNlri(MpReachNlri {
457                afi: Afi::Ipv6,
458                safi: Safi::Unicast,
459                next_hop: std::net::IpAddr::V6("fe80::1".parse().unwrap()),
460                announced: vec![],
461                flowspec_announced: vec![],
462                evpn_announced: vec![],
463            }),
464        ];
465        let err = validate_update_attributes(&attrs, true, false, true).unwrap_err();
466        assert_eq!(err.subcode, update_subcode::INVALID_NEXT_HOP);
467    }
468
469    #[test]
470    fn mp_reach_nlri_reject_loopback_v6_next_hop() {
471        use crate::attribute::MpReachNlri;
472        use crate::capability::{Afi, Safi};
473
474        let attrs = vec![
475            PathAttribute::Origin(Origin::Igp),
476            PathAttribute::AsPath(AsPath {
477                segments: vec![AsPathSegment::AsSequence(vec![65001])],
478            }),
479            PathAttribute::MpReachNlri(MpReachNlri {
480                afi: Afi::Ipv6,
481                safi: Safi::Unicast,
482                next_hop: std::net::IpAddr::V6(std::net::Ipv6Addr::LOCALHOST),
483                announced: vec![],
484                flowspec_announced: vec![],
485                evpn_announced: vec![],
486            }),
487        ];
488        let err = validate_update_attributes(&attrs, true, false, true).unwrap_err();
489        assert_eq!(err.subcode, update_subcode::INVALID_NEXT_HOP);
490    }
491
492    #[test]
493    fn is_valid_ipv6_nexthop_accepts_global() {
494        assert!(super::is_valid_ipv6_nexthop(
495            &"2001:db8::1".parse().unwrap()
496        ));
497    }
498
499    #[test]
500    fn is_valid_ipv6_nexthop_rejects_unspecified() {
501        assert!(!super::is_valid_ipv6_nexthop(
502            &std::net::Ipv6Addr::UNSPECIFIED
503        ));
504    }
505
506    #[test]
507    fn is_valid_ipv6_nexthop_rejects_loopback() {
508        assert!(!super::is_valid_ipv6_nexthop(
509            &std::net::Ipv6Addr::LOCALHOST
510        ));
511    }
512
513    #[test]
514    fn is_valid_ipv6_nexthop_rejects_link_local() {
515        assert!(!super::is_valid_ipv6_nexthop(&"fe80::1".parse().unwrap()));
516    }
517
518    #[test]
519    fn is_valid_ipv6_nexthop_rejects_multicast() {
520        assert!(!super::is_valid_ipv6_nexthop(&"ff02::1".parse().unwrap()));
521    }
522
523    #[test]
524    fn mp_reach_nlri_reject_multicast_v6_next_hop() {
525        use crate::attribute::MpReachNlri;
526        use crate::capability::{Afi, Safi};
527
528        let attrs = vec![
529            PathAttribute::Origin(Origin::Igp),
530            PathAttribute::AsPath(AsPath {
531                segments: vec![AsPathSegment::AsSequence(vec![65001])],
532            }),
533            PathAttribute::MpReachNlri(MpReachNlri {
534                afi: Afi::Ipv6,
535                safi: Safi::Unicast,
536                // ff02::1 is multicast
537                next_hop: std::net::IpAddr::V6("ff02::1".parse().unwrap()),
538                announced: vec![],
539                flowspec_announced: vec![],
540                evpn_announced: vec![],
541            }),
542        ];
543        let err = validate_update_attributes(&attrs, true, false, true).unwrap_err();
544        assert_eq!(err.subcode, update_subcode::INVALID_NEXT_HOP);
545    }
546}