Files
ferrix/crates/server/src/export.rs
T
Rayhaan JaufeerallyandClaude Fable 5.1 1e2e8f2d86 Pass learned routes on to peers, with marks and control communities
What was to be scripted is configuration, done natively:

- import { add_communities, add_large_communities, remove_communities,
  local_pref }: every route taken from a peer is marked (where it was
  learned, say), with nobody else's word for our marks taken, and given
  its preference, which selection goes by.
- export { add_communities, add_large_communities, remove_communities,
  as_path_prepend, learned_routes }, for a peer or for all that have none
  of their own.
- control_communities: the communities by which routes say which peers
  they are passed on to, as exchanges' route servers have them (0:P, L:P,
  0:L, and the large forms L:0:P, L:1:P, L:0:0), taken off as the route
  goes on.

And what the last of these is for: with export.learned_routes a peer is
told the best route of every prefix the others announced. The rules are
BGP's and cannot be turned off: not back where it came from, not to a
peer whose AS is on the path, not between two internal peers, not past
NO_EXPORT or NO_ADVERTISE; our AS on the path and ourselves as next hop
to another network, LOCAL_PREF to our own; to a route server's client
the path, MED and next hop as they came (RFC 7947). Which routes go where
follows from the roles of RFC 9234: a route that came down or across goes
on down only, so that with roles on every peer this makes us transit for
our customers and nothing else. A peer without a role is told everything,
and the daemon says so at startup.

Each session follows the table's best routes with a reader and keeps the
prefixes it has announced, so that it can withdraw them. Routes that go
out alike go in one UPDATE: changes are gathered for 50 ms first, and a
table that is given out anew is gathered whole (at most four sessions at
a time; it is tens of megabytes). For that to work the table's journal
had to give out together what was written together: every change now
carries a time on a table-wide clock, a reader's pass reads every shard
up to one time, no further than the changes that are complete in every
shard, and a resync takes its places in the journals as of one time.

One peer sending 1.23 M routes over a session, another told of them:
in our table after 0.47 s, all heard after 2.1 s, four routes an UPDATE.
The lab has a phase, transit: FRR as our customer and BIRD as our
provider, routes each way with the right paths and marks, one kept back
by a community, and pings between their networks through us.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-02 23:48:08 +02:00

778 lines
28 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: 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<Control>,
}
/// 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<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 (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<PathAttribute>,
next_hop: Option<NlriNextHop>,
source: Source,
routes: IpVersion,
) -> Option<Outgoing> {
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<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
}
/// 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<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> {
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<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,
..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()))
}
struct Case<'a> {
policy: &'a ExportPolicy,
role: Option<PeerRole>,
to_ibgp: bool,
from_ibgp: bool,
control: Option<Control>,
}
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,
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<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,
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<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);
}
}