Files
ferrix/crates/server/src/config.rs
T
Rayhaan JaufeerallyandClaude Fable 5.1 277b973f1a Status page: what moved, what is missing, and a dark room
The attention strip's names lead to the peer's row, which lights up for a
moment. The IPv4 and IPv6 sessions of one neighbour (NAME-v4, NAME-v6) are
listed together, joined down the side, the name said once in full. A route
count that moved by more than 2% between refreshes carries ▲ or ▼ for two
minutes; a row whose state or count changed while the tab was in the
background carries ● until it is clicked. A compact mode drops the next
hop and session columns, and is remembered by the browser. Colours are
variables, with a second set for prefers-color-scheme: dark.

A new per-peer `expected_routes` says about how many routes a peer sends
when all is well. The daemon only passes it to the API; the page lists an
established peer with under half of it as needing attention, a minute
after the session came up, with the expected count beside the actual one.
An established session with next to no routes looked, until now, exactly
like one that was fine.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-07 23:16:01 +02:00

1532 lines
57 KiB
Rust

use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, 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::SendReceive;
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, ExportProblem, ImportProblem, OriginateProblem, PeerProblem};
use crate::family::FamilySet;
use crate::marks::{Control, Marks, Patterns, Scrub};
use crate::policy::{
ExportLimits, ExportPolicy, ImportPolicy, ImportRule, PeerRole, PrefixSet, ReflectorIds,
};
use crate::proto::config::{self as proto, BgpConfig, OriginatedRoute, Peer};
use crate::restart::GracefulRestartConfig;
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,
/// The cluster this router reflects routes for (RFC 4456), where it
/// has route reflector clients.
pub cluster_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<SocketAddr>,
pub peers: Vec<PeerConfig>,
/// 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<LocalRoute>,
/// Address for the HTTP API; `None` disables it.
pub http_endpoint: Option<SocketAddr>,
/// Where to install the best routes in the kernel, if at all.
pub kernel: Option<FibConfig>,
/// Where route origin validation gets its data; `None` for no
/// validation.
pub rpki: Option<RpkiConfig>,
/// What a peer is told that has no `export` of its own.
pub export: Arc<ExportPolicy>,
/// Limits on what goes to a peer that has none of its own, if it is
/// one they are for ([`ExportLimits::apply_to`]).
pub export_limits: Option<Arc<ExportLimits>>,
/// The communities by which routes say who is told of them, if they
/// are honoured.
pub control: Option<Control>,
/// Names for communities, as (community or pattern, name), for the
/// status page.
pub community_names: Vec<(String, String)>,
/// Graceful restart for peers that have no setting of their own; and
/// the restart time this daemon gives, which decides how long it waits
/// after it starts for its peers to send their routes.
pub graceful_restart: GracefulRestartConfig,
}
#[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<IpAddr>,
/// 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<SendReceive>,
/// Which of the peer's routes are taken.
pub import: ImportPolicy,
/// What the peer is to us, if that was said.
pub role: Option<PeerRole>,
/// Refuse the peer if it does not say what it is itself (RFC 9234
/// §4.2, "strict mode").
pub require_role: bool,
/// What the peer is told, and how.
pub export: Arc<ExportPolicy>,
/// Limits on what the peer is told, if it is one they are for
/// ([`ExportLimits::apply_to`]): its own, or the top-level ones.
pub export_limits: Option<Arc<ExportLimits>>,
/// Graceful restart with the peer (RFC 4724).
pub graceful_restart: GracefulRestartConfig,
/// The groups the session is in, for control communities to name it
/// by.
pub groups: Vec<u32>,
/// Pass on to this peer, in our own AS, the routes learned from our
/// other routers, and its routes to them (RFC 4456).
pub reflector_client: bool,
/// Do not dial the peer: wait for it to connect.
pub passive: bool,
/// About how many routes the peer sends when all is well, for the
/// status page to compare with; nothing here acts on it.
pub expected_routes: Option<u32>,
}
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<Self, ConfigError> {
let router_id = parse_identifier(&proto.router_id)
.ok_or_else(|| ConfigError::RouterId(proto.router_id.clone()))?;
if proto.local_asn == 0 {
return Err(ConfigError::LocalAsn);
}
let cluster_id = if proto.cluster_id.is_empty() {
router_id
} else {
parse_identifier(&proto.cluster_id)
.ok_or_else(|| ConfigError::ClusterId(proto.cluster_id.clone()))?
};
let listen_endpoints = parse_listen_endpoints(&proto.listen_endpoints)?;
let defaults = PeerDefaults {
export: Arc::new(match proto.export.as_ref() {
Some(export) => parse_export(export)?,
None => ExportPolicy::default(),
}),
export_limits: match proto.export_limits.as_ref() {
Some(limits) => parse_export_limits(limits)
.map_err(ConfigError::ExportLimits)?
.map(Arc::new),
None => None,
},
graceful_restart: match proto.graceful_restart.as_ref() {
Some(own) => GracefulRestartConfig::default().overridden_by(own)?,
None => GracefulRestartConfig::default(),
},
};
let mut peers = parse_peers(proto.peers, proto.local_asn, &defaults)?;
check_roles(
&peers,
&defaults.export,
proto.accept_unconfigured_peers,
proto.local_asn,
)?;
let originate = parse_originate(&proto.originate)?;
let community_names = parse_community_names(&proto.community_names)?;
let ours = Ours {
local_asn: proto.local_asn,
router_id,
cluster_id,
control_communities: proto.control_communities,
};
ours.tell_peers(&mut peers, &originate);
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,
cluster_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,
export: defaults.export,
export_limits: defaults.export_limits,
control: proto.control_communities.then_some(Control {
local_asn: proto.local_asn,
}),
community_names,
graceful_restart: defaults.graceful_restart,
})
}
}
/// The names for communities, each checked to name a community or a
/// pattern of them.
fn parse_community_names(
names: &[proto::CommunityName],
) -> Result<Vec<(String, String)>, ConfigError> {
names
.iter()
.map(|entry| {
Patterns::parse(std::slice::from_ref(&entry.community)).map_err(|problem| {
ConfigError::CommunityName {
community: entry.community.clone(),
problem,
}
})?;
if entry.name.trim().is_empty() {
return Err(ConfigError::CommunityNameEmpty {
community: entry.community.clone(),
});
}
Ok((entry.community.clone(), entry.name.clone()))
})
.collect()
}
/// Where BGP connections are taken: as configured, or on every address.
fn parse_listen_endpoints(
endpoints: &[proto::TcpEndpoint],
) -> Result<Vec<SocketAddr>, ConfigError> {
if endpoints.is_empty() {
return Ok(vec![
SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), DEFAULT_BGP_PORT),
SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), DEFAULT_BGP_PORT),
]);
}
endpoints
.iter()
.map(|ep| parse_endpoint(&ep.address, ep.port))
.collect()
}
/// What a peer gets that has no setting of its own.
struct PeerDefaults {
export: Arc<ExportPolicy>,
export_limits: Option<Arc<ExportLimits>>,
graceful_restart: GracefulRestartConfig,
}
/// The peers, each in order on its own and told from the others: by name,
/// which the dialer reports, and by address, which a peer that connects
/// to us is known by.
fn parse_peers(
peers: Vec<Peer>,
local_asn: u32,
defaults: &PeerDefaults,
) -> Result<Vec<PeerConfig>, ConfigError> {
let mut parsed: Vec<PeerConfig> = Vec::with_capacity(peers.len());
for peer in peers {
let name = peer.name.clone();
let rejected = |problem| ConfigError::Peer {
peer: name.clone(),
problem,
};
let peer = parse_peer(peer, defaults).map_err(rejected)?;
// RFC 9234 is about what networks are to each other.
if peer.role.is_some() && peer.remote_asn == local_asn {
return Err(rejected(PeerProblem::RoleInOwnAs));
}
if peer.reflector_client && peer.remote_asn != local_asn {
return Err(rejected(PeerProblem::ReflectorClientOutsideAs));
}
if parsed.iter().any(|earlier| earlier.name == peer.name) {
return Err(rejected(PeerProblem::DuplicateName));
}
if parsed.iter().any(|earlier| {
earlier.endpoint.ip == peer.endpoint.ip
&& earlier.endpoint.interface == peer.endpoint.interface
}) {
return Err(rejected(PeerProblem::DuplicateAddress));
}
parsed.push(peer);
}
Ok(parsed)
}
/// Where routes learned from one peer go on to others, which go where is
/// decided by the peers' roles: a peer in another AS without one would
/// have its routes go to everyone, and be told of everything. Peers that
/// are not configured have no role either.
fn check_roles(
peers: &[PeerConfig],
export: &ExportPolicy,
accept_unconfigured_peers: bool,
local_asn: u32,
) -> Result<(), ConfigError> {
let learned_go_on =
export.learned_routes || peers.iter().any(|peer| peer.export.learned_routes);
if learned_go_on
&& let Some(peer) = peers
.iter()
.find(|peer| peer.remote_asn != local_asn && peer.role.is_none())
{
return Err(ConfigError::Peer {
peer: peer.name.clone(),
problem: PeerProblem::NoRole,
});
}
if accept_unconfigured_peers && export.learned_routes {
return Err(ConfigError::UnconfiguredPeersWithoutRole);
}
Ok(())
}
/// The routes this router originates, each prefix once.
fn parse_originate(entries: &[proto::OriginatedRoute]) -> Result<Vec<LocalRoute>, ConfigError> {
let mut originate: Vec<LocalRoute> = Vec::with_capacity(entries.len());
for entry in entries {
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);
}
Ok(originate)
}
/// What is this router's own, which every peer's import policy has to
/// know of.
struct Ours {
local_asn: u32,
router_id: Ipv4Addr,
cluster_id: Ipv4Addr,
control_communities: bool,
}
impl Ours {
/// Gives each peer's import policy what of ours it is to look out
/// for: our routers, what we originate and how we would reflect; the
/// others, our address space and our communities, which are not
/// theirs to send unless they are to steer their own routes.
fn tell_peers(&self, peers: &mut [PeerConfig], originate: &[LocalRoute]) {
let mut own_prefixes = PrefixSet::default();
let mut originated = PrefixSet::default();
for route in originate {
own_prefixes.allow(route.prefix, route.prefix.address_family().bits());
originated.allow(route.prefix, route.prefix.length());
}
let own_prefixes = Arc::new(own_prefixes);
let originated = Arc::new(originated);
for peer in peers {
if peer.remote_asn == self.local_asn {
peer.import.originated = Arc::clone(&originated);
peer.import.reflection = Some(ReflectorIds {
router_id: self.router_id,
cluster_id: self.cluster_id,
});
} else {
peer.import.own_prefixes = Arc::clone(&own_prefixes);
if !peer.import.accept_control_communities {
peer.import.scrub = Some(Scrub {
local_asn: self.local_asn,
zero: self.control_communities,
});
}
}
}
}
}
/// A BGP identifier (RFC 4271 §4.2) as the configuration writes one: a
/// dotted quad other than 0.0.0.0, which peers refuse, as we do theirs.
fn parse_identifier(text: &str) -> Option<Ipv4Addr> {
Ipv4Addr::from_str(text)
.ok()
.filter(|id| !id.is_unspecified())
}
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())
}),
}
}
/// How many more times our AS goes on a path, if `prepend` is a number of
/// times we allow.
fn parse_prepend(prepend: u32) -> Option<u8> {
u8::try_from(prepend)
.ok()
.filter(|prepend| u32::from(*prepend) <= MAX_AS_PATH_PREPEND)
}
fn parse_export(export: &proto::ExportPolicy) -> Result<ExportPolicy, ExportProblem> {
Ok(ExportPolicy {
learned_routes: export.learned_routes,
marks: Marks::parse(
&export.remove_communities,
&export.add_communities,
&export.add_large_communities,
)?,
prepend: parse_prepend(export.as_path_prepend).ok_or(ExportProblem::Prepend {
value: export.as_path_prepend,
max: MAX_AS_PATH_PREPEND,
})?,
})
}
/// Limits on what is announced; `None` for a block that says nothing,
/// which lifts them.
fn parse_export_limits(
limits: &proto::ExportLimits,
) -> Result<Option<ExportLimits>, ImportProblem> {
let limits = ExportLimits {
prefixes: if limits.prefixes.is_empty() {
None
} else {
Some(parse_prefixes(&limits.prefixes)?)
},
max_prefixes: Some(limits.max_prefixes).filter(|limit| *limit > 0),
};
Ok((limits != ExportLimits::default()).then_some(limits))
}
fn parse_peer(peer: Peer, defaults: &PeerDefaults) -> Result<PeerConfig, PeerProblem> {
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::<Result<_, _>>()?
};
let add_path = match peer.add_path.enum_value() {
Ok(proto::AddPath::ADD_PATH_OFF) => None,
Ok(proto::AddPath::ADD_PATH_RECEIVE) => Some(SendReceive::Receive),
Ok(proto::AddPath::ADD_PATH_SEND) => Some(SendReceive::Send),
Ok(proto::AddPath::ADD_PATH_BOTH) => Some(SendReceive::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);
}
if peer.groups.contains(&0) {
return Err(PeerProblem::Group);
}
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,
export: match peer.export.as_ref() {
Some(own) => Arc::new(parse_export(own)?),
None => Arc::clone(&defaults.export),
},
export_limits: match peer.export_limits.as_ref() {
Some(own) => parse_export_limits(own)
.map_err(PeerProblem::ExportLimits)?
.map(Arc::new),
None => defaults.export_limits.clone(),
},
graceful_restart: match peer.graceful_restart.as_ref() {
Some(own) => defaults.graceful_restart.overridden_by(own)?,
None => defaults.graceful_restart,
},
groups: peer.groups,
reflector_client: peer.route_reflector_client,
passive: peer.passive,
expected_routes: Some(peer.expected_routes).filter(|n| *n > 0),
})
}
fn parse_rpki(rpki: &proto::Rpki) -> Result<RpkiConfig, ConfigError> {
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::<Result<_, _>>()?;
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<ImportPolicy, ImportProblem> {
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 prefixes = parse_prefixes(&import.prefixes)?;
let rules = import
.rules
.iter()
.enumerate()
.map(|(at, rule)| {
parse_rule(rule).map_err(|problem| ImportProblem::Rule {
number: at + 1,
problem: Box::new(problem),
})
})
.collect::<Result<Vec<_>, _>>()?;
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(),
originated: Arc::default(),
accept_control_communities: import.accept_control_communities,
scrub: None,
marks: Marks::parse(
&import.remove_communities,
&import.add_communities,
&import.add_large_communities,
)?,
local_pref: import.local_pref,
rules,
reflection: None,
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<u32>| !asns.is_empty()),
})
}
/// A list of prefixes, each with how much longer a prefix inside it may
/// be.
fn parse_prefixes(ranges: &[proto::PrefixRange]) -> Result<PrefixSet, ImportProblem> {
let mut prefixes = PrefixSet::default();
for range in ranges {
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(prefixes)
}
fn parse_rule(rule: &proto::ImportRule) -> Result<ImportRule, ImportProblem> {
Ok(ImportRule {
communities: Patterns::parse(&rule.match_communities)?,
prefixes: if rule.match_prefixes.is_empty() {
None
} else {
Some(parse_prefixes(&rule.match_prefixes)?)
},
local_pref: rule.local_pref,
marks: Marks::parse(&[], &rule.add_communities, &rule.add_large_communities)?,
})
}
fn parse_port(port: u32) -> Result<u16, ConfigError> {
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<SocketAddr, ConfigError> {
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<LocalRoute, OriginateProblem> {
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 = parse_prepend(route.as_path_prepend).ok_or(OriginateProblem::Prepend {
value: route.as_path_prepend,
max: MAX_AS_PATH_PREPEND,
})?;
let communities = route
.communities
.iter()
.map(|c| community::parse_standard(c))
.collect::<Result<_, _>>()?;
let large_communities = route
.large_communities
.iter()
.map(|c| community::parse_large(c))
.collect::<Result<_, _>>()?;
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<Config, ConfigError> {
let proto = protobuf::text_format::parse_from_str::<BgpConfig>(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";
/// Our communities are taken off what other networks send, unless a
/// peer is to steer its routes with them; not off what our own
/// routers send. `0:n` too where control communities make it ours.
#[test]
fn our_communities_are_taken_only_from_whom_they_are_ours() {
let config = parse(&format!(
"{BASE}control_communities: true
peers {{ name: \"peer\" endpoint {{ address: \"192.0.2.2\" }} remote_asn: 64496 }}
peers {{ name: \"customer\" endpoint {{ address: \"192.0.2.3\" }} remote_asn: 64497
import {{ accept_control_communities: true }} }}
peers {{ name: \"internal\" endpoint {{ address: \"192.0.2.4\" }} remote_asn: 65000 }}"
))
.unwrap();
let scrubs: Vec<_> = config.peers.iter().map(|peer| peer.import.scrub).collect();
assert_eq!(
scrubs,
[
Some(Scrub {
local_asn: 65000,
zero: true
}),
None,
None
]
);
}
#[test]
fn import_rules_are_parsed_in_order() {
let policy = import_of(
"import {
rules { match_communities: \"65000:101:2\" local_pref: 130 }
rules { match_prefixes { prefix: \"203.0.113.0/24\" }
add_large_communities: \"65000:31:10\" }
}",
)
.unwrap();
assert_eq!(policy.rules.len(), 2);
assert_eq!(policy.rules[0].local_pref, Some(130));
assert!(policy.rules[0].prefixes.is_none());
let prefix = IpPrefix::try_from("203.0.113.0/24").unwrap();
assert!(policy.rules[1].holds_for_prefix(&prefix));
assert!(!policy.rules[1].holds_for_prefix(&IpPrefix::try_from("198.51.100.0/24").unwrap()));
assert!(policy.rules[1].holds_for(&[]), "no communities asked for");
// A rule that is wrong is named by its place in the list.
let wrong =
import_of("import { rules {} rules { match_communities: \"65000\" } }").unwrap_err();
assert!(wrong.to_string().contains("rule 2: "), "{wrong}");
}
/// A route reflector's clients are routers of our own AS; the cluster
/// is the router's own unless given, and what comes from our routers is
/// checked against both.
#[test]
fn route_reflection_is_for_our_own_routers() {
let config = parse(&format!(
"{BASE}peers {{ name: \"client\" endpoint {{ address: \"192.0.2.2\" }} remote_asn: 65000
route_reflector_client: true }}
peers {{ name: \"external\" endpoint {{ address: \"192.0.2.3\" }} remote_asn: 64496 }}"
))
.unwrap();
assert_eq!(config.cluster_id, config.router_id);
assert!(config.peers[0].reflector_client);
assert_eq!(
config.peers[0].import.reflection,
Some(ReflectorIds {
router_id: config.router_id,
cluster_id: config.router_id
})
);
assert_eq!(config.peers[1].import.reflection, None);
let given = parse(&format!("{BASE}cluster_id: \"10.0.0.9\"")).unwrap();
assert_eq!(given.cluster_id, Ipv4Addr::new(10, 0, 0, 9));
assert_eq!(
rejected(&format!("{BASE}cluster_id: \"0.0.0.0\"")),
ConfigError::ClusterId("0.0.0.0".into())
);
let outside = rejected(&format!(
"{BASE}peers {{ name: \"r1\" endpoint {{ address: \"192.0.2.2\" }} remote_asn: 64496
route_reflector_client: true }}"
));
assert!(
matches!(
&outside,
ConfigError::Peer {
problem: PeerProblem::ReflectorClientOutsideAs,
..
}
),
"{outside:?}"
);
}
#[test]
fn sessions_are_put_in_groups() {
let config = parse(&format!(
"{BASE}peers {{ name: \"r1\" endpoint {{ address: \"192.0.2.2\" }} remote_asn: 1
groups: 2 groups: 10 }}"
))
.unwrap();
assert_eq!(config.peers[0].groups, [2, 10]);
let zero = rejected(&format!(
"{BASE}peers {{ name: \"r1\" endpoint {{ address: \"192.0.2.2\" }} remote_asn: 1 groups: 0 }}"
));
assert!(
matches!(
&zero,
ConfigError::Peer {
problem: PeerProblem::Group,
..
}
),
"{zero:?}"
);
}
/// What we originate is refused from other networks, with what is
/// inside it; from a router of our own it is ignored, and only the
/// prefix itself.
#[test]
fn originated_prefixes_are_ours_or_shared_by_where_a_route_comes_from() {
let config = parse(&format!(
"{BASE}originate {{ prefix: \"203.0.113.0/24\" }}
peers {{ name: \"external\" endpoint {{ address: \"192.0.2.2\" }} remote_asn: 64496 }}
peers {{ name: \"internal\" endpoint {{ address: \"192.0.2.3\" }} remote_asn: 65000 }}"
))
.unwrap();
let ours = IpPrefix::try_from("203.0.113.0/24").unwrap();
let inside = IpPrefix::try_from("203.0.113.0/25").unwrap();
let [external, internal] = &config.peers[..] else {
panic!("two peers");
};
assert!(external.import.own_prefixes.allows(&ours));
assert!(external.import.own_prefixes.allows(&inside));
assert!(external.import.originated.is_empty());
assert!(internal.import.own_prefixes.is_empty());
assert!(internal.import.originated.allows(&ours));
assert!(!internal.import.originated.allows(&inside));
}
#[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 { value: 17, 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(SendReceive::Receive)
);
assert_eq!(add_path("add_path: ADD_PATH_BOTH"), Some(SendReceive::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::<Vec<_>>(),
["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::<IpAddr>().unwrap().into())
.map(|p| &*p.name),
Some("192.0.2.3")
);
assert!(
config
.peer_at("192.0.2.4".parse::<IpAddr>().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<ImportPolicy, ConfigError> {
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() {
// From another network, without our communities.
let general = ImportPolicy {
scrub: Some(Scrub {
local_asn: 65000,
zero: false,
}),
..ImportPolicy::default()
};
assert_eq!(import_of("").unwrap(), general);
assert_eq!(import_of("import {}").unwrap(), general);
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'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
);
}
/// Where learned routes are passed on, every peer in another AS has
/// to have a role: without one, its routes would go to everyone and it
/// would be told of everything. Where they are not, or for our own
/// routers, there is nothing for a role to decide.
#[test]
fn a_peer_in_another_as_needs_a_role_where_learned_routes_go_on() {
let peer = |body: &str| {
format!("peers {{ name: \"p\" endpoint {{ address: \"192.0.2.2\" }} {body} }}")
};
let learned = "export { learned_routes: true }";
assert_eq!(
rejected(&format!("{BASE}{learned}{}", peer("remote_asn: 64496"))),
ConfigError::Peer {
peer: "p".into(),
problem: PeerProblem::NoRole
}
);
// The peer's own export passes them on; or another peer's does.
assert!(matches!(
rejected(&format!(
"{BASE}{}",
peer(&format!("remote_asn: 64496 {learned}"))
)),
ConfigError::Peer {
problem: PeerProblem::NoRole,
..
}
));
assert!(matches!(
rejected(&format!(
"{BASE}{}
peers {{ name: \"c\" endpoint {{ address: \"192.0.2.3\" }} remote_asn: 64497
role: CUSTOMER {learned} }}",
peer("remote_asn: 64496")
)),
ConfigError::Peer {
problem: PeerProblem::NoRole,
..
}
));
parse(&format!(
"{BASE}{learned}{}",
peer("remote_asn: 64496 role: PEER")
))
.unwrap();
parse(&format!("{BASE}{learned}{}", peer("remote_asn: 65000"))).unwrap();
parse(&format!("{BASE}{}", peer("remote_asn: 64496"))).unwrap();
// Peers that are not configured have no role either.
assert_eq!(
rejected(&format!("{BASE}{learned} accept_unconfigured_peers: true")),
ConfigError::UnconfiguredPeersWithoutRole
);
parse(&format!("{BASE}accept_unconfigured_peers: true")).unwrap();
}
/// The export limits at the top hold for every peer that has none of
/// its own; a peer's own take their place, and an empty one lifts
/// them. Which peers they apply to is the role's to say, when a
/// session comes up.
#[test]
fn export_limits_are_the_top_levels_or_a_peers_own() {
let config = parse(&format!(
"{BASE}export_limits {{
prefixes {{ prefix: \"192.0.2.0/24\" }}
prefixes {{ prefix: \"2001:db8::/32\" max_length: 48 }}
max_prefixes: 10
}}
peers {{ name: \"a\" endpoint {{ address: \"192.0.2.2\" }} remote_asn: 64496
role: PEER }}
peers {{ name: \"b\" endpoint {{ address: \"192.0.2.3\" }} remote_asn: 64497
role: PROVIDER export_limits {{ max_prefixes: 50 }} }}
peers {{ name: \"c\" endpoint {{ address: \"192.0.2.4\" }} remote_asn: 64498
role: PEER export_limits {{ }} }}"
))
.unwrap();
let prefix = |text: &str| IpPrefix::try_from(text).unwrap();
let top = config.export_limits.as_deref().expect("top-level limits");
assert_eq!(top.max_prefixes, Some(10));
assert!(top.allows(&prefix("192.0.2.0/24")));
assert!(!top.allows(&prefix("192.0.2.0/25")));
assert!(top.allows(&prefix("2001:db8:1::/48")));
assert!(!top.allows(&prefix("2001:db8:1::/49")));
assert!(!top.allows(&prefix("198.51.100.0/24")));
assert_eq!(config.peers[0].export_limits.as_deref(), Some(top));
let own = config.peers[1].export_limits.as_deref().expect("its own");
assert_eq!(own.max_prefixes, Some(50));
assert!(own.allows(&prefix("198.51.100.0/24")));
assert_eq!(config.peers[2].export_limits, None);
assert!(matches!(
rejected(&format!(
"{BASE}export_limits {{ prefixes {{ prefix: \"192.0.2.0/33\" }} }}"
)),
ConfigError::ExportLimits(ImportProblem::Prefix { .. })
));
assert!(matches!(
rejected(&format!(
"{BASE}peers {{ name: \"a\" endpoint {{ address: \"192.0.2.2\" }} remote_asn: 1
export_limits {{ prefixes {{ prefix: \"192.0.2.0/24\" max_length: 16 }} }} }}"
)),
ConfigError::Peer {
problem: PeerProblem::ExportLimits(ImportProblem::MaxLength { .. }),
..
}
));
}
/// What a peer is expected to send is kept for the status page; zero,
/// the default, is nothing to compare with.
#[test]
fn expected_routes_are_kept_when_given() {
let config = parse(&format!(
"{BASE}peers {{ name: \"a\" endpoint {{ address: \"192.0.2.2\" }} remote_asn: 1
expected_routes: 900000 }}
peers {{ name: \"b\" endpoint {{ address: \"192.0.2.3\" }} remote_asn: 2 }}"
))
.unwrap();
let expected: Vec<Option<u32>> = config
.peers
.iter()
.map(|peer| peer.expected_routes)
.collect();
assert_eq!(expected, [Some(900_000), None]);
}
/// A peer is dialled unless it is passive.
#[test]
fn a_peer_can_be_left_to_connect() {
let config = parse(&format!(
"{BASE}peers {{ name: \"a\" endpoint {{ address: \"192.0.2.2\" }} remote_asn: 1 }}
peers {{ name: \"b\" endpoint {{ address: \"192.0.2.3\" }} remote_asn: 2
passive: true }}"
))
.unwrap();
let passive: Vec<bool> = config.peers.iter().map(|peer| peer.passive).collect();
assert_eq!(passive, [false, true]);
}
}