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

-1
View File
@@ -9,7 +9,6 @@ repository.workspace = true
version.workspace = true
[dependencies]
bgp-policy.workspace = true
bgp-server.workspace = true
bgp-web.workspace = true
clap.workspace = true
+2 -117
View File
@@ -42,9 +42,8 @@ connections, with RFC 4271 collision detection), announces its originated
routes to every peer with itself as next hop, judges what peers send by each
peer's import policy, keeps what passes, chooses a best route for each
prefix, and on Linux can put those best routes in the kernel so that the
machine forwards by them. The operator's own rules for what is taken, how
it is marked and ranked, and what each peer is told are a script. It does
not yet pass routes learned from one peer on to another.
machine forwards by them. It does not yet pass routes learned from one peer
on to another, and has no export policy.
## Who may connect
@@ -90,112 +89,6 @@ A refused route is dropped, along with the route it would have replaced.
`bgp_routes_rejected_total` counts them by reason, and the status page shows
how many routes each peer has and how many were refused.
## Policy scripts
The checks above say what no network should take from another. What this
network wants beyond that (which routes it prefers, what it marks them
with, what it tells which neighbor) is said in a script, in
[Rhai](https://rhai.rs). A function is given a route and the peer it comes
from or goes to, may change the route, and returns whether it is taken:
```rust
const BLACKHOLE = "65535:666";
const LEARNED_FROM = #{ customer: "64601:1000", peer: "64601:2000", provider: "64601:3000" };
const PREFERENCE = #{ customer: 300, peer: 200, provider: 100 };
// Routes on their way in.
fn from_peer(route, peer) {
if route.has_community(BLACKHOLE) { return false; }
route.remove_community("64601:*"); // nobody else's word for our marks
route.add_community(LEARNED_FROM[peer.role]); // where we learned it
route.local_pref = PREFERENCE[peer.role];
true
}
// Routes on their way out.
fn to_peer(route, peer) {
if route.has_community("64601:3000") && peer.role != "customer" { return false; }
if peer.name == "backup-transit" { route.prepend(2); }
route.remove_community("64601:*");
true
}
```
```
policy { path: "policy.rhai" } # beside the configuration file
peers {
name: "transit-a" ...
role: PROVIDER
import { function: "from_peer" } # after every check of `import`
export { function: "to_peer" }
}
```
`policy { import_function: ... export_function: ... }` names the functions
for peers that name none. `testdata/policy.rhai` is a longer example.
**What a function is given.**
| | |
|---|---|
| `route.prefix`, `route.prefix_len`, `route.family` | `"203.0.113.0/24"`, `24`, `"ipv4"` |
| `route.within("203.0.112.0/22")` | whether the prefix is that one or inside it |
| `route.as_path`, `route.as_path_len` | `[64602, 3356, 15169]`, `3` |
| `route.origin_as`, `route.neighbor_as`, `route.path_has(3356)` | the last and first AS of the path; whether an AS is on it |
| `route.origin`, `route.next_hop` | `"igp"`, `"egp"` or `"incomplete"`; `"192.0.2.1"` |
| `route.communities`, `route.large_communities` | `["3356:3", ...]`, `["64601:1:2", ...]` |
| `route.has_community("3356:3")` | also `(3356, 3)`, and `"3356:*"` for any of that AS |
| `route.add_community(..)`, `route.remove_community("3356:*")` | `"*:*"` removes all |
| `route.has_large_community(..)`, `add_large_community(..)`, `remove_large_community(..)` | the same, as `"64601:1:2"` |
| `route.local_pref`, `route.med` | the number, or `()` for none; set with `route.local_pref = 200`, unset with `= ()` |
| `route.prepend(2)` | on the way out: our AS that many more times on the path |
| `peer.name`, `peer.asn`, `peer.address`, `peer.ibgp`, `peer.local_asn` | |
| `peer.role` | `"customer"`, `"provider"`, `"peer"`, `"route_server"`, `"route_server_client"`, or `""` |
**How it fits in.**
- On the way in, a function is asked after every check of the peer's
`import` has passed, and before the checks whose answer can change (the
RPKI's, and whether the next hop can be reached). A route it refuses is
counted as "refused by policy function". What it changes is what goes
into the table: a `local_pref` it sets is what route selection goes by.
- On the way out it is asked about every route the peer would be sent. The
daemon announces the routes it originates; it does not yet pass on what
it learned from other peers. When it does, those routes will go through
the same function, with the communities they were given on the way in.
LOCAL_PREF does not leave the AS whatever a function sets.
- The script is read when the daemon starts. One that does not parse, or a
function that is named and not there, stops it from starting. `bgp
--check config.textpb` reads the script and says which function is
whose. Changing the script takes a restart.
- A function that fails on a route (it asks for something that is not
there, or does not end) has refused that route: failing is not a way
through. `bgp_policy_failures_total{function}` counts these, and the log
says why, at most once a minute.
- A script can only compute. It has no files, no network and no clock, and
a function is stopped after 200,000 steps.
**What it costs.** Rhai walks the script's syntax tree; a call like the
ones above takes one to three microseconds, where the general checks take
a fraction of one. So a function is not asked about every route. Routes
that arrive in one UPDATE share their attributes, and a function that does
not ask for the prefix (`prefix`, `prefix_len`, `within`) gives one answer
for all of them. One that does is asked about each. With one peer sending
a table of 1.23 million routes over a session (`cargo bench -p bgp-bench
--bench policy`, on a laptop):
| | Time until all are in the table |
|---|---|
| no function | 0.37 s |
| a function that does nothing | 0.51 s |
| one that marks and ranks every route | 0.80 s |
| the same, and it looks at the prefix | 3.4 s |
So: filter by prefix with `import { prefixes ... }` and the length limits,
which cost next to nothing, and keep scripts to what routes carry. A
function that has to look at the prefix still takes in a full table in a
few seconds.
## Route origin validation (RPKI)
With `rpki { caches { address: "..." port: 3323 } }` in the configuration,
@@ -476,7 +369,6 @@ Routinator 0.15.2. What it covers:
| `limits` | A peer whose role does not fit gets no session; a peer over its prefix limit gets Cease / Maximum Number of Prefixes and is held down |
| `hijacks` | FRR announces our own prefix, a route whose next hop is BIRD, and the exchange LAN's own prefix: none is taken |
| `nexthops` | A better route from FRR whose next hop cannot be reached is held and BIRD's is chosen and installed; they swap when the next hop becomes a neighbor, and swap back when it stops being one |
| `policy` | A policy script: routes marked with where they were learned and ranked by it, which changes which of FRR's and BIRD's route is chosen and installed; FRR told our route with a longer path and a mark, BIRD not told it |
| `v6only` | The exchange LAN without IPv4 addresses: one IPv6 session with each router for both families, IPv4 routes with IPv6 next hops (RFC 8950) in BGP and in the kernel, IPv4 pings across |
| `linklocal` | The exchange LAN with link-local addresses only, and the same ones on a second interface: sessions with FRR (by interface, with radvd beside the daemon) and BIRD at `fe80::…%ix0`, both families in BGP and in the kernel, pings across, the interface taken down and up |
@@ -524,8 +416,6 @@ For alerting, `/metricz` has:
| `bgp_peer_max_prefixes{peer}` | The peer's limit, where it has one |
| `bgp_prefix_limit_exceeded_total{peer}` | Sessions ended for going over the limit |
| `bgp_connections_refused_total{reason}` | Connections closed without a session |
| `bgp_policy_runs_total{direction}` | Times a policy function was asked about a route |
| `bgp_policy_failures_total{function}` | Times one failed, which refuses the route |
| `bgp_peer_up{peer}` | 1 while the session with a configured peer is Established, else 0 |
| `bgp_build_info{version}` | Always 1; says which version is running |
@@ -564,11 +454,6 @@ groups:
- alert: BgpHijackAttempt
expr: increase(bgp_routes_rejected_total{reason=~"prefix is our own|next hop is not the peer"}[10m]) > 0
labels: { severity: page }
# A function of the policy script is failing: every route it fails
# on is refused.
- alert: BgpPolicyFunctionFailing
expr: increase(bgp_policy_failures_total[10m]) > 0
labels: { severity: page }
# The kernel is refusing routes, or its answers are being lost.
- alert: BgpKernelErrors
expr: increase(bgp_kernel_errors_total[10m]) > 0
+2 -24
View File
@@ -1,4 +1,4 @@
use std::path::{Path, PathBuf};
use std::path::PathBuf;
use std::sync::Arc;
use bgp_server::config::Config;
@@ -39,13 +39,7 @@ async fn main() -> Result<()> {
.wrap_err_with(|| format!("Failed to read {}", args.config.display()))?;
let proto_config = protobuf::text_format::parse_from_str::<BgpConfig>(&config_string)
.wrap_err("Failed to parse config file")?;
// What the configuration names by a relative path is beside it.
let beside = args
.config
.parent()
.filter(|directory| !directory.as_os_str().is_empty())
.unwrap_or(Path::new("."));
let config = Config::from_proto_in(proto_config, beside).wrap_err("Invalid configuration")?;
let config = Config::from_proto(proto_config).wrap_err("Invalid configuration")?;
if args.check {
println!(
"{} is in order: AS{}, peers: {}, routes originated: {}",
@@ -54,22 +48,6 @@ async fn main() -> Result<()> {
config.peers.len(),
config.originate.len()
);
if let Some(policy) = &config.policy {
println!("policy functions: {}", policy.functions().join(", "));
for peer in &config.peers {
let name = |hook: &Option<bgp_policy::Hook>| {
hook.as_ref()
.map_or("-", bgp_policy::Hook::name)
.to_string()
};
println!(
" {}: import {}, export {}",
peer.name,
name(&peer.import_hook),
name(&peer.export_hook)
);
}
}
return Ok(());
}