Add a documentation website, built with Zola
ci / check (push) Canceled after 0s
ci / docs (push) Canceled after 0s
ci / interop (push) Canceled after 0s
ci / fuzz (push) Canceled after 0s

The documentation moves from READMEs into a site in docs/: getting
started, a user guide, operations, a reference, and development pages,
written in the style of network vendors' documentation (docs/STYLE.md).
bin/bgp/README.md and docs/RFCs.md are folded into it.

- A theme of its own in the International Typographic Style, in blues,
  with self-hosted Inter and JetBrains Mono, search over Zola's index,
  dark mode, and no resources loaded from other hosts.
- Example configurations in docs/examples/, shown on the pages and
  checked with `bgp --check` in CI.
- A test that fails when a field of config.proto is missing from the
  configuration reference.
- A CI job that builds the site; Zola fails on broken links and anchors.
- A hardened systemd unit, tried under systemd with kernel routes on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Rayhaan JaufeerallyandClaude Opus 5.5 committed 2026-10-03 15:12:43 +02:00
1 parent 9820a5d3bc
commit dc275f99d7
61 files changed
+5632 -623

No files matched your search

+24 -4
View File
@@ -31,15 +31,35 @@ jobs:
env: env:
RUSTDOCFLAGS: -D warnings RUSTDOCFLAGS: -D warnings
run: cargo doc --locked --workspace --no-deps --document-private-items run: cargo doc --locked --workspace --no-deps --document-private-items
# The example operators start from, through the daemon itself. # The examples operators start from, here and in the documentation,
- name: Example configuration # through the daemon itself.
run: cargo run --locked -p bgp -- --check testdata/server.textpb - name: Example configurations
run: |
for config in testdata/server.textpb docs/examples/*.textpb; do
cargo run --locked -p bgp -- --check "$config"
done
# The tests that change a kernel's routes, in a network namespace of # The tests that change a kernel's routes, in a network namespace of
# their own; and two daemons that route for each other. # their own; and two daemons that route for each other.
- name: Kernel tests - name: Kernel tests
run: scripts/kernel-tests.sh run: scripts/kernel-tests.sh
# The daemon against FRR, BIRD and Routinator; see bin/bgp/README.md. # The documentation site. Zola fails the build on a link to a page or an
# anchor that is not there.
docs:
runs-on: ubuntu-latest
env:
ZOLA_VERSION: "0.22.1"
steps:
- uses: actions/checkout@v4
- name: Install Zola
run: |
curl -sSfL "https://github.com/getzola/zola/releases/download/v${ZOLA_VERSION}/zola-v${ZOLA_VERSION}-x86_64-unknown-linux-gnu.tar.gz" \
| sudo tar -xz -C /usr/local/bin zola
- name: Build
run: zola --root docs build
# The daemon against FRR, BIRD and Routinator; see
# docs/content/docs/development/testing.md.
interop: interop:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
+2
View File
@@ -1,3 +1,5 @@
/target /target
.DS_Store .DS_Store
.claude/ .claude/
/docs/public
/docs/static/giallo.css
+35 -11
View File
@@ -6,20 +6,16 @@ The aim of this project is to provide a modern, high performance, secure, progra
## Current status ## Current status
The daemon holds sessions with configured peers, takes their routes through an import policy, validates them against the RPKI (route origin validation and ASPA), picks the best route for each prefix and, on Linux, installs it in the kernel. It announces the routes it originates and, to the peers configured for it, the best of what it learned from the others, as far as RFC 9234's roles and the routes' communities allow. The daemon establishes BGP sessions with configured neighbors, evaluates received routes against import policy, validates them with the RPKI (route origin validation and ASPA), selects the best path for each prefix, and, on Linux, installs it in the kernel. It advertises the routes that it originates and, to neighbors configured for it, the best paths learned from other neighbors, as BGP roles (RFC 9234) and communities permit.
- [`bin/bgp/README.md`](bin/bgp/README.md): what the daemon does, how to configure and run it, and how it is tested against FRR, BIRD and Routinator. Ferrix has not been deployed in a production network.
- [`docs/RFCs.md`](docs/RFCs.md): which parts of which RFCs are implemented.
- [`crates/server/proto/config.proto`](crates/server/proto/config.proto): the configuration, field by field.
It has not yet run in a production network. ### Unsupported Features
### Not there yet - Route reflection, per-client best path selection for route servers, and ADD-PATH for advertised routes
- TCP-AO, TCP MD5, and GTSM
- Route reflection, per-client selection for a route server, ADD-PATH for learned routes - Graceful restart, BFD, and configuration changes without a restart
- TCP-AO / TCP MD5 and GTSM - Recursive resolution of next hops that are not directly connected
- Graceful restart, BFD, changing the configuration without a restart
- Next hops that are not directly connected
### Layout ### Layout
@@ -34,6 +30,33 @@ It has not yet run in a production network.
| `crates/web` | The status page, HTTP API and metrics | | `crates/web` | The status page, HTTP API and metrics |
| `bin/bgp` | The daemon | | `bin/bgp` | The daemon |
## Documentation
The documentation is a website built from [`docs/`](docs/) with
[Zola](https://www.getzola.org):
```
zola --root docs serve # http://127.0.0.1:1111
```
- [Overview](docs/content/docs/getting-started/introduction.md) and
[quick start](docs/content/docs/getting-started/quick-start.md)
- User guide: [BGP neighbors](docs/content/docs/guides/peers.md),
[import policy](docs/content/docs/guides/import-policy.md),
[BGP roles](docs/content/docs/guides/roles.md),
[RPKI](docs/content/docs/guides/rpki.md),
[kernel route installation](docs/content/docs/guides/kernel.md),
[route advertisement](docs/content/docs/guides/passing-routes-on.md),
[configuration examples](docs/examples/)
- [Deployment](docs/content/docs/operations/deployment.md) and
[monitoring](docs/content/docs/operations/monitoring.md)
- Reference: [configuration](docs/content/docs/reference/configuration.md),
[HTTP API](docs/content/docs/reference/http-api.md),
[metrics](docs/content/docs/reference/metrics.md),
[supported standards](docs/content/docs/reference/rfcs.md)
- [Architecture](docs/content/docs/development/architecture.md) and
[testing](docs/content/docs/development/testing.md)
## Development ## Development
CI runs these on every push, and fails on any warning: CI runs these on every push, and fails on any warning:
@@ -43,6 +66,7 @@ cargo fmt --all -- --check
cargo clippy --workspace --all-targets -- -D warnings cargo clippy --workspace --all-targets -- -D warnings
cargo test --workspace cargo test --workspace
RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps --document-private-items RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps --document-private-items
zola --root docs build
``` ```
It also runs the tests that need a real Linux kernel It also runs the tests that need a real Linux kernel
+11 -554
View File
@@ -1,560 +1,17 @@
# BGP # bgp
This crate provides the `bgp` binary, which runs the Border Gateway Protocol The daemon: the BGP speaker, the RTR client, the kernel route installer and
daemon together with its read-only HTTP status interface. the status page, in one binary.
``` ```
cargo run -- path/to/config.textpb cargo run -p bgp -- --check docs/examples/quick-start.textpb
cargo run -p bgp -- docs/examples/quick-start.textpb
``` ```
`--check` reads the configuration, says whether it is in order, and stops How to configure, run and operate it is in the documentation, which is
without listening on anything or touching a route: for trying a change built from `docs/` (`zola --root docs serve`):
before the daemon is restarted with it.
The configuration format is defined in `crates/server/proto/config.proto`; - [Quick start](../../docs/content/docs/getting-started/quick-start.md)
`testdata/server.textpb` is a worked example. The daemon is configured - [Configuration reference](../../docs/content/docs/reference/configuration.md)
entirely through that file: peers, and the routes it originates - [Deployment](../../docs/content/docs/operations/deployment.md) and
(`originate { prefix: ... }`). There is no way to change either at runtime. [monitoring](../../docs/content/docs/operations/monitoring.md)
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/kernel` | The routes in the kernel: how many, and why the others are not; the machine's interfaces |
| `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, 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. Peers whose `export` says so are told the best of
what the others announced, as the roles of RFC 9234 allow.
## 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, paths that do not begin with the peer's own AS, and paths with an
AS_SET in them (RFC 9774).
- The next hop has to be the peer itself (`accept_third_party_next_hop`
turns that off; a peer whose role is `ROUTE_SERVER` is exempt). A next
hop is where our traffic will be sent: a peer may draw it to itself, not
point it at another member of an exchange or at a machine on some other
network of ours.
- The prefixes this router originates, and anything inside them, are not
taken from another network, unless the peer's `prefixes` list names them
(a customer with part of our space).
- Always refused: a path with our own AS number in it, and a next hop that
is no address to send to (unspecified, loopback, multicast, our own).
- 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 configured peer is one router: a second connection from its address
under another BGP Identifier is refused, so the limits above are not
multiplied by opening more sessions. And a peer whose sessions keep coming
up and failing within a minute is kept down, from the third time for a
second and for twice as long each time after, up to five minutes: every
such session costs a look through the whole table.
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.
## Passing routes on
A peer whose `export` has `learned_routes: true` is told the best route of
every prefix the others announced, not only what we originate. The rules
are the usual ones and cannot be turned off: not back to the peer a route
came from, not to a peer whose AS is on its path already, not from one
peer in our own AS to another, not past NO_EXPORT or NO_ADVERTISE. A
route goes out with our AS added to its path and ourselves as next hop,
without the MED it came with; to a peer in our own AS the path is as it
was and LOCAL_PREF goes along; to a `ROUTE_SERVER_CLIENT` path, MED and
(where the client can use it) next hop are left as they came, as a route
server sends them (RFC 7947).
**What goes where is decided by roles (RFC 9234).** A route learned from a
provider, a peer or a route server is marked "only to customer" on the way
in and goes on to customers only; a customer's route goes to everyone. So
with roles on every peer, `learned_routes` makes us transit for our
customers and nothing else. A peer without a role has no such rule, and is
told every route; the daemon warns about that at startup. The top-level
`export` is for every peer that has none of its own:
```
export { learned_routes: true }
peers { name: "customer-a" role: CUSTOMER ... }
peers { name: "transit-a" role: PROVIDER ... }
```
**Marks.** `import` puts communities on every route taken from a peer, and
takes some off first; `export` does the same for every route announced to
one. Where a route was learned, which router it went through:
```
peers {
name: "ixp-a-rs"
import {
remove_communities: "64601:*" # nobody else's word for our marks
add_communities: "64601:1001" # learned at IXP A
add_large_communities: "64601:1:1001"
local_pref: 200
}
}
export { add_large_communities: "64601:2:1" } # went through router 1
```
Patterns for `remove_communities` are `64601:100`, `64601:*`, `*:*`, or
with three parts for large communities. `import.local_pref` is what route
selection goes by: of two routes for a prefix the one with the higher is
chosen, whatever else they have.
**Control communities.** With `control_communities: true`, a route's
communities say which peers it is passed on to, in the form Internet
exchanges' route servers use (`L` our AS, `P` a peer's):
| Standard | Large | |
|---|---|---|
| `0:P` | `L:0:P` | not to the peer with AS `P` |
| `L:P` | `L:1:P` | to the peer with AS `P`, and then only to peers named so |
| `0:L` | `L:0:0` | to nobody |
"Not" wins over "to"; a route with none of them goes to everyone it
otherwise would. Standard communities only have room for AS numbers below
65536; the large forms work for any. They are taken off a route as it is
passed on, and they hold for routes we originate too. With this on,
standard communities `L:n` mean "to the peer with AS n", so mark routes
with large communities (`L:2:n` and up) instead.
**Cost.** A decision is a few comparisons on the route's communities and
path. Routes that go out alike go in one UPDATE: changes are gathered for
50 ms before they go, and a peer that is given the whole table (when it
comes up, or asks for it again) has it gathered first, so that the
prefixes that share attributes, which are spread over the table's shards,
go together. At most four sessions gather a table at once; the others wait
their turn. `cargo bench -p bgp-bench --bench transit`, with one peer
sending 1.23 million routes over a session and another being told of them,
on a laptop: in our table after 0.47 s, all of it heard by the other peer
after 2.1 s, in UPDATEs of four routes each on average.
**Not there yet.** Per-client best path selection for a route server (every
client is told the one best route); ADD-PATH for learned routes (only the
best is sent); route reflection; a way to send pre-built messages to
several peers at once.
## 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.
## Roles and route leaks (RFC 9234)
`role` on a peer says what the peer is to us: `PROVIDER`, `CUSTOMER`, `PEER`,
`ROUTE_SERVER` or `ROUTE_SERVER_CLIENT`. It is not for peers in our own AS.
With it:
- **The two ends have to agree.** Our OPEN says what we are to the peer (its
provider, if it is our customer). A peer whose OPEN says it is something
else than configured is refused with OPEN Message Error / Role Mismatch:
one side thinking "customer" and the other "peer" is how leaks happen. A
peer that says nothing (it does not implement the RFC) is accepted, unless
`require_role: true`. The status page shows the role, with a tick when the
peer confirmed it.
- **Routes are marked "only to customer"** (the OTC attribute) when they come
in from a provider, a peer or a route server, and when ours go out to a
customer, a peer or a route server client. The mark stays on the route.
- **A marked route coming the wrong way is a leak and is refused**: from a
customer or a route server client always, and from a peer unless the peer
put the mark there itself. The reason shown is `route leak (Only to
Customer)`. No setting turns this off, as the RFC requires.
This catches leaks even where nobody on the path has an ASPA, as long as the
networks on either side of the leaker mark their routes.
## 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;
- `role` on 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
}
```
## IPv4 over a link that has only IPv6 addresses (RFC 8950)
A session over IPv6 carries IPv4 routes too, with IPv6 next hops, if the
peer agrees: the daemon offers the Extended Next Hop capability on every
session over IPv6 whose address families include IPv4 unicast, and nothing
has to be configured for it on this side. (On FRR it is `neighbor ...
capability extended-nexthop`, on BIRD `extended next hop on` in the IPv4
channel.) The peers table on the status page says "IPv4 with IPv6 next
hops" for a session that agreed on it.
Our IPv4 routes then go out with the session's own IPv6 address as next
hop, and the peer's arrive with its IPv6 address, or with a global and a
link-local one. In the kernel such a route is an IPv4 route by way of an
IPv6 neighbor (`10.2.0.0/24 via inet6 2001:db8::2 dev eth0`, Linux 5.2 and
later).
## Peers at link-local addresses
A link with no addresses but the link-local ones every interface has
(`fe80::/10`) is enough for a session. Such an address means something
only on its link, so the peer is the address and the interface together:
```
peers {
name: "upstream"
endpoint { address: "fe80::1%eno3" }
remote_asn: 64602
}
```
- The interface is looked up by its name each time it is needed: one that
is not there yet, or is made anew with another number (a tunnel that is
restarted), is found when it is. Until then the peer's state says there
is no such interface.
- The same address on two interfaces is two peers. A link-local address
without an interface is refused, as is an interface on any other
address.
- To take the peer's connections the daemon must listen on `::` (as it
does by default), not on one global address.
- Both address families go over the session (IPv4 as above, if the peer
agrees). Our routes go with our link-local address as next hop, in both
places MP_REACH_NLRI has for one, as FRR sends its own. The peer's next
hop is taken in any of the forms in use: the link-local address alone,
twice, or after `::`. In the kernel the routes go by way of that address
on the session's interface (`10.2.0.0/24 via inet6 fe80::1 dev eno3`).
- `local_address`, if set to a link-local address of ours, is taken to be
on the peer's interface.
With FRR, configure the peer by interface (`neighbor eno3 interface
remote-as ...`, "BGP unnumbered"). FRR then learns who is on the link from
IPv6 router advertisements, which this daemon does not send: run `radvd`
(or the like) on the interface beside it. FRR's older `neighbor fe80::1
interface eno3` brings the session up but FRR 10.5 then takes every route
from us for unreachable. BIRD needs only `neighbor fe80::1%eno3 as ...`.
## Forwarding: routes in the kernel (Linux)
With `kernel {}` in the configuration the daemon puts the best route of every
prefix in the kernel's routing table, over netlink, and keeps it there as the
best routes change. Without it the daemon only speaks BGP and the machine's
forwarding is not touched.
```
kernel {
table: 254 # the main table; the default
metric: 20 # the default
}
```
- **What is installed.** A best route whose next hop is a neighbor: an
address in the network of one of the machine's interfaces that is up. For
an IPv6 route with a link-local next hop, the interface is the one the
peer is a neighbor on. A route whose next hop is further away (a route
from a peer in our own AS that did not set itself as next hop, say)
cannot be installed: looking up the way to such a next hop through other
routes is not done yet.
- **Routes that cannot be installed are not chosen.** With `kernel {}`, a
route whose next hop cannot be reached is not taken into the table, so
that it cannot be the best route of its prefix while another peer has
one that works (RFC 4271 §9.1.2.1). It is held aside, counted as refused
for "next hop unreachable" and shown as held on the status page, and
taken as soon as the machine's interfaces change so that it can be
reached; a route in the table whose next hop goes away is held in the
same way. Without `kernel {}` the daemon does not know what can be
reached, and takes every route.
- **Nexthop objects.** Where the kernel has them (Linux 5.3 and later) a
gateway is put there once, as a nexthop object, and routes name it; `ip
route` shows such routes with `nhid`, and `ip nexthop` the objects. The
kernel then checks the gateway once and not for every route, which makes
a full table about a third faster to install, and lets all of them be
removed at once when the daemon stops. The objects the daemon makes are
its own: it does not use anyone else's, puts its own back if one is
removed, and removes them when it stops. `without_nexthop_objects: true`
has every route carry its own gateway instead, as on older kernels.
- **What is the machine's own.** A route for a network the machine is
itself on, or for part of one, is never installed, whatever the policy
let into the table: the kernel reaches those directly. A peer on a shared
LAN that announces part of the LAN would otherwise take the traffic to
its neighbors, and our sessions with them. The status page counts such
routes as "inside a network of this machine".
- **What is ours.** Routes are marked `proto bgp`. Every route with that
mark in the configured table with the configured metric is taken to be
this daemon's, and no other route is ever changed or removed. So do not
run two daemons that install BGP routes into the same table with the same
metric.
- **Starting and stopping.** At startup, routes of ours left by an earlier
run are kept, so that the machine goes on forwarding while the peers send
their tables again; those that no peer has announced again after
`stale_seconds` (120 unless set) are removed. On SIGINT or
SIGTERM the daemon closes its sessions and then removes its routes. After
a crash they stay until the next start.
- **Interfaces.** The daemon learns the machine's interfaces and addresses
from the kernel and is told when they change; a second after the last
change, and five seconds after the first at the latest, it works all
routes out again, since any next hop may resolve differently.
- **When the kernel does not answer.** The kernel says which requests it
refused. If it had more to say than there was room for, or says nothing
for ten seconds, the daemon asks it what it has of our routes and puts
right what is not as it should be.
- **Privileges.** Changing routes needs the `CAP_NET_ADMIN` capability, and
listening on port 179 needs `CAP_NET_BIND_SERVICE`; neither needs root.
Without the first the daemon stops as soon as the kernel refuses it.
```
setcap cap_net_admin,cap_net_bind_service+ep /usr/local/bin/bgp
```
`ip route show proto bgp` shows what is there. `/api/kernel` and the status
page show how many routes are installed, how many are not and why, the last
refusal by the kernel, and the interfaces; `/metricz` has `bgp_kernel_routes`,
`bgp_kernel_routes_unresolved`, `bgp_kernel_routes_refused`,
`bgp_kernel_route_changes_total{change}` and `bgp_kernel_errors_total`.
To forward packets for others the machine also needs
`net.ipv4.ip_forward=1` and `net.ipv6.conf.all.forwarding=1`; the daemon does
not set them.
### How long it takes
A million IPv4 routes, about the size of the Internet's table, go into the
kernel in a little over a second and come out in half a second (release
build, in a virtual machine on an Apple M-series laptop; the test
`many_routes_are_installed_and_removed` with `FERRIX_ROUTES=1000000`).
About three quarters of that is the kernel's own work on each route.
What keeps the rest small: requests go to the kernel hundreds to a write,
with one acknowledgement for each write and not for each route; a task of
its own does the writing, so the next batch is worked out while the kernel
is busy with the last; and what a next hop resolves to is worked out once
for each next hop, not once for each route.
### Testing against a real kernel
The tests that change a kernel's routes are not run by `cargo test`.
`scripts/kernel-tests.sh` runs them on Linux, each in a network namespace
made for it (`scripts/in-namespace.sh` runs any one command that way): the route installer against `ip route`, the daemon with a peer,
and two daemons that route for each other with pings across. From macOS,
`scripts/linux-vm.sh` runs it (or any command, such as `cargo test`) in a
Lima virtual machine that it creates on first use.
## Interoperability
`scripts/interop/lab.sh` runs the daemon against FRR, BIRD and Routinator,
each in a network namespace of its own on one exchange LAN, and checks what
every side ends up with. It needs Linux (`scripts/linux-vm.sh
scripts/interop/lab.sh` from macOS; `scripts/interop/install.sh` installs
the others on Ubuntu). Last run against FRR 10.5.1, BIRD 2.18 and
Routinator 0.15.2. What it covers:
| Phase | What is checked |
|-------|-----------------|
| `basics` | Sessions over IPv4 and IPv6; routes both ways with path, next hop, communities and large communities intact; four-octet AS numbers; ADD-PATH from BIRD; pings across by the installed routes |
| `roles` | RFC 9234 roles agree with FRR (peer) and BIRD (provider); Only to Customer is set where it should be, in both directions |
| `rpki` | RTR version 2 with Routinator; an invalid route from FRR is refused and held, and taken after Routinator restarts with data that allows it |
| `refresh` | Routes added and withdrawn by FRR; FRR's ROUTE-REFRESH answered; BIRD re-sending |
| `flaps` | Five session resets; FRR and BIRD killed and restarted; a cut wire noticed by the hold timer |
| `restart` | The daemon stopped in good order (kernel emptied, peers told) and killed (routes kept, stale one cleared by the next start) |
| `scale` | A table from BIRD (`SCALE_ROUTES`, a million for the Internet's size) into the table and the kernel; the daemon killed and restarted under it; the table withdrawn |
| `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 |
| `transit` | FRR as our customer and BIRD as our provider: FRR's routes reach BIRD with our AS on the path, BIRD's reach FRR marked only to customer, one of FRR's is kept from BIRD by a community, and packets go between their networks through us |
| `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 |
`scripts/interop/real-rpki.sh` checks the RTR client against a Routinator
that has the real RPKI, ASPAs included: the daemon has to end up with as
many VRPs and ASPAs as the validator says it has.
Not covered: routers other than FRR and BIRD; IPv6 routes over an IPv4
session (the daemon does not send them); an interface that is removed and
made anew under a session; long runs.
## 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 |
| `bgp_routes_exported_total{change}` | Learned routes announced to, and withdrawn from, other peers |
| `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 |
Rules to start from:
```yaml
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 }
# A configured peer has no session.
- alert: BgpPeerDown
expr: bgp_peer_up == 0
for: 5m
labels: { severity: page }
# Someone is trying to have our own prefixes, or our traffic, go
# where they should not.
- alert: BgpHijackAttempt
expr: increase(bgp_routes_rejected_total{reason=~"prefix is our own|next hop is not the peer"}[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
labels: { severity: warning }
```
## 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
```
+101
View File
@@ -0,0 +1,101 @@
//! The configuration reference on the documentation site describes every
//! field of the configuration's schema.
//!
//! The schema is `proto/config.proto`; the reference is
//! `docs/content/docs/reference/configuration.md`, with a section for each
//! message and enum (`## Peer`) that names each of its fields or values in
//! backquotes. A field added to the schema and not to the reference fails
//! here, so that operators can find every setting the daemon has.
use std::collections::BTreeMap;
use std::path::Path;
/// The messages and enums of a `.proto` file, each with its fields or
/// values, in the order they are declared.
fn declarations(proto: &str) -> BTreeMap<String, Vec<String>> {
let mut found = BTreeMap::new();
let mut current: Option<(String, Vec<String>)> = None;
for line in proto.lines() {
let line = line.trim();
if line.starts_with("//") || line.is_empty() {
continue;
}
let words: Vec<&str> = line.split_whitespace().collect();
match words.as_slice() {
["message" | "enum", name, "{"] => current = Some((name.to_string(), Vec::new())),
["}"] => {
if let Some((name, members)) = current.take() {
found.insert(name, members);
}
}
// `repeated Peer peers = 4;`, `string name = 1;`, `PEER = 3;`
_ => {
let Some((_, members)) = current.as_mut() else {
continue;
};
let Some(at) = words.iter().position(|word| *word == "=") else {
continue;
};
let name = words[at - 1];
// An enum's zero value is what is left out; there is
// nothing to configure.
if !name.ends_with("_UNSPECIFIED") {
members.push(name.to_string());
}
}
}
}
found
}
/// The text of each `## ` section of a Markdown page, by its heading.
fn sections(page: &str) -> BTreeMap<String, String> {
let mut found = BTreeMap::new();
let mut current: Option<(String, String)> = None;
for line in page.lines() {
if let Some(heading) = line.strip_prefix("## ") {
if let Some((name, text)) = current.take() {
found.insert(name, text);
}
current = Some((heading.trim().to_string(), String::new()));
} else if let Some((_, text)) = current.as_mut() {
text.push_str(line);
text.push('\n');
}
}
if let Some((name, text)) = current {
found.insert(name, text);
}
found
}
#[test]
fn the_configuration_reference_has_every_field() {
let crate_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
let proto = std::fs::read_to_string(crate_dir.join("proto/config.proto")).unwrap();
let reference_path = crate_dir.join("../../docs/content/docs/reference/configuration.md");
let reference = std::fs::read_to_string(&reference_path).unwrap();
let declared = declarations(&proto);
assert!(declared.contains_key("BgpConfig") && declared.contains_key("Peer"));
let documented = sections(&reference);
let mut missing = Vec::new();
for (name, members) in &declared {
let Some(section) = documented.get(name) else {
missing.push(format!("a section `## {name}`"));
continue;
};
for member in members {
if !section.contains(&format!("`{member}`")) {
missing.push(format!("`{member}` under `## {name}`"));
}
}
}
assert!(
missing.is_empty(),
"{} does not describe:\n {}",
reference_path.display(),
missing.join("\n ")
);
}
+127
View File
@@ -0,0 +1,127 @@
# Documentation style
The documentation follows the conventions of network equipment vendors'
technical documentation: Juniper TechLibrary, Cisco configuration guides,
and Nokia SR OS guides. Operators read these every day; Ferrix's
documentation should read like them. This file is the checklist.
## Topic types
Every page is one of three kinds, and its title says which.
| Type | Answers | Title pattern | Examples |
|---|---|---|---|
| Concept | What is it, how does it behave | `<Feature> Overview`, `Understanding <Feature>` | BGP Import Policy Overview |
| Task | How do I configure or operate it | `Configuring <Feature>`, `Example: Configuring <Feature>`, `Verifying <Feature>` | Configuring Prefix Limits |
| Reference | What exactly does this element do | The element's name, or `<Thing> Reference` | `max_prefixes`, Metrics Reference |
A feature page in the user guide combines them in this order, with these
section titles where they apply:
1. `Understanding <Feature>` or `<Feature> Overview`: what it is and how it behaves.
2. `Configuring <Feature>`: the task, with an example.
3. `Verifying <Feature>`: how to see that it works.
4. `Limitations` (or `Restrictions`): what is not supported.
## Titles and headings
- Title Case for page titles and section headings: "Configuring Route
Origin Validation", "Default Import Checks".
- Headings are noun phrases or gerund phrases naming a feature or a task.
Never a sentence, a question, a metaphor, or a story ("A route's way in",
"Who may connect", "What it costs").
- Do not number headings in the text; the site numbers them.
## Sentences
- Present tense, active voice, declarative: "Ferrix rejects routes whose
AS path contains the local AS number."
- The subject is the product, a component, or a protocol element: "Ferrix",
"the daemon", "the BGP session", "the import policy", "the router". Use
"you" for the operator's actions: "You can configure…", "To limit the
number of prefixes, configure `max_prefixes`."
- Never "we", "us" or "our". Write "the local AS", "the local router",
"routes that Ferrix originates".
- One idea per sentence; 12 to 25 words is typical.
- Open a concept with a definition: "Route origin validation (ROV) is a
mechanism that…".
- Spell out an acronym at its first use on a page: "Resource Public Key
Infrastructure (RPKI)". Common ones (BGP, AS, IP, TCP) need not be.
- State defaults explicitly: "By default, …" in prose; "Default:" in
reference entries.
- Ranges and units: "1 through 65535", "600 seconds", "4,294,967,295".
- Introduce lists and tables with a sentence ending in a colon: "Ferrix
rejects the following routes:", "Table 1 lists the default checks."
- Give reasons only where they affect an operational decision, and plainly:
"This prevents a neighbor from directing traffic to a third party."
- No figurative language, idioms, rhetorical questions, aphorisms, jokes,
marketing adjectives ("blazing", "seamless"), or colloquial verbs
("cut off", "draw traffic", "hand over").
## Terminology
Use the standard BGP and vendor term, not a paraphrase.
| Use | Not |
|---|---|
| accept, reject | take, refuse, judge |
| advertise, withdraw | announce to, tell, pass on (as a verb for advertisement) |
| receive | hear, be told |
| neighbor, peer | the other side |
| best path, active route | the chosen route, the one best route |
| session is established, torn down | comes up, goes down, is cut |
| install (in the kernel/FIB) | put in |
| routing table (Loc-RIB), Adj-RIB-In | the table |
| local preference (`LOCAL_PREF`) | how much it is preferred |
| next hop is resolved / unresolvable | can / cannot be reached |
| hold-down | kept down |
| configuration statement, field | setting |
| originate | announce our own |
Ferrix-specific terms are defined once, where they are introduced, and
then used consistently:
- **Held route**: a received route that was rejected for a reason that can
change without a new UPDATE (RPKI invalid, ASPA invalid, unresolvable next
hop). Ferrix re-evaluates held routes when that condition changes.
- **Learned route**: a route received from a neighbor, as opposed to an
originated route.
- **Control community**: a community that restricts which neighbors a
route is advertised to.
## Notes and admonitions
Use them for information that is not part of the main flow, and use the
right kind:
| Marker | For |
|---|---|
| `> [!NOTE]` | Additional information the reader may need. |
| `> [!TIP]` | A recommended practice or a shortcut. |
| `> [!IMPORTANT]` | Information that affects correct operation. |
| `> [!WARNING]` | An action or setting that can disrupt service. |
| `> [!CAUTION]` | An action or setting with security consequences. |
## Procedures
- Number the steps. Each step is one action, in the imperative: "Configure
the neighbor's address and AS number."
- Follow a step with the configuration or command it needs, then the result
where it matters: "The daemon reports that the configuration is valid."
- End a task with how to verify it.
## Configuration and command elements
- Field names, values, commands, file names and metric names in code
font: `max_prefixes`, `PROVIDER`, `bgp --check`, `/metricz`.
- In the configuration reference, describe a field with a verb in the third
person: "Specifies…", "Enables…", "Limits…". Give its default and range.
- Show complete, valid configuration fragments. Whole example
configurations live in `docs/examples/` and are validated by CI.
## Examples and placeholders
- Use documentation addresses and AS numbers: 192.0.2.0/24,
198.51.100.0/24, 203.0.113.0/24, 2001:db8::/32; AS 64496 through 64511
and 65536 through 65551.
- Name sample routers and peers plainly: `transit-a`, `customer-65536`.
+48
View File
@@ -0,0 +1,48 @@
# The documentation site for ferrix, built with Zola (https://www.getzola.org).
#
# zola --root docs serve # http://127.0.0.1:1111, rebuilt on every change
# zola --root docs build # into docs/public
#
# base_url is where the site is published. `zola build --base-url ...`
# overrides it for a build elsewhere.
base_url = "https://ferrix.rayhaan.net"
title = "Ferrix BGP"
description = "Ferrix is a BGP daemon for Linux routers. It filters received routes, validates them with the RPKI, prevents route leaks with BGP roles, and installs the best paths in the kernel."
default_language = "en"
compile_sass = true
minify_html = false
generate_feeds = false
build_search_index = true
[markdown]
smart_punctuation = false
github_alerts = true
insert_anchor_links = "heading"
# Code is coloured by the site's own stylesheet: the highlighter only marks
# what each token is (style "class"). It also writes a giallo.css of the
# theme named here into static/, which the site does not use.
[markdown.highlighting]
theme = "github-light"
style = "class"
# The configuration's own format, which the highlighter does not know.
extra_grammars = ["syntaxes/textproto.json"]
[search]
index_format = "elasticlunr_javascript"
include_title = true
include_description = true
include_content = true
truncate_content_length = 4000
[extra]
repository = "https://git.rayhaan.net/rayhaan/ferrix"
version = "0.1.0"
# The top bar.
nav = [
{ name = "Docs", path = "docs/getting-started/introduction/" },
{ name = "Configuration", path = "docs/reference/configuration/" },
{ name = "RFCs", path = "docs/reference/rfcs/" },
]
+3
View File
@@ -0,0 +1,3 @@
+++
title = "Ferrix BGP"
+++
+5
View File
@@ -0,0 +1,5 @@
+++
title = "Documentation"
description = "Installation, configuration, operation, and development documentation for the Ferrix BGP daemon."
sort_by = "weight"
+++
+9
View File
@@ -0,0 +1,9 @@
+++
title = "Development"
description = "Architecture, testing, and contribution guidelines."
weight = 5
sort_by = "weight"
[extra]
number = "05"
+++
@@ -0,0 +1,193 @@
+++
title = "Architecture Overview"
description = "Describes the internal components of Ferrix and how a route is processed from reception to kernel installation and advertisement."
weight = 1
[extra]
nav = "Architecture"
+++
This topic describes the internal components of Ferrix and how they
process a route. It is intended for developers who work on the source
code. Figure 1 shows the components and the path of a route through them.
<figure class="diagram">
<svg viewBox="0 0 820 340" width="820" role="img" aria-labelledby="arch-title">
<title id="arch-title">Route processing in Ferrix</title>
<defs><marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0 0 L10 5 L0 10 z" class="head"/></marker></defs>
<path class="edge" d="M140 182 H176" marker-end="url(#arrow)"/>
<path class="edge" d="M310 182 H346" marker-end="url(#arrow)"/>
<path class="edge" d="M480 182 H516" marker-end="url(#arrow)"/>
<path class="edge" d="M640 160 C660 160 655 88 676 88" marker-end="url(#arrow)"/>
<path class="edge" d="M640 182 H676" marker-end="url(#arrow)"/>
<path class="edge" d="M640 204 C660 204 655 268 676 268" marker-end="url(#arrow)"/>
<path class="edge" d="M415 270 V218" marker-end="url(#arrow)"/>
<path class="edge edge-back" d="M740 60 V28 H245 V146" marker-end="url(#arrow)"/>
<text class="s" x="492" y="22" text-anchor="middle">UPDATE messages to neighbors, through the session output queue</text>
<rect class="box" x="20" y="150" width="120" height="64"/>
<text class="t" x="80" y="178" text-anchor="middle">Neighbors</text>
<text class="s" x="80" y="197" text-anchor="middle">TCP, port 179</text>
<rect class="box" x="180" y="150" width="130" height="64"/>
<text class="t" x="245" y="178" text-anchor="middle">Sessions</text>
<text class="s" x="245" y="197" text-anchor="middle">one task each</text>
<rect class="box" x="350" y="150" width="130" height="64"/>
<text class="t" x="415" y="178" text-anchor="middle">Import</text>
<text class="s" x="415" y="197" text-anchor="middle">policy · RPKI</text>
<rect class="box box-accent" x="520" y="110" width="120" height="144"/>
<text class="t on-accent" x="580" y="150" text-anchor="middle">Routing</text>
<text class="t on-accent" x="580" y="168" text-anchor="middle">table</text>
<text class="s on-accent" x="580" y="196" text-anchor="middle">shards</text>
<text class="s on-accent" x="580" y="214" text-anchor="middle">best paths</text>
<text class="s on-accent" x="580" y="232" text-anchor="middle">journal</text>
<rect class="box" x="680" y="60" width="124" height="56"/>
<text class="t" x="742" y="84" text-anchor="middle">Exporters</text>
<text class="s" x="742" y="102" text-anchor="middle">one per session</text>
<rect class="box" x="680" y="154" width="124" height="56"/>
<text class="t" x="742" y="178" text-anchor="middle">Kernel</text>
<text class="s" x="742" y="196" text-anchor="middle">netlink</text>
<rect class="box" x="680" y="240" width="124" height="56"/>
<text class="t" x="742" y="264" text-anchor="middle">Status, API</text>
<text class="s" x="742" y="282" text-anchor="middle">HTTP, metrics</text>
<rect class="box" x="350" y="270" width="130" height="56"/>
<text class="t" x="415" y="294" text-anchor="middle">RTR client</text>
<text class="s" x="415" y="312" text-anchor="middle">RPKI caches</text>
</svg>
<figcaption>Figure 1: Route processing in Ferrix</figcaption>
</figure>
## Design Principles
Ferrix is built on the following principles:
- **Memory safety.** The workspace lints forbid `unsafe` code in every
crate.
- **No central routing table task.** Each session task applies the routes
of an UPDATE message to the routing table directly, and locks only the
shards that contain the affected prefixes. Sessions that update different
shards do not wait for each other. A session that must wait is slowed
down, which provides back-pressure without an unbounded queue in front of
the routing table.
- **Independent consumers.** The components that act on best path changes
(the export task of each session, kernel route installation, and the
status page) read the changes from a journal at their own rate. A slow
consumer does not delay the routing table or other consumers.
- **Fault isolation.** A panic ends only the task in which it occurs. If a
session task panics, Ferrix closes the session, notifies the neighbor,
and removes the neighbor's routes. Shared tables use locks that are not
poisoned by a panic. The daemon exits only if it loses a component that
it cannot operate without.
- **Resistance to neighbor-triggered load.** Ferrix rate-limits log
messages that a neighbor can trigger, closes connections from
unconfigured addresses before reading from them, and places flapping
neighbors in hold-down.
## Inbound Route Processing
### Connection Management
The dialer (`server::dialer`) initiates a connection to each configured
neighbor and retries with exponential backoff. The listener accepts
connections from the addresses of configured neighbors only. The collision
module (`server::collision`) retains one connection for each neighbor when
both speakers initiate a connection (RFC 4271 Section 6.8).
### BGP Finite State Machine
The BGP finite state machine (`server::fsm`) implements RFC 4271 Section 8
without input or output. It receives events and returns the actions to
perform, so that each state transition can be tested with a function call.
The connection driver (`server::connection`) converts received bytes and
timer expirations into events, and performs the returned actions.
### Message Decoding
The `packet` crate decodes and encodes BGP messages. Errors in UPDATE
messages are handled as specified in RFC 7606: depending on the error, the
attribute is discarded or the routes are treated as withdrawn. All parsers
are fuzz-tested.
### Import Processing
The `server::routes` module applies the routes of an UPDATE message to the
routing table, according to the neighbor's import policy:
- `server::policy` evaluates each route against the import policy.
- `server::rpki` validates the route against the data that the RTR client
(`rpki` crate) receives.
- `server::reach` determines whether the next hop is resolvable.
A route that is rejected for a reason that can change without a new UPDATE
(RPKI invalid, ASPA invalid, or an unresolvable next hop) is stored as a
held route. Ferrix reevaluates held routes when the RPKI data or the
interfaces change.
### Routing Table
The routing table (`rib::table`) combines the Adj-RIBs-In and the Loc-RIB
(RFC 4271 Section 3.2) in one structure, organized by prefix. The entry for
a prefix contains the routes from all neighbors and identifies the best
path. After a change, best path selection examines only the entry for the
affected prefix. The routing table is designed for hundreds of full
routing tables:
- A route occupies 16 bytes: a pointer to its path attribute set, and one
word that encodes the neighbor and the first steps of best path selection
as a single comparable value.
- Path attribute sets are stored once and shared by all routes with
identical attributes. Unused sets are removed periodically.
- The table is divided into shards by a hash of the prefix. Each shard has
its own lock.
## Outbound Route Processing
### Change Journal
Each shard of the routing table maintains a journal of the prefixes whose
best path changed. Each consumer reads the journal through its own reader.
The journal records that the best path of a prefix changed, not the new
best path; the reader retrieves the current best path when it processes the
entry. A reader that falls too far behind compares its state with the
routing table instead. Changes made together carry the same table-wide
sequence stamp, so that a reader processes them together and they are
advertised in the same UPDATE messages.
### Route Advertisement
The `server::advertise` module runs an export task for each session. The
task advertises the originated routes and, if the export policy enables
`learned_routes`, the best paths learned from other neighbors, as the
`server::export` module permits for each route. The task records the routes
that it has advertised, groups routes with identical attributes into one
UPDATE message, and waits when the session's output queue is full.
### Kernel Route Installation
The `fib` crate reads best path changes and maintains the kernel routing
table over rtnetlink, with one route for each prefix through the next hop
of its best path. It also receives the router's interfaces and addresses
from the kernel, which determine whether a next hop is resolvable.
### HTTP Interface
The `web` crate provides read-only access to the state owned by the server.
It does not store routing state.
## Crates
Table 1 lists the crates of the workspace.
| Crate | Contents |
|---|---|
| `crates/primitives` | Prefixes, address families, and parsing utilities |
| `crates/packet` | BGP message encoding and decoding |
| `crates/rib` | The routing table, best path selection, and the change journal |
| `crates/rpki` | The RTR client, route origin validation, and ASPA verification |
| `crates/fib` | Kernel route installation over netlink |
| `crates/server` | Sessions, the finite state machine, policy, export, and configuration |
| `crates/web` | The status page, HTTP API, and metrics |
| `crates/integration-tests` | Tests that run the server against in-process BGP peers |
| `crates/bench` | Benchmarks, with a synthetic Internet routing table |
| `bin/bgp` | The daemon |
The API documentation of each crate (`cargo doc --open`) describes its
modules. Module-level comments document the reasons for each design.
@@ -0,0 +1,73 @@
+++
title = "Contributing"
description = "Describes the checks that continuous integration performs, the coding conventions, and how to change the documentation."
weight = 3
+++
## Continuous Integration Checks
Continuous integration runs the following commands on every push and pull
request, and fails on any warning:
```bash
cargo fmt --all -- --check
cargo clippy --locked --workspace --all-targets -- -D warnings
cargo test --locked --workspace
RUSTDOCFLAGS="-D warnings" cargo doc --locked --workspace --no-deps --document-private-items
for f in testdata/server.textpb docs/examples/*.textpb; do cargo run --locked -p bgp -- --check "$f"; done
zola --root docs build
```
It also runs the kernel tests, the interoperability lab, and each fuzz
target for 1 minute. See [Testing](@/docs/development/testing.md). In local
builds, warnings are not treated as errors.
## Coding Conventions
- **Memory safety.** `unsafe` code is forbidden. The lints are configured in
`[workspace.lints]` in the root `Cargo.toml`, and every crate inherits
them.
- **Comments.** Each module begins with a comment that describes its
purpose and the reasons for its design. Comments on code explain why the
code is written as it is, not what it does.
- **External input.** All input from neighbors, RPKI caches, and the kernel
is parsed without panicking, and each parser has a fuzz target. Events
that a neighbor can trigger repeatedly are counted in a metric and
rate-limited in the log.
- **Tests.** Tests are located next to the code that they test. Each defect
fix includes a test with the input that exposed the defect.
- **Configuration validation.** The daemon validates the configuration at
startup and reports each error with its location.
## Adding a Configuration Statement
1. Add the field to `crates/server/proto/config.proto`, with a comment that
describes its effect and default value.
2. Validate the field in `crates/server/src/config.rs`.
3. Describe the field in `docs/content/docs/reference/configuration.md`.
The test `the_configuration_reference_has_every_field` fails until the
field is described.
## Changing the Documentation
The documentation is built with [Zola](https://www.getzola.org) from the
`docs/` directory:
```bash
zola --root docs serve # http://127.0.0.1:1111, rebuilt on every change
zola --root docs build # output in docs/public
```
The documentation follows the conventions in `docs/STYLE.md`, which are
based on the technical documentation of network equipment vendors. In
addition:
- Each page is a Markdown file in `docs/content/docs/`. Each subdirectory is
a section in the navigation, and pages are ordered by `weight`.
- Link to other pages with `@/docs/...md` paths. Zola verifies these links
and their anchors at build time.
- Complete configurations are stored in `docs/examples/`, validated by
continuous integration with `bgp --check`, and included in pages with
`{{/* example(name="edge") */}}`.
- The theme is in `docs/templates` and `docs/sass`. The fonts are in
`docs/static/fonts`. The site does not load resources from other hosts.
+182
View File
@@ -0,0 +1,182 @@
+++
title = "Testing"
description = "Describes the Ferrix test suites: unit and integration tests, property tests, kernel tests, the interoperability lab, fuzz testing, and benchmarks."
weight = 2
+++
Ferrix is tested at several levels, from individual functions to networks
of routers. Continuous integration runs all levels on every push and fails
on any compiler warning. Table 1 summarizes the test suites.
| Suite | Scope | Command |
|---|---|---|
| Unit and integration tests | All crates, and the server against in-process peers | `cargo test --workspace` |
| Property tests | Encoding and decoding of generated messages | Included in `cargo test` |
| Kernel tests | Route installation in a Linux kernel, in network namespaces | `scripts/kernel-tests.sh` |
| Interoperability lab | The daemon with FRR, BIRD, and Routinator | `scripts/interop/lab.sh` |
| Fuzz tests | All parsers of external input | `cargo fuzz run <target>` |
| Benchmarks | Route processing performance | `cargo bench -p bgp-bench` |
## Unit and Integration Tests
To run the unit and integration tests:
```bash
cargo test --workspace
```
The `crates/integration-tests` crate runs the server in-process against
test peers that exchange BGP messages over sockets. It covers session
establishment, import policy, RPKI validation, BGP roles, route
advertisement, connection collisions, and prefix limits. The BGP finite
state machine has no input or output, so its state transitions are tested
with direct function calls.
## Property Tests
The property tests verify that generated messages are decoded to the same
value after encoding, and that no input causes a parser to panic. They run
as part of `cargo test` in approximately 1 second. To run more cases:
```bash
PROPTEST_CASES=1000000 cargo test --release -p bgp-packet --test properties
```
## Kernel Tests
The tests that modify a kernel routing table are not run by `cargo test`.
The script `scripts/kernel-tests.sh` runs them on Linux, each in its own
network namespace. The script `scripts/in-namespace.sh` runs a single
command in the same way. The kernel tests include:
- Route installation and removal, verified with `ip route`.
- The daemon with a BGP peer.
- Two daemons that exchange routes, verified with `ping`
(`scripts/two-routers.sh`).
On macOS, the script `scripts/linux-vm.sh` runs the kernel tests, or any
other command, in a [Lima](https://lima-vm.io) virtual machine. The script
creates the virtual machine on first use.
```bash
scripts/linux-vm.sh # kernel tests
scripts/linux-vm.sh cargo test # all tests, on Linux
scripts/linux-vm.sh bash # interactive shell
```
## Interoperability Lab
The script `scripts/interop/lab.sh` tests the daemon with FRR, BIRD, and
Routinator. Each router runs in its own network namespace, connected to a
shared exchange LAN. Routinator serves RPKI data defined for the lab, and
does not retrieve data from the Internet.
The lab requires Linux, `sudo`, and a release build of the daemon. The
script `scripts/interop/install.sh` installs FRR, BIRD, radvd, and
Routinator on Ubuntu.
```bash
cargo build --release -p bgp
scripts/interop/lab.sh # all phases
PHASES="basics transit" scripts/interop/lab.sh # selected phases
scripts/interop/lab.sh up # set up the lab and leave it running
```
On macOS, prefix each command with `scripts/linux-vm.sh`. The lab was last
run with FRR 10.5.1, BIRD 2.18, and Routinator 0.15.2. Table 2 lists the
test phases.
| Phase | Verified behavior |
|---|---|
| `basics` | IPv4 and IPv6 sessions; routes in both directions with AS path, next hop, communities, and large communities preserved; four-octet AS numbers; ADD-PATH from BIRD; forwarding with `ping` over the installed routes |
| `roles` | BGP role negotiation with FRR (peer) and BIRD (provider); OTC attribute added and verified in both directions |
| `rpki` | RTR version 2 with Routinator; an RPKI invalid route from FRR is rejected and held, and accepted after Routinator restarts with data that authorizes it |
| `refresh` | Routes added and withdrawn by FRR; ROUTE-REFRESH from FRR; route readvertisement by BIRD |
| `flaps` | Five session resets; FRR and BIRD terminated and restarted; link failure detected by the hold timer |
| `restart` | Daemon stopped normally (kernel routes removed, neighbors notified) and terminated (routes retained, stale route removed at the next start) |
| `scale` | A routing table from BIRD (`SCALE_ROUTES`; 1 million routes for a full table) installed in the routing table and the kernel; daemon terminated and restarted under load; table withdrawn |
| `limits` | Session rejected on role mismatch; Cease NOTIFICATION with Maximum Number of Prefixes Reached, and hold-down |
| `hijacks` | FRR advertises a local prefix, a route with BIRD as third-party next hop, and the exchange LAN prefix; all are rejected |
| `nexthops` | A preferred route from FRR with an unresolvable next hop is held, and the route from BIRD is selected and installed; the selection changes when the next hop becomes resolvable, and reverts when it becomes unresolvable |
| `transit` | FRR as customer and BIRD as provider: FRR's routes are advertised to BIRD with the local AS prepended; BIRD's routes are advertised to FRR with the OTC attribute; a control community prevents the advertisement of one FRR route to BIRD; traffic is forwarded between their networks |
| `v6only` | Exchange LAN without IPv4 addresses: one IPv6 session with each router for both address families; IPv4 routes with IPv6 next hops (RFC 8950) in BGP and in the kernel; IPv4 forwarding verified with `ping` |
| `linklocal` | Exchange LAN with link-local addresses only, with the same addresses on a second interface: sessions with FRR (by interface, with radvd) and BIRD at `fe80::…%ix0`; both address families in BGP and in the kernel; forwarding verified with `ping`; interface disabled and enabled |
The script `scripts/interop/real-rpki.sh` tests the RTR client against a
Routinator instance with the global RPKI data, including ASPAs. The test
verifies that the daemon receives the same number of VRPs and ASPAs that
the validator reports.
The lab does not cover routers other than FRR and BIRD, IPv6 routes over
IPv4 sessions (which Ferrix does not advertise), an interface that is
deleted and recreated during a session, or long-running operation.
## Fuzz Testing
The `fuzz/` directory contains coverage-guided fuzz targets for all parsers
of external input. Table 3 lists the targets.
| Target | Input |
|---|---|
| `message` | Raw bytes received on a BGP connection |
| `message_body` | A message body with a valid header, to reach the body parsers |
| `attrs` | Path attribute bytes stored in the routing table |
| `prefix_text` | A prefix in text form |
| `config` | The configuration file |
| `rtr` | A stream of RTR PDUs from an RPKI cache |
| `netlink` | Messages from the kernel routing socket |
For the `message` and `message_body` targets, a failure is either a panic or
a message that is decoded but cannot be encoded and decoded back to the
same value.
Fuzz testing requires a nightly toolchain and `cargo-fuzz`:
```bash
rustup toolchain install nightly
cargo install cargo-fuzz
cd fuzz
cargo +nightly fuzz run message_body corpus/message_body seeds/message_body -- -max_total_time=300
```
The first directory is the working corpus, which is not stored in the
repository. The `seeds/` directory contains valid inputs. Continuous
integration runs each target for 1 minute.
When a target fails, the input is saved in `artifacts/<target>/`. To
reproduce the failure, run `cargo +nightly fuzz run <target> <file>`. After
you fix the defect, add a unit test with the input next to the affected
code.
## Benchmarks
Table 4 lists the benchmarks.
| Benchmark | Measurement |
|---|---|
| `full_table` | Processing of a full routing table, from received bytes to best path selection. `BENCH_PEERS` sets the number of neighbors (default: 4); `BENCH_SCALE_DOWN` reduces the table size. |
| `transit` | Route advertisement: one neighbor advertises the table, and a second neighbor receives every route. The time is measured from the first byte sent to the last route received. |
| `real_table` | The `full_table` measurement on a RIPE RIS routing table dump. See `crates/bench/data/README.md` for how to obtain a dump. |
```bash
cargo bench -p bgp-bench --bench full_table
cargo bench -p bgp-bench --bench transit
cargo bench -p bgp-bench --bench real_table
```
The `full_table` and `transit` benchmarks use a synthetic routing table, so
that they require no data files and produce reproducible results. The
table has the characteristics of the Internet routing table that affect
performance: approximately 1 million IPv4 prefixes, mostly /24, and
250,000 IPv6 prefixes, mostly /48; prefixes with shared attributes grouped
in UPDATE messages; AS paths of several hops; and communities on most
routes.
Table 5 lists results on an Apple M-series computer with a release build.
| Measurement | Result |
|---|---|
| 1.23 million routes from one neighbor, accepted into the routing table | 0.47 seconds |
| The same routes received by a second neighbor (`transit`) | 2.1 seconds, approximately 4 routes per UPDATE message |
| 1 million IPv4 routes installed in the kernel (Linux virtual machine) | Slightly more than 1 second |
| The same routes removed from the kernel | 0.5 seconds |
@@ -0,0 +1,9 @@
+++
title = "Getting Started"
description = "Product overview, installation, and a first BGP session."
weight = 1
sort_by = "weight"
[extra]
number = "01"
+++
@@ -0,0 +1,103 @@
+++
title = "Installing Ferrix"
description = "Describes how to build Ferrix from source or as a container image, and how to grant the daemon the privileges it requires."
weight = 2
[extra]
nav = "Installation"
+++
Ferrix is distributed as source code. You build the `bgp` binary with
Cargo, or build a container image with the provided `Dockerfile`.
## Requirements
- A stable Rust toolchain from [rustup.rs](https://rustup.rs). The container
image uses Rust 1.92.
- Linux, for kernel route installation. On other operating systems, Ferrix
operates as a BGP speaker only and does not modify forwarding.
The configuration schema is compiled in pure Rust; the Protocol Buffers
compiler (`protoc`) is not required.
## Building from Source
To build and install the daemon:
1. Clone the repository.
```bash
git clone https://git.rayhaan.net/rayhaan/ferrix.git
cd ferrix
```
2. Build the release binary.
```bash
cargo build --release --locked -p bgp
```
The `--locked` option builds with the dependency versions recorded in
`Cargo.lock`, which are the versions tested in continuous integration.
3. Install the binary.
```bash
sudo install -m 0755 target/release/bgp /usr/local/bin/bgp
```
4. Verify the installation.
```bash
bgp --version
```
The command displays the installed version, for example `bgp 0.1.0`.
## Building a Container Image
The `Dockerfile` in the repository builds the daemon in a Rust build stage
and copies the binary into a Debian slim image.
```bash
docker build -t ferrix .
docker run --rm --network host \
-v /etc/ferrix:/etc/ferrix:ro \
ferrix
```
By default, the container reads the configuration from
`/etc/ferrix/server.textpb`. The container requires the host network
namespace (`--network host`). If the configuration includes `kernel`, the
container also requires the `NET_ADMIN` capability (`--cap-add NET_ADMIN`).
## Granting Privileges
Ferrix does not require root privileges. Table 1 lists the Linux
capabilities that it requires.
| Capability | Required for |
|---|---|
| `CAP_NET_BIND_SERVICE` | Listening on TCP port 179 |
| `CAP_NET_ADMIN` | Installing routes in the kernel (`kernel` statement) |
To grant the capabilities to the binary:
```bash
sudo setcap cap_net_admin,cap_net_bind_service+ep /usr/local/bin/bgp
```
Alternatively, grant them to the systemd service. See
[Deploying Ferrix](@/docs/operations/deployment.md).
> [!NOTE]
> Ferrix does not enable IP forwarding. To forward transit traffic, set
> `net.ipv4.ip_forward=1` and `net.ipv6.conf.all.forwarding=1`:
>
> ```bash
> sudo sysctl -w net.ipv4.ip_forward=1 net.ipv6.conf.all.forwarding=1
> ```
## Next Steps
To establish a first BGP session, see [Quick Start](@/docs/getting-started/quick-start.md).
@@ -0,0 +1,96 @@
+++
title = "Ferrix Overview"
description = "Describes Ferrix, its capabilities, its current limitations, and the structure of its source code."
weight = 1
[extra]
nav = "Overview"
+++
Ferrix is a Border Gateway Protocol (BGP) daemon for Linux routers at the
edge of a network. It establishes BGP sessions with transit providers,
peers, Internet exchange route servers, and customers, and it installs the
best path for each prefix in the Linux kernel. Ferrix is written in Rust and
contains no `unsafe` code.
Ferrix applies the standard inbound route filters to every external
neighbor by default: bogon prefixes and AS numbers, overly specific
prefixes, long AS paths, third-party next hops, and the local router's own
prefixes. Route origin validation, ASPA verification, and BGP roles each
require one configuration statement. See
[Comparison with the NLNOG BGP Filter Guide](@/docs/guides/import-policy.md#comparison-with-the-nlnog-bgp-filter-guide).
The daemon is a single binary, `bgp`. It contains the BGP speaker, the
RPKI-to-Router (RTR) client, the kernel route installer, and an HTTP status
interface.
## Features
Ferrix provides the following capabilities:
- **Import policy.** Ferrix validates every route that a neighbor
advertises before the route enters the routing table. By default, it
rejects bogon prefixes and AS numbers, routes whose next hop is not the
advertising neighbor, and routes for prefixes that the local router
originates. You can configure per-neighbor prefix lists, origin AS lists,
and prefix limits. See [Import Policy](@/docs/guides/import-policy.md).
- **RPKI validation.** Ferrix performs route origin validation (ROV) and
AS path verification with Autonomous System Provider Authorization (ASPA)
data received over RTR. See [RPKI](@/docs/guides/rpki.md).
- **Route leak prevention.** Ferrix implements BGP roles and the Only to
Customer (OTC) attribute (RFC 9234). See
[BGP Roles](@/docs/guides/roles.md).
- **Kernel route installation.** On Linux, Ferrix installs the active route
for each prefix in the kernel routing table over netlink. Routes with an
unresolvable next hop are excluded from best path selection. See
[Kernel Route Installation](@/docs/guides/kernel.md).
- **Route advertisement.** Ferrix advertises the routes that it originates
to all neighbors. It can also advertise learned routes, subject to BGP
roles, communities, and export policy. See
[Route Advertisement](@/docs/guides/passing-routes-on.md).
- **Monitoring.** Ferrix provides a status page, a JSON API, a server-sent
event stream, and Prometheus metrics. See
[Monitoring Ferrix](@/docs/operations/monitoring.md).
## Release Status
> [!IMPORTANT]
> Ferrix 0.1.0 is the first release. It has not been deployed in a
> production network. Ferrix is tested against FRR, BIRD, and Routinator in
> an automated interoperability lab and against real Linux kernels.
Ferrix 0.1.0 does not support the following features:
- Route reflection, per-client best path selection for route servers, and
ADD-PATH for advertised routes.
- TCP Authentication Option (TCP-AO), TCP MD5 signatures, and the
Generalized TTL Security Mechanism (GTSM).
- Graceful restart, Bidirectional Forwarding Detection (BFD), and
configuration changes without a restart.
- Recursive resolution of next hops that are not directly connected.
For the implementation status of each RFC, see
[Supported Standards](@/docs/reference/rfcs.md).
## Source Code Organization
The Ferrix source code is a Cargo workspace. Table 1 lists its crates.
| Crate | Contents |
|-------|----------|
| `crates/primitives` | Prefixes, address families, and parsing utilities |
| `crates/packet` | BGP message encoding and decoding |
| `crates/rib` | The routing table, best path selection, and the change journal |
| `crates/rpki` | The RTR client, route origin validation, and ASPA verification |
| `crates/fib` | Kernel route installation over netlink |
| `crates/server` | Session management, the BGP finite state machine, policy, and configuration |
| `crates/web` | The status page, HTTP API, and metrics |
| `bin/bgp` | The daemon |
For a description of how these components interact, see
[Architecture Overview](@/docs/development/architecture.md).
## License
Ferrix is licensed under the Apache License 2.0. The source code is
available at [git.rayhaan.net/rayhaan/ferrix](https://git.rayhaan.net/rayhaan/ferrix).
@@ -0,0 +1,117 @@
+++
title = "Quick Start"
description = "Describes how to establish a BGP session between Ferrix and a test peer on a single host, and how to verify the routes that Ferrix accepts and rejects."
weight = 3
+++
This procedure establishes a BGP session between Ferrix and a test peer
over the loopback interface. The procedure requires no privileges and does
not modify the kernel routing table.
The test peer, `fake_peer`, is a minimal BGP speaker included in the
repository. It advertises the prefixes that you specify, displays every
UPDATE message that it receives, and optionally advertises received routes
back to Ferrix.
## Before You Begin
Build Ferrix from source as described in
[Installing Ferrix](@/docs/getting-started/installation.md). Run the
commands in this procedure from the root of the repository.
## Configuring Ferrix
The following configuration defines one neighbor, two originated routes,
and the HTTP status interface:
{{ example(name="quick-start") }}
For a description of each statement, see the
[Configuration Reference](@/docs/reference/configuration.md).
1. Validate the configuration.
```bash
cargo run -p bgp -- --check docs/examples/quick-start.textpb
```
The daemon reports that the configuration is valid:
```
docs/examples/quick-start.textpb is in order: AS64500, peers: 1, routes originated: 2
```
2. Start the daemon.
```bash
cargo run -p bgp -- docs/examples/quick-start.textpb
```
The daemon listens on `127.0.0.1:1790`. It also attempts to connect to
the neighbor on port 1791, where no peer is listening; these attempts
fail until the test peer connects.
## Establishing the Session
1. In a second terminal, start the test peer. The peer uses AS 65001,
advertises `203.0.113.0/24`, and with `--echo` advertises every route
that it receives back to Ferrix.
```bash
cargo run -p bgp-integration-tests --example fake_peer -- \
--server 127.0.0.1:1790 --asn 65001 --announce 203.0.113.0/24 --echo
```
2. Verify that the session is established. The test peer displays output
similar to the following:
```
established with 127.0.0.1:1790 (AS64500 router-id 192.0.2.1)
announced 203.0.113.0/24
UPDATE 198.51.100.0/24 attrs=[Origin(IGP), AsPath(...[64500]...), NextHop(127.0.0.1)]
echoed 198.51.100.0/24 back with path [65001, 64500]
```
The peer received the route `198.51.100.0/24` from Ferrix and
advertised it back with AS 65001 prepended.
## Verifying Received Routes
1. Display the routes received for `203.0.113.0/24`:
```bash
curl -s '127.0.0.1:8080/api/routes/received?prefix=203.0.113.0/24'
```
```json
{"routes":[{"peer_name":"test-peer","prefix":"203.0.113.0/24","best":true,
"summary":{"origin":"IGP","as_path":[65001],"next_hop":"10.0.0.2", ...}}]}
```
Ferrix accepted the route and selected it as the best path.
2. Display the rejected routes:
```bash
curl -s 127.0.0.1:8080/api/routes/rejected
```
```json
[{"peer":"test-peer","total":1,"by_reason":{"own AS in path":1},
"recent":[{"prefix":"198.51.100.0/24","reason":"own AS in path","as_path":[65001,64500]}]}]
```
Ferrix rejected the route that the peer advertised back, because its AS
path contains the local AS number (64500). This is BGP loop detection.
The status page at [http://127.0.0.1:8080/statusz](http://127.0.0.1:8080/statusz)
displays the same information. Rejected routes are also counted in the
`bgp_routes_rejected_total` metric at
[`/metricz`](http://127.0.0.1:8080/metricz). For alerting rules based on
this metric, see [Monitoring Ferrix](@/docs/operations/monitoring.md#alerting-rules).
## Next Steps
- [Configuring BGP Neighbors](@/docs/guides/peers.md)
- [Import Policy](@/docs/guides/import-policy.md)
- [Configuration Examples](@/docs/guides/examples.md)
+9
View File
@@ -0,0 +1,9 @@
+++
title = "User Guide"
description = "Configuring BGP neighbors, import policy, route leak prevention, RPKI validation, forwarding, and route advertisement."
weight = 2
sort_by = "weight"
[extra]
number = "02"
+++
+77
View File
@@ -0,0 +1,77 @@
+++
title = "Configuration Examples"
description = "Provides complete configurations for a single-homed edge network, a network with an Internet exchange connection, and a transit network."
weight = 7
+++
This topic provides complete configurations for three common deployments.
Each configuration is stored in the `docs/examples/` directory of the
repository, and continuous integration validates each one with
`bgp --check`.
The examples use documentation addresses and AS numbers. Replace them with
your own.
> [!NOTE]
> Documentation prefixes such as `192.0.2.0/24` and `2001:db8::/32` are
> bogon prefixes. In a production network, Ferrix rejects routes for them
> from neighbors by default.
## Example: Edge Network with One Transit Provider
This configuration is for a network with one transit provider and one
BGP session for each address family. It enables route origin validation,
ASPA verification, and kernel route installation.
{{ example(name="edge") }}
Before you deploy this configuration, adjust the following statements:
- `max_prefixes`: set a value above the number of routes that the provider
advertises, with margin for growth.
- `rpki`: specify the address of your RPKI cache. See
[Running a Validator](@/docs/guides/rpki.md#running-a-validator).
- If the provider advertises only a default route, configure
`accept_default_route: true` in its `import` statement, and reduce
`max_prefixes` accordingly.
## Example: Network with an Internet Exchange Connection
This configuration is for a network with one transit provider, a session
with an Internet exchange route server, and a bilateral session with one
peer at the exchange. Routes learned at the exchange have a higher local
preference than transit routes. Every accepted route is marked with a
large community that records where it was learned.
{{ example(name="exchange") }}
The configuration has the following characteristics:
- The route server session includes `accept_any_first_as`, because the
route server does not prepend its AS number. The role `ROUTE_SERVER`
permits next hops other than the route server's address.
- The bilateral peer can advertise only its registered prefixes and origin
AS. See [Generating Lists with bgpq4](@/docs/guides/import-policy.md#generating-lists-with-bgpq4).
- Each import policy removes the local AS's informational communities
before it adds its own.
## Example: Transit Network
This configuration is for a network with two transit providers and one
customer. The customer's routes are advertised to all neighbors. Routes
from the providers are advertised to the customer only. The customer can
restrict the advertisement of its routes with control communities.
{{ example(name="transit") }}
The configuration has the following characteristics:
- Every neighbor has a role. The roles prevent the advertisement of one
provider's routes to the other provider. See
[Roles and Route Advertisement](@/docs/guides/passing-routes-on.md#roles-and-route-advertisement).
- The customer's routes have a local preference of 200, so that they are
preferred over routes for the same prefixes from the providers.
- The `export` statement of `transit-b` replaces the top-level `export`
statement for that neighbor, and therefore repeats `learned_routes: true`.
- The customer's `remove_communities` statement removes only the
`64500:2:*` communities, and retains the customer's control communities.
+254
View File
@@ -0,0 +1,254 @@
+++
title = "Import Policy"
description = "Describes how Ferrix validates routes received from neighbors: the default checks, per-neighbor prefix and origin lists, prefix limits, and attribute modification."
weight = 2
+++
The import policy determines which routes received from a neighbor are
accepted into the routing table. Ferrix evaluates every route in an UPDATE
message against the import policy of the neighbor that advertised it. You
configure the import policy with the `import` statement of each neighbor.
## Understanding Import Policy
A neighbor can advertise any prefix with any attributes. If a route that a
neighbor is not authorized to advertise is accepted, the local router uses
it for forwarding and can advertise it to other neighbors. The import
policy rejects such routes before they enter the routing table.
When Ferrix rejects a route, it also removes any route previously accepted
from the same neighbor for the same prefix. Ferrix counts rejected routes by
reason and retains the most recent rejections for inspection. See
[Rejected Routes](@/docs/operations/monitoring.md#rejected-routes).
## Default Checks
Ferrix applies the checks in Table 1 to routes from every neighbor, without
configuration. Each check can be disabled for a neighbor that requires it,
such as a test network or a transit provider that advertises a default
route.
| Rejected route | Statement to disable | Rejection reason |
|---|---|---|
| Prefix that is not routed on the Internet: private, loopback, link-local, documentation, multicast, or reserved | `accept_bogons` | `bogon prefix` |
| Private, reserved, or documentation AS number in the AS path | `accept_bogons` | `bogon AS in path` |
| IPv4 prefix longer than /24, or IPv6 prefix longer than /48 | `max_ipv4_prefix_length`, `max_ipv6_prefix_length` | `prefix too specific` |
| Default route, `0.0.0.0/0` or `::/0` | `accept_default_route` | `prefix too short` |
| AS path that does not begin with the neighbor's AS number | `accept_any_first_as` | `path does not begin with the peer's AS` |
| AS path longer than 64 AS numbers | `max_as_path_length` | `path too long` |
| AS path that contains an AS_SET (RFC 9774) | `accept_as_sets` | `AS_SET in path` |
| Next hop other than the neighbor's address | `accept_third_party_next_hop` | `next hop is not the peer` |
| Prefix originated by the local router, or a more specific prefix within it | Listing the prefix in the neighbor's `prefixes` | `prefix is our own` |
For an Internet exchange route server, configure `accept_any_first_as`,
because a route server does not prepend its AS number to the AS path. The
next hop check does not apply to a neighbor with the role `ROUTE_SERVER`.
For IBGP neighbors (where `remote_asn` equals `local_asn`), Ferrix applies
only the checks on the prefix. The AS path checks do not apply, and the
next hop can be any usable address.
Ferrix always rejects the routes in Table 2. These checks cannot be
disabled.
| Rejected route | Rejection reason |
|---|---|
| Route from an EBGP neighbor whose AS path contains the local AS number | `own AS in path` |
| Route whose next hop is unusable: unspecified, loopback, multicast, or an address of the local router | `next hop unusable` |
| Route with the Only to Customer attribute received from the wrong direction (see [BGP Roles](@/docs/guides/roles.md)) | `route leak (Only to Customer)` |
### Next Hop Check
The next hop of a route is the address to which the local router forwards
traffic for the prefix. By default, Ferrix requires the next hop of a route
from an EBGP neighbor to be the neighbor's own address. This prevents a
neighbor from directing traffic to a third party, such as another member of
an Internet exchange or a host in another network of the local operator.
An Internet exchange route server advertises routes with the addresses of
the originating members as next hops. For this reason, the check does not
apply to a neighbor with the role `ROUTE_SERVER`.
### Local Prefix Check
By default, Ferrix rejects routes from EBGP neighbors for prefixes that it
originates, and for more specific prefixes within them. Accepting such a
route would direct traffic for the local network to an external network.
If a customer is authorized to advertise part of the local address space,
include the prefix in the customer's `prefixes` list. When a neighbor has a
`prefixes` list, the local prefix check does not apply to it.
## Comparison with the NLNOG BGP Filter Guide
The [NLNOG BGP Filter Guide](https://bgpfilterguide.nlnog.net/) lists the
inbound filters that operators commonly configure on EBGP sessions. Many
BGP implementations, including FRR, BIRD, and Cisco IOS XR, reject all
routes from an EBGP neighbor until a policy is configured (RFC 8212), and
the operator writes these filters in the implementation's policy language.
Ferrix applies most of them by default. Table 3 compares the guide with
Ferrix.
| Filter in the guide | Ferrix |
|---|---|
| Reject RPKI invalid routes | One statement: `rpki` |
| Attest trust anchor prefixes (SLURM) | Configured in the RPKI validator |
| Bogon ASN filtering | Default |
| Bogon prefix filtering | Default |
| No small prefixes | Default: longer than /24 or /48 |
| No IXP peering LAN leaks | Partial: such routes are never installed in the kernel, but they are not rejected |
| No long AS paths | Default: longer than 64 AS numbers |
| No known transit ASNs in the AS path | Not supported. BGP roles and ASPA verification detect many of the same route leaks. |
| Strip high community counts | Not supported |
| Remainder accept term with communities and local preference | `add_communities`, `add_large_communities`, `local_pref` |
| Graceful shutdown support | Not supported |
Ferrix also applies the following checks, which the guide does not list,
by default: the next hop must be the neighbor, routes for local prefixes
are rejected, AS paths with an AS_SET are rejected, and the AS path must
begin with the neighbor's AS number.
> [!NOTE]
> Ferrix does not implement the default behavior of RFC 8212 on import: it
> accepts routes from an EBGP neighbor that pass the default checks. On
> export, Ferrix advertises only originated routes unless `learned_routes`
> is enabled, so that learned routes are not advertised without explicit
> configuration.
## Configuring Prefix and Origin Lists
To restrict the routes that a neighbor can advertise, configure a prefix
list, an origin AS list, or both:
```textproto
import {
prefixes { prefix: "193.0.0.0/21" max_length: 24 }
prefixes { prefix: "2001:67c:2e8::/48" }
origin_asns: 3333
}
```
- `prefixes` lists the prefixes that the neighbor can advertise. The
`max_length` option also permits more specific prefixes up to the
specified length. If the list is empty, all prefixes are permitted.
Rejection reason: `prefix not allowed`.
- `origin_asns` lists the AS numbers from which the neighbor's routes can
originate. The origin AS is the last AS number in the AS path. If the list
is empty, all origin AS numbers are permitted. Rejection reason:
`origin AS not allowed`.
### Generating Lists with bgpq4
You can generate prefix and origin AS lists from Internet Routing Registry
(IRR) data with [bgpq4](https://github.com/bgp/bgpq4). The following
commands query the registry for an AS-SET and convert the output to Ferrix
configuration statements with `jq`:
```bash
bgpq4 -4 -R 24 -j -l prefixes AS-EXAMPLE > v4.json
bgpq4 -6 -R 48 -j -l prefixes AS-EXAMPLE > v6.json
bgpq4 -t -j -l asns AS-EXAMPLE > asns.json
jq -r '.prefixes[] | "prefixes { prefix: \"\(.prefix)\""
+ (if .exact then "" else " max_length: \(."less-equal")" end) + " }"' v4.json v6.json
jq -r '.asns[] | "origin_asns: \(.)"' asns.json
```
The commands produce statements such as the following, for the neighbor's
`import` statement:
```textproto
prefixes { prefix: "193.0.0.0/21" max_length: 24 }
prefixes { prefix: "193.0.10.0/23" max_length: 24 }
origin_asns: 3333
```
> [!TIP]
> Regenerate the lists periodically, validate the new configuration with
> `bgp --check`, and restart the daemon. An outdated list rejects the
> neighbor's new prefixes with the reason `prefix not allowed`.
## Prefix Limits
The `max_prefixes` statement specifies the maximum number of routes that a
neighbor can have in the routing table. If the neighbor exceeds the limit,
Ferrix sends a Cease NOTIFICATION message with the subcode Maximum Number
of Prefixes Reached, tears down the session, and places the neighbor in
hold-down for `hold_down_seconds`. During hold-down, Ferrix neither
initiates nor accepts connections for the neighbor.
```textproto
import {
max_prefixes: 1500000
hold_down_seconds: 600
}
```
By default, the number of prefixes is not limited. The default hold-down
is 600 seconds.
> [!IMPORTANT]
> Configure `max_prefixes` for every neighbor, with a value somewhat above
> the number of routes that the neighbor is expected to advertise. Without
> a limit, a neighbor that leaks a full routing table is accepted.
The metrics `bgp_peer_routes` and `bgp_peer_max_prefixes` report the
current number of routes and the limit for each neighbor. See
[Alerting Rules](@/docs/operations/monitoring.md#alerting-rules).
## RPKI and ASPA Validation
If RPKI validation is configured, Ferrix also rejects routes whose origin
is RPKI invalid and, for neighbors with a role, routes whose AS path is
ASPA invalid. See [RPKI](@/docs/guides/rpki.md).
## Modifying Route Attributes
The import policy can add and remove communities on accepted routes, and
set their local preference:
```textproto
peers {
name: "ixp-a-rs"
import {
remove_communities: "64500:2:*"
remove_communities: "64500:3:*"
add_large_communities: "64500:2:1001"
local_pref: 150
}
}
```
The statements have the following effect:
- `remove_communities` removes matching communities. Ferrix applies it
before adding communities. A pattern has two fields for a standard
community and three for a large community; each field is a number or `*`.
Examples: `64500:100`, `64500:*`, `64500:2:*`, `*:*`.
- `add_communities` and `add_large_communities` add communities to every
accepted route. Use them to record where a route was learned, for other
routers in the local AS and for export policy.
- `local_pref` sets the local preference of every accepted route. Best path
selection compares local preference first; the route with the higher
value is preferred. Default: 100.
> [!TIP]
> Remove the local AS's informational communities from received routes, so
> that a neighbor cannot set them. A common local preference scheme assigns
> 200 to customer routes, 150 to peer and route server routes, and 100 to
> transit routes.
The examples in this documentation use large communities `64500:2:x` to
record where a route was learned and `64500:3:r` to record the router that
advertised it. These values do not overlap the
[control communities](@/docs/guides/passing-routes-on.md#control-communities),
which use `64500:0:*` and `64500:1:*`, or the standard communities
`64500:n`, which control communities interpret as "advertise to AS n".
## Performance
Ferrix evaluates each route once, when the session's task processes the
UPDATE message. Each check is a comparison or a table lookup. Attribute
modifications are applied once for each UPDATE message, because all routes
in a message share the same attributes. A full routing table from one
neighbor is accepted into the routing table in less than 0.5 seconds.
+170
View File
@@ -0,0 +1,170 @@
+++
title = "Kernel Route Installation"
description = "Describes how Ferrix installs active routes in the Linux kernel routing table over netlink, and how it handles next hop resolution, restarts, and kernel errors."
weight = 5
[extra]
nav = "Kernel Route Installation"
+++
Ferrix can install the active route for each prefix in the Linux kernel
routing table, so that the router forwards traffic according to BGP. Ferrix
communicates with the kernel over rtnetlink. Kernel route installation is
supported on Linux only.
If the `kernel` statement is not configured, Ferrix operates as a BGP
speaker only and does not modify the kernel routing table.
## Configuring Kernel Route Installation
The following example enables kernel route installation with the default
values:
```textproto
kernel {
table: 254
metric: 20
}
```
| Statement | Description | Default |
|---|---|---|
| `table` | Kernel routing table in which routes are installed | 254 (`main`) |
| `metric` | Metric of the installed routes. The kernel prefers the route with the lowest metric. | 20 |
| `without_nexthop_objects` | Installs each route with its own gateway instead of a nexthop object | Disabled |
| `stale_seconds` | Time for which routes from a previous run are retained at startup | 120 seconds |
Ferrix requires the `CAP_NET_ADMIN` capability to modify the kernel routing
table. If the kernel rejects a request for lack of privileges, the daemon
exits. See [Granting Privileges](@/docs/getting-started/installation.md#granting-privileges).
> [!NOTE]
> Ferrix does not enable IP forwarding. Set `net.ipv4.ip_forward=1` and
> `net.ipv6.conf.all.forwarding=1` to forward transit traffic.
## Next Hop Resolution
Ferrix installs a route only if its next hop is directly connected: the
next hop must be an address in a subnet of an interface that is up. For an
IPv6 route with a link-local next hop, the outgoing interface is the
interface on which the neighbor's session is established.
Ferrix does not resolve next hops recursively through other routes.
Therefore, a route whose next hop is not directly connected, such as a
route from an IBGP neighbor that does not set itself as the next hop, is
not installed.
### Exclusion of Unresolvable Routes
If the `kernel` statement is configured, Ferrix excludes routes with an
unresolvable next hop from best path selection (RFC 4271 Section 9.1.2.1).
This ensures that such a route is not selected while a usable route for the
same prefix exists.
A route with an unresolvable next hop is a held route. Ferrix counts it as
rejected with the reason `next hop unreachable`, and displays it as held on
the status page. When an interface or address change makes the next hop
resolvable, Ferrix accepts the route. A route in the routing table whose
next hop becomes unresolvable is also moved to the held state.
If the `kernel` statement is not configured, Ferrix has no information about
next hop reachability and accepts all routes.
## Connected Networks
Ferrix never installs a route for a subnet of a local interface, or for a
prefix within such a subnet, even if the import policy accepts it. The
kernel reaches these destinations directly. Without this rule, a neighbor
on a shared LAN that advertises part of the LAN could attract the traffic
for other hosts on the LAN, including the BGP sessions with them.
The status page counts such routes as "inside a network of this machine".
## Route Ownership
Ferrix installs routes with the protocol `bgp` (`proto bgp`). Ferrix
considers every route with this protocol, in the configured table and with
the configured metric, to be its own. It does not modify or remove any
other route.
> [!WARNING]
> Do not run two routing daemons that install BGP routes in the same table
> with the same metric. Each daemon would remove the other's routes.
## Nexthop Objects
On Linux 5.3 and later, Ferrix installs each gateway once as a nexthop
object, and installs routes that reference the object. The kernel then
validates each gateway once instead of once for each route. This reduces
the time to install a full routing table by approximately one third, and
allows all routes to be removed at once when the daemon stops.
The command `ip route` displays these routes with an `nhid` attribute, and
the command `ip nexthop` displays the objects. Ferrix uses only the nexthop
objects that it creates, recreates them if they are removed, and removes
them when it stops.
To install each route with its own gateway, as on older kernels, configure
`without_nexthop_objects: true`.
## Startup and Shutdown Behavior
Ferrix handles its kernel routes as follows:
- **At startup**, Ferrix retains the routes installed by a previous run, so
that the router continues to forward traffic while the neighbors
readvertise their routes. After `stale_seconds`, Ferrix removes the routes
that no neighbor has readvertised.
- **On SIGINT or SIGTERM**, Ferrix closes its BGP sessions and removes its
routes from the kernel.
- **After a crash**, the routes remain in the kernel until the next start.
## Interface Changes
Ferrix receives the router's interfaces and addresses from the kernel, and
receives a notification when they change. Ferrix reevaluates all routes 1
second after the last change, and at most 5 seconds after the first change,
because a change can affect the resolution of any next hop.
## Kernel Error Handling
The kernel reports each request that it rejects. If the kernel's
notifications exceed the socket buffer, or if the kernel does not respond
within 10 seconds, Ferrix reads the kernel routing table and corrects any
difference from the expected state.
## Verifying Installed Routes
To display the installed routes, use the following commands:
```bash
ip route show proto bgp
ip -6 route show proto bgp
ip nexthop show
```
The status page and the HTTP API endpoint `/api/kernel` display the number
of installed routes, the number of routes not installed and the reason, the
last error reported by the kernel, and the interfaces. Table 2 lists the
related metrics.
| Metric | Description |
|---|---|
| `bgp_kernel_routes` | Number of routes installed by Ferrix |
| `bgp_kernel_routes_unresolved` | Number of prefixes whose active route is not installed because its next hop is unresolvable |
| `bgp_kernel_routes_refused` | Number of prefixes whose active route the kernel rejected |
| `bgp_kernel_route_changes_total{change}` | Number of route installations and removals requested |
| `bgp_kernel_errors_total` | Number of requests that the kernel rejected |
## Performance
In a virtual machine on an Apple M-series computer, with a release build,
Ferrix installs 1 million IPv4 routes in slightly more than 1 second and removes
them in approximately 0.5 seconds. Approximately three quarters of this
time is spent in the kernel. Ferrix reduces its own share as follows:
- It sends hundreds of requests in each write to the netlink socket, and
requests one acknowledgment for each write instead of each route.
- A dedicated task writes to the socket, so that the next batch is prepared
while the kernel processes the previous one.
- It resolves each next hop once, not once for each route.
@@ -0,0 +1,196 @@
+++
title = "Route Advertisement"
description = "Describes how Ferrix advertises originated and learned routes, how BGP roles and communities restrict advertisement, and how to configure export policy."
weight = 6
+++
Ferrix advertises two kinds of routes to its neighbors:
- **Originated routes**, configured with the `originate` statement. Ferrix
advertises them to every neighbor.
- **Learned routes**, received from other neighbors. Ferrix advertises the
best path for each prefix to the neighbors whose export policy enables
`learned_routes`.
## Configuring Originated Routes
The following example originates one IPv4 and one IPv6 prefix:
```textproto
originate { prefix: "198.51.100.0/24" }
originate {
prefix: "2001:db8:100::/48"
med: 50
as_path_prepend: 1
large_communities: "64500:2:0"
}
```
Ferrix advertises an originated route with an AS path that contains the
local AS number and, on each session, the local address of the session as
the next hop. For the optional attributes, see
[OriginatedRoute](@/docs/reference/configuration.md#originatedroute).
## Advertising Learned Routes
To advertise learned routes to a neighbor, enable `learned_routes` in its
export policy:
```textproto
export { learned_routes: true }
```
The top-level `export` statement applies to every neighbor that does not
have its own `export` statement. A neighbor's `export` statement replaces
the top-level statement entirely.
Ferrix does not advertise a learned route in the following cases. These
rules cannot be disabled:
- To the neighbor from which the route was received.
- To a neighbor whose AS number is in the AS path of the route.
- From one IBGP neighbor to another IBGP neighbor. Route reflection is not
supported.
- To an EBGP neighbor, if the route has the `NO_EXPORT` community; to any
neighbor, if the route has the `NO_ADVERTISE` community.
### Attributes of Advertised Routes
Table 1 lists how Ferrix modifies the attributes of a learned route for
each type of neighbor.
| Neighbor type | AS path | Next hop | MED | Local preference |
|---|---|---|---|---|
| EBGP | Local AS prepended | Local address of the session | Removed | Not sent |
| IBGP | Unchanged | Local address of the session | Unchanged | Sent; 100 if the route has none |
| `ROUTE_SERVER_CLIENT` (RFC 7947) | Unchanged | Unchanged, if the client can use it | Unchanged | Not sent |
Ferrix advertises unknown optional transitive attributes with the Partial
bit set, and does not advertise unknown non-transitive attributes.
### Roles and Route Advertisement
Ferrix adds the Only to Customer (OTC) attribute to routes received from
providers, peers, and route servers (RFC 9234), and advertises routes with
the OTC attribute only to customers. Routes received from customers are
advertised to all neighbors. Therefore, when every neighbor has a role, the
local router provides transit only to its customers.
```textproto
export { learned_routes: true }
peers { name: "customer-a" role: CUSTOMER ... }
peers { name: "transit-a" role: PROVIDER ... }
peers { name: "ixp-a-rs" role: ROUTE_SERVER ... }
```
> [!CAUTION]
> A neighbor without a role is not subject to this restriction. If
> `learned_routes` is enabled for such a neighbor, Ferrix advertises all
> learned routes to it, including routes from providers and peers. Ferrix
> logs a warning at startup for each such neighbor.
## Configuring Export Policy
The export policy can add and remove communities on advertised routes, and
prepend the local AS number to the AS path:
```textproto
export {
remove_communities: "64500:2:*"
add_large_communities: "64500:3:1"
as_path_prepend: 2
}
```
| Statement | Description |
|---|---|
| `remove_communities` | Removes matching communities before communities are added. A pattern has two fields for a standard community and three for a large community; each field is a number or `*`. |
| `add_communities`, `add_large_communities` | Adds communities to every advertised route. |
| `as_path_prepend` | Prepends the local AS number the specified number of additional times. Range: 0 through 16. Default: 0. |
## Control Communities
Control communities allow a route's communities to restrict the neighbors
to which Ferrix advertises it. The format is the one used by Internet
exchange route servers. To enable control communities, configure the
following top-level statement:
```textproto
control_communities: true
```
Table 2 lists the control communities, where `L` is the local AS number and
`P` is a neighbor's AS number.
| Standard community | Large community | Action |
|---|---|---|
| `0:P` | `L:0:P` | Do not advertise to the neighbor with AS `P`. |
| `L:P` | `L:1:P` | Advertise to the neighbor with AS `P`. If present, advertise only to neighbors named in such communities. |
| `0:L` | `L:0:0` | Do not advertise to any neighbor. |
Control communities are evaluated as follows:
- "Do not advertise" takes precedence over "advertise".
- A route without control communities is advertised as the other rules
permit.
- Standard communities can express AS numbers up to 65535 only. Use the
large community forms for four-octet AS numbers.
- Ferrix removes control communities from a route when it advertises the
route.
- Control communities apply to originated routes and to communities added
by the import policy.
The following example prevents the advertisement of an originated route to
AS 64496:
```textproto
control_communities: true
originate {
prefix: "198.51.100.0/24"
large_communities: "64500:0:64496"
}
```
> [!IMPORTANT]
> When control communities are enabled, Ferrix interprets the standard
> community `L:n` as "advertise to AS n", and the large communities `L:0:n`
> and `L:1:n` as listed in Table 2. Use large communities `L:2:n` and higher
> for informational communities. Do not remove customers' control
> communities with `remove_communities`.
## Update Generation
Ferrix groups routes with identical attributes into a single UPDATE
message. It collects changes for 50 milliseconds before it sends them.
When a neighbor requires the full routing table, at session establishment
or after a ROUTE-REFRESH message, Ferrix collects the entire table before
it sends it, so that prefixes with identical attributes share UPDATE
messages. At most four sessions collect a full table concurrently; other
sessions wait.
Each session has its own export task, which records the routes advertised
on the session. If the neighbor reads slowly, only that session's task
waits; other sessions are not affected.
In the `transit` benchmark, in which one neighbor advertises 1.23 million
routes and Ferrix advertises them to a second neighbor, the second neighbor
receives all routes after 2.1 seconds, with an average of four routes in
each UPDATE message.
## Verifying Route Advertisement
The metric `bgp_routes_exported_total{change}` counts learned routes that
Ferrix advertised (`announced`) and withdrew (`withdrawn`). Ferrix does not
provide a view of the routes advertised to a neighbor (Adj-RIB-Out); to
display them, use the neighbor's own tools.
## Limitations
- Per-client best path selection for route servers is not supported. Every
client receives the same best path.
- ADD-PATH is not supported for advertisement; only the best path is
advertised.
- Route reflection is not supported.
- UPDATE messages are generated separately for each session, even when
several sessions receive identical routes.
+221
View File
@@ -0,0 +1,221 @@
+++
title = "Configuring BGP Neighbors"
description = "Describes how to configure BGP neighbors, address families, link-local neighbors, ADD-PATH, and session timers, and how Ferrix filters inbound connections."
weight = 1
[extra]
nav = "BGP Neighbors"
+++
A BGP neighbor (peer) is a BGP speaker with which Ferrix establishes a
session over TCP. Each neighbor is defined by a `peers` statement in the
configuration.
## Configuring a Neighbor
The following example configures an external BGP (EBGP) neighbor:
```textproto
peers {
name: "transit-a"
endpoint { address: "203.0.113.1" }
remote_asn: 64496
local_address: "203.0.113.2"
hold_time_seconds: 90
role: PROVIDER
import { max_prefixes: 1500000 }
}
```
Table 1 describes the statements in the example.
| Statement | Description |
|---|---|
| `name` | Name of the neighbor in logs, metrics, and the status page. |
| `endpoint` | Address and TCP port of the neighbor. The default port is 179. |
| `remote_asn` | AS number of the neighbor. If it equals `local_asn`, the neighbor is an internal BGP (IBGP) neighbor. |
| `local_address` | Source address for connections to the neighbor, and the next hop of routes advertised to it. |
| `hold_time_seconds` | Hold time proposed in the OPEN message. |
| `role` | BGP role of the neighbor (RFC 9234). See [BGP Roles](@/docs/guides/roles.md). |
| `import` | Import policy for routes received from the neighbor. See [Import Policy](@/docs/guides/import-policy.md). |
Configure `local_address` when the router has several addresses and the
neighbor expects a specific one.
## Session Establishment
Ferrix initiates a TCP connection to each configured neighbor and also
accepts connections from it. If both connections succeed, Ferrix resolves
the collision as specified in RFC 4271 Section 6.8: the connection
initiated by the speaker with the higher BGP identifier is retained.
If a connection attempt fails, Ferrix retries after `connect_retry_seconds`
(default: 5 seconds). The interval doubles after each failure, up to a
maximum of 2 minutes.
Ferrix rejects a neighbor whose OPEN message contains an AS number other
than `remote_asn`, with the NOTIFICATION message OPEN Message Error / Bad
Peer AS.
## Inbound Connection Filtering
Ferrix accepts inbound connections only from the addresses of configured
neighbors, and limits the number of concurrent connections from each
address. Ferrix closes any other connection before reading from it and
increments the `bgp_connections_refused_total` metric.
Ferrix treats each configured neighbor as a single router. A second
connection from the same address with a different BGP identifier is
rejected. This prevents a neighbor from exceeding its prefix limit by
establishing additional sessions.
> [!CAUTION]
> The `accept_unconfigured_peers` statement accepts sessions from any
> address, with any AS number, and accepts their routes. Use it only for
> testing on an isolated network.
## Listening Addresses
By default, Ferrix listens on `0.0.0.0:179` and `[::]:179`. To listen on
specific addresses, configure `listen_endpoints`:
```textproto
listen_endpoints { address: "192.0.2.1" }
listen_endpoints { address: "2001:db8::1" }
```
## Address Families
Ferrix supports the IPv4 unicast and IPv6 unicast address families. Each
speaker lists the families that it supports in its OPEN message, and a
family is enabled on the session only if both speakers list it (RFC 4760).
By default, Ferrix proposes both families. To propose only IPv6 unicast:
```textproto
address_families: IPV6_UNICAST
```
If the neighbor supports none of the proposed families, Ferrix rejects the
session.
### IPv4 Routes with IPv6 Next Hops
Ferrix supports the advertisement of IPv4 routes with IPv6 next hops over a
session between IPv6 addresses (RFC 8950). On every IPv6 session that
carries IPv4 unicast, Ferrix proposes the Extended Next Hop capability; no
configuration is required. On the neighbor, enable the capability as
follows:
- FRR: `neighbor ... capability extended-nexthop`
- BIRD: `extended next hop on` in the IPv4 channel
When the capability is negotiated, Ferrix advertises IPv4 routes with the
session's local IPv6 address as the next hop. It accepts IPv4 routes with an
IPv6 next hop, or with a global and a link-local next hop. The kernel
installs such routes through the IPv6 neighbor, for example
`10.2.0.0/24 via inet6 2001:db8::2 dev eth0`. This requires Linux 5.2 or
later. The status page displays "IPv4 with IPv6 next hops" for the session.
## Link-Local Neighbors
Ferrix can establish a session with a neighbor at an IPv6 link-local
address (`fe80::/10`). Because a link-local address is significant only on
one link, you specify the interface with the address:
```textproto
peers {
name: "upstream"
endpoint { address: "fe80::1%eno3" }
remote_asn: 64496
}
```
Link-local neighbors have the following behavior:
- Ferrix resolves the interface name to an interface index each time it is
needed. If the interface does not exist, the neighbor state displays that
the interface is missing; the session is established when the interface
is created. An interface that is recreated with a new index, such as a
restarted tunnel, is found automatically.
- The same link-local address on two interfaces identifies two neighbors.
Ferrix rejects a link-local address without an interface, and an
interface specified with a non-link-local address.
- To accept connections from link-local neighbors, Ferrix must listen on
`::`, which is the default.
- Ferrix advertises routes with its link-local address as the next hop, in
both next hop fields of MP_REACH_NLRI. It accepts a link-local next hop in
any of the encodings in use: the link-local address alone, the link-local
address twice, or the link-local address after `::`.
- The kernel installs the routes through the link-local address on the
session's interface, for example `10.2.0.0/24 via inet6 fe80::1 dev eno3`.
- If `local_address` is a link-local address, Ferrix uses it on the
neighbor's interface.
> [!TIP]
> On FRR, configure the neighbor by interface (`neighbor eno3 interface
> remote-as ...`). FRR discovers the neighbor's address from IPv6 router
> advertisements, which Ferrix does not send; run `radvd` on the interface.
> With the `neighbor fe80::1 interface eno3` form, FRR 10.5 establishes the
> session but marks the received routes as inaccessible. On BIRD, configure
> `neighbor fe80::1%eno3 as ...`.
## ADD-PATH
By default, a neighbor advertises one path for each prefix. With ADD-PATH
(RFC 7911), a neighbor can advertise multiple paths for the same prefix. To
accept multiple paths from a neighbor:
```textproto
add_path: ADD_PATH_RECEIVE
```
ADD-PATH is enabled for each address family and direction only if the
neighbor also supports it. Ferrix negotiates `ADD_PATH_SEND` and
`ADD_PATH_BOTH`, but it advertises only one path for each prefix.
## Hold Timer
`hold_time_seconds` specifies the hold time that Ferrix proposes in its
OPEN message. The session uses the lower of the two proposed values. If no
message is received from the neighbor within the hold time, Ferrix tears
down the session.
Range: 0, or 3 through 65535 seconds. A value of 0 disables keepalive
messages and the hold timer. Default: 90 seconds.
Ferrix also implements the send hold timer (RFC 9687). If Ferrix cannot
write to the neighbor for 8 minutes or twice the hold time, whichever is
longer, it tears down the session. This prevents a neighbor that does not
read from the connection from blocking route advertisement indefinitely.
## Session Flap Damping
If sessions with a neighbor are established and then fail within 1 minute
repeatedly, Ferrix places the neighbor in hold-down. The hold-down begins
at the third such failure with a duration of 1 second, and doubles at each
further failure, up to a maximum of 5 minutes. During hold-down, Ferrix
does not establish a session with the neighbor.
The end of a session requires Ferrix to remove the neighbor's routes from
the routing table. Hold-down limits the processing that a flapping
neighbor causes.
A neighbor that exceeds its prefix limit is placed in hold-down for
`hold_down_seconds`. See [Prefix Limits](@/docs/guides/import-policy.md#prefix-limits).
## Administrative Shutdown
When the daemon receives SIGINT or SIGTERM, it sends a Cease NOTIFICATION
message with the subcode Administrative Shutdown to every neighbor. If the
`kernel` statement is configured, it then removes its routes from the
kernel. See [Kernel Route Installation](@/docs/guides/kernel.md).
## Verifying BGP Sessions
To verify the state of each neighbor, use one of the following:
- The status page, `/statusz`, lists each neighbor with its session state,
address families, role, and number of routes.
- The HTTP API, `/api/peers`. See [HTTP API Reference](@/docs/reference/http-api.md#peers).
- The metric `bgp_peer_up{peer}`, which is 1 while the session is
established and 0 otherwise.
+118
View File
@@ -0,0 +1,118 @@
+++
title = "BGP Roles"
description = "Describes BGP roles and the Only to Customer attribute (RFC 9234), which Ferrix uses to detect and prevent route leaks."
weight = 3
+++
A BGP role describes the relationship between two neighbors, such as
provider and customer. Ferrix uses BGP roles, as specified in RFC 9234, to
detect and prevent route leaks.
## Understanding Route Leaks
A route leak is the advertisement of a route beyond its intended scope. A
typical example is a network that receives a route from one transit
provider and advertises it to another transit provider. The leaking network
then carries traffic between the two providers, which neither provider
intended.
Most route leaks are configuration errors. A common cause is a mismatch in
the relationship that the two neighbors have configured: one neighbor
treats the session as a customer session, and the other treats it as a
peer session.
## Supported Roles
The `role` statement specifies the role of the neighbor in relation to the
local router. Table 1 lists the roles.
| Role | The neighbor |
|---|---|
| `PROVIDER` | Provides transit to the local AS. |
| `CUSTOMER` | Receives transit from the local AS. |
| `PEER` | Exchanges its own routes and its customers' routes with the local AS. |
| `ROUTE_SERVER` | Is an Internet exchange route server. |
| `ROUTE_SERVER_CLIENT` | Is a client of a route server operated by the local AS. |
Roles apply only to EBGP neighbors. Ferrix rejects a configuration that
assigns a role to an IBGP neighbor.
## Configuring BGP Roles
The following example configures a transit provider and requires the
provider to confirm its role:
```textproto
peers {
name: "transit-a"
endpoint { address: "203.0.113.1" }
remote_asn: 64496
role: PROVIDER
require_role: true
}
```
> [!TIP]
> Configure a role for every EBGP neighbor. The role enables route leak
> detection and ASPA verification, and a role mismatch is detected when the
> session is established.
## Role Negotiation
Ferrix advertises its own role in the BGP Role capability of the OPEN
message. For example, if the neighbor's role is `CUSTOMER`, Ferrix
advertises the role `PROVIDER`.
If the neighbor advertises a role that does not correspond to the
configured role, Ferrix rejects the session with the NOTIFICATION message
OPEN Message Error / Role Mismatch.
If the neighbor does not advertise a role, for example because it does not
implement RFC 9234, Ferrix accepts the session and uses the configured
role. To reject such neighbors, configure `require_role: true`.
The status page displays the role of each neighbor, and indicates whether
the neighbor confirmed it.
## Only to Customer Attribute
The Only to Customer (OTC) attribute marks a route that can be advertised
only to customers. The attribute contains the AS number that added it.
Ferrix adds the OTC attribute in the following cases:
- On import, to routes received from a provider, a peer, or a route server.
- On export, to routes advertised to a customer, a peer, or a route server
client.
The attribute is retained when the route is advertised further.
Ferrix rejects a route with the OTC attribute in the following cases, with
the rejection reason `route leak (Only to Customer)`:
- The route is received from a customer or a route server client.
- The route is received from a peer, and the OTC attribute contains an AS
number other than the peer's.
These checks cannot be disabled, as required by RFC 9234. They detect a
route leak even if no AS on the path has published an ASPA, provided that
the neighbors of the leaking AS add the OTC attribute.
## Roles and Route Advertisement
If `learned_routes` is enabled, a route with the OTC attribute is
advertised only to customers. Because Ferrix adds the attribute to routes
from providers, peers, and route servers, the local router provides transit
only to its customers when all neighbors have a role.
A neighbor without a role is not subject to this restriction and receives
all routes. Ferrix logs a warning at startup for each such neighbor that
has `learned_routes` enabled. See
[Route Advertisement](@/docs/guides/passing-routes-on.md#roles-and-route-advertisement).
## Roles and ASPA Verification
ASPA verification depends on the direction from which a route is received.
Ferrix determines the direction from the neighbor's role, and does not
verify the AS paths of routes from a neighbor without a role. See
[AS Path Verification](@/docs/guides/rpki.md#as-path-verification-with-aspa).
+167
View File
@@ -0,0 +1,167 @@
+++
title = "RPKI Origin Validation and ASPA"
description = "Describes route origin validation (RFC 6811) and AS path verification with ASPA, using data received from RPKI caches over the RPKI-to-Router protocol."
weight = 4
[extra]
nav = "RPKI and ASPA"
+++
The Resource Public Key Infrastructure (RPKI) allows the holders of IP
address space and AS numbers to publish signed authorizations. Ferrix uses
two kinds of authorization:
- A Route Origin Authorization (ROA) specifies which AS can originate
routes for a prefix.
- An Autonomous System Provider Authorization (ASPA) specifies the transit
providers of an AS.
RPKI validators, such as Routinator, rpki-client with StayRTR, and FORT,
retrieve and verify these objects. A router receives the result from the
validator's cache over the RPKI-to-Router (RTR) protocol (RFC 8210). Ferrix
includes an RTR client.
## Configuring RPKI Caches
The `rpki` statement specifies one or more RPKI caches:
```textproto
rpki {
caches { address: "127.0.0.1" port: 3323 }
caches { address: "192.0.2.53" port: 3323 }
}
```
Ferrix connects to one cache at a time, in the order in which they are
configured, and connects to the next cache if the current one fails. The
port is required.
Ferrix requests RTR version 2 (draft-ietf-sidrops-8210bis), which includes
ASPA data. If the cache supports only version 1 or 0, Ferrix uses that
version. The status page displays the version in use, and displays a
warning if the version does not include ASPA data.
### Timers
By default, Ferrix uses the refresh, retry, and expire intervals that the
cache provides. You can configure them as follows:
```textproto
rpki {
caches { address: "127.0.0.1" port: 3323 }
refresh_seconds: 3600
retry_seconds: 600
expire_seconds: 7200
}
```
| Statement | Description | Default |
|---|---|---|
| `refresh_seconds` | Interval at which Ferrix requests updates from the cache | Cache value, or 3600 seconds |
| `retry_seconds` | Interval after which Ferrix retries a failed request | Cache value, or 600 seconds |
| `expire_seconds` | Time after which Ferrix discards data that no cache has confirmed | Cache value, or 7200 seconds |
When the data expires, Ferrix discards it, and no route is considered
invalid until new data is received.
### Running a Validator
The following command starts Routinator with an RTR server on port 3323:
```bash
routinator server --rtr 127.0.0.1:3323
```
> [!CAUTION]
> The RTR connection uses plain TCP, without authentication or integrity
> protection. Run the validator on the same host or on a trusted network.
> An attacker who can modify RTR data can cause valid routes to be
> rejected, or can disable ASPA verification by negotiating an older
> protocol version.
## Route Origin Validation
Route origin validation (ROV) compares the origin AS and prefix length of a
route with the Validated ROA Payloads (VRPs) received from the cache (RFC
6811). Ferrix validates every route from an EBGP neighbor. The result is one
of the following states:
- **Invalid**: a VRP covers the prefix, but no VRP authorizes the origin
AS for the prefix length. Ferrix rejects the route with the reason
`RPKI invalid`, unless the neighbor's import policy includes
`accept_rpki_invalid: true`.
- **Valid**: a VRP authorizes the origin AS for the prefix length. Ferrix
accepts the route.
- **Not found**: no VRP covers the prefix. Ferrix accepts the route.
The validation state does not otherwise affect best path selection.
## AS Path Verification with ASPA
Route origin validation verifies only the origin AS. It does not detect a
route leak, or a forged AS path that ends with the correct origin AS. ASPA
verification (draft-ietf-sidrops-aspa-verification, version 24) checks that
each AS in the path is permitted to advertise the route to the next AS,
according to the ASPAs of the ASs in the path.
ASPA verification requires the following:
- An RPKI cache that supports RTR version 2.
- A `role` for the neighbor, because the permitted shape of an AS path
depends on the direction from which the route is received. Table 1
lists the verification that applies to each role.
| Role | Verification |
|---|---|
| `CUSTOMER` | Upstream: the path must consist of customer-to-provider hops only. |
| `PEER` | Upstream. |
| `ROUTE_SERVER` | Upstream. The path need not begin with the route server's AS number. |
| `ROUTE_SERVER_CLIENT` | Upstream. |
| `PROVIDER` | Downstream: the path can contain upstream hops, one peering hop, and downstream hops. |
Ferrix does not verify the AS paths of routes from a neighbor without a
role; it performs only route origin validation for them.
The result is one of the following states:
- **Invalid**: Ferrix rejects the route with the reason `ASPA invalid`,
unless the neighbor's import policy includes `accept_aspa_invalid: true`.
An AS path that contains an AS_SET is invalid. An AS path that does not
begin with the neighbor's AS number is invalid, except for routes from a
route server.
- **Valid**: Ferrix accepts the route.
- **Unknown**: Ferrix accepts the route. Most AS paths are unknown while
ASPA deployment is limited.
## Revalidation
RPKI data changes continuously. Ferrix revalidates affected routes when the
data changes, without requiring the neighbor to readvertise them:
- A route in the routing table that becomes invalid is removed.
- A route that was rejected as invalid is retained as a **held route**. When
the route is no longer invalid, Ferrix accepts it.
The status page displays the number of held routes for each neighbor.
## Verifying RPKI Validation
The status page displays the cache, the RTR version, the age of the data,
and the validation state of each received route. The HTTP API provides the
same information at `/api/rpki`. Table 2 lists the related metrics.
| Metric | Description |
|---|---|
| `bgp_rpki_vrps{family}` | Number of VRPs in use, by address family |
| `bgp_rpki_aspas` | Number of customer ASs with an ASPA in use |
| `bgp_rpki_updates_total` | Number of times the VRPs or ASPAs changed |
| `bgp_routes_rejected_total{reason="RPKI invalid"}` | Routes rejected by route origin validation |
| `bgp_routes_rejected_total{reason="ASPA invalid"}` | Routes rejected by ASPA verification |
## Limitations
- Routes from IBGP neighbors and originated routes are not validated.
- The validation state is not used as a step in best path selection.
- The AS paths of advertised routes are not verified.
- RTR is supported over plain TCP only; SSH, TLS, TCP-MD5, and TCP-AO are
not supported.
+9
View File
@@ -0,0 +1,9 @@
+++
title = "Operations"
description = "Deploying, monitoring, and maintaining Ferrix in production."
weight = 3
sort_by = "weight"
[extra]
number = "03"
+++
+180
View File
@@ -0,0 +1,180 @@
+++
title = "Deploying Ferrix"
description = "Describes how to run Ferrix as a systemd service, secure the hosts and interfaces around it, change its configuration, and restart it."
weight = 1
[extra]
nav = "Deployment"
+++
This topic describes how to deploy Ferrix on a Linux router as a systemd
service, and the security measures that a production deployment requires.
## Running Ferrix as a systemd Service
The provided systemd unit runs the daemon as an unprivileged user, with
only the capabilities that it requires, and restricts its access to the
rest of the system.
To install the service:
1. Create the system user and the configuration directory.
```bash
sudo useradd --system --no-create-home --shell /usr/sbin/nologin ferrix
sudo install -d -m 0750 -o root -g ferrix /etc/ferrix
sudo install -m 0640 -o root -g ferrix server.textpb /etc/ferrix/server.textpb
```
2. Install the unit file as `/etc/systemd/system/ferrix.service`.
{{ file(name="ferrix.service", lang="ini") }}
3. Enable and start the service.
```bash
sudo systemctl daemon-reload
sudo systemctl enable --now ferrix
```
4. Verify that the service is running.
```bash
systemctl status ferrix
journalctl -u ferrix -f
```
The `ExecStartPre` command validates the configuration before the daemon
starts. If the configuration is invalid, the service does not start, and
the journal contains the error.
The sandboxing options in the unit have been tested with kernel route
installation enabled. The command `systemd-analyze security ferrix` rates
the unit's exposure level as 2.1 (OK).
## Securing the Deployment
### HTTP Interface
> [!CAUTION]
> The status page and HTTP API do not require authentication. They expose
> the configuration, all received routes, and the state of every neighbor.
> Bind `http_endpoint` to a loopback address, or place it behind a reverse
> proxy that requires authentication.
### BGP Port Filtering
Ferrix closes connections from addresses that are not configured neighbors
before it reads from them. Ferrix does not support TCP-AO or TCP MD5
authentication. Configure the host firewall to accept connections to TCP
port 179 only from neighbor addresses.
The following nftables rules, in the `input` chain of an `inet filter`
table, drop BGP connections from any other address:
```bash
nft add rule inet filter input tcp dport 179 ip saddr != { 203.0.113.0, 203.0.113.2 } drop
nft add rule inet filter input tcp dport 179 ip6 saddr != { 2001:db8:ffff:1::1 } drop
```
### RPKI Cache Connection
The RTR connection is not authenticated. Run the RPKI validator on the same
host or on a trusted network. See [RPKI](@/docs/guides/rpki.md).
### Metrics Collection
Collect metrics from `/metricz` over the loopback interface, or through
the authenticating reverse proxy.
### Neighbor Policy
Configure `max_prefixes` and `role` for every EBGP neighbor, and prefix and
origin AS lists for every neighbor whose routes are known. See
[Import Policy](@/docs/guides/import-policy.md).
## Logging
The daemon writes its log to standard output. Under systemd, the log is
stored in the journal. When standard output is not a terminal, the log
contains no color codes.
The `--level` option sets the minimum level for the daemon's own messages.
The `RUST_LOG` environment variable sets the level for other components.
Table 1 lists the messages at each level.
| Level | Messages |
|---|---|
| `info` (default) | Session state changes, the first rejected route from each neighbor and at most one per minute thereafter, and kernel route installation state |
| `debug` | Additionally, one message for each UPDATE received and each route advertised |
> [!NOTE]
> At the `debug` level, a full routing table produces millions of log
> messages.
Ferrix rate-limits log messages that a neighbor can trigger, such as
messages about malformed attributes or rejected connections. Each
occurrence is counted in a metric. The log contains at most one message for
each interval, with the number of suppressed occurrences.
## Changing the Configuration
Ferrix reads its configuration at startup only. To change the
configuration:
1. Validate the new configuration.
```bash
sudo bgp --check /etc/ferrix/server.textpb.new
```
2. Replace the active configuration.
```bash
sudo mv /etc/ferrix/server.textpb.new /etc/ferrix/server.textpb
```
3. Restart the service.
```bash
sudo systemctl restart ferrix
```
## Restart Behavior
When the daemon stops in response to SIGTERM, it sends a Cease
NOTIFICATION message with the subcode Administrative Shutdown to every
neighbor, and removes its routes from the kernel. The neighbors withdraw
the routes that they received from Ferrix. Traffic uses alternative paths
until the sessions are reestablished and the routing tables are exchanged
again. For a full routing table, this takes several seconds.
If the daemon terminates unexpectedly, its routes remain in the kernel, and
the router continues to forward traffic. At the next start, Ferrix retains
these routes for `stale_seconds` (default: 120 seconds) while the neighbors
readvertise their routes, and then removes the routes that were not
readvertised.
> [!WARNING]
> A restart interrupts forwarding through the router for the time that the
> sessions take to converge. Restart Ferrix in a maintenance window, or
> after traffic has been drained to another router.
## Running Ferrix in a Container
The container image built from the repository's `Dockerfile` runs
`bgp /etc/ferrix/server.textpb --level info`. The container requires the
host network namespace and the configuration directory. If kernel route
installation is enabled, it also requires the `NET_ADMIN` capability.
```bash
docker run -d --name ferrix --restart unless-stopped \
--network host --cap-add NET_ADMIN \
-v /etc/ferrix:/etc/ferrix:ro \
ferrix
```
> [!NOTE]
> The container image runs the daemon as root. Because the container uses
> the host network namespace, the systemd service is the recommended
> deployment method on a router.
+163
View File
@@ -0,0 +1,163 @@
+++
title = "Monitoring Ferrix"
description = "Describes the status page, route lookup, rejected route records, and the metrics and alerting rules for monitoring Ferrix."
weight = 2
[extra]
nav = "Monitoring"
+++
Ferrix provides a status page, a JSON API, an event stream, and Prometheus
metrics over HTTP. To enable the HTTP interface, configure `http_endpoint`:
```textproto
http_endpoint { address: "127.0.0.1" port: 8080 }
```
Table 1 lists the HTTP endpoints. All endpoints are read-only.
| Path | Description |
|---|---|
| `/statusz` | Status page |
| `/api/...` | JSON API. See [HTTP API Reference](@/docs/reference/http-api.md). |
| `/api/events` | Server-sent events for session and route changes |
| `/metricz` | Prometheus metrics. See [Metrics Reference](@/docs/reference/metrics.md). |
## Status Page
The status page, `/statusz`, displays the following information:
- The configuration that was read at startup.
- Each neighbor, with its session state, address families, role and role
confirmation, number of accepted, rejected, and held routes, and last
error.
- The originated routes, and a lookup of received routes.
- RPKI validation: the cache, the RTR version, and the age of the data.
- Kernel route installation: the number of installed routes, the number of
routes not installed and the reason, the last error reported by the
kernel, and the interfaces.
- Rejected routes, by neighbor and by reason.
- A live event log.
## Route Lookup
The status page provides a lookup of received routes. The HTTP endpoint
`/api/routes/received` accepts the same queries. Table 2 lists the query
parameters.
| Parameter | Result |
|---|---|
| `prefix=193.0.0.0/21` | All routes for the prefix, best path first |
| `address=193.0.0.1` | All prefixes that contain the address, most specific first |
| `within=193.0.0.0/16` | The prefix and all more specific prefixes |
| `peer=NAME` | Only routes from the neighbor, identified by name or address |
| `best=true` | Only the best path for each prefix |
| `limit=N` | At most N routes. Default: 1000. Maximum: 100000. |
The following example displays the best path for the destination
`193.0.0.1`:
```bash
curl -s '127.0.0.1:8080/api/routes/received?address=193.0.0.1&best=true' | jq
```
> [!NOTE]
> The `within`, `peer`, and `best` parameters, when used without `prefix`
> or `address`, scan the entire routing table. With a full routing table,
> such a query takes noticeably longer.
## Rejected Routes
Ferrix does not store rejected routes in the routing table. For each
neighbor, it records the number of rejected routes for each reason, and the
200 most recent rejected routes with their AS paths. The status page
displays this information under "Refused routes", and the HTTP endpoint
`/api/routes/rejected` returns it as JSON. To display the records for one
neighbor, add `?peer=NAME`.
```json
[{"peer":"transit-a","total":3,"by_reason":{"RPKI invalid":2,"bogon prefix":1},
"recent":[{"prefix":"203.0.113.0/24","reason":"RPKI invalid","as_path":[64496,64511]}]}]
```
The records are retained after the session ends. Ferrix logs the first
rejected route from each neighbor, and at most one rejected route per
minute thereafter.
For a description of each rejection reason, see
[Import Policy](@/docs/guides/import-policy.md#default-checks).
## Alerting Rules
The following Prometheus alerting rules cover the conditions that require
operator action: route hijack and route leak attempts, prefix limits,
session failures, loss of RPKI data, and kernel errors.
```yaml
groups:
- name: bgp
rules:
# A neighbor advertises routes that its import policy rejects. The
# cause is a route leak or hijack at the neighbor, or an outdated
# prefix list.
- alert: BgpRoutesRejected
expr: sum by (peer, reason) (increase(bgp_routes_rejected_total[10m])) > 0
labels: { severity: warning }
# A route that contains the local AS number was received: a routing
# loop, or another network using the local AS number.
- alert: BgpOwnAsInPath
expr: increase(bgp_routes_rejected_total{reason="own AS in path"}[10m]) > 0
labels: { severity: page }
# A neighbor advertised a prefix of the local network, or a route
# with a third-party next hop.
- 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 neighbor is approaching its prefix limit.
- alert: BgpPeerNearPrefixLimit
expr: bgp_peer_routes / on (peer) bgp_peer_max_prefixes > 0.8
for: 10m
labels: { severity: warning }
# A session was torn down because the neighbor exceeded its prefix
# limit.
- alert: BgpPrefixLimitExceeded
expr: increase(bgp_prefix_limit_exceeded_total[1h]) > 0
labels: { severity: page }
# A configured neighbor has no established session.
- alert: BgpPeerDown
expr: bgp_peer_up == 0
for: 5m
labels: { severity: page }
# No RPKI data is available. Routes are not validated.
- alert: BgpRpkiNoData
expr: sum(bgp_rpki_vrps) == 0
for: 15m
labels: { severity: page }
# The kernel rejected route installation requests.
- alert: BgpKernelErrors
expr: increase(bgp_kernel_errors_total[10m]) > 0
labels: { severity: warning }
```
## Dashboard Queries
Table 3 lists queries for a monitoring dashboard.
| Panel | Query |
|---|---|
| Established sessions | `sum(bgp_peer_up)`, of `count(bgp_peer_up)` |
| Routes by neighbor | `bgp_peer_routes` |
| Prefixes with a best path | `bgp_rib_prefixes` |
| Rejected routes by reason | `sum by (reason) (rate(bgp_routes_rejected_total[5m]))` |
| UPDATE messages received and sent | `rate(bgp_messages_received_total{type="update"}[5m])`, `rate(bgp_messages_sent_total{type="update"}[5m])` |
| Kernel routes | `bgp_kernel_routes`, `bgp_kernel_routes_unresolved` |
| RPKI data | `bgp_rpki_vrps`, `bgp_rpki_aspas` |
The metric `bgp_build_info{version}` reports the running version.
+9
View File
@@ -0,0 +1,9 @@
+++
title = "Reference"
description = "Configuration statements, command-line options, HTTP API, metrics, and supported standards."
weight = 4
sort_by = "weight"
[extra]
number = "04"
+++
@@ -0,0 +1,80 @@
+++
title = "Command-Line Reference"
description = "Describes the syntax, options, environment variables, and signals of the bgp command."
weight = 2
[extra]
nav = "Command Line"
+++
## Syntax
```
bgp [OPTIONS] <CONFIG>
```
## Description
Starts the Ferrix BGP daemon with the specified configuration file, or
validates the file.
## Options
| Option | Description |
|---|---|
| `<CONFIG>` | Path to the configuration file. See [Configuration Reference](@/docs/reference/configuration.md). |
| `--check` | Validates the configuration file and exits. The daemon does not listen on any port and does not modify any route. |
| `-l`, `--level <LEVEL>` | Specifies the minimum log level for the daemon's messages: `error`, `warn`, `info`, `debug`, or `trace`. Default: `info`. At `debug` and lower, the daemon logs every UPDATE message and every advertised route. |
| `-V`, `--version` | Displays the version and exits. |
| `-h`, `--help` | Displays help and exits. |
## Exit Status
| Status | Meaning |
|---|---|
| 0 | With `--check`: the configuration is valid. Otherwise: the daemon stopped in response to a signal. |
| 1 | The configuration is invalid, or the daemon failed. The error is written to standard error. |
## Environment Variables
| Variable | Description |
|---|---|
| `RUST_LOG` | Specifies the log level for components other than the daemon's own crates, for example `RUST_LOG=axum=debug` for the HTTP server. |
## Signals
| Signal | Action |
|---|---|
| `SIGTERM`, `SIGINT` | Sends a Cease NOTIFICATION message with the subcode Administrative Shutdown to every neighbor, removes the daemon's routes from the kernel, and exits. |
Ferrix does not reload its configuration while it runs. To apply a new
configuration, restart the daemon. See
[Changing the Configuration](@/docs/operations/deployment.md#changing-the-configuration).
## Examples
To validate a configuration file:
```bash
bgp --check /etc/ferrix/server.textpb
```
```
/etc/ferrix/server.textpb is in order: AS64500, peers: 3, routes originated: 2
```
If the file is invalid, the command displays the error and exits with
status 1:
```
Error: Invalid configuration
Caused by:
peer "x": endpoint is invalid: fe80::1 is a link-local address, which needs the interface it is on: fe80::1%eth0, for example
```
To start the daemon with debug logging:
```bash
bgp /etc/ferrix/server.textpb --level debug
```
@@ -0,0 +1,194 @@
+++
title = "Configuration Reference"
description = "Describes every statement of the Ferrix configuration file, with its type, default value, and range."
weight = 1
[extra]
nav = "Configuration"
+++
Ferrix reads its configuration from a single file at startup. The file
contains a `BgpConfig` Protocol Buffers message in text format. The schema
is defined in
[`crates/server/proto/config.proto`](https://git.rayhaan.net/rayhaan/ferrix/src/branch/main/crates/server/proto/config.proto).
A test in the source tree verifies that this topic describes every field of
the schema.
## File Format
The following example shows the elements of the text format:
```textproto
# A comment.
router_id: "192.0.2.1" # string
local_asn: 64500 # number
collision_detect_established_state: true # boolean
peers { # message
name: "a"
role: PEER # enumeration value
address_families: IPV4_UNICAST # repeated field: one line per value
address_families: IPV6_UNICAST
}
peers { name: "b" } # repeated message
```
Statements can appear in any order. An omitted statement has its default
value. Ferrix rejects a file that contains an unknown statement or a value
that cannot be parsed. To validate a file, use `bgp --check`.
## BgpConfig
Top-level statements.
| Statement | Default | Description |
|---|---|---|
| `router_id` <span class="type">string</span> | Required | Specifies the BGP identifier as a dotted-quad IPv4 address other than `0.0.0.0`. Required on IPv6-only routers (RFC 4271). |
| `local_asn` <span class="type">uint32</span> | Required | Specifies the local AS number. Four-octet AS numbers are supported (RFC 6793). Range: 1 through 4,294,967,295. |
| `listen_endpoints` <span class="type">[TcpEndpoint](#tcpendpoint), repeated</span> | `0.0.0.0:179`, `[::]:179` | Specifies the addresses and ports on which Ferrix accepts BGP connections. |
| `peers` <span class="type">[Peer](#peer), repeated</span> | None | Specifies the BGP neighbors. |
| `originate` <span class="type">[OriginatedRoute](#originatedroute), repeated</span> | None | Specifies the routes that Ferrix originates and advertises to every neighbor. |
| `http_endpoint` <span class="type">[TcpEndpoint](#tcpendpoint)</span> | Disabled | Specifies the address and port of the status page, HTTP API, and metrics. The port is required. The interface is not authenticated. |
| `connect_retry_seconds` <span class="type">uint32</span> | 5 | Specifies the initial interval between connection attempts to a neighbor, in seconds. The interval doubles after each failure, up to 120 seconds. |
| `collision_detect_established_state` <span class="type">bool</span> | `false` | Enables connection collision detection for sessions in the Established state (RFC 4271 Section 8.1.1). When disabled, Ferrix closes a new connection from a neighbor that has an established session. |
| `accept_unconfigured_peers` <span class="type">bool</span> | `false` | Accepts sessions from any address, with any AS number. Use only for testing on an isolated network. |
| `rpki` <span class="type">[Rpki](#rpki)</span> | Disabled | Enables RPKI route origin validation and ASPA verification. |
| `kernel` <span class="type">[Kernel](#kernel)</span> | Disabled | Enables kernel route installation (Linux only). |
| `export` <span class="type">[ExportPolicy](#exportpolicy)</span> | Originated routes only | Specifies the export policy for neighbors that have no `export` statement. |
| `control_communities` <span class="type">bool</span> | `false` | Enables control communities, which restrict the neighbors to which a route is advertised. See [Control Communities](@/docs/guides/passing-routes-on.md#control-communities). |
## Peer
Statements for a BGP neighbor.
| Statement | Default | Description |
|---|---|---|
| `name` <span class="type">string</span> | The neighbor's address | Specifies the name of the neighbor in logs, metrics, and the status page. Must be unique. |
| `endpoint` <span class="type">[TcpEndpoint](#tcpendpoint)</span> | Required | Specifies the address and port of the neighbor. For a link-local address, include the interface: `fe80::1%eth0`. Must be unique. |
| `remote_asn` <span class="type">uint32</span> | Required | Specifies the AS number of the neighbor. If it equals `local_asn`, the neighbor is an IBGP neighbor. Range: 1 through 4,294,967,295. |
| `hold_time_seconds` <span class="type">uint32, optional</span> | 90 | Specifies the hold time proposed in the OPEN message, in seconds. Range: 0, or 3 through 65535. A value of 0 disables keepalive messages and the hold timer. |
| `local_address` <span class="type">string</span> | Selected by the operating system | Specifies the source address for connections to the neighbor, and the next hop of routes advertised to it. Must be of the same address family as `endpoint`. |
| `address_families` <span class="type">[AddressFamily](#addressfamily), repeated</span> | All | Specifies the address families proposed to the neighbor. A family is enabled only if the neighbor also proposes it (RFC 4760). |
| `add_path` <span class="type">[AddPath](#addpath)</span> | `ADD_PATH_OFF` | Specifies the ADD-PATH directions proposed to the neighbor (RFC 7911). |
| `import` <span class="type">[ImportPolicy](#importpolicy)</span> | Default checks | Specifies the import policy for routes received from the neighbor. |
| `role` <span class="type">[Role](#role)</span> | None | Specifies the BGP role of the neighbor (RFC 9234). Not permitted for IBGP neighbors. |
| `require_role` <span class="type">bool</span> | `false` | Rejects the session if the neighbor does not advertise a BGP role. Requires `role`. |
| `export` <span class="type">[ExportPolicy](#exportpolicy)</span> | Top-level `export` | Specifies the export policy for the neighbor. Replaces the top-level `export` statement. |
## TcpEndpoint
An IP address and TCP port.
| Statement | Default | Description |
|---|---|---|
| `address` <span class="type">string</span> | Required | Specifies an IPv4 or IPv6 address. A neighbor's link-local IPv6 address requires the interface: `fe80::1%eth0`. |
| `port` <span class="type">uint32</span> | 179 | Specifies the TCP port. Required for `http_endpoint` and RPKI caches. |
## ImportPolicy
Statements that determine which routes received from a neighbor are
accepted. See [Import Policy](@/docs/guides/import-policy.md).
| Statement | Default | Description |
|---|---|---|
| `max_prefixes` <span class="type">uint32</span> | 0 (no limit) | Specifies the maximum number of routes that the neighbor can have in the routing table. If the limit is exceeded, Ferrix tears down the session and places the neighbor in hold-down. |
| `hold_down_seconds` <span class="type">uint32</span> | 600 | Specifies the hold-down time, in seconds, after the neighbor exceeds `max_prefixes`. |
| `accept_bogons` <span class="type">bool</span> | `false` | Accepts bogon prefixes, and AS paths that contain private, reserved, or documentation AS numbers. |
| `accept_default_route` <span class="type">bool</span> | `false` | Accepts the default routes `0.0.0.0/0` and `::/0`. |
| `max_ipv4_prefix_length` <span class="type">uint32</span> | 24 | Specifies the longest IPv4 prefix accepted. |
| `max_ipv6_prefix_length` <span class="type">uint32</span> | 48 | Specifies the longest IPv6 prefix accepted. |
| `accept_any_first_as` <span class="type">bool</span> | `false` | Accepts AS paths that do not begin with the neighbor's AS number. Required for Internet exchange route servers. |
| `max_as_path_length` <span class="type">uint32</span> | 64 | Specifies the longest AS path accepted, in AS numbers. |
| `prefixes` <span class="type">[PrefixRange](#prefixrange), repeated</span> | All prefixes | Specifies the prefixes that the neighbor can advertise. |
| `origin_asns` <span class="type">uint32, repeated</span> | All AS numbers | Specifies the AS numbers from which the neighbor's routes can originate. |
| `accept_rpki_invalid` <span class="type">bool</span> | `false` | Accepts routes whose origin is RPKI invalid. |
| `accept_aspa_invalid` <span class="type">bool</span> | `false` | Accepts routes whose AS path is ASPA invalid. |
| `accept_as_sets` <span class="type">bool</span> | `false` | Accepts AS paths that contain an AS_SET (RFC 9774). |
| `accept_third_party_next_hop` <span class="type">bool</span> | `false` | Accepts next hops other than the neighbor's address. Not required for a neighbor with the role `ROUTE_SERVER`. |
| `add_communities` <span class="type">string, repeated</span> | None | Specifies standard communities added to accepted routes, for example `64500:100` or `NO_EXPORT`. |
| `add_large_communities` <span class="type">string, repeated</span> | None | Specifies large communities added to accepted routes, for example `64500:2:1`. |
| `remove_communities` <span class="type">string, repeated</span> | None | Specifies patterns of communities removed from accepted routes, for example `64500:100`, `64500:*`, or `64500:2:*`. Applied before communities are added. |
| `local_pref` <span class="type">uint32, optional</span> | 100 | Specifies the local preference of accepted routes. |
## PrefixRange
A prefix and, optionally, the more specific prefixes within it.
| Statement | Default | Description |
|---|---|---|
| `prefix` <span class="type">string</span> | Required | Specifies a prefix in CIDR notation, for example `193.0.0.0/21`. |
| `max_length` <span class="type">uint32</span> | Length of `prefix` | Specifies the longest more specific prefix that is also permitted. |
## ExportPolicy
Statements that determine which routes are advertised to a neighbor. See
[Route Advertisement](@/docs/guides/passing-routes-on.md).
| Statement | Default | Description |
|---|---|---|
| `learned_routes` <span class="type">bool</span> | `false` | Enables the advertisement of learned routes, subject to BGP roles and communities. |
| `add_communities` <span class="type">string, repeated</span> | None | Specifies standard communities added to advertised routes. |
| `add_large_communities` <span class="type">string, repeated</span> | None | Specifies large communities added to advertised routes. |
| `remove_communities` <span class="type">string, repeated</span> | None | Specifies patterns of communities removed from advertised routes. Applied before communities are added. |
| `as_path_prepend` <span class="type">uint32</span> | 0 | Specifies the number of additional times the local AS number is prepended to the AS path. Range: 0 through 16. |
## OriginatedRoute
A route that Ferrix originates.
| Statement | Default | Description |
|---|---|---|
| `prefix` <span class="type">string</span> | Required | Specifies the prefix in CIDR notation, for example `198.51.100.0/24` or `2001:db8:100::/40`. |
| `origin` <span class="type">string</span> | `IGP` | Specifies the ORIGIN attribute: `IGP`, `EGP`, or `INCOMPLETE`. |
| `as_path_prepend` <span class="type">uint32</span> | 0 | Specifies the number of additional times the local AS number is prepended to the AS path. Range: 0 through 16. |
| `med` <span class="type">uint32, optional</span> | None | Specifies the MULTI_EXIT_DISC attribute. If not set, the attribute is not sent. |
| `communities` <span class="type">string, repeated</span> | None | Specifies standard communities (RFC 1997), for example `64500:100` or `NO_EXPORT`. |
| `large_communities` <span class="type">string, repeated</span> | None | Specifies large communities (RFC 8092), for example `64500:2:1`. |
## Rpki
Statements for RPKI validation. See [RPKI Origin Validation and ASPA](@/docs/guides/rpki.md).
| Statement | Default | Description |
|---|---|---|
| `caches` <span class="type">[TcpEndpoint](#tcpendpoint), repeated</span> | Required | Specifies the RTR servers of RPKI caches, in order of preference. The port is required. |
| `refresh_seconds` <span class="type">uint32</span> | Cache value, or 3600 | Specifies the interval at which Ferrix requests updates, in seconds. |
| `retry_seconds` <span class="type">uint32</span> | Cache value, or 600 | Specifies the interval after which Ferrix retries a failed request, in seconds. |
| `expire_seconds` <span class="type">uint32</span> | Cache value, or 7200 | Specifies the time after which Ferrix discards data that no cache has confirmed, in seconds. |
## Kernel
Statements for kernel route installation. See
[Kernel Route Installation](@/docs/guides/kernel.md).
| Statement | Default | Description |
|---|---|---|
| `table` <span class="type">uint32</span> | 254 (`main`) | Specifies the kernel routing table. |
| `metric` <span class="type">uint32, optional</span> | 20 | Specifies the metric of installed routes. |
| `without_nexthop_objects` <span class="type">bool</span> | `false` | Installs each route with its own gateway instead of a nexthop object. |
| `stale_seconds` <span class="type">uint32, optional</span> | 120 | Specifies the time, in seconds, for which routes installed by a previous run are retained at startup. A value of 0 removes them immediately. |
## Role
| Value | Description |
|---|---|
| `PROVIDER` | The neighbor provides transit to the local AS. |
| `CUSTOMER` | The neighbor receives transit from the local AS. |
| `PEER` | The neighbor exchanges its own and its customers' routes with the local AS. |
| `ROUTE_SERVER` | The neighbor is an Internet exchange route server. |
| `ROUTE_SERVER_CLIENT` | The neighbor is a client of a route server operated by the local AS. |
## AddressFamily
| Value | Description |
|---|---|
| `IPV4_UNICAST` | IPv4 unicast (AFI 1, SAFI 1) |
| `IPV6_UNICAST` | IPv6 unicast (AFI 2, SAFI 1) |
## AddPath
| Value | Description |
|---|---|
| `ADD_PATH_OFF` | ADD-PATH is disabled. |
| `ADD_PATH_RECEIVE` | Ferrix accepts multiple paths for each prefix from the neighbor. |
| `ADD_PATH_SEND` | Ferrix proposes to send multiple paths. Ferrix currently sends one path for each prefix. |
| `ADD_PATH_BOTH` | Both `ADD_PATH_RECEIVE` and `ADD_PATH_SEND`. |
+204
View File
@@ -0,0 +1,204 @@
+++
title = "HTTP API Reference"
description = "Describes the endpoints of the Ferrix HTTP interface: the JSON API, the event stream, the status page, and the metrics endpoint."
weight = 3
[extra]
nav = "HTTP API"
+++
Ferrix serves its HTTP interface on the address configured with
`http_endpoint`. All endpoints are read-only and use the `GET` method. If a
query parameter is invalid, the server returns status 400 (Bad Request)
with a description of the error.
> [!CAUTION]
> The HTTP interface does not require authentication. Bind it to a loopback
> address, or place it behind a reverse proxy that requires authentication.
## Endpoints
| Endpoint | Description |
|---|---|
| `/` | Redirects to `/statusz`. |
| `/statusz` | Status page. |
| `/api/status` | Router ID, AS number, uptime, and table sizes. |
| `/api/config` | The configuration that was read at startup. |
| `/api/peers` | Configured neighbors and their sessions. |
| `/api/routes/local` | Originated routes. |
| `/api/routes/received` | Received routes. |
| `/api/routes/rejected` | Rejected routes, by neighbor. |
| `/api/rpki` | RPKI cache, data, and data age. |
| `/api/kernel` | Kernel route installation state and interfaces. |
| `/api/events` | Session and route changes, as server-sent events. |
| `/metricz` | Prometheus metrics. See [Metrics Reference](@/docs/reference/metrics.md). |
## Peers
`/api/peers` returns an array with one object for each configured
neighbor:
```json
{
"name": "test-peer",
"peer_addr": "127.0.0.1:64815",
"peer_asn": 65001,
"state": "Established",
"direction": "inbound",
"local_addr": "127.0.0.1:1790",
"uptime_secs": 2,
"four_octet_asn": true,
"extended_next_hop": false,
"address_families": ["IPv4 unicast", "IPv6 unicast"],
"add_path_receive": [],
"add_path_send": [],
"routes": 1,
"rejected_routes": 1,
"held_routes": 0,
"role": null,
"role_confirmed": null,
"last_error": null,
"retry_at_unix": null
}
```
| Field | Description |
|---|---|
| `state` | Session state: `Established`, `Connecting`, `Backoff`, or `Idle`. |
| `direction` | `inbound` if the neighbor initiated the connection, `outbound` otherwise. |
| `routes` | Number of the neighbor's routes in the routing table. |
| `rejected_routes` | Number of the neighbor's routes rejected by its import policy. |
| `held_routes` | Number of held routes: routes rejected for a reason that can change without a new UPDATE, such as RPKI validity or next hop resolution. |
| `role_confirmed` | Whether the neighbor advertised a BGP role that matches the configured role. |
| `retry_at_unix` | For a neighbor without a session, the time of the next connection attempt. |
## Received Routes
`/api/routes/received` returns received routes, one page at a time. Table 1
lists the query parameters.
| Parameter | Description |
|---|---|
| `prefix` | Returns all routes for the prefix, best path first. Example: `prefix=193.0.0.0/21`. |
| `address` | Returns all routes for the prefixes that contain the address, most specific first. Example: `address=193.0.0.1`. |
| `within` | Returns the routes for the prefix and all more specific prefixes. Example: `within=193.0.0.0/16`. |
| `peer` | Returns only routes from the neighbor, identified by name or address. |
| `best` | If `true`, returns only the best path for each prefix. |
| `limit` | Specifies the approximate number of routes in a page. Default: 1000. Maximum: 100000. |
| `after` | Returns the page after the specified prefix. Use the `next_after` value of the previous page. |
| `before` | Returns the page before the specified prefix. Use the `prev_before` value of the next page. |
A page contains only complete prefixes. It ends before the prefix whose
routes would exceed `limit`, but always contains at least one prefix.
Prefixes are sorted by address, with IPv4 prefixes first.
```json
{
"routes": [{
"peer_name": "test-peer",
"peer_addr": "127.0.0.1:64815",
"peer_asn": 65001,
"prefix": "203.0.113.0/24",
"path_id": 0,
"best": true,
"rpki": null,
"aspa": null,
"summary": {
"origin": "IGP",
"as_path": [65001],
"next_hop": "10.0.0.2",
"med": null,
"local_pref": null,
"communities": ["65001:1"],
"large_communities": []
},
"attributes": [ ... ]
}],
"next_after": null,
"prev_before": null
}
```
| Field | Description |
|---|---|
| `path_id` | ADD-PATH path identifier, or 0. |
| `best` | Whether the route is the best path for the prefix. |
| `rpki`, `aspa` | Route origin validation and ASPA verification state, if RPKI validation is configured. |
| `attributes` | All path attributes, as received. |
## Rejected Routes
`/api/routes/rejected` returns, for each neighbor with rejected routes, the
number of rejected routes for each reason and the 200 most recent rejected
routes. To return the records for one neighbor, add `?peer=NAME`.
```json
[{
"peer": "test-peer",
"peer_addr": "127.0.0.1",
"total": 1,
"by_reason": { "own AS in path": 1 },
"recent": [{
"prefix": "198.51.100.0/24",
"path_id": 0,
"reason": "own AS in path",
"as_path": [65001, 64500],
"at_unix": 1791020702
}]
}]
```
## Kernel
`/api/kernel` returns the state of kernel route installation:
```json
{
"enabled": true,
"table": 254,
"metric": 20,
"synced": true,
"installed": 0,
"kept": 0,
"unresolved": {},
"refused": 0,
"errors": 0,
"last_error": null,
"interfaces": [
{ "name": "eth0", "index": 2, "up": true,
"addresses": ["192.168.5.15/24", "fe80::5055:55ff:fed4:2526/64"] }
]
}
```
| Field | Description |
|---|---|
| `synced` | Whether the kernel routing table has been synchronized with the routing table since the daemon started. |
| `installed` | Number of routes installed. |
| `kept` | Number of routes from a previous run that are retained during startup. |
| `unresolved` | Number of prefixes whose active route is not installed, by reason. |
| `refused` | Number of prefixes whose active route the kernel rejected. |
| `errors` | Number of requests that the kernel rejected. |
## Events
`/api/events` is a stream of
[server-sent events](https://html.spec.whatwg.org/multipage/server-sent-events.html).
Each event has the name `bgp` and contains a JSON object. The `type` field
identifies the event, as listed in Table 2.
| `type` | Description |
|---|---|
| `session_up` | A session was established. The `peer` field has the format of `/api/peers`. |
| `session_down` | A session ended. Fields: `peer_addr`, `peer_asn`. |
| `route_refresh_requested` | A neighbor sent a ROUTE-REFRESH message. |
| `local_announced`, `local_withdrawn` | An originated route was advertised or withdrawn. |
| `best_changed` | The best path for a prefix changed. The `route` field contains the new best path. |
| `best_withdrawn` | A prefix no longer has a route. |
| `lagged` | The client did not read events quickly enough and missed events on `stream`. Reload the corresponding table. |
The following command displays the event stream:
```bash
curl -sN 127.0.0.1:8080/api/events
```
+71
View File
@@ -0,0 +1,71 @@
+++
title = "Metrics Reference"
description = "Describes the Prometheus metrics that Ferrix exports at /metricz, with their types and labels."
weight = 4
[extra]
nav = "Metrics"
+++
Ferrix exports Prometheus metrics at `/metricz` on the HTTP interface. For
alerting rules based on these metrics, see
[Alerting Rules](@/docs/operations/monitoring.md#alerting-rules).
## Session Metrics
| Metric | Type | Description |
|---|---|---|
| `bgp_peer_up{peer}` | Gauge | 1 if the session with the configured neighbor is established; 0 otherwise. |
| `bgp_sessions_established` | Gauge | Number of sessions in the Established state. |
| `bgp_sessions_established_total` | Counter | Number of sessions that reached the Established state. |
| `bgp_messages_sent_total{type}` | Counter | Number of BGP messages sent, by type: `open`, `update`, `notification`, `keepalive`, `route_refresh`. |
| `bgp_messages_received_total{type}` | Counter | Number of BGP messages received, by type. |
| `bgp_update_errors_total{handling}` | Counter | Number of errors in received UPDATE messages that did not reset the session (RFC 7606), by handling: `treat-as-withdraw`, `attribute-discard`. |
| `bgp_connections_refused_total{reason}` | Counter | Number of inbound connections closed without a session, by reason: `not a configured peer`, `too many connections from the address`, `peer is held down`. |
| `bgp_connection_collisions_total{closed}` | Counter | Number of connection collisions resolved, by the connection that was closed: `new`, `existing`. |
## Route Metrics
| Metric | Type | Description |
|---|---|---|
| `bgp_peer_routes{peer}` | Gauge | Number of the neighbor's routes in the routing table. |
| `bgp_peer_max_prefixes{peer}` | Gauge | Prefix limit of the neighbor (`import.max_prefixes`), if configured. |
| `bgp_prefix_limit_exceeded_total{peer}` | Counter | Number of sessions torn down because the neighbor exceeded its prefix limit. |
| `bgp_routes_rejected_total{peer,reason}` | Counter | Number of routes rejected by the import policy, by neighbor and reason. For the reasons, see [Import Policy](@/docs/guides/import-policy.md#default-checks). |
| `bgp_routes_exported_total{change}` | Counter | Number of learned routes advertised (`announced`) to or withdrawn (`withdrawn`) from neighbors. |
| `bgp_local_routes` | Gauge | Number of originated routes. |
## Routing Table Metrics
| Metric | Type | Description |
|---|---|---|
| `bgp_rib_routes` | Gauge | Number of received routes, counted separately for each neighbor. |
| `bgp_rib_prefixes` | Gauge | Number of prefixes with at least one route. |
| `bgp_rib_attribute_sets` | Gauge | Number of distinct sets of path attributes stored. |
| `bgp_rib_journal_entries` | Gauge | Number of best path changes not yet processed by all consumers. |
## RPKI Metrics
| Metric | Type | Description |
|---|---|---|
| `bgp_rpki_vrps{family}` | Gauge | Number of Validated ROA Payloads in use, by address family: `ipv4`, `ipv6`. |
| `bgp_rpki_aspas` | Gauge | Number of customer ASs with an ASPA in use. |
| `bgp_rpki_updates_total` | Counter | Number of times the VRPs or ASPAs changed. |
## Kernel Metrics
These metrics are exported if the `kernel` statement is configured.
| Metric | Type | Description |
|---|---|---|
| `bgp_kernel_routes` | Gauge | Number of routes installed by Ferrix in the kernel. |
| `bgp_kernel_routes_unresolved` | Gauge | Number of prefixes whose active route is not installed because its next hop is unresolvable. |
| `bgp_kernel_routes_refused` | Gauge | Number of prefixes whose active route the kernel rejected. |
| `bgp_kernel_route_changes_total{change}` | Counter | Number of changes requested from the kernel, by kind: `install`, `remove`. |
| `bgp_kernel_errors_total` | Counter | Number of requests that the kernel rejected. |
## Build Metrics
| Metric | Type | Description |
|---|---|---|
| `bgp_build_info{version}` | Gauge | Always 1. The `version` label contains the running version. |
@@ -1,19 +1,25 @@
# RFCs +++
title = "Supported Standards"
description = "Lists the RFCs and Internet-Drafts that Ferrix supports, and the implementation status of each feature."
weight = 5
Tracks which RFCs are implemented, partially implemented, or only represented [extra]
in the wire format layer. nav = "Supported Standards"
+++
**Legend** This topic lists the RFCs and Internet-Drafts that Ferrix supports, and the
- ✅ Implemented — functional end-to-end implementation status of each feature. Table 1 describes the status values.
- 🟡 Wire format only — parsed and serialised correctly, but not yet acted on
- ❌ Not implemented
--- | Status | Meaning |
|---|---|
| ✅ | Supported. |
| 🟡 | Encoded and decoded only. Ferrix parses and generates the element but does not act on it. |
| ❌ | Not supported. |
## Core BGP ## Core BGP
### RFC 4271 — A Border Gateway Protocol 4 (BGP-4) ### RFC 4271 — A Border Gateway Protocol 4 (BGP-4)
https://datatracker.ietf.org/doc/html/rfc4271 <https://datatracker.ietf.org/doc/html/rfc4271>
The base protocol specification. The base protocol specification.
@@ -30,25 +36,24 @@ The base protocol specification.
| §6.7 / RFC 4486 | Cease / Administrative Shutdown sent to every peer on SIGINT or SIGTERM | ✅ | | §6.7 / RFC 4486 | Cease / Administrative Shutdown sent to every peer on SIGINT or SIGTERM | ✅ |
| §6.8 | Connection collision detection (BGP Identifier comparison, Cease / Connection Collision Resolution) | ✅ | | §6.8 | Connection collision detection (BGP Identifier comparison, Cease / Connection Collision Resolution) | ✅ |
| §8.1.1 | Optional `CollisionDetectEstablishedState` (`collision_detect_established_state`) | ✅ | | §8.1.1 | Optional `CollisionDetectEstablishedState` (`collision_detect_established_state`) | ✅ |
| §6.2 | Bad BGP Identifier check (peer using our identifier, or 0.0.0.0) | ✅ | | §6.2 | Bad BGP Identifier check (neighbor uses the local identifier, or 0.0.0.0) | ✅ |
| §9 | Decision process / best-path selection | ✅ | | §9 | Decision process / best-path selection | ✅ |
| §9.1.2 | Best routes installed for forwarding (Linux kernel, over netlink), where the next hop is directly connected | ✅ | | §9.1.2 | Best routes installed for forwarding (Linux kernel, over netlink), where the next hop is directly connected | ✅ |
| §9.1.2.1 | Routes with an unresolvable next hop excluded from selection | ✅ where routes are installed in the kernel: such a route is held out of the table until its next hop can be reached | | §9.1.2.1 | Routes with an unresolvable next hop excluded from selection | ✅ where routes are installed in the kernel: such a route is held out of the table until its next hop can be reached |
| §9.1.2.1 | Next hops resolved through other routes | ❌ (only next hops that are neighbors count as reachable) | | §9.1.2.1 | Next hops resolved through other routes | ❌ (only next hops that are neighbors count as reachable) |
| §9 | Adj-RIB-In (per-peer table of received routes) | ✅ | | §9 | Adj-RIB-In (per-peer table of received routes) | ✅ |
| §9 | Locally originated routes, advertised to all established peers | ✅ | | §9 | Locally originated routes, advertised to all established peers | ✅ |
| §9 | Loc-RIB / Adj-RIB-Out: the best route of each prefix passed on to peers (`export.learned_routes`), with our AS on the path and ourselves as next hop; MED and LOCAL_PREF not sent to other networks; LOCAL_PREF to our own (§5.1.4, §5.1.5, §9.1.3) | ✅ | | §9 | Advertisement of the best path for each prefix to neighbors (`export.learned_routes`), with the local AS prepended and the local address as next hop; MED and LOCAL_PREF not sent to EBGP neighbors; LOCAL_PREF sent to IBGP neighbors (§5.1.4, §5.1.5, §9.1.3) | ✅ |
| §9.2.1 | No route learned from one internal peer passed to another (no route reflection) | ✅ | | §9.2.1 | Routes learned from an IBGP neighbor not advertised to other IBGP neighbors (no route reflection) | ✅ |
| §9.1.3 | A route not sent to a peer whose AS is on its path | ✅ | | §9.1.3 | Routes not advertised to a neighbor whose AS is in the AS path | ✅ |
| §5 | Unknown optional transitive attributes passed on with the Partial bit set; non-transitive ones dropped | ✅ | | §5 | Unknown optional transitive attributes advertised with the Partial bit set; unknown non-transitive attributes not advertised | ✅ |
| §9.2 | Hold timer reset on any received message (not only KEEPALIVE) | ✅ | | §9.2 | Hold timer reset on any received message (not only KEEPALIVE) | ✅ |
---
## Capability and Session Extensions ## Capability and Session Extensions
### RFC 6793 — BGP Support for Four-Octet Autonomous System (AS) Number Space ### RFC 6793 — BGP Support for Four-Octet Autonomous System (AS) Number Space
https://datatracker.ietf.org/doc/html/rfc6793 <https://datatracker.ietf.org/doc/html/rfc6793>
| Feature | Status | | Feature | Status |
|---------|--------| |---------|--------|
@@ -59,15 +64,15 @@ https://datatracker.ietf.org/doc/html/rfc6793
| AS4_PATH from a two-octet peer merged into AS_PATH on receipt (§4.2.3); one longer than AS_PATH ignored | ✅ | | AS4_PATH from a two-octet peer merged into AS_PATH on receipt (§4.2.3); one longer than AS_PATH ignored | ✅ |
### RFC 9774 — Deprecation of AS_SET and AS_CONFED_SET in BGP ### RFC 9774 — Deprecation of AS_SET and AS_CONFED_SET in BGP
https://datatracker.ietf.org/doc/html/rfc9774 <https://datatracker.ietf.org/doc/html/rfc9774>
| Feature | Status | | Feature | Status |
|---------|--------| |---------|--------|
| Routes with an AS_SET in their path refused on import (`accept_as_sets` takes them) | ✅ | | Routes with an AS_SET in the AS path rejected on import (configurable with `accept_as_sets`) | ✅ |
| AS_SET never sent | ✅ | | AS_SET never sent | ✅ |
### RFC 9072 — Extended Optional Parameters Length for BGP OPEN Message ### RFC 9072 — Extended Optional Parameters Length for BGP OPEN Message
https://datatracker.ietf.org/doc/html/rfc9072 <https://datatracker.ietf.org/doc/html/rfc9072>
| Feature | Status | | Feature | Status |
|---------|--------| |---------|--------|
@@ -75,24 +80,24 @@ https://datatracker.ietf.org/doc/html/rfc9072
| Serialising extended-length OPEN when options exceed 255 bytes | ✅ | | Serialising extended-length OPEN when options exceed 255 bytes | ✅ |
### RFC 2918 — Route Refresh Capability for BGP-4 ### RFC 2918 — Route Refresh Capability for BGP-4
https://datatracker.ietf.org/doc/html/rfc2918 <https://datatracker.ietf.org/doc/html/rfc2918>
| Feature | Status | | Feature | Status |
|---------|--------| |---------|--------|
| `RouteRefresh` capability advertisement | ✅ | | `RouteRefresh` capability advertisement | ✅ |
| `ROUTE-REFRESH` message type (5) framing | ✅ | | `ROUTE-REFRESH` message type (5) framing | ✅ |
| AFI/SAFI payload parsing | ✅ | | AFI/SAFI payload parsing | ✅ |
| Acting on received ROUTE-REFRESH (re-advertising all local routes, regardless of the AFI/SAFI asked for) | ✅ | | Received ROUTE-REFRESH processed by readvertising all routes for the session, for all AFI/SAFIs | ✅ |
### RFC 7313 — Enhanced Route Refresh Capability for BGP-4 ### RFC 7313 — Enhanced Route Refresh Capability for BGP-4
https://datatracker.ietf.org/doc/html/rfc7313 <https://datatracker.ietf.org/doc/html/rfc7313>
| Feature | Status | | Feature | Status |
|---------|--------| |---------|--------|
| `EnhancedRouteRefresh` capability wire format | 🟡 | | `EnhancedRouteRefresh` capability wire format | 🟡 |
### RFC 4724 — Graceful Restart Mechanism for BGP ### RFC 4724 — Graceful Restart Mechanism for BGP
https://datatracker.ietf.org/doc/html/rfc4724 <https://datatracker.ietf.org/doc/html/rfc4724>
| Feature | Status | | Feature | Status |
|---------|--------| |---------|--------|
@@ -100,26 +105,25 @@ https://datatracker.ietf.org/doc/html/rfc4724
| Restart state and forwarding preserved across sessions | ❌ | | Restart state and forwarding preserved across sessions | ❌ |
### RFC 9494 — Long-Lived Graceful Restart for BGP ### RFC 9494 — Long-Lived Graceful Restart for BGP
https://datatracker.ietf.org/doc/html/rfc9494 <https://datatracker.ietf.org/doc/html/rfc9494>
| Feature | Status | | Feature | Status |
|---------|--------| |---------|--------|
| `LongLivedGracefulRestart` capability wire format | 🟡 | | `LongLivedGracefulRestart` capability wire format | 🟡 |
### RFC 8654 — Extended Message Support for BGP ### RFC 8654 — Extended Message Support for BGP
https://datatracker.ietf.org/doc/html/rfc8654 <https://datatracker.ietf.org/doc/html/rfc8654>
| Feature | Status | | Feature | Status |
|---------|--------| |---------|--------|
| `ExtendedMessage` capability wire format | 🟡 | | `ExtendedMessage` capability wire format | 🟡 |
| Raising `BGP_MAX_MSG_LEN` to 65535 when negotiated | ❌ | | Raising `BGP_MAX_MSG_LEN` to 65535 when negotiated | ❌ |
---
## Multiprotocol and NLRI ## Multiprotocol and NLRI
### RFC 2858 — Multiprotocol Extensions for BGP-4 ### RFC 2858 — Multiprotocol Extensions for BGP-4
https://datatracker.ietf.org/doc/html/rfc2858 <https://datatracker.ietf.org/doc/html/rfc2858>
| Feature | Status | | Feature | Status |
|---------|--------| |---------|--------|
@@ -129,33 +133,32 @@ https://datatracker.ietf.org/doc/html/rfc2858
| IPv4 and IPv6 next-hop encoding | ✅ | | IPv4 and IPv6 next-hop encoding | ✅ |
### RFC 8950 — Advertising IPv4 Network Layer Reachability Information (NLRI) with an IPv6 Next Hop ### RFC 8950 — Advertising IPv4 Network Layer Reachability Information (NLRI) with an IPv6 Next Hop
https://datatracker.ietf.org/doc/html/rfc8950 <https://datatracker.ietf.org/doc/html/rfc8950>
| Feature | Status | | Feature | Status |
|---------|--------| |---------|--------|
| `ExtendedNextHop` capability offered on sessions over IPv6 that carry IPv4 unicast, and used if the peer offers it | ✅ | | `ExtendedNextHop` capability offered on sessions over IPv6 that carry IPv4 unicast, and used if the peer offers it | ✅ |
| IPv4 unicast routes sent and withdrawn in MP_REACH_NLRI / MP_UNREACH_NLRI with our IPv6 address as next hop | ✅ | | IPv4 unicast routes advertised and withdrawn in MP_REACH_NLRI / MP_UNREACH_NLRI with the local IPv6 address as next hop | ✅ |
| IPv4 unicast routes received with an IPv6 next hop (global, or global and link-local) | ✅ | | IPv4 unicast routes received with an IPv6 next hop (global, or global and link-local) | ✅ |
| Such routes installed in the kernel (IPv4 route by way of an IPv6 neighbor) | ✅ | | Such routes installed in the kernel (IPv4 route by way of an IPv6 neighbor) | ✅ |
| Other families than IPv4 unicast (VPN-IPv4, labelled) | ❌ | | Other families than IPv4 unicast (VPN-IPv4, labelled) | ❌ |
| Sessions between link-local addresses (`fe80::1%eth0`), with link-local next hops | ✅ | | Sessions between link-local addresses (`fe80::1%eth0`), with link-local next hops | ✅ |
---
## Path Attributes and Communities ## Path Attributes and Communities
### RFC 1997 — BGP Communities Attribute ### RFC 1997 — BGP Communities Attribute
https://datatracker.ietf.org/doc/html/rfc1997 <https://datatracker.ietf.org/doc/html/rfc1997>
| Feature | Status | | Feature | Status |
|---------|--------| |---------|--------|
| `COMMUNITIES` path attribute (parse, serialise) | ✅ | | `COMMUNITIES` path attribute (parse, serialise) | ✅ |
| Attach to locally originated routes | ✅ | | Attach to locally originated routes | ✅ |
| NO_EXPORT, NO_EXPORT_SUBCONFED and NO_ADVERTISE honoured when learned routes are passed on | ✅ | | NO_EXPORT, NO_EXPORT_SUBCONFED, and NO_ADVERTISE observed when learned routes are advertised | ✅ |
| Communities put on and taken off routes by configuration (`import` and `export`) | ✅ | | Communities added and removed by import and export policy | ✅ |
### RFC 4360 — BGP Extended Communities Attribute ### RFC 4360 — BGP Extended Communities Attribute
https://datatracker.ietf.org/doc/html/rfc4360 <https://datatracker.ietf.org/doc/html/rfc4360>
| Feature | Status | | Feature | Status |
|---------|--------| |---------|--------|
@@ -163,14 +166,13 @@ https://datatracker.ietf.org/doc/html/rfc4360
| AS-specific, IPv4-address-specific, opaque, route-target, route-origin subtypes | ✅ | | AS-specific, IPv4-address-specific, opaque, route-target, route-origin subtypes | ✅ |
### RFC 8092 — BGP Large Communities Attribute ### RFC 8092 — BGP Large Communities Attribute
https://datatracker.ietf.org/doc/html/rfc8092 <https://datatracker.ietf.org/doc/html/rfc8092>
| Feature | Status | | Feature | Status |
|---------|--------| |---------|--------|
| `LARGE_COMMUNITIES` path attribute (parse, serialise) | ✅ | | `LARGE_COMMUNITIES` path attribute (parse, serialise) | ✅ |
| Attach to locally originated routes | ✅ | | Attach to locally originated routes | ✅ |
---
## Other Capabilities (Wire Format Only) ## Other Capabilities (Wire Format Only)
@@ -182,35 +184,33 @@ functional behaviour implemented beyond that.
| RFC 5291 | Outbound Route Filtering (ORF) | 🟡 | | RFC 5291 | Outbound Route Filtering (ORF) | 🟡 |
| RFC 8205 | BGPsec | 🟡 | | RFC 8205 | BGPsec | 🟡 |
---
## Error Handling ## Error Handling
### RFC 9687 — BGP Send Hold Timer ### RFC 9687 — BGP Send Hold Timer
https://datatracker.ietf.org/doc/html/rfc9687 <https://datatracker.ietf.org/doc/html/rfc9687>
| Feature | Status | | Feature | Status |
|---------|--------| |---------|--------|
| `SendHoldTimer` error code (8) defined | ✅ | | `SendHoldTimer` error code (8) defined | ✅ |
| Send hold timer enforcement (a write that makes no progress ends the session) | ✅ | | Send hold timer enforcement (a write that makes no progress ends the session) | ✅ |
---
## Route Security ## Route Security
### RFC 6811 — BGP Prefix Origin Validation ### RFC 6811 — BGP Prefix Origin Validation
https://datatracker.ietf.org/doc/html/rfc6811 <https://datatracker.ietf.org/doc/html/rfc6811>
| Feature | Status | | Feature | Status |
|---------|--------| |---------|--------|
| Validation states (valid, invalid, not found) for routes from external peers | ✅ | | Validation states (valid, invalid, not found) for routes from external peers | ✅ |
| Invalid routes refused, per peer (`accept_rpki_invalid` to take them) | ✅ | | Invalid routes rejected, per neighbor (configurable with `accept_rpki_invalid`) | ✅ |
| Routes revalidated when the VRPs change, in both directions | ✅ | | Routes revalidated when the VRPs change, in both directions | ✅ |
| Validation of routes from internal peers and of originated routes | ❌ | | Validation of routes from internal peers and of originated routes | ❌ |
| Validity as a step of route selection (RFC 8097 communities) | ❌ | | Validity as a step of route selection (RFC 8097 communities) | ❌ |
### draft-ietf-sidrops-aspa-verification — BGP AS_PATH Verification Based on ASPA ### draft-ietf-sidrops-aspa-verification — BGP AS_PATH Verification Based on ASPA
https://datatracker.ietf.org/doc/draft-ietf-sidrops-aspa-verification/ <https://datatracker.ietf.org/doc/draft-ietf-sidrops-aspa-verification/>
Implemented from version 24 of the draft. Implemented from version 24 of the draft.
@@ -219,36 +219,36 @@ Implemented from version 24 of the draft.
| Upstream verification (routes from customers, peers, route servers and route server clients) | ✅ | | Upstream verification (routes from customers, peers, route servers and route server clients) | ✅ |
| Downstream verification (routes from providers) | ✅ | | Downstream verification (routes from providers) | ✅ |
| Per-peer role (`role`); paths of peers without one are not verified | ✅ | | Per-peer role (`role`); paths of peers without one are not verified | ✅ |
| Invalid paths refused, per peer (`accept_aspa_invalid` to take them) | ✅ | | Invalid paths rejected, per neighbor (configurable with `accept_aspa_invalid`) | ✅ |
| Routes revalidated when the ASPAs change, in both directions | ✅ | | Routes revalidated when the ASPAs change, in both directions | ✅ |
| Paths with an AS_SET invalid | ✅ | | Paths with an AS_SET invalid | ✅ |
| Verification on export | ❌ (learned routes are not exported yet) | | Verification on export | ❌ (the AS paths of advertised routes are not verified) |
### RFC 7947 — Internet Exchange BGP Route Server ### RFC 7947 — Internet Exchange BGP Route Server
https://datatracker.ietf.org/doc/html/rfc7947 <https://datatracker.ietf.org/doc/html/rfc7947>
| Feature | Status | | Feature | Status |
|---------|--------| |---------|--------|
| §2.2.1 / §2.2.2: to a `ROUTE_SERVER_CLIENT` the path is not prepended and the next hop and MED are left as they came | ✅ | | §2.2.1 / §2.2.2: to a `ROUTE_SERVER_CLIENT` the path is not prepended and the next hop and MED are left as they came | ✅ |
| §2.2.2.2 / §4.6: communities by which a client says which other clients are told of its route (`control_communities`) | ✅ | | §2.2.2.2 / §4.6: communities with which a client restricts the advertisement of its routes to other clients (`control_communities`) | ✅ |
| Per-client best path selection (a different best route for each client) | ❌ (every client is told the one best route) | | Per-client best path selection (a different best route for each client) | ❌ (every client receives the same best path) |
### RFC 9234 — Route Leak Prevention and Detection Using Roles in UPDATE and OPEN Messages ### RFC 9234 — Route Leak Prevention and Detection Using Roles in UPDATE and OPEN Messages
https://datatracker.ietf.org/doc/html/rfc9234 <https://datatracker.ietf.org/doc/html/rfc9234>
| Feature | Status | | Feature | Status |
|---------|--------| |---------|--------|
| BGP Role capability sent for peers with a configured `role` | ✅ | | BGP Role capability sent for peers with a configured `role` | ✅ |
| Role Mismatch (OPEN Message Error, subcode 11) when the peer's role does not pair with ours, or it sends differing roles | ✅ | | Role Mismatch (OPEN Message Error, subcode 11) when the neighbor's role does not correspond to the local role, or the neighbor advertises conflicting roles | ✅ |
| Strict mode (`require_role`) | ✅ | | Strict mode (`require_role`) | ✅ |
| Only to Customer attribute: parse, serialise, treat-as-withdraw when malformed | ✅ | | Only to Customer attribute: parse, serialise, treat-as-withdraw when malformed | ✅ |
| Ingress: marked routes from customers, route server clients and (unless marked by the peer itself) peers refused | ✅ | | Ingress: routes with OTC rejected from customers, route server clients, and (unless the OTC value is the peer's AS) peers | ✅ |
| Ingress: routes from providers, peers and route servers marked | ✅ | | Ingress: routes from providers, peers and route servers marked | ✅ |
| Egress: originated routes to customers, peers and route server clients marked | ✅ | | Egress: originated routes to customers, peers and route server clients marked | ✅ |
| Egress: marked routes not sent to providers, peers or route servers | ✅ | | Egress: marked routes not sent to providers, peers or route servers | ✅ |
### RFC 8210 — The RPKI to Router Protocol, Version 1 (RFC 6810, version 0; draft-ietf-sidrops-8210bis, version 2) ### RFC 8210 — The RPKI to Router Protocol, Version 1 (RFC 6810, version 0; draft-ietf-sidrops-8210bis, version 2)
https://datatracker.ietf.org/doc/html/rfc8210 <https://datatracker.ietf.org/doc/html/rfc8210>
Version 2 is implemented from version 23 of the draft. Version 2 is implemented from version 23 of the draft.
@@ -259,13 +259,13 @@ Version 2 is implemented from version 23 of the draft.
| Version 2, with fallback to 1 and 0; several caches with failover | ✅ | | Version 2, with fallback to 1 and 0; several caches with failover | ✅ |
| ASPA PDU (version 2) | ✅ | | ASPA PDU (version 2) | ✅ |
| Error Reports sent to the cache | ✅ | | Error Reports sent to the cache | ✅ |
| Version 2's ordering of PDUs enforced on what a cache sends | ❌ (any order is taken) | | Version 2's ordering of PDUs enforced on what a cache sends | ❌ (PDUs are accepted in any order) |
| Transport over plain TCP | ✅ | | Transport over plain TCP | ✅ |
| Transport over SSH, TLS, TCP-MD5 or TCP-AO | ❌ | | Transport over SSH, TLS, TCP-MD5 or TCP-AO | ❌ |
| Router keys (BGPsec) | 🟡 read and ignored | | Router keys (BGPsec) | 🟡 read and ignored |
### RFC 7911 — Advertisement of Multiple Paths in BGP (ADD-PATH) ### RFC 7911 — Advertisement of Multiple Paths in BGP (ADD-PATH)
https://datatracker.ietf.org/doc/html/rfc7911 <https://datatracker.ietf.org/doc/html/rfc7911>
| Feature | Status | | Feature | Status |
|---------|--------| |---------|--------|
@@ -277,7 +277,7 @@ https://datatracker.ietf.org/doc/html/rfc7911
| Feature | Status | | Feature | Status |
|---------|--------| |---------|--------|
| Own AS in path refused (loop detection) | ✅ | | Routes with the local AS in the AS path rejected (loop detection) | ✅ |
| Bogon prefixes and AS numbers, prefix length bounds, first-AS check | ✅ | | Bogon prefixes and AS numbers, prefix length bounds, first-AS check | ✅ |
| Per-peer prefix and origin AS allow-lists | ✅ | | Per-peer prefix and origin AS allow-lists | ✅ |
| Maximum prefixes with Cease / Maximum Number of Prefixes Reached and hold-down | ✅ | | Maximum prefixes with Cease / Maximum Number of Prefixes Reached and hold-down | ✅ |
+38
View File
@@ -0,0 +1,38 @@
# Edge network with one transit provider and one session for each address
# family. Route origin validation, ASPA verification, and kernel route
# installation are enabled.
router_id: "198.51.100.1"
local_asn: 64500
peers {
name: "transit-a-v4"
endpoint { address: "203.0.113.0" }
local_address: "203.0.113.1"
remote_asn: 64496
role: PROVIDER
address_families: IPV4_UNICAST
# Above the size of the IPv4 Internet routing table.
import { max_prefixes: 1200000 }
}
peers {
name: "transit-a-v6"
endpoint { address: "2001:db8:ffff:1::1" }
local_address: "2001:db8:ffff:1::2"
remote_asn: 64496
role: PROVIDER
address_families: IPV6_UNICAST
import { max_prefixes: 300000 }
}
originate { prefix: "198.51.100.0/24" }
originate { prefix: "2001:db8:100::/40" }
# RPKI cache on the local host, for example Routinator.
rpki { caches { address: "::1" port: 3323 } }
# Requires Linux and CAP_NET_ADMIN.
kernel {}
# Not authenticated. Bind to a loopback address.
http_endpoint { address: "127.0.0.1" port: 8080 }
+73
View File
@@ -0,0 +1,73 @@
# Network with one transit provider, a session with an Internet exchange
# route server, and a bilateral session with one peer at the exchange.
#
# Routes learned at the exchange have local preference 150, and transit
# routes have the default of 100. Accepted routes are marked with a large
# community that identifies where they were learned (64500:2:x). Advertised
# routes are marked with the advertising router (64500:3:r).
router_id: "198.51.100.1"
local_asn: 64500
peers {
name: "transit-a"
endpoint { address: "2001:db8:ffff:1::1" }
remote_asn: 64496
role: PROVIDER
import {
max_prefixes: 1500000
remove_communities: "64500:2:*"
remove_communities: "64500:3:*"
add_large_communities: "64500:2:100" # Learned from transit-a.
}
}
# The route server does not prepend its AS number. Its routes have the
# addresses of the originating members as next hops, which the role
# ROUTE_SERVER permits.
peers {
name: "ixp-a-rs1"
endpoint { address: "2001:db8:ffff:ff::fe" }
remote_asn: 64511
role: ROUTE_SERVER
import {
max_prefixes: 250000
accept_any_first_as: true
remove_communities: "64500:2:*"
remove_communities: "64500:3:*"
add_large_communities: "64500:2:1001" # Learned at exchange A.
local_pref: 150
}
}
# Bilateral peer at the exchange. The prefix and origin AS lists are
# generated from the IRR with bgpq4.
peers {
name: "ixp-a-as64499"
endpoint { address: "2001:db8:ffff:ff::99" }
remote_asn: 64499
role: PEER
require_role: true
import {
max_prefixes: 100
prefixes { prefix: "2001:db8:9900::/40" max_length: 48 }
origin_asns: 64499
remove_communities: "64500:2:*"
remove_communities: "64500:3:*"
add_large_communities: "64500:2:1001"
local_pref: 150
}
}
originate { prefix: "2001:db8:100::/40" }
# Applies to all neighbors: marks advertised routes with router 1.
export { add_large_communities: "64500:3:1" }
rpki {
caches { address: "::1" port: 3323 }
caches { address: "2001:db8:100::53" port: 3323 }
}
kernel {}
http_endpoint { address: "127.0.0.1" port: 8080 }
+52
View File
@@ -0,0 +1,52 @@
# /etc/systemd/system/ferrix.service
#
# Runs the Ferrix daemon as an unprivileged user, with only the
# capabilities that it requires, and restricts its access to the system.
# The log is written to the journal: journalctl -u ferrix.
[Unit]
Description=Ferrix BGP daemon
Documentation=https://ferrix.rayhaan.net/docs/
Wants=network-online.target
After=network-online.target
[Service]
Type=exec
User=ferrix
Group=ferrix
# Validate the configuration before starting. An invalid configuration
# prevents the start, and the error is written to the journal.
ExecStartPre=/usr/local/bin/bgp --check /etc/ferrix/server.textpb
ExecStart=/usr/local/bin/bgp /etc/ferrix/server.textpb --level info
Restart=on-failure
RestartSec=5s
# Time to notify all neighbors and remove the kernel routes.
TimeoutStopSec=30s
# TCP port 179, and the kernel routing table.
AmbientCapabilities=CAP_NET_BIND_SERVICE CAP_NET_ADMIN
CapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_NET_ADMIN
NoNewPrivileges=yes
# BGP, RTR, HTTP, and netlink sockets only.
RestrictAddressFamilies=AF_INET AF_INET6 AF_NETLINK AF_UNIX
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
PrivateDevices=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectKernelLogs=yes
ProtectControlGroups=yes
ProtectClock=yes
ProtectHostname=yes
RestrictNamespaces=yes
RestrictRealtime=yes
RestrictSUIDSGID=yes
LockPersonality=yes
MemoryDenyWriteExecute=yes
SystemCallArchitectures=native
SystemCallFilter=@system-service
UMask=0077
[Install]
WantedBy=multi-user.target
+30
View File
@@ -0,0 +1,30 @@
# Quick start: Ferrix and a test peer on one host, over loopback.
router_id: "192.0.2.1"
local_asn: 64500
# A port above 1023, so that no privileges are required.
listen_endpoints { address: "127.0.0.1" port: 1790 }
peers {
name: "test-peer"
endpoint { address: "127.0.0.1" port: 1791 }
remote_asn: 65001
import {
max_prefixes: 1000
# The test peer advertises documentation prefixes and uses a private
# AS number. Both are rejected by default.
accept_bogons: true
# The test peer connects from 127.0.0.1 and advertises 10.0.0.2 as the
# next hop.
accept_third_party_next_hop: true
}
}
originate { prefix: "198.51.100.0/24" }
originate {
prefix: "2001:db8:100::/48"
communities: "64500:100"
}
# Status page, HTTP API, and metrics. Not authenticated.
http_endpoint { address: "127.0.0.1" port: 8080 }
+70
View File
@@ -0,0 +1,70 @@
# Transit network with two providers and one customer. Customer routes are
# advertised to all neighbors. Provider routes are advertised to the
# customer only. The BGP roles of the neighbors enforce this.
router_id: "198.51.100.1"
local_asn: 64500
# Advertise learned routes to all neighbors, as BGP roles permit.
export { learned_routes: true }
# Customers can restrict the advertisement of their routes with the large
# communities 64500:0:P (not to AS P), 64500:1:P (only to AS P), and
# 64500:0:0 (to no neighbor), or the standard communities 0:P, 64500:P, and
# 0:64500.
control_communities: true
peers {
name: "transit-a"
endpoint { address: "203.0.113.0" }
remote_asn: 64496
role: PROVIDER
import {
max_prefixes: 1500000
remove_communities: "64500:2:*"
add_large_communities: "64500:2:100"
}
}
peers {
name: "transit-b"
endpoint { address: "203.0.113.2" }
remote_asn: 64497
role: PROVIDER
import {
max_prefixes: 1500000
remove_communities: "64500:2:*"
add_large_communities: "64500:2:200"
}
# Prepend to make the path through transit-b less preferred. This
# statement replaces the top-level export statement for this neighbor.
export { learned_routes: true as_path_prepend: 2 }
}
# Customer with a four-octet AS number. The prefix and origin AS lists
# restrict its routes to its registered address space. Local preference
# 200 makes its routes preferred over the same prefixes from providers.
peers {
name: "customer-65536"
endpoint { address: "203.0.113.5" }
remote_asn: 65536
role: CUSTOMER
require_role: true
import {
max_prefixes: 50
prefixes { prefix: "192.0.2.0/24" }
prefixes { prefix: "2001:db8:a00::/40" max_length: 48 }
origin_asns: 65536
remove_communities: "64500:2:*"
add_large_communities: "64500:2:300"
local_pref: 200
}
}
originate { prefix: "198.51.100.0/24" }
originate { prefix: "2001:db8:100::/40" }
rpki { caches { address: "::1" port: 3323 } }
kernel {}
http_endpoint { address: "127.0.0.1" port: 8080 }
+1080
View File
File diff suppressed because it is too large. Load diff
+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect width="32" height="32" fill="#0b2fa8"/>
<rect x="9" y="9" width="14" height="14" fill="#ffffff"/>
</svg>

After

Width:  |  Height:  |  Size: 176 B

+92
View File
@@ -0,0 +1,92 @@
Copyright (c) 2016 The Inter Project Authors (https://github.com/rsms/inter)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION AND CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Binary file not shown.
+93
View File
@@ -0,0 +1,93 @@
Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Binary file not shown.
Binary file not shown.
+13
View File
@@ -0,0 +1,13 @@
# Fonts
Served with the site, so that no page loads anything from elsewhere. Both
are under the SIL Open Font License 1.1; the licences are beside them.
| File | From | SHA-256 |
|---|---|---|
| `InterVariable.woff2` | Inter 4.1, `web/` in https://github.com/rsms/inter/releases/download/v4.1/Inter-4.1.zip | `693b77d4f32ee9b8bfc995589b5fad5e99adf2832738661f5402f9978429a8e3` |
| `JetBrainsMono-Regular.woff2` | JetBrains Mono 2.304, `fonts/webfonts/` in https://github.com/JetBrains/JetBrainsMono/releases/download/v2.304/JetBrainsMono-2.304.zip | `a9cb1cd82332b23a47e3a1239d25d13c86d16c4220695e34b243effa999f45f2` |
| `JetBrainsMono-Medium.woff2` | the same | `086c48dfbea9ddaff1320f7e09399b8e2924e88ce67453721255db3bdbb5a353` |
Inter is variable in weight and in optical size: large headings get its
display cut, with tighter spacing and finer detail, without a second font.
+8
View File
@@ -0,0 +1,8 @@
# Images
| File | Source | Licence | SHA-256 |
|---|---|---|---|
| `hero-2400.jpg`, `hero-1200.jpg` | "Fiber to the home" by Ronald Crow, https://unsplash.com/photos/a-bunch-of-yellow-and-green-wires-and-wires-8oR7XykOjk4, resized by images.unsplash.com to 2400 and 1200 pixels wide | [Unsplash License](https://unsplash.com/license): free to use; attribution not required, given on the front page | `c2bc75367507116c005e6c28773c18c4677a8255ac9887bdfe6c33de95caec27` (2400), `17712eb5701415d8faa8f5341ef145ddd17132c7bafccac033d789051bc59fa4` (1200) |
The front page shows the photograph in two colours (see `.hero-photo` in
`sass/site.scss`); the files themselves are the original colour.
Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 434 KiB

+242
View File
@@ -0,0 +1,242 @@
// The little the site does in the browser: the theme switch, the contents
// on narrow screens, copying code, following the reader in "On this page",
// and search over the index Zola builds. Nothing is fetched from elsewhere.
"use strict";
(function () {
var root = document.documentElement;
// --- Theme ---------------------------------------------------------------
function currentTheme() {
if (root.dataset.theme) return root.dataset.theme;
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
}
function remember(theme) {
try { localStorage.setItem("theme", theme); } catch (e) {}
}
document.querySelectorAll("[data-theme-toggle]").forEach(function (button) {
button.addEventListener("click", function () {
var next = currentTheme() === "dark" ? "light" : "dark";
root.dataset.theme = next;
remember(next);
});
});
// --- Contents, on narrow screens -----------------------------------------
var menuButton = document.querySelector("[data-menu-toggle]");
function setMenu(open) {
document.body.classList.toggle("menu-open", open);
if (menuButton) menuButton.setAttribute("aria-expanded", String(open));
}
if (menuButton) {
menuButton.addEventListener("click", function () {
setMenu(!document.body.classList.contains("menu-open"));
});
document.addEventListener("click", function (event) {
if (!document.body.classList.contains("menu-open")) return;
if (event.target.closest("[data-sidebar], [data-menu-toggle]")) return;
setMenu(false);
});
}
// --- Code blocks: a button to copy ----------------------------------------
var COPY = '<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="8.5" y="8.5" width="12" height="12" rx="2"/><path d="M15.5 8.5V5.5a2 2 0 0 0-2-2h-8a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h3"/></svg>';
var DONE = '<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12.5 4.5 4.5L19 7.5"/></svg>';
document.querySelectorAll(".prose pre").forEach(function (pre) {
var wrap = document.createElement("div");
wrap.className = "code-block";
pre.parentNode.insertBefore(wrap, pre);
wrap.appendChild(pre);
var code = pre.querySelector("code");
var button = document.createElement("button");
button.type = "button";
button.className = "copy-button";
button.setAttribute("aria-label", "Copy");
button.innerHTML = COPY;
button.addEventListener("click", function () {
var text = (code || pre).innerText.replace(/\n$/, "");
navigator.clipboard.writeText(text).then(function () {
button.innerHTML = DONE;
button.classList.add("copied");
setTimeout(function () {
button.innerHTML = COPY;
button.classList.remove("copied");
}, 1400);
});
});
wrap.appendChild(button);
});
// --- On this page: the heading being read --------------------------------
var tocLinks = Array.prototype.slice.call(document.querySelectorAll("[data-toc-link]"));
if (tocLinks.length && "IntersectionObserver" in window) {
var byId = {};
var headings = [];
tocLinks.forEach(function (link) {
var id = decodeURIComponent(link.hash.slice(1));
var heading = document.getElementById(id);
if (heading) { byId[id] = link; headings.push(heading); }
});
var visible = new Set();
function mark() {
// The first heading on screen; failing that, the last one above it.
var current = null;
for (var i = 0; i < headings.length; i++) {
if (visible.has(headings[i])) { current = headings[i]; break; }
if (headings[i].getBoundingClientRect().top < 120) current = headings[i];
}
tocLinks.forEach(function (link) { link.classList.remove("active"); });
if (current) byId[current.id].classList.add("active");
}
var observer = new IntersectionObserver(function (entries) {
entries.forEach(function (entry) {
if (entry.isIntersecting) visible.add(entry.target); else visible.delete(entry.target);
});
mark();
}, { rootMargin: "-60px 0px -55% 0px" });
headings.forEach(function (heading) { observer.observe(heading); });
}
// --- Search --------------------------------------------------------------
var search = document.querySelector("[data-search]");
var input = document.querySelector("[data-search-input]");
var results = document.querySelector("[data-search-results]");
var empty = document.querySelector("[data-search-empty]");
var index = null;
var loading = null;
var selected = -1;
function load(src) {
return new Promise(function (resolve, reject) {
var script = document.createElement("script");
script.src = src;
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
}
function loadIndex() {
if (!loading) {
loading = load(document.body.dataset.searchEngine)
.then(function () { return load(document.body.dataset.searchIndex); })
.then(function () { index = window.elasticlunr.Index.load(window.searchIndex); });
}
return loading;
}
function openSearch() {
search.hidden = false;
document.body.style.overflow = "hidden";
input.focus();
input.select();
loadIndex().then(run);
}
function closeSearch() {
search.hidden = true;
document.body.style.overflow = "";
}
function escape(text) {
return text.replace(/[&<>"]/g, function (c) {
return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[c];
});
}
// A piece of the page around the first word searched for.
function excerpt(body, terms) {
var lower = body.toLowerCase();
var at = -1;
for (var i = 0; i < terms.length && at < 0; i++) at = lower.indexOf(terms[i]);
var start = Math.max(0, at - 60);
var text = (start > 0 ? "…" : "") + body.slice(start, start + 220);
var html = escape(text);
terms.forEach(function (term) {
if (term.length < 2) return;
var pattern = new RegExp("(" + term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + ")", "gi");
html = html.replace(pattern, "<mark>$1</mark>");
});
return html;
}
function sectionOf(url) {
var path = url.replace(/^https?:\/\/[^/]+/, "");
var parts = path.split("/").filter(Boolean);
if (parts[0] !== "docs" || parts.length < 3) return "";
return parts[1].replace(/-/g, " ").replace(/^./, function (c) { return c.toUpperCase(); });
}
function run() {
if (!index) return;
var query = input.value.trim();
results.innerHTML = "";
selected = -1;
empty.hidden = true;
if (!query) return;
var found = index.search(query, {
bool: "AND",
expand: true,
fields: { title: { boost: 3 }, description: { boost: 2 }, body: { boost: 1 } },
}).slice(0, 10);
if (!found.length) { empty.hidden = false; return; }
var terms = query.toLowerCase().split(/\s+/).filter(Boolean);
found.forEach(function (hit, i) {
var doc = index.documentStore.getDoc(hit.ref);
var item = document.createElement("li");
var section = sectionOf(hit.ref);
item.innerHTML =
'<a href="' + escape(hit.ref) + '" role="option" id="search-' + i + '">' +
(section ? '<span class="search-result-section">' + escape(section) + "</span>" : "") +
'<span class="search-result-title">' + escape(doc.title || "Ferrix BGP") + "</span>" +
'<span class="search-result-text">' + excerpt(doc.body || doc.description || "", terms) + "</span></a>";
results.appendChild(item);
});
select(0);
}
function select(i) {
var links = results.querySelectorAll("a");
if (!links.length) return;
selected = (i + links.length) % links.length;
links.forEach(function (link, j) { link.setAttribute("aria-selected", String(j === selected)); });
links[selected].scrollIntoView({ block: "nearest" });
}
if (search && input) {
document.querySelectorAll("[data-search-open]").forEach(function (button) {
button.addEventListener("click", openSearch);
});
document.querySelectorAll("[data-search-close]").forEach(function (el) {
el.addEventListener("click", closeSearch);
});
input.addEventListener("input", run);
input.addEventListener("keydown", function (event) {
if (event.key === "ArrowDown") { event.preventDefault(); select(selected + 1); }
else if (event.key === "ArrowUp") { event.preventDefault(); select(selected - 1); }
else if (event.key === "Enter") {
var link = results.querySelectorAll("a")[selected];
if (link) window.location.href = link.href;
}
});
document.addEventListener("keydown", function (event) {
var typing = /^(INPUT|TEXTAREA|SELECT)$/.test(document.activeElement.tagName) || document.activeElement.isContentEditable;
if (event.key === "Escape" && !search.hidden) { closeSearch(); return; }
if (event.key === "Escape") { setMenu(false); return; }
if ((event.key === "/" && !typing) || (event.key === "k" && (event.metaKey || event.ctrlKey))) {
event.preventDefault();
openSearch();
}
});
}
})();
+57
View File
@@ -0,0 +1,57 @@
{
"name": "textproto",
"scopeName": "source.textproto",
"fileTypes": ["textproto", "textpb", "pbtxt"],
"patterns": [
{ "include": "#comment" },
{ "include": "#block" },
{ "include": "#field" },
{ "include": "#string" },
{ "include": "#constant" },
{ "include": "#number" }
],
"repository": {
"comment": {
"name": "comment.line.number-sign.textproto",
"match": "#.*$"
},
"block": {
"comment": "A field whose value is a message: `peers {`",
"match": "\\b([A-Za-z_][A-Za-z0-9_]*)\\s*:?\\s*(?=\\{)",
"captures": { "1": { "name": "entity.name.tag.textproto" } }
},
"field": {
"match": "\\b([A-Za-z_][A-Za-z0-9_]*)\\s*(:)",
"captures": {
"1": { "name": "support.type.property-name.textproto" },
"2": { "name": "punctuation.separator.key-value.textproto" }
}
},
"string": {
"patterns": [
{
"name": "string.quoted.double.textproto",
"begin": "\"",
"end": "\"",
"patterns": [{ "name": "constant.character.escape.textproto", "match": "\\\\." }]
},
{
"name": "string.quoted.single.textproto",
"begin": "'",
"end": "'",
"patterns": [{ "name": "constant.character.escape.textproto", "match": "\\\\." }]
}
]
},
"constant": {
"patterns": [
{ "name": "constant.language.boolean.textproto", "match": "\\b(true|false)\\b" },
{ "name": "constant.language.enum.textproto", "match": "\\b[A-Z][A-Z0-9_]*\\b" }
]
},
"number": {
"name": "constant.numeric.textproto",
"match": "-?\\b(0x[0-9A-Fa-f]+|[0-9]+(\\.[0-9]+)?([eE][+-]?[0-9]+)?)\\b"
}
}
}
+17
View File
@@ -0,0 +1,17 @@
{% extends "base.html" %}
{%- import "macros/icons.html" as icons -%}
{% block title %}Not found · {{ config.title }}{% endblock title %}
{% block body_class %}layout-home{% endblock body_class %}
{% block body %}
<main id="main" class="not-found">
<span class="eyebrow">404 · Destination unreachable</span>
<h1>No route to this page.</h1>
<p>The requested page does not exist. It may have been moved or renamed. Search the documentation, or return to the documentation home page.</p>
<div class="hero-actions">
<a class="button button-primary" href="{{ get_url(path='docs/') }}">Documentation {{ icons::arrow_right() }}</a>
<button class="button button-ghost" type="button" data-search-open>Search</button>
</div>
</main>
{% endblock body %}
+87
View File
@@ -0,0 +1,87 @@
{%- import "macros/icons.html" as icons -%}
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}{{ config.title }}{% endblock title %}</title>
<meta name="description" content="{% block description %}{{ config.description }}{% endblock description %}">
<meta name="color-scheme" content="light dark">
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff">
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#06101e">
<link rel="icon" href="{{ get_url(path='favicon.svg') }}" type="image/svg+xml">
<link rel="preload" href="{{ get_url(path='fonts/InterVariable.woff2') }}" as="font" type="font/woff2" crossorigin>
<link rel="stylesheet" href="{{ get_url(path='site.css', cachebust=true) }}">
{#- The theme is set before the page is drawn, so that it does not flash. #}
<script>
try {
var theme = localStorage.getItem("theme");
if (theme === "light" || theme === "dark") document.documentElement.dataset.theme = theme;
} catch (e) {}
</script>
<script src="{{ get_url(path='js/site.js', cachebust=true) }}" defer></script>
</head>
<body class="{% block body_class %}{% endblock body_class %}"
data-search-engine="{{ get_url(path='elasticlunr.min.js') }}"
data-search-index="{{ get_url(path='search_index.en.js') }}">
<a class="skip" href="#main">Skip to content</a>
<header class="topbar">
<div class="topbar-inner">
{%- block menu_button %}{% endblock menu_button %}
<a class="brand" href="{{ get_url(path='') }}" aria-label="Ferrix home">
<span class="brand-mark" aria-hidden="true"></span>
<span class="brand-name">Ferrix</span>
<span class="brand-version">{{ config.extra.version }}</span>
</a>
<nav class="topnav" aria-label="Main">
{%- for item in config.extra.nav %}
<a href="{{ get_url(path=item.path) }}"{% if current_path and current_path is starting_with("/" ~ item.path) %} aria-current="page"{% endif %}>{{ item.name }}</a>
{%- endfor %}
</nav>
<div class="topbar-actions">
<button class="search-button" type="button" data-search-open aria-label="Search the documentation">
{{ icons::search() }}
<span class="search-button-label">Search</span>
<kbd class="search-button-key">/</kbd>
</button>
<a class="icon-button" href="{{ config.extra.repository }}" aria-label="Source code">{{ icons::code() }}</a>
<button class="icon-button" type="button" data-theme-toggle aria-label="Switch between light and dark">
<span class="theme-icon-light">{{ icons::sun() }}</span>
<span class="theme-icon-dark">{{ icons::moon() }}</span>
</button>
</div>
</div>
</header>
{% block body %}{% endblock body %}
<footer class="footer">
<div class="footer-inner">
<div class="footer-brand">
<span class="brand-mark" aria-hidden="true"></span>
<span>Ferrix BGP {{ config.extra.version }}</span>
</div>
<nav class="footer-links" aria-label="Footer">
<a href="{{ get_url(path='docs/getting-started/introduction/') }}">Documentation</a>
<a href="{{ get_url(path='docs/reference/configuration/') }}">Configuration</a>
<a href="{{ get_url(path='docs/reference/rfcs/') }}">RFCs</a>
<a href="{{ config.extra.repository }}">Source</a>
<span>Apache License 2.0</span>
</nav>
</div>
</footer>
<div class="search" data-search hidden>
<div class="search-backdrop" data-search-close></div>
<div class="search-dialog" role="dialog" aria-modal="true" aria-label="Search">
<div class="search-field">
{{ icons::search() }}
<input type="search" placeholder="Search the documentation" autocomplete="off" spellcheck="false" data-search-input aria-label="Search">
<kbd data-search-close>esc</kbd>
</div>
<ul class="search-results" data-search-results role="listbox"></ul>
<p class="search-empty" data-search-empty hidden>Nothing found.</p>
</div>
</div>
</body>
</html>
+157
View File
@@ -0,0 +1,157 @@
{% extends "base.html" %}
{%- import "macros/icons.html" as icons -%}
{% block title %}{{ config.title }} · Routing security for every network{% endblock title %}
{% block body_class %}layout-home{% endblock body_class %}
{% block body %}
<main id="main">
<section class="hero">
{#- The photograph is set in two colours by the stylesheet; see .hero-photo. #}
<div class="hero-photo" aria-hidden="true">
<img src="{{ get_url(path='images/hero-2400.jpg') }}"
srcset="{{ get_url(path='images/hero-1200.jpg') }} 1200w, {{ get_url(path='images/hero-2400.jpg') }} 2400w"
sizes="100vw" alt="" decoding="async" fetchpriority="high">
<div class="hero-tone"></div>
<div class="hero-shade"></div>
</div>
<div class="hero-copy">
<span class="eyebrow">BGP-4 · RFC 4271 · version {{ config.extra.version }}</span>
<h1 class="hero-title">Routing security for every network.</h1>
<p class="hero-text">
Ferrix is a BGP daemon for Linux routers. It filters the routes it
receives, validates them with the RPKI, prevents route leaks with
BGP roles, and installs the best paths in the kernel. It is free
software, written in Rust.
</p>
<div class="hero-actions">
<a class="button button-light" href="{{ get_url(path='docs/getting-started/quick-start/') }}">Quick start {{ icons::arrow_right() }}</a>
<a class="button button-outline-light" href="{{ get_url(path='docs/getting-started/introduction/') }}">Documentation</a>
</div>
<p class="hero-foot"><span class="prompt">$</span>cargo build --release -p bgp</p>
</div>
<p class="hero-credit">Source: <a href="https://unsplash.com/photos/a-bunch-of-yellow-and-green-wires-and-wires-8oR7XykOjk4">Unsplash</a></p>
</section>
<section class="home-section">
<div class="home-grid">
<div class="home-label"><span class="eyebrow">01</span>Default Import Policy</div>
<div>
<p class="home-note">Ferrix applies the following checks to every route from an external neighbor by default. Nine of them can be disabled per neighbor where required.</p>
<ol class="checks">
<li><span>01</span>Bogon prefixes</li>
<li><span>02</span>Bogon AS numbers in the path</li>
<li><span>03</span>Prefixes longer than /24 or /48</li>
<li><span>04</span>Default route</li>
<li><span>05</span>AS paths longer than 64</li>
<li><span>06</span>AS_SET in the path</li>
<li><span>07</span>First AS is not the neighbor</li>
<li><span>08</span>Local AS in the path</li>
<li><span>09</span>Next hop is not the neighbor</li>
<li><span>10</span>Next hop is unusable</li>
<li><span>11</span>Local prefixes</li>
</ol>
<p class="home-note">Each check reports a distinct rejection reason. RPKI origin validation, ASPA verification, BGP roles, prefix limits, and prefix and origin lists are configured per neighbor.</p>
<a class="text-link" href="{{ get_url(path='docs/guides/import-policy/') }}#comparison-with-the-nlnog-bgp-filter-guide">Comparison with the NLNOG BGP Filter Guide {{ icons::arrow_right() }}</a>
</div>
</div>
</section>
<section class="home-section">
<div class="home-grid">
<div class="home-label"><span class="eyebrow">02</span>Specifications</div>
<dl class="spec">
<div><dt>Protocol</dt><dd>BGP-4, four-octet AS numbers, MP-BGP</dd></div>
<div><dt>Address families</dt><dd>IPv4 and IPv6 unicast; IPv4 with IPv6 next hops (RFC 8950)</dd></div>
<div><dt>Validation</dt><dd>RPKI origin validation and ASPA; RTR versions 0 through 2</dd></div>
<div><dt>Route leak prevention</dt><dd>BGP roles and Only to Customer (RFC 9234)</dd></div>
<div><dt>Neighbors</dt><dd>Global and link-local addresses</dd></div>
<div><dt>Policy</dt><dd>Prefix and origin lists, prefix limits, communities, control communities</dd></div>
<div><dt>Forwarding</dt><dd>Linux kernel over netlink, with nexthop objects</dd></div>
<div><dt>Kernel installation</dt><dd>1 million routes in about 1 second</dd></div>
<div><dt>Monitoring</dt><dd>Status page, JSON API, Prometheus metrics</dd></div>
<div><dt>License</dt><dd>Apache 2.0</dd></div>
</dl>
</div>
</section>
<section class="home-section">
<div class="home-grid">
<div class="home-label"><span class="eyebrow">03</span>Features</div>
<div class="features">
<a class="feature" href="{{ get_url(path='docs/guides/import-policy/') }}">
<span class="eyebrow">03.1</span>
<h3>Import Policy</h3>
<p>Rejects bogon prefixes and AS numbers, third-party next hops, and local prefixes by default. Supports prefix lists, origin AS lists, and prefix limits.</p>
</a>
<a class="feature" href="{{ get_url(path='docs/guides/rpki/') }}">
<span class="eyebrow">03.2</span>
<h3>RPKI Validation</h3>
<p>Route origin validation and ASPA verification. Routes are revalidated when the RPKI data changes.</p>
</a>
<a class="feature" href="{{ get_url(path='docs/guides/roles/') }}">
<span class="eyebrow">03.3</span>
<h3>Route Leak Prevention</h3>
<p>BGP role negotiation and the Only to Customer attribute, as specified in RFC 9234.</p>
</a>
<a class="feature" href="{{ get_url(path='docs/guides/kernel/') }}">
<span class="eyebrow">03.4</span>
<h3>Kernel Route Installation</h3>
<p>Installs active routes over netlink. Routes with unresolvable next hops are excluded from best path selection.</p>
</a>
<a class="feature" href="{{ get_url(path='docs/guides/passing-routes-on/') }}">
<span class="eyebrow">03.5</span>
<h3>Route Advertisement</h3>
<p>Advertises learned routes as BGP roles permit, with export policy and route server control communities.</p>
</a>
<a class="feature" href="{{ get_url(path='docs/guides/peers/') }}">
<span class="eyebrow">03.6</span>
<h3>Neighbor Types</h3>
<p>Link-local neighbors such as <code>fe80::1%eth0</code>, IPv4 routes with IPv6 next hops, and ADD-PATH.</p>
</a>
<a class="feature" href="{{ get_url(path='docs/operations/monitoring/') }}">
<span class="eyebrow">03.7</span>
<h3>Monitoring</h3>
<p>Records every rejected route with its reason. Provides Prometheus metrics and alerting rules.</p>
</a>
<a class="feature" href="{{ get_url(path='docs/development/architecture/') }}">
<span class="eyebrow">03.8</span>
<h3>Reliability</h3>
<p>No <code>unsafe</code> code and fuzz-tested parsers. A failure in one session does not affect other sessions.</p>
</a>
</div>
</div>
</section>
<section class="home-section">
<div class="home-grid">
<div class="home-label"><span class="eyebrow">04</span>Interoperability</div>
<div>
<p class="home-note">An automated lab tests Ferrix with FRR 10.5.1, BIRD 2.18, and Routinator 0.15.2, in the following phases:</p>
<ol class="phases">
{%- for phase in ["basics", "roles", "rpki", "refresh", "flaps", "restart", "scale", "limits", "hijacks", "nexthops", "transit", "v6only", "linklocal"] %}
<li><span>{% if loop.index < 10 %}0{% endif %}{{ loop.index }}</span>{{ phase }}</li>
{%- endfor %}
</ol>
<a class="text-link" href="{{ get_url(path='docs/development/testing/') }}#interoperability-lab">Test phases {{ icons::arrow_right() }}</a>
</div>
</div>
</section>
<section class="home-section">
<div class="home-grid">
<div class="home-label"><span class="eyebrow">05</span>Getting Started</div>
<div class="start">
<div>
<p>Establish a BGP session with a test peer on a single host, without privileges and without modifying the kernel routing table.</p>
<a class="text-link" href="{{ get_url(path='docs/getting-started/quick-start/') }}">Quick start {{ icons::arrow_right() }}</a>
</div>
<pre><span class="prompt">$ </span>cargo run -p bgp -- docs/examples/quick-start.textpb
<span class="prompt">$ </span>cargo run -p bgp-integration-tests --example fake_peer -- \
--server 127.0.0.1:1790 --asn 65001 --announce 203.0.113.0/24
<span class="comment"># then http://127.0.0.1:8080/statusz</span></pre>
</div>
</div>
</section>
</main>
{% endblock body %}
+74
View File
@@ -0,0 +1,74 @@
{#- The documentation's navigation: every section under docs/, by weight, and
the pages in each, by weight. -#}
{%- macro sidebar(current) -%}
{%- set docs = get_section(path="docs/_index.md") -%}
<nav class="sidebar-nav" aria-label="Documentation">
{%- for sub_path in docs.subsections %}
{%- set sub = get_section(path=sub_path) %}
<div class="nav-group">
<a class="nav-group-title" href="{{ sub.permalink }}"{% if sub.path == current %} aria-current="page"{% endif %}><span>{{ sub.extra.number }}</span>{{ sub.title }}</a>
<ul>
{%- for p in sub.pages %}
<li><a href="{{ p.permalink }}"{% if p.path == current %} aria-current="page"{% endif %}>{{ p.extra.nav | default(value=p.title) }}</a></li>
{%- endfor %}
</ul>
</div>
{%- endfor %}
</nav>
{%- endmacro sidebar -%}
{#- The pages before and after this one, in the order of the sidebar. -#}
{%- macro pager(current) -%}
{%- set docs = get_section(path="docs/_index.md") -%}
{%- set_global flat = [] -%}
{%- for sub_path in docs.subsections -%}
{%- set sub = get_section(path=sub_path) -%}
{%- for p in sub.pages -%}
{%- set_global flat = flat | concat(with=p) -%}
{%- endfor -%}
{%- endfor -%}
{%- set_global at = -1 -%}
{%- for p in flat -%}
{%- if p.path == current %}{% set_global at = loop.index0 %}{% endif -%}
{%- endfor -%}
{%- if at >= 0 %}
<nav class="pager" aria-label="Pages">
{%- if at > 0 %}
{%- set prev = flat | nth(n=at - 1) %}
<a class="pager-link pager-prev" href="{{ prev.permalink }}">
<span class="pager-label">Previous</span>
<span class="pager-title">{{ prev.title }}</span>
</a>
{%- else %}<span></span>{% endif %}
{%- if at + 1 < flat | length %}
{%- set next = flat | nth(n=at + 1) %}
<a class="pager-link pager-next" href="{{ next.permalink }}">
<span class="pager-label">Next</span>
<span class="pager-title">{{ next.title }}</span>
</a>
{%- endif %}
</nav>
{%- endif %}
{%- endmacro pager -%}
{%- macro toc(entries) -%}
{%- if entries | length > 0 %}
<nav class="toc" aria-label="On this page">
<p class="toc-title">On this page</p>
<ul>
{%- for h2 in entries %}
<li><a href="#{{ h2.id }}" data-toc-link>{{ h2.title }}</a>
{%- if h2.children | length > 0 %}
<ul>
{%- for h3 in h2.children %}
<li><a href="#{{ h3.id }}" data-toc-link>{{ h3.title }}</a></li>
{%- endfor %}
</ul>
{%- endif %}
</li>
{%- endfor %}
</ul>
</nav>
{%- endif %}
{%- endmacro toc -%}
+14
View File
@@ -0,0 +1,14 @@
{#- Icons, drawn on a 24 by 24 grid with a 1.75-pixel stroke in the current
colour: square ends, as the type is. -#}
{%- macro svg_open() -%}
<svg class="icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="square" aria-hidden="true">
{%- endmacro svg_open -%}
{%- macro search() -%}{{ self::svg_open() }}<circle cx="11" cy="11" r="6.5"/><path d="m16 16 4.5 4.5"/></svg>{%- endmacro search -%}
{%- macro code() -%}{{ self::svg_open() }}<path d="m16 18 6-6-6-6"/><path d="m8 6-6 6 6 6"/></svg>{%- endmacro code -%}
{%- macro sun() -%}{{ self::svg_open() }}<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg>{%- endmacro sun -%}
{%- macro moon() -%}{{ self::svg_open() }}<path d="M20.5 13.5A8.5 8.5 0 1 1 10.5 3.5a6.6 6.6 0 0 0 10 10z"/></svg>{%- endmacro moon -%}
{%- macro menu() -%}{{ self::svg_open() }}<path d="M4 7h16M4 12h16M4 17h16"/></svg>{%- endmacro menu -%}
{%- macro arrow_right() -%}{{ self::svg_open() }}<path d="M4 12h15M13 6l6 6-6 6"/></svg>{%- endmacro arrow_right -%}
{%- macro file() -%}{{ self::svg_open() }}<path d="M14 3H6v18h12V7z"/><path d="M14 3v4h4"/></svg>{%- endmacro file -%}
+39
View File
@@ -0,0 +1,39 @@
{% extends "base.html" %}
{%- import "macros/icons.html" as icons -%}
{%- import "macros/docs.html" as docs -%}
{% block title %}{{ page.title }} · {{ config.title }}{% endblock title %}
{% block description %}{{ page.description | default(value=config.description) }}{% endblock description %}
{% block body_class %}layout-docs{% endblock body_class %}
{% block menu_button %}
<button class="icon-button menu-button" type="button" data-menu-toggle aria-label="Show the contents" aria-expanded="false">{{ icons::menu() }}</button>
{% endblock menu_button %}
{% block body %}
{%- set group = get_section(path=page.ancestors | last, metadata_only=true) %}
<div class="docs-layout">
<aside class="sidebar" data-sidebar>
{{ docs::sidebar(current=page.path) }}
</aside>
<main id="main" class="docs-main">
<article class="prose">
<header class="page-header">
<span class="eyebrow">{{ group.extra.number }} — {{ group.title }}</span>
<h1>{{ page.title }}</h1>
{%- if page.description %}
<p class="lead">{{ page.description }}</p>
{%- endif %}
</header>
{{ page.content | safe }}
</article>
{{ docs::pager(current=page.path) }}
<p class="page-source">
<a href="{{ config.extra.repository }}/src/branch/main/docs/content/{{ page.relative_path }}">{{ icons::file() }} This page's source</a>
</p>
</main>
<aside class="toc-column">
{{ docs::toc(entries=page.toc) }}
</aside>
</div>
{% endblock body %}
+61
View File
@@ -0,0 +1,61 @@
{% extends "base.html" %}
{%- import "macros/icons.html" as icons -%}
{%- import "macros/docs.html" as docs -%}
{% block title %}{{ section.title }} · {{ config.title }}{% endblock title %}
{% block description %}{{ section.description | default(value=config.description) }}{% endblock description %}
{% block body_class %}layout-docs{% endblock body_class %}
{% block menu_button %}
<button class="icon-button menu-button" type="button" data-menu-toggle aria-label="Show the contents" aria-expanded="false">{{ icons::menu() }}</button>
{% endblock menu_button %}
{% block body %}
<div class="docs-layout">
<aside class="sidebar" data-sidebar>
{{ docs::sidebar(current=section.path) }}
</aside>
<main id="main" class="docs-main">
<article class="prose">
<header class="page-header">
<span class="eyebrow">{% if section.extra.number %}{{ section.extra.number }} — {% endif %}Documentation</span>
<h1>{{ section.title }}</h1>
{%- if section.description %}
<p class="lead">{{ section.description }}</p>
{%- endif %}
</header>
{{ section.content | safe }}
</article>
{#- A group lists its pages; the top of the documentation lists the groups. #}
{%- if section.pages | length > 0 %}
<div class="cards">
{%- for p in section.pages %}
<a class="card" href="{{ p.permalink }}">
<span class="card-number">{{ section.extra.number }}.{{ loop.index }}</span>
<span class="card-title">{{ p.title }} {{ icons::arrow_right() }}</span>
{%- if p.description %}<span class="card-text">{{ p.description }}</span>{% endif %}
</a>
{%- endfor %}
</div>
{%- else %}
{%- for sub_path in section.subsections %}
{%- set sub = get_section(path=sub_path) %}
<section class="group-overview">
<h2><span>{{ sub.extra.number }}</span><a href="{{ sub.permalink }}">{{ sub.title }}</a></h2>
{%- if sub.description %}<p>{{ sub.description }}</p>{% endif %}
<div class="cards">
{%- for p in sub.pages %}
<a class="card" href="{{ p.permalink }}">
<span class="card-number">{{ sub.extra.number }}.{{ loop.index }}</span>
<span class="card-title">{{ p.title }} {{ icons::arrow_right() }}</span>
{%- if p.description %}<span class="card-text">{{ p.description }}</span>{% endif %}
</a>
{%- endfor %}
</div>
</section>
{%- endfor %}
{%- endif %}
</main>
<aside class="toc-column"></aside>
</div>
{% endblock body %}
+9
View File
@@ -0,0 +1,9 @@
{#- A configuration from docs/examples, shown whole. CI checks each of them
with `bgp --check`, so what is shown is known to be in order. -#}
{%- set text = load_data(path="examples/" ~ name ~ ".textpb", format="plain") -%}
{%- set fenced = "```textproto
" ~ text ~ "```" -%}
<figure class="example">
<figcaption>docs/examples/{{ name }}.textpb</figcaption>
{{ fenced | markdown | safe }}
</figure>
+8
View File
@@ -0,0 +1,8 @@
{#- A file from docs/examples, shown whole and highlighted as `lang`. -#}
{%- set text = load_data(path="examples/" ~ name, format="plain") -%}
{%- set fenced = "```" ~ lang ~ "
" ~ text ~ "```" -%}
<figure class="example">
<figcaption>docs/examples/{{ name }}</figcaption>
{{ fenced | markdown | safe }}
</figure>