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