Route reflection, with ourselves as next hop

A route learned from one router of our AS went to no other (RFC 4271
§9.2.1), so every router needed a session with every other. A router at
the end of a single link, with one session to its neighbour inside the AS,
heard only of what that neighbour learned itself, and nothing of what the
neighbour had from the routers behind it.

A peer in our own AS can now be made a route reflector client
(`route_reflector_client`, RFC 4456). What a client sends goes to all our
routers; what the others send goes to the clients. A reflected route gets
ORIGINATOR_ID (where it started, if it does not say) and our cluster ID at
the front of CLUSTER_LIST; it is not sent back to the router it started
at, nor round to a cluster it has been through, and one that comes back
anyway is refused as "route reflected back to us". The cluster ID is the
router ID unless `cluster_id` says otherwise. The two attributes are now
decoded as such rather than kept as bytes, and are never sent to other
networks.

Unlike a standard reflector, this one is the next hop of what it reflects,
as it is of everything it announces: there is no resolving a next hop
that is not a neighbour, so a client reaches what lies behind another
router through the reflector.

Only peers in our own AS can be clients; the configuration refuses the
setting for any other. Nothing changes in what other networks are told:
Only to Customer marks go along with reflected routes, and every
receiving session keeps its own max_prefixes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Rayhaan JaufeerallyandClaude Opus 5.5 committed 2026-10-05 18:10:35 +02:00
1 parent 164b1099f1
commit b16227c51f
34 files changed
+693 -26

No files matched your search

+1
View File
@@ -164,6 +164,7 @@ fn into_table(wire: &[Vec<BytesMut>], routes: usize, threads: usize) {
address: Ipv4Addr::new(192, 0, 2, peer as u8 + 1).into(),
interface: 0,
ibgp: false,
reflector_client: false,
})
.expect("room for the peer")
})
+1
View File
@@ -283,6 +283,7 @@ fn add_peers(table: &RibTable, dump: &Dump, peers: &[usize]) -> Vec<PeerIndex> {
address: dump.peers[peer].address,
interface: 0,
ibgp: false,
reflector_client: false,
})
.expect("room for the peer")
})
+2
View File
@@ -122,6 +122,7 @@ impl Machine {
address: self.neighbor(host).into(),
interface: 0,
ibgp: false,
reflector_client: false,
})
.unwrap()
}
@@ -721,6 +722,7 @@ async fn an_ipv4_route_goes_by_way_of_an_ipv6_neighbor() {
address: neighbor,
interface: 0,
ibgp: false,
reflector_client: false,
})
.unwrap();
// Both families by way of the same neighbor.
@@ -57,6 +57,7 @@ impl TestServer {
pub async fn start_with(local_asn: u32, customize: impl FnOnce(&mut Config)) -> Result<Self> {
let mut config = Config {
router_id: Ipv4Addr::new(127, 0, 0, 1),
cluster_id: Ipv4Addr::new(127, 0, 0, 1),
local_asn,
hold_time_secs: 90,
listen_endpoints: vec![SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0)],
@@ -42,6 +42,7 @@ async fn server_offering(
c.peers = vec![PeerConfig {
graceful_restart: Default::default(),
groups: Vec::new(),
reflector_client: false,
name: "route-server-client".into(),
endpoint: SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 1).into(),
remote_asn: PEER_ASN,
@@ -20,6 +20,7 @@ fn peer_at(endpoint: SocketAddr) -> PeerConfig {
PeerConfig {
graceful_restart: Default::default(),
groups: Vec::new(),
reflector_client: false,
name: "r1".into(),
endpoint: endpoint.into(),
remote_asn: PEER_ASN,
+1
View File
@@ -59,6 +59,7 @@ async fn server_with(
c.peers = vec![PeerConfig {
graceful_restart: Default::default(),
groups: Vec::new(),
reflector_client: false,
name: "r1".into(),
endpoint: endpoint.into(),
remote_asn: PEER_ASN,
@@ -38,6 +38,7 @@ async fn server_dialing(listener: &TcpListener) -> Result<TestServer> {
c.peers = vec![PeerConfig {
graceful_restart: Default::default(),
groups: Vec::new(),
reflector_client: false,
name: "r1".into(),
endpoint: endpoint.into(),
remote_asn: PEER_ASN,
@@ -169,6 +169,7 @@ async fn test_refused_routes_can_be_seen_and_counted() -> Result<()> {
c.peers = vec![PeerConfig {
graceful_restart: Default::default(),
groups: Vec::new(),
reflector_client: false,
name: "refused-demo".into(),
endpoint: endpoint.into(),
remote_asn: 3333,
+4
View File
@@ -29,6 +29,7 @@ async fn server_dialing(listener: &TcpListener, remote_asn: u32) -> Result<TestS
c.peers = vec![PeerConfig {
graceful_restart: Default::default(),
groups: Vec::new(),
reflector_client: false,
name: "r1".into(),
endpoint: endpoint.into(),
remote_asn,
@@ -189,6 +190,7 @@ async fn test_inbound_from_configured_address_uses_config() -> Result<()> {
c.peers = vec![PeerConfig {
graceful_restart: Default::default(),
groups: Vec::new(),
reflector_client: false,
name: "r1".into(),
endpoint: SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 1).into(),
remote_asn: PEER_ASN,
@@ -243,6 +245,7 @@ async fn test_local_address_is_used_as_the_source() -> Result<()> {
c.peers = vec![PeerConfig {
graceful_restart: Default::default(),
groups: Vec::new(),
reflector_client: false,
name: "r1".into(),
endpoint: endpoint.into(),
remote_asn: PEER_ASN,
@@ -266,6 +269,7 @@ async fn test_local_address_is_used_as_the_source() -> Result<()> {
c.peers = vec![PeerConfig {
graceful_restart: Default::default(),
groups: Vec::new(),
reflector_client: false,
name: "r1".into(),
endpoint: endpoint.into(),
remote_asn: PEER_ASN,
@@ -44,6 +44,7 @@ async fn server_with(import: ImportPolicy) -> Result<(TestServer, TcpListener)>
c.peers = vec![PeerConfig {
graceful_restart: Default::default(),
groups: Vec::new(),
reflector_client: false,
name: "r1".into(),
endpoint: endpoint.into(),
remote_asn: PEER_ASN,
@@ -185,6 +185,7 @@ async fn test_no_family_in_common_refuses_the_session() -> Result<()> {
c.peers = vec![PeerConfig {
graceful_restart: Default::default(),
groups: Vec::new(),
reflector_client: false,
name: "v6-only".into(),
endpoint: SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 1).into(),
remote_asn: PEER_ASN,
+1
View File
@@ -50,6 +50,7 @@ async fn server_with(role: Option<PeerRole>, require_role: bool) -> Result<Setup
c.peers = vec![PeerConfig {
graceful_restart: Default::default(),
groups: Vec::new(),
reflector_client: false,
name: "r1".into(),
endpoint: endpoint.into(),
remote_asn: PEER_ASN,
+1
View File
@@ -69,6 +69,7 @@ async fn server_with(
c.peers = vec![PeerConfig {
graceful_restart: Default::default(),
groups: Vec::new(),
reflector_client: false,
name: "r1".into(),
endpoint: endpoint.into(),
remote_asn: PEER_ASN,
+40
View File
@@ -74,6 +74,13 @@ pub enum PathAttribute {
/// customer, a peer or a route server client by the AS with this
/// number, and from there on may only go down to customers.
OnlyToCustomer(u32),
/// ORIGINATOR_ID (RFC 4456 §8): the BGP Identifier of the router in our
/// AS that learned the route from outside, put on by the first router
/// that passes the route from one router of the AS to another.
OriginatorId(Ipv4Addr),
/// CLUSTER_LIST (RFC 4456 §8): the clusters the route has been passed
/// on by inside our AS, the last first.
ClusterList(Vec<Ipv4Addr>),
/// An attribute this implementation does not interpret, kept as it
/// came so that it can be passed on.
UnknownPathAttribute {
@@ -198,6 +205,8 @@ impl PathAttribute {
Self::As4Path(_) => AS4_PATH,
Self::LargeCommunities(_) => LARGE_COMMUNITIES,
Self::OnlyToCustomer(_) => ONLY_TO_CUSTOMER,
Self::OriginatorId(_) => ORIGINATOR_ID,
Self::ClusterList(_) => CLUSTER_LIST,
Self::UnknownPathAttribute { type_code, .. } => *type_code,
}
}
@@ -275,6 +284,31 @@ impl PathAttribute {
})?;
Self::OnlyToCustomer(u32::from_be_bytes(asn))
}
ORIGINATOR_ID => {
let id = <[u8; 4]>::try_from(value).map_err(|_| {
malformed(format!(
"ORIGINATOR_ID is {} octets long, not four",
value.len()
))
})?;
Self::OriginatorId(Ipv4Addr::from(id))
}
CLUSTER_LIST => {
if value.is_empty() || !value.len().is_multiple_of(4) {
return Err(malformed(format!(
"CLUSTER_LIST is {} octets long, not a multiple of four",
value.len()
)));
}
Self::ClusterList(
value
.as_chunks::<4>()
.0
.iter()
.map(|id| Ipv4Addr::from(*id))
.collect(),
)
}
_ => Self::UnknownPathAttribute {
flags,
type_code,
@@ -332,6 +366,12 @@ impl PathAttribute {
Self::As4Path(a) => a.to_wire_with_width(true, out)?,
Self::LargeCommunities(a) => a.to_wire(out),
Self::OnlyToCustomer(asn) => out.put_u32(*asn),
Self::OriginatorId(id) => out.put_slice(&id.octets()),
Self::ClusterList(ids) => {
for id in ids {
out.put_slice(&id.octets());
}
}
Self::UnknownPathAttribute { payload, .. } => out.put_slice(payload),
}
Ok(())
+15 -2
View File
@@ -406,8 +406,6 @@ impl<'a> AttributeReader<'a> {
// Known by their definition but not modelled: kept as bytes, once
// their length has been checked.
let length_is_valid = match raw.type_code {
ORIGINATOR_ID => Some(value.len() == 4),
CLUSTER_LIST => Some(!value.is_empty() && value.len().is_multiple_of(4)),
AS4_AGGREGATOR => Some(value.len() == 8),
_ => None,
};
@@ -899,6 +897,21 @@ mod tests {
let reflected = UpdateMessage::decode(&ibgp(), &body(&[], &attributes, &NLRI)).unwrap();
assert!(reflected.errors.is_empty());
assert_eq!(reflected.update.path_attributes.len(), 6);
assert!(
reflected
.update
.path_attributes
.contains(&PathAttribute::OriginatorId(Ipv4Addr::new(10, 0, 0, 1)))
);
assert!(
reflected
.update
.path_attributes
.contains(&PathAttribute::ClusterList(vec![
Ipv4Addr::new(10, 0, 0, 2),
Ipv4Addr::new(10, 0, 0, 3)
]))
);
let mut attributes = mandatory();
attributes.push(local_pref());
+18
View File
@@ -292,6 +292,22 @@ fn path_attributes(session: Session) -> impl Strategy<Value = Vec<PathAttribute>
})
})),
proptest::option::of(any::<u32>().prop_map(PathAttribute::OnlyToCustomer)),
// Route reflection's, which only an internal peer sends.
proptest::option::of((any::<[u8; 4]>(), vec(any::<[u8; 4]>(), 1..4))).prop_map(
move |reflected| {
reflected
.filter(|_| session.ibgp)
.map(|(originator, clusters)| {
vec![
PathAttribute::OriginatorId(Ipv4Addr::from(originator)),
PathAttribute::ClusterList(
clusters.into_iter().map(Ipv4Addr::from).collect(),
),
]
})
.unwrap_or_default()
},
),
unknown_attributes(),
);
(mandatory, optional).prop_map(|(mut attributes, optional)| {
@@ -306,6 +322,7 @@ fn path_attributes(session: Session) -> impl Strategy<Value = Vec<PathAttribute>
as4,
large,
otc,
reflected,
unknown,
) = optional;
attributes.extend(
@@ -324,6 +341,7 @@ fn path_attributes(session: Session) -> impl Strategy<Value = Vec<PathAttribute>
.into_iter()
.flatten(),
);
attributes.extend(reflected);
attributes.extend(unknown);
attributes
})
+4
View File
@@ -58,6 +58,10 @@ pub struct PeerInfo {
pub interface: u32,
/// Whether the peer is in our own AS.
pub ibgp: bool,
/// Whether routes learned from our other routers are passed on to the
/// peer, and its routes to them: whether it is a route reflector
/// client of ours (RFC 4456).
pub reflector_client: bool,
}
/// A peer's number in a table, given out by [`RibTable::add_peer`]. Small,
+2
View File
@@ -63,6 +63,7 @@ pub(super) fn external(number: u8) -> PeerInfo {
address: IpAddr::from(Ipv4Addr::new(192, 0, 2, number)),
interface: 0,
ibgp: false,
reflector_client: false,
}
}
@@ -70,6 +71,7 @@ pub(super) fn internal(number: u8) -> PeerInfo {
PeerInfo {
asn: 64999,
ibgp: true,
reflector_client: false,
..external(number)
}
}
+18
View File
@@ -100,6 +100,13 @@ message BgpConfig {
// Graceful restart, for every peer that has no `graceful_restart` of its
// own. Left out, it is on, as the defaults below have it.
GracefulRestart graceful_restart = 14;
// Where this router passes on routes from one router of our AS to
// another (Peer.route_reflector_client), the cluster it does so for, as
// a dotted-quad: what it puts on such routes, and what it does not take
// back (RFC 4456). Give routers that serve the same clients the same
// cluster ID. Defaults to router_id.
string cluster_id = 15;
}
// Graceful restart (RFC 4724): BGP restarting without the routes going.
@@ -304,6 +311,17 @@ message Peer {
// the same numbers on all routers of the AS: a route marked on one is
// acted on by another. From 1.
repeated uint32 groups = 13;
// Pass on to this peer, a router of our own AS, the routes learned from
// our other routers, and pass on the routes it sends to all of them: we
// are its route reflector (RFC 4456). Without it, as BGP has it, a route
// learned from one router of our AS goes to no other, and every router
// needs a session with every other. Routes go with this router as next
// hop, so that the peer can reach what lies behind another router
// through this one. Only for a peer in our own AS: other networks are
// told of learned routes by `export` and the roles alone. The peer's
// `export` still has to pass on learned routes.
bool route_reflector_client = 14;
}
// What is announced to a peer.
+130 -9
View File
@@ -23,7 +23,7 @@
//! routes over a restart of ours drops those it has not had again then.
use std::collections::{HashMap, HashSet};
use std::net::{IpAddr, Ipv6Addr};
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use std::sync::Arc;
use std::time::Duration;
@@ -44,7 +44,7 @@ use tokio::sync::{Notify, OwnedSemaphorePermit, Semaphore, watch};
use tokio::task::JoinHandle;
use tracing::{debug, info, warn};
use crate::export::{self, Egress, Outgoing, Source};
use crate::export::{self, Egress, Outgoing, Reflection, Source};
use crate::family::FamilySet;
use crate::marks::Control;
use crate::metrics::ROUTES_EXPORTED;
@@ -59,6 +59,7 @@ pub fn spawn_advertiser(
registry: SessionRegistry,
local_asn: u32,
control: Option<Control>,
cluster_id: Ipv4Addr,
converged: watch::Receiver<bool>,
) -> JoinHandle<()> {
let advertiser = Advertiser {
@@ -69,6 +70,7 @@ pub fn spawn_advertiser(
registry,
local_asn,
control,
cluster_id,
resyncs: Arc::new(Semaphore::new(RESYNCS_AT_ONCE)),
exporters: HashMap::new(),
};
@@ -96,6 +98,8 @@ struct Advertiser {
registry: SessionRegistry,
local_asn: u32,
control: Option<Control>,
/// The cluster this router reflects routes for (RFC 4456).
cluster_id: Ipv4Addr,
/// Leave for a session to gather the whole table before it sends it;
/// see `Learned::gathered`.
resyncs: Arc<Semaphore>,
@@ -151,6 +155,7 @@ impl Advertiser {
rib: Arc::clone(&self.rib),
resyncs: Arc::clone(&self.resyncs),
control: self.control,
cluster_id: self.cluster_id,
session: session.clone(),
local_asn: self.local_asn,
resend: Arc::clone(&resend),
@@ -196,6 +201,7 @@ struct Exporter {
rib: Arc<RibTable>,
resyncs: Arc<Semaphore>,
control: Option<Control>,
cluster_id: Ipv4Addr,
learned: Option<Learned>,
local_rib: LocalRib,
changes: Receiver<LocalRibEvent>,
@@ -498,6 +504,11 @@ impl Exporter {
policy: &session.export,
groups: &session.groups,
control: self.control,
reflection: Reflection {
cluster_id: self.cluster_id,
client: session.reflector_client,
peer_router_id: session.peer_router_id,
},
};
// What the peer is to end up with for each prefix heard of: the
// last word on it counts. While the whole table is being given out
@@ -507,7 +518,7 @@ impl Exporter {
let mut verdicts = learned.gathered.take().unwrap_or_default();
let mut synced = false;
// Who the routes are from, asked of the table once for each.
let mut sources: HashMap<PeerIndex, Option<bool>> = HashMap::new();
let mut sources: HashMap<PeerIndex, Option<Source>> = HashMap::new();
for item in news {
match item {
News::Reset => {
@@ -535,17 +546,21 @@ impl Exporter {
peer,
attrs,
} => {
let ibgp = *sources
.entry(peer)
.or_insert_with(|| self.rib.peer(peer).map(|peer| peer.ibgp));
let dressed = match ibgp {
let source = *sources.entry(peer).or_insert_with(|| {
self.rib.peer(peer).map(|info| Source {
peer,
ibgp: info.ibgp,
reflector_client: info.reflector_client,
router_id: info.router_id,
})
});
let dressed = match source {
// What we originate ourselves is announced as
// that, and not as someone else's route for it.
Some(ibgp)
Some(source)
if session.families.carries(&prefix)
&& !self.announced.contains(&prefix) =>
{
let source = Source { peer, ibgp };
learned.dressed(&attrs, source, prefix.address_family(), &egress)
}
// The peer has gone, and its routes with it.
@@ -676,6 +691,11 @@ impl Exporter {
policy: &session.export,
groups: &session.groups,
control: self.control,
reflection: Reflection {
cluster_id: self.cluster_id,
client: session.reflector_client,
peer_router_id: session.peer_router_id,
},
};
if !egress.dress(&mut update.path_attributes) {
debug!(
@@ -1294,6 +1314,7 @@ mod tests {
registry.clone(),
65000,
None,
Ipv4Addr::new(192, 0, 2, 1),
watch::channel(true).1,
);
Self {
@@ -1329,6 +1350,7 @@ mod tests {
address,
interface: 0,
ibgp: false,
reflector_client: false,
})
.unwrap();
let (tx, outbound) = mpsc::channel(capacity);
@@ -1398,6 +1420,105 @@ mod tests {
}
}
/// A route from a router of our AS reaches a route reflector client
/// of ours through the advertiser, marked with where it started and our
/// cluster, with its preference, and with us as next hop.
#[tokio::test]
async fn a_route_from_one_of_our_routers_is_reflected_to_a_client() {
let registry = SessionRegistry::new();
let rib = Arc::new(RibTable::new(4));
let cluster = Ipv4Addr::new(10, 0, 0, 100);
spawn_advertiser(
LocalRib::spawn(),
Arc::clone(&rib),
registry.clone(),
65000,
None,
cluster,
watch::channel(true).1,
);
let ours = |n: u8, client| PeerInfo {
asn: 65000,
router_id: Ipv4Addr::new(10, 0, 0, n),
address: IpAddr::V4(Ipv4Addr::new(192, 0, 2, n)),
interface: 0,
ibgp: true,
reflector_client: client,
};
// Another router of ours, not a client, with a route in the table.
let other = rib.add_peer(ours(1, false)).unwrap();
let came = [
PathAttribute::Origin(OriginPathAttribute::IGP),
AsPathAttribute::from_asns(vec![64496]),
PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::new(192, 0, 2, 1))),
PathAttribute::LocalPref(LocalPrefPathAttribute(200)),
];
let attrs = rib.attrs().intern(&came, None).unwrap();
rib.announce(other, &attrs, &[Nlri::from(nth_prefix(1))]);
// A client that is told of learned routes.
let negotiated = Negotiated {
peer_asn: 65000,
ibgp: true,
peer_identifier: Ipv4Addr::new(10, 0, 0, 2),
hold_time_secs: 90,
four_octet_asn: true,
route_refresh: false,
families: FamilySet::supported(),
add_path_receive: FamilySet::default(),
add_path_send: FamilySet::default(),
role_confirmed: false,
extended_next_hop: false,
graceful_restart: None,
};
let rib_peer = rib.add_peer(ours(2, true)).unwrap();
let (tx, mut outbound) = mpsc::channel(16);
let mut handle = SessionHandle::new(
PeerId {
addr: (Ipv4Addr::new(192, 0, 2, 2), 179).into(),
asn: 65000,
},
None,
Direction::Inbound,
(Ipv4Addr::new(192, 0, 2, 100), 179).into(),
&negotiated,
rib_peer,
tx,
);
handle.export = Arc::new(crate::policy::ExportPolicy {
learned_routes: true,
..crate::policy::ExportPolicy::default()
});
handle.reflector_client = true;
let _registration = registry.register(handle);
let update = tokio::time::timeout(Duration::from_secs(10), async {
loop {
match outbound.recv().await {
Some(Message::Update(update)) if !update.announced_routes.is_empty() => {
return update;
}
Some(_) => {}
None => panic!("the session's queue closed"),
}
}
})
.await
.expect("the client is told of the route");
assert_eq!(update.announced_routes, [Nlri::from(nth_prefix(1))]);
assert_eq!(
update.path_attributes,
[
PathAttribute::Origin(OriginPathAttribute::IGP),
AsPathAttribute::from_asns(vec![64496]),
PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::new(192, 0, 2, 100))),
PathAttribute::LocalPref(LocalPrefPathAttribute(200)),
PathAttribute::OriginatorId(Ipv4Addr::new(10, 0, 0, 1)),
PathAttribute::ClusterList(vec![cluster]),
]
);
}
/// One peer that takes nothing must not keep the others from being
/// sent their routes. There used to be one task announcing to every
/// session in turn, and it waited for whichever queue was full.
+69 -1
View File
@@ -16,7 +16,7 @@ use crate::address::{Endpoint, PeerAddress};
use crate::error::{ConfigError, ExportProblem, ImportProblem, OriginateProblem, PeerProblem};
use crate::family::FamilySet;
use crate::marks::{Control, Marks, Patterns, Scrub};
use crate::policy::{ExportPolicy, ImportPolicy, ImportRule, PeerRole, PrefixSet};
use crate::policy::{ExportPolicy, ImportPolicy, ImportRule, PeerRole, PrefixSet, Reflection};
use crate::proto::config::{self as proto, BgpConfig, OriginatedRoute, Peer};
use crate::restart::GracefulRestartConfig;
@@ -28,6 +28,9 @@ const MAX_AS_PATH_PREPEND: u32 = 16;
/// Validated, Rust-native server configuration derived from the proto config.
pub struct Config {
pub router_id: Ipv4Addr,
/// The cluster this router reflects routes for (RFC 4456), where it
/// has route reflector clients.
pub cluster_id: Ipv4Addr,
pub local_asn: u32,
/// The hold time offered to a peer that is not in the configuration
/// (see `accept_unconfigured_peers`). Configured peers have their own.
@@ -94,6 +97,9 @@ pub struct PeerConfig {
/// The groups the session is in, for control communities to name it
/// by.
pub groups: Vec<u32>,
/// Pass on to this peer, in our own AS, the routes learned from our
/// other routers, and its routes to them (RFC 4456).
pub reflector_client: bool,
}
impl Config {
@@ -112,6 +118,14 @@ impl Config {
if proto.local_asn == 0 {
return Err(ConfigError::LocalAsn);
}
let cluster_id = if proto.cluster_id.is_empty() {
router_id
} else {
Ipv4Addr::from_str(&proto.cluster_id)
.ok()
.filter(|id| !id.is_unspecified())
.ok_or_else(|| ConfigError::ClusterId(proto.cluster_id.clone()))?
};
let listen_endpoints = if proto.listen_endpoints.is_empty() {
vec![
@@ -149,6 +163,9 @@ impl Config {
if peer.role.is_some() && peer.remote_asn == proto.local_asn {
return Err(rejected(PeerProblem::RoleInOwnAs));
}
if peer.reflector_client && peer.remote_asn != proto.local_asn {
return Err(rejected(PeerProblem::ReflectorClientOutsideAs));
}
// A peer is told from the others by its name in what the
// dialer reports, and by its address when it connects to us.
if peers.iter().any(|earlier| earlier.name == peer.name) {
@@ -190,6 +207,10 @@ impl Config {
for peer in &mut peers {
if peer.remote_asn == proto.local_asn {
peer.import.originated = Arc::clone(&originated);
peer.import.reflection = Some(Reflection {
router_id,
cluster_id,
});
} else {
peer.import.own_prefixes = Arc::clone(&own_prefixes);
// What is ours to say is not taken from other networks,
@@ -223,6 +244,7 @@ impl Config {
Ok(Self {
router_id,
cluster_id,
local_asn: proto.local_asn,
hold_time_secs: DEFAULT_HOLD_TIME_SECS,
listen_endpoints,
@@ -375,6 +397,7 @@ fn parse_peer(
None => graceful_restart,
},
groups: peer.groups,
reflector_client: peer.route_reflector_client,
})
}
@@ -464,6 +487,7 @@ fn parse_import(import: &proto::ImportPolicy) -> Result<ImportPolicy, ImportProb
)?,
local_pref: import.local_pref,
rules,
reflection: None,
max_as_path_length: match import.max_as_path_length {
0 => defaults.max_as_path_length,
length => u16::try_from(length).map_err(|_| ImportProblem::PathLength(length))?,
@@ -671,6 +695,50 @@ mod tests {
assert!(wrong.to_string().contains("rule 2: "), "{wrong}");
}
/// A route reflector's clients are routers of our own AS; the cluster
/// is the router's own unless given, and what comes from our routers is
/// checked against both.
#[test]
fn route_reflection_is_for_our_own_routers() {
let config = parse(&format!(
"{BASE}peers {{ name: \"client\" endpoint {{ address: \"192.0.2.2\" }} remote_asn: 65000
route_reflector_client: true }}
peers {{ name: \"external\" endpoint {{ address: \"192.0.2.3\" }} remote_asn: 64496 }}"
))
.unwrap();
assert_eq!(config.cluster_id, config.router_id);
assert!(config.peers[0].reflector_client);
assert_eq!(
config.peers[0].import.reflection,
Some(Reflection {
router_id: config.router_id,
cluster_id: config.router_id
})
);
assert_eq!(config.peers[1].import.reflection, None);
let given = parse(&format!("{BASE}cluster_id: \"10.0.0.9\"")).unwrap();
assert_eq!(given.cluster_id, Ipv4Addr::new(10, 0, 0, 9));
assert_eq!(
rejected(&format!("{BASE}cluster_id: \"0.0.0.0\"")),
ConfigError::ClusterId("0.0.0.0".into())
);
let outside = rejected(&format!(
"{BASE}peers {{ name: \"r1\" endpoint {{ address: \"192.0.2.2\" }} remote_asn: 64496
route_reflector_client: true }}"
));
assert!(
matches!(
&outside,
ConfigError::Peer {
problem: PeerProblem::ReflectorClientOutsideAs,
..
}
),
"{outside:?}"
);
}
#[test]
fn sessions_are_put_in_groups() {
let config = parse(&format!(
+11
View File
@@ -116,6 +116,8 @@ pub struct PeerSettings {
pub export: Arc<ExportPolicy>,
/// The groups the session is in, for control communities.
pub groups: Arc<[u32]>,
/// Whether the peer is a route reflector client of ours (RFC 4456).
pub reflector_client: bool,
/// Graceful restart with the peer.
pub graceful_restart: GracefulRestartConfig,
}
@@ -140,6 +142,7 @@ impl PeerSettings {
require_role: false,
export,
groups: Arc::default(),
reflector_client: false,
graceful_restart,
}
}
@@ -158,6 +161,7 @@ impl From<&PeerConfig> for PeerSettings {
require_role: peer.require_role,
export: Arc::clone(&peer.export),
groups: peer.groups.as_slice().into(),
reflector_client: peer.reflector_client,
graceful_restart: peer.graceful_restart,
}
}
@@ -573,6 +577,8 @@ struct Session {
export: Arc<ExportPolicy>,
/// The groups the session is in, for control communities.
groups: Arc<[u32]>,
/// Whether the peer is a route reflector client of ours (RFC 4456).
reflector_client: bool,
/// Where what is refused is noted.
refusals: Arc<PeerRefusals>,
/// Graceful restart with the peer, as configured.
@@ -636,6 +642,7 @@ impl Session {
role: peer.role,
export: peer.export,
groups: peer.groups,
reflector_client: peer.reflector_client,
said: Said::new(),
graceful_restart: peer.graceful_restart,
leave,
@@ -907,6 +914,8 @@ impl Session {
address: self.peer_addr.ip(),
interface: PeerAddress::from(self.peer_addr).interface,
ibgp: negotiated.ibgp,
// The configuration allows it only inside our AS.
reflector_client: self.reflector_client && negotiated.ibgp,
};
// A peer back from a restart takes over the routes kept of it.
let kept = self
@@ -938,6 +947,7 @@ impl Session {
handle.role_confirmed = negotiated.role_confirmed;
handle.export = Arc::clone(&self.export);
handle.groups = Arc::clone(&self.groups);
handle.reflector_client = self.reflector_client && negotiated.ibgp;
// Routes from other networks are validated, where there is data
// to validate against and the peer's policy does not say to take
// invalid ones anyway.
@@ -1603,6 +1613,7 @@ mod tests {
address: Ipv4Addr::new(192, 0, 2, 1).into(),
interface: 0,
ibgp: false,
reflector_client: false,
})
.unwrap();
let update = UpdateMessage {
+4
View File
@@ -164,6 +164,8 @@ pub enum ServerError {
pub enum ConfigError {
#[error("invalid router_id {0:?}: must be a dotted-quad IPv4 address other than 0.0.0.0")]
RouterId(String),
#[error("invalid cluster_id {0:?}: must be a dotted-quad IPv4 address other than 0.0.0.0")]
ClusterId(String),
#[error("local_asn must be non-zero")]
LocalAsn,
#[error("invalid IP address {0:?}")]
@@ -222,6 +224,8 @@ pub enum PeerProblem {
Role,
#[error("groups has a 0 in it; groups are numbered from 1")]
Group,
#[error("route_reflector_client is set for a peer in another AS; it is for routers of our own")]
ReflectorClientOutsideAs,
#[error("import: {0}")]
Import(#[from] ImportProblem),
#[error("export: {0}")]
+199 -6
View File
@@ -10,8 +10,9 @@
//!
//! - 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 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
@@ -21,10 +22,15 @@
//!
//! 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).
//! 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;
use std::net::{IpAddr, Ipv4Addr};
use bgp_packet::message::{
AsPathAttribute, AsPathSegment, LocalPrefPathAttribute, MpReachNlriPathAttribute,
@@ -54,6 +60,20 @@ pub struct Egress<'a> {
/// 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.
@@ -62,6 +82,11 @@ 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
@@ -140,7 +165,12 @@ impl Egress<'_> {
if !self.policy.learned_routes || source.peer == self.rib_peer {
return None;
}
if source.ibgp && towards.ibgp {
// 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).
let reflected = source.ibgp && towards.ibgp;
if reflected && !source.reflector_client && !self.reflection.client {
return None;
}
let mut came_with = next_hop;
@@ -192,6 +222,9 @@ impl Egress<'_> {
// (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).
@@ -204,6 +237,9 @@ impl Egress<'_> {
}
_ => true,
});
if reflected && !self.reflect(&mut attributes, source) {
return None;
}
if towards.ibgp {
if !attributes
.iter()
@@ -252,6 +288,39 @@ impl Egress<'_> {
}
}
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() {
@@ -399,6 +468,7 @@ mod tests {
address: Ipv4Addr::new(192, 0, 2, n).into(),
interface: 0,
ibgp: false,
reflector_client: false,
})
.unwrap()
};
@@ -445,6 +515,22 @@ mod tests {
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<'_> {
@@ -457,10 +543,13 @@ mod tests {
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)
}
@@ -474,6 +563,8 @@ mod tests {
from_ibgp: false,
groups: &[],
control: None,
to_client: false,
from_client: false,
}
}
@@ -537,10 +628,13 @@ mod tests {
policy: &policy,
groups: &[],
control: None,
reflection: reflection(false),
};
let source = Source {
peer: from,
ibgp: false,
reflector_client: false,
router_id: FROM_ID,
};
assert!(
egress
@@ -610,6 +704,105 @@ mod tests {
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]
+74 -1
View File
@@ -122,6 +122,20 @@ pub struct ImportPolicy {
/// What is done to some of the routes that are taken, beyond the
/// above: each route by the first rule that holds for it.
pub rules: Vec<ImportRule>,
/// For a peer in our own AS, what a route passed on inside the AS
/// must not say it has already been through (RFC 4456 §8); `None` for
/// a peer in another AS, whose routes carry no such thing.
pub reflection: Option<Reflection>,
}
/// Who we are to routes passed on from one router of our AS to another:
/// a route whose ORIGINATOR_ID is our BGP Identifier started here, and one
/// whose CLUSTER_LIST holds our cluster ID has been passed on by us
/// already. Either has gone round in a circle, and is not taken.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Reflection {
pub router_id: Ipv4Addr,
pub cluster_id: Ipv4Addr,
}
/// What is done to the routes with some communities, or for some
@@ -191,6 +205,7 @@ impl Default for ImportPolicy {
marks: Marks::default(),
local_pref: None,
rules: Vec::new(),
reflection: None,
}
}
}
@@ -308,6 +323,9 @@ pub enum Rejection {
/// inside one: the machine reaches it directly, and a route by way of
/// a peer would compete with that.
OwnNetwork,
/// The route was passed on inside our AS and has come back: it says it
/// started at this router, or was passed on by it (RFC 4456 §8).
ReflectionLoop,
}
impl Rejection {
@@ -321,7 +339,7 @@ impl Rejection {
)
}
pub const ALL: [Self; 19] = [
pub const ALL: [Self; 20] = [
Self::BogonPrefix,
Self::TooShort,
Self::TooSpecific,
@@ -341,6 +359,7 @@ impl Rejection {
Self::OwnPrefix,
Self::NextHopUnreachable,
Self::OwnNetwork,
Self::ReflectionLoop,
];
/// Where the reason is in [`Rejection::ALL`].
@@ -369,6 +388,7 @@ impl Rejection {
Self::OwnPrefix => "prefix is our own",
Self::NextHopUnreachable => "next hop unreachable",
Self::OwnNetwork => "prefix is a network of this router",
Self::ReflectionLoop => "route reflected back to us",
}
}
}
@@ -500,6 +520,16 @@ impl ImportPolicy {
.is_some_and(|segment| segment.ordered);
let origin = facts.last.filter(|_| ends_in_sequence);
if from.ibgp {
if let Some(us) = self.reflection {
let came_back = attributes.iter().any(|attribute| match attribute {
PathAttribute::OriginatorId(originator) => *originator == us.router_id,
PathAttribute::ClusterList(clusters) => clusters.contains(&us.cluster_id),
_ => false,
});
if came_back {
return Err(Rejection::ReflectionLoop);
}
}
return Ok(origin);
}
@@ -1100,6 +1130,49 @@ mod tests {
);
}
/// A route passed on inside our AS that says it started here, or that
/// it was passed on by our cluster already, has gone round in a circle
/// (RFC 4456 §8).
#[test]
fn a_reflected_route_that_comes_back_is_not_taken() {
let us = Ipv4Addr::new(10, 0, 0, 1);
let cluster = Ipv4Addr::new(10, 0, 0, 9);
let policy = ImportPolicy {
reflection: Some(Reflection {
router_id: us,
cluster_id: cluster,
}),
..ImportPolicy::default()
};
let internal = RouteSource {
peer_asn: 1299,
ibgp: true,
..EXTERNAL
};
let reflected = |originator, clusters: &[Ipv4Addr]| {
let mut attributes = path(&[]);
attributes.push(PathAttribute::OriginatorId(originator));
attributes.push(PathAttribute::ClusterList(clusters.to_vec()));
attributes
};
let elsewhere = Ipv4Addr::new(10, 0, 0, 5);
let other_cluster = Ipv4Addr::new(10, 0, 0, 7);
assert_eq!(
policy
.check_path(&reflected(elsewhere, &[other_cluster]), &internal)
.map(drop),
Ok(())
);
assert_eq!(
policy.check_path(&reflected(us, &[other_cluster]), &internal),
Err(Rejection::ReflectionLoop)
);
assert_eq!(
policy.check_path(&reflected(elsewhere, &[other_cluster, cluster]), &internal),
Err(Rejection::ReflectionLoop)
);
}
#[test]
fn the_path_must_begin_with_the_peers_as_unless_it_is_a_route_server() {
let policy = ImportPolicy::default();
+1
View File
@@ -284,6 +284,7 @@ mod tests {
address: address.into(),
interface: 0,
ibgp: false,
reflector_client: false,
})
.unwrap(),
address,
+1
View File
@@ -249,6 +249,7 @@ impl BgpServer {
self.registry.clone(),
self.config.local_asn,
self.config.control,
self.config.cluster_id,
self.restarts.convergence().watch(),
),
));
+7 -1
View File
@@ -6,7 +6,7 @@
//! the registry to enumerate live sessions and queue outbound messages.
use std::collections::HashMap;
use std::net::SocketAddr;
use std::net::{Ipv4Addr, SocketAddr};
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, RwLock};
use std::time::{Duration, SystemTime};
@@ -167,6 +167,10 @@ pub struct SessionHandle {
pub export: Arc<ExportPolicy>,
/// The groups the session is in, for control communities.
pub groups: Arc<[u32]>,
/// Whether the peer is a route reflector client of ours (RFC 4456).
pub reflector_client: bool,
/// The peer's BGP Identifier.
pub peer_router_id: Ipv4Addr,
outbound: mpsc::Sender<Message>,
/// Which of the RPKI's checks the peer's routes are subject to, if
/// any, and where the session is told to look at its routes again.
@@ -206,6 +210,8 @@ impl SessionHandle {
counters: Arc::default(),
export: Arc::default(),
groups: Arc::default(),
reflector_client: false,
peer_router_id: negotiated.peer_identifier,
outbound,
validation: None,
}
+1
View File
@@ -1096,6 +1096,7 @@ mod tests {
address: Ipv4Addr::new(192, 0, 2, n).into(),
interface: 0,
ibgp: false,
reflector_client: false,
})
.unwrap()
});