The work of the speaker-cleanup branch as one commit; the branch keeps the step-by-step history. Sessions - The state machine is a pure function from events to actions, driven by one task per connection. Idle, Connect and Active belong to a dialer that keeps configured peers up with backoff. - Capabilities are negotiated: address families, four-octet AS numbers with AS_TRANS and AS4_PATH, route refresh, ADD-PATH per family and direction. - Collision detection per RFC 4271 section 6.8, by address and identifier. - Connections are taken only from configured peers, a few per address. - A panic in a session is contained and answered with a Cease; a core task that stops shuts the server down with a Cease to every peer. - Writes that make no progress are given up (RFC 9687). Codec - Malformed UPDATEs are handled as RFC 7606 says: treat-as-withdraw, attribute discard, or session reset, with the NOTIFICATION to send. - Address families are open types; any can be named and ignored. - Prefixes are fixed-size values, normalised, with path identifiers. - Lengths are filled in after writing; one definition of each type code. - Property tests and fuzz targets for every decoder. Routing table - Adj-RIBs-In and Loc-RIB in one sharded table that sessions write to directly. Routes are sixteen bytes; attribute sets are interned in wire form. Best-path selection per RFC 4271 section 9.1.2.2 with deterministic MED. - A journal per shard lets followers read changes at their own pace; one that falls behind is resynchronised. - On a RIPE RIS dump of 148 peers and 39.3M routes: loaded in 2.0 s on 12 cores, about 52 bytes a route. Announcing - Originated routes come from the configuration. Each session has its own exporter with an Adj-RIB-Out, so a slow peer delays only itself. - Routes to a peer in our own AS have an empty path and a LOCAL_PREF. Operations - HTTP status page, JSON API, event stream and Prometheus metrics. - Benchmarks on synthetic and real tables. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
249 lines
9.1 KiB
Rust
249 lines
9.1 KiB
Rust
//! Error types of the server crate.
|
|
|
|
use std::io;
|
|
use std::net::SocketAddr;
|
|
use std::time::Duration;
|
|
|
|
use bgp_packet::errors::{BgpError, CeaseSubcode, DecodeError, FsmSubcode, OpenMsgSubcode};
|
|
use bgp_packet::message::NotificationMessage;
|
|
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;
|
|
|
|
/// 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 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 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::Collision => BgpError::Cease(CeaseSubcode::ConnCollisionResolution),
|
|
Self::TableFull(_) => BgpError::Cease(CeaseSubcode::OutOfResources),
|
|
// 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 },
|
|
}
|
|
|
|
/// 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("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("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,
|
|
}
|
|
|
|
/// 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
|
|
);
|
|
}
|
|
}
|