A code-quality pass over the periphery. The packet crate frames a message with `begin_message` and `finish_message`, which the exporter's hand-written UPDATE header can use instead of a bare type code, and keeps the NLRI reader and writer together as a pair; the rule that the body's prefixes are IPv4 unicast lives once; a session with two-octet AS numbers no longer reserves the attributes' wire copy it never keeps. The TCP diagnostics read netlink with the netlink module's own attribute walk and helpers instead of copies of them. The kernel follower learns what the kernel has in one named step used at start and on relearning, keeps its batch buffers between wakes, and builds its task in a constructor. The mesh benchmark, which had grown six phases in one main, is a `Mesh` with named phases and a `Receiver`, with one report of the sessions for both deadlines; the collector capabilities, the KEEPALIVE bytes, the draining of a session and the printing of a table row are helpers shared by the benches, the examples and the integration tests. The MRT reader uses the packet crate's attribute splitter. Testing: a message framed by hand is the one `to_wire` writes; a short tcp_info from an old kernel leaves the later fields zero; the synthetic table is the same every run and every announcement decodes to itself. The packing example gives the same figures on the RIS dump before and after. The crates' tests, the integration tests, the kernel tests and every lab phase pass. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1220 lines
45 KiB
Rust
1220 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::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,
|
|
};
|
|
use crate::wire::body_has_path_id;
|
|
|
|
/// 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,
|
|
/// What was wrong, in the order met; empty when the UPDATE was well
|
|
/// formed. Where a message of any type was decoded this is
|
|
/// [`Decoded::update_errors`](crate::message::Decoded::update_errors).
|
|
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> {
|
|
match parse_nlri_list(IpVersion::Ipv4, body_has_path_id(ctx), 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. Left empty on a
|
|
/// session with two-octet AS numbers, whose bytes no table keeps (see
|
|
/// [`DecodedUpdate::attributes_wire`]), so that nothing is copied or
|
|
/// reserved for them.
|
|
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(if ctx.four_octet_asn { wire_len } else { 0 }),
|
|
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.ctx.four_octet_asn {
|
|
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::constants::AfiSafi;
|
|
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],
|
|
}
|
|
);
|
|
}
|
|
}
|