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
20#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
22pub struct UpdateValidationOptions {
23 pub allow_ipv4_link_local_mp_reach_next_hop: bool,
28}
29
30const MANDATORY_ATTRS: &[u8] = &[attr_type::ORIGIN, attr_type::AS_PATH];
32
33pub 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
60pub 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 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
113fn 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
128fn check_unrecognized_wellknown(attrs: &[PathAttribute]) -> Result<(), UpdateError> {
130 for attr in attrs {
131 if let PathAttribute::Unknown(raw) = attr {
132 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
144fn 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 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
178fn check_next_hop(addr: std::net::Ipv4Addr) -> Result<(), UpdateError> {
180 let octets = addr.octets();
181
182 if addr.is_unspecified() {
184 return Err(UpdateError {
185 subcode: update_subcode::INVALID_NEXT_HOP,
186 data: octets.to_vec(),
187 });
188 }
189
190 if addr.is_loopback() {
192 return Err(UpdateError {
193 subcode: update_subcode::INVALID_NEXT_HOP,
194 data: octets.to_vec(),
195 });
196 }
197
198 if addr.is_multicast() {
200 return Err(UpdateError {
201 subcode: update_subcode::INVALID_NEXT_HOP,
202 data: octets.to_vec(),
203 });
204 }
205
206 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
217fn 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
260fn is_ipv6_link_local(addr: &std::net::Ipv6Addr) -> bool {
262 (addr.segments()[0] & 0xffc0) == 0xfe80
263}
264
265#[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
277fn 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 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 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, 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 #[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 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 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 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 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 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 #[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 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 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 #[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 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 #[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}