The RIB handles swallowed every failure of their actor: an UPDATE applied to a stopped Adj-RIB-In vanished, and a snapshot of it came back empty. Sessions stayed up, peers' routes went nowhere, and the status page showed a healthy router with no routes. An actor stops only by panicking, so this is the state the server would be in after a bug. - Every operation on AdjRibIn and LocalRib returns Result<_, RibStopped>. LocalRibError is replaced by it. - The server waits on both actors, the advertiser and the dialers. If one stops it sends every peer Cease / Administrative Shutdown and returns an error, so the process exits and can be restarted. Before, the tasks' handles were held and never looked at. - A session that cannot store an UPDATE ends with the same Cease. - The HTTP API answers 503 with the reason for anything that needs a table it cannot read, including /metricz: a scrape that fails is noticed, last known sizes are not. Also: - The routes of a session are now removed even if its task is torn down without running its cleanup (a panic), instead of staying in the table as routes of a peer that is gone. - Table sizes for the status page and metrics are asked for as a count instead of copying the whole table to take its length. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
218 lines
7.6 KiB
Rust
218 lines
7.6 KiB
Rust
//! Error types of the server crate.
|
|
|
|
use std::io;
|
|
use std::net::SocketAddr;
|
|
|
|
use bgp_packet::errors::{BgpError, CeaseSubcode, DecodeError, FsmSubcode, OpenMsgSubcode};
|
|
use bgp_packet::message::NotificationMessage;
|
|
use bgp_primitives::parser::EncodeError;
|
|
use bgp_rib::RibStopped;
|
|
use thiserror::Error;
|
|
|
|
use crate::family::FamilySet;
|
|
use crate::fsm::State;
|
|
|
|
/// 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 },
|
|
#[error("closed by connection collision resolution")]
|
|
Collision,
|
|
/// The peer offers none of the address families configured for it, so
|
|
/// the session could carry nothing.
|
|
/// The table the session's routes go into is gone. The server is on
|
|
/// its way down; the peer is told so.
|
|
#[error(transparent)]
|
|
RibStopped(#[from] RibStopped),
|
|
#[error("no address family in common: we offer {ours}, the peer offers {theirs}")]
|
|
NoCommonAddressFamily { ours: FamilySet, theirs: FamilySet },
|
|
}
|
|
|
|
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::Encode(_) | Self::NotifiedByPeer(_) => return None,
|
|
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::Collision => BgpError::Cease(CeaseSubcode::ConnCollisionResolution),
|
|
Self::RibStopped(_) => BgpError::Cease(CeaseSubcode::AdminShutdown),
|
|
// 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.
|
|
#[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 },
|
|
}
|
|
|
|
/// 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")]
|
|
RouterId(String),
|
|
#[error("local_asn must be non-zero")]
|
|
LocalAsn,
|
|
#[error("invalid IP address {0:?}")]
|
|
Address(String),
|
|
#[error("port {0} is out of range")]
|
|
Port(u32),
|
|
#[error("http_endpoint.port is required")]
|
|
HttpPort,
|
|
#[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("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,
|
|
}
|
|
|
|
/// 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![]))
|
|
);
|
|
// A broken connection cannot carry a NOTIFICATION.
|
|
assert_eq!(
|
|
notification(SessionError::Io(io::ErrorKind::BrokenPipe.into())),
|
|
None
|
|
);
|
|
}
|
|
}
|