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:
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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -58,26 +58,43 @@ message BgpConfig {
|
||||
// own.
|
||||
ExportPolicy export = 12;
|
||||
|
||||
// Let routes say, with communities, which peers they are passed on to,
|
||||
// as the route servers of Internet exchanges do. With L our own AS
|
||||
// number and P a peer's:
|
||||
// Let routes say, with communities, which peers they are passed on to
|
||||
// and how far away we look to them, as the route servers of Internet
|
||||
// exchanges and transit networks do. With L our own AS number, P a
|
||||
// peer's, and G a group of sessions (`groups` of a peer):
|
||||
//
|
||||
// 0:P or L:0:P not to the peer with AS P
|
||||
// L:P or L:1:P to the peer with AS P, and then only to peers
|
||||
// named so
|
||||
// 0:L or L:0:0 to nobody
|
||||
// 0:P or L:0:P not to the peer with AS P
|
||||
// L:P or L:1:P to the peer with AS P, and then only to peers
|
||||
// named so
|
||||
// 0:L or L:0:0 to nobody
|
||||
// L:11:P with our AS once more on the path to the peer
|
||||
// L:12:P with AS P; twice, three times. P 0 is every
|
||||
// L:13:P peer in another AS.
|
||||
// L:20:G not to the sessions of group G
|
||||
// L:21:G to the sessions of group G, and then only to
|
||||
// sessions named so, by group or AS
|
||||
// L:31:G with our AS once more on the path to the
|
||||
// L:32:G sessions of group G; twice, three times
|
||||
// L:33:G
|
||||
//
|
||||
// A route with none of these goes to everyone it otherwise would. "Not"
|
||||
// wins over "to". The two-part form is a standard community, with room
|
||||
// for AS numbers below 65536; the three-part form is a large community
|
||||
// and works for any. They are taken off a route as it is passed on.
|
||||
// wins over "to". Of several prepends that apply, the most counts, on
|
||||
// top of the session's export.as_path_prepend. The two-part form is a
|
||||
// standard community, with room for AS numbers below 65536; the
|
||||
// three-part forms are large communities and work for any.
|
||||
//
|
||||
// They are honoured on routes we originate too (`communities` of
|
||||
// `originate`), and on what import.add_communities puts on.
|
||||
// They are acted on where a route leaves our AS, and taken off there:
|
||||
// to a peer in our own AS a route goes with them, for that router to act
|
||||
// on. They are honoured on routes we originate too (`communities` and
|
||||
// `large_communities` of `originate`), on what import.add_communities
|
||||
// puts on, and on what a peer in our own AS, or one with
|
||||
// import.accept_control_communities, sends. From any other peer the
|
||||
// communities of our own AS are taken off as its routes come in, so that
|
||||
// nobody outside can say what is done with our routes.
|
||||
//
|
||||
// With this on, standard communities L:n are taken for "to the peer
|
||||
// with AS n": mark routes with something else (large communities
|
||||
// L:2:n and up, say).
|
||||
// L:100:n and up, say; functions 2 to 9 are left for marks too).
|
||||
bool control_communities = 13;
|
||||
|
||||
// Graceful restart, for every peer that has no `graceful_restart` of its
|
||||
@@ -280,6 +297,13 @@ message Peer {
|
||||
// Graceful restart with this peer. What is set here takes the place of
|
||||
// what the top-level `graceful_restart` says.
|
||||
GracefulRestart graceful_restart = 12;
|
||||
|
||||
// The groups the session is in, by number, for control communities to
|
||||
// name sessions by (see control_communities): those at one Internet
|
||||
// exchange, say, or those over one link, whatever AS the peers are. Use
|
||||
// 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;
|
||||
}
|
||||
|
||||
// What is announced to a peer.
|
||||
@@ -426,6 +450,15 @@ message ImportPolicy {
|
||||
// The LOCAL_PREF of the peer's routes: of two routes for a prefix the
|
||||
// one with the higher is chosen, whatever else they have. Left out, 100.
|
||||
optional uint32 local_pref = 18;
|
||||
|
||||
// By default the communities of our own AS (L:n and L:n:m; and 0:n,
|
||||
// with control_communities on) are taken off the routes of a peer in
|
||||
// another AS as they come in, before add_communities puts ours on: what
|
||||
// they ask of us is not for others to ask. True keeps them, for a
|
||||
// customer that is to steer its own routes with control communities, or
|
||||
// a route server's client. Not for a peer in our own AS, whose routes
|
||||
// keep them in any case.
|
||||
bool accept_control_communities = 19;
|
||||
}
|
||||
|
||||
// A prefix, and optionally the more specific prefixes inside it.
|
||||
|
||||
@@ -496,6 +496,7 @@ impl Exporter {
|
||||
peer_asn: session.peer.asn,
|
||||
rib_peer: session.rib_peer,
|
||||
policy: &session.export,
|
||||
groups: &session.groups,
|
||||
control: self.control,
|
||||
};
|
||||
// What the peer is to end up with for each prefix heard of: the
|
||||
@@ -673,6 +674,7 @@ impl Exporter {
|
||||
peer_asn: session.peer.asn,
|
||||
rib_peer: session.rib_peer,
|
||||
policy: &session.export,
|
||||
groups: &session.groups,
|
||||
control: self.control,
|
||||
};
|
||||
if !egress.dress(&mut update.path_attributes) {
|
||||
|
||||
@@ -15,7 +15,7 @@ use bgp_rpki::RpkiConfig;
|
||||
use crate::address::{Endpoint, PeerAddress};
|
||||
use crate::error::{ConfigError, ExportProblem, ImportProblem, OriginateProblem, PeerProblem};
|
||||
use crate::family::FamilySet;
|
||||
use crate::marks::{Control, Marks};
|
||||
use crate::marks::{Control, Marks, Scrub};
|
||||
use crate::policy::{ExportPolicy, ImportPolicy, PeerRole, PrefixSet};
|
||||
use crate::proto::config::{self as proto, BgpConfig, OriginatedRoute, Peer};
|
||||
use crate::restart::GracefulRestartConfig;
|
||||
@@ -91,6 +91,9 @@ pub struct PeerConfig {
|
||||
pub export: Arc<ExportPolicy>,
|
||||
/// Graceful restart with the peer (RFC 4724).
|
||||
pub graceful_restart: GracefulRestartConfig,
|
||||
/// The groups the session is in, for control communities to name it
|
||||
/// by.
|
||||
pub groups: Vec<u32>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
@@ -189,6 +192,15 @@ impl Config {
|
||||
peer.import.originated = Arc::clone(&originated);
|
||||
} else {
|
||||
peer.import.own_prefixes = Arc::clone(&own_prefixes);
|
||||
// What is ours to say is not taken from other networks,
|
||||
// unless they are to say it: customers that steer their
|
||||
// own routes.
|
||||
if !peer.import.accept_control_communities {
|
||||
peer.import.scrub = Some(Scrub {
|
||||
local_asn: proto.local_asn,
|
||||
zero: proto.control_communities,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -334,6 +346,9 @@ fn parse_peer(
|
||||
if peer.require_role && role.is_none() {
|
||||
return Err(PeerProblem::RequireRoleWithoutRole);
|
||||
}
|
||||
if peer.groups.contains(&0) {
|
||||
return Err(PeerProblem::Group);
|
||||
}
|
||||
|
||||
Ok(PeerConfig {
|
||||
// A peer without a name goes by its address.
|
||||
@@ -359,6 +374,7 @@ fn parse_peer(
|
||||
Some(own) => graceful_restart.overridden_by(own)?,
|
||||
None => graceful_restart,
|
||||
},
|
||||
groups: peer.groups,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -448,6 +464,8 @@ fn parse_import(import: &proto::ImportPolicy) -> Result<ImportPolicy, ImportProb
|
||||
accept_third_party_next_hop: import.accept_third_party_next_hop,
|
||||
own_prefixes: Arc::default(),
|
||||
originated: Arc::default(),
|
||||
accept_control_communities: import.accept_control_communities,
|
||||
scrub: None,
|
||||
marks: Marks::parse(
|
||||
&import.remove_communities,
|
||||
&import.add_communities,
|
||||
@@ -570,6 +588,56 @@ mod tests {
|
||||
|
||||
const BASE: &str = "router_id: \"192.0.2.1\" local_asn: 65000\n";
|
||||
|
||||
/// Our communities are taken off what other networks send, unless a
|
||||
/// peer is to steer its routes with them; not off what our own
|
||||
/// routers send. `0:n` too where control communities make it ours.
|
||||
#[test]
|
||||
fn our_communities_are_taken_only_from_whom_they_are_ours() {
|
||||
let config = parse(&format!(
|
||||
"{BASE}control_communities: true
|
||||
peers {{ name: \"peer\" endpoint {{ address: \"192.0.2.2\" }} remote_asn: 64496 }}
|
||||
peers {{ name: \"customer\" endpoint {{ address: \"192.0.2.3\" }} remote_asn: 64497
|
||||
import {{ accept_control_communities: true }} }}
|
||||
peers {{ name: \"internal\" endpoint {{ address: \"192.0.2.4\" }} remote_asn: 65000 }}"
|
||||
))
|
||||
.unwrap();
|
||||
let scrubs: Vec<_> = config.peers.iter().map(|peer| peer.import.scrub).collect();
|
||||
assert_eq!(
|
||||
scrubs,
|
||||
[
|
||||
Some(Scrub {
|
||||
local_asn: 65000,
|
||||
zero: true
|
||||
}),
|
||||
None,
|
||||
None
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sessions_are_put_in_groups() {
|
||||
let config = parse(&format!(
|
||||
"{BASE}peers {{ name: \"r1\" endpoint {{ address: \"192.0.2.2\" }} remote_asn: 1
|
||||
groups: 2 groups: 10 }}"
|
||||
))
|
||||
.unwrap();
|
||||
assert_eq!(config.peers[0].groups, [2, 10]);
|
||||
let zero = rejected(&format!(
|
||||
"{BASE}peers {{ name: \"r1\" endpoint {{ address: \"192.0.2.2\" }} remote_asn: 1 groups: 0 }}"
|
||||
));
|
||||
assert!(
|
||||
matches!(
|
||||
&zero,
|
||||
ConfigError::Peer {
|
||||
problem: PeerProblem::Group,
|
||||
..
|
||||
}
|
||||
),
|
||||
"{zero:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// What we originate is refused from other networks, with what is
|
||||
/// inside it; from a router of our own it is ignored, and only the
|
||||
/// prefix itself.
|
||||
@@ -845,8 +913,16 @@ mod tests {
|
||||
/// checks; what is said overrides them one by one.
|
||||
#[test]
|
||||
fn an_import_policy_starts_from_the_general_checks() {
|
||||
assert_eq!(import_of("").unwrap(), ImportPolicy::default());
|
||||
assert_eq!(import_of("import {}").unwrap(), ImportPolicy::default());
|
||||
// From another network, without our communities.
|
||||
let general = ImportPolicy {
|
||||
scrub: Some(Scrub {
|
||||
local_asn: 65000,
|
||||
zero: false,
|
||||
}),
|
||||
..ImportPolicy::default()
|
||||
};
|
||||
assert_eq!(import_of("").unwrap(), general);
|
||||
assert_eq!(import_of("import {}").unwrap(), general);
|
||||
|
||||
let policy = import_of(
|
||||
"import {
|
||||
|
||||
@@ -114,6 +114,8 @@ pub struct PeerSettings {
|
||||
pub require_role: bool,
|
||||
/// What the peer is told, and how.
|
||||
pub export: Arc<ExportPolicy>,
|
||||
/// The groups the session is in, for control communities.
|
||||
pub groups: Arc<[u32]>,
|
||||
/// Graceful restart with the peer.
|
||||
pub graceful_restart: GracefulRestartConfig,
|
||||
}
|
||||
@@ -137,6 +139,7 @@ impl PeerSettings {
|
||||
role: None,
|
||||
require_role: false,
|
||||
export,
|
||||
groups: Arc::default(),
|
||||
graceful_restart,
|
||||
}
|
||||
}
|
||||
@@ -154,6 +157,7 @@ impl From<&PeerConfig> for PeerSettings {
|
||||
role: peer.role,
|
||||
require_role: peer.require_role,
|
||||
export: Arc::clone(&peer.export),
|
||||
groups: peer.groups.as_slice().into(),
|
||||
graceful_restart: peer.graceful_restart,
|
||||
}
|
||||
}
|
||||
@@ -567,6 +571,8 @@ struct Session {
|
||||
role: Option<PeerRole>,
|
||||
/// What the peer is told, and how.
|
||||
export: Arc<ExportPolicy>,
|
||||
/// The groups the session is in, for control communities.
|
||||
groups: Arc<[u32]>,
|
||||
/// Where what is refused is noted.
|
||||
refusals: Arc<PeerRefusals>,
|
||||
/// Graceful restart with the peer, as configured.
|
||||
@@ -629,6 +635,7 @@ impl Session {
|
||||
import: peer.import,
|
||||
role: peer.role,
|
||||
export: peer.export,
|
||||
groups: peer.groups,
|
||||
said: Said::new(),
|
||||
graceful_restart: peer.graceful_restart,
|
||||
leave,
|
||||
@@ -930,6 +937,7 @@ impl Session {
|
||||
handle.role = self.role;
|
||||
handle.role_confirmed = negotiated.role_confirmed;
|
||||
handle.export = Arc::clone(&self.export);
|
||||
handle.groups = Arc::clone(&self.groups);
|
||||
// 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.
|
||||
|
||||
@@ -220,6 +220,8 @@ pub enum PeerProblem {
|
||||
RequireRoleWithoutRole,
|
||||
#[error("role has a value that is not one of its settings")]
|
||||
Role,
|
||||
#[error("groups has a 0 in it; groups are numbered from 1")]
|
||||
Group,
|
||||
#[error("import: {0}")]
|
||||
Import(#[from] ImportProblem),
|
||||
#[error("export: {0}")]
|
||||
|
||||
+120
-10
@@ -15,7 +15,9 @@
|
||||
//! - 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`].
|
||||
//! 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
|
||||
@@ -33,7 +35,7 @@ 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::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
|
||||
@@ -46,6 +48,9 @@ pub struct Egress<'a> {
|
||||
/// 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>,
|
||||
@@ -78,16 +83,25 @@ impl Egress<'_> {
|
||||
/// 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) {
|
||||
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 self.policy.prepend > 0 && !towards.ibgp {
|
||||
let ours = std::iter::repeat_n(towards.local_asn, usize::from(self.policy.prepend));
|
||||
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);
|
||||
@@ -361,14 +375,14 @@ mod tests {
|
||||
use std::net::Ipv4Addr;
|
||||
|
||||
use bgp_packet::message::{
|
||||
CommunitiesPathAttribute, MultiExitDiscPathAttribute, OriginPathAttribute,
|
||||
PathAttributeFlags,
|
||||
CommunitiesPathAttribute, LargeCommunitiesPathAttribute, LargeCommunity,
|
||||
MultiExitDiscPathAttribute, OriginPathAttribute, PathAttributeFlags,
|
||||
};
|
||||
use bgp_primitives::ip_prefix::IpPrefix;
|
||||
use bgp_rib::table::{PeerInfo, RibTable};
|
||||
|
||||
use super::*;
|
||||
use crate::marks::Marks;
|
||||
use crate::marks::{Marks, large};
|
||||
|
||||
const LOCAL: u32 = 64601;
|
||||
const THEM: u32 = 64602;
|
||||
@@ -429,6 +443,7 @@ mod tests {
|
||||
role: Option<PeerRole>,
|
||||
to_ibgp: bool,
|
||||
from_ibgp: bool,
|
||||
groups: &'a [u32],
|
||||
control: Option<Control>,
|
||||
}
|
||||
|
||||
@@ -440,6 +455,7 @@ mod tests {
|
||||
peer_asn: if self.to_ibgp { LOCAL } else { THEM },
|
||||
rib_peer: to,
|
||||
policy: self.policy,
|
||||
groups: self.groups,
|
||||
control: self.control,
|
||||
};
|
||||
let source = Source {
|
||||
@@ -456,6 +472,7 @@ mod tests {
|
||||
role: None,
|
||||
to_ibgp: false,
|
||||
from_ibgp: false,
|
||||
groups: &[],
|
||||
control: None,
|
||||
}
|
||||
}
|
||||
@@ -518,6 +535,7 @@ mod tests {
|
||||
peer_asn: THEM,
|
||||
rib_peer: from,
|
||||
policy: &policy,
|
||||
groups: &[],
|
||||
control: None,
|
||||
};
|
||||
let source = Source {
|
||||
@@ -711,6 +729,98 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
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]
|
||||
|
||||
+265
-48
@@ -9,7 +9,9 @@
|
||||
//! - [`Marks`]: communities taken off and put on every route from a peer
|
||||
//! (`import`) or to one (`export`).
|
||||
//! - [`Control`]: communities on a route that say which peers it is, and
|
||||
//! is not, passed on to.
|
||||
//! is not, passed on to, and how far away we look to them.
|
||||
//! - [`Scrub`]: the communities of our own AS taken off what other
|
||||
//! networks send, so that only we and those we let can ask for that.
|
||||
|
||||
use bgp_packet::community::{CommunityError, parse_large, parse_standard};
|
||||
use bgp_packet::message::{
|
||||
@@ -196,26 +198,52 @@ pub(crate) fn put(attributes: &mut Vec<PathAttribute>, attribute: PathAttribute)
|
||||
}
|
||||
}
|
||||
|
||||
/// The communities by which a route says who is told of it, as Internet
|
||||
/// exchanges' route servers have them. With `L` our own AS number and `P`
|
||||
/// a peer's:
|
||||
/// The communities by which a route says who is told of it, and with how
|
||||
/// many prepends, as Internet exchanges' route servers and transit
|
||||
/// networks have them. With `L` our own AS number, `P` a peer's and `G` a
|
||||
/// group of sessions (`groups` of a peer):
|
||||
///
|
||||
/// | Standard | Large | |
|
||||
/// |---|---|---|
|
||||
/// | `0:P` | `L:0:P` | not to the peer with AS `P` |
|
||||
/// | `L:P` | `L:1:P` | to the peer with AS `P`, and with that only to peers named so |
|
||||
/// | `0:L` | `L:0:0` | to nobody |
|
||||
/// | | `L:11:P`, `L:12:P`, `L:13:P` | our AS once, twice, three times more on the path to AS `P`; `P` 0 for everyone |
|
||||
/// | | `L:20:G` | not to the sessions of group `G` |
|
||||
/// | | `L:21:G` | to the sessions of group `G`, and with that only to sessions named so |
|
||||
/// | | `L:31:G`, `L:32:G`, `L:33:G` | our AS once, twice, three times more on the path to group `G` |
|
||||
///
|
||||
/// A route with none of them goes to everyone. What says "not" wins over
|
||||
/// what says "to". A standard community has room for AS numbers below
|
||||
/// 65536 only; the large ones are for the others, and work for all.
|
||||
/// what says "to"; of several prepends, the most counts. A standard
|
||||
/// community has room for AS numbers below 65536 only; the large ones
|
||||
/// are for the others, and work for all.
|
||||
///
|
||||
/// These are taken off a route as it is passed on: they were for us.
|
||||
/// They are acted on where a route leaves our AS, and taken off there:
|
||||
/// they were for us. Other large communities of ours are marks, which go
|
||||
/// on.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Control {
|
||||
pub local_asn: u32,
|
||||
}
|
||||
|
||||
/// What a large community of ours asks for, by its second part.
|
||||
const NOT_TO_AS: u32 = 0;
|
||||
const ONLY_TO_AS: u32 = 1;
|
||||
const PREPEND_TO_AS: [u32; 3] = [11, 12, 13];
|
||||
const NOT_TO_GROUP: u32 = 20;
|
||||
const ONLY_TO_GROUP: u32 = 21;
|
||||
const PREPEND_TO_GROUP: [u32; 3] = [31, 32, 33];
|
||||
|
||||
/// Whether a large community of ours with `function` as its second part
|
||||
/// asks for something, and is not a mark.
|
||||
fn is_action(function: u32) -> bool {
|
||||
matches!(
|
||||
function,
|
||||
NOT_TO_AS | ONLY_TO_AS | NOT_TO_GROUP | ONLY_TO_GROUP
|
||||
) || PREPEND_TO_AS.contains(&function)
|
||||
|| PREPEND_TO_GROUP.contains(&function)
|
||||
}
|
||||
|
||||
/// The well-known communities that limit how far a route goes
|
||||
/// (RFC 1997).
|
||||
const NO_EXPORT: (u16, u16) = (0xFFFF, 0xFF01);
|
||||
@@ -233,22 +261,38 @@ pub fn well_known_allow(communities: &[(u16, u16)], peer_is_internal: bool) -> b
|
||||
})
|
||||
}
|
||||
|
||||
/// A session a route may go out on, as control communities name it.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Named<'a> {
|
||||
pub asn: u32,
|
||||
pub groups: &'a [u32],
|
||||
}
|
||||
|
||||
impl Named<'_> {
|
||||
fn is(&self, asn: u32) -> bool {
|
||||
asn == self.asn
|
||||
}
|
||||
|
||||
fn in_group(&self, group: u32) -> bool {
|
||||
self.groups.contains(&group)
|
||||
}
|
||||
}
|
||||
|
||||
impl Control {
|
||||
/// Our AS number as half a standard community, if it fits.
|
||||
fn local_16(&self) -> Option<u16> {
|
||||
u16::try_from(self.local_asn).ok()
|
||||
}
|
||||
|
||||
/// Whether a route with these communities is passed on to the peer
|
||||
/// with AS number `peer_asn`.
|
||||
/// Whether a route with these communities is passed on to `session`.
|
||||
pub fn allows(
|
||||
&self,
|
||||
communities: &[(u16, u16)],
|
||||
large: &[LargeCommunity],
|
||||
peer_asn: u32,
|
||||
session: Named<'_>,
|
||||
) -> bool {
|
||||
let local_16 = self.local_16();
|
||||
let peer_16 = u16::try_from(peer_asn).ok();
|
||||
let peer_16 = u16::try_from(session.asn).ok();
|
||||
let (mut not_to_peer, mut to_peer, mut to_nobody, mut to_some) =
|
||||
(false, false, false, false);
|
||||
for (high, low) in communities {
|
||||
@@ -267,10 +311,15 @@ impl Control {
|
||||
continue;
|
||||
}
|
||||
match (community.data_1, community.data_2) {
|
||||
(0, 0) => to_nobody = true,
|
||||
(0, asn) => not_to_peer |= asn == peer_asn,
|
||||
(1, asn) => {
|
||||
to_peer |= asn == peer_asn;
|
||||
(NOT_TO_AS, 0) => to_nobody = true,
|
||||
(NOT_TO_AS, asn) => not_to_peer |= session.is(asn),
|
||||
(ONLY_TO_AS, asn) => {
|
||||
to_peer |= session.is(asn);
|
||||
to_some = true;
|
||||
}
|
||||
(NOT_TO_GROUP, group) => not_to_peer |= session.in_group(group),
|
||||
(ONLY_TO_GROUP, group) => {
|
||||
to_peer |= session.in_group(group);
|
||||
to_some = true;
|
||||
}
|
||||
_ => {}
|
||||
@@ -279,31 +328,103 @@ impl Control {
|
||||
!not_to_peer && (to_peer || !(to_nobody || to_some))
|
||||
}
|
||||
|
||||
/// How many more times our AS goes on the path of a route with these
|
||||
/// large communities to `session`: the most that one of them asks.
|
||||
pub fn prepends(&self, large: &[LargeCommunity], session: Named<'_>) -> u8 {
|
||||
let mut most = 0;
|
||||
for community in large {
|
||||
if community.global_admin != self.local_asn {
|
||||
continue;
|
||||
}
|
||||
let (function, target) = (community.data_1, community.data_2);
|
||||
let times = if let Some(at) = PREPEND_TO_AS.iter().position(|f| *f == function) {
|
||||
(target == 0 || session.is(target)).then_some(at + 1)
|
||||
} else if let Some(at) = PREPEND_TO_GROUP.iter().position(|f| *f == function) {
|
||||
session.in_group(target).then_some(at + 1)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
// At most three: the position in a list of three, plus one.
|
||||
most = most.max(times.map_or(0, |times| times as u8));
|
||||
}
|
||||
most
|
||||
}
|
||||
|
||||
/// Takes off `attributes` the communities that were for us to act on.
|
||||
pub fn strip(&self, attributes: &mut Vec<PathAttribute>) {
|
||||
let local_16 = self.local_16();
|
||||
let ours = |community: &(u16, u16)| community.0 == 0 || Some(community.0) == local_16;
|
||||
let has = attributes.iter().any(|attribute| match attribute {
|
||||
PathAttribute::Communities(communities) => communities.0.iter().any(ours),
|
||||
_ => false,
|
||||
});
|
||||
if has {
|
||||
let mut communities = take_communities(attributes);
|
||||
communities.retain(|community| !ours(community));
|
||||
put_communities(attributes, communities);
|
||||
}
|
||||
let ours_large = |community: &LargeCommunity| {
|
||||
community.global_admin == self.local_asn && community.data_1 <= 1
|
||||
};
|
||||
let has_large = attributes.iter().any(|attribute| match attribute {
|
||||
PathAttribute::LargeCommunities(large) => large.communities.iter().any(ours_large),
|
||||
_ => false,
|
||||
});
|
||||
if has_large {
|
||||
let mut large = take_large_communities(attributes);
|
||||
large.retain(|community| !ours_large(community));
|
||||
put_large_communities(attributes, large);
|
||||
}
|
||||
remove_where(
|
||||
attributes,
|
||||
|community| community.0 == 0 || Some(community.0) == local_16,
|
||||
|community| community.global_admin == self.local_asn && is_action(community.data_1),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The communities of our own AS, taken off the routes of a peer in
|
||||
/// another AS as they come in. What they ask of us (see [`Control`]), and
|
||||
/// what our marks say of a route, is not for others to say: a peer at an
|
||||
/// exchange that put `L:0:0` on its routes would decide which of our
|
||||
/// customers hear of them. The standard form of "not to" (`0:P`) is only
|
||||
/// ours to act on where control communities are, and so taken off only
|
||||
/// then.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Scrub {
|
||||
pub local_asn: u32,
|
||||
/// Take off `0:n` too.
|
||||
pub zero: bool,
|
||||
}
|
||||
|
||||
impl Scrub {
|
||||
fn ours(&self, community: &(u16, u16)) -> bool {
|
||||
(self.zero && community.0 == 0) || u32::from(community.0) == self.local_asn
|
||||
}
|
||||
|
||||
fn ours_large(&self, community: &LargeCommunity) -> bool {
|
||||
community.global_admin == self.local_asn
|
||||
}
|
||||
|
||||
/// Whether `attributes` have a community to take off: asked of every
|
||||
/// UPDATE, so that those without one are taken as they came.
|
||||
pub fn finds(&self, attributes: &[PathAttribute]) -> bool {
|
||||
let (standard, large) = communities_of(attributes);
|
||||
standard.iter().any(|community| self.ours(community))
|
||||
|| large.iter().any(|community| self.ours_large(community))
|
||||
}
|
||||
|
||||
pub fn apply(&self, attributes: &mut Vec<PathAttribute>) {
|
||||
remove_where(
|
||||
attributes,
|
||||
|community| self.ours(community),
|
||||
|community| self.ours_large(community),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Takes off `attributes` the standard communities for which `standard`
|
||||
/// holds and the large ones for which `large` does, leaving the
|
||||
/// attributes as they were if there are none.
|
||||
fn remove_where(
|
||||
attributes: &mut Vec<PathAttribute>,
|
||||
standard: impl Fn(&(u16, u16)) -> bool,
|
||||
large: impl Fn(&LargeCommunity) -> bool,
|
||||
) {
|
||||
let (has, has_large) = {
|
||||
let (communities, large_communities) = communities_of(attributes);
|
||||
(
|
||||
communities.iter().any(&standard),
|
||||
large_communities.iter().any(&large),
|
||||
)
|
||||
};
|
||||
if has {
|
||||
let mut communities = take_communities(attributes);
|
||||
communities.retain(|community| !standard(community));
|
||||
put_communities(attributes, communities);
|
||||
}
|
||||
if has_large {
|
||||
let mut large_communities = take_large_communities(attributes);
|
||||
large_communities.retain(|community| !large(community));
|
||||
put_large_communities(attributes, large_communities);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -413,7 +534,7 @@ mod tests {
|
||||
fn communities_say_which_peers_a_route_goes_to() {
|
||||
let control = Control { local_asn: 64601 };
|
||||
let to = |communities: &[(u16, u16)], large: &[LargeCommunity], peer: u32| {
|
||||
control.allows(communities, large, peer)
|
||||
control.allows(communities, large, named(peer, &[]))
|
||||
};
|
||||
// Nothing said: everyone.
|
||||
assert!(to(&[(3356, 3)], &[], 64602));
|
||||
@@ -451,6 +572,7 @@ mod tests {
|
||||
&[large(3356, 0, 64602), large(64601, 2, 64602)],
|
||||
64602
|
||||
));
|
||||
assert!(to(&[], &[large(64601, 100, 64602)], 64602));
|
||||
// A peer whose AS number does not fit a standard community is not
|
||||
// meant by one.
|
||||
assert!(to(&[(0, 1)], &[], 65537));
|
||||
@@ -458,27 +580,122 @@ mod tests {
|
||||
// Our own AS number may not fit either: then only the large ones
|
||||
// can name us.
|
||||
let control = Control { local_asn: wide };
|
||||
assert!(control.allows(&[(0, 64602)], &[], 64603));
|
||||
assert!(!control.allows(&[(0, 64602)], &[], 64602));
|
||||
assert!(!control.allows(&[], &[large(wide, 0, 0)], 64602));
|
||||
assert!(control.allows(&[(0, 64602)], &[], named(64603, &[])));
|
||||
assert!(!control.allows(&[(0, 64602)], &[], named(64602, &[])));
|
||||
assert!(!control.allows(&[], &[large(wide, 0, 0)], named(64602, &[])));
|
||||
}
|
||||
|
||||
fn named(asn: u32, groups: &[u32]) -> Named<'_> {
|
||||
Named { asn, groups }
|
||||
}
|
||||
|
||||
/// Sessions are named by group as well as by AS: the same network at
|
||||
/// two exchanges is two sessions in two groups.
|
||||
#[test]
|
||||
fn communities_name_sessions_by_group() {
|
||||
let control = Control { local_asn: 64601 };
|
||||
let at_one = named(13335, &[1]);
|
||||
let at_two = named(13335, &[2, 10]);
|
||||
let allows = |large: &[LargeCommunity], session| control.allows(&[], large, session);
|
||||
// Not to group 2: the same peer elsewhere still hears of it.
|
||||
assert!(!allows(&[large(64601, 20, 2)], at_two));
|
||||
assert!(allows(&[large(64601, 20, 2)], at_one));
|
||||
// Only to group 1, or to group 1 and AS 13335 anywhere.
|
||||
assert!(allows(&[large(64601, 21, 1)], at_one));
|
||||
assert!(!allows(&[large(64601, 21, 1)], at_two));
|
||||
assert!(!allows(&[large(64601, 21, 1)], named(64602, &[])));
|
||||
assert!(allows(
|
||||
&[large(64601, 21, 1), large(64601, 1, 13335)],
|
||||
at_two
|
||||
));
|
||||
// A session in several groups is in each of them.
|
||||
assert!(!allows(&[large(64601, 20, 10)], at_two));
|
||||
// "Not" wins, whichever way the session is named.
|
||||
assert!(!allows(
|
||||
&[large(64601, 1, 13335), large(64601, 20, 2)],
|
||||
at_two
|
||||
));
|
||||
assert!(!allows(
|
||||
&[large(64601, 21, 2), large(64601, 0, 13335)],
|
||||
at_two
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn communities_say_how_far_away_we_look() {
|
||||
let control = Control { local_asn: 64601 };
|
||||
let at_two = named(13335, &[2]);
|
||||
let prepends = |large: &[LargeCommunity], session| control.prepends(large, session);
|
||||
assert_eq!(prepends(&[], at_two), 0);
|
||||
// To a group, to an AS, to everyone.
|
||||
assert_eq!(prepends(&[large(64601, 31, 2)], at_two), 1);
|
||||
assert_eq!(prepends(&[large(64601, 33, 2)], at_two), 3);
|
||||
assert_eq!(prepends(&[large(64601, 31, 1)], at_two), 0);
|
||||
assert_eq!(prepends(&[large(64601, 12, 13335)], at_two), 2);
|
||||
assert_eq!(prepends(&[large(64601, 12, 64602)], at_two), 0);
|
||||
assert_eq!(prepends(&[large(64601, 11, 0)], at_two), 1);
|
||||
// The most counts, not the sum.
|
||||
assert_eq!(
|
||||
prepends(
|
||||
&[
|
||||
large(64601, 31, 2),
|
||||
large(64601, 13, 13335),
|
||||
large(64601, 12, 0)
|
||||
],
|
||||
at_two
|
||||
),
|
||||
3
|
||||
);
|
||||
// Group 0 is no group; someone else's say nothing.
|
||||
assert_eq!(prepends(&[large(64601, 31, 0)], named(13335, &[])), 0);
|
||||
assert_eq!(prepends(&[large(3356, 13, 0)], at_two), 0);
|
||||
}
|
||||
|
||||
/// From another network, ours are taken off, and nothing else is.
|
||||
#[test]
|
||||
fn our_communities_are_not_taken_from_others() {
|
||||
let scrub = Scrub {
|
||||
local_asn: 64601,
|
||||
zero: true,
|
||||
};
|
||||
let mut attributes = route(
|
||||
&[(0, 64602), (3356, 3), (64601, 900)],
|
||||
&[large(64601, 0, 0), large(64601, 100, 1), large(3356, 0, 5)],
|
||||
);
|
||||
assert!(scrub.finds(&attributes));
|
||||
scrub.apply(&mut attributes);
|
||||
assert_eq!(attributes, route(&[(3356, 3)], &[large(3356, 0, 5)]));
|
||||
assert!(!scrub.finds(&attributes));
|
||||
|
||||
// `0:n` only where it means something to us.
|
||||
let scrub = Scrub {
|
||||
zero: false,
|
||||
..scrub
|
||||
};
|
||||
assert!(!scrub.finds(&route(&[(0, 64602)], &[])));
|
||||
// An AS number too wide for a standard community has none of them.
|
||||
let wide = Scrub {
|
||||
local_asn: 4_200_000_001,
|
||||
zero: false,
|
||||
};
|
||||
assert!(!wide.finds(&route(&[(64601, 1)], &[large(64601, 0, 0)])));
|
||||
assert!(wide.finds(&route(&[], &[large(4_200_000_001, 100, 1)])));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn what_was_for_us_is_taken_off_as_a_route_is_passed_on() {
|
||||
let control = Control { local_asn: 64601 };
|
||||
let actions = [0, 1, 11, 12, 13, 20, 21, 31, 32, 33].map(|f| large(64601, f, 5));
|
||||
let marks = [2, 3, 9, 10, 14, 30, 100].map(|f| large(64601, f, 7));
|
||||
let mut attributes = route(
|
||||
&[(0, 64602), (3356, 3), (64601, 64603)],
|
||||
&[
|
||||
large(64601, 0, 5),
|
||||
large(64601, 1, 6),
|
||||
large(64601, 2, 7),
|
||||
large(3356, 0, 5),
|
||||
],
|
||||
&[&actions[..], &marks[..], &[large(3356, 0, 5)]].concat(),
|
||||
);
|
||||
control.strip(&mut attributes);
|
||||
// Our marks go on; what was for us to do does not.
|
||||
assert_eq!(
|
||||
attributes,
|
||||
route(&[(3356, 3)], &[large(64601, 2, 7), large(3356, 0, 5)])
|
||||
route(&[(3356, 3)], &[&marks[..], &[large(3356, 0, 5)]].concat())
|
||||
);
|
||||
let mut all = route(&[(0, 1)], &[large(64601, 0, 0)]);
|
||||
control.strip(&mut all);
|
||||
|
||||
@@ -37,7 +37,7 @@ use bgp_primitives::constants::IpVersion;
|
||||
use bgp_primitives::ip_prefix::IpPrefix;
|
||||
|
||||
use crate::address::PeerAddress;
|
||||
use crate::marks::{self, Marks};
|
||||
use crate::marks::{self, Marks, Scrub};
|
||||
use crate::sync::IgnorePoison;
|
||||
|
||||
/// The longest prefixes routed between networks: anything more specific is
|
||||
@@ -106,6 +106,14 @@ pub struct ImportPolicy {
|
||||
/// counted as rejected. Empty for a peer in another AS, whose routes
|
||||
/// for them `own_prefixes` refuses.
|
||||
pub originated: Arc<PrefixSet>,
|
||||
/// Keep the communities of our own AS on the peer's routes, for it to
|
||||
/// steer them with control communities: a customer. Without it, those
|
||||
/// of a peer in another AS are taken off by `scrub`.
|
||||
pub accept_control_communities: bool,
|
||||
/// The communities of our own AS taken off the peer's routes as they
|
||||
/// come in, first; `None` for a peer in our own AS and for one with
|
||||
/// `accept_control_communities`.
|
||||
pub scrub: Option<Scrub>,
|
||||
/// Communities taken off and put on every route that is taken.
|
||||
pub marks: Marks,
|
||||
/// The LOCAL_PREF every route that is taken is given; `None` leaves
|
||||
@@ -134,6 +142,8 @@ impl Default for ImportPolicy {
|
||||
accept_third_party_next_hop: false,
|
||||
own_prefixes: Arc::default(),
|
||||
originated: Arc::default(),
|
||||
accept_control_communities: false,
|
||||
scrub: None,
|
||||
marks: Marks::default(),
|
||||
local_pref: None,
|
||||
}
|
||||
@@ -162,14 +172,21 @@ impl ImportPolicy {
|
||||
}
|
||||
|
||||
impl ImportPolicy {
|
||||
/// Whether routes are changed on their way in.
|
||||
pub fn changes_routes(&self) -> bool {
|
||||
!self.marks.is_empty() || self.local_pref.is_some()
|
||||
/// Whether routes with `attributes` are changed on their way in.
|
||||
/// Asked of every UPDATE: those that come as they are to be kept are
|
||||
/// taken as they came, without being written out again.
|
||||
pub fn changes(&self, attributes: &[PathAttribute]) -> bool {
|
||||
!self.marks.is_empty()
|
||||
|| self.local_pref.is_some()
|
||||
|| self.scrub.is_some_and(|scrub| scrub.finds(attributes))
|
||||
}
|
||||
|
||||
/// Makes of `attributes` what the route is kept with: marked, and
|
||||
/// with its preference.
|
||||
/// Makes of `attributes` what the route is kept with: without what
|
||||
/// others may not say for us, marked, and with its preference.
|
||||
pub fn change(&self, attributes: &mut Vec<PathAttribute>) {
|
||||
if let Some(scrub) = self.scrub {
|
||||
scrub.apply(attributes);
|
||||
}
|
||||
self.marks.apply(attributes);
|
||||
if let Some(local_pref) = self.local_pref {
|
||||
marks::put(
|
||||
|
||||
@@ -370,7 +370,7 @@ impl Applying<'_> {
|
||||
// What the policy puts on the peer's routes (communities that say
|
||||
// where they were learned, a preference) is the same for all of
|
||||
// an UPDATE's routes, and worked out once for them.
|
||||
if self.import.policy.changes_routes() {
|
||||
if self.import.policy.changes(&self.update.path_attributes) {
|
||||
let changed = self.changed.get_or_init(|| {
|
||||
let mut attributes = self.update.path_attributes.clone();
|
||||
// Not the route's own: they hold the prefixes.
|
||||
|
||||
@@ -165,6 +165,8 @@ pub struct SessionHandle {
|
||||
pub counters: Arc<SessionCounters>,
|
||||
/// What the peer is told, and how.
|
||||
pub export: Arc<ExportPolicy>,
|
||||
/// The groups the session is in, for control communities.
|
||||
pub groups: Arc<[u32]>,
|
||||
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.
|
||||
@@ -203,6 +205,7 @@ impl SessionHandle {
|
||||
graceful_restart: negotiated.graceful_restart.clone(),
|
||||
counters: Arc::default(),
|
||||
export: Arc::default(),
|
||||
groups: Arc::default(),
|
||||
outbound,
|
||||
validation: None,
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user