//! 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, 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, /// 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>, } /// 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 { 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, 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, 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 { 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) { 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, errors: Vec, /// 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, /// 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, 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 { 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 { 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 { 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 = Vec::new(); let mut withdraw = |family, prefixes: Vec| 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 { let mut wire = vec![flags, type_code, value.len() as u8]; wire.extend_from_slice(value); wire } fn origin() -> Vec { attr(WELL_KNOWN, ORIGIN, &[0]) } /// AS_SEQUENCE of AS65001, four octets wide. fn as_path() -> Vec { attr(WELL_KNOWN, AS_PATH, &[2, 1, 0, 0, 0xfd, 0xe9]) } fn next_hop() -> Vec { attr(WELL_KNOWN, NEXT_HOP, &[192, 0, 2, 1]) } fn local_pref() -> Vec { attr(WELL_KNOWN, LOCAL_PREF, &[0, 0, 0, 100]) } /// The three attributes every IPv4 announcement needs. fn mandatory() -> Vec> { 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], nlri: &[u8]) -> Vec { 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) -> DecodedUpdate { let mut attributes = mandatory(); attributes.push(extra); UpdateMessage::decode(ctx, &body(&[], &attributes, &NLRI)).unwrap() } fn handled(decoded: &DecodedUpdate) -> Vec<(Handling, Option)> { decoded .errors .iter() .map(|e| (e.handling, e.attribute)) .collect() } #[track_caller] fn assert_withdrawn(decoded: &DecodedUpdate, attribute: Option, 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)> = 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)> = 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> = 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> = 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)> = 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| { 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| { 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, wide: Vec| { 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 { 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>, 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| 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], } ); } }