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>
92 lines
4.0 KiB
Protocol Buffer Text Format
92 lines
4.0 KiB
Protocol Buffer Text Format
# A worked example; the fields are documented in
|
|
# crates/server/proto/config.proto.
|
|
router_id: "10.0.0.1"
|
|
local_asn: 65000
|
|
|
|
# Where to accept BGP connections. Port 179 needs privileges; pick another
|
|
# port to run unprivileged against a peer that dials a custom port.
|
|
listen_endpoints { address: "127.0.0.1" port: 179 }
|
|
listen_endpoints { address: "::1" port: 179 }
|
|
|
|
# A peer we connect to and accept connections from. Connections from any
|
|
# other address are closed unread, unless accept_unconfigured_peers is set,
|
|
# which is for a closed test network only.
|
|
peers {
|
|
name: "r1"
|
|
endpoint { address: "127.0.0.1" port: 1791 }
|
|
remote_asn: 65001
|
|
hold_time_seconds: 30
|
|
# Offer only these families to the peer. Left out, all are offered.
|
|
address_families: IPV4_UNICAST
|
|
address_families: IPV6_UNICAST
|
|
# Take several routes for a prefix from this peer, if it offers to send
|
|
# them (ADD-PATH). Also ADD_PATH_SEND and ADD_PATH_BOTH. Off by default.
|
|
add_path: ADD_PATH_RECEIVE
|
|
# What the peer is to us: PROVIDER, CUSTOMER, PEER, ROUTE_SERVER or
|
|
# ROUTE_SERVER_CLIENT. With it the peer must not claim to be something
|
|
# else (RFC 9234), routes are marked "only to customer" and leaks of
|
|
# marked routes are refused; and with a validator that has ASPAs, the
|
|
# AS paths of the peer's routes are verified.
|
|
# role: PEER
|
|
# Refuse the peer if its OPEN does not say what it is.
|
|
# require_role: true
|
|
# Which of the peer's routes are taken. Without this block the general
|
|
# checks still apply: no private or reserved prefixes or AS numbers,
|
|
# nothing longer than /24 or /48, no default route, and a path that
|
|
# begins with the peer's AS. This example is a test network that uses
|
|
# exactly such prefixes and AS numbers, so it takes them.
|
|
import {
|
|
# More routes than this ends the session and keeps the peer down
|
|
# for hold_down_seconds (600 unless set).
|
|
max_prefixes: 1000
|
|
accept_bogons: true
|
|
# A route's next hop has to be the peer itself. The test peer of
|
|
# this example is at 127.0.0.1, which nothing can be sent by way
|
|
# of, and names another address; so here any is taken.
|
|
accept_third_party_next_hop: true
|
|
# For a real peer: the prefixes and origin ASes it may announce,
|
|
# as generated from the routing registries.
|
|
# prefixes { prefix: "193.0.0.0/21" max_length: 24 }
|
|
# origin_asns: 3333
|
|
# And then the operator's own rules: a function of the policy
|
|
# script (see `policy` below), which may refuse the route or mark
|
|
# and rank it.
|
|
# function: "from_peer"
|
|
}
|
|
# What the peer is told also goes through a function of the script.
|
|
# export { function: "to_peer" }
|
|
}
|
|
connect_retry_seconds: 5
|
|
|
|
# Let a restarted peer replace a stale Established session immediately
|
|
# (RFC 4271 CollisionDetectEstablishedState). Default false.
|
|
collision_detect_established_state: false
|
|
|
|
# Routes this router originates, announced to every peer.
|
|
originate { prefix: "10.100.0.0/16" }
|
|
originate {
|
|
prefix: "2001:db8:100::/48"
|
|
med: 50
|
|
communities: "65000:100"
|
|
large_communities: "65000:1:2"
|
|
}
|
|
|
|
# The operator's own rules for routes, as a script beside this file; see
|
|
# policy.rhai here for an example, and "Policy scripts" in
|
|
# bin/bgp/README.md. `bgp --check` reads it and says what is wrong.
|
|
# policy { path: "policy.rhai" }
|
|
|
|
# Validation with the RPKI: refuse routes whose origin AS is not the one the
|
|
# holder of the addresses authorised, and, from peers with a role, routes
|
|
# whose AS path is a leak. Point it at a validator's RTR server.
|
|
# rpki { caches { address: "127.0.0.1" port: 3323 } }
|
|
|
|
# Put the best route of every prefix in the kernel's routing table, so that
|
|
# this machine forwards by them (Linux; needs CAP_NET_ADMIN). Routes are
|
|
# marked "proto bgp"; table and metric default to 254 (main) and 20.
|
|
# kernel {}
|
|
|
|
# Read-only status page, JSON API and metrics. It has no authentication:
|
|
# keep it on loopback or behind a reverse proxy.
|
|
http_endpoint { address: "127.0.0.1" port: 8080 }
|