Pass learned routes on to peers, with marks and control communities
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>
This commit is contained in:
1 parent
9367c38907
commit
1e2e8f2d86
34 files changed
+2938
-69
No files matched your search
@@ -38,5 +38,9 @@ harness = false
|
||||
name = "real_table"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "transit"
|
||||
harness = false
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1,152 @@
|
||||
//! What passing routes on costs: one peer sends the table over a session,
|
||||
//! and another is told of every route, over its own. The time is from the
|
||||
//! first byte sent to the last route heard; alongside, the time for the
|
||||
//! same table to be in our own table and no further.
|
||||
|
||||
use std::net::Ipv4Addr;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use bgp_bench::{Prefixes, Shape, announcements, encoded};
|
||||
use bgp_integration_tests::test_peer::TestPeer;
|
||||
use bgp_integration_tests::test_server::TestServer;
|
||||
use bgp_packet::message::PathAttribute;
|
||||
use bgp_server::marks::{Control, Marks};
|
||||
use bgp_server::policy::ExportPolicy;
|
||||
use bytes::BytesMut;
|
||||
|
||||
fn env(name: &str, default: usize) -> usize {
|
||||
std::env::var(name)
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(default)
|
||||
}
|
||||
|
||||
/// How a server passes routes on in one run.
|
||||
#[derive(Clone, Copy)]
|
||||
enum Passing {
|
||||
Not,
|
||||
Plain,
|
||||
/// With the communities that say who is told, and a mark put on every
|
||||
/// route.
|
||||
Dressed,
|
||||
}
|
||||
|
||||
/// Sends the table from one peer and, where it is passed on, hears all of
|
||||
/// it at another. Returns when the table was in, and when it was all
|
||||
/// heard.
|
||||
fn run(
|
||||
wire: &[BytesMut],
|
||||
routes: usize,
|
||||
heard_routes: usize,
|
||||
passing: Passing,
|
||||
) -> (Duration, Option<Duration>) {
|
||||
let runtime = tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.expect("a runtime");
|
||||
runtime.block_on(async {
|
||||
let server = TestServer::start_with(4_200_000_000, |config| {
|
||||
config.export = Arc::new(ExportPolicy {
|
||||
learned_routes: !matches!(passing, Passing::Not),
|
||||
marks: match passing {
|
||||
Passing::Dressed => Marks::parse(&[], &["64601:7".to_string()], &[]).unwrap(),
|
||||
_ => Marks::default(),
|
||||
},
|
||||
prepend: 0,
|
||||
});
|
||||
config.control = matches!(passing, Passing::Dressed).then_some(Control {
|
||||
local_asn: 4_200_000_000,
|
||||
});
|
||||
})
|
||||
.await
|
||||
.expect("a server");
|
||||
let mut hearer = TestPeer::connect(server.addr).await.expect("a connection");
|
||||
hearer
|
||||
// An AS number that is on none of the generated paths: a route
|
||||
// is not passed to a peer whose AS it has been through.
|
||||
.handshake(4_100_000_000, Ipv4Addr::new(10, 0, 0, 100), 90)
|
||||
.await
|
||||
.expect("a session");
|
||||
let mut sender = TestPeer::connect(server.addr).await.expect("a connection");
|
||||
sender
|
||||
.handshake(64513, Ipv4Addr::new(10, 0, 0, 1), 90)
|
||||
.await
|
||||
.expect("a session");
|
||||
let stream: Vec<u8> = wire.iter().flat_map(|m| m.iter().copied()).collect();
|
||||
let started = Instant::now();
|
||||
let sending = tokio::spawn(async move {
|
||||
sender.send_raw(&stream).await.expect("the server reads");
|
||||
sender
|
||||
});
|
||||
let hearing = tokio::spawn(async move {
|
||||
if matches!(passing, Passing::Not) {
|
||||
return (hearer, None, 0);
|
||||
}
|
||||
let mut heard = 0usize;
|
||||
let mut messages = 0usize;
|
||||
while heard < heard_routes {
|
||||
let update = hearer.recv_update().await.expect("an UPDATE");
|
||||
messages += 1;
|
||||
heard += update.announced_routes.len();
|
||||
heard += update
|
||||
.path_attributes
|
||||
.iter()
|
||||
.map(|attribute| match attribute {
|
||||
PathAttribute::MpReachNlri(mp) => mp.prefixes.len(),
|
||||
_ => 0,
|
||||
})
|
||||
.sum::<usize>();
|
||||
}
|
||||
(hearer, Some(started.elapsed()), messages)
|
||||
});
|
||||
while server.rib.len() < routes {
|
||||
tokio::time::sleep(Duration::from_millis(2)).await;
|
||||
}
|
||||
let in_table = started.elapsed();
|
||||
let (hearer, heard, messages) = hearing.await.expect("the hearer");
|
||||
if messages > 0 {
|
||||
println!(
|
||||
" ({messages} UPDATEs, {:.1} routes each)",
|
||||
heard_routes as f64 / messages as f64
|
||||
);
|
||||
}
|
||||
drop(sending.await);
|
||||
drop(hearer);
|
||||
(in_table, heard)
|
||||
})
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let shape = Shape::INTERNET.scaled_down(env("BENCH_SCALE_DOWN", 1));
|
||||
let prefixes = Prefixes::generate(shape);
|
||||
let wire = encoded(&announcements(&prefixes, shape, 1));
|
||||
let routes = shape.prefixes();
|
||||
// Over a session on IPv4 only the IPv4 routes go.
|
||||
let heard_routes = shape.ipv4_prefixes;
|
||||
println!(
|
||||
"== One peer sends {routes} routes in {} UPDATEs over TCP; another, over IPv4, is told of the {heard_routes} IPv4 ones",
|
||||
wire.len()
|
||||
);
|
||||
for (what, passing) in [
|
||||
("not passed on", Passing::Not),
|
||||
("passed on", Passing::Plain),
|
||||
(
|
||||
"passed on, with control communities and a mark",
|
||||
Passing::Dressed,
|
||||
),
|
||||
] {
|
||||
let (in_table, heard) = run(&wire, routes, heard_routes, passing);
|
||||
let heard = heard.map_or(String::new(), |heard| {
|
||||
format!(
|
||||
" all heard by the other peer after {:>6.3} s ({:.2} M routes/s)",
|
||||
heard.as_secs_f64(),
|
||||
heard_routes as f64 / heard.as_secs_f64() / 1e6
|
||||
)
|
||||
});
|
||||
println!(
|
||||
"{what:<48} in the table after {:>6.3} s{heard}",
|
||||
in_table.as_secs_f64()
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user