Files
ferrix/crates/server/src/error.rs
T
Rayhaan JaufeerallyandClaude Fable 5.1 9367c38907 Remove policy scripts
Rhai made a decision about a route cost one to three microseconds,
some two hundred times what the same costs in Rust. That was bearable
where a function is asked once for an UPDATE, and is not where every
route is weighed against every peer, as it is when routes are passed
on. What is wanted of a policy (mark routes by where they came from,
mark what is announced, let communities say who is told what) needs no
language; it is to be configuration, done natively.

Kept from that work: route selection goes by a LOCAL_PREF that policy
set on a route from an external peer, and the lab's configuration can
be added to by a phase.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-02 22:47:57 +02:00

323 lines
12 KiB
Rust

//! Error types of the server crate.
use std::io;
use std::net::SocketAddr;
use crate::address::EndpointProblem;
use std::time::Duration;
use bgp_packet::errors::{BgpError, CeaseSubcode, DecodeError, FsmSubcode, OpenMsgSubcode};
use bgp_packet::message::NotificationMessage;
use bgp_packet::open::BgpRole;
use bgp_primitives::parser::EncodeError;
use bgp_rib::RibStopped;
use bgp_rib::table::TooManyPeers;
use thiserror::Error;
use crate::family::FamilySet;
use crate::fsm::State;
use crate::policy::PeerRole;
/// Says what is wrong between what a peer is configured as and what it
/// says it is.
fn role_mismatch(configured: Option<PeerRole>, theirs: Option<BgpRole>) -> String {
let configured = configured.map_or("no role is configured".to_string(), |role| {
format!("the peer is configured as our {}", role.as_str())
});
match theirs {
Some(theirs) => format!("role mismatch: {configured} and says it is {theirs:?}"),
None => format!(
"role mismatch: {configured} and does not say what it is, or says more than one thing"
),
}
}
/// Why a session ended abnormally.
#[derive(Debug, Error)]
pub enum SessionError {
#[error("connection failed: {0}")]
Io(#[from] io::Error),
#[error("received a message that could not be decoded: {0}")]
Decode(#[from] DecodeError),
#[error("could not encode a message: {0}")]
Encode(#[from] EncodeError),
/// The peer sent nothing for as long as the pre-negotiation hold time.
#[error("timed out waiting for a message in the {0} state")]
Timeout(State),
#[error("hold timer expired")]
HoldTimerExpired,
#[error("received {received} in the {state} state")]
UnexpectedMessage {
state: State,
received: &'static str,
},
/// The peer ended the session. Nothing is sent in return.
#[error("peer sent NOTIFICATION: {0}")]
NotifiedByPeer(BgpError),
#[error("refused the peer's OPEN: {0}")]
OpenRefused(BgpError),
#[error("peer announced AS{actual} but AS{expected} was configured")]
WrongPeerAs { expected: u32, actual: u32 },
/// RFC 9234 §4.2. `theirs` is `None` if the peer did not say, or said
/// more than one thing.
#[error("{}", role_mismatch(*.configured, *.theirs))]
RoleMismatch {
configured: Option<PeerRole>,
theirs: Option<BgpRole>,
},
#[error("closed by connection collision resolution")]
Collision,
/// The routing table has no room for another peer.
#[error(transparent)]
TableFull(#[from] TooManyPeers),
/// A bug: the session's own code panicked. The peer is told the session
/// is being reset, and the session is cleaned up like any other.
#[error("the session panicked: {0}")]
Panicked(String),
/// The peer offers none of the address families configured for it, so
/// the session could carry nothing.
#[error("no address family in common: we offer {ours}, the peer offers {theirs}")]
NoCommonAddressFamily { ours: FamilySet, theirs: FamilySet },
/// The peer has more routes than it is allowed (RFC 4486). It is kept
/// down for a while after.
#[error("the peer went over its limit of {limit} routes")]
PrefixLimitExceeded { limit: u32 },
/// The peer took nothing we sent for this long (RFC 9687): it has
/// stopped reading, and there is no telling it so.
#[error("the peer accepted no data for {0:?}")]
SendHoldTimerExpired(Duration),
}
impl SessionError {
/// The NOTIFICATION that tells the peer about this error (RFC 4271 §6),
/// or `None` when there is nothing to say or no way to say it.
pub fn notification(&self) -> Option<NotificationMessage> {
let error = match self {
Self::Io(_) | Self::SendHoldTimerExpired(_) | Self::NotifiedByPeer(_) => return None,
// Something of ours could not be written out, which is a bug
// here: as for a panic, the session is reset.
Self::Encode(_) => BgpError::Cease(CeaseSubcode::AdminReset),
Self::Decode(e) => {
return Some(NotificationMessage {
error: e.notification.clone(),
data: e.data.clone(),
});
}
Self::Timeout(_) | Self::HoldTimerExpired => BgpError::HoldTimer,
Self::UnexpectedMessage { state, .. } => BgpError::FsmError(match state {
State::OpenSent => FsmSubcode::UnexpectedOpenSent,
State::OpenConfirm => FsmSubcode::UnexpectedOpenConfirm,
// A closed machine handles no messages; Established is the
// only other state one can arrive in.
State::Established | State::Closed => FsmSubcode::UnexpectedEstablished,
}),
Self::OpenRefused(error) => error.clone(),
Self::WrongPeerAs { .. } => BgpError::OpenMsg(OpenMsgSubcode::BadPeerAs),
Self::RoleMismatch { .. } => BgpError::OpenMsg(OpenMsgSubcode::RoleMismatch),
Self::Collision => BgpError::Cease(CeaseSubcode::ConnCollisionResolution),
Self::TableFull(_) => BgpError::Cease(CeaseSubcode::OutOfResources),
Self::PrefixLimitExceeded { .. } => BgpError::Cease(CeaseSubcode::MaxPrefixes),
// The session will be set up again; nothing is wrong at the
// peer's end.
Self::Panicked(_) => BgpError::Cease(CeaseSubcode::AdminReset),
// RFC 5492 §5: the Data lists the capabilities we need and the
// peer did not offer.
Self::NoCommonAddressFamily { ours, .. } => {
return Some(NotificationMessage {
error: BgpError::OpenMsg(OpenMsgSubcode::UnsupportedCapability),
data: ours.capabilities_wire(),
});
}
};
Some(NotificationMessage {
error,
data: vec![],
})
}
}
/// Why the server could not start, or could not go on.
#[derive(Debug, Error)]
pub enum ServerError {
#[error("failed to bind listener on {addr}: {source}")]
Bind {
addr: SocketAddr,
#[source]
source: io::Error,
},
#[error("listener failed: {0}")]
Io(#[from] io::Error),
/// A RIB's actor died. The server cannot work without its tables.
#[error(transparent)]
RibStopped(#[from] RibStopped),
/// A task the server depends on ended, which none of them do unless
/// they panic.
#[error("the {task} task stopped unexpectedly: {reason}")]
TaskStopped { task: &'static str, reason: String },
/// Routes were to be installed in the kernel, and cannot be.
#[error(transparent)]
Kernel(#[from] bgp_fib::FibError),
}
/// Why a configuration was rejected.
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum ConfigError {
#[error("invalid router_id {0:?}: must be a dotted-quad IPv4 address other than 0.0.0.0")]
RouterId(String),
#[error("local_asn must be non-zero")]
LocalAsn,
#[error("invalid IP address {0:?}")]
Address(String),
#[error(transparent)]
Endpoint(#[from] EndpointProblem),
#[error("port {0} is out of range")]
Port(u32),
#[error("http_endpoint.port is required")]
HttpPort,
#[error("rpki: no caches are listed")]
RpkiNoCaches,
#[error("rpki: the port of cache {0:?} is required")]
RpkiCachePort(String),
#[error("peer {peer:?}: {problem}")]
Peer { peer: String, problem: PeerProblem },
#[error("originate {prefix:?}: {problem}")]
Originate {
prefix: String,
problem: OriginateProblem,
},
}
/// What is wrong with one `peers` entry.
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum PeerProblem {
#[error("endpoint is missing")]
MissingEndpoint,
#[error("endpoint is invalid: {0}")]
Endpoint(Box<ConfigError>),
#[error("remote_asn must be non-zero")]
RemoteAsn,
#[error("hold_time_seconds must be 0 or at least 3")]
HoldTime,
#[error("there is another peer of this name")]
DuplicateName,
#[error("there is another peer at this address")]
DuplicateAddress,
#[error("local_address {0:?} is not an IP address")]
LocalAddress(String),
#[error("local_address and endpoint are different address families")]
AddressFamilyMismatch,
#[error("address_families has a value that is not an address family")]
AddressFamily,
#[error("add_path has a value that is not one of its settings")]
AddPath,
#[error("role is set for a peer in our own AS; roles are between networks")]
RoleInOwnAs,
#[error("require_role is set without a role")]
RequireRoleWithoutRole,
#[error("role has a value that is not one of its settings")]
Role,
#[error("import: {0}")]
Import(#[from] ImportProblem),
}
/// What is wrong with a peer's `import` policy.
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum ImportProblem {
#[error("prefix {prefix:?}: {problem}")]
Prefix {
prefix: String,
problem: bgp_primitives::ip_prefix::PrefixError,
},
#[error(
"prefix {prefix:?}: max_length {max_length} is not between its own length and {longest}"
)]
MaxLength {
prefix: String,
max_length: u32,
longest: u8,
},
#[error("{field} of {value} is longer than an address of {longest} bits")]
PrefixLength {
field: &'static str,
value: u32,
longest: u8,
},
#[error("max_as_path_length of {0} is more than a path can hold")]
PathLength(u32),
}
/// What is wrong with one `originate` entry.
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum OriginateProblem {
#[error("{0}")]
Prefix(#[from] bgp_primitives::ip_prefix::PrefixError),
#[error("origin {0:?} must be IGP, EGP or INCOMPLETE")]
Origin(String),
#[error("as_path_prepend must be at most {max}")]
Prepend { max: u32 },
#[error("{0}")]
Community(#[from] bgp_packet::community::CommunityError),
#[error("listed more than once")]
Duplicate,
}
#[cfg(test)]
mod tests {
use bgp_packet::errors::MsgHeaderSubcode;
use super::*;
#[test]
fn each_session_error_names_the_notification_rfc_4271_assigns_it() {
let notification = |e: SessionError| e.notification().map(|n| (n.error, n.data));
assert_eq!(
notification(SessionError::Decode(DecodeError {
notification: BgpError::MsgHeader(MsgHeaderSubcode::BadMessageLength),
data: vec![0x13, 0x88],
detail: "too long".into(),
})),
Some((
BgpError::MsgHeader(MsgHeaderSubcode::BadMessageLength),
vec![0x13, 0x88]
))
);
assert_eq!(
notification(SessionError::Timeout(State::OpenSent)),
Some((BgpError::HoldTimer, vec![]))
);
assert_eq!(
notification(SessionError::UnexpectedMessage {
state: State::OpenConfirm,
received: "update"
}),
Some((
BgpError::FsmError(FsmSubcode::UnexpectedOpenConfirm),
vec![]
))
);
assert_eq!(
notification(SessionError::WrongPeerAs {
expected: 1,
actual: 2
}),
Some((BgpError::OpenMsg(OpenMsgSubcode::BadPeerAs), vec![]))
);
// Something of ours that cannot be written is our fault: the peer
// is told the session is being reset, not left to time it out.
assert_eq!(
notification(SessionError::Encode(EncodeError::TooLong {
what: "path attribute",
len: 70000,
max: 65535
})),
Some((BgpError::Cease(CeaseSubcode::AdminReset), vec![]))
);
// A broken connection cannot carry a NOTIFICATION.
assert_eq!(
notification(SessionError::Io(io::ErrorKind::BrokenPipe.into())),
None
);
}
}