//! 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: there is //! no route reflection here); //! - 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 those of [`Control`]. //! //! 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. To a //! route server's client the route goes as it came (RFC 7947 §2.2). use std::net::IpAddr; use bgp_packet::message::{ AsPathAttribute, AsPathSegment, LocalPrefPathAttribute, MpReachNlriPathAttribute, MpUnreachNlriPathAttribute, NextHopPathAttribute, NlriNextHop, PathAttribute, UpdateMessage, }; use bgp_primitives::constants::{AfiSafi, IpVersion}; use bgp_primitives::ip_prefix::Nlri; use bgp_rib::table::{DEFAULT_LOCAL_PREF, PeerIndex}; use crate::advertise::{Towards, ipv6_next_hop}; use crate::marks::{self, Control, 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 communities by which routes say who is told of them, if they /// are honoured. pub control: Option, } /// 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, } /// 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 (communities, large) = communities_of(attributes); if let Some(control) = self.control { if !control.allows(communities, large, self.peer_asn) { return false; } control.strip(attributes); } self.policy.marks.apply(attributes); if self.policy.prepend > 0 && !towards.ibgp { let ours = std::iter::repeat_n(towards.local_asn, usize::from(self.policy.prepend)); 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 towards = &self.towards; if !self.policy.learned_routes || source.peer == self.rib_peer { return None; } if source.ibgp && towards.ibgp { return None; } let mut came_with = next_hop; let mut marked = false; let mut on_path = false; for attribute in &attributes { match attribute { PathAttribute::NextHop(next_hop) => { came_with = came_with.or(Some(NlriNextHop::Ipv4(next_hop.0))); } 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 None; } // 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 None; } // It came down or across once: it goes on down only. if marked && !towards.ibgp && towards .role .is_some_and(|role| !role.may_be_sent_only_to_customer_routes()) { return None; } 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, // 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::UnknownPathAttribute { flags, .. } => { let passes = flags.optional() && flags.transitive(); if passes { flags.set_partial(true); } passes } _ => true, }); if towards.ibgp { if !attributes .iter() .any(|attribute| matches!(attribute, PathAttribute::LocalPref(_))) { marks::put( &mut 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( &mut attributes, AsPathAttribute::from_asns(vec![towards.local_asn]), ), } } if !self.dress(&mut attributes) { return None; } // Ourselves, unless we are a route server and the client can use // the next hop the route came with. 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(_)) }; let next_hop = came_with .filter(|theirs| transparent && same_kind(theirs)) .unwrap_or(ours); Some(Outgoing { attributes, next_hop, }) } } /// 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 } /// 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. 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 { let at_once = (UPDATE_ROOM - 16) / 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, MultiExitDiscPathAttribute, OriginPathAttribute, PathAttributeFlags, }; use bgp_primitives::ip_prefix::IpPrefix; use bgp_rib::table::{PeerInfo, RibTable}; use super::*; use crate::marks::Marks; 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, }) .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, ..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())) } struct Case<'a> { policy: &'a ExportPolicy, role: Option, to_ibgp: bool, from_ibgp: bool, control: Option, } 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, control: self.control, }; let source = Source { peer: from, ibgp: self.from_ibgp, }; egress.outgoing(attributes, None, source, IpVersion::Ipv4) } } fn plain(policy: &ExportPolicy) -> Case<'_> { Case { policy, role: None, to_ibgp: false, from_ibgp: false, control: None, } } /// 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::UnknownPathAttribute { flags: PathAttributeFlags::new(true, true, false, false), type_code: 200, payload: vec![1], }, PathAttribute::UnknownPathAttribute { 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::UnknownPathAttribute { // 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, control: None, }; let source = Source { peer: from, ibgp: false, }; 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()); } /// 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, 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() ); } /// 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); } }