Files
ferrix/crates/server/src/export.rs
T
Rayhaan JaufeerallyandClaude Fable 5.1 b5ca42244d export: a lighter gather, and UPDATEs to the session by the piece
Two things kept an exporter from its peer. A table being gathered to
go out anew kept a map of every prefix heard, to let a later word on a
prefix supersede an earlier one, and dressed every set of attributes
before the first UPDATE went: a map insert and a parse-and-encode a
route that the peer waited on. And the session's queue took UPDATEs
one by one, a thousand at most, so the exporter stood at the queue
once a hundred kilobytes were in it, behind a writer that takes a
megabyte: the two took turns instead of running together.

The journal gives a table out shard by shard, each prefix once, and a
change meanwhile comes after the table as news of its own, so there is
no second word to keep track of while gathering: the map is gone, and
a prefix the peer is not to have is simply not gathered, as what it
held of it is withdrawn once the table is through. Each set is dressed
as its turn to go comes, so the first UPDATE goes once the table is
gathered; only when the gathered routes alone would be over the
peer's limit are the sets dressed first, to know which cannot be and
take their routes with them, so that none goes if too many would. The
UPDATEs for a set now go to the session in pieces of 64 KiB or so,
each of whole messages, and the queue takes 64 of those: a few
megabytes ahead of the writer, not a hundred kilobytes. The session
counts the messages in a piece by their headers.

Testing: the byte-for-byte test of the encoded UPDATEs now encodes
thirty thousand routes and checks the pieces are of whole messages
and of the size meant. The server tests, the integration tests, the
kernel tests and every lab phase pass. In the Linux VM, three rounds
alternating with the previous commit, a new peer's first UPDATE comes
1.8 s after its session is up instead of 3.1 s, and it has its four
million routes in 3.8 s instead of 4.1 s; the smaller of the two
routers' tables crosses in 3.0 s instead of 4.2 s and the larger in
5.0 s instead of 5.2 s, at the same CPU time.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-10 15:28:40 +02:00

1405 lines
54 KiB
Rust

//! 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<Control>,
/// 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<PathAttribute>,
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<PathAttribute>) -> 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<PathAttribute>,
next_hop: Option<NlriNextHop>,
source: Source,
routes: IpVersion,
) -> Option<Outgoing> {
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<PathAttribute>,
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<NlriNextHop>,
) -> Option<NlriNextHop> {
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<PathAttribute>, 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<Item = u32>) {
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<PathAttribute>, 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<Self> {
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<BytesMut> {
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<UpdateMessage> {
// 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<UpdateMessage> {
// 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<PeerRole>, 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<PathAttribute>) -> Vec<PathAttribute> {
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<Nlri> = (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<BytesMut> = 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<u8> = bytes
.iter()
.flat_map(|piece| piece.iter().copied())
.collect();
let want: Vec<u8> = 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<PeerRole>,
to_ibgp: bool,
from_ibgp: bool,
groups: &'a [u32],
control: Option<Control>,
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<PathAttribute>) -> Option<Outgoing> {
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<Ipv4Addr>| {
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<PathAttribute>| {
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<String> { 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<Nlri> = (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<u8> = 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::<usize>(),
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<Nlri> = (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::<std::net::Ipv6Addr>().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]);
}
}