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#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct UpdateError {
14 pub subcode: u8,
16 pub data: Vec<u8>,
18}
19
20const MANDATORY_ATTRS: &[u8] = &[attr_type::ORIGIN, attr_type::AS_PATH];
22
23pub 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 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
72fn 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
87fn check_unrecognized_wellknown(attrs: &[PathAttribute]) -> Result<(), UpdateError> {
89 for attr in attrs {
90 if let PathAttribute::Unknown(raw) = attr {
91 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
103fn 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 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
137fn check_next_hop(addr: std::net::Ipv4Addr) -> Result<(), UpdateError> {
139 let octets = addr.octets();
140
141 if addr.is_unspecified() {
143 return Err(UpdateError {
144 subcode: update_subcode::INVALID_NEXT_HOP,
145 data: octets.to_vec(),
146 });
147 }
148
149 if addr.is_loopback() {
151 return Err(UpdateError {
152 subcode: update_subcode::INVALID_NEXT_HOP,
153 data: octets.to_vec(),
154 });
155 }
156
157 if addr.is_multicast() {
159 return Err(UpdateError {
160 subcode: update_subcode::INVALID_NEXT_HOP,
161 data: octets.to_vec(),
162 });
163 }
164
165 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
176fn 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
213fn is_ipv6_link_local(addr: &std::net::Ipv6Addr) -> bool {
215 (addr.segments()[0] & 0xffc0) == 0xfe80
216}
217
218#[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
230fn 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 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 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, 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 #[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 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 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 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 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 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 #[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 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 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 #[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 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 #[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}