Policy scripts: the operator's own rules for routes, in Rhai
A new crate, bgp-policy, runs functions of a script on routes: on their
way in, after the general checks of a peer's import and before the
checks that can change (RPKI, next hop reachability); and on their way
out to a peer. A function is given the route and the peer, may refuse
the route, and may change its communities, large communities,
LOCAL_PREF and MED, and on the way out prepend our AS:
fn from_peer(route, peer) {
if route.has_community("65535:666") { return false; }
route.add_community(LEARNED_FROM[peer.role]);
route.local_pref = PREFERENCE[peer.role];
true
}
Configuration: policy { path | source, import_function, export_function }
and, for a peer, import { function } and export { function }. The script
is compiled at startup; `bgp --check` reads it and says which function
is whose. A function that fails on a route has refused it. Scripts have
no files, network or clock, and are stopped after 200,000 steps.
Route selection now goes by a LOCAL_PREF on a route from an external
peer: the decoder never lets a peer's own through, so one that is there
was set by policy.
What it costs was measured before it was built on (benches/policy.rs):
a call that tests a few communities and changes two takes 1 to 3
microseconds, some two hundred times what Rust would. So a function is
asked once for the routes of an UPDATE, which share their attributes,
unless it asks for the prefix. One peer sending 1.23 M routes over a
session: 0.37 s without a function, 0.80 s with one that marks and
ranks every route, 3.4 s if it also looks at the prefix.
Export applies to what is announced, which today is what the daemon
originates; learned routes are not passed on yet.
Tested by unit tests of the crate, sessions end to end, a fuzz target on
the example script (testdata/policy.rhai), and a lab phase in which a
script changes which of FRR's and BIRD's route is chosen and what each
of them is told.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
f1d80c66df
commit
cbf0df1eef
52 files changed
+3467
-43
No files matched your search
@@ -0,0 +1,84 @@
|
||||
//! The operator's policy script, asked about routes with whatever
|
||||
//! attributes a peer can send. The example script (testdata/policy.rhai)
|
||||
//! touches most of what a script can; whatever the route, a function
|
||||
//! either answers or fails, and never panics. What it changes must still
|
||||
//! be attributes that can be written.
|
||||
#![no_main]
|
||||
|
||||
use std::sync::{Arc, LazyLock};
|
||||
|
||||
use bgp_packet::message::{Message, PathAttribute, UpdateMessage};
|
||||
use bgp_policy::{Direction, Hook, Peer, Policy, Subject};
|
||||
use bgp_primitives::ip_prefix::IpPrefix;
|
||||
use bgp_primitives::parser::ParserContext;
|
||||
use bytes::BytesMut;
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
|
||||
static HOOKS: LazyLock<(Arc<Policy>, Vec<Hook>)> = LazyLock::new(|| {
|
||||
let policy = Policy::compile(include_str!("../../testdata/policy.rhai")).unwrap();
|
||||
let hooks = ["from_peer", "to_peer"]
|
||||
.into_iter()
|
||||
.map(|name| policy.hook(name).unwrap())
|
||||
.collect();
|
||||
(policy, hooks)
|
||||
});
|
||||
|
||||
fuzz_target!(|data: &[u8]| {
|
||||
let Some((selector, attributes)) = data.split_first() else {
|
||||
return;
|
||||
};
|
||||
let ctx = ParserContext {
|
||||
four_octet_asn: true,
|
||||
ibgp: false,
|
||||
add_path: Vec::new(),
|
||||
};
|
||||
// An UPDATE with no routes withdrawn, these attributes, and one route.
|
||||
let mut body = vec![0, 0];
|
||||
let attributes = &attributes[..attributes.len().min(4000)];
|
||||
body.extend_from_slice(&(attributes.len() as u16).to_be_bytes());
|
||||
body.extend_from_slice(attributes);
|
||||
body.extend_from_slice(&[24, 203, 0, 113]);
|
||||
let Ok(decoded) = UpdateMessage::decode(&ctx, &body) else {
|
||||
return;
|
||||
};
|
||||
let attributes = decoded.update.path_attributes;
|
||||
let prefix = IpPrefix::try_from("203.0.113.0/24").unwrap();
|
||||
let roles = ["", "customer", "peer", "provider", "route_server"];
|
||||
let peer = Peer::new(
|
||||
"backup-transit",
|
||||
std::net::Ipv4Addr::new(192, 0, 2, 2).into(),
|
||||
64602,
|
||||
65000,
|
||||
roles[usize::from(selector >> 1) % roles.len()],
|
||||
);
|
||||
let (hook, direction) = if selector & 1 == 0 {
|
||||
(&HOOKS.1[0], Direction::Import)
|
||||
} else {
|
||||
(&HOOKS.1[1], Direction::Export)
|
||||
};
|
||||
let subject = Subject {
|
||||
prefix: &prefix,
|
||||
attributes: &attributes,
|
||||
next_hop: None,
|
||||
direction,
|
||||
};
|
||||
let Ok(verdict) = hook.run(&peer, &subject) else {
|
||||
return;
|
||||
};
|
||||
if let Some(changed) = verdict.attributes {
|
||||
let changed: Vec<PathAttribute> = changed;
|
||||
let update = UpdateMessage {
|
||||
withdrawn_routes: vec![],
|
||||
path_attributes: changed,
|
||||
announced_routes: vec![],
|
||||
};
|
||||
let mut wire = BytesMut::new();
|
||||
// Too long to send is a thing that can be; anything else is not.
|
||||
if let Err(e) = Message::Update(update).to_wire(&ctx, &mut wire) {
|
||||
assert!(
|
||||
matches!(e, bgp_primitives::parser::EncodeError::TooLong { .. }),
|
||||
"{e}"
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user