//! Whether a route goes to a peer, and as what. //! //! This is asked for every route and every peer it might go to, so it is //! plain comparisons on what the route carries; nothing here waits or //! looks anything up. [`crate::advertise`] does the sending. //! //! A route we originate always may go, as far as the rules below let it. //! A route learned from a peer is passed on only to peers whose `export` //! says so, and then: //! //! - not back to the peer it came from, and not to a peer whose AS is on //! its path already (it would refuse it as a loop); //! - not from one peer in our own AS to another (RFC 4271 §9.2.1), unless //! one of the two is a route reflector client of ours (RFC 4456), and //! then not back to the router the route started at; //! - not up or across if it came down or across: a route marked "only to //! customer" goes to customers only (RFC 9234 §5, egress, 2); //! - not where its communities say it is not to go: NO_EXPORT and //! NO_ADVERTISE (RFC 1997), and, where it leaves our AS, those of //! [`Control`], which also say how many more times our AS goes on its //! path. //! //! What goes is changed as BGP has it: our AS on the path and ourselves as //! next hop towards another network, the MED it came with dropped; the //! path as it was and a LOCAL_PREF towards a peer in our own AS. A route //! passed from one router of our AS to another is marked with where it //! started and with our cluster (ORIGINATOR_ID, CLUSTER_LIST), and goes, //! like every route, with ourselves as next hop: there is no resolving a //! next hop that is not a neighbor, so the peer reaches what lies behind //! the other router through us. To a route server's client the route goes //! as it came (RFC 7947 §2.2). use std::net::{IpAddr, Ipv4Addr}; use bgp_packet::message::{ AsPathAttribute, AsPathSegment, BGP_MARKER, LocalPrefPathAttribute, MpReachNlriPathAttribute, MpUnreachNlriPathAttribute, NextHopPathAttribute, NlriNextHop, PathAttribute, UpdateMessage, attr_type, write_nlri, }; use bgp_primitives::constants::{AfiSafi, IpVersion}; use bgp_primitives::ip_prefix::Nlri; use bgp_primitives::parser::ParserContext; use bgp_rib::table::{DEFAULT_LOCAL_PREF, PeerIndex}; use bytes::{BufMut, Bytes, BytesMut}; use crate::advertise::{Towards, ipv6_next_hop}; use crate::marks::{self, Control, Named, communities_of, well_known_allow}; use crate::policy::{ExportPolicy, PeerRole}; /// The session a route may go out on, as far as that decides whether it /// does and as what. #[derive(Debug, Clone, Copy)] pub struct Egress<'a> { pub towards: Towards, pub peer_asn: u32, /// The peer's number in the routing table: what its own routes there /// are marked with. pub rib_peer: PeerIndex, pub policy: &'a ExportPolicy, /// The groups the session is in, for control communities to name it /// by. pub groups: &'a [u32], /// The communities by which routes say who is told of them, if they /// are honoured. pub control: Option, /// What route reflection needs to know of the session. pub reflection: Reflection, } /// Route reflection (RFC 4456), as far as one session goes. #[derive(Debug, Clone, Copy)] pub struct Reflection { /// The cluster this router reflects routes for. pub cluster_id: Ipv4Addr, /// Whether the peer is a route reflector client of ours. pub client: bool, /// The peer's BGP Identifier: a route that started at the peer is not /// passed back to it. pub peer_router_id: Ipv4Addr, } /// Where a learned route came from. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct Source { pub peer: PeerIndex, /// Whether that peer is in our own AS. pub ibgp: bool, /// Whether that peer is a route reflector client of ours. pub reflector_client: bool, /// Its BGP Identifier: where the route started inside our AS, if it /// says nothing else. pub router_id: Ipv4Addr, } /// A route ready to be announced on a session: its attributes, but for /// the ones that depend on which prefixes go with them. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Outgoing { pub attributes: Vec, pub next_hop: NlriNextHop, } impl Egress<'_> { fn transparent(&self) -> bool { !self.towards.ibgp && self.towards.role == Some(PeerRole::RouteServerClient) } /// What is common to every route on its way out, originated or /// learned: whether the communities that are for us to act on let it /// go to this peer, and what it is marked with when it does. Returns /// whether it goes. pub fn dress(&self, attributes: &mut Vec) -> bool { let towards = &self.towards; let mut prepends = self.policy.prepend; // Control communities are acted on where a route leaves our AS. To // a peer in it the route goes with them, for that router to act // on where it passes the route on. if let Some(control) = self.control.filter(|_| !towards.ibgp) { let (communities, large) = communities_of(attributes); let session = Named { asn: self.peer_asn, groups: self.groups, }; if !control.allows(communities, large, session) { return false; } prepends = prepends.saturating_add(control.prepends(large, session)); control.strip(attributes); } self.policy.marks.apply(attributes); if prepends > 0 && !towards.ibgp { let ours = std::iter::repeat_n(towards.local_asn, usize::from(prepends)); for attribute in attributes.iter_mut() { if let PathAttribute::AsPath(path) = attribute { prepend(path, ours); break; } } } // A route that goes down to a customer or across to a peer may not // come up again from there: it is marked, for every network after // us to see (RFC 9234 §5, egress, 1). let marked = attributes .iter() .any(|attribute| matches!(attribute, PathAttribute::OnlyToCustomer(_))); if !marked && let Some(marked_by) = towards .role .filter(|_| !towards.ibgp) .and_then(|role| role.only_to_customer_on_export(towards.local_asn)) { marks::put(attributes, PathAttribute::OnlyToCustomer(marked_by)); } true } /// The route with `attributes` and `next_hop`, learned from `source`, /// as it is announced on this session for prefixes of IP version /// `routes`; `None` if it is not to be. pub fn outgoing( &self, mut attributes: Vec, next_hop: Option, source: Source, routes: IpVersion, ) -> Option { let reflected = source.ibgp && self.towards.ibgp; if !self.may_go(&attributes, source, reflected) { return None; } let came_with = next_hop.or_else(|| { attributes.iter().find_map(|attribute| match attribute { PathAttribute::NextHop(next_hop) => Some(NlriNextHop::Ipv4(next_hop.0)), _ => None, }) }); if !self.rewrite(&mut attributes, source, reflected) { return None; } let next_hop = self.next_hop_for(routes, came_with)?; Some(Outgoing { attributes, next_hop, }) } /// Whether a route with these attributes, from `source`, is for this /// session at all, as far as who may be told of what goes: the export /// policy, route reflection, the AS path, the well-known communities /// and the Only to Customer mark. fn may_go(&self, attributes: &[PathAttribute], source: Source, reflected: bool) -> bool { let towards = &self.towards; if !self.policy.learned_routes || source.peer == self.rib_peer { return false; } // From one router of our AS to another only by way of a route // reflector, which we are only for our clients: what a client sends // goes to all our routers, and what the others send goes to the // clients (RFC 4456 §6). if reflected && !source.reflector_client && !self.reflection.client { return false; } let mut marked = false; let mut on_path = false; for attribute in attributes { match attribute { PathAttribute::OnlyToCustomer(_) => marked = true, PathAttribute::AsPath(path) => { on_path = path .segments .iter() .any(|segment| segment.path.contains(&self.peer_asn)); } _ => {} } } // The peer would refuse it as having been there before. if on_path && !towards.ibgp { return false; } // Whoever sent it said how far it may go (RFC 1997). On a route // of our own the same communities are for those who get it from // us, and it goes out with them. if !well_known_allow(communities_of(attributes).0, towards.ibgp) { return false; } // It came down or across once: it goes on down only. let only_down = marked && !towards.ibgp && towards .role .is_some_and(|role| !role.may_be_sent_only_to_customer_routes()); !only_down } /// Makes the attributes those of the route as this session announces /// it: what is ours to say is taken off, what is for routers of the /// AS it was learned in stays only inside ours, a reflected route gets /// its reflection attributes, and the path and LOCAL_PREF are as the /// peer is to see them. Then the session's own marks ([`Egress::dress`]). /// `false` if the route is not to go after all. fn rewrite( &self, attributes: &mut Vec, source: Source, reflected: bool, ) -> bool { let towards = &self.towards; let transparent = self.transparent(); attributes.retain_mut(|attribute| match attribute { // Ours to say, each way. PathAttribute::NextHop(_) | PathAttribute::MpReachNlri(_) | PathAttribute::MpUnreachNlri(_) | PathAttribute::As4Path(_) => false, // For the routers of the AS it was learned in, and for ours // (RFC 4271 §5.1.4, §5.1.5). PathAttribute::MultiExitDisc(_) => towards.ibgp || transparent, PathAttribute::LocalPref(_) => towards.ibgp, // Only for routers of our AS, and only for a route that is // reflected from one to another; put on afresh below. PathAttribute::OriginatorId(_) | PathAttribute::ClusterList(_) => reflected, // What we do not understand goes on if it says it may, marked // as having passed a router that did not look at it; and no // further if not (RFC 4271 §5). PathAttribute::Unknown { flags, .. } => { let passes = flags.optional() && flags.transitive(); if passes { flags.set_partial(true); } passes } _ => true, }); if reflected && !self.reflect(attributes, source) { return false; } if towards.ibgp { if !attributes .iter() .any(|attribute| matches!(attribute, PathAttribute::LocalPref(_))) { marks::put( attributes, PathAttribute::LocalPref(LocalPrefPathAttribute(DEFAULT_LOCAL_PREF)), ); } } else if !transparent { let ours = std::iter::once(towards.local_asn); match attributes.iter_mut().find_map(|attribute| match attribute { PathAttribute::AsPath(path) => Some(path), _ => None, }) { Some(path) => prepend(path, ours), None => marks::put( attributes, AsPathAttribute::from_asns(vec![towards.local_asn]), ), } } self.dress(attributes) } /// The next hop of a route for prefixes of IP version `routes`: /// ourselves, unless we are a route server and the client can use the /// next hop the route came with. `None` where our address on the /// session is of the wrong kind for the routes. fn next_hop_for( &self, routes: IpVersion, came_with: Option, ) -> Option { let towards = &self.towards; let ours = match (routes, towards.next_hop) { (IpVersion::Ipv4, IpAddr::V4(ours)) => NlriNextHop::Ipv4(ours), (IpVersion::Ipv6, IpAddr::V6(ours)) => ipv6_next_hop(ours), (IpVersion::Ipv4, IpAddr::V6(ours)) if towards.extended_next_hop => ipv6_next_hop(ours), _ => return None, }; let same_kind = |theirs: &NlriNextHop| { matches!(theirs, NlriNextHop::Ipv4(_)) == matches!(ours, NlriNextHop::Ipv4(_)) }; Some( came_with .filter(|theirs| self.transparent() && same_kind(theirs)) .unwrap_or(ours), ) } } impl Egress<'_> { /// Marks a route reflected from one router of our AS to another with /// where it started and with our cluster (RFC 4456 §8). Returns whether /// it goes: not back to the router it started at, nor round a circle /// of reflectors that has been through our cluster already. fn reflect(&self, attributes: &mut Vec, source: Source) -> bool { let originator = attributes .iter() .find_map(|attribute| match attribute { PathAttribute::OriginatorId(originator) => Some(*originator), _ => None, }) .unwrap_or(source.router_id); if originator == self.reflection.peer_router_id { return false; } let mut clusters = attributes .iter() .find_map(|attribute| match attribute { PathAttribute::ClusterList(clusters) => Some(clusters.clone()), _ => None, }) .unwrap_or_default(); if clusters.contains(&self.reflection.cluster_id) { return false; } clusters.insert(0, self.reflection.cluster_id); marks::put(attributes, PathAttribute::OriginatorId(originator)); marks::put(attributes, PathAttribute::ClusterList(clusters)); true } } /// Puts `ours` at the front of `path`. fn prepend(path: &mut AsPathAttribute, ours: impl Iterator) { match path.segments.first_mut() { Some(first) if first.ordered => { first.path.splice(0..0, ours); } _ => path.segments.insert( 0, AsPathSegment { ordered: true, path: ours.collect(), }, ), } } /// Makes `attributes` ready for the wire of a session: the path also as /// AS4_PATH where the session cannot carry its numbers (RFC 6793 §4.2.2), /// and everything in the order of the type codes. pub(crate) fn finish(attributes: &mut Vec, towards: &Towards) { attributes.retain(|attribute| !matches!(attribute, PathAttribute::As4Path(_))); if !towards.four_octet_asn { let as4_path = attributes.iter().find_map(|attribute| match attribute { PathAttribute::AsPath(path) => path.as4_path(), _ => None, }); attributes.extend(as4_path); } attributes.sort_by_key(PathAttribute::type_code); } /// How many octets a message has room for after its header and the /// lengths every UPDATE has. const UPDATE_ROOM: usize = 4096 - 19 - 4; /// The most octets a prefix takes in an UPDATE: a path identifier, a /// length, and the address. fn nlri_octets(routes: IpVersion) -> usize { 4 + 1 + usize::from(routes.bits()) / 8 } /// A route's attributes as they go out on one session, encoded once and /// kept: what every UPDATE announcing a route with them carries, less the /// next hop, which goes with the prefixes. `at_next_hop` and /// `at_mp_reach` are where in `bytes` a NEXT_HOP (type 3) and an /// MP_REACH_NLRI (type 14) go, so that the attributes stay in order of /// type code. #[derive(Debug)] pub struct EncodedAttributes { pub bytes: Bytes, pub at_next_hop: usize, pub at_mp_reach: usize, } impl EncodedAttributes { /// Encodes `attributes`, which are `finish`ed, for a session with /// `four_octet_asn`. `None` if they cannot be encoded. pub fn encode(attributes: &[PathAttribute], four_octet_asn: bool) -> Option { let ctx = ParserContext { four_octet_asn, ibgp: false, add_path: Vec::new(), }; let mut out = BytesMut::with_capacity(256); let mut at_next_hop = None; let mut at_mp_reach = None; for attribute in attributes { let code = attribute.type_code(); if code > attr_type::NEXT_HOP && at_next_hop.is_none() { at_next_hop = Some(out.len()); } if code > attr_type::MP_REACH_NLRI && at_mp_reach.is_none() { at_mp_reach = Some(out.len()); } attribute.to_wire(&ctx, &mut out).ok()?; } Some(Self { at_next_hop: at_next_hop.unwrap_or(out.len()), at_mp_reach: at_mp_reach.unwrap_or(out.len()), bytes: out.freeze(), }) } pub fn octets(&self) -> usize { self.bytes.len() } } /// What the next hop takes, in its own attribute or with the prefixes, /// and a little to spare. const NEXT_HOP_ROOM: usize = 64; /// How many octets of UPDATEs go to a session in one piece: what the /// session hands its writer at a time. A piece holds whole messages, /// so it runs over by at most one. pub(crate) const ENCODED_PIECE: usize = 64 * 1024; /// The UPDATE messages, complete with their headers, that announce /// `routes`, all of IP version `version`, with the attributes `encoded` /// and `next_hop`: as few as they fit in, in pieces of /// [`ENCODED_PIECE`] octets or so, each of whole messages. `ctx` is the /// session's, and says whether path identifiers go with the family's /// prefixes (RFC 7911). /// /// What [`announcements`] builds as messages, built as bytes: the /// attributes are not parsed, cloned or encoded again for each message. pub(crate) fn encoded_announcements( encoded: &EncodedAttributes, next_hop: NlriNextHop, version: IpVersion, routes: &[Nlri], ctx: &ParserContext, ) -> Vec { let room = UPDATE_ROOM.saturating_sub(encoded.octets() + NEXT_HOP_ROOM); let at_once = (room / nlri_octets(version)).max(1); let in_body = matches!(next_hop, NlriNextHop::Ipv4(_)) && version == IpVersion::Ipv4; let with_path_id = ctx.add_path.contains(&AfiSafi::unicast(version)); let mut pieces = Vec::new(); let mut out = BytesMut::with_capacity(ENCODED_PIECE.min(routes.len() * 64 + 4096)); for routes in routes.chunks(at_once) { { out.put_slice(&BGP_MARKER); let length_at = out.len(); out.put_u16(0); // The type of an UPDATE (RFC 4271 §4.1). out.put_u8(2); // No withdrawn routes. out.put_u16(0); let attributes_at = out.len(); out.put_u16(0); if in_body { let NlriNextHop::Ipv4(next_hop) = next_hop else { unreachable!("in_body means an IPv4 next hop"); }; out.put_slice(&encoded.bytes[..encoded.at_next_hop]); PathAttribute::NextHop(NextHopPathAttribute(next_hop)) .to_wire(ctx, &mut out) .expect("a next hop encodes"); out.put_slice(&encoded.bytes[encoded.at_next_hop..]); } else { out.put_slice(&encoded.bytes[..encoded.at_mp_reach]); PathAttribute::MpReachNlri(MpReachNlriPathAttribute { family: AfiSafi::unicast(version), next_hop, prefixes: routes.to_vec(), }) .to_wire(ctx, &mut out) .expect("a chunk that fits encodes"); out.put_slice(&encoded.bytes[encoded.at_mp_reach..]); } let attributes_len = out.len() - attributes_at - 2; out[attributes_at..attributes_at + 2] .copy_from_slice(&(attributes_len as u16).to_be_bytes()); if in_body { write_nlri(routes, with_path_id, &mut out); } let length = out.len() - length_at + BGP_MARKER.len(); out[length_at..length_at + 2].copy_from_slice(&(length as u16).to_be_bytes()); } if out.len() >= ENCODED_PIECE { pieces.push(out.split()); } } if !out.is_empty() { pieces.push(out); } pieces } /// The UPDATEs that announce `routes`, all of IP version `version`, as /// `outgoing`: as few as they fit in. `attributes_octets` is how long /// `outgoing`'s attributes are on this session's wire. /// /// What [`encoded_announcements`] does, built as messages: kept as what /// its bytes are checked against. #[cfg(test)] pub(crate) fn announcements( outgoing: &Outgoing, version: IpVersion, routes: &[Nlri], towards: &Towards, attributes_octets: usize, ) -> Vec { // What the next hop takes, in its own attribute or with the prefixes, // and a little to spare. const NEXT_HOP_ROOM: usize = 64; let room = UPDATE_ROOM.saturating_sub(attributes_octets + NEXT_HOP_ROOM); let at_once = (room / nlri_octets(version)).max(1); let in_body = matches!(outgoing.next_hop, NlriNextHop::Ipv4(_)) && version == IpVersion::Ipv4; routes .chunks(at_once) .map(|routes| { let mut attributes = outgoing.attributes.clone(); let mut announced_routes = Vec::new(); match outgoing.next_hop { NlriNextHop::Ipv4(next_hop) if in_body => { attributes.push(PathAttribute::NextHop(NextHopPathAttribute(next_hop))); announced_routes = routes.to_vec(); } next_hop => attributes.push(PathAttribute::MpReachNlri(MpReachNlriPathAttribute { family: AfiSafi::unicast(version), next_hop, prefixes: routes.to_vec(), })), } finish(&mut attributes, towards); UpdateMessage { withdrawn_routes: vec![], path_attributes: attributes, announced_routes, } }) .collect() } /// The UPDATEs that withdraw `routes`, all of IP version `version`. pub(crate) fn withdrawals( version: IpVersion, routes: &[Nlri], towards: &Towards, ) -> Vec { // What the message needs besides the prefixes: its two length fields, // or the header of MP_UNREACH_NLRI; 16 octets is more than either. const WITHDRAWAL_OVERHEAD: usize = 16; let at_once = (UPDATE_ROOM - WITHDRAWAL_OVERHEAD) / nlri_octets(version); // IPv4 routes that went in MP_REACH_NLRI are withdrawn the same way. let in_body = version == IpVersion::Ipv4 && !(towards.extended_next_hop && towards.next_hop.is_ipv6()); routes .chunks(at_once) .map(|routes| { if in_body { UpdateMessage { withdrawn_routes: routes.to_vec(), path_attributes: vec![], announced_routes: vec![], } } else { UpdateMessage { withdrawn_routes: vec![], path_attributes: vec![PathAttribute::MpUnreachNlri( MpUnreachNlriPathAttribute { family: AfiSafi::unicast(version), prefixes: routes.to_vec(), }, )], announced_routes: vec![], } } }) .collect() } #[cfg(test)] mod tests { use std::net::Ipv4Addr; use bgp_packet::message::{ CommunitiesPathAttribute, LargeCommunitiesPathAttribute, LargeCommunity, Message, MultiExitDiscPathAttribute, OriginPathAttribute, PathAttributeFlags, }; use bgp_primitives::ip_prefix::IpPrefix; use bgp_rib::table::{PeerInfo, RibTable}; use super::*; use crate::marks::{Marks, large}; const LOCAL: u32 = 64601; const THEM: u32 = 64602; /// Two peers' numbers in a table: the one a route is from, and the one /// it might go to. fn peers() -> (PeerIndex, PeerIndex) { let table = RibTable::new(1); let peer = |n: u8| { table .add_peer(PeerInfo { asn: 64600 + u32::from(n), router_id: Ipv4Addr::new(10, 0, 0, n), address: Ipv4Addr::new(192, 0, 2, n).into(), interface: 0, ibgp: false, reflector_client: false, }) .unwrap() }; (peer(1), peer(2)) } fn towards(role: Option, ibgp: bool) -> Towards { Towards { local_asn: LOCAL, next_hop: Ipv4Addr::new(192, 0, 2, 1).into(), four_octet_asn: true, ibgp, role, extended_next_hop: false, } } fn all_routes() -> ExportPolicy { ExportPolicy { learned_routes: true, all_paths: false, ..ExportPolicy::default() } } /// A route from AS 64700 by way of AS 3356, next hop 192.0.2.9. fn route(more: Vec) -> Vec { let mut attributes = vec![ PathAttribute::Origin(OriginPathAttribute::IGP), AsPathAttribute::from_asns(vec![3356, 64700]), PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::new(192, 0, 2, 9))), ]; attributes.extend(more); attributes } fn communities(communities: &[(u16, u16)]) -> PathAttribute { PathAttribute::Communities(CommunitiesPathAttribute(communities.to_vec())) } /// What `encoded_announcements` writes is, byte for byte, what the /// messages `announcements` builds encode to: with the next hop in /// its own attribute (IPv4) and in MP_REACH_NLRI (IPv6), with and /// without path identifiers, to two-octet and four-octet AS speakers /// (so with the attributes around NEXT_HOP, MP_REACH_NLRI and /// AS4_PATH in order), and over as many messages when the routes do /// not fit in one. #[test] fn encoded_announcements_are_the_messages_on_the_wire() { use std::net::Ipv6Addr; for four_octet_asn in [true, false] { for with_path_id in [false, true] { for version in [IpVersion::Ipv4, IpVersion::Ipv6] { let towards = Towards { four_octet_asn, ..towards(None, false) }; let next_hop = match version { IpVersion::Ipv4 => NlriNextHop::Ipv4(Ipv4Addr::new(192, 0, 2, 1)), IpVersion::Ipv6 => NlriNextHop::Ipv6WithLl { global: Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1), link_local: Ipv6Addr::new(0xfe80, 0, 0, 0, 0, 0, 0, 1), }, }; let attributes = vec![ PathAttribute::Origin(OriginPathAttribute::IGP), AsPathAttribute::from_asns(vec![4_200_000_000, 3356, 64700]), PathAttribute::MultiExitDisc(MultiExitDiscPathAttribute(10)), communities(&[(3356, 100), (64700, 1)]), PathAttribute::LargeCommunities(LargeCommunitiesPathAttribute { communities: vec![LargeCommunity { global_admin: 64700, data_1: 1, data_2: 2, }], }), ]; let routes: Vec = (0..30_000u32) .map(|i| Nlri { prefix: match version { IpVersion::Ipv4 => IpPrefix::new( version, Ipv4Addr::from(0x0a00_0000 + i * 256).octets(), 24, ), IpVersion::Ipv6 => IpPrefix::new( version, Ipv6Addr::new(0x2001, 0xdb8, i as u16, 0, 0, 0, 0, 0).octets(), 48, ), } .unwrap(), path_id: if with_path_id { i + 1 } else { 0 }, }) .collect(); let ctx = ParserContext { four_octet_asn, ibgp: false, add_path: if with_path_id { vec![AfiSafi::unicast(version)] } else { Vec::new() }, }; let mut finished = attributes.clone(); finish(&mut finished, &towards); let encoded = EncodedAttributes::encode(&finished, four_octet_asn).unwrap(); let bytes = encoded_announcements(&encoded, next_hop, version, &routes, &ctx); let outgoing = Outgoing { attributes, next_hop, }; let messages = announcements(&outgoing, version, &routes, &towards, encoded.octets()); let expected: Vec = messages .into_iter() .map(|update| { let mut out = BytesMut::new(); Message::Update(update).to_wire(&ctx, &mut out).unwrap(); out }) .collect(); let what = format!( "{version:?}, path ids {with_path_id}, four-octet {four_octet_asn}" ); assert!(expected.len() > 1, "{what}: more than one message"); for update in &expected { assert!(update.len() <= 4096, "{what}: a message fits"); } // In pieces of whole messages, each of about the size // meant, all but the last at least that. assert!(bytes.len() > 1, "{what}: more than one piece"); for (n, piece) in bytes.iter().enumerate() { assert!( n == bytes.len() - 1 || (ENCODED_PIECE..ENCODED_PIECE + 4096).contains(&piece.len()), "{what}: piece {n} is {} octets", piece.len() ); let mut at = 0; while at < piece.len() { let length = usize::from(u16::from_be_bytes([piece[at + 16], piece[at + 17]])); at += length; } assert_eq!(at, piece.len(), "{what}: piece {n} is of whole messages"); } let got: Vec = bytes .iter() .flat_map(|piece| piece.iter().copied()) .collect(); let want: Vec = expected.iter().flat_map(|m| m.iter().copied()).collect(); assert_eq!(got, want, "{what}: the same octets"); } } } } struct Case<'a> { policy: &'a ExportPolicy, role: Option, to_ibgp: bool, from_ibgp: bool, groups: &'a [u32], control: Option, to_client: bool, from_client: bool, } /// Our cluster, and the BGP Identifiers of the peers a route comes /// from and goes to. const CLUSTER: Ipv4Addr = Ipv4Addr::new(10, 0, 0, 9); const FROM_ID: Ipv4Addr = Ipv4Addr::new(10, 0, 0, 1); const TO_ID: Ipv4Addr = Ipv4Addr::new(10, 0, 0, 2); fn reflection(client: bool) -> Reflection { Reflection { cluster_id: CLUSTER, client, peer_router_id: TO_ID, } } impl Case<'_> { fn out(&self, attributes: Vec) -> Option { let (from, to) = peers(); let egress = Egress { towards: towards(self.role, self.to_ibgp), peer_asn: if self.to_ibgp { LOCAL } else { THEM }, rib_peer: to, policy: self.policy, groups: self.groups, control: self.control, reflection: reflection(self.to_client), }; let source = Source { peer: from, ibgp: self.from_ibgp, reflector_client: self.from_client, router_id: FROM_ID, }; egress.outgoing(attributes, None, source, IpVersion::Ipv4) } } fn plain(policy: &ExportPolicy) -> Case<'_> { Case { policy, role: None, to_ibgp: false, from_ibgp: false, groups: &[], control: None, to_client: false, from_client: false, } } /// To another network: our AS on the path, ourselves as next hop, and /// nothing that was between the route's last AS and us. #[test] fn a_route_is_passed_on_to_another_network_as_bgp_has_it() { let policy = all_routes(); let came = route(vec![ PathAttribute::MultiExitDisc(MultiExitDiscPathAttribute(50)), PathAttribute::LocalPref(LocalPrefPathAttribute(300)), communities(&[(3356, 3)]), // Understood by nobody here: one that may go on, one that may // not. PathAttribute::Unknown { flags: PathAttributeFlags::new(true, true, false, false), type_code: 200, payload: vec![1], }, PathAttribute::Unknown { flags: PathAttributeFlags::new(true, false, false, false), type_code: 201, payload: vec![2], }, ]); let outgoing = plain(&policy).out(came).unwrap(); assert_eq!( outgoing.next_hop, NlriNextHop::Ipv4(Ipv4Addr::new(192, 0, 2, 1)) ); assert_eq!( outgoing.attributes, [ PathAttribute::Origin(OriginPathAttribute::IGP), AsPathAttribute::from_asns(vec![LOCAL, 3356, 64700]), communities(&[(3356, 3)]), PathAttribute::Unknown { // Marked as having passed a router that did not know it. flags: PathAttributeFlags::new(true, true, true, false), type_code: 200, payload: vec![1], }, ] ); } #[test] fn a_route_is_passed_on_only_where_it_may_go() { let policy = all_routes(); let (from, _) = peers(); // Only if the peer is told of learned routes at all. let originated_only = ExportPolicy::default(); assert!(plain(&originated_only).out(route(vec![])).is_none()); assert!(plain(&policy).out(route(vec![])).is_some()); // Not back to where it came from. let egress = Egress { towards: towards(None, false), peer_asn: THEM, rib_peer: from, policy: &policy, groups: &[], control: None, reflection: reflection(false), }; let source = Source { peer: from, ibgp: false, reflector_client: false, router_id: FROM_ID, }; assert!( egress .outgoing(route(vec![]), None, source, IpVersion::Ipv4) .is_none() ); // Not to a peer whose AS it has been through. let mut through_them = route(vec![]); through_them[1] = AsPathAttribute::from_asns(vec![3356, THEM, 64700]); assert!(plain(&policy).out(through_them).is_none()); // Not where the communities it came with say not to (RFC 1997). let no_export = route(vec![communities(&[(0xFFFF, 0xFF01)])]); assert!(plain(&policy).out(no_export.clone()).is_none()); let internal = Case { to_ibgp: true, ..plain(&policy) }; assert!(internal.out(no_export).is_some()); let no_advertise = route(vec![communities(&[(0xFFFF, 0xFF02)])]); assert!(internal.out(no_advertise).is_none()); } /// Inside our own AS the path is as it came and LOCAL_PREF goes along; /// and a route does not go from one internal peer to another. #[test] fn a_route_goes_to_a_peer_in_our_own_as_unchanged_but_for_the_next_hop() { let policy = all_routes(); let internal = Case { to_ibgp: true, ..plain(&policy) }; let came = route(vec![PathAttribute::MultiExitDisc( MultiExitDiscPathAttribute(50), )]); let outgoing = internal.out(came).unwrap(); assert_eq!( outgoing.attributes, [ PathAttribute::Origin(OriginPathAttribute::IGP), AsPathAttribute::from_asns(vec![3356, 64700]), PathAttribute::MultiExitDisc(MultiExitDiscPathAttribute(50)), PathAttribute::LocalPref(LocalPrefPathAttribute(DEFAULT_LOCAL_PREF)), ] ); // The preference import policy gave it is what the others hear. let preferred = route(vec![PathAttribute::LocalPref(LocalPrefPathAttribute(300))]); assert!( internal .out(preferred) .unwrap() .attributes .contains(&PathAttribute::LocalPref(LocalPrefPathAttribute(300))) ); let between_internal = Case { to_ibgp: true, from_ibgp: true, ..plain(&policy) }; assert!(between_internal.out(route(vec![])).is_none()); let from_internal = Case { from_ibgp: true, ..plain(&policy) }; assert!(from_internal.out(route(vec![])).is_some()); } /// Route reflection (RFC 4456): between routers of our AS only by way /// of a client of ours, marked with where the route started and with /// our cluster, and never back to where it started or round a circle. #[test] fn a_route_is_reflected_to_and_from_clients_only() { let policy = all_routes(); let between = |from_client, to_client| Case { to_ibgp: true, from_ibgp: true, from_client, to_client, ..plain(&policy) }; let preferred = || route(vec![PathAttribute::LocalPref(LocalPrefPathAttribute(200))]); // Neither a client: as BGP has it, nothing. assert!(between(false, false).out(preferred()).is_none()); // To a client, and from one: marked, the preference as it was, and // ourselves as next hop. for (from_client, to_client) in [(false, true), (true, false), (true, true)] { let outgoing = between(from_client, to_client) .out(preferred()) .unwrap_or_else(|| panic!("from {from_client} to {to_client}")); assert_eq!( outgoing.attributes, [ PathAttribute::Origin(OriginPathAttribute::IGP), AsPathAttribute::from_asns(vec![3356, 64700]), PathAttribute::LocalPref(LocalPrefPathAttribute(200)), PathAttribute::OriginatorId(FROM_ID), PathAttribute::ClusterList(vec![CLUSTER]), ] ); assert_eq!( outgoing.next_hop, NlriNextHop::Ipv4(Ipv4Addr::new(192, 0, 2, 1)) ); } // Reflected before: where it started stays, our cluster goes first. let elsewhere = Ipv4Addr::new(10, 0, 0, 5); let other_cluster = Ipv4Addr::new(10, 0, 0, 7); let reflected_before = |originator, clusters: Vec| { route(vec![ PathAttribute::LocalPref(LocalPrefPathAttribute(200)), PathAttribute::OriginatorId(originator), PathAttribute::ClusterList(clusters), ]) }; let outgoing = between(false, true) .out(reflected_before(elsewhere, vec![other_cluster])) .unwrap(); assert!( outgoing .attributes .contains(&PathAttribute::OriginatorId(elsewhere)) ); assert!( outgoing .attributes .contains(&PathAttribute::ClusterList(vec![CLUSTER, other_cluster])) ); // Not back to the router it started at, nor round to our cluster. assert!( between(false, true) .out(reflected_before(TO_ID, vec![other_cluster])) .is_none() ); assert!( between(false, true) .out(reflected_before(elsewhere, vec![other_cluster, CLUSTER])) .is_none() ); // To another network they are not sent; a route from another // network to a client of ours gets none. let from_inside = Case { from_ibgp: true, ..plain(&policy) }; let outgoing = from_inside .out(reflected_before(elsewhere, vec![other_cluster])) .unwrap(); assert!(!outgoing.attributes.iter().any(|attribute| matches!( attribute, PathAttribute::OriginatorId(_) | PathAttribute::ClusterList(_) ))); let to_client = Case { to_ibgp: true, to_client: true, ..plain(&policy) }; let outgoing = to_client.out(route(vec![])).unwrap(); assert!(!outgoing.attributes.iter().any(|attribute| matches!( attribute, PathAttribute::OriginatorId(_) | PathAttribute::ClusterList(_) ))); } /// RFC 9234 §5, egress: what came down or across goes on down only, /// and what goes down or across is marked. #[test] fn a_route_only_for_customers_goes_to_customers_only() { use PeerRole::*; let policy = all_routes(); let marked = || route(vec![PathAttribute::OnlyToCustomer(3356)]); for (role, goes) in [ (Customer, true), (RouteServerClient, true), (Provider, false), (Peer, false), (RouteServer, false), ] { let case = Case { role: Some(role), ..plain(&policy) }; assert_eq!(case.out(marked()).is_some(), goes, "{role:?}"); } // The mark it has is kept; one it lacks is put on, with our AS, // on the way down or across. let mark = |role, came: Vec| { let case = Case { role: Some(role), ..plain(&policy) }; case.out(came) .unwrap() .attributes .iter() .find_map(|attribute| match attribute { PathAttribute::OnlyToCustomer(asn) => Some(*asn), _ => None, }) }; assert_eq!(mark(Customer, marked()), Some(3356)); assert_eq!(mark(Customer, route(vec![])), Some(LOCAL)); assert_eq!(mark(Peer, route(vec![])), Some(LOCAL)); assert_eq!(mark(Provider, route(vec![])), None); // A peer that is nothing in particular: no rule, no mark. assert!(plain(&policy).out(marked()).is_some()); } /// RFC 7947 §2.2: a route server is not on the path, and not in the /// way of the packets. #[test] fn a_route_server_passes_routes_on_as_they_came() { let policy = all_routes(); let client = Case { role: Some(PeerRole::RouteServerClient), ..plain(&policy) }; let came = route(vec![PathAttribute::MultiExitDisc( MultiExitDiscPathAttribute(50), )]); let outgoing = client.out(came).unwrap(); assert_eq!( outgoing.next_hop, NlriNextHop::Ipv4(Ipv4Addr::new(192, 0, 2, 9)) ); assert_eq!( outgoing.attributes, [ PathAttribute::Origin(OriginPathAttribute::IGP), AsPathAttribute::from_asns(vec![3356, 64700]), PathAttribute::MultiExitDisc(MultiExitDiscPathAttribute(50)), PathAttribute::OnlyToCustomer(LOCAL), ] ); } /// The marks of `export`, and the communities that say who is told. #[test] fn communities_decide_and_are_changed_on_the_way_out() { let strings = |texts: &[&str]| -> Vec { texts.iter().map(|text| text.to_string()).collect() }; let policy = ExportPolicy { learned_routes: true, all_paths: false, marks: Marks::parse(&strings(&["64601:900"]), &strings(&["64601:5"]), &[]).unwrap(), prepend: 2, }; let control = Some(Control { local_asn: LOCAL }); let case = Case { control, ..plain(&policy) }; // Not to this peer; to this peer only; and with nothing said. assert!(case.out(route(vec![communities(&[(0, 64602)])])).is_none()); let outgoing = case .out(route(vec![communities(&[ (64601, 64602), (3356, 3), (64601, 900), ])])) .unwrap(); assert_eq!( outgoing.attributes, [ PathAttribute::Origin(OriginPathAttribute::IGP), // Ours once for passing it on, and twice more by policy. AsPathAttribute::from_asns(vec![LOCAL, LOCAL, LOCAL, 3356, 64700]), // What was for us is gone; the router's mark is on. communities(&[(3356, 3), (64601, 5)]), ] ); assert!( case.out(route(vec![communities(&[(64601, 64999)])])) .is_none(), "for another peer only" ); // Without control communities the same are only communities. assert!( plain(&policy) .out(route(vec![communities(&[(0, 64602)])])) .is_some() ); } fn large_communities(communities: &[LargeCommunity]) -> PathAttribute { PathAttribute::LargeCommunities(LargeCommunitiesPathAttribute { communities: communities.to_vec(), }) } /// Control communities are for where a route leaves our AS. To a /// router of our own the route goes with them, whatever they say, for /// it to act on; to another network they decide, by group as well as /// by AS, and are taken off, while our marks go on. #[test] fn control_communities_are_acted_on_where_routes_leave_our_as() { let policy = all_routes(); let control = Some(Control { local_asn: LOCAL }); let mark = large(LOCAL, 100, 7); let tagged = |actions: &[LargeCommunity]| { route(vec![large_communities( &[actions, std::slice::from_ref(&mark)].concat(), )]) }; // Inside: as it came, though it says "to nobody". let inside = Case { to_ibgp: true, control, ..plain(&policy) }; let nobody = [large(LOCAL, 0, 0), large(LOCAL, 31, 2)]; let kept = inside .out(tagged(&nobody)) .expect("our own router hears of it"); assert!( kept.attributes.contains(&large_communities( &[&nobody[..], std::slice::from_ref(&mark)].concat() )), "{:?}", kept.attributes ); // Outside, it is done as it says. let at_two = Case { groups: &[2], control, ..plain(&policy) }; assert!(at_two.out(tagged(&nobody)).is_none()); // Prepended where the group is named, and not elsewhere; the // community that asked is gone, the mark is not. let prepend_at_two = [large(LOCAL, 31, 2)]; let elsewhere = Case { groups: &[1], control, ..plain(&policy) }; for (case, path) in [ (&at_two, vec![LOCAL, LOCAL, 3356, 64700]), (&elsewhere, vec![LOCAL, 3356, 64700]), ] { let outgoing = case.out(tagged(&prepend_at_two)).unwrap(); assert_eq!( outgoing.attributes, [ PathAttribute::Origin(OriginPathAttribute::IGP), AsPathAttribute::from_asns(path), large_communities(std::slice::from_ref(&mark)), ] ); } // A route without one, as an anycast prefix would be, is not. let bare = at_two.out(route(vec![])).unwrap(); assert_eq!( bare.attributes[1], AsPathAttribute::from_asns(vec![LOCAL, 3356, 64700]) ); // The session's own prepend and the route's add up. let prepending = ExportPolicy { prepend: 1, ..all_routes() }; let both = Case { groups: &[2], control, ..plain(&prepending) }; let outgoing = both.out(tagged(&prepend_at_two)).unwrap(); assert_eq!( outgoing.attributes[1], AsPathAttribute::from_asns(vec![LOCAL, LOCAL, LOCAL, 3356, 64700]) ); } /// As many prefixes to a message as fit, and the path as AS4_PATH too /// where the session cannot carry it. #[test] fn routes_that_go_out_alike_share_their_messages() { let outgoing = Outgoing { attributes: vec![ PathAttribute::Origin(OriginPathAttribute::IGP), AsPathAttribute::from_asns(vec![LOCAL, 4_200_000_001]), ], next_hop: NlriNextHop::Ipv4(Ipv4Addr::new(192, 0, 2, 1)), }; let routes: Vec = (0..3000u32) .map(|n| { let [_, _, a, b] = n.to_be_bytes(); IpPrefix::new(IpVersion::Ipv4, [100, a, b, 0], 24) .unwrap() .into() }) .collect(); let narrow = Towards { four_octet_asn: false, ..towards(None, false) }; let updates = announcements(&outgoing, IpVersion::Ipv4, &routes, &narrow, 40); assert!(updates.len() < 8, "{} messages", updates.len()); let told: usize = updates.iter().map(|u| u.announced_routes.len()).sum(); assert_eq!(told, routes.len()); let ctx = bgp_primitives::parser::ParserContext { four_octet_asn: false, ibgp: false, add_path: Vec::new(), }; for update in &updates { let codes: Vec = update .path_attributes .iter() .map(PathAttribute::type_code) .collect(); assert_eq!(codes, [1, 2, 3, 17], "AS4_PATH with it"); let mut wire = bytes::BytesMut::new(); bgp_packet::message::Message::Update(update.clone()) .to_wire(&ctx, &mut wire) .expect("it fits a message"); } // IPv6 routes, and IPv4 ones with an IPv6 next hop, go in // MP_REACH_NLRI; and are withdrawn in MP_UNREACH_NLRI. let v6 = Outgoing { next_hop: NlriNextHop::Ipv6("2001:db8::1".parse().unwrap()), ..outgoing }; let update = &announcements(&v6, IpVersion::Ipv4, &routes[..2], &narrow, 40)[0]; assert!(update.announced_routes.is_empty()); assert!(update.path_attributes.iter().any(|attribute| matches!( attribute, PathAttribute::MpReachNlri(mp) if mp.prefixes.len() == 2 ))); let gone = withdrawals(IpVersion::Ipv4, &routes, &narrow); assert_eq!( gone.iter().map(|u| u.withdrawn_routes.len()).sum::(), routes.len() ); assert!(gone.len() < 8); let in_mp_unreach = |update: &UpdateMessage, count: usize| { update.withdrawn_routes.is_empty() && update.path_attributes.iter().any(|attribute| { matches!(attribute, PathAttribute::MpUnreachNlri(mp) if mp.prefixes.len() == count) }) }; let v6_routes: Vec = (0..3) .map(|n| { IpPrefix::try_from(format!("2001:db8:{n}::/48").as_str()) .unwrap() .into() }) .collect(); let gone = withdrawals(IpVersion::Ipv6, &v6_routes, &narrow); assert_eq!(gone.len(), 1); assert!(in_mp_unreach(&gone[0], 3), "{:?}", gone[0]); // IPv4 routes that went with IPv6 next hops (RFC 8950) too. let over_v6 = Towards { extended_next_hop: true, next_hop: "2001:db8::2".parse::().unwrap().into(), ..narrow }; let gone = withdrawals(IpVersion::Ipv4, &routes[..2], &over_v6); assert_eq!(gone.len(), 1); assert!(in_mp_unreach(&gone[0], 2), "{:?}", gone[0]); } }