A session could only be configured for a peer we would then dial, every few seconds to two minutes, for as long as it was not up. A new peer whose side was not ready yet filled the log with failed attempts, and a peer that opens its sessions itself was dialled for nothing. A peer with `passive: true` is not dialled. A connection from its address is taken as for any configured peer, and the session comes up with all that is configured for it: role, import policy, export limits, hold-downs. Until then the status page and /api/peers show it as `Passive`, waiting for the peer to connect; after a session, with how that session ended, which nothing else would say since nobody dials it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
189 lines
6.3 KiB
Rust
189 lines
6.3 KiB
Rust
//! Export limits, end to end: what a peer above or beside us is told is
|
|
//! held to the prefixes they list and to as many routes as they allow,
|
|
//! whatever else would send it more.
|
|
|
|
use std::collections::BTreeSet;
|
|
use std::net::Ipv4Addr;
|
|
use std::sync::Arc;
|
|
use std::time::Duration;
|
|
|
|
use bgp_integration_tests::default_policy;
|
|
use bgp_integration_tests::test_peer::TestPeer;
|
|
use bgp_integration_tests::test_server::TestServer;
|
|
use bgp_packet::errors::{BgpError, CeaseSubcode};
|
|
use bgp_packet::message::Message;
|
|
use bgp_packet::open::{BgpRole, Capability};
|
|
use bgp_primitives::constants::AfiSafi;
|
|
use bgp_primitives::ip_prefix::IpPrefix;
|
|
use bgp_rib::LocalRoute;
|
|
use bgp_server::config::PeerConfig;
|
|
use bgp_server::family::FamilySet;
|
|
use bgp_server::policy::{ExportLimits, PeerRole, PrefixSet};
|
|
use eyre::{Result, bail};
|
|
use tokio::net::TcpListener;
|
|
|
|
const LOCAL_ASN: u32 = 1299;
|
|
const PEER_ASN: u32 = 3333;
|
|
const TIMEOUT: Duration = Duration::from_secs(5);
|
|
|
|
fn prefix(s: &str) -> IpPrefix {
|
|
IpPrefix::try_from(s).unwrap()
|
|
}
|
|
|
|
/// What a test holds on to: the server, and the listener its dialling
|
|
/// goes to.
|
|
struct Setup {
|
|
server: TestServer,
|
|
_listener: TcpListener,
|
|
}
|
|
|
|
/// A server that originates `originated`, with one configured peer at the
|
|
/// address the test connects from, which is a peer of ours held to
|
|
/// `limits`.
|
|
async fn server_with(originated: &[&str], limits: ExportLimits) -> Result<Setup> {
|
|
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await?;
|
|
let endpoint = listener.local_addr()?;
|
|
let originate = originated
|
|
.iter()
|
|
.map(|p| LocalRoute::new(prefix(p), Default::default()))
|
|
.collect();
|
|
let server = TestServer::start_with(LOCAL_ASN, |c| {
|
|
c.accept_unconfigured_peers = false;
|
|
c.originate = originate;
|
|
c.peers = vec![PeerConfig {
|
|
graceful_restart: Default::default(),
|
|
groups: Vec::new(),
|
|
export_limits: Some(Arc::new(limits)),
|
|
passive: false,
|
|
reflector_client: false,
|
|
name: "r1".into(),
|
|
endpoint: endpoint.into(),
|
|
remote_asn: PEER_ASN,
|
|
hold_time_secs: 90,
|
|
local_address: None,
|
|
families: FamilySet::supported(),
|
|
add_path: None,
|
|
import: default_policy(),
|
|
role: Some(PeerRole::Peer),
|
|
require_role: false,
|
|
export: Default::default(),
|
|
}];
|
|
})
|
|
.await?;
|
|
Ok(Setup {
|
|
server,
|
|
_listener: listener,
|
|
})
|
|
}
|
|
|
|
/// A session with the server, as its peer.
|
|
async fn established(server: &TestServer) -> Result<TestPeer> {
|
|
let mut peer = TestPeer::connect(server.addr).await?;
|
|
let capabilities = vec![
|
|
Capability::FourByteAsn { asn: PEER_ASN },
|
|
Capability::multiprotocol(AfiSafi::IPV4_UNICAST),
|
|
Capability::multiprotocol(AfiSafi::IPV6_UNICAST),
|
|
Capability::BgpRole {
|
|
role: BgpRole::Peer,
|
|
},
|
|
];
|
|
peer.handshake_with(PEER_ASN, Ipv4Addr::new(10, 0, 0, 2), 90, capabilities)
|
|
.await?;
|
|
Ok(peer)
|
|
}
|
|
|
|
/// What the peer has been told once it has been quiet for a while: the
|
|
/// prefixes it holds, and the NOTIFICATION that ended the session, if one
|
|
/// did.
|
|
async fn told(peer: &mut TestPeer) -> (BTreeSet<String>, Option<(BgpError, Vec<u8>)>) {
|
|
let mut routes = BTreeSet::new();
|
|
while let Ok(Ok(message)) = tokio::time::timeout(Duration::from_millis(500), peer.recv()).await
|
|
{
|
|
match message {
|
|
Message::Update(update) => {
|
|
for route in &update.withdrawn_routes {
|
|
routes.remove(&route.prefix.to_string());
|
|
}
|
|
for route in &update.announced_routes {
|
|
routes.insert(route.prefix.to_string());
|
|
}
|
|
}
|
|
Message::Notification(notification) => {
|
|
return (routes, Some((notification.error, notification.data)));
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
(routes, None)
|
|
}
|
|
|
|
fn allowing(prefixes: &[&str]) -> Option<PrefixSet> {
|
|
let mut set = PrefixSet::default();
|
|
for p in prefixes {
|
|
let p = prefix(p);
|
|
set.allow(p, p.length());
|
|
}
|
|
Some(set)
|
|
}
|
|
|
|
/// A route of ours whose prefix is not in the peer's export limits is not
|
|
/// announced to it; the one that is, is.
|
|
#[tokio::test]
|
|
async fn test_a_route_outside_the_export_limits_is_not_announced() -> Result<()> {
|
|
let limits = ExportLimits {
|
|
prefixes: allowing(&["193.0.0.0/21"]),
|
|
max_prefixes: None,
|
|
};
|
|
let Setup { server, .. } = server_with(&["193.0.0.0/21", "198.51.100.0/24"], limits).await?;
|
|
let mut peer = established(&server).await?;
|
|
let (routes, ended) = told(&mut peer).await;
|
|
assert_eq!(routes, BTreeSet::from(["193.0.0.0/21".to_string()]));
|
|
assert_eq!(ended, None);
|
|
Ok(())
|
|
}
|
|
|
|
/// One route more than the export limit allows is not announced: the
|
|
/// session ends with an Administrative Shutdown that says why (RFC 9003),
|
|
/// and the peer is kept down, so that it is told nothing rather than too
|
|
/// much.
|
|
#[tokio::test]
|
|
async fn test_going_over_the_export_limit_ends_the_session_and_keeps_the_peer_down() -> Result<()> {
|
|
let limits = ExportLimits {
|
|
prefixes: None,
|
|
max_prefixes: Some(2),
|
|
};
|
|
let Setup { server, .. } = server_with(&["193.0.0.0/21", "193.0.8.0/21"], limits).await?;
|
|
let mut peer = established(&server).await?;
|
|
let (routes, ended) = told(&mut peer).await;
|
|
assert_eq!(routes.len(), 2);
|
|
assert_eq!(ended, None);
|
|
|
|
server
|
|
.local_rib
|
|
.announce(LocalRoute::new(prefix("193.0.16.0/21"), Default::default()))
|
|
.await?;
|
|
let (more, ended) = told(&mut peer).await;
|
|
assert_eq!(more, BTreeSet::new(), "nothing more is announced");
|
|
let Some((error, data)) = ended else {
|
|
bail!("the session was not ended");
|
|
};
|
|
assert_eq!(error, BgpError::Cease(CeaseSubcode::AdminShutdown));
|
|
let text = String::from_utf8(data[1..].to_vec())?;
|
|
assert!(
|
|
text.starts_with("export limit of 2 routes reached"),
|
|
"{text}"
|
|
);
|
|
assert!(peer.is_closed().await);
|
|
|
|
// The peer is kept down: a new connection is closed unanswered.
|
|
let mut again = TestPeer::connect(server.addr).await?;
|
|
assert!(
|
|
matches!(
|
|
tokio::time::timeout(TIMEOUT, again.recv()).await,
|
|
Ok(Err(_))
|
|
),
|
|
"a held-down peer gets no session"
|
|
);
|
|
Ok(())
|
|
}
|