Files
ferrix/testdata/server.textpb
T
Rayhaan JaufeerallyandClaude Fable 5.1 cbf0df1eef Policy scripts: the operator's own rules for routes, in Rhai
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>
2026-10-02 21:09:39 +02:00

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 }