Origin validation says who may originate a prefix and nothing about the
rest of the path: it does not catch a route leak, nor a hijack that keeps
the right origin at the end of a made-up path. ASPAs, in which a network
lists its providers, do.
bgp-rpki:
- aspa: the ASPAs and the upstream and downstream verification procedures
of draft-ietf-sidrops-aspa-verification-24, tested against the draft's
wording as written.
- rtr: protocol version 2 (draft-ietf-sidrops-8210bis-23) with the ASPA
PDU and the new error codes.
- client: asks for version 2 and falls back to whatever older version the
cache speaks; keeps ASPAs with the VRPs, applied whole or not at all;
publishes which customers' ASPAs changed; tells the cache why it is
being left (Error Report); does not ask one version's serial of another.
Server:
- A peer has a role (provider, customer, peer, route server, route server
client), which decides how its paths are verified. Without one they are
not.
- A route whose path is invalid is refused ("ASPA invalid") and held, like
one with an invalid origin; accept_aspa_invalid exempts a peer.
- When ASPAs change, routes in the table whose path has a changed customer
on it are verified again, and held routes are released when they pass
both checks.
- The revalidator finds a route's peer by index instead of searching a
list for every route.
The status page and API show each peer's role, each route's path validity,
the number of ASPAs and the RTR version in use; bgp_rpki_aspas is on
/metricz.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
10 KiB
BGP
This crate provides the bgp binary, which runs the Border Gateway Protocol
daemon together with its read-only HTTP status interface.
cargo run -- path/to/config.textpb
The configuration format is defined in crates/server/proto/config.proto;
testdata/server.textpb is a worked example. The daemon is configured
entirely through that file: peers, and the routes it originates
(originate { prefix: ... }). There is no way to change either at runtime.
When http_endpoint is set the binary also serves:
| Path | Purpose |
|---|---|
GET /statusz |
Status page: configuration, peers and session state, originated and received routes, live event log |
GET /api/status |
Router ID, ASN, uptime and table sizes |
GET /api/config |
The configuration file as read at startup |
GET /api/peers |
Configured peers and established sessions |
GET /api/routes/local |
Originated routes |
GET /api/routes/received |
Routes learned from peers; see "Finding a route" |
GET /api/routes/rejected |
Routes peers announced that were refused, by peer |
GET /api/rpki |
Validation with the RPKI: the cache, the data and its age |
GET /api/events |
Server-sent events for session and route changes |
GET /metricz |
Prometheus metrics |
Everything under it is read-only and unauthenticated. Bind it to loopback or put it behind a reverse proxy.
What it does today
It establishes sessions (both dialling configured peers and accepting connections, with RFC 4271 collision detection), announces its originated routes to every peer with itself as next hop, and records what peers send in an Adj-RIB-In. It does not select best paths, re-advertise received routes or program a forwarding table, and it has no import or export policy.
Who may connect
Connections are taken only from the addresses of configured peers, and from
each of those only a few at a time. Anything else is closed unread and
counted in bgp_connections_refused_total. accept_unconfigured_peers: true
lets any host open a session as any AS; it exists for tests on a closed
network.
What is taken from a peer
Every route a peer announces is judged before it goes into the routing
table (import in a peer's configuration; see config.proto):
- Refused for any peer unless turned off: prefixes that are not routed on the Internet (private, documentation, multicast, reserved), prefixes longer than /24 or /48, a default route, paths with a private or reserved AS number, and paths that do not begin with the peer's own AS.
- Always refused: a path with our own AS number in it.
- If configured: a list of the prefixes the peer may announce and of the AS numbers its routes may originate from.
max_prefixes: one route more ends the session with Cease / Maximum Number of Prefixes Reached and keeps the peer down for ten minutes.
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.
Route origin validation (RPKI)
With rpki { caches { address: "..." port: 3323 } } in the configuration,
the daemon keeps the validated ROA payloads of a validator (Routinator,
rpki-client with StayRTR, FORT) over the RPKI-to-Router protocol, and
validates every route from peers outside its own AS (RFC 6811):
- invalid (the holder of the addresses authorised another origin, or a
shorter prefix): refused, like any route the import policy refuses, unless
the peer has
accept_rpki_invalid: true; - valid and not found: taken. Validity does not otherwise change which route is chosen.
The data changes all the time, and the routes follow without the peer having to send them again: a route in the table that becomes invalid is taken out, and a route refused as invalid is kept aside ("held") and taken when it no longer is. If no cache confirms the data for the expire interval (two hours unless the cache or the configuration says otherwise) it is dropped, and nothing is invalid any more.
The status page shows the cache, how fresh the data is, each route's
validity, and how many of each peer's routes are held. /api/rpki has the
same as JSON; bgp_rpki_vrps{family}, bgp_rpki_aspas and
bgp_rpki_updates_total are on /metricz. Refused routes are counted under
the reasons RPKI invalid and ASPA invalid.
The connection to the cache is plain TCP, without authentication: run the validator on the same host or on a network you trust.
AS path verification (ASPA)
Origin validation says who may originate a prefix. It says nothing about the rest of the path, so it does not catch a route leak (a network passing its provider's or peer's routes on to another provider or peer), nor a hijack that keeps the right origin at the end of a made-up path. ASPAs do: in one, a network lists its providers, and a path that goes from that network to anyone else, where only a provider could come next, is invalid (draft-ietf-sidrops-aspa-verification).
Two things are needed for paths to be verified:
- a validator that hands out ASPAs, which takes version 2 of the RPKI-to-Router protocol (draft-ietf-sidrops-8210bis). The daemon asks for version 2 and falls back to 1 or 0 if that is what the validator speaks; the status page says which is in use, and warns when it is one without ASPAs;
roleon the peer, saying what the peer is to us, because what a path may look like depends on who hands it over:
role |
The peer | Its paths |
|---|---|---|
CUSTOMER |
buys transit from us | must lead up through providers all the way |
PEER |
exchanges its own and its customers' routes with us | the same |
ROUTE_SERVER |
is an exchange's route server | the same; the path need not begin with the route server's AS |
ROUTE_SERVER_CLIENT |
is a client of ours, where we are the route server | the same |
PROVIDER |
sells us transit | may lead up, across one peering, and down |
A peer without a role has its origins validated and its paths not.
A path is invalid (refused, unless the peer has
accept_aspa_invalid: true), valid, or unknown, which is what most
paths are while few networks have an ASPA; unknown ones are taken. A path
with an AS_SET in it is invalid, and so is one that does not begin with the
peer's AS (other than from a route server). As with origins, routes follow
the data: held while invalid, taken when they no longer are.
rpki { caches { address: "127.0.0.1" port: 3323 } }
peers {
name: "transit"
endpoint { address: "192.0.2.1" }
remote_asn: 64496
role: PROVIDER
}
Finding a route
The status page has a look-up box over the received routes, and
/api/routes/received takes the same questions:
| Query | Answer |
|---|---|
?prefix=193.0.0.0/21 |
Every route for that prefix, the best first |
?address=193.0.0.1 |
Every prefix that covers the address, most specific first |
?within=193.0.0.0/16 |
That prefix and everything inside it |
&peer=NAME |
Only routes from that peer (its name or its address) |
&best=true |
Only the route chosen for each prefix |
&limit=N |
At most N routes (1000 unless given, 100000 at most) |
within, peer and best on their own look through the whole table, which
on a full table takes a moment.
Seeing and alerting on refused routes
A refused route is not in the table. What is kept instead, for each peer, is
how many were refused for each reason and the latest 200 of them with their
AS paths. The status page lists them under "Refused routes";
/api/routes/rejected[?peer=NAME] has them as JSON. The record is kept when
the session ends, so a peer that was cut off can still be looked into. The
log has a line for a peer's first refused route and then at most one a
minute.
For alerting, /metricz has:
| Metric | Meaning |
|---|---|
bgp_routes_rejected_total{peer,reason} |
Routes refused, by peer and why |
bgp_peer_routes{peer} |
Routes an established peer has in the table |
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 |
Rules to start from:
groups:
- name: bgp
rules:
# A peer is announcing what its policy does not allow: a leak or a
# hijack on its side, or a list on ours that is out of date.
- alert: BgpRoutesRefused
expr: sum by (peer, reason) (increase(bgp_routes_rejected_total[10m])) > 0
labels: { severity: warning }
# A route that has been through our own AS came back: a loop, or
# someone announcing our AS number.
- alert: BgpOwnAsInPath
expr: increase(bgp_routes_rejected_total{reason="own AS in path"}[10m]) > 0
labels: { severity: page }
# A peer is close to its limit; the limit may need raising before the
# session is cut.
- alert: BgpPeerNearPrefixLimit
expr: bgp_peer_routes / on (peer) bgp_peer_max_prefixes > 0.8
for: 10m
labels: { severity: warning }
- alert: BgpPrefixLimitExceeded
expr: increase(bgp_prefix_limit_exceeded_total[1h]) > 0
labels: { severity: page }
Trying it without a router
fake_peer is a minimal BGP speaker that connects to the daemon, announces a
prefix, prints every UPDATE it receives and, with --echo, re-announces each
received prefix back with its own ASN prepended (dropping NO_EXPORT routes).
The prefix it announces shows up in the received table. The ones it echoes
have been through our AS already, so they are refused as loops and counted
in the peer's refused routes:
cargo run -- testdata/server.textpb
cargo run -p bgp-integration-tests --example fake_peer -- \
--server 127.0.0.1:179 --asn 65001 --announce 203.0.113.0/24 --echo
Please refer to the other implementation crates in this repository where most of the actual implementation lives.
Warning: This is still very much a work in progress and is not yet ready for production use.