Remove policy scripts

Rhai made a decision about a route cost one to three microseconds,
some two hundred times what the same costs in Rust. That was bearable
where a function is asked once for an UPDATE, and is not where every
route is weighed against every peer, as it is when routes are passed
on. What is wanted of a policy (mark routes by where they came from,
mark what is announced, let communities say who is told what) needs no
language; it is to be configuration, done natively.

Kept from that work: route selection goes by a LOCAL_PREF that policy
set on a route from an external peer, and the lab's configuration can
be added to by a phase.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Rayhaan JaufeerallyandClaude Fable 5.1 committed 2026-10-02 22:47:57 +02:00
1 parent cbf0df1eef
commit 9367c38907
48 files changed
+21 -3435

No files matched your search

+5 -174
View File
@@ -1,5 +1,4 @@
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::path::Path;
use std::str::FromStr;
use std::sync::Arc;
use std::time::Duration;
@@ -8,7 +7,6 @@ use bgp_fib::FibConfig;
use bgp_packet::community;
use bgp_packet::message::OriginPathAttribute;
use bgp_packet::open::AddPathSendRecv;
use bgp_policy::{Hook, Policy};
use bgp_primitives::constants::{AfiSafi, IpVersion};
use bgp_primitives::ip_prefix::IpPrefix;
use bgp_rib::{LocalRoute, LocalRouteAttrs};
@@ -54,8 +52,6 @@ pub struct Config {
/// Where route origin validation gets its data; `None` for no
/// validation.
pub rpki: Option<RpkiConfig>,
/// The policy script, if there is one.
pub policy: Option<Arc<Policy>>,
}
#[derive(Debug, Clone)]
@@ -80,10 +76,6 @@ pub struct PeerConfig {
/// Refuse the peer if it does not say what it is itself (RFC 9234
/// §4.2, "strict mode").
pub require_role: bool,
/// The operator's own rules for the routes the peer sends, and for
/// those it is sent: functions of the policy script.
pub import_hook: Option<Hook>,
pub export_hook: Option<Hook>,
}
impl Config {
@@ -92,45 +84,7 @@ impl Config {
self.peers.iter().find(|peer| peer.endpoint.is_at(address))
}
/// Reads a configuration whose files, if it names any, are beside it
/// in the working directory.
pub fn from_proto(proto: BgpConfig) -> Result<Self, ConfigError> {
Self::from_proto_in(proto, Path::new("."))
}
/// Reads a configuration that came from a file in `directory`: other
/// files it names by a relative path are looked for there.
pub fn from_proto_in(proto: BgpConfig, directory: &Path) -> Result<Self, ConfigError> {
let policy = proto
.policy
.as_ref()
.map(|policy| load_policy(policy, directory))
.transpose()?;
let default_hooks = proto.policy.as_ref().map_or(("", ""), |policy| {
(
policy.import_function.as_str(),
policy.export_function.as_str(),
)
});
// The function `named`, or the one for peers that name none.
let hook = |named: &str, default: &str| -> Result<Option<Hook>, PeerProblem> {
let name = if named.is_empty() { default } else { named };
match (&policy, name) {
(_, "") => Ok(None),
(Some(policy), name) => Ok(Some(policy.hook(name)?)),
(None, name) => Err(PeerProblem::NoPolicyScript(name.to_string())),
}
};
// A function named for peers that name none has to be there
// whether or not there is such a peer.
if let Some(policy) = &policy {
for default in [default_hooks.0, default_hooks.1] {
if !default.is_empty() {
policy.hook(default)?;
}
}
}
// Peers refuse an identifier of 0.0.0.0, as we do theirs.
let router_id = Ipv4Addr::from_str(&proto.router_id)
.ok()
@@ -164,10 +118,7 @@ impl Config {
peer: name.clone(),
problem,
};
let functions = (peer.import.function.clone(), peer.export.function.clone());
let mut peer = parse_peer(peer).map_err(rejected)?;
peer.import_hook = hook(&functions.0, default_hooks.0).map_err(rejected)?;
peer.export_hook = hook(&functions.1, default_hooks.1).map_err(rejected)?;
let peer = parse_peer(peer).map_err(rejected)?;
// RFC 9234 is about what networks are to each other.
if peer.role.is_some() && peer.remote_asn == proto.local_asn {
return Err(rejected(PeerProblem::RoleInOwnAs));
@@ -243,7 +194,6 @@ impl Config {
http_endpoint,
kernel: proto.kernel.as_ref().map(parse_kernel),
rpki,
policy,
})
}
}
@@ -349,29 +299,9 @@ fn parse_peer(peer: Peer) -> Result<PeerConfig, PeerProblem> {
import,
role,
require_role: peer.require_role,
import_hook: None,
export_hook: None,
})
}
/// Reads the policy script, from the configuration itself or from the
/// file it names.
fn load_policy(policy: &proto::PolicyScript, directory: &Path) -> Result<Arc<Policy>, ConfigError> {
let source = match (policy.path.as_str(), policy.source.as_str()) {
("", "") => return Err(ConfigError::PolicySource),
("", source) => source.to_string(),
(path, "") => {
let path = directory.join(path);
std::fs::read_to_string(&path).map_err(|e| ConfigError::PolicyFile {
path: path.display().to_string(),
error: e.to_string(),
})?
}
(_, _) => return Err(ConfigError::PolicySource),
};
Ok(Policy::compile(&source)?)
}
fn parse_rpki(rpki: &proto::Rpki) -> Result<RpkiConfig, ConfigError> {
if rpki.caches.is_empty() {
return Err(ConfigError::RpkiNoCaches);
@@ -532,94 +462,6 @@ mod tests {
parse(text).err().expect(text)
}
/// Parses a configuration whose files are in `testdata`.
fn parse_in_testdata(text: &str) -> Result<Config, ConfigError> {
let proto = protobuf::text_format::parse_from_str::<BgpConfig>(text)
.expect("the test's textproto is well formed");
let testdata = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../testdata");
Config::from_proto_in(proto, &testdata)
}
/// The operator's rules are a script: in a file beside the
/// configuration or in the configuration itself, with a function for
/// each direction of each peer, or one for all that name none.
#[test]
fn the_policy_script_is_read_and_its_functions_found() {
const SCRIPT: &str = "fn a(route, peer) { true } fn b(route, peer) { false }";
let with = |policy: &str, peers: &str| {
parse(&format!(
"{BASE}policy {{ source: \"{SCRIPT}\" {policy} }}\n\
peers {{ name: \"r1\" endpoint {{ address: \"192.0.2.2\" }} remote_asn: 1 {peers} }}\n\
peers {{ name: \"r2\" endpoint {{ address: \"192.0.2.3\" }} remote_asn: 2 }}"
))
};
let names = |config: &Config| -> Vec<(Option<String>, Option<String>)> {
let name = |hook: &Option<Hook>| hook.as_ref().map(|hook| hook.name().to_string());
config
.peers
.iter()
.map(|peer| (name(&peer.import_hook), name(&peer.export_hook)))
.collect()
};
// Nobody names a function, and there is none for those: none run.
let config = with("", "").unwrap();
assert_eq!(config.policy.as_ref().unwrap().functions(), ["a", "b"]);
assert_eq!(names(&config), [(None, None), (None, None)]);
// One peer names its own; the other gets the one for the rest.
let config = with(
"import_function: \"a\"",
"import { function: \"b\" } export { function: \"a\" }",
)
.unwrap();
let (a, b) = (Some("a".to_string()), Some("b".to_string()));
assert_eq!(names(&config), [(b, a.clone()), (a, None)]);
// A function that is not there is found now, not when a route comes.
for (policy, peers) in [
("", "import { function: \"c\" }"),
("", "export { function: \"c\" }"),
("export_function: \"c\"", ""),
] {
let said = with(policy, peers).err().expect(peers).to_string();
assert!(
said.contains("the script has no function \"c\"; it has: a, b"),
"{said}"
);
}
// So is a function named without a script, and a script that is
// not one.
let said = rejected(&format!(
"{BASE}peers {{ name: \"r1\" endpoint {{ address: \"192.0.2.2\" }} remote_asn: 1 \
import {{ function: \"a\" }} }}"
))
.to_string();
assert_eq!(
said,
"peer \"r1\": names the policy function \"a\", and there is no policy script"
);
let said = rejected(&format!("{BASE}policy {{ source: \"fn a(route {{\" }}")).to_string();
assert!(
said.starts_with("policy: the script does not parse"),
"{said}"
);
for neither_or_both in ["policy {}", "policy { path: \"a\" source: \"b\" }"] {
assert_eq!(
rejected(&format!("{BASE}{neither_or_both}")),
ConfigError::PolicySource
);
}
// From a file beside the configuration.
let config =
parse_in_testdata(&format!("{BASE}policy {{ path: \"policy.rhai\" }}")).unwrap();
assert!(!config.policy.unwrap().functions().is_empty());
let said = parse_in_testdata(&format!("{BASE}policy {{ path: \"nowhere.rhai\" }}"))
.err()
.unwrap()
.to_string();
assert!(said.starts_with("policy: cannot read "), "{said}");
}
/// The example that the documentation points to, and that operators
/// start from: as it is, and with what it shows commented out put in.
#[test]
@@ -651,18 +493,7 @@ mod tests {
})
.collect();
assert_ne!(shown, example, "nothing was taken for configuration");
let config = parse_in_testdata(&shown).unwrap_or_else(|e| panic!("{e}\n{shown}"));
// The script it names is the example script, and the functions it
// names are in it.
assert!(config.policy.is_some(), "{shown}");
assert_eq!(
config.peers[0].import_hook.as_ref().map(Hook::name),
Some("from_peer")
);
assert_eq!(
config.peers[0].export_hook.as_ref().map(Hook::name),
Some("to_peer")
);
let config = parse(&shown).unwrap_or_else(|e| panic!("{e}\n{shown}"));
assert!(config.rpki.is_some(), "{shown}");
assert!(config.kernel.is_some(), "{shown}");
assert_eq!(config.peers[0].role, Some(PeerRole::Peer));
@@ -851,17 +682,17 @@ mod tests {
other => panic!("{other}"),
};
assert_eq!(
problem(peer("r1", "192.0.2.2") + peer("r1", "192.0.2.3").as_str()),
problem(peer("r1", "192.0.2.2") + &peer("r1", "192.0.2.3")),
PeerProblem::DuplicateName
);
assert_eq!(
problem(peer("r1", "192.0.2.2") + peer("r2", "192.0.2.2").as_str()),
problem(peer("r1", "192.0.2.2") + &peer("r2", "192.0.2.2")),
PeerProblem::DuplicateAddress
);
// A link-local address is one peer on each interface it is on.
assert_eq!(
problem(peer("r1", "fe80::1%eth0") + peer("r2", "fe80::1%eth0").as_str()),
problem(peer("r1", "fe80::1%eth0") + &peer("r2", "fe80::1%eth0")),
PeerProblem::DuplicateAddress
);
let config = parse(&format!(