use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::str::FromStr; use std::sync::Arc; use std::time::Duration; use bgp_fib::FibConfig; use bgp_packet::community; use bgp_packet::message::OriginPathAttribute; use bgp_packet::open::AddPathSendRecv; use bgp_primitives::constants::{AfiSafi, IpVersion}; use bgp_primitives::ip_prefix::IpPrefix; use bgp_rib::{LocalRoute, LocalRouteAttrs}; use bgp_rpki::RpkiConfig; use crate::address::{Endpoint, PeerAddress}; use crate::error::{ConfigError, ImportProblem, OriginateProblem, PeerProblem}; use crate::family::FamilySet; use crate::policy::{ImportPolicy, PeerRole, PrefixSet}; use crate::proto::config::{self as proto, BgpConfig, OriginatedRoute, Peer}; const DEFAULT_BGP_PORT: u16 = 179; const DEFAULT_HOLD_TIME_SECS: u16 = 90; const DEFAULT_CONNECT_RETRY: Duration = Duration::from_secs(5); const MAX_AS_PATH_PREPEND: u32 = 16; /// Validated, Rust-native server configuration derived from the proto config. pub struct Config { pub router_id: Ipv4Addr, pub local_asn: u32, /// The hold time offered to a peer that is not in the configuration /// (see `accept_unconfigured_peers`). Configured peers have their own. pub hold_time_secs: u16, pub listen_endpoints: Vec, pub peers: Vec, /// Initial delay between attempts to connect to a configured peer; /// doubles on each failure. pub connect_retry: Duration, /// Resolve collisions against Established sessions by BGP Identifier /// instead of always closing the new connection (RFC 4271 §8.1.1 /// `CollisionDetectEstablishedState`). pub collision_detect_established_state: bool, /// Take connections from addresses that are not those of configured /// peers. Off unless asked for: with it, any host that can reach us can /// open a session and announce routes. pub accept_unconfigured_peers: bool, /// Routes this router originates. pub originate: Vec, /// Address for the HTTP API; `None` disables it. pub http_endpoint: Option, /// Where to install the best routes in the kernel, if at all. pub kernel: Option, /// Where route origin validation gets its data; `None` for no /// validation. pub rpki: Option, } #[derive(Debug, Clone)] pub struct PeerConfig { pub name: String, /// Where the peer is. A link-local address has its interface with it. pub endpoint: Endpoint, pub remote_asn: u32, pub hold_time_secs: u16, /// Source address for connections we open to this peer; `None` lets the /// operating system choose. pub local_address: Option, /// The address families offered to this peer. pub families: FamilySet, /// The directions ADD-PATH (RFC 7911) is offered to this peer for, on /// every one of `families`. `None` offers it for neither. pub add_path: Option, /// Which of the peer's routes are taken. pub import: ImportPolicy, /// What the peer is to us, if that was said. pub role: Option, /// Refuse the peer if it does not say what it is itself (RFC 9234 /// §4.2, "strict mode"). pub require_role: bool, } impl Config { /// The configured peer at `address`, if there is one. pub fn peer_at(&self, address: PeerAddress) -> Option<&PeerConfig> { self.peers.iter().find(|peer| peer.endpoint.is_at(address)) } pub fn from_proto(proto: BgpConfig) -> Result { // Peers refuse an identifier of 0.0.0.0, as we do theirs. let router_id = Ipv4Addr::from_str(&proto.router_id) .ok() .filter(|id| !id.is_unspecified()) .ok_or_else(|| ConfigError::RouterId(proto.router_id.clone()))?; if proto.local_asn == 0 { return Err(ConfigError::LocalAsn); } let listen_endpoints = if proto.listen_endpoints.is_empty() { vec![ SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), DEFAULT_BGP_PORT), SocketAddr::new( IpAddr::V6(std::net::Ipv6Addr::UNSPECIFIED), DEFAULT_BGP_PORT, ), ] } else { proto .listen_endpoints .iter() .map(|ep| parse_endpoint(&ep.address, ep.port)) .collect::>()? }; let mut peers: Vec = Vec::with_capacity(proto.peers.len()); for peer in proto.peers { let name = peer.name.clone(); let rejected = |problem| ConfigError::Peer { peer: name.clone(), problem, }; let peer = parse_peer(peer).map_err(rejected)?; // RFC 9234 is about what networks are to each other. if peer.role.is_some() && peer.remote_asn == proto.local_asn { return Err(rejected(PeerProblem::RoleInOwnAs)); } // A peer is told from the others by its name in what the // dialer reports, and by its address when it connects to us. if peers.iter().any(|earlier| earlier.name == peer.name) { return Err(rejected(PeerProblem::DuplicateName)); } if peers.iter().any(|earlier| { earlier.endpoint.ip == peer.endpoint.ip && earlier.endpoint.interface == peer.endpoint.interface }) { return Err(rejected(PeerProblem::DuplicateAddress)); } peers.push(peer); } let mut originate: Vec = Vec::with_capacity(proto.originate.len()); for entry in &proto.originate { let rejected = |problem| ConfigError::Originate { prefix: entry.prefix.clone(), problem, }; let route = parse_originated_route(entry).map_err(rejected)?; if originate .iter() .any(|earlier| earlier.prefix == route.prefix) { return Err(rejected(OriginateProblem::Duplicate)); } originate.push(route); } // What we originate is not taken from other networks. let mut own_prefixes = PrefixSet::default(); for route in &originate { own_prefixes.allow(route.prefix, route.prefix.address_family().bits()); } let own_prefixes = Arc::new(own_prefixes); for peer in &mut peers { if peer.remote_asn != proto.local_asn { peer.import.own_prefixes = Arc::clone(&own_prefixes); } } let http_endpoint = match proto.http_endpoint.into_option() { Some(ep) if ep.port == 0 => return Err(ConfigError::HttpPort), Some(ep) => Some(parse_endpoint(&ep.address, ep.port)?), None => None, }; let rpki = match proto.rpki.as_ref() { Some(rpki) => Some(parse_rpki(rpki)?), None => None, }; let connect_retry = if proto.connect_retry_seconds == 0 { DEFAULT_CONNECT_RETRY } else { Duration::from_secs(proto.connect_retry_seconds as u64) }; Ok(Self { router_id, local_asn: proto.local_asn, hold_time_secs: DEFAULT_HOLD_TIME_SECS, listen_endpoints, peers, connect_retry, collision_detect_established_state: proto.collision_detect_established_state, accept_unconfigured_peers: proto.accept_unconfigured_peers, originate, http_endpoint, kernel: proto.kernel.as_ref().map(parse_kernel), rpki, }) } } fn parse_kernel(kernel: &proto::Kernel) -> FibConfig { let defaults = FibConfig::default(); FibConfig { // The kernel has no table 0 to put routes in. table: match kernel.table { 0 => defaults.table, table => table, }, metric: kernel.metric.unwrap_or(defaults.metric), nexthop_objects: !kernel.without_nexthop_objects, stale_time: kernel.stale_seconds.map_or(defaults.stale_time, |seconds| { Duration::from_secs(seconds.into()) }), } } fn parse_peer(peer: Peer) -> Result { let ep = peer.endpoint.as_ref().ok_or(PeerProblem::MissingEndpoint)?; let endpoint = parse_port(ep.port) .and_then(|port| Ok(Endpoint::parse(&ep.address, port)?)) .map_err(|e| PeerProblem::Endpoint(Box::new(e)))?; if peer.remote_asn == 0 { return Err(PeerProblem::RemoteAsn); } let hold_time_secs = match peer.hold_time_seconds { None => DEFAULT_HOLD_TIME_SECS, // RFC 4271 §4.2: zero, or at least three seconds. Some(1 | 2) => return Err(PeerProblem::HoldTime), Some(secs) => u16::try_from(secs).map_err(|_| PeerProblem::HoldTime)?, }; let local_address = if peer.local_address.is_empty() { None } else { let ip = IpAddr::from_str(&peer.local_address) .map_err(|_| PeerProblem::LocalAddress(peer.local_address.clone()))?; if ip.is_ipv4() != endpoint.ip.is_ipv4() { return Err(PeerProblem::AddressFamilyMismatch); } Some(ip) }; let families = if peer.address_families.is_empty() { FamilySet::supported() } else { peer.address_families .iter() .map(|family| match family.enum_value() { Ok(proto::AddressFamily::IPV4_UNICAST) => Ok(AfiSafi::IPV4_UNICAST), Ok(proto::AddressFamily::IPV6_UNICAST) => Ok(AfiSafi::IPV6_UNICAST), Ok(proto::AddressFamily::ADDRESS_FAMILY_UNSPECIFIED) | Err(_) => { Err(PeerProblem::AddressFamily) } }) .collect::>()? }; let add_path = match peer.add_path.enum_value() { Ok(proto::AddPath::ADD_PATH_OFF) => None, Ok(proto::AddPath::ADD_PATH_RECEIVE) => Some(AddPathSendRecv::Receive), Ok(proto::AddPath::ADD_PATH_SEND) => Some(AddPathSendRecv::Send), Ok(proto::AddPath::ADD_PATH_BOTH) => Some(AddPathSendRecv::Both), Err(_) => return Err(PeerProblem::AddPath), }; let import = match peer.import.as_ref() { Some(import) => parse_import(import)?, None => ImportPolicy::default(), }; let role = match peer.role.enum_value() { Ok(proto::Role::ROLE_UNSPECIFIED) => None, Ok(proto::Role::PROVIDER) => Some(PeerRole::Provider), Ok(proto::Role::CUSTOMER) => Some(PeerRole::Customer), Ok(proto::Role::PEER) => Some(PeerRole::Peer), Ok(proto::Role::ROUTE_SERVER) => Some(PeerRole::RouteServer), Ok(proto::Role::ROUTE_SERVER_CLIENT) => Some(PeerRole::RouteServerClient), Err(_) => return Err(PeerProblem::Role), }; if peer.require_role && role.is_none() { return Err(PeerProblem::RequireRoleWithoutRole); } Ok(PeerConfig { // A peer without a name goes by its address. name: if peer.name.is_empty() { endpoint.to_string() } else { peer.name }, endpoint, remote_asn: peer.remote_asn, hold_time_secs, local_address, families, add_path, import, role, require_role: peer.require_role, }) } fn parse_rpki(rpki: &proto::Rpki) -> Result { if rpki.caches.is_empty() { return Err(ConfigError::RpkiNoCaches); } let caches = rpki .caches .iter() .map(|cache| match cache.port { // There is no port a cache can be assumed to listen on. 0 => Err(ConfigError::RpkiCachePort(cache.address.clone())), port => parse_endpoint(&cache.address, port), }) .collect::>()?; let seconds = |seconds: u32| (seconds > 0).then(|| Duration::from_secs(seconds.into())); Ok(RpkiConfig { caches, refresh: seconds(rpki.refresh_seconds), retry: seconds(rpki.retry_seconds), expire: seconds(rpki.expire_seconds), }) } fn parse_import(import: &proto::ImportPolicy) -> Result { let defaults = ImportPolicy::default(); // A length of zero is the field left out. let longest = |field, value: u32, version: IpVersion, default: u8| match value { 0 => Ok(default), value => u8::try_from(value) .ok() .filter(|length| *length <= version.bits()) .ok_or(ImportProblem::PrefixLength { field, value, longest: version.bits(), }), }; let mut prefixes = PrefixSet::default(); for range in &import.prefixes { let prefix = IpPrefix::try_from(range.prefix.as_str()).map_err(|problem| ImportProblem::Prefix { prefix: range.prefix.clone(), problem, })?; let longest = prefix.address_family().bits(); let max_length = match range.max_length { 0 => prefix.length(), max_length => u8::try_from(max_length) .ok() .filter(|max| (prefix.length()..=longest).contains(max)) .ok_or(ImportProblem::MaxLength { prefix: range.prefix.clone(), max_length, longest, })?, }; prefixes.allow(prefix, max_length); } Ok(ImportPolicy { max_prefixes: Some(import.max_prefixes).filter(|limit| *limit > 0), hold_down: match import.hold_down_seconds { 0 => defaults.hold_down, seconds => Duration::from_secs(seconds.into()), }, accept_bogons: import.accept_bogons, accept_default_route: import.accept_default_route, max_ipv4_length: longest( "max_ipv4_prefix_length", import.max_ipv4_prefix_length, IpVersion::Ipv4, defaults.max_ipv4_length, )?, max_ipv6_length: longest( "max_ipv6_prefix_length", import.max_ipv6_prefix_length, IpVersion::Ipv6, defaults.max_ipv6_length, )?, accept_any_first_as: import.accept_any_first_as, accept_rpki_invalid: import.accept_rpki_invalid, accept_aspa_invalid: import.accept_aspa_invalid, accept_as_sets: import.accept_as_sets, accept_third_party_next_hop: import.accept_third_party_next_hop, own_prefixes: Arc::default(), max_as_path_length: match import.max_as_path_length { 0 => defaults.max_as_path_length, length => u16::try_from(length).map_err(|_| ImportProblem::PathLength(length))?, }, prefixes: Some(prefixes).filter(|prefixes| !prefixes.is_empty()), origin_asns: Some(import.origin_asns.iter().copied().collect()) .filter(|asns: &std::collections::HashSet| !asns.is_empty()), }) } fn parse_port(port: u32) -> Result { match port { 0 => Ok(DEFAULT_BGP_PORT), port => u16::try_from(port).map_err(|_| ConfigError::Port(port)), } } fn parse_endpoint(address: &str, port: u32) -> Result { let port = parse_port(port)?; let ip = IpAddr::from_str(address).map_err(|_| ConfigError::Address(address.to_string()))?; Ok(SocketAddr::new(ip, port)) } fn parse_originated_route(route: &OriginatedRoute) -> Result { let prefix = IpPrefix::try_from(route.prefix.as_str())?; let origin = match route.origin.to_ascii_uppercase().as_str() { "" | "IGP" => OriginPathAttribute::IGP, "EGP" => OriginPathAttribute::EGP, "INCOMPLETE" => OriginPathAttribute::INCOMPLETE, _ => return Err(OriginateProblem::Origin(route.origin.clone())), }; let prepend = u8::try_from(route.as_path_prepend) .ok() .filter(|&n| u32::from(n) <= MAX_AS_PATH_PREPEND) .ok_or(OriginateProblem::Prepend { max: MAX_AS_PATH_PREPEND, })?; let communities = route .communities .iter() .map(|c| community::parse_standard(c)) .collect::>()?; let large_communities = route .large_communities .iter() .map(|c| community::parse_large(c)) .collect::>()?; Ok(LocalRoute::new( prefix, LocalRouteAttrs { origin, prepend, med: route.med, communities, large_communities, }, )) } #[cfg(test)] mod tests { use bgp_primitives::ip_prefix::PrefixError; use super::*; fn parse(text: &str) -> Result { let proto = protobuf::text_format::parse_from_str::(text) .expect("the test's textproto is well formed"); Config::from_proto(proto) } fn rejected(text: &str) -> ConfigError { parse(text).err().expect(text) } /// The example that the documentation points to, and that operators /// start from: as it is, and with what it shows commented out put in. #[test] fn the_example_configuration_is_in_order() { let example = include_str!("../../../testdata/server.textpb"); let config = parse(example).expect("the example as it is"); assert_eq!(config.peers.len(), 1); assert!(config.peers[0].import.accept_third_party_next_hop); assert!(config.rpki.is_none() && config.kernel.is_none()); // A commented line that is a field, or a block on one line, is // configuration shown for the taking. let shown: String = example .lines() .map(|line| { let (indent, rest) = line.split_at(line.len() - line.trim_start().len()); let field = rest.strip_prefix("# ").filter(|field| { let name_ends = field.find([':', ' ']).unwrap_or(0); let is_name = name_ends > 0 && field[..name_ends] .chars() .all(|c| c.is_ascii_lowercase() || c == '_'); let after = &field[name_ends..]; is_name && (after.starts_with(": ") && !after[2..].contains(' ') || after.starts_with(" {") && after.ends_with('}')) }); format!("{indent}{}\n", field.unwrap_or(rest)) }) .collect(); assert_ne!(shown, example, "nothing was taken for configuration"); let config = parse(&shown).unwrap_or_else(|e| panic!("{e}\n{shown}")); assert!(config.rpki.is_some(), "{shown}"); assert!(config.kernel.is_some(), "{shown}"); assert_eq!(config.peers[0].role, Some(PeerRole::Peer)); assert!(config.peers[0].require_role); assert!(config.peers[0].import.prefixes.is_some()); } const BASE: &str = "router_id: \"192.0.2.1\" local_asn: 65000\n"; #[test] fn originated_routes_are_parsed_with_their_attributes() { let config = parse(&format!( "{BASE}originate {{ prefix: \"192.0.2.0/24\" }} originate {{ prefix: \"2001:db8::/32\" origin: \"incomplete\" as_path_prepend: 2 med: 0 communities: \"65000:100\" communities: \"NO_EXPORT\" large_communities: \"65000:1:2\" }}" )) .unwrap(); assert_eq!(config.originate.len(), 2); let plain = &config.originate[0]; assert_eq!(plain.prefix, IpPrefix::try_from("192.0.2.0/24").unwrap()); assert_eq!(plain.attrs, LocalRouteAttrs::default()); let full = &config.originate[1].attrs; assert_eq!(full.origin, OriginPathAttribute::INCOMPLETE); assert_eq!(full.prepend, 2); assert_eq!( full.med, Some(0), "an explicit MED of 0 is not the same as no MED" ); assert_eq!(full.communities, vec![(65000, 100), (65535, 65281)]); assert_eq!(full.large_communities.len(), 1); } #[test] fn bad_originated_routes_are_rejected_with_the_entry_and_the_reason() { let problem = |body: &str| match rejected(&format!("{BASE}originate {{ {body} }}")) { ConfigError::Originate { problem, .. } => problem, other => panic!("{body}: {other}"), }; assert_eq!( problem("prefix: \"not-a-prefix\""), OriginateProblem::Prefix(PrefixError::Format("not-a-prefix".into())) ); assert_eq!( problem("prefix: \"192.0.2.0/24\" origin: \"static\""), OriginateProblem::Origin("static".into()) ); assert_eq!( problem("prefix: \"192.0.2.0/24\" as_path_prepend: 17"), OriginateProblem::Prepend { max: 16 } ); assert!(matches!( problem("prefix: \"192.0.2.0/24\" communities: \"70000:1\""), OriginateProblem::Community(_) )); assert!(matches!( problem("prefix: \"192.0.2.0/24\" large_communities: \"1:2\""), OriginateProblem::Community(_) )); let duplicate = rejected(&format!( "{BASE}originate {{ prefix: \"192.0.2.0/24\" }} originate {{ prefix: \"192.0.2.0/24\" med: 5 }}" )); assert_eq!( duplicate, ConfigError::Originate { prefix: "192.0.2.0/24".into(), problem: OriginateProblem::Duplicate } ); // The message names the entry, so the operator can find it. assert_eq!( duplicate.to_string(), "originate \"192.0.2.0/24\": listed more than once" ); } #[test] fn a_peer_is_offered_every_address_family_unless_some_are_listed() { let families = |body: &str| { let config = parse(&format!( "{BASE}peers {{ name: \"r1\" endpoint {{ address: \"192.0.2.2\" }} remote_asn: 1 {body} }}" )) .unwrap(); config.peers[0].families.clone() }; assert_eq!(families(""), FamilySet::supported()); assert_eq!( families("address_families: IPV6_UNICAST"), [AfiSafi::IPV6_UNICAST].into_iter().collect() ); assert_eq!( families("address_families: IPV4_UNICAST address_families: IPV6_UNICAST"), FamilySet::supported() ); } #[test] fn add_path_is_off_unless_a_peer_asks_for_it() { let add_path = |body: &str| { let config = parse(&format!( "{BASE}peers {{ name: \"r1\" endpoint {{ address: \"192.0.2.2\" }} remote_asn: 1 {body} }}" )) .unwrap(); config.peers[0].add_path }; assert_eq!(add_path(""), None); assert_eq!(add_path("add_path: ADD_PATH_OFF"), None); assert_eq!( add_path("add_path: ADD_PATH_RECEIVE"), Some(AddPathSendRecv::Receive) ); assert_eq!( add_path("add_path: ADD_PATH_BOTH"), Some(AddPathSendRecv::Both) ); } #[test] fn bad_peers_and_endpoints_are_rejected_with_the_peer_named() { assert_eq!( rejected("router_id: \"nope\" local_asn: 1"), ConfigError::RouterId("nope".into()) ); assert_eq!(rejected("router_id: \"192.0.2.1\""), ConfigError::LocalAsn); let problem = |body: &str| match rejected(&format!("{BASE}peers {{ name: \"r1\" {body} }}")) { ConfigError::Peer { peer, problem } => { assert_eq!(peer, "r1"); problem } other => panic!("{body}: {other}"), }; let endpoint = "endpoint { address: \"192.0.2.2\" }"; assert_eq!(problem("remote_asn: 1"), PeerProblem::MissingEndpoint); assert_eq!(problem(endpoint), PeerProblem::RemoteAsn); assert_eq!( problem(&format!("{endpoint} remote_asn: 1 hold_time_seconds: 2")), PeerProblem::HoldTime ); assert_eq!( problem(&format!( "{endpoint} remote_asn: 1 hold_time_seconds: 70000" )), PeerProblem::HoldTime ); assert_eq!( problem(&format!("{endpoint} remote_asn: 1 local_address: \"::1\"")), PeerProblem::AddressFamilyMismatch ); assert_eq!( problem(&format!( "{endpoint} remote_asn: 1 address_families: ADDRESS_FAMILY_UNSPECIFIED" )), PeerProblem::AddressFamily ); assert_eq!( problem("endpoint { address: \"192.0.2.2\" port: 70000 } remote_asn: 1"), PeerProblem::Endpoint(Box::new(ConfigError::Port(70000))) ); assert_eq!( rejected(&format!("{BASE}http_endpoint {{ address: \"127.0.0.1\" }}")), ConfigError::HttpPort ); } /// Peers are told apart by name where the dialer reports on them and by /// address when they connect to us; two that share either would be /// taken for each other. #[test] fn peers_must_differ_in_name_and_address() { let peer = |name: &str, address: &str| { format!( "peers {{ name: \"{name}\" endpoint {{ address: \"{address}\" }} remote_asn: 1 }}\n" ) }; let problem = |peers: String| match rejected(&format!("{BASE}{peers}")) { ConfigError::Peer { problem, .. } => problem, other => panic!("{other}"), }; assert_eq!( problem(peer("r1", "192.0.2.2") + &peer("r1", "192.0.2.3")), PeerProblem::DuplicateName ); assert_eq!( problem(peer("r1", "192.0.2.2") + &peer("r2", "192.0.2.2")), PeerProblem::DuplicateAddress ); // A link-local address is one peer on each interface it is on. assert_eq!( problem(peer("r1", "fe80::1%eth0") + &peer("r2", "fe80::1%eth0")), PeerProblem::DuplicateAddress ); let config = parse(&format!( "{BASE}{}{}", peer("", "fe80::1%eth0"), peer("", "fe80::1%eth1") )) .unwrap(); assert_eq!( config.peers.iter().map(|p| &p.name).collect::>(), ["fe80::1%eth0", "fe80::1%eth1"] ); // And nothing without one. assert_eq!( problem(peer("r1", "fe80::1")).to_string(), "endpoint is invalid: fe80::1 is a link-local address, which needs the interface it \ is on: fe80::1%eth0, for example" ); // A peer without a name goes by its address, so two of them differ. let config = parse(&format!( "{BASE}{}{}", peer("", "192.0.2.2"), peer("", "192.0.2.3") )) .unwrap(); assert_eq!(config.peers[0].name, "192.0.2.2"); assert_eq!(config.peers[1].name, "192.0.2.3"); assert_eq!( config .peer_at("192.0.2.3".parse::().unwrap().into()) .map(|p| &*p.name), Some("192.0.2.3") ); assert!( config .peer_at("192.0.2.4".parse::().unwrap().into()) .is_none() ); } /// A peer would refuse our OPEN. #[test] fn a_router_id_of_zero_is_rejected() { assert_eq!( rejected("router_id: \"0.0.0.0\" local_asn: 1"), ConfigError::RouterId("0.0.0.0".into()) ); } fn import_of(body: &str) -> Result { parse(&format!( "{BASE}peers {{ name: \"r1\" endpoint {{ address: \"192.0.2.2\" }} remote_asn: 1 {body} }}" )) .map(|config| config.peers[0].import.clone()) } /// A peer with nothing said about its routes still gets the general /// checks; what is said overrides them one by one. #[test] fn an_import_policy_starts_from_the_general_checks() { assert_eq!(import_of("").unwrap(), ImportPolicy::default()); assert_eq!(import_of("import {}").unwrap(), ImportPolicy::default()); let policy = import_of( "import { max_prefixes: 1000 hold_down_seconds: 60 accept_default_route: true accept_any_first_as: true max_ipv4_prefix_length: 32 max_as_path_length: 20 prefixes { prefix: \"193.0.0.0/21\" max_length: 24 } prefixes { prefix: \"2001:67c:2e8::/48\" } origin_asns: 3333 origin_asns: 2121 }", ) .unwrap(); assert_eq!(policy.max_prefixes, Some(1000)); assert_eq!(policy.hold_down, Duration::from_secs(60)); assert!(policy.accept_default_route && policy.accept_any_first_as); assert!(!policy.accept_bogons); assert_eq!((policy.max_ipv4_length, policy.max_ipv6_length), (32, 48)); assert_eq!(policy.max_as_path_length, 20); assert_eq!(policy.origin_asns, Some([3333, 2121].into())); let prefixes = policy.prefixes.unwrap(); let p = |s: &str| IpPrefix::try_from(s).unwrap(); assert!(prefixes.allows(&p("193.0.4.0/24"))); assert!(!prefixes.allows(&p("193.0.4.0/25"))); assert!(prefixes.allows(&p("2001:67c:2e8::/48"))); assert!(!prefixes.allows(&p("2001:67c:2e8:1::/64"))); } #[test] fn a_bad_import_policy_is_rejected_with_the_reason() { let problem = |body: &str| match import_of(&format!("import {{ {body} }}")) { Err(ConfigError::Peer { problem: PeerProblem::Import(problem), .. }) => problem, other => panic!("{body}: {:?}", other.map(|_| ())), }; assert!(matches!( problem("prefixes { prefix: \"193.0.0.0\" }"), ImportProblem::Prefix { .. } )); // Shorter than the prefix itself, and longer than an address. assert_eq!( problem("prefixes { prefix: \"193.0.0.0/21\" max_length: 20 }"), ImportProblem::MaxLength { prefix: "193.0.0.0/21".into(), max_length: 20, longest: 32 } ); assert!(matches!( problem("prefixes { prefix: \"193.0.0.0/21\" max_length: 33 }"), ImportProblem::MaxLength { .. } )); assert_eq!( problem("max_ipv4_prefix_length: 33"), ImportProblem::PrefixLength { field: "max_ipv4_prefix_length", value: 33, longest: 32 } ); assert_eq!( problem("max_as_path_length: 70000"), ImportProblem::PathLength(70000) ); // The message says which peer and which entry. assert_eq!( import_of("import { prefixes { prefix: \"193.0.0.0/21\" max_length: 20 } }") .err() .unwrap() .to_string(), "peer \"r1\": import: prefix \"193.0.0.0/21\": max_length 20 is not between \ its own length and 32" ); } #[test] fn the_kernel_is_left_alone_unless_asked_for() { assert_eq!(parse(BASE).unwrap().kernel, None); assert_eq!( parse(&format!("{BASE}kernel {{}}")).unwrap().kernel, Some(FibConfig { table: 254, metric: 20, nexthop_objects: true, stale_time: Duration::from_secs(120), }) ); assert_eq!( parse(&format!( "{BASE}kernel {{ table: 1000 metric: 150 without_nexthop_objects: true \ stale_seconds: 30 }}" )) .unwrap() .kernel, Some(FibConfig { table: 1000, metric: 150, nexthop_objects: false, stale_time: Duration::from_secs(30), }) ); } #[test] fn route_origin_validation_is_off_unless_caches_are_given() { assert!(parse(BASE).unwrap().rpki.is_none()); let config = parse(&format!( "{BASE}rpki {{ caches {{ address: \"192.0.2.10\" port: 3323 }} caches {{ address: \"2001:db8::10\" port: 8282 }} expire_seconds: 3600 }}" )) .unwrap(); let rpki = config.rpki.unwrap(); assert_eq!( rpki.caches, [ "192.0.2.10:3323".parse().unwrap(), "[2001:db8::10]:8282".parse().unwrap() ] ); // What is not set is left to the cache. assert_eq!(rpki.refresh, None); assert_eq!(rpki.expire, Some(Duration::from_secs(3600))); assert_eq!( rejected(&format!("{BASE}rpki {{}}")), ConfigError::RpkiNoCaches ); // No port is assumed for a cache. assert_eq!( rejected(&format!( "{BASE}rpki {{ caches {{ address: \"192.0.2.10\" }} }}" )), ConfigError::RpkiCachePort("192.0.2.10".into()) ); // A peer can be exempt. // A peer's paths are verified only if it is said what the peer is. let role_of = |body: &str| { parse(&format!( "{BASE}peers {{ endpoint {{ address: \"192.0.2.2\" }} remote_asn: 1 {body} }}" )) .map(|config| config.peers[0].role) }; assert_eq!(role_of("").unwrap(), None); assert_eq!(role_of("role: CUSTOMER").unwrap(), Some(PeerRole::Customer)); assert_eq!( role_of("role: ROUTE_SERVER").unwrap(), Some(PeerRole::RouteServer) ); // Roles are between networks, and a peer can be required to say // its own only where we know what to expect. let problem = |body: &str| match role_of(body).err().unwrap() { ConfigError::Peer { problem, .. } => problem, other => panic!("{other:?}"), }; assert_eq!( problem("role: PEER remote_asn: 65000"), PeerProblem::RoleInOwnAs ); assert_eq!( problem("require_role: true"), PeerProblem::RequireRoleWithoutRole ); let config = parse(&format!( "{BASE}peers {{ endpoint {{ address: \"192.0.2.2\" }} remote_asn: 1 \ role: PEER require_role: true }}" )) .unwrap(); assert!(config.peers[0].require_role); assert!(!import_of("").unwrap().accept_aspa_invalid); assert!( import_of("import { accept_aspa_invalid: true }") .unwrap() .accept_aspa_invalid ); assert!(!import_of("").unwrap().accept_rpki_invalid); assert!( import_of("import { accept_rpki_invalid: true }") .unwrap() .accept_rpki_invalid ); } }