A new crate, bgp-policy, runs functions of a script on routes: on their
way in, after the general checks of a peer's import and before the
checks that can change (RPKI, next hop reachability); and on their way
out to a peer. A function is given the route and the peer, may refuse
the route, and may change its communities, large communities,
LOCAL_PREF and MED, and on the way out prepend our AS:
fn from_peer(route, peer) {
if route.has_community("65535:666") { return false; }
route.add_community(LEARNED_FROM[peer.role]);
route.local_pref = PREFERENCE[peer.role];
true
}
Configuration: policy { path | source, import_function, export_function }
and, for a peer, import { function } and export { function }. The script
is compiled at startup; `bgp --check` reads it and says which function
is whose. A function that fails on a route has refused it. Scripts have
no files, network or clock, and are stopped after 200,000 steps.
Route selection now goes by a LOCAL_PREF on a route from an external
peer: the decoder never lets a peer's own through, so one that is there
was set by policy.
What it costs was measured before it was built on (benches/policy.rs):
a call that tests a few communities and changes two takes 1 to 3
microseconds, some two hundred times what Rust would. So a function is
asked once for the routes of an UPDATE, which share their attributes,
unless it asks for the prefix. One peer sending 1.23 M routes over a
session: 0.37 s without a function, 0.80 s with one that marks and
ranks every route, 3.4 s if it also looks at the prefix.
Export applies to what is announced, which today is what the daemon
originates; learned routes are not passed on yet.
Tested by unit tests of the crate, sessions end to end, a fuzz target on
the example script (testdata/policy.rhai), and a lab phase in which a
script changes which of FRR's and BIRD's route is chosen and what each
of them is told.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
52 lines
2.1 KiB
Markdown
52 lines
2.1 KiB
Markdown
# Fuzz targets
|
|
|
|
Coverage-guided fuzzing of everything that reads input we do not control.
|
|
The property tests (`cargo test`, e.g. `crates/packet/tests/properties.rs`)
|
|
check the same things on generated input and run on every build; these
|
|
targets go further by letting libFuzzer follow code coverage for as long as
|
|
you leave them running.
|
|
|
|
| Target | Input |
|
|
|----------------|------------------------------------------------------------------|
|
|
| `message` | Raw bytes as they come off a TCP connection |
|
|
| `message_body` | A message body behind a correct header (reaches the body parsers) |
|
|
| `prefix_text` | A prefix as text |
|
|
| `config` | The configuration file |
|
|
| `rtr` | A stream of RTR PDUs, as an RPKI cache sends them |
|
|
| `netlink` | Messages from the kernel's routing socket |
|
|
| `attrs` | The routing table's reading of stored attribute bytes |
|
|
| `policy` | The example policy script on routes with any attributes |
|
|
|
|
For the two message targets a finding is either a panic, or a message the
|
|
decoder accepted that cannot be written and read back as itself.
|
|
|
|
## Running
|
|
|
|
libFuzzer needs a nightly toolchain and `cargo-fuzz`:
|
|
|
|
```bash
|
|
rustup toolchain install nightly
|
|
cargo install cargo-fuzz
|
|
```
|
|
|
|
Then, from this directory:
|
|
|
|
```bash
|
|
cargo +nightly fuzz run message_body corpus/message_body seeds/message_body -- -max_total_time=300
|
|
```
|
|
|
|
The first directory is the working corpus, which the fuzzer adds to and
|
|
which is not checked in. `seeds/` holds a few valid inputs to start from.
|
|
Leave out `-max_total_time` to run until interrupted.
|
|
|
|
## When it finds something
|
|
|
|
The input is saved under `artifacts/<target>/`. Reproduce it with
|
|
|
|
```bash
|
|
cargo +nightly fuzz run message_body artifacts/message_body/crash-...
|
|
```
|
|
|
|
Fix the bug, and add a unit test with the input next to the code it broke,
|
|
so that `cargo test` keeps it fixed without anyone having to run the fuzzer.
|