A review of the wire codec and the routing table, with the fixes. Bugs in the codec: - A NOTIFICATION with a code or subcode we have no name for (subcode 0, "unspecific", is common) failed to decode and was answered as a bad message. Every pair now reads, as BgpError::Other. - Optional parameters in the extended form of RFC 9072 were read and written with one-octet parameter lengths; they are two. - An ORF capability listing more than one address family made the whole OPEN malformed. - Bytes after an OPEN's optional parameters were accepted. - Extended communities were split into fields by rules that were wrong for the common types. They are kept as their eight octets. - Several length fields were computed in u16 and could wrap on encode. Simplifications in the codec: - Lengths are filled in after the value is written, for messages, attributes, capabilities and the UPDATE's two fields. The parallel wire_len functions, which had to be kept in step by hand, are gone. Message::to_wire no longer encodes into a second buffer. - Type codes for messages, attributes and capabilities are each defined once. The strum discriminant enums and their hand-written conversions are removed. - One function splits an attribute off a buffer; the UPDATE decoder, the plain attribute parser and the table's key reader all use it. - The UPDATE decoder's 185-line loop is an AttributeReader with one method that decides what becomes of an attribute. Duplicate detection is a bitset, not a HashSet allocated per UPDATE. - ParserContext::four_octet_asn is a bool. The "context not set" error path, which treated a programming error as a malformed route, is gone. - Codec functions take a context only if they read it, and return a Result only if they can fail. - MP_REACH_NLRI and MP_UNREACH_NLRI hold an AfiSafi. - AddressFamilyId is IpVersion, which is what it was. - Deserialize is no longer derived on codec types: nothing used it, and it bypassed IpPrefix's invariants. IpPrefix::to_wire, which was broken and unused, is removed. Bugs in the table: - The index of a row's best route was sixteen bits. A peer using ADD-PATH could send more routes for a prefix than that counts, and the index wrapped. It is 32 bits, at no cost in row size. - Shards and attribute-store stripes were picked from the top bits of the hash, which are the bits hashbrown keeps as the tag it compares first. With 64 shards each shard's table had 2 of 128 tag values. They are now picked from bits the table does not use. - remove_peer held the peer list's lock for the whole scan of the table, so no session could establish while a peer was removed. - When more prefixes went at once than were left, the forced trim dropped a fixed few entries, reset every reader, and re-ran the tidy on every later write. It drops the older half. - Readers were woken by every change, including a repeated announcement and changes their journal does not record. - Reader::poll with a budget of zero never made progress. Simplifications in the table: - The two journals of a shard are one Journals value written through three calls; the five places that kept them in step by hand are gone. Whether an entry is still live is decided in one place. - The shortcuts of route selection moved to select.rs, next to the invariant they depend on, which is now written down. - announce and withdraw take routes with or without path identifiers; the _paths variants are removed. - The selection key is read off the wire form only; the second implementation over parsed attributes is removed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
222 lines
6.1 KiB
Rust
222 lines
6.1 KiB
Rust
use serde::Serialize;
|
|
use strum::FromRepr;
|
|
use thiserror::Error;
|
|
|
|
/// A field holds a number that is not one of the values defined for it.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
|
|
#[error("invalid {what} value {value}")]
|
|
pub struct InvalidValue {
|
|
pub what: &'static str,
|
|
pub value: u32,
|
|
}
|
|
|
|
/// A message that could not be decoded, together with the NOTIFICATION that
|
|
/// reports the problem to the peer (RFC 4271 §6).
|
|
#[derive(Debug, Clone, PartialEq, Eq, Error)]
|
|
#[error("{detail}")]
|
|
pub struct DecodeError {
|
|
/// The error code and subcode to send.
|
|
pub notification: BgpError,
|
|
/// The Data field of that NOTIFICATION, where the RFC defines one.
|
|
pub data: Vec<u8>,
|
|
/// What was wrong, for the log.
|
|
pub detail: String,
|
|
}
|
|
|
|
/// The error a NOTIFICATION reports: its code and subcode (RFC 4271 §4.5).
|
|
#[derive(Debug, Clone, PartialEq, Eq, Error, Serialize)]
|
|
pub enum BgpError {
|
|
#[error("Message header error: {0}")]
|
|
MsgHeader(MsgHeaderSubcode),
|
|
|
|
#[error("Open message error: {0}")]
|
|
OpenMsg(OpenMsgSubcode),
|
|
|
|
#[error("Update message error: {0}")]
|
|
UpdateMsg(UpdateMsgSubcode),
|
|
|
|
#[error("Hold timer expired")]
|
|
HoldTimer,
|
|
|
|
#[error("Finite state machine error: {0}")]
|
|
FsmError(FsmSubcode),
|
|
|
|
#[error("Cease: {0}")]
|
|
Cease(CeaseSubcode),
|
|
|
|
#[error("Route refresh message error: {0}")]
|
|
RouteRefresh(RouteRefreshSubcode),
|
|
|
|
#[error("Send hold timer expired")]
|
|
SendHoldTimer,
|
|
|
|
/// A code or subcode this implementation has no name for. A peer may
|
|
/// send any: new ones are assigned, and zero ("unspecific") is always
|
|
/// allowed. The session ends the same way, and the numbers are kept
|
|
/// for the log.
|
|
#[error("error code {code} subcode {subcode}")]
|
|
Other { code: u8, subcode: u8 },
|
|
}
|
|
|
|
impl BgpError {
|
|
/// The error for a code and subcode off the wire. Every pair has one,
|
|
/// so that a peer's NOTIFICATION can always be read.
|
|
pub fn from_code_subcode(code: u8, subcode: u8) -> Self {
|
|
let known = match code {
|
|
1 => MsgHeaderSubcode::from_repr(subcode).map(Self::MsgHeader),
|
|
2 => OpenMsgSubcode::from_repr(subcode).map(Self::OpenMsg),
|
|
3 => UpdateMsgSubcode::from_repr(subcode).map(Self::UpdateMsg),
|
|
4 => (subcode == 0).then_some(Self::HoldTimer),
|
|
5 => FsmSubcode::from_repr(subcode).map(Self::FsmError),
|
|
6 => CeaseSubcode::from_repr(subcode).map(Self::Cease),
|
|
7 => RouteRefreshSubcode::from_repr(subcode).map(Self::RouteRefresh),
|
|
8 => (subcode == 0).then_some(Self::SendHoldTimer),
|
|
_ => None,
|
|
};
|
|
known.unwrap_or(Self::Other { code, subcode })
|
|
}
|
|
|
|
pub fn code_subcode(&self) -> (u8, u8) {
|
|
match self {
|
|
Self::MsgHeader(s) => (1, *s as u8),
|
|
Self::OpenMsg(s) => (2, *s as u8),
|
|
Self::UpdateMsg(s) => (3, *s as u8),
|
|
Self::HoldTimer => (4, 0),
|
|
Self::FsmError(s) => (5, *s as u8),
|
|
Self::Cease(s) => (6, *s as u8),
|
|
Self::RouteRefresh(s) => (7, *s as u8),
|
|
Self::SendHoldTimer => (8, 0),
|
|
Self::Other { code, subcode } => (*code, *subcode),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error, Serialize, FromRepr)]
|
|
#[repr(u8)]
|
|
pub enum MsgHeaderSubcode {
|
|
#[error("Connection not synchronized")]
|
|
ConnNotSynchronized = 1,
|
|
|
|
#[error("Bad message length")]
|
|
BadMessageLength = 2,
|
|
|
|
#[error("Bad message type")]
|
|
BadMessageType = 3,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error, Serialize, FromRepr)]
|
|
#[repr(u8)]
|
|
pub enum OpenMsgSubcode {
|
|
#[error("Unspecific")]
|
|
Unspecific = 0,
|
|
|
|
#[error("Unsupported BGP version number")]
|
|
UnsupportedVersion = 1,
|
|
|
|
#[error("Bad peer AS number")]
|
|
BadPeerAs = 2,
|
|
|
|
#[error("Bad BGP identifier")]
|
|
BadBgpId = 3,
|
|
|
|
#[error("Unsupported optional parameter")]
|
|
UnsupportedOptionalParam = 4,
|
|
|
|
#[error("Unacceptable hold time")]
|
|
UnacceptableHoldTime = 6,
|
|
|
|
#[error("Unsupported capability")]
|
|
UnsupportedCapability = 7,
|
|
|
|
#[error("Role mismatch")]
|
|
RoleMismatch = 8,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error, Serialize, FromRepr)]
|
|
#[repr(u8)]
|
|
pub enum UpdateMsgSubcode {
|
|
#[error("Malformed attribute list")]
|
|
MalformedAttrs = 1,
|
|
|
|
#[error("Unrecognized well known attribute")]
|
|
UnrecognizedWellKnownAttr = 2,
|
|
|
|
#[error("Missing well known attribute")]
|
|
MissingWellKnown = 3,
|
|
|
|
#[error("Attribute flags error")]
|
|
AttributeFlags = 4,
|
|
|
|
#[error("Attribute length error")]
|
|
AttributeLength = 5,
|
|
|
|
#[error("Invalid origin")]
|
|
InvalidOrigin = 6,
|
|
|
|
#[error("Invalid next hop")]
|
|
InvalidNextHop = 8,
|
|
|
|
#[error("Optional attribute error")]
|
|
OptionalAttribute = 9,
|
|
|
|
#[error("Invalid network field")]
|
|
InvalidNetworkField = 10,
|
|
|
|
#[error("Malformed AS path")]
|
|
MalformedAsPath = 11,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error, Serialize, FromRepr)]
|
|
#[repr(u8)]
|
|
pub enum FsmSubcode {
|
|
#[error("Received an unexpected message in OpenSent state")]
|
|
UnexpectedOpenSent = 1,
|
|
|
|
#[error("Received an unexpected message in OpenConfirm state")]
|
|
UnexpectedOpenConfirm = 2,
|
|
|
|
#[error("Received an unexpected message in Established state")]
|
|
UnexpectedEstablished = 3,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error, Serialize, FromRepr)]
|
|
#[repr(u8)]
|
|
pub enum CeaseSubcode {
|
|
#[error("Maximum number of prefixes reached")]
|
|
MaxPrefixes = 1,
|
|
|
|
#[error("Administrative shutdown")]
|
|
AdminShutdown = 2,
|
|
|
|
#[error("Peer deconfigured")]
|
|
PeerDeconf = 3,
|
|
|
|
#[error("Administrative reset")]
|
|
AdminReset = 4,
|
|
|
|
#[error("Connection rejected")]
|
|
ConnRejected = 5,
|
|
|
|
#[error("Configuration change")]
|
|
ConfChange = 6,
|
|
|
|
#[error("Connection collision resolution")]
|
|
ConnCollisionResolution = 7,
|
|
|
|
#[error("Out of resources")]
|
|
OutOfResources = 8,
|
|
|
|
#[error("Hard reset")]
|
|
HardReset = 9,
|
|
|
|
#[error("BFD down")]
|
|
BfdDown = 10,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error, Serialize, FromRepr)]
|
|
#[repr(u8)]
|
|
pub enum RouteRefreshSubcode {
|
|
#[error("Invalid message length")]
|
|
InvalidLength = 1,
|
|
}
|