Cleanup and implementation of Update / Notification parser / serializers.

This commit is contained in:
Rayhaan Jaufeerally committed 2026-04-18 12:58:51 +02:00
1 parent dfe066c9ab
commit 5ae8f6e249
22 files changed
+1573 -65

No files matched your search

+13 -12
View File
@@ -2,18 +2,19 @@
name = "bgp-packet"
description.workspace = true
edition.workspace = true
homepage.workspace = true
license.workspace = true
repository.workspace = true
version.workspace = true
edition.workspace = true
homepage.workspace = true
license.workspace = true
repository.workspace = true
version.workspace = true
[dependencies]
bitfield.workspace = true
bytes.workspace = true
eyre.workspace = true
nom.workspace = true
serde.workspace = true
bitfield.workspace = true
bytes.workspace = true
eyre.workspace = true
nom.workspace = true
serde.workspace = true
serde_repr.workspace = true
thiserror.workspace = true
strum.workspace = true
thiserror.workspace = true
strum.workspace = true
bgp-primitives.workspace = true
+201 -7
View File
@@ -1,6 +1,7 @@
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Debug, Error)]
#[derive(Debug, Clone, PartialEq, Eq, Error, Serialize, Deserialize)]
#[repr(u8)]
pub enum BgpError {
#[error("Message header error")]
@@ -28,7 +29,36 @@ pub enum BgpError {
SendHoldTimer = 8,
}
#[derive(Debug, Error)]
impl BgpError {
pub fn from_code_subcode(code: u8, subcode: u8) -> Result<Self, ()> {
match code {
1 => Ok(Self::MsgHeader(MsgHeaderSubcode::try_from(subcode)?)),
2 => Ok(Self::OpenMsg(OpenMsgSubcode::try_from(subcode)?)),
3 => Ok(Self::UpdateMsg(UpdateMsgSubcode::try_from(subcode)?)),
4 => Ok(Self::HoldTimer),
5 => Ok(Self::FsmError(FsmSubcode::try_from(subcode)?)),
6 => Ok(Self::Cease(CeaseSubcode::try_from(subcode)?)),
7 => Ok(Self::RouteRefresh(RouteRefreshSubcode::try_from(subcode)?)),
8 => Ok(Self::SendHoldTimer),
_ => Err(()),
}
}
pub fn code_subcode(&self) -> (u8, u8) {
match self {
Self::MsgHeader(s) => (1, u8::from(s)),
Self::OpenMsg(s) => (2, u8::from(s)),
Self::UpdateMsg(s) => (3, u8::from(s)),
Self::HoldTimer => (4, 0),
Self::FsmError(s) => (5, u8::from(s)),
Self::Cease(s) => (6, u8::from(s)),
Self::RouteRefresh(s) => (7, u8::from(s)),
Self::SendHoldTimer => (8, 0),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Error, Serialize, Deserialize)]
pub enum MsgHeaderSubcode {
#[error("Connection not synchronized")]
ConnNotSynchronized = 1,
@@ -40,7 +70,29 @@ pub enum MsgHeaderSubcode {
BadMessagType = 3,
}
#[derive(Debug, Error)]
impl TryFrom<u8> for MsgHeaderSubcode {
type Error = ();
fn try_from(v: u8) -> Result<Self, ()> {
match v {
1 => Ok(Self::ConnNotSynchronized),
2 => Ok(Self::BadMessageLength),
3 => Ok(Self::BadMessagType),
_ => Err(()),
}
}
}
impl From<&MsgHeaderSubcode> for u8 {
fn from(s: &MsgHeaderSubcode) -> u8 {
match s {
MsgHeaderSubcode::ConnNotSynchronized => 1,
MsgHeaderSubcode::BadMessageLength => 2,
MsgHeaderSubcode::BadMessagType => 3,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Error, Serialize, Deserialize)]
pub enum OpenMsgSubcode {
#[error("Unsupported BGP version number")]
UnsupportedVersion = 1,
@@ -64,7 +116,37 @@ pub enum OpenMsgSubcode {
RoleMismatch = 8,
}
#[derive(Debug, Error)]
impl TryFrom<u8> for OpenMsgSubcode {
type Error = ();
fn try_from(v: u8) -> Result<Self, ()> {
match v {
1 => Ok(Self::UnsupportedVersion),
2 => Ok(Self::BadPeerAs),
3 => Ok(Self::BadBgpId),
4 => Ok(Self::UnsupportedOptionalParam),
6 => Ok(Self::UnacceptableHoldTime),
7 => Ok(Self::UnsupportedCapability),
8 => Ok(Self::RoleMismatch),
_ => Err(()),
}
}
}
impl From<&OpenMsgSubcode> for u8 {
fn from(s: &OpenMsgSubcode) -> u8 {
match s {
OpenMsgSubcode::UnsupportedVersion => 1,
OpenMsgSubcode::BadPeerAs => 2,
OpenMsgSubcode::BadBgpId => 3,
OpenMsgSubcode::UnsupportedOptionalParam => 4,
OpenMsgSubcode::UnacceptableHoldTime => 6,
OpenMsgSubcode::UnsupportedCapability => 7,
OpenMsgSubcode::RoleMismatch => 8,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Error, Serialize, Deserialize)]
pub enum UpdateMsgSubcode {
#[error("Malformed attribute list")]
MalforedAttrs = 1,
@@ -97,7 +179,43 @@ pub enum UpdateMsgSubcode {
MalformedAsPath = 11,
}
#[derive(Debug, Error)]
impl TryFrom<u8> for UpdateMsgSubcode {
type Error = ();
fn try_from(v: u8) -> Result<Self, ()> {
match v {
1 => Ok(Self::MalforedAttrs),
2 => Ok(Self::UnrecognizedWellKnownAttr),
3 => Ok(Self::MissingWellKnown),
4 => Ok(Self::AttributeFlags),
5 => Ok(Self::AttributeLength),
6 => Ok(Self::InvalidOrigin),
8 => Ok(Self::InvalidNextHop),
9 => Ok(Self::OptionalAttribute),
10 => Ok(Self::InvalidNetworkField),
11 => Ok(Self::MalformedAsPath),
_ => Err(()),
}
}
}
impl From<&UpdateMsgSubcode> for u8 {
fn from(s: &UpdateMsgSubcode) -> u8 {
match s {
UpdateMsgSubcode::MalforedAttrs => 1,
UpdateMsgSubcode::UnrecognizedWellKnownAttr => 2,
UpdateMsgSubcode::MissingWellKnown => 3,
UpdateMsgSubcode::AttributeFlags => 4,
UpdateMsgSubcode::AttributeLength => 5,
UpdateMsgSubcode::InvalidOrigin => 6,
UpdateMsgSubcode::InvalidNextHop => 8,
UpdateMsgSubcode::OptionalAttribute => 9,
UpdateMsgSubcode::InvalidNetworkField => 10,
UpdateMsgSubcode::MalformedAsPath => 11,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Error, Serialize, Deserialize)]
pub enum FsmSubcode {
#[error("Received an unexpected message in OpenSent state")]
UnexpectedOpenSent = 1,
@@ -109,7 +227,29 @@ pub enum FsmSubcode {
UnexpectedEstablished = 3,
}
#[derive(Debug, Error)]
impl TryFrom<u8> for FsmSubcode {
type Error = ();
fn try_from(v: u8) -> Result<Self, ()> {
match v {
1 => Ok(Self::UnexpectedOpenSent),
2 => Ok(Self::UnexpectedOpenConfirm),
3 => Ok(Self::UnexpectedEstablished),
_ => Err(()),
}
}
}
impl From<&FsmSubcode> for u8 {
fn from(s: &FsmSubcode) -> u8 {
match s {
FsmSubcode::UnexpectedOpenSent => 1,
FsmSubcode::UnexpectedOpenConfirm => 2,
FsmSubcode::UnexpectedEstablished => 3,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Error, Serialize, Deserialize)]
pub enum CeaseSubcode {
#[error("Maximum number of prefixes reached")]
MaxPrefixes = 1,
@@ -142,8 +282,62 @@ pub enum CeaseSubcode {
BfdDown = 10,
}
#[derive(Debug, Error)]
impl TryFrom<u8> for CeaseSubcode {
type Error = ();
fn try_from(v: u8) -> Result<Self, ()> {
match v {
1 => Ok(Self::MaxPrefixes),
2 => Ok(Self::AdminShutdown),
3 => Ok(Self::PeerDeconf),
4 => Ok(Self::AdminReset),
5 => Ok(Self::ConnRejected),
6 => Ok(Self::ConfChange),
7 => Ok(Self::ConnCollisionResolution),
8 => Ok(Self::OutOfResources),
9 => Ok(Self::HardReset),
10 => Ok(Self::BfdDown),
_ => Err(()),
}
}
}
impl From<&CeaseSubcode> for u8 {
fn from(s: &CeaseSubcode) -> u8 {
match s {
CeaseSubcode::MaxPrefixes => 1,
CeaseSubcode::AdminShutdown => 2,
CeaseSubcode::PeerDeconf => 3,
CeaseSubcode::AdminReset => 4,
CeaseSubcode::ConnRejected => 5,
CeaseSubcode::ConfChange => 6,
CeaseSubcode::ConnCollisionResolution => 7,
CeaseSubcode::OutOfResources => 8,
CeaseSubcode::HardReset => 9,
CeaseSubcode::BfdDown => 10,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Error, Serialize, Deserialize)]
pub enum RouteRefreshSubcode {
#[error("Invalid message length")]
InvalidLength = 1,
}
impl TryFrom<u8> for RouteRefreshSubcode {
type Error = ();
fn try_from(v: u8) -> Result<Self, ()> {
match v {
1 => Ok(Self::InvalidLength),
_ => Err(()),
}
}
}
impl From<&RouteRefreshSubcode> for u8 {
fn from(s: &RouteRefreshSubcode) -> u8 {
match s {
RouteRefreshSubcode::InvalidLength => 1,
}
}
}
-3
View File
@@ -1,6 +1,3 @@
pub mod constants;
pub mod errors;
pub mod ip_prefix;
pub mod message;
pub mod open;
pub mod parser;
+423 -25
View File
@@ -18,12 +18,14 @@ use serde::{Deserialize, Serialize};
use serde_repr::{Deserialize_repr, Serialize_repr};
use strum::EnumDiscriminants;
use crate::constants::AddressFamilyId;
use crate::constants::SubsequentAfi;
use crate::ip_prefix::IpPrefix;
use bgp_primitives::constants::AddressFamilyId;
use bgp_primitives::constants::SubsequentAfi;
use bgp_primitives::ip_prefix::IpPrefix;
use bgp_primitives::parser::BgpParserError;
use bgp_primitives::parser::ParserContext;
use crate::errors::BgpError;
use crate::open::OpenMessage;
use crate::parser::BgpParserError;
use crate::parser::ParserContext;
/// A sentinel AS number used to denote that the actual AS number is provided as
/// a 4 byte value in the capabilities instead.
@@ -47,7 +49,7 @@ pub enum Message {
impl Message {}
/// Represents a BGP Update message.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct UpdateMessage {
pub withdrawn_routes: Vec<IpPrefix>,
pub path_attributes: Vec<PathAttribute>,
@@ -55,22 +57,120 @@ pub struct UpdateMessage {
}
impl UpdateMessage {
/// Parses a BGP UPDATE message body per RFC 4271 §4.3. The buffer passed in
/// must contain exactly the UPDATE payload (i.e. without the 19 byte BGP
/// message header). The NLRI section has no explicit length and is inferred
/// as the remainder of the buffer, so passing extra trailing bytes will
/// cause them to be (mis-)parsed as NLRI.
pub fn from_wire<'a>(
_ctx: &ParserContext,
_buf: &'a [u8],
ctx: &ParserContext,
buf: &'a [u8],
) -> IResult<&'a [u8], Self, BgpParserError<&'a [u8]>> {
todo!();
// Withdrawn routes. Per RFC 4271 these are always IPv4 prefixes; other
// address families are carried in the MP_UNREACH_NLRI path attribute.
let (buf, withdrawn_len) = be_u16(buf)?;
let (buf, withdrawn_buf) = nom::bytes::take(withdrawn_len as usize).parse(buf)?;
let (withdrawn_rem, withdrawn_routes) =
nom::multi::many0(|buf| parse_prefix(AddressFamilyId::Ipv4, buf))
.parse(withdrawn_buf)?;
if !withdrawn_rem.is_empty() {
return Err(Failure(BgpParserError::Eyre(eyre!(
"Leftover bytes {:x?} after parsing withdrawn routes",
withdrawn_rem
))));
}
// Path attributes.
let (buf, total_pa_len) = be_u16(buf)?;
let (buf, pa_buf) = nom::bytes::take(total_pa_len as usize).parse(buf)?;
let (pa_rem, path_attributes) =
nom::multi::many0(|buf| PathAttribute::from_wire(ctx, buf)).parse(pa_buf)?;
if !pa_rem.is_empty() {
return Err(Failure(BgpParserError::Eyre(eyre!(
"Leftover bytes {:x?} after parsing path attributes",
pa_rem
))));
}
// NLRI. As with withdrawn routes, these are IPv4 prefixes per RFC 4271.
// They occupy the remainder of the UPDATE body; if the caller has
// delimited `buf` correctly then `many0` will consume it entirely.
let (buf, announced_routes) =
nom::multi::many0(|buf| parse_prefix(AddressFamilyId::Ipv4, buf)).parse(buf)?;
if !buf.is_empty() {
return Err(Failure(BgpParserError::Eyre(eyre!(
"Leftover bytes {:x?} after parsing NLRI",
buf
))));
}
Ok((
buf,
Self {
withdrawn_routes,
path_attributes,
announced_routes,
},
))
}
pub fn to_wire(&self, _ctx: &ParserContext, _out: &mut BytesMut) -> Result<()> {
todo!()
pub fn to_wire(&self, ctx: &ParserContext, out: &mut BytesMut) -> Result<()> {
// Withdrawn routes.
let withdrawn_len = prefix_list_wire_len(&self.withdrawn_routes)
.wrap_err("Withdrawn routes length exceeds u16::MAX")?;
out.put_u16(withdrawn_len);
for prefix in &self.withdrawn_routes {
out.put_u8(prefix.length);
out.put(prefix.prefix.as_slice());
}
// Path attributes.
let pa_total: u32 = self
.path_attributes
.iter()
.map(|p| p.wire_len(ctx).map(|v| v as u32))
.sum::<Result<u32>>()?;
let pa_total = u16::try_from(pa_total)
.map_err(|_| eyre!("Total path attribute length exceeds u16::MAX"))?;
out.put_u16(pa_total);
for pa in &self.path_attributes {
pa.to_wire(ctx, out)?;
}
// NLRI (consumes the remainder of the message).
for prefix in &self.announced_routes {
out.put_u8(prefix.length);
out.put(prefix.prefix.as_slice());
}
Ok(())
}
pub fn wire_len(&self, _ctx: &ParserContext) -> Result<u16> {
todo!()
pub fn wire_len(&self, ctx: &ParserContext) -> Result<u16> {
// 2 bytes for the withdrawn length field + 2 bytes for the total path
// attribute length field.
let mut total: u32 = 4;
for prefix in &self.withdrawn_routes {
total += 1 + prefix.prefix.len() as u32;
}
for pa in &self.path_attributes {
total += pa.wire_len(ctx)? as u32;
}
for prefix in &self.announced_routes {
total += 1 + prefix.prefix.len() as u32;
}
u16::try_from(total).map_err(|_| eyre!("UPDATE message length exceeds u16::MAX"))
}
}
/// Computes the total wire length of a list of IPv4/IPv6 NLRI prefixes as they
/// appear in the withdrawn or NLRI sections of an UPDATE message (each prefix
/// is encoded as a 1-byte length followed by `ceil(length/8)` prefix bytes).
fn prefix_list_wire_len(prefixes: &[IpPrefix]) -> Result<u16> {
let total: u32 = prefixes.iter().map(|p| 1 + p.prefix.len() as u32).sum();
u16::try_from(total).map_err(|_| eyre!("Prefix list length exceeds u16::MAX"))
}
bitfield! {
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct PathAttributeFlags(u8);
@@ -207,6 +307,20 @@ impl PathAttribute {
};
let discriminant = PathAttributeDiscriminants::from(type_code);
if matches!(
discriminant,
PathAttributeDiscriminants::UnknownPathAttribute
) {
let (buf, pa_buf) = nom::bytes::take(length).parse(buf)?;
return Ok((
buf,
PathAttribute::UnknownPathAttribute {
flags: attr_flags,
type_code,
payload: pa_buf.to_vec(),
},
));
}
Self::parse_known_path_attribute(ctx, buf, discriminant, length)
}
@@ -332,18 +446,48 @@ impl PathAttribute {
payload,
} => {
out.put_u8(flags.0);
out.put_u8(*type_code);
if flags.extended_length() {
out.put_u16(payload.len() as u16);
} else {
out.put_u8(payload.len() as u8);
}
out.put_u8(*type_code);
out.put(payload.as_slice());
}
}
Ok(())
}
/// Returns the total on-wire length of this path attribute, including the
/// attribute header (flags + type + length) and the payload.
pub fn wire_len(&self, ctx: &ParserContext) -> Result<u16> {
let payload_len: u16 = match self {
PathAttribute::Origin(a) => a.wire_len(ctx)?,
PathAttribute::AsPath(a) => a.wire_len(ctx)?,
PathAttribute::NextHop(a) => a.wire_len(ctx)?,
PathAttribute::MultiExitDisc(a) => a.wire_len(ctx)?,
PathAttribute::LocalPref(a) => a.wire_len(ctx)?,
PathAttribute::AtomicAggregate(a) => a.wire_len(ctx)?,
PathAttribute::Aggregator(a) => a.wire_len(ctx)?,
PathAttribute::Communitites(a) => a.wire_len(ctx)?,
PathAttribute::MpReachNlri(a) => a.wire_len(ctx)?,
PathAttribute::MpUnreachNlri(a) => a.wire_len(ctx)?,
PathAttribute::ExtendedCommunities(a) => a.wire_len(ctx)?,
PathAttribute::LargeCommunities(a) => a.wire_len(ctx)?,
PathAttribute::UnknownPathAttribute { flags, payload, .. } => {
// An unknown attribute preserves its original extended_length
// flag on re-serialization, so use it verbatim here rather than
// re-deriving it from the payload length.
let payload_len = u16::try_from(payload.len())
.map_err(|_| eyre!("UnknownPathAttribute payload exceeds u16::MAX"))?;
let header_len: u16 = if flags.extended_length() { 4 } else { 3 };
return Ok(header_len + payload_len);
}
};
let header_len: u16 = if payload_len > u8::MAX as u16 { 4 } else { 3 };
Ok(header_len + payload_len)
}
}
/// Origin path attribute is a mandatory attribute defined in RFC4271.
@@ -535,7 +679,7 @@ impl NextHopPathAttribute {
Ok(())
}
pub fn wire_len(&self, _: &ParserContext) -> Result<u32> {
pub fn wire_len(&self, _: &ParserContext) -> Result<u16> {
Ok(4)
}
}
@@ -1168,8 +1312,47 @@ impl MpUnreachNlriPathAttribute {
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NotificationMessage {}
/// Represents a BGP NOTIFICATION message per RFC 4271 §4.5.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct NotificationMessage {
pub error: BgpError,
/// Optional diagnostic data; may be empty.
pub data: Vec<u8>,
}
impl NotificationMessage {
/// Parses a NOTIFICATION message body (without the 19-byte BGP header).
/// Consumes the entire buffer; any remaining bytes become `data`.
pub fn from_wire<'a>(
_ctx: &ParserContext,
buf: &'a [u8],
) -> IResult<&'a [u8], Self, BgpParserError<&'a [u8]>> {
let (buf, error_code) = be_u8(buf)?;
let (buf, error_subcode) = be_u8(buf)?;
let error = BgpError::from_code_subcode(error_code, error_subcode).map_err(|_| {
Failure(BgpParserError::Eyre(eyre!(
"Unknown NOTIFICATION error code {} subcode {}",
error_code,
error_subcode
)))
})?;
Ok((&[], Self { error, data: buf.to_vec() }))
}
pub fn to_wire(&self, _ctx: &ParserContext, buf: &mut BytesMut) -> Result<()> {
let (code, subcode) = self.error.code_subcode();
buf.put_u8(code);
buf.put_u8(subcode);
buf.put_slice(&self.data);
Ok(())
}
pub fn wire_len(&self) -> u16 {
2 + self.data.len() as u16
}
}
#[cfg(test)]
mod tests {
@@ -1181,16 +1364,18 @@ mod tests {
use bytes::BytesMut;
use eyre::{Result, eyre};
use crate::{
use crate::message::{
AggregatorPathAttribute, AsPathAttribute, AsPathSegment, AtomicAggregatePathAttribute,
CommunitiesPathAttribute, ExtendedCommunitiesPathAttribute, ExtendedCommunity,
LargeCommunitiesPathAttribute, LargeCommunity, LocalPrefPathAttribute,
MpReachNlriPathAttribute, MpUnreachNlriPathAttribute, MultiExitDiscPathAttribute,
NextHopPathAttribute, NlriNextHop, NotificationMessage, OriginPathAttribute, PathAttribute,
PathAttributeFlags, UpdateMessage,
};
use bgp_primitives::{
constants::{AddressFamilyId, SubsequentAfi},
ip_prefix::IpPrefix,
message::{
AggregatorPathAttribute, AsPathAttribute, AsPathSegment, AtomicAggregatePathAttribute,
CommunitiesPathAttribute, ExtendedCommunitiesPathAttribute, ExtendedCommunity,
LargeCommunitiesPathAttribute, LargeCommunity, LocalPrefPathAttribute,
MpReachNlriPathAttribute, MpUnreachNlriPathAttribute, MultiExitDiscPathAttribute,
NextHopPathAttribute, NlriNextHop, OriginPathAttribute, PathAttribute,
},
parser::ParserContext,
};
@@ -1415,4 +1600,217 @@ mod tests {
}]
})
);
// Unknown Path Attribute. Ensures we both parse an unrecognized type code
// into the catch-all variant and re-serialize it with the correct field
// ordering (flags, type, length, payload) per RFC 4271 §4.3.
test_path_attribute_roundtrip!(
test_unknown_path_attribute,
&[0x80, 0x63, 0x04, 0xde, 0xad, 0xbe, 0xef],
PathAttribute::UnknownPathAttribute {
flags: PathAttributeFlags::new(
/*optional=*/ true, /*transitive=*/ false, /*partial=*/ false,
/*extended_length=*/ false,
),
type_code: 0x63,
payload: vec![0xde, 0xad, 0xbe, 0xef],
}
);
// -- UPDATE message roundtrip tests --
/// Asserts that an UPDATE message roundtrips byte-for-byte and that
/// wire_len agrees with the length of the encoded output.
macro_rules! test_update_roundtrip {
($name:ident, $input_bytes:expr, $expected:expr) => {
#[test]
fn $name() -> Result<()> {
let ctx = &default_v6_context();
let (rem, parsed) = UpdateMessage::from_wire(ctx, $input_bytes)?;
assert!(rem.is_empty(), "Expected no trailing bytes, got {:x?}", rem);
assert_eq!(parsed, $expected);
let mut out = BytesMut::with_capacity(u16::MAX as usize);
parsed.to_wire(ctx, &mut out)?;
assert_eq!(out.to_vec(), $input_bytes);
assert_eq!(parsed.wire_len(ctx)?, $input_bytes.len() as u16);
Ok(())
}
};
}
// Wrapper to generate a IPv4 prefix used in tests below.
fn v4_prefix(length: u8, bytes: &[u8]) -> IpPrefix {
IpPrefix {
address_family: AddressFamilyId::Ipv4,
prefix: bytes.to_vec(),
length,
}
}
/// A small set of path attributes that commonly accompany an IPv4 unicast
/// announcement: Origin=IGP, AS_PATH=[65000], NEXT_HOP=192.0.2.1.
fn sample_ipv4_path_attributes() -> Vec<PathAttribute> {
vec![
PathAttribute::Origin(OriginPathAttribute::IGP),
PathAttribute::AsPath(AsPathAttribute {
segments: vec![AsPathSegment {
ordered: true,
path: vec![65000],
}],
}),
PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::new(192, 0, 2, 1))),
]
}
// Empty UPDATE (equivalent to a keepalive-shaped body): no withdrawn
// routes, no path attributes, no NLRI.
test_update_roundtrip!(
test_update_empty,
&[0x00, 0x00, 0x00, 0x00],
UpdateMessage {
withdrawn_routes: vec![],
path_attributes: vec![],
announced_routes: vec![],
}
);
// Pure announcement: no withdrawn routes, standard v4 path attributes, one
// NLRI prefix (10.0.0.0/8).
test_update_roundtrip!(
test_update_announcement,
&[
// Withdrawn routes length = 0.
0x00, 0x00, // Total path attribute length = 20 bytes.
0x00, 0x14, // ORIGIN = IGP.
0x40, 0x01, 0x01, 0x00, // AS_PATH = AS_SEQUENCE [65000] (4-octet ASNs).
0x40, 0x02, 0x06, 0x02, 0x01, 0x00, 0x00, 0xfd, 0xe8,
// NEXT_HOP = 192.0.2.1.
0x40, 0x03, 0x04, 0xc0, 0x00, 0x02, 0x01, // NLRI: 10.0.0.0/8.
0x08, 0x0a,
],
UpdateMessage {
withdrawn_routes: vec![],
path_attributes: sample_ipv4_path_attributes(),
announced_routes: vec![v4_prefix(8, &[0x0a])],
}
);
// Pure withdrawal: one withdrawn route, no path attributes, no NLRI. RFC
// 4271 allows (and indeed requires) this shape for unreach updates.
test_update_roundtrip!(
test_update_withdrawal,
&[
// Withdrawn routes length = 2.
0x00, 0x02, // 10.0.0.0/8.
0x08, 0x0a, // Total path attribute length = 0.
0x00, 0x00,
],
UpdateMessage {
withdrawn_routes: vec![v4_prefix(8, &[0x0a])],
path_attributes: vec![],
announced_routes: vec![],
}
);
// Mixed withdrawal + announcement with multiple NLRI entries at different
// prefix lengths, exercising the variable-length NLRI parser tail.
test_update_roundtrip!(
test_update_mixed,
&[
// Withdrawn routes length = 2.
0x00, 0x02, // 10.0.0.0/8 withdrawn.
0x08, 0x0a, // Total path attribute length = 20.
0x00, 0x14, // ORIGIN = IGP.
0x40, 0x01, 0x01, 0x00, // AS_PATH = AS_SEQUENCE [65000].
0x40, 0x02, 0x06, 0x02, 0x01, 0x00, 0x00, 0xfd, 0xe8,
// NEXT_HOP = 192.0.2.1.
0x40, 0x03, 0x04, 0xc0, 0x00, 0x02, 0x01, // NLRI: 172.16.0.0/16.
0x10, 0xac, 0x10, // NLRI: 192.168.1.0/24.
0x18, 0xc0, 0xa8, 0x01,
],
UpdateMessage {
withdrawn_routes: vec![v4_prefix(8, &[0x0a])],
path_attributes: sample_ipv4_path_attributes(),
announced_routes: vec![
v4_prefix(16, &[0xac, 0x10]),
v4_prefix(24, &[0xc0, 0xa8, 0x01]),
],
}
);
// An UPDATE carrying an unrecognized path attribute.
test_update_roundtrip!(
test_update_with_unknown_path_attribute,
&[
// Withdrawn routes length = 0.
0x00, 0x00, // Total path attribute length = 7.
0x00, 0x07, // Unknown attribute: flags=0x80 (optional), type=0x63, len=4.
0x80, 0x63, 0x04, 0xde, 0xad, 0xbe, 0xef,
],
UpdateMessage {
withdrawn_routes: vec![],
path_attributes: vec![PathAttribute::UnknownPathAttribute {
flags: PathAttributeFlags::new(true, false, false, false),
type_code: 0x63,
payload: vec![0xde, 0xad, 0xbe, 0xef],
}],
announced_routes: vec![],
}
);
// Rejects trailing garbage after the NLRI section (a partial prefix).
#[test]
fn test_update_rejects_trailing_garbage() {
let ctx = default_v6_context();
// Well-formed withdrawn_len=0, pa_len=0, followed by a single byte that
// declares a prefix length of 8 but provides no prefix bytes.
let buf = &[0x00, 0x00, 0x00, 0x00, 0x08];
let result = UpdateMessage::from_wire(&ctx, buf);
assert!(
result.is_err(),
"Expected a parse error for trailing garbage, got {:?}",
result
);
}
#[test]
fn test_notification_roundtrip() -> Result<()> {
use crate::errors::{BgpError, CeaseSubcode};
let ctx = &default_v6_context();
let msg = NotificationMessage {
error: BgpError::Cease(CeaseSubcode::AdminShutdown),
data: vec![0xde, 0xad],
};
let mut buf = BytesMut::new();
msg.to_wire(ctx, &mut buf)?;
assert_eq!(buf.as_ref(), &[6, 2, 0xde, 0xad]);
let (rem, parsed) = NotificationMessage::from_wire(ctx, &buf).unwrap();
assert!(rem.is_empty());
assert_eq!(parsed, msg);
Ok(())
}
#[test]
fn test_notification_no_data() -> Result<()> {
use crate::errors::{BgpError, FsmSubcode};
let ctx = &default_v6_context();
let msg = NotificationMessage {
error: BgpError::FsmError(FsmSubcode::UnexpectedEstablished),
data: vec![],
};
let mut buf = BytesMut::new();
msg.to_wire(ctx, &mut buf)?;
assert_eq!(buf.as_ref(), &[5, 3]);
let (rem, parsed) = NotificationMessage::from_wire(ctx, &buf).unwrap();
assert!(rem.is_empty());
assert_eq!(parsed, msg);
Ok(())
}
}
+4 -3
View File
@@ -12,7 +12,7 @@ use nom::{
use serde::{Deserialize, Serialize};
use strum::EnumDiscriminants;
use crate::{
use bgp_primitives::{
constants::{AddressFamilyId, SubsequentAfi},
parser::{BgpParserError, ParserContext, ProtocolErrorValues},
};
@@ -814,9 +814,10 @@ mod tests {
use bytes::BytesMut;
use eyre::Result;
use crate::{
use crate::open::{Capability, ExtendedNextHop, GracefulRestartEntry, OpenMessage, OpenOption};
use bgp_primitives::{
constants::{AddressFamilyId, SubsequentAfi},
open::{Capability, ExtendedNextHop, GracefulRestartEntry, OpenMessage, OpenOption},
parser::ParserContext,
};
+19
View File
@@ -0,0 +1,19 @@
[package]
name = "bgp-primitives"
description = "Primitive types for working with routing information"
edition.workspace = true
homepage.workspace = true
license.workspace = true
repository.workspace = true
version.workspace = true
[dependencies]
bitfield.workspace = true
bytes.workspace = true
eyre.workspace = true
nom.workspace = true
serde.workspace = true
serde_repr.workspace = true
thiserror.workspace = true
strum.workspace = true
File renamed without changes.
File renamed without changes.
+4
View File
@@ -0,0 +1,4 @@
pub mod constants;
pub mod ip_prefix;
pub mod parser;
pub mod poptrie;
File renamed without changes.
+176
View File
@@ -0,0 +1,176 @@
use std::collections::BTreeMap;
#[derive(Debug)]
pub struct Poptrie<T> {
pub inners: Vec<Inner>,
pub leaves: Vec<Leaf<T>>,
}
#[derive(Debug)]
pub struct Inner {
/// A bit vector that describes the child internal nodes.
pub internal_v: u64,
/// A bit vector that describes the child leaf nodes.
pub leaf_v: u64,
/// Starting offset where the child interior nodes of this node begins.
pub inner_offset: u64,
/// Starting offset where the child leaf nodes of this node begins.
pub leaf_offset: u64,
}
#[derive(Debug)]
pub struct Leaf<T> {
pub data: T,
}
#[inline]
fn extract(width: u8, offset: u8, v: u32, bits: u8) -> u8 {
let shift = bits.saturating_sub(width * (offset + 1));
let mask = 0b111111 << shift;
((v & mask) >> shift) as u8
}
fn extract_32(width: u8, offset: u8, v: u32) -> u8 {
extract(width, offset, v, 32)
}
fn extract_128(width: u8, offset: u8, v: u32) -> u8 {
extract(width, offset, v, 128)
}
impl<T: Clone> Poptrie<T> {
pub fn match_u32(&self, addr: u32) -> Option<&T> {
let mut i = 0_u64;
let mut v = self.inners.get(i as usize)?.internal_v;
let mut offset = 0;
let mut n = extract(6, offset, addr, 32);
let mut result = None;
while (v & (1 << n)) != 0 {
let base = self.inners.get(i as usize)?.inner_offset;
let arg = v & ((2 << n) - 1);
let bc = arg.count_ones() as u64;
i = base + bc - 1;
v = self.inners.get(i as usize)?.internal_v;
offset += 1;
n = extract(6, offset, addr, 32);
let base = self.inners.get(i as usize)?.leaf_offset;
let v = self.inners.get(i as usize)?.leaf_v;
if (v & (1 << n)) != 0 {
let i = base - 1;
result = Some(&self.leaves.get(i as usize)?.data);
}
}
let base = self.inners.get(i as usize)?.leaf_offset;
let v = self.inners.get(i as usize)?.leaf_v;
if (v & (1 << n)) != 0 {
i = base - 1;
result = Some(&self.leaves.get(i as usize)?.data);
}
result
}
fn new(&mut self, rt: BTreeMap<(u32, u8), T>) {
const DEPTH: u8 = 6;
let mut forest = vec![(0, rt)];
let mut i_offset = 1;
for _ in 0..DEPTH {
let mut subforest = Vec::<(u8, BTreeMap<(u32, u8), T>)>::new();
let mut children = 0;
let mut siblings = 0;
for (_, tree) in &forest {
let mut iv = 0_u64;
let mut lv = 0_u64;
let mut subsubforest = Vec::<(u8, BTreeMap<(u32, u8), T>)>::new();
for (r, e) in tree {
let k = extract(6, DEPTH, r.0, 32);
let consumed = core::cmp::min((DEPTH + 1) * 6, 32);
if r.1 < consumed {
if ((1 << k) & iv) == 0 {
lv |= 1 << k;
self.leaves.push(Leaf { data: e.clone() });
}
// If the prefix of the route entry is less than but not equal
// to the consumed number of bits, we need to add those bits
// to the bitvec.
if r.1 != consumed {
// Shift the extra bits and add to the bitvec for this
// internal node.
let extra = 1_u32 << (consumed - r.1);
for i in 1..(extra).min(64 - k as u32) {
lv |= 1_u64 << (k as u32 + i);
self.leaves.push(Leaf { data: e.clone() });
}
}
continue;
}
iv |= 1 << k;
match subsubforest.iter_mut().find(|x| x.0 == k) {
Some(ref mut entry) => {
entry.1.insert(*r, e.clone());
}
None => {
let mut tbl = BTreeMap::new();
tbl.insert(*r, e.clone());
subsubforest.push((k, tbl));
if iv > 0 {
children += 1;
}
}
}
}
if iv > 0 || lv > 0 {
self.inners.push(Inner {
internal_v: iv,
leaf_v: lv,
inner_offset: if iv > 0 { i_offset + siblings } else { 0 },
leaf_offset: self.leaves.len() as u64,
});
if iv > 0 {
siblings += 1;
}
}
subforest.extend_from_slice(subsubforest.as_slice());
}
i_offset += children;
forest = subforest;
}
}
}
#[cfg(test)]
mod tests {
use std::{collections::BTreeMap, net::Ipv4Addr};
use crate::poptrie::Poptrie;
#[test]
fn test_poptrie() {
let mut map: BTreeMap<(u32, u8), Ipv4Addr> = Default::default();
map.insert(
(Ipv4Addr::new(127, 0, 0, 0).into(), 8),
Ipv4Addr::new(10, 0, 0, 1),
);
let mut poptrie: Poptrie<Ipv4Addr> = Poptrie {
inners: vec![],
leaves: vec![],
};
poptrie.new(map);
println!("Success");
dbg!(poptrie);
}
}
+15
View File
@@ -0,0 +1,15 @@
[package]
name = "bgp-server"
edition.workspace = true
[dependencies]
clap.workspace = true
eyre.workspace = true
prometheus.workspace = true
tokio.workspace = true
tracing-subscriber.workspace = true
tracing.workspace = true
protobuf.workspace = true
[build-dependencies]
protobuf-codegen.workspace = true
+9
View File
@@ -0,0 +1,9 @@
use protobuf_codegen::Codegen;
fn main() {
Codegen::new()
.input("proto/config.proto")
.includes(&["proto"])
.cargo_out_dir("protos")
.run_from_script();
}
+32
View File
@@ -0,0 +1,32 @@
syntax = "proto3";
package bgp.server;
// Top level BGP Server configuration.
message BgpConfig {
// The IPv4 address used as the router ID in BGP.
string router_id = 1;
// The local AS Number to use when communicating with peers.
uint32 local_asn = 2;
// Endpoints to listen on.
repeated TcpEndpoint listen_endpoints = 3;
// Peers configured on this server.
repeated Peer peers = 4;
}
// Represents an IP address and port that can be used for listening or connecting.
message TcpEndpoint {
// Address to listen on
string address = 1;
// Port to listen on.
uint32 port = 2;
}
// Configuration of a BGP peer.
message Peer {
string name = 1;
TcpEndpoint address = 2;
uint32 remote_asn = 3;
}
+5
View File
@@ -0,0 +1,5 @@
pub mod tracing;
pub mod proto {
include!(concat!(env!("OUT_DIR"), "/protos/mod.rs"));
}
+26
View File
@@ -0,0 +1,26 @@
use bgp_server::proto;
use clap::Parser;
use eyre::{Context, Result};
use tracing::info;
use crate::proto::config::BgpConfig;
#[derive(Parser, Debug)]
struct ServerArgs {
/// Path to the configuration file.
config: String,
}
#[tokio::main]
async fn main() -> Result<()> {
info!("Starting bgp server");
let args = ServerArgs::parse();
let config_string =
std::fs::read_to_string(args.config).wrap_err("Failed to open config file")?;
let config = protobuf::text_format::parse_from_str::<BgpConfig>(&config_string)?;
info!("Read config file: {}", config);
Ok(())
}
+30
View File
@@ -0,0 +1,30 @@
use clap::Args;
use eyre::Result;
use tracing::Level;
use tracing_subscriber::EnvFilter;
/// Default targets that we enable logging for.
const DEFAULT_TARGETS: [&str; 1] = ["bgp_server"];
#[derive(Debug, Args)]
pub struct TracingArgs {
/// Set the minimum log level.
#[arg(short, long, global = true, default_value_t = Level::TRACE, help_heading = "Tracing")]
level: Level,
/// Disable logging to stdout.
#[arg(long, global = true, default_value_t = false, help_heading = "Tracing")]
quiet: bool,
}
impl TracingArgs {
pub fn install(&self) -> Result<()> {
let filter = DEFAULT_TARGETS.into_iter().try_fold(
EnvFilter::builder().from_env_lossy(),
|filter, target| -> Result<_> {
Ok(filter.add_directive(format!("{}={}", target, self.level).parse()?))
},
)?;
Ok(())
}
}