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>
323 lines
12 KiB
Rust
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
|
|
);
|
|
}
|
|
}
|