Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions bgp/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,9 @@ pub enum Error {
#[error("Unexpected ASN: {0}")]
UnexpectedAsn(ExpectationMismatch<u32>),

#[error("Peer used reserved AS 0 (RFC 7607)")]
ReservedPeerAsn,

#[error("Hold time too small")]
HoldTimeTooSmall,

Expand Down
184 changes: 184 additions & 0 deletions bgp/src/messages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use nom::{
number::complete::{be_u8, be_u16, be_u32, u8 as parse_u8},
};
use path_attribute_flags::*;
use rdb::Asn;
use std::{
collections::{BTreeSet, HashSet},
fmt::{Display, Formatter},
Expand Down Expand Up @@ -1373,6 +1374,11 @@ pub fn path_attribute_value_from_wire(
segments.push(seg);
input = out;
}
if as_path_contains_reserved_asn(&segments) {
return Err(UpdateParseErrorReason::ReservedAsZero {
type_code: PathAttributeTypeCode::AsPath,
});
}
Ok(PathAttributeValue::As4Path(segments))
}
PathAttributeTypeCode::NextHop => {
Expand Down Expand Up @@ -1425,6 +1431,11 @@ pub fn path_attribute_value_from_wire(
segments.push(seg);
input = out;
}
if as_path_contains_reserved_asn(&segments) {
return Err(UpdateParseErrorReason::ReservedAsZero {
type_code: PathAttributeTypeCode::As4Path,
});
}
Ok(PathAttributeValue::As4Path(segments))
}
PathAttributeTypeCode::Communities => {
Expand Down Expand Up @@ -1506,6 +1517,11 @@ pub fn path_attribute_value_from_wire(
detail: e,
}
})?;
if Asn::from(agg.asn).is_reserved() {
return Err(UpdateParseErrorReason::ReservedAsZero {
type_code: PathAttributeTypeCode::Aggregator,
});
}
Ok(PathAttributeValue::Aggregator(agg))
}
PathAttributeTypeCode::As4Aggregator => {
Expand All @@ -1524,6 +1540,11 @@ pub fn path_attribute_value_from_wire(
detail: e,
}
})?;
if Asn::from(agg.asn).is_reserved() {
return Err(UpdateParseErrorReason::ReservedAsZero {
type_code: PathAttributeTypeCode::As4Aggregator,
});
}
Ok(PathAttributeValue::As4Aggregator(agg))
}
}
Expand Down Expand Up @@ -1555,6 +1576,14 @@ pub fn as4_path_segment_to_wire(
Ok(buf)
}

/// RFC 7607: AS 0 is reserved and MUST NOT appear in an AS path attribute.
fn as_path_contains_reserved_asn(segments: &[As4PathSegment]) -> bool {
segments
.iter()
.flat_map(|seg| seg.value.iter())
.any(|asn| Asn::from(*asn).is_reserved())
}

pub fn as4_path_segment_from_wire(
input: &[u8],
) -> Result<(&[u8], As4PathSegment), Error> {
Expand Down Expand Up @@ -3087,6 +3116,161 @@ mod tests {
assert!(!msg.nlri.is_empty(), "Expected NLRI to be present");
}

// =========================================================================
// RFC 7607 (AS 0) tests
// =========================================================================

/// Append a path-attributes length and NLRI to a partial UPDATE buffer,
/// patching the length field at `len_offset`.
fn finish_update(buf: &mut Vec<u8>, len_offset: usize, attrs_start: usize) {
let attrs_len = (buf.len() - attrs_start) as u16;
buf[len_offset..len_offset + 2]
.copy_from_slice(&attrs_len.to_be_bytes());
// NLRI: 198.51.100.0/24
buf.push(24);
buf.extend_from_slice(&[198, 51, 100]);
}

/// RFC 7607: AS 0 in AS_PATH makes the UPDATE malformed (treat-as-withdraw).
#[test]
fn as_path_reserved_as_zero_treated_as_withdraw() {
let mut buf = Vec::new();
buf.extend_from_slice(&0u16.to_be_bytes()); // withdrawn routes length
let len_offset = buf.len();
buf.extend_from_slice(&0u16.to_be_bytes()); // path attrs length
let attrs_start = buf.len();

// ORIGIN (IGP)
buf.extend_from_slice(&[0x40, 1, 1, 0]);
// AS_PATH: AS_SEQUENCE with a single AS of 0
buf.extend_from_slice(&[0x40, 2, 6, 2, 1]);
buf.extend_from_slice(&0u32.to_be_bytes());
// NEXT_HOP
buf.extend_from_slice(&[0x40, 3, 4, 192, 0, 2, 1]);

finish_update(&mut buf, len_offset, attrs_start);

let msg = update_message_from_wire(&buf)
.expect("parse should succeed with treat-as-withdraw");
assert!(
treat_as_withdraw(&msg.errors),
"AS 0 in AS_PATH should be treat-as-withdraw"
);
assert!(
msg.errors.iter().any(|(reason, action)| matches!(
reason,
UpdateParseErrorReason::ReservedAsZero {
type_code: PathAttributeTypeCode::AsPath
}
) && matches!(
action,
AttributeAction::TreatAsWithdraw
)),
"expected ReservedAsZero(AsPath) treat-as-withdraw, got {:?}",
msg.errors
);
}

/// RFC 7607: AS 0 in AGGREGATOR makes the UPDATE malformed; per RFC 7606
/// the AGGREGATOR is an informational attribute, so it is discarded while
/// the rest of the UPDATE is retained.
#[test]
fn aggregator_reserved_as_zero_discarded() {
let mut buf = Vec::new();
buf.extend_from_slice(&0u16.to_be_bytes());
let len_offset = buf.len();
buf.extend_from_slice(&0u16.to_be_bytes());
let attrs_start = buf.len();

// ORIGIN, AS_PATH (AS 65000), NEXT_HOP - all valid
buf.extend_from_slice(&[0x40, 1, 1, 0]);
buf.extend_from_slice(&[0x40, 2, 6, 2, 1]);
buf.extend_from_slice(&65000u32.to_be_bytes());
buf.extend_from_slice(&[0x40, 3, 4, 192, 0, 2, 1]);
// AGGREGATOR: 2-byte AS of 0 + IPv4
buf.extend_from_slice(&[0xc0, 7, 6, 0, 0, 192, 0, 2, 1]);

finish_update(&mut buf, len_offset, attrs_start);

let msg = update_message_from_wire(&buf).expect("parse should succeed");
assert!(
!treat_as_withdraw(&msg.errors),
"AGGREGATOR error must not withdraw the route"
);
assert!(
msg.errors.iter().any(|(reason, action)| matches!(
reason,
UpdateParseErrorReason::ReservedAsZero {
type_code: PathAttributeTypeCode::Aggregator
}
) && matches!(
action,
AttributeAction::Discard
)),
"expected ReservedAsZero(Aggregator) discard, got {:?}",
msg.errors
);
assert!(
!msg.path_attributes.iter().any(|pa| matches!(
pa.value,
PathAttributeValue::Aggregator(_)
)),
"AGGREGATOR with AS 0 should be discarded"
);
}

/// RFC 7607: AS 0 in AS4_AGGREGATOR is handled the same as AGGREGATOR.
#[test]
fn as4_aggregator_reserved_as_zero_discarded() {
let mut buf = Vec::new();
buf.extend_from_slice(&0u16.to_be_bytes());
let len_offset = buf.len();
buf.extend_from_slice(&0u16.to_be_bytes());
let attrs_start = buf.len();

buf.extend_from_slice(&[0x40, 1, 1, 0]);
buf.extend_from_slice(&[0x40, 2, 6, 2, 1]);
buf.extend_from_slice(&65000u32.to_be_bytes());
buf.extend_from_slice(&[0x40, 3, 4, 192, 0, 2, 1]);
// AS4_AGGREGATOR: 4-byte AS of 0 + IPv4
buf.extend_from_slice(&[0xc0, 18, 8]);
buf.extend_from_slice(&0u32.to_be_bytes());
buf.extend_from_slice(&[192, 0, 2, 1]);

finish_update(&mut buf, len_offset, attrs_start);

let msg = update_message_from_wire(&buf).expect("parse should succeed");
assert!(!treat_as_withdraw(&msg.errors));
assert!(
msg.errors.iter().any(|(reason, action)| matches!(
reason,
UpdateParseErrorReason::ReservedAsZero {
type_code: PathAttributeTypeCode::As4Aggregator
}
) && matches!(
action,
AttributeAction::Discard
)),
"expected ReservedAsZero(As4Aggregator) discard, got {:?}",
msg.errors
);
assert!(!msg.path_attributes.iter().any(|pa| matches!(
pa.value,
PathAttributeValue::As4Aggregator(_)
)));
}

/// RFC 7607: a peer that advertises AS 0 in its OPEN must be detected as
/// using a reserved ASN (the session layer rejects it with Bad Peer AS).
#[test]
fn open_peer_as_zero_is_reserved() {
let om = OpenMessage::new4(0, 30, 0xaabbccdd, false);
assert!(Asn::from(om.asn()).is_reserved());

let om = OpenMessage::new2(0, 30, 0xaabbccdd, false);
assert!(Asn::from(om.asn()).is_reserved());
}

// =========================================================================
// BgpNexthop tests
// =========================================================================
Expand Down
3 changes: 2 additions & 1 deletion bgp/src/proptest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,8 @@ fn path_origin_strategy() -> impl Strategy<Value = PathOrigin> {
fn as_path_segment_strategy() -> impl Strategy<Value = As4PathSegment> {
(
prop_oneof![Just(AsPathType::AsSet), Just(AsPathType::AsSequence)],
prop::collection::vec(any::<u32>(), 1..5),
// RFC 7607: AS 0 is reserved and rejected on parse, so never generate it.
prop::collection::vec(1..=u32::MAX, 1..5),
)
.prop_map(|(typ, value)| As4PathSegment { typ, value })
}
Expand Down
9 changes: 9 additions & 0 deletions bgp/src/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7173,6 +7173,15 @@ impl<Cnx: BgpConnection + 'static> SessionRunner<Cnx> {
/// Handle an open message
fn handle_open(&self, conn: &Cnx, om: &OpenMessage) -> Result<(), Error> {
let remote_asn = om.asn();
if Asn::from(remote_asn).is_reserved() {
self.send_notification(
conn,
ErrorCode::Open,
ErrorSubcode::Open(OpenErrorSubcode::BadPeerAS),
);
self.unregister_conn(conn.id());
return Err(Error::ReservedPeerAsn);
}
if let Some(expected_remote_asn) = lock!(self.session).remote_asn
&& remote_asn != expected_remote_asn
{
Expand Down
9 changes: 9 additions & 0 deletions mg-api-types/versions/src/impls/bgp/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ pub enum UpdateParseErrorReason {
InvalidOriginValue { value: u8 },
/// AS_PATH attribute is malformed
MalformedAsPath { detail: String },
/// Reserved AS 0 found in an AS path or aggregator attribute (RFC 7607)
ReservedAsZero { type_code: PathAttributeTypeCode },
/// Attribute flags are invalid for this type
InvalidAttributeFlags { type_code: u8, flags: u8 },

Expand Down Expand Up @@ -159,6 +161,13 @@ impl Display for UpdateParseErrorReason {
Self::InvalidOriginValue { value } => {
write!(f, "invalid ORIGIN value: {}", value)
}
Self::ReservedAsZero { type_code } => {
write!(
f,
"reserved AS 0 in {:?} attribute (RFC 7607)",
type_code
)
}
Self::MalformedAsPath { detail } => {
write!(f, "malformed AS_PATH: {}", detail)
}
Expand Down
7 changes: 7 additions & 0 deletions mgd/src/bgp_admin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2371,6 +2371,13 @@ pub(crate) mod helpers {
id: rq.id,
};

if cfg.asn.is_reserved() {
return Err(Error::InvalidRequest(
"AS 0 is reserved and cannot be used as a local ASN (RFC 7607)"
.into(),
));
}

let db = ctx.db.clone();

let router = Arc::new(Router::<BgpConnectionTcp>::new(
Expand Down
8 changes: 8 additions & 0 deletions mgd/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,14 @@ fn start_bgp_routers(
dlog!(context.log, info, "starting bgp routers: {routers:#?}");
let mut guard = context.bgp.router.lock().expect("lock bgp routers");
for (asn, info) in routers {
if rdb::Asn::FourOctet(asn).is_reserved() {
dlog!(
context.log,
warn,
"skipping persisted BGP router with reserved AS {asn} (RFC 7607)"
);
continue;
}
bgp_admin::helpers::add_router(
context.clone(),
mg_api_types::bgp::config::Router {
Expand Down
16 changes: 16 additions & 0 deletions rdb/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -252,12 +252,19 @@ impl From<u16> for Asn {
}

impl Asn {
/// AS 0 is reserved by IANA and MUST NOT appear on the wire (RFC 7607).
pub const RESERVED: u32 = 0;

pub fn as_u32(&self) -> u32 {
match self {
Self::TwoOctet(value) => u32::from(*value),
Self::FourOctet(value) => *value,
}
}

pub fn is_reserved(&self) -> bool {
self.as_u32() == Self::RESERVED
}
}

pub fn to_buf<T: ?Sized + Serialize>(value: &T) -> Result<Vec<u8>> {
Expand Down Expand Up @@ -410,6 +417,15 @@ mod test {
}
}

/// AS 0 is reserved (RFC 7607) regardless of two- or four-octet encoding.
#[test]
fn asn_reserved_is_zero_only() {
assert!(Asn::TwoOctet(0).is_reserved());
assert!(Asn::FourOctet(0).is_reserved());
assert!(!Asn::TwoOctet(1).is_reserved());
assert!(!Asn::FourOctet(65000).is_reserved());
}

/// BGP path identity is purely PeerId. Two paths with the
/// same PeerId are Equal regardless of all other fields.
/// Different PeerIds are never Equal, even when everything
Expand Down