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)?;
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(addr: IpAddr) -> Result<(), UpdateError> {
178 match addr {
179 IpAddr::V4(v4) => check_next_hop(v4)?,
180 IpAddr::V6(v6) => {
181 if !is_valid_ipv6_nexthop(&v6) {
182 return Err(UpdateError {
183 subcode: update_subcode::INVALID_NEXT_HOP,
184 data: v6.octets().to_vec(),
185 });
186 }
187 }
188 }
189 Ok(())
190}
191
192fn is_ipv6_link_local(addr: &std::net::Ipv6Addr) -> bool {
194 (addr.segments()[0] & 0xffc0) == 0xfe80
195}
196
197#[must_use]
202pub fn is_valid_ipv6_nexthop(addr: &std::net::Ipv6Addr) -> bool {
203 !addr.is_unspecified()
204 && !addr.is_loopback()
205 && !addr.is_multicast()
206 && !is_ipv6_link_local(addr)
207}
208
209fn check_as_path(path: &AsPath) -> Result<(), UpdateError> {
211 for segment in &path.segments {
212 let asns = match segment {
213 AsPathSegment::AsSet(asns) | AsPathSegment::AsSequence(asns) => asns,
214 };
215 if asns.is_empty() {
216 return Err(UpdateError {
217 subcode: update_subcode::MALFORMED_AS_PATH,
218 data: vec![],
219 });
220 }
221 }
222 Ok(())
223}
224
225#[cfg(test)]
226mod tests {
227 use std::net::Ipv4Addr;
228
229 use bytes::Bytes;
230
231 use super::*;
232 use crate::attribute::{Origin, RawAttribute};
233
234 fn basic_attrs(next_hop: Ipv4Addr) -> Vec<PathAttribute> {
235 vec![
236 PathAttribute::Origin(Origin::Igp),
237 PathAttribute::AsPath(AsPath {
238 segments: vec![AsPathSegment::AsSequence(vec![65001])],
239 }),
240 PathAttribute::NextHop(next_hop),
241 ]
242 }
243
244 #[test]
245 fn valid_ebgp_update() {
246 let attrs = basic_attrs(Ipv4Addr::new(10, 0, 0, 1));
247 assert!(validate_update_attributes(&attrs, true, true, true).is_ok());
248 }
249
250 #[test]
251 fn valid_ibgp_update_no_next_hop() {
252 let attrs = vec![
254 PathAttribute::Origin(Origin::Igp),
255 PathAttribute::AsPath(AsPath {
256 segments: vec![AsPathSegment::AsSequence(vec![65001])],
257 }),
258 ];
259 assert!(validate_update_attributes(&attrs, true, true, false).is_ok());
260 }
261
262 #[test]
263 fn withdrawal_only_no_attrs_ok() {
264 assert!(validate_update_attributes(&[], false, false, true).is_ok());
266 }
267
268 #[test]
269 fn reject_duplicate_type() {
270 let attrs = vec![
271 PathAttribute::Origin(Origin::Igp),
272 PathAttribute::Origin(Origin::Egp),
273 ];
274 let err = validate_update_attributes(&attrs, false, false, true).unwrap_err();
275 assert_eq!(err.subcode, update_subcode::MALFORMED_ATTRIBUTE_LIST);
276 }
277
278 #[test]
279 fn reject_missing_origin() {
280 let attrs = vec![
281 PathAttribute::AsPath(AsPath {
282 segments: vec![AsPathSegment::AsSequence(vec![65001])],
283 }),
284 PathAttribute::NextHop(Ipv4Addr::new(10, 0, 0, 1)),
285 ];
286 let err = validate_update_attributes(&attrs, true, true, true).unwrap_err();
287 assert_eq!(err.subcode, update_subcode::MISSING_WELLKNOWN);
288 }
289
290 #[test]
291 fn reject_missing_as_path() {
292 let attrs = vec![
293 PathAttribute::Origin(Origin::Igp),
294 PathAttribute::NextHop(Ipv4Addr::new(10, 0, 0, 1)),
295 ];
296 let err = validate_update_attributes(&attrs, true, true, true).unwrap_err();
297 assert_eq!(err.subcode, update_subcode::MISSING_WELLKNOWN);
298 }
299
300 #[test]
301 fn reject_missing_next_hop_ebgp() {
302 let attrs = vec![
303 PathAttribute::Origin(Origin::Igp),
304 PathAttribute::AsPath(AsPath {
305 segments: vec![AsPathSegment::AsSequence(vec![65001])],
306 }),
307 ];
308 let err = validate_update_attributes(&attrs, true, true, true).unwrap_err();
309 assert_eq!(err.subcode, update_subcode::MISSING_WELLKNOWN);
310 assert_eq!(err.data, vec![attr_type::NEXT_HOP]);
311 }
312
313 #[test]
314 fn reject_next_hop_unspecified() {
315 let attrs = basic_attrs(Ipv4Addr::UNSPECIFIED);
316 let err = validate_update_attributes(&attrs, true, true, true).unwrap_err();
317 assert_eq!(err.subcode, update_subcode::INVALID_NEXT_HOP);
318 }
319
320 #[test]
321 fn reject_next_hop_loopback() {
322 let attrs = basic_attrs(Ipv4Addr::LOCALHOST);
323 let err = validate_update_attributes(&attrs, true, true, true).unwrap_err();
324 assert_eq!(err.subcode, update_subcode::INVALID_NEXT_HOP);
325 }
326
327 #[test]
328 fn reject_next_hop_multicast() {
329 let attrs = basic_attrs(Ipv4Addr::new(224, 0, 0, 1));
330 let err = validate_update_attributes(&attrs, true, true, true).unwrap_err();
331 assert_eq!(err.subcode, update_subcode::INVALID_NEXT_HOP);
332 }
333
334 #[test]
335 fn reject_next_hop_broadcast() {
336 let attrs = basic_attrs(Ipv4Addr::BROADCAST);
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_empty_as_path_segment() {
343 let attrs = vec![
344 PathAttribute::Origin(Origin::Igp),
345 PathAttribute::AsPath(AsPath {
346 segments: vec![AsPathSegment::AsSequence(vec![])],
347 }),
348 PathAttribute::NextHop(Ipv4Addr::new(10, 0, 0, 1)),
349 ];
350 let err = validate_update_attributes(&attrs, true, true, true).unwrap_err();
351 assert_eq!(err.subcode, update_subcode::MALFORMED_AS_PATH);
352 }
353
354 #[test]
355 fn reject_unrecognized_wellknown() {
356 let attrs = vec![PathAttribute::Unknown(RawAttribute {
357 flags: attr_flags::TRANSITIVE, type_code: 99,
359 data: Bytes::from_static(&[1, 2, 3]),
360 })];
361 let err = validate_update_attributes(&attrs, false, false, true).unwrap_err();
362 assert_eq!(err.subcode, update_subcode::UNRECOGNIZED_WELLKNOWN);
363 }
364
365 #[test]
366 fn optional_unknown_attribute_ok() {
367 let attrs = vec![PathAttribute::Unknown(RawAttribute {
368 flags: attr_flags::OPTIONAL | attr_flags::TRANSITIVE,
369 type_code: 99,
370 data: Bytes::from_static(&[1, 2, 3]),
371 })];
372 assert!(validate_update_attributes(&attrs, false, false, true).is_ok());
373 }
374
375 #[test]
378 fn mp_reach_nlri_no_body_next_hop_required_for_ebgp() {
379 use crate::attribute::MpReachNlri;
380 use crate::capability::{Afi, Safi};
381 use crate::nlri::{Ipv6Prefix, NlriEntry, Prefix};
382
383 let attrs = vec![
385 PathAttribute::Origin(Origin::Igp),
386 PathAttribute::AsPath(AsPath {
387 segments: vec![AsPathSegment::AsSequence(vec![65001])],
388 }),
389 PathAttribute::MpReachNlri(MpReachNlri {
390 afi: Afi::Ipv6,
391 safi: Safi::Unicast,
392 next_hop: std::net::IpAddr::V6("2001:db8::1".parse().unwrap()),
393 link_local_next_hop: None,
394 announced: vec![NlriEntry {
395 path_id: 0,
396 prefix: Prefix::V6(Ipv6Prefix::new("2001:db8::".parse().unwrap(), 32)),
397 }],
398 flowspec_announced: vec![],
399 evpn_announced: vec![],
400 }),
401 ];
402 assert!(validate_update_attributes(&attrs, true, false, true).is_ok());
404 }
405
406 #[test]
407 fn mixed_update_requires_body_next_hop_for_ebgp() {
408 use crate::attribute::MpReachNlri;
409 use crate::capability::{Afi, Safi};
410 use crate::nlri::{Ipv6Prefix, NlriEntry, Prefix};
411
412 let attrs = vec![
414 PathAttribute::Origin(Origin::Igp),
415 PathAttribute::AsPath(AsPath {
416 segments: vec![AsPathSegment::AsSequence(vec![65001])],
417 }),
418 PathAttribute::MpReachNlri(MpReachNlri {
419 afi: Afi::Ipv6,
420 safi: Safi::Unicast,
421 next_hop: std::net::IpAddr::V6("2001:db8::1".parse().unwrap()),
422 link_local_next_hop: None,
423 announced: vec![NlriEntry {
424 path_id: 0,
425 prefix: Prefix::V6(Ipv6Prefix::new("2001:db8::".parse().unwrap(), 32)),
426 }],
427 flowspec_announced: vec![],
428 evpn_announced: vec![],
429 }),
430 ];
431 let err = validate_update_attributes(&attrs, true, true, true).unwrap_err();
434 assert_eq!(err.subcode, update_subcode::MISSING_WELLKNOWN);
435 assert_eq!(err.data, vec![attr_type::NEXT_HOP]);
436 }
437
438 #[test]
439 fn mp_reach_nlri_reject_unspecified_v6_next_hop() {
440 use crate::attribute::MpReachNlri;
441 use crate::capability::{Afi, Safi};
442
443 let attrs = vec![
444 PathAttribute::Origin(Origin::Igp),
445 PathAttribute::AsPath(AsPath {
446 segments: vec![AsPathSegment::AsSequence(vec![65001])],
447 }),
448 PathAttribute::MpReachNlri(MpReachNlri {
449 afi: Afi::Ipv6,
450 safi: Safi::Unicast,
451 next_hop: std::net::IpAddr::V6(std::net::Ipv6Addr::UNSPECIFIED),
452 link_local_next_hop: None,
453 announced: vec![],
454 flowspec_announced: vec![],
455 evpn_announced: vec![],
456 }),
457 ];
458 let err = validate_update_attributes(&attrs, true, false, true).unwrap_err();
459 assert_eq!(err.subcode, update_subcode::INVALID_NEXT_HOP);
460 }
461
462 #[test]
463 fn mp_reach_nlri_reject_link_local_v6_next_hop() {
464 use crate::attribute::MpReachNlri;
465 use crate::capability::{Afi, Safi};
466
467 let attrs = vec![
468 PathAttribute::Origin(Origin::Igp),
469 PathAttribute::AsPath(AsPath {
470 segments: vec![AsPathSegment::AsSequence(vec![65001])],
471 }),
472 PathAttribute::MpReachNlri(MpReachNlri {
473 afi: Afi::Ipv6,
474 safi: Safi::Unicast,
475 next_hop: std::net::IpAddr::V6("fe80::1".parse().unwrap()),
476 link_local_next_hop: None,
477 announced: vec![],
478 flowspec_announced: vec![],
479 evpn_announced: vec![],
480 }),
481 ];
482 let err = validate_update_attributes(&attrs, true, false, true).unwrap_err();
483 assert_eq!(err.subcode, update_subcode::INVALID_NEXT_HOP);
484 }
485
486 #[test]
487 fn mp_reach_nlri_reject_loopback_v6_next_hop() {
488 use crate::attribute::MpReachNlri;
489 use crate::capability::{Afi, Safi};
490
491 let attrs = vec![
492 PathAttribute::Origin(Origin::Igp),
493 PathAttribute::AsPath(AsPath {
494 segments: vec![AsPathSegment::AsSequence(vec![65001])],
495 }),
496 PathAttribute::MpReachNlri(MpReachNlri {
497 afi: Afi::Ipv6,
498 safi: Safi::Unicast,
499 next_hop: std::net::IpAddr::V6(std::net::Ipv6Addr::LOCALHOST),
500 link_local_next_hop: None,
501 announced: vec![],
502 flowspec_announced: vec![],
503 evpn_announced: vec![],
504 }),
505 ];
506 let err = validate_update_attributes(&attrs, true, false, true).unwrap_err();
507 assert_eq!(err.subcode, update_subcode::INVALID_NEXT_HOP);
508 }
509
510 #[test]
511 fn is_valid_ipv6_nexthop_accepts_global() {
512 assert!(super::is_valid_ipv6_nexthop(
513 &"2001:db8::1".parse().unwrap()
514 ));
515 }
516
517 #[test]
518 fn is_valid_ipv6_nexthop_rejects_unspecified() {
519 assert!(!super::is_valid_ipv6_nexthop(
520 &std::net::Ipv6Addr::UNSPECIFIED
521 ));
522 }
523
524 #[test]
525 fn is_valid_ipv6_nexthop_rejects_loopback() {
526 assert!(!super::is_valid_ipv6_nexthop(
527 &std::net::Ipv6Addr::LOCALHOST
528 ));
529 }
530
531 #[test]
532 fn is_valid_ipv6_nexthop_rejects_link_local() {
533 assert!(!super::is_valid_ipv6_nexthop(&"fe80::1".parse().unwrap()));
534 }
535
536 #[test]
537 fn is_valid_ipv6_nexthop_rejects_multicast() {
538 assert!(!super::is_valid_ipv6_nexthop(&"ff02::1".parse().unwrap()));
539 }
540
541 #[test]
542 fn mp_reach_nlri_reject_multicast_v6_next_hop() {
543 use crate::attribute::MpReachNlri;
544 use crate::capability::{Afi, Safi};
545
546 let attrs = vec![
547 PathAttribute::Origin(Origin::Igp),
548 PathAttribute::AsPath(AsPath {
549 segments: vec![AsPathSegment::AsSequence(vec![65001])],
550 }),
551 PathAttribute::MpReachNlri(MpReachNlri {
552 afi: Afi::Ipv6,
553 safi: Safi::Unicast,
554 next_hop: std::net::IpAddr::V6("ff02::1".parse().unwrap()),
556 link_local_next_hop: None,
557 announced: vec![],
558 flowspec_announced: vec![],
559 evpn_announced: vec![],
560 }),
561 ];
562 let err = validate_update_attributes(&attrs, true, false, true).unwrap_err();
563 assert_eq!(err.subcode, update_subcode::INVALID_NEXT_HOP);
564 }
565
566 #[test]
577 fn mp_reach_flowspec_unspecified_next_hop_is_valid() {
578 use crate::attribute::MpReachNlri;
579 use crate::capability::{Afi, Safi};
580
581 let attrs = vec![
582 PathAttribute::Origin(Origin::Igp),
583 PathAttribute::AsPath(AsPath {
584 segments: vec![AsPathSegment::AsSequence(vec![65001])],
585 }),
586 PathAttribute::MpReachNlri(MpReachNlri {
587 afi: Afi::Ipv4,
588 safi: Safi::FlowSpec,
589 next_hop: std::net::IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED),
593 link_local_next_hop: None,
594 announced: vec![],
595 flowspec_announced: vec![],
596 evpn_announced: vec![],
597 }),
598 ];
599 assert!(
602 validate_update_attributes(&attrs, false, false, true).is_ok(),
603 "FlowSpec MP_REACH with 0.0.0.0 next-hop must pass — RFC 8955 §6.1 \
604 specifies the next-hop value is irrelevant for FlowSpec and \
605 recommends 0. The pre-fix path tore sessions against every \
606 RFC-compliant FlowSpec peer."
607 );
608 }
609}