//! 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, theirs: Option) -> 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, theirs: Option, }, #[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 { 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), #[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 ); } }