Parse the Multiple Labels capability instead of panicking

The capability parser had a todo!() for capability code 8 (RFC 8277), and
the encoder two more. A peer advertising it took the session task down.
It is now parsed into its (AFI, SAFI, count) entries and round-trips.

Also carries clippy's mechanical fixes for this file.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Rayhaan JaufeerallyandClaude Fable 5.1 committed 2026-10-01 21:01:26 +02:00
1 parent 2ab6c1c38b
commit 126368852a
1 file changed
+80 -14
+80 -14
View File
@@ -171,11 +171,9 @@ impl OpenOption {
} }
Ok((buf, Self::Capabilities(capabilities))) Ok((buf, Self::Capabilities(capabilities)))
} }
other => { other => Err(Failure(BgpParserError::ProtocolError(
return Err(Failure(BgpParserError::ProtocolError( ProtocolErrorValues::UnknownOpenOption(other),
ProtocolErrorValues::UnknownOpenOption(other), ))),
)));
}
} }
} }
@@ -207,6 +205,15 @@ impl OpenOption {
} }
} }
/// One address family in a Multiple Labels capability (RFC 8277 §2.1).
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub struct MultiLabelEntry {
pub afi: u16,
pub safi: u8,
/// Maximum number of labels in one NLRI of this family.
pub count: u8,
}
#[derive(Debug, Serialize, Deserialize, EnumDiscriminants, PartialEq, Eq)] #[derive(Debug, Serialize, Deserialize, EnumDiscriminants, PartialEq, Eq)]
#[repr(u8)] #[repr(u8)]
pub enum Capability { pub enum Capability {
@@ -235,8 +242,11 @@ pub enum Capability {
header: BgpSecHeader, header: BgpSecHeader,
afi: AddressFamilyId, afi: AddressFamilyId,
} = 7, } = 7,
/// Multiple labels compatibility (RFC 8277). /// Multiple labels (RFC 8277 §2.1): for each address family, how many
MultiLabelCompat {} = 8, /// labels the speaker can handle in one NLRI.
MultiLabelCompat {
entries: Vec<MultiLabelEntry>,
} = 8,
BgpRole { BgpRole {
role: BgpRole, role: BgpRole,
} = 9, } = 9,
@@ -292,7 +302,7 @@ impl Capability {
Capability::ExtendedNextHop { .. } => CapabilityDiscriminants::ExtendedNextHop as u8, Capability::ExtendedNextHop { .. } => CapabilityDiscriminants::ExtendedNextHop as u8,
Capability::ExtendedMessage => CapabilityDiscriminants::ExtendedMessage as u8, Capability::ExtendedMessage => CapabilityDiscriminants::ExtendedMessage as u8,
Capability::BgpSec { .. } => CapabilityDiscriminants::BgpSec as u8, Capability::BgpSec { .. } => CapabilityDiscriminants::BgpSec as u8,
Capability::MultiLabelCompat {} => CapabilityDiscriminants::MultiLabelCompat as u8, Capability::MultiLabelCompat { .. } => CapabilityDiscriminants::MultiLabelCompat as u8,
Capability::BgpRole { .. } => CapabilityDiscriminants::BgpRole as u8, Capability::BgpRole { .. } => CapabilityDiscriminants::BgpRole as u8,
Capability::GracefulRestart { .. } => CapabilityDiscriminants::GracefulRestart as u8, Capability::GracefulRestart { .. } => CapabilityDiscriminants::GracefulRestart as u8,
Capability::FourByteAsn { .. } => CapabilityDiscriminants::FourByteAsn as u8, Capability::FourByteAsn { .. } => CapabilityDiscriminants::FourByteAsn as u8,
@@ -414,11 +424,25 @@ impl Capability {
)) ))
} }
v if v == CapabilityDiscriminants::MultiLabelCompat as u8 => { v if v == CapabilityDiscriminants::MultiLabelCompat as u8 => {
todo!() // The AFI and SAFI are kept as numbers: this capability is
// used with labelled families we have no named constant for.
let parse_entry = |buf: &'a [u8]| -> IResult<
&'a [u8],
MultiLabelEntry,
BgpParserError<&'a [u8]>,
> {
let (buf, afi) = be_u16(buf)?;
let (buf, safi) = be_u8(buf)?;
let (buf, count) = be_u8(buf)?;
Ok((buf, MultiLabelEntry { afi, safi, count }))
};
let (cap_buf, entries) = nom::multi::many1(parse_entry).parse(cap_buf)?;
require_empty!(cap_buf, "MultiLabelCompat");
Ok((buf, Self::MultiLabelCompat { entries }))
} }
v if v == CapabilityDiscriminants::BgpRole as u8 => { v if v == CapabilityDiscriminants::BgpRole as u8 => {
let (cap_buf, role) = be_u8(cap_buf)?; let (cap_buf, role) = be_u8(cap_buf)?;
let role = BgpRole::try_from(role).map_err(|e| fail!("{}", e))?; let role = BgpRole::from(role);
require_empty!(cap_buf, "BgpRole"); require_empty!(cap_buf, "BgpRole");
Ok((buf, Self::BgpRole { role })) Ok((buf, Self::BgpRole { role }))
} }
@@ -555,7 +579,13 @@ impl Capability {
out.put_u8(header.0); out.put_u8(header.0);
out.put_u16(*afi as u16); out.put_u16(*afi as u16);
} }
Capability::MultiLabelCompat {} => todo!(), Capability::MultiLabelCompat { entries } => {
for entry in entries {
out.put_u16(entry.afi);
out.put_u8(entry.safi);
out.put_u8(entry.count);
}
}
Capability::BgpRole { role } => { Capability::BgpRole { role } => {
out.put_u8(role.into()); out.put_u8(role.into());
} }
@@ -600,7 +630,7 @@ impl Capability {
for llgr in llgrs { for llgr in llgrs {
out.put_u16(llgr.afi as u16); out.put_u16(llgr.afi as u16);
out.put_u8(llgr.safi as u8); out.put_u8(llgr.safi as u8);
out.put_u8(llgr.flags as u8); out.put_u8(llgr.flags);
out.put(&llgr.stale_time.to_be_bytes()[1..]); out.put(&llgr.stale_time.to_be_bytes()[1..]);
} }
} }
@@ -621,7 +651,7 @@ impl Capability {
Capability::ExtendedNextHop { entries } => 6 * entries.len() as u8, Capability::ExtendedNextHop { entries } => 6 * entries.len() as u8,
Capability::ExtendedMessage => 0, Capability::ExtendedMessage => 0,
Capability::BgpSec { .. } => 3, Capability::BgpSec { .. } => 3,
Capability::MultiLabelCompat {} => todo!(), Capability::MultiLabelCompat { entries } => 4 * entries.len() as u8,
Capability::BgpRole { .. } => 1, Capability::BgpRole { .. } => 1,
Capability::GracefulRestart { entries, .. } => 2 + 4 * entries.len() as u8, Capability::GracefulRestart { entries, .. } => 2 + 4 * entries.len() as u8,
Capability::FourByteAsn { .. } => 4, Capability::FourByteAsn { .. } => 4,
@@ -818,7 +848,9 @@ mod tests {
use bytes::BytesMut; use bytes::BytesMut;
use eyre::Result; use eyre::Result;
use crate::open::{Capability, ExtendedNextHop, GracefulRestartEntry, OpenMessage, OpenOption}; use crate::open::{
Capability, ExtendedNextHop, GracefulRestartEntry, MultiLabelEntry, OpenMessage, OpenOption,
};
use bgp_primitives::{ use bgp_primitives::{
constants::{AddressFamilyId, SubsequentAfi}, constants::{AddressFamilyId, SubsequentAfi},
@@ -986,6 +1018,40 @@ mod tests {
/// Returning the (empty) per-capability buffer instead makes the list /// Returning the (empty) per-capability buffer instead makes the list
/// parser stop early and silently lose every later capability, e.g. the /// parser stop early and silently lose every later capability, e.g. the
/// 4-octet ASN that routers list after graceful restart. /// 4-octet ASN that routers list after graceful restart.
/// A peer advertising Multiple Labels used to hit a `todo!()` and take
/// the session task down with it.
#[test]
fn multiple_labels_capability_round_trips() {
let ctx = ParserContext::default();
// code 8, length 8: (IPv4, labelled unicast, 2) and (IPv6, labelled unicast, 3)
let wire = [0x08, 0x08, 0x00, 0x01, 0x04, 0x02, 0x00, 0x02, 0x04, 0x03];
let (rest, cap) = Capability::from_wire(&ctx, &wire).unwrap();
assert!(rest.is_empty());
assert_eq!(
cap,
Capability::MultiLabelCompat {
entries: vec![
MultiLabelEntry {
afi: 1,
safi: 4,
count: 2
},
MultiLabelEntry {
afi: 2,
safi: 4,
count: 3
},
]
}
);
let mut out = BytesMut::new();
cap.to_wire(&ctx, &mut out).unwrap();
assert_eq!(&out[..], &wire[..]);
// A truncated entry is a parse error, not a panic.
assert!(Capability::from_wire(&ctx, &[0x08, 0x03, 0x00, 0x01, 0x04]).is_err());
}
#[test] #[test]
fn capability_parsers_leave_the_following_bytes() { fn capability_parsers_leave_the_following_bytes() {
let ctx = ParserContext::default(); let ctx = ParserContext::default();