- 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>
1215 lines
45 KiB
Rust
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],
|
|
}
|
|
);
|
|
}
|
|
}
|