Files
ferrix/crates/packet/src/errors.rs
T
Rayhaan JaufeerallyandClaude Fable 5.1 a8aba52562 Review pass on the codec and the table: bugs fixed, encoders simplified
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>
2026-10-02 10:32:27 +02:00

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,
}