use std::collections::HashSet;
use std::net::IpAddr;
use crate::attribute::{AsPath, AsPathSegment, PathAttribute, attr_error_data};
use crate::capability::Safi;
use crate::constants::{attr_flags, attr_type};
use crate::notification::update_subcode;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UpdateError {
pub subcode: u8,
pub data: Vec<u8>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct UpdateValidationOptions {
pub allow_ipv4_link_local_mp_reach_next_hop: bool,
}
const MANDATORY_ATTRS: &[u8] = &[attr_type::ORIGIN, attr_type::AS_PATH];
pub fn validate_update_attributes(
attrs: &[PathAttribute],
has_nlri: bool,
has_body_nlri: bool,
is_ebgp: bool,
) -> Result<(), UpdateError> {
validate_update_attributes_with_options(
attrs,
has_nlri,
has_body_nlri,
is_ebgp,
UpdateValidationOptions::default(),
)
}
pub fn validate_update_attributes_with_options(
attrs: &[PathAttribute],
has_nlri: bool,
has_body_nlri: bool,
is_ebgp: bool,
options: UpdateValidationOptions,
) -> Result<(), UpdateError> {
check_duplicate_types(attrs)?;
check_unrecognized_wellknown(attrs)?;
if has_nlri {
check_mandatory_present(attrs, has_body_nlri, is_ebgp)?;
}
for attr in attrs {
match attr {
PathAttribute::NextHop(addr) => check_next_hop(*addr)?,
PathAttribute::AsPath(path) => check_as_path(path)?,
PathAttribute::MpReachNlri(mp) if mp.safi != Safi::FlowSpec => {
let allow_link_local_primary = options.allow_ipv4_link_local_mp_reach_next_hop
&& mp.afi == crate::capability::Afi::Ipv4
&& mp.safi == Safi::Unicast;
check_mp_reach_next_hop(
mp.next_hop,
mp.link_local_next_hop,
allow_link_local_primary,
)?;
}
_ => {}
}
}
Ok(())
}
fn check_duplicate_types(attrs: &[PathAttribute]) -> Result<(), UpdateError> {
let mut seen = HashSet::new();
for attr in attrs {
let tc = attr.type_code();
if !seen.insert(tc) {
return Err(UpdateError {
subcode: update_subcode::MALFORMED_ATTRIBUTE_LIST,
data: vec![],
});
}
}
Ok(())
}
fn check_unrecognized_wellknown(attrs: &[PathAttribute]) -> Result<(), UpdateError> {
for attr in attrs {
if let PathAttribute::Unknown(raw) = attr {
if (raw.flags & attr_flags::OPTIONAL) == 0 {
return Err(UpdateError {
subcode: update_subcode::UNRECOGNIZED_WELLKNOWN,
data: attr_error_data(raw.flags, raw.type_code, &raw.data),
});
}
}
}
Ok(())
}
fn check_mandatory_present(
attrs: &[PathAttribute],
has_body_nlri: bool,
is_ebgp: bool,
) -> Result<(), UpdateError> {
let present: HashSet<u8> = attrs.iter().map(PathAttribute::type_code).collect();
for &tc in MANDATORY_ATTRS {
if !present.contains(&tc) {
return Err(UpdateError {
subcode: update_subcode::MISSING_WELLKNOWN,
data: vec![tc],
});
}
}
if is_ebgp && has_body_nlri && !present.contains(&attr_type::NEXT_HOP) {
return Err(UpdateError {
subcode: update_subcode::MISSING_WELLKNOWN,
data: vec![attr_type::NEXT_HOP],
});
}
Ok(())
}
fn check_next_hop(addr: std::net::Ipv4Addr) -> Result<(), UpdateError> {
let octets = addr.octets();
if addr.is_unspecified() {
return Err(UpdateError {
subcode: update_subcode::INVALID_NEXT_HOP,
data: octets.to_vec(),
});
}
if addr.is_loopback() {
return Err(UpdateError {
subcode: update_subcode::INVALID_NEXT_HOP,
data: octets.to_vec(),
});
}
if addr.is_multicast() {
return Err(UpdateError {
subcode: update_subcode::INVALID_NEXT_HOP,
data: octets.to_vec(),
});
}
if addr.is_broadcast() {
return Err(UpdateError {
subcode: update_subcode::INVALID_NEXT_HOP,
data: octets.to_vec(),
});
}
Ok(())
}
fn check_mp_reach_next_hop(
addr: IpAddr,
link_local: Option<std::net::Ipv6Addr>,
allow_link_local_primary: bool,
) -> Result<(), UpdateError> {
match addr {
IpAddr::V4(v4) => check_next_hop(v4)?,
IpAddr::V6(v6) => {
let valid = if allow_link_local_primary && is_ipv6_link_local(&v6) {
link_local.is_some_and(|ll| ll == v6)
} else {
is_valid_ipv6_nexthop(&v6)
};
if !valid {
return Err(UpdateError {
subcode: update_subcode::INVALID_NEXT_HOP,
data: v6.octets().to_vec(),
});
}
}
}
if let Some(ll) = link_local
&& !is_ipv6_link_local(&ll)
{
return Err(UpdateError {
subcode: update_subcode::INVALID_NEXT_HOP,
data: ll.octets().to_vec(),
});
}
Ok(())
}
fn is_ipv6_link_local(addr: &std::net::Ipv6Addr) -> bool {
(addr.segments()[0] & 0xffc0) == 0xfe80
}
#[must_use]
pub fn is_valid_ipv6_nexthop(addr: &std::net::Ipv6Addr) -> bool {
!addr.is_unspecified()
&& !addr.is_loopback()
&& !addr.is_multicast()
&& !is_ipv6_link_local(addr)
}
fn check_as_path(path: &AsPath) -> Result<(), UpdateError> {
for segment in &path.segments {
let asns = match segment {
AsPathSegment::AsSet(asns) | AsPathSegment::AsSequence(asns) => asns,
};
if asns.is_empty() {
return Err(UpdateError {
subcode: update_subcode::MALFORMED_AS_PATH,
data: vec![],
});
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use std::net::Ipv4Addr;
use bytes::Bytes;
use super::*;
use crate::attribute::{Origin, RawAttribute};
fn basic_attrs(next_hop: Ipv4Addr) -> Vec<PathAttribute> {
vec![
PathAttribute::Origin(Origin::Igp),
PathAttribute::AsPath(AsPath {
segments: vec![AsPathSegment::AsSequence(vec![65001])],
}),
PathAttribute::NextHop(next_hop),
]
}
#[test]
fn valid_ebgp_update() {
let attrs = basic_attrs(Ipv4Addr::new(10, 0, 0, 1));
assert!(validate_update_attributes(&attrs, true, true, true).is_ok());
}
#[test]
fn valid_ibgp_update_no_next_hop() {
let attrs = vec![
PathAttribute::Origin(Origin::Igp),
PathAttribute::AsPath(AsPath {
segments: vec![AsPathSegment::AsSequence(vec![65001])],
}),
];
assert!(validate_update_attributes(&attrs, true, true, false).is_ok());
}
#[test]
fn withdrawal_only_no_attrs_ok() {
assert!(validate_update_attributes(&[], false, false, true).is_ok());
}
#[test]
fn reject_duplicate_type() {
let attrs = vec![
PathAttribute::Origin(Origin::Igp),
PathAttribute::Origin(Origin::Egp),
];
let err = validate_update_attributes(&attrs, false, false, true).unwrap_err();
assert_eq!(err.subcode, update_subcode::MALFORMED_ATTRIBUTE_LIST);
}
#[test]
fn reject_missing_origin() {
let attrs = vec![
PathAttribute::AsPath(AsPath {
segments: vec![AsPathSegment::AsSequence(vec![65001])],
}),
PathAttribute::NextHop(Ipv4Addr::new(10, 0, 0, 1)),
];
let err = validate_update_attributes(&attrs, true, true, true).unwrap_err();
assert_eq!(err.subcode, update_subcode::MISSING_WELLKNOWN);
}
#[test]
fn reject_missing_as_path() {
let attrs = vec![
PathAttribute::Origin(Origin::Igp),
PathAttribute::NextHop(Ipv4Addr::new(10, 0, 0, 1)),
];
let err = validate_update_attributes(&attrs, true, true, true).unwrap_err();
assert_eq!(err.subcode, update_subcode::MISSING_WELLKNOWN);
}
#[test]
fn reject_missing_next_hop_ebgp() {
let attrs = vec![
PathAttribute::Origin(Origin::Igp),
PathAttribute::AsPath(AsPath {
segments: vec![AsPathSegment::AsSequence(vec![65001])],
}),
];
let err = validate_update_attributes(&attrs, true, true, true).unwrap_err();
assert_eq!(err.subcode, update_subcode::MISSING_WELLKNOWN);
assert_eq!(err.data, vec![attr_type::NEXT_HOP]);
}
#[test]
fn reject_next_hop_unspecified() {
let attrs = basic_attrs(Ipv4Addr::UNSPECIFIED);
let err = validate_update_attributes(&attrs, true, true, true).unwrap_err();
assert_eq!(err.subcode, update_subcode::INVALID_NEXT_HOP);
}
#[test]
fn reject_next_hop_loopback() {
let attrs = basic_attrs(Ipv4Addr::LOCALHOST);
let err = validate_update_attributes(&attrs, true, true, true).unwrap_err();
assert_eq!(err.subcode, update_subcode::INVALID_NEXT_HOP);
}
#[test]
fn reject_next_hop_multicast() {
let attrs = basic_attrs(Ipv4Addr::new(224, 0, 0, 1));
let err = validate_update_attributes(&attrs, true, true, true).unwrap_err();
assert_eq!(err.subcode, update_subcode::INVALID_NEXT_HOP);
}
#[test]
fn reject_next_hop_broadcast() {
let attrs = basic_attrs(Ipv4Addr::BROADCAST);
let err = validate_update_attributes(&attrs, true, true, true).unwrap_err();
assert_eq!(err.subcode, update_subcode::INVALID_NEXT_HOP);
}
#[test]
fn reject_empty_as_path_segment() {
let attrs = vec![
PathAttribute::Origin(Origin::Igp),
PathAttribute::AsPath(AsPath {
segments: vec![AsPathSegment::AsSequence(vec![])],
}),
PathAttribute::NextHop(Ipv4Addr::new(10, 0, 0, 1)),
];
let err = validate_update_attributes(&attrs, true, true, true).unwrap_err();
assert_eq!(err.subcode, update_subcode::MALFORMED_AS_PATH);
}
#[test]
fn reject_unrecognized_wellknown() {
let attrs = vec![PathAttribute::Unknown(RawAttribute {
flags: attr_flags::TRANSITIVE, type_code: 99,
data: Bytes::from_static(&[1, 2, 3]),
})];
let err = validate_update_attributes(&attrs, false, false, true).unwrap_err();
assert_eq!(err.subcode, update_subcode::UNRECOGNIZED_WELLKNOWN);
}
#[test]
fn optional_unknown_attribute_ok() {
let attrs = vec![PathAttribute::Unknown(RawAttribute {
flags: attr_flags::OPTIONAL | attr_flags::TRANSITIVE,
type_code: 99,
data: Bytes::from_static(&[1, 2, 3]),
})];
assert!(validate_update_attributes(&attrs, false, false, true).is_ok());
}
#[test]
fn mp_reach_nlri_no_body_next_hop_required_for_ebgp() {
use crate::attribute::MpReachNlri;
use crate::capability::{Afi, Safi};
use crate::nlri::{Ipv6Prefix, NlriEntry, Prefix};
let attrs = vec![
PathAttribute::Origin(Origin::Igp),
PathAttribute::AsPath(AsPath {
segments: vec![AsPathSegment::AsSequence(vec![65001])],
}),
PathAttribute::MpReachNlri(MpReachNlri {
afi: Afi::Ipv6,
safi: Safi::Unicast,
next_hop: std::net::IpAddr::V6("2001:db8::1".parse().unwrap()),
link_local_next_hop: None,
announced: vec![NlriEntry {
path_id: 0,
prefix: Prefix::V6(Ipv6Prefix::new("2001:db8::".parse().unwrap(), 32)),
}],
flowspec_announced: vec![],
evpn_announced: vec![],
}),
];
assert!(validate_update_attributes(&attrs, true, false, true).is_ok());
}
#[test]
fn mixed_update_requires_body_next_hop_for_ebgp() {
use crate::attribute::MpReachNlri;
use crate::capability::{Afi, Safi};
use crate::nlri::{Ipv6Prefix, NlriEntry, Prefix};
let attrs = vec![
PathAttribute::Origin(Origin::Igp),
PathAttribute::AsPath(AsPath {
segments: vec![AsPathSegment::AsSequence(vec![65001])],
}),
PathAttribute::MpReachNlri(MpReachNlri {
afi: Afi::Ipv6,
safi: Safi::Unicast,
next_hop: std::net::IpAddr::V6("2001:db8::1".parse().unwrap()),
link_local_next_hop: None,
announced: vec![NlriEntry {
path_id: 0,
prefix: Prefix::V6(Ipv6Prefix::new("2001:db8::".parse().unwrap(), 32)),
}],
flowspec_announced: vec![],
evpn_announced: vec![],
}),
];
let err = validate_update_attributes(&attrs, true, true, true).unwrap_err();
assert_eq!(err.subcode, update_subcode::MISSING_WELLKNOWN);
assert_eq!(err.data, vec![attr_type::NEXT_HOP]);
}
#[test]
fn mp_reach_nlri_reject_unspecified_v6_next_hop() {
use crate::attribute::MpReachNlri;
use crate::capability::{Afi, Safi};
let attrs = vec![
PathAttribute::Origin(Origin::Igp),
PathAttribute::AsPath(AsPath {
segments: vec![AsPathSegment::AsSequence(vec![65001])],
}),
PathAttribute::MpReachNlri(MpReachNlri {
afi: Afi::Ipv6,
safi: Safi::Unicast,
next_hop: std::net::IpAddr::V6(std::net::Ipv6Addr::UNSPECIFIED),
link_local_next_hop: None,
announced: vec![],
flowspec_announced: vec![],
evpn_announced: vec![],
}),
];
let err = validate_update_attributes(&attrs, true, false, true).unwrap_err();
assert_eq!(err.subcode, update_subcode::INVALID_NEXT_HOP);
}
#[test]
fn mp_reach_nlri_reject_link_local_v6_next_hop() {
use crate::attribute::MpReachNlri;
use crate::capability::{Afi, Safi};
let attrs = vec![
PathAttribute::Origin(Origin::Igp),
PathAttribute::AsPath(AsPath {
segments: vec![AsPathSegment::AsSequence(vec![65001])],
}),
PathAttribute::MpReachNlri(MpReachNlri {
afi: Afi::Ipv6,
safi: Safi::Unicast,
next_hop: std::net::IpAddr::V6("fe80::1".parse().unwrap()),
link_local_next_hop: None,
announced: vec![],
flowspec_announced: vec![],
evpn_announced: vec![],
}),
];
let err = validate_update_attributes(&attrs, true, false, true).unwrap_err();
assert_eq!(err.subcode, update_subcode::INVALID_NEXT_HOP);
}
#[test]
fn mp_reach_nlri_allows_link_local_primary_only_for_opted_in_ipv4() {
use crate::attribute::MpReachNlri;
use crate::capability::{Afi, Safi};
let attrs = vec![
PathAttribute::Origin(Origin::Igp),
PathAttribute::AsPath(AsPath {
segments: vec![AsPathSegment::AsSequence(vec![65001])],
}),
PathAttribute::MpReachNlri(MpReachNlri {
afi: Afi::Ipv4,
safi: Safi::Unicast,
next_hop: std::net::IpAddr::V6("fe80::1".parse().unwrap()),
link_local_next_hop: Some("fe80::1".parse().unwrap()),
announced: vec![],
flowspec_announced: vec![],
evpn_announced: vec![],
}),
];
assert!(validate_update_attributes(&attrs, true, false, true).is_err());
assert!(
validate_update_attributes_with_options(
&attrs,
true,
false,
true,
UpdateValidationOptions {
allow_ipv4_link_local_mp_reach_next_hop: true,
},
)
.is_ok()
);
let mut attrs_without_companion = attrs.clone();
if let PathAttribute::MpReachNlri(mp) = &mut attrs_without_companion[2] {
mp.link_local_next_hop = None;
}
assert!(
validate_update_attributes_with_options(
&attrs_without_companion,
true,
false,
true,
UpdateValidationOptions {
allow_ipv4_link_local_mp_reach_next_hop: true,
},
)
.is_err()
);
}
#[test]
fn mp_reach_nlri_reject_loopback_v6_next_hop() {
use crate::attribute::MpReachNlri;
use crate::capability::{Afi, Safi};
let attrs = vec![
PathAttribute::Origin(Origin::Igp),
PathAttribute::AsPath(AsPath {
segments: vec![AsPathSegment::AsSequence(vec![65001])],
}),
PathAttribute::MpReachNlri(MpReachNlri {
afi: Afi::Ipv6,
safi: Safi::Unicast,
next_hop: std::net::IpAddr::V6(std::net::Ipv6Addr::LOCALHOST),
link_local_next_hop: None,
announced: vec![],
flowspec_announced: vec![],
evpn_announced: vec![],
}),
];
let err = validate_update_attributes(&attrs, true, false, true).unwrap_err();
assert_eq!(err.subcode, update_subcode::INVALID_NEXT_HOP);
}
#[test]
fn is_valid_ipv6_nexthop_accepts_global() {
assert!(super::is_valid_ipv6_nexthop(
&"2001:db8::1".parse().unwrap()
));
}
#[test]
fn is_valid_ipv6_nexthop_rejects_unspecified() {
assert!(!super::is_valid_ipv6_nexthop(
&std::net::Ipv6Addr::UNSPECIFIED
));
}
#[test]
fn is_valid_ipv6_nexthop_rejects_loopback() {
assert!(!super::is_valid_ipv6_nexthop(
&std::net::Ipv6Addr::LOCALHOST
));
}
#[test]
fn is_valid_ipv6_nexthop_rejects_link_local() {
assert!(!super::is_valid_ipv6_nexthop(&"fe80::1".parse().unwrap()));
}
#[test]
fn is_valid_ipv6_nexthop_rejects_multicast() {
assert!(!super::is_valid_ipv6_nexthop(&"ff02::1".parse().unwrap()));
}
#[test]
fn mp_reach_nlri_reject_multicast_v6_next_hop() {
use crate::attribute::MpReachNlri;
use crate::capability::{Afi, Safi};
let attrs = vec![
PathAttribute::Origin(Origin::Igp),
PathAttribute::AsPath(AsPath {
segments: vec![AsPathSegment::AsSequence(vec![65001])],
}),
PathAttribute::MpReachNlri(MpReachNlri {
afi: Afi::Ipv6,
safi: Safi::Unicast,
next_hop: std::net::IpAddr::V6("ff02::1".parse().unwrap()),
link_local_next_hop: None,
announced: vec![],
flowspec_announced: vec![],
evpn_announced: vec![],
}),
];
let err = validate_update_attributes(&attrs, true, false, true).unwrap_err();
assert_eq!(err.subcode, update_subcode::INVALID_NEXT_HOP);
}
#[test]
fn mp_reach_flowspec_unspecified_next_hop_is_valid() {
use crate::attribute::MpReachNlri;
use crate::capability::{Afi, Safi};
let attrs = vec![
PathAttribute::Origin(Origin::Igp),
PathAttribute::AsPath(AsPath {
segments: vec![AsPathSegment::AsSequence(vec![65001])],
}),
PathAttribute::MpReachNlri(MpReachNlri {
afi: Afi::Ipv4,
safi: Safi::FlowSpec,
next_hop: std::net::IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED),
link_local_next_hop: None,
announced: vec![],
flowspec_announced: vec![],
evpn_announced: vec![],
}),
];
assert!(
validate_update_attributes(&attrs, false, false, true).is_ok(),
"FlowSpec MP_REACH with 0.0.0.0 next-hop must pass — RFC 8955 §6.1 \
specifies the next-hop value is irrelevant for FlowSpec and \
recommends 0. The pre-fix path tore sessions against every \
RFC-compliant FlowSpec peer."
);
}
#[test]
fn mp_reach_ipv6_invalid_link_local_segment_rejected() {
use crate::attribute::MpReachNlri;
use crate::capability::{Afi, Safi};
let attrs = vec![
PathAttribute::Origin(Origin::Igp),
PathAttribute::AsPath(AsPath {
segments: vec![AsPathSegment::AsSequence(vec![65001])],
}),
PathAttribute::MpReachNlri(MpReachNlri {
afi: Afi::Ipv6,
safi: Safi::Unicast,
next_hop: std::net::IpAddr::V6("2001:db8::1".parse().unwrap()),
link_local_next_hop: Some("2001:db8::2".parse().unwrap()),
announced: vec![],
flowspec_announced: vec![],
evpn_announced: vec![],
}),
];
let err = validate_update_attributes(&attrs, false, false, true).unwrap_err();
assert_eq!(err.subcode, update_subcode::INVALID_NEXT_HOP);
}
#[test]
fn mp_reach_ipv6_global_plus_link_local_accepted() {
use crate::attribute::MpReachNlri;
use crate::capability::{Afi, Safi};
let attrs = vec![
PathAttribute::Origin(Origin::Igp),
PathAttribute::AsPath(AsPath {
segments: vec![AsPathSegment::AsSequence(vec![65001])],
}),
PathAttribute::MpReachNlri(MpReachNlri {
afi: Afi::Ipv6,
safi: Safi::Unicast,
next_hop: std::net::IpAddr::V6("2001:db8::1".parse().unwrap()),
link_local_next_hop: Some("fe80::1".parse().unwrap()),
announced: vec![],
flowspec_announced: vec![],
evpn_announced: vec![],
}),
];
assert!(validate_update_attributes(&attrs, false, false, true).is_ok());
}
}