Cleanup and implementation of Update / Notification parser / serializers.
This commit is contained in:
1 parent
dfe066c9ab
commit
5ae8f6e249
22 files changed
+1573
-65
No files matched your search
+13
-12
@@ -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
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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(())
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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.
@@ -0,0 +1,4 @@
|
||||
pub mod constants;
|
||||
pub mod ip_prefix;
|
||||
pub mod parser;
|
||||
pub mod poptrie;
|
||||
File renamed without changes.
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
pub mod tracing;
|
||||
|
||||
pub mod proto {
|
||||
include!(concat!(env!("OUT_DIR"), "/protos/mod.rs"));
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user