What was to be scripted is configuration, done natively:
- import { add_communities, add_large_communities, remove_communities,
local_pref }: every route taken from a peer is marked (where it was
learned, say), with nobody else's word for our marks taken, and given
its preference, which selection goes by.
- export { add_communities, add_large_communities, remove_communities,
as_path_prepend, learned_routes }, for a peer or for all that have none
of their own.
- control_communities: the communities by which routes say which peers
they are passed on to, as exchanges' route servers have them (0:P, L:P,
0:L, and the large forms L:0:P, L:1:P, L:0:0), taken off as the route
goes on.
And what the last of these is for: with export.learned_routes a peer is
told the best route of every prefix the others announced. The rules are
BGP's and cannot be turned off: not back where it came from, not to a
peer whose AS is on the path, not between two internal peers, not past
NO_EXPORT or NO_ADVERTISE; our AS on the path and ourselves as next hop
to another network, LOCAL_PREF to our own; to a route server's client
the path, MED and next hop as they came (RFC 7947). Which routes go where
follows from the roles of RFC 9234: a route that came down or across goes
on down only, so that with roles on every peer this makes us transit for
our customers and nothing else. A peer without a role is told everything,
and the daemon says so at startup.
Each session follows the table's best routes with a reader and keeps the
prefixes it has announced, so that it can withdraw them. Routes that go
out alike go in one UPDATE: changes are gathered for 50 ms first, and a
table that is given out anew is gathered whole (at most four sessions at
a time; it is tens of megabytes). For that to work the table's journal
had to give out together what was written together: every change now
carries a time on a table-wide clock, a reader's pass reads every shard
up to one time, no further than the changes that are complete in every
shard, and a resync takes its places in the journals as of one time.
One peer sending 1.23 M routes over a session, another told of them:
in our table after 0.47 s, all heard after 2.1 s, four routes an UPDATE.
The lab has a phase, transit: FRR as our customer and BIRD as our
provider, routes each way with the right paths and marks, one kept back
by a community, and pings between their networks through us.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
115 lines
3.9 KiB
Rust
115 lines
3.9 KiB
Rust
//! Who may connect: only the addresses of configured peers, and each of
|
|
//! them only so many times at once.
|
|
|
|
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_server::config::PeerConfig;
|
|
use bgp_server::family::FamilySet;
|
|
use bgp_server::policy::ImportPolicy;
|
|
use eyre::Result;
|
|
use tokio::net::TcpListener;
|
|
|
|
const LOCAL_ASN: u32 = 65000;
|
|
const PEER_ASN: u32 = 65001;
|
|
const TIMEOUT: Duration = Duration::from_secs(5);
|
|
|
|
fn peer_at(endpoint: SocketAddr) -> PeerConfig {
|
|
PeerConfig {
|
|
name: "r1".into(),
|
|
endpoint: endpoint.into(),
|
|
remote_asn: PEER_ASN,
|
|
hold_time_secs: 90,
|
|
local_address: None,
|
|
families: FamilySet::supported(),
|
|
add_path: None,
|
|
import: ImportPolicy::permissive(),
|
|
role: None,
|
|
require_role: false,
|
|
export: Default::default(),
|
|
}
|
|
}
|
|
|
|
/// Whether the server closed the connection without sending anything.
|
|
async fn closed_unanswered(peer: &mut TestPeer) -> bool {
|
|
matches!(tokio::time::timeout(TIMEOUT, peer.recv()).await, Ok(Err(_)))
|
|
}
|
|
|
|
/// A host that is not a configured peer gets nothing: no OPEN to answer,
|
|
/// no session, no routes taken from it. Any host used to be able to open a
|
|
/// session as any AS it named.
|
|
#[tokio::test]
|
|
async fn test_a_host_that_is_not_a_configured_peer_is_refused() -> Result<()> {
|
|
// The only peer configured is somewhere else (documentation space).
|
|
let server = TestServer::start_with(LOCAL_ASN, |c| {
|
|
c.accept_unconfigured_peers = false;
|
|
c.peers = vec![peer_at("192.0.2.9:179".parse().unwrap())];
|
|
})
|
|
.await?;
|
|
|
|
let mut stranger = TestPeer::connect(server.addr).await?;
|
|
assert!(closed_unanswered(&mut stranger).await);
|
|
assert!(server.registry.list().is_empty());
|
|
Ok(())
|
|
}
|
|
|
|
/// The address of a configured peer is let in, and held to what is
|
|
/// configured for that peer.
|
|
#[tokio::test]
|
|
async fn test_a_configured_peer_is_let_in() -> Result<()> {
|
|
// Somewhere for the server's own dialling to go; never answered.
|
|
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await?;
|
|
let endpoint = listener.local_addr()?;
|
|
let server = TestServer::start_with(LOCAL_ASN, |c| {
|
|
c.accept_unconfigured_peers = false;
|
|
c.peers = vec![peer_at(endpoint)];
|
|
})
|
|
.await?;
|
|
|
|
let mut peer = TestPeer::connect(server.addr).await?;
|
|
peer.handshake(PEER_ASN, Ipv4Addr::new(10, 0, 0, 2), 90)
|
|
.await?;
|
|
tokio::time::timeout(TIMEOUT, async {
|
|
while server.registry.list().is_empty() {
|
|
tokio::time::sleep(Duration::from_millis(10)).await;
|
|
}
|
|
})
|
|
.await?;
|
|
assert_eq!(server.registry.list()[0].name.as_deref(), Some("r1"));
|
|
Ok(())
|
|
}
|
|
|
|
/// One address may hold only a few connections at a time, so that one host
|
|
/// cannot use up the server's sockets with connections that never get as
|
|
/// far as a session.
|
|
#[tokio::test]
|
|
async fn test_one_address_may_open_only_a_few_connections() -> Result<()> {
|
|
let server = TestServer::start(LOCAL_ASN).await?;
|
|
|
|
// Four that say nothing: each is sent the server's OPEN and waited on.
|
|
let mut held = Vec::new();
|
|
for _ in 0..4 {
|
|
let mut peer = TestPeer::connect(server.addr).await?;
|
|
tokio::time::timeout(TIMEOUT, peer.recv_open()).await??;
|
|
held.push(peer);
|
|
}
|
|
let mut one_too_many = TestPeer::connect(server.addr).await?;
|
|
assert!(closed_unanswered(&mut one_too_many).await);
|
|
|
|
// When one of them goes, there is room again.
|
|
held.pop();
|
|
tokio::time::timeout(TIMEOUT, async {
|
|
loop {
|
|
let mut peer = TestPeer::connect(server.addr).await?;
|
|
if peer.recv_open().await.is_ok() {
|
|
return Ok::<_, eyre::Report>(());
|
|
}
|
|
tokio::time::sleep(Duration::from_millis(20)).await;
|
|
}
|
|
})
|
|
.await??;
|
|
Ok(())
|
|
}
|