Control communities: session groups, prepends, and only ours to give

Control communities could say which peers a route goes to, by AS. Now
they can also name sessions by group, and ask for prepends:

  L:11:P .. L:13:P   our AS once to three times more towards AS P
                     (P 0: towards every peer in another AS)
  L:20:G / L:21:G    not to / only to the sessions of group G
  L:31:G .. L:33:G   our AS once to three times more towards group G

A group is a number given to sessions in the configuration (`groups` of
a peer): those at one exchange, or those over one link. The same network
at two exchanges is one AS but two sessions, which is what steering
inbound traffic between exchanges needs. Of several prepends that apply
the most counts, on top of the session's own as_path_prepend. Functions
2 to 9, as the documentation already promised, and 100 and up stay
informational; 10 to 99 are kept for actions to come.

They are acted on where a route leaves our AS, and taken off there; our
informational communities are not. To a peer in our own AS a route goes
with them, unchanged, so that a mark put on at one router is acted on at
another. Before, they were acted on and taken off on internal sessions
too, so a route marked "to nobody" did not even reach our own routers.

And they are ours to give: the communities of our own AS (L:n, L:n:m,
and 0:n where control communities are on) are taken off every route
from a peer in another AS as it comes in, before import marks are put
on. A peer at an exchange could otherwise decide where its routes go
from us, and set our informational marks. A customer that is to steer
its own routes is given import.accept_control_communities. An UPDATE
without any of ours is still taken as it came, without being written
out again.

This changes what a customer that set control communities gets: it now
needs accept_control_communities. The transit example and the interop
lab say so; the lab also has FRR, as a customer, ask for a prepend
towards BIRD's group, and BIRD, as a provider, have its "not to" taken
off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Rayhaan JaufeerallyandClaude Opus 5.5 committed 2026-10-05 16:12:05 +02:00
1 parent 3609451bdc
commit 72444c2706
25 files changed
+833 -120

No files matched your search

@@ -41,6 +41,7 @@ async fn server_offering(
TestServer::start_with(LOCAL_ASN, |c| {
c.peers = vec![PeerConfig {
graceful_restart: Default::default(),
groups: Vec::new(),
name: "route-server-client".into(),
endpoint: SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 1).into(),
remote_asn: PEER_ASN,
@@ -19,6 +19,7 @@ const TIMEOUT: Duration = Duration::from_secs(5);
fn peer_at(endpoint: SocketAddr) -> PeerConfig {
PeerConfig {
graceful_restart: Default::default(),
groups: Vec::new(),
name: "r1".into(),
endpoint: endpoint.into(),
remote_asn: PEER_ASN,
+1
View File
@@ -58,6 +58,7 @@ async fn server_with(
});
c.peers = vec![PeerConfig {
graceful_restart: Default::default(),
groups: Vec::new(),
name: "r1".into(),
endpoint: endpoint.into(),
remote_asn: PEER_ASN,
@@ -37,6 +37,7 @@ async fn server_dialing(listener: &TcpListener) -> Result<TestServer> {
c.connect_retry = Duration::from_millis(200);
c.peers = vec![PeerConfig {
graceful_restart: Default::default(),
groups: Vec::new(),
name: "r1".into(),
endpoint: endpoint.into(),
remote_asn: PEER_ASN,
@@ -168,6 +168,7 @@ async fn test_refused_routes_can_be_seen_and_counted() -> Result<()> {
c.accept_unconfigured_peers = false;
c.peers = vec![PeerConfig {
graceful_restart: Default::default(),
groups: Vec::new(),
name: "refused-demo".into(),
endpoint: endpoint.into(),
remote_asn: 3333,
+4
View File
@@ -28,6 +28,7 @@ async fn server_dialing(listener: &TcpListener, remote_asn: u32) -> Result<TestS
c.connect_retry = Duration::from_millis(200);
c.peers = vec![PeerConfig {
graceful_restart: Default::default(),
groups: Vec::new(),
name: "r1".into(),
endpoint: endpoint.into(),
remote_asn,
@@ -187,6 +188,7 @@ async fn test_inbound_from_configured_address_uses_config() -> Result<()> {
c.connect_retry = Duration::from_secs(60);
c.peers = vec![PeerConfig {
graceful_restart: Default::default(),
groups: Vec::new(),
name: "r1".into(),
endpoint: SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 1).into(),
remote_asn: PEER_ASN,
@@ -240,6 +242,7 @@ async fn test_local_address_is_used_as_the_source() -> Result<()> {
c.connect_retry = Duration::from_millis(200);
c.peers = vec![PeerConfig {
graceful_restart: Default::default(),
groups: Vec::new(),
name: "r1".into(),
endpoint: endpoint.into(),
remote_asn: PEER_ASN,
@@ -262,6 +265,7 @@ async fn test_local_address_is_used_as_the_source() -> Result<()> {
c.connect_retry = Duration::from_millis(200);
c.peers = vec![PeerConfig {
graceful_restart: Default::default(),
groups: Vec::new(),
name: "r1".into(),
endpoint: endpoint.into(),
remote_asn: PEER_ASN,
@@ -10,12 +10,13 @@ use bgp_integration_tests::test_peer::TestPeer;
use bgp_integration_tests::test_server::TestServer;
use bgp_packet::errors::{BgpError, CeaseSubcode};
use bgp_packet::message::{
AsPathAttribute, Message, NextHopPathAttribute, OriginPathAttribute, PathAttribute,
UpdateMessage,
AsPathAttribute, CommunitiesPathAttribute, LargeCommunitiesPathAttribute, LargeCommunity,
Message, NextHopPathAttribute, OriginPathAttribute, PathAttribute, UpdateMessage,
};
use bgp_primitives::ip_prefix::IpPrefix;
use bgp_server::config::PeerConfig;
use bgp_server::family::FamilySet;
use bgp_server::marks::Scrub;
use bgp_server::policy::{ImportPolicy, PrefixSet};
use bgp_server::session::DialState;
use eyre::Result;
@@ -42,6 +43,7 @@ async fn server_with(import: ImportPolicy) -> Result<(TestServer, TcpListener)>
c.connect_retry = Duration::from_millis(50);
c.peers = vec![PeerConfig {
graceful_restart: Default::default(),
groups: Vec::new(),
name: "r1".into(),
endpoint: endpoint.into(),
remote_asn: PEER_ASN,
@@ -343,3 +345,92 @@ async fn test_a_peers_routes_are_marked_and_ranked_as_configured() -> Result<()>
assert_eq!(server.rib.attrs().len(), 1, "they share their attributes");
Ok(())
}
/// The communities of our own AS are taken off what another network
/// sends, before the route is kept, and nothing else is; from a peer that
/// is to steer its routes with them (a customer), they are kept.
#[tokio::test]
async fn test_our_communities_are_not_taken_from_other_networks() -> Result<()> {
let ours_and_theirs = || {
vec![
PathAttribute::Communities(CommunitiesPathAttribute(vec![
(0, 13335),
(3356, 3),
(1299, 5),
])),
PathAttribute::LargeCommunities(LargeCommunitiesPathAttribute {
communities: vec![large(1299, 0, 0), large(1299, 31, 2), large(3356, 1, 2)],
}),
]
};
let kept = |server: &TestServer| -> Vec<PathAttribute> {
let routes = server.received_routes();
routes[0]
.attributes
.iter()
.filter(|attribute| {
matches!(
attribute,
PathAttribute::Communities(_) | PathAttribute::LargeCommunities(_)
)
})
.cloned()
.collect()
};
let scrubbed = ImportPolicy {
scrub: Some(Scrub {
local_asn: LOCAL_ASN,
zero: true,
}),
..default_policy()
};
let (server, _listener) = server_with(scrubbed).await?;
let mut peer = established(&server).await?;
announce_with(&mut peer, &["193.0.0.0/21"], ours_and_theirs()).await?;
table_after(&server, 1, 0).await?;
assert_eq!(
kept(&server),
[
PathAttribute::Communities(CommunitiesPathAttribute(vec![(3356, 3)])),
PathAttribute::LargeCommunities(LargeCommunitiesPathAttribute {
communities: vec![large(3356, 1, 2)],
}),
]
);
let (server, _listener) = server_with(default_policy()).await?;
let mut peer = established(&server).await?;
announce_with(&mut peer, &["193.0.0.0/21"], ours_and_theirs()).await?;
table_after(&server, 1, 0).await?;
assert_eq!(kept(&server), ours_and_theirs());
Ok(())
}
fn large(global_admin: u32, data_1: u32, data_2: u32) -> LargeCommunity {
LargeCommunity {
global_admin,
data_1,
data_2,
}
}
/// Announces IPv4 `prefixes` from the peer itself, with `more` attributes.
async fn announce_with(
peer: &mut TestPeer,
prefixes: &[&str],
more: Vec<PathAttribute>,
) -> Result<()> {
let mut path_attributes = vec![
PathAttribute::Origin(OriginPathAttribute::IGP),
AsPathAttribute::from_asns(vec![PEER_ASN]),
PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::new(192, 0, 2, 1))),
];
path_attributes.extend(more);
peer.send(&Message::Update(UpdateMessage {
withdrawn_routes: vec![],
path_attributes,
announced_routes: prefixes.iter().map(|p| prefix(p).into()).collect(),
}))
.await
}
@@ -184,6 +184,7 @@ async fn test_no_family_in_common_refuses_the_session() -> Result<()> {
// Inbound connections from this address inherit the peer's settings.
c.peers = vec![PeerConfig {
graceful_restart: Default::default(),
groups: Vec::new(),
name: "v6-only".into(),
endpoint: SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 1).into(),
remote_asn: PEER_ASN,
+1
View File
@@ -49,6 +49,7 @@ async fn server_with(role: Option<PeerRole>, require_role: bool) -> Result<Setup
c.originate = vec![LocalRoute::new(prefix("193.0.0.0/21"), Default::default())];
c.peers = vec![PeerConfig {
graceful_restart: Default::default(),
groups: Vec::new(),
name: "r1".into(),
endpoint: endpoint.into(),
remote_asn: PEER_ASN,
+1
View File
@@ -68,6 +68,7 @@ async fn server_with(
});
c.peers = vec![PeerConfig {
graceful_restart: Default::default(),
groups: Vec::new(),
name: "r1".into(),
endpoint: endpoint.into(),
remote_asn: PEER_ASN,