Files
ferrix/crates/packet/src/update_decode.rs
T
Rayhaan JaufeerallyandClaude Fable 5.1 673920d48b Names that said the same thing twice, or two things once
- PathAttribute::UnknownPathAttribute and Capability::UnknownCapability
  are PathAttribute::Unknown and Capability::Unknown: the enum already
  says what kind of thing is unknown.
- The capability entry types are all named XxxEntry now
  (ExtendedNextHopEntry, LongLivedGracefulRestartEntry), rather than two
  of them sharing a name with the variant that holds a list of them; and
  MultiLabelCompat is MultiLabel, since nothing explained the "Compat".
- Two different things were both called Reflection: what a session needs
  to reflect routes (export.rs, still Reflection) and the identifiers a
  reflected route must not carry back to us (policy.rs, now
  ReflectorIds).
- well_known_allow's peer_is_internal is ibgp, as everywhere else.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-06 11:35:12 +02:00

1215 lines
45 KiB
Rust

//! Decoding of UPDATE messages with the error handling of RFC 7606.
//!
//! RFC 4271 answered every malformed UPDATE by resetting the session, which
//! withdraws every route learned over it. One bad attribute, often
//! originated by a router several ASes away and passed on faithfully by a
//! peer that did not look at it, could take down a session carrying a full
//! table. RFC 7606 keeps the session where that is safe:
//!
//! - **Treat-as-withdraw.** The routes the UPDATE announces are handled as
//! if it had withdrawn them. Wrong attributes must not be used, and
//! keeping the routes' previous attributes would be wrong too.
//! - **Attribute discard.** The attribute is dropped and the routes are
//! kept, for the few attributes that have no effect on route selection.
//! - **Session reset** remains for errors that leave the prefixes
//! themselves in doubt: if we cannot tell which routes a message is
//! about, we cannot withdraw them.
//!
//! [`UpdateMessage::decode`] applies this. It returns the UPDATE as it is to
//! be acted on, with treat-as-withdraw and attribute discard already carried
//! out, together with a report of what was wrong, for the log.
use bgp_primitives::constants::{AfiSafi, IpVersion};
use bgp_primitives::ip_prefix::Nlri;
use bgp_primitives::parser::ParserContext;
use crate::errors::{BgpError, DecodeError, UpdateMsgSubcode};
use crate::message::attr_type::*;
use crate::message::{
MpUnreachNlriPathAttribute, NotAnAttribute, PathAttribute, RawAttribute, UpdateMessage,
defined_flags, describe, parse_nlri_list, split_attribute,
};
/// How an error in an UPDATE was handled, short of resetting the session.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Handling {
/// The routes the UPDATE announced were withdrawn instead.
TreatAsWithdraw,
/// The attribute was dropped; the routes were kept.
AttributeDiscard,
}
impl Handling {
pub fn as_str(self) -> &'static str {
match self {
Self::TreatAsWithdraw => "treat-as-withdraw",
Self::AttributeDiscard => "attribute-discard",
}
}
}
/// Something wrong in an UPDATE that the session survived.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UpdateError {
pub handling: Handling,
/// Type code of the attribute at fault, when it is one attribute.
pub attribute: Option<u8>,
pub detail: String,
}
/// An UPDATE as it is to be acted on, and what was wrong with it as sent.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DecodedUpdate {
/// The UPDATE after treat-as-withdraw and attribute discard.
pub update: UpdateMessage,
/// Empty when the UPDATE was well formed.
pub errors: Vec<UpdateError>,
/// The attributes in `update`, as they were on the wire, for a table
/// that keeps attributes in that form and need not write them out
/// again. MP_REACH_NLRI and MP_UNREACH_NLRI are left out, since they
/// name prefixes, and so is every attribute that was discarded.
///
/// `None` when the UPDATE has no such attributes, when its routes were
/// withdrawn because of an error, or when the bytes are not in the one
/// form such a table keeps: on a session with two-octet AS numbers.
pub attributes_wire: Option<Vec<u8>>,
}
/// How a malformed value of the attribute with this type code is handled
/// (RFC 7606 §7, and RFC 6793 §6 for the AS4 attributes). `None` means the
/// session is reset: an error inside MP_REACH_NLRI or MP_UNREACH_NLRI leaves
/// it unknown which routes the UPDATE is about.
fn handling_of_malformed(type_code: u8) -> Option<Handling> {
match type_code {
MP_REACH_NLRI | MP_UNREACH_NLRI => None,
// These do not affect which route is selected.
ATOMIC_AGGREGATE | AGGREGATOR | AS4_PATH | AS4_AGGREGATOR => {
Some(Handling::AttributeDiscard)
}
_ => Some(Handling::TreatAsWithdraw),
}
}
fn reset(subcode: UpdateMsgSubcode, data: Vec<u8>, detail: String) -> DecodeError {
DecodeError {
notification: BgpError::UpdateMsg(subcode),
data,
detail,
}
}
/// Parses a field of the message body that is nothing but prefixes. An
/// error here cannot be recovered from: the prefixes are what
/// treat-as-withdraw would act on.
fn prefixes(ctx: &ParserContext, field: &str, buf: &[u8]) -> Result<Vec<Nlri>, DecodeError> {
// The prefixes in the body are IPv4 unicast (RFC 4271 §4.3).
let with_path_id = ctx.has_add_path(AfiSafi::IPV4_UNICAST);
match parse_nlri_list(IpVersion::Ipv4, with_path_id, buf) {
Ok((_, prefixes)) => Ok(prefixes),
Err(e) => Err(reset(
UpdateMsgSubcode::InvalidNetworkField,
vec![],
format!("malformed {field}: {}", describe(&e)),
)),
}
}
/// Splits off the field at the front of `buf` that begins with its own
/// two-octet length: the field, and what follows it. The two such fields
/// frame an UPDATE; if they do not fit, there is no telling where anything
/// is (RFC 7606 §3.e).
fn length_prefixed<'a>(buf: &'a [u8], what: &str) -> Result<(&'a [u8], &'a [u8]), DecodeError> {
let framing = |detail: String| {
reset(
UpdateMsgSubcode::MalformedAttrs,
vec![],
format!("malformed UPDATE: {detail}"),
)
};
let Some((length, rest)) = buf.split_first_chunk::<2>() else {
return Err(framing(format!("no room for the {what} length")));
};
let length = usize::from(u16::from_be_bytes(*length));
rest.split_at_checked(length)
.ok_or_else(|| framing(format!("{what} length {length} exceeds the message")))
}
impl UpdateMessage {
/// Decodes the body of an UPDATE message (what follows the 19-octet
/// header) with the error handling of RFC 7606.
///
/// `Err` is an error that resets the session, with the NOTIFICATION to
/// send. Every other error is handled here and listed in the result.
pub fn decode(ctx: &ParserContext, body: &[u8]) -> Result<DecodedUpdate, DecodeError> {
let (withdrawn_buf, body) = length_prefixed(body, "withdrawn routes")?;
let (attributes_buf, nlri_buf) = length_prefixed(body, "path attribute")?;
let withdrawn_routes = prefixes(ctx, "withdrawn routes", withdrawn_buf)?;
let announced_routes = prefixes(ctx, "NLRI", nlri_buf)?;
let mut reader = AttributeReader::new(ctx, attributes_buf.len());
reader.read_all(attributes_buf)?;
let AttributeReader {
attributes: path_attributes,
mut errors,
kept_wire,
..
} = reader;
let mut update = UpdateMessage {
withdrawn_routes,
path_attributes,
announced_routes,
};
let mut withdrawn = errors
.iter()
.any(|e| e.handling == Handling::TreatAsWithdraw);
if !withdrawn && let Some(missing) = missing_mandatory(ctx, &update) {
errors.push(UpdateError {
handling: Handling::TreatAsWithdraw,
attribute: Some(missing),
detail: format!("mandatory attribute {missing} is missing"),
});
withdrawn = true;
}
if withdrawn {
update = as_withdrawal(update);
}
let attributes_wire =
(!withdrawn && ctx.four_octet_asn && !kept_wire.is_empty()).then_some(kept_wire);
Ok(DecodedUpdate {
update,
errors,
attributes_wire,
})
}
}
/// Makes one path of the two a session with two-octet AS numbers brings:
/// AS_PATH becomes the path as it really is, and AS4_PATH is gone, used or
/// not (RFC 6793 §4.2.3).
///
/// For whoever receives an UPDATE to do once, before anything looks at
/// the path: the checks on it, the choice between routes, validation
/// against the RPKI now and again later. Were each to read the two for
/// itself, a peer could have them disagree about where a route comes
/// from.
pub fn merge_as4_path(attributes: &mut Vec<PathAttribute>) {
let Some(at) = attributes
.iter()
.position(|attribute| matches!(attribute, PathAttribute::As4Path(_)))
else {
return;
};
let wide = match attributes.remove(at) {
PathAttribute::As4Path(wide) => wide,
other => unreachable!("{other:?} is not the AS4_PATH found at {at}"),
};
for attribute in attributes {
if let PathAttribute::AsPath(path) = attribute {
if let Some(merged) = path.with_as4_path(&wide) {
*path = merged;
}
return;
}
}
}
/// What to do with an attribute, once it is clear.
enum Verdict {
/// Use it. `route_property` is false for one that was read only to find
/// the routes it names, and is no part of the routes' attributes.
Keep {
attribute: PathAttribute,
route_property: bool,
},
/// Leave it out, having noted why.
Skip,
}
/// Reads the path attributes of one UPDATE, leaving out the ones that are
/// discarded and noting every error that does not reset the session.
struct AttributeReader<'a> {
ctx: &'a ParserContext,
attributes: Vec<PathAttribute>,
errors: Vec<UpdateError>,
/// The bytes of the attributes that are kept and are a property of a
/// route: all but MP_REACH_NLRI and MP_UNREACH_NLRI.
kept_wire: Vec<u8>,
/// The type codes met so far, one bit each.
seen: [u64; 4],
}
impl<'a> AttributeReader<'a> {
fn new(ctx: &'a ParserContext, wire_len: usize) -> Self {
Self {
ctx,
attributes: Vec::new(),
errors: Vec::new(),
kept_wire: Vec::with_capacity(wire_len),
seen: [0; 4],
}
}
fn error(&mut self, handling: Handling, attribute: Option<u8>, detail: String) {
self.errors.push(UpdateError {
handling,
attribute,
detail,
});
}
/// Notes that `type_code` has been met, and returns whether it had
/// been already.
fn seen_before(&mut self, type_code: u8) -> bool {
let (word, bit) = (usize::from(type_code / 64), 1u64 << (type_code % 64));
let before = self.seen[word] & bit != 0;
self.seen[word] |= bit;
before
}
fn read_all(&mut self, mut buf: &[u8]) -> Result<(), DecodeError> {
while !buf.is_empty() {
let (raw, rest) = match split_attribute(buf) {
Ok(split) => split,
Err(e) => {
// An attribute that does not fit in what is left of the
// attributes (RFC 7606 §4). Where the next one would
// start is unknown, so the rest is not read. Routes
// found so far are withdrawn.
let attribute = match e {
NotAnAttribute::NoHeader => None,
NotAnAttribute::Truncated { type_code, .. } => Some(type_code),
};
self.error(Handling::TreatAsWithdraw, attribute, e.to_string());
return Ok(());
}
};
buf = rest;
if let Verdict::Keep {
attribute,
route_property,
} = self.judge(&raw)?
{
if route_property {
self.kept_wire.extend_from_slice(raw.wire);
}
self.attributes.push(attribute);
}
}
Ok(())
}
/// Decides what becomes of one attribute. `Err` resets the session.
fn judge(&mut self, raw: &RawAttribute<'_>) -> Result<Verdict, DecodeError> {
let (flags, type_code, wire) = (&raw.flags, raw.type_code, raw.wire);
let names_routes = matches!(type_code, MP_REACH_NLRI | MP_UNREACH_NLRI);
// RFC 7606 §3.g: a second MP_REACH_NLRI or MP_UNREACH_NLRI makes it
// unclear which routes are meant; a second anything else is ignored.
if self.seen_before(type_code) {
if names_routes {
return Err(reset(
UpdateMsgSubcode::MalformedAttrs,
vec![],
format!("attribute {type_code} appears more than once"),
));
}
self.error(
Handling::AttributeDiscard,
Some(type_code),
format!("attribute {type_code} appears more than once; kept the first"),
);
return Ok(Verdict::Skip);
}
let Some((optional, transitive)) = defined_flags(type_code) else {
// An attribute we have no definition for. One marked well-known
// is one every speaker is required to understand (RFC 4271
// §6.3); an optional one is carried along as it is.
if !flags.optional() {
return Err(reset(
UpdateMsgSubcode::UnrecognizedWellKnownAttr,
wire.to_vec(),
format!("unrecognised well-known attribute {type_code}"),
));
}
return Ok(Verdict::Keep {
attribute: opaque(raw),
route_property: true,
});
};
let flags_are_wrong = flags.optional() != optional || flags.transitive() != transitive;
if flags_are_wrong {
// RFC 7606 §3.c. The value is still read below if it is one that
// names routes, since those have to be withdrawn.
self.error(
Handling::TreatAsWithdraw,
Some(type_code),
format!(
"attribute {type_code} has flags {:#04x} that conflict with its definition",
flags.0
),
);
if !names_routes {
return Ok(Verdict::Skip);
}
}
// Attributes that only mean something between internal peers are
// dropped when an external one sends them (RFC 7606 §7.5, §7.9,
// §7.10), well formed or not.
if !self.ctx.ibgp && matches!(type_code, LOCAL_PREF | ORIGINATOR_ID | CLUSTER_LIST) {
self.error(
Handling::AttributeDiscard,
Some(type_code),
format!("attribute {type_code} from an external peer"),
);
return Ok(Verdict::Skip);
}
// A speaker with four-octet AS numbers has no use for AS4_PATH and
// AS4_AGGREGATOR and must not send them to another (RFC 6793 §6).
if self.ctx.four_octet_asn && matches!(type_code, AS4_PATH | AS4_AGGREGATOR) {
self.error(
Handling::AttributeDiscard,
Some(type_code),
format!("attribute {type_code} on a session with four-octet AS numbers"),
);
return Ok(Verdict::Skip);
}
match self.parse(raw) {
Ok(attribute) => Ok(Verdict::Keep {
attribute,
route_property: !flags_are_wrong && !names_routes,
}),
Err(detail) => {
let detail = format!("malformed attribute {type_code}: {detail}");
match handling_of_malformed(type_code) {
Some(handling) => {
self.error(handling, Some(type_code), detail);
Ok(Verdict::Skip)
}
None => Err(reset(
UpdateMsgSubcode::OptionalAttribute,
wire.to_vec(),
detail,
)),
}
}
}
}
/// Reads the value of an attribute whose definition is known. The error
/// is what was wrong with it, as text.
fn parse(&self, raw: &RawAttribute<'_>) -> Result<PathAttribute, String> {
let value = raw.value;
// Known by their definition but not modelled: kept as bytes, once
// their length has been checked.
let length_is_valid = match raw.type_code {
AS4_AGGREGATOR => Some(value.len() == 8),
_ => None,
};
match length_is_valid {
Some(true) => Ok(opaque(raw)),
Some(false) => Err(format!("a length of {} is not valid", value.len())),
None => PathAttribute::parse_value(self.ctx, raw.flags.clone(), raw.type_code, value)
.map_err(|e| describe(&e)),
}
}
}
/// The attribute kept as bytes.
fn opaque(raw: &RawAttribute<'_>) -> PathAttribute {
PathAttribute::Unknown {
flags: raw.flags.clone(),
type_code: raw.type_code,
payload: raw.value.to_vec(),
}
}
/// The type code of a mandatory attribute that an UPDATE announcing routes
/// lacks (RFC 4271 §5, RFC 4760 §3, RFC 7606 §3.d). An UPDATE that only
/// withdraws needs no attributes.
fn missing_mandatory(ctx: &ParserContext, update: &UpdateMessage) -> Option<u8> {
let in_body = !update.announced_routes.is_empty();
let in_mp_reach = update
.path_attributes
.iter()
.any(|a| matches!(a, PathAttribute::MpReachNlri(mp) if !mp.prefixes.is_empty()));
if !in_body && !in_mp_reach {
return None;
}
// Routes in MP_REACH_NLRI carry their next hop there; LOCAL_PREF is
// for internal peers.
let required = [
(ORIGIN, true),
(AS_PATH, true),
(NEXT_HOP, in_body),
(LOCAL_PREF, ctx.ibgp),
];
required
.into_iter()
.filter(|(_, needed)| *needed)
.map(|(type_code, _)| type_code)
.find(|type_code| {
!update
.path_attributes
.iter()
.any(|attribute| attribute.type_code() == *type_code)
})
}
/// `update` with every route it announces withdrawn instead, and no other
/// attributes: they are not to be trusted, and a withdrawal has none.
fn as_withdrawal(update: UpdateMessage) -> UpdateMessage {
let mut withdrawn_routes = update.withdrawn_routes;
withdrawn_routes.extend(update.announced_routes);
let mut unreach: Vec<MpUnreachNlriPathAttribute> = Vec::new();
let mut withdraw =
|family, prefixes: Vec<Nlri>| match unreach.iter_mut().find(|u| u.family == family) {
Some(existing) => existing.prefixes.extend(prefixes),
None => unreach.push(MpUnreachNlriPathAttribute { family, prefixes }),
};
for attribute in update.path_attributes {
match attribute {
PathAttribute::MpUnreachNlri(mp) => withdraw(mp.family, mp.prefixes),
PathAttribute::MpReachNlri(mp) => withdraw(mp.family, mp.prefixes),
_ => {}
}
}
UpdateMessage {
withdrawn_routes,
path_attributes: unreach
.into_iter()
.map(PathAttribute::MpUnreachNlri)
.collect(),
announced_routes: vec![],
}
}
#[cfg(test)]
mod tests {
use std::net::Ipv4Addr;
use crate::message::PathAttributeFlags;
use bgp_primitives::ip_prefix::IpPrefix;
use super::*;
use crate::message::{
AsPathAttribute, LocalPrefPathAttribute, MultiExitDiscPathAttribute, NextHopPathAttribute,
OriginPathAttribute,
};
const WELL_KNOWN: u8 = 0x40;
const OPTIONAL: u8 = 0x80;
const OPTIONAL_TRANSITIVE: u8 = 0xc0;
fn ebgp() -> ParserContext {
ParserContext {
four_octet_asn: true,
ibgp: false,
add_path: Vec::new(),
}
}
fn ibgp() -> ParserContext {
ParserContext {
four_octet_asn: true,
ibgp: true,
add_path: Vec::new(),
}
}
fn attr(flags: u8, type_code: u8, value: &[u8]) -> Vec<u8> {
let mut wire = vec![flags, type_code, value.len() as u8];
wire.extend_from_slice(value);
wire
}
fn origin() -> Vec<u8> {
attr(WELL_KNOWN, ORIGIN, &[0])
}
/// AS_SEQUENCE of AS65001, four octets wide.
fn as_path() -> Vec<u8> {
attr(WELL_KNOWN, AS_PATH, &[2, 1, 0, 0, 0xfd, 0xe9])
}
fn next_hop() -> Vec<u8> {
attr(WELL_KNOWN, NEXT_HOP, &[192, 0, 2, 1])
}
fn local_pref() -> Vec<u8> {
attr(WELL_KNOWN, LOCAL_PREF, &[0, 0, 0, 100])
}
/// The three attributes every IPv4 announcement needs.
fn mandatory() -> Vec<Vec<u8>> {
vec![origin(), as_path(), next_hop()]
}
/// 10.1.2.0/24
const NLRI: [u8; 4] = [24, 10, 1, 2];
fn route() -> IpPrefix {
IpPrefix::try_from("10.1.2.0/24").unwrap()
}
fn body(withdrawn: &[u8], attributes: &[Vec<u8>], nlri: &[u8]) -> Vec<u8> {
let attributes = attributes.concat();
let mut body = (withdrawn.len() as u16).to_be_bytes().to_vec();
body.extend_from_slice(withdrawn);
body.extend_from_slice(&(attributes.len() as u16).to_be_bytes());
body.extend_from_slice(&attributes);
body.extend_from_slice(nlri);
body
}
/// An announcement of 10.1.2.0/24 with the mandatory attributes and
/// `extra`.
fn announce_with(ctx: &ParserContext, extra: Vec<u8>) -> DecodedUpdate {
let mut attributes = mandatory();
attributes.push(extra);
UpdateMessage::decode(ctx, &body(&[], &attributes, &NLRI)).unwrap()
}
fn handled(decoded: &DecodedUpdate) -> Vec<(Handling, Option<u8>)> {
decoded
.errors
.iter()
.map(|e| (e.handling, e.attribute))
.collect()
}
#[track_caller]
fn assert_withdrawn(decoded: &DecodedUpdate, attribute: Option<u8>, case: &str) {
assert_eq!(
handled(decoded),
[(Handling::TreatAsWithdraw, attribute)],
"{case}"
);
assert_eq!(decoded.update.withdrawn_routes, [route()], "{case}");
assert!(decoded.update.announced_routes.is_empty(), "{case}");
assert!(decoded.update.path_attributes.is_empty(), "{case}");
}
#[test]
fn a_well_formed_update_is_left_alone() {
let decoded = announce_with(&ebgp(), attr(OPTIONAL, MULTI_EXIT_DISC, &[0, 0, 0, 50]));
assert!(decoded.errors.is_empty());
assert_eq!(decoded.update.announced_routes, [route()]);
assert_eq!(
decoded.update.path_attributes,
[
PathAttribute::Origin(OriginPathAttribute::IGP),
AsPathAttribute::from_asns(vec![65001]),
PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::new(192, 0, 2, 1))),
PathAttribute::MultiExitDisc(MultiExitDiscPathAttribute(50)),
]
);
}
#[test]
fn the_kept_attributes_are_handed_over_as_they_were_on_the_wire() {
// All of a well-formed UPDATE's attributes, but not MP_REACH_NLRI.
let attributes = vec![mp_reach_v6(), origin(), as_path(), next_hop()];
let decoded = UpdateMessage::decode(&ebgp(), &body(&[], &attributes, &NLRI)).unwrap();
assert_eq!(
decoded.attributes_wire,
Some([origin(), as_path(), next_hop()].concat())
);
// Not an attribute that was discarded.
let decoded = announce_with(&ebgp(), local_pref());
assert_eq!(decoded.attributes_wire, Some(mandatory().concat()));
// Nothing when the routes were withdrawn instead: there is no route
// for the attributes to belong to.
let decoded = announce_with(&ebgp(), attr(OPTIONAL, MULTI_EXIT_DISC, &[1]));
assert_eq!(decoded.attributes_wire, None);
let decoded = UpdateMessage::decode(&ebgp(), &body(&NLRI, &[], &[])).unwrap();
assert_eq!(decoded.attributes_wire, None);
// Nor on a session with two-octet AS numbers, where the bytes are
// not in the form a table keeps.
let two_octet = ParserContext {
four_octet_asn: false,
ibgp: false,
add_path: Vec::new(),
};
let attributes = vec![
origin(),
attr(WELL_KNOWN, AS_PATH, &[2, 1, 0xfd, 0xe9]),
next_hop(),
];
let decoded = UpdateMessage::decode(&two_octet, &body(&[], &attributes, &NLRI)).unwrap();
assert!(decoded.errors.is_empty());
assert_eq!(decoded.attributes_wire, None);
}
#[test]
fn only_to_customer_is_read_and_kept_as_it_came() {
let wire = crate::message::only_to_customer_wire(3333);
assert_eq!(
wire.to_vec(),
attr(OPTIONAL_TRANSITIVE, ONLY_TO_CUSTOMER, &[0, 0, 0x0d, 0x05])
);
let decoded = announce_with(&ebgp(), wire.to_vec());
assert!(decoded.errors.is_empty());
assert_eq!(
decoded.update.path_attributes.last(),
Some(&PathAttribute::OnlyToCustomer(3333))
);
assert_eq!(
decoded.attributes_wire,
Some([mandatory().concat(), wire.to_vec()].concat())
);
// And is written as it is read.
let mut out = bytes::BytesMut::new();
PathAttribute::OnlyToCustomer(3333)
.to_wire(&ebgp(), &mut out)
.unwrap();
assert_eq!(out[..], wire);
}
#[test]
fn an_update_that_announces_nothing_needs_no_attributes() {
// End-of-RIB, and a plain withdrawal.
for wire in [body(&[], &[], &[]), body(&NLRI, &[], &[])] {
let decoded = UpdateMessage::decode(&ebgp(), &wire).unwrap();
assert!(decoded.errors.is_empty());
}
}
#[test]
fn a_malformed_attribute_withdraws_the_routes_and_keeps_the_session() {
let cases: Vec<(&str, u8, Vec<u8>)> = vec![
(
"MED of three octets",
MULTI_EXIT_DISC,
attr(OPTIONAL, MULTI_EXIT_DISC, &[0, 0, 50]),
),
(
"COMMUNITIES with nothing in it",
COMMUNITIES,
attr(OPTIONAL_TRANSITIVE, COMMUNITIES, &[]),
),
(
"COMMUNITIES that is not a multiple of four",
COMMUNITIES,
attr(OPTIONAL_TRANSITIVE, COMMUNITIES, &[0xfd, 0xe8, 0, 100, 1]),
),
(
"EXTENDED COMMUNITIES of seven octets",
EXTENDED_COMMUNITIES,
attr(OPTIONAL_TRANSITIVE, EXTENDED_COMMUNITIES, &[0; 7]),
),
(
"LARGE_COMMUNITY of eleven octets",
LARGE_COMMUNITIES,
attr(OPTIONAL_TRANSITIVE, LARGE_COMMUNITIES, &[0; 11]),
),
(
"MED marked transitive",
MULTI_EXIT_DISC,
attr(OPTIONAL_TRANSITIVE, MULTI_EXIT_DISC, &[0, 0, 0, 50]),
),
// RFC 9234 §5.
(
"Only to Customer of two octets",
ONLY_TO_CUSTOMER,
attr(OPTIONAL_TRANSITIVE, ONLY_TO_CUSTOMER, &[0xfd, 0xe8]),
),
(
"Only to Customer of eight octets",
ONLY_TO_CUSTOMER,
attr(OPTIONAL_TRANSITIVE, ONLY_TO_CUSTOMER, &[0; 8]),
),
];
for (case, type_code, extra) in cases {
let decoded = announce_with(&ebgp(), extra);
assert_withdrawn(&decoded, Some(type_code), case);
}
}
#[test]
fn a_malformed_mandatory_attribute_withdraws_the_routes() {
// Each replaces the good attribute of its type.
let cases: Vec<(&str, u8, Vec<u8>)> = vec![
("ORIGIN of 5", ORIGIN, attr(WELL_KNOWN, ORIGIN, &[5])),
(
"ORIGIN of two octets",
ORIGIN,
attr(WELL_KNOWN, ORIGIN, &[0, 0]),
),
(
"ORIGIN marked optional",
ORIGIN,
attr(OPTIONAL_TRANSITIVE, ORIGIN, &[0]),
),
(
"AS_PATH with a confederation segment",
AS_PATH,
attr(WELL_KNOWN, AS_PATH, &[3, 1, 0, 0, 0xfd, 0xe9]),
),
(
"AS_PATH with an empty segment",
AS_PATH,
attr(WELL_KNOWN, AS_PATH, &[2, 0]),
),
(
"AS_PATH cut short",
AS_PATH,
attr(WELL_KNOWN, AS_PATH, &[2, 2, 0, 0, 0xfd, 0xe9]),
),
(
"NEXT_HOP of three octets",
NEXT_HOP,
attr(WELL_KNOWN, NEXT_HOP, &[192, 0, 2]),
),
];
for (case, type_code, bad) in cases {
let attributes: Vec<Vec<u8>> = mandatory()
.into_iter()
.map(|good| {
if good[1] == type_code {
bad.clone()
} else {
good
}
})
.collect();
let decoded = UpdateMessage::decode(&ebgp(), &body(&[], &attributes, &NLRI)).unwrap();
assert_withdrawn(&decoded, Some(type_code), case);
}
}
#[test]
fn a_missing_mandatory_attribute_withdraws_the_routes() {
for missing in [ORIGIN, AS_PATH, NEXT_HOP] {
let attributes: Vec<Vec<u8>> = mandatory()
.into_iter()
.filter(|a| a[1] != missing)
.collect();
let decoded = UpdateMessage::decode(&ebgp(), &body(&[], &attributes, &NLRI)).unwrap();
assert_withdrawn(&decoded, Some(missing), &format!("without {missing}"));
}
}
#[test]
fn an_attribute_that_overruns_the_attributes_withdraws_what_was_found() {
// A MED that claims 200 octets.
let mut attributes = mandatory();
attributes.push(vec![OPTIONAL, MULTI_EXIT_DISC, 200, 0, 0]);
let decoded = UpdateMessage::decode(&ebgp(), &body(&[], &attributes, &NLRI)).unwrap();
assert_withdrawn(&decoded, Some(MULTI_EXIT_DISC), "overrun");
// Two stray octets after the last attribute: not even a header.
let mut attributes = mandatory();
attributes.push(vec![OPTIONAL, MULTI_EXIT_DISC]);
let decoded = UpdateMessage::decode(&ebgp(), &body(&[], &attributes, &NLRI)).unwrap();
assert_withdrawn(&decoded, None, "stray octets");
}
#[test]
fn attributes_that_do_not_affect_selection_are_dropped_and_the_route_kept() {
let cases: Vec<(&str, u8, Vec<u8>)> = vec![
(
"ATOMIC_AGGREGATE with a value",
ATOMIC_AGGREGATE,
attr(WELL_KNOWN, ATOMIC_AGGREGATE, &[1]),
),
(
"AGGREGATOR of five octets",
AGGREGATOR,
attr(OPTIONAL_TRANSITIVE, AGGREGATOR, &[0; 5]),
),
("LOCAL_PREF from an external peer", LOCAL_PREF, local_pref()),
(
"ORIGINATOR_ID from an external peer",
ORIGINATOR_ID,
attr(OPTIONAL, ORIGINATOR_ID, &[10, 0, 0, 1]),
),
(
"CLUSTER_LIST from an external peer",
CLUSTER_LIST,
attr(OPTIONAL, CLUSTER_LIST, &[10, 0, 0, 1]),
),
(
"AS4_PATH between four-octet speakers",
AS4_PATH,
attr(OPTIONAL_TRANSITIVE, AS4_PATH, &[2, 1, 0, 0, 0xfd, 0xe9]),
),
("a second ORIGIN", ORIGIN, origin()),
];
for (case, type_code, extra) in cases {
let decoded = announce_with(&ebgp(), extra);
assert_eq!(
handled(&decoded),
[(Handling::AttributeDiscard, Some(type_code))],
"{case}"
);
assert_eq!(decoded.update.announced_routes, [route()], "{case}");
assert_eq!(decoded.update.path_attributes.len(), 3, "{case}");
}
}
#[test]
fn a_repeated_attribute_keeps_its_first_value() {
let mut attributes = mandatory();
attributes.push(attr(OPTIONAL, MULTI_EXIT_DISC, &[0, 0, 0, 50]));
attributes.push(attr(OPTIONAL, MULTI_EXIT_DISC, &[0, 0, 0, 99]));
let decoded = UpdateMessage::decode(&ebgp(), &body(&[], &attributes, &NLRI)).unwrap();
assert_eq!(
handled(&decoded),
[(Handling::AttributeDiscard, Some(MULTI_EXIT_DISC))]
);
assert!(
decoded
.update
.path_attributes
.contains(&PathAttribute::MultiExitDisc(MultiExitDiscPathAttribute(
50
)))
);
}
#[test]
fn internal_peers_must_send_local_pref_and_may_send_reflection_attributes() {
let kept = announce_with(&ibgp(), local_pref());
assert!(kept.errors.is_empty());
assert!(
kept.update
.path_attributes
.contains(&PathAttribute::LocalPref(LocalPrefPathAttribute(100)))
);
let missing = UpdateMessage::decode(&ibgp(), &body(&[], &mandatory(), &NLRI)).unwrap();
assert_withdrawn(&missing, Some(LOCAL_PREF), "no LOCAL_PREF");
let short = announce_with(&ibgp(), attr(WELL_KNOWN, LOCAL_PREF, &[0, 0, 100]));
assert_withdrawn(&short, Some(LOCAL_PREF), "short LOCAL_PREF");
let mut attributes = mandatory();
attributes.push(local_pref());
attributes.push(attr(OPTIONAL, ORIGINATOR_ID, &[10, 0, 0, 1]));
attributes.push(attr(OPTIONAL, CLUSTER_LIST, &[10, 0, 0, 2, 10, 0, 0, 3]));
let reflected = UpdateMessage::decode(&ibgp(), &body(&[], &attributes, &NLRI)).unwrap();
assert!(reflected.errors.is_empty());
assert_eq!(reflected.update.path_attributes.len(), 6);
assert!(
reflected
.update
.path_attributes
.contains(&PathAttribute::OriginatorId(Ipv4Addr::new(10, 0, 0, 1)))
);
assert!(
reflected
.update
.path_attributes
.contains(&PathAttribute::ClusterList(vec![
Ipv4Addr::new(10, 0, 0, 2),
Ipv4Addr::new(10, 0, 0, 3)
]))
);
let mut attributes = mandatory();
attributes.push(local_pref());
attributes.push(attr(OPTIONAL, ORIGINATOR_ID, &[10, 0, 1]));
let bad = UpdateMessage::decode(&ibgp(), &body(&[], &attributes, &NLRI)).unwrap();
assert_withdrawn(&bad, Some(ORIGINATOR_ID), "short ORIGINATOR_ID");
}
#[test]
fn as4_path_is_read_from_a_two_octet_peer_and_dropped_if_malformed() {
let ctx = ParserContext {
four_octet_asn: false,
ibgp: false,
add_path: Vec::new(),
};
// On this session AS_PATH is two octets wide.
let attributes = |as4_path: Vec<u8>| {
vec![
origin(),
attr(WELL_KNOWN, AS_PATH, &[2, 1, 0x5b, 0xa0]),
next_hop(),
as4_path,
]
};
let good = attr(
OPTIONAL_TRANSITIVE,
AS4_PATH,
&[2, 1, 0xfa, 0x56, 0xea, 0x01],
);
let decoded = UpdateMessage::decode(&ctx, &body(&[], &attributes(good), &NLRI)).unwrap();
assert!(decoded.errors.is_empty());
assert!(
decoded
.update
.path_attributes
.iter()
.any(|a| matches!(a, PathAttribute::As4Path(_)))
);
// The two made one: AS 4200000001 where AS_PATH had AS_TRANS, and
// no AS4_PATH.
let mut one_path = decoded.update.path_attributes;
merge_as4_path(&mut one_path);
assert_eq!(
one_path,
[
PathAttribute::Origin(OriginPathAttribute::IGP),
AsPathAttribute::from_asns(vec![4_200_000_001]),
PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::new(192, 0, 2, 1))),
]
);
let bad = attr(OPTIONAL_TRANSITIVE, AS4_PATH, &[2, 1, 0xfa, 0x56]);
let decoded = UpdateMessage::decode(&ctx, &body(&[], &attributes(bad), &NLRI)).unwrap();
assert_eq!(
handled(&decoded),
[(Handling::AttributeDiscard, Some(AS4_PATH))]
);
assert_eq!(decoded.update.announced_routes, [route()]);
}
/// AS4_AGGREGATOR (RFC 6793 §4.2.2) is kept as it came, once its
/// length is known to be the four octets of an AS number and the four
/// of an address. One of another length is dropped and the route kept
/// (RFC 6793 §6), like a malformed AGGREGATOR.
#[test]
fn as4_aggregator_is_kept_opaque_and_dropped_if_malformed() {
let ctx = ParserContext {
four_octet_asn: false,
ibgp: false,
add_path: Vec::new(),
};
let attributes = |as4_aggregator: Vec<u8>| {
vec![
origin(),
attr(WELL_KNOWN, AS_PATH, &[2, 1, 0x5b, 0xa0]),
next_hop(),
as4_aggregator,
]
};
// AS 4200000001, 192.0.2.9.
let value = [0xfa, 0x56, 0xea, 0x01, 192, 0, 2, 9];
let good = attr(OPTIONAL_TRANSITIVE, AS4_AGGREGATOR, &value);
let decoded = UpdateMessage::decode(&ctx, &body(&[], &attributes(good), &NLRI)).unwrap();
assert!(decoded.errors.is_empty());
assert_eq!(
decoded.update.path_attributes.last(),
Some(&PathAttribute::Unknown {
flags: PathAttributeFlags(OPTIONAL_TRANSITIVE),
type_code: AS4_AGGREGATOR,
payload: value.to_vec(),
})
);
for (case, bad) in [
("two-octet AS number", &value[2..]),
("no address", &value[..4]),
("nothing", &[][..]),
] {
let bad = attr(OPTIONAL_TRANSITIVE, AS4_AGGREGATOR, bad);
let decoded = UpdateMessage::decode(&ctx, &body(&[], &attributes(bad), &NLRI)).unwrap();
assert_eq!(
handled(&decoded),
[(Handling::AttributeDiscard, Some(AS4_AGGREGATOR))],
"{case}"
);
assert_eq!(decoded.update.announced_routes, [route()], "{case}");
assert_eq!(decoded.update.path_attributes.len(), 3, "{case}");
}
}
/// RFC 6793 §4.2.3. AS4_PATH is the tail of the path; the front is
/// kept from AS_PATH. One longer than the path is not used, and is
/// gone all the same: nothing after the decoder is to see two paths,
/// or a peer could have them disagree about a route's origin.
#[test]
fn as4_path_is_merged_into_the_path() {
use crate::message::AsPathSegment;
let sequence = |path: &[u32]| AsPathSegment {
ordered: true,
path: path.to_vec(),
};
let set = |path: &[u32]| AsPathSegment {
ordered: false,
path: path.to_vec(),
};
let merged = |narrow: Vec<AsPathSegment>, wide: Vec<AsPathSegment>| {
let mut attributes = vec![
PathAttribute::Origin(OriginPathAttribute::IGP),
PathAttribute::As4Path(AsPathAttribute { segments: wide }),
PathAttribute::AsPath(AsPathAttribute { segments: narrow }),
];
merge_as4_path(&mut attributes);
assert_eq!(attributes.len(), 2, "AS4_PATH is gone");
match attributes.pop() {
Some(PathAttribute::AsPath(path)) => path.segments,
other => panic!("{other:?}"),
}
};
assert_eq!(
merged(
vec![sequence(&[3333, 23456, 23456])],
vec![sequence(&[210_036, 4_200_000_001])]
),
[sequence(&[3333]), sequence(&[210_036, 4_200_000_001])]
);
// As long as the path: all of it.
assert_eq!(
merged(vec![sequence(&[23456])], vec![sequence(&[210_036])]),
[sequence(&[210_036])]
);
// Longer than the path: not used. The hijack this would allow is
// a path [3333] that says it ends at someone else's AS.
assert_eq!(
merged(vec![sequence(&[3333])], vec![sequence(&[64500, 1299])]),
[sequence(&[3333])]
);
// A set counts as one, whole.
assert_eq!(
merged(
vec![sequence(&[3333]), set(&[1, 2]), sequence(&[23456])],
vec![sequence(&[210_036])]
),
[sequence(&[3333]), set(&[1, 2]), sequence(&[210_036])]
);
assert_eq!(
merged(
vec![sequence(&[3333, 23456])],
vec![set(&[210_036, 210_037])]
),
[sequence(&[3333]), set(&[210_036, 210_037])]
);
}
/// 2001:db8:1::/48 via 2001:db8::1.
fn mp_reach_v6() -> Vec<u8> {
let mut value = vec![0, 2, 1, 16];
value.extend_from_slice(&[0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]);
value.extend_from_slice(&[0, 48, 0x20, 0x01, 0x0d, 0xb8, 0x00, 0x01]);
attr(OPTIONAL, MP_REACH_NLRI, &value)
}
#[test]
fn routes_in_mp_reach_are_withdrawn_too() {
let v6 = IpPrefix::try_from("2001:db8:1::/48").unwrap();
let withdrawn = |attributes: Vec<Vec<u8>>, case: &str| {
let decoded = UpdateMessage::decode(&ebgp(), &body(&[], &attributes, &[])).unwrap();
assert!(
decoded
.errors
.iter()
.any(|e| e.handling == Handling::TreatAsWithdraw),
"{case}"
);
assert_eq!(
decoded.update.path_attributes,
[PathAttribute::MpUnreachNlri(MpUnreachNlriPathAttribute {
family: AfiSafi::IPV6_UNICAST,
prefixes: vec![v6.into()],
})],
"{case}"
);
};
// A well-formed one first: no NEXT_HOP attribute is needed.
let good = vec![mp_reach_v6(), origin(), as_path()];
let decoded = UpdateMessage::decode(&ebgp(), &body(&[], &good, &[])).unwrap();
assert!(decoded.errors.is_empty());
assert!(matches!(
&decoded.update.path_attributes[0],
PathAttribute::MpReachNlri(mp) if mp.family == AfiSafi::IPV6_UNICAST
));
withdrawn(
vec![mp_reach_v6(), attr(WELL_KNOWN, ORIGIN, &[9]), as_path()],
"bad ORIGIN",
);
withdrawn(vec![mp_reach_v6(), origin()], "no AS_PATH");
// Wrong flags on MP_REACH_NLRI itself: its routes are still found.
let mut misflagged = mp_reach_v6();
misflagged[0] = OPTIONAL_TRANSITIVE;
withdrawn(vec![misflagged, origin(), as_path()], "misflagged");
}
#[test]
fn errors_that_leave_the_routes_in_doubt_reset_the_session() {
let subcode = |wire: Vec<u8>| match UpdateMessage::decode(&ebgp(), &wire) {
Err(e) => (e.notification, e.data),
Ok(decoded) => panic!("expected a reset, got {decoded:?}"),
};
let update_error = |subcode| BgpError::UpdateMsg(subcode);
// The lengths that frame the message do not fit it.
assert_eq!(
subcode(vec![0, 9, 24, 10, 1, 2, 0, 0]).0,
update_error(UpdateMsgSubcode::MalformedAttrs)
);
assert_eq!(
subcode(vec![0, 0, 0, 9, 0x40, 1, 1, 0]).0,
update_error(UpdateMsgSubcode::MalformedAttrs)
);
assert_eq!(
subcode(vec![0]).0,
update_error(UpdateMsgSubcode::MalformedAttrs)
);
// A prefix that cannot be one.
assert_eq!(
subcode(body(&[], &mandatory(), &[33, 10, 1, 2, 3, 4])).0,
update_error(UpdateMsgSubcode::InvalidNetworkField)
);
assert_eq!(
subcode(body(&[24, 10, 1], &[], &[])).0,
update_error(UpdateMsgSubcode::InvalidNetworkField)
);
// Two MP_REACH_NLRI: which routes are meant?
assert_eq!(
subcode(body(
&[],
&[mp_reach_v6(), mp_reach_v6(), origin(), as_path()],
&[]
))
.0,
update_error(UpdateMsgSubcode::MalformedAttrs)
);
// MP_REACH_NLRI with a next hop of the wrong length.
let bad_mp_reach = attr(OPTIONAL, MP_REACH_NLRI, &[0, 2, 1, 3, 1, 2, 3, 0]);
let (error, data) = subcode(body(&[], &[bad_mp_reach.clone(), origin(), as_path()], &[]));
assert_eq!(error, update_error(UpdateMsgSubcode::OptionalAttribute));
assert_eq!(data, bad_mp_reach);
// An attribute nobody has defined, marked as one everybody must know.
let unknown = attr(WELL_KNOWN, 99, &[1, 2]);
let mut attributes = mandatory();
attributes.push(unknown.clone());
let (error, data) = subcode(body(&[], &attributes, &NLRI));
assert_eq!(
error,
update_error(UpdateMsgSubcode::UnrecognizedWellKnownAttr)
);
assert_eq!(data, unknown);
}
#[test]
fn an_unknown_optional_attribute_is_carried_as_it_came() {
let decoded = announce_with(&ebgp(), attr(0xe0, 99, &[1, 2, 3]));
assert!(decoded.errors.is_empty());
assert_eq!(
decoded.update.path_attributes[3],
PathAttribute::Unknown {
flags: PathAttributeFlags(0xe0),
type_code: 99,
payload: vec![1, 2, 3],
}
);
}
}