Add a documentation website, built with Zola
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:
1 parent
9820a5d3bc
commit
dc275f99d7
61 files changed
+5632
-623
No files matched your search
@@ -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:
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
/target
|
/target
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.claude/
|
.claude/
|
||||||
|
/docs/public
|
||||||
|
/docs/static/giallo.css
|
||||||
@@ -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
@@ -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
|
|
||||||
```
|
|
||||||
@@ -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
@@ -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`.
|
||||||
@@ -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/" },
|
||||||
|
]
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
+++
|
||||||
|
title = "Ferrix BGP"
|
||||||
|
+++
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
+++
|
||||||
|
title = "Documentation"
|
||||||
|
description = "Installation, configuration, operation, and development documentation for the Ferrix BGP daemon."
|
||||||
|
sort_by = "weight"
|
||||||
|
+++
|
||||||
@@ -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.
|
||||||
@@ -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)
|
||||||
@@ -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"
|
||||||
|
+++
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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).
|
||||||
@@ -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.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
+++
|
||||||
|
title = "Operations"
|
||||||
|
description = "Deploying, monitoring, and maintaining Ferrix in production."
|
||||||
|
weight = 3
|
||||||
|
sort_by = "weight"
|
||||||
|
|
||||||
|
[extra]
|
||||||
|
number = "03"
|
||||||
|
+++
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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`. |
|
||||||
@@ -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
|
||||||
|
```
|
||||||
@@ -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 | ✅ |
|
||||||
@@ -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 }
|
||||||
@@ -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 }
|
||||||
@@ -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
|
||||||
@@ -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 }
|
||||||
@@ -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
File diff suppressed because it is too large.
Load diff
Vendored
+4
@@ -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 |
Vendored
+92
@@ -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.
|
||||||
BIN
Binary file not shown.
+93
@@ -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.
|
||||||
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
+13
@@ -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.
|
||||||
Vendored
+8
@@ -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.
|
||||||
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 147 KiB |
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 434 KiB |
Vendored
+242
@@ -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 { "&": "&", "<": "<", ">": ">", '"': """ }[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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})();
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+17
@@ -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 %}
|
||||||
Vendored
+87
@@ -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>
|
||||||
Vendored
+157
@@ -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 %}
|
||||||
Vendored
+74
@@ -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 -%}
|
||||||
Vendored
+14
@@ -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 -%}
|
||||||
Vendored
+39
@@ -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 %}
|
||||||
Vendored
+61
@@ -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
@@ -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>
|
||||||
Vendored
+8
@@ -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>
|
||||||
Reference in new issue
Block a user