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
@@ -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
@@ -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
@@ -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(());
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user