What other networks were told of rested on one thing: the Only to Customer mark that routes from providers, peers and route servers get as they come in. A route without it, from a session without a role or by a mistake anywhere along the way, went to every peer, from every router of ours, and nothing of ours stood in the way. export_limits are a second check, independent of the roles, the marks and the export policy. For peers above or beside us (PROVIDER, PEER, ROUTE_SERVER, and any in another AS without a role): - prefixes: only routes for these go out, learned or our own. With our and our customers' space listed, a leak of anyone else's routes goes nowhere above us. - max_prefixes: if more routes were to go out than this, none of them does. The session ends with a Cease / Administrative Shutdown whose Shutdown Communication says why (RFC 9003), and the peer is held down for its import.hold_down_seconds, so that it is told nothing rather than too much. The whole table is counted before any of it is sent. They are set at the top and hold for every such peer that has none of its own; unlike `export`, a peer's own export policy does not drop them. A peer's own export_limits take their place, and an empty one lifts them. Customers, route server clients and our own routers are not limited. A configuration in which learned routes are passed on and a peer in another AS has no role is now refused, as is accept_unconfigured_peers with learned routes: without a role, a peer's routes go to everyone and it is told of everything. This used to be a warning. At startup, a warning for each peer the limits apply to that has none, and for each originated route the limits leave out. A metric, bgp_export_limit_exceeded_total, counts the sessions ended for it. The interop lab has a phase in which a customer leaks: a route outside its space never reaches BIRD, and when more were to go than the limit, BIRD's session ends and its log has our reason. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
298 lines
11 KiB
Rust
298 lines
11 KiB
Rust
//! End-to-end tests of ADD-PATH (RFC 7911): a peer sending several routes
|
|
//! for one prefix, told apart by path identifiers.
|
|
|
|
use std::net::{Ipv4Addr, SocketAddr};
|
|
use std::time::Duration;
|
|
|
|
use bgp_integration_tests::test_peer::TestPeer;
|
|
use bgp_integration_tests::test_server::TestServer;
|
|
use bgp_packet::message::{
|
|
AsPathAttribute, Message, NextHopPathAttribute, OriginPathAttribute, PathAttribute,
|
|
UpdateMessage,
|
|
};
|
|
use bgp_packet::open::{AddPathEntry, AddPathSendRecv, Capability, OpenMessage};
|
|
use bgp_primitives::constants::AfiSafi;
|
|
use bgp_primitives::ip_prefix::{IpPrefix, Nlri};
|
|
use bgp_rib::{LocalRoute, LocalRouteAttrs};
|
|
use bgp_server::config::PeerConfig;
|
|
use bgp_server::family::{FamilySet, SUPPORTED};
|
|
use bgp_server::policy::ImportPolicy;
|
|
use eyre::Result;
|
|
use serde_json::Value;
|
|
|
|
const LOCAL_ASN: u32 = 65000;
|
|
const PEER_ASN: u32 = 65001;
|
|
const TIMEOUT: Duration = Duration::from_secs(5);
|
|
|
|
fn prefix(s: &str) -> IpPrefix {
|
|
IpPrefix::try_from(s).unwrap()
|
|
}
|
|
|
|
/// A server on which connections from this machine are those of a
|
|
/// configured peer, with ADD-PATH offered to it as `add_path` says.
|
|
async fn server_offering(
|
|
add_path: Option<AddPathSendRecv>,
|
|
originate: &[&str],
|
|
) -> Result<TestServer> {
|
|
let originate: Vec<LocalRoute> = originate
|
|
.iter()
|
|
.map(|p| LocalRoute::new(prefix(p), LocalRouteAttrs::default()))
|
|
.collect();
|
|
TestServer::start_with(LOCAL_ASN, |c| {
|
|
c.peers = vec![PeerConfig {
|
|
graceful_restart: Default::default(),
|
|
groups: Vec::new(),
|
|
export_limits: None,
|
|
reflector_client: false,
|
|
name: "route-server-client".into(),
|
|
endpoint: SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 1).into(),
|
|
remote_asn: PEER_ASN,
|
|
hold_time_secs: 90,
|
|
local_address: None,
|
|
families: FamilySet::supported(),
|
|
add_path,
|
|
import: ImportPolicy::permissive(),
|
|
role: None,
|
|
require_role: false,
|
|
export: Default::default(),
|
|
}];
|
|
c.originate = originate;
|
|
// Keep the dialer out of the way.
|
|
c.connect_retry = Duration::from_secs(3600);
|
|
})
|
|
.await
|
|
}
|
|
|
|
/// A session from a peer whose OPEN offers ADD-PATH as `offer` says, for
|
|
/// IPv4 unicast. Returns the peer and the server's OPEN.
|
|
async fn peer_offering(
|
|
server: &TestServer,
|
|
offer: Option<AddPathSendRecv>,
|
|
) -> Result<(TestPeer, OpenMessage)> {
|
|
let mut capabilities = vec![Capability::FourByteAsn { asn: PEER_ASN }];
|
|
capabilities.extend(SUPPORTED.into_iter().map(Capability::multiprotocol));
|
|
if let Some(send_recv) = offer {
|
|
capabilities.push(Capability::AddPath {
|
|
entries: vec![AddPathEntry {
|
|
afi: AfiSafi::IPV4_UNICAST.afi,
|
|
safi: AfiSafi::IPV4_UNICAST.safi,
|
|
send_recv,
|
|
}],
|
|
});
|
|
}
|
|
let mut peer = TestPeer::connect(server.addr).await?;
|
|
let open = peer
|
|
.handshake_with(PEER_ASN, Ipv4Addr::new(10, 0, 0, 1), 90, capabilities)
|
|
.await?;
|
|
tokio::time::timeout(TIMEOUT, async {
|
|
while server.registry.list().is_empty() {
|
|
tokio::time::sleep(Duration::from_millis(5)).await;
|
|
}
|
|
})
|
|
.await?;
|
|
Ok((peer, open))
|
|
}
|
|
|
|
fn add_path_offered(open: &OpenMessage) -> Vec<(AfiSafi, AddPathSendRecv)> {
|
|
open.capabilities()
|
|
.filter_map(|capability| match capability {
|
|
Capability::AddPath { entries } => Some(entries),
|
|
_ => None,
|
|
})
|
|
.flatten()
|
|
.map(|entry| (entry.family(), entry.send_recv))
|
|
.collect()
|
|
}
|
|
|
|
fn announcement(nlri: Nlri, as_path: &[u32]) -> Message {
|
|
Message::Update(UpdateMessage {
|
|
withdrawn_routes: vec![],
|
|
path_attributes: vec![
|
|
PathAttribute::Origin(OriginPathAttribute::IGP),
|
|
AsPathAttribute::from_asns(as_path.to_vec()),
|
|
PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::new(192, 0, 2, 1))),
|
|
],
|
|
announced_routes: vec![nlri],
|
|
})
|
|
}
|
|
|
|
async fn wait_for_routes(server: &TestServer, routes: usize) -> Result<()> {
|
|
tokio::time::timeout(TIMEOUT, async {
|
|
while server.rib.len() != routes {
|
|
tokio::time::sleep(Duration::from_millis(5)).await;
|
|
}
|
|
})
|
|
.await?;
|
|
Ok(())
|
|
}
|
|
|
|
/// The routes the server holds, as (path identifier, AS path, best).
|
|
fn held(server: &TestServer) -> Vec<(u32, Vec<u32>, bool)> {
|
|
let mut held: Vec<(u32, Vec<u32>, bool)> = server
|
|
.received_routes()
|
|
.into_iter()
|
|
.map(|route| {
|
|
let path = route
|
|
.attributes
|
|
.iter()
|
|
.find_map(|attribute| match attribute {
|
|
PathAttribute::AsPath(path) => Some(path.segments[0].path.clone()),
|
|
_ => None,
|
|
})
|
|
.unwrap_or_default();
|
|
(route.path_id, path, route.best)
|
|
})
|
|
.collect();
|
|
held.sort();
|
|
held
|
|
}
|
|
|
|
/// A peer that sends several routes for one prefix: each is kept, the best
|
|
/// of them is chosen, and each is withdrawn on its own.
|
|
#[tokio::test]
|
|
async fn test_several_routes_for_a_prefix_from_one_peer() -> Result<()> {
|
|
let server = server_offering(Some(AddPathSendRecv::Receive), &[]).await?;
|
|
let (base, _http) = server.serve_http().await?;
|
|
let (mut peer, open) = peer_offering(&server, Some(AddPathSendRecv::Send)).await?;
|
|
|
|
// The server offered to receive, for every family it offers.
|
|
assert_eq!(
|
|
add_path_offered(&open),
|
|
SUPPORTED.map(|family| (family, AddPathSendRecv::Receive))
|
|
);
|
|
// In use for the one family the peer sends for, and in that direction.
|
|
let session = &server.registry.list()[0];
|
|
assert_eq!(
|
|
session.add_path_receive,
|
|
[AfiSafi::IPV4_UNICAST].into_iter().collect()
|
|
);
|
|
assert!(session.add_path_send.is_empty());
|
|
peer.send_add_path = vec![AfiSafi::IPV4_UNICAST];
|
|
|
|
let p = prefix("203.0.113.0/24");
|
|
let path = |path_id| Nlri { prefix: p, path_id };
|
|
peer.send(&announcement(path(1), &[PEER_ASN, 64999, 64998]))
|
|
.await?;
|
|
peer.send(&announcement(path(2), &[PEER_ASN, 64998]))
|
|
.await?;
|
|
wait_for_routes(&server, 2).await?;
|
|
assert_eq!(
|
|
held(&server),
|
|
[
|
|
(1, vec![PEER_ASN, 64999, 64998], false),
|
|
(2, vec![PEER_ASN, 64998], true),
|
|
]
|
|
);
|
|
assert_eq!(server.rib.prefix_count(), 1);
|
|
|
|
// The API tells them apart too.
|
|
let routes: Value = reqwest::get(format!("{base}/api/routes/received"))
|
|
.await?
|
|
.json()
|
|
.await?;
|
|
let mut ids: Vec<u64> = routes["routes"]
|
|
.as_array()
|
|
.unwrap()
|
|
.iter()
|
|
.map(|route| route["path_id"].as_u64().unwrap())
|
|
.collect();
|
|
ids.sort_unstable();
|
|
assert_eq!(ids, [1, 2]);
|
|
let peers: Value = reqwest::get(format!("{base}/api/peers"))
|
|
.await?
|
|
.json()
|
|
.await?;
|
|
assert_eq!(
|
|
peers[0]["add_path_receive"],
|
|
serde_json::json!(["IPv4 unicast"])
|
|
);
|
|
|
|
// Announcing a path again replaces that path only.
|
|
peer.send(&announcement(path(1), &[PEER_ASN])).await?;
|
|
tokio::time::timeout(TIMEOUT, async {
|
|
while held(&server)[0].1.len() != 1 {
|
|
tokio::time::sleep(Duration::from_millis(5)).await;
|
|
}
|
|
})
|
|
.await?;
|
|
assert_eq!(
|
|
held(&server),
|
|
[(1, vec![PEER_ASN], true), (2, vec![PEER_ASN, 64998], false),]
|
|
);
|
|
|
|
// And withdrawing one leaves the other.
|
|
peer.send(&Message::Update(UpdateMessage {
|
|
withdrawn_routes: vec![path(1)],
|
|
path_attributes: vec![],
|
|
announced_routes: vec![],
|
|
}))
|
|
.await?;
|
|
wait_for_routes(&server, 1).await?;
|
|
assert_eq!(held(&server), [(2, vec![PEER_ASN, 64998], true)]);
|
|
|
|
// When the session ends, all of its routes go.
|
|
peer.send(&announcement(path(3), &[PEER_ASN, 1, 2, 3]))
|
|
.await?;
|
|
wait_for_routes(&server, 2).await?;
|
|
drop(peer);
|
|
wait_for_routes(&server, 0).await?;
|
|
Ok(())
|
|
}
|
|
|
|
/// ADD-PATH changes how prefixes are written. It must be in use only when
|
|
/// both sides said so; a peer that offers it to a server that does not is
|
|
/// spoken to, and listened to, in plain BGP.
|
|
#[tokio::test]
|
|
async fn test_add_path_is_not_used_unless_both_sides_offer_it() -> Result<()> {
|
|
let server = server_offering(None, &["10.100.0.0/16"]).await?;
|
|
let (mut peer, open) = peer_offering(&server, Some(AddPathSendRecv::Both)).await?;
|
|
assert!(add_path_offered(&open).is_empty());
|
|
let session = &server.registry.list()[0];
|
|
assert!(session.add_path_receive.is_empty() && session.add_path_send.is_empty());
|
|
|
|
// Plain prefixes both ways.
|
|
let update = tokio::time::timeout(TIMEOUT, peer.recv_update()).await??;
|
|
assert_eq!(update.announced_routes, vec![prefix("10.100.0.0/16")]);
|
|
peer.send(&announcement(prefix("203.0.113.0/24").into(), &[PEER_ASN]))
|
|
.await?;
|
|
wait_for_routes(&server, 1).await?;
|
|
assert_eq!(held(&server), [(0, vec![PEER_ASN], true)]);
|
|
|
|
// And the other way round: offered by the server, not by the peer.
|
|
let server = server_offering(Some(AddPathSendRecv::Both), &["10.100.0.0/16"]).await?;
|
|
let (mut peer, open) = peer_offering(&server, None).await?;
|
|
assert_eq!(add_path_offered(&open).len(), SUPPORTED.len());
|
|
let update = tokio::time::timeout(TIMEOUT, peer.recv_update()).await??;
|
|
assert_eq!(update.announced_routes, vec![prefix("10.100.0.0/16")]);
|
|
Ok(())
|
|
}
|
|
|
|
/// Where the peer offered to receive and the server to send, the server's
|
|
/// prefixes carry a path identifier, and a peer that reads them so
|
|
/// understands them.
|
|
#[tokio::test]
|
|
async fn test_prefixes_are_sent_with_path_identifiers_when_agreed() -> Result<()> {
|
|
let server = server_offering(Some(AddPathSendRecv::Both), &["10.100.0.0/16"]).await?;
|
|
let (mut peer, _) = peer_offering(&server, Some(AddPathSendRecv::Receive)).await?;
|
|
let session = &server.registry.list()[0];
|
|
assert_eq!(
|
|
session.add_path_send,
|
|
[AfiSafi::IPV4_UNICAST].into_iter().collect()
|
|
);
|
|
assert!(session.add_path_receive.is_empty());
|
|
|
|
// Read as agreed: with identifiers.
|
|
peer.ctx.add_path = vec![AfiSafi::IPV4_UNICAST];
|
|
let update = tokio::time::timeout(TIMEOUT, peer.recv_update()).await??;
|
|
assert_eq!(update.announced_routes.len(), 1);
|
|
assert_eq!(update.announced_routes[0].prefix, prefix("10.100.0.0/16"));
|
|
|
|
// The peer did not offer to send, so the server reads its prefixes
|
|
// plain.
|
|
peer.send(&announcement(prefix("203.0.113.0/24").into(), &[PEER_ASN]))
|
|
.await?;
|
|
wait_for_routes(&server, 1).await?;
|
|
assert_eq!(held(&server), [(0, vec![PEER_ASN], true)]);
|
|
Ok(())
|
|
}
|