A peer's endpoint may be a link-local IPv6 address with the interface it is on. Such a peer is the address and the interface together, wherever peers are told apart: admission, collisions, hold-downs, the refusal log, the status page. The interface is looked up by name each time it is needed, so one that appears later or is made anew is found. Our routes go out with our link-local address in both places of MP_REACH_NLRI's next hop, as FRR sends its own. A peer's link-local next hop is taken alone, twice, or after "::" (BIRD), and resolved in the kernel on the interface of the session rather than by looking fe80::/64 up, which every interface has. The lab has a new phase, linklocal: sessions with FRR (by interface, with radvd beside the daemon) and BIRD over a LAN with link-local addresses only, both families, pings across, the interface down and up. 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
|
|
);
|
|
}
|
|
}
|