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:
1 parent
cbf0df1eef
commit
9367c38907
48 files changed
+21
-3435
No files matched your search
+5
-174
@@ -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!(
|
||||
|
||||
Reference in new issue
Block a user