Compare commits
65
Commits
No files matched your search
@@ -1,7 +1,2 @@
|
||||
target
|
||||
.git
|
||||
.claude
|
||||
fuzz/target
|
||||
fuzz/corpus
|
||||
fuzz/artifacts
|
||||
crates/bench/data
|
||||
@@ -23,58 +23,9 @@ jobs:
|
||||
- name: Format
|
||||
run: cargo fmt --all -- --check
|
||||
- name: Lint
|
||||
run: cargo clippy --locked --workspace --all-targets -- -D warnings
|
||||
# The memory-counting allocator of the benchmarks is behind a feature
|
||||
# that the workspace build does not turn on.
|
||||
- name: Lint the benchmarks with memory counting
|
||||
run: cargo clippy --locked -p bgp-bench --all-targets --features count-memory -- -D warnings
|
||||
run: cargo clippy --workspace --all-targets -- -D warnings
|
||||
- name: Test
|
||||
run: cargo test --locked --workspace
|
||||
# A link in the documentation to something that is not there.
|
||||
- name: Documentation
|
||||
env:
|
||||
RUSTDOCFLAGS: -D warnings
|
||||
run: cargo doc --locked --workspace --no-deps --document-private-items
|
||||
# The examples operators start from, here and in the documentation,
|
||||
# through the daemon itself.
|
||||
- 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
|
||||
# their own; and two daemons that route for each other.
|
||||
- name: Kernel tests
|
||||
run: scripts/kernel-tests.sh
|
||||
|
||||
# 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:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- name: Install FRR, BIRD and Routinator
|
||||
run: scripts/interop/install.sh
|
||||
- name: Build
|
||||
run: cargo build --release --locked -p bgp
|
||||
- name: Interoperability lab
|
||||
run: scripts/interop/lab.sh
|
||||
run: cargo test --workspace
|
||||
|
||||
# A short run of each fuzz target: enough to notice a parser change that
|
||||
# breaks on input the seeds are close to. Long runs are done by hand; see
|
||||
@@ -93,7 +44,7 @@ jobs:
|
||||
env:
|
||||
RUSTFLAGS: ""
|
||||
run: |
|
||||
for target in message message_body attrs prefix_text config rtr netlink; do
|
||||
for target in message message_body prefix_text config; do
|
||||
mkdir -p "corpus/$target"
|
||||
cargo fuzz run "$target" "corpus/$target" "seeds/$target" -- -max_total_time=60
|
||||
done
|
||||
+1
-2
@@ -1,5 +1,4 @@
|
||||
/target
|
||||
__pycache__/
|
||||
.DS_Store
|
||||
.claude/
|
||||
/docs/public
|
||||
/docs/static/giallo.css
|
||||
Generated
+13
-151
@@ -157,22 +157,20 @@ checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5"
|
||||
|
||||
[[package]]
|
||||
name = "bgp"
|
||||
version = "0.2.6"
|
||||
version = "0.0.2"
|
||||
dependencies = [
|
||||
"bgp-server",
|
||||
"bgp-web",
|
||||
"clap",
|
||||
"eyre",
|
||||
"protobuf",
|
||||
"tikv-jemallocator",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bgp-bench"
|
||||
version = "0.2.6"
|
||||
version = "0.0.2"
|
||||
dependencies = [
|
||||
"bgp-integration-tests",
|
||||
"bgp-packet",
|
||||
@@ -182,38 +180,18 @@ dependencies = [
|
||||
"bytes",
|
||||
"flate2",
|
||||
"stats_alloc",
|
||||
"tikv-jemallocator",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bgp-fib"
|
||||
version = "0.2.6"
|
||||
dependencies = [
|
||||
"bgp-packet",
|
||||
"bgp-primitives",
|
||||
"bgp-rib",
|
||||
"bytes",
|
||||
"foldhash",
|
||||
"netlink-sys",
|
||||
"nix",
|
||||
"prometheus",
|
||||
"proptest",
|
||||
"thiserror 2.0.12",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bgp-integration-tests"
|
||||
version = "0.2.6"
|
||||
version = "0.0.2"
|
||||
dependencies = [
|
||||
"bgp-fib",
|
||||
"bgp-packet",
|
||||
"bgp-primitives",
|
||||
"bgp-rib",
|
||||
"bgp-rpki",
|
||||
"bgp-server",
|
||||
"bgp-web",
|
||||
"bytes",
|
||||
@@ -226,7 +204,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "bgp-packet"
|
||||
version = "0.2.6"
|
||||
version = "0.0.2"
|
||||
dependencies = [
|
||||
"bgp-primitives",
|
||||
"bitfield",
|
||||
@@ -242,19 +220,21 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "bgp-primitives"
|
||||
version = "0.2.6"
|
||||
version = "0.0.2"
|
||||
dependencies = [
|
||||
"bitfield",
|
||||
"bytes",
|
||||
"nom",
|
||||
"proptest",
|
||||
"serde",
|
||||
"serde_repr",
|
||||
"strum",
|
||||
"thiserror 2.0.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bgp-rib"
|
||||
version = "0.2.6"
|
||||
version = "0.0.2"
|
||||
dependencies = [
|
||||
"bgp-packet",
|
||||
"bgp-primitives",
|
||||
@@ -265,34 +245,19 @@ dependencies = [
|
||||
"serde",
|
||||
"thiserror 2.0.12",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bgp-rpki"
|
||||
version = "0.2.6"
|
||||
dependencies = [
|
||||
"bgp-primitives",
|
||||
"bytes",
|
||||
"foldhash",
|
||||
"proptest",
|
||||
"thiserror 2.0.12",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bgp-server"
|
||||
version = "0.2.6"
|
||||
version = "0.0.2"
|
||||
dependencies = [
|
||||
"bgp-fib",
|
||||
"bgp-packet",
|
||||
"bgp-primitives",
|
||||
"bgp-rib",
|
||||
"bgp-rpki",
|
||||
"bytes",
|
||||
"foldhash",
|
||||
"clap",
|
||||
"futures",
|
||||
"nix",
|
||||
"prometheus",
|
||||
"proptest",
|
||||
"protobuf",
|
||||
@@ -301,24 +266,21 @@ dependencies = [
|
||||
"thiserror 2.0.12",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bgp-web"
|
||||
version = "0.2.6"
|
||||
version = "0.0.2"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"bgp-fib",
|
||||
"bgp-packet",
|
||||
"bgp-primitives",
|
||||
"bgp-rib",
|
||||
"bgp-rpki",
|
||||
"bgp-server",
|
||||
"futures",
|
||||
"nix",
|
||||
"prometheus",
|
||||
"serde",
|
||||
"tikv-jemalloc-ctl",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tracing",
|
||||
@@ -377,28 +339,12 @@ version = "1.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f74872d07caf508b30a21f6836e7d7016a2eaf7d9ff4f48deaa58cd8a0407630"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "cfg_aliases"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.53"
|
||||
@@ -503,12 +449,6 @@ version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484"
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.1.10"
|
||||
@@ -978,15 +918,6 @@ version = "2.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
||||
|
||||
[[package]]
|
||||
name = "memoffset"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mime"
|
||||
version = "0.3.17"
|
||||
@@ -1014,32 +945,6 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "netlink-sys"
|
||||
version = "0.8.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd6c30ed10fa69cc491d491b85cc971f6bdeb8e7367b7cde2ee6cc878d583fae"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-util",
|
||||
"libc",
|
||||
"log",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.31.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
"memoffset",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "8.0.0"
|
||||
@@ -1102,12 +1007,6 @@ dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "paste"
|
||||
version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.2"
|
||||
@@ -1502,12 +1401,6 @@ dependencies = [
|
||||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.2"
|
||||
@@ -1689,37 +1582,6 @@ dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tikv-jemalloc-ctl"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a184c43b8ab2f41df2733b55556e3f5f632f4aeaa205b1bb018f574b7f5f142"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"paste",
|
||||
"tikv-jemalloc-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tikv-jemalloc-sys"
|
||||
version = "0.7.1+5.3.1-0-g81034ce1f1373e37dc865038e1bc8eeecf559ce8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a2825c78386b4ae0314074867860ba9577875de945f05992c38815cbec327f0"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tikv-jemallocator"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "249f09e49ab1609436f34c776e84231bead18d6a955f119f939bdc1d847561bd"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"tikv-jemalloc-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinystr"
|
||||
version = "0.8.4"
|
||||
|
||||
+1
-15
@@ -12,7 +12,7 @@ edition = "2024"
|
||||
homepage = "https://rayhaan.ch"
|
||||
license = "Apache-2.0"
|
||||
repository = "https://git.rayhaan.net/rayhaan/ferrix"
|
||||
version = "0.2.6"
|
||||
version = "0.0.2"
|
||||
|
||||
# Every crate opts in with `[lints] workspace = true`. CI turns warnings into
|
||||
# errors, so a lint added here is enforced.
|
||||
@@ -27,11 +27,9 @@ unimplemented = "warn"
|
||||
|
||||
[workspace.dependencies]
|
||||
# --- Our crates ---
|
||||
bgp-fib = { path = "crates/fib" }
|
||||
bgp-packet = { path = "crates/packet" }
|
||||
bgp-primitives = { path = "crates/primitives" }
|
||||
bgp-rib = { path = "crates/rib" }
|
||||
bgp-rpki = { path = "crates/rpki" }
|
||||
bgp-server = { path = "crates/server" }
|
||||
bgp-web = { path = "crates/web" }
|
||||
bgp-integration-tests = { path = "crates/integration-tests" }
|
||||
@@ -43,7 +41,6 @@ bytes = "1.10.1"
|
||||
eyre = "0.6.12"
|
||||
nom = "8.0.0"
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
nix = { version = "0.31", default-features = false, features = ["net"] }
|
||||
socket2 = "0.6"
|
||||
serde_json = "1.0.140"
|
||||
serde_repr = "0.1.20"
|
||||
@@ -63,14 +60,3 @@ hashbrown = "0.17"
|
||||
axum = "0.8.9"
|
||||
tokio-stream = { version = "0.1.19", features = ["sync"] }
|
||||
reqwest = { version = "0.13.5", default-features = false, features = ["json"] }
|
||||
# The daemon's memory allocator; see bin/bgp/src/main.rs. With its
|
||||
# statistics, which the status page and the metrics show.
|
||||
tikv-jemallocator = { version = "0.7.0", features = ["stats"] }
|
||||
tikv-jemalloc-ctl = { version = "0.7.0", features = ["stats"] }
|
||||
|
||||
# `cargo build --profile profiling`: the release build, with the line tables
|
||||
# a profiler needs to name functions. See scripts/profile/.
|
||||
[profile.profiling]
|
||||
inherits = "release"
|
||||
debug = "line-tables-only"
|
||||
strip = "none"
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
FROM rust:1.92-bookworm AS build
|
||||
WORKDIR /src
|
||||
COPY . .
|
||||
RUN cargo build --release --locked -p bgp
|
||||
RUN cargo build --release -p bgp
|
||||
|
||||
FROM debian:bookworm-slim
|
||||
COPY --from=build /src/target/release/bgp /usr/local/bin/bgp
|
||||
|
||||
@@ -1,193 +1,57 @@
|
||||
# Ferrix BGP
|
||||
# BGP
|
||||
|
||||
Ferrix is a BGP daemon for Linux routers, written in Rust. It exchanges
|
||||
routes with neighbors, filters them by policy and the RPKI, selects the
|
||||
best path for each prefix, installs it in the kernel, and passes routes on
|
||||
as BGP roles and communities permit. It aims to be small, safe, observable,
|
||||
and configured from one file that can be read by someone who did not write
|
||||
it.
|
||||
The `bgp` crate is an implementation of the Border Gateway Protocol, which is used to exchange routing and control plane information between Autonomous Systems.
|
||||
|
||||
## Status
|
||||
The aim of this project is to provide a modern, high performance, secure, programmatic and customizable software package that can be useful to network operators, researchers, and people interested in Internet.
|
||||
|
||||
Ferrix routes the author's network, AS210036: three routers carrying full
|
||||
tables from transit providers, route servers at several Internet exchanges,
|
||||
and bilateral peers, with routes reflected between the routers over iBGP.
|
||||
It is young software all the same. Read the [limitations](#not-supported)
|
||||
before relying on it, and treat version `0.x` as meaning that configuration
|
||||
and APIs may still change between releases.
|
||||
## Current status.
|
||||
|
||||
What it does:
|
||||
The project is currently being rewritten from scratch to be more modular and maintainable, as well as to fix deeper issues with the previous implementation.
|
||||
|
||||
- BGP-4 over IPv4 and IPv6, with four-octet AS numbers, multiprotocol
|
||||
reach for both families, IPv4 routes with IPv6 next hops (RFC 8950),
|
||||
route refresh, graceful restart and ADD-PATH receive.
|
||||
- Import policy per neighbor: prefix limits, prefix and origin AS lists,
|
||||
bogon checks, AS path checks, and communities added on the way in.
|
||||
- Route origin validation and ASPA path verification against an RTR cache.
|
||||
- BGP roles (RFC 9234) and the Only to Customer attribute, with export
|
||||
limits as a second line of defence against route leaks.
|
||||
- Best path selection and installation of the result in the Linux kernel
|
||||
over netlink, in a routing table of your choice.
|
||||
- Route reflection between routers of the same AS, and passive neighbors.
|
||||
- A status page, a JSON API, server-sent events, and Prometheus metrics.
|
||||
- Graceful restart of the daemon itself for upgrades, with routes left in
|
||||
the kernel while it is away.
|
||||
⚠️ Warning: This is currently a work in progress and not ready for use.
|
||||
|
||||
### Not supported
|
||||
### 🛣️ Roadmap to v1.0
|
||||
|
||||
- Per-client best path selection for route servers, and ADD-PATH for
|
||||
advertised routes
|
||||
- TCP-AO, TCP MD5, and GTSM
|
||||
- Long-Lived Graceful Restart, BFD, and configuration changes without a
|
||||
restart
|
||||
- Address families other than IPv4 and IPv6 unicast
|
||||
We aim for the 1.0 release to be functional and stable to operate peering sessions, and to install routes in the Linux Kernel.
|
||||
|
||||
## Building
|
||||
|
||||
You need a stable Rust toolchain from [rustup.rs](https://rustup.rs), and a
|
||||
C compiler and `make` for the jemalloc allocator the daemon uses (for
|
||||
example `build-essential` on Debian and Ubuntu, `base-devel` on Arch Linux,
|
||||
or the Xcode command line tools on macOS). The Protocol Buffers compiler is
|
||||
not needed: the configuration schema is compiled in pure Rust.
|
||||
|
||||
```bash
|
||||
git clone https://git.rayhaan.net/rayhaan/ferrix.git
|
||||
cd ferrix
|
||||
cargo build --release --locked -p bgp
|
||||
```
|
||||
|
||||
The binary is `target/release/bgp`. `--locked` builds with the dependency
|
||||
versions recorded in `Cargo.lock`, which are the versions CI tested.
|
||||
|
||||
To try it without touching any router, run the quick-start configuration.
|
||||
It listens on loopback, needs no privileges, and serves the status page at
|
||||
<http://127.0.0.1:8080>; the [quick start
|
||||
guide](docs/content/docs/getting-started/quick-start.md) adds a test peer
|
||||
that announces routes to it.
|
||||
|
||||
```bash
|
||||
target/release/bgp --check docs/examples/quick-start.textpb
|
||||
target/release/bgp docs/examples/quick-start.textpb
|
||||
```
|
||||
|
||||
Other ways to build:
|
||||
|
||||
- **A static Linux executable** that runs on any x86_64 distribution with
|
||||
nothing else installed: `scripts/build-static.sh`. It needs the
|
||||
`x86_64-unknown-linux-musl` target and a musl C compiler; the script's
|
||||
header says how to install them, including on macOS.
|
||||
- **A container image** with the provided `Dockerfile`:
|
||||
`docker build -t ferrix .`
|
||||
|
||||
Kernel route installation needs Linux and `CAP_NET_ADMIN`; everywhere else
|
||||
Ferrix is a BGP speaker only. The [installation
|
||||
guide](docs/content/docs/getting-started/installation.md) and the
|
||||
[deployment guide](docs/content/docs/operations/deployment.md) cover
|
||||
privileges, the systemd unit, and upgrading without withdrawing routes.
|
||||
|
||||
## Documentation
|
||||
|
||||
The documentation is a website built from [`docs/`](docs/) with
|
||||
[Zola](https://www.getzola.org): `zola --root docs serve`, then open
|
||||
<http://127.0.0.1:1111>. The sources are Markdown with Zola front matter,
|
||||
shortcodes and internal links, so the forge shows them with those parts
|
||||
unrendered; [`docs/README.md`](docs/README.md) explains what each is. The
|
||||
pages:
|
||||
|
||||
- [Overview](docs/content/docs/getting-started/introduction.md),
|
||||
[installation](docs/content/docs/getting-started/installation.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),
|
||||
[graceful restart](docs/content/docs/guides/graceful-restart.md),
|
||||
[configuration examples](docs/content/docs/guides/examples.md)
|
||||
- [Deployment](docs/content/docs/operations/deployment.md) and
|
||||
[monitoring](docs/content/docs/operations/monitoring.md)
|
||||
- Reference: [configuration](docs/content/docs/reference/configuration.md),
|
||||
[command line](docs/content/docs/reference/command-line.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),
|
||||
[testing](docs/content/docs/development/testing.md) and
|
||||
[contributing](docs/content/docs/development/contributing.md)
|
||||
|
||||
Complete configurations for an edge router, an exchange router and a
|
||||
transit customer are in [`docs/examples/`](docs/examples/); CI checks that
|
||||
each of them loads.
|
||||
|
||||
## Security
|
||||
|
||||
- The status page and HTTP API are not authenticated and show the whole
|
||||
configuration and routing table. Bind `http_endpoint` to a loopback
|
||||
address, or put it behind a proxy that authenticates.
|
||||
- Ferrix does not implement TCP-MD5 or TCP-AO. Filter TCP port 179 in the
|
||||
host firewall so that only configured neighbors can connect.
|
||||
- Everything read from the network, the RPKI cache, and the kernel is
|
||||
parsed without panicking; each parser has property tests and a fuzz
|
||||
target. `unsafe` code is forbidden throughout.
|
||||
|
||||
If you find a vulnerability, please contact the maintainer directly rather
|
||||
than opening a public issue.
|
||||
|
||||
## Layout
|
||||
|
||||
| Crate | What is in it |
|
||||
| -------------------------- | -------------------------------------------------------------------------- |
|
||||
| `crates/primitives` | Prefixes, address families, parsing helpers |
|
||||
| `crates/packet` | The wire format: messages, attributes, capabilities |
|
||||
| `crates/rib` | The routing table, best route selection, the journal of changes |
|
||||
| `crates/rpki` | The RTR client; origin validation and ASPA path verification |
|
||||
| `crates/fib` | The best routes in the Linux kernel, over netlink |
|
||||
| `crates/server` | Sessions, the state machine, policy, configuration |
|
||||
| `crates/web` | The status page, HTTP API and metrics |
|
||||
| `crates/integration-tests` | A test server and test peers, and the tests that run the daemon end to end |
|
||||
| `crates/bench` | Benchmarks at the scale of the Internet routing table |
|
||||
| `bin/bgp` | The daemon |
|
||||
- ☐ Implement basic parsers for the fundamental types required
|
||||
- ✅ Path Attributes from RFC4271
|
||||
- ✅ Open message
|
||||
- ✅ Capabilities from RFC4271
|
||||
- ✅ Update message
|
||||
- ☐ Notification message
|
||||
- ✅ Implement scaffolding required to start and configure a server
|
||||
- ✅ Implement connection logic to establish sessions with peers (passive and active)
|
||||
- ✅ Routes originated from the configuration, with communities, announced to all peers
|
||||
- ✅ Read-only HTTP API, status page and Prometheus metrics
|
||||
- ☐ Implement an efficient data structure for storing prefixes (e.g. Poptrie)
|
||||
- ☐ Filtering for peer RIB in / out
|
||||
- ☐ Main / auxiliary RIBs for exchanging routes between peers
|
||||
|
||||
## Development
|
||||
|
||||
CI runs these on every push, and fails on any warning:
|
||||
CI runs these three on every push, 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
|
||||
zola --root docs build
|
||||
cargo clippy --workspace --all-targets -- -D warnings
|
||||
cargo test --workspace
|
||||
```
|
||||
|
||||
It also runs the tests that need a real Linux kernel
|
||||
(`scripts/kernel-tests.sh`) and a lab that peers the daemon with FRR, BIRD
|
||||
and Routinator (`scripts/interop/lab.sh`); `scripts/linux-vm.sh` runs
|
||||
either from macOS in a Lima VM.
|
||||
Lints are configured once, under `[workspace.lints]` in the root `Cargo.toml`.
|
||||
`unsafe` code is forbidden throughout.
|
||||
|
||||
`cargo test` includes property tests: generated messages must survive being
|
||||
written and read back, and no input, however damaged, may make a parser
|
||||
panic. They take about a second; for a longer hunt:
|
||||
|
||||
```bash
|
||||
```
|
||||
PROPTEST_CASES=1000000 cargo test --release -p bgp-packet --test properties
|
||||
```
|
||||
|
||||
Coverage-guided fuzz targets for the same parsers are in `fuzz/`; see
|
||||
[`fuzz/README.md`](fuzz/README.md). CI runs each of them for a minute.
|
||||
|
||||
### Development with AI agents
|
||||
|
||||
Much of Ferrix is written with an AI coding agent (Claude Code) working
|
||||
alongside the maintainer, who directs the work, reviews every change, and
|
||||
runs the result on real routers. Nothing is merged on the agent's say-so:
|
||||
every change passes the checks above, the kernel tests and the
|
||||
interoperability lab, and the changes that matter are tried on the network
|
||||
first. Commits made with an agent carry a `Co-Authored-By` trailer that
|
||||
says so. If you contribute, you are welcome to work the same way, or not;
|
||||
what is reviewed is the change, not how it was produced.
|
||||
`fuzz/README.md`. CI runs each of them for a minute.
|
||||
|
||||
## License
|
||||
|
||||
Apache License 2.0; see [`LICENSE`](LICENSE).
|
||||
Apache License 2.0; see `LICENSE`.
|
||||
@@ -16,23 +16,6 @@ eyre.workspace = true
|
||||
protobuf.workspace = true
|
||||
tokio.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
|
||||
# jemalloc does not build with the MSVC toolchain; there the system
|
||||
# allocator is used.
|
||||
[target.'cfg(not(target_env = "msvc"))'.dependencies]
|
||||
tikv-jemallocator.workspace = true
|
||||
|
||||
# jemalloc's background thread returns freed memory to the kernel on time
|
||||
# (see the memory figures on the status page); jemalloc has it on Linux
|
||||
# only, and warns at every start elsewhere if asked for it.
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
tikv-jemallocator = { workspace = true, features = ["background_threads"] }
|
||||
|
||||
[features]
|
||||
# jemalloc's heap profiler, for scripts/profile/full-table.sh with
|
||||
# HEAP=1: not for a production build, where it costs memory and time.
|
||||
heap-profiling = ["tikv-jemallocator/profiling"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
+51
-11
@@ -1,17 +1,57 @@
|
||||
# bgp
|
||||
# BGP
|
||||
|
||||
The daemon: the BGP speaker, the RTR client, the kernel route installer and
|
||||
the status page, in one binary.
|
||||
This crate provides the `bgp` binary, which runs the Border Gateway Protocol
|
||||
daemon together with its read-only HTTP status interface.
|
||||
|
||||
```
|
||||
cargo run -p bgp -- --check docs/examples/quick-start.textpb
|
||||
cargo run -p bgp -- docs/examples/quick-start.textpb
|
||||
cargo run -- path/to/config.textpb
|
||||
```
|
||||
|
||||
How to configure, run and operate it is in the documentation, which is
|
||||
built from `docs/` (`zola --root docs serve`):
|
||||
The configuration format is defined in `crates/server/proto/config.proto`;
|
||||
`testdata/server.textpb` is a worked example. The daemon is configured
|
||||
entirely through that file: peers, and the routes it originates
|
||||
(`originate { prefix: ... }`). There is no way to change either at runtime.
|
||||
|
||||
- [Quick start](../../docs/content/docs/getting-started/quick-start.md)
|
||||
- [Configuration reference](../../docs/content/docs/reference/configuration.md)
|
||||
- [Deployment](../../docs/content/docs/operations/deployment.md) and
|
||||
[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` | Adj-RIB-In contents |
|
||||
| `GET /api/events` | Server-sent events for session and route changes |
|
||||
| `GET /metricz` | Prometheus metrics |
|
||||
|
||||
Everything under it is read-only and unauthenticated. Bind it to loopback or
|
||||
put it behind a reverse proxy.
|
||||
|
||||
## What it does today
|
||||
|
||||
It establishes sessions (both dialling configured peers and accepting
|
||||
connections, with RFC 4271 collision detection), announces its originated
|
||||
routes to every peer with itself as next hop, and records what peers send in
|
||||
an Adj-RIB-In. It does not select best paths, re-advertise received routes or
|
||||
program a forwarding table, and it has no import or export policy.
|
||||
|
||||
## 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).
|
||||
That makes originated prefixes show up in the received table as if they had
|
||||
travelled through a neighbouring AS:
|
||||
|
||||
```
|
||||
cargo run -- testdata/server.textpb
|
||||
cargo run -p bgp-integration-tests --example fake_peer -- \
|
||||
--server 127.0.0.1:179 --asn 65001 --announce 203.0.113.0/24 --echo
|
||||
```
|
||||
|
||||
Please refer to the other implementation crates in this repository where most
|
||||
of the actual implementation lives.
|
||||
|
||||
Warning: This is still very much a work in progress and is not yet ready for
|
||||
production use.
|
||||
@@ -1,36 +0,0 @@
|
||||
//! What the daemon logs, and how much.
|
||||
|
||||
use std::io::IsTerminal;
|
||||
|
||||
use clap::Args;
|
||||
use tracing::Level;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
use tracing_subscriber::filter::ParseError;
|
||||
|
||||
/// What the level given on the command line is for: this program and the
|
||||
/// crates it is made of, which all begin with `bgp`. Everything else (the
|
||||
/// HTTP server, say) stays at what `RUST_LOG` says, or silent.
|
||||
const OURS: &str = "bgp";
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
pub struct LoggingArgs {
|
||||
/// Set the minimum log level. At `debug` and below there is a line for
|
||||
/// every UPDATE and every route announced.
|
||||
#[arg(short, long, global = true, default_value_t = Level::INFO, help_heading = "Logging")]
|
||||
level: Level,
|
||||
}
|
||||
|
||||
impl LoggingArgs {
|
||||
pub fn install(&self) -> Result<(), ParseError> {
|
||||
let filter = EnvFilter::builder()
|
||||
.from_env_lossy()
|
||||
.add_directive(format!("{OURS}={}", self.level).parse()?);
|
||||
// Colours for a person at a terminal; plain text for the journal
|
||||
// or a file, where escape codes are noise.
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(filter)
|
||||
.with_ansi(std::io::stdout().is_terminal())
|
||||
.init();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
+21
-98
@@ -1,68 +1,34 @@
|
||||
use std::path::PathBuf;
|
||||
use std::process::ExitCode;
|
||||
use std::sync::Arc;
|
||||
|
||||
use bgp_server::config::Config;
|
||||
use bgp_server::proto::config::BgpConfig;
|
||||
use bgp_server::server::{BgpServer, leave_for_restart, shut_down};
|
||||
use bgp_web::{AppState, CommunityName, RouterInfo};
|
||||
use bgp_server::server::{BgpServer, close_sessions};
|
||||
use bgp_server::tracing::TracingArgs;
|
||||
use bgp_web::{AppState, RouterInfo};
|
||||
use clap::Parser;
|
||||
use eyre::{Context, Result};
|
||||
use tracing::{error, info};
|
||||
|
||||
use crate::logging::LoggingArgs;
|
||||
|
||||
mod logging;
|
||||
|
||||
// jemalloc, rather than the C library's allocator. Taking in a full table
|
||||
// allocates from many threads at once, which glibc's allocator handles
|
||||
// less well and musl's, in the static build, much worse.
|
||||
#[cfg(not(target_env = "msvc"))]
|
||||
#[global_allocator]
|
||||
static ALLOCATOR: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc;
|
||||
|
||||
/// The exit status after leaving to be started again (SIGUSR1):
|
||||
/// EX_TEMPFAIL, on which the systemd unit starts the daemon again
|
||||
/// (`RestartForceExitStatus=75`).
|
||||
const RESTART_EXIT_STATUS: u8 = 75;
|
||||
|
||||
/// A BGP speaker.
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(version)]
|
||||
struct ServerArgs {
|
||||
/// Path to the textproto configuration file.
|
||||
config: PathBuf,
|
||||
|
||||
/// Read the configuration, say whether it is in order, and stop:
|
||||
/// nothing is listened on and no route is touched.
|
||||
#[arg(long)]
|
||||
check: bool,
|
||||
config: String,
|
||||
|
||||
#[command(flatten)]
|
||||
logging: LoggingArgs,
|
||||
tracing: TracingArgs,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<ExitCode> {
|
||||
async fn main() -> Result<()> {
|
||||
let args = ServerArgs::parse();
|
||||
args.logging.install()?;
|
||||
args.tracing.install()?;
|
||||
log_panics();
|
||||
|
||||
let config_string = std::fs::read_to_string(&args.config)
|
||||
.wrap_err_with(|| format!("Failed to read {}", args.config.display()))?;
|
||||
let config_string =
|
||||
std::fs::read_to_string(&args.config).wrap_err("Failed to open config file")?;
|
||||
let proto_config = protobuf::text_format::parse_from_str::<BgpConfig>(&config_string)
|
||||
.wrap_err("Failed to parse config file")?;
|
||||
let config = Config::from_proto(proto_config).wrap_err("Invalid configuration")?;
|
||||
if args.check {
|
||||
println!(
|
||||
"{} is in order: AS{}, peers: {}, routes originated: {}",
|
||||
args.config.display(),
|
||||
config.local_asn,
|
||||
config.peers.len(),
|
||||
config.originate.len()
|
||||
);
|
||||
return Ok(ExitCode::SUCCESS);
|
||||
}
|
||||
|
||||
let server = BgpServer::bind(config).await?;
|
||||
|
||||
@@ -79,27 +45,11 @@ async fn main() -> Result<ExitCode> {
|
||||
server.config().local_asn,
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
)
|
||||
.with_config(args.config.to_string_lossy(), &config_string)
|
||||
.with_control_communities(server.config().control.is_some())
|
||||
.with_community_names(
|
||||
server
|
||||
.config()
|
||||
.community_names
|
||||
.iter()
|
||||
.map(|(community, name)| CommunityName {
|
||||
community: community.clone(),
|
||||
name: name.clone(),
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
.with_config(&args.config, &config_string),
|
||||
),
|
||||
registry: server.registry.clone(),
|
||||
local_rib: server.local_rib.clone(),
|
||||
rib: Arc::clone(&server.rib),
|
||||
rejections: server.rejections.clone(),
|
||||
rpki: server.rpki.clone(),
|
||||
kernel: server.kernel.clone(),
|
||||
restarts: Arc::clone(&server.restarts),
|
||||
};
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = bgp_web::serve_on(state, listener).await {
|
||||
@@ -111,30 +61,13 @@ async fn main() -> Result<ExitCode> {
|
||||
// Leave politely: tell every peer we are going (Cease / Administrative
|
||||
// Shutdown) rather than letting them find a dead TCP connection, which
|
||||
// makes them slower to take us back.
|
||||
// And take our routes out of the kernel: with us gone nothing would
|
||||
// keep them true.
|
||||
// Unless we are to be back at once (SIGUSR1, for an upgrade): then the
|
||||
// routes stay, and peers that agreed to graceful restart keep ours.
|
||||
let (registry, kernel) = (server.registry.clone(), server.kernel.clone());
|
||||
let registry = server.registry.clone();
|
||||
tokio::select! {
|
||||
result = server.run() => {
|
||||
result?;
|
||||
Ok(ExitCode::SUCCESS)
|
||||
}
|
||||
signal = signalled() => match signal {
|
||||
Signal::Stop(name) => {
|
||||
info!(signal = name, "Shutting down, closing BGP sessions");
|
||||
shut_down(®istry, kernel.as_ref()).await;
|
||||
Ok(ExitCode::SUCCESS)
|
||||
}
|
||||
Signal::Restart => {
|
||||
info!(
|
||||
signal = "SIGUSR1",
|
||||
"Leaving to be started again: routes stay in the kernel, and peers keep ours"
|
||||
);
|
||||
leave_for_restart(®istry, kernel.as_ref()).await;
|
||||
Ok(ExitCode::from(RESTART_EXIT_STATUS))
|
||||
}
|
||||
result = server.run() => Ok(result?),
|
||||
signal = shutdown_signal() => {
|
||||
info!(signal, "Shutting down, closing BGP sessions");
|
||||
close_sessions(®istry).await;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -161,30 +94,20 @@ fn log_panics() {
|
||||
}));
|
||||
}
|
||||
|
||||
/// What a signal asks of the daemon.
|
||||
enum Signal {
|
||||
/// To stop: SIGINT or SIGTERM, by name.
|
||||
Stop(&'static str),
|
||||
/// To leave, to be started again at once: SIGUSR1.
|
||||
Restart,
|
||||
}
|
||||
|
||||
/// Resolves with the first signal received that ends the daemon.
|
||||
async fn signalled() -> Signal {
|
||||
/// Resolves with the name of the first termination signal received.
|
||||
async fn shutdown_signal() -> &'static str {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use tokio::signal::unix::{SignalKind, signal};
|
||||
let mut term = signal(SignalKind::terminate()).expect("install SIGTERM handler");
|
||||
let mut restart = signal(SignalKind::user_defined1()).expect("install SIGUSR1 handler");
|
||||
tokio::select! {
|
||||
_ = tokio::signal::ctrl_c() => Signal::Stop("SIGINT"),
|
||||
_ = term.recv() => Signal::Stop("SIGTERM"),
|
||||
_ = restart.recv() => Signal::Restart,
|
||||
_ = tokio::signal::ctrl_c() => "SIGINT",
|
||||
_ = term.recv() => "SIGTERM",
|
||||
}
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
let _ = tokio::signal::ctrl_c().await;
|
||||
Signal::Stop("ctrl-c")
|
||||
"ctrl-c"
|
||||
}
|
||||
}
|
||||
+1
-11
@@ -16,14 +16,12 @@ bytes.workspace = true
|
||||
flate2 = "1"
|
||||
stats_alloc = { version = "0.1.10", optional = true }
|
||||
|
||||
[target.'cfg(not(target_env = "msvc"))'.dependencies]
|
||||
tikv-jemallocator.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
bgp-integration-tests.workspace = true
|
||||
bgp-rib.workspace = true
|
||||
bgp-server.workspace = true
|
||||
tokio.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
|
||||
[features]
|
||||
@@ -40,13 +38,5 @@ harness = false
|
||||
name = "real_table"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "transit"
|
||||
harness = false
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[[bench]]
|
||||
name = "mesh"
|
||||
harness = false
|
||||
@@ -3,8 +3,7 @@
|
||||
//!
|
||||
//! Run with `cargo bench -p bgp-bench --bench full_table`. Set
|
||||
//! `BENCH_PEERS` (default 4) and `BENCH_SCALE_DOWN` (default 1; 10 makes the
|
||||
//! table ten times smaller) to change the size. `BENCH_PRESIZE=0` leaves
|
||||
//! the table to grow as it fills, as it does without `expected_routes`. The table is synthetic (see
|
||||
//! table ten times smaller) to change the size. The table is synthetic (see
|
||||
//! the crate documentation); `real_table` runs on a real one.
|
||||
//!
|
||||
//! This is a stopwatch around whole operations at full size, not a
|
||||
@@ -18,8 +17,7 @@ use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use bgp_bench::memory::{Region, describe};
|
||||
use bgp_bench::report::{mib, per_second, row};
|
||||
use bgp_bench::{Prefixes, Shape, announcements, encoded, env, wire_context};
|
||||
use bgp_bench::{Prefixes, Shape, announcements, encoded, wire_context};
|
||||
use bgp_integration_tests::test_peer::TestPeer;
|
||||
use bgp_integration_tests::test_server::TestServer;
|
||||
use bgp_packet::message::{Message, PathAttribute};
|
||||
@@ -27,6 +25,30 @@ use bgp_primitives::ip_prefix::IpPrefix;
|
||||
use bgp_rib::table::{PeerIndex, PeerInfo, RibTable};
|
||||
use bytes::BytesMut;
|
||||
|
||||
fn env(name: &str, default: usize) -> usize {
|
||||
std::env::var(name)
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(default)
|
||||
}
|
||||
|
||||
fn mib(bytes: usize) -> f64 {
|
||||
bytes as f64 / (1024.0 * 1024.0)
|
||||
}
|
||||
|
||||
fn per_second(count: usize, elapsed: Duration) -> String {
|
||||
let rate = count as f64 / elapsed.as_secs_f64();
|
||||
if rate >= 1e6 {
|
||||
format!("{:.2} M/s", rate / 1e6)
|
||||
} else {
|
||||
format!("{:.0} k/s", rate / 1e3)
|
||||
}
|
||||
}
|
||||
|
||||
fn row(what: &str, elapsed: Duration, detail: String) {
|
||||
println!("{what:<44} {:>9.3} s {detail}", elapsed.as_secs_f64());
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let peers = env("BENCH_PEERS", 4) as u32;
|
||||
let shape = Shape::INTERNET.scaled_down(env("BENCH_SCALE_DOWN", 1));
|
||||
@@ -83,9 +105,7 @@ fn main() {
|
||||
.build()
|
||||
.expect("a runtime");
|
||||
runtime.block_on(async {
|
||||
// An AS number that is in none of the generated paths: a route
|
||||
// with the server's own AS in its path is refused as a loop.
|
||||
let server = TestServer::start(4_200_000_000).await.expect("a server");
|
||||
let server = TestServer::start(65000).await.expect("a server");
|
||||
let mut senders = Vec::new();
|
||||
for (index, messages) in wire.iter().enumerate() {
|
||||
let peer = index as u32 + 1;
|
||||
@@ -94,8 +114,6 @@ fn main() {
|
||||
.handshake(64512 + peer, Ipv4Addr::new(10, 0, 0, peer as u8), 90)
|
||||
.await
|
||||
.expect("a session");
|
||||
// The same bytes `table_stream` would make, from the messages
|
||||
// already encoded for the decoding above.
|
||||
let stream: Vec<u8> = messages.iter().flat_map(|m| m.iter().copied()).collect();
|
||||
senders.push((session, stream));
|
||||
}
|
||||
@@ -135,9 +153,6 @@ fn main() {
|
||||
fn into_table(wire: &[Vec<BytesMut>], routes: usize, threads: usize) {
|
||||
let region = Region::begin();
|
||||
let table = RibTable::new(threads * 16);
|
||||
if env("BENCH_PRESIZE", 1) == 1 {
|
||||
table.expect_prefixes(routes / wire.len());
|
||||
}
|
||||
let peers: Vec<PeerIndex> = (0..wire.len())
|
||||
.map(|peer| {
|
||||
table
|
||||
@@ -145,9 +160,7 @@ fn into_table(wire: &[Vec<BytesMut>], routes: usize, threads: usize) {
|
||||
asn: 64512 + peer as u32 + 1,
|
||||
router_id: Ipv4Addr::from(peer as u32 + 1),
|
||||
address: Ipv4Addr::new(192, 0, 2, peer as u8 + 1).into(),
|
||||
interface: 0,
|
||||
ibgp: false,
|
||||
reflector_client: false,
|
||||
})
|
||||
.expect("room for the peer")
|
||||
})
|
||||
|
||||
@@ -1,659 +0,0 @@
|
||||
//! Two routers of one AS, each taking in full tables from its own peers
|
||||
//! and sending the other every path (ADD-PATH both ways, `all_paths`), as
|
||||
//! a mesh of routers that each hold the network's whole view do. How
|
||||
//! long until each router has the other's routes is what this measures:
|
||||
//! the exporter reading the table's journal while sessions fill the
|
||||
//! table, which nothing else exercises at this size.
|
||||
//!
|
||||
//! Then a new peer takes the first router's table, as a collector or a
|
||||
//! router coming back does: how soon its first UPDATE arrives and how
|
||||
//! long the table takes are measured too. It is a peer of the routers'
|
||||
//! AS over IPv4, so what it gets is the first router's IPv4 routes from
|
||||
//! its own peers: the other router's are not reflected to it, and IPv6
|
||||
//! routes need an IPv6 session.
|
||||
//!
|
||||
//! Run with `cargo bench -p bgp-bench --bench mesh`. `BENCH_PEERS`
|
||||
//! (default 4) feed the first router and `BENCH_PEERS_B` (default 2) the
|
||||
//! second; `BENCH_SCALE_DOWN` shrinks the table. The routers peer over
|
||||
//! IPv6 loopback and the feeders come over IPv4 loopback; the new peer
|
||||
//! comes from 127.0.0.2, which Linux has and macOS does not, so that
|
||||
//! part is skipped on macOS. With `BENCH_RECEIVER_EARLY=1` the new peer
|
||||
//! comes as the feeders start instead, so that its exporter goes
|
||||
//! through the table while the table is being filled, as a router's
|
||||
//! do after a restart. `BENCH_COLLECTORS` (default 0) more peers of the
|
||||
//! first router, from 127.0.1.0/24, are peers in the sense of RFC 9234
|
||||
//! told of learned routes, and the feeders' routes are marked as having
|
||||
//! come across from a peer, so none of them goes: as many exporters
|
||||
//! following the table while it fills and passing nearly all of it
|
||||
//! over, as a router with many peers at exchanges has.
|
||||
|
||||
use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr};
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use bgp_bench::report::row;
|
||||
use bgp_bench::{Prefixes, Shape, announcements, encoded, env, table_stream};
|
||||
use bgp_integration_tests::peer_config;
|
||||
use bgp_integration_tests::test_peer::{TestPeer, collector_capabilities};
|
||||
use bgp_integration_tests::test_server::TestServer;
|
||||
use bgp_packet::attributes::PathAttribute;
|
||||
use bgp_packet::message::msg_type;
|
||||
use bgp_packet::open::SendReceive;
|
||||
use bgp_server::config::PeerConfig;
|
||||
use bgp_server::family::SUPPORTED;
|
||||
use bgp_server::policy::{ExportPolicy, ImportPolicy, PeerRole};
|
||||
use bgp_server::session::SessionRegistry;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt, BufReader};
|
||||
use tokio::net::tcp::OwnedWriteHalf;
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
/// An AS number that is in none of the synthetic paths.
|
||||
const ASN: u32 = 4_200_000_000;
|
||||
|
||||
/// Where the peer that takes the whole table connects from.
|
||||
const RECEIVER: Ipv4Addr = Ipv4Addr::new(127, 0, 0, 2);
|
||||
|
||||
/// Where the collectors connect from: this and the addresses after it.
|
||||
fn collector_address(n: u32) -> Ipv4Addr {
|
||||
Ipv4Addr::new(127, 0, 1, n as u8)
|
||||
}
|
||||
|
||||
/// The collectors' AS numbers start here.
|
||||
const COLLECTOR_ASN: u32 = 65000;
|
||||
|
||||
/// How often the tables are looked at while they fill.
|
||||
const POLL: Duration = Duration::from_millis(5);
|
||||
|
||||
/// What the routers send each other, and the receiver: every path.
|
||||
fn every_path() -> Arc<ExportPolicy> {
|
||||
Arc::new(ExportPolicy {
|
||||
learned_routes: true,
|
||||
all_paths: true,
|
||||
..ExportPolicy::default()
|
||||
})
|
||||
}
|
||||
|
||||
fn free_port() -> u16 {
|
||||
std::net::TcpListener::bind((Ipv6Addr::LOCALHOST, 0))
|
||||
.expect("a port")
|
||||
.local_addr()
|
||||
.expect("an address")
|
||||
.port()
|
||||
}
|
||||
|
||||
/// A router of the mesh, listening for feeders on IPv4 loopback and for
|
||||
/// the other router on IPv6 loopback at `port`, which it dials at
|
||||
/// `other_port`, knowing it as `other_name`.
|
||||
async fn router(
|
||||
other_name: &'static str,
|
||||
id: u8,
|
||||
port: u16,
|
||||
other_port: u16,
|
||||
with_receiver: bool,
|
||||
collectors: u32,
|
||||
) -> TestServer {
|
||||
TestServer::start_with(ASN, |c| {
|
||||
// Two routers of one AS must not share a BGP identifier.
|
||||
c.router_id = Ipv4Addr::new(192, 0, 2, id);
|
||||
c.cluster_id = c.router_id;
|
||||
c.listen_endpoints = vec![
|
||||
SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 0),
|
||||
SocketAddr::new(Ipv6Addr::LOCALHOST.into(), port),
|
||||
];
|
||||
c.peers = vec![PeerConfig {
|
||||
add_path: Some(SendReceive::Both),
|
||||
import: ImportPolicy::permissive(),
|
||||
export: every_path(),
|
||||
..peer_config(
|
||||
other_name,
|
||||
SocketAddr::new(Ipv6Addr::LOCALHOST.into(), other_port),
|
||||
ASN,
|
||||
)
|
||||
}];
|
||||
if with_receiver {
|
||||
c.peers.push(PeerConfig {
|
||||
add_path: Some(SendReceive::Send),
|
||||
export: every_path(),
|
||||
..peer_config("receiver", SocketAddr::new(RECEIVER.into(), 179), ASN)
|
||||
});
|
||||
}
|
||||
for n in 1..=collectors {
|
||||
c.peers.push(PeerConfig {
|
||||
role: Some(PeerRole::Peer),
|
||||
export: Arc::new(ExportPolicy {
|
||||
learned_routes: true,
|
||||
all_paths: false,
|
||||
..ExportPolicy::default()
|
||||
}),
|
||||
..peer_config(
|
||||
"collector",
|
||||
SocketAddr::new(collector_address(n).into(), 179),
|
||||
COLLECTOR_ASN + n,
|
||||
)
|
||||
});
|
||||
}
|
||||
c.connect_retry = Duration::from_millis(200);
|
||||
})
|
||||
.await
|
||||
.expect("a router")
|
||||
}
|
||||
|
||||
/// Connects `count` feeders to `router`, numbered from `first`, and
|
||||
/// returns their sessions with the bytes each is to send.
|
||||
async fn feeders(
|
||||
router: &TestServer,
|
||||
prefixes: &Prefixes,
|
||||
shape: Shape,
|
||||
first: u32,
|
||||
count: u32,
|
||||
marked: bool,
|
||||
) -> Vec<(TestPeer, Vec<u8>)> {
|
||||
let mut sessions = Vec::new();
|
||||
for peer in first..first + count {
|
||||
let stream = if marked {
|
||||
// As having come across from a peer (RFC 9234): not for the
|
||||
// collectors, which are peers too.
|
||||
let mut updates = announcements(prefixes, shape, peer);
|
||||
for update in &mut updates {
|
||||
update
|
||||
.path_attributes
|
||||
.push(PathAttribute::OnlyToCustomer(64512 + peer));
|
||||
}
|
||||
encoded(&updates)
|
||||
.iter()
|
||||
.flat_map(|m| m.iter().copied())
|
||||
.collect()
|
||||
} else {
|
||||
table_stream(prefixes, shape, peer)
|
||||
};
|
||||
let mut session = TestPeer::connect(router.addr).await.expect("a connection");
|
||||
// No hold timer: a feeder has nothing to say after its table.
|
||||
session
|
||||
.handshake(64512 + peer, Ipv4Addr::new(10, 0, 0, peer as u8), 0)
|
||||
.await
|
||||
.expect("a session");
|
||||
sessions.push((session, stream));
|
||||
}
|
||||
sessions
|
||||
}
|
||||
|
||||
/// Every session of `registry`, one line each, for the panic when a
|
||||
/// deadline passes: where the routes have got to, and what each session
|
||||
/// was to be sent.
|
||||
fn sessions_report(registry: &SessionRegistry) -> Vec<String> {
|
||||
registry
|
||||
.list()
|
||||
.iter()
|
||||
.map(|s| {
|
||||
format!(
|
||||
"{} {:?} export {:?} add_path_send {:?} routes {} rejected {} advertised {}",
|
||||
s.peer,
|
||||
s.name,
|
||||
s.export,
|
||||
s.add_path_send,
|
||||
s.counters.routes(),
|
||||
s.counters.rejected(),
|
||||
s.counters.advertised()
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// How big a run is, from the environment.
|
||||
struct Settings {
|
||||
peers_a: u32,
|
||||
peers_b: u32,
|
||||
shape: Shape,
|
||||
collectors: u32,
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
fn from_env() -> Self {
|
||||
Self {
|
||||
peers_a: env("BENCH_PEERS", 4) as u32,
|
||||
peers_b: env("BENCH_PEERS_B", 2) as u32,
|
||||
shape: Shape::INTERNET.scaled_down(env("BENCH_SCALE_DOWN", 1)),
|
||||
collectors: env("BENCH_COLLECTORS", 0) as u32,
|
||||
}
|
||||
}
|
||||
|
||||
/// How many routes A's own feeders bring, and B's.
|
||||
fn own_routes(&self) -> (usize, usize) {
|
||||
let per_peer = self.shape.prefixes();
|
||||
(
|
||||
per_peer * self.peers_a as usize,
|
||||
per_peer * self.peers_b as usize,
|
||||
)
|
||||
}
|
||||
|
||||
/// How many routes the receiver is to be sent: A's IPv4 routes from
|
||||
/// its own peers.
|
||||
fn receiver_routes(&self) -> usize {
|
||||
self.shape.ipv4_prefixes * self.peers_a as usize
|
||||
}
|
||||
}
|
||||
|
||||
/// The two routers, up and peered, with the collectors' sessions once
|
||||
/// they are connected.
|
||||
struct Mesh {
|
||||
settings: Settings,
|
||||
prefixes: Prefixes,
|
||||
a: TestServer,
|
||||
b: TestServer,
|
||||
/// The collectors' connections, drained, held until the end.
|
||||
collectors: Vec<(OwnedWriteHalf, JoinHandle<()>)>,
|
||||
}
|
||||
|
||||
/// The feeders sending, from when they began.
|
||||
struct Feeding {
|
||||
started: Instant,
|
||||
/// Each hands its session back once its table is written.
|
||||
sending: Vec<JoinHandle<TestPeer>>,
|
||||
}
|
||||
|
||||
/// When each router had what: its own peers' routes, and the other's too.
|
||||
struct Tables {
|
||||
a_own: Duration,
|
||||
b_own: Duration,
|
||||
a_all: Duration,
|
||||
b_all: Duration,
|
||||
}
|
||||
|
||||
impl Mesh {
|
||||
/// Starts the routers, with the collectors and receiver configured at
|
||||
/// A, and waits for them to peer.
|
||||
async fn start(settings: Settings, prefixes: Prefixes) -> Self {
|
||||
let (port_a, port_b) = (free_port(), free_port());
|
||||
let a = router("b", 1, port_a, port_b, true, settings.collectors).await;
|
||||
let b = router("a", 2, port_b, port_a, false, 0).await;
|
||||
a.wait_for_sessions(1).await.expect("the mesh session at A");
|
||||
b.wait_for_sessions(1).await.expect("the mesh session at B");
|
||||
Self {
|
||||
settings,
|
||||
prefixes,
|
||||
a,
|
||||
b,
|
||||
collectors: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Connects the collectors to A. They are there before the feeders, as
|
||||
/// a router's peers are before its tables arrive after a restart. An
|
||||
/// address that is not this machine's ends the connecting, with a
|
||||
/// word.
|
||||
async fn connect_collectors(&mut self) {
|
||||
for n in 1..=self.settings.collectors {
|
||||
let socket = tokio::net::TcpSocket::new_v4().expect("a socket");
|
||||
if let Err(e) = socket.bind(SocketAddr::new(collector_address(n).into(), 0)) {
|
||||
println!(
|
||||
"collectors: skipped, {} is not ours ({e})",
|
||||
collector_address(n)
|
||||
);
|
||||
break;
|
||||
}
|
||||
let stream = socket.connect(self.a.addr).await.expect("A listens");
|
||||
let mut session = TestPeer::from_stream(stream);
|
||||
session
|
||||
.handshake(COLLECTOR_ASN + n, Ipv4Addr::new(192, 0, 3, n as u8), 0)
|
||||
.await
|
||||
.expect("A takes a collector's session");
|
||||
self.collectors.push(session.drain());
|
||||
}
|
||||
if !self.collectors.is_empty() {
|
||||
self.a
|
||||
.wait_for_sessions(1 + self.collectors.len())
|
||||
.await
|
||||
.expect("the collectors' sessions at A");
|
||||
}
|
||||
}
|
||||
|
||||
/// Connects the feeders to both routers and starts them sending.
|
||||
async fn feed(&self) -> Feeding {
|
||||
let Settings {
|
||||
peers_a,
|
||||
peers_b,
|
||||
shape,
|
||||
..
|
||||
} = self.settings;
|
||||
let marked = !self.collectors.is_empty();
|
||||
let feed_a = feeders(&self.a, &self.prefixes, shape, 1, peers_a, marked).await;
|
||||
let feed_b = feeders(&self.b, &self.prefixes, shape, 1 + peers_a, peers_b, false).await;
|
||||
let started = Instant::now();
|
||||
let sending = feed_a
|
||||
.into_iter()
|
||||
.chain(feed_b)
|
||||
.map(|(mut session, stream)| {
|
||||
tokio::spawn(async move {
|
||||
session.send_raw(&stream).await.expect("the router reads");
|
||||
session
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
Feeding { started, sending }
|
||||
}
|
||||
|
||||
/// The receiver taking A's table while it fills, if `BENCH_RECEIVER_EARLY`
|
||||
/// asks for that: as the feeders start (1), or once A has its own
|
||||
/// peers' routes and B's are still coming (2), as a router's sessions
|
||||
/// come up after a restart while its peers' tables arrive.
|
||||
fn early_receiver(&self, deadline: Duration) -> Option<JoinHandle<()>> {
|
||||
let when = env("BENCH_RECEIVER_EARLY", 0);
|
||||
if when == 0 {
|
||||
return None;
|
||||
}
|
||||
let (own_a, _) = self.settings.own_routes();
|
||||
let routes = self.settings.receiver_routes();
|
||||
let (addr, registry, rib) = (
|
||||
self.a.addr,
|
||||
self.a.registry.clone(),
|
||||
Arc::clone(&self.a.rib),
|
||||
);
|
||||
Some(tokio::spawn(async move {
|
||||
while when == 2 && rib.len() < own_a {
|
||||
tokio::time::sleep(POLL).await;
|
||||
}
|
||||
resync(addr, registry, routes, deadline).await
|
||||
}))
|
||||
}
|
||||
|
||||
/// Waits until both routers have every route, noting when each had
|
||||
/// what; or panics once `deadline` has passed since `started`.
|
||||
async fn wait_for_tables(&self, started: Instant, deadline: Duration) -> Tables {
|
||||
let (own_a, own_b) = self.settings.own_routes();
|
||||
let mut a_own = None;
|
||||
let mut b_own = None;
|
||||
let mut a_all = None;
|
||||
let mut b_all = None;
|
||||
let (a_all, b_all) = loop {
|
||||
tokio::time::sleep(POLL).await;
|
||||
let (at_a, at_b) = (self.a.rib.len(), self.b.rib.len());
|
||||
let now = started.elapsed();
|
||||
if a_own.is_none() && at_a >= own_a {
|
||||
a_own = Some(now);
|
||||
}
|
||||
if b_own.is_none() && at_b >= own_b {
|
||||
b_own = Some(now);
|
||||
}
|
||||
if a_all.is_none() && at_a >= own_a + own_b {
|
||||
a_all = Some(now);
|
||||
}
|
||||
if b_all.is_none() && at_b >= own_a + own_b {
|
||||
b_all = Some(now);
|
||||
}
|
||||
if let (Some(a_all), Some(b_all)) = (a_all, b_all) {
|
||||
break (a_all, b_all);
|
||||
}
|
||||
if now > deadline {
|
||||
panic!(
|
||||
"not all routes arrived: A has {at_a} {:?}, B has {at_b} {:?}",
|
||||
sessions_report(&self.a.registry),
|
||||
sessions_report(&self.b.registry)
|
||||
);
|
||||
}
|
||||
};
|
||||
Tables {
|
||||
a_own: a_own.unwrap_or_default(),
|
||||
b_own: b_own.unwrap_or_default(),
|
||||
a_all,
|
||||
b_all,
|
||||
}
|
||||
}
|
||||
|
||||
/// One row for each of the four moments of `tables`, with the rate the
|
||||
/// routes that made the moment came at.
|
||||
fn report(&self, tables: &Tables) {
|
||||
let (own_a, own_b) = self.settings.own_routes();
|
||||
let rate = |when: Duration, routes: usize| {
|
||||
format!("{:.2} M routes/s", routes as f64 / when.as_secs_f64() / 1e6)
|
||||
};
|
||||
row(
|
||||
"A has its own peers' routes",
|
||||
tables.a_own,
|
||||
rate(tables.a_own, own_a),
|
||||
);
|
||||
row(
|
||||
"B has its own peers' routes",
|
||||
tables.b_own,
|
||||
rate(tables.b_own, own_b),
|
||||
);
|
||||
row(
|
||||
"A has B's routes too (B's exporter, A's session)",
|
||||
tables.a_all,
|
||||
rate(tables.a_all, own_b),
|
||||
);
|
||||
row(
|
||||
"B has A's routes too (A's exporter, B's session)",
|
||||
tables.b_all,
|
||||
rate(tables.b_all, own_a),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl Feeding {
|
||||
/// Waits for every feeder to have written its table, and returns the
|
||||
/// sessions, which are to stay up until the end: a feeder going would
|
||||
/// have its routes withdrawn from both tables meanwhile.
|
||||
async fn finished(self) -> Vec<TestPeer> {
|
||||
let mut feeders = Vec::new();
|
||||
for session in self.sending {
|
||||
feeders.push(session.await.expect("a feeder is done"));
|
||||
}
|
||||
feeders
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// The routers' logs, with `BENCH_LOG` as a `RUST_LOG` filter.
|
||||
if let Ok(filter) = std::env::var("BENCH_LOG") {
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(filter)
|
||||
.with_writer(std::io::stderr)
|
||||
.init();
|
||||
}
|
||||
let settings = Settings::from_env();
|
||||
let deadline = Duration::from_secs(env("BENCH_DEADLINE", 600) as u64);
|
||||
println!(
|
||||
"== Two routers of AS{ASN}, every path both ways: A fed by {} peers, B by {}, {} prefixes each",
|
||||
settings.peers_a,
|
||||
settings.peers_b,
|
||||
settings.shape.prefixes()
|
||||
);
|
||||
let prefixes = Prefixes::generate(settings.shape);
|
||||
|
||||
let runtime = tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.expect("a runtime");
|
||||
runtime.block_on(async {
|
||||
let mut mesh = Mesh::start(settings, prefixes).await;
|
||||
mesh.connect_collectors().await;
|
||||
let early_receiver = mesh.early_receiver(deadline);
|
||||
let feeding = mesh.feed().await;
|
||||
let tables = mesh.wait_for_tables(feeding.started, deadline).await;
|
||||
mesh.report(&tables);
|
||||
let feeders = feeding.finished().await;
|
||||
match early_receiver {
|
||||
Some(receiver) => receiver.await.expect("the receiver is done"),
|
||||
None => {
|
||||
resync(
|
||||
mesh.a.addr,
|
||||
mesh.a.registry.clone(),
|
||||
mesh.settings.receiver_routes(),
|
||||
deadline,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
drop(feeders);
|
||||
drop(mesh);
|
||||
});
|
||||
}
|
||||
|
||||
/// The new peer taking a router's table: counting the UPDATEs it is sent
|
||||
/// while keeping the session alive.
|
||||
struct Receiver {
|
||||
started: Instant,
|
||||
/// When the first UPDATE arrived, in microseconds after `started`; 0
|
||||
/// until it has.
|
||||
first: Arc<AtomicU64>,
|
||||
updates: Arc<AtomicU64>,
|
||||
counting: JoinHandle<()>,
|
||||
keepalives: JoinHandle<()>,
|
||||
}
|
||||
|
||||
impl Receiver {
|
||||
/// A session with the router at `addr` from [`RECEIVER`], as a
|
||||
/// collector of every path; or `None`, with a word, where that address
|
||||
/// is not this machine's to connect from.
|
||||
async fn connect(addr: SocketAddr) -> Option<TestPeer> {
|
||||
let socket = tokio::net::TcpSocket::new_v4().expect("a socket");
|
||||
if let Err(e) = socket.bind(SocketAddr::new(RECEIVER.into(), 0)) {
|
||||
println!("A new peer taking A's routes: skipped, {RECEIVER} is not ours ({e})");
|
||||
return None;
|
||||
}
|
||||
let stream = socket.connect(addr).await.expect("A listens");
|
||||
let mut session = TestPeer::from_stream(stream);
|
||||
session
|
||||
.handshake_with(
|
||||
ASN,
|
||||
Ipv4Addr::new(192, 0, 2, 9),
|
||||
90,
|
||||
collector_capabilities(ASN, &SUPPORTED, false),
|
||||
)
|
||||
.await
|
||||
.expect("A takes the receiver's session");
|
||||
Some(session)
|
||||
}
|
||||
|
||||
/// Starts counting what `session` is sent, from now. Every millionth
|
||||
/// UPDATE is noted, as is a NOTIFICATION or the connection closing:
|
||||
/// what went wrong, if the table never all arrives.
|
||||
fn count_updates(session: TestPeer) -> Self {
|
||||
let started = Instant::now();
|
||||
let (reader, mut writer) = session.into_stream().into_split();
|
||||
// KEEPALIVEs, so that the session outlives the hold time.
|
||||
let keepalives = tokio::spawn(async move {
|
||||
let keepalive = TestPeer::keepalive_bytes();
|
||||
loop {
|
||||
tokio::time::sleep(Duration::from_secs(20)).await;
|
||||
if writer.write_all(&keepalive).await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
let first = Arc::new(AtomicU64::new(0));
|
||||
let updates = Arc::new(AtomicU64::new(0));
|
||||
let counting = tokio::spawn({
|
||||
let (first, updates) = (Arc::clone(&first), Arc::clone(&updates));
|
||||
async move {
|
||||
let mut reader = BufReader::with_capacity(1 << 20, reader);
|
||||
let mut header = [0u8; 19];
|
||||
let mut body = vec![0u8; 4096];
|
||||
while reader.read_exact(&mut header).await.is_ok() {
|
||||
let length = usize::from(u16::from_be_bytes([header[16], header[17]]));
|
||||
if reader.read_exact(&mut body[..length - 19]).await.is_err() {
|
||||
break;
|
||||
}
|
||||
match header[18] {
|
||||
msg_type::UPDATE => {
|
||||
let _ = first.compare_exchange(
|
||||
0,
|
||||
started.elapsed().as_micros() as u64,
|
||||
Ordering::Relaxed,
|
||||
Ordering::Relaxed,
|
||||
);
|
||||
let n = updates.fetch_add(1, Ordering::Relaxed) + 1;
|
||||
if n % 1_000_000 == 0 {
|
||||
println!(
|
||||
" the receiver has read {n} UPDATEs after {:.2} s",
|
||||
started.elapsed().as_secs_f64()
|
||||
);
|
||||
}
|
||||
}
|
||||
msg_type::NOTIFICATION => println!(
|
||||
"the receiver was sent a NOTIFICATION, error {} subcode {}, after {} UPDATEs",
|
||||
body[0],
|
||||
body[1],
|
||||
updates.load(Ordering::Relaxed)
|
||||
),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
println!(
|
||||
"the receiver's connection closed after {} UPDATEs",
|
||||
updates.load(Ordering::Relaxed)
|
||||
);
|
||||
}
|
||||
});
|
||||
Self {
|
||||
started,
|
||||
first,
|
||||
updates,
|
||||
counting,
|
||||
keepalives,
|
||||
}
|
||||
}
|
||||
|
||||
/// Waits until the router whose sessions are in `registry` has
|
||||
/// advertised `routes` routes to the receiver, and returns how many it
|
||||
/// has; or panics once `deadline` has passed.
|
||||
async fn wait_for_advertised(
|
||||
&self,
|
||||
registry: &SessionRegistry,
|
||||
routes: usize,
|
||||
deadline: Duration,
|
||||
) -> u64 {
|
||||
loop {
|
||||
tokio::time::sleep(POLL).await;
|
||||
let advertised = registry
|
||||
.list()
|
||||
.iter()
|
||||
.find(|s| s.peer.addr.ip() == RECEIVER)
|
||||
.map_or(0, |s| s.counters.advertised());
|
||||
if advertised as usize >= routes {
|
||||
return advertised;
|
||||
}
|
||||
if self.started.elapsed() > deadline {
|
||||
panic!(
|
||||
"the receiver has {advertised} of {routes} routes and read {} UPDATEs; sessions at A: {:#?}",
|
||||
self.updates.load(Ordering::Relaxed),
|
||||
sessions_report(registry)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Prints the row for the table being all there, `advertised` routes
|
||||
/// of it, and stops counting.
|
||||
fn report(self, advertised: u64) {
|
||||
let took = self.started.elapsed();
|
||||
self.counting.abort();
|
||||
self.keepalives.abort();
|
||||
let first = Duration::from_micros(self.first.load(Ordering::Relaxed));
|
||||
row(
|
||||
"A new peer has A's IPv4 routes (A's exporter)",
|
||||
took,
|
||||
format!(
|
||||
"{:.2} M routes/s first UPDATE after {:.0} ms, {} UPDATEs",
|
||||
advertised as f64 / took.as_secs_f64() / 1e6,
|
||||
first.as_secs_f64() * 1e3,
|
||||
self.updates.load(Ordering::Relaxed),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A new peer takes the table of the router at `addr`, whose sessions
|
||||
/// are in `registry`: `routes` routes with every path. How long until
|
||||
/// the first UPDATE, and until all of it is sent.
|
||||
async fn resync(addr: SocketAddr, registry: SessionRegistry, routes: usize, deadline: Duration) {
|
||||
let Some(session) = Receiver::connect(addr).await else {
|
||||
return;
|
||||
};
|
||||
let receiver = Receiver::count_updates(session);
|
||||
let advertised = receiver
|
||||
.wait_for_advertised(®istry, routes, deadline)
|
||||
.await;
|
||||
receiver.report(advertised);
|
||||
}
|
||||
@@ -11,20 +11,30 @@ use std::net::Ipv4Addr;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||
use std::time::Instant;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use bgp_bench::memory::Region;
|
||||
use bgp_bench::mrt::{Dump, parse_attributes};
|
||||
use bgp_bench::report::{mib, per_second, row};
|
||||
use bgp_packet::message::{NlriNextHop, PathAttribute};
|
||||
use bgp_primitives::ip_prefix::IpPrefix;
|
||||
use bgp_rib::table::{Event, PeerIndex, PeerInfo, RibTable};
|
||||
|
||||
/// Bytes allocated and not freed since `region` began; zero when memory
|
||||
/// is not being counted (see `bgp_bench::memory`), and zero too in the
|
||||
/// odd case of more freed than allocated, which is nothing held.
|
||||
fn retained(region: &Region) -> usize {
|
||||
usize::try_from(region.held().unwrap_or(0)).unwrap_or(0)
|
||||
/// is not being counted (see `bgp_bench::memory`).
|
||||
fn retained(region: &Region) -> i64 {
|
||||
region.held().unwrap_or(0)
|
||||
}
|
||||
|
||||
fn mib(bytes: i64) -> f64 {
|
||||
bytes as f64 / (1024.0 * 1024.0)
|
||||
}
|
||||
|
||||
fn per_second(count: usize, elapsed: Duration) -> String {
|
||||
format!("{:.2} M/s", count as f64 / elapsed.as_secs_f64() / 1e6)
|
||||
}
|
||||
|
||||
fn row(what: &str, elapsed: Duration, detail: String) {
|
||||
println!("{what:<44} {:>9.3} s {detail}", elapsed.as_secs_f64());
|
||||
}
|
||||
|
||||
fn dump_path() -> Option<PathBuf> {
|
||||
@@ -271,9 +281,7 @@ fn add_peers(table: &RibTable, dump: &Dump, peers: &[usize]) -> Vec<PeerIndex> {
|
||||
asn: dump.peers[peer].asn,
|
||||
router_id: Ipv4Addr::from(peer as u32 + 1),
|
||||
address: dump.peers[peer].address,
|
||||
interface: 0,
|
||||
ibgp: false,
|
||||
reflector_client: false,
|
||||
})
|
||||
.expect("room for the peer")
|
||||
})
|
||||
|
||||
@@ -1,146 +0,0 @@
|
||||
//! What passing routes on costs: one peer sends the table over a session,
|
||||
//! and another is told of every route, over its own. The time is from the
|
||||
//! first byte sent to the last route heard; alongside, the time for the
|
||||
//! same table to be in our own table and no further.
|
||||
|
||||
use std::net::Ipv4Addr;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use bgp_bench::{Prefixes, Shape, announcements, encoded, env};
|
||||
use bgp_integration_tests::test_peer::TestPeer;
|
||||
use bgp_integration_tests::test_server::TestServer;
|
||||
use bgp_packet::message::PathAttribute;
|
||||
use bgp_server::marks::{Control, Marks};
|
||||
use bgp_server::policy::ExportPolicy;
|
||||
use bytes::BytesMut;
|
||||
|
||||
/// How a server passes routes on in one run.
|
||||
#[derive(Clone, Copy)]
|
||||
enum Passing {
|
||||
Not,
|
||||
Plain,
|
||||
/// With the communities that say who is told, and a mark put on every
|
||||
/// route.
|
||||
Dressed,
|
||||
}
|
||||
|
||||
/// Sends the table from one peer and, where it is passed on, hears all of
|
||||
/// it at another. Returns when the table was in, and when it was all
|
||||
/// heard.
|
||||
fn run(
|
||||
wire: &[BytesMut],
|
||||
routes: usize,
|
||||
heard_routes: usize,
|
||||
passing: Passing,
|
||||
) -> (Duration, Option<Duration>) {
|
||||
let runtime = tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.expect("a runtime");
|
||||
runtime.block_on(async {
|
||||
let server = TestServer::start_with(4_200_000_000, |config| {
|
||||
config.export = Arc::new(ExportPolicy {
|
||||
learned_routes: !matches!(passing, Passing::Not),
|
||||
all_paths: false,
|
||||
marks: match passing {
|
||||
Passing::Dressed => Marks::parse(&[], &["64601:7".to_string()], &[]).unwrap(),
|
||||
_ => Marks::default(),
|
||||
},
|
||||
prepend: 0,
|
||||
});
|
||||
config.control = matches!(passing, Passing::Dressed).then_some(Control {
|
||||
local_asn: 4_200_000_000,
|
||||
});
|
||||
})
|
||||
.await
|
||||
.expect("a server");
|
||||
let mut hearer = TestPeer::connect(server.addr).await.expect("a connection");
|
||||
hearer
|
||||
// An AS number that is on none of the generated paths: a route
|
||||
// is not passed to a peer whose AS it has been through.
|
||||
.handshake(4_100_000_000, Ipv4Addr::new(10, 0, 0, 100), 90)
|
||||
.await
|
||||
.expect("a session");
|
||||
let mut sender = TestPeer::connect(server.addr).await.expect("a connection");
|
||||
sender
|
||||
.handshake(64513, Ipv4Addr::new(10, 0, 0, 1), 90)
|
||||
.await
|
||||
.expect("a session");
|
||||
let stream: Vec<u8> = wire.iter().flat_map(|m| m.iter().copied()).collect();
|
||||
let started = Instant::now();
|
||||
let sending = tokio::spawn(async move {
|
||||
sender.send_raw(&stream).await.expect("the server reads");
|
||||
sender
|
||||
});
|
||||
let hearing = tokio::spawn(async move {
|
||||
if matches!(passing, Passing::Not) {
|
||||
return (hearer, None, 0);
|
||||
}
|
||||
let mut heard = 0usize;
|
||||
let mut messages = 0usize;
|
||||
while heard < heard_routes {
|
||||
let update = hearer.recv_update().await.expect("an UPDATE");
|
||||
messages += 1;
|
||||
heard += update.announced_routes.len();
|
||||
heard += update
|
||||
.path_attributes
|
||||
.iter()
|
||||
.map(|attribute| match attribute {
|
||||
PathAttribute::MpReachNlri(mp) => mp.prefixes.len(),
|
||||
_ => 0,
|
||||
})
|
||||
.sum::<usize>();
|
||||
}
|
||||
(hearer, Some(started.elapsed()), messages)
|
||||
});
|
||||
while server.rib.len() < routes {
|
||||
tokio::time::sleep(Duration::from_millis(2)).await;
|
||||
}
|
||||
let in_table = started.elapsed();
|
||||
let (hearer, heard, messages) = hearing.await.expect("the hearer");
|
||||
if messages > 0 {
|
||||
println!(
|
||||
" ({messages} UPDATEs, {:.1} routes each)",
|
||||
heard_routes as f64 / messages as f64
|
||||
);
|
||||
}
|
||||
drop(sending.await);
|
||||
drop(hearer);
|
||||
(in_table, heard)
|
||||
})
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let shape = Shape::INTERNET.scaled_down(env("BENCH_SCALE_DOWN", 1));
|
||||
let prefixes = Prefixes::generate(shape);
|
||||
let wire = encoded(&announcements(&prefixes, shape, 1));
|
||||
let routes = shape.prefixes();
|
||||
// Over a session on IPv4 only the IPv4 routes go.
|
||||
let heard_routes = shape.ipv4_prefixes;
|
||||
println!(
|
||||
"== One peer sends {routes} routes in {} UPDATEs over TCP; another, over IPv4, is told of the {heard_routes} IPv4 ones",
|
||||
wire.len()
|
||||
);
|
||||
for (what, passing) in [
|
||||
("not passed on", Passing::Not),
|
||||
("passed on", Passing::Plain),
|
||||
(
|
||||
"passed on, with control communities and a mark",
|
||||
Passing::Dressed,
|
||||
),
|
||||
] {
|
||||
let (in_table, heard) = run(&wire, routes, heard_routes, passing);
|
||||
let heard = heard.map_or(String::new(), |heard| {
|
||||
format!(
|
||||
" all heard by the other peer after {:>6.3} s ({:.2} M routes/s)",
|
||||
heard.as_secs_f64(),
|
||||
heard_routes as f64 / heard.as_secs_f64() / 1e6
|
||||
)
|
||||
});
|
||||
println!(
|
||||
"{what:<48} in the table after {:>6.3} s{heard}",
|
||||
in_table.as_secs_f64()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,181 +0,0 @@
|
||||
//! Feeds the synthetic Internet routing table (see the crate documentation)
|
||||
//! to a running daemon over TCP, as one or more peers would, and holds the
|
||||
//! sessions open afterwards. This is the load for profiling the daemon as
|
||||
//! it takes in a full table; `scripts/profile/full-table.sh` runs it under
|
||||
//! `perf`.
|
||||
//!
|
||||
//! Settings, from the environment:
|
||||
//!
|
||||
//! - `FEED_TO`: where the daemon listens (default `127.0.0.1:1790`).
|
||||
//! - `FEED_PEERS`: how many peers send the table (default 1).
|
||||
//! - `FEED_SCALE_DOWN`: as `BENCH_SCALE_DOWN`; 10 makes the table ten
|
||||
//! times smaller (default 1).
|
||||
//! - `FEED_FROM`: the first three octets of an IPv4 /24, "192.0.2". Peer N
|
||||
//! connects from .N, which is also the next hop of its IPv4 routes.
|
||||
//! Without it, peers connect from wherever the system puts them.
|
||||
//! - `FEED_ASN`: the first peer's AS number (default 64513); the others
|
||||
//! follow on from it.
|
||||
//! - `FEED_HOLD_SECS`: how long the sessions stay open after the table is
|
||||
//! sent (default 30).
|
||||
//! - `FEED_COLLECTOR=1`: before the peers, one more session, as a route
|
||||
//! collector would have: over IPv6, to `FEED_COLLECTOR_TO` (default
|
||||
//! `[::1]:1790`) from `FEED_COLLECTOR_FROM` if set, offering to receive
|
||||
//! several routes for a prefix (ADD-PATH) for both families and IPv4
|
||||
//! routes with IPv6 next hops, and reading and dropping whatever it is
|
||||
//! sent. Its AS number is 212232.
|
||||
//!
|
||||
//! What is printed: when the sessions are up and how long the writes took.
|
||||
//! The writes end when the kernel's socket buffers have taken the last
|
||||
//! bytes, which is before the daemon has read them: when the table is in
|
||||
//! is for the daemon's API to say.
|
||||
|
||||
use std::net::{Ipv4Addr, SocketAddr};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use bgp_bench::report::mib;
|
||||
use bgp_bench::{Prefixes, Shape, env, table_stream};
|
||||
use bgp_integration_tests::test_peer::{TestPeer, collector_capabilities};
|
||||
use bgp_primitives::constants::AfiSafi;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio::net::TcpSocket;
|
||||
use tokio::net::tcp::OwnedWriteHalf;
|
||||
|
||||
/// How often KEEPALIVEs go out while the sessions are held open: well
|
||||
/// inside the 90 s hold time.
|
||||
const KEEPALIVE_EVERY: Duration = Duration::from_secs(20);
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let to: SocketAddr = std::env::var("FEED_TO")
|
||||
.unwrap_or_else(|_| "127.0.0.1:1790".into())
|
||||
.parse()
|
||||
.expect("FEED_TO is an address and port");
|
||||
let peers = env("FEED_PEERS", 1) as u32;
|
||||
let shape = Shape::INTERNET.scaled_down(env("FEED_SCALE_DOWN", 1));
|
||||
let from = std::env::var("FEED_FROM").ok();
|
||||
let first_asn = env("FEED_ASN", 64513) as u32;
|
||||
let hold = Duration::from_secs(env("FEED_HOLD_SECS", 30) as u64);
|
||||
|
||||
let started = Instant::now();
|
||||
let prefixes = Prefixes::generate(shape);
|
||||
let tables: Vec<Vec<u8>> = (1..=peers)
|
||||
.map(|peer| table_stream(&prefixes, shape, peer))
|
||||
.collect();
|
||||
let routes = shape.prefixes() * peers as usize;
|
||||
let bytes: usize = tables.iter().map(Vec::len).sum();
|
||||
println!(
|
||||
"table: {} IPv4 + {} IPv6 prefixes from each of {peers} peer(s): {routes} routes, {:.1} MiB on the wire, made in {:.2} s",
|
||||
shape.ipv4_prefixes,
|
||||
shape.ipv6_prefixes,
|
||||
mib(bytes),
|
||||
started.elapsed().as_secs_f64()
|
||||
);
|
||||
|
||||
// The collector first, so that it is there as the table arrives, as a
|
||||
// router's collector session is after a restart.
|
||||
let collector = env("FEED_COLLECTOR", 0) == 1;
|
||||
let mut draining = Vec::new();
|
||||
let mut collector_writer = None;
|
||||
if collector {
|
||||
let to: SocketAddr = std::env::var("FEED_COLLECTOR_TO")
|
||||
.unwrap_or_else(|_| "[::1]:1790".into())
|
||||
.parse()
|
||||
.expect("FEED_COLLECTOR_TO is an IPv6 address and port");
|
||||
let socket = TcpSocket::new_v6().expect("a socket");
|
||||
if let Ok(from) = std::env::var("FEED_COLLECTOR_FROM") {
|
||||
let address: std::net::Ipv6Addr = from.parse().expect("an IPv6 address");
|
||||
socket
|
||||
.bind(SocketAddr::new(address.into(), 0))
|
||||
.expect("the collector's address is ours to connect from");
|
||||
}
|
||||
let stream = socket.connect(to).await.expect("the daemon listens");
|
||||
let mut session = TestPeer::from_stream(stream);
|
||||
session
|
||||
.handshake_with(
|
||||
212_232,
|
||||
Ipv4Addr::new(10, 0, 0, 200),
|
||||
90,
|
||||
collector_capabilities(
|
||||
212_232,
|
||||
&[AfiSafi::IPV4_UNICAST, AfiSafi::IPV6_UNICAST],
|
||||
true,
|
||||
),
|
||||
)
|
||||
.await
|
||||
.expect("the daemon takes the collector's session");
|
||||
let (writer, sink) = session.drain();
|
||||
draining.push(sink);
|
||||
println!("collector session established");
|
||||
collector_writer = Some(writer);
|
||||
}
|
||||
|
||||
let mut sessions = Vec::new();
|
||||
for peer in 1..=peers {
|
||||
let socket = TcpSocket::new_v4().expect("a socket");
|
||||
if let Some(from) = &from {
|
||||
let address: Ipv4Addr = format!("{from}.{peer}")
|
||||
.parse()
|
||||
.expect("FEED_FROM is the first three octets of an IPv4 address");
|
||||
socket
|
||||
.bind(SocketAddr::new(address.into(), 0))
|
||||
.expect("the peer's address is ours to connect from");
|
||||
}
|
||||
let stream = socket.connect(to).await.expect("the daemon listens");
|
||||
let mut session = TestPeer::from_stream(stream);
|
||||
session
|
||||
.handshake(
|
||||
first_asn + peer - 1,
|
||||
Ipv4Addr::new(10, 0, 0, peer as u8),
|
||||
90,
|
||||
)
|
||||
.await
|
||||
.expect("the daemon takes the session");
|
||||
// Whatever the daemon sends back, KEEPALIVEs as a rule, is read
|
||||
// and dropped, so that the writer is never held up.
|
||||
let (writer, sink) = session.drain();
|
||||
draining.push(sink);
|
||||
sessions.push(writer);
|
||||
}
|
||||
println!("{peers} session(s) established; sending");
|
||||
|
||||
let started = Instant::now();
|
||||
let senders: Vec<_> = sessions
|
||||
.into_iter()
|
||||
.zip(tables)
|
||||
.map(|(mut writer, table)| {
|
||||
tokio::spawn(async move {
|
||||
let started = Instant::now();
|
||||
writer.write_all(&table).await.expect("the daemon reads");
|
||||
(writer, started.elapsed())
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
let mut writers: Vec<OwnedWriteHalf> = collector_writer.into_iter().collect();
|
||||
for (index, sender) in senders.into_iter().enumerate() {
|
||||
let (writer, took) = sender.await.expect("the sender runs");
|
||||
println!(
|
||||
"peer {}: table written in {:.3} s",
|
||||
index + 1,
|
||||
took.as_secs_f64()
|
||||
);
|
||||
writers.push(writer);
|
||||
}
|
||||
let elapsed = started.elapsed();
|
||||
println!(
|
||||
"all {routes} routes written in {:.3} s: {:.2} M routes/s into the sockets",
|
||||
elapsed.as_secs_f64(),
|
||||
routes as f64 / elapsed.as_secs_f64() / 1e6
|
||||
);
|
||||
|
||||
let keepalive = TestPeer::keepalive_bytes();
|
||||
let deadline = Instant::now() + hold;
|
||||
while Instant::now() < deadline {
|
||||
tokio::time::sleep(KEEPALIVE_EVERY.min(deadline - Instant::now())).await;
|
||||
for writer in &mut writers {
|
||||
// A session the daemon closed is left alone.
|
||||
let _ = writer.write_all(&keepalive).await;
|
||||
}
|
||||
}
|
||||
println!("sessions closed after {} s", hold.as_secs());
|
||||
drop(draining);
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
//! How many UPDATEs, and how many octets, a table takes when the routes
|
||||
//! that share attributes are grouped only within slices of the table
|
||||
//! taken in shard order, for a few slice sizes: what streaming a table
|
||||
//! out in slices costs against grouping it whole.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::hash::{BuildHasher, RandomState};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use bgp_bench::mrt::Dump;
|
||||
|
||||
fn main() {
|
||||
let path = std::env::var("BENCH_MRT")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|_| "crates/bench/data/rrc03-bview.20260915.0000.gz".into());
|
||||
let dump = Dump::read(&path).expect("a readable dump");
|
||||
let peers = dump.largest_peers(usize::MAX);
|
||||
let full: Vec<usize> = peers
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|&p| dump.routes[p].len() > 800_000)
|
||||
.collect();
|
||||
println!("{} peers with a full table", full.len());
|
||||
let hasher = RandomState::new();
|
||||
for (what, chosen) in [
|
||||
("one full table", vec![full[0]]),
|
||||
("every full table, every path", full.clone()),
|
||||
] {
|
||||
// (hash of prefix, attribute set, prefix octets) in shard order.
|
||||
let mut routes: Vec<(u64, u32, usize)> = chosen
|
||||
.iter()
|
||||
.flat_map(|&p| dump.routes[p].iter())
|
||||
.map(|(prefix, set)| {
|
||||
(
|
||||
hasher.hash_one(prefix),
|
||||
*set,
|
||||
1 + usize::from(prefix.length()).div_ceil(8) + 4,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
routes.sort_unstable_by_key(|r| r.0);
|
||||
println!("== {what}: {} routes", routes.len());
|
||||
for slice in [4_096usize, 16_384, 65_536, 262_144, usize::MAX] {
|
||||
let mut updates = 0usize;
|
||||
let mut octets = 0usize;
|
||||
for chunk in routes.chunks(slice.min(routes.len())) {
|
||||
let mut groups: HashMap<u32, usize> = HashMap::new();
|
||||
for &(_, set, prefix_octets) in chunk {
|
||||
*groups.entry(set).or_insert(0) += prefix_octets;
|
||||
}
|
||||
for (set, prefix_octets) in groups {
|
||||
// One UPDATE per 4096 octets of prefixes, roughly.
|
||||
let attrs = dump.attribute_sets[set as usize].len();
|
||||
let messages = prefix_octets.div_ceil(4000 - attrs.min(3000)).max(1);
|
||||
updates += messages;
|
||||
octets += messages * (23 + attrs) + prefix_octets;
|
||||
}
|
||||
}
|
||||
println!(
|
||||
" slice {:>9}: {:>9} UPDATEs, {:>5.1} MB on the wire",
|
||||
if slice == usize::MAX {
|
||||
"whole".to_string()
|
||||
} else {
|
||||
slice.to_string()
|
||||
},
|
||||
updates,
|
||||
octets as f64 / 1e6
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
-98
@@ -1,10 +1,9 @@
|
||||
//! A synthetic copy of the Internet routing table, as the UPDATE messages a
|
||||
//! peer would send to announce it.
|
||||
//!
|
||||
//! It is made up, not captured, so that the benchmarks of a synthetic table
|
||||
//! need no data files and give the same input every run; the one of a real
|
||||
//! table reads an MRT dump instead, see [`mrt`]. It is shaped like the real
|
||||
//! thing where that matters for cost:
|
||||
//! It is made up, not captured, so that the benchmarks need no data files
|
||||
//! and give the same input every run. It is shaped like the real thing where
|
||||
//! that matters for cost:
|
||||
//!
|
||||
//! - about a million IPv4 prefixes, most of them /24s, and about a quarter
|
||||
//! of a million IPv6 prefixes, most of them /48s;
|
||||
@@ -16,14 +15,6 @@
|
||||
|
||||
pub mod memory;
|
||||
pub mod mrt;
|
||||
pub mod report;
|
||||
|
||||
// The benchmarks allocate as the daemon does (bin/bgp/src/main.rs), with
|
||||
// jemalloc; the C library's allocator would put its own locks in the
|
||||
// profile. Not when memory is counted: that needs the counting one.
|
||||
#[cfg(all(not(feature = "count-memory"), not(target_env = "msvc")))]
|
||||
#[global_allocator]
|
||||
static ALLOCATOR: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc;
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::net::{Ipv4Addr, Ipv6Addr};
|
||||
@@ -172,7 +163,7 @@ pub fn announcements(prefixes: &Prefixes, shape: Shape, peer: u32) -> Vec<Update
|
||||
let hops = 2 + rng.below(4) as usize;
|
||||
let mut path = vec![peer_asn];
|
||||
path.extend((0..hops).map(|_| 1 + rng.below(400_000) as u32));
|
||||
let attributes = vec![
|
||||
let mut attributes = vec![
|
||||
PathAttribute::Origin(OriginPathAttribute::IGP),
|
||||
PathAttribute::AsPath(AsPathAttribute {
|
||||
segments: vec![AsPathSegment {
|
||||
@@ -208,7 +199,7 @@ pub fn announcements(prefixes: &Prefixes, shape: Shape, peer: u32) -> Vec<Update
|
||||
} else {
|
||||
None
|
||||
};
|
||||
(attributes, later, large)
|
||||
(std::mem::take(&mut attributes), later, large)
|
||||
};
|
||||
|
||||
let mut updates = Vec::new();
|
||||
@@ -267,16 +258,6 @@ pub fn announcements(prefixes: &Prefixes, shape: Shape, peer: u32) -> Vec<Update
|
||||
updates
|
||||
}
|
||||
|
||||
/// The whole table as the peer numbered `peer` sends it, one stream of
|
||||
/// bytes: [`announcements`], [`encoded`] and run together, for a session
|
||||
/// that writes the table in one go.
|
||||
pub fn table_stream(prefixes: &Prefixes, shape: Shape, peer: u32) -> Vec<u8> {
|
||||
encoded(&announcements(prefixes, shape, peer))
|
||||
.iter()
|
||||
.flat_map(|message| message.iter().copied())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// `updates` as they are on the wire, on a session with four-octet AS
|
||||
/// numbers.
|
||||
pub fn encoded(updates: &[UpdateMessage]) -> Vec<BytesMut> {
|
||||
@@ -302,77 +283,3 @@ pub fn wire_context() -> ParserContext {
|
||||
add_path: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The value of the environment variable `name` as a number, or `default`
|
||||
/// when it is not set or is not one: how the benchmarks take their size.
|
||||
pub fn env(name: &str, default: usize) -> usize {
|
||||
std::env::var(name)
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(default)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// A small table, enough for every prefix length and attribute to
|
||||
/// come up.
|
||||
fn shape() -> Shape {
|
||||
Shape::INTERNET.scaled_down(1000)
|
||||
}
|
||||
|
||||
/// The benchmarks are compared from one run to the next, so the table
|
||||
/// they are fed must not change between runs: the generator is
|
||||
/// seeded, and nothing in it depends on where things are in memory.
|
||||
#[test]
|
||||
fn the_table_is_the_same_every_run() {
|
||||
let shape = shape();
|
||||
let once = Prefixes::generate(shape);
|
||||
let again = Prefixes::generate(shape);
|
||||
assert_eq!(once.ipv4, again.ipv4);
|
||||
assert_eq!(once.ipv6, again.ipv6);
|
||||
assert_eq!(once.ipv4.len(), shape.ipv4_prefixes);
|
||||
assert_eq!(once.ipv6.len(), shape.ipv6_prefixes);
|
||||
for peer in [1, 2] {
|
||||
assert_eq!(
|
||||
table_stream(&once, shape, peer),
|
||||
table_stream(&again, shape, peer)
|
||||
);
|
||||
}
|
||||
assert_ne!(
|
||||
table_stream(&once, shape, 1),
|
||||
table_stream(&once, shape, 2),
|
||||
"each peer has attributes of its own"
|
||||
);
|
||||
}
|
||||
|
||||
/// What a peer of the benchmarks sends is well formed, and says what
|
||||
/// it was meant to: every UPDATE reads back as it was made.
|
||||
#[test]
|
||||
fn every_announcement_decodes_to_itself() {
|
||||
let shape = shape();
|
||||
let prefixes = Prefixes::generate(shape);
|
||||
let updates = announcements(&prefixes, shape, 1);
|
||||
let ctx = wire_context();
|
||||
let announced: usize = updates
|
||||
.iter()
|
||||
.map(|update| {
|
||||
update.announced_routes.len()
|
||||
+ update
|
||||
.path_attributes
|
||||
.iter()
|
||||
.map(|attribute| match attribute {
|
||||
PathAttribute::MpReachNlri(reach) => reach.prefixes.len(),
|
||||
_ => 0,
|
||||
})
|
||||
.sum::<usize>()
|
||||
})
|
||||
.sum();
|
||||
assert_eq!(announced, shape.prefixes());
|
||||
for (update, wire) in updates.iter().zip(encoded(&updates)) {
|
||||
let decoded = Message::decode(&ctx, &wire).expect("well formed");
|
||||
assert_eq!(decoded, Message::Update(update.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
+26
-16
@@ -10,7 +10,7 @@ use std::io::{self, BufReader, Read};
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
|
||||
use std::path::Path;
|
||||
|
||||
use bgp_packet::message::{NlriNextHop, PathAttribute, attr_type, split_attribute};
|
||||
use bgp_packet::message::{NlriNextHop, PathAttribute};
|
||||
use bgp_primitives::constants::IpVersion;
|
||||
use bgp_primitives::ip_prefix::IpPrefix;
|
||||
use flate2::read::MultiGzDecoder;
|
||||
@@ -21,6 +21,7 @@ const TABLE_DUMP_V2: u16 = 13;
|
||||
const PEER_INDEX_TABLE: u16 = 1;
|
||||
const RIB_IPV4_UNICAST: u16 = 2;
|
||||
const RIB_IPV6_UNICAST: u16 = 4;
|
||||
const MP_REACH_NLRI: u8 = 14;
|
||||
|
||||
/// A peer of the collector.
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -204,23 +205,32 @@ pub fn parse_attributes(mut wire: &[u8]) -> Option<(Vec<PathAttribute>, Option<N
|
||||
let mut attributes = Vec::new();
|
||||
let mut next_hop = None;
|
||||
while !wire.is_empty() {
|
||||
let (raw, rest) = split_attribute(wire).ok()?;
|
||||
if raw.type_code == attr_type::MP_REACH_NLRI {
|
||||
// The value is the next hop's length and the next hop, and
|
||||
// nothing of the family, reserved octet and prefixes that
|
||||
// follow in an UPDATE, so the attribute's own parser would
|
||||
// refuse it.
|
||||
let (length, address) = raw.value.split_first()?;
|
||||
if usize::from(*length) != address.len() {
|
||||
return None;
|
||||
}
|
||||
next_hop = Some(NlriNextHop::from_bytes(address)?);
|
||||
} else {
|
||||
let ([], attribute) = PathAttribute::from_wire(&ctx, raw.wire).ok()? else {
|
||||
return None;
|
||||
if wire.get(1) == Some(&MP_REACH_NLRI) {
|
||||
let extended = wire[0] & 0x10 != 0;
|
||||
let (length, header) = if extended {
|
||||
(
|
||||
usize::from(u16::from_be_bytes([*wire.get(2)?, *wire.get(3)?])),
|
||||
4,
|
||||
)
|
||||
} else {
|
||||
(usize::from(*wire.get(2)?), 3)
|
||||
};
|
||||
attributes.push(attribute);
|
||||
let value = wire.get(header..header + length)?;
|
||||
let address = |bytes: &[u8]| Some(Ipv6Addr::from(<[u8; 16]>::try_from(bytes).ok()?));
|
||||
next_hop = match value {
|
||||
[4, a, b, c, d] => Some(NlriNextHop::Ipv4(Ipv4Addr::new(*a, *b, *c, *d))),
|
||||
[16, rest @ ..] => Some(NlriNextHop::Ipv6(address(rest)?)),
|
||||
[32, rest @ ..] if rest.len() == 32 => Some(NlriNextHop::Ipv6WithLl {
|
||||
global: address(&rest[..16])?,
|
||||
link_local: address(&rest[16..])?,
|
||||
}),
|
||||
_ => return None,
|
||||
};
|
||||
wire = &wire[header + length..];
|
||||
continue;
|
||||
}
|
||||
let (rest, attribute) = PathAttribute::from_wire(&ctx, wire).ok()?;
|
||||
attributes.push(attribute);
|
||||
wire = rest;
|
||||
}
|
||||
Some((attributes, next_hop))
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
//! How the benchmarks print what they measured: one row a measurement,
|
||||
//! what was measured in a column wide enough for the longest of them, the
|
||||
//! time, and whatever else there is to say about it. The rows are what a
|
||||
//! run is compared against, so their shape is settled here once.
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
/// How wide the column that says what was measured is. The widest row is
|
||||
/// the mesh benchmark's, so that is what every benchmark lines up to.
|
||||
pub const WIDTH: usize = 52;
|
||||
|
||||
/// `bytes` in mebibytes.
|
||||
pub fn mib(bytes: usize) -> f64 {
|
||||
bytes as f64 / (1024.0 * 1024.0)
|
||||
}
|
||||
|
||||
/// `count` things in `elapsed`, as a rate: millions a second where there
|
||||
/// are that many, thousands otherwise.
|
||||
pub fn per_second(count: usize, elapsed: Duration) -> String {
|
||||
let rate = count as f64 / elapsed.as_secs_f64();
|
||||
if rate >= 1e6 {
|
||||
format!("{:.2} M/s", rate / 1e6)
|
||||
} else {
|
||||
format!("{:.0} k/s", rate / 1e3)
|
||||
}
|
||||
}
|
||||
|
||||
/// Prints one row: `what` took `elapsed`, and `detail` says more.
|
||||
pub fn row(what: &str, elapsed: Duration, detail: impl std::fmt::Display) {
|
||||
println!("{what:<WIDTH$} {:>8.3} s {detail}", elapsed.as_secs_f64());
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
[package]
|
||||
name = "bgp-fib"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
homepage.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
bgp-packet.workspace = true
|
||||
bgp-primitives.workspace = true
|
||||
bgp-rib.workspace = true
|
||||
bytes.workspace = true
|
||||
foldhash.workspace = true
|
||||
prometheus.workspace = true
|
||||
thiserror.workspace = true
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
tracing.workspace = true
|
||||
|
||||
# The socket to the kernel. Only Linux has it; everything else in this
|
||||
# crate builds, and is tested, anywhere.
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
netlink-sys = { version = "0.8", features = ["tokio_socket"] }
|
||||
# For a socket option netlink-sys has no name for.
|
||||
nix = { workspace = true, features = ["socket"] }
|
||||
|
||||
[dev-dependencies]
|
||||
proptest.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -1,286 +0,0 @@
|
||||
//! The machine's interfaces and their addresses, as the kernel reports
|
||||
//! them, and what they say about a next hop: whether it can be reached
|
||||
//! directly, and from which interface.
|
||||
|
||||
use std::collections::{BTreeMap, HashSet};
|
||||
use std::net::IpAddr;
|
||||
|
||||
use bgp_primitives::constants::IpVersion;
|
||||
use bgp_primitives::ip_prefix::IpPrefix;
|
||||
|
||||
use crate::InterfaceStatus;
|
||||
use crate::netlink::{Address, Link, Message};
|
||||
|
||||
/// The interface a neighbor is reached on.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Connected {
|
||||
/// The interface's index.
|
||||
pub interface: u32,
|
||||
/// Our own address in the network the neighbor is in.
|
||||
pub local: IpAddr,
|
||||
}
|
||||
|
||||
/// The interfaces there are and the addresses they have.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Interfaces {
|
||||
/// By index, which is the order they are given out in too.
|
||||
links: BTreeMap<u32, Link>,
|
||||
addresses: HashSet<Address>,
|
||||
}
|
||||
|
||||
/// `address` as the prefix that is only it.
|
||||
fn host(address: IpAddr) -> IpPrefix {
|
||||
match address {
|
||||
IpAddr::V4(v4) => IpPrefix::new(IpVersion::Ipv4, v4.octets(), 32),
|
||||
IpAddr::V6(v6) => IpPrefix::new(IpVersion::Ipv6, v6.octets(), 128),
|
||||
}
|
||||
.expect("an address is a prefix of its own length")
|
||||
}
|
||||
|
||||
impl Interfaces {
|
||||
/// Takes in what a message from the kernel says about links and
|
||||
/// addresses. Returns whether anything is different for it.
|
||||
pub fn apply(&mut self, message: &Message) -> bool {
|
||||
match message {
|
||||
Message::Link {
|
||||
present: true,
|
||||
link,
|
||||
} => self.links.insert(link.index, link.clone()).as_ref() != Some(link),
|
||||
Message::Link {
|
||||
present: false,
|
||||
link,
|
||||
} => {
|
||||
self.addresses
|
||||
.retain(|address| address.interface != link.index);
|
||||
self.links.remove(&link.index).is_some()
|
||||
}
|
||||
Message::Address {
|
||||
present: true,
|
||||
address,
|
||||
} => self.addresses.insert(*address),
|
||||
Message::Address {
|
||||
present: false,
|
||||
address,
|
||||
} => self.addresses.remove(address),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// The interfaces, in order of index.
|
||||
pub fn links(&self) -> impl Iterator<Item = &Link> {
|
||||
self.links.values()
|
||||
}
|
||||
|
||||
/// The interfaces and their addresses, for showing: in order of index,
|
||||
/// each with its addresses in order.
|
||||
pub fn status(&self) -> Vec<InterfaceStatus> {
|
||||
self.links()
|
||||
.map(|link| {
|
||||
let mut addresses: Vec<(IpAddr, u8)> = self
|
||||
.addresses_of(link.index)
|
||||
.map(|address| (address.local, address.network.length()))
|
||||
.collect();
|
||||
addresses.sort();
|
||||
InterfaceStatus {
|
||||
index: link.index,
|
||||
name: link.name.clone(),
|
||||
up: link.up,
|
||||
addresses,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn link(&self, index: u32) -> Option<&Link> {
|
||||
self.links.get(&index)
|
||||
}
|
||||
|
||||
/// The addresses of the interface with this index.
|
||||
pub fn addresses_of(&self, interface: u32) -> impl Iterator<Item = &Address> {
|
||||
self.addresses
|
||||
.iter()
|
||||
.filter(move |address| address.interface == interface)
|
||||
}
|
||||
|
||||
/// Whether `address` is one of this machine's own.
|
||||
pub fn is_local(&self, address: IpAddr) -> bool {
|
||||
self.addresses.iter().any(|ours| ours.local == address)
|
||||
}
|
||||
|
||||
/// Whether `prefix` is the network of one of this machine's addresses,
|
||||
/// or inside one. Whether the interface is up does not matter: the
|
||||
/// network is ours either way.
|
||||
pub fn has_network_around(&self, prefix: &IpPrefix) -> bool {
|
||||
self.addresses
|
||||
.iter()
|
||||
.any(|address| address.network.contains(prefix))
|
||||
}
|
||||
|
||||
/// Where `neighbor` can be reached without a gateway: the interface
|
||||
/// that is up and has an address in a network `neighbor` is in, the
|
||||
/// most specific such network if there are several. `None` if there is
|
||||
/// none, or if `neighbor` is this machine itself.
|
||||
pub fn connected(&self, neighbor: IpAddr) -> Option<Connected> {
|
||||
if self.is_local(neighbor) {
|
||||
return None;
|
||||
}
|
||||
let neighbor = host(neighbor);
|
||||
self.addresses
|
||||
.iter()
|
||||
.filter(|address| address.network.contains(&neighbor))
|
||||
.filter(|address| {
|
||||
self.links
|
||||
.get(&address.interface)
|
||||
.is_some_and(|link| link.up)
|
||||
})
|
||||
.max_by_key(|address| address.network.length())
|
||||
.map(|address| Connected {
|
||||
interface: address.interface,
|
||||
local: address.local,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn link(index: u32, name: &str, up: bool) -> Message {
|
||||
Message::Link {
|
||||
present: true,
|
||||
link: Link {
|
||||
index,
|
||||
name: name.into(),
|
||||
up,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn address(interface: u32, local: &str, network: &str) -> Address {
|
||||
Address {
|
||||
interface,
|
||||
local: local.parse().unwrap(),
|
||||
network: IpPrefix::try_from(network).unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
fn present(address: Address) -> Message {
|
||||
Message::Address {
|
||||
present: true,
|
||||
address,
|
||||
}
|
||||
}
|
||||
|
||||
fn some() -> Interfaces {
|
||||
let mut interfaces = Interfaces::default();
|
||||
for message in [
|
||||
link(1, "lo", true),
|
||||
link(2, "eth0", true),
|
||||
link(3, "eth1", true),
|
||||
link(4, "eth2", false),
|
||||
present(address(1, "127.0.0.1", "127.0.0.0/8")),
|
||||
present(address(2, "192.0.2.1", "192.0.2.0/24")),
|
||||
present(address(2, "2001:db8::1", "2001:db8::/64")),
|
||||
// A more specific network inside eth0's, on eth1.
|
||||
present(address(3, "192.0.2.129", "192.0.2.128/25")),
|
||||
present(address(4, "198.51.100.1", "198.51.100.0/24")),
|
||||
] {
|
||||
assert!(interfaces.apply(&message));
|
||||
}
|
||||
interfaces
|
||||
}
|
||||
|
||||
fn connected(interfaces: &Interfaces, neighbor: &str) -> Option<(u32, String)> {
|
||||
interfaces
|
||||
.connected(neighbor.parse().unwrap())
|
||||
.map(|c| (c.interface, c.local.to_string()))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_neighbor_is_reached_from_the_interface_whose_network_it_is_in() {
|
||||
let interfaces = some();
|
||||
assert_eq!(
|
||||
connected(&interfaces, "192.0.2.7"),
|
||||
Some((2, "192.0.2.1".into()))
|
||||
);
|
||||
assert_eq!(
|
||||
connected(&interfaces, "2001:db8::7"),
|
||||
Some((2, "2001:db8::1".into()))
|
||||
);
|
||||
// The most specific network wins.
|
||||
assert_eq!(
|
||||
connected(&interfaces, "192.0.2.200"),
|
||||
Some((3, "192.0.2.129".into()))
|
||||
);
|
||||
// Not in any network of ours.
|
||||
assert_eq!(connected(&interfaces, "203.0.113.1"), None);
|
||||
assert_eq!(connected(&interfaces, "2001:db8:1::1"), None);
|
||||
// In the network of an interface that is down.
|
||||
assert_eq!(connected(&interfaces, "198.51.100.7"), None);
|
||||
// Ourselves.
|
||||
assert_eq!(connected(&interfaces, "192.0.2.1"), None);
|
||||
assert!(interfaces.is_local("192.0.2.1".parse().unwrap()));
|
||||
assert!(!interfaces.is_local("192.0.2.7".parse().unwrap()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn what_the_kernel_reports_changes_what_is_reachable() {
|
||||
let mut interfaces = some();
|
||||
// Told again what is known: nothing changes.
|
||||
assert!(!interfaces.apply(&link(2, "eth0", true)));
|
||||
assert!(!interfaces.apply(&present(address(2, "192.0.2.1", "192.0.2.0/24"))));
|
||||
|
||||
// An interface comes up.
|
||||
assert!(interfaces.apply(&link(4, "eth2", true)));
|
||||
assert_eq!(
|
||||
connected(&interfaces, "198.51.100.7"),
|
||||
Some((4, "198.51.100.1".into()))
|
||||
);
|
||||
// An address goes.
|
||||
assert!(interfaces.apply(&Message::Address {
|
||||
present: false,
|
||||
address: address(3, "192.0.2.129", "192.0.2.128/25"),
|
||||
}));
|
||||
assert_eq!(
|
||||
connected(&interfaces, "192.0.2.200"),
|
||||
Some((2, "192.0.2.1".into()))
|
||||
);
|
||||
// An interface goes, and its addresses with it.
|
||||
assert!(interfaces.apply(&Message::Link {
|
||||
present: false,
|
||||
link: Link {
|
||||
index: 2,
|
||||
name: "eth0".into(),
|
||||
up: true
|
||||
},
|
||||
}));
|
||||
assert_eq!(connected(&interfaces, "192.0.2.7"), None);
|
||||
assert_eq!(interfaces.addresses_of(2).count(), 0);
|
||||
// What is not about interfaces changes nothing.
|
||||
assert!(!interfaces.apply(&Message::Done));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_interfaces_are_shown_in_order() {
|
||||
let mut interfaces = some();
|
||||
// Learned last, listed in its place.
|
||||
assert!(interfaces.apply(&link(0, "dummy0", false)));
|
||||
let shown = interfaces.status();
|
||||
let indexes: Vec<u32> = shown.iter().map(|interface| interface.index).collect();
|
||||
assert_eq!(indexes, [0, 1, 2, 3, 4]);
|
||||
assert_eq!(
|
||||
shown[2],
|
||||
InterfaceStatus {
|
||||
index: 2,
|
||||
name: "eth0".into(),
|
||||
up: true,
|
||||
addresses: vec![
|
||||
("192.0.2.1".parse().unwrap(), 24),
|
||||
("2001:db8::1".parse().unwrap(), 64),
|
||||
],
|
||||
}
|
||||
);
|
||||
assert_eq!((shown[4].up, shown[4].addresses.len()), (false, 1));
|
||||
assert!(shown[0].addresses.is_empty());
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,331 +0,0 @@
|
||||
//! Putting the best routes into the kernel's routing table, so that the
|
||||
//! machine forwards packets by them (Linux, over rtnetlink).
|
||||
//!
|
||||
//! The routing table chooses a best route for each prefix. This crate
|
||||
//! follows those choices and keeps the kernel's table the same: one route
|
||||
//! for each prefix, to the best route's next hop. It also learns from the
|
||||
//! kernel what interfaces and addresses the machine has, which is what
|
||||
//! says whether a next hop can be reached at all.
|
||||
//!
|
||||
//! - [`netlink`] is the kernel's messages, as bytes and back.
|
||||
//! - [`interfaces`] is the machine's interfaces and what is reachable on
|
||||
//! them; [`routes`] the kernel's own routes, which say the way to what
|
||||
//! is not; [`topology`] the two together with the kernel's nexthop
|
||||
//! objects, as one view.
|
||||
//! - [`plan`] works out what to ask the kernel for.
|
||||
//! - The rest, which only Linux has, asks it.
|
||||
//!
|
||||
//! # What is and is not touched
|
||||
//!
|
||||
//! Our routes are marked as BGP's ([`netlink::PROTOCOL_BGP`]) and carry
|
||||
//! the configured metric, in the configured table. Only routes with that
|
||||
//! mark, metric and table are ever replaced or removed; and all such
|
||||
//! routes are taken to be ours, so two daemons must not share them.
|
||||
//! Routes left behind by a run that ended badly are found at the next
|
||||
//! start, and removed unless they are still wanted. A run that ends in
|
||||
//! good order removes its routes.
|
||||
//!
|
||||
//! # What is installed
|
||||
//!
|
||||
//! A best route whose next hop can be reached: a neighbor, which is an
|
||||
//! address in the network of one of the machine's interfaces, or an
|
||||
//! address the kernel's own routes know the way to. In the second case
|
||||
//! the route goes into the kernel with the gateway and interface of the
|
||||
//! route that knows the way, since the kernel does not look a gateway up
|
||||
//! in turn: a route whose next hop is a router of our own AS, reached over
|
||||
//! a tunnel by a static route, is installed as a route into the tunnel.
|
||||
//! Which of the kernel's routes count for this, and which never do, is the
|
||||
//! business of [`routes`].
|
||||
|
||||
pub mod interfaces;
|
||||
#[cfg(target_os = "linux")]
|
||||
mod kernel;
|
||||
pub mod netlink;
|
||||
pub mod plan;
|
||||
pub mod routes;
|
||||
pub mod sockdiag;
|
||||
pub mod topology;
|
||||
|
||||
use std::net::IpAddr;
|
||||
use std::sync::{Arc, Mutex, PoisonError};
|
||||
use std::time::Duration;
|
||||
|
||||
use bgp_rib::table::RibTable;
|
||||
use tokio::sync::watch;
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
/// The metric our routes get unless configured otherwise: the one other
|
||||
/// routing daemons give routes from external BGP peers, so that ours rank
|
||||
/// against static and connected routes as an operator expects.
|
||||
pub const DEFAULT_METRIC: u32 = 20;
|
||||
|
||||
/// How long routes left by an earlier run are kept unless configured
|
||||
/// otherwise: time for sessions to come up and tables to be sent.
|
||||
pub const DEFAULT_STALE_TIME: Duration = Duration::from_secs(120);
|
||||
|
||||
/// Where our routes go in the kernel.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct FibConfig {
|
||||
/// The routing table: [`netlink::TABLE_MAIN`] to forward by them.
|
||||
pub table: u32,
|
||||
/// The metric, which with the table says which routes are ours.
|
||||
pub metric: u32,
|
||||
/// Have routes name nexthop objects where the kernel has them (Linux
|
||||
/// 5.3 and later), which makes installing many routes a good deal
|
||||
/// faster. Without, every route carries its own gateway.
|
||||
pub nexthop_objects: bool,
|
||||
/// How long routes of ours that an earlier run left in the kernel are
|
||||
/// kept after a start without the routing table having them. The
|
||||
/// table is empty until the peers have sent theirs again; removing
|
||||
/// everything at once would stop the machine forwarding meanwhile.
|
||||
/// After this long, what has not been announced again goes.
|
||||
pub stale_time: Duration,
|
||||
/// The kernel's table whose routes say the way to a next hop that is
|
||||
/// not a neighbor: normally `main`, with the connected and static
|
||||
/// routes. BGP's own routes in it never count; see [`routes`].
|
||||
pub resolution_table: u32,
|
||||
/// Whether the default route counts as the way to a next hop.
|
||||
pub resolve_via_default: bool,
|
||||
}
|
||||
|
||||
impl Default for FibConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
table: netlink::TABLE_MAIN,
|
||||
metric: DEFAULT_METRIC,
|
||||
nexthop_objects: true,
|
||||
stale_time: DEFAULT_STALE_TIME,
|
||||
resolution_table: netlink::TABLE_MAIN,
|
||||
resolve_via_default: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Why routes cannot be kept in the kernel.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum FibError {
|
||||
#[error("installing routes in the kernel is only supported on Linux")]
|
||||
Unsupported,
|
||||
#[error("the kernel's routing socket failed: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("the kernel sent something that is not netlink: {0}")]
|
||||
Malformed(#[from] netlink::NetlinkError),
|
||||
#[error("the kernel refused to list {what}: error {errno}")]
|
||||
Dump { what: &'static str, errno: i32 },
|
||||
#[error(
|
||||
"not permitted to change the kernel's routes: the daemon needs the CAP_NET_ADMIN \
|
||||
capability (or root)"
|
||||
)]
|
||||
NotPermitted,
|
||||
}
|
||||
|
||||
/// One of the machine's interfaces, for showing.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct InterfaceStatus {
|
||||
pub index: u32,
|
||||
pub name: String,
|
||||
pub up: bool,
|
||||
/// Its addresses, each with the length of the network it is in.
|
||||
pub addresses: Vec<(IpAddr, u8)>,
|
||||
}
|
||||
|
||||
/// How things stand with the kernel, for showing.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Status {
|
||||
/// Whether the kernel has been brought in step with the routing table
|
||||
/// at least once since the start.
|
||||
pub synced: bool,
|
||||
/// How many routes of ours the kernel has.
|
||||
pub installed: usize,
|
||||
/// How many of them are kept from an earlier run though the routing
|
||||
/// table does not have them (yet); see [`FibConfig::stale_time`].
|
||||
pub kept: usize,
|
||||
/// How many of them are blackhole routes, which drop the packets for
|
||||
/// their prefix.
|
||||
pub blackholes: usize,
|
||||
/// Prefixes whose best route is not in the kernel because its next
|
||||
/// hop is not usable, by why.
|
||||
pub unresolved: Vec<(&'static str, usize)>,
|
||||
/// Prefixes whose best route the kernel refused.
|
||||
pub refused: usize,
|
||||
/// How many requests the kernel has refused in all.
|
||||
pub errors: u64,
|
||||
/// The last such refusal.
|
||||
pub last_error: Option<String>,
|
||||
pub interfaces: Vec<InterfaceStatus>,
|
||||
}
|
||||
|
||||
/// The machine's interfaces, routes and nexthop objects as the kernel
|
||||
/// last described them, as far as next hops are concerned; `None` until
|
||||
/// it has. Whoever holds one is told when they change.
|
||||
pub type TopologyWatch = watch::Receiver<Option<Arc<topology::Topology>>>;
|
||||
|
||||
/// How the task that keeps the kernel's routes is to end.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum Stop {
|
||||
/// Taking our routes out of the kernel.
|
||||
Withdraw,
|
||||
/// Leaving them there, for the next run to find.
|
||||
Keep,
|
||||
}
|
||||
|
||||
struct Shared {
|
||||
config: FibConfig,
|
||||
status: Mutex<Status>,
|
||||
topology: watch::Sender<Option<Arc<topology::Topology>>>,
|
||||
stop: watch::Sender<Option<Stop>>,
|
||||
/// How many routes of an earlier run were found in the kernel at the
|
||||
/// start, once that has been looked at.
|
||||
adopted: watch::Sender<Option<usize>>,
|
||||
/// Set to have what is kept of an earlier run go now.
|
||||
release: watch::Sender<bool>,
|
||||
/// Whether the task has ended.
|
||||
ended: watch::Sender<bool>,
|
||||
}
|
||||
|
||||
/// Handle to the task that keeps the kernel's routes. Cheap to clone.
|
||||
#[derive(Clone)]
|
||||
pub struct Fib {
|
||||
shared: Arc<Shared>,
|
||||
}
|
||||
|
||||
impl Fib {
|
||||
/// Starts keeping the kernel's routes in step with the best routes of
|
||||
/// `rib`, and returns a handle with the task that does it. The task
|
||||
/// ends only by failing, or when [`Fib::stop`] or [`Fib::leave`] is
|
||||
/// called.
|
||||
///
|
||||
/// Must be called from within a Tokio runtime. Fails at once where
|
||||
/// there is no kernel to talk to in this way.
|
||||
pub fn spawn(
|
||||
rib: Arc<RibTable>,
|
||||
config: FibConfig,
|
||||
) -> Result<(Self, JoinHandle<Result<(), FibError>>), FibError> {
|
||||
let (stop, stopping) = watch::channel(None);
|
||||
let fib = Self {
|
||||
shared: Arc::new(Shared {
|
||||
config,
|
||||
status: Mutex::default(),
|
||||
topology: watch::channel(None).0,
|
||||
stop,
|
||||
adopted: watch::channel(None).0,
|
||||
release: watch::channel(false).0,
|
||||
ended: watch::channel(false).0,
|
||||
}),
|
||||
};
|
||||
let task = start(rib, fib.clone(), stopping)?;
|
||||
Ok((fib, task))
|
||||
}
|
||||
|
||||
pub fn config(&self) -> FibConfig {
|
||||
self.shared.config
|
||||
}
|
||||
|
||||
/// The kernel's view of the machine, for those who want to know
|
||||
/// whether a next hop can be reached before the kernel is asked to use
|
||||
/// it.
|
||||
pub fn topology(&self) -> TopologyWatch {
|
||||
self.shared.topology.subscribe()
|
||||
}
|
||||
|
||||
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
|
||||
fn publish_topology(&self, topology: &topology::Topology) {
|
||||
self.shared
|
||||
.topology
|
||||
.send_replace(Some(Arc::new(topology.clone())));
|
||||
}
|
||||
|
||||
pub fn status(&self) -> Status {
|
||||
self.shared
|
||||
.status
|
||||
.lock()
|
||||
.unwrap_or_else(PoisonError::into_inner)
|
||||
.clone()
|
||||
}
|
||||
|
||||
/// Has the task take our routes out of the kernel and end, and waits
|
||||
/// until it has. Returns at once if it has ended already, for whatever
|
||||
/// reason; the task's own result says how.
|
||||
pub async fn stop(&self) {
|
||||
self.end(Stop::Withdraw).await;
|
||||
}
|
||||
|
||||
/// Has the task stop following the routing table and end, leaving our
|
||||
/// routes in the kernel for the next run to find and keep for a while
|
||||
/// (see [`FibConfig::stale_time`]), and waits until it has. For a
|
||||
/// restart that is to go unnoticed by the packets being forwarded.
|
||||
pub async fn leave(&self) {
|
||||
self.end(Stop::Keep).await;
|
||||
}
|
||||
|
||||
async fn end(&self, how: Stop) {
|
||||
let mut ended = self.shared.ended.subscribe();
|
||||
self.shared.stop.send_if_modified(|stop| {
|
||||
let first = stop.is_none();
|
||||
if first {
|
||||
*stop = Some(how);
|
||||
}
|
||||
first
|
||||
});
|
||||
// Fails only if the handle that holds the sender is gone, and this
|
||||
// is one.
|
||||
let _ = ended.wait_for(|ended| *ended).await;
|
||||
}
|
||||
|
||||
/// How many routes of an earlier run the kernel had at the start, once
|
||||
/// the task has looked; zero if it ended before it did.
|
||||
pub async fn adopted(&self) -> usize {
|
||||
let mut adopted = self.shared.adopted.subscribe();
|
||||
let mut ended = self.shared.ended.subscribe();
|
||||
tokio::select! {
|
||||
found = adopted.wait_for(Option::is_some) => {
|
||||
found.ok().and_then(|found| *found).unwrap_or(0)
|
||||
}
|
||||
_ = ended.wait_for(|ended| *ended) => 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Has the routes of an earlier run that the routing table does not
|
||||
/// have go now, rather than when [`FibConfig::stale_time`] is up: the
|
||||
/// peers have all sent their routes, and what is not among them is no
|
||||
/// longer anybody's.
|
||||
pub fn release_kept(&self) {
|
||||
self.shared.release.send_replace(true);
|
||||
}
|
||||
|
||||
/// Changes the status, and gives back what `change` does.
|
||||
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
|
||||
fn update<R>(&self, change: impl FnOnce(&mut Status) -> R) -> R {
|
||||
change(
|
||||
&mut self
|
||||
.shared
|
||||
.status
|
||||
.lock()
|
||||
.unwrap_or_else(PoisonError::into_inner),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn start(
|
||||
rib: Arc<RibTable>,
|
||||
fib: Fib,
|
||||
stopping: watch::Receiver<Option<Stop>>,
|
||||
) -> Result<JoinHandle<Result<(), FibError>>, FibError> {
|
||||
// Opened here, so that a machine that cannot is known of at once.
|
||||
let sockets = kernel::Sockets::open()?;
|
||||
Ok(tokio::spawn(async move {
|
||||
let result = kernel::run(rib, fib.clone(), sockets, stopping).await;
|
||||
fib.shared.ended.send_replace(true);
|
||||
result
|
||||
}))
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
fn start(
|
||||
_rib: Arc<RibTable>,
|
||||
_fib: Fib,
|
||||
_stopping: watch::Receiver<Option<Stop>>,
|
||||
) -> Result<JoinHandle<Result<(), FibError>>, FibError> {
|
||||
Err(FibError::Unsupported)
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -1,266 +0,0 @@
|
||||
//! The kernel's own routes, as far as next hops are concerned: the way to
|
||||
//! a next hop that is not a neighbor on any of our interfaces.
|
||||
//!
|
||||
//! Only the routes of one table count, the resolution table, which is
|
||||
//! normally `main`: the connected and static routes, and an IGP's if there
|
||||
//! is one. Routes put there by BGP, ours or another daemon's, never count:
|
||||
//! a next hop that is reached only by way of a BGP route is reached by way
|
||||
//! of something that may itself depend on a next hop, and a table that
|
||||
//! leans on itself like that is one that falls over in a way nobody can
|
||||
//! predict. The default route counts only if asked for: on a router, the
|
||||
//! default points at a transit provider, and a next hop that is "reached"
|
||||
//! only by sending the packets to the Internet at large is not reached.
|
||||
|
||||
use std::net::IpAddr;
|
||||
|
||||
use bgp_primitives::constants::IpVersion;
|
||||
use bgp_primitives::ip_prefix::IpPrefix;
|
||||
|
||||
use crate::netlink::{Message, PROTOCOL_BGP, Route};
|
||||
use crate::plan::HashMap;
|
||||
|
||||
/// The routes of the resolution table that may say the way to a next hop.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct KernelRoutes {
|
||||
table: u32,
|
||||
/// By prefix, the routes there are for it: usually one, but the kernel
|
||||
/// keeps one for each metric.
|
||||
routes: HashMap<IpPrefix, Vec<Route>>,
|
||||
/// How many prefixes there are of each length, by IP version: the
|
||||
/// lengths a lookup has to try.
|
||||
lengths: [[u32; 129]; 2],
|
||||
}
|
||||
|
||||
/// `address` as the prefix that is only it.
|
||||
pub(crate) fn host(address: IpAddr) -> IpPrefix {
|
||||
match address {
|
||||
IpAddr::V4(v4) => IpPrefix::new(IpVersion::Ipv4, v4.octets(), 32),
|
||||
IpAddr::V6(v6) => IpPrefix::new(IpVersion::Ipv6, v6.octets(), 128),
|
||||
}
|
||||
.expect("an address is a prefix of its own length")
|
||||
}
|
||||
|
||||
fn version_index(version: IpVersion) -> usize {
|
||||
match version {
|
||||
IpVersion::Ipv4 => 0,
|
||||
IpVersion::Ipv6 => 1,
|
||||
}
|
||||
}
|
||||
|
||||
impl KernelRoutes {
|
||||
pub fn new(table: u32) -> Self {
|
||||
Self {
|
||||
table,
|
||||
routes: HashMap::default(),
|
||||
lengths: [[0; 129]; 2],
|
||||
}
|
||||
}
|
||||
|
||||
/// The table the routes are of.
|
||||
pub fn table(&self) -> u32 {
|
||||
self.table
|
||||
}
|
||||
|
||||
/// Whether `route` is one that may say the way to a next hop.
|
||||
fn counts(&self, route: &Route) -> bool {
|
||||
route.table == self.table && route.protocol != PROTOCOL_BGP
|
||||
}
|
||||
|
||||
/// Takes in what a message from the kernel says about routes. Returns
|
||||
/// whether anything is different for it.
|
||||
pub fn apply(&mut self, message: &Message) -> bool {
|
||||
let Message::Route { present, route } = message else {
|
||||
return false;
|
||||
};
|
||||
if !self.counts(route) {
|
||||
return false;
|
||||
}
|
||||
let version = version_index(route.prefix.address_family());
|
||||
let length = usize::from(route.prefix.length());
|
||||
if *present {
|
||||
let routes = self.routes.entry(route.prefix).or_default();
|
||||
// A route for a prefix with a metric there is one for already
|
||||
// takes that one's place: the kernel keeps one per metric.
|
||||
match routes.iter_mut().find(|known| known.metric == route.metric) {
|
||||
Some(known) if known == route => false,
|
||||
Some(known) => {
|
||||
*known = *route;
|
||||
true
|
||||
}
|
||||
None => {
|
||||
if routes.is_empty() {
|
||||
self.lengths[version][length] += 1;
|
||||
}
|
||||
// In order of metric, so that two tables with the
|
||||
// same routes compare equal however they were told.
|
||||
let at = routes.partition_point(|known| known.metric < route.metric);
|
||||
routes.insert(at, *route);
|
||||
true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let Some(routes) = self.routes.get_mut(&route.prefix) else {
|
||||
return false;
|
||||
};
|
||||
let before = routes.len();
|
||||
routes.retain(|known| known.metric != route.metric);
|
||||
let (after, none_left) = (routes.len(), routes.is_empty());
|
||||
if none_left {
|
||||
self.routes.remove(&route.prefix);
|
||||
self.lengths[version][length] -= 1;
|
||||
}
|
||||
before != after
|
||||
}
|
||||
}
|
||||
|
||||
/// How many prefixes have a route.
|
||||
pub fn len(&self) -> usize {
|
||||
self.routes.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.routes.is_empty()
|
||||
}
|
||||
|
||||
/// The route the kernel would send packets for `address` by, from
|
||||
/// this table: the one for the longest prefix that covers it, and of
|
||||
/// several for that prefix the one with the lowest metric. The
|
||||
/// default route too, if there is one and nothing longer covers the
|
||||
/// address.
|
||||
pub fn lookup(&self, address: IpAddr) -> Option<&Route> {
|
||||
let whole = host(address);
|
||||
let lengths = &self.lengths[version_index(whole.address_family())];
|
||||
(0..=whole.length())
|
||||
.rev()
|
||||
.filter(|length| lengths[usize::from(*length)] > 0)
|
||||
.find_map(|length| self.routes.get(&whole.shortened(length)))
|
||||
.and_then(|routes| routes.iter().min_by_key(|route| route.metric))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::netlink::{KIND_UNICAST, TABLE_MAIN};
|
||||
|
||||
fn prefix(s: &str) -> IpPrefix {
|
||||
IpPrefix::try_from(s).unwrap()
|
||||
}
|
||||
|
||||
fn route(prefix_text: &str, gateway: Option<&str>, interface: u32, metric: u32) -> Route {
|
||||
Route {
|
||||
prefix: prefix(prefix_text),
|
||||
table: TABLE_MAIN,
|
||||
protocol: 4, // static
|
||||
metric,
|
||||
gateway: gateway.map(|g| g.parse().unwrap()),
|
||||
interface: Some(interface),
|
||||
nexthop: None,
|
||||
kind: KIND_UNICAST,
|
||||
}
|
||||
}
|
||||
|
||||
fn present(route: Route) -> Message {
|
||||
Message::Route {
|
||||
present: true,
|
||||
route,
|
||||
}
|
||||
}
|
||||
|
||||
fn gone(route: Route) -> Message {
|
||||
Message::Route {
|
||||
present: false,
|
||||
route,
|
||||
}
|
||||
}
|
||||
|
||||
fn lookup(routes: &KernelRoutes, address: &str) -> Option<(String, Option<String>, u32)> {
|
||||
routes.lookup(address.parse().unwrap()).map(|route| {
|
||||
(
|
||||
route.prefix.to_string(),
|
||||
route.gateway.map(|g| g.to_string()),
|
||||
route.metric,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_longest_prefix_with_the_lowest_metric_is_the_way() {
|
||||
let mut routes = KernelRoutes::new(TABLE_MAIN);
|
||||
for r in [
|
||||
route("0.0.0.0/0", Some("192.0.2.1"), 2, 100),
|
||||
route("10.0.0.0/8", Some("192.0.2.9"), 2, 0),
|
||||
route("10.1.0.0/16", None, 7, 0),
|
||||
route("10.1.0.0/16", Some("198.51.100.1"), 3, 50),
|
||||
route("2001:db8::/32", Some("2001:db8::1"), 2, 1024),
|
||||
] {
|
||||
assert!(routes.apply(&present(r)));
|
||||
}
|
||||
assert_eq!(routes.len(), 4);
|
||||
assert_eq!(
|
||||
lookup(&routes, "10.2.3.4"),
|
||||
Some(("10.0.0.0/8".into(), Some("192.0.2.9".into()), 0))
|
||||
);
|
||||
// Two routes for the prefix: the lower metric wins.
|
||||
assert_eq!(
|
||||
lookup(&routes, "10.1.2.3"),
|
||||
Some(("10.1.0.0/16".into(), None, 0))
|
||||
);
|
||||
assert_eq!(
|
||||
lookup(&routes, "203.0.113.1"),
|
||||
Some(("0.0.0.0/0".into(), Some("192.0.2.1".into()), 100))
|
||||
);
|
||||
assert_eq!(
|
||||
lookup(&routes, "2001:db8:1::1"),
|
||||
Some(("2001:db8::/32".into(), Some("2001:db8::1".into()), 1024))
|
||||
);
|
||||
assert_eq!(lookup(&routes, "2001:db9::1"), None);
|
||||
|
||||
// Told again what is known: nothing changes.
|
||||
assert!(!routes.apply(&present(route("10.0.0.0/8", Some("192.0.2.9"), 2, 0))));
|
||||
// The same prefix and metric with another gateway: replaced.
|
||||
assert!(routes.apply(&present(route("10.0.0.0/8", Some("192.0.2.8"), 2, 0))));
|
||||
assert_eq!(
|
||||
lookup(&routes, "10.2.3.4"),
|
||||
Some(("10.0.0.0/8".into(), Some("192.0.2.8".into()), 0))
|
||||
);
|
||||
// The better of the two for 10.1/16 goes: the other is the way.
|
||||
assert!(routes.apply(&gone(route("10.1.0.0/16", None, 7, 0))));
|
||||
assert_eq!(
|
||||
lookup(&routes, "10.1.2.3"),
|
||||
Some(("10.1.0.0/16".into(), Some("198.51.100.1".into()), 50))
|
||||
);
|
||||
assert!(routes.apply(&gone(route("10.1.0.0/16", Some("198.51.100.1"), 3, 50))));
|
||||
assert_eq!(
|
||||
lookup(&routes, "10.1.2.3"),
|
||||
Some(("10.0.0.0/8".into(), Some("192.0.2.8".into()), 0))
|
||||
);
|
||||
// Gone already: nothing changes.
|
||||
assert!(!routes.apply(&gone(route("10.1.0.0/16", None, 7, 0))));
|
||||
assert_eq!(routes.len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn routes_of_other_tables_and_of_bgp_do_not_count() {
|
||||
let mut routes = KernelRoutes::new(TABLE_MAIN);
|
||||
let other_table = Route {
|
||||
table: 200,
|
||||
..route("10.0.0.0/8", Some("192.0.2.9"), 2, 0)
|
||||
};
|
||||
let bgp = Route {
|
||||
protocol: PROTOCOL_BGP,
|
||||
..route("10.0.0.0/8", Some("192.0.2.9"), 2, 20)
|
||||
};
|
||||
assert!(!routes.apply(&present(other_table)));
|
||||
assert!(!routes.apply(&present(bgp)));
|
||||
assert!(routes.is_empty());
|
||||
assert_eq!(lookup(&routes, "10.2.3.4"), None);
|
||||
// What is not about routes changes nothing either.
|
||||
assert!(!routes.apply(&Message::Done));
|
||||
|
||||
let mut table_200 = KernelRoutes::new(200);
|
||||
assert!(table_200.apply(&present(other_table)));
|
||||
assert_eq!(table_200.table(), 200);
|
||||
assert_eq!(table_200.len(), 1);
|
||||
}
|
||||
}
|
||||
@@ -1,420 +0,0 @@
|
||||
//! What the kernel knows about one of our TCP connections: the round-trip
|
||||
//! time, the windows, the queues and what has held the connection up,
|
||||
//! asked over the socket diagnostics netlink family (`NETLINK_SOCK_DIAG`,
|
||||
//! as `ss -ti` asks). For the status page, when someone wants to know
|
||||
//! why a session is slow.
|
||||
|
||||
use std::io;
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
|
||||
use crate::netlink::{
|
||||
AF_INET, AF_INET6, HEADER_LEN, attributes, flag, message_type, ne_u16, ne_u32, ne_u64,
|
||||
};
|
||||
|
||||
/// A TCP connection as the kernel sees it. Times are in microseconds,
|
||||
/// sizes in octets, rates in octets per second, unless said otherwise.
|
||||
/// Fields the running kernel does not report are zero.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct TcpConnection {
|
||||
/// The TCP state, by its number (1 is ESTABLISHED); see
|
||||
/// [`TcpConnection::state_name`].
|
||||
pub state: u8,
|
||||
/// The congestion control algorithm, if the kernel named it.
|
||||
pub congestion: Option<String>,
|
||||
/// Octets received and not yet read by us.
|
||||
pub recv_queue: u32,
|
||||
/// Octets written by us and not yet acknowledged by the peer.
|
||||
pub send_queue: u32,
|
||||
pub rtt: u32,
|
||||
pub rtt_variance: u32,
|
||||
pub min_rtt: u32,
|
||||
pub rto: u32,
|
||||
pub mss: u32,
|
||||
pub pmtu: u32,
|
||||
/// The congestion window, in segments.
|
||||
pub cwnd: u32,
|
||||
/// The slow start threshold, in segments; `None` if there is none.
|
||||
pub ssthresh: Option<u32>,
|
||||
/// What the peer lets us send: its receive window.
|
||||
pub send_window: u32,
|
||||
/// What we let the peer send.
|
||||
pub receive_window: u32,
|
||||
pub receive_space: u32,
|
||||
pub receive_ssthresh: u32,
|
||||
pub send_window_scale: u8,
|
||||
pub receive_window_scale: u8,
|
||||
/// Segments sent and not acknowledged.
|
||||
pub unacked: u32,
|
||||
/// Segments retransmitted and still out.
|
||||
pub retrans: u32,
|
||||
pub lost: u32,
|
||||
pub total_retrans: u32,
|
||||
pub bytes_retrans: u64,
|
||||
/// Octets in the socket's buffer that are not sent yet.
|
||||
pub not_sent: u32,
|
||||
pub pacing_rate: u64,
|
||||
pub delivery_rate: u64,
|
||||
pub bytes_sent: u64,
|
||||
pub bytes_acked: u64,
|
||||
pub bytes_received: u64,
|
||||
pub segments_out: u32,
|
||||
pub segments_in: u32,
|
||||
/// How long the connection had something to send.
|
||||
pub busy: u64,
|
||||
/// Of that, how long it was held up by the peer's receive window.
|
||||
pub limited_by_receive_window: u64,
|
||||
/// And how long by our own send buffer being full.
|
||||
pub limited_by_send_buffer: u64,
|
||||
pub delivered: u32,
|
||||
pub reordering: u32,
|
||||
/// Milliseconds since data was last sent, received, and since the
|
||||
/// last acknowledgement came.
|
||||
pub last_data_sent_ms: u32,
|
||||
pub last_data_received_ms: u32,
|
||||
pub last_ack_received_ms: u32,
|
||||
}
|
||||
|
||||
impl TcpConnection {
|
||||
pub fn state_name(&self) -> &'static str {
|
||||
match self.state {
|
||||
1 => "established",
|
||||
2 => "syn-sent",
|
||||
3 => "syn-received",
|
||||
4 => "fin-wait-1",
|
||||
5 => "fin-wait-2",
|
||||
6 => "time-wait",
|
||||
7 => "close",
|
||||
8 => "close-wait",
|
||||
9 => "last-ack",
|
||||
10 => "listen",
|
||||
11 => "closing",
|
||||
_ => "unknown",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const SOCK_DIAG_BY_FAMILY: u16 = 20;
|
||||
const IPPROTO_TCP: u8 = 6;
|
||||
const INET_DIAG_INFO: u16 = 2;
|
||||
const INET_DIAG_CONG: u16 = 4;
|
||||
/// `idiag_ext` bits asking for those two.
|
||||
const EXTENSIONS: u8 = (1 << (INET_DIAG_INFO - 1)) | (1 << (INET_DIAG_CONG - 1));
|
||||
const NO_COOKIE: u32 = u32::MAX;
|
||||
const REQUEST_LEN: usize = HEADER_LEN + 56;
|
||||
const DIAG_MSG_LEN: usize = 72;
|
||||
|
||||
/// The request for the connection from `local` to `peer`, with `sequence`.
|
||||
pub fn request(local: SocketAddr, peer: SocketAddr, sequence: u32) -> Vec<u8> {
|
||||
let family = match local {
|
||||
SocketAddr::V4(_) => AF_INET,
|
||||
SocketAddr::V6(_) => AF_INET6,
|
||||
};
|
||||
let mut buf = Vec::with_capacity(REQUEST_LEN);
|
||||
buf.extend_from_slice(&(REQUEST_LEN as u32).to_ne_bytes());
|
||||
buf.extend_from_slice(&SOCK_DIAG_BY_FAMILY.to_ne_bytes());
|
||||
buf.extend_from_slice(&flag::REQUEST.to_ne_bytes());
|
||||
buf.extend_from_slice(&sequence.to_ne_bytes());
|
||||
buf.extend_from_slice(&0u32.to_ne_bytes());
|
||||
// inet_diag_req_v2
|
||||
buf.extend_from_slice(&[family, IPPROTO_TCP, EXTENSIONS, 0]);
|
||||
buf.extend_from_slice(&u32::MAX.to_ne_bytes()); // any state
|
||||
// inet_diag_sockid: ports in network order, addresses as 16 octets.
|
||||
buf.extend_from_slice(&local.port().to_be_bytes());
|
||||
buf.extend_from_slice(&peer.port().to_be_bytes());
|
||||
buf.extend_from_slice(&address_octets(local.ip()));
|
||||
buf.extend_from_slice(&address_octets(peer.ip()));
|
||||
// The interface, which a socket on a link-local address is bound to
|
||||
// and is found by.
|
||||
let interface = match local {
|
||||
SocketAddr::V6(v6) => v6.scope_id(),
|
||||
SocketAddr::V4(_) => 0,
|
||||
};
|
||||
buf.extend_from_slice(&interface.to_ne_bytes());
|
||||
buf.extend_from_slice(&NO_COOKIE.to_ne_bytes());
|
||||
buf.extend_from_slice(&NO_COOKIE.to_ne_bytes());
|
||||
debug_assert_eq!(buf.len(), REQUEST_LEN);
|
||||
buf
|
||||
}
|
||||
|
||||
fn address_octets(address: IpAddr) -> [u8; 16] {
|
||||
let mut octets = [0u8; 16];
|
||||
match address {
|
||||
IpAddr::V4(v4) => octets[..4].copy_from_slice(&v4.octets()),
|
||||
IpAddr::V6(v6) => octets = v6.octets(),
|
||||
}
|
||||
octets
|
||||
}
|
||||
|
||||
/// What the kernel answered.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum Answer {
|
||||
Connection(Box<TcpConnection>),
|
||||
/// No such connection.
|
||||
None,
|
||||
/// The kernel refused, with this error number.
|
||||
Refused(i32),
|
||||
}
|
||||
|
||||
/// Reads the kernel's answer to a [`request`].
|
||||
pub fn parse(buf: &[u8]) -> io::Result<Answer> {
|
||||
let header = buf
|
||||
.get(..HEADER_LEN)
|
||||
.ok_or_else(|| io::Error::other("short netlink answer"))?;
|
||||
let length = ne_u32(&header[0..4]) as usize;
|
||||
let kind = ne_u16(&header[4..6]);
|
||||
let message = buf
|
||||
.get(HEADER_LEN..length.min(buf.len()))
|
||||
.ok_or_else(|| io::Error::other("short netlink answer"))?;
|
||||
if kind == message_type::ERROR {
|
||||
let errno = -(message
|
||||
.first_chunk::<4>()
|
||||
.map_or(0, |e| i32::from_ne_bytes(*e)));
|
||||
// ENOENT: no such socket.
|
||||
return Ok(if errno == 2 {
|
||||
Answer::None
|
||||
} else {
|
||||
Answer::Refused(errno)
|
||||
});
|
||||
}
|
||||
if kind != SOCK_DIAG_BY_FAMILY || message.len() < DIAG_MSG_LEN {
|
||||
return Err(io::Error::other("not a socket diagnostic"));
|
||||
}
|
||||
let mut connection = TcpConnection {
|
||||
state: message[1],
|
||||
recv_queue: ne_u32(&message[56..60]),
|
||||
send_queue: ne_u32(&message[60..64]),
|
||||
..TcpConnection::default()
|
||||
};
|
||||
for (kind, value) in attributes(&message[DIAG_MSG_LEN..]) {
|
||||
match kind {
|
||||
INET_DIAG_INFO => fill_from_tcp_info(&mut connection, value),
|
||||
INET_DIAG_CONG => {
|
||||
let name = value.split(|b| *b == 0).next().unwrap_or(&[]);
|
||||
connection.congestion = Some(String::from_utf8_lossy(name).into_owned());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Ok(Answer::Connection(Box::new(connection)))
|
||||
}
|
||||
|
||||
/// Fills in what `tcp_info` (linux/tcp.h) has, as far as the kernel sent
|
||||
/// it: the structure has grown over the years, and a field that is not
|
||||
/// there is left at zero.
|
||||
fn fill_from_tcp_info(c: &mut TcpConnection, info: &[u8]) {
|
||||
let u32_at = |at: usize| info.get(at..at + 4).map_or(0, ne_u32);
|
||||
let u64_at = |at: usize| info.get(at..at + 8).map_or(0, ne_u64);
|
||||
c.send_window_scale = info.get(6).map_or(0, |b| b & 0x0f);
|
||||
c.receive_window_scale = info.get(6).map_or(0, |b| b >> 4);
|
||||
c.rto = u32_at(8);
|
||||
c.mss = u32_at(16);
|
||||
c.unacked = u32_at(24);
|
||||
c.lost = u32_at(32);
|
||||
c.retrans = u32_at(36);
|
||||
c.last_data_sent_ms = u32_at(44);
|
||||
c.last_data_received_ms = u32_at(52);
|
||||
c.last_ack_received_ms = u32_at(56);
|
||||
c.pmtu = u32_at(60);
|
||||
c.receive_ssthresh = u32_at(64);
|
||||
c.rtt = u32_at(68);
|
||||
c.rtt_variance = u32_at(72);
|
||||
c.ssthresh = Some(u32_at(76)).filter(|t| *t < 0x7fff_ffff);
|
||||
c.cwnd = u32_at(80);
|
||||
c.reordering = u32_at(88);
|
||||
c.receive_space = u32_at(96);
|
||||
c.total_retrans = u32_at(100);
|
||||
c.pacing_rate = u64_at(104);
|
||||
c.bytes_acked = u64_at(120);
|
||||
c.bytes_received = u64_at(128);
|
||||
c.segments_out = u32_at(136);
|
||||
c.segments_in = u32_at(140);
|
||||
c.not_sent = u32_at(144);
|
||||
c.min_rtt = u32_at(148);
|
||||
c.delivery_rate = u64_at(160);
|
||||
c.busy = u64_at(168);
|
||||
c.limited_by_receive_window = u64_at(176);
|
||||
c.limited_by_send_buffer = u64_at(184);
|
||||
c.delivered = u32_at(192);
|
||||
c.bytes_sent = u64_at(200);
|
||||
c.bytes_retrans = u64_at(208);
|
||||
c.send_window = u32_at(228);
|
||||
c.receive_window = u32_at(232);
|
||||
}
|
||||
|
||||
/// Asks the kernel about the TCP connection from `local` to `peer`.
|
||||
/// `Ok(None)` if there is no such connection, or on an operating system
|
||||
/// that cannot be asked.
|
||||
#[cfg(target_os = "linux")]
|
||||
pub async fn tcp_connection(
|
||||
local: SocketAddr,
|
||||
peer: SocketAddr,
|
||||
) -> io::Result<Option<TcpConnection>> {
|
||||
use netlink_sys::protocols::NETLINK_SOCK_DIAG;
|
||||
use netlink_sys::{AsyncSocket, AsyncSocketExt, TokioSocket};
|
||||
|
||||
let mut socket = TokioSocket::new(NETLINK_SOCK_DIAG)?;
|
||||
socket
|
||||
.socket_mut()
|
||||
.bind(&netlink_sys::SocketAddr::new(0, 0))?;
|
||||
let sequence = 1;
|
||||
socket.send(&request(local, peer, sequence)).await?;
|
||||
let mut buf = bytes::BytesMut::with_capacity(4096);
|
||||
tokio::time::timeout(std::time::Duration::from_secs(2), socket.recv(&mut buf))
|
||||
.await
|
||||
.map_err(|_| io::Error::new(io::ErrorKind::TimedOut, "the kernel did not answer"))??;
|
||||
match parse(&buf)? {
|
||||
Answer::Connection(connection) => Ok(Some(*connection)),
|
||||
Answer::None => Ok(None),
|
||||
Answer::Refused(errno) => Err(io::Error::from_raw_os_error(errno)),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
pub async fn tcp_connection(
|
||||
_local: SocketAddr,
|
||||
_peer: SocketAddr,
|
||||
) -> io::Result<Option<TcpConnection>> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// A reply as the kernel writes it, for an established connection,
|
||||
/// with the first `info_len` octets of a `tcp_info` of the full
|
||||
/// modern length: an older kernel sends a shorter one.
|
||||
fn reply(info_len: usize) -> Vec<u8> {
|
||||
let mut info = vec![0u8; 248];
|
||||
info[0] = 1; // established
|
||||
info[6] = 0x7a; // snd_wscale 10, rcv_wscale 7
|
||||
let put32 = |info: &mut Vec<u8>, at: usize, v: u32| {
|
||||
info[at..at + 4].copy_from_slice(&v.to_ne_bytes())
|
||||
};
|
||||
let put64 = |info: &mut Vec<u8>, at: usize, v: u64| {
|
||||
info[at..at + 8].copy_from_slice(&v.to_ne_bytes())
|
||||
};
|
||||
put32(&mut info, 16, 1448); // mss
|
||||
put32(&mut info, 24, 12); // unacked
|
||||
put32(&mut info, 68, 2300); // rtt
|
||||
put32(&mut info, 76, 0x7fff_ffff); // no ssthresh
|
||||
put32(&mut info, 80, 42); // cwnd
|
||||
put32(&mut info, 144, 65536); // notsent
|
||||
put64(&mut info, 176, 900_000); // rwnd limited
|
||||
put64(&mut info, 168, 1_000_000); // busy
|
||||
put32(&mut info, 228, 131072); // snd_wnd
|
||||
put32(&mut info, 232, 262144); // rcv_wnd
|
||||
info.truncate(info_len);
|
||||
let cong = b"cubic\0";
|
||||
let mut message = vec![0u8; DIAG_MSG_LEN];
|
||||
message[1] = 1;
|
||||
message[60..64].copy_from_slice(&80_000u32.to_ne_bytes()); // wqueue
|
||||
// attributes: INFO, then CONG
|
||||
let mut attrs = Vec::new();
|
||||
attrs.extend_from_slice(&((4 + info.len()) as u16).to_ne_bytes());
|
||||
attrs.extend_from_slice(&INET_DIAG_INFO.to_ne_bytes());
|
||||
attrs.extend_from_slice(&info);
|
||||
attrs.extend_from_slice(&((4 + cong.len()) as u16).to_ne_bytes());
|
||||
attrs.extend_from_slice(&INET_DIAG_CONG.to_ne_bytes());
|
||||
attrs.extend_from_slice(cong);
|
||||
while attrs.len() % 4 != 0 {
|
||||
attrs.push(0);
|
||||
}
|
||||
let length = HEADER_LEN + message.len() + attrs.len();
|
||||
let mut buf = Vec::new();
|
||||
buf.extend_from_slice(&(length as u32).to_ne_bytes());
|
||||
buf.extend_from_slice(&SOCK_DIAG_BY_FAMILY.to_ne_bytes());
|
||||
buf.extend_from_slice(&0u16.to_ne_bytes()); // flags
|
||||
buf.extend_from_slice(&1u32.to_ne_bytes()); // sequence
|
||||
buf.extend_from_slice(&0u32.to_ne_bytes()); // port
|
||||
buf.extend_from_slice(&message);
|
||||
buf.extend_from_slice(&attrs);
|
||||
buf
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_reply_is_read_field_by_field() {
|
||||
let Answer::Connection(c) = parse(&reply(248)).unwrap() else {
|
||||
panic!("a connection");
|
||||
};
|
||||
assert_eq!(c.state_name(), "established");
|
||||
assert_eq!(c.congestion.as_deref(), Some("cubic"));
|
||||
assert_eq!(c.send_queue, 80_000);
|
||||
assert_eq!((c.send_window_scale, c.receive_window_scale), (10, 7));
|
||||
assert_eq!((c.mss, c.unacked, c.rtt, c.cwnd), (1448, 12, 2300, 42));
|
||||
assert_eq!(c.ssthresh, None);
|
||||
assert_eq!(c.not_sent, 65536);
|
||||
assert_eq!((c.busy, c.limited_by_receive_window), (1_000_000, 900_000));
|
||||
assert_eq!((c.send_window, c.receive_window), (131072, 262144));
|
||||
}
|
||||
|
||||
/// Linux 4.x sends 104 octets of `tcp_info`, up to `tcpi_total_retrans`:
|
||||
/// what is there is read, and what came later is left at zero rather
|
||||
/// than read from the attribute after it.
|
||||
#[test]
|
||||
fn a_short_tcp_info_from_an_old_kernel_leaves_later_fields_zero() {
|
||||
let Answer::Connection(c) = parse(&reply(104)).unwrap() else {
|
||||
panic!("a connection");
|
||||
};
|
||||
assert_eq!(c.congestion.as_deref(), Some("cubic"));
|
||||
assert_eq!((c.mss, c.rtt, c.cwnd), (1448, 2300, 42));
|
||||
assert_eq!((c.busy, c.send_window), (0, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_such_connection_and_a_refusal_are_told_apart() {
|
||||
let error = |errno: i32| {
|
||||
let mut buf = vec![0u8; 20];
|
||||
buf[0..4].copy_from_slice(&20u32.to_ne_bytes());
|
||||
buf[4..6].copy_from_slice(&message_type::ERROR.to_ne_bytes());
|
||||
buf[16..20].copy_from_slice(&(-errno).to_ne_bytes());
|
||||
buf
|
||||
};
|
||||
assert_eq!(parse(&error(2)).unwrap(), Answer::None);
|
||||
assert_eq!(parse(&error(13)).unwrap(), Answer::Refused(13));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_request_names_the_connection() {
|
||||
let local: SocketAddr = "[2001:db8::1]:179".parse().unwrap();
|
||||
let peer: SocketAddr = "[2001:db8::2]:40000".parse().unwrap();
|
||||
let request = request(local, peer, 7);
|
||||
assert_eq!(request.len(), REQUEST_LEN);
|
||||
assert_eq!(request[16], AF_INET6);
|
||||
assert_eq!(&request[24..26], &179u16.to_be_bytes());
|
||||
assert_eq!(&request[26..28], &40000u16.to_be_bytes());
|
||||
assert_eq!(
|
||||
&request[28..44],
|
||||
&"2001:db8::1"
|
||||
.parse::<std::net::Ipv6Addr>()
|
||||
.unwrap()
|
||||
.octets()
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[tokio::test]
|
||||
async fn the_kernel_tells_of_a_connection_of_ours() {
|
||||
use tokio::io::AsyncWriteExt;
|
||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let mut client = tokio::net::TcpStream::connect(listener.local_addr().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
let (_server, _) = listener.accept().await.unwrap();
|
||||
client.write_all(&[0u8; 1000]).await.unwrap();
|
||||
let connection = tcp_connection(client.local_addr().unwrap(), client.peer_addr().unwrap())
|
||||
.await
|
||||
.unwrap()
|
||||
.expect("the kernel knows it");
|
||||
assert_eq!(connection.state_name(), "established");
|
||||
assert!(connection.mss > 0 && connection.cwnd > 0);
|
||||
assert!(connection.bytes_sent >= 1000 || connection.bytes_acked >= 1000);
|
||||
let none = tcp_connection(
|
||||
"127.0.0.1:1".parse().unwrap(),
|
||||
"127.0.0.1:2".parse().unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(none.is_none());
|
||||
}
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
//! What the kernel knows that says whether, and how, a next hop can be
|
||||
//! reached: the machine's interfaces and addresses, the routes of the
|
||||
//! resolution table, and the kernel's nexthop objects that those routes
|
||||
//! may name. One value, replaced as a whole when any of it changes, so
|
||||
//! that everyone judging next hops judges them by one moment's view.
|
||||
|
||||
use crate::interfaces::Interfaces;
|
||||
use crate::netlink::{Message, Nexthop};
|
||||
use crate::plan::HashMap;
|
||||
use crate::routes::KernelRoutes;
|
||||
|
||||
/// The kernel's view of the machine, as far as next hops are concerned.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Topology {
|
||||
pub interfaces: Interfaces,
|
||||
/// The routes a next hop that is not a neighbor is reached by.
|
||||
pub routes: KernelRoutes,
|
||||
/// Every nexthop object the kernel has, by number, whoever made it:
|
||||
/// a route of the resolution table may name one instead of carrying
|
||||
/// its own gateway.
|
||||
pub nexthops: HashMap<u32, Nexthop>,
|
||||
/// Whether the default route counts as the way to a next hop.
|
||||
pub via_default: bool,
|
||||
}
|
||||
|
||||
impl Topology {
|
||||
/// Nothing known yet, with next hops to be resolved by the routes of
|
||||
/// `resolution_table`.
|
||||
pub fn new(resolution_table: u32, via_default: bool) -> Self {
|
||||
Self {
|
||||
interfaces: Interfaces::default(),
|
||||
routes: KernelRoutes::new(resolution_table),
|
||||
nexthops: HashMap::default(),
|
||||
via_default,
|
||||
}
|
||||
}
|
||||
|
||||
/// Takes in what a message from the kernel says. Returns whether
|
||||
/// anything is different for it: then what any next hop resolves to
|
||||
/// may be different too.
|
||||
pub fn apply(&mut self, message: &Message) -> bool {
|
||||
match message {
|
||||
Message::Nexthop {
|
||||
present: true,
|
||||
nexthop,
|
||||
} => self.nexthops.insert(nexthop.id, *nexthop) != Some(*nexthop),
|
||||
Message::Nexthop {
|
||||
present: false,
|
||||
nexthop,
|
||||
} => self.nexthops.remove(&nexthop.id).is_some(),
|
||||
Message::Route { .. } => self.routes.apply(message),
|
||||
_ => self.interfaces.apply(message),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::netlink::{KIND_UNICAST, Link, PROTOCOL_BGP, Route, TABLE_MAIN};
|
||||
use bgp_primitives::ip_prefix::IpPrefix;
|
||||
|
||||
#[test]
|
||||
fn each_kind_of_news_goes_where_it_belongs() {
|
||||
let mut topology = Topology::new(TABLE_MAIN, false);
|
||||
let nexthop = Nexthop {
|
||||
id: 7,
|
||||
protocol: 4,
|
||||
gateway: Some("192.0.2.1".parse().unwrap()),
|
||||
interface: Some(2),
|
||||
};
|
||||
let present = |nexthop| Message::Nexthop {
|
||||
present: true,
|
||||
nexthop,
|
||||
};
|
||||
assert!(topology.apply(&present(nexthop)));
|
||||
assert!(!topology.apply(&present(nexthop)));
|
||||
assert!(topology.apply(&present(Nexthop {
|
||||
interface: Some(3),
|
||||
..nexthop
|
||||
})));
|
||||
assert_eq!(topology.nexthops[&7].interface, Some(3));
|
||||
assert!(topology.apply(&Message::Nexthop {
|
||||
present: false,
|
||||
nexthop,
|
||||
}));
|
||||
assert!(topology.nexthops.is_empty());
|
||||
|
||||
let route = Route {
|
||||
prefix: IpPrefix::try_from("10.0.0.0/8").unwrap(),
|
||||
table: TABLE_MAIN,
|
||||
protocol: 4,
|
||||
metric: 0,
|
||||
gateway: Some("192.0.2.1".parse().unwrap()),
|
||||
interface: Some(2),
|
||||
nexthop: None,
|
||||
kind: KIND_UNICAST,
|
||||
};
|
||||
assert!(topology.apply(&Message::Route {
|
||||
present: true,
|
||||
route,
|
||||
}));
|
||||
assert_eq!(topology.routes.len(), 1);
|
||||
// BGP's routes are not the way to anything.
|
||||
assert!(!topology.apply(&Message::Route {
|
||||
present: true,
|
||||
route: Route {
|
||||
protocol: PROTOCOL_BGP,
|
||||
..route
|
||||
},
|
||||
}));
|
||||
assert!(topology.apply(&Message::Link {
|
||||
present: true,
|
||||
link: Link {
|
||||
index: 2,
|
||||
name: "eth0".into(),
|
||||
up: true,
|
||||
},
|
||||
}));
|
||||
assert!(topology.interfaces.link(2).is_some());
|
||||
assert!(!topology.apply(&Message::Done));
|
||||
}
|
||||
}
|
||||
@@ -1,910 +0,0 @@
|
||||
//! The kernel's routes against a real kernel: what `ip route` shows after
|
||||
//! the routing table changes.
|
||||
//!
|
||||
//! These change the machine's interfaces and routes, so they are not run
|
||||
//! by `cargo test`. `scripts/kernel-tests.sh` runs them in a network
|
||||
//! namespace of their own, with the capability they need
|
||||
//! (CAP_NET_ADMIN) and nothing more.
|
||||
#![cfg(target_os = "linux")]
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
use std::net::{IpAddr, Ipv4Addr};
|
||||
use std::process::Command;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use bgp_fib::{Fib, FibConfig, FibError};
|
||||
use bgp_packet::message::{
|
||||
AsPathAttribute, CommunitiesPathAttribute, MultiExitDiscPathAttribute, NextHopPathAttribute,
|
||||
NlriNextHop, OriginPathAttribute, PathAttribute,
|
||||
};
|
||||
use bgp_primitives::ip_prefix::IpPrefix;
|
||||
use bgp_rib::table::{AttrSet, PeerIndex, PeerInfo, RibTable};
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
const TIMEOUT: Duration = Duration::from_secs(10);
|
||||
|
||||
fn prefix(s: &str) -> IpPrefix {
|
||||
IpPrefix::try_from(s).unwrap()
|
||||
}
|
||||
|
||||
/// Runs `ip` and returns what it printed. Panics if it fails.
|
||||
#[track_caller]
|
||||
fn ip(arguments: &str) -> String {
|
||||
let output = Command::new("ip")
|
||||
.args(arguments.split_whitespace())
|
||||
.output()
|
||||
.expect("the ip command is there");
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"ip {arguments}: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
String::from_utf8_lossy(&output.stdout).into_owned()
|
||||
}
|
||||
|
||||
/// What a test has of the machine: an interface of its own with
|
||||
/// 10.`n`.0.1/24 and 2001:db8:`n`::1/64 on it, and a routing table of its
|
||||
/// own. Tests run side by side in one network namespace, and `n` keeps
|
||||
/// them apart.
|
||||
struct Machine {
|
||||
n: u8,
|
||||
rib: Arc<RibTable>,
|
||||
/// Whether routes name nexthop objects, which is how it is unless a
|
||||
/// test says otherwise.
|
||||
nexthop_objects: bool,
|
||||
/// How long routes of an earlier run are kept: not at all, unless a
|
||||
/// test says otherwise.
|
||||
stale_time: Duration,
|
||||
}
|
||||
|
||||
impl Machine {
|
||||
fn new(n: u8) -> Self {
|
||||
let machine = Self {
|
||||
n,
|
||||
rib: Arc::new(RibTable::new(4)),
|
||||
nexthop_objects: true,
|
||||
stale_time: Duration::ZERO,
|
||||
};
|
||||
ip(&format!("link add {} type dummy", machine.interface()));
|
||||
machine.add_addresses();
|
||||
ip(&format!("link set {} up", machine.interface()));
|
||||
machine
|
||||
}
|
||||
|
||||
fn interface(&self) -> String {
|
||||
format!("ferrix{}", self.n)
|
||||
}
|
||||
|
||||
fn add_addresses(&self) {
|
||||
let (n, interface) = (self.n, self.interface());
|
||||
ip(&format!("addr add 10.{n}.0.1/24 dev {interface}"));
|
||||
ip(&format!(
|
||||
"-6 addr add 2001:db8:{n}::1/64 dev {interface} nodad"
|
||||
));
|
||||
}
|
||||
|
||||
fn add_addresses_v4(&self) {
|
||||
ip(&format!(
|
||||
"addr add 10.{}.0.1/24 dev {}",
|
||||
self.n,
|
||||
self.interface()
|
||||
));
|
||||
}
|
||||
|
||||
fn table(&self) -> u32 {
|
||||
1000 + u32::from(self.n)
|
||||
}
|
||||
|
||||
/// A neighbor on the test's interface.
|
||||
fn neighbor(&self, host: u8) -> Ipv4Addr {
|
||||
Ipv4Addr::new(10, self.n, 0, host)
|
||||
}
|
||||
|
||||
fn start(&self) -> (Fib, JoinHandle<Result<(), FibError>>) {
|
||||
Fib::spawn(
|
||||
Arc::clone(&self.rib),
|
||||
FibConfig {
|
||||
table: self.table(),
|
||||
metric: 20,
|
||||
nexthop_objects: self.nexthop_objects,
|
||||
stale_time: self.stale_time,
|
||||
..FibConfig::default()
|
||||
},
|
||||
)
|
||||
.expect("the kernel can be talked to")
|
||||
}
|
||||
|
||||
fn peer(&self, host: u8) -> PeerIndex {
|
||||
self.rib
|
||||
.add_peer(PeerInfo {
|
||||
asn: 65000 + u32::from(host),
|
||||
router_id: self.neighbor(host),
|
||||
address: self.neighbor(host).into(),
|
||||
interface: 0,
|
||||
ibgp: false,
|
||||
reflector_client: false,
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Attributes with the next hop `next_hop` and, to say which of two
|
||||
/// routes is the better, `med`.
|
||||
fn via(&self, next_hop: IpAddr, med: u32) -> Arc<AttrSet> {
|
||||
let mut attributes = vec![
|
||||
PathAttribute::Origin(OriginPathAttribute::IGP),
|
||||
AsPathAttribute::from_asns(vec![65001]),
|
||||
PathAttribute::MultiExitDisc(MultiExitDiscPathAttribute(med)),
|
||||
];
|
||||
let mp_next_hop = match next_hop {
|
||||
IpAddr::V4(v4) => {
|
||||
attributes.push(PathAttribute::NextHop(NextHopPathAttribute(v4)));
|
||||
None
|
||||
}
|
||||
IpAddr::V6(v6) => Some(NlriNextHop::Ipv6(v6)),
|
||||
};
|
||||
self.rib
|
||||
.attrs()
|
||||
.intern(&attributes, mp_next_hop.as_ref())
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// The routes in the test's table, as `ip route` prints them.
|
||||
fn kernel_routes(&self) -> BTreeSet<String> {
|
||||
let table = self.table();
|
||||
let mut routes = BTreeSet::new();
|
||||
for family in ["-4", "-6"] {
|
||||
// A table nothing was ever put in does not exist yet.
|
||||
let output = Command::new("ip")
|
||||
.args([family, "route", "show", "table", &table.to_string()])
|
||||
.output()
|
||||
.unwrap();
|
||||
routes.extend(
|
||||
String::from_utf8_lossy(&output.stdout)
|
||||
.lines()
|
||||
.map(|line| line.split_whitespace().collect::<Vec<_>>().join(" "))
|
||||
// IPv6 routes are shown with "pref medium".
|
||||
.map(|line| line.replace(" pref medium", ""))
|
||||
// A route that names a nexthop object is shown with
|
||||
// its number, which is the kernel's to choose.
|
||||
.map(|line| match line.split_once(" nhid ") {
|
||||
Some((prefix, rest)) => {
|
||||
let rest = rest.split_once(' ').map_or("", |(_, rest)| rest);
|
||||
format!("{prefix} {rest}")
|
||||
}
|
||||
None => line,
|
||||
})
|
||||
.filter(|line| !line.is_empty()),
|
||||
);
|
||||
}
|
||||
routes
|
||||
}
|
||||
|
||||
/// Waits until the kernel's table holds exactly `expected`.
|
||||
#[track_caller]
|
||||
fn kernel_becomes(&self, expected: &[String]) {
|
||||
let expected: BTreeSet<String> = expected.iter().cloned().collect();
|
||||
let start = Instant::now();
|
||||
while self.kernel_routes() != expected {
|
||||
assert!(
|
||||
start.elapsed() < TIMEOUT,
|
||||
"the kernel has {:#?}; expected {expected:#?}",
|
||||
self.kernel_routes()
|
||||
);
|
||||
std::thread::sleep(Duration::from_millis(20));
|
||||
}
|
||||
}
|
||||
|
||||
/// How `ip route` shows a route of ours.
|
||||
fn shown(&self, prefix: &str, gateway: impl std::fmt::Display) -> String {
|
||||
format!(
|
||||
"{prefix} via {gateway} dev {} proto bgp metric 20",
|
||||
self.interface()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Machine {
|
||||
fn drop(&mut self) {
|
||||
let _ = Command::new("ip")
|
||||
.args(["link", "del", &self.interface()])
|
||||
.output();
|
||||
for family in ["-4", "-6"] {
|
||||
let _ = Command::new("ip")
|
||||
.args([family, "route", "flush", "table", &self.table().to_string()])
|
||||
.output();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Waits for `condition` to hold of the status.
|
||||
#[track_caller]
|
||||
fn status_becomes(fib: &Fib, what: &str, condition: impl Fn(&bgp_fib::Status) -> bool) {
|
||||
let start = Instant::now();
|
||||
while !condition(&fib.status()) {
|
||||
assert!(
|
||||
start.elapsed() < TIMEOUT,
|
||||
"{what}: the status is {:#?}",
|
||||
fib.status()
|
||||
);
|
||||
std::thread::sleep(Duration::from_millis(20));
|
||||
}
|
||||
}
|
||||
|
||||
/// The best route of each prefix is in the kernel, and follows the table:
|
||||
/// when the best route changes, when it goes, and when there is none whose
|
||||
/// next hop can be reached.
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
#[ignore = "changes the machine's routes; run scripts/kernel-tests.sh"]
|
||||
async fn the_kernel_has_the_best_route_of_each_prefix() {
|
||||
let machine = Machine::new(1);
|
||||
let (near, far) = (machine.peer(2), machine.peer(3));
|
||||
let via_near = machine.via(machine.neighbor(2).into(), 50);
|
||||
// What was there before the daemon started is installed too.
|
||||
machine
|
||||
.rib
|
||||
.announce(near, &via_near, &[prefix("203.0.113.0/24")]);
|
||||
let (fib, task) = machine.start();
|
||||
machine.kernel_becomes(&[machine.shown("203.0.113.0/24", machine.neighbor(2))]);
|
||||
status_becomes(&fib, "in step", |status| {
|
||||
status.synced && status.installed == 1
|
||||
});
|
||||
|
||||
// More routes, IPv6 among them.
|
||||
let v6_neighbor: IpAddr = "2001:db8:1::2".parse().unwrap();
|
||||
machine.rib.announce(
|
||||
near,
|
||||
&machine.via(v6_neighbor, 50),
|
||||
&[prefix("2001:db8:100::/48")],
|
||||
);
|
||||
machine
|
||||
.rib
|
||||
.announce(near, &via_near, &[prefix("198.18.0.0/15")]);
|
||||
machine.kernel_becomes(&[
|
||||
machine.shown("203.0.113.0/24", machine.neighbor(2)),
|
||||
machine.shown("198.18.0.0/15", machine.neighbor(2)),
|
||||
machine.shown("2001:db8:100::/48", v6_neighbor),
|
||||
]);
|
||||
|
||||
// A better route from another peer takes the first one's place.
|
||||
let via_far = machine.via(machine.neighbor(3).into(), 10);
|
||||
machine
|
||||
.rib
|
||||
.announce(far, &via_far, &[prefix("203.0.113.0/24")]);
|
||||
machine.kernel_becomes(&[
|
||||
machine.shown("203.0.113.0/24", machine.neighbor(3)),
|
||||
machine.shown("198.18.0.0/15", machine.neighbor(2)),
|
||||
machine.shown("2001:db8:100::/48", v6_neighbor),
|
||||
]);
|
||||
// It goes: the first is the best again.
|
||||
machine.rib.withdraw(far, &[prefix("203.0.113.0/24")]);
|
||||
machine.kernel_becomes(&[
|
||||
machine.shown("203.0.113.0/24", machine.neighbor(2)),
|
||||
machine.shown("198.18.0.0/15", machine.neighbor(2)),
|
||||
machine.shown("2001:db8:100::/48", v6_neighbor),
|
||||
]);
|
||||
|
||||
// A best route whose next hop is not a neighbor is not installed, and
|
||||
// the route it replaced does not stay.
|
||||
let unreachable = machine.via("172.31.9.9".parse().unwrap(), 5);
|
||||
machine
|
||||
.rib
|
||||
.announce(far, &unreachable, &[prefix("198.18.0.0/15")]);
|
||||
machine.kernel_becomes(&[
|
||||
machine.shown("203.0.113.0/24", machine.neighbor(2)),
|
||||
machine.shown("2001:db8:100::/48", v6_neighbor),
|
||||
]);
|
||||
status_becomes(&fib, "one unresolved", |status| {
|
||||
status.installed == 2 && status.unresolved == [("no route to the next hop", 1)]
|
||||
});
|
||||
|
||||
// A session ends: its routes go.
|
||||
machine.rib.remove_peer(near);
|
||||
machine.kernel_becomes(&[]);
|
||||
status_becomes(&fib, "nothing installed", |status| status.installed == 0);
|
||||
|
||||
fib.stop().await;
|
||||
task.await.unwrap().unwrap();
|
||||
}
|
||||
|
||||
/// Routes of ours left by a run that ended badly are removed at the next
|
||||
/// start unless they are still wanted. Routes that are not ours are left
|
||||
/// alone, whatever they are for.
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
#[ignore = "changes the machine's routes; run scripts/kernel-tests.sh"]
|
||||
async fn routes_left_behind_are_cleared_and_others_are_not_touched() {
|
||||
let machine = Machine::new(2);
|
||||
let (table, interface) = (machine.table(), machine.interface());
|
||||
let add = |route: &str| ip(&format!("route add {route} dev {interface} table {table}"));
|
||||
// Ours, from before: one that is still right, one that has another
|
||||
// next hop now, one that nobody announces any more.
|
||||
add("203.0.113.0/24 via 10.2.0.2 proto bgp metric 20");
|
||||
add("198.18.0.0/15 via 10.2.0.9 proto bgp metric 20");
|
||||
add("192.88.99.0/24 via 10.2.0.2 proto bgp metric 20");
|
||||
// Not ours: someone's static route, and a BGP route with another
|
||||
// metric, which is how another daemon's would differ.
|
||||
add("192.0.2.0/24 via 10.2.0.7 proto static metric 20");
|
||||
add("192.88.99.0/24 via 10.2.0.7 proto bgp metric 30");
|
||||
let others = [
|
||||
format!("192.0.2.0/24 via 10.2.0.7 dev {interface} proto static metric 20"),
|
||||
format!("192.88.99.0/24 via 10.2.0.7 dev {interface} proto bgp metric 30"),
|
||||
];
|
||||
|
||||
let peer = machine.peer(2);
|
||||
let via = machine.via(machine.neighbor(2).into(), 0);
|
||||
machine.rib.announce(
|
||||
peer,
|
||||
&via,
|
||||
&[prefix("203.0.113.0/24"), prefix("198.18.0.0/15")],
|
||||
);
|
||||
let (fib, task) = machine.start();
|
||||
machine.kernel_becomes(&[
|
||||
machine.shown("203.0.113.0/24", machine.neighbor(2)),
|
||||
machine.shown("198.18.0.0/15", machine.neighbor(2)),
|
||||
others[0].clone(),
|
||||
others[1].clone(),
|
||||
]);
|
||||
|
||||
// A run that ends in good order takes its routes with it, and only
|
||||
// its own.
|
||||
fib.stop().await;
|
||||
task.await.unwrap().unwrap();
|
||||
machine.kernel_becomes(&others);
|
||||
}
|
||||
|
||||
/// What a next hop resolves to depends on the machine's interfaces. When
|
||||
/// they change, the routes are worked out again.
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
#[ignore = "changes the machine's routes; run scripts/kernel-tests.sh"]
|
||||
async fn routes_follow_the_machines_interfaces() {
|
||||
let machine = Machine::new(3);
|
||||
let interface = machine.interface();
|
||||
// The interface has no addresses yet: nothing is a neighbor.
|
||||
ip(&format!("addr flush dev {interface}"));
|
||||
let peer = machine.peer(2);
|
||||
let via = machine.via(machine.neighbor(2).into(), 0);
|
||||
machine
|
||||
.rib
|
||||
.announce(peer, &via, &[prefix("203.0.113.0/24")]);
|
||||
let (fib, task) = machine.start();
|
||||
status_becomes(&fib, "unresolved", |status| {
|
||||
status.synced && status.unresolved == [("no route to the next hop", 1)]
|
||||
});
|
||||
assert!(machine.kernel_routes().is_empty());
|
||||
|
||||
// It gets its address: the next hop is a neighbor now.
|
||||
machine.add_addresses();
|
||||
machine.kernel_becomes(&[machine.shown("203.0.113.0/24", machine.neighbor(2))]);
|
||||
status_becomes(&fib, "the interface is shown", |status| {
|
||||
status.unresolved.is_empty()
|
||||
&& status.interfaces.iter().any(|shown| {
|
||||
shown.name == interface
|
||||
&& shown.up
|
||||
&& shown
|
||||
.addresses
|
||||
.contains(&(IpAddr::from(machine.neighbor(1)), 24))
|
||||
})
|
||||
});
|
||||
|
||||
// It loses it. The kernel drops the route by itself; what matters is
|
||||
// that we know it has.
|
||||
ip(&format!("addr del 10.3.0.1/24 dev {interface}"));
|
||||
machine.kernel_becomes(&[]);
|
||||
status_becomes(&fib, "unresolved again", |status| {
|
||||
status.installed == 0 && status.unresolved.len() == 1
|
||||
});
|
||||
|
||||
// The interface goes down and comes straight back, faster than we
|
||||
// work the routes out again. The kernel dropped the route when it went
|
||||
// down, though nothing about the route has changed for us.
|
||||
machine.add_addresses_v4();
|
||||
machine.kernel_becomes(&[machine.shown("203.0.113.0/24", machine.neighbor(2))]);
|
||||
ip(&format!("link set {interface} down"));
|
||||
ip(&format!("link set {interface} up"));
|
||||
assert!(machine.kernel_routes().is_empty(), "the kernel dropped it");
|
||||
machine.kernel_becomes(&[machine.shown("203.0.113.0/24", machine.neighbor(2))]);
|
||||
ip(&format!("addr del 10.3.0.1/24 dev {interface}"));
|
||||
machine.kernel_becomes(&[]);
|
||||
|
||||
// And gets it back, with the interface having been down meanwhile.
|
||||
ip(&format!("link set {interface} down"));
|
||||
ip(&format!("addr add 10.3.0.1/24 dev {interface}"));
|
||||
ip(&format!("link set {interface} up"));
|
||||
machine.kernel_becomes(&[machine.shown("203.0.113.0/24", machine.neighbor(2))]);
|
||||
|
||||
fib.stop().await;
|
||||
task.await.unwrap().unwrap();
|
||||
machine.kernel_becomes(&[]);
|
||||
}
|
||||
|
||||
/// A next hop that is no neighbor goes the way the kernel's own routes
|
||||
/// say: a static route's gateway, a tunnel's interface, a nexthop object.
|
||||
/// When those routes change, so do ours; a route that discards packets is
|
||||
/// the way to nowhere.
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
#[ignore = "changes the machine's routes; run scripts/kernel-tests.sh"]
|
||||
async fn a_far_next_hop_goes_the_way_the_kernels_routes_say() {
|
||||
let machine = Machine::new(10);
|
||||
let interface = machine.interface();
|
||||
let peer = machine.peer(2);
|
||||
// Next hops that are not in any network of ours.
|
||||
let far = machine.via("172.31.10.9".parse().unwrap(), 0);
|
||||
let farther = machine.via("172.31.11.9".parse().unwrap(), 0);
|
||||
machine
|
||||
.rib
|
||||
.announce(peer, &far, &[prefix("203.0.113.0/24")]);
|
||||
machine
|
||||
.rib
|
||||
.announce(peer, &farther, &[prefix("198.18.0.0/15")]);
|
||||
let (fib, task) = machine.start();
|
||||
status_becomes(&fib, "no way to either", |status| {
|
||||
status.synced && status.unresolved == [("no route to the next hop", 2)]
|
||||
});
|
||||
assert!(machine.kernel_routes().is_empty());
|
||||
|
||||
// A static route with a gateway says the way to one: the route goes
|
||||
// in with that gateway.
|
||||
ip(&format!(
|
||||
"route add 172.31.10.0/24 via 10.10.0.9 dev {interface}"
|
||||
));
|
||||
machine.kernel_becomes(&[machine.shown("203.0.113.0/24", "10.10.0.9")]);
|
||||
// A route onto the interface, as a tunnel's is, says the way to the
|
||||
// other: the next hop itself is the neighbor there.
|
||||
ip(&format!("route add 172.31.11.0/24 dev {interface}"));
|
||||
machine.kernel_becomes(&[
|
||||
machine.shown("203.0.113.0/24", "10.10.0.9"),
|
||||
machine.shown("198.18.0.0/15", "172.31.11.9"),
|
||||
]);
|
||||
status_becomes(&fib, "both resolved", |status| {
|
||||
status.unresolved.is_empty() && status.installed == 2
|
||||
});
|
||||
|
||||
// The way changes: so does the route. Then it goes, and so does the
|
||||
// route.
|
||||
ip(&format!(
|
||||
"route replace 172.31.10.0/24 via 10.10.0.8 dev {interface}"
|
||||
));
|
||||
machine.kernel_becomes(&[
|
||||
machine.shown("203.0.113.0/24", "10.10.0.8"),
|
||||
machine.shown("198.18.0.0/15", "172.31.11.9"),
|
||||
]);
|
||||
ip("route del 172.31.10.0/24");
|
||||
machine.kernel_becomes(&[machine.shown("198.18.0.0/15", "172.31.11.9")]);
|
||||
status_becomes(&fib, "one unresolved again", |status| {
|
||||
status.unresolved == [("no route to the next hop", 1)]
|
||||
});
|
||||
|
||||
// A route that names a nexthop object goes where the object says.
|
||||
ip(&format!(
|
||||
"nexthop add id 1010 via 10.10.0.7 dev {interface}"
|
||||
));
|
||||
ip("route add 172.31.10.0/24 nhid 1010");
|
||||
machine.kernel_becomes(&[
|
||||
machine.shown("203.0.113.0/24", "10.10.0.7"),
|
||||
machine.shown("198.18.0.0/15", "172.31.11.9"),
|
||||
]);
|
||||
|
||||
// The tunnel's route becomes a blackhole: the way to nowhere.
|
||||
ip("route replace blackhole 172.31.11.0/24");
|
||||
machine.kernel_becomes(&[machine.shown("203.0.113.0/24", "10.10.0.7")]);
|
||||
status_becomes(&fib, "discarded", |status| {
|
||||
status.unresolved == [("the route to the next hop discards packets", 1)]
|
||||
});
|
||||
|
||||
ip("route del 172.31.10.0/24");
|
||||
ip("route del blackhole 172.31.11.0/24");
|
||||
ip("nexthop del id 1010");
|
||||
fib.stop().await;
|
||||
task.await.unwrap().unwrap();
|
||||
machine.kernel_becomes(&[]);
|
||||
}
|
||||
|
||||
/// A route that asks for its packets to be dropped goes into the kernel
|
||||
/// as a blackhole route, whatever its next hop, and comes out again with
|
||||
/// the request.
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
#[ignore = "changes the machine's routes; run scripts/kernel-tests.sh"]
|
||||
async fn a_blackhole_request_becomes_a_blackhole_route() {
|
||||
let machine = Machine::new(13);
|
||||
let peer = machine.peer(2);
|
||||
// Asked for from a next hop nothing is a neighbor of, and with one
|
||||
// ordinary route beside it to show the two apart.
|
||||
let mut attributes = vec![
|
||||
PathAttribute::Origin(OriginPathAttribute::IGP),
|
||||
AsPathAttribute::from_asns(vec![65002]),
|
||||
PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::new(172, 31, 11, 9))),
|
||||
PathAttribute::Communities(CommunitiesPathAttribute(vec![(65002, 1), (0xFFFF, 0x029A)])),
|
||||
];
|
||||
let blackhole = machine.rib.attrs().intern(&attributes, None).unwrap();
|
||||
attributes.pop();
|
||||
attributes.push(PathAttribute::Communities(CommunitiesPathAttribute(vec![
|
||||
(65002, 1),
|
||||
])));
|
||||
let plain = machine.rib.attrs().intern(&attributes, None).unwrap();
|
||||
machine.rib.announce(
|
||||
peer,
|
||||
&blackhole,
|
||||
&[prefix("203.0.113.9/32"), prefix("2001:db8:100::9/128")],
|
||||
);
|
||||
machine
|
||||
.rib
|
||||
.announce(peer, &plain, &[prefix("198.18.0.0/15")]);
|
||||
let (fib, task) = machine.start();
|
||||
// The kernel puts an IPv6 blackhole route on lo, and says so.
|
||||
machine.kernel_becomes(&[
|
||||
"blackhole 203.0.113.9 proto bgp metric 20".into(),
|
||||
"blackhole 2001:db8:100::9 dev lo proto bgp metric 20".into(),
|
||||
]);
|
||||
status_becomes(&fib, "two blackholes, one unresolved", |status| {
|
||||
status.synced
|
||||
&& status.installed == 2
|
||||
&& status.blackholes == 2
|
||||
&& status.unresolved == [("no route to the next hop", 1)]
|
||||
});
|
||||
|
||||
// The request is withdrawn: the blackhole goes.
|
||||
machine.rib.withdraw(peer, &[prefix("203.0.113.9/32")]);
|
||||
machine.kernel_becomes(&["blackhole 2001:db8:100::9 dev lo proto bgp metric 20".into()]);
|
||||
|
||||
// A blackhole left by an earlier run is found and kept like any other.
|
||||
fib.leave().await;
|
||||
task.await.unwrap().unwrap();
|
||||
let (fib, task) = machine.start();
|
||||
status_becomes(&fib, "the blackhole is adopted", |status| {
|
||||
status.synced && status.installed == 1 && status.blackholes == 1
|
||||
});
|
||||
fib.stop().await;
|
||||
task.await.unwrap().unwrap();
|
||||
machine.kernel_becomes(&[]);
|
||||
}
|
||||
|
||||
/// A table's worth of routes goes in, and comes out, in reasonable time.
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
#[ignore = "changes the machine's routes; run scripts/kernel-tests.sh"]
|
||||
async fn many_routes_are_installed_and_removed() {
|
||||
// FERRIX_ROUTES=1000000 for the size of the Internet's IPv4 table.
|
||||
let routes: usize = std::env::var("FERRIX_ROUTES")
|
||||
.ok()
|
||||
.and_then(|routes| routes.parse().ok())
|
||||
.unwrap_or(200_000);
|
||||
let machine = Machine::new(4);
|
||||
let peer = machine.peer(2);
|
||||
let via = machine.via(machine.neighbor(2).into(), 0);
|
||||
let prefixes: Vec<IpPrefix> = (0..routes as u32)
|
||||
.map(|n| {
|
||||
let [_, a, b, c] = n.to_be_bytes();
|
||||
IpPrefix::new(
|
||||
bgp_primitives::constants::IpVersion::Ipv4,
|
||||
[100 + a, b, c, 0],
|
||||
24,
|
||||
)
|
||||
.unwrap()
|
||||
})
|
||||
.collect();
|
||||
machine.rib.announce(peer, &via, &prefixes);
|
||||
|
||||
let started = Instant::now();
|
||||
let (fib, task) = machine.start();
|
||||
status_becomes(&fib, "all installed", |status| {
|
||||
status.synced && status.installed == routes
|
||||
});
|
||||
let installed_in = started.elapsed();
|
||||
assert_eq!(machine.kernel_routes().len(), routes);
|
||||
assert_eq!(fib.status().errors, 0, "{:?}", fib.status().last_error);
|
||||
|
||||
let started = Instant::now();
|
||||
fib.stop().await;
|
||||
task.await.unwrap().unwrap();
|
||||
let removed_in = started.elapsed();
|
||||
assert!(machine.kernel_routes().is_empty());
|
||||
println!(
|
||||
"{routes} routes: installed in {installed_in:?} ({:.0}/s), removed in {removed_in:?}",
|
||||
routes as f64 / installed_in.as_secs_f64()
|
||||
);
|
||||
}
|
||||
|
||||
/// Someone empties the table behind our back (`ip route flush`), and then
|
||||
/// the peers withdraw everything: the kernel refuses every removal, a
|
||||
/// table's worth of rejections at once. That is more than the socket holds
|
||||
/// answers for, and must be no more than a reason to look again.
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
#[ignore = "changes the machine's routes; run scripts/kernel-tests.sh"]
|
||||
async fn a_flood_of_refusals_does_not_stop_the_daemon() {
|
||||
const ROUTES: u32 = 20_000;
|
||||
let mut machine = Machine::new(11);
|
||||
machine.nexthop_objects = false;
|
||||
let peer = machine.peer(2);
|
||||
let via = machine.via(machine.neighbor(2).into(), 0);
|
||||
let prefixes: Vec<IpPrefix> = (0..ROUTES)
|
||||
.map(|n| {
|
||||
let [_, a, b, c] = n.to_be_bytes();
|
||||
IpPrefix::new(
|
||||
bgp_primitives::constants::IpVersion::Ipv4,
|
||||
[100 + a, b, c, 0],
|
||||
24,
|
||||
)
|
||||
.unwrap()
|
||||
})
|
||||
.collect();
|
||||
machine.rib.announce(peer, &via, &prefixes);
|
||||
let (fib, task) = machine.start();
|
||||
status_becomes(&fib, "all installed", |status| {
|
||||
status.synced && status.installed == ROUTES as usize
|
||||
});
|
||||
|
||||
ip(&format!("route flush table {}", machine.table()));
|
||||
assert!(machine.kernel_routes().is_empty());
|
||||
let routes: Vec<bgp_primitives::ip_prefix::Nlri> =
|
||||
prefixes.iter().map(|prefix| (*prefix).into()).collect();
|
||||
machine.rib.withdraw(peer, &routes);
|
||||
status_becomes(&fib, "nothing installed", |status| {
|
||||
status.synced && status.installed == 0
|
||||
});
|
||||
assert!(
|
||||
!task.is_finished(),
|
||||
"the task that talks to the kernel ended"
|
||||
);
|
||||
|
||||
// And it goes on working.
|
||||
machine.rib.announce(peer, &via, &prefixes[..3]);
|
||||
status_becomes(&fib, "three installed", |status| {
|
||||
status.synced && status.installed == 3
|
||||
});
|
||||
assert_eq!(machine.kernel_routes().len(), 3);
|
||||
assert!(!task.is_finished());
|
||||
fib.stop().await;
|
||||
task.await.unwrap().unwrap();
|
||||
}
|
||||
|
||||
/// Routes name a nexthop object for their gateway where the kernel has
|
||||
/// such objects. The object is ours: made when first needed, put back if
|
||||
/// someone removes it, and removed when we stop. Other objects are not
|
||||
/// touched.
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
#[ignore = "changes the machine's routes; run scripts/kernel-tests.sh"]
|
||||
async fn routes_name_a_nexthop_object_that_is_ours_to_keep() {
|
||||
let machine = Machine::new(5);
|
||||
let interface = machine.interface();
|
||||
// Someone else's object, to the same gateway even.
|
||||
ip(&format!(
|
||||
"nexthop add id 50005 via 10.5.0.2 dev {interface} protocol bgp"
|
||||
));
|
||||
let nexthops = || -> Vec<String> {
|
||||
ip("nexthop show")
|
||||
.lines()
|
||||
.filter(|line| line.contains(&format!("dev {interface} ")))
|
||||
.map(|line| line.trim().to_string())
|
||||
.collect()
|
||||
};
|
||||
let peer = machine.peer(2);
|
||||
let via = machine.via(machine.neighbor(2).into(), 0);
|
||||
machine.rib.announce(
|
||||
peer,
|
||||
&via,
|
||||
&[prefix("203.0.113.0/24"), prefix("198.18.0.0/15")],
|
||||
);
|
||||
let (fib, task) = machine.start();
|
||||
let both = [
|
||||
machine.shown("203.0.113.0/24", machine.neighbor(2)),
|
||||
machine.shown("198.18.0.0/15", machine.neighbor(2)),
|
||||
];
|
||||
machine.kernel_becomes(&both);
|
||||
|
||||
// One object of ours beside the other, and both routes name it.
|
||||
let ours: Vec<String> = nexthops()
|
||||
.into_iter()
|
||||
.filter(|line| !line.starts_with("id 50005 "))
|
||||
.collect();
|
||||
assert_eq!(ours.len(), 1, "{:?}", nexthops());
|
||||
assert!(ours[0].contains("via 10.5.0.2") && ours[0].contains("proto bgp"));
|
||||
let id = ours[0].split_whitespace().nth(1).unwrap().to_string();
|
||||
let raw = ip(&format!("route show table {}", machine.table()));
|
||||
assert_eq!(raw.matches(&format!("nhid {id} ")).count(), 2, "{raw}");
|
||||
|
||||
// Someone removes our object, and the kernel our routes with it. We
|
||||
// are told, and put both back.
|
||||
ip(&format!("nexthop del id {id}"));
|
||||
machine.kernel_becomes(&both);
|
||||
|
||||
// When we stop, our object goes with our routes; the other stays.
|
||||
fib.stop().await;
|
||||
task.await.unwrap().unwrap();
|
||||
machine.kernel_becomes(&[]);
|
||||
assert_eq!(nexthops().len(), 1, "{:?}", nexthops());
|
||||
assert!(nexthops()[0].starts_with("id 50005 "));
|
||||
ip("nexthop del id 50005");
|
||||
}
|
||||
|
||||
/// On a kernel without nexthop objects, or where the configuration says
|
||||
/// so, every route carries its own gateway.
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
#[ignore = "changes the machine's routes; run scripts/kernel-tests.sh"]
|
||||
async fn routes_can_carry_their_own_gateway() {
|
||||
let mut machine = Machine::new(6);
|
||||
machine.nexthop_objects = false;
|
||||
let peer = machine.peer(2);
|
||||
let via = machine.via(machine.neighbor(2).into(), 0);
|
||||
machine
|
||||
.rib
|
||||
.announce(peer, &via, &[prefix("203.0.113.0/24")]);
|
||||
let (fib, task) = machine.start();
|
||||
machine.kernel_becomes(&[machine.shown("203.0.113.0/24", machine.neighbor(2))]);
|
||||
let raw = ip(&format!("route show table {}", machine.table()));
|
||||
assert!(!raw.contains("nhid"), "{raw}");
|
||||
assert!(
|
||||
!ip("nexthop show").contains(&format!("dev {} ", machine.interface())),
|
||||
"no nexthop object was made"
|
||||
);
|
||||
|
||||
// The next hop changes, and the route goes.
|
||||
let other = machine.via(machine.neighbor(3).into(), 0);
|
||||
machine
|
||||
.rib
|
||||
.announce(peer, &other, &[prefix("203.0.113.0/24")]);
|
||||
machine.kernel_becomes(&[machine.shown("203.0.113.0/24", machine.neighbor(3))]);
|
||||
machine.rib.withdraw(peer, &[prefix("203.0.113.0/24")]);
|
||||
machine.kernel_becomes(&[]);
|
||||
|
||||
fib.stop().await;
|
||||
task.await.unwrap().unwrap();
|
||||
}
|
||||
|
||||
/// When the daemon starts, its table is empty until its peers have sent
|
||||
/// theirs. The routes an earlier run left are kept for a while, so that
|
||||
/// the machine goes on forwarding; then those nobody announced again go.
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
#[ignore = "changes the machine's routes; run scripts/kernel-tests.sh"]
|
||||
async fn routes_of_an_earlier_run_are_kept_while_peers_come_back() {
|
||||
let mut machine = Machine::new(7);
|
||||
machine.stale_time = Duration::from_secs(3);
|
||||
let (table, interface) = (machine.table(), machine.interface());
|
||||
let add = |route: &str| ip(&format!("route add {route} dev {interface} table {table}"));
|
||||
add("203.0.113.0/24 via 10.7.0.2 proto bgp metric 20");
|
||||
add("198.18.0.0/15 via 10.7.0.2 proto bgp metric 20");
|
||||
let left = [
|
||||
machine.shown("203.0.113.0/24", machine.neighbor(2)),
|
||||
machine.shown("198.18.0.0/15", machine.neighbor(2)),
|
||||
];
|
||||
|
||||
// The daemon starts with nothing in its table: both stay.
|
||||
let (fib, task) = machine.start();
|
||||
status_becomes(&fib, "in step, keeping two", |status| {
|
||||
status.synced && status.installed == 2 && status.kept == 2
|
||||
});
|
||||
assert_eq!(machine.kernel_routes(), left.iter().cloned().collect());
|
||||
|
||||
// A peer comes back with one of them.
|
||||
let peer = machine.peer(2);
|
||||
let via = machine.via(machine.neighbor(2).into(), 0);
|
||||
machine
|
||||
.rib
|
||||
.announce(peer, &via, &[prefix("203.0.113.0/24")]);
|
||||
status_becomes(&fib, "one confirmed", |status| status.kept == 1);
|
||||
assert_eq!(machine.kernel_routes(), left.iter().cloned().collect());
|
||||
|
||||
// Time is up: the one nobody announced goes.
|
||||
machine.kernel_becomes(&[machine.shown("203.0.113.0/24", machine.neighbor(2))]);
|
||||
status_becomes(&fib, "none kept", |status| {
|
||||
status.installed == 1 && status.kept == 0
|
||||
});
|
||||
|
||||
fib.stop().await;
|
||||
task.await.unwrap().unwrap();
|
||||
}
|
||||
|
||||
/// A restart that is to go unnoticed (graceful restart): the routes are
|
||||
/// left in the kernel, by way of their nexthop objects; the next run
|
||||
/// finds them, says how many, and lets those nobody announced again go
|
||||
/// as soon as it is told the peers have all sent theirs.
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
#[ignore = "changes the machine's routes; run scripts/kernel-tests.sh"]
|
||||
async fn routes_stay_in_the_kernel_over_a_restart() {
|
||||
let mut machine = Machine::new(12);
|
||||
machine.stale_time = Duration::from_secs(60);
|
||||
let (fib, task) = machine.start();
|
||||
assert_eq!(fib.adopted().await, 0);
|
||||
let peer = machine.peer(2);
|
||||
let via = machine.via(machine.neighbor(2).into(), 0);
|
||||
machine.rib.announce(
|
||||
peer,
|
||||
&via,
|
||||
&[prefix("203.0.113.0/24"), prefix("198.18.0.0/15")],
|
||||
);
|
||||
let both = [
|
||||
machine.shown("198.18.0.0/15", machine.neighbor(2)),
|
||||
machine.shown("203.0.113.0/24", machine.neighbor(2)),
|
||||
];
|
||||
machine.kernel_becomes(&both);
|
||||
|
||||
fib.leave().await;
|
||||
task.await.unwrap().unwrap();
|
||||
assert_eq!(machine.kernel_routes(), both.iter().cloned().collect());
|
||||
|
||||
// The next run, with a table that is empty until the peers are back.
|
||||
machine.rib = Arc::new(RibTable::new(4));
|
||||
let (fib, task) = machine.start();
|
||||
assert_eq!(fib.adopted().await, 2);
|
||||
status_becomes(&fib, "keeping both", |status| status.kept == 2);
|
||||
let peer = machine.peer(2);
|
||||
let via = machine.via(machine.neighbor(2).into(), 0);
|
||||
machine
|
||||
.rib
|
||||
.announce(peer, &via, &[prefix("203.0.113.0/24")]);
|
||||
status_becomes(&fib, "one confirmed", |status| status.kept == 1);
|
||||
|
||||
// Every peer has sent its routes: what was not among them goes now,
|
||||
// and not a minute later.
|
||||
fib.release_kept();
|
||||
machine.kernel_becomes(&[machine.shown("203.0.113.0/24", machine.neighbor(2))]);
|
||||
|
||||
fib.stop().await;
|
||||
task.await.unwrap().unwrap();
|
||||
}
|
||||
|
||||
/// An IPv4 route whose next hop is an IPv6 neighbor: routes over a link
|
||||
/// that has only IPv6 addresses (RFC 8950). The kernel takes such a route,
|
||||
/// by way of a nexthop object or with its gateway spelled out.
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
#[ignore = "changes the machine's routes; run scripts/kernel-tests.sh"]
|
||||
async fn an_ipv4_route_goes_by_way_of_an_ipv6_neighbor() {
|
||||
for (n, nexthop_objects) in [(8, true), (9, false)] {
|
||||
let mut machine = Machine::new(n);
|
||||
machine.nexthop_objects = nexthop_objects;
|
||||
// Only IPv6 on the link.
|
||||
ip(&format!(
|
||||
"addr del 10.{n}.0.1/24 dev {}",
|
||||
machine.interface()
|
||||
));
|
||||
let neighbor: IpAddr = format!("2001:db8:{n}::2").parse().unwrap();
|
||||
let peer = machine
|
||||
.rib
|
||||
.add_peer(PeerInfo {
|
||||
asn: 65002,
|
||||
router_id: machine.neighbor(2),
|
||||
address: neighbor,
|
||||
interface: 0,
|
||||
ibgp: false,
|
||||
reflector_client: false,
|
||||
})
|
||||
.unwrap();
|
||||
// Both families by way of the same neighbor.
|
||||
let via = machine.via(neighbor, 0);
|
||||
machine.rib.announce(
|
||||
peer,
|
||||
&via,
|
||||
&[prefix("203.0.113.0/24"), prefix("2001:db8:100::/48")],
|
||||
);
|
||||
// And an IPv4 route whose next hop has a link-local address too,
|
||||
// with a global one that is not on any link of ours.
|
||||
let link_local = machine
|
||||
.rib
|
||||
.attrs()
|
||||
.intern(
|
||||
&[
|
||||
PathAttribute::Origin(OriginPathAttribute::IGP),
|
||||
AsPathAttribute::from_asns(vec![65002]),
|
||||
],
|
||||
Some(&NlriNextHop::Ipv6WithLl {
|
||||
global: "2001:db8:ffff::2".parse().unwrap(),
|
||||
link_local: "fe80::2".parse().unwrap(),
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
machine
|
||||
.rib
|
||||
.announce(peer, &link_local, &[prefix("198.18.0.0/15")]);
|
||||
|
||||
let (fib, task) = machine.start();
|
||||
let interface = machine.interface();
|
||||
machine.kernel_becomes(&[
|
||||
format!("203.0.113.0/24 via inet6 {neighbor} dev {interface} proto bgp metric 20"),
|
||||
format!("198.18.0.0/15 via inet6 fe80::2 dev {interface} proto bgp metric 20"),
|
||||
machine.shown("2001:db8:100::/48", neighbor),
|
||||
]);
|
||||
assert_eq!(fib.status().errors, 0, "{:?}", fib.status().last_error);
|
||||
|
||||
fib.stop().await;
|
||||
task.await.unwrap().unwrap();
|
||||
machine.kernel_becomes(&[]);
|
||||
}
|
||||
}
|
||||
@@ -9,22 +9,17 @@ version.workspace = true
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
bgp-fib.workspace = true
|
||||
bgp-packet.workspace = true
|
||||
bgp-primitives.workspace = true
|
||||
bgp-rib.workspace = true
|
||||
bgp-rpki.workspace = true
|
||||
bgp-server.workspace = true
|
||||
bgp-web.workspace = true
|
||||
bytes.workspace = true
|
||||
clap.workspace = true
|
||||
eyre.workspace = true
|
||||
reqwest.workspace = true
|
||||
serde_json.workspace = true
|
||||
tokio.workspace = true
|
||||
|
||||
# Used by the examples only.
|
||||
[dev-dependencies]
|
||||
clap.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -16,9 +16,11 @@
|
||||
use std::net::Ipv4Addr;
|
||||
use std::time::Duration;
|
||||
|
||||
use bgp_integration_tests::announcement::{Announcement, withdrawal};
|
||||
use bgp_integration_tests::test_peer::TestPeer;
|
||||
use bgp_packet::message::{Message, PathAttribute, UpdateMessage};
|
||||
use bgp_packet::message::{
|
||||
AsPathAttribute, CommunitiesPathAttribute, Message, NextHopPathAttribute, OriginPathAttribute,
|
||||
PathAttribute, UpdateMessage,
|
||||
};
|
||||
use bgp_primitives::ip_prefix::IpPrefix;
|
||||
use clap::Parser;
|
||||
use eyre::Result;
|
||||
@@ -86,8 +88,8 @@ async fn run(args: &Args, mut peer: TestPeer, addr: std::net::SocketAddr) -> Res
|
||||
|
||||
for p in &args.announce {
|
||||
let prefix = IpPrefix::try_from(p.as_str())?;
|
||||
peer.announce(announce(prefix, vec![args.asn], vec![(args.asn as u16, 1)]))
|
||||
.await?;
|
||||
let update = announce(prefix, vec![args.asn], vec![(args.asn as u16, 1)]);
|
||||
peer.send(&Message::Update(update)).await?;
|
||||
println!("announced {prefix}");
|
||||
}
|
||||
|
||||
@@ -96,8 +98,7 @@ async fn run(args: &Args, mut peer: TestPeer, addr: std::net::SocketAddr) -> Res
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = ticker.tick() => peer.send(&Message::KeepAlive).await?,
|
||||
// However long the server is quiet: this peer stays on the line.
|
||||
msg = peer.recv_untimed() => match msg? {
|
||||
msg = peer.recv() => match msg? {
|
||||
Message::KeepAlive => {}
|
||||
Message::Update(u) => {
|
||||
for w in &u.withdrawn_routes {
|
||||
@@ -120,10 +121,22 @@ async fn run(args: &Args, mut peer: TestPeer, addr: std::net::SocketAddr) -> Res
|
||||
}
|
||||
}
|
||||
|
||||
fn announce(prefix: IpPrefix, as_path: Vec<u32>, communities: Vec<(u16, u16)>) -> Announcement {
|
||||
Announcement::of([prefix.into()], &as_path)
|
||||
.with_next_hop(Ipv4Addr::new(10, 0, 0, 2))
|
||||
.with_communities(&communities)
|
||||
fn announce(prefix: IpPrefix, as_path: Vec<u32>, communities: Vec<(u16, u16)>) -> UpdateMessage {
|
||||
let mut path_attributes = vec![
|
||||
PathAttribute::Origin(OriginPathAttribute::IGP),
|
||||
AsPathAttribute::from_asns(as_path),
|
||||
PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::new(10, 0, 0, 2))),
|
||||
];
|
||||
if !communities.is_empty() {
|
||||
path_attributes.push(PathAttribute::Communities(CommunitiesPathAttribute(
|
||||
communities,
|
||||
)));
|
||||
}
|
||||
UpdateMessage {
|
||||
withdrawn_routes: vec![],
|
||||
path_attributes,
|
||||
announced_routes: vec![prefix.into()],
|
||||
}
|
||||
}
|
||||
|
||||
async fn echo_back(peer: &mut TestPeer, u: &UpdateMessage, asn: u32) -> Result<()> {
|
||||
@@ -144,13 +157,21 @@ async fn echo_back(peer: &mut TestPeer, u: &UpdateMessage, asn: u32) -> Result<(
|
||||
}
|
||||
for nlri in &u.announced_routes {
|
||||
let prefix = nlri.prefix;
|
||||
peer.announce(announce(prefix, as_path.clone(), communities.clone()))
|
||||
.await?;
|
||||
peer.send(&Message::Update(announce(
|
||||
prefix,
|
||||
as_path.clone(),
|
||||
communities.clone(),
|
||||
)))
|
||||
.await?;
|
||||
println!(" echoed {prefix} back with path {as_path:?}");
|
||||
}
|
||||
if !u.withdrawn_routes.is_empty() {
|
||||
peer.announce(withdrawal(u.withdrawn_routes.iter().cloned()))
|
||||
.await?;
|
||||
peer.send(&Message::Update(UpdateMessage {
|
||||
withdrawn_routes: u.withdrawn_routes.clone(),
|
||||
path_attributes: vec![],
|
||||
announced_routes: vec![],
|
||||
}))
|
||||
.await?;
|
||||
println!(
|
||||
" echoed withdraw of {:?}",
|
||||
u.withdrawn_routes
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
//! The UPDATEs a test peer sends, built from what a test has to say about
|
||||
//! them and nothing more.
|
||||
|
||||
use std::net::Ipv4Addr;
|
||||
|
||||
use bgp_packet::message::{
|
||||
AsPathAttribute, CommunitiesPathAttribute, Message, NextHopPathAttribute, OriginPathAttribute,
|
||||
PathAttribute, UpdateMessage,
|
||||
};
|
||||
use bgp_primitives::ip_prefix::Nlri;
|
||||
|
||||
use crate::prefix;
|
||||
|
||||
/// An UPDATE announcing IPv4 routes as the peers of these tests do: with
|
||||
/// ORIGIN IGP, the AS path given, and 192.0.2.1 as the next hop unless
|
||||
/// another is set. Attributes are written in order of type code.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Announcement {
|
||||
routes: Vec<Nlri>,
|
||||
path: Vec<u32>,
|
||||
next_hop: Ipv4Addr,
|
||||
only_to_customer: Option<u32>,
|
||||
communities: Vec<(u16, u16)>,
|
||||
more: Vec<PathAttribute>,
|
||||
}
|
||||
|
||||
/// Announces `prefixes`, written as text, with the AS path `path`.
|
||||
pub fn announcement(prefixes: &[&str], path: &[u32]) -> Announcement {
|
||||
Announcement::of(prefixes.iter().map(|p| prefix(p).into()), path)
|
||||
}
|
||||
|
||||
/// Withdraws `routes`.
|
||||
pub fn withdrawal(routes: impl IntoIterator<Item = Nlri>) -> UpdateMessage {
|
||||
UpdateMessage {
|
||||
withdrawn_routes: routes.into_iter().collect(),
|
||||
path_attributes: vec![],
|
||||
announced_routes: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
impl Announcement {
|
||||
/// Announces `routes`, prefixes with or without path identifiers, with
|
||||
/// the AS path `path`.
|
||||
pub fn of(routes: impl IntoIterator<Item = Nlri>, path: &[u32]) -> Self {
|
||||
Self {
|
||||
routes: routes.into_iter().collect(),
|
||||
path: path.to_vec(),
|
||||
next_hop: Ipv4Addr::new(192, 0, 2, 1),
|
||||
only_to_customer: None,
|
||||
communities: Vec::new(),
|
||||
more: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_next_hop(mut self, next_hop: Ipv4Addr) -> Self {
|
||||
self.next_hop = next_hop;
|
||||
self
|
||||
}
|
||||
|
||||
/// With a COMMUNITIES attribute carrying `communities`, if there are
|
||||
/// any.
|
||||
pub fn with_communities(mut self, communities: &[(u16, u16)]) -> Self {
|
||||
self.communities = communities.to_vec();
|
||||
self
|
||||
}
|
||||
|
||||
/// Marked as only to go to customers, by `asn` (RFC 9234).
|
||||
pub fn with_only_to_customer(mut self, asn: u32) -> Self {
|
||||
self.only_to_customer = Some(asn);
|
||||
self
|
||||
}
|
||||
|
||||
/// With `attributes` after the mandatory ones, as given.
|
||||
pub fn with_attributes(mut self, attributes: impl IntoIterator<Item = PathAttribute>) -> Self {
|
||||
self.more.extend(attributes);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Announcement> for UpdateMessage {
|
||||
fn from(announcement: Announcement) -> Self {
|
||||
let mut path_attributes = vec![
|
||||
PathAttribute::Origin(OriginPathAttribute::IGP),
|
||||
AsPathAttribute::from_asns(announcement.path),
|
||||
PathAttribute::NextHop(NextHopPathAttribute(announcement.next_hop)),
|
||||
];
|
||||
if !announcement.communities.is_empty() {
|
||||
path_attributes.push(PathAttribute::Communities(CommunitiesPathAttribute(
|
||||
announcement.communities,
|
||||
)));
|
||||
}
|
||||
path_attributes.extend(
|
||||
announcement
|
||||
.only_to_customer
|
||||
.map(PathAttribute::OnlyToCustomer),
|
||||
);
|
||||
path_attributes.extend(announcement.more);
|
||||
UpdateMessage {
|
||||
withdrawn_routes: vec![],
|
||||
path_attributes,
|
||||
announced_routes: announcement.routes,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Announcement> for Message {
|
||||
fn from(announcement: Announcement) -> Self {
|
||||
Message::Update(announcement.into())
|
||||
}
|
||||
}
|
||||
@@ -1,101 +1,2 @@
|
||||
pub mod announcement;
|
||||
pub mod test_cache;
|
||||
pub mod test_peer;
|
||||
pub mod test_server;
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use bgp_primitives::ip_prefix::IpPrefix;
|
||||
use bgp_server::address::Endpoint;
|
||||
use bgp_server::config::PeerConfig;
|
||||
use bgp_server::family::FamilySet;
|
||||
use eyre::{Result, bail};
|
||||
use serde_json::Value;
|
||||
|
||||
/// How long a test waits for something the server does on its own: a
|
||||
/// route reaching the table, a session coming up, a message arriving.
|
||||
/// Everything happens on the loopback, so it is generous.
|
||||
pub const TIMEOUT: Duration = Duration::from_secs(5);
|
||||
|
||||
/// The policy a peer has with nothing configured, but for one thing: the
|
||||
/// peers of these tests are all at 127.0.0.1, which no route can have as
|
||||
/// its next hop, so their routes name another.
|
||||
pub fn default_policy() -> bgp_server::policy::ImportPolicy {
|
||||
bgp_server::policy::ImportPolicy {
|
||||
accept_third_party_next_hop: true,
|
||||
accept_blackholes: false,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// A prefix written as text. The tests' prefixes are literals, so a bad
|
||||
/// one is a mistake in the test.
|
||||
pub fn prefix(s: &str) -> IpPrefix {
|
||||
IpPrefix::try_from(s).unwrap()
|
||||
}
|
||||
|
||||
/// A configured peer with nothing said about it but where it is and what
|
||||
/// AS it is in: the default hold time, every family, the default policy of
|
||||
/// these tests, and no role, limits, groups or ADD-PATH. A test says what
|
||||
/// else with struct update syntax: `PeerConfig { role: Some(..),
|
||||
/// ..peer_config(..) }`.
|
||||
pub fn peer_config(name: &str, endpoint: impl Into<Endpoint>, remote_asn: u32) -> PeerConfig {
|
||||
PeerConfig {
|
||||
name: name.into(),
|
||||
endpoint: endpoint.into(),
|
||||
remote_asn,
|
||||
hold_time_secs: 90,
|
||||
local_address: None,
|
||||
families: FamilySet::supported(),
|
||||
add_path: None,
|
||||
import: default_policy(),
|
||||
role: None,
|
||||
require_role: false,
|
||||
export: Default::default(),
|
||||
export_limits: None,
|
||||
graceful_restart: Default::default(),
|
||||
groups: Vec::new(),
|
||||
reflector_client: false,
|
||||
passive: false,
|
||||
expected_routes: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Waits until `done` holds, looking every few milliseconds, or fails
|
||||
/// with `what` once [`TIMEOUT`] has passed.
|
||||
pub async fn wait_until(what: &str, mut done: impl FnMut() -> bool) -> Result<()> {
|
||||
let waited = tokio::time::timeout(TIMEOUT, async {
|
||||
while !done() {
|
||||
tokio::time::sleep(Duration::from_millis(5)).await;
|
||||
}
|
||||
})
|
||||
.await;
|
||||
if waited.is_err() {
|
||||
bail!("timed out waiting until {what}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Waits until `get` has something, and returns it; fails with `what`
|
||||
/// once [`TIMEOUT`] has passed.
|
||||
pub async fn wait_for<T>(what: &str, mut get: impl FnMut() -> Option<T>) -> Result<T> {
|
||||
let mut got = None;
|
||||
wait_until(what, || {
|
||||
got = get();
|
||||
got.is_some()
|
||||
})
|
||||
.await?;
|
||||
Ok(got.expect("the wait ended with something"))
|
||||
}
|
||||
|
||||
/// `GET`s `url` and reads the JSON it answers with. An error status is an
|
||||
/// error.
|
||||
pub async fn get_json(client: &reqwest::Client, url: &str) -> Result<Value> {
|
||||
Ok(client
|
||||
.get(url)
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?
|
||||
.json()
|
||||
.await?)
|
||||
}
|
||||
@@ -1,156 +0,0 @@
|
||||
//! An RPKI cache for tests: an RTR server that hands out whatever VRPs and
|
||||
//! ASPAs the test says there are.
|
||||
|
||||
use std::net::{Ipv4Addr, SocketAddr};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use bgp_rpki::Vrp;
|
||||
use bgp_rpki::rtr::{Pdu, Timers, VERSION_2};
|
||||
use bytes::BytesMut;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::{TcpListener, TcpStream};
|
||||
use tokio::sync::watch;
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
const SESSION: u16 = 0x1234;
|
||||
|
||||
/// What the cache has to give.
|
||||
#[derive(Clone, Default)]
|
||||
struct Data {
|
||||
vrps: Vec<Vrp>,
|
||||
/// Customer ASes and their providers.
|
||||
aspas: Vec<(u32, Vec<u32>)>,
|
||||
serial: u32,
|
||||
}
|
||||
|
||||
/// The cache. Stops, closing its connections, when dropped.
|
||||
pub struct TestCache {
|
||||
address: SocketAddr,
|
||||
data: Arc<Mutex<Data>>,
|
||||
/// The serial, for telling connected routers there is something new.
|
||||
serial: watch::Sender<u32>,
|
||||
task: JoinHandle<()>,
|
||||
}
|
||||
|
||||
impl TestCache {
|
||||
pub async fn start(vrps: Vec<Vrp>) -> Self {
|
||||
Self::start_with(vrps, vec![]).await
|
||||
}
|
||||
|
||||
/// A cache with ASPAs too: customer ASes and their providers.
|
||||
pub async fn start_with(vrps: Vec<Vrp>, aspas: Vec<(u32, Vec<u32>)>) -> Self {
|
||||
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await.unwrap();
|
||||
let address = listener.local_addr().unwrap();
|
||||
let data = Arc::new(Mutex::new(Data {
|
||||
vrps,
|
||||
aspas,
|
||||
serial: 1,
|
||||
}));
|
||||
let (serial, _) = watch::channel(1);
|
||||
let task = tokio::spawn({
|
||||
let (data, serial) = (Arc::clone(&data), serial.clone());
|
||||
async move {
|
||||
// Connections end with this task: they are part of it.
|
||||
let mut connections = tokio::task::JoinSet::new();
|
||||
loop {
|
||||
let Ok((stream, _)) = listener.accept().await else {
|
||||
return;
|
||||
};
|
||||
connections.spawn(serve(stream, Arc::clone(&data), serial.subscribe()));
|
||||
}
|
||||
}
|
||||
});
|
||||
Self {
|
||||
address,
|
||||
data,
|
||||
serial,
|
||||
task,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn address(&self) -> SocketAddr {
|
||||
self.address
|
||||
}
|
||||
|
||||
/// Replaces the VRPs and tells connected routers.
|
||||
pub fn set(&self, vrps: Vec<Vrp>) {
|
||||
self.change(|data| data.vrps = vrps);
|
||||
}
|
||||
|
||||
/// Replaces the ASPAs and tells connected routers.
|
||||
pub fn set_aspas(&self, aspas: Vec<(u32, Vec<u32>)>) {
|
||||
self.change(|data| data.aspas = aspas);
|
||||
}
|
||||
|
||||
fn change(&self, change: impl FnOnce(&mut Data)) {
|
||||
let serial = {
|
||||
let mut data = self.data.lock().unwrap();
|
||||
change(&mut data);
|
||||
data.serial += 1;
|
||||
data.serial
|
||||
};
|
||||
self.serial.send_replace(serial);
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TestCache {
|
||||
fn drop(&mut self) {
|
||||
self.task.abort();
|
||||
}
|
||||
}
|
||||
|
||||
/// One router's connection. Asked for changes, this cache always says to
|
||||
/// ask for everything: the router works out what changed.
|
||||
async fn serve(mut stream: TcpStream, data: Arc<Mutex<Data>>, mut serial: watch::Receiver<u32>) {
|
||||
let mut buf = BytesMut::new();
|
||||
// The version the router speaks, once it has said something.
|
||||
let mut spoken = VERSION_2;
|
||||
loop {
|
||||
let mut out = BytesMut::new();
|
||||
tokio::select! {
|
||||
read = stream.read_buf(&mut buf) => {
|
||||
if !matches!(read, Ok(read) if read > 0) {
|
||||
return;
|
||||
}
|
||||
while let Ok(Some((version, pdu, used))) = Pdu::decode(&buf) {
|
||||
let _ = buf.split_to(used);
|
||||
spoken = version;
|
||||
match pdu {
|
||||
Pdu::SerialQuery { .. } => Pdu::CacheReset.encode(version, &mut out),
|
||||
Pdu::ResetQuery => {
|
||||
let data = data.lock().unwrap().clone();
|
||||
Pdu::CacheResponse { session_id: SESSION }.encode(version, &mut out);
|
||||
for vrp in data.vrps {
|
||||
Pdu::Prefix { announce: true, vrp }.encode(version, &mut out);
|
||||
}
|
||||
// Only version 2 has ASPAs.
|
||||
for (customer, providers) in data.aspas {
|
||||
if version >= VERSION_2 {
|
||||
Pdu::Aspa { announce: true, customer, providers }
|
||||
.encode(version, &mut out);
|
||||
}
|
||||
}
|
||||
Pdu::EndOfData {
|
||||
session_id: SESSION,
|
||||
serial: data.serial,
|
||||
timers: Some(Timers::default()),
|
||||
}
|
||||
.encode(version, &mut out);
|
||||
}
|
||||
_ => return,
|
||||
}
|
||||
}
|
||||
}
|
||||
changed = serial.changed() => {
|
||||
if changed.is_err() {
|
||||
return;
|
||||
}
|
||||
let serial = *serial.borrow_and_update();
|
||||
Pdu::SerialNotify { session_id: SESSION, serial }.encode(spoken, &mut out);
|
||||
}
|
||||
}
|
||||
if stream.write_all(&out).await.is_err() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,55 +1,13 @@
|
||||
use std::net::{Ipv4Addr, SocketAddr};
|
||||
use std::time::Duration;
|
||||
|
||||
use bgp_packet::message::{BGP_HEADER_LEN, Message, NotificationMessage, UpdateMessage};
|
||||
use bgp_packet::open::{
|
||||
AddPathEntry, Capability, ExtendedNextHopEntry, OpenMessage, OpenOption, SendReceive,
|
||||
};
|
||||
use bgp_packet::message::{BGP_HEADER_LEN, Message, UpdateMessage};
|
||||
use bgp_packet::open::{Capability, OpenMessage, OpenOption};
|
||||
use bgp_primitives::constants::{Afi, AfiSafi, Safi};
|
||||
use bgp_primitives::parser::ParserContext;
|
||||
use bytes::BytesMut;
|
||||
use eyre::{Result, bail, eyre};
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::tcp::OwnedWriteHalf;
|
||||
use tokio::net::{TcpListener, TcpStream};
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
use crate::TIMEOUT;
|
||||
|
||||
/// The capabilities a route collector offers in its OPEN: four-octet AS
|
||||
/// numbers as `asn`, each of `families`, and to be sent several routes
|
||||
/// for a prefix (ADD-PATH, RFC 7911) in each of them; with
|
||||
/// `ipv4_over_ipv6`, IPv4 routes with IPv6 next hops too (RFC 8950), as a
|
||||
/// collector on an IPv6-only session wants. For the tests and benchmarks
|
||||
/// that play a collector taking a router's whole table.
|
||||
pub fn collector_capabilities(
|
||||
asn: u32,
|
||||
families: &[AfiSafi],
|
||||
ipv4_over_ipv6: bool,
|
||||
) -> Vec<Capability> {
|
||||
let mut capabilities = vec![Capability::FourByteAsn { asn }];
|
||||
capabilities.extend(families.iter().copied().map(Capability::multiprotocol));
|
||||
capabilities.push(Capability::AddPath {
|
||||
entries: families
|
||||
.iter()
|
||||
.map(|family| AddPathEntry {
|
||||
afi: family.afi,
|
||||
safi: family.safi,
|
||||
send_recv: SendReceive::Receive,
|
||||
})
|
||||
.collect(),
|
||||
});
|
||||
if ipv4_over_ipv6 {
|
||||
capabilities.push(Capability::ExtendedNextHop {
|
||||
entries: vec![ExtendedNextHopEntry {
|
||||
afi: Afi::IPV4,
|
||||
safi: Safi::UNICAST,
|
||||
nh_afi: Afi::IPV6,
|
||||
}],
|
||||
});
|
||||
}
|
||||
capabilities
|
||||
}
|
||||
use tokio::net::TcpStream;
|
||||
|
||||
/// A minimal BGP peer for use in tests. Connects over TCP and can exchange
|
||||
/// framed BGP messages using the `bgp-packet` wire format.
|
||||
@@ -72,13 +30,6 @@ impl TestPeer {
|
||||
Ok(Self::from_stream(stream))
|
||||
}
|
||||
|
||||
/// The next connection to `listener`, as the server dials it. It is
|
||||
/// waited for at most [`TIMEOUT`].
|
||||
pub async fn accept(listener: &TcpListener) -> Result<Self> {
|
||||
let (stream, _) = tokio::time::timeout(TIMEOUT, listener.accept()).await??;
|
||||
Ok(Self::from_stream(stream))
|
||||
}
|
||||
|
||||
/// Wraps an already-connected stream, e.g. one accepted from a server
|
||||
/// that dialled us.
|
||||
pub fn from_stream(stream: TcpStream) -> Self {
|
||||
@@ -90,43 +41,6 @@ impl TestPeer {
|
||||
}
|
||||
}
|
||||
|
||||
/// The connection itself, for a test or a tool that goes on without
|
||||
/// the framing, once the handshake is done.
|
||||
pub fn into_stream(self) -> TcpStream {
|
||||
self.stream
|
||||
}
|
||||
|
||||
/// The connection with a task reading and dropping whatever the server
|
||||
/// sends over it, and the half that is left for writing: for a peer
|
||||
/// that has nothing to hear, as one feeding a table or taking one by
|
||||
/// the megabyte has not, and whose writer must never be held up by
|
||||
/// what it has not read. The task ends with the connection, or when
|
||||
/// its handle is dropped.
|
||||
pub fn drain(self) -> (OwnedWriteHalf, JoinHandle<()>) {
|
||||
let (mut reader, writer) = self.stream.into_split();
|
||||
let sink = tokio::spawn(async move {
|
||||
let mut sink = vec![0u8; 1 << 16];
|
||||
while matches!(reader.read(&mut sink).await, Ok(n) if n > 0) {}
|
||||
});
|
||||
(writer, sink)
|
||||
}
|
||||
|
||||
/// A KEEPALIVE as it goes on the wire, for whoever writes to the
|
||||
/// connection without the framing and has to keep the session alive
|
||||
/// past its hold time.
|
||||
pub fn keepalive_bytes() -> Vec<u8> {
|
||||
let mut wire = BytesMut::new();
|
||||
Message::KeepAlive
|
||||
.to_wire(&ParserContext::default(), &mut wire)
|
||||
.expect("a KEEPALIVE is nineteen octets");
|
||||
wire.to_vec()
|
||||
}
|
||||
|
||||
/// Our end of the connection: what the server sees the peer as.
|
||||
pub fn local_addr(&self) -> Result<SocketAddr> {
|
||||
Ok(self.stream.local_addr()?)
|
||||
}
|
||||
|
||||
/// Send a BGP message.
|
||||
pub async fn send(&mut self, msg: &Message) -> Result<()> {
|
||||
let send_ctx = ParserContext {
|
||||
@@ -139,32 +53,8 @@ impl TestPeer {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send an UPDATE: an [`Announcement`](crate::announcement::Announcement),
|
||||
/// a [`withdrawal`](crate::announcement::withdrawal), or one built by
|
||||
/// hand.
|
||||
pub async fn announce(&mut self, update: impl Into<UpdateMessage>) -> Result<()> {
|
||||
self.send(&Message::Update(update.into())).await
|
||||
}
|
||||
|
||||
/// Read exactly one BGP message, waiting at most [`TIMEOUT`] for it. A
|
||||
/// test that expects nothing to arrive says for how long with
|
||||
/// [`TestPeer::recv_within`].
|
||||
/// Read exactly one BGP message.
|
||||
pub async fn recv(&mut self) -> Result<Message> {
|
||||
self.recv_within(TIMEOUT).await
|
||||
}
|
||||
|
||||
/// Read exactly one BGP message, waiting at most `wait` for it.
|
||||
pub async fn recv_within(&mut self, wait: Duration) -> Result<Message> {
|
||||
match tokio::time::timeout(wait, self.recv_untimed()).await {
|
||||
Ok(message) => message,
|
||||
Err(_) => Err(eyre::eyre!("no message from the server within {wait:?}")),
|
||||
}
|
||||
}
|
||||
|
||||
/// Read exactly one BGP message, however long it takes to come. For a
|
||||
/// peer that is to stay on the line, like the `fake_peer` example;
|
||||
/// tests want [`TestPeer::recv`].
|
||||
pub async fn recv_untimed(&mut self) -> Result<Message> {
|
||||
let mut header = [0u8; BGP_HEADER_LEN as usize];
|
||||
self.stream.read_exact(&mut header).await?;
|
||||
|
||||
@@ -189,19 +79,7 @@ impl TestPeer {
|
||||
match self.recv().await? {
|
||||
Message::Update(u) => return Ok(u),
|
||||
Message::KeepAlive => {}
|
||||
other => return Err(eyre::eyre!("Expected UPDATE, got {:?}", other)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Read messages until a NOTIFICATION arrives, skipping KEEPALIVEs. Any
|
||||
/// other message type is an error.
|
||||
pub async fn expect_notification(&mut self) -> Result<NotificationMessage> {
|
||||
loop {
|
||||
match self.recv().await? {
|
||||
Message::Notification(n) => return Ok(n),
|
||||
Message::KeepAlive => {}
|
||||
other => return Err(eyre::eyre!("expected NOTIFICATION, got {other:?}")),
|
||||
other => bail!("Expected UPDATE, got {:?}", other),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -210,9 +88,7 @@ impl TestPeer {
|
||||
pub async fn recv_open(&mut self) -> Result<OpenMessage> {
|
||||
match self.recv().await? {
|
||||
Message::Open(o) => {
|
||||
// AS numbers are four octets wide only if both sides
|
||||
// offered it (RFC 6793 §4.1): the server's offer is read
|
||||
// here, ours was noted when the OPEN went out.
|
||||
// Enable 4-octet ASN parsing if the peer advertised it.
|
||||
let four_byte = o.options.iter().any(|opt| {
|
||||
matches!(opt, OpenOption::Capabilities(caps)
|
||||
if caps.iter().any(|c| matches!(c, Capability::FourByteAsn { .. })))
|
||||
@@ -220,7 +96,7 @@ impl TestPeer {
|
||||
self.ctx.four_octet_asn = four_byte && self.offered_four_octet_asn;
|
||||
Ok(o)
|
||||
}
|
||||
other => Err(eyre::eyre!("Expected OPEN, got {:?}", other)),
|
||||
other => bail!("Expected OPEN, got {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,48 +186,28 @@ impl TestPeer {
|
||||
|
||||
async fn finish_handshake(&mut self) -> Result<OpenMessage> {
|
||||
let server_open = self.recv_open().await?;
|
||||
// The server's KEEPALIVE says it accepted our OPEN; anything else
|
||||
// is its refusal, or a server out of step.
|
||||
// Expect server KEEPALIVE
|
||||
match self.recv().await? {
|
||||
Message::KeepAlive => {}
|
||||
other => {
|
||||
return Err(eyre::eyre!(
|
||||
"Expected KEEPALIVE from server, got {:?}",
|
||||
other
|
||||
));
|
||||
}
|
||||
other => bail!("Expected KEEPALIVE from server, got {:?}", other),
|
||||
}
|
||||
// Ours says the same of its OPEN, and the session is established.
|
||||
// Send our KEEPALIVE
|
||||
self.send(&Message::KeepAlive).await?;
|
||||
Ok(server_open)
|
||||
}
|
||||
|
||||
/// Writes `bytes` as they are, with no framing of its own: for a test
|
||||
/// that sends what no message would be, and for the benchmarks, which
|
||||
/// send a table encoded once as one stream rather than message by
|
||||
/// message.
|
||||
/// Send raw bytes without framing — useful for testing invalid input.
|
||||
pub async fn send_raw(&mut self, bytes: &[u8]) -> Result<()> {
|
||||
self.stream.write_all(bytes).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Whether the server closes the connection within `wait`. Whatever it
|
||||
/// still sends first, KEEPALIVEs as a rule, is read past: the end of
|
||||
/// the stream, or anything unreadable, is what counts.
|
||||
pub async fn closed_within(&mut self, wait: Duration) -> bool {
|
||||
tokio::time::timeout(wait, async { while self.recv_untimed().await.is_ok() {} })
|
||||
.await
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
/// Whether the server closes the connection within [`TIMEOUT`] without
|
||||
/// having sent anything: what a peer that is not let in sees. A server
|
||||
/// that sends its OPEN first, or that leaves the connection open, is
|
||||
/// answering.
|
||||
pub async fn closed_unanswered(&mut self) -> bool {
|
||||
matches!(
|
||||
tokio::time::timeout(TIMEOUT, self.recv_untimed()).await,
|
||||
Ok(Err(_))
|
||||
)
|
||||
/// Returns true if the remote end has closed the connection.
|
||||
pub async fn is_closed(&mut self) -> bool {
|
||||
let mut buf = [0u8; 1];
|
||||
match self.stream.read(&mut buf).await {
|
||||
Ok(0) | Err(_) => true,
|
||||
Ok(_) => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,51 +1,31 @@
|
||||
use std::collections::BTreeSet;
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||
use std::sync::Arc;
|
||||
|
||||
use bgp_fib::Fib;
|
||||
use bgp_packet::message::{NlriNextHop, PathAttribute};
|
||||
use bgp_packet::open::Capability;
|
||||
use bgp_primitives::ip_prefix::IpPrefix;
|
||||
use bgp_rib::LocalRib;
|
||||
use bgp_rib::table::{Event, PeerIndex, Reader, RibTable};
|
||||
use bgp_rpki::Rpki;
|
||||
use bgp_server::config::{Config, PeerConfig};
|
||||
use bgp_server::config::Config;
|
||||
use bgp_server::error::ServerError;
|
||||
use bgp_server::rejections::RejectionLog;
|
||||
use bgp_server::restart::Restarts;
|
||||
use bgp_server::server::BgpServer;
|
||||
use bgp_server::session::{SessionHandle, SessionRegistry};
|
||||
use bgp_server::session::SessionRegistry;
|
||||
use bgp_web::{AppState, RouterInfo};
|
||||
use eyre::{Result, eyre};
|
||||
use eyre::Result;
|
||||
use tokio::net::TcpListener;
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
use crate::test_peer::TestPeer;
|
||||
use crate::{peer_config, wait_for, wait_until};
|
||||
|
||||
/// A BGP server started on an ephemeral port for testing.
|
||||
/// Aborts the server task when dropped.
|
||||
pub struct TestServer {
|
||||
handle: JoinHandle<Result<(), ServerError>>,
|
||||
/// The single IPv4 loopback address the server is bound to.
|
||||
pub addr: SocketAddr,
|
||||
/// Every address it is bound to, in the order configured: `addr`
|
||||
/// first.
|
||||
pub addrs: Vec<SocketAddr>,
|
||||
/// The server's routing table.
|
||||
pub rib: Arc<RibTable>,
|
||||
/// Handle to the server's locally originated routes.
|
||||
pub local_rib: LocalRib,
|
||||
/// Live view of the server's established sessions.
|
||||
pub registry: SessionRegistry,
|
||||
/// What peers announced that was not taken.
|
||||
pub rejections: RejectionLog,
|
||||
/// The data for route origin validation, if the server validates.
|
||||
pub rpki: Option<Rpki>,
|
||||
/// What keeps the best routes in the kernel, if the server does.
|
||||
pub kernel: Option<Fib>,
|
||||
/// The routes kept of restarting peers (graceful restart).
|
||||
pub restarts: Arc<Restarts>,
|
||||
pub local_asn: u32,
|
||||
}
|
||||
|
||||
@@ -65,177 +45,37 @@ impl TestServer {
|
||||
pub async fn start_with(local_asn: u32, customize: impl FnOnce(&mut Config)) -> Result<Self> {
|
||||
let mut config = Config {
|
||||
router_id: Ipv4Addr::new(127, 0, 0, 1),
|
||||
cluster_id: Ipv4Addr::new(127, 0, 0, 1),
|
||||
local_asn,
|
||||
hold_time_secs: 90,
|
||||
listen_endpoints: vec![SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0)],
|
||||
peers: vec![],
|
||||
connect_retry: std::time::Duration::from_secs(5),
|
||||
collision_detect_established_state: false,
|
||||
// Most tests play a peer that connects from wherever the
|
||||
// system puts it; the ones about who may connect turn this off.
|
||||
accept_unconfigured_peers: true,
|
||||
originate: vec![],
|
||||
http_endpoint: None,
|
||||
rpki: None,
|
||||
kernel: None,
|
||||
export: Default::default(),
|
||||
export_limits: None,
|
||||
community_names: Vec::new(),
|
||||
control: None,
|
||||
graceful_restart: Default::default(),
|
||||
};
|
||||
customize(&mut config);
|
||||
|
||||
let bound = BgpServer::bind(config).await?;
|
||||
let addrs = bound.local_addrs();
|
||||
let addr = *addrs.first().expect("at least one listener");
|
||||
let addr = bound
|
||||
.local_addrs()
|
||||
.into_iter()
|
||||
.next()
|
||||
.expect("at least one listener");
|
||||
let rib = Arc::clone(&bound.rib);
|
||||
let local_rib = bound.local_rib.clone();
|
||||
let registry = bound.registry.clone();
|
||||
let rejections = bound.rejections.clone();
|
||||
let rpki = bound.rpki.clone();
|
||||
let kernel = bound.kernel.clone();
|
||||
let restarts = Arc::clone(&bound.restarts);
|
||||
|
||||
let handle = tokio::spawn(bound.run());
|
||||
Ok(Self {
|
||||
handle,
|
||||
addr,
|
||||
addrs,
|
||||
rib,
|
||||
local_rib,
|
||||
registry,
|
||||
rejections,
|
||||
rpki,
|
||||
kernel,
|
||||
restarts,
|
||||
local_asn,
|
||||
})
|
||||
}
|
||||
|
||||
/// A server in `local_asn` that takes connections from one configured
|
||||
/// peer only: "r1", in `remote_asn`, at the loopback address the test
|
||||
/// connects from. `customize` edits the configuration and the peer's
|
||||
/// before the server starts. The server dials the peer too; where that
|
||||
/// goes is returned with it, for the test to answer or to leave alone.
|
||||
pub async fn with_peer(
|
||||
local_asn: u32,
|
||||
remote_asn: u32,
|
||||
customize: impl FnOnce(&mut Config, &mut PeerConfig),
|
||||
) -> Result<ServerWithPeer> {
|
||||
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await?;
|
||||
let endpoint = listener.local_addr()?;
|
||||
let server = Self::start_with(local_asn, |config| {
|
||||
config.accept_unconfigured_peers = false;
|
||||
let mut peer = peer_config("r1", endpoint, remote_asn);
|
||||
customize(config, &mut peer);
|
||||
config.peers = vec![peer];
|
||||
})
|
||||
.await?;
|
||||
Ok(ServerWithPeer { server, listener })
|
||||
}
|
||||
}
|
||||
|
||||
/// A server with one configured peer, and the listener at the peer's
|
||||
/// address, which the server's dialling goes to.
|
||||
pub struct ServerWithPeer {
|
||||
pub server: TestServer,
|
||||
pub listener: TcpListener,
|
||||
}
|
||||
|
||||
impl TestServer {
|
||||
/// A session with the server from a peer in `asn` with `router_id`,
|
||||
/// offering the capabilities of a typical dual-stack speaker. Returns
|
||||
/// once the server lists the session as established. Distinct peers
|
||||
/// need distinct identifiers, or the second connection is (correctly)
|
||||
/// treated as a collision with the first.
|
||||
pub async fn established(&self, asn: u32, router_id: Ipv4Addr) -> Result<TestPeer> {
|
||||
let mut peer = TestPeer::connect(self.addr).await?;
|
||||
peer.handshake(asn, router_id, 90).await?;
|
||||
self.listed(&peer).await?;
|
||||
Ok(peer)
|
||||
}
|
||||
|
||||
/// As [`TestServer::established`], with an OPEN carrying exactly
|
||||
/// `capabilities`.
|
||||
pub async fn established_with(
|
||||
&self,
|
||||
asn: u32,
|
||||
router_id: Ipv4Addr,
|
||||
capabilities: Vec<Capability>,
|
||||
) -> Result<TestPeer> {
|
||||
let mut peer = TestPeer::connect(self.addr).await?;
|
||||
peer.handshake_with(asn, router_id, 90, capabilities)
|
||||
.await?;
|
||||
self.listed(&peer).await?;
|
||||
Ok(peer)
|
||||
}
|
||||
|
||||
/// Waits until the registry lists `peer`'s session.
|
||||
async fn listed(&self, peer: &TestPeer) -> Result<()> {
|
||||
let from = peer.local_addr()?;
|
||||
wait_until("the session is established", || {
|
||||
self.registry
|
||||
.list()
|
||||
.iter()
|
||||
.any(|session| session.peer.addr == from)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// Waits until the registry lists `n` established sessions.
|
||||
pub async fn wait_for_sessions(&self, n: usize) -> Result<()> {
|
||||
wait_until(&format!("{n} sessions are established"), || {
|
||||
self.registry.list().len() == n
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// Waits until a session is established, and returns it.
|
||||
pub async fn wait_for_session(&self) -> Result<SessionHandle> {
|
||||
wait_for("a session is established", || {
|
||||
self.registry.list().into_iter().next()
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// Waits until the table holds `routes` routes.
|
||||
pub async fn wait_for_routes(&self, routes: usize) -> Result<()> {
|
||||
wait_until(&format!("the table holds {routes} routes"), || {
|
||||
self.rib.len() == routes
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// Waits until the table holds exactly the prefixes `expected` and the
|
||||
/// peer has `held` routes held aside.
|
||||
pub async fn table_becomes(&self, expected: &[&str], held: u64) -> Result<()> {
|
||||
let expected: BTreeSet<String> = expected.iter().map(|p| p.to_string()).collect();
|
||||
let table = || -> BTreeSet<String> {
|
||||
self.received_routes()
|
||||
.iter()
|
||||
.map(|route| route.prefix.to_string())
|
||||
.collect()
|
||||
};
|
||||
let held_now = || {
|
||||
self.registry
|
||||
.list()
|
||||
.first()
|
||||
.map_or(0, |session| session.counters.held())
|
||||
};
|
||||
wait_until("the table is as expected", || {
|
||||
table() == expected && held_now() == held
|
||||
})
|
||||
.await
|
||||
.map_err(|_| {
|
||||
eyre!(
|
||||
"the table has {:?} with {} held; expected {expected:?} with {held} held",
|
||||
table(),
|
||||
held_now()
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A route in the server's table, as a test wants to look at it.
|
||||
@@ -350,10 +190,6 @@ impl TestServer {
|
||||
registry: self.registry.clone(),
|
||||
local_rib: self.local_rib.clone(),
|
||||
rib: Arc::clone(&self.rib),
|
||||
rejections: self.rejections.clone(),
|
||||
rpki: self.rpki.clone(),
|
||||
kernel: self.kernel.clone(),
|
||||
restarts: Arc::clone(&self.restarts),
|
||||
};
|
||||
let handle = tokio::spawn(bgp_web::serve_on(state, listener));
|
||||
Ok((base_url, HttpGuard { handle }))
|
||||
|
||||
@@ -1,45 +1,51 @@
|
||||
//! End-to-end tests of ADD-PATH (RFC 7911): a peer sending several routes
|
||||
//! for one prefix, told apart by path identifiers.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr};
|
||||
use std::sync::Arc;
|
||||
use std::net::{Ipv4Addr, SocketAddr};
|
||||
use std::time::Duration;
|
||||
|
||||
use bgp_integration_tests::announcement::{Announcement, withdrawal};
|
||||
use bgp_integration_tests::test_peer::{TestPeer, collector_capabilities};
|
||||
use bgp_integration_tests::test_peer::TestPeer;
|
||||
use bgp_integration_tests::test_server::TestServer;
|
||||
use bgp_integration_tests::{peer_config, prefix, wait_until};
|
||||
use bgp_packet::message::{
|
||||
AsPathAttribute, Message, MpReachNlriPathAttribute, NlriNextHop, OriginPathAttribute,
|
||||
PathAttribute, UpdateMessage,
|
||||
AsPathAttribute, Message, NextHopPathAttribute, OriginPathAttribute, PathAttribute,
|
||||
UpdateMessage,
|
||||
};
|
||||
use bgp_packet::open::{AddPathEntry, Capability, OpenMessage, SendReceive};
|
||||
use bgp_packet::open::{AddPathEntry, AddPathSendRecv, Capability, OpenMessage};
|
||||
use bgp_primitives::constants::AfiSafi;
|
||||
use bgp_primitives::ip_prefix::Nlri;
|
||||
use bgp_primitives::ip_prefix::{IpPrefix, Nlri};
|
||||
use bgp_rib::{LocalRoute, LocalRouteAttrs};
|
||||
use bgp_server::config::PeerConfig;
|
||||
use bgp_server::family::SUPPORTED;
|
||||
use bgp_server::policy::{ExportPolicy, ImportPolicy};
|
||||
use bgp_server::family::{FamilySet, SUPPORTED};
|
||||
use eyre::Result;
|
||||
use serde_json::Value;
|
||||
|
||||
const LOCAL_ASN: u32 = 65000;
|
||||
const PEER_ASN: u32 = 65001;
|
||||
const TIMEOUT: Duration = Duration::from_secs(5);
|
||||
|
||||
fn prefix(s: &str) -> IpPrefix {
|
||||
IpPrefix::try_from(s).unwrap()
|
||||
}
|
||||
|
||||
/// A server on which connections from this machine are those of a
|
||||
/// configured peer, with ADD-PATH offered to it as `add_path` says.
|
||||
async fn server_offering(add_path: Option<SendReceive>, originate: &[&str]) -> Result<TestServer> {
|
||||
async fn server_offering(
|
||||
add_path: Option<AddPathSendRecv>,
|
||||
originate: &[&str],
|
||||
) -> Result<TestServer> {
|
||||
let originate: Vec<LocalRoute> = originate
|
||||
.iter()
|
||||
.map(|p| LocalRoute::new(prefix(p), LocalRouteAttrs::default()))
|
||||
.collect();
|
||||
TestServer::start_with(LOCAL_ASN, |c| {
|
||||
let at = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 1);
|
||||
c.peers = vec![PeerConfig {
|
||||
name: "route-server-client".into(),
|
||||
endpoint: SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 1),
|
||||
remote_asn: PEER_ASN,
|
||||
hold_time_secs: 90,
|
||||
local_address: None,
|
||||
families: FamilySet::supported(),
|
||||
add_path,
|
||||
import: ImportPolicy::permissive(),
|
||||
..peer_config("route-server-client", at, PEER_ASN)
|
||||
}];
|
||||
c.originate = originate;
|
||||
// Keep the dialer out of the way.
|
||||
@@ -52,7 +58,7 @@ async fn server_offering(add_path: Option<SendReceive>, originate: &[&str]) -> R
|
||||
/// IPv4 unicast. Returns the peer and the server's OPEN.
|
||||
async fn peer_offering(
|
||||
server: &TestServer,
|
||||
offer: Option<SendReceive>,
|
||||
offer: Option<AddPathSendRecv>,
|
||||
) -> Result<(TestPeer, OpenMessage)> {
|
||||
let mut capabilities = vec![Capability::FourByteAsn { asn: PEER_ASN }];
|
||||
capabilities.extend(SUPPORTED.into_iter().map(Capability::multiprotocol));
|
||||
@@ -69,11 +75,16 @@ async fn peer_offering(
|
||||
let open = peer
|
||||
.handshake_with(PEER_ASN, Ipv4Addr::new(10, 0, 0, 1), 90, capabilities)
|
||||
.await?;
|
||||
server.wait_for_session().await?;
|
||||
tokio::time::timeout(TIMEOUT, async {
|
||||
while server.registry.list().is_empty() {
|
||||
tokio::time::sleep(Duration::from_millis(5)).await;
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
Ok((peer, open))
|
||||
}
|
||||
|
||||
fn add_path_offered(open: &OpenMessage) -> Vec<(AfiSafi, SendReceive)> {
|
||||
fn add_path_offered(open: &OpenMessage) -> Vec<(AfiSafi, AddPathSendRecv)> {
|
||||
open.capabilities()
|
||||
.filter_map(|capability| match capability {
|
||||
Capability::AddPath { entries } => Some(entries),
|
||||
@@ -84,8 +95,26 @@ fn add_path_offered(open: &OpenMessage) -> Vec<(AfiSafi, SendReceive)> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn announcement(nlri: Nlri, as_path: &[u32]) -> Announcement {
|
||||
Announcement::of([nlri], as_path)
|
||||
fn announcement(nlri: Nlri, as_path: &[u32]) -> Message {
|
||||
Message::Update(UpdateMessage {
|
||||
withdrawn_routes: vec![],
|
||||
path_attributes: vec![
|
||||
PathAttribute::Origin(OriginPathAttribute::IGP),
|
||||
AsPathAttribute::from_asns(as_path.to_vec()),
|
||||
PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::new(192, 0, 2, 1))),
|
||||
],
|
||||
announced_routes: vec![nlri],
|
||||
})
|
||||
}
|
||||
|
||||
async fn wait_for_routes(server: &TestServer, routes: usize) -> Result<()> {
|
||||
tokio::time::timeout(TIMEOUT, async {
|
||||
while server.rib.len() != routes {
|
||||
tokio::time::sleep(Duration::from_millis(5)).await;
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The routes the server holds, as (path identifier, AS path, best).
|
||||
@@ -113,14 +142,14 @@ fn held(server: &TestServer) -> Vec<(u32, Vec<u32>, bool)> {
|
||||
/// of them is chosen, and each is withdrawn on its own.
|
||||
#[tokio::test]
|
||||
async fn test_several_routes_for_a_prefix_from_one_peer() -> Result<()> {
|
||||
let server = server_offering(Some(SendReceive::Receive), &[]).await?;
|
||||
let server = server_offering(Some(AddPathSendRecv::Receive), &[]).await?;
|
||||
let (base, _http) = server.serve_http().await?;
|
||||
let (mut peer, open) = peer_offering(&server, Some(SendReceive::Send)).await?;
|
||||
let (mut peer, open) = peer_offering(&server, Some(AddPathSendRecv::Send)).await?;
|
||||
|
||||
// The server offered to receive, for every family it offers.
|
||||
assert_eq!(
|
||||
add_path_offered(&open),
|
||||
SUPPORTED.map(|family| (family, SendReceive::Receive))
|
||||
SUPPORTED.map(|family| (family, AddPathSendRecv::Receive))
|
||||
);
|
||||
// In use for the one family the peer sends for, and in that direction.
|
||||
let session = &server.registry.list()[0];
|
||||
@@ -133,11 +162,11 @@ async fn test_several_routes_for_a_prefix_from_one_peer() -> Result<()> {
|
||||
|
||||
let p = prefix("203.0.113.0/24");
|
||||
let path = |path_id| Nlri { prefix: p, path_id };
|
||||
peer.announce(announcement(path(1), &[PEER_ASN, 64999, 64998]))
|
||||
peer.send(&announcement(path(1), &[PEER_ASN, 64999, 64998]))
|
||||
.await?;
|
||||
peer.announce(announcement(path(2), &[PEER_ASN, 64998]))
|
||||
peer.send(&announcement(path(2), &[PEER_ASN, 64998]))
|
||||
.await?;
|
||||
server.wait_for_routes(2).await?;
|
||||
wait_for_routes(&server, 2).await?;
|
||||
assert_eq!(
|
||||
held(&server),
|
||||
[
|
||||
@@ -152,7 +181,7 @@ async fn test_several_routes_for_a_prefix_from_one_peer() -> Result<()> {
|
||||
.await?
|
||||
.json()
|
||||
.await?;
|
||||
let mut ids: Vec<u64> = routes["routes"]
|
||||
let mut ids: Vec<u64> = routes
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
@@ -170,24 +199,34 @@ async fn test_several_routes_for_a_prefix_from_one_peer() -> Result<()> {
|
||||
);
|
||||
|
||||
// Announcing a path again replaces that path only.
|
||||
peer.announce(announcement(path(1), &[PEER_ASN])).await?;
|
||||
wait_until("the path is replaced", || held(&server)[0].1.len() == 1).await?;
|
||||
peer.send(&announcement(path(1), &[PEER_ASN])).await?;
|
||||
tokio::time::timeout(TIMEOUT, async {
|
||||
while held(&server)[0].1.len() != 1 {
|
||||
tokio::time::sleep(Duration::from_millis(5)).await;
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
assert_eq!(
|
||||
held(&server),
|
||||
[(1, vec![PEER_ASN], true), (2, vec![PEER_ASN, 64998], false),]
|
||||
);
|
||||
|
||||
// And withdrawing one leaves the other.
|
||||
peer.announce(withdrawal([path(1)])).await?;
|
||||
server.wait_for_routes(1).await?;
|
||||
peer.send(&Message::Update(UpdateMessage {
|
||||
withdrawn_routes: vec![path(1)],
|
||||
path_attributes: vec![],
|
||||
announced_routes: vec![],
|
||||
}))
|
||||
.await?;
|
||||
wait_for_routes(&server, 1).await?;
|
||||
assert_eq!(held(&server), [(2, vec![PEER_ASN, 64998], true)]);
|
||||
|
||||
// When the session ends, all of its routes go.
|
||||
peer.announce(announcement(path(3), &[PEER_ASN, 1, 2, 3]))
|
||||
peer.send(&announcement(path(3), &[PEER_ASN, 1, 2, 3]))
|
||||
.await?;
|
||||
server.wait_for_routes(2).await?;
|
||||
wait_for_routes(&server, 2).await?;
|
||||
drop(peer);
|
||||
server.wait_for_routes(0).await?;
|
||||
wait_for_routes(&server, 0).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -197,24 +236,24 @@ async fn test_several_routes_for_a_prefix_from_one_peer() -> Result<()> {
|
||||
#[tokio::test]
|
||||
async fn test_add_path_is_not_used_unless_both_sides_offer_it() -> Result<()> {
|
||||
let server = server_offering(None, &["10.100.0.0/16"]).await?;
|
||||
let (mut peer, open) = peer_offering(&server, Some(SendReceive::Both)).await?;
|
||||
let (mut peer, open) = peer_offering(&server, Some(AddPathSendRecv::Both)).await?;
|
||||
assert!(add_path_offered(&open).is_empty());
|
||||
let session = &server.registry.list()[0];
|
||||
assert!(session.add_path_receive.is_empty() && session.add_path_send.is_empty());
|
||||
|
||||
// Plain prefixes both ways.
|
||||
let update = peer.recv_update().await?;
|
||||
let update = tokio::time::timeout(TIMEOUT, peer.recv_update()).await??;
|
||||
assert_eq!(update.announced_routes, vec![prefix("10.100.0.0/16")]);
|
||||
peer.announce(announcement(prefix("203.0.113.0/24").into(), &[PEER_ASN]))
|
||||
peer.send(&announcement(prefix("203.0.113.0/24").into(), &[PEER_ASN]))
|
||||
.await?;
|
||||
server.wait_for_routes(1).await?;
|
||||
wait_for_routes(&server, 1).await?;
|
||||
assert_eq!(held(&server), [(0, vec![PEER_ASN], true)]);
|
||||
|
||||
// And the other way round: offered by the server, not by the peer.
|
||||
let server = server_offering(Some(SendReceive::Both), &["10.100.0.0/16"]).await?;
|
||||
let server = server_offering(Some(AddPathSendRecv::Both), &["10.100.0.0/16"]).await?;
|
||||
let (mut peer, open) = peer_offering(&server, None).await?;
|
||||
assert_eq!(add_path_offered(&open).len(), SUPPORTED.len());
|
||||
let update = peer.recv_update().await?;
|
||||
let update = tokio::time::timeout(TIMEOUT, peer.recv_update()).await??;
|
||||
assert_eq!(update.announced_routes, vec![prefix("10.100.0.0/16")]);
|
||||
Ok(())
|
||||
}
|
||||
@@ -224,8 +263,8 @@ async fn test_add_path_is_not_used_unless_both_sides_offer_it() -> Result<()> {
|
||||
/// understands them.
|
||||
#[tokio::test]
|
||||
async fn test_prefixes_are_sent_with_path_identifiers_when_agreed() -> Result<()> {
|
||||
let server = server_offering(Some(SendReceive::Both), &["10.100.0.0/16"]).await?;
|
||||
let (mut peer, _) = peer_offering(&server, Some(SendReceive::Receive)).await?;
|
||||
let server = server_offering(Some(AddPathSendRecv::Both), &["10.100.0.0/16"]).await?;
|
||||
let (mut peer, _) = peer_offering(&server, Some(AddPathSendRecv::Receive)).await?;
|
||||
let session = &server.registry.list()[0];
|
||||
assert_eq!(
|
||||
session.add_path_send,
|
||||
@@ -235,335 +274,15 @@ async fn test_prefixes_are_sent_with_path_identifiers_when_agreed() -> Result<()
|
||||
|
||||
// Read as agreed: with identifiers.
|
||||
peer.ctx.add_path = vec![AfiSafi::IPV4_UNICAST];
|
||||
let update = peer.recv_update().await?;
|
||||
let update = tokio::time::timeout(TIMEOUT, peer.recv_update()).await??;
|
||||
assert_eq!(update.announced_routes.len(), 1);
|
||||
assert_eq!(update.announced_routes[0].prefix, prefix("10.100.0.0/16"));
|
||||
|
||||
// The peer did not offer to send, so the server reads its prefixes
|
||||
// plain.
|
||||
peer.announce(announcement(prefix("203.0.113.0/24").into(), &[PEER_ASN]))
|
||||
peer.send(&announcement(prefix("203.0.113.0/24").into(), &[PEER_ASN]))
|
||||
.await?;
|
||||
server.wait_for_routes(1).await?;
|
||||
wait_for_routes(&server, 1).await?;
|
||||
assert_eq!(held(&server), [(0, vec![PEER_ASN], true)]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
const COLLECTOR_ASN: u32 = 65100;
|
||||
|
||||
/// What a collector holds: for each prefix and path identifier, the
|
||||
/// route's AS path.
|
||||
type Held = BTreeMap<(String, u32), Vec<u32>>;
|
||||
|
||||
/// Reads what the collector is told, into `held`, until `done` says so.
|
||||
async fn told(
|
||||
collector: &mut TestPeer,
|
||||
held: &mut Held,
|
||||
done: impl Fn(&Held) -> bool,
|
||||
) -> Result<()> {
|
||||
while !done(held) {
|
||||
let update = collector.recv_update().await?;
|
||||
let path = update
|
||||
.path_attributes
|
||||
.iter()
|
||||
.find_map(|attribute| match attribute {
|
||||
PathAttribute::AsPath(path) => Some(path.segments[0].path.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or_default();
|
||||
for nlri in &update.withdrawn_routes {
|
||||
held.remove(&(nlri.prefix.to_string(), nlri.path_id));
|
||||
}
|
||||
for nlri in &update.announced_routes {
|
||||
held.insert((nlri.prefix.to_string(), nlri.path_id), path.clone());
|
||||
}
|
||||
// And what came and went in the multiprotocol attributes.
|
||||
for attribute in &update.path_attributes {
|
||||
match attribute {
|
||||
PathAttribute::MpReachNlri(reach) => {
|
||||
for nlri in &reach.prefixes {
|
||||
held.insert((nlri.prefix.to_string(), nlri.path_id), path.clone());
|
||||
}
|
||||
}
|
||||
PathAttribute::MpUnreachNlri(unreach) => {
|
||||
for nlri in &unreach.prefixes {
|
||||
held.remove(&(nlri.prefix.to_string(), nlri.path_id));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The routes held for 203.0.113.0/24, by path identifier.
|
||||
fn paths(held: &Held) -> Vec<(u32, Vec<u32>)> {
|
||||
held.iter()
|
||||
.map(|((prefix, id), path)| {
|
||||
assert_eq!(prefix, "203.0.113.0/24");
|
||||
(*id, path.clone())
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// A collector that agreed to be sent several routes for a prefix, and
|
||||
/// whose export policy says to send it every route, is told of every
|
||||
/// peer's route for a prefix, each with a path identifier of its own that
|
||||
/// stays with the route; a route that goes is withdrawn by its identifier.
|
||||
#[tokio::test]
|
||||
async fn test_every_route_goes_to_a_collector_told_apart_by_path_identifiers() -> Result<()> {
|
||||
let server = TestServer::start_with(LOCAL_ASN, |c| {
|
||||
// The collector is the configured peer at the IPv4 loopback
|
||||
// address; the feeders come over IPv6 loopback as peers of no
|
||||
// account.
|
||||
c.listen_endpoints
|
||||
.push(SocketAddr::new(Ipv6Addr::LOCALHOST.into(), 0));
|
||||
c.peers = vec![PeerConfig {
|
||||
add_path: Some(SendReceive::Send),
|
||||
import: ImportPolicy::permissive(),
|
||||
export: Arc::new(ExportPolicy {
|
||||
learned_routes: true,
|
||||
all_paths: true,
|
||||
..ExportPolicy::default()
|
||||
}),
|
||||
..peer_config(
|
||||
"collector",
|
||||
SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 1),
|
||||
COLLECTOR_ASN,
|
||||
)
|
||||
}];
|
||||
c.connect_retry = Duration::from_secs(3600);
|
||||
})
|
||||
.await?;
|
||||
let route = |path: &[u32]| Announcement::of([prefix("203.0.113.0/24").into()], path);
|
||||
let feeder = async |n: u32| -> Result<TestPeer> {
|
||||
let mut peer = TestPeer::connect(server.addrs[1]).await?;
|
||||
peer.handshake(PEER_ASN + n, Ipv4Addr::new(10, 0, 0, n as u8 + 1), 90)
|
||||
.await?;
|
||||
Ok(peer)
|
||||
};
|
||||
let mut a = feeder(0).await?;
|
||||
let mut b = feeder(1).await?;
|
||||
server.wait_for_sessions(2).await?;
|
||||
a.announce(route(&[PEER_ASN, 64510])).await?;
|
||||
b.announce(route(&[PEER_ASN + 1, 64510])).await?;
|
||||
server.wait_for_routes(2).await?;
|
||||
|
||||
let mut collector = TestPeer::connect(server.addr).await?;
|
||||
collector
|
||||
.handshake_with(
|
||||
COLLECTOR_ASN,
|
||||
Ipv4Addr::new(10, 0, 0, 100),
|
||||
90,
|
||||
collector_capabilities(COLLECTOR_ASN, &SUPPORTED, false),
|
||||
)
|
||||
.await?;
|
||||
collector.ctx.add_path = SUPPORTED.to_vec();
|
||||
|
||||
// Both routes, told apart.
|
||||
let mut held = Held::new();
|
||||
told(&mut collector, &mut held, |held| held.len() == 2).await?;
|
||||
assert_eq!(
|
||||
paths(&held),
|
||||
[
|
||||
(1, vec![LOCAL_ASN, PEER_ASN, 64510]),
|
||||
(2, vec![LOCAL_ASN, PEER_ASN + 1, 64510]),
|
||||
]
|
||||
);
|
||||
|
||||
// The second peer's route goes: withdrawn by its identifier, the
|
||||
// first's untouched.
|
||||
b.announce(withdrawal([prefix("203.0.113.0/24").into()]))
|
||||
.await?;
|
||||
told(&mut collector, &mut held, |held| held.len() == 1).await?;
|
||||
assert_eq!(paths(&held), [(1, vec![LOCAL_ASN, PEER_ASN, 64510])]);
|
||||
|
||||
// The first peer's route changes: it goes out again with the same
|
||||
// identifier. A third peer's route gets the lowest free one.
|
||||
a.announce(route(&[PEER_ASN, 64511, 64510])).await?;
|
||||
let mut c = feeder(2).await?;
|
||||
server.wait_for_sessions(4).await?;
|
||||
c.announce(route(&[PEER_ASN + 2, 64510])).await?;
|
||||
let changed = |held: &Held| {
|
||||
held.len() == 2
|
||||
&& held.get(&("203.0.113.0/24".to_string(), 1))
|
||||
== Some(&vec![LOCAL_ASN, PEER_ASN, 64511, 64510])
|
||||
};
|
||||
told(&mut collector, &mut held, changed).await?;
|
||||
assert_eq!(
|
||||
paths(&held),
|
||||
[
|
||||
(1, vec![LOCAL_ASN, PEER_ASN, 64511, 64510]),
|
||||
(2, vec![LOCAL_ASN, PEER_ASN + 2, 64510]),
|
||||
]
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A collector that is connected before the table arrives is told the
|
||||
/// whole table, however large: the walk of the table that a new follower
|
||||
/// starts with takes many batches with a table of some size, and goes on
|
||||
/// while the feeders keep sending.
|
||||
#[tokio::test]
|
||||
async fn test_a_large_table_reaches_a_collector_that_was_there_first() -> Result<()> {
|
||||
const PREFIXES: u32 = 40_000;
|
||||
let server = TestServer::start_with(LOCAL_ASN, |c| {
|
||||
c.listen_endpoints
|
||||
.push(SocketAddr::new(Ipv6Addr::LOCALHOST.into(), 0));
|
||||
c.peers = vec![PeerConfig {
|
||||
add_path: Some(SendReceive::Send),
|
||||
import: ImportPolicy::permissive(),
|
||||
export: Arc::new(ExportPolicy {
|
||||
learned_routes: true,
|
||||
all_paths: true,
|
||||
..ExportPolicy::default()
|
||||
}),
|
||||
..peer_config(
|
||||
"collector",
|
||||
SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 1),
|
||||
COLLECTOR_ASN,
|
||||
)
|
||||
}];
|
||||
c.connect_retry = Duration::from_secs(3600);
|
||||
})
|
||||
.await?;
|
||||
let mut collector = TestPeer::connect(server.addr).await?;
|
||||
collector
|
||||
.handshake_with(
|
||||
COLLECTOR_ASN,
|
||||
Ipv4Addr::new(10, 0, 0, 100),
|
||||
90,
|
||||
collector_capabilities(COLLECTOR_ASN, &SUPPORTED, false),
|
||||
)
|
||||
.await?;
|
||||
collector.ctx.add_path = SUPPORTED.to_vec();
|
||||
server.wait_for_sessions(1).await?;
|
||||
|
||||
// Two feeders, each sending the same prefixes a hundred to an UPDATE.
|
||||
let nth = |n: u32| {
|
||||
let [_, b, c, _] = (0x0a00_0000u32 + n * 256).to_be_bytes();
|
||||
prefix(&format!("10.{b}.{c}.0/24"))
|
||||
};
|
||||
let mut feeders = Vec::new();
|
||||
for n in 0..2u32 {
|
||||
let mut peer = TestPeer::connect(server.addrs[1]).await?;
|
||||
peer.handshake(PEER_ASN + n, Ipv4Addr::new(10, 0, 0, n as u8 + 1), 90)
|
||||
.await?;
|
||||
feeders.push(peer);
|
||||
}
|
||||
server.wait_for_sessions(3).await?;
|
||||
for start in (0..PREFIXES).step_by(100) {
|
||||
for (n, feeder) in feeders.iter_mut().enumerate() {
|
||||
feeder
|
||||
.announce(Announcement::of(
|
||||
(start..start + 100).map(|i| nth(i).into()),
|
||||
&[PEER_ASN + n as u32, 64510],
|
||||
))
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
let mut held = Held::new();
|
||||
told(&mut collector, &mut held, |held| {
|
||||
held.len() == 2 * PREFIXES as usize
|
||||
})
|
||||
.await?;
|
||||
let ids: std::collections::BTreeSet<u32> = held.keys().map(|(_, id)| *id).collect();
|
||||
assert_eq!(ids.into_iter().collect::<Vec<_>>(), [1, 2]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// As a router at an exchange feeds a collector: one IPv6 session that
|
||||
/// carries both families, with ADD-PATH for both and IPv4 routes with
|
||||
/// IPv6 next hops (RFC 8950), and a table of some size from two feeders.
|
||||
#[tokio::test]
|
||||
async fn test_both_families_reach_a_collector_over_one_ipv6_session() -> Result<()> {
|
||||
const V4: u32 = 20_000;
|
||||
const V6: u32 = 1_000;
|
||||
let server = TestServer::start_with(LOCAL_ASN, |c| {
|
||||
c.listen_endpoints
|
||||
.push(SocketAddr::new(Ipv6Addr::LOCALHOST.into(), 0));
|
||||
c.peers = vec![PeerConfig {
|
||||
add_path: Some(SendReceive::Send),
|
||||
import: ImportPolicy::permissive(),
|
||||
export: Arc::new(ExportPolicy {
|
||||
learned_routes: true,
|
||||
all_paths: true,
|
||||
..ExportPolicy::default()
|
||||
}),
|
||||
..peer_config(
|
||||
"collector",
|
||||
SocketAddr::new(Ipv6Addr::LOCALHOST.into(), 1),
|
||||
COLLECTOR_ASN,
|
||||
)
|
||||
}];
|
||||
c.connect_retry = Duration::from_secs(3600);
|
||||
})
|
||||
.await?;
|
||||
let mut collector = TestPeer::connect(server.addrs[1]).await?;
|
||||
collector
|
||||
.handshake_with(
|
||||
COLLECTOR_ASN,
|
||||
Ipv4Addr::new(10, 0, 0, 100),
|
||||
90,
|
||||
collector_capabilities(COLLECTOR_ASN, &SUPPORTED, true),
|
||||
)
|
||||
.await?;
|
||||
collector.ctx.add_path = SUPPORTED.to_vec();
|
||||
server.wait_for_sessions(1).await?;
|
||||
let session = &server.registry.list()[0];
|
||||
assert!(session.extended_next_hop, "RFC 8950 agreed");
|
||||
assert_eq!(session.add_path_send.iter().count(), 2);
|
||||
|
||||
let v4 = |n: u32| {
|
||||
let [_, b, c, _] = (0x0a00_0000u32 + n * 256).to_be_bytes();
|
||||
prefix(&format!("10.{b}.{c}.0/24"))
|
||||
};
|
||||
let v6 = |n: u32| prefix(&format!("2001:db8:{:x}::/48", 0x1000 + n));
|
||||
let mut feeders = Vec::new();
|
||||
for n in 0..2u32 {
|
||||
let mut peer = TestPeer::connect(server.addr).await?;
|
||||
peer.handshake(PEER_ASN + n, Ipv4Addr::new(10, 0, 0, n as u8 + 1), 90)
|
||||
.await?;
|
||||
feeders.push(peer);
|
||||
}
|
||||
server.wait_for_sessions(3).await?;
|
||||
for start in (0..V4).step_by(100) {
|
||||
for (n, feeder) in feeders.iter_mut().enumerate() {
|
||||
feeder
|
||||
.announce(Announcement::of(
|
||||
(start..start + 100).map(|i| v4(i).into()),
|
||||
&[PEER_ASN + n as u32, 64510],
|
||||
))
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
for start in (0..V6).step_by(100) {
|
||||
for (n, feeder) in feeders.iter_mut().enumerate() {
|
||||
let update = UpdateMessage {
|
||||
withdrawn_routes: vec![],
|
||||
path_attributes: vec![
|
||||
PathAttribute::Origin(OriginPathAttribute::IGP),
|
||||
AsPathAttribute::from_asns(vec![PEER_ASN + n as u32, 64510]),
|
||||
PathAttribute::MpReachNlri(MpReachNlriPathAttribute {
|
||||
family: AfiSafi::IPV6_UNICAST,
|
||||
next_hop: NlriNextHop::Ipv6("2001:db8::9".parse().unwrap()),
|
||||
prefixes: (start..start + 100).map(|i| v6(i).into()).collect(),
|
||||
}),
|
||||
],
|
||||
announced_routes: vec![],
|
||||
};
|
||||
feeder.send(&Message::Update(update)).await?;
|
||||
}
|
||||
}
|
||||
let mut held = Held::new();
|
||||
told(&mut collector, &mut held, |held| {
|
||||
held.len() == 2 * (V4 + V6) as usize
|
||||
})
|
||||
.await?;
|
||||
let v6_routes = held
|
||||
.keys()
|
||||
.filter(|(prefix, _)| prefix.contains(':'))
|
||||
.count();
|
||||
assert_eq!(v6_routes, 2 * V6 as usize);
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
//! Who may connect: only the addresses of configured peers, and each of
|
||||
//! them only so many times at once.
|
||||
|
||||
use std::net::{Ipv4Addr, SocketAddr};
|
||||
use std::time::Duration;
|
||||
|
||||
use bgp_integration_tests::test_peer::TestPeer;
|
||||
use bgp_integration_tests::test_server::{ServerWithPeer, TestServer};
|
||||
use bgp_integration_tests::{TIMEOUT, peer_config};
|
||||
use bgp_server::config::PeerConfig;
|
||||
use bgp_server::policy::ImportPolicy;
|
||||
use eyre::Result;
|
||||
|
||||
const LOCAL_ASN: u32 = 65000;
|
||||
const PEER_ASN: u32 = 65001;
|
||||
|
||||
/// A host that is not a configured peer gets nothing: no OPEN to answer,
|
||||
/// no session, no routes taken from it. Any host used to be able to open a
|
||||
/// session as any AS it named.
|
||||
#[tokio::test]
|
||||
async fn test_a_host_that_is_not_a_configured_peer_is_refused() -> Result<()> {
|
||||
// The only peer configured is somewhere else (documentation space).
|
||||
let server = TestServer::start_with(LOCAL_ASN, |c| {
|
||||
c.accept_unconfigured_peers = false;
|
||||
let elsewhere: SocketAddr = "192.0.2.9:179".parse().unwrap();
|
||||
c.peers = vec![PeerConfig {
|
||||
import: ImportPolicy::permissive(),
|
||||
..peer_config("r1", elsewhere, PEER_ASN)
|
||||
}];
|
||||
})
|
||||
.await?;
|
||||
|
||||
let mut stranger = TestPeer::connect(server.addr).await?;
|
||||
assert!(stranger.closed_unanswered().await);
|
||||
assert!(server.registry.list().is_empty());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The address of a configured peer is let in, and held to what is
|
||||
/// configured for that peer.
|
||||
#[tokio::test]
|
||||
async fn test_a_configured_peer_is_let_in() -> Result<()> {
|
||||
// The server's own dialling goes to the listener; it is never answered.
|
||||
let ServerWithPeer {
|
||||
server,
|
||||
listener: _listener,
|
||||
} = TestServer::with_peer(LOCAL_ASN, PEER_ASN, |_, peer| {
|
||||
peer.import = ImportPolicy::permissive();
|
||||
})
|
||||
.await?;
|
||||
|
||||
let _peer = server
|
||||
.established(PEER_ASN, Ipv4Addr::new(10, 0, 0, 2))
|
||||
.await?;
|
||||
assert_eq!(server.registry.list()[0].name.as_deref(), Some("r1"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// One address may hold only a few connections at a time, so that one host
|
||||
/// cannot use up the server's sockets with connections that never get as
|
||||
/// far as a session.
|
||||
#[tokio::test]
|
||||
async fn test_one_address_may_open_only_a_few_connections() -> Result<()> {
|
||||
let server = TestServer::start(LOCAL_ASN).await?;
|
||||
|
||||
// Four that say nothing: each is sent the server's OPEN and waited on.
|
||||
let mut held = Vec::new();
|
||||
for _ in 0..4 {
|
||||
let mut peer = TestPeer::connect(server.addr).await?;
|
||||
peer.recv_open().await?;
|
||||
held.push(peer);
|
||||
}
|
||||
let mut one_too_many = TestPeer::connect(server.addr).await?;
|
||||
assert!(one_too_many.closed_unanswered().await);
|
||||
|
||||
// When one of them goes, there is room again.
|
||||
held.pop();
|
||||
tokio::time::timeout(TIMEOUT, async {
|
||||
loop {
|
||||
let mut peer = TestPeer::connect(server.addr).await?;
|
||||
if peer.recv_open().await.is_ok() {
|
||||
return Ok::<_, eyre::Report>(());
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(20)).await;
|
||||
}
|
||||
})
|
||||
.await??;
|
||||
Ok(())
|
||||
}
|
||||
@@ -4,21 +4,25 @@
|
||||
use std::net::Ipv4Addr;
|
||||
use std::time::Duration;
|
||||
|
||||
use bgp_integration_tests::announcement::announcement;
|
||||
use bgp_integration_tests::test_peer::TestPeer;
|
||||
use bgp_integration_tests::test_server::{RouteChange, TestServer};
|
||||
use bgp_integration_tests::{TIMEOUT, prefix};
|
||||
use bgp_packet::message::{
|
||||
AsPathAttribute, CommunitiesPathAttribute, LargeCommunitiesPathAttribute, LargeCommunity,
|
||||
LocalPrefPathAttribute, NextHopPathAttribute, OriginPathAttribute, PathAttribute,
|
||||
UpdateMessage,
|
||||
};
|
||||
use bgp_primitives::ip_prefix::IpPrefix;
|
||||
use bgp_rib::{LocalRoute, LocalRouteAttrs};
|
||||
use bgp_server::session::SessionEvent;
|
||||
use eyre::Result;
|
||||
|
||||
const LOCAL_ASN: u32 = 65000;
|
||||
const PEER_ASN: u32 = 65001;
|
||||
const TIMEOUT: Duration = Duration::from_secs(5);
|
||||
|
||||
fn prefix(s: &str) -> IpPrefix {
|
||||
IpPrefix::try_from(s).unwrap()
|
||||
}
|
||||
|
||||
fn route(p: &str, attrs: LocalRouteAttrs) -> LocalRoute {
|
||||
LocalRoute::new(prefix(p), attrs)
|
||||
@@ -28,12 +32,27 @@ async fn established_peer(server: &TestServer) -> Result<TestPeer> {
|
||||
established_peer_with_id(server, Ipv4Addr::new(10, 0, 0, 1)).await
|
||||
}
|
||||
|
||||
/// Distinct routers need distinct BGP Identifiers, or the second connection
|
||||
/// is (correctly) treated as a collision with the first.
|
||||
async fn established_peer_with_id(server: &TestServer, router_id: Ipv4Addr) -> Result<TestPeer> {
|
||||
server.established(PEER_ASN, router_id).await
|
||||
let mut peer = TestPeer::connect(server.addr).await?;
|
||||
peer.handshake(PEER_ASN, router_id, 90).await?;
|
||||
Ok(peer)
|
||||
}
|
||||
|
||||
async fn next_update(peer: &mut TestPeer) -> Result<UpdateMessage> {
|
||||
peer.recv_update().await
|
||||
tokio::time::timeout(TIMEOUT, peer.recv_update()).await?
|
||||
}
|
||||
|
||||
/// Waits until the registry reports `n` established sessions.
|
||||
async fn wait_for_sessions(server: &TestServer, n: usize) -> Result<()> {
|
||||
tokio::time::timeout(TIMEOUT, async {
|
||||
while server.registry.list().len() != n {
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -78,7 +97,7 @@ async fn test_registry_tracks_session_lifecycle() -> Result<()> {
|
||||
async fn test_announce_after_session_up() -> Result<()> {
|
||||
let server = TestServer::start(LOCAL_ASN).await?;
|
||||
let mut peer = established_peer(&server).await?;
|
||||
server.wait_for_sessions(1).await?;
|
||||
wait_for_sessions(&server, 1).await?;
|
||||
|
||||
server
|
||||
.local_rib
|
||||
@@ -155,7 +174,7 @@ async fn test_announce_reaches_all_sessions() -> Result<()> {
|
||||
let server = TestServer::start(LOCAL_ASN).await?;
|
||||
let mut a = established_peer_with_id(&server, Ipv4Addr::new(10, 0, 0, 1)).await?;
|
||||
let mut b = established_peer_with_id(&server, Ipv4Addr::new(10, 0, 0, 2)).await?;
|
||||
server.wait_for_sessions(2).await?;
|
||||
wait_for_sessions(&server, 2).await?;
|
||||
|
||||
server
|
||||
.local_rib
|
||||
@@ -173,7 +192,7 @@ async fn test_announce_reaches_all_sessions() -> Result<()> {
|
||||
async fn test_withdraw_sends_withdrawn_route() -> Result<()> {
|
||||
let server = TestServer::start(LOCAL_ASN).await?;
|
||||
let mut peer = established_peer(&server).await?;
|
||||
server.wait_for_sessions(1).await?;
|
||||
wait_for_sessions(&server, 1).await?;
|
||||
|
||||
server
|
||||
.local_rib
|
||||
@@ -194,7 +213,7 @@ async fn test_withdraw_sends_withdrawn_route() -> Result<()> {
|
||||
async fn test_reannounce_with_prepend_and_med() -> Result<()> {
|
||||
let server = TestServer::start(LOCAL_ASN).await?;
|
||||
let mut peer = established_peer(&server).await?;
|
||||
server.wait_for_sessions(1).await?;
|
||||
wait_for_sessions(&server, 1).await?;
|
||||
|
||||
server
|
||||
.local_rib
|
||||
@@ -239,7 +258,7 @@ async fn test_keepalives_continue_alongside_updates() -> Result<()> {
|
||||
let mut peer = TestPeer::connect(server.addr).await?;
|
||||
peer.handshake(PEER_ASN, Ipv4Addr::new(10, 0, 0, 1), 3)
|
||||
.await?;
|
||||
server.wait_for_sessions(1).await?;
|
||||
wait_for_sessions(&server, 1).await?;
|
||||
|
||||
server
|
||||
.local_rib
|
||||
@@ -270,12 +289,22 @@ async fn test_received_routes_removed_when_session_closes() -> Result<()> {
|
||||
let mut rib_events = server.route_changes();
|
||||
let mut session_events = server.registry.subscribe();
|
||||
let mut peer = established_peer(&server).await?;
|
||||
server.wait_for_sessions(1).await?;
|
||||
wait_for_sessions(&server, 1).await?;
|
||||
|
||||
peer.announce(
|
||||
announcement(&["203.0.113.0/24", "198.51.100.0/24"], &[PEER_ASN])
|
||||
.with_next_hop(Ipv4Addr::new(10, 0, 0, 1)),
|
||||
)
|
||||
peer.send(&bgp_packet::message::Message::Update(
|
||||
bgp_packet::message::UpdateMessage {
|
||||
withdrawn_routes: vec![],
|
||||
path_attributes: vec![
|
||||
PathAttribute::Origin(OriginPathAttribute::IGP),
|
||||
AsPathAttribute::from_asns(vec![PEER_ASN]),
|
||||
PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::new(10, 0, 0, 1))),
|
||||
],
|
||||
announced_routes: vec![
|
||||
prefix("203.0.113.0/24").into(),
|
||||
prefix("198.51.100.0/24").into(),
|
||||
],
|
||||
},
|
||||
))
|
||||
.await?;
|
||||
for _ in 0..2 {
|
||||
let ev = tokio::time::timeout(TIMEOUT, rib_events.recv()).await??;
|
||||
@@ -322,7 +351,7 @@ const ROUTE_REFRESH_IPV4: &[u8] = &[
|
||||
async fn test_route_refresh_resends_local_routes() -> Result<()> {
|
||||
let server = TestServer::start(LOCAL_ASN).await?;
|
||||
let mut peer = established_peer(&server).await?;
|
||||
server.wait_for_sessions(1).await?;
|
||||
wait_for_sessions(&server, 1).await?;
|
||||
|
||||
server
|
||||
.local_rib
|
||||
@@ -357,26 +386,33 @@ async fn test_route_refresh_resends_local_routes() -> Result<()> {
|
||||
#[tokio::test]
|
||||
async fn test_shutdown_sends_cease_to_every_peer() -> Result<()> {
|
||||
use bgp_packet::errors::{BgpError, CeaseSubcode};
|
||||
use bgp_packet::message::Message;
|
||||
|
||||
let server = TestServer::start(LOCAL_ASN).await?;
|
||||
let mut a = established_peer_with_id(&server, Ipv4Addr::new(10, 0, 0, 1)).await?;
|
||||
let mut b = established_peer_with_id(&server, Ipv4Addr::new(10, 0, 0, 2)).await?;
|
||||
server.wait_for_sessions(2).await?;
|
||||
wait_for_sessions(&server, 2).await?;
|
||||
|
||||
let left = server
|
||||
.registry
|
||||
.leave(bgp_server::restart::Leave::Stop, Duration::from_secs(2))
|
||||
.await;
|
||||
let left = server.registry.shutdown(Duration::from_secs(2)).await;
|
||||
assert_eq!(left, 0, "sessions still open after shutdown");
|
||||
assert!(server.registry.is_shutting_down());
|
||||
|
||||
for peer in [&mut a, &mut b] {
|
||||
let notif = peer.expect_notification().await?;
|
||||
let notif = tokio::time::timeout(TIMEOUT, async {
|
||||
loop {
|
||||
match peer.recv().await? {
|
||||
Message::Notification(n) => return Ok::<_, eyre::Error>(n),
|
||||
Message::KeepAlive => {}
|
||||
other => eyre::bail!("unexpected {other:?}"),
|
||||
}
|
||||
}
|
||||
})
|
||||
.await??;
|
||||
assert!(
|
||||
matches!(notif.error, BgpError::Cease(CeaseSubcode::AdminShutdown)),
|
||||
"{notif:?}"
|
||||
);
|
||||
assert!(peer.closed_within(TIMEOUT).await);
|
||||
assert!(peer.is_closed().await);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -431,7 +467,7 @@ async fn test_a_peer_in_our_own_as_gets_an_empty_path_and_a_local_pref() -> Resu
|
||||
// The peer reads as a router in the server's AS does: one outside it
|
||||
// would drop the LOCAL_PREF.
|
||||
peer.ctx.ibgp = true;
|
||||
server.wait_for_sessions(1).await?;
|
||||
wait_for_sessions(&server, 1).await?;
|
||||
assert!(server.registry.list()[0].ibgp);
|
||||
|
||||
server
|
||||
|
||||
@@ -1,275 +0,0 @@
|
||||
//! AS path verification with ASPAs, end to end: a server that gets ASPAs
|
||||
//! from a cache, a peer that announces routes, and the ASPAs changing
|
||||
//! under both.
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
use std::net::Ipv4Addr;
|
||||
use std::time::Duration;
|
||||
|
||||
use bgp_integration_tests::announcement::announcement;
|
||||
use bgp_integration_tests::test_cache::TestCache;
|
||||
use bgp_integration_tests::test_peer::TestPeer;
|
||||
use bgp_integration_tests::test_server::{ServerWithPeer, TestServer};
|
||||
use bgp_integration_tests::{default_policy, prefix, wait_until};
|
||||
use bgp_packet::message::PathAttribute;
|
||||
use bgp_rpki::{CacheState, RpkiConfig};
|
||||
use bgp_server::policy::{ImportPolicy, PeerRole};
|
||||
use eyre::Result;
|
||||
use serde_json::Value;
|
||||
|
||||
const LOCAL_ASN: u32 = 1299;
|
||||
const PEER_ASN: u32 = 3333;
|
||||
const PEER_ID: Ipv4Addr = Ipv4Addr::new(10, 0, 0, 2);
|
||||
|
||||
/// A server that validates against `cache`, with one configured peer at
|
||||
/// the address the test connects from, which is `role` to it. Returns once
|
||||
/// the server has the cache's data.
|
||||
async fn server_with(
|
||||
cache: &TestCache,
|
||||
role: Option<PeerRole>,
|
||||
import: ImportPolicy,
|
||||
) -> Result<ServerWithPeer> {
|
||||
let cache = cache.address();
|
||||
let setup = TestServer::with_peer(LOCAL_ASN, PEER_ASN, |c, peer| {
|
||||
c.rpki = Some(RpkiConfig {
|
||||
caches: vec![cache],
|
||||
retry: Some(Duration::from_millis(100)),
|
||||
..RpkiConfig::default()
|
||||
});
|
||||
peer.import = import;
|
||||
peer.role = role;
|
||||
})
|
||||
.await?;
|
||||
let rpki = setup.server.rpki.clone().expect("validation is configured");
|
||||
wait_until("the server has the cache's data", || {
|
||||
rpki.status().state == CacheState::Synchronised
|
||||
})
|
||||
.await?;
|
||||
Ok(setup)
|
||||
}
|
||||
|
||||
async fn established(server: &TestServer) -> Result<TestPeer> {
|
||||
server.established(PEER_ASN, PEER_ID).await
|
||||
}
|
||||
|
||||
/// Announces `prefixes` with the AS path `path`.
|
||||
async fn announce(peer: &mut TestPeer, prefixes: &[&str], path: &[u32]) -> Result<()> {
|
||||
peer.announce(announcement(prefixes, path)).await
|
||||
}
|
||||
|
||||
/// AS 15169 buys from 3356 and says so; 3356 and the peer, 3333, have no
|
||||
/// providers.
|
||||
fn aspas() -> Vec<(u32, Vec<u32>)> {
|
||||
vec![(15169, vec![3356]), (3356, vec![0]), (PEER_ASN, vec![0])]
|
||||
}
|
||||
|
||||
/// A peer that hands us a route it got from one of its own peers is
|
||||
/// leaking it. The route is refused; routes whose paths are attested, or
|
||||
/// that nothing is known about, are taken.
|
||||
#[tokio::test]
|
||||
async fn test_a_leaked_route_is_refused_and_the_rest_are_taken() -> Result<()> {
|
||||
let cache = TestCache::start_with(vec![], aspas()).await;
|
||||
let ServerWithPeer { server, .. } =
|
||||
server_with(&cache, Some(PeerRole::Peer), default_policy()).await?;
|
||||
let (base, _http) = server.serve_http().await?;
|
||||
let mut peer = established(&server).await?;
|
||||
|
||||
// The peer's own route; one of a customer of its that has no ASPA;
|
||||
// and 15169's route by way of 3356, which is not for 3333 to pass on
|
||||
// to a peer: 3356 is not its customer.
|
||||
announce(&mut peer, &["193.0.0.0/21"], &[PEER_ASN]).await?;
|
||||
announce(&mut peer, &["194.0.0.0/24"], &[PEER_ASN, 2121]).await?;
|
||||
announce(&mut peer, &["8.8.8.0/24"], &[PEER_ASN, 3356, 15169]).await?;
|
||||
server
|
||||
.table_becomes(&["193.0.0.0/21", "194.0.0.0/24"], 1)
|
||||
.await?;
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let get = |path: &str| {
|
||||
let request = client.get(format!("{base}{path}"));
|
||||
async move { Ok::<Value, eyre::Report>(request.send().await?.error_for_status()?.json().await?) }
|
||||
};
|
||||
let routes = get("/api/routes/received").await?["routes"].take();
|
||||
let validity: Vec<(&str, &str)> = routes
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|r| (r["prefix"].as_str().unwrap(), r["aspa"].as_str().unwrap()))
|
||||
.collect();
|
||||
assert_eq!(
|
||||
validity,
|
||||
[("193.0.0.0/21", "valid"), ("194.0.0.0/24", "unknown")]
|
||||
);
|
||||
let refused = get("/api/routes/rejected").await?;
|
||||
assert_eq!(
|
||||
refused[0]["by_reason"],
|
||||
serde_json::json!({"ASPA invalid": 1})
|
||||
);
|
||||
let peers = get("/api/peers").await?;
|
||||
assert_eq!(peers[0]["role"], "peer");
|
||||
assert_eq!(peers[0]["held_routes"], 1);
|
||||
let rpki = get("/api/rpki").await?;
|
||||
assert_eq!(rpki["aspas"], 3);
|
||||
assert_eq!(rpki["protocol_version"], 2);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// What a path may look like depends on who it comes from. The same route
|
||||
/// that a peer may not pass on is in order from a provider, which may hand
|
||||
/// us anything it has. Where it is not said what the peer is, paths are
|
||||
/// not verified.
|
||||
#[tokio::test]
|
||||
async fn test_a_route_that_has_crossed_a_peering_is_taken_from_a_provider_only() -> Result<()> {
|
||||
for (role, taken) in [
|
||||
(Some(PeerRole::Provider), true),
|
||||
(Some(PeerRole::Customer), false),
|
||||
(Some(PeerRole::Peer), false),
|
||||
(Some(PeerRole::RouteServerClient), false),
|
||||
(None, true),
|
||||
] {
|
||||
let cache = TestCache::start_with(vec![], aspas()).await;
|
||||
let ServerWithPeer { server, .. } = server_with(&cache, role, default_policy()).await?;
|
||||
let mut peer = established(&server).await?;
|
||||
announce(&mut peer, &["8.8.8.0/24"], &[PEER_ASN, 3356, 15169]).await?;
|
||||
let expected = if taken {
|
||||
server.table_becomes(&["8.8.8.0/24"], 0).await
|
||||
} else {
|
||||
server.table_becomes(&[], 1).await
|
||||
};
|
||||
expected.map_err(|e| eyre::eyre!("from a peer that is {role:?}: {e}"))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// An Internet exchange's route server does not put its AS on the path, so
|
||||
/// the path need not begin with the route server's AS. A leak is refused
|
||||
/// across it all the same.
|
||||
#[tokio::test]
|
||||
async fn test_paths_from_a_route_server_do_not_begin_with_its_as() -> Result<()> {
|
||||
let cache = TestCache::start_with(vec![], aspas()).await;
|
||||
let import = ImportPolicy {
|
||||
accept_any_first_as: true,
|
||||
..default_policy()
|
||||
};
|
||||
let ServerWithPeer { server, .. } =
|
||||
server_with(&cache, Some(PeerRole::RouteServer), import).await?;
|
||||
let mut peer = established(&server).await?;
|
||||
|
||||
// 3356's customer route, as 3356 gave it to the route server.
|
||||
announce(&mut peer, &["8.8.8.0/24"], &[3356, 15169]).await?;
|
||||
// The same route leaked to the route server by another member.
|
||||
announce(&mut peer, &["8.8.4.0/24"], &[2121, 3356, 15169]).await?;
|
||||
server.table_becomes(&["8.8.8.0/24"], 1).await
|
||||
}
|
||||
|
||||
/// Validity changes with the ASPAs, and the routes follow: one that was
|
||||
/// taken goes when its path becomes invalid, and one that was refused is
|
||||
/// taken when it no longer is. The peer sends nothing more meanwhile.
|
||||
#[tokio::test]
|
||||
async fn test_routes_follow_the_aspas() -> Result<()> {
|
||||
let cache = TestCache::start_with(vec![], aspas()).await;
|
||||
let ServerWithPeer { server, .. } =
|
||||
server_with(&cache, Some(PeerRole::Peer), default_policy()).await?;
|
||||
let mut peer = established(&server).await?;
|
||||
|
||||
announce(&mut peer, &["194.0.0.0/24"], &[PEER_ASN, 2121]).await?;
|
||||
announce(&mut peer, &["8.8.8.0/24"], &[PEER_ASN, 3356, 15169]).await?;
|
||||
server.table_becomes(&["194.0.0.0/24"], 1).await?;
|
||||
|
||||
// 2121 says that its only provider is 1101: its route by way of 3333
|
||||
// is now invalid, and goes.
|
||||
let mut changed = aspas();
|
||||
changed.push((2121, vec![1101]));
|
||||
cache.set_aspas(changed);
|
||||
server.table_becomes(&[], 2).await?;
|
||||
let refused = &server.rejections.peers()[0].1;
|
||||
assert_eq!(refused.total(), 2);
|
||||
assert_eq!(refused.recent()[0].prefix, prefix("194.0.0.0/24"));
|
||||
assert_eq!(&refused.recent()[0].as_path[..], [PEER_ASN, 2121]);
|
||||
|
||||
// 3356 becomes a customer of 3333, and 2121 too: both routes come
|
||||
// back, with the paths they were announced with.
|
||||
cache.set_aspas(vec![
|
||||
(15169, vec![3356]),
|
||||
(3356, vec![PEER_ASN]),
|
||||
(PEER_ASN, vec![0]),
|
||||
(2121, vec![1101, PEER_ASN]),
|
||||
]);
|
||||
server
|
||||
.table_becomes(&["194.0.0.0/24", "8.8.8.0/24"], 0)
|
||||
.await?;
|
||||
let paths: BTreeSet<Vec<u32>> = server
|
||||
.received_routes()
|
||||
.iter()
|
||||
.flat_map(|route| &route.attributes)
|
||||
.filter_map(|attribute| match attribute {
|
||||
PathAttribute::AsPath(path) => Some(path.segments[0].path.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(
|
||||
paths,
|
||||
[vec![PEER_ASN, 2121], vec![PEER_ASN, 3356, 15169]].into()
|
||||
);
|
||||
|
||||
// The validator loses its ASPAs altogether: nothing is known about
|
||||
// any path, and nothing is refused.
|
||||
cache.set_aspas(vec![
|
||||
(15169, vec![3356]),
|
||||
(3356, vec![0]),
|
||||
(PEER_ASN, vec![0]),
|
||||
]);
|
||||
server.table_becomes(&["194.0.0.0/24"], 1).await?;
|
||||
cache.set_aspas(vec![]);
|
||||
server
|
||||
.table_becomes(&["194.0.0.0/24", "8.8.8.0/24"], 0)
|
||||
.await
|
||||
}
|
||||
|
||||
/// A peer can be exempt: its routes with invalid paths are taken, and
|
||||
/// shown for what they are.
|
||||
#[tokio::test]
|
||||
async fn test_a_peer_can_be_exempt_from_path_verification() -> Result<()> {
|
||||
let cache = TestCache::start_with(vec![], aspas()).await;
|
||||
let import = ImportPolicy {
|
||||
accept_aspa_invalid: true,
|
||||
..default_policy()
|
||||
};
|
||||
let ServerWithPeer { server, .. } = server_with(&cache, Some(PeerRole::Peer), import).await?;
|
||||
let (base, _http) = server.serve_http().await?;
|
||||
let mut peer = established(&server).await?;
|
||||
|
||||
announce(&mut peer, &["8.8.8.0/24"], &[PEER_ASN, 3356, 15169]).await?;
|
||||
server.table_becomes(&["8.8.8.0/24"], 0).await?;
|
||||
let routes: Value = reqwest::get(format!("{base}/api/routes/received"))
|
||||
.await?
|
||||
.json()
|
||||
.await?;
|
||||
assert_eq!(routes["routes"][0]["aspa"], "invalid");
|
||||
assert!(server.rejections.peers().is_empty());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A route can fail both checks. It is taken only when it passes both.
|
||||
#[tokio::test]
|
||||
async fn test_a_route_is_held_until_origin_and_path_are_both_in_order() -> Result<()> {
|
||||
let google = |asn| bgp_rpki::Vrp {
|
||||
prefix: prefix("8.8.8.0/24"),
|
||||
max_length: 24,
|
||||
asn,
|
||||
};
|
||||
let cache = TestCache::start_with(vec![google(13335)], aspas()).await;
|
||||
let ServerWithPeer { server, .. } =
|
||||
server_with(&cache, Some(PeerRole::Peer), default_policy()).await?;
|
||||
let mut peer = established(&server).await?;
|
||||
announce(&mut peer, &["8.8.8.0/24"], &[PEER_ASN, 3356, 15169]).await?;
|
||||
server.table_becomes(&[], 1).await?;
|
||||
|
||||
// The origin is put right; the path is still a leak.
|
||||
cache.set(vec![google(15169)]);
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
server.table_becomes(&[], 1).await?;
|
||||
|
||||
cache.set_aspas(vec![(15169, vec![3356]), (3356, vec![PEER_ASN])]);
|
||||
server.table_becomes(&["8.8.8.0/24"], 0).await
|
||||
}
|
||||
@@ -4,7 +4,6 @@ use std::time::Duration;
|
||||
use bgp_integration_tests::test_peer::TestPeer;
|
||||
use bgp_integration_tests::test_server::RouteChange;
|
||||
use bgp_integration_tests::test_server::TestServer;
|
||||
use bgp_integration_tests::{TIMEOUT, wait_until};
|
||||
use bgp_packet::errors::{BgpError, CeaseSubcode, FsmSubcode, MsgHeaderSubcode, UpdateMsgSubcode};
|
||||
use bgp_packet::message::{
|
||||
AsPathAttribute, Message, MpReachNlriPathAttribute, MpUnreachNlriPathAttribute,
|
||||
@@ -15,6 +14,8 @@ use bgp_primitives::constants::{AfiSafi, IpVersion};
|
||||
use bgp_primitives::ip_prefix::IpPrefix;
|
||||
use eyre::Result;
|
||||
|
||||
const TIMEOUT: Duration = Duration::from_secs(5);
|
||||
|
||||
/// The attributes every announcement of IPv4 routes must carry. An UPDATE
|
||||
/// without them has its routes withdrawn instead (RFC 7606).
|
||||
fn mandatory_attributes() -> Vec<PathAttribute> {
|
||||
@@ -97,6 +98,20 @@ async fn test_bad_marker_does_not_crash_server() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Reads until a NOTIFICATION arrives, skipping KEEPALIVEs.
|
||||
async fn expect_notification(peer: &mut TestPeer) -> Result<NotificationMessage> {
|
||||
tokio::time::timeout(TIMEOUT, async {
|
||||
loop {
|
||||
match peer.recv().await? {
|
||||
Message::Notification(n) => return Ok(n),
|
||||
Message::KeepAlive => {}
|
||||
other => eyre::bail!("expected NOTIFICATION, got {other:?}"),
|
||||
}
|
||||
}
|
||||
})
|
||||
.await?
|
||||
}
|
||||
|
||||
/// A header with the given length and type, followed by `body`.
|
||||
fn framed(length: u16, msg_type: u8, body: &[u8]) -> Vec<u8> {
|
||||
let mut buf = vec![0xFF; 16];
|
||||
@@ -155,7 +170,7 @@ async fn test_malformed_messages_are_answered_with_the_matching_notification() -
|
||||
peer.handshake(65001, Ipv4Addr::new(10, 0, 1, i as u8 + 1), 90)
|
||||
.await?;
|
||||
peer.send_raw(&bytes).await?;
|
||||
let notification = peer.expect_notification().await?;
|
||||
let notification = expect_notification(&mut peer).await?;
|
||||
assert_eq!(
|
||||
(notification.error, notification.data),
|
||||
(error.clone(), data.clone()),
|
||||
@@ -166,7 +181,7 @@ async fn test_malformed_messages_are_answered_with_the_matching_notification() -
|
||||
let mut peer = TestPeer::connect(server.addr).await?;
|
||||
let _ = peer.recv_open().await?;
|
||||
peer.send_raw(&bytes).await?;
|
||||
let notification = peer.expect_notification().await?;
|
||||
let notification = expect_notification(&mut peer).await?;
|
||||
assert_eq!(
|
||||
(notification.error, notification.data),
|
||||
(error, data),
|
||||
@@ -179,7 +194,7 @@ async fn test_malformed_messages_are_answered_with_the_matching_notification() -
|
||||
let mut peer = TestPeer::connect(server.addr).await?;
|
||||
let _ = peer.recv_open().await?;
|
||||
peer.send(&Message::KeepAlive).await?;
|
||||
let notification = peer.expect_notification().await?;
|
||||
let notification = expect_notification(&mut peer).await?;
|
||||
assert_eq!(
|
||||
notification.error,
|
||||
BgpError::FsmError(FsmSubcode::UnexpectedOpenSent)
|
||||
@@ -213,10 +228,13 @@ async fn test_message_split_across_a_keepalive_tick_is_still_one_message() -> Re
|
||||
tokio::time::sleep(Duration::from_millis(1300)).await;
|
||||
peer.send_raw(&update[10..]).await?;
|
||||
|
||||
wait_until("the split UPDATE reaches the RIB", || {
|
||||
server.received_routes().iter().any(|e| e.prefix == prefix)
|
||||
tokio::time::timeout(TIMEOUT, async {
|
||||
while !server.received_routes().iter().any(|e| e.prefix == prefix) {
|
||||
tokio::time::sleep(Duration::from_millis(20)).await;
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
.await
|
||||
.map_err(|_| eyre::eyre!("the split UPDATE never reached the RIB"))?;
|
||||
assert_eq!(server.registry.list().len(), 1, "session was dropped");
|
||||
Ok(())
|
||||
}
|
||||
@@ -248,7 +266,7 @@ async fn test_server_sends_keepalives() -> Result<()> {
|
||||
peer.handshake(65001, Ipv4Addr::new(10, 0, 0, 1), 3).await?;
|
||||
|
||||
// We should receive a KEEPALIVE within ~1 second (hold/3).
|
||||
let msg = peer.recv_within(Duration::from_secs(3)).await?;
|
||||
let msg = tokio::time::timeout(Duration::from_secs(3), peer.recv()).await??;
|
||||
assert!(
|
||||
matches!(msg, Message::KeepAlive),
|
||||
"Expected KEEPALIVE, got {:?}",
|
||||
@@ -268,7 +286,16 @@ async fn test_hold_timer_expires() -> Result<()> {
|
||||
// Don't send any keepalives. The server will send keepalives until its own
|
||||
// hold timer fires and it sends a NOTIFICATION. Drain messages until we
|
||||
// see it (or the outer timeout fires).
|
||||
let notif = tokio::time::timeout(Duration::from_secs(6), peer.expect_notification()).await??;
|
||||
let notif = tokio::time::timeout(Duration::from_secs(6), async {
|
||||
loop {
|
||||
match peer.recv().await? {
|
||||
Message::Notification(n) => return Ok::<_, eyre::Error>(n),
|
||||
Message::KeepAlive => {} // server keepalive — keep waiting
|
||||
other => eyre::bail!("Unexpected message: {:?}", other),
|
||||
}
|
||||
}
|
||||
})
|
||||
.await??;
|
||||
assert!(
|
||||
matches!(notif.error, BgpError::HoldTimer),
|
||||
"Expected HoldTimer NOTIFICATION, got {:?}",
|
||||
@@ -292,7 +319,8 @@ async fn test_peer_sends_notification() -> Result<()> {
|
||||
.await?;
|
||||
|
||||
// Server should close the connection cleanly.
|
||||
assert!(peer.closed_within(TIMEOUT).await);
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
assert!(peer.is_closed().await);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -2,34 +2,66 @@
|
||||
//! peers, which of them the server chooses, and how that is shown.
|
||||
|
||||
use std::net::Ipv4Addr;
|
||||
use std::time::Duration;
|
||||
|
||||
use bgp_integration_tests::announcement::{Announcement, withdrawal as withdrawal_of};
|
||||
use bgp_integration_tests::test_peer::TestPeer;
|
||||
use bgp_integration_tests::test_server::{RouteChange, TestServer};
|
||||
use bgp_integration_tests::{TIMEOUT, get_json, prefix};
|
||||
use bgp_packet::message::{PathAttribute, UpdateMessage};
|
||||
use bgp_packet::message::{
|
||||
AsPathAttribute, Message, NextHopPathAttribute, OriginPathAttribute, PathAttribute,
|
||||
UpdateMessage,
|
||||
};
|
||||
use bgp_primitives::ip_prefix::{IpPrefix, Nlri};
|
||||
use eyre::Result;
|
||||
use serde_json::Value;
|
||||
|
||||
const LOCAL_ASN: u32 = 65000;
|
||||
const TIMEOUT: Duration = Duration::from_secs(5);
|
||||
|
||||
fn prefix(s: &str) -> IpPrefix {
|
||||
IpPrefix::try_from(s).unwrap()
|
||||
}
|
||||
|
||||
/// A session from the peer numbered `number`, in AS 65000 + `number`.
|
||||
async fn peer(server: &TestServer, number: u8) -> Result<TestPeer> {
|
||||
server
|
||||
.established(
|
||||
LOCAL_ASN + u32::from(number),
|
||||
Ipv4Addr::new(10, 0, 0, number),
|
||||
)
|
||||
.await
|
||||
let mut peer = TestPeer::connect(server.addr).await?;
|
||||
peer.handshake(
|
||||
LOCAL_ASN + u32::from(number),
|
||||
Ipv4Addr::new(10, 0, 0, number),
|
||||
90,
|
||||
)
|
||||
.await?;
|
||||
Ok(peer)
|
||||
}
|
||||
|
||||
fn announcement(prefixes: &[IpPrefix], as_path: &[u32]) -> Announcement {
|
||||
Announcement::of(Nlri::plain(prefixes.iter().copied()), as_path)
|
||||
fn announcement(prefixes: &[IpPrefix], as_path: &[u32]) -> Message {
|
||||
Message::Update(UpdateMessage {
|
||||
withdrawn_routes: vec![],
|
||||
path_attributes: vec![
|
||||
PathAttribute::Origin(OriginPathAttribute::IGP),
|
||||
AsPathAttribute::from_asns(as_path.to_vec()),
|
||||
PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::new(192, 0, 2, 1))),
|
||||
],
|
||||
announced_routes: Nlri::plain(prefixes.iter().copied()),
|
||||
})
|
||||
}
|
||||
|
||||
fn withdrawal(prefixes: &[IpPrefix]) -> UpdateMessage {
|
||||
withdrawal_of(Nlri::plain(prefixes.iter().copied()))
|
||||
fn withdrawal(prefixes: &[IpPrefix]) -> Message {
|
||||
Message::Update(UpdateMessage {
|
||||
withdrawn_routes: Nlri::plain(prefixes.iter().copied()),
|
||||
path_attributes: vec![],
|
||||
announced_routes: vec![],
|
||||
})
|
||||
}
|
||||
|
||||
/// Waits until the table holds `routes` routes.
|
||||
async fn wait_for_routes(server: &TestServer, routes: usize) -> Result<()> {
|
||||
tokio::time::timeout(TIMEOUT, async {
|
||||
while server.rib.len() != routes {
|
||||
tokio::time::sleep(Duration::from_millis(5)).await;
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The AS path of the route the server holds as best for `prefix`.
|
||||
@@ -46,6 +78,16 @@ fn best_path(server: &TestServer, prefix: &IpPrefix) -> Option<Vec<u32>> {
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_json(client: &reqwest::Client, url: &str) -> Result<Value> {
|
||||
Ok(client
|
||||
.get(url)
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?
|
||||
.json()
|
||||
.await?)
|
||||
}
|
||||
|
||||
/// Two peers offer the same prefix. The route with the shorter AS path is
|
||||
/// chosen; when it goes, the other is.
|
||||
#[tokio::test]
|
||||
@@ -57,21 +99,21 @@ async fn test_the_best_route_is_chosen_and_replaced_when_it_goes() -> Result<()>
|
||||
let p = prefix("203.0.113.0/24");
|
||||
|
||||
let mut far = peer(&server, 1).await?;
|
||||
far.announce(announcement(&[p], &[65001, 64999, 64998]))
|
||||
far.send(&announcement(&[p], &[65001, 64999, 64998]))
|
||||
.await?;
|
||||
server.wait_for_routes(1).await?;
|
||||
wait_for_routes(&server, 1).await?;
|
||||
assert_eq!(best_path(&server, &p), Some(vec![65001, 64999, 64998]));
|
||||
|
||||
let mut near = peer(&server, 2).await?;
|
||||
near.announce(announcement(&[p], &[65002, 64998])).await?;
|
||||
server.wait_for_routes(2).await?;
|
||||
near.send(&announcement(&[p], &[65002, 64998])).await?;
|
||||
wait_for_routes(&server, 2).await?;
|
||||
assert_eq!(best_path(&server, &p), Some(vec![65002, 64998]));
|
||||
|
||||
// Both routes are kept and shown; one is marked.
|
||||
let status = get_json(&client, &format!("{base}/api/status")).await?;
|
||||
assert_eq!(status["received_routes"], 2);
|
||||
assert_eq!(status["received_prefixes"], 1);
|
||||
let routes = get_json(&client, &format!("{base}/api/routes/received")).await?["routes"].take();
|
||||
let routes = get_json(&client, &format!("{base}/api/routes/received")).await?;
|
||||
let best: Vec<&Value> = routes
|
||||
.as_array()
|
||||
.unwrap()
|
||||
@@ -81,27 +123,14 @@ async fn test_the_best_route_is_chosen_and_replaced_when_it_goes() -> Result<()>
|
||||
assert_eq!(routes.as_array().unwrap().len(), 2);
|
||||
assert_eq!(best.len(), 1);
|
||||
assert_eq!(best[0]["peer_asn"], 65002);
|
||||
// Listed from the best down, each with its place in selection.
|
||||
let ranks: Vec<(u64, u64)> = routes
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|route| {
|
||||
(
|
||||
route["rank"].as_u64().unwrap(),
|
||||
route["peer_asn"].as_u64().unwrap(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(ranks, vec![(1, 65002), (2, 65001)]);
|
||||
|
||||
// The session with the best route ends: the other route takes over.
|
||||
drop(near);
|
||||
server.wait_for_routes(1).await?;
|
||||
wait_for_routes(&server, 1).await?;
|
||||
assert_eq!(best_path(&server, &p), Some(vec![65001, 64999, 64998]));
|
||||
|
||||
far.announce(withdrawal(&[p])).await?;
|
||||
server.wait_for_routes(0).await?;
|
||||
far.send(&withdrawal(&[p])).await?;
|
||||
wait_for_routes(&server, 0).await?;
|
||||
assert_eq!(best_path(&server, &p), None);
|
||||
|
||||
// A follower of the best routes is told of each change: the first
|
||||
@@ -128,9 +157,9 @@ async fn test_changes_of_best_route_are_on_the_event_stream() -> Result<()> {
|
||||
// A route that is there before the client connects is not an event.
|
||||
let mut first = peer(&server, 1).await?;
|
||||
first
|
||||
.announce(announcement(&[prefix("198.51.100.0/24")], &[65001]))
|
||||
.send(&announcement(&[prefix("198.51.100.0/24")], &[65001]))
|
||||
.await?;
|
||||
server.wait_for_routes(1).await?;
|
||||
wait_for_routes(&server, 1).await?;
|
||||
|
||||
let mut stream = reqwest::Client::new()
|
||||
.get(format!("{base}/api/events"))
|
||||
@@ -138,10 +167,10 @@ async fn test_changes_of_best_route_are_on_the_event_stream() -> Result<()> {
|
||||
.await?
|
||||
.error_for_status()?;
|
||||
|
||||
first.announce(announcement(&[p], &[65001, 64998])).await?;
|
||||
server.wait_for_routes(2).await?;
|
||||
first.announce(withdrawal(&[p])).await?;
|
||||
server.wait_for_routes(1).await?;
|
||||
first.send(&announcement(&[p], &[65001, 64998])).await?;
|
||||
wait_for_routes(&server, 2).await?;
|
||||
first.send(&withdrawal(&[p])).await?;
|
||||
wait_for_routes(&server, 1).await?;
|
||||
|
||||
let mut text = String::new();
|
||||
let mut events: Vec<Value> = Vec::new();
|
||||
@@ -194,10 +223,10 @@ async fn test_received_routes_are_returned_up_to_a_limit() -> Result<()> {
|
||||
|
||||
let prefixes: Vec<IpPrefix> = (0..40).map(|n| prefix(&format!("10.{n}.0.0/16"))).collect();
|
||||
let mut peer = peer(&server, 1).await?;
|
||||
peer.announce(announcement(&prefixes, &[65001])).await?;
|
||||
server.wait_for_routes(40).await?;
|
||||
peer.send(&announcement(&prefixes, &[65001])).await?;
|
||||
wait_for_routes(&server, 40).await?;
|
||||
|
||||
let count = |page: Value| page["routes"].as_array().unwrap().len();
|
||||
let count = |routes: Value| routes.as_array().unwrap().len();
|
||||
let url = format!("{base}/api/routes/received");
|
||||
assert_eq!(count(get_json(&client, &url).await?), 40);
|
||||
assert_eq!(
|
||||
|
||||
@@ -1,141 +0,0 @@
|
||||
//! Blackhole requests (RFC 7999): refused by default, and with
|
||||
//! `accept_blackholes` taken for one address inside what the peer may
|
||||
//! announce, whatever their next hop.
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
use std::net::Ipv4Addr;
|
||||
use std::time::Duration;
|
||||
|
||||
use bgp_integration_tests::announcement::announcement;
|
||||
use bgp_integration_tests::test_peer::TestPeer;
|
||||
use bgp_integration_tests::test_server::{ServerWithPeer, TestServer};
|
||||
use bgp_integration_tests::{default_policy, prefix, wait_until};
|
||||
use bgp_server::policy::{ImportPolicy, PrefixSet};
|
||||
use eyre::Result;
|
||||
|
||||
const LOCAL_ASN: u32 = 1299;
|
||||
const PEER_ASN: u32 = 3333;
|
||||
const PEER_ID: Ipv4Addr = Ipv4Addr::new(10, 0, 0, 2);
|
||||
const BLACKHOLE: (u16, u16) = (0xFFFF, 0x029A);
|
||||
|
||||
async fn server_with(import: ImportPolicy) -> Result<ServerWithPeer> {
|
||||
TestServer::with_peer(LOCAL_ASN, PEER_ASN, |c, peer| {
|
||||
c.connect_retry = Duration::from_millis(50);
|
||||
peer.import = import;
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// Waits until the server has taken `taken` routes of the session's and
|
||||
/// refused `refused`, and returns the prefixes in its table.
|
||||
async fn table_after(server: &TestServer, taken: u64, refused: u64) -> Result<BTreeSet<String>> {
|
||||
let counters = server.registry.list()[0].counters.clone();
|
||||
wait_until("the routes are dealt with", || {
|
||||
(counters.routes(), counters.rejected()) == (taken, refused)
|
||||
})
|
||||
.await
|
||||
.map_err(|_| {
|
||||
eyre::eyre!(
|
||||
"the server took {} routes and refused {}; expected {taken} and {refused}",
|
||||
counters.routes(),
|
||||
counters.rejected()
|
||||
)
|
||||
})?;
|
||||
Ok(server
|
||||
.received_routes()
|
||||
.iter()
|
||||
.map(|route| route.prefix.to_string())
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// The policy that lets the peer announce 193.0.0.0/21 and nothing
|
||||
/// longer than a /24 inside it, as a customer's would.
|
||||
fn customer_policy(accept_blackholes: bool) -> ImportPolicy {
|
||||
let mut prefixes = PrefixSet::default();
|
||||
prefixes.allow(prefix("193.0.0.0/21"), 24);
|
||||
ImportPolicy {
|
||||
prefixes: Some(prefixes),
|
||||
accept_blackholes,
|
||||
..default_policy()
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_blackhole_requests_are_refused_unless_accepted() -> Result<()> {
|
||||
let with_peer = server_with(customer_policy(false)).await?;
|
||||
let server = &with_peer.server;
|
||||
let mut peer: TestPeer = server.established(PEER_ASN, PEER_ID).await?;
|
||||
peer.announce(announcement(&["193.0.0.9/32"], &[PEER_ASN]).with_communities(&[BLACKHOLE]))
|
||||
.await?;
|
||||
peer.announce(announcement(&["193.0.0.0/24"], &[PEER_ASN]))
|
||||
.await?;
|
||||
assert_eq!(table_after(server, 1, 1).await?, set(&["193.0.0.0/24"]));
|
||||
let refused = server.rejections.peers();
|
||||
assert_eq!(refused.len(), 1);
|
||||
assert_eq!(
|
||||
refused[0]
|
||||
.1
|
||||
.by_reason()
|
||||
.iter()
|
||||
.map(|(reason, n)| (reason.as_str(), *n))
|
||||
.collect::<Vec<_>>(),
|
||||
[("blackhole request", 1)]
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_a_blackhole_request_is_taken_for_one_address_in_the_peers_space() -> Result<()> {
|
||||
let with_peer = server_with(customer_policy(true)).await?;
|
||||
let server = &with_peer.server;
|
||||
let mut peer: TestPeer = server.established(PEER_ASN, PEER_ID).await?;
|
||||
// One address inside the peer's prefix, from a next hop that is no
|
||||
// neighbor and would not pass as an ordinary route's.
|
||||
peer.announce(
|
||||
announcement(&["193.0.0.9/32"], &[PEER_ASN])
|
||||
.with_next_hop(Ipv4Addr::new(172, 31, 0, 9))
|
||||
.with_communities(&[BLACKHOLE]),
|
||||
)
|
||||
.await?;
|
||||
// More than one address: not a blackhole request that is taken.
|
||||
peer.announce(announcement(&["193.0.0.0/25"], &[PEER_ASN]).with_communities(&[BLACKHOLE]))
|
||||
.await?;
|
||||
// One address outside the peer's space.
|
||||
peer.announce(announcement(&["193.0.8.9/32"], &[PEER_ASN]).with_communities(&[BLACKHOLE]))
|
||||
.await?;
|
||||
// And an ordinary route, taken as ever.
|
||||
peer.announce(announcement(&["193.0.0.0/24"], &[PEER_ASN]))
|
||||
.await?;
|
||||
assert_eq!(
|
||||
table_after(server, 2, 2).await?,
|
||||
set(&["193.0.0.0/24", "193.0.0.9/32"])
|
||||
);
|
||||
let refused = server.rejections.peers();
|
||||
let mut reasons: Vec<(&str, u64)> = refused[0]
|
||||
.1
|
||||
.by_reason()
|
||||
.iter()
|
||||
.map(|(reason, n)| (reason.as_str(), *n))
|
||||
.collect();
|
||||
reasons.sort();
|
||||
assert_eq!(
|
||||
reasons,
|
||||
[
|
||||
("blackhole for more than one address", 1),
|
||||
("prefix not allowed", 1)
|
||||
]
|
||||
);
|
||||
// The route is in the table with its request on it, for the kernel
|
||||
// and for whoever it goes on to.
|
||||
let routes = server.received_routes();
|
||||
let blackhole = routes
|
||||
.iter()
|
||||
.find(|route| route.prefix == prefix("193.0.0.9/32"))
|
||||
.expect("the blackhole route is in the table");
|
||||
assert!(ImportPolicy::is_blackhole_request(&blackhole.attributes));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set(prefixes: &[&str]) -> BTreeSet<String> {
|
||||
prefixes.iter().map(|p| p.to_string()).collect()
|
||||
}
|
||||
@@ -3,33 +3,46 @@
|
||||
//! The test server's BGP Identifier is 127.0.0.1. A test peer picks an
|
||||
//! identifier above or below that to steer which connection must survive.
|
||||
|
||||
use std::net::Ipv4Addr;
|
||||
use std::net::{Ipv4Addr, SocketAddr};
|
||||
use std::time::Duration;
|
||||
|
||||
use bgp_integration_tests::announcement::announcement;
|
||||
use bgp_integration_tests::test_peer::TestPeer;
|
||||
use bgp_integration_tests::test_server::{ServerWithPeer, TestServer};
|
||||
use bgp_integration_tests::{TIMEOUT, prefix, wait_until};
|
||||
use bgp_integration_tests::test_server::TestServer;
|
||||
use bgp_packet::errors::{BgpError, CeaseSubcode, OpenMsgSubcode};
|
||||
use bgp_packet::message::{Message, NotificationMessage};
|
||||
use bgp_packet::message::{
|
||||
AsPathAttribute, Message, NextHopPathAttribute, NotificationMessage, OriginPathAttribute,
|
||||
PathAttribute, UpdateMessage,
|
||||
};
|
||||
use bgp_primitives::ip_prefix::IpPrefix;
|
||||
use bgp_rib::{LocalRoute, LocalRouteAttrs};
|
||||
use bgp_server::collision::Direction;
|
||||
use bgp_server::policy::ImportPolicy;
|
||||
use bgp_server::config::PeerConfig;
|
||||
use bgp_server::family::FamilySet;
|
||||
use eyre::Result;
|
||||
use tokio::net::TcpListener;
|
||||
|
||||
const LOCAL_ASN: u32 = 65000;
|
||||
const PEER_ASN: u32 = 65001;
|
||||
const TIMEOUT: Duration = Duration::from_secs(5);
|
||||
|
||||
/// Above the server's 127.0.0.1.
|
||||
const HIGH_ID: Ipv4Addr = Ipv4Addr::new(192, 0, 2, 1);
|
||||
/// Below the server's 127.0.0.1.
|
||||
const LOW_ID: Ipv4Addr = Ipv4Addr::new(10, 0, 0, 1);
|
||||
|
||||
/// A server that dials its peer "r1" every 200 ms until it has a session.
|
||||
async fn server_dialing() -> Result<ServerWithPeer> {
|
||||
TestServer::with_peer(LOCAL_ASN, PEER_ASN, |c, peer| {
|
||||
async fn server_dialing(listener: &TcpListener) -> Result<TestServer> {
|
||||
let endpoint: SocketAddr = listener.local_addr()?;
|
||||
TestServer::start_with(LOCAL_ASN, |c| {
|
||||
c.connect_retry = Duration::from_millis(200);
|
||||
peer.import = ImportPolicy::permissive();
|
||||
c.peers = vec![PeerConfig {
|
||||
name: "r1".into(),
|
||||
endpoint,
|
||||
remote_asn: PEER_ASN,
|
||||
hold_time_secs: 90,
|
||||
local_address: None,
|
||||
families: FamilySet::supported(),
|
||||
add_path: None,
|
||||
}];
|
||||
})
|
||||
.await
|
||||
}
|
||||
@@ -44,13 +57,27 @@ async fn open_only(peer: &mut TestPeer, router_id: Ipv4Addr) -> Result<()> {
|
||||
|
||||
/// Finishes the handshake begun by [`open_only`].
|
||||
async fn confirm(peer: &mut TestPeer) -> Result<()> {
|
||||
match peer.recv().await? {
|
||||
match tokio::time::timeout(TIMEOUT, peer.recv()).await?? {
|
||||
Message::KeepAlive => {}
|
||||
other => eyre::bail!("expected KEEPALIVE, got {other:?}"),
|
||||
}
|
||||
peer.send(&Message::KeepAlive).await
|
||||
}
|
||||
|
||||
/// Reads until a NOTIFICATION arrives, skipping KEEPALIVEs.
|
||||
async fn expect_notification(peer: &mut TestPeer) -> Result<NotificationMessage> {
|
||||
tokio::time::timeout(TIMEOUT, async {
|
||||
loop {
|
||||
match peer.recv().await? {
|
||||
Message::Notification(n) => return Ok(n),
|
||||
Message::KeepAlive => {}
|
||||
other => eyre::bail!("expected NOTIFICATION, got {other:?}"),
|
||||
}
|
||||
}
|
||||
})
|
||||
.await?
|
||||
}
|
||||
|
||||
fn is_collision_cease(n: &NotificationMessage) -> bool {
|
||||
matches!(
|
||||
n.error,
|
||||
@@ -58,26 +85,38 @@ fn is_collision_cease(n: &NotificationMessage) -> bool {
|
||||
)
|
||||
}
|
||||
|
||||
async fn wait_for_sessions(server: &TestServer, n: usize) -> Result<()> {
|
||||
tokio::time::timeout(TIMEOUT, async {
|
||||
while server.registry.list().len() != n {
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Peer has the higher identifier, so the connection the *peer* initiated
|
||||
/// must survive: the server closes its own outbound connection.
|
||||
#[tokio::test]
|
||||
async fn test_simultaneous_open_peer_higher_keeps_inbound() -> Result<()> {
|
||||
let ServerWithPeer { server, listener } = server_dialing().await?;
|
||||
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await?;
|
||||
let server = server_dialing(&listener).await?;
|
||||
|
||||
// Server's outbound connection reaches OpenConfirm.
|
||||
let mut dialled = TestPeer::accept(&listener).await?;
|
||||
let (stream, _) = tokio::time::timeout(TIMEOUT, listener.accept()).await??;
|
||||
let mut dialled = TestPeer::from_stream(stream);
|
||||
open_only(&mut dialled, HIGH_ID).await?;
|
||||
|
||||
// The peer now also dials the server.
|
||||
let mut inbound = TestPeer::connect(server.addr).await?;
|
||||
open_only(&mut inbound, HIGH_ID).await?;
|
||||
|
||||
let notif = dialled.expect_notification().await?;
|
||||
let notif = expect_notification(&mut dialled).await?;
|
||||
assert!(is_collision_cease(¬if), "{notif:?}");
|
||||
assert!(dialled.closed_within(TIMEOUT).await);
|
||||
assert!(dialled.is_closed().await);
|
||||
|
||||
confirm(&mut inbound).await?;
|
||||
server.wait_for_sessions(1).await?;
|
||||
wait_for_sessions(&server, 1).await?;
|
||||
let session = server.registry.list().remove(0);
|
||||
assert_eq!(session.direction, Direction::Inbound);
|
||||
assert_eq!(session.name.as_deref(), Some("r1"));
|
||||
@@ -86,11 +125,11 @@ async fn test_simultaneous_open_peer_higher_keeps_inbound() -> Result<()> {
|
||||
server
|
||||
.local_rib
|
||||
.announce(LocalRoute::new(
|
||||
prefix("10.100.1.0/24"),
|
||||
IpPrefix::try_from("10.100.1.0/24")?,
|
||||
LocalRouteAttrs::default(),
|
||||
))
|
||||
.await?;
|
||||
let update = inbound.recv_update().await?;
|
||||
let update = tokio::time::timeout(TIMEOUT, inbound.recv_update()).await??;
|
||||
assert_eq!(update.announced_routes[0].to_string(), "10.100.1.0/24");
|
||||
|
||||
// The dialer notices the surviving session and does not open another.
|
||||
@@ -106,20 +145,22 @@ async fn test_simultaneous_open_peer_higher_keeps_inbound() -> Result<()> {
|
||||
/// must survive: the peer's inbound connection is refused.
|
||||
#[tokio::test]
|
||||
async fn test_simultaneous_open_peer_lower_keeps_outbound() -> Result<()> {
|
||||
let ServerWithPeer { server, listener } = server_dialing().await?;
|
||||
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await?;
|
||||
let server = server_dialing(&listener).await?;
|
||||
|
||||
let mut dialled = TestPeer::accept(&listener).await?;
|
||||
let (stream, _) = tokio::time::timeout(TIMEOUT, listener.accept()).await??;
|
||||
let mut dialled = TestPeer::from_stream(stream);
|
||||
open_only(&mut dialled, LOW_ID).await?;
|
||||
|
||||
let mut inbound = TestPeer::connect(server.addr).await?;
|
||||
open_only(&mut inbound, LOW_ID).await?;
|
||||
|
||||
let notif = inbound.expect_notification().await?;
|
||||
let notif = expect_notification(&mut inbound).await?;
|
||||
assert!(is_collision_cease(¬if), "{notif:?}");
|
||||
assert!(inbound.closed_within(TIMEOUT).await);
|
||||
assert!(inbound.is_closed().await);
|
||||
|
||||
confirm(&mut dialled).await?;
|
||||
server.wait_for_sessions(1).await?;
|
||||
wait_for_sessions(&server, 1).await?;
|
||||
assert_eq!(server.registry.list()[0].direction, Direction::Outbound);
|
||||
Ok(())
|
||||
}
|
||||
@@ -128,20 +169,22 @@ async fn test_simultaneous_open_peer_lower_keeps_outbound() -> Result<()> {
|
||||
/// outcome: here the inbound connection reaches OpenConfirm first.
|
||||
#[tokio::test]
|
||||
async fn test_outcome_is_independent_of_arrival_order() -> Result<()> {
|
||||
let ServerWithPeer { server, listener } = server_dialing().await?;
|
||||
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await?;
|
||||
let server = server_dialing(&listener).await?;
|
||||
|
||||
let mut dialled = TestPeer::accept(&listener).await?;
|
||||
let (stream, _) = tokio::time::timeout(TIMEOUT, listener.accept()).await??;
|
||||
let mut dialled = TestPeer::from_stream(stream);
|
||||
|
||||
// Inbound first this time; peer is lower so outbound must still win.
|
||||
let mut inbound = TestPeer::connect(server.addr).await?;
|
||||
open_only(&mut inbound, LOW_ID).await?;
|
||||
open_only(&mut dialled, LOW_ID).await?;
|
||||
|
||||
let notif = inbound.expect_notification().await?;
|
||||
let notif = expect_notification(&mut inbound).await?;
|
||||
assert!(is_collision_cease(¬if), "{notif:?}");
|
||||
|
||||
confirm(&mut dialled).await?;
|
||||
server.wait_for_sessions(1).await?;
|
||||
wait_for_sessions(&server, 1).await?;
|
||||
assert_eq!(server.registry.list()[0].direction, Direction::Outbound);
|
||||
Ok(())
|
||||
}
|
||||
@@ -154,11 +197,11 @@ async fn test_established_session_wins_by_default() -> Result<()> {
|
||||
|
||||
let mut first = TestPeer::connect(server.addr).await?;
|
||||
first.handshake(PEER_ASN, HIGH_ID, 90).await?;
|
||||
server.wait_for_sessions(1).await?;
|
||||
wait_for_sessions(&server, 1).await?;
|
||||
|
||||
let mut second = TestPeer::connect(server.addr).await?;
|
||||
open_only(&mut second, HIGH_ID).await?;
|
||||
let notif = second.expect_notification().await?;
|
||||
let notif = expect_notification(&mut second).await?;
|
||||
assert!(is_collision_cease(¬if), "{notif:?}");
|
||||
|
||||
// The original is untouched.
|
||||
@@ -166,11 +209,11 @@ async fn test_established_session_wins_by_default() -> Result<()> {
|
||||
server
|
||||
.local_rib
|
||||
.announce(LocalRoute::new(
|
||||
prefix("10.100.1.0/24"),
|
||||
IpPrefix::try_from("10.100.1.0/24")?,
|
||||
LocalRouteAttrs::default(),
|
||||
))
|
||||
.await?;
|
||||
let update = first.recv_update().await?;
|
||||
let update = tokio::time::timeout(TIMEOUT, first.recv_update()).await??;
|
||||
assert_eq!(update.announced_routes[0].to_string(), "10.100.1.0/24");
|
||||
Ok(())
|
||||
}
|
||||
@@ -185,26 +228,41 @@ async fn test_restarted_peer_replaces_stale_session_when_enabled() -> Result<()>
|
||||
|
||||
let mut stale = TestPeer::connect(server.addr).await?;
|
||||
stale.handshake(PEER_ASN, HIGH_ID, 90).await?;
|
||||
server.wait_for_sessions(1).await?;
|
||||
wait_for_sessions(&server, 1).await?;
|
||||
stale
|
||||
.announce(
|
||||
announcement(&["203.0.113.0/24"], &[PEER_ASN])
|
||||
.with_next_hop(Ipv4Addr::new(10, 0, 0, 9)),
|
||||
)
|
||||
.send(&Message::Update(UpdateMessage {
|
||||
withdrawn_routes: vec![],
|
||||
path_attributes: vec![
|
||||
PathAttribute::Origin(OriginPathAttribute::IGP),
|
||||
AsPathAttribute::from_asns(vec![PEER_ASN]),
|
||||
PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::new(10, 0, 0, 9))),
|
||||
],
|
||||
announced_routes: vec![IpPrefix::try_from("203.0.113.0/24")?.into()],
|
||||
}))
|
||||
.await?;
|
||||
server.wait_for_routes(1).await?;
|
||||
tokio::time::timeout(TIMEOUT, async {
|
||||
while server.received_routes().is_empty() {
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
let stale_addr = server.registry.list()[0].peer.addr;
|
||||
|
||||
// "Restart": a fresh connection from the same router.
|
||||
let mut fresh = TestPeer::connect(server.addr).await?;
|
||||
fresh.handshake(PEER_ASN, HIGH_ID, 90).await?;
|
||||
|
||||
let notif = stale.expect_notification().await?;
|
||||
let notif = expect_notification(&mut stale).await?;
|
||||
assert!(is_collision_cease(¬if), "{notif:?}");
|
||||
|
||||
wait_until("the fresh session has replaced the stale one", || {
|
||||
let sessions = server.registry.list();
|
||||
sessions.len() == 1 && sessions[0].peer.addr != stale_addr
|
||||
tokio::time::timeout(TIMEOUT, async {
|
||||
loop {
|
||||
let sessions = server.registry.list();
|
||||
if sessions.len() == 1 && sessions[0].peer.addr != stale_addr {
|
||||
break;
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
assert!(
|
||||
@@ -222,7 +280,7 @@ async fn test_distinct_identifiers_do_not_collide() -> Result<()> {
|
||||
a.handshake(PEER_ASN, HIGH_ID, 90).await?;
|
||||
let mut b = TestPeer::connect(server.addr).await?;
|
||||
b.handshake(PEER_ASN, LOW_ID, 90).await?;
|
||||
server.wait_for_sessions(2).await?;
|
||||
wait_for_sessions(&server, 2).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -232,13 +290,13 @@ async fn test_identifier_is_released_when_connection_ends() -> Result<()> {
|
||||
let server = TestServer::start(LOCAL_ASN).await?;
|
||||
let mut first = TestPeer::connect(server.addr).await?;
|
||||
first.handshake(PEER_ASN, HIGH_ID, 90).await?;
|
||||
server.wait_for_sessions(1).await?;
|
||||
wait_for_sessions(&server, 1).await?;
|
||||
drop(first);
|
||||
server.wait_for_sessions(0).await?;
|
||||
wait_for_sessions(&server, 0).await?;
|
||||
|
||||
let mut second = TestPeer::connect(server.addr).await?;
|
||||
second.handshake(PEER_ASN, HIGH_ID, 90).await?;
|
||||
server.wait_for_sessions(1).await?;
|
||||
wait_for_sessions(&server, 1).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -248,7 +306,7 @@ async fn test_peer_with_our_identifier_is_refused() -> Result<()> {
|
||||
let server = TestServer::start(LOCAL_ASN).await?;
|
||||
let mut peer = TestPeer::connect(server.addr).await?;
|
||||
open_only(&mut peer, Ipv4Addr::new(127, 0, 0, 1)).await?;
|
||||
let notif = peer.expect_notification().await?;
|
||||
let notif = expect_notification(&mut peer).await?;
|
||||
assert!(
|
||||
matches!(notif.error, BgpError::OpenMsg(OpenMsgSubcode::BadBgpId)),
|
||||
"{notif:?}"
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
//! A peer that sends us a table while not reading what we send it must
|
||||
//! not stop us reading it: two routers sending each other their tables at
|
||||
//! once would otherwise each wait on the other for ever.
|
||||
|
||||
use std::net::Ipv4Addr;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use bgp_integration_tests::announcement::Announcement;
|
||||
use bgp_integration_tests::prefix;
|
||||
use bgp_integration_tests::test_peer::TestPeer;
|
||||
use bgp_integration_tests::test_server::TestServer;
|
||||
use bgp_server::policy::ExportPolicy;
|
||||
use eyre::Result;
|
||||
|
||||
const LOCAL_ASN: u32 = 64601;
|
||||
const ROUTES: u32 = 100_000;
|
||||
|
||||
fn nth(n: u32) -> bgp_primitives::ip_prefix::IpPrefix {
|
||||
let [a, b, c, _] = (0x0a00_0000u32 + n * 256).to_be_bytes();
|
||||
prefix(&format!("{a}.{b}.{c}.0/24"))
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_peer_that_does_not_read_is_still_read() -> Result<()> {
|
||||
let server = TestServer::start_with(LOCAL_ASN, |c| {
|
||||
c.export = Arc::new(ExportPolicy {
|
||||
learned_routes: true,
|
||||
all_paths: false,
|
||||
..ExportPolicy::default()
|
||||
});
|
||||
})
|
||||
.await?;
|
||||
// One peer's table, for the server to pass on.
|
||||
let mut first = server
|
||||
.established(64701, Ipv4Addr::new(10, 0, 0, 1))
|
||||
.await?;
|
||||
// A route to an UPDATE, each with attributes of its own, as a real
|
||||
// table has them: passing it on is then megabytes, not a few hundred
|
||||
// UPDATEs that the socket buffers would take whole.
|
||||
for i in 0..ROUTES {
|
||||
first
|
||||
.announce(
|
||||
Announcement::of([nth(i).into()], &[64701, 64510])
|
||||
.with_communities(&[((i >> 16) as u16 + 1, i as u16)]),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
server.wait_for_routes(ROUTES as usize).await?;
|
||||
|
||||
// A second peer, which the server starts sending that table to as
|
||||
// soon as it is up, sends a table of its own and reads nothing back:
|
||||
// the server's writes to it fill its socket and stop. The server has
|
||||
// to go on reading it all the same. The peer's receive buffer is
|
||||
// small, as a real link's window would be, so that this happens with
|
||||
// the first few hundred kilobytes and not after the loopback's
|
||||
// megabytes.
|
||||
let socket = tokio::net::TcpSocket::new_v4()?;
|
||||
socket.set_recv_buffer_size(16 * 1024)?;
|
||||
let stream = socket.connect(server.addr).await?;
|
||||
let mut second = TestPeer::from_stream(stream);
|
||||
second
|
||||
.handshake(64702, Ipv4Addr::new(10, 0, 0, 2), 90)
|
||||
.await?;
|
||||
server.wait_for_sessions(2).await?;
|
||||
// Sent over a couple of seconds, so that the server's export to the
|
||||
// peer is well under way while the peer is still sending.
|
||||
let sending = async {
|
||||
for start in (0..ROUTES).step_by(100) {
|
||||
second
|
||||
.announce(Announcement::of(
|
||||
(start..start + 100).map(|i| nth(i).into()),
|
||||
&[64702, 64510],
|
||||
))
|
||||
.await?;
|
||||
if start % 2000 == 0 {
|
||||
tokio::time::sleep(Duration::from_millis(40)).await;
|
||||
}
|
||||
}
|
||||
Ok::<_, eyre::Report>(())
|
||||
};
|
||||
tokio::time::timeout(Duration::from_secs(30), sending)
|
||||
.await
|
||||
.expect("the server reads the peer's table though the peer reads nothing")?;
|
||||
tokio::time::timeout(
|
||||
Duration::from_secs(30),
|
||||
server.wait_for_routes(2 * ROUTES as usize),
|
||||
)
|
||||
.await
|
||||
.expect("every route is in the table")?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,261 +0,0 @@
|
||||
//! Finding out what the server has and why: looking routes up, and seeing
|
||||
//! what was refused.
|
||||
|
||||
use std::net::Ipv4Addr;
|
||||
|
||||
use bgp_integration_tests::announcement::announcement;
|
||||
use bgp_integration_tests::test_server::{ServerWithPeer, TestServer};
|
||||
use bgp_integration_tests::{default_policy, wait_until};
|
||||
use bgp_server::policy::ImportPolicy;
|
||||
use eyre::Result;
|
||||
use serde_json::Value;
|
||||
|
||||
/// The routes a query returns, as "prefix from AS, best or not".
|
||||
async fn routes(client: &reqwest::Client, base: &str, query: &str) -> Result<Vec<String>> {
|
||||
let reply = client
|
||||
.get(format!("{base}/api/routes/received?{query}"))
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?;
|
||||
let page: Value = reply.json().await?;
|
||||
Ok(page["routes"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|r| {
|
||||
format!(
|
||||
"{} AS{}{}",
|
||||
r["prefix"].as_str().unwrap(),
|
||||
r["peer_asn"],
|
||||
if r["best"] == true { " best" } else { "" }
|
||||
)
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// The questions asked when something is wrong with a prefix: who sends it
|
||||
/// and which route won, what covers an address, what is inside a block.
|
||||
#[tokio::test]
|
||||
async fn test_routes_can_be_looked_up() -> Result<()> {
|
||||
let server = TestServer::start(65000).await?;
|
||||
let (base, _http) = server.serve_http().await?;
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let mut one = server
|
||||
.established(65001, Ipv4Addr::new(10, 0, 0, 1))
|
||||
.await?;
|
||||
let mut two = server
|
||||
.established(65002, Ipv4Addr::new(10, 0, 0, 2))
|
||||
.await?;
|
||||
one.announce(announcement(
|
||||
&["10.0.0.0/8", "10.1.0.0/16", "10.1.2.0/24", "192.0.2.0/24"],
|
||||
&[65001],
|
||||
))
|
||||
.await?;
|
||||
// A longer path for one of the same prefixes: it loses.
|
||||
two.announce(announcement(&["10.1.0.0/16"], &[65002, 65003]))
|
||||
.await?;
|
||||
server.wait_for_routes(5).await?;
|
||||
|
||||
// One prefix: every route for it, the best first.
|
||||
assert_eq!(
|
||||
routes(&client, &base, "prefix=10.1.0.0/16").await?,
|
||||
["10.1.0.0/16 AS65001 best", "10.1.0.0/16 AS65002"]
|
||||
);
|
||||
assert!(
|
||||
routes(&client, &base, "prefix=10.2.0.0/16")
|
||||
.await?
|
||||
.is_empty()
|
||||
);
|
||||
|
||||
// An address: every prefix that covers it, the most specific first.
|
||||
assert_eq!(
|
||||
routes(&client, &base, "address=10.1.2.3").await?,
|
||||
[
|
||||
"10.1.2.0/24 AS65001 best",
|
||||
"10.1.0.0/16 AS65001 best",
|
||||
"10.1.0.0/16 AS65002",
|
||||
"10.0.0.0/8 AS65001 best"
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
routes(&client, &base, "address=10.200.0.1").await?,
|
||||
["10.0.0.0/8 AS65001 best"]
|
||||
);
|
||||
|
||||
// A block: itself and everything inside it.
|
||||
assert_eq!(
|
||||
routes(&client, &base, "within=10.1.0.0/16").await?,
|
||||
[
|
||||
"10.1.0.0/16 AS65001 best",
|
||||
"10.1.0.0/16 AS65002",
|
||||
"10.1.2.0/24 AS65001 best"
|
||||
]
|
||||
);
|
||||
|
||||
// Only what was chosen; and everything, in address order.
|
||||
assert_eq!(
|
||||
routes(&client, &base, "within=10.0.0.0/8&best=true").await?,
|
||||
[
|
||||
"10.0.0.0/8 AS65001 best",
|
||||
"10.1.0.0/16 AS65001 best",
|
||||
"10.1.2.0/24 AS65001 best"
|
||||
]
|
||||
);
|
||||
assert_eq!(routes(&client, &base, "").await?.len(), 5);
|
||||
// A page holds whole prefixes: at most the limit, unless one prefix
|
||||
// alone has more.
|
||||
assert!((1..=2).contains(&routes(&client, &base, "limit=2").await?.len()));
|
||||
|
||||
// One peer's routes, by its address; a peer that is not there has none.
|
||||
assert_eq!(routes(&client, &base, "peer=127.0.0.1").await?.len(), 5);
|
||||
assert!(routes(&client, &base, "peer=192.0.2.9").await?.is_empty());
|
||||
|
||||
// A query that makes no sense says so.
|
||||
for (query, complaint) in [
|
||||
("prefix=10.1.0.0", "is not a prefix"),
|
||||
("address=example", "is not an IP address"),
|
||||
("prefix=10.0.0.0/8&address=10.0.0.1", "at most one of"),
|
||||
] {
|
||||
let reply = client
|
||||
.get(format!("{base}/api/routes/received?{query}"))
|
||||
.send()
|
||||
.await?;
|
||||
assert_eq!(reply.status(), 400, "{query}");
|
||||
let text = reply.text().await?;
|
||||
assert!(text.contains(complaint), "{query}: {text}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A refused route is not in the table, so it has to be findable somewhere
|
||||
/// else: which peer, which prefix, with what path, and why. And there is
|
||||
/// something to alert on.
|
||||
#[tokio::test]
|
||||
async fn test_refused_routes_can_be_seen_and_counted() -> Result<()> {
|
||||
let ServerWithPeer {
|
||||
server,
|
||||
listener: _listener,
|
||||
} = TestServer::with_peer(1299, 3333, |_, peer| {
|
||||
peer.name = "refused-demo".into();
|
||||
peer.import = ImportPolicy {
|
||||
max_prefixes: Some(100),
|
||||
..default_policy()
|
||||
};
|
||||
})
|
||||
.await?;
|
||||
let (base, _http) = server.serve_http().await?;
|
||||
let client = reqwest::Client::new();
|
||||
let get = |path: &str| {
|
||||
let request = client.get(format!("{base}{path}"));
|
||||
async move { Ok::<_, eyre::Report>(request.send().await?.error_for_status()?) }
|
||||
};
|
||||
|
||||
let refused: Value = get("/api/routes/rejected").await?.json().await?;
|
||||
assert_eq!(refused, serde_json::json!([]), "nothing refused yet");
|
||||
|
||||
let mut peer = server.established(3333, Ipv4Addr::new(10, 0, 0, 2)).await?;
|
||||
peer.announce(announcement(&["193.0.0.0/21", "10.0.0.0/8"], &[3333]))
|
||||
.await?;
|
||||
peer.announce(announcement(
|
||||
&["194.0.1.0/24", "194.0.2.0/24"],
|
||||
&[3333, 64512],
|
||||
))
|
||||
.await?;
|
||||
peer.announce(announcement(&["193.0.9.0/25"], &[3333, 2121]))
|
||||
.await?;
|
||||
wait_until("four routes are refused", || {
|
||||
server
|
||||
.registry
|
||||
.list()
|
||||
.first()
|
||||
.map(|s| s.counters.rejected())
|
||||
== Some(4)
|
||||
})
|
||||
.await?;
|
||||
|
||||
let refused: Value = get("/api/routes/rejected").await?.json().await?;
|
||||
let record = &refused[0];
|
||||
assert_eq!(record["peer"], "refused-demo");
|
||||
assert_eq!(record["peer_addr"], "127.0.0.1");
|
||||
assert_eq!(record["total"], 4);
|
||||
assert_eq!(
|
||||
record["by_reason"],
|
||||
serde_json::json!({"bogon AS in path": 2, "bogon prefix": 1, "prefix too specific": 1})
|
||||
);
|
||||
// The latest first, each with the path it came with.
|
||||
let recent: Vec<(String, String, String)> = record["recent"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|r| {
|
||||
(
|
||||
r["prefix"].as_str().unwrap().to_string(),
|
||||
r["reason"].as_str().unwrap().to_string(),
|
||||
r["as_path"].to_string(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let row = |p: &str, why: &str, path: &str| (p.to_string(), why.to_string(), path.to_string());
|
||||
assert_eq!(
|
||||
recent,
|
||||
[
|
||||
row("193.0.9.0/25", "prefix too specific", "[3333,2121]"),
|
||||
row("194.0.2.0/24", "bogon AS in path", "[3333,64512]"),
|
||||
row("194.0.1.0/24", "bogon AS in path", "[3333,64512]"),
|
||||
row("10.0.0.0/8", "bogon prefix", "[3333]"),
|
||||
]
|
||||
);
|
||||
// And with all its attributes, as for a route that was taken, to see
|
||||
// what came along with it.
|
||||
let latest = &record["recent"][0];
|
||||
assert_eq!(latest["summary"]["next_hop"], "192.0.2.1");
|
||||
assert_eq!(latest["summary"]["origin"], "IGP");
|
||||
assert_eq!(
|
||||
latest["summary"]["as_path"],
|
||||
serde_json::json!([3333, 2121])
|
||||
);
|
||||
assert_eq!(
|
||||
latest["attributes"].as_array().map(Vec::len),
|
||||
Some(3),
|
||||
"ORIGIN, AS_PATH and NEXT_HOP: {latest}"
|
||||
);
|
||||
// Asked for by peer, by name or by address.
|
||||
for peer in ["refused-demo", "127.0.0.1"] {
|
||||
let one: Value = get(&format!("/api/routes/rejected?peer={peer}"))
|
||||
.await?
|
||||
.json()
|
||||
.await?;
|
||||
assert_eq!(one.as_array().unwrap().len(), 1, "{peer}");
|
||||
}
|
||||
let none: Value = get("/api/routes/rejected?peer=someone-else")
|
||||
.await?
|
||||
.json()
|
||||
.await?;
|
||||
assert_eq!(none, serde_json::json!([]));
|
||||
|
||||
// The peer's line in the list of peers has the numbers too.
|
||||
let peers: Value = get("/api/peers").await?.json().await?;
|
||||
assert_eq!(peers[0]["routes"], 1);
|
||||
assert_eq!(peers[0]["rejected_routes"], 4);
|
||||
|
||||
// What an alert would be made from.
|
||||
let metrics = get("/metricz").await?.text().await?;
|
||||
for line in [
|
||||
r#"bgp_routes_rejected_total{peer="refused-demo",reason="bogon AS in path"} 2"#,
|
||||
r#"bgp_routes_rejected_total{peer="refused-demo",reason="bogon prefix"} 1"#,
|
||||
r#"bgp_peer_routes{peer="refused-demo"} 1"#,
|
||||
r#"bgp_peer_max_prefixes{peer="refused-demo"} 100"#,
|
||||
] {
|
||||
assert!(metrics.contains(line), "{line} is not in:\n{metrics}");
|
||||
}
|
||||
|
||||
// The record outlives the session; the count of routes does not.
|
||||
drop(peer);
|
||||
server.wait_for_sessions(0).await?;
|
||||
let refused: Value = get("/api/routes/rejected").await?.json().await?;
|
||||
assert_eq!(refused[0]["total"], 4);
|
||||
let metrics = get("/metricz").await?.text().await?;
|
||||
assert!(!metrics.contains(r#"bgp_peer_routes{peer="refused-demo"}"#));
|
||||
Ok(())
|
||||
}
|
||||
@@ -5,47 +5,68 @@ use std::net::{Ipv4Addr, SocketAddr};
|
||||
use std::time::Duration;
|
||||
|
||||
use bgp_integration_tests::test_peer::TestPeer;
|
||||
use bgp_integration_tests::test_server::{ServerWithPeer, TestServer};
|
||||
use bgp_integration_tests::{TIMEOUT, peer_config, prefix, wait_for, wait_until};
|
||||
use bgp_integration_tests::test_server::TestServer;
|
||||
use bgp_packet::errors::{BgpError, OpenMsgSubcode};
|
||||
use bgp_packet::message::Message;
|
||||
use bgp_primitives::ip_prefix::IpPrefix;
|
||||
use bgp_rib::{LocalRoute, LocalRouteAttrs};
|
||||
use bgp_server::config::PeerConfig;
|
||||
use bgp_server::policy::ImportPolicy;
|
||||
use bgp_server::family::FamilySet;
|
||||
use bgp_server::session::DialState;
|
||||
use eyre::Result;
|
||||
use tokio::net::TcpListener;
|
||||
|
||||
const LOCAL_ASN: u32 = 65000;
|
||||
const PEER_ASN: u32 = 65001;
|
||||
const PEER_ID: Ipv4Addr = Ipv4Addr::new(10, 0, 0, 2);
|
||||
const TIMEOUT: Duration = Duration::from_secs(5);
|
||||
|
||||
/// A server that dials its peer "r1", in `remote_asn`, every 200 ms.
|
||||
async fn server_dialing(remote_asn: u32) -> Result<ServerWithPeer> {
|
||||
server_with_peer(remote_asn, false).await
|
||||
}
|
||||
|
||||
/// A server with peer "r1" in `remote_asn`, which it dials every 200 ms
|
||||
/// unless the peer is `passive`.
|
||||
async fn server_with_peer(remote_asn: u32, passive: bool) -> Result<ServerWithPeer> {
|
||||
TestServer::with_peer(LOCAL_ASN, remote_asn, |c, peer| {
|
||||
/// Starts a server configured to dial `listener`'s address as peer "r1".
|
||||
async fn server_dialing(listener: &TcpListener, remote_asn: u32) -> Result<TestServer> {
|
||||
let endpoint: SocketAddr = listener.local_addr()?;
|
||||
TestServer::start_with(LOCAL_ASN, |c| {
|
||||
c.connect_retry = Duration::from_millis(200);
|
||||
peer.import = ImportPolicy::permissive();
|
||||
peer.passive = passive;
|
||||
c.peers = vec![PeerConfig {
|
||||
name: "r1".into(),
|
||||
endpoint,
|
||||
remote_asn,
|
||||
hold_time_secs: 90,
|
||||
local_address: None,
|
||||
families: FamilySet::supported(),
|
||||
add_path: None,
|
||||
}];
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn accept(listener: &TcpListener) -> Result<TestPeer> {
|
||||
let (stream, _) = tokio::time::timeout(TIMEOUT, listener.accept()).await??;
|
||||
Ok(TestPeer::from_stream(stream))
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_server_dials_configured_peer_and_establishes() -> Result<()> {
|
||||
let ServerWithPeer { server, listener } = server_dialing(PEER_ASN).await?;
|
||||
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await?;
|
||||
let server = server_dialing(&listener, PEER_ASN).await?;
|
||||
|
||||
let mut peer = TestPeer::accept(&listener).await?;
|
||||
let mut peer = accept(&listener).await?;
|
||||
// Both sides send OPEN on connect; the test peer's handshake order works
|
||||
// whichever side opened TCP.
|
||||
let open = peer.handshake(PEER_ASN, PEER_ID, 90).await?;
|
||||
let open = peer
|
||||
.handshake(PEER_ASN, Ipv4Addr::new(10, 0, 0, 2), 90)
|
||||
.await?;
|
||||
assert_eq!(open.asn, LOCAL_ASN as u16);
|
||||
|
||||
let listed = server.wait_for_session().await?;
|
||||
let mut events = server.registry.subscribe();
|
||||
// Either the Up event is still queued or the session is already listed.
|
||||
let listed = tokio::time::timeout(TIMEOUT, async {
|
||||
loop {
|
||||
if let Some(s) = server.registry.list().first().cloned() {
|
||||
return s;
|
||||
}
|
||||
let _ = events.recv().await;
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
assert_eq!(listed.name.as_deref(), Some("r1"));
|
||||
assert_eq!(listed.peer.asn, PEER_ASN);
|
||||
assert_eq!(listed.peer.addr, listener.local_addr()?);
|
||||
@@ -67,26 +88,39 @@ async fn test_server_dials_configured_peer_and_establishes() -> Result<()> {
|
||||
server
|
||||
.local_rib
|
||||
.announce(LocalRoute::new(
|
||||
prefix("10.100.1.0/24"),
|
||||
IpPrefix::try_from("10.100.1.0/24")?,
|
||||
LocalRouteAttrs::default(),
|
||||
))
|
||||
.await?;
|
||||
let update = peer.recv_update().await?;
|
||||
let update = tokio::time::timeout(TIMEOUT, peer.recv_update()).await??;
|
||||
assert_eq!(update.announced_routes[0].to_string(), "10.100.1.0/24");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_server_reconnects_after_peer_closes() -> Result<()> {
|
||||
let ServerWithPeer { server, listener } = server_dialing(PEER_ASN).await?;
|
||||
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await?;
|
||||
let server = server_dialing(&listener, PEER_ASN).await?;
|
||||
|
||||
let mut first = TestPeer::accept(&listener).await?;
|
||||
first.handshake(PEER_ASN, PEER_ID, 90).await?;
|
||||
server.wait_for_session().await?;
|
||||
let mut first = accept(&listener).await?;
|
||||
first
|
||||
.handshake(PEER_ASN, Ipv4Addr::new(10, 0, 0, 2), 90)
|
||||
.await?;
|
||||
tokio::time::timeout(TIMEOUT, async {
|
||||
while server.registry.list().is_empty() {
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
drop(first);
|
||||
|
||||
// Session goes away, then the dialer comes back.
|
||||
server.wait_for_sessions(0).await?;
|
||||
tokio::time::timeout(TIMEOUT, async {
|
||||
while !server.registry.list().is_empty() {
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
let (_, state) = server
|
||||
.registry
|
||||
.configured_peers()
|
||||
@@ -95,30 +129,45 @@ async fn test_server_reconnects_after_peer_closes() -> Result<()> {
|
||||
.unwrap();
|
||||
assert!(matches!(state, DialState::Backoff { .. }), "{state:?}");
|
||||
|
||||
let mut second = TestPeer::accept(&listener).await?;
|
||||
second.handshake(PEER_ASN, PEER_ID, 90).await?;
|
||||
server.wait_for_session().await?;
|
||||
let mut second = accept(&listener).await?;
|
||||
second
|
||||
.handshake(PEER_ASN, Ipv4Addr::new(10, 0, 0, 2), 90)
|
||||
.await?;
|
||||
tokio::time::timeout(TIMEOUT, async {
|
||||
while server.registry.list().is_empty() {
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_wrong_asn_is_refused_with_bad_peer_as() -> Result<()> {
|
||||
let ServerWithPeer {
|
||||
server: _server,
|
||||
listener,
|
||||
} = server_dialing(PEER_ASN).await?;
|
||||
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await?;
|
||||
let _server = server_dialing(&listener, PEER_ASN).await?;
|
||||
|
||||
let mut peer = TestPeer::accept(&listener).await?;
|
||||
peer.send_open(65099, PEER_ID, 90).await?;
|
||||
let mut peer = accept(&listener).await?;
|
||||
peer.send_open(65099, Ipv4Addr::new(10, 0, 0, 2), 90)
|
||||
.await?;
|
||||
let _ = peer.recv_open().await?;
|
||||
let notif = peer.expect_notification().await?;
|
||||
let notif = tokio::time::timeout(TIMEOUT, async {
|
||||
loop {
|
||||
match peer.recv().await? {
|
||||
Message::Notification(n) => return Ok::<_, eyre::Error>(n),
|
||||
Message::KeepAlive => {}
|
||||
other => eyre::bail!("unexpected {other:?}"),
|
||||
}
|
||||
}
|
||||
})
|
||||
.await??;
|
||||
assert!(
|
||||
matches!(notif.error, BgpError::OpenMsg(OpenMsgSubcode::BadPeerAs)),
|
||||
"{notif:?}"
|
||||
);
|
||||
|
||||
// The dialer retries rather than giving up.
|
||||
let _again = TestPeer::accept(&listener).await?;
|
||||
let _again = accept(&listener).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -130,19 +179,23 @@ async fn test_inbound_from_configured_address_uses_config() -> Result<()> {
|
||||
// so the dialer never succeeds; then connect inbound from loopback.
|
||||
let server = TestServer::start_with(LOCAL_ASN, |c| {
|
||||
c.connect_retry = Duration::from_secs(60);
|
||||
let at = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 1);
|
||||
c.peers = vec![PeerConfig {
|
||||
import: ImportPolicy::permissive(),
|
||||
..peer_config("r1", at, PEER_ASN)
|
||||
name: "r1".into(),
|
||||
endpoint: SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 1),
|
||||
remote_asn: PEER_ASN,
|
||||
hold_time_secs: 90,
|
||||
local_address: None,
|
||||
families: FamilySet::supported(),
|
||||
add_path: None,
|
||||
}];
|
||||
})
|
||||
.await?;
|
||||
|
||||
// Wrong ASN inbound → refused.
|
||||
let mut bad = TestPeer::connect(server.addr).await?;
|
||||
bad.send_open(65099, PEER_ID, 90).await?;
|
||||
bad.send_open(65099, Ipv4Addr::new(10, 0, 0, 2), 90).await?;
|
||||
let _ = bad.recv_open().await?;
|
||||
let msg = bad.recv().await?;
|
||||
let msg = tokio::time::timeout(TIMEOUT, bad.recv()).await??;
|
||||
assert!(
|
||||
matches!(&msg, Message::Notification(n) if matches!(n.error, BgpError::OpenMsg(OpenMsgSubcode::BadPeerAs))),
|
||||
"{msg:?}"
|
||||
@@ -150,8 +203,17 @@ async fn test_inbound_from_configured_address_uses_config() -> Result<()> {
|
||||
|
||||
// Right ASN inbound → established, named.
|
||||
let mut good = TestPeer::connect(server.addr).await?;
|
||||
good.handshake(PEER_ASN, PEER_ID, 90).await?;
|
||||
let listed = server.wait_for_session().await?;
|
||||
good.handshake(PEER_ASN, Ipv4Addr::new(10, 0, 0, 2), 90)
|
||||
.await?;
|
||||
let listed = tokio::time::timeout(TIMEOUT, async {
|
||||
loop {
|
||||
if let Some(s) = server.registry.list().first().cloned() {
|
||||
return s;
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
assert_eq!(listed.name.as_deref(), Some("r1"));
|
||||
Ok(())
|
||||
}
|
||||
@@ -161,33 +223,46 @@ async fn test_inbound_from_configured_address_uses_config() -> Result<()> {
|
||||
/// reported as the reason for backing off rather than silently ignored.
|
||||
#[tokio::test]
|
||||
async fn test_local_address_is_used_as_the_source() -> Result<()> {
|
||||
let ServerWithPeer {
|
||||
server: _server,
|
||||
listener,
|
||||
} = TestServer::with_peer(LOCAL_ASN, PEER_ASN, |c, peer| {
|
||||
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await?;
|
||||
let endpoint: SocketAddr = listener.local_addr()?;
|
||||
let _server = TestServer::start_with(LOCAL_ASN, |c| {
|
||||
c.connect_retry = Duration::from_millis(200);
|
||||
peer.import = ImportPolicy::permissive();
|
||||
peer.local_address = Some(Ipv4Addr::LOCALHOST.into());
|
||||
c.peers = vec![PeerConfig {
|
||||
name: "r1".into(),
|
||||
endpoint,
|
||||
remote_asn: PEER_ASN,
|
||||
hold_time_secs: 90,
|
||||
local_address: Some(Ipv4Addr::LOCALHOST.into()),
|
||||
families: FamilySet::supported(),
|
||||
add_path: None,
|
||||
}];
|
||||
})
|
||||
.await?;
|
||||
let (_stream, from) = tokio::time::timeout(TIMEOUT, listener.accept()).await??;
|
||||
assert_eq!(from.ip(), std::net::IpAddr::from(Ipv4Addr::LOCALHOST));
|
||||
|
||||
// 192.0.2.1 is documentation space; no host has it.
|
||||
let endpoint = listener.local_addr()?;
|
||||
let server = TestServer::start_with(LOCAL_ASN, |c| {
|
||||
c.connect_retry = Duration::from_millis(200);
|
||||
c.peers = vec![PeerConfig {
|
||||
name: "r1".into(),
|
||||
endpoint,
|
||||
remote_asn: PEER_ASN,
|
||||
hold_time_secs: 90,
|
||||
local_address: Some(Ipv4Addr::new(192, 0, 2, 1).into()),
|
||||
import: ImportPolicy::permissive(),
|
||||
..peer_config("r1", endpoint, PEER_ASN)
|
||||
families: FamilySet::supported(),
|
||||
add_path: None,
|
||||
}];
|
||||
})
|
||||
.await?;
|
||||
let reason = wait_for("the dialer is backing off", || {
|
||||
match server.registry.configured_peers().into_iter().next() {
|
||||
Some((_, DialState::Backoff { last_error, .. })) => Some(last_error),
|
||||
_ => None,
|
||||
let reason = tokio::time::timeout(TIMEOUT, async {
|
||||
loop {
|
||||
if let Some((_, DialState::Backoff { last_error, .. })) =
|
||||
server.registry.configured_peers().into_iter().next()
|
||||
{
|
||||
return last_error;
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(20)).await;
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
@@ -202,14 +277,16 @@ async fn test_local_address_is_used_as_the_source() -> Result<()> {
|
||||
/// such a peer was dialled every few seconds for ever.
|
||||
#[tokio::test]
|
||||
async fn test_a_peer_that_refuses_the_session_is_dialled_ever_more_slowly() -> Result<()> {
|
||||
let ServerWithPeer { server, listener } = server_dialing(PEER_ASN).await?;
|
||||
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await?;
|
||||
let server = server_dialing(&listener, PEER_ASN).await?;
|
||||
|
||||
let mut waits = Vec::new();
|
||||
for _ in 0..3 {
|
||||
let mut peer = TestPeer::accept(&listener).await?;
|
||||
peer.send_open(65099, PEER_ID, 90).await?;
|
||||
let mut peer = accept(&listener).await?;
|
||||
peer.send_open(65099, Ipv4Addr::new(10, 0, 0, 2), 90)
|
||||
.await?;
|
||||
let _ = peer.recv_open().await?;
|
||||
let refusal = peer.recv().await?;
|
||||
let refusal = tokio::time::timeout(TIMEOUT, peer.recv()).await??;
|
||||
assert!(
|
||||
matches!(&refusal, Message::Notification(n) if n.error == BgpError::OpenMsg(OpenMsgSubcode::BadPeerAs)),
|
||||
"{refusal:?}"
|
||||
@@ -217,17 +294,17 @@ async fn test_a_peer_that_refuses_the_session_is_dialled_ever_more_slowly() -> R
|
||||
drop(peer);
|
||||
|
||||
// How long the dialer says it will wait before trying again.
|
||||
let wait = wait_for("the dialer is backing off", || {
|
||||
match server.registry.configured_peers().into_iter().next() {
|
||||
Some((_, DialState::Backoff { until, last_error })) => {
|
||||
let wait = tokio::time::timeout(TIMEOUT, async {
|
||||
loop {
|
||||
if let Some((_, DialState::Backoff { until, last_error })) =
|
||||
server.registry.configured_peers().into_iter().next()
|
||||
{
|
||||
assert!(last_error.contains("AS65099"), "{last_error}");
|
||||
Some(
|
||||
until
|
||||
.duration_since(std::time::SystemTime::now())
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
return until
|
||||
.duration_since(std::time::SystemTime::now())
|
||||
.unwrap_or_default();
|
||||
}
|
||||
_ => None,
|
||||
tokio::time::sleep(Duration::from_millis(5)).await;
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
@@ -240,45 +317,3 @@ async fn test_a_peer_that_refuses_the_session_is_dialled_ever_more_slowly() -> R
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A passive peer is not dialled: its session comes up when it connects,
|
||||
/// and until then, and after, it is shown as waiting, with how its last
|
||||
/// session ended.
|
||||
#[tokio::test]
|
||||
async fn test_a_passive_peer_is_not_dialled_and_comes_up_when_it_connects() -> Result<()> {
|
||||
let ServerWithPeer { server, listener } = server_with_peer(PEER_ASN, true).await?;
|
||||
let state = || server.registry.configured_peers()[0].1.clone();
|
||||
|
||||
// Many retry intervals pass, and nothing connects.
|
||||
assert!(
|
||||
tokio::time::timeout(Duration::from_secs(1), listener.accept())
|
||||
.await
|
||||
.is_err(),
|
||||
"a passive peer was dialled"
|
||||
);
|
||||
assert_eq!(state(), DialState::Passive { last_error: None });
|
||||
|
||||
let mut peer = TestPeer::connect(server.addr).await?;
|
||||
peer.handshake(PEER_ASN, PEER_ID, 90).await?;
|
||||
let listed = server.wait_for_session().await?;
|
||||
assert_eq!(listed.name.as_deref(), Some("r1"));
|
||||
|
||||
drop(peer);
|
||||
wait_until("the peer is shown as waiting, with how it ended", || {
|
||||
matches!(
|
||||
state(),
|
||||
DialState::Passive {
|
||||
last_error: Some(_)
|
||||
}
|
||||
)
|
||||
})
|
||||
.await?;
|
||||
assert!(server.registry.list().is_empty());
|
||||
assert!(
|
||||
tokio::time::timeout(Duration::from_millis(600), listener.accept())
|
||||
.await
|
||||
.is_err(),
|
||||
"a passive peer was dialled after its session ended"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
//! Export limits, end to end: what a peer above or beside us is told is
|
||||
//! held to the prefixes they list and to as many routes as they allow,
|
||||
//! whatever else would send it more.
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
use std::net::Ipv4Addr;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use bgp_integration_tests::test_peer::TestPeer;
|
||||
use bgp_integration_tests::test_server::{ServerWithPeer, TestServer};
|
||||
use bgp_integration_tests::{TIMEOUT, prefix};
|
||||
use bgp_packet::errors::{BgpError, CeaseSubcode};
|
||||
use bgp_packet::message::Message;
|
||||
use bgp_packet::open::{BgpRole, Capability};
|
||||
use bgp_primitives::constants::AfiSafi;
|
||||
use bgp_rib::LocalRoute;
|
||||
use bgp_server::policy::{ExportLimits, PeerRole, PrefixSet};
|
||||
use eyre::{Result, bail};
|
||||
|
||||
const LOCAL_ASN: u32 = 1299;
|
||||
const PEER_ASN: u32 = 3333;
|
||||
const PEER_ID: Ipv4Addr = Ipv4Addr::new(10, 0, 0, 2);
|
||||
|
||||
/// A server that originates `originated`, with one configured peer at the
|
||||
/// address the test connects from, which is a peer of ours held to
|
||||
/// `limits`.
|
||||
async fn server_with(originated: &[&str], limits: ExportLimits) -> Result<ServerWithPeer> {
|
||||
let originate = originated
|
||||
.iter()
|
||||
.map(|p| LocalRoute::new(prefix(p), Default::default()))
|
||||
.collect();
|
||||
TestServer::with_peer(LOCAL_ASN, PEER_ASN, |c, peer| {
|
||||
c.originate = originate;
|
||||
peer.export_limits = Some(Arc::new(limits));
|
||||
peer.role = Some(PeerRole::Peer);
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// A session with the server, as its peer.
|
||||
async fn established(server: &TestServer) -> Result<TestPeer> {
|
||||
let capabilities = vec![
|
||||
Capability::FourByteAsn { asn: PEER_ASN },
|
||||
Capability::multiprotocol(AfiSafi::IPV4_UNICAST),
|
||||
Capability::multiprotocol(AfiSafi::IPV6_UNICAST),
|
||||
Capability::BgpRole {
|
||||
role: BgpRole::Peer,
|
||||
},
|
||||
];
|
||||
server
|
||||
.established_with(PEER_ASN, PEER_ID, capabilities)
|
||||
.await
|
||||
}
|
||||
|
||||
/// What the peer has been told once it has been quiet for a while: the
|
||||
/// prefixes it holds, and the NOTIFICATION that ended the session, if one
|
||||
/// did.
|
||||
async fn told(peer: &mut TestPeer) -> (BTreeSet<String>, Option<(BgpError, Vec<u8>)>) {
|
||||
let mut routes = BTreeSet::new();
|
||||
while let Ok(message) = peer.recv_within(Duration::from_millis(500)).await {
|
||||
match message {
|
||||
Message::Update(update) => {
|
||||
for route in &update.withdrawn_routes {
|
||||
routes.remove(&route.prefix.to_string());
|
||||
}
|
||||
for route in &update.announced_routes {
|
||||
routes.insert(route.prefix.to_string());
|
||||
}
|
||||
}
|
||||
Message::Notification(notification) => {
|
||||
return (routes, Some((notification.error, notification.data)));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
(routes, None)
|
||||
}
|
||||
|
||||
fn allowing(prefixes: &[&str]) -> Option<PrefixSet> {
|
||||
let mut set = PrefixSet::default();
|
||||
for p in prefixes {
|
||||
let p = prefix(p);
|
||||
set.allow(p, p.length());
|
||||
}
|
||||
Some(set)
|
||||
}
|
||||
|
||||
/// A route of ours whose prefix is not in the peer's export limits is not
|
||||
/// announced to it; the one that is, is.
|
||||
#[tokio::test]
|
||||
async fn test_a_route_outside_the_export_limits_is_not_announced() -> Result<()> {
|
||||
let limits = ExportLimits {
|
||||
prefixes: allowing(&["193.0.0.0/21"]),
|
||||
max_prefixes: None,
|
||||
};
|
||||
let ServerWithPeer { server, .. } =
|
||||
server_with(&["193.0.0.0/21", "198.51.100.0/24"], limits).await?;
|
||||
let mut peer = established(&server).await?;
|
||||
let (routes, ended) = told(&mut peer).await;
|
||||
assert_eq!(routes, BTreeSet::from(["193.0.0.0/21".to_string()]));
|
||||
assert_eq!(ended, None);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// One route more than the export limit allows is not announced: the
|
||||
/// session ends with an Administrative Shutdown that says why (RFC 9003),
|
||||
/// and the peer is kept down, so that it is told nothing rather than too
|
||||
/// much.
|
||||
#[tokio::test]
|
||||
async fn test_going_over_the_export_limit_ends_the_session_and_keeps_the_peer_down() -> Result<()> {
|
||||
let limits = ExportLimits {
|
||||
prefixes: None,
|
||||
max_prefixes: Some(2),
|
||||
};
|
||||
let ServerWithPeer { server, .. } =
|
||||
server_with(&["193.0.0.0/21", "193.0.8.0/21"], limits).await?;
|
||||
let mut peer = established(&server).await?;
|
||||
let (routes, ended) = told(&mut peer).await;
|
||||
assert_eq!(routes.len(), 2);
|
||||
assert_eq!(ended, None);
|
||||
|
||||
server
|
||||
.local_rib
|
||||
.announce(LocalRoute::new(prefix("193.0.16.0/21"), Default::default()))
|
||||
.await?;
|
||||
let (more, ended) = told(&mut peer).await;
|
||||
assert_eq!(more, BTreeSet::new(), "nothing more is announced");
|
||||
let Some((error, data)) = ended else {
|
||||
bail!("the session was not ended");
|
||||
};
|
||||
assert_eq!(error, BgpError::Cease(CeaseSubcode::AdminShutdown));
|
||||
let text = String::from_utf8(data[1..].to_vec())?;
|
||||
assert!(
|
||||
text.starts_with("export limit of 2 routes reached"),
|
||||
"{text}"
|
||||
);
|
||||
assert!(peer.closed_within(TIMEOUT).await);
|
||||
|
||||
// The peer is kept down: a new connection is closed unanswered.
|
||||
let mut again = TestPeer::connect(server.addr).await?;
|
||||
assert!(
|
||||
again.closed_unanswered().await,
|
||||
"a held-down peer gets no session"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
@@ -6,24 +6,52 @@
|
||||
//! stops with an error, for whatever supervises it to restart it.
|
||||
|
||||
use std::net::Ipv4Addr;
|
||||
use std::time::Duration;
|
||||
|
||||
use bgp_integration_tests::TIMEOUT;
|
||||
use bgp_integration_tests::test_peer::TestPeer;
|
||||
use bgp_integration_tests::test_server::TestServer;
|
||||
use bgp_packet::errors::{BgpError, CeaseSubcode};
|
||||
use bgp_packet::message::Message;
|
||||
use bgp_rib::RibStopped;
|
||||
use bgp_server::error::ServerError;
|
||||
use eyre::Result;
|
||||
use eyre::{Result, bail};
|
||||
|
||||
const LOCAL_ASN: u32 = 65000;
|
||||
const PEER_ASN: u32 = 65001;
|
||||
const TIMEOUT: Duration = Duration::from_secs(5);
|
||||
|
||||
async fn established(server: &TestServer) -> Result<TestPeer> {
|
||||
let mut peer = TestPeer::connect(server.addr).await?;
|
||||
peer.handshake(PEER_ASN, Ipv4Addr::new(10, 0, 0, 1), 90)
|
||||
.await?;
|
||||
tokio::time::timeout(TIMEOUT, async {
|
||||
while server.registry.list().is_empty() {
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
Ok(peer)
|
||||
}
|
||||
|
||||
/// Reads until the server's NOTIFICATION, skipping KEEPALIVEs.
|
||||
async fn farewell(peer: &mut TestPeer) -> Result<BgpError> {
|
||||
tokio::time::timeout(TIMEOUT, async {
|
||||
loop {
|
||||
match peer.recv().await? {
|
||||
Message::KeepAlive => {}
|
||||
Message::Notification(n) => return Ok(n.error),
|
||||
other => bail!("expected NOTIFICATION, got {other:?}"),
|
||||
}
|
||||
}
|
||||
})
|
||||
.await?
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_losing_the_local_rib_stops_the_server_and_tells_the_peers() -> Result<()> {
|
||||
let mut server = TestServer::start(LOCAL_ASN).await?;
|
||||
let (base, _http) = server.serve_http().await?;
|
||||
let mut peer = server
|
||||
.established(PEER_ASN, Ipv4Addr::new(10, 0, 0, 1))
|
||||
.await?;
|
||||
let mut peer = established(&server).await?;
|
||||
|
||||
server.local_rib.stop_for_testing();
|
||||
|
||||
@@ -33,7 +61,7 @@ async fn test_losing_the_local_rib_stops_the_server_and_tells_the_peers() -> Res
|
||||
"{outcome:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
peer.expect_notification().await?.error,
|
||||
farewell(&mut peer).await?,
|
||||
BgpError::Cease(CeaseSubcode::AdminShutdown)
|
||||
);
|
||||
|
||||
|
||||
@@ -1,332 +0,0 @@
|
||||
//! Graceful restart (RFC 4724, RFC 8538): a peer that restarts keeps its
|
||||
//! routes with us while it does, and we keep ours with it while we do.
|
||||
|
||||
use std::net::Ipv4Addr;
|
||||
use std::time::Duration;
|
||||
|
||||
use bgp_integration_tests::announcement::{Announcement, announcement};
|
||||
use bgp_integration_tests::test_peer::TestPeer;
|
||||
use bgp_integration_tests::test_server::TestServer;
|
||||
use bgp_integration_tests::{TIMEOUT, wait_until as until};
|
||||
use bgp_packet::errors::{BgpError, CeaseSubcode};
|
||||
use bgp_packet::message::{Message, NotificationMessage, UpdateMessage};
|
||||
use bgp_packet::open::{Capability, GracefulRestartEntry};
|
||||
use bgp_primitives::constants::{Afi, AfiSafi, Safi};
|
||||
use bgp_server::restart::Leave;
|
||||
use eyre::{Result, bail};
|
||||
|
||||
const PEER_ASN: u32 = 65001;
|
||||
const PEER_ID: Ipv4Addr = Ipv4Addr::new(10, 0, 0, 2);
|
||||
const PREFIXES: [&str; 3] = ["10.1.0.0/16", "10.2.0.0/16", "10.3.0.0/16"];
|
||||
|
||||
/// The graceful restart capability of a peer that speaks IPv4 and IPv6.
|
||||
fn restart(restarted: bool, notification: bool, forwarding: bool, time: u16) -> Capability {
|
||||
Capability::GracefulRestart {
|
||||
restart_state: restarted,
|
||||
restart_time: time,
|
||||
notification,
|
||||
entries: [Afi::IPV4, Afi::IPV6]
|
||||
.into_iter()
|
||||
.map(|afi| GracefulRestartEntry {
|
||||
afi,
|
||||
safi: Safi::UNICAST,
|
||||
preserve_forwarding: forwarding,
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
/// What a dual-stack peer offers, with `more` besides.
|
||||
fn capabilities(more: impl IntoIterator<Item = Capability>) -> Vec<Capability> {
|
||||
let mut capabilities = vec![
|
||||
Capability::FourByteAsn { asn: PEER_ASN },
|
||||
Capability::MultiProtocol {
|
||||
afi: Afi::IPV4,
|
||||
safi: Safi::UNICAST,
|
||||
},
|
||||
Capability::MultiProtocol {
|
||||
afi: Afi::IPV6,
|
||||
safi: Safi::UNICAST,
|
||||
},
|
||||
Capability::RouteRefresh,
|
||||
];
|
||||
capabilities.extend(more);
|
||||
capabilities
|
||||
}
|
||||
|
||||
/// The peer's own routes.
|
||||
fn own(prefixes: &[&str]) -> Announcement {
|
||||
announcement(prefixes, &[PEER_ASN])
|
||||
}
|
||||
|
||||
fn end_of_rib(family: AfiSafi) -> UpdateMessage {
|
||||
UpdateMessage::end_of_rib(family)
|
||||
}
|
||||
|
||||
/// The prefixes in the server's table, and how many of its routes are
|
||||
/// stale.
|
||||
fn table(server: &TestServer) -> (Vec<String>, usize) {
|
||||
let mut prefixes = Vec::new();
|
||||
let mut stale = 0;
|
||||
server.rib.for_each_route(|route| {
|
||||
prefixes.push(route.prefix.to_string());
|
||||
stale += usize::from(route.stale);
|
||||
});
|
||||
prefixes.sort();
|
||||
(prefixes, stale)
|
||||
}
|
||||
|
||||
/// A peer that has announced `PREFIXES` with graceful restart as `offer`
|
||||
/// says, and is Established.
|
||||
async fn peer_with_routes(server: &TestServer, offer: Capability) -> Result<TestPeer> {
|
||||
let mut peer = TestPeer::connect(server.addr).await?;
|
||||
peer.handshake_with(PEER_ASN, PEER_ID, 90, capabilities([offer]))
|
||||
.await?;
|
||||
peer.announce(own(&PREFIXES)).await?;
|
||||
until("the routes are in", || server.rib.len() == 3).await?;
|
||||
Ok(peer)
|
||||
}
|
||||
|
||||
/// The peer's connection fails; its routes are kept, stale, and used as
|
||||
/// before. It comes back, sends some of them again, and its End-of-RIB:
|
||||
/// the others go.
|
||||
#[tokio::test]
|
||||
async fn test_a_restarting_peer_keeps_its_routes_until_it_has_sent_them_again() -> Result<()> {
|
||||
let server = TestServer::start(65000).await?;
|
||||
let peer = peer_with_routes(&server, restart(false, true, true, 90)).await?;
|
||||
drop(peer);
|
||||
|
||||
until("the session is down", || server.registry.list().is_empty()).await?;
|
||||
until("the routes are stale", || table(&server).1 == 3).await?;
|
||||
let kept = server.restarts.kept();
|
||||
assert_eq!(kept.len(), 1);
|
||||
assert_eq!(kept[0].1.routes, 3);
|
||||
// Still chosen: the RFC has stale routes used like any others.
|
||||
assert!(server.received_routes().iter().all(|route| route.best));
|
||||
// And shown, as the peer's, stale.
|
||||
let (base, _http) = server.serve_http().await?;
|
||||
let received: serde_json::Value = reqwest::get(format!("{base}/api/routes/received"))
|
||||
.await?
|
||||
.json()
|
||||
.await?;
|
||||
let routes = received["routes"].as_array().unwrap();
|
||||
assert_eq!(routes.len(), 3, "{received}");
|
||||
for route in routes {
|
||||
assert_eq!(route["stale"], true, "{route}");
|
||||
assert_eq!(route["peer_asn"], PEER_ASN, "{route}");
|
||||
}
|
||||
|
||||
let mut peer = TestPeer::connect(server.addr).await?;
|
||||
peer.handshake_with(
|
||||
PEER_ASN,
|
||||
PEER_ID,
|
||||
90,
|
||||
capabilities([restart(true, true, true, 90)]),
|
||||
)
|
||||
.await?;
|
||||
until("the routes are taken over", || {
|
||||
server.registry.list().len() == 1 && server.restarts.kept().is_empty()
|
||||
})
|
||||
.await?;
|
||||
peer.announce(own(&PREFIXES[..2])).await?;
|
||||
until("two are fresh", || table(&server).1 == 1).await?;
|
||||
// The peer has them all, as far as its limit goes.
|
||||
assert_eq!(server.registry.list()[0].counters.routes(), 3);
|
||||
|
||||
peer.announce(end_of_rib(AfiSafi::IPV4_UNICAST)).await?;
|
||||
until("the one not sent again is gone", || {
|
||||
table(&server) == (vec!["10.1.0.0/16".into(), "10.2.0.0/16".into()], 0)
|
||||
})
|
||||
.await?;
|
||||
until("it is counted out", || {
|
||||
server
|
||||
.registry
|
||||
.list()
|
||||
.first()
|
||||
.is_some_and(|session| session.counters.routes() == 2)
|
||||
})
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// What ends a session decides whether the peer is restarting: a Hard
|
||||
/// Reset never is, and a NOTIFICATION is only where both sides said so
|
||||
/// (RFC 8538). A peer that does not offer graceful restart never is.
|
||||
#[tokio::test]
|
||||
async fn test_only_a_restart_keeps_the_routes() -> Result<()> {
|
||||
let cease = |subcode| NotificationMessage {
|
||||
error: BgpError::Cease(subcode),
|
||||
data: vec![],
|
||||
};
|
||||
let hard_reset = NotificationMessage {
|
||||
error: BgpError::Cease(CeaseSubcode::HardReset),
|
||||
data: vec![6, 2],
|
||||
};
|
||||
for (offer, goodbye, kept) in [
|
||||
(
|
||||
Some(restart(false, true, true, 90)),
|
||||
Some(cease(CeaseSubcode::AdminReset)),
|
||||
true,
|
||||
),
|
||||
(
|
||||
Some(restart(false, true, true, 90)),
|
||||
Some(hard_reset.clone()),
|
||||
false,
|
||||
),
|
||||
(
|
||||
Some(restart(false, false, true, 90)),
|
||||
Some(cease(CeaseSubcode::AdminReset)),
|
||||
false,
|
||||
),
|
||||
(Some(restart(false, false, true, 90)), None, true),
|
||||
(Some(restart(false, true, true, 0)), None, false),
|
||||
(None, None, false),
|
||||
] {
|
||||
let server = TestServer::start(65000).await?;
|
||||
let mut peer = TestPeer::connect(server.addr).await?;
|
||||
peer.handshake_with(PEER_ASN, PEER_ID, 90, capabilities(offer))
|
||||
.await?;
|
||||
peer.announce(own(&PREFIXES)).await?;
|
||||
until("the routes are in", || server.rib.len() == 3).await?;
|
||||
if let Some(goodbye) = &goodbye {
|
||||
peer.send(&Message::Notification(goodbye.clone())).await?;
|
||||
}
|
||||
drop(peer);
|
||||
until("the session is down", || server.registry.list().is_empty()).await?;
|
||||
if kept {
|
||||
until("the routes are stale", || table(&server).1 == 3).await?;
|
||||
} else {
|
||||
until("the routes are gone", || server.rib.is_empty()).await?;
|
||||
assert!(server.restarts.kept().is_empty());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A peer that does not come back within its restart time has its routes
|
||||
/// removed then.
|
||||
#[tokio::test]
|
||||
async fn test_the_routes_go_when_the_peer_does_not_come_back_in_time() -> Result<()> {
|
||||
let server = TestServer::start(65000).await?;
|
||||
let peer = peer_with_routes(&server, restart(false, true, true, 1)).await?;
|
||||
drop(peer);
|
||||
until("the routes are stale", || table(&server).1 == 3).await?;
|
||||
until("the routes are gone", || server.rib.is_empty()).await?;
|
||||
assert!(server.restarts.kept().is_empty());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A peer that comes back without having kept its forwarding (F bit), or
|
||||
/// without graceful restart at all, has its stale routes removed at once.
|
||||
#[tokio::test]
|
||||
async fn test_routes_whose_forwarding_was_lost_go_when_the_peer_is_back() -> Result<()> {
|
||||
for back_with in [Some(restart(true, true, false, 90)), None] {
|
||||
let server = TestServer::start(65000).await?;
|
||||
let peer = peer_with_routes(&server, restart(false, true, true, 90)).await?;
|
||||
drop(peer);
|
||||
until("the routes are stale", || table(&server).1 == 3).await?;
|
||||
|
||||
let mut peer = TestPeer::connect(server.addr).await?;
|
||||
peer.handshake_with(PEER_ASN, PEER_ID, 90, capabilities(back_with.clone()))
|
||||
.await?;
|
||||
until("the stale routes are gone", || server.rib.is_empty()).await?;
|
||||
until("they are counted out", || {
|
||||
server
|
||||
.registry
|
||||
.list()
|
||||
.first()
|
||||
.is_some_and(|session| session.counters.routes() == 0)
|
||||
})
|
||||
.await?;
|
||||
drop(peer);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Our OPEN offers graceful restart, as just restarted, and a peer that
|
||||
/// agreed is sent End-of-RIB markers once it has our routes.
|
||||
#[tokio::test]
|
||||
async fn test_we_offer_graceful_restart_and_say_when_our_routes_are_all_sent() -> Result<()> {
|
||||
let server = TestServer::start(65000).await?;
|
||||
let mut peer = TestPeer::connect(server.addr).await?;
|
||||
let open = peer
|
||||
.handshake_with(
|
||||
PEER_ASN,
|
||||
PEER_ID,
|
||||
90,
|
||||
capabilities([restart(false, true, true, 90)]),
|
||||
)
|
||||
.await?;
|
||||
let offered = open.capabilities().find_map(|capability| match capability {
|
||||
Capability::GracefulRestart {
|
||||
restart_state,
|
||||
restart_time,
|
||||
notification,
|
||||
entries,
|
||||
} => Some((
|
||||
*restart_state,
|
||||
*restart_time,
|
||||
*notification,
|
||||
entries.clone(),
|
||||
)),
|
||||
_ => None,
|
||||
});
|
||||
let Some((restarted, time, notification, entries)) = offered else {
|
||||
bail!("no graceful restart in {open:?}");
|
||||
};
|
||||
assert!(restarted, "started just now");
|
||||
assert_eq!(time, 120);
|
||||
assert!(notification);
|
||||
// Without kernel routes, our forwarding is not ours to lose.
|
||||
assert!(entries.iter().all(|entry| entry.preserve_forwarding));
|
||||
|
||||
// Nothing is originated: the markers are all there is.
|
||||
let mut markers = Vec::new();
|
||||
while markers.len() < 2 {
|
||||
let update = peer.recv_update().await?;
|
||||
match update.end_of_rib_family() {
|
||||
Some(family) => markers.push(family),
|
||||
None => bail!("expected End-of-RIB, got {update:?}"),
|
||||
}
|
||||
}
|
||||
markers.sort();
|
||||
assert_eq!(markers, [AfiSafi::IPV4_UNICAST, AfiSafi::IPV6_UNICAST]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Leaving to restart closes the connection without a NOTIFICATION where
|
||||
/// graceful restart was agreed, so that the peer keeps our routes; leaving
|
||||
/// for good is a Hard Reset there, and a Cease elsewhere.
|
||||
#[tokio::test]
|
||||
async fn test_leaving_tells_each_peer_as_graceful_restart_has_it() -> Result<()> {
|
||||
let hard_reset = BgpError::Cease(CeaseSubcode::HardReset);
|
||||
let shutdown = BgpError::Cease(CeaseSubcode::AdminShutdown);
|
||||
for (leave, offer, told) in [
|
||||
(Leave::Restart, Some(restart(false, true, true, 90)), None),
|
||||
(Leave::Restart, None, Some(shutdown.clone())),
|
||||
(
|
||||
Leave::Stop,
|
||||
Some(restart(false, true, true, 90)),
|
||||
Some(hard_reset),
|
||||
),
|
||||
(Leave::Stop, None, Some(shutdown)),
|
||||
] {
|
||||
let server = TestServer::start(65000).await?;
|
||||
let mut peer = TestPeer::connect(server.addr).await?;
|
||||
peer.handshake_with(PEER_ASN, PEER_ID, 90, capabilities(offer.clone()))
|
||||
.await?;
|
||||
until("the session is up", || server.registry.list().len() == 1).await?;
|
||||
server.registry.leave(leave, Duration::from_secs(2)).await;
|
||||
let mut notification = None;
|
||||
// Whatever came before (End-of-RIB markers) is passed over.
|
||||
while let Ok(message) = peer.recv().await {
|
||||
if let Message::Notification(n) = message {
|
||||
notification = Some(n.error);
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert_eq!(notification, told, "{leave:?} with {offer:?}");
|
||||
assert!(peer.closed_within(TIMEOUT).await);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -3,22 +3,46 @@
|
||||
use std::net::Ipv4Addr;
|
||||
use std::time::Duration;
|
||||
|
||||
use bgp_integration_tests::announcement::announcement;
|
||||
use bgp_integration_tests::test_peer::TestPeer;
|
||||
use bgp_integration_tests::test_server::TestServer;
|
||||
use bgp_integration_tests::{TIMEOUT, get_json, prefix};
|
||||
use bgp_packet::message::LargeCommunity;
|
||||
use bgp_packet::message::{
|
||||
AsPathAttribute, CommunitiesPathAttribute, LargeCommunity, Message, NextHopPathAttribute,
|
||||
OriginPathAttribute, PathAttribute, UpdateMessage,
|
||||
};
|
||||
use bgp_primitives::ip_prefix::IpPrefix;
|
||||
use bgp_rib::{LocalRoute, LocalRouteAttrs};
|
||||
use eyre::Result;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
const LOCAL_ASN: u32 = 65000;
|
||||
const PEER_ASN: u32 = 65001;
|
||||
const TIMEOUT: Duration = Duration::from_secs(5);
|
||||
|
||||
fn prefix(s: &str) -> IpPrefix {
|
||||
IpPrefix::try_from(s).unwrap()
|
||||
}
|
||||
|
||||
async fn established_peer(server: &TestServer) -> Result<TestPeer> {
|
||||
server
|
||||
.established(PEER_ASN, Ipv4Addr::new(10, 0, 0, 1))
|
||||
.await
|
||||
let mut peer = TestPeer::connect(server.addr).await?;
|
||||
peer.handshake(PEER_ASN, Ipv4Addr::new(10, 0, 0, 1), 90)
|
||||
.await?;
|
||||
tokio::time::timeout(TIMEOUT, async {
|
||||
while server.registry.list().is_empty() {
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
Ok(peer)
|
||||
}
|
||||
|
||||
async fn get_json(client: &reqwest::Client, url: &str) -> Result<Value> {
|
||||
Ok(client
|
||||
.get(url)
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?
|
||||
.json()
|
||||
.await?)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -52,27 +76,6 @@ async fn test_status_and_peers() -> Result<()> {
|
||||
peers[0]["address_families"],
|
||||
serde_json::json!(["IPv4 unicast", "IPv6 unicast"])
|
||||
);
|
||||
|
||||
// The session's TCP connection, by the address the peer list gives.
|
||||
// What the kernel says of it depends on the platform; that it was
|
||||
// asked, and why it had nothing to say if it had not, does not.
|
||||
let peer_addr = peers[0]["peer_addr"].as_str().unwrap();
|
||||
let tcp = get_json(&client, &format!("{base}/api/tcp?peer={peer_addr}")).await?;
|
||||
let available = tcp["available"].as_bool().expect("available is a bool");
|
||||
if available {
|
||||
assert!(tcp["connection"].is_object(), "{tcp}");
|
||||
} else {
|
||||
assert!(tcp["reason"].is_string(), "{tcp}");
|
||||
}
|
||||
// The address alone names the peer too, as it does elsewhere.
|
||||
let by_address = get_json(&client, &format!("{base}/api/tcp?peer=127.0.0.1")).await?;
|
||||
assert_eq!(by_address["available"], tcp["available"]);
|
||||
// A peer there is no session with is a bad request, not an answer.
|
||||
let nobody = client
|
||||
.get(format!("{base}/api/tcp?peer=nobody"))
|
||||
.send()
|
||||
.await?;
|
||||
assert_eq!(nobody.status(), 400);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -152,17 +155,21 @@ async fn test_received_routes_summary() -> Result<()> {
|
||||
let client = reqwest::Client::new();
|
||||
let mut peer = established_peer(&server).await?;
|
||||
|
||||
peer.announce(
|
||||
announcement(&["203.0.113.0/24"], &[PEER_ASN, 65002])
|
||||
.with_next_hop(Ipv4Addr::new(10, 0, 0, 1))
|
||||
.with_communities(&[(65001, 7)]),
|
||||
)
|
||||
peer.send(&Message::Update(UpdateMessage {
|
||||
withdrawn_routes: vec![],
|
||||
path_attributes: vec![
|
||||
PathAttribute::Origin(OriginPathAttribute::IGP),
|
||||
AsPathAttribute::from_asns(vec![PEER_ASN, 65002]),
|
||||
PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::new(10, 0, 0, 1))),
|
||||
PathAttribute::Communities(CommunitiesPathAttribute(vec![(65001, 7)])),
|
||||
],
|
||||
announced_routes: vec![prefix("203.0.113.0/24").into()],
|
||||
}))
|
||||
.await?;
|
||||
|
||||
let received = tokio::time::timeout(TIMEOUT, async {
|
||||
loop {
|
||||
let v =
|
||||
get_json(&client, &format!("{base}/api/routes/received")).await?["routes"].take();
|
||||
let v = get_json(&client, &format!("{base}/api/routes/received")).await?;
|
||||
if !v.as_array().unwrap().is_empty() {
|
||||
return Ok::<_, eyre::Error>(v);
|
||||
}
|
||||
|
||||
@@ -1,456 +0,0 @@
|
||||
//! What is taken from a peer: the checks every route goes through, and the
|
||||
//! limit on how many a peer may have.
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
use std::net::Ipv4Addr;
|
||||
use std::time::Duration;
|
||||
|
||||
use bgp_integration_tests::announcement::announcement;
|
||||
use bgp_integration_tests::test_peer::TestPeer;
|
||||
use bgp_integration_tests::test_server::{ServerWithPeer, TestServer};
|
||||
use bgp_integration_tests::{default_policy, prefix, wait_for, wait_until};
|
||||
use bgp_packet::errors::{BgpError, CeaseSubcode};
|
||||
use bgp_packet::message::{
|
||||
CommunitiesPathAttribute, LargeCommunitiesPathAttribute, LargeCommunity, PathAttribute,
|
||||
};
|
||||
use bgp_server::marks::{Marks, Patterns, Scrub};
|
||||
use bgp_server::policy::{ImportPolicy, ImportRule, PrefixSet};
|
||||
use bgp_server::session::DialState;
|
||||
use eyre::Result;
|
||||
|
||||
/// AS numbers as they are on the Internet: the checks refuse the private
|
||||
/// ones the other tests use.
|
||||
const LOCAL_ASN: u32 = 1299;
|
||||
const PEER_ASN: u32 = 3333;
|
||||
const PEER_ID: Ipv4Addr = Ipv4Addr::new(10, 0, 0, 2);
|
||||
|
||||
/// A server with one configured peer, at the address the test connects
|
||||
/// from, whose routes are judged by `import`. The listener that keeps the
|
||||
/// server's own dialling occupied comes with it.
|
||||
async fn server_with(import: ImportPolicy) -> Result<ServerWithPeer> {
|
||||
TestServer::with_peer(LOCAL_ASN, PEER_ASN, |c, peer| {
|
||||
c.connect_retry = Duration::from_millis(50);
|
||||
peer.import = import;
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn established(server: &TestServer) -> Result<TestPeer> {
|
||||
server.established(PEER_ASN, PEER_ID).await
|
||||
}
|
||||
|
||||
/// Announces IPv4 `prefixes` with the AS path `path`.
|
||||
async fn announce(peer: &mut TestPeer, prefixes: &[&str], path: &[u32]) -> Result<()> {
|
||||
peer.announce(announcement(prefixes, path)).await
|
||||
}
|
||||
|
||||
/// Waits until the server has dealt with `count` announced routes of the
|
||||
/// session's, taken or refused, and returns the prefixes in its table.
|
||||
async fn table_after(server: &TestServer, taken: u64, refused: u64) -> Result<BTreeSet<String>> {
|
||||
let counters = server.registry.list()[0].counters.clone();
|
||||
wait_until("the routes are dealt with", || {
|
||||
(counters.routes(), counters.rejected()) == (taken, refused)
|
||||
})
|
||||
.await
|
||||
.map_err(|_| {
|
||||
eyre::eyre!(
|
||||
"the server took {} routes and refused {}; expected {taken} and {refused}",
|
||||
counters.routes(),
|
||||
counters.rejected()
|
||||
)
|
||||
})?;
|
||||
Ok(server
|
||||
.received_routes()
|
||||
.iter()
|
||||
.map(|route| route.prefix.to_string())
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn set(prefixes: &[&str]) -> BTreeSet<String> {
|
||||
prefixes.iter().map(|p| p.to_string()).collect()
|
||||
}
|
||||
|
||||
/// With nothing configured for a peer but its address and AS, its routes
|
||||
/// still go through the checks that hold for anyone: only space that is
|
||||
/// routed on the Internet, nothing too specific, a path that begins with
|
||||
/// the peer and has no reserved AS in it, and never one that has been
|
||||
/// through us. Everything a peer sent used to be taken.
|
||||
#[tokio::test]
|
||||
async fn test_routes_that_no_peer_should_announce_are_refused() -> Result<()> {
|
||||
let ServerWithPeer {
|
||||
server,
|
||||
listener: _listener,
|
||||
} = server_with(default_policy()).await?;
|
||||
let mut peer = established(&server).await?;
|
||||
|
||||
// One UPDATE with good and bad prefixes: the good ones are taken.
|
||||
announce(
|
||||
&mut peer,
|
||||
&[
|
||||
"193.0.0.0/21",
|
||||
"10.0.0.0/8", // private
|
||||
"193.0.10.0/25", // more specific than is routed
|
||||
"0.0.0.0/0", // default
|
||||
"193.0.16.0/24",
|
||||
],
|
||||
&[PEER_ASN],
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(
|
||||
table_after(&server, 2, 3).await?,
|
||||
set(&["193.0.0.0/21", "193.0.16.0/24"])
|
||||
);
|
||||
|
||||
// Paths that are refused whatever the prefix: not the peer's own,
|
||||
// with a private AS in it, and one that has been through us.
|
||||
announce(&mut peer, &["194.0.1.0/24"], &[174, PEER_ASN]).await?;
|
||||
announce(&mut peer, &["194.0.2.0/24"], &[PEER_ASN, 64512]).await?;
|
||||
announce(&mut peer, &["194.0.3.0/24"], &[PEER_ASN, LOCAL_ASN, 174]).await?;
|
||||
assert_eq!(
|
||||
table_after(&server, 2, 6).await?,
|
||||
set(&["193.0.0.0/21", "193.0.16.0/24"])
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A route that is refused takes the place of the one the peer had
|
||||
/// announced before, which goes: the peer no longer has the old route,
|
||||
/// and the new one is not acceptable.
|
||||
#[tokio::test]
|
||||
async fn test_a_refused_route_replaces_the_one_that_was_taken() -> Result<()> {
|
||||
let ServerWithPeer {
|
||||
server,
|
||||
listener: _listener,
|
||||
} = server_with(default_policy()).await?;
|
||||
let mut peer = established(&server).await?;
|
||||
|
||||
announce(&mut peer, &["193.0.0.0/21", "193.0.16.0/24"], &[PEER_ASN]).await?;
|
||||
assert_eq!(table_after(&server, 2, 0).await?.len(), 2);
|
||||
|
||||
announce(&mut peer, &["193.0.0.0/21"], &[PEER_ASN, 64512]).await?;
|
||||
assert_eq!(table_after(&server, 1, 1).await?, set(&["193.0.16.0/24"]));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A peer with a list of what it may announce is held to it: its own
|
||||
/// prefixes, originated by the AS numbers given.
|
||||
#[tokio::test]
|
||||
async fn test_a_peer_is_held_to_its_prefix_list_and_origins() -> Result<()> {
|
||||
let mut prefixes = PrefixSet::default();
|
||||
prefixes.allow(prefix("193.0.0.0/21"), 24);
|
||||
let ServerWithPeer {
|
||||
server,
|
||||
listener: _listener,
|
||||
} = server_with(ImportPolicy {
|
||||
prefixes: Some(prefixes),
|
||||
origin_asns: Some([PEER_ASN, 2121].into()),
|
||||
..default_policy()
|
||||
})
|
||||
.await?;
|
||||
let mut peer = established(&server).await?;
|
||||
|
||||
// Its own, and a more specific that the list allows.
|
||||
announce(&mut peer, &["193.0.0.0/21", "193.0.4.0/24"], &[PEER_ASN]).await?;
|
||||
// A customer of its that is listed as an origin.
|
||||
announce(&mut peer, &["193.0.6.0/24"], &[PEER_ASN, 2121]).await?;
|
||||
// Someone else's prefix, as a hijack or a leak would be.
|
||||
announce(&mut peer, &["8.8.8.0/24"], &[PEER_ASN]).await?;
|
||||
// Its own prefix, but passed on from an origin that is not listed.
|
||||
announce(&mut peer, &["193.0.7.0/24"], &[PEER_ASN, 15169]).await?;
|
||||
|
||||
assert_eq!(
|
||||
table_after(&server, 3, 2).await?,
|
||||
set(&["193.0.0.0/21", "193.0.4.0/24", "193.0.6.0/24"])
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A peer that sends more routes than it is allowed is cut off and told
|
||||
/// why, and kept down for a while: neither its connections are taken nor
|
||||
/// is it dialled. A session that came straight back would only fill up
|
||||
/// again.
|
||||
#[tokio::test]
|
||||
async fn test_a_peer_over_its_limit_is_cut_off_and_kept_down() -> Result<()> {
|
||||
let ServerWithPeer { server, listener } = server_with(ImportPolicy {
|
||||
max_prefixes: Some(3),
|
||||
hold_down: Duration::from_millis(1500),
|
||||
..default_policy()
|
||||
})
|
||||
.await?;
|
||||
// The server dials the peer as well; that connection is left unanswered
|
||||
// until the end.
|
||||
let dialled = TestPeer::accept(&listener).await?;
|
||||
|
||||
let mut peer = established(&server).await?;
|
||||
// Refused routes do not count towards the limit, and replacing a
|
||||
// route does not either.
|
||||
announce(
|
||||
&mut peer,
|
||||
&["193.0.0.0/24", "193.0.1.0/24", "10.0.0.0/8"],
|
||||
&[PEER_ASN],
|
||||
)
|
||||
.await?;
|
||||
announce(&mut peer, &["193.0.1.0/24", "193.0.2.0/24"], &[PEER_ASN]).await?;
|
||||
assert_eq!(table_after(&server, 3, 1).await?.len(), 3);
|
||||
|
||||
announce(&mut peer, &["193.0.3.0/24"], &[PEER_ASN]).await?;
|
||||
let notification = peer.expect_notification().await?;
|
||||
assert_eq!(
|
||||
notification.error,
|
||||
BgpError::Cease(CeaseSubcode::MaxPrefixes)
|
||||
);
|
||||
drop(peer);
|
||||
wait_until("the peer's routes are gone", || {
|
||||
server.received_routes().is_empty()
|
||||
})
|
||||
.await?;
|
||||
|
||||
// Held down: a new connection gets nothing.
|
||||
let mut early = TestPeer::connect(server.addr).await?;
|
||||
assert!(
|
||||
early.closed_unanswered().await,
|
||||
"a peer that is held down is not answered"
|
||||
);
|
||||
// Nor is it dialled: once the connection the server had open to it is
|
||||
// gone, the dialer waits out the hold-down and says so.
|
||||
drop(dialled);
|
||||
let reason = wait_for("the dialer says the peer is held down", || {
|
||||
match server.registry.configured_peers().into_iter().next() {
|
||||
Some((_, DialState::Backoff { last_error, .. }))
|
||||
if last_error.contains("held down") =>
|
||||
{
|
||||
Some(last_error)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
assert!(reason.contains("limit of routes"), "{reason}");
|
||||
|
||||
// When the time is up the peer is dialled again, and taken back.
|
||||
TestPeer::accept(&listener).await?;
|
||||
let mut later = TestPeer::connect(server.addr).await?;
|
||||
later.handshake(PEER_ASN, PEER_ID, 90).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Routes from a peer are marked with where they were learned and given
|
||||
/// a preference, as the peer's `import` says; and marks that mean
|
||||
/// something to us are not taken on the peer's word.
|
||||
#[tokio::test]
|
||||
async fn test_a_peers_routes_are_marked_and_ranked_as_configured() -> Result<()> {
|
||||
use bgp_packet::message::{CommunitiesPathAttribute, LocalPrefPathAttribute};
|
||||
use bgp_server::marks::Marks;
|
||||
let ServerWithPeer {
|
||||
server,
|
||||
listener: _listener,
|
||||
} = server_with(ImportPolicy {
|
||||
marks: Marks::parse(
|
||||
&["1299:*".to_string()],
|
||||
&["1299:1001".to_string()],
|
||||
&["1299:9:7".to_string()],
|
||||
)
|
||||
.unwrap(),
|
||||
local_pref: Some(250),
|
||||
..default_policy()
|
||||
})
|
||||
.await?;
|
||||
let mut peer = established(&server).await?;
|
||||
peer.announce(
|
||||
announcement(&["193.0.0.0/21", "193.0.8.0/22"], &[PEER_ASN])
|
||||
.with_communities(&[(3333, 5), (1299, 2002)]),
|
||||
)
|
||||
.await?;
|
||||
table_after(&server, 2, 0).await?;
|
||||
let routes = server.received_routes();
|
||||
for route in &routes {
|
||||
assert!(
|
||||
route
|
||||
.attributes
|
||||
.contains(&PathAttribute::Communities(CommunitiesPathAttribute(vec![
|
||||
(3333, 5),
|
||||
(1299, 1001)
|
||||
]))),
|
||||
"{route:?}"
|
||||
);
|
||||
assert!(
|
||||
route
|
||||
.attributes
|
||||
.contains(&PathAttribute::LocalPref(LocalPrefPathAttribute(250))),
|
||||
"{route:?}"
|
||||
);
|
||||
assert!(
|
||||
route.attributes.iter().any(|attribute| matches!(
|
||||
attribute,
|
||||
PathAttribute::LargeCommunities(large) if large.communities.len() == 1
|
||||
)),
|
||||
"{route:?}"
|
||||
);
|
||||
}
|
||||
assert_eq!(server.rib.attrs().len(), 1, "they share their attributes");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The communities of our own AS are taken off what another network
|
||||
/// sends, before the route is kept, and nothing else is; from a peer that
|
||||
/// is to steer its routes with them (a customer), they are kept.
|
||||
#[tokio::test]
|
||||
async fn test_our_communities_are_not_taken_from_other_networks() -> Result<()> {
|
||||
let ours_and_theirs = || {
|
||||
vec![
|
||||
PathAttribute::Communities(CommunitiesPathAttribute(vec![
|
||||
(0, 13335),
|
||||
(3356, 3),
|
||||
(1299, 5),
|
||||
])),
|
||||
PathAttribute::LargeCommunities(LargeCommunitiesPathAttribute {
|
||||
communities: vec![large(1299, 0, 0), large(1299, 31, 2), large(3356, 1, 2)],
|
||||
}),
|
||||
]
|
||||
};
|
||||
let kept = |server: &TestServer| -> Vec<PathAttribute> {
|
||||
let routes = server.received_routes();
|
||||
routes[0]
|
||||
.attributes
|
||||
.iter()
|
||||
.filter(|attribute| {
|
||||
matches!(
|
||||
attribute,
|
||||
PathAttribute::Communities(_) | PathAttribute::LargeCommunities(_)
|
||||
)
|
||||
})
|
||||
.cloned()
|
||||
.collect()
|
||||
};
|
||||
|
||||
let scrubbed = ImportPolicy {
|
||||
scrub: Some(Scrub {
|
||||
local_asn: LOCAL_ASN,
|
||||
zero: true,
|
||||
}),
|
||||
..default_policy()
|
||||
};
|
||||
let ServerWithPeer {
|
||||
server,
|
||||
listener: _listener,
|
||||
} = server_with(scrubbed).await?;
|
||||
let mut peer = established(&server).await?;
|
||||
announce_with(&mut peer, &["193.0.0.0/21"], ours_and_theirs()).await?;
|
||||
table_after(&server, 1, 0).await?;
|
||||
assert_eq!(
|
||||
kept(&server),
|
||||
[
|
||||
PathAttribute::Communities(CommunitiesPathAttribute(vec![(3356, 3)])),
|
||||
PathAttribute::LargeCommunities(LargeCommunitiesPathAttribute {
|
||||
communities: vec![large(3356, 1, 2)],
|
||||
}),
|
||||
]
|
||||
);
|
||||
|
||||
let ServerWithPeer {
|
||||
server,
|
||||
listener: _listener,
|
||||
} = server_with(default_policy()).await?;
|
||||
let mut peer = established(&server).await?;
|
||||
announce_with(&mut peer, &["193.0.0.0/21"], ours_and_theirs()).await?;
|
||||
table_after(&server, 1, 0).await?;
|
||||
assert_eq!(kept(&server), ours_and_theirs());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn large(global_admin: u32, data_1: u32, data_2: u32) -> LargeCommunity {
|
||||
LargeCommunity {
|
||||
global_admin,
|
||||
data_1,
|
||||
data_2,
|
||||
}
|
||||
}
|
||||
|
||||
/// Announces IPv4 `prefixes` from the peer itself, with `more` attributes.
|
||||
async fn announce_with(
|
||||
peer: &mut TestPeer,
|
||||
prefixes: &[&str],
|
||||
more: Vec<PathAttribute>,
|
||||
) -> Result<()> {
|
||||
peer.announce(announcement(prefixes, &[PEER_ASN]).with_attributes(more))
|
||||
.await
|
||||
}
|
||||
|
||||
/// Rules do to some routes what the rest of the policy does to all: by
|
||||
/// their communities, as the session has marked them, or by their prefix,
|
||||
/// even when they came in one UPDATE with others. The first rule that
|
||||
/// holds for a route is the one applied.
|
||||
#[tokio::test]
|
||||
async fn test_rules_apply_to_the_routes_they_hold_for() -> Result<()> {
|
||||
let mut within = PrefixSet::default();
|
||||
within.allow(prefix("193.0.0.0/21"), 24);
|
||||
let rule = |communities: &[&str], prefixes: Option<PrefixSet>, local_pref, add: &[&str]| {
|
||||
let strings = |texts: &[&str]| texts.iter().map(|t| t.to_string()).collect::<Vec<_>>();
|
||||
ImportRule {
|
||||
communities: Patterns::parse(&strings(communities)).unwrap(),
|
||||
prefixes,
|
||||
local_pref,
|
||||
marks: Marks::parse(&[], &[], &strings(add)).unwrap(),
|
||||
}
|
||||
};
|
||||
let policy = ImportPolicy {
|
||||
// The session marks where its routes were learned.
|
||||
marks: Marks::parse(&[], &[], &["1299:101:1".to_string()]).unwrap(),
|
||||
rules: vec![
|
||||
rule(&["3356:3"], None, Some(50), &[]),
|
||||
rule(&[], Some(within), None, &["1299:31:10"]),
|
||||
rule(&["1299:101:*"], None, Some(120), &[]),
|
||||
],
|
||||
..default_policy()
|
||||
};
|
||||
let ServerWithPeer {
|
||||
server,
|
||||
listener: _listener,
|
||||
} = server_with(policy).await?;
|
||||
let mut peer = established(&server).await?;
|
||||
announce_with(
|
||||
&mut peer,
|
||||
&["194.0.0.0/16"],
|
||||
vec![PathAttribute::Communities(CommunitiesPathAttribute(vec![
|
||||
(3356, 3),
|
||||
]))],
|
||||
)
|
||||
.await?;
|
||||
announce_with(&mut peer, &["193.0.0.0/22", "195.0.0.0/16"], vec![]).await?;
|
||||
table_after(&server, 3, 0).await?;
|
||||
|
||||
let kept = |wanted: &str| {
|
||||
let routes = server.received_routes();
|
||||
let route = routes
|
||||
.iter()
|
||||
.find(|route| route.prefix.to_string() == wanted)
|
||||
.unwrap_or_else(|| panic!("{wanted} is in the table"));
|
||||
let local_pref = route
|
||||
.attributes
|
||||
.iter()
|
||||
.find_map(|attribute| match attribute {
|
||||
PathAttribute::LocalPref(local_pref) => Some(local_pref.0),
|
||||
_ => None,
|
||||
});
|
||||
let large = route
|
||||
.attributes
|
||||
.iter()
|
||||
.find_map(|attribute| match attribute {
|
||||
PathAttribute::LargeCommunities(large) => Some(large.communities.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or_default();
|
||||
(local_pref, large)
|
||||
};
|
||||
let learned = large(1299, 101, 1);
|
||||
// By its community: the first rule, and only that one.
|
||||
assert_eq!(kept("194.0.0.0/16"), (Some(50), vec![learned.clone()]));
|
||||
// By its prefix, apart from the route it came with.
|
||||
assert_eq!(
|
||||
kept("193.0.0.0/22"),
|
||||
(None, vec![learned.clone(), large(1299, 31, 10)])
|
||||
);
|
||||
// By the mark the session put on.
|
||||
assert_eq!(kept("195.0.0.0/16"), (Some(120), vec![learned]));
|
||||
Ok(())
|
||||
}
|
||||
@@ -12,6 +12,8 @@ use bgp_primitives::ip_prefix::IpPrefix;
|
||||
use bgp_rib::{LocalRoute, LocalRouteAttrs};
|
||||
use eyre::Result;
|
||||
|
||||
const TIMEOUT: Duration = Duration::from_secs(5);
|
||||
|
||||
/// The OPEN a BIRD 2 router sends for an IPv6-only session from AS210036:
|
||||
/// one optional parameter carrying MP(IPv6 unicast), route refresh,
|
||||
/// extended message, graceful restart (aware only), 4-octet AS, enhanced
|
||||
@@ -75,13 +77,19 @@ async fn test_bird_open_and_end_of_rib_are_accepted() -> Result<()> {
|
||||
|
||||
peer.send_raw(BIRD_OPEN).await?;
|
||||
peer.recv_open().await?;
|
||||
match peer.recv().await? {
|
||||
match tokio::time::timeout(TIMEOUT, peer.recv()).await?? {
|
||||
Message::KeepAlive => {}
|
||||
other => eyre::bail!("server did not accept BIRD's OPEN: {other:?}"),
|
||||
}
|
||||
peer.send(&Message::KeepAlive).await?;
|
||||
|
||||
let session = server.wait_for_session().await?;
|
||||
tokio::time::timeout(TIMEOUT, async {
|
||||
while server.registry.list().is_empty() {
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
let session = server.registry.list().remove(0);
|
||||
assert_eq!(
|
||||
session.peer.asn, 210036,
|
||||
"4-octet ASN must come from the capability, not AS_TRANS"
|
||||
@@ -118,8 +126,6 @@ async fn test_ipv6_prefix_update_shape() -> Result<()> {
|
||||
next_hop: nh.into(),
|
||||
four_octet_asn: true,
|
||||
ibgp: false,
|
||||
role: None,
|
||||
extended_next_hop: false,
|
||||
},
|
||||
)
|
||||
.expect("ipv6 over ipv6");
|
||||
|
||||
@@ -1,172 +0,0 @@
|
||||
//! The whole daemon against a real kernel: a peer announces routes over a
|
||||
//! session, and `ip route` shows them.
|
||||
//!
|
||||
//! This changes the machine's interfaces and routes, so it is not run by
|
||||
//! `cargo test`; `scripts/kernel-tests.sh` runs it in a network namespace
|
||||
//! of its own.
|
||||
#![cfg(target_os = "linux")]
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
use std::net::Ipv4Addr;
|
||||
use std::process::Command;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use bgp_fib::FibConfig;
|
||||
use bgp_integration_tests::announcement::{announcement, withdrawal};
|
||||
use bgp_integration_tests::prefix;
|
||||
use bgp_integration_tests::test_peer::TestPeer;
|
||||
use bgp_integration_tests::test_server::TestServer;
|
||||
use eyre::Result;
|
||||
use serde_json::Value;
|
||||
|
||||
/// The kernel is given more time than the loopback tests allow.
|
||||
const TIMEOUT: Duration = Duration::from_secs(10);
|
||||
const TABLE: u32 = 1100;
|
||||
const INTERFACE: &str = "ferrix100";
|
||||
|
||||
#[track_caller]
|
||||
fn ip(arguments: &str) {
|
||||
let output = Command::new("ip")
|
||||
.args(arguments.split_whitespace())
|
||||
.output()
|
||||
.expect("the ip command is there");
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"ip {arguments}: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
|
||||
fn kernel_routes() -> BTreeSet<String> {
|
||||
let output = Command::new("ip")
|
||||
.args(["route", "show", "table", &TABLE.to_string()])
|
||||
.output()
|
||||
.unwrap();
|
||||
String::from_utf8_lossy(&output.stdout)
|
||||
.lines()
|
||||
.map(|line| line.split_whitespace().collect::<Vec<_>>().join(" "))
|
||||
// The number of the nexthop object a route names is the kernel's
|
||||
// to choose.
|
||||
.map(|line| match line.split_once(" nhid ") {
|
||||
Some((prefix, rest)) => {
|
||||
let rest = rest.split_once(' ').map_or("", |(_, rest)| rest);
|
||||
format!("{prefix} {rest}")
|
||||
}
|
||||
None => line,
|
||||
})
|
||||
.filter(|line| !line.is_empty())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn kernel_becomes(expected: &[&str]) {
|
||||
let expected: BTreeSet<String> = expected.iter().map(|route| route.to_string()).collect();
|
||||
let start = Instant::now();
|
||||
while kernel_routes() != expected {
|
||||
assert!(
|
||||
start.elapsed() < TIMEOUT,
|
||||
"the kernel has {:#?}; expected {expected:#?}",
|
||||
kernel_routes()
|
||||
);
|
||||
std::thread::sleep(Duration::from_millis(20));
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes the test's interface, and its routes with it, when dropped.
|
||||
struct Interface;
|
||||
|
||||
impl Drop for Interface {
|
||||
fn drop(&mut self) {
|
||||
let _ = Command::new("ip").args(["link", "del", INTERFACE]).output();
|
||||
let _ = Command::new("ip")
|
||||
.args(["route", "flush", "table", &TABLE.to_string()])
|
||||
.output();
|
||||
}
|
||||
}
|
||||
|
||||
async fn announce(peer: &mut TestPeer, prefixes: &[&str], next_hop: Ipv4Addr) -> Result<()> {
|
||||
peer.announce(announcement(prefixes, &[65001]).with_next_hop(next_hop))
|
||||
.await
|
||||
}
|
||||
|
||||
/// A peer's routes go from the session to the kernel, and out of it again
|
||||
/// when they are withdrawn and when the session ends.
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
#[ignore = "changes the machine's routes; run scripts/kernel-tests.sh"]
|
||||
async fn test_routes_from_a_session_reach_the_kernel() -> Result<()> {
|
||||
ip(&format!("link add {INTERFACE} type dummy"));
|
||||
let _interface = Interface;
|
||||
ip(&format!("addr add 10.100.0.1/24 dev {INTERFACE}"));
|
||||
ip(&format!("link set {INTERFACE} up"));
|
||||
|
||||
let server = TestServer::start_with(65000, |config| {
|
||||
config.kernel = Some(FibConfig {
|
||||
table: TABLE,
|
||||
..FibConfig::default()
|
||||
});
|
||||
})
|
||||
.await?;
|
||||
let (base, _http) = server.serve_http().await?;
|
||||
let mut peer = TestPeer::connect(server.addr).await?;
|
||||
peer.handshake(65001, Ipv4Addr::new(10, 0, 0, 2), 90)
|
||||
.await?;
|
||||
|
||||
// The peer speaks from the loopback and names a router on the test's
|
||||
// interface as next hop, as a route server would.
|
||||
let neighbor = Ipv4Addr::new(10, 100, 0, 2);
|
||||
announce(&mut peer, &["203.0.113.0/24", "198.18.0.0/15"], neighbor).await?;
|
||||
// One by way of a router that is not a neighbor.
|
||||
announce(&mut peer, &["192.88.99.0/24"], Ipv4Addr::new(172, 31, 0, 1)).await?;
|
||||
kernel_becomes(&[
|
||||
"203.0.113.0/24 via 10.100.0.2 dev ferrix100 proto bgp metric 20",
|
||||
"198.18.0.0/15 via 10.100.0.2 dev ferrix100 proto bgp metric 20",
|
||||
]);
|
||||
|
||||
let kernel: Value = reqwest::get(format!("{base}/api/kernel"))
|
||||
.await?
|
||||
.json()
|
||||
.await?;
|
||||
assert_eq!(kernel["enabled"], true);
|
||||
assert_eq!(kernel["table"], TABLE);
|
||||
assert_eq!(kernel["installed"], 2);
|
||||
// The route by way of a router that is no neighbor is not in the
|
||||
// table for the kernel to be asked for: it is held, in case the
|
||||
// router becomes one.
|
||||
assert_eq!(kernel["unresolved"], serde_json::json!({}));
|
||||
let session = server.registry.list().pop().expect("the session");
|
||||
assert_eq!(session.counters.held(), 1);
|
||||
assert_eq!(server.rib.len(), 2);
|
||||
let refused: Value = reqwest::get(format!("{base}/api/routes/rejected"))
|
||||
.await?
|
||||
.json()
|
||||
.await?;
|
||||
assert_eq!(
|
||||
refused[0]["by_reason"],
|
||||
serde_json::json!({"next hop unreachable": 1})
|
||||
);
|
||||
let shown = kernel["interfaces"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.find(|interface| interface["name"] == INTERFACE)
|
||||
.expect("the test's interface is listed");
|
||||
assert_eq!(shown["up"], true);
|
||||
// Its own address, beside the link-local one the kernel gave it.
|
||||
assert!(
|
||||
shown["addresses"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.contains(&serde_json::json!("10.100.0.1/24")),
|
||||
"{shown}"
|
||||
);
|
||||
|
||||
// One is withdrawn.
|
||||
peer.announce(withdrawal([prefix("198.18.0.0/15").into()]))
|
||||
.await?;
|
||||
kernel_becomes(&["203.0.113.0/24 via 10.100.0.2 dev ferrix100 proto bgp metric 20"]);
|
||||
|
||||
// The session ends: packets must not go on following its routes.
|
||||
drop(peer);
|
||||
kernel_becomes(&[]);
|
||||
Ok(())
|
||||
}
|
||||
@@ -4,25 +4,29 @@
|
||||
use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr};
|
||||
use std::time::Duration;
|
||||
|
||||
use bgp_integration_tests::announcement::announcement;
|
||||
use bgp_integration_tests::test_peer::TestPeer;
|
||||
use bgp_integration_tests::test_server::{ReceivedRoute, TestServer};
|
||||
use bgp_integration_tests::{peer_config, prefix, wait_for};
|
||||
use bgp_integration_tests::test_server::TestServer;
|
||||
use bgp_packet::errors::{BgpError, OpenMsgSubcode};
|
||||
use bgp_packet::message::{
|
||||
AsPathAttribute, Message, MpReachNlriPathAttribute, NlriNextHop, PathAttribute, UpdateMessage,
|
||||
AsPathAttribute, Message, MpReachNlriPathAttribute, NextHopPathAttribute, NlriNextHop,
|
||||
OriginPathAttribute, PathAttribute, UpdateMessage,
|
||||
};
|
||||
use bgp_packet::open::Capability;
|
||||
use bgp_primitives::constants::{Afi, AfiSafi, Safi};
|
||||
use bgp_primitives::ip_prefix::IpPrefix;
|
||||
use bgp_rib::{LocalRoute, LocalRouteAttrs};
|
||||
use bgp_server::config::PeerConfig;
|
||||
use bgp_server::family::{FamilySet, SUPPORTED};
|
||||
use bgp_server::policy::ImportPolicy;
|
||||
use eyre::{Result, bail};
|
||||
|
||||
const LOCAL_ASN: u32 = 65000;
|
||||
const PEER_ASN: u32 = 65001;
|
||||
const PEER_ID: Ipv4Addr = Ipv4Addr::new(10, 0, 0, 1);
|
||||
const TIMEOUT: Duration = Duration::from_secs(5);
|
||||
|
||||
fn prefix(s: &str) -> IpPrefix {
|
||||
IpPrefix::try_from(s).unwrap()
|
||||
}
|
||||
|
||||
fn only(family: AfiSafi) -> FamilySet {
|
||||
[family].into_iter().collect()
|
||||
@@ -32,26 +36,35 @@ fn four_octet() -> Capability {
|
||||
Capability::FourByteAsn { asn: PEER_ASN }
|
||||
}
|
||||
|
||||
/// Waits until the server has a route, and returns what it has.
|
||||
async fn some_routes(server: &TestServer) -> Result<Vec<ReceivedRoute>> {
|
||||
wait_for("a route is in the table", || {
|
||||
let routes = server.received_routes();
|
||||
(!routes.is_empty()).then_some(routes)
|
||||
async fn wait_for_session(server: &TestServer) -> Result<bgp_server::session::SessionHandle> {
|
||||
Ok(tokio::time::timeout(TIMEOUT, async {
|
||||
loop {
|
||||
if let Some(session) = server.registry.list().into_iter().next() {
|
||||
return session;
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
}
|
||||
})
|
||||
.await
|
||||
.await?)
|
||||
}
|
||||
|
||||
/// An UPDATE announcing one IPv4 route in the body and one IPv6 route in
|
||||
/// MP_REACH_NLRI.
|
||||
fn dual_stack_update() -> UpdateMessage {
|
||||
let ipv6 = PathAttribute::MpReachNlri(MpReachNlriPathAttribute {
|
||||
family: AfiSafi::IPV6_UNICAST,
|
||||
next_hop: NlriNextHop::Ipv6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1)),
|
||||
prefixes: vec![prefix("2001:db8:1::/48").into()],
|
||||
});
|
||||
announcement(&["192.0.2.0/24"], &[PEER_ASN])
|
||||
.with_attributes([ipv6])
|
||||
.into()
|
||||
UpdateMessage {
|
||||
withdrawn_routes: vec![],
|
||||
path_attributes: vec![
|
||||
PathAttribute::Origin(OriginPathAttribute::IGP),
|
||||
AsPathAttribute::from_asns(vec![PEER_ASN]),
|
||||
PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::new(192, 0, 2, 1))),
|
||||
PathAttribute::MpReachNlri(MpReachNlriPathAttribute {
|
||||
family: AfiSafi::IPV6_UNICAST,
|
||||
next_hop: NlriNextHop::Ipv6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1)),
|
||||
prefixes: vec![prefix("2001:db8:1::/48").into()],
|
||||
}),
|
||||
],
|
||||
announced_routes: vec![prefix("192.0.2.0/24").into()],
|
||||
}
|
||||
}
|
||||
|
||||
/// The session carries the families both OPENs offered, and the server's
|
||||
@@ -79,7 +92,7 @@ async fn test_families_are_the_ones_both_sides_offered() -> Result<()> {
|
||||
"the server's OPEN offers {family}"
|
||||
);
|
||||
}
|
||||
let session = server.wait_for_session().await?;
|
||||
let session = wait_for_session(&server).await?;
|
||||
assert_eq!(session.families, only(AfiSafi::IPV4_UNICAST));
|
||||
Ok(())
|
||||
}
|
||||
@@ -100,11 +113,20 @@ async fn test_routes_of_a_family_not_negotiated_are_not_accepted() -> Result<()>
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
server.wait_for_session().await?;
|
||||
wait_for_session(&server).await?;
|
||||
|
||||
peer.announce(dual_stack_update()).await?;
|
||||
peer.send(&Message::Update(dual_stack_update())).await?;
|
||||
|
||||
let routes = some_routes(&server).await?;
|
||||
let routes = tokio::time::timeout(TIMEOUT, async {
|
||||
loop {
|
||||
let routes = server.received_routes();
|
||||
if !routes.is_empty() {
|
||||
return routes;
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
// One UPDATE is applied as a whole, so the IPv6 route is not on its way.
|
||||
assert_eq!(routes.len(), 1);
|
||||
assert_eq!(routes[0].prefix, prefix("192.0.2.0/24"));
|
||||
@@ -136,7 +158,7 @@ async fn test_routes_of_a_family_not_negotiated_are_not_sent() -> Result<()> {
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
let session = server.wait_for_session().await?;
|
||||
let session = wait_for_session(&server).await?;
|
||||
assert_eq!(session.families, only(AfiSafi::IPV6_UNICAST));
|
||||
|
||||
// A second peer that takes IPv4 is sent the route. The advertiser
|
||||
@@ -145,7 +167,7 @@ async fn test_routes_of_a_family_not_negotiated_are_not_sent() -> Result<()> {
|
||||
let mut dual = TestPeer::connect(server.addr).await?;
|
||||
dual.handshake(PEER_ASN, Ipv4Addr::new(10, 0, 0, 2), 90)
|
||||
.await?;
|
||||
let update = dual.recv_update().await?;
|
||||
let update = tokio::time::timeout(TIMEOUT, dual.recv_update()).await??;
|
||||
assert_eq!(update.announced_routes, vec![prefix("10.100.0.0/16")]);
|
||||
|
||||
let nothing = tokio::time::timeout(Duration::from_millis(200), v6_only.recv_update()).await;
|
||||
@@ -159,11 +181,14 @@ async fn test_routes_of_a_family_not_negotiated_are_not_sent() -> Result<()> {
|
||||
async fn test_no_family_in_common_refuses_the_session() -> Result<()> {
|
||||
let server = TestServer::start_with(LOCAL_ASN, |c| {
|
||||
// Inbound connections from this address inherit the peer's settings.
|
||||
let at = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 1);
|
||||
c.peers = vec![PeerConfig {
|
||||
name: "v6-only".into(),
|
||||
endpoint: SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 1),
|
||||
remote_asn: PEER_ASN,
|
||||
hold_time_secs: 90,
|
||||
local_address: None,
|
||||
families: only(AfiSafi::IPV6_UNICAST),
|
||||
import: ImportPolicy::permissive(),
|
||||
..peer_config("v6-only", at, PEER_ASN)
|
||||
add_path: None,
|
||||
}];
|
||||
// Keep the dialer out of the way.
|
||||
c.connect_retry = Duration::from_secs(3600);
|
||||
@@ -188,7 +213,7 @@ async fn test_no_family_in_common_refuses_the_session() -> Result<()> {
|
||||
.collect();
|
||||
assert_eq!(offered, [&Capability::multiprotocol(AfiSafi::IPV6_UNICAST)]);
|
||||
|
||||
match peer.recv().await? {
|
||||
match tokio::time::timeout(TIMEOUT, peer.recv()).await?? {
|
||||
Message::Notification(n) => {
|
||||
assert_eq!(
|
||||
n.error,
|
||||
@@ -221,7 +246,7 @@ async fn test_a_peer_offering_an_unsupported_family_is_still_accepted() -> Resul
|
||||
)
|
||||
.await?;
|
||||
|
||||
let session = server.wait_for_session().await?;
|
||||
let session = wait_for_session(&server).await?;
|
||||
assert_eq!(session.families, only(AfiSafi::IPV4_UNICAST));
|
||||
assert!(
|
||||
session.four_octet_asn,
|
||||
@@ -246,13 +271,13 @@ async fn test_a_peer_without_capabilities_gets_two_octet_as_paths() -> Result<()
|
||||
|
||||
let mut peer = TestPeer::connect(server.addr).await?;
|
||||
peer.handshake_with(PEER_ASN, PEER_ID, 90, vec![]).await?;
|
||||
let session = server.wait_for_session().await?;
|
||||
let session = wait_for_session(&server).await?;
|
||||
assert!(!session.four_octet_asn);
|
||||
assert_eq!(session.families, only(AfiSafi::IPV4_UNICAST));
|
||||
|
||||
// The test peer now decodes AS_PATH two octets wide, as such a speaker
|
||||
// would; an UPDATE encoded four octets wide would not parse.
|
||||
let update = peer.recv_update().await?;
|
||||
let update = tokio::time::timeout(TIMEOUT, peer.recv_update()).await??;
|
||||
assert_eq!(update.announced_routes, vec![prefix("10.100.0.0/16")]);
|
||||
assert!(
|
||||
update
|
||||
@@ -267,9 +292,26 @@ async fn test_a_peer_without_capabilities_gets_two_octet_as_paths() -> Result<()
|
||||
assert_eq!(as4_path, Some(vec![WIDE_ASN]));
|
||||
|
||||
// And its own two-octet paths are read correctly.
|
||||
peer.announce(announcement(&["192.0.2.0/24"], &[PEER_ASN, 64512]))
|
||||
.await?;
|
||||
let routes = some_routes(&server).await?;
|
||||
peer.send(&Message::Update(UpdateMessage {
|
||||
withdrawn_routes: vec![],
|
||||
path_attributes: vec![
|
||||
PathAttribute::Origin(OriginPathAttribute::IGP),
|
||||
AsPathAttribute::from_asns(vec![PEER_ASN, 64512]),
|
||||
PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::new(192, 0, 2, 1))),
|
||||
],
|
||||
announced_routes: vec![prefix("192.0.2.0/24").into()],
|
||||
}))
|
||||
.await?;
|
||||
let routes = tokio::time::timeout(TIMEOUT, async {
|
||||
loop {
|
||||
let routes = server.received_routes();
|
||||
if !routes.is_empty() {
|
||||
return routes;
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
assert!(
|
||||
routes[0]
|
||||
.attributes
|
||||
|
||||
@@ -1,338 +0,0 @@
|
||||
//! Roles and route leak prevention (RFC 9234), end to end: what the two
|
||||
//! ends say they are to each other, and what becomes of routes that are
|
||||
//! marked as only to go to customers.
|
||||
|
||||
use std::net::{Ipv4Addr, Ipv6Addr};
|
||||
|
||||
use bgp_integration_tests::announcement::announcement;
|
||||
use bgp_integration_tests::test_peer::TestPeer;
|
||||
use bgp_integration_tests::test_server::{ServerWithPeer, TestServer};
|
||||
use bgp_integration_tests::{prefix, wait_until};
|
||||
use bgp_packet::errors::{BgpError, OpenMsgSubcode};
|
||||
use bgp_packet::message::{
|
||||
AsPathAttribute, Message, MpReachNlriPathAttribute, NlriNextHop, OriginPathAttribute,
|
||||
PathAttribute, UpdateMessage,
|
||||
};
|
||||
use bgp_packet::open::{BgpRole, Capability};
|
||||
use bgp_primitives::constants::AfiSafi;
|
||||
use bgp_rib::LocalRoute;
|
||||
use bgp_server::policy::PeerRole;
|
||||
use eyre::{Result, bail};
|
||||
use serde_json::Value;
|
||||
|
||||
const LOCAL_ASN: u32 = 1299;
|
||||
const PEER_ASN: u32 = 3333;
|
||||
const PEER_ID: Ipv4Addr = Ipv4Addr::new(10, 0, 0, 2);
|
||||
|
||||
/// A server with one configured peer at the address the test connects
|
||||
/// from, which is `role` to it, and which originates 193.0.0.0/21.
|
||||
async fn server_with(role: Option<PeerRole>, require_role: bool) -> Result<ServerWithPeer> {
|
||||
TestServer::with_peer(LOCAL_ASN, PEER_ASN, |c, peer| {
|
||||
c.originate = vec![LocalRoute::new(prefix("193.0.0.0/21"), Default::default())];
|
||||
peer.role = role;
|
||||
peer.require_role = require_role;
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// The capabilities of an ordinary peer, and with `role` what it says it
|
||||
/// is.
|
||||
fn capabilities(role: Option<BgpRole>) -> Vec<Capability> {
|
||||
let mut capabilities = vec![
|
||||
Capability::FourByteAsn { asn: PEER_ASN },
|
||||
Capability::multiprotocol(AfiSafi::IPV4_UNICAST),
|
||||
Capability::multiprotocol(AfiSafi::IPV6_UNICAST),
|
||||
Capability::RouteRefresh,
|
||||
];
|
||||
capabilities.extend(role.map(|role| Capability::BgpRole { role }));
|
||||
capabilities
|
||||
}
|
||||
|
||||
/// A session with the server, on which the peer says it is `says`.
|
||||
async fn established(server: &TestServer, says: Option<BgpRole>) -> Result<TestPeer> {
|
||||
server
|
||||
.established_with(PEER_ASN, PEER_ID, capabilities(says))
|
||||
.await
|
||||
}
|
||||
|
||||
/// Announces `prefixes` with the AS path `path` and, with `marked_by`, an
|
||||
/// Only to Customer attribute with that AS number.
|
||||
async fn announce(
|
||||
peer: &mut TestPeer,
|
||||
prefixes: &[&str],
|
||||
path: &[u32],
|
||||
marked_by: Option<u32>,
|
||||
) -> Result<()> {
|
||||
let mut announcement = announcement(prefixes, path);
|
||||
if let Some(asn) = marked_by {
|
||||
announcement = announcement.with_only_to_customer(asn);
|
||||
}
|
||||
peer.announce(announcement).await
|
||||
}
|
||||
|
||||
/// Waits until the table holds `count` routes, and returns the mark each
|
||||
/// has, by prefix.
|
||||
async fn marks(server: &TestServer, count: usize) -> Result<Vec<(String, Option<u32>)>> {
|
||||
server.wait_for_routes(count).await.map_err(|_| {
|
||||
eyre::eyre!(
|
||||
"expected {count} routes, the table has {:?}",
|
||||
server.received_routes()
|
||||
)
|
||||
})?;
|
||||
let mut marks: Vec<(String, Option<u32>)> = server
|
||||
.received_routes()
|
||||
.iter()
|
||||
.map(|route| {
|
||||
let mark = route
|
||||
.attributes
|
||||
.iter()
|
||||
.find_map(|attribute| match attribute {
|
||||
PathAttribute::OnlyToCustomer(asn) => Some(*asn),
|
||||
_ => None,
|
||||
});
|
||||
(route.prefix.to_string(), mark)
|
||||
})
|
||||
.collect();
|
||||
marks.sort();
|
||||
Ok(marks)
|
||||
}
|
||||
|
||||
/// The role the server's OPEN announces.
|
||||
fn announced_role(open: &bgp_packet::open::OpenMessage) -> Option<BgpRole> {
|
||||
open.capabilities().find_map(|capability| match capability {
|
||||
Capability::BgpRole { role } => Some(*role),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Both ends say what they are, and it fits: the session comes up, and is
|
||||
/// shown as one whose peer confirmed its role.
|
||||
#[tokio::test]
|
||||
async fn test_roles_that_fit_together_are_confirmed() -> Result<()> {
|
||||
let ServerWithPeer { server, .. } = server_with(Some(PeerRole::Customer), false).await?;
|
||||
let (base, _http) = server.serve_http().await?;
|
||||
let mut peer = TestPeer::connect(server.addr).await?;
|
||||
let open = peer
|
||||
.handshake_with(PEER_ASN, PEER_ID, 90, capabilities(Some(BgpRole::Customer)))
|
||||
.await?;
|
||||
// The peer is our customer: we are its provider.
|
||||
assert_eq!(announced_role(&open), Some(BgpRole::Provider));
|
||||
|
||||
server.wait_for_session().await?;
|
||||
let peers: Value = reqwest::get(format!("{base}/api/peers"))
|
||||
.await?
|
||||
.json()
|
||||
.await?;
|
||||
assert_eq!(peers[0]["role"], "customer");
|
||||
assert_eq!(peers[0]["role_confirmed"], true);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The commonest way to a leak is the two ends not agreeing on what they
|
||||
/// are to each other. A peer that says it is something else than it is
|
||||
/// configured as is refused, with the error RFC 9234 gives for it.
|
||||
#[tokio::test]
|
||||
async fn test_a_peer_that_says_it_is_something_else_is_refused() -> Result<()> {
|
||||
let ServerWithPeer { server, .. } = server_with(Some(PeerRole::Customer), false).await?;
|
||||
let mut peer = TestPeer::connect(server.addr).await?;
|
||||
// It thinks it is our peer; we have it down as a customer.
|
||||
peer.send_open_with(PEER_ASN, PEER_ID, 90, capabilities(Some(BgpRole::Peer)))
|
||||
.await?;
|
||||
peer.recv_open().await?;
|
||||
match peer.recv().await? {
|
||||
Message::Notification(n) => {
|
||||
assert_eq!(n.error, BgpError::OpenMsg(OpenMsgSubcode::RoleMismatch));
|
||||
// Subcode 11 on the wire.
|
||||
assert_eq!(n.error.code_subcode(), (2, 11));
|
||||
}
|
||||
other => bail!("expected NOTIFICATION, got {other:?}"),
|
||||
}
|
||||
assert!(server.registry.list().is_empty());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A peer that says nothing does not implement RFC 9234. It is taken at
|
||||
/// our word for what it is, unless the configuration requires it to say.
|
||||
#[tokio::test]
|
||||
async fn test_a_peer_that_does_not_say_is_refused_only_where_required() -> Result<()> {
|
||||
let ServerWithPeer { server, .. } = server_with(Some(PeerRole::Customer), false).await?;
|
||||
let (base, _http) = server.serve_http().await?;
|
||||
let _peer = established(&server, None).await?;
|
||||
let peers: Value = reqwest::get(format!("{base}/api/peers"))
|
||||
.await?
|
||||
.json()
|
||||
.await?;
|
||||
assert_eq!(peers[0]["role"], "customer");
|
||||
assert_eq!(peers[0]["role_confirmed"], false);
|
||||
|
||||
let ServerWithPeer { server, .. } = server_with(Some(PeerRole::Customer), true).await?;
|
||||
let mut peer = TestPeer::connect(server.addr).await?;
|
||||
peer.send_open_with(PEER_ASN, PEER_ID, 90, capabilities(None))
|
||||
.await?;
|
||||
peer.recv_open().await?;
|
||||
match peer.recv().await? {
|
||||
Message::Notification(n) => {
|
||||
assert_eq!(n.error, BgpError::OpenMsg(OpenMsgSubcode::RoleMismatch));
|
||||
}
|
||||
other => bail!("expected NOTIFICATION, got {other:?}"),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A route marked as only to go to customers has been handed down or
|
||||
/// across once. Coming up from a customer it is a leak, and is refused
|
||||
/// whatever else the peer's policy takes.
|
||||
#[tokio::test]
|
||||
async fn test_a_marked_route_from_a_customer_is_a_leak() -> Result<()> {
|
||||
let ServerWithPeer { server, .. } = server_with(Some(PeerRole::Customer), false).await?;
|
||||
let mut peer = established(&server, Some(BgpRole::Customer)).await?;
|
||||
|
||||
// The customer's own route, and one its other provider gave it.
|
||||
announce(&mut peer, &["194.0.0.0/24"], &[PEER_ASN], None).await?;
|
||||
announce(
|
||||
&mut peer,
|
||||
&["8.8.8.0/24"],
|
||||
&[PEER_ASN, 3356, 15169],
|
||||
Some(3356),
|
||||
)
|
||||
.await?;
|
||||
// A customer's routes may go anywhere: they are not marked on the way
|
||||
// in.
|
||||
assert_eq!(
|
||||
marks(&server, 1).await?,
|
||||
[("194.0.0.0/24".to_string(), None)]
|
||||
);
|
||||
wait_until("a route is refused", || {
|
||||
!server.rejections.peers().is_empty()
|
||||
})
|
||||
.await?;
|
||||
let refused = &server.rejections.peers()[0].1;
|
||||
assert_eq!(refused.total(), 1);
|
||||
assert_eq!(refused.recent()[0].prefix, prefix("8.8.8.0/24"));
|
||||
assert_eq!(
|
||||
refused.recent()[0].reason.as_str(),
|
||||
"route leak (Only to Customer)"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// From a peer a marked route is in order only if the peer marked it
|
||||
/// itself, on its way to us. A mark of someone else's means the peer got
|
||||
/// the route from above or from another peer.
|
||||
#[tokio::test]
|
||||
async fn test_a_route_marked_by_someone_else_is_a_leak_from_a_peer() -> Result<()> {
|
||||
let ServerWithPeer { server, .. } = server_with(Some(PeerRole::Peer), false).await?;
|
||||
let mut peer = established(&server, Some(BgpRole::Peer)).await?;
|
||||
|
||||
announce(&mut peer, &["194.0.0.0/24"], &[PEER_ASN], Some(PEER_ASN)).await?;
|
||||
announce(
|
||||
&mut peer,
|
||||
&["8.8.8.0/24"],
|
||||
&[PEER_ASN, 3356, 15169],
|
||||
Some(3356),
|
||||
)
|
||||
.await?;
|
||||
// A peer that does not mark its routes has them marked for it.
|
||||
announce(&mut peer, &["195.0.0.0/24"], &[PEER_ASN, 2121], None).await?;
|
||||
assert_eq!(
|
||||
marks(&server, 2).await?,
|
||||
[
|
||||
("194.0.0.0/24".to_string(), Some(PEER_ASN)),
|
||||
("195.0.0.0/24".to_string(), Some(PEER_ASN)),
|
||||
]
|
||||
);
|
||||
assert_eq!(server.rejections.peers()[0].1.total(), 1);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A route from a provider is marked on the way in, if it is not already:
|
||||
/// from here it may only go down. That holds for both address families,
|
||||
/// and whether or not the peer's attributes could be stored as they came.
|
||||
#[tokio::test]
|
||||
async fn test_routes_from_a_provider_are_marked_on_the_way_in() -> Result<()> {
|
||||
for four_octet in [true, false] {
|
||||
let ServerWithPeer { server, .. } = server_with(Some(PeerRole::Provider), false).await?;
|
||||
let mut peer = TestPeer::connect(server.addr).await?;
|
||||
let mut offered = capabilities(Some(BgpRole::Provider));
|
||||
if !four_octet {
|
||||
offered.retain(|capability| !matches!(capability, Capability::FourByteAsn { .. }));
|
||||
}
|
||||
peer.handshake_with(PEER_ASN, PEER_ID, 90, offered).await?;
|
||||
|
||||
announce(&mut peer, &["194.0.0.0/24"], &[PEER_ASN, 2121], None).await?;
|
||||
// One that someone further up has marked already keeps that mark.
|
||||
announce(
|
||||
&mut peer,
|
||||
&["8.8.8.0/24"],
|
||||
&[PEER_ASN, 3356, 15169],
|
||||
Some(3356),
|
||||
)
|
||||
.await?;
|
||||
peer.send(&Message::Update(UpdateMessage {
|
||||
withdrawn_routes: vec![],
|
||||
path_attributes: vec![
|
||||
PathAttribute::Origin(OriginPathAttribute::IGP),
|
||||
AsPathAttribute::from_asns(vec![PEER_ASN, 2121]),
|
||||
PathAttribute::MpReachNlri(MpReachNlriPathAttribute {
|
||||
family: AfiSafi::IPV6_UNICAST,
|
||||
next_hop: NlriNextHop::Ipv6(Ipv6Addr::new(0x2001, 0x7f8, 0, 0, 0, 0, 0, 1)),
|
||||
prefixes: vec![prefix("2a00:1450::/32").into()],
|
||||
}),
|
||||
],
|
||||
announced_routes: vec![],
|
||||
}))
|
||||
.await?;
|
||||
assert_eq!(
|
||||
marks(&server, 3).await?,
|
||||
[
|
||||
("194.0.0.0/24".to_string(), Some(PEER_ASN)),
|
||||
("2a00:1450::/32".to_string(), Some(PEER_ASN)),
|
||||
("8.8.8.0/24".to_string(), Some(3356)),
|
||||
],
|
||||
"four-octet AS numbers: {four_octet}"
|
||||
);
|
||||
assert!(server.rejections.peers().is_empty());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Our own routes are marked on their way down to a customer or across to
|
||||
/// a peer, and not on their way up to a provider.
|
||||
#[tokio::test]
|
||||
async fn test_our_routes_are_marked_on_the_way_down_or_across() -> Result<()> {
|
||||
for (peer_is, says, marked) in [
|
||||
(PeerRole::Customer, BgpRole::Customer, true),
|
||||
(PeerRole::Peer, BgpRole::Peer, true),
|
||||
(
|
||||
PeerRole::RouteServerClient,
|
||||
BgpRole::RouteServerClient,
|
||||
true,
|
||||
),
|
||||
(PeerRole::Provider, BgpRole::Provider, false),
|
||||
(PeerRole::RouteServer, BgpRole::RouteServer, false),
|
||||
] {
|
||||
let ServerWithPeer { server, .. } = server_with(Some(peer_is), false).await?;
|
||||
let mut peer = established(&server, Some(says)).await?;
|
||||
let update = peer.recv_update().await?;
|
||||
assert_eq!(update.announced_routes, [prefix("193.0.0.0/21")]);
|
||||
let mark = update
|
||||
.path_attributes
|
||||
.iter()
|
||||
.find_map(|attribute| match attribute {
|
||||
PathAttribute::OnlyToCustomer(asn) => Some(*asn),
|
||||
_ => None,
|
||||
});
|
||||
assert_eq!(mark, marked.then_some(LOCAL_ASN), "to our {peer_is:?}");
|
||||
}
|
||||
// Nothing is said to a peer whose role is not known.
|
||||
let ServerWithPeer { server, .. } = server_with(None, false).await?;
|
||||
let mut peer = established(&server, None).await?;
|
||||
let update = peer.recv_update().await?;
|
||||
assert!(
|
||||
!update
|
||||
.path_attributes
|
||||
.iter()
|
||||
.any(|attribute| matches!(attribute, PathAttribute::OnlyToCustomer(_)))
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,291 +0,0 @@
|
||||
//! Route origin validation, end to end: a server that validates against a
|
||||
//! cache, a peer that announces routes, and the validation data changing
|
||||
//! under both.
|
||||
|
||||
use std::net::Ipv4Addr;
|
||||
use std::time::Duration;
|
||||
|
||||
use bgp_integration_tests::announcement::{announcement, withdrawal};
|
||||
use bgp_integration_tests::test_cache::TestCache;
|
||||
use bgp_integration_tests::test_peer::TestPeer;
|
||||
use bgp_integration_tests::test_server::{ServerWithPeer, TestServer};
|
||||
use bgp_integration_tests::{default_policy, prefix, wait_until};
|
||||
use bgp_packet::message::{AsPathAttribute, PathAttribute};
|
||||
use bgp_rpki::{CacheState, RpkiConfig, Vrp};
|
||||
use bgp_server::policy::ImportPolicy;
|
||||
use eyre::Result;
|
||||
use serde_json::Value;
|
||||
|
||||
const LOCAL_ASN: u32 = 1299;
|
||||
const PEER_ASN: u32 = 3333;
|
||||
const PEER_ID: Ipv4Addr = Ipv4Addr::new(10, 0, 0, 2);
|
||||
|
||||
fn vrp(p: &str, max_length: u8, asn: u32) -> Vrp {
|
||||
Vrp {
|
||||
prefix: prefix(p),
|
||||
max_length,
|
||||
asn,
|
||||
}
|
||||
}
|
||||
|
||||
/// A server that validates against `cache`, with one configured peer at
|
||||
/// the address the test connects from. Returns once the server has the
|
||||
/// cache's data.
|
||||
async fn server_with(
|
||||
cache: &TestCache,
|
||||
expire: Option<Duration>,
|
||||
import: ImportPolicy,
|
||||
) -> Result<ServerWithPeer> {
|
||||
let cache = cache.address();
|
||||
let setup = TestServer::with_peer(LOCAL_ASN, PEER_ASN, |c, peer| {
|
||||
c.rpki = Some(RpkiConfig {
|
||||
caches: vec![cache],
|
||||
// Data never expires sooner than a refresh and a retry take.
|
||||
refresh: expire.map(|expire| expire / 2),
|
||||
retry: Some(Duration::from_millis(100)),
|
||||
expire,
|
||||
});
|
||||
peer.import = import;
|
||||
})
|
||||
.await?;
|
||||
let rpki = setup.server.rpki.clone().expect("validation is configured");
|
||||
wait_until("the server has the cache's data", || {
|
||||
rpki.status().state == CacheState::Synchronised
|
||||
})
|
||||
.await?;
|
||||
Ok(setup)
|
||||
}
|
||||
|
||||
async fn established(server: &TestServer) -> Result<TestPeer> {
|
||||
server.established(PEER_ASN, PEER_ID).await
|
||||
}
|
||||
|
||||
/// Announces `prefixes` as originated by `origin`, reached through the
|
||||
/// peer.
|
||||
async fn announce(peer: &mut TestPeer, prefixes: &[&str], origin: u32) -> Result<()> {
|
||||
let path = if origin == PEER_ASN {
|
||||
vec![PEER_ASN]
|
||||
} else {
|
||||
vec![PEER_ASN, origin]
|
||||
};
|
||||
peer.announce(announcement(prefixes, &path)).await
|
||||
}
|
||||
|
||||
/// A route whose origin is not the one the holder of the addresses
|
||||
/// authorised is refused. One that is authorised is taken, and so is one
|
||||
/// that nothing is known about.
|
||||
#[tokio::test]
|
||||
async fn test_an_invalid_route_is_refused_and_the_rest_are_taken() -> Result<()> {
|
||||
let cache = TestCache::start(vec![vrp("193.0.0.0/21", 24, PEER_ASN)]).await;
|
||||
let ServerWithPeer { server, .. } = server_with(&cache, None, default_policy()).await?;
|
||||
let (base, _http) = server.serve_http().await?;
|
||||
let mut peer = established(&server).await?;
|
||||
|
||||
announce(&mut peer, &["193.0.0.0/21", "194.0.0.0/24"], PEER_ASN).await?;
|
||||
// Someone else's AS as the origin of a covered prefix; and the right
|
||||
// AS, more specific than it may announce.
|
||||
announce(&mut peer, &["193.0.4.0/24"], 15169).await?;
|
||||
announce(&mut peer, &["193.0.6.0/25"], PEER_ASN).await?;
|
||||
server
|
||||
.table_becomes(&["193.0.0.0/21", "194.0.0.0/24"], 1)
|
||||
.await?;
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let get = |path: &str| {
|
||||
let request = client.get(format!("{base}{path}"));
|
||||
async move { Ok::<Value, eyre::Report>(request.send().await?.error_for_status()?.json().await?) }
|
||||
};
|
||||
// Each route says how it stands.
|
||||
let routes = get("/api/routes/received").await?["routes"].take();
|
||||
let validity: Vec<(&str, &str)> = routes
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|r| (r["prefix"].as_str().unwrap(), r["rpki"].as_str().unwrap()))
|
||||
.collect();
|
||||
assert_eq!(
|
||||
validity,
|
||||
[("193.0.0.0/21", "valid"), ("194.0.0.0/24", "not found")]
|
||||
);
|
||||
// The refused one is on record with its reason. The too-specific one
|
||||
// was refused before validation came to it.
|
||||
let refused = get("/api/routes/rejected").await?;
|
||||
assert_eq!(
|
||||
refused[0]["by_reason"],
|
||||
serde_json::json!({"RPKI invalid": 1, "prefix too specific": 1})
|
||||
);
|
||||
let peers = get("/api/peers").await?;
|
||||
assert_eq!(peers[0]["routes"], 2);
|
||||
assert_eq!(peers[0]["held_routes"], 1);
|
||||
|
||||
let rpki = get("/api/rpki").await?;
|
||||
assert_eq!(rpki["enabled"], true);
|
||||
assert_eq!(rpki["state"], "synchronised");
|
||||
assert_eq!(rpki["vrps_ipv4"], 1);
|
||||
assert_eq!(rpki["cache"], cache.address().to_string());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Validity changes with the validation data, and the routes follow: one
|
||||
/// that was taken goes when it becomes invalid, and one that was refused
|
||||
/// as invalid is taken when it no longer is. The peer sends nothing more
|
||||
/// meanwhile; it has said all it has to say.
|
||||
#[tokio::test]
|
||||
async fn test_routes_follow_the_validation_data() -> Result<()> {
|
||||
let ripe = vrp("193.0.0.0/21", 24, PEER_ASN);
|
||||
let cache = TestCache::start(vec![ripe]).await;
|
||||
let ServerWithPeer { server, .. } = server_with(&cache, None, default_policy()).await?;
|
||||
let mut peer = established(&server).await?;
|
||||
|
||||
// Nothing is known about 8.8.8.0/24, so it is taken. 193.0.4.0/24 is
|
||||
// covered, and not for this origin.
|
||||
announce(&mut peer, &["8.8.8.0/24", "193.0.4.0/24"], 15169).await?;
|
||||
server.table_becomes(&["8.8.8.0/24"], 1).await?;
|
||||
|
||||
// The holder of 8.8.8.0/24 authorises another AS: the route in the
|
||||
// table is now invalid, and goes.
|
||||
cache.set(vec![ripe, vrp("8.8.8.0/24", 24, 13335)]);
|
||||
server.table_becomes(&[], 2).await?;
|
||||
let refused = &server.rejections.peers()[0].1;
|
||||
assert_eq!(refused.total(), 2);
|
||||
assert_eq!(refused.recent()[0].prefix, prefix("8.8.8.0/24"));
|
||||
assert_eq!(&refused.recent()[0].as_path[..], [PEER_ASN, 15169]);
|
||||
|
||||
// Both are authorised for the origin they have: both come back, with
|
||||
// the attributes they were announced with.
|
||||
cache.set(vec![
|
||||
ripe,
|
||||
vrp("8.8.8.0/24", 24, 15169),
|
||||
vrp("193.0.4.0/24", 24, 15169),
|
||||
]);
|
||||
server
|
||||
.table_becomes(&["8.8.8.0/24", "193.0.4.0/24"], 0)
|
||||
.await?;
|
||||
for route in server.received_routes() {
|
||||
assert!(
|
||||
route
|
||||
.attributes
|
||||
.contains(&AsPathAttribute::from_asns(vec![PEER_ASN, 15169])),
|
||||
"{route:?}"
|
||||
);
|
||||
}
|
||||
assert_eq!(server.registry.list()[0].counters.routes(), 2);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A held route is the peer's to withdraw or replace like any other. One
|
||||
/// it has withdrawn does not appear when it would have become valid.
|
||||
#[tokio::test]
|
||||
async fn test_a_held_route_the_peer_withdrew_does_not_come_back() -> Result<()> {
|
||||
let ripe = vrp("193.0.0.0/21", 24, PEER_ASN);
|
||||
let cache = TestCache::start(vec![ripe]).await;
|
||||
let ServerWithPeer { server, .. } = server_with(&cache, None, default_policy()).await?;
|
||||
let mut peer = established(&server).await?;
|
||||
|
||||
announce(&mut peer, &["193.0.4.0/24", "193.0.5.0/24"], 15169).await?;
|
||||
server.table_becomes(&[], 2).await?;
|
||||
|
||||
// One is withdrawn, the other announced again with the right origin.
|
||||
peer.announce(withdrawal([prefix("193.0.4.0/24").into()]))
|
||||
.await?;
|
||||
announce(&mut peer, &["193.0.5.0/24"], PEER_ASN).await?;
|
||||
server.table_becomes(&["193.0.5.0/24"], 0).await?;
|
||||
|
||||
// Now the old origin would be valid for both. Neither old route is
|
||||
// there to be brought back.
|
||||
cache.set(vec![ripe, vrp("193.0.4.0/23", 24, 15169)]);
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
server.table_becomes(&["193.0.5.0/24"], 0).await?;
|
||||
assert!(
|
||||
server.received_routes()[0]
|
||||
.attributes
|
||||
.contains(&AsPathAttribute::from_asns(vec![PEER_ASN])),
|
||||
"the route the peer has now"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A peer can be exempt: its invalid routes are taken, and shown for what
|
||||
/// they are.
|
||||
#[tokio::test]
|
||||
async fn test_a_peer_can_be_exempt_from_validation() -> Result<()> {
|
||||
let cache = TestCache::start(vec![vrp("193.0.0.0/21", 24, PEER_ASN)]).await;
|
||||
let import = ImportPolicy {
|
||||
accept_rpki_invalid: true,
|
||||
..default_policy()
|
||||
};
|
||||
let ServerWithPeer { server, .. } = server_with(&cache, None, import).await?;
|
||||
let (base, _http) = server.serve_http().await?;
|
||||
let mut peer = established(&server).await?;
|
||||
|
||||
announce(&mut peer, &["193.0.4.0/24"], 15169).await?;
|
||||
server.table_becomes(&["193.0.4.0/24"], 0).await?;
|
||||
let routes: Value = reqwest::get(format!("{base}/api/routes/received"))
|
||||
.await?
|
||||
.json()
|
||||
.await?;
|
||||
assert_eq!(routes["routes"][0]["rpki"], "invalid");
|
||||
assert!(server.rejections.peers().is_empty());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Validation data that no cache has confirmed for too long is dropped,
|
||||
/// and with it every verdict: routes held as invalid are taken. Refusing
|
||||
/// routes on the word of data that may be long out of date would be worse.
|
||||
#[tokio::test]
|
||||
async fn test_held_routes_are_taken_when_the_validation_data_expires() -> Result<()> {
|
||||
let cache = TestCache::start(vec![vrp("193.0.0.0/21", 24, PEER_ASN)]).await;
|
||||
let ServerWithPeer { server, .. } =
|
||||
server_with(&cache, Some(Duration::from_millis(1500)), default_policy()).await?;
|
||||
let mut peer = established(&server).await?;
|
||||
announce(&mut peer, &["193.0.4.0/24"], 15169).await?;
|
||||
server.table_becomes(&[], 1).await?;
|
||||
|
||||
drop(cache);
|
||||
server.table_becomes(&["193.0.4.0/24"], 0).await?;
|
||||
assert!(server.rpki.as_ref().unwrap().current().vrps.is_empty());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A hijack that was possible: on a session with two-octet AS numbers a
|
||||
/// route has two paths, AS_PATH and AS4_PATH, and a peer can make them
|
||||
/// say different things. Here AS_PATH is the peer alone, and an AS4_PATH
|
||||
/// too long to be used ends at the AS that holds the addresses. The route
|
||||
/// is the peer's and invalid, when it arrives and when it is looked at
|
||||
/// again; nothing may take the unused path's word for where it comes from.
|
||||
#[tokio::test]
|
||||
async fn test_an_unused_as4_path_does_not_make_a_route_valid() -> Result<()> {
|
||||
use bgp_packet::message::AsPathSegment;
|
||||
const HOLDER: u32 = 15169;
|
||||
let cache = TestCache::start(vec![vrp("193.0.0.0/21", 24, HOLDER)]).await;
|
||||
let ServerWithPeer { server, .. } = server_with(&cache, None, default_policy()).await?;
|
||||
|
||||
// No capabilities: AS numbers are two octets wide on this session.
|
||||
let mut peer = server.established_with(PEER_ASN, PEER_ID, vec![]).await?;
|
||||
assert!(!server.registry.list()[0].four_octet_asn);
|
||||
|
||||
let as4_path = PathAttribute::As4Path(AsPathAttribute {
|
||||
segments: vec![AsPathSegment {
|
||||
ordered: true,
|
||||
path: vec![3356, HOLDER],
|
||||
}],
|
||||
});
|
||||
peer.announce(announcement(&["193.0.4.0/24"], &[PEER_ASN]).with_attributes([as4_path]))
|
||||
.await?;
|
||||
server.table_becomes(&[], 1).await?;
|
||||
|
||||
// The data changes, and every held route is looked at again.
|
||||
cache.set(vec![
|
||||
vrp("193.0.0.0/21", 24, HOLDER),
|
||||
vrp("198.51.100.0/24", 24, 64500),
|
||||
]);
|
||||
let rpki = server.rpki.clone().unwrap();
|
||||
wait_until("the server has the new data", || {
|
||||
rpki.current().vrps.len() == 2
|
||||
})
|
||||
.await?;
|
||||
// Time enough for the look, and then some.
|
||||
tokio::time::sleep(Duration::from_millis(600)).await;
|
||||
server.table_becomes(&[], 1).await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,356 +0,0 @@
|
||||
//! Passing routes on: what one peer announced reaches the others, as far
|
||||
//! as the rules for that let it.
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
use std::net::Ipv4Addr;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use bgp_integration_tests::announcement::{announcement, withdrawal};
|
||||
use bgp_integration_tests::test_peer::TestPeer;
|
||||
use bgp_integration_tests::test_server::TestServer;
|
||||
use bgp_integration_tests::{TIMEOUT, prefix};
|
||||
use bgp_packet::message::{
|
||||
CommunitiesPathAttribute, Message, NextHopPathAttribute, PathAttribute, RouteRefreshMessage,
|
||||
UpdateMessage,
|
||||
};
|
||||
use bgp_primitives::constants::AfiSafi;
|
||||
use bgp_rib::{LocalRoute, LocalRouteAttrs};
|
||||
use bgp_server::marks::{Control, Marks};
|
||||
use bgp_server::policy::ExportPolicy;
|
||||
use eyre::Result;
|
||||
|
||||
const LOCAL_ASN: u32 = 64601;
|
||||
|
||||
/// A server that passes on to every peer what it learned from the others.
|
||||
async fn transit(customize: impl FnOnce(&mut bgp_server::config::Config)) -> Result<TestServer> {
|
||||
TestServer::start_with(LOCAL_ASN, |config| {
|
||||
config.export = Arc::new(ExportPolicy {
|
||||
learned_routes: true,
|
||||
all_paths: false,
|
||||
..ExportPolicy::default()
|
||||
});
|
||||
customize(config);
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// The peer with AS `64700 + n`, its session up.
|
||||
async fn peer(server: &TestServer, n: u8) -> Result<TestPeer> {
|
||||
server
|
||||
.established(64700 + u32::from(n), Ipv4Addr::new(10, 0, 0, n))
|
||||
.await
|
||||
}
|
||||
|
||||
async fn announce(
|
||||
peer: &mut TestPeer,
|
||||
prefixes: &[&str],
|
||||
path: &[u32],
|
||||
communities: &[(u16, u16)],
|
||||
) -> Result<()> {
|
||||
peer.announce(announcement(prefixes, path).with_communities(communities))
|
||||
.await
|
||||
}
|
||||
|
||||
async fn withdraw(peer: &mut TestPeer, prefixes: &[&str]) -> Result<()> {
|
||||
peer.announce(withdrawal(prefixes.iter().map(|p| prefix(p).into())))
|
||||
.await
|
||||
}
|
||||
|
||||
/// What a peer has been told so far, as the prefixes it would now have
|
||||
/// and the path each came with.
|
||||
#[derive(Default, Debug)]
|
||||
struct Told {
|
||||
routes: std::collections::BTreeMap<String, Vec<u32>>,
|
||||
updates: usize,
|
||||
}
|
||||
|
||||
impl Told {
|
||||
fn take(&mut self, update: &UpdateMessage) {
|
||||
self.updates += 1;
|
||||
for route in &update.withdrawn_routes {
|
||||
self.routes.remove(&route.prefix.to_string());
|
||||
}
|
||||
let path: Vec<u32> = update
|
||||
.path_attributes
|
||||
.iter()
|
||||
.find_map(|attribute| match attribute {
|
||||
PathAttribute::AsPath(path) => Some(
|
||||
path.segments
|
||||
.iter()
|
||||
.flat_map(|segment| segment.path.clone())
|
||||
.collect(),
|
||||
),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or_default();
|
||||
for route in &update.announced_routes {
|
||||
self.routes.insert(route.prefix.to_string(), path.clone());
|
||||
}
|
||||
}
|
||||
|
||||
fn prefixes(&self) -> BTreeSet<&str> {
|
||||
self.routes.keys().map(String::as_str).collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads what the peer is sent until it has exactly `expected`.
|
||||
async fn told_becomes(peer: &mut TestPeer, told: &mut Told, expected: &[&str]) -> Result<()> {
|
||||
let expected: BTreeSet<&str> = expected.iter().copied().collect();
|
||||
let reading = async {
|
||||
while told.prefixes() != expected {
|
||||
let update = peer.recv_update().await?;
|
||||
told.take(&update);
|
||||
}
|
||||
Ok::<(), eyre::Report>(())
|
||||
};
|
||||
match tokio::time::timeout(TIMEOUT, reading).await {
|
||||
Ok(read) => read,
|
||||
Err(_) => Err(eyre::eyre!(
|
||||
"the peer has {:?}; expected {expected:?}",
|
||||
told.prefixes()
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Nothing more is sent to the peer for a while.
|
||||
async fn told_nothing(peer: &mut TestPeer) {
|
||||
let more = tokio::time::timeout(Duration::from_millis(300), peer.recv_update()).await;
|
||||
assert!(more.is_err(), "the peer was sent {more:?}");
|
||||
}
|
||||
|
||||
/// What one peer announces the others are told, with our AS on the path
|
||||
/// and ourselves as next hop; and what it takes back, they are told too.
|
||||
#[tokio::test]
|
||||
async fn test_routes_from_one_peer_reach_the_others() -> Result<()> {
|
||||
let server = transit(|_| {}).await?;
|
||||
let mut one = peer(&server, 1).await?;
|
||||
let mut two = peer(&server, 2).await?;
|
||||
|
||||
announce(
|
||||
&mut one,
|
||||
&["10.1.0.0/16", "10.2.0.0/16", "10.3.0.0/16"],
|
||||
&[64701, 65000],
|
||||
&[],
|
||||
)
|
||||
.await?;
|
||||
let mut told = Told::default();
|
||||
told_becomes(
|
||||
&mut two,
|
||||
&mut told,
|
||||
&["10.1.0.0/16", "10.2.0.0/16", "10.3.0.0/16"],
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(told.routes["10.1.0.0/16"], [LOCAL_ASN, 64701, 65000]);
|
||||
assert_eq!(
|
||||
told.updates, 1,
|
||||
"routes that go out alike go in one message"
|
||||
);
|
||||
// Not back to where they came from.
|
||||
told_nothing(&mut one).await;
|
||||
|
||||
// The next hop is ourselves: the address of our end of the session.
|
||||
announce(&mut one, &["10.4.0.0/16"], &[64701], &[]).await?;
|
||||
let update = two.recv_update().await?;
|
||||
assert!(
|
||||
update
|
||||
.path_attributes
|
||||
.contains(&PathAttribute::NextHop(NextHopPathAttribute(
|
||||
Ipv4Addr::LOCALHOST
|
||||
))),
|
||||
"{update:?}"
|
||||
);
|
||||
told.take(&update);
|
||||
|
||||
// One is taken back; then the peer goes, and its routes with it.
|
||||
withdraw(&mut one, &["10.2.0.0/16"]).await?;
|
||||
told_becomes(
|
||||
&mut two,
|
||||
&mut told,
|
||||
&["10.1.0.0/16", "10.3.0.0/16", "10.4.0.0/16"],
|
||||
)
|
||||
.await?;
|
||||
drop(one);
|
||||
told_becomes(&mut two, &mut told, &[]).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A peer is told of the best route, and of the next best when that one
|
||||
/// goes; and a peer that comes later is told what there is.
|
||||
#[tokio::test]
|
||||
async fn test_a_peer_is_kept_at_the_best_route() -> Result<()> {
|
||||
let server = transit(|_| {}).await?;
|
||||
let mut near = peer(&server, 1).await?;
|
||||
let mut far = peer(&server, 2).await?;
|
||||
let mut watcher = peer(&server, 3).await?;
|
||||
let mut told = Told::default();
|
||||
|
||||
announce(&mut far, &["10.1.0.0/16"], &[64702, 65000, 65001], &[]).await?;
|
||||
told_becomes(&mut watcher, &mut told, &["10.1.0.0/16"]).await?;
|
||||
assert_eq!(told.routes["10.1.0.0/16"], [LOCAL_ASN, 64702, 65000, 65001]);
|
||||
|
||||
// A better route for it comes: that is the one told now.
|
||||
announce(&mut near, &["10.1.0.0/16"], &[64701, 65001], &[]).await?;
|
||||
let update = watcher.recv_update().await?;
|
||||
told.take(&update);
|
||||
assert_eq!(told.routes["10.1.0.0/16"], [LOCAL_ASN, 64701, 65001]);
|
||||
assert!(
|
||||
update.withdrawn_routes.is_empty(),
|
||||
"replaced, not withdrawn"
|
||||
);
|
||||
|
||||
// And goes again: back to the other, with no gap.
|
||||
withdraw(&mut near, &["10.1.0.0/16"]).await?;
|
||||
let update = watcher.recv_update().await?;
|
||||
told.take(&update);
|
||||
assert_eq!(told.routes["10.1.0.0/16"], [LOCAL_ASN, 64702, 65000, 65001]);
|
||||
|
||||
// The other peer was told of the better route while it was the best,
|
||||
// and then that it is gone: the one left is its own, which it is not
|
||||
// told of.
|
||||
let mut told_far = Told::default();
|
||||
told_becomes(&mut far, &mut told_far, &["10.1.0.0/16"]).await?;
|
||||
let gone = far.recv_update().await?;
|
||||
assert_eq!(gone.withdrawn_routes, vec![prefix("10.1.0.0/16")]);
|
||||
told_nothing(&mut far).await;
|
||||
|
||||
// A peer that comes now is told what there is; and again if it asks.
|
||||
let mut late = peer(&server, 4).await?;
|
||||
let mut late_told = Told::default();
|
||||
told_becomes(&mut late, &mut late_told, &["10.1.0.0/16"]).await?;
|
||||
late.send(&Message::RouteRefresh(RouteRefreshMessage::request(
|
||||
AfiSafi::IPV4_UNICAST,
|
||||
)))
|
||||
.await?;
|
||||
let again = late.recv_update().await?;
|
||||
assert_eq!(again.announced_routes, vec![prefix("10.1.0.0/16")]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A route is not sent to a peer whose AS is on its path already, nor
|
||||
/// anywhere if it says NO_EXPORT; and what we originate goes to everyone
|
||||
/// beside what was learned.
|
||||
#[tokio::test]
|
||||
async fn test_routes_stop_where_they_must() -> Result<()> {
|
||||
let server = transit(|config| {
|
||||
config.originate = vec![LocalRoute::new(
|
||||
prefix("10.99.0.0/16"),
|
||||
LocalRouteAttrs::default(),
|
||||
)];
|
||||
})
|
||||
.await?;
|
||||
let mut one = peer(&server, 1).await?;
|
||||
let mut two = peer(&server, 2).await?;
|
||||
let mut told = Told::default();
|
||||
told_becomes(&mut two, &mut told, &["10.99.0.0/16"]).await?;
|
||||
assert_eq!(told.routes["10.99.0.0/16"], [LOCAL_ASN]);
|
||||
|
||||
// Through the second peer's AS already; and not for other networks.
|
||||
announce(&mut one, &["10.1.0.0/16"], &[64701, 64702, 65000], &[]).await?;
|
||||
announce(&mut one, &["10.2.0.0/16"], &[64701], &[(0xFFFF, 0xFF01)]).await?;
|
||||
announce(&mut one, &["10.3.0.0/16"], &[64701], &[]).await?;
|
||||
told_becomes(&mut two, &mut told, &["10.99.0.0/16", "10.3.0.0/16"]).await?;
|
||||
told_nothing(&mut two).await;
|
||||
assert_eq!(server.rib.len(), 3, "all three were taken");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Communities on a route say which peers it is passed on to, and are
|
||||
/// gone when it is; and every route to a peer gets the marks of `export`.
|
||||
#[tokio::test]
|
||||
async fn test_communities_say_who_is_told() -> Result<()> {
|
||||
let server = transit(|config| {
|
||||
config.control = Some(Control {
|
||||
local_asn: LOCAL_ASN,
|
||||
});
|
||||
config.export = Arc::new(ExportPolicy {
|
||||
learned_routes: true,
|
||||
all_paths: false,
|
||||
marks: Marks::parse(&[], &["64601:7".to_string()], &[]).unwrap(),
|
||||
prepend: 1,
|
||||
});
|
||||
config.originate = vec![LocalRoute::new(
|
||||
prefix("10.99.0.0/16"),
|
||||
LocalRouteAttrs {
|
||||
// Our own route, and not for the second peer.
|
||||
communities: vec![(0, 64702)],
|
||||
..LocalRouteAttrs::default()
|
||||
},
|
||||
)];
|
||||
})
|
||||
.await?;
|
||||
let mut one = peer(&server, 1).await?;
|
||||
let mut two = peer(&server, 2).await?;
|
||||
let mut three = peer(&server, 3).await?;
|
||||
let (mut told_two, mut told_three) = (Told::default(), Told::default());
|
||||
|
||||
// To everyone but the second; to the second only; to nobody; and
|
||||
// with nothing said.
|
||||
announce(
|
||||
&mut one,
|
||||
&["10.1.0.0/16"],
|
||||
&[64701],
|
||||
&[(0, 64702), (3356, 3)],
|
||||
)
|
||||
.await?;
|
||||
announce(&mut one, &["10.2.0.0/16"], &[64701], &[(64601, 64702)]).await?;
|
||||
announce(&mut one, &["10.3.0.0/16"], &[64701], &[(0, 64601)]).await?;
|
||||
announce(&mut one, &["10.4.0.0/16"], &[64701], &[]).await?;
|
||||
|
||||
told_becomes(&mut two, &mut told_two, &["10.2.0.0/16", "10.4.0.0/16"]).await?;
|
||||
told_becomes(
|
||||
&mut three,
|
||||
&mut told_three,
|
||||
&["10.99.0.0/16", "10.1.0.0/16", "10.4.0.0/16"],
|
||||
)
|
||||
.await?;
|
||||
told_nothing(&mut two).await;
|
||||
told_nothing(&mut three).await;
|
||||
// Our AS once for passing it on, and once more by `export`.
|
||||
assert_eq!(
|
||||
told_three.routes["10.1.0.0/16"],
|
||||
[LOCAL_ASN, LOCAL_ASN, 64701]
|
||||
);
|
||||
assert_eq!(told_three.routes["10.99.0.0/16"], [LOCAL_ASN, LOCAL_ASN]);
|
||||
|
||||
// What was for us to act on is gone, and the router's mark is on.
|
||||
announce(
|
||||
&mut one,
|
||||
&["10.5.0.0/16"],
|
||||
&[64701],
|
||||
&[(0, 64702), (3356, 3)],
|
||||
)
|
||||
.await?;
|
||||
let update = three.recv_update().await?;
|
||||
assert!(
|
||||
update
|
||||
.path_attributes
|
||||
.contains(&PathAttribute::Communities(CommunitiesPathAttribute(vec![
|
||||
(3356, 3),
|
||||
(64601, 7)
|
||||
]))),
|
||||
"{update:?}"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Without `learned_routes` a peer is told what we originate and nothing
|
||||
/// else, as before there was such a thing.
|
||||
#[tokio::test]
|
||||
async fn test_nothing_learned_is_passed_on_unless_asked_for() -> Result<()> {
|
||||
let server = TestServer::start_with(LOCAL_ASN, |config| {
|
||||
config.originate = vec![LocalRoute::new(
|
||||
prefix("10.99.0.0/16"),
|
||||
LocalRouteAttrs::default(),
|
||||
)];
|
||||
})
|
||||
.await?;
|
||||
let mut one = peer(&server, 1).await?;
|
||||
let mut two = peer(&server, 2).await?;
|
||||
let mut told = Told::default();
|
||||
told_becomes(&mut two, &mut told, &["10.99.0.0/16"]).await?;
|
||||
announce(&mut one, &["10.1.0.0/16"], &[64701], &[]).await?;
|
||||
server.wait_for_routes(1).await?;
|
||||
told_nothing(&mut two).await;
|
||||
Ok(())
|
||||
}
|
||||
@@ -2,18 +2,19 @@
|
||||
//! cost the routes in the UPDATE, not the session.
|
||||
|
||||
use std::net::Ipv4Addr;
|
||||
use std::time::Duration;
|
||||
|
||||
use bgp_integration_tests::test_peer::TestPeer;
|
||||
use bgp_integration_tests::test_server::{ReceivedRoute, TestServer};
|
||||
use bgp_integration_tests::{prefix, wait_for};
|
||||
use bgp_packet::errors::{BgpError, UpdateMsgSubcode};
|
||||
use bgp_packet::message::{LocalPrefPathAttribute, PathAttribute};
|
||||
use bgp_packet::message::{LocalPrefPathAttribute, Message, PathAttribute};
|
||||
use bgp_primitives::ip_prefix::IpPrefix;
|
||||
use bgp_server::metrics::UPDATE_ERRORS;
|
||||
use eyre::Result;
|
||||
use eyre::{Result, bail};
|
||||
|
||||
const LOCAL_ASN: u32 = 65000;
|
||||
const PEER_ASN: u32 = 65001;
|
||||
const TIMEOUT: Duration = Duration::from_secs(5);
|
||||
|
||||
const WELL_KNOWN: u8 = 0x40;
|
||||
const OPTIONAL_TRANSITIVE: u8 = 0xc0;
|
||||
@@ -57,24 +58,31 @@ fn update(attributes: &[Vec<u8>], nlri: &[u8]) -> Vec<u8> {
|
||||
const ROUTE_A: [u8; 3] = [16, 10, 1];
|
||||
const ROUTE_B: [u8; 3] = [16, 10, 2];
|
||||
|
||||
fn prefix(s: &str) -> IpPrefix {
|
||||
IpPrefix::try_from(s).unwrap()
|
||||
}
|
||||
|
||||
async fn established(server: &TestServer, asn: u32) -> Result<TestPeer> {
|
||||
server.established(asn, Ipv4Addr::new(10, 0, 0, 1)).await
|
||||
let mut peer = TestPeer::connect(server.addr).await?;
|
||||
peer.handshake(asn, Ipv4Addr::new(10, 0, 0, 1), 90).await?;
|
||||
Ok(peer)
|
||||
}
|
||||
|
||||
/// Waits until the server's Adj-RIB-In holds exactly `prefixes`.
|
||||
async fn wait_for_routes(server: &TestServer, prefixes: &[&str]) -> Result<Vec<ReceivedRoute>> {
|
||||
let mut expected: Vec<IpPrefix> = prefixes.iter().map(|p| prefix(p)).collect();
|
||||
expected.sort_by_key(|p| p.to_string());
|
||||
wait_for(&format!("the table holds {prefixes:?}"), || {
|
||||
let mut routes = server.received_routes();
|
||||
routes.sort_by_key(|r| r.prefix.to_string());
|
||||
routes
|
||||
.iter()
|
||||
.map(|r| &r.prefix)
|
||||
.eq(expected.iter())
|
||||
.then_some(routes)
|
||||
Ok(tokio::time::timeout(TIMEOUT, async {
|
||||
loop {
|
||||
let mut routes = server.received_routes();
|
||||
routes.sort_by_key(|r| r.prefix.to_string());
|
||||
if routes.iter().map(|r| &r.prefix).eq(expected.iter()) {
|
||||
return routes;
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
}
|
||||
})
|
||||
.await
|
||||
.await?)
|
||||
}
|
||||
|
||||
/// The situation RFC 7606 exists for: a route is in the table, and then an
|
||||
@@ -197,11 +205,24 @@ async fn test_an_unreadable_prefix_still_resets_the_session() -> Result<()> {
|
||||
))
|
||||
.await?;
|
||||
|
||||
let notification = peer.expect_notification().await?;
|
||||
assert_eq!(
|
||||
notification.error,
|
||||
BgpError::UpdateMsg(UpdateMsgSubcode::InvalidNetworkField)
|
||||
);
|
||||
server.wait_for_sessions(0).await?;
|
||||
loop {
|
||||
match tokio::time::timeout(TIMEOUT, peer.recv()).await?? {
|
||||
Message::KeepAlive => {}
|
||||
Message::Notification(n) => {
|
||||
assert_eq!(
|
||||
n.error,
|
||||
BgpError::UpdateMsg(UpdateMsgSubcode::InvalidNetworkField)
|
||||
);
|
||||
break;
|
||||
}
|
||||
other => bail!("expected NOTIFICATION, got {other:?}"),
|
||||
}
|
||||
}
|
||||
tokio::time::timeout(TIMEOUT, async {
|
||||
while !server.registry.list().is_empty() {
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -2,5 +2,6 @@
|
||||
|
||||
This crate implements parsers and serialisers for the Border Gateway Protocol wire format.
|
||||
|
||||
It is used by the daemon in `bin/bgp`, and tested with property tests
|
||||
(`tests/properties.rs`) and fuzz targets (`fuzz/`).
|
||||
## ⚠️ Current status
|
||||
|
||||
This crate is still under development and is not yet ready for use.
|
||||
@@ -1,936 +0,0 @@
|
||||
//! Path attributes: what an UPDATE says about the routes it announces
|
||||
//! (RFC 4271 §5 and those that added to it), each with how it is read
|
||||
//! from the wire and written to it.
|
||||
//!
|
||||
//! Everything here is re-exported from [`crate::message`], which is where
|
||||
//! the rest of the tree names it from.
|
||||
|
||||
use std::net::{Ipv4Addr, Ipv6Addr};
|
||||
|
||||
use bitfield::bitfield;
|
||||
use bytes::{BufMut, BytesMut};
|
||||
use nom::Parser;
|
||||
use nom::number::complete::{be_u8, be_u32};
|
||||
use serde::Serialize;
|
||||
use serde_repr::Serialize_repr;
|
||||
|
||||
use bgp_primitives::constants::{AfiSafi, IpVersion, Safi};
|
||||
use bgp_primitives::ip_prefix::{IpPrefix, Nlri};
|
||||
use bgp_primitives::parser::{BgpParserError, EncodeError, ParserContext};
|
||||
|
||||
use crate::wire::{
|
||||
Parsed, list, malformed, parse_afi_safi, parse_asn, put_afi_safi, put_asn, put_length, records,
|
||||
whole,
|
||||
};
|
||||
|
||||
bitfield! {
|
||||
#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
|
||||
pub struct PathAttributeFlags(u8);
|
||||
impl new;
|
||||
u8;
|
||||
pub optional, set_optional: 7;
|
||||
pub transitive, set_transitive: 6;
|
||||
pub partial, set_partial: 5;
|
||||
pub extended_length, set_extended_length: 4;
|
||||
}
|
||||
|
||||
/// Path attribute type codes.
|
||||
pub mod attr_type {
|
||||
pub const ORIGIN: u8 = 1;
|
||||
pub const AS_PATH: u8 = 2;
|
||||
pub const NEXT_HOP: u8 = 3;
|
||||
pub const MULTI_EXIT_DISC: u8 = 4;
|
||||
pub const LOCAL_PREF: u8 = 5;
|
||||
pub const ATOMIC_AGGREGATE: u8 = 6;
|
||||
pub const AGGREGATOR: u8 = 7;
|
||||
pub const COMMUNITIES: u8 = 8;
|
||||
pub const ORIGINATOR_ID: u8 = 9;
|
||||
pub const CLUSTER_LIST: u8 = 10;
|
||||
pub const MP_REACH_NLRI: u8 = 14;
|
||||
pub const MP_UNREACH_NLRI: u8 = 15;
|
||||
pub const EXTENDED_COMMUNITIES: u8 = 16;
|
||||
pub const AS4_PATH: u8 = 17;
|
||||
pub const AS4_AGGREGATOR: u8 = 18;
|
||||
pub const LARGE_COMMUNITIES: u8 = 32;
|
||||
pub const ONLY_TO_CUSTOMER: u8 = 35;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
|
||||
pub enum PathAttribute {
|
||||
Origin(OriginPathAttribute),
|
||||
AsPath(AsPathAttribute),
|
||||
NextHop(NextHopPathAttribute),
|
||||
MultiExitDisc(MultiExitDiscPathAttribute),
|
||||
LocalPref(LocalPrefPathAttribute),
|
||||
AtomicAggregate(AtomicAggregatePathAttribute),
|
||||
Aggregator(AggregatorPathAttribute),
|
||||
Communities(CommunitiesPathAttribute),
|
||||
MpReachNlri(MpReachNlriPathAttribute),
|
||||
MpUnreachNlri(MpUnreachNlriPathAttribute),
|
||||
ExtendedCommunities(ExtendedCommunitiesPathAttribute),
|
||||
/// The AS path with four-octet AS numbers, for a route that crosses
|
||||
/// speakers that only handle two-octet ones (RFC 6793 §3).
|
||||
As4Path(AsPathAttribute),
|
||||
LargeCommunities(LargeCommunitiesPathAttribute),
|
||||
/// Only to Customer (RFC 9234 §5): the route has been handed to a
|
||||
/// customer, a peer or a route server client by the AS with this
|
||||
/// number, and from there on may only go down to customers.
|
||||
OnlyToCustomer(u32),
|
||||
/// ORIGINATOR_ID (RFC 4456 §8): the BGP Identifier of the router in our
|
||||
/// AS that learned the route from outside, put on by the first router
|
||||
/// that passes the route from one router of the AS to another.
|
||||
OriginatorId(Ipv4Addr),
|
||||
/// CLUSTER_LIST (RFC 4456 §8): the clusters the route has been passed
|
||||
/// on by inside our AS, the last first.
|
||||
ClusterList(Vec<Ipv4Addr>),
|
||||
/// An attribute this implementation does not interpret, kept as it
|
||||
/// came so that it can be passed on.
|
||||
Unknown {
|
||||
flags: PathAttributeFlags,
|
||||
type_code: u8,
|
||||
payload: Vec<u8>,
|
||||
},
|
||||
}
|
||||
|
||||
/// The Optional and Transitive flags that the attribute with this type code
|
||||
/// is defined to have, for the attributes whose definition we know. A
|
||||
/// received attribute with other values is in error (RFC 7606 §3.c).
|
||||
pub(crate) fn defined_flags(type_code: u8) -> Option<(bool, bool)> {
|
||||
use attr_type::*;
|
||||
const WELL_KNOWN: (bool, bool) = (false, true);
|
||||
const OPTIONAL_TRANSITIVE: (bool, bool) = (true, true);
|
||||
const OPTIONAL_NON_TRANSITIVE: (bool, bool) = (true, false);
|
||||
Some(match type_code {
|
||||
ORIGIN | AS_PATH | NEXT_HOP | LOCAL_PREF | ATOMIC_AGGREGATE => WELL_KNOWN,
|
||||
MULTI_EXIT_DISC | ORIGINATOR_ID | CLUSTER_LIST | MP_REACH_NLRI | MP_UNREACH_NLRI => {
|
||||
OPTIONAL_NON_TRANSITIVE
|
||||
}
|
||||
AGGREGATOR | COMMUNITIES | EXTENDED_COMMUNITIES | AS4_PATH | AS4_AGGREGATOR
|
||||
| LARGE_COMMUNITIES | ONLY_TO_CUSTOMER => OPTIONAL_TRANSITIVE,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
/// One attribute as it lies on the wire, split out but not interpreted.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RawAttribute<'a> {
|
||||
pub flags: PathAttributeFlags,
|
||||
pub type_code: u8,
|
||||
pub value: &'a [u8],
|
||||
/// The whole attribute, header and value.
|
||||
pub wire: &'a [u8],
|
||||
}
|
||||
|
||||
/// Why the bytes at the front of a buffer are not an attribute.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum NotAnAttribute {
|
||||
/// Too few octets for flags, type and length.
|
||||
NoHeader,
|
||||
/// The length says more than there is.
|
||||
Truncated {
|
||||
type_code: u8,
|
||||
length: usize,
|
||||
available: usize,
|
||||
},
|
||||
}
|
||||
|
||||
impl std::fmt::Display for NotAnAttribute {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::NoHeader => f.write_str("too few octets for an attribute"),
|
||||
Self::Truncated {
|
||||
type_code,
|
||||
length,
|
||||
available,
|
||||
} => write!(
|
||||
f,
|
||||
"attribute {type_code} has length {length} but only {available} octets remain"
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Splits the attribute at the front of `buf` from what follows it.
|
||||
pub fn split_attribute(buf: &[u8]) -> Result<(RawAttribute<'_>, &[u8]), NotAnAttribute> {
|
||||
let [flags, type_code, rest @ ..] = buf else {
|
||||
return Err(NotAnAttribute::NoHeader);
|
||||
};
|
||||
let flags = PathAttributeFlags(*flags);
|
||||
let (length, rest) = match (flags.extended_length(), rest) {
|
||||
(true, [hi, lo, rest @ ..]) => (usize::from(u16::from_be_bytes([*hi, *lo])), rest),
|
||||
(false, [length, rest @ ..]) => (usize::from(*length), rest),
|
||||
_ => return Err(NotAnAttribute::NoHeader),
|
||||
};
|
||||
let Some((value, rest)) = rest.split_at_checked(length) else {
|
||||
return Err(NotAnAttribute::Truncated {
|
||||
type_code: *type_code,
|
||||
length,
|
||||
available: rest.len(),
|
||||
});
|
||||
};
|
||||
let raw = RawAttribute {
|
||||
flags,
|
||||
type_code: *type_code,
|
||||
value,
|
||||
wire: &buf[..buf.len() - rest.len()],
|
||||
};
|
||||
Ok((raw, rest))
|
||||
}
|
||||
|
||||
impl PathAttribute {
|
||||
pub fn type_code(&self) -> u8 {
|
||||
use attr_type::*;
|
||||
match self {
|
||||
Self::Origin(_) => ORIGIN,
|
||||
Self::AsPath(_) => AS_PATH,
|
||||
Self::NextHop(_) => NEXT_HOP,
|
||||
Self::MultiExitDisc(_) => MULTI_EXIT_DISC,
|
||||
Self::LocalPref(_) => LOCAL_PREF,
|
||||
Self::AtomicAggregate(_) => ATOMIC_AGGREGATE,
|
||||
Self::Aggregator(_) => AGGREGATOR,
|
||||
Self::Communities(_) => COMMUNITIES,
|
||||
Self::MpReachNlri(_) => MP_REACH_NLRI,
|
||||
Self::MpUnreachNlri(_) => MP_UNREACH_NLRI,
|
||||
Self::ExtendedCommunities(_) => EXTENDED_COMMUNITIES,
|
||||
Self::As4Path(_) => AS4_PATH,
|
||||
Self::LargeCommunities(_) => LARGE_COMMUNITIES,
|
||||
Self::OnlyToCustomer(_) => ONLY_TO_CUSTOMER,
|
||||
Self::OriginatorId(_) => ORIGINATOR_ID,
|
||||
Self::ClusterList(_) => CLUSTER_LIST,
|
||||
Self::Unknown { type_code, .. } => *type_code,
|
||||
}
|
||||
}
|
||||
|
||||
/// The flags to send the attribute with, but for Extended Length, which
|
||||
/// depends on how long the value turns out to be.
|
||||
fn flags(&self) -> PathAttributeFlags {
|
||||
match self {
|
||||
// As they came: the Partial bit has to survive (RFC 4271 §5).
|
||||
Self::Unknown { flags, .. } => flags.clone(),
|
||||
known => {
|
||||
let (optional, transitive) = defined_flags(known.type_code())
|
||||
.expect("every attribute with a variant has defined flags");
|
||||
PathAttributeFlags::new(optional, transitive, false, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses one attribute, header and value, from the front of `buf`.
|
||||
/// The flags are not checked against the attribute's definition;
|
||||
/// [`crate::message::UpdateMessage::decode`] does that for attributes off a session.
|
||||
pub fn from_wire<'a>(ctx: &ParserContext, buf: &'a [u8]) -> Parsed<'a, Self> {
|
||||
let (raw, rest) = split_attribute(buf).map_err(|e| malformed(e.to_string()))?;
|
||||
let attribute = Self::parse_value(ctx, raw.flags, raw.type_code, raw.value)?;
|
||||
Ok((rest, attribute))
|
||||
}
|
||||
|
||||
/// Makes the attribute with this type code from its value. `value` is
|
||||
/// exactly the value, and all of it has to be used. A type code without
|
||||
/// a variant of its own is kept as bytes.
|
||||
pub(crate) fn parse_value<'a>(
|
||||
ctx: &ParserContext,
|
||||
flags: PathAttributeFlags,
|
||||
type_code: u8,
|
||||
value: &'a [u8],
|
||||
) -> Result<Self, nom::Err<BgpParserError<&'a [u8]>>> {
|
||||
use attr_type::*;
|
||||
Ok(match type_code {
|
||||
ORIGIN => Self::Origin(whole("ORIGIN", OriginPathAttribute::from_wire(value)?)?),
|
||||
AS_PATH => Self::AsPath(whole("AS_PATH", AsPathAttribute::from_wire(ctx, value)?)?),
|
||||
NEXT_HOP => Self::NextHop(whole("NEXT_HOP", NextHopPathAttribute::from_wire(value)?)?),
|
||||
MULTI_EXIT_DISC => Self::MultiExitDisc(whole(
|
||||
"MULTI_EXIT_DISC",
|
||||
MultiExitDiscPathAttribute::from_wire(value)?,
|
||||
)?),
|
||||
LOCAL_PREF => Self::LocalPref(whole(
|
||||
"LOCAL_PREF",
|
||||
LocalPrefPathAttribute::from_wire(value)?,
|
||||
)?),
|
||||
ATOMIC_AGGREGATE => Self::AtomicAggregate(whole(
|
||||
"ATOMIC_AGGREGATE",
|
||||
(value, AtomicAggregatePathAttribute {}),
|
||||
)?),
|
||||
AGGREGATOR => Self::Aggregator(whole(
|
||||
"AGGREGATOR",
|
||||
AggregatorPathAttribute::from_wire(ctx, value)?,
|
||||
)?),
|
||||
COMMUNITIES => Self::Communities(whole(
|
||||
"COMMUNITIES",
|
||||
CommunitiesPathAttribute::from_wire(value)?,
|
||||
)?),
|
||||
MP_REACH_NLRI => Self::MpReachNlri(whole(
|
||||
"MP_REACH_NLRI",
|
||||
MpReachNlriPathAttribute::from_wire(ctx, value)?,
|
||||
)?),
|
||||
MP_UNREACH_NLRI => Self::MpUnreachNlri(whole(
|
||||
"MP_UNREACH_NLRI",
|
||||
MpUnreachNlriPathAttribute::from_wire(ctx, value)?,
|
||||
)?),
|
||||
EXTENDED_COMMUNITIES => Self::ExtendedCommunities(whole(
|
||||
"EXTENDED_COMMUNITIES",
|
||||
ExtendedCommunitiesPathAttribute::from_wire(value)?,
|
||||
)?),
|
||||
// Always four octets wide, whatever the session's AS_PATH is.
|
||||
AS4_PATH => Self::As4Path(whole(
|
||||
"AS4_PATH",
|
||||
AsPathAttribute::from_wire_with_width(true, value)?,
|
||||
)?),
|
||||
LARGE_COMMUNITIES => Self::LargeCommunities(whole(
|
||||
"LARGE_COMMUNITIES",
|
||||
LargeCommunitiesPathAttribute::from_wire(value)?,
|
||||
)?),
|
||||
ONLY_TO_CUSTOMER => {
|
||||
let asn = <[u8; 4]>::try_from(value).map_err(|_| {
|
||||
malformed(format!(
|
||||
"Only to Customer is {} octets long, not four",
|
||||
value.len()
|
||||
))
|
||||
})?;
|
||||
Self::OnlyToCustomer(u32::from_be_bytes(asn))
|
||||
}
|
||||
ORIGINATOR_ID => {
|
||||
let id = <[u8; 4]>::try_from(value).map_err(|_| {
|
||||
malformed(format!(
|
||||
"ORIGINATOR_ID is {} octets long, not four",
|
||||
value.len()
|
||||
))
|
||||
})?;
|
||||
Self::OriginatorId(Ipv4Addr::from(id))
|
||||
}
|
||||
CLUSTER_LIST => Self::ClusterList(
|
||||
records::<4>(value, "CLUSTER_LIST")?
|
||||
.iter()
|
||||
.map(|id| Ipv4Addr::from(*id))
|
||||
.collect(),
|
||||
),
|
||||
_ => Self::Unknown {
|
||||
flags,
|
||||
type_code,
|
||||
payload: value.to_vec(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/// Writes the attribute, header and value, to the end of `out`.
|
||||
pub fn to_wire(&self, ctx: &ParserContext, out: &mut BytesMut) -> Result<(), EncodeError> {
|
||||
// A header with a one-octet length first; whether the value needs
|
||||
// the two-octet one is known once it is written.
|
||||
let start = out.len();
|
||||
let mut flags = self.flags();
|
||||
out.put_u8(0);
|
||||
out.put_u8(self.type_code());
|
||||
let length_at = out.len();
|
||||
out.put_u8(0);
|
||||
self.value_to_wire(ctx, out)?;
|
||||
let length = out.len() - length_at - 1;
|
||||
|
||||
if let Ok(short) = u8::try_from(length)
|
||||
&& !flags.extended_length()
|
||||
{
|
||||
out[length_at] = short;
|
||||
} else {
|
||||
flags.set_extended_length(true);
|
||||
// Move the value up by one octet to make room.
|
||||
out.put_u8(0);
|
||||
out.copy_within(length_at + 1..length_at + 1 + length, length_at + 2);
|
||||
put_length(out, length_at, 2, length, u16::MAX.into(), "path attribute")?;
|
||||
}
|
||||
out[start] = flags.0;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn value_to_wire(&self, ctx: &ParserContext, out: &mut BytesMut) -> Result<(), EncodeError> {
|
||||
match self {
|
||||
Self::Origin(a) => a.to_wire(out),
|
||||
Self::AsPath(a) => a.to_wire(ctx, out)?,
|
||||
Self::NextHop(a) => a.to_wire(out),
|
||||
Self::MultiExitDisc(a) => a.to_wire(out),
|
||||
Self::LocalPref(a) => a.to_wire(out),
|
||||
Self::AtomicAggregate(_) => {}
|
||||
Self::Aggregator(a) => a.to_wire(ctx, out),
|
||||
Self::Communities(a) => a.to_wire(out),
|
||||
Self::MpReachNlri(a) => a.to_wire(ctx, out),
|
||||
Self::MpUnreachNlri(a) => a.to_wire(ctx, out),
|
||||
Self::ExtendedCommunities(a) => a.to_wire(out),
|
||||
Self::As4Path(a) => a.to_wire_with_width(true, out)?,
|
||||
Self::LargeCommunities(a) => a.to_wire(out),
|
||||
Self::OnlyToCustomer(asn) => out.put_u32(*asn),
|
||||
Self::OriginatorId(id) => out.put_slice(&id.octets()),
|
||||
Self::ClusterList(ids) => {
|
||||
for id in ids {
|
||||
out.put_slice(&id.octets());
|
||||
}
|
||||
}
|
||||
Self::Unknown { payload, .. } => out.put_slice(payload),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// An Only to Customer attribute with this AS number, as it is on the
|
||||
/// wire: for adding to attributes that are kept as bytes.
|
||||
pub fn only_to_customer_wire(asn: u32) -> [u8; 7] {
|
||||
const OPTIONAL_TRANSITIVE: u8 = 0xc0;
|
||||
let [a, b, c, d] = asn.to_be_bytes();
|
||||
[
|
||||
OPTIONAL_TRANSITIVE,
|
||||
attr_type::ONLY_TO_CUSTOMER,
|
||||
4,
|
||||
a,
|
||||
b,
|
||||
c,
|
||||
d,
|
||||
]
|
||||
}
|
||||
|
||||
/// ORIGIN: how the route came into BGP (RFC 4271 §5.1.1).
|
||||
#[derive(Debug, PartialEq, Eq, Copy, Clone, Serialize_repr)]
|
||||
#[repr(u8)]
|
||||
pub enum OriginPathAttribute {
|
||||
IGP = 0,
|
||||
EGP = 1,
|
||||
INCOMPLETE = 2,
|
||||
}
|
||||
|
||||
impl OriginPathAttribute {
|
||||
pub fn from_wire(buf: &[u8]) -> Parsed<'_, Self> {
|
||||
let (buf, byte) = be_u8(buf)?;
|
||||
let origin = match byte {
|
||||
0 => Self::IGP,
|
||||
1 => Self::EGP,
|
||||
2 => Self::INCOMPLETE,
|
||||
other => return Err(malformed(format!("unknown ORIGIN {other}"))),
|
||||
};
|
||||
Ok((buf, origin))
|
||||
}
|
||||
|
||||
pub fn to_wire(&self, out: &mut BytesMut) {
|
||||
out.put_u8(*self as u8);
|
||||
}
|
||||
}
|
||||
|
||||
/// AS_PATH: the ASes a route has passed through, as a list of segments
|
||||
/// (RFC 4271 §4.3). Each segment is a type (1 for AS_SET, 2 for
|
||||
/// AS_SEQUENCE), a count in one octet, and that many AS numbers.
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Serialize)]
|
||||
pub struct AsPathAttribute {
|
||||
pub segments: Vec<AsPathSegment>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Serialize)]
|
||||
pub struct AsPathSegment {
|
||||
/// True for an AS_SEQUENCE, false for an AS_SET.
|
||||
pub ordered: bool,
|
||||
pub path: Vec<u32>,
|
||||
}
|
||||
|
||||
impl AsPathAttribute {
|
||||
pub fn from_asns(asns: Vec<u32>) -> PathAttribute {
|
||||
let segment = AsPathSegment {
|
||||
ordered: true,
|
||||
path: asns,
|
||||
};
|
||||
PathAttribute::AsPath(AsPathAttribute {
|
||||
segments: vec![segment],
|
||||
})
|
||||
}
|
||||
|
||||
/// How long the path is, as RFC 4271 §9.1.2.2 counts: one for each AS
|
||||
/// of a sequence, one for a whole set.
|
||||
pub fn hops(&self) -> usize {
|
||||
self.segments
|
||||
.iter()
|
||||
.map(|segment| {
|
||||
if segment.ordered {
|
||||
segment.path.len()
|
||||
} else {
|
||||
1
|
||||
}
|
||||
})
|
||||
.sum()
|
||||
}
|
||||
|
||||
/// The path as it really is, given what a two-octet session carried:
|
||||
/// this, with AS_TRANS wherever a number did not fit, and `wide`, the
|
||||
/// AS4_PATH that has the numbers themselves for the path's tail
|
||||
/// (RFC 6793 §4.2.3). What `wide` does not cover is kept from the
|
||||
/// front of this.
|
||||
///
|
||||
/// `None` if `wide` is longer than the path, which is not to be used:
|
||||
/// some speaker on the way that knew nothing of AS4_PATH changed the
|
||||
/// one and not the other.
|
||||
pub fn with_as4_path(&self, wide: &AsPathAttribute) -> Option<AsPathAttribute> {
|
||||
let mut keep = self.hops().checked_sub(wide.hops())?;
|
||||
let mut segments = Vec::with_capacity(self.segments.len() + wide.segments.len());
|
||||
for segment in &self.segments {
|
||||
if keep == 0 {
|
||||
break;
|
||||
}
|
||||
let (hops, taken) = if segment.ordered {
|
||||
let taken = segment.path.len().min(keep);
|
||||
(taken, &segment.path[..taken])
|
||||
} else {
|
||||
(1, &segment.path[..])
|
||||
};
|
||||
keep -= hops;
|
||||
segments.push(AsPathSegment {
|
||||
ordered: segment.ordered,
|
||||
path: taken.to_vec(),
|
||||
});
|
||||
}
|
||||
segments.extend(wide.segments.iter().cloned());
|
||||
Some(AsPathAttribute { segments })
|
||||
}
|
||||
|
||||
/// Parses an AS_PATH with AS numbers as wide as the session negotiated
|
||||
/// (RFC 6793).
|
||||
pub fn from_wire<'a>(ctx: &ParserContext, buf: &'a [u8]) -> Parsed<'a, Self> {
|
||||
Self::from_wire_with_width(ctx.four_octet_asn, buf)
|
||||
}
|
||||
|
||||
fn from_wire_with_width(four_octet: bool, buf: &[u8]) -> Parsed<'_, Self> {
|
||||
let parse_segment = |buf| -> Parsed<'_, AsPathSegment> {
|
||||
let (buf, typ) = be_u8(buf)?;
|
||||
let (buf, len) = be_u8(buf)?;
|
||||
// AS_SET and AS_SEQUENCE. The confederation segment types of
|
||||
// RFC 5065 are an error from a peer outside a confederation,
|
||||
// and this speaker is never inside one.
|
||||
if typ != 1 && typ != 2 {
|
||||
return Err(malformed(format!(
|
||||
"unrecognised AS path segment type {typ}"
|
||||
)));
|
||||
}
|
||||
if len == 0 {
|
||||
return Err(malformed("AS path segment with no AS numbers"));
|
||||
}
|
||||
let count = usize::from(len);
|
||||
let (buf, path) =
|
||||
nom::multi::many_m_n(count, count, |buf| parse_asn(four_octet, buf)).parse(buf)?;
|
||||
Ok((
|
||||
buf,
|
||||
AsPathSegment {
|
||||
ordered: typ == 2,
|
||||
path,
|
||||
},
|
||||
))
|
||||
};
|
||||
let (buf, segments) = list(buf, parse_segment)?;
|
||||
Ok((buf, Self { segments }))
|
||||
}
|
||||
|
||||
/// Writes the path with AS numbers as wide as the session negotiated.
|
||||
/// On a two-octet session an AS number that does not fit is written as
|
||||
/// AS_TRANS (RFC 6793 §4.2.2); [`AsPathAttribute::as4_path`] is what
|
||||
/// carries the real numbers alongside.
|
||||
pub fn to_wire(&self, ctx: &ParserContext, out: &mut BytesMut) -> Result<(), EncodeError> {
|
||||
self.to_wire_with_width(ctx.four_octet_asn, out)
|
||||
}
|
||||
|
||||
fn to_wire_with_width(&self, four_octet: bool, out: &mut BytesMut) -> Result<(), EncodeError> {
|
||||
for segment in &self.segments {
|
||||
out.put_u8(if segment.ordered { 2 } else { 1 });
|
||||
let count = u8::try_from(segment.path.len()).map_err(|_| EncodeError::TooLong {
|
||||
what: "AS path segment",
|
||||
len: segment.path.len(),
|
||||
max: u8::MAX.into(),
|
||||
})?;
|
||||
out.put_u8(count);
|
||||
for asn in &segment.path {
|
||||
put_asn(four_octet, *asn, out);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The AS4_PATH to send with this path on a two-octet session, if one is
|
||||
/// needed: it is when some AS number does not fit in two octets and so
|
||||
/// goes out as AS_TRANS in the AS_PATH itself (RFC 6793 §4.2.2).
|
||||
pub fn as4_path(&self) -> Option<PathAttribute> {
|
||||
self.segments
|
||||
.iter()
|
||||
.flat_map(|segment| &segment.path)
|
||||
.any(|asn| u16::try_from(*asn).is_err())
|
||||
.then(|| PathAttribute::As4Path(self.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Serialize)]
|
||||
pub struct NextHopPathAttribute(pub Ipv4Addr);
|
||||
|
||||
impl NextHopPathAttribute {
|
||||
pub fn from_wire(buf: &[u8]) -> Parsed<'_, Self> {
|
||||
let (buf, address) = be_u32(buf)?;
|
||||
Ok((buf, Self(Ipv4Addr::from(address))))
|
||||
}
|
||||
|
||||
pub fn to_wire(&self, out: &mut BytesMut) {
|
||||
out.put_u32(self.0.into());
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Serialize)]
|
||||
pub struct MultiExitDiscPathAttribute(pub u32);
|
||||
|
||||
impl MultiExitDiscPathAttribute {
|
||||
pub fn from_wire(buf: &[u8]) -> Parsed<'_, Self> {
|
||||
let (buf, value) = be_u32(buf)?;
|
||||
Ok((buf, Self(value)))
|
||||
}
|
||||
|
||||
pub fn to_wire(&self, out: &mut BytesMut) {
|
||||
out.put_u32(self.0);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Serialize)]
|
||||
pub struct LocalPrefPathAttribute(pub u32);
|
||||
|
||||
impl LocalPrefPathAttribute {
|
||||
pub fn from_wire(buf: &[u8]) -> Parsed<'_, Self> {
|
||||
let (buf, value) = be_u32(buf)?;
|
||||
Ok((buf, Self(value)))
|
||||
}
|
||||
|
||||
pub fn to_wire(&self, out: &mut BytesMut) {
|
||||
out.put_u32(self.0);
|
||||
}
|
||||
}
|
||||
|
||||
/// ATOMIC_AGGREGATE: a marker with no value (RFC 4271 §5.1.6).
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Serialize)]
|
||||
pub struct AtomicAggregatePathAttribute {}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Serialize)]
|
||||
pub struct AggregatorPathAttribute {
|
||||
pub asn: u32,
|
||||
pub ip: Ipv4Addr,
|
||||
}
|
||||
|
||||
impl AggregatorPathAttribute {
|
||||
/// The AS number is as wide as the session's AS numbers (RFC 6793), so
|
||||
/// the attribute is six or eight octets.
|
||||
pub fn from_wire<'a>(ctx: &ParserContext, buf: &'a [u8]) -> Parsed<'a, Self> {
|
||||
let (buf, asn) = parse_asn(ctx.four_octet_asn, buf)?;
|
||||
let (buf, ip) = be_u32(buf)?;
|
||||
Ok((
|
||||
buf,
|
||||
Self {
|
||||
asn,
|
||||
ip: Ipv4Addr::from(ip),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
/// On a two-octet session an AS number that does not fit is written as
|
||||
/// AS_TRANS (RFC 6793 §4.2.2).
|
||||
pub fn to_wire(&self, ctx: &ParserContext, out: &mut BytesMut) {
|
||||
put_asn(ctx.four_octet_asn, self.asn, out);
|
||||
out.put_u32(self.ip.into());
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Serialize)]
|
||||
pub struct CommunitiesPathAttribute(pub Vec<(u16, u16)>);
|
||||
|
||||
impl CommunitiesPathAttribute {
|
||||
/// Reads all of `buf` as communities, four octets each.
|
||||
pub fn from_wire(buf: &[u8]) -> Parsed<'_, Self> {
|
||||
let values = records::<4>(buf, "COMMUNITIES")?
|
||||
.iter()
|
||||
.map(|[a, b, c, d]| (u16::from_be_bytes([*a, *b]), u16::from_be_bytes([*c, *d])))
|
||||
.collect();
|
||||
Ok((&buf[buf.len()..], CommunitiesPathAttribute(values)))
|
||||
}
|
||||
|
||||
pub fn to_wire(&self, out: &mut BytesMut) {
|
||||
for value in &self.0 {
|
||||
out.put_u16(value.0);
|
||||
out.put_u16(value.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// EXTENDED COMMUNITIES (RFC 4360).
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Serialize)]
|
||||
pub struct ExtendedCommunitiesPathAttribute {
|
||||
pub extended_communities: Vec<ExtendedCommunity>,
|
||||
}
|
||||
|
||||
impl ExtendedCommunitiesPathAttribute {
|
||||
/// Reads all of `buf` as extended communities, eight octets each.
|
||||
pub fn from_wire(buf: &[u8]) -> Parsed<'_, Self> {
|
||||
let communities = records::<8>(buf, "EXTENDED_COMMUNITIES")?;
|
||||
Ok((
|
||||
&buf[buf.len()..],
|
||||
Self {
|
||||
extended_communities: communities.iter().copied().map(ExtendedCommunity).collect(),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
pub fn to_wire(&self, out: &mut BytesMut) {
|
||||
for community in &self.extended_communities {
|
||||
out.put_slice(&community.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One extended community: eight octets, of which the first is the type
|
||||
/// and, for most types, the second a sub-type (RFC 4360 §2). How the rest
|
||||
/// divides into an administrator and a value depends on the type, and is
|
||||
/// left to whoever acts on a particular community.
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize)]
|
||||
pub struct ExtendedCommunity(pub [u8; 8]);
|
||||
|
||||
impl ExtendedCommunity {
|
||||
pub fn typ(&self) -> u8 {
|
||||
self.0[0]
|
||||
}
|
||||
|
||||
pub fn sub_typ(&self) -> u8 {
|
||||
self.0[1]
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
|
||||
pub struct LargeCommunitiesPathAttribute {
|
||||
pub communities: Vec<LargeCommunity>,
|
||||
}
|
||||
|
||||
impl LargeCommunitiesPathAttribute {
|
||||
/// Reads all of `buf` as large communities, twelve octets each.
|
||||
pub fn from_wire(buf: &[u8]) -> Parsed<'_, Self> {
|
||||
let communities = records::<12>(buf, "LARGE_COMMUNITIES")?
|
||||
.iter()
|
||||
.map(|community| {
|
||||
let (_, community) =
|
||||
LargeCommunity::from_wire(community).expect("twelve octets are one");
|
||||
community
|
||||
})
|
||||
.collect();
|
||||
Ok((&buf[buf.len()..], Self { communities }))
|
||||
}
|
||||
|
||||
pub fn to_wire(&self, out: &mut BytesMut) {
|
||||
for community in &self.communities {
|
||||
community.to_wire(out);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
|
||||
pub struct LargeCommunity {
|
||||
pub global_admin: u32,
|
||||
pub data_1: u32,
|
||||
pub data_2: u32,
|
||||
}
|
||||
|
||||
impl LargeCommunity {
|
||||
pub fn from_wire(buf: &[u8]) -> Parsed<'_, Self> {
|
||||
let (buf, global_admin) = be_u32(buf)?;
|
||||
let (buf, data_1) = be_u32(buf)?;
|
||||
let (buf, data_2) = be_u32(buf)?;
|
||||
Ok((
|
||||
buf,
|
||||
Self {
|
||||
global_admin,
|
||||
data_1,
|
||||
data_2,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
pub fn to_wire(&self, out: &mut BytesMut) {
|
||||
out.put_u32(self.global_admin);
|
||||
out.put_u32(self.data_1);
|
||||
out.put_u32(self.data_2);
|
||||
}
|
||||
}
|
||||
|
||||
/// The next hop of routes in MP_REACH_NLRI.
|
||||
#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq, Hash)]
|
||||
pub enum NlriNextHop {
|
||||
Ipv4(Ipv4Addr),
|
||||
/// A global IPv6 address.
|
||||
Ipv6(Ipv6Addr),
|
||||
/// A global IPv6 address and a link-local one (RFC 2545 §3).
|
||||
Ipv6WithLl {
|
||||
global: Ipv6Addr,
|
||||
link_local: Ipv6Addr,
|
||||
},
|
||||
}
|
||||
|
||||
impl std::fmt::Display for NlriNextHop {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
NlriNextHop::Ipv4(addr) => write!(f, "{addr}"),
|
||||
NlriNextHop::Ipv6(addr) => write!(f, "{addr}"),
|
||||
NlriNextHop::Ipv6WithLl { global, link_local } => {
|
||||
write!(f, "{global} (link-local {link_local})")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl NlriNextHop {
|
||||
/// Reads a next hop that is all of `bytes`. `None` if they are not the
|
||||
/// length of one.
|
||||
pub fn from_bytes(bytes: &[u8]) -> Option<Self> {
|
||||
let v6 = |bytes: &[u8]| <[u8; 16]>::try_from(bytes).ok().map(Ipv6Addr::from);
|
||||
match bytes.len() {
|
||||
4 => <[u8; 4]>::try_from(bytes)
|
||||
.ok()
|
||||
.map(|a| Self::Ipv4(a.into())),
|
||||
16 => v6(bytes).map(Self::Ipv6),
|
||||
32 => Some(Self::Ipv6WithLl {
|
||||
global: v6(&bytes[..16])?,
|
||||
link_local: v6(&bytes[16..])?,
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_wire(&self, out: &mut BytesMut) {
|
||||
match self {
|
||||
NlriNextHop::Ipv4(address) => out.put_slice(&address.octets()),
|
||||
NlriNextHop::Ipv6(address) => out.put_slice(&address.octets()),
|
||||
NlriNextHop::Ipv6WithLl { global, link_local } => {
|
||||
out.put_slice(&global.octets());
|
||||
out.put_slice(&link_local.octets());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// How many octets the next hop is on the wire.
|
||||
pub fn wire_len(&self) -> u8 {
|
||||
match self {
|
||||
NlriNextHop::Ipv4(_) => 4,
|
||||
NlriNextHop::Ipv6(_) => 16,
|
||||
NlriNextHop::Ipv6WithLl { .. } => 32,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses one prefix as an UPDATE names it: with a path identifier in
|
||||
/// front if the session uses them for the family (RFC 7911 §3).
|
||||
fn parse_nlri(version: IpVersion, with_path_id: bool, buf: &[u8]) -> Parsed<'_, Nlri> {
|
||||
let (buf, path_id) = if with_path_id { be_u32(buf)? } else { (buf, 0) };
|
||||
let (buf, prefix_len) = be_u8(buf)?;
|
||||
let byte_len = usize::from(prefix_len.div_ceil(8));
|
||||
let (buf, prefix_bytes) = nom::bytes::take(byte_len).parse(buf)?;
|
||||
let prefix =
|
||||
IpPrefix::new(version, prefix_bytes, prefix_len).map_err(|e| malformed(e.to_string()))?;
|
||||
Ok((buf, Nlri { prefix, path_id }))
|
||||
}
|
||||
|
||||
/// Parses a field that is nothing but prefixes, to its end.
|
||||
pub(crate) fn parse_nlri_list(
|
||||
version: IpVersion,
|
||||
with_path_id: bool,
|
||||
buf: &[u8],
|
||||
) -> Parsed<'_, Vec<Nlri>> {
|
||||
list(buf, |buf| parse_nlri(version, with_path_id, buf))
|
||||
}
|
||||
|
||||
/// Writes prefixes as `parse_nlri_list` reads them: for each, its path
|
||||
/// identifier if the session uses them for the family (RFC 7911 §3), its
|
||||
/// length in bits, and the bytes that length covers.
|
||||
pub fn put_nlri_list(nlri: &[Nlri], with_path_id: bool, out: &mut BytesMut) {
|
||||
for n in nlri {
|
||||
if with_path_id {
|
||||
out.put_u32(n.path_id);
|
||||
}
|
||||
out.put_u8(n.prefix.length());
|
||||
out.put(n.prefix.bytes());
|
||||
}
|
||||
}
|
||||
|
||||
/// The IP version of the prefixes that make up the NLRI of `family`.
|
||||
///
|
||||
/// Any family can be named in MP_REACH_NLRI and MP_UNREACH_NLRI, but only
|
||||
/// unicast and multicast routes of IPv4 and IPv6 are plain prefixes, and
|
||||
/// those are the only NLRI this parser reads. A peer does not send routes
|
||||
/// of a family that was not negotiated, so the others do not come up.
|
||||
fn ip_version_of_nlri<'a>(
|
||||
family: AfiSafi,
|
||||
) -> Result<IpVersion, nom::Err<BgpParserError<&'a [u8]>>> {
|
||||
let unsupported = || malformed(format!("NLRI of {family} is not supported"));
|
||||
if family.safi != Safi::UNICAST && family.safi != Safi::MULTICAST {
|
||||
return Err(unsupported());
|
||||
}
|
||||
IpVersion::try_from(family.afi).map_err(|_| unsupported())
|
||||
}
|
||||
|
||||
/// MP_REACH_NLRI: routes of a family other than the one in the message
|
||||
/// body, with their next hop (RFC 4760 §3).
|
||||
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
|
||||
pub struct MpReachNlriPathAttribute {
|
||||
pub family: AfiSafi,
|
||||
pub next_hop: NlriNextHop,
|
||||
pub prefixes: Vec<Nlri>,
|
||||
}
|
||||
|
||||
impl MpReachNlriPathAttribute {
|
||||
pub fn from_wire<'a>(ctx: &ParserContext, buf: &'a [u8]) -> Parsed<'a, Self> {
|
||||
let (buf, family) = parse_afi_safi(buf)?;
|
||||
let version = ip_version_of_nlri(family)?;
|
||||
|
||||
let (buf, next_hop_len) = be_u8(buf)?;
|
||||
let (buf, next_hop) = nom::bytes::take(usize::from(next_hop_len)).parse(buf)?;
|
||||
// For IPv6 routes, a global address, optionally followed by a
|
||||
// link-local one (RFC 2545). For IPv4 routes an IPv4 address, or
|
||||
// the same as for IPv6: routes over a link that has only IPv6
|
||||
// addresses (RFC 8950). Which of the two it is shows in its length.
|
||||
let next_hop = NlriNextHop::from_bytes(next_hop)
|
||||
.filter(|next_hop| {
|
||||
version == IpVersion::Ipv4 || !matches!(next_hop, NlriNextHop::Ipv4(_))
|
||||
})
|
||||
.ok_or_else(|| {
|
||||
malformed(format!(
|
||||
"a next hop of {next_hop_len} octets is not valid for {family}"
|
||||
))
|
||||
})?;
|
||||
|
||||
// Reserved octet (formerly the SNPA count), for every family.
|
||||
let (buf, _) = be_u8(buf)?;
|
||||
let (buf, prefixes) = parse_nlri_list(version, ctx.has_add_path(family), buf)?;
|
||||
Ok((
|
||||
buf,
|
||||
Self {
|
||||
family,
|
||||
next_hop,
|
||||
prefixes,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
pub fn to_wire(&self, ctx: &ParserContext, out: &mut BytesMut) {
|
||||
put_afi_safi(self.family, out);
|
||||
out.put_u8(self.next_hop.wire_len());
|
||||
self.next_hop.to_wire(out);
|
||||
out.put_u8(0);
|
||||
put_nlri_list(&self.prefixes, ctx.has_add_path(self.family), out);
|
||||
}
|
||||
}
|
||||
|
||||
/// MP_UNREACH_NLRI: withdrawals of routes of a family other than the one
|
||||
/// in the message body (RFC 4760 §4).
|
||||
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
|
||||
pub struct MpUnreachNlriPathAttribute {
|
||||
pub family: AfiSafi,
|
||||
pub prefixes: Vec<Nlri>,
|
||||
}
|
||||
|
||||
impl MpUnreachNlriPathAttribute {
|
||||
pub fn from_wire<'a>(ctx: &ParserContext, buf: &'a [u8]) -> Parsed<'a, Self> {
|
||||
let (buf, family) = parse_afi_safi(buf)?;
|
||||
let version = ip_version_of_nlri(family)?;
|
||||
let (buf, prefixes) = parse_nlri_list(version, ctx.has_add_path(family), buf)?;
|
||||
Ok((buf, Self { family, prefixes }))
|
||||
}
|
||||
|
||||
pub fn to_wire(&self, ctx: &ParserContext, out: &mut BytesMut) {
|
||||
put_afi_safi(self.family, out);
|
||||
put_nlri_list(&self.prefixes, ctx.has_add_path(self.family), out);
|
||||
}
|
||||
}
|
||||
@@ -128,9 +128,8 @@ pub enum OpenMsgSubcode {
|
||||
#[error("Unsupported capability")]
|
||||
UnsupportedCapability = 7,
|
||||
|
||||
/// RFC 9234 §4.2.
|
||||
#[error("Role mismatch")]
|
||||
RoleMismatch = 11,
|
||||
RoleMismatch = 8,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error, Serialize, FromRepr)]
|
||||
@@ -170,10 +169,6 @@ pub enum UpdateMsgSubcode {
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error, Serialize, FromRepr)]
|
||||
#[repr(u8)]
|
||||
pub enum FsmSubcode {
|
||||
/// RFC 6608: what speakers from before it send, and FRR still does.
|
||||
#[error("unspecified")]
|
||||
Unspecified = 0,
|
||||
|
||||
#[error("Received an unexpected message in OpenSent state")]
|
||||
UnexpectedOpenSent = 1,
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
pub mod attributes;
|
||||
pub mod community;
|
||||
pub mod errors;
|
||||
pub mod message;
|
||||
pub mod open;
|
||||
pub mod update_decode;
|
||||
mod wire;
|
||||
+916
-183
File diff suppressed because it is too large.
Load diff
+100
-83
@@ -4,7 +4,7 @@ use bitfield::bitfield;
|
||||
use bytes::{BufMut, BytesMut};
|
||||
use nom::{
|
||||
Err::Failure,
|
||||
Parser,
|
||||
IResult, Parser,
|
||||
number::complete::{be_u8, be_u16, be_u24, be_u32},
|
||||
};
|
||||
use serde::Serialize;
|
||||
@@ -16,8 +16,26 @@ use bgp_primitives::{
|
||||
parser::{BgpParserError, EncodeError, ProtocolErrorValues},
|
||||
};
|
||||
|
||||
use crate::message::BGP4_VERSION;
|
||||
use crate::wire::{Parsed, fill_length, list, malformed, parse_afi_safi, put_afi_safi, whole};
|
||||
type Parsed<'a, T> = IResult<&'a [u8], T, BgpParserError<&'a [u8]>>;
|
||||
|
||||
macro_rules! fail {
|
||||
( $($arg:expr),* $(,)? ) => {
|
||||
Failure(BgpParserError::Malformed(format!($($arg),*)))
|
||||
};
|
||||
}
|
||||
|
||||
/// Parses `buf` to its end as a list of `item`s. Unlike nom's `many0`,
|
||||
/// which stops quietly at an item it cannot read, an error in an item is an
|
||||
/// error of the list.
|
||||
fn list<'a, T>(mut buf: &'a [u8], item: impl Fn(&'a [u8]) -> Parsed<'a, T>) -> Parsed<'a, Vec<T>> {
|
||||
let mut items = Vec::new();
|
||||
while !buf.is_empty() {
|
||||
let (rest, parsed) = item(buf)?;
|
||||
items.push(parsed);
|
||||
buf = rest;
|
||||
}
|
||||
Ok((buf, items))
|
||||
}
|
||||
|
||||
/// The optional parameter type that marks an OPEN whose optional parameters
|
||||
/// have two-octet lengths (RFC 9072).
|
||||
@@ -44,7 +62,7 @@ impl OpenMessage {
|
||||
/// in a well-formed OPEN is nothing.
|
||||
pub fn from_wire(buf: &[u8]) -> Parsed<'_, Self> {
|
||||
let (buf, version) = be_u8(buf)?;
|
||||
if version != BGP4_VERSION {
|
||||
if version != 4 {
|
||||
return Err(Failure(BgpParserError::ProtocolError(
|
||||
ProtocolErrorValues::UnsupportedVersion(version),
|
||||
)));
|
||||
@@ -237,7 +255,7 @@ pub enum Capability {
|
||||
OutboundRouteFilter(Vec<OrfFamily>),
|
||||
/// Extended Next Hop encoding (RFC 8950).
|
||||
ExtendedNextHop {
|
||||
entries: Vec<ExtendedNextHopEntry>,
|
||||
entries: Vec<ExtendedNextHop>,
|
||||
},
|
||||
/// Extended Message (RFC 8654).
|
||||
ExtendedMessage,
|
||||
@@ -248,7 +266,7 @@ pub enum Capability {
|
||||
},
|
||||
/// Multiple labels (RFC 8277 §2.1): for each address family, how many
|
||||
/// labels the speaker can handle in one NLRI.
|
||||
MultiLabel {
|
||||
MultiLabelCompat {
|
||||
entries: Vec<MultiLabelEntry>,
|
||||
},
|
||||
BgpRole {
|
||||
@@ -262,9 +280,6 @@ pub enum Capability {
|
||||
/// The estimated time in seconds for the session to be
|
||||
/// re-established after a restart.
|
||||
restart_time: u16,
|
||||
/// The speaker applies graceful restart to sessions that end with
|
||||
/// a NOTIFICATION too, other than a Hard Reset (RFC 8538).
|
||||
notification: bool,
|
||||
/// The address families whose routes are kept over a restart.
|
||||
entries: Vec<GracefulRestartEntry>,
|
||||
},
|
||||
@@ -280,10 +295,10 @@ pub enum Capability {
|
||||
/// Enhanced Route Refresh (RFC 7313).
|
||||
EnhancedRouteRefresh,
|
||||
/// Long Lived Graceful Restart (RFC 9494).
|
||||
LongLivedGracefulRestart(Vec<LongLivedGracefulRestartEntry>),
|
||||
LongLivedGracefulRestart(Vec<LongLivedGracefulRestart>),
|
||||
/// A capability this implementation has no definition for, kept as it
|
||||
/// came (RFC 5492 §3: what is not understood is ignored).
|
||||
Unknown {
|
||||
UnknownCapability {
|
||||
type_code: u8,
|
||||
value: Vec<u8>,
|
||||
},
|
||||
@@ -298,6 +313,12 @@ fn afi_reserved_safi(buf: &[u8]) -> Parsed<'_, (Afi, Safi)> {
|
||||
Ok((buf, (Afi(afi), Safi(safi))))
|
||||
}
|
||||
|
||||
fn afi_safi(buf: &[u8]) -> Parsed<'_, (Afi, Safi)> {
|
||||
let (buf, afi) = be_u16(buf)?;
|
||||
let (buf, safi) = be_u8(buf)?;
|
||||
Ok((buf, (Afi(afi), Safi(safi))))
|
||||
}
|
||||
|
||||
impl Capability {
|
||||
fn type_code(&self) -> u8 {
|
||||
use cap_type::*;
|
||||
@@ -308,14 +329,14 @@ impl Capability {
|
||||
Self::ExtendedNextHop { .. } => EXTENDED_NEXT_HOP,
|
||||
Self::ExtendedMessage => EXTENDED_MESSAGE,
|
||||
Self::BgpSec { .. } => BGP_SEC,
|
||||
Self::MultiLabel { .. } => MULTI_LABEL,
|
||||
Self::MultiLabelCompat { .. } => MULTI_LABEL,
|
||||
Self::BgpRole { .. } => BGP_ROLE,
|
||||
Self::GracefulRestart { .. } => GRACEFUL_RESTART,
|
||||
Self::FourByteAsn { .. } => FOUR_BYTE_ASN,
|
||||
Self::AddPath { .. } => ADD_PATH,
|
||||
Self::EnhancedRouteRefresh => ENHANCED_ROUTE_REFRESH,
|
||||
Self::LongLivedGracefulRestart(_) => LONG_LIVED_GRACEFUL_RESTART,
|
||||
Self::Unknown { type_code, .. } => *type_code,
|
||||
Self::UnknownCapability { type_code, .. } => *type_code,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -363,10 +384,10 @@ impl Capability {
|
||||
// else: no SAFI is larger than 255.
|
||||
let safi = u8::try_from(safi)
|
||||
.map(Safi)
|
||||
.map_err(|_| malformed(format!("SAFI {safi} does not fit in one octet")))?;
|
||||
.map_err(|_| fail!("SAFI {} does not fit in one octet", safi))?;
|
||||
Ok((
|
||||
buf,
|
||||
ExtendedNextHopEntry {
|
||||
ExtendedNextHop {
|
||||
afi: Afi(afi),
|
||||
safi,
|
||||
nh_afi: Afi(nh_afi),
|
||||
@@ -374,7 +395,7 @@ impl Capability {
|
||||
))
|
||||
})?;
|
||||
if entries.is_empty() {
|
||||
return Err(malformed("Extended Next Hop capability with no entries"));
|
||||
return Err(fail!("Extended Next Hop capability with no entries"));
|
||||
}
|
||||
(rest, Self::ExtendedNextHop { entries })
|
||||
}
|
||||
@@ -392,14 +413,14 @@ impl Capability {
|
||||
}
|
||||
MULTI_LABEL => {
|
||||
let (rest, entries) = list(value, |buf| {
|
||||
let (buf, AfiSafi { afi, safi }) = parse_afi_safi(buf)?;
|
||||
let (buf, (afi, safi)) = afi_safi(buf)?;
|
||||
let (buf, count) = be_u8(buf)?;
|
||||
Ok((buf, MultiLabelEntry { afi, safi, count }))
|
||||
})?;
|
||||
if entries.is_empty() {
|
||||
return Err(malformed("Multiple Labels capability with no entries"));
|
||||
return Err(fail!("Multiple Labels capability with no entries"));
|
||||
}
|
||||
(rest, Self::MultiLabel { entries })
|
||||
(rest, Self::MultiLabelCompat { entries })
|
||||
}
|
||||
BGP_ROLE => {
|
||||
let (rest, role) = be_u8(value)?;
|
||||
@@ -414,7 +435,7 @@ impl Capability {
|
||||
// Four bits of flags and twelve of restart time.
|
||||
let (rest, flags_restart_time) = be_u16(value)?;
|
||||
let (rest, entries) = list(rest, |buf| {
|
||||
let (buf, AfiSafi { afi, safi }) = parse_afi_safi(buf)?;
|
||||
let (buf, (afi, safi)) = afi_safi(buf)?;
|
||||
let (buf, flags) = be_u8(buf)?;
|
||||
Ok((
|
||||
buf,
|
||||
@@ -429,7 +450,6 @@ impl Capability {
|
||||
rest,
|
||||
Self::GracefulRestart {
|
||||
restart_state: flags_restart_time & (1 << 15) != 0,
|
||||
notification: flags_restart_time & (1 << 14) != 0,
|
||||
restart_time: flags_restart_time & 0xfff,
|
||||
entries,
|
||||
},
|
||||
@@ -452,7 +472,7 @@ impl Capability {
|
||||
Some(AddPathEntry {
|
||||
afi: Afi(u16::from_be_bytes([entry[0], entry[1]])),
|
||||
safi: Safi(entry[2]),
|
||||
send_recv: SendReceive::try_from(entry[3]).ok()?,
|
||||
send_recv: AddPathSendRecv::try_from(entry[3]).ok()?,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
@@ -461,12 +481,12 @@ impl Capability {
|
||||
ENHANCED_ROUTE_REFRESH => (value, Self::EnhancedRouteRefresh),
|
||||
LONG_LIVED_GRACEFUL_RESTART => {
|
||||
let (rest, entries) = list(value, |buf| {
|
||||
let (buf, AfiSafi { afi, safi }) = parse_afi_safi(buf)?;
|
||||
let (buf, (afi, safi)) = afi_safi(buf)?;
|
||||
let (buf, flags) = be_u8(buf)?;
|
||||
let (buf, stale_time) = be_u24(buf)?;
|
||||
Ok((
|
||||
buf,
|
||||
LongLivedGracefulRestartEntry {
|
||||
LongLivedGracefulRestart {
|
||||
afi,
|
||||
safi,
|
||||
flags,
|
||||
@@ -478,13 +498,19 @@ impl Capability {
|
||||
}
|
||||
type_code => (
|
||||
&[][..],
|
||||
Self::Unknown {
|
||||
Self::UnknownCapability {
|
||||
type_code,
|
||||
value: value.to_vec(),
|
||||
},
|
||||
),
|
||||
};
|
||||
whole(format_args!("capability {type_code}"), (rest, capability))
|
||||
if !rest.is_empty() {
|
||||
return Err(fail!(
|
||||
"{} octets left over in capability {type_code}",
|
||||
rest.len()
|
||||
));
|
||||
}
|
||||
Ok(capability)
|
||||
}
|
||||
|
||||
/// Writes the capability, with its type and length, to the end of
|
||||
@@ -495,7 +521,13 @@ impl Capability {
|
||||
let length_at = out.len();
|
||||
out.put_u8(0);
|
||||
self.value_to_wire(out)?;
|
||||
fill_length(out, length_at, 1, u8::MAX.into(), "capability")
|
||||
let length = out.len() - length_at - 1;
|
||||
out[length_at] = u8::try_from(length).map_err(|_| EncodeError::TooLong {
|
||||
what: "capability",
|
||||
len: length,
|
||||
max: u8::MAX.into(),
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn value_to_wire(&self, out: &mut BytesMut) -> Result<(), EncodeError> {
|
||||
@@ -524,9 +556,10 @@ impl Capability {
|
||||
out.put_u8(header.0);
|
||||
out.put_u16(afi.0);
|
||||
}
|
||||
Capability::MultiLabel { entries } => {
|
||||
Capability::MultiLabelCompat { entries } => {
|
||||
for entry in entries {
|
||||
put_afi_safi(AfiSafi::new(entry.afi, entry.safi), out);
|
||||
out.put_u16(entry.afi.0);
|
||||
out.put_u8(entry.safi.0);
|
||||
out.put_u8(entry.count);
|
||||
}
|
||||
}
|
||||
@@ -534,7 +567,6 @@ impl Capability {
|
||||
Capability::GracefulRestart {
|
||||
restart_state,
|
||||
restart_time,
|
||||
notification,
|
||||
entries,
|
||||
} => {
|
||||
if *restart_time > 0xfff {
|
||||
@@ -544,29 +576,30 @@ impl Capability {
|
||||
max: 0xfff,
|
||||
});
|
||||
}
|
||||
out.put_u16(
|
||||
u16::from(*restart_state) << 15 | u16::from(*notification) << 14 | restart_time,
|
||||
);
|
||||
out.put_u16(u16::from(*restart_state) << 15 | restart_time);
|
||||
for entry in entries {
|
||||
put_afi_safi(AfiSafi::new(entry.afi, entry.safi), out);
|
||||
out.put_u16(entry.afi.0);
|
||||
out.put_u8(entry.safi.0);
|
||||
out.put_u8(u8::from(entry.preserve_forwarding) << 7);
|
||||
}
|
||||
}
|
||||
Capability::FourByteAsn { asn } => out.put_u32(*asn),
|
||||
Capability::AddPath { entries } => {
|
||||
for entry in entries {
|
||||
put_afi_safi(entry.family(), out);
|
||||
out.put_u16(entry.afi.0);
|
||||
out.put_u8(entry.safi.0);
|
||||
out.put_u8(entry.send_recv as u8);
|
||||
}
|
||||
}
|
||||
Capability::LongLivedGracefulRestart(entries) => {
|
||||
for entry in entries {
|
||||
put_afi_safi(AfiSafi::new(entry.afi, entry.safi), out);
|
||||
out.put_u16(entry.afi.0);
|
||||
out.put_u8(entry.safi.0);
|
||||
out.put_u8(entry.flags);
|
||||
out.put_slice(&entry.stale_time.to_be_bytes()[1..]);
|
||||
}
|
||||
}
|
||||
Capability::Unknown { value, .. } => out.put_slice(value),
|
||||
Capability::UnknownCapability { value, .. } => out.put_slice(value),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -589,8 +622,8 @@ impl OrfFamily {
|
||||
for _ in 0..count {
|
||||
let (rest, orf_type) = be_u8(buf)?;
|
||||
let (rest, send_recv) = be_u8(rest)?;
|
||||
let send_recv = SendReceive::try_from(send_recv)
|
||||
.map_err(|e| malformed(format!("Failed to parse ORF send/recv: {e}")))?;
|
||||
let send_recv = OrfSendRecv::try_from(send_recv)
|
||||
.map_err(|e| fail!("Failed to parse ORF send/recv: {}", e))?;
|
||||
orfs.push(OutboundRouteFilterEntry {
|
||||
r#type: OrfType(orf_type),
|
||||
send_recv,
|
||||
@@ -622,7 +655,7 @@ impl OrfFamily {
|
||||
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
|
||||
pub struct OutboundRouteFilterEntry {
|
||||
pub r#type: OrfType,
|
||||
pub send_recv: SendReceive,
|
||||
pub send_recv: OrfSendRecv,
|
||||
}
|
||||
|
||||
/// An ORF type. Any number is one: most are unassigned or vendor-specific.
|
||||
@@ -647,6 +680,9 @@ pub enum SendReceive {
|
||||
Both = 3,
|
||||
}
|
||||
|
||||
pub type OrfSendRecv = SendReceive;
|
||||
pub type AddPathSendRecv = SendReceive;
|
||||
|
||||
impl SendReceive {
|
||||
/// Whether the speaker that advertised this sends.
|
||||
pub fn sends(self) -> bool {
|
||||
@@ -680,7 +716,7 @@ impl TryFrom<u8> for SendReceive {
|
||||
pub struct AddPathEntry {
|
||||
pub afi: Afi,
|
||||
pub safi: Safi,
|
||||
pub send_recv: SendReceive,
|
||||
pub send_recv: AddPathSendRecv,
|
||||
}
|
||||
|
||||
impl AddPathEntry {
|
||||
@@ -689,11 +725,11 @@ impl AddPathEntry {
|
||||
}
|
||||
}
|
||||
|
||||
/// `ExtendedNextHopEntry` allows for the nexthop to be for a different address family
|
||||
/// `ExtendedNextHop` allows for the nexthop to be for a different address family
|
||||
/// than the one being used to advertise prefixes for. E.g. it allows for sending
|
||||
/// IPv4 prefixes with an IPv6 nexthop.
|
||||
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
|
||||
pub struct ExtendedNextHopEntry {
|
||||
pub struct ExtendedNextHop {
|
||||
/// The Address Family to update the nexthop format for.
|
||||
pub afi: Afi,
|
||||
/// The Subsequent Address Family to update the nexthop format for.
|
||||
@@ -740,7 +776,7 @@ impl From<&BgpRole> for u8 {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
|
||||
pub struct LongLivedGracefulRestartEntry {
|
||||
pub struct LongLivedGracefulRestart {
|
||||
pub afi: Afi,
|
||||
pub safi: Safi,
|
||||
pub flags: u8,
|
||||
@@ -777,8 +813,9 @@ mod tests {
|
||||
use eyre::Result;
|
||||
|
||||
use crate::open::{
|
||||
AddPathEntry, Capability, ExtendedNextHopEntry, GracefulRestartEntry, MultiLabelEntry,
|
||||
OpenMessage, OpenOption, OrfFamily, OrfType, OutboundRouteFilterEntry, SendReceive,
|
||||
AddPathEntry, AddPathSendRecv, Capability, ExtendedNextHop, GracefulRestartEntry,
|
||||
MultiLabelEntry, OpenMessage, OpenOption, OrfFamily, OrfSendRecv, OrfType,
|
||||
OutboundRouteFilterEntry,
|
||||
};
|
||||
|
||||
use bgp_primitives::constants::{Afi, Safi};
|
||||
@@ -799,7 +836,7 @@ mod tests {
|
||||
}
|
||||
|
||||
test_capability_roundtrip!(
|
||||
test_multiprotocol,
|
||||
test_multiptotocol,
|
||||
&[0x01, 0x04, 0x00, 0x01, 0x00, 0x01],
|
||||
Capability::MultiProtocol {
|
||||
afi: Afi::IPV4,
|
||||
@@ -812,7 +849,7 @@ mod tests {
|
||||
test_capability_roundtrip!(
|
||||
test_route_refresh_cisco_unsupported,
|
||||
&[0x80, 0x00],
|
||||
Capability::Unknown {
|
||||
Capability::UnknownCapability {
|
||||
type_code: 0x80,
|
||||
value: vec![]
|
||||
}
|
||||
@@ -830,7 +867,6 @@ mod tests {
|
||||
Capability::GracefulRestart {
|
||||
restart_state: false,
|
||||
restart_time: 300,
|
||||
notification: false,
|
||||
entries: vec![GracefulRestartEntry {
|
||||
afi: Afi::IPV4,
|
||||
safi: Safi::UNICAST,
|
||||
@@ -839,23 +875,6 @@ mod tests {
|
||||
}
|
||||
);
|
||||
|
||||
// Restarted (R), with Notification support (N, RFC 8538), and the
|
||||
// forwarding of IPv6 routes kept (F).
|
||||
test_capability_roundtrip!(
|
||||
test_graceful_restart_with_flags,
|
||||
&[0x40, 0x06, 0xc0, 0x78, 0x00, 0x02, 0x01, 0x80],
|
||||
Capability::GracefulRestart {
|
||||
restart_state: true,
|
||||
restart_time: 120,
|
||||
notification: true,
|
||||
entries: vec![GracefulRestartEntry {
|
||||
afi: Afi::IPV6,
|
||||
safi: Safi::UNICAST,
|
||||
preserve_forwarding: true,
|
||||
}]
|
||||
}
|
||||
);
|
||||
|
||||
test_capability_roundtrip!(
|
||||
test_extended_nexthop,
|
||||
&[
|
||||
@@ -864,17 +883,17 @@ mod tests {
|
||||
],
|
||||
Capability::ExtendedNextHop {
|
||||
entries: vec![
|
||||
ExtendedNextHopEntry {
|
||||
ExtendedNextHop {
|
||||
afi: Afi::IPV4,
|
||||
safi: Safi::UNICAST,
|
||||
nh_afi: Afi::IPV6,
|
||||
},
|
||||
ExtendedNextHopEntry {
|
||||
ExtendedNextHop {
|
||||
afi: Afi::IPV4,
|
||||
safi: Safi::MULTICAST,
|
||||
nh_afi: Afi::IPV6,
|
||||
},
|
||||
ExtendedNextHopEntry {
|
||||
ExtendedNextHop {
|
||||
afi: Afi::IPV4,
|
||||
safi: Safi::MPLS_VPN,
|
||||
nh_afi: Afi::IPV6
|
||||
@@ -902,7 +921,7 @@ mod tests {
|
||||
afi: Afi::IPV4,
|
||||
safi: Safi::UNICAST,
|
||||
}]),
|
||||
OpenOption::Capabilities(vec![Capability::Unknown {
|
||||
OpenOption::Capabilities(vec![Capability::UnknownCapability {
|
||||
type_code: 0x80,
|
||||
value: vec![],
|
||||
}]),
|
||||
@@ -911,7 +930,6 @@ mod tests {
|
||||
OpenOption::Capabilities(vec![Capability::GracefulRestart {
|
||||
restart_state: false,
|
||||
restart_time: 300,
|
||||
notification: false,
|
||||
entries: vec![GracefulRestartEntry {
|
||||
afi: Afi::IPV4,
|
||||
safi: Safi::UNICAST,
|
||||
@@ -920,17 +938,17 @@ mod tests {
|
||||
}]),
|
||||
OpenOption::Capabilities(vec![Capability::ExtendedNextHop {
|
||||
entries: vec![
|
||||
ExtendedNextHopEntry {
|
||||
ExtendedNextHop {
|
||||
afi: Afi::IPV4,
|
||||
safi: Safi::UNICAST,
|
||||
nh_afi: Afi::IPV6,
|
||||
},
|
||||
ExtendedNextHopEntry {
|
||||
ExtendedNextHop {
|
||||
afi: Afi::IPV4,
|
||||
safi: Safi::MULTICAST,
|
||||
nh_afi: Afi::IPV6,
|
||||
},
|
||||
ExtendedNextHopEntry {
|
||||
ExtendedNextHop {
|
||||
afi: Afi::IPV4,
|
||||
safi: Safi::MPLS_VPN,
|
||||
nh_afi: Afi::IPV6,
|
||||
@@ -958,7 +976,7 @@ mod tests {
|
||||
assert!(rest.is_empty());
|
||||
assert_eq!(
|
||||
cap,
|
||||
Capability::MultiLabel {
|
||||
Capability::MultiLabelCompat {
|
||||
entries: vec![
|
||||
MultiLabelEntry {
|
||||
afi: Afi::IPV4,
|
||||
@@ -1001,7 +1019,7 @@ mod tests {
|
||||
entries: vec![AddPathEntry {
|
||||
afi: evpn.0,
|
||||
safi: evpn.1,
|
||||
send_recv: SendReceive::Both,
|
||||
send_recv: AddPathSendRecv::Both,
|
||||
}],
|
||||
},
|
||||
),
|
||||
@@ -1010,7 +1028,6 @@ mod tests {
|
||||
Capability::GracefulRestart {
|
||||
restart_state: false,
|
||||
restart_time: 120,
|
||||
notification: false,
|
||||
entries: vec![GracefulRestartEntry {
|
||||
afi: evpn.0,
|
||||
safi: evpn.1,
|
||||
@@ -1050,12 +1067,12 @@ mod tests {
|
||||
AddPathEntry {
|
||||
afi: Afi::IPV4,
|
||||
safi: Safi::UNICAST,
|
||||
send_recv: SendReceive::Receive,
|
||||
send_recv: AddPathSendRecv::Receive,
|
||||
},
|
||||
AddPathEntry {
|
||||
afi: Afi::IPV6,
|
||||
safi: Safi::UNICAST,
|
||||
send_recv: SendReceive::Both,
|
||||
send_recv: AddPathSendRecv::Both,
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -1115,8 +1132,8 @@ mod tests {
|
||||
assert_eq!(
|
||||
cap,
|
||||
Capability::OutboundRouteFilter(vec![
|
||||
family(Afi::IPV4, SendReceive::Both),
|
||||
family(Afi::IPV6, SendReceive::Receive),
|
||||
family(Afi::IPV4, OrfSendRecv::Both),
|
||||
family(Afi::IPV6, OrfSendRecv::Receive),
|
||||
])
|
||||
);
|
||||
let mut out = BytesMut::new();
|
||||
@@ -1132,7 +1149,7 @@ mod tests {
|
||||
#[test]
|
||||
fn long_optional_parameters_use_two_octet_lengths() {
|
||||
let capabilities: Vec<Capability> = (0..60)
|
||||
.map(|i| Capability::Unknown {
|
||||
.map(|i| Capability::UnknownCapability {
|
||||
type_code: 200,
|
||||
value: vec![i; 4],
|
||||
})
|
||||
@@ -1190,7 +1207,7 @@ mod tests {
|
||||
/// used to be written with the length wrapped around.
|
||||
#[test]
|
||||
fn a_capability_too_long_for_its_length_octet_is_refused() {
|
||||
let entry = ExtendedNextHopEntry {
|
||||
let entry = ExtendedNextHop {
|
||||
afi: Afi::IPV4,
|
||||
safi: Safi::UNICAST,
|
||||
nh_afi: Afi::IPV6,
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
//! be acted on, with treat-as-withdraw and attribute discard already carried
|
||||
//! out, together with a report of what was wrong, for the log.
|
||||
|
||||
use bgp_primitives::constants::IpVersion;
|
||||
use bgp_primitives::constants::{AfiSafi, IpVersion};
|
||||
use bgp_primitives::ip_prefix::Nlri;
|
||||
use bgp_primitives::parser::ParserContext;
|
||||
|
||||
@@ -29,7 +29,6 @@ use crate::message::{
|
||||
MpUnreachNlriPathAttribute, NotAnAttribute, PathAttribute, RawAttribute, UpdateMessage,
|
||||
defined_flags, describe, parse_nlri_list, split_attribute,
|
||||
};
|
||||
use crate::wire::body_has_path_id;
|
||||
|
||||
/// How an error in an UPDATE was handled, short of resetting the session.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -63,9 +62,7 @@ pub struct UpdateError {
|
||||
pub struct DecodedUpdate {
|
||||
/// The UPDATE after treat-as-withdraw and attribute discard.
|
||||
pub update: UpdateMessage,
|
||||
/// What was wrong, in the order met; empty when the UPDATE was well
|
||||
/// formed. Where a message of any type was decoded this is
|
||||
/// [`Decoded::update_errors`](crate::message::Decoded::update_errors).
|
||||
/// Empty when the UPDATE was well formed.
|
||||
pub errors: Vec<UpdateError>,
|
||||
/// The attributes in `update`, as they were on the wire, for a table
|
||||
/// that keeps attributes in that form and need not write them out
|
||||
@@ -105,7 +102,9 @@ fn reset(subcode: UpdateMsgSubcode, data: Vec<u8>, detail: String) -> DecodeErro
|
||||
/// error here cannot be recovered from: the prefixes are what
|
||||
/// treat-as-withdraw would act on.
|
||||
fn prefixes(ctx: &ParserContext, field: &str, buf: &[u8]) -> Result<Vec<Nlri>, DecodeError> {
|
||||
match parse_nlri_list(IpVersion::Ipv4, body_has_path_id(ctx), buf) {
|
||||
// The prefixes in the body are IPv4 unicast (RFC 4271 §4.3).
|
||||
let with_path_id = ctx.has_add_path(AfiSafi::IPV4_UNICAST);
|
||||
match parse_nlri_list(IpVersion::Ipv4, with_path_id, buf) {
|
||||
Ok((_, prefixes)) => Ok(prefixes),
|
||||
Err(e) => Err(reset(
|
||||
UpdateMsgSubcode::InvalidNetworkField,
|
||||
@@ -186,36 +185,6 @@ impl UpdateMessage {
|
||||
}
|
||||
}
|
||||
|
||||
/// Makes one path of the two a session with two-octet AS numbers brings:
|
||||
/// AS_PATH becomes the path as it really is, and AS4_PATH is gone, used or
|
||||
/// not (RFC 6793 §4.2.3).
|
||||
///
|
||||
/// For whoever receives an UPDATE to do once, before anything looks at
|
||||
/// the path: the checks on it, the choice between routes, validation
|
||||
/// against the RPKI now and again later. Were each to read the two for
|
||||
/// itself, a peer could have them disagree about where a route comes
|
||||
/// from.
|
||||
pub fn merge_as4_path(attributes: &mut Vec<PathAttribute>) {
|
||||
let Some(at) = attributes
|
||||
.iter()
|
||||
.position(|attribute| matches!(attribute, PathAttribute::As4Path(_)))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let wide = match attributes.remove(at) {
|
||||
PathAttribute::As4Path(wide) => wide,
|
||||
other => unreachable!("{other:?} is not the AS4_PATH found at {at}"),
|
||||
};
|
||||
for attribute in attributes {
|
||||
if let PathAttribute::AsPath(path) = attribute {
|
||||
if let Some(merged) = path.with_as4_path(&wide) {
|
||||
*path = merged;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// What to do with an attribute, once it is clear.
|
||||
enum Verdict {
|
||||
/// Use it. `route_property` is false for one that was read only to find
|
||||
@@ -235,10 +204,7 @@ struct AttributeReader<'a> {
|
||||
attributes: Vec<PathAttribute>,
|
||||
errors: Vec<UpdateError>,
|
||||
/// The bytes of the attributes that are kept and are a property of a
|
||||
/// route: all but MP_REACH_NLRI and MP_UNREACH_NLRI. Left empty on a
|
||||
/// session with two-octet AS numbers, whose bytes no table keeps (see
|
||||
/// [`DecodedUpdate::attributes_wire`]), so that nothing is copied or
|
||||
/// reserved for them.
|
||||
/// route: all but MP_REACH_NLRI and MP_UNREACH_NLRI.
|
||||
kept_wire: Vec<u8>,
|
||||
/// The type codes met so far, one bit each.
|
||||
seen: [u64; 4],
|
||||
@@ -250,7 +216,7 @@ impl<'a> AttributeReader<'a> {
|
||||
ctx,
|
||||
attributes: Vec::new(),
|
||||
errors: Vec::new(),
|
||||
kept_wire: Vec::with_capacity(if ctx.four_octet_asn { wire_len } else { 0 }),
|
||||
kept_wire: Vec::with_capacity(wire_len),
|
||||
seen: [0; 4],
|
||||
}
|
||||
}
|
||||
@@ -295,7 +261,7 @@ impl<'a> AttributeReader<'a> {
|
||||
route_property,
|
||||
} = self.judge(&raw)?
|
||||
{
|
||||
if route_property && self.ctx.four_octet_asn {
|
||||
if route_property {
|
||||
self.kept_wire.extend_from_slice(raw.wire);
|
||||
}
|
||||
self.attributes.push(attribute);
|
||||
@@ -411,6 +377,8 @@ impl<'a> AttributeReader<'a> {
|
||||
// Known by their definition but not modelled: kept as bytes, once
|
||||
// their length has been checked.
|
||||
let length_is_valid = match raw.type_code {
|
||||
ORIGINATOR_ID => Some(value.len() == 4),
|
||||
CLUSTER_LIST => Some(!value.is_empty() && value.len().is_multiple_of(4)),
|
||||
AS4_AGGREGATOR => Some(value.len() == 8),
|
||||
_ => None,
|
||||
};
|
||||
@@ -425,7 +393,7 @@ impl<'a> AttributeReader<'a> {
|
||||
|
||||
/// The attribute kept as bytes.
|
||||
fn opaque(raw: &RawAttribute<'_>) -> PathAttribute {
|
||||
PathAttribute::Unknown {
|
||||
PathAttribute::UnknownPathAttribute {
|
||||
flags: raw.flags.clone(),
|
||||
type_code: raw.type_code,
|
||||
payload: raw.value.to_vec(),
|
||||
@@ -498,7 +466,6 @@ mod tests {
|
||||
use std::net::Ipv4Addr;
|
||||
|
||||
use crate::message::PathAttributeFlags;
|
||||
use bgp_primitives::constants::AfiSafi;
|
||||
use bgp_primitives::ip_prefix::IpPrefix;
|
||||
|
||||
use super::*;
|
||||
@@ -507,6 +474,11 @@ mod tests {
|
||||
OriginPathAttribute,
|
||||
};
|
||||
|
||||
const MED: u8 = 4;
|
||||
const COMMUNITIES: u8 = 8;
|
||||
const EXTENDED_COMMUNITIES: u8 = 16;
|
||||
const LARGE_COMMUNITY: u8 = 32;
|
||||
|
||||
const WELL_KNOWN: u8 = 0x40;
|
||||
const OPTIONAL: u8 = 0x80;
|
||||
const OPTIONAL_TRANSITIVE: u8 = 0xc0;
|
||||
@@ -602,7 +574,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn a_well_formed_update_is_left_alone() {
|
||||
let decoded = announce_with(&ebgp(), attr(OPTIONAL, MULTI_EXIT_DISC, &[0, 0, 0, 50]));
|
||||
let decoded = announce_with(&ebgp(), attr(OPTIONAL, MED, &[0, 0, 0, 50]));
|
||||
assert!(decoded.errors.is_empty());
|
||||
assert_eq!(decoded.update.announced_routes, [route()]);
|
||||
assert_eq!(
|
||||
@@ -632,7 +604,7 @@ mod tests {
|
||||
|
||||
// Nothing when the routes were withdrawn instead: there is no route
|
||||
// for the attributes to belong to.
|
||||
let decoded = announce_with(&ebgp(), attr(OPTIONAL, MULTI_EXIT_DISC, &[1]));
|
||||
let decoded = announce_with(&ebgp(), attr(OPTIONAL, MED, &[1]));
|
||||
assert_eq!(decoded.attributes_wire, None);
|
||||
let decoded = UpdateMessage::decode(&ebgp(), &body(&NLRI, &[], &[])).unwrap();
|
||||
assert_eq!(decoded.attributes_wire, None);
|
||||
@@ -654,31 +626,6 @@ mod tests {
|
||||
assert_eq!(decoded.attributes_wire, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_to_customer_is_read_and_kept_as_it_came() {
|
||||
let wire = crate::message::only_to_customer_wire(3333);
|
||||
assert_eq!(
|
||||
wire.to_vec(),
|
||||
attr(OPTIONAL_TRANSITIVE, ONLY_TO_CUSTOMER, &[0, 0, 0x0d, 0x05])
|
||||
);
|
||||
let decoded = announce_with(&ebgp(), wire.to_vec());
|
||||
assert!(decoded.errors.is_empty());
|
||||
assert_eq!(
|
||||
decoded.update.path_attributes.last(),
|
||||
Some(&PathAttribute::OnlyToCustomer(3333))
|
||||
);
|
||||
assert_eq!(
|
||||
decoded.attributes_wire,
|
||||
Some([mandatory().concat(), wire.to_vec()].concat())
|
||||
);
|
||||
// And is written as it is read.
|
||||
let mut out = bytes::BytesMut::new();
|
||||
PathAttribute::OnlyToCustomer(3333)
|
||||
.to_wire(&ebgp(), &mut out)
|
||||
.unwrap();
|
||||
assert_eq!(out[..], wire);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_update_that_announces_nothing_needs_no_attributes() {
|
||||
// End-of-RIB, and a plain withdrawal.
|
||||
@@ -691,11 +638,7 @@ mod tests {
|
||||
#[test]
|
||||
fn a_malformed_attribute_withdraws_the_routes_and_keeps_the_session() {
|
||||
let cases: Vec<(&str, u8, Vec<u8>)> = vec![
|
||||
(
|
||||
"MED of three octets",
|
||||
MULTI_EXIT_DISC,
|
||||
attr(OPTIONAL, MULTI_EXIT_DISC, &[0, 0, 50]),
|
||||
),
|
||||
("MED of three octets", MED, attr(OPTIONAL, MED, &[0, 0, 50])),
|
||||
(
|
||||
"COMMUNITIES with nothing in it",
|
||||
COMMUNITIES,
|
||||
@@ -713,24 +656,13 @@ mod tests {
|
||||
),
|
||||
(
|
||||
"LARGE_COMMUNITY of eleven octets",
|
||||
LARGE_COMMUNITIES,
|
||||
attr(OPTIONAL_TRANSITIVE, LARGE_COMMUNITIES, &[0; 11]),
|
||||
LARGE_COMMUNITY,
|
||||
attr(OPTIONAL_TRANSITIVE, LARGE_COMMUNITY, &[0; 11]),
|
||||
),
|
||||
(
|
||||
"MED marked transitive",
|
||||
MULTI_EXIT_DISC,
|
||||
attr(OPTIONAL_TRANSITIVE, MULTI_EXIT_DISC, &[0, 0, 0, 50]),
|
||||
),
|
||||
// RFC 9234 §5.
|
||||
(
|
||||
"Only to Customer of two octets",
|
||||
ONLY_TO_CUSTOMER,
|
||||
attr(OPTIONAL_TRANSITIVE, ONLY_TO_CUSTOMER, &[0xfd, 0xe8]),
|
||||
),
|
||||
(
|
||||
"Only to Customer of eight octets",
|
||||
ONLY_TO_CUSTOMER,
|
||||
attr(OPTIONAL_TRANSITIVE, ONLY_TO_CUSTOMER, &[0; 8]),
|
||||
MED,
|
||||
attr(OPTIONAL_TRANSITIVE, MED, &[0, 0, 0, 50]),
|
||||
),
|
||||
];
|
||||
for (case, type_code, extra) in cases {
|
||||
@@ -807,13 +739,13 @@ mod tests {
|
||||
fn an_attribute_that_overruns_the_attributes_withdraws_what_was_found() {
|
||||
// A MED that claims 200 octets.
|
||||
let mut attributes = mandatory();
|
||||
attributes.push(vec![OPTIONAL, MULTI_EXIT_DISC, 200, 0, 0]);
|
||||
attributes.push(vec![OPTIONAL, MED, 200, 0, 0]);
|
||||
let decoded = UpdateMessage::decode(&ebgp(), &body(&[], &attributes, &NLRI)).unwrap();
|
||||
assert_withdrawn(&decoded, Some(MULTI_EXIT_DISC), "overrun");
|
||||
assert_withdrawn(&decoded, Some(MED), "overrun");
|
||||
|
||||
// Two stray octets after the last attribute: not even a header.
|
||||
let mut attributes = mandatory();
|
||||
attributes.push(vec![OPTIONAL, MULTI_EXIT_DISC]);
|
||||
attributes.push(vec![OPTIONAL, MED]);
|
||||
let decoded = UpdateMessage::decode(&ebgp(), &body(&[], &attributes, &NLRI)).unwrap();
|
||||
assert_withdrawn(&decoded, None, "stray octets");
|
||||
}
|
||||
@@ -864,13 +796,10 @@ mod tests {
|
||||
#[test]
|
||||
fn a_repeated_attribute_keeps_its_first_value() {
|
||||
let mut attributes = mandatory();
|
||||
attributes.push(attr(OPTIONAL, MULTI_EXIT_DISC, &[0, 0, 0, 50]));
|
||||
attributes.push(attr(OPTIONAL, MULTI_EXIT_DISC, &[0, 0, 0, 99]));
|
||||
attributes.push(attr(OPTIONAL, MED, &[0, 0, 0, 50]));
|
||||
attributes.push(attr(OPTIONAL, MED, &[0, 0, 0, 99]));
|
||||
let decoded = UpdateMessage::decode(&ebgp(), &body(&[], &attributes, &NLRI)).unwrap();
|
||||
assert_eq!(
|
||||
handled(&decoded),
|
||||
[(Handling::AttributeDiscard, Some(MULTI_EXIT_DISC))]
|
||||
);
|
||||
assert_eq!(handled(&decoded), [(Handling::AttributeDiscard, Some(MED))]);
|
||||
assert!(
|
||||
decoded
|
||||
.update
|
||||
@@ -904,21 +833,6 @@ mod tests {
|
||||
let reflected = UpdateMessage::decode(&ibgp(), &body(&[], &attributes, &NLRI)).unwrap();
|
||||
assert!(reflected.errors.is_empty());
|
||||
assert_eq!(reflected.update.path_attributes.len(), 6);
|
||||
assert!(
|
||||
reflected
|
||||
.update
|
||||
.path_attributes
|
||||
.contains(&PathAttribute::OriginatorId(Ipv4Addr::new(10, 0, 0, 1)))
|
||||
);
|
||||
assert!(
|
||||
reflected
|
||||
.update
|
||||
.path_attributes
|
||||
.contains(&PathAttribute::ClusterList(vec![
|
||||
Ipv4Addr::new(10, 0, 0, 2),
|
||||
Ipv4Addr::new(10, 0, 0, 3)
|
||||
]))
|
||||
);
|
||||
|
||||
let mut attributes = mandatory();
|
||||
attributes.push(local_pref());
|
||||
@@ -957,18 +871,6 @@ mod tests {
|
||||
.iter()
|
||||
.any(|a| matches!(a, PathAttribute::As4Path(_)))
|
||||
);
|
||||
// The two made one: AS 4200000001 where AS_PATH had AS_TRANS, and
|
||||
// no AS4_PATH.
|
||||
let mut one_path = decoded.update.path_attributes;
|
||||
merge_as4_path(&mut one_path);
|
||||
assert_eq!(
|
||||
one_path,
|
||||
[
|
||||
PathAttribute::Origin(OriginPathAttribute::IGP),
|
||||
AsPathAttribute::from_asns(vec![4_200_000_001]),
|
||||
PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::new(192, 0, 2, 1))),
|
||||
]
|
||||
);
|
||||
|
||||
let bad = attr(OPTIONAL_TRANSITIVE, AS4_PATH, &[2, 1, 0xfa, 0x56]);
|
||||
let decoded = UpdateMessage::decode(&ctx, &body(&[], &attributes(bad), &NLRI)).unwrap();
|
||||
@@ -979,119 +881,6 @@ mod tests {
|
||||
assert_eq!(decoded.update.announced_routes, [route()]);
|
||||
}
|
||||
|
||||
/// AS4_AGGREGATOR (RFC 6793 §4.2.2) is kept as it came, once its
|
||||
/// length is known to be the four octets of an AS number and the four
|
||||
/// of an address. One of another length is dropped and the route kept
|
||||
/// (RFC 6793 §6), like a malformed AGGREGATOR.
|
||||
#[test]
|
||||
fn as4_aggregator_is_kept_opaque_and_dropped_if_malformed() {
|
||||
let ctx = ParserContext {
|
||||
four_octet_asn: false,
|
||||
ibgp: false,
|
||||
add_path: Vec::new(),
|
||||
};
|
||||
let attributes = |as4_aggregator: Vec<u8>| {
|
||||
vec![
|
||||
origin(),
|
||||
attr(WELL_KNOWN, AS_PATH, &[2, 1, 0x5b, 0xa0]),
|
||||
next_hop(),
|
||||
as4_aggregator,
|
||||
]
|
||||
};
|
||||
// AS 4200000001, 192.0.2.9.
|
||||
let value = [0xfa, 0x56, 0xea, 0x01, 192, 0, 2, 9];
|
||||
let good = attr(OPTIONAL_TRANSITIVE, AS4_AGGREGATOR, &value);
|
||||
let decoded = UpdateMessage::decode(&ctx, &body(&[], &attributes(good), &NLRI)).unwrap();
|
||||
assert!(decoded.errors.is_empty());
|
||||
assert_eq!(
|
||||
decoded.update.path_attributes.last(),
|
||||
Some(&PathAttribute::Unknown {
|
||||
flags: PathAttributeFlags(OPTIONAL_TRANSITIVE),
|
||||
type_code: AS4_AGGREGATOR,
|
||||
payload: value.to_vec(),
|
||||
})
|
||||
);
|
||||
|
||||
for (case, bad) in [
|
||||
("two-octet AS number", &value[2..]),
|
||||
("no address", &value[..4]),
|
||||
("nothing", &[][..]),
|
||||
] {
|
||||
let bad = attr(OPTIONAL_TRANSITIVE, AS4_AGGREGATOR, bad);
|
||||
let decoded = UpdateMessage::decode(&ctx, &body(&[], &attributes(bad), &NLRI)).unwrap();
|
||||
assert_eq!(
|
||||
handled(&decoded),
|
||||
[(Handling::AttributeDiscard, Some(AS4_AGGREGATOR))],
|
||||
"{case}"
|
||||
);
|
||||
assert_eq!(decoded.update.announced_routes, [route()], "{case}");
|
||||
assert_eq!(decoded.update.path_attributes.len(), 3, "{case}");
|
||||
}
|
||||
}
|
||||
|
||||
/// RFC 6793 §4.2.3. AS4_PATH is the tail of the path; the front is
|
||||
/// kept from AS_PATH. One longer than the path is not used, and is
|
||||
/// gone all the same: nothing after the decoder is to see two paths,
|
||||
/// or a peer could have them disagree about a route's origin.
|
||||
#[test]
|
||||
fn as4_path_is_merged_into_the_path() {
|
||||
use crate::message::AsPathSegment;
|
||||
let sequence = |path: &[u32]| AsPathSegment {
|
||||
ordered: true,
|
||||
path: path.to_vec(),
|
||||
};
|
||||
let set = |path: &[u32]| AsPathSegment {
|
||||
ordered: false,
|
||||
path: path.to_vec(),
|
||||
};
|
||||
let merged = |narrow: Vec<AsPathSegment>, wide: Vec<AsPathSegment>| {
|
||||
let mut attributes = vec![
|
||||
PathAttribute::Origin(OriginPathAttribute::IGP),
|
||||
PathAttribute::As4Path(AsPathAttribute { segments: wide }),
|
||||
PathAttribute::AsPath(AsPathAttribute { segments: narrow }),
|
||||
];
|
||||
merge_as4_path(&mut attributes);
|
||||
assert_eq!(attributes.len(), 2, "AS4_PATH is gone");
|
||||
match attributes.pop() {
|
||||
Some(PathAttribute::AsPath(path)) => path.segments,
|
||||
other => panic!("{other:?}"),
|
||||
}
|
||||
};
|
||||
assert_eq!(
|
||||
merged(
|
||||
vec![sequence(&[3333, 23456, 23456])],
|
||||
vec![sequence(&[210_036, 4_200_000_001])]
|
||||
),
|
||||
[sequence(&[3333]), sequence(&[210_036, 4_200_000_001])]
|
||||
);
|
||||
// As long as the path: all of it.
|
||||
assert_eq!(
|
||||
merged(vec![sequence(&[23456])], vec![sequence(&[210_036])]),
|
||||
[sequence(&[210_036])]
|
||||
);
|
||||
// Longer than the path: not used. The hijack this would allow is
|
||||
// a path [3333] that says it ends at someone else's AS.
|
||||
assert_eq!(
|
||||
merged(vec![sequence(&[3333])], vec![sequence(&[64500, 1299])]),
|
||||
[sequence(&[3333])]
|
||||
);
|
||||
// A set counts as one, whole.
|
||||
assert_eq!(
|
||||
merged(
|
||||
vec![sequence(&[3333]), set(&[1, 2]), sequence(&[23456])],
|
||||
vec![sequence(&[210_036])]
|
||||
),
|
||||
[sequence(&[3333]), set(&[1, 2]), sequence(&[210_036])]
|
||||
);
|
||||
assert_eq!(
|
||||
merged(
|
||||
vec![sequence(&[3333, 23456])],
|
||||
vec![set(&[210_036, 210_037])]
|
||||
),
|
||||
[sequence(&[3333]), set(&[210_036, 210_037])]
|
||||
);
|
||||
}
|
||||
|
||||
/// 2001:db8:1::/48 via 2001:db8::1.
|
||||
fn mp_reach_v6() -> Vec<u8> {
|
||||
let mut value = vec![0, 2, 1, 16];
|
||||
@@ -1209,7 +998,7 @@ mod tests {
|
||||
assert!(decoded.errors.is_empty());
|
||||
assert_eq!(
|
||||
decoded.update.path_attributes[3],
|
||||
PathAttribute::Unknown {
|
||||
PathAttribute::UnknownPathAttribute {
|
||||
flags: PathAttributeFlags(0xe0),
|
||||
type_code: 99,
|
||||
payload: vec![1, 2, 3],
|
||||
|
||||
@@ -1,198 +0,0 @@
|
||||
//! What the readers and writers of the wire format share: the shape of a
|
||||
//! parser's result and its errors, the fields that several messages and
|
||||
//! attributes have in common, and the lengths that are written before what
|
||||
//! they count.
|
||||
|
||||
use bytes::{BufMut, BytesMut};
|
||||
use nom::Err::Failure;
|
||||
use nom::IResult;
|
||||
use nom::number::complete::{be_u8, be_u16, be_u32};
|
||||
|
||||
use bgp_primitives::constants::{Afi, AfiSafi, Safi};
|
||||
use bgp_primitives::parser::{BgpParserError, EncodeError, ParserContext};
|
||||
|
||||
use crate::message::AS_TRANS;
|
||||
|
||||
/// A parser's result: what is left of the input, and what was read.
|
||||
pub(crate) type Parsed<'a, T> = IResult<&'a [u8], T, BgpParserError<&'a [u8]>>;
|
||||
|
||||
/// The error for input that does not follow the format; `detail` says how.
|
||||
pub(crate) fn malformed<'a>(detail: impl Into<String>) -> nom::Err<BgpParserError<&'a [u8]>> {
|
||||
Failure(BgpParserError::Malformed(detail.into()))
|
||||
}
|
||||
|
||||
/// Parses `buf` to its end as a list of `item`s. Unlike nom's `many0`,
|
||||
/// which stops quietly at an item it cannot read, an error in an item is an
|
||||
/// error of the list.
|
||||
pub(crate) fn list<'a, T>(
|
||||
mut buf: &'a [u8],
|
||||
item: impl Fn(&'a [u8]) -> Parsed<'a, T>,
|
||||
) -> Parsed<'a, Vec<T>> {
|
||||
let mut items = Vec::new();
|
||||
while !buf.is_empty() {
|
||||
let (rest, parsed) = item(buf)?;
|
||||
items.push(parsed);
|
||||
buf = rest;
|
||||
}
|
||||
Ok((buf, items))
|
||||
}
|
||||
|
||||
/// A parsed value, provided the parser used all of the input. `what` names
|
||||
/// the input in the error.
|
||||
pub(crate) fn whole<T>(
|
||||
what: impl std::fmt::Display,
|
||||
(rest, parsed): (&[u8], T),
|
||||
) -> Result<T, nom::Err<BgpParserError<&[u8]>>> {
|
||||
if rest.is_empty() {
|
||||
Ok(parsed)
|
||||
} else {
|
||||
Err(malformed(format!(
|
||||
"{} octets left over in {what}",
|
||||
rest.len()
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
/// `value` as a list of one or more records of `N` octets each: how the
|
||||
/// community attributes and CLUSTER_LIST are laid out. An empty list is
|
||||
/// malformed (RFC 7606 §7.8 for COMMUNITIES, and the others alike). `what`
|
||||
/// names the attribute in the error.
|
||||
pub(crate) fn records<'a, const N: usize>(
|
||||
value: &'a [u8],
|
||||
what: &str,
|
||||
) -> Result<&'a [[u8; N]], nom::Err<BgpParserError<&'a [u8]>>> {
|
||||
let (records, rest) = value.as_chunks::<N>();
|
||||
if records.is_empty() || !rest.is_empty() {
|
||||
return Err(malformed(format!(
|
||||
"{what} is {} octets long, not a non-empty multiple of {N}",
|
||||
value.len()
|
||||
)));
|
||||
}
|
||||
Ok(records)
|
||||
}
|
||||
|
||||
/// Parses an AS number as wide as the session's: four octets or two
|
||||
/// (RFC 6793).
|
||||
pub(crate) fn parse_asn(four_octet: bool, buf: &[u8]) -> Parsed<'_, u32> {
|
||||
if four_octet {
|
||||
be_u32(buf)
|
||||
} else {
|
||||
be_u16(buf).map(|(buf, asn)| (buf, u32::from(asn)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Writes an AS number as wide as the session's. On a two-octet session
|
||||
/// one that does not fit is written as AS_TRANS (RFC 6793 §4.2.2).
|
||||
pub(crate) fn put_asn(four_octet: bool, asn: u32, out: &mut BytesMut) {
|
||||
if four_octet {
|
||||
out.put_u32(asn);
|
||||
} else {
|
||||
out.put_u16(u16::try_from(asn).unwrap_or(AS_TRANS));
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the prefixes in the body of an UPDATE carry path identifiers
|
||||
/// on a session with `ctx`. The body holds IPv4 unicast routes and nothing
|
||||
/// else (RFC 4271 §4.3), so that is the family whose ADD-PATH agreement
|
||||
/// decides it; every other family's prefixes are in MP_REACH_NLRI and
|
||||
/// MP_UNREACH_NLRI, which name their family.
|
||||
pub(crate) fn body_has_path_id(ctx: &ParserContext) -> bool {
|
||||
ctx.has_add_path(AfiSafi::IPV4_UNICAST)
|
||||
}
|
||||
|
||||
/// Parses an address family as the multiprotocol attributes and most
|
||||
/// capabilities name it: a two-octet AFI and a one-octet SAFI.
|
||||
pub(crate) fn parse_afi_safi(buf: &[u8]) -> Parsed<'_, AfiSafi> {
|
||||
let (buf, afi) = be_u16(buf)?;
|
||||
let (buf, safi) = be_u8(buf)?;
|
||||
Ok((buf, AfiSafi::new(Afi(afi), Safi(safi))))
|
||||
}
|
||||
|
||||
/// Writes an address family as [`parse_afi_safi`] reads it.
|
||||
pub(crate) fn put_afi_safi(family: AfiSafi, out: &mut BytesMut) {
|
||||
out.put_u16(family.afi.0);
|
||||
out.put_u8(family.safi.0);
|
||||
}
|
||||
|
||||
/// Writes `length` into the field of `width` octets reserved at `at`, if
|
||||
/// it is no more than `max`. `what` names the field in the error.
|
||||
pub(crate) fn put_length(
|
||||
out: &mut BytesMut,
|
||||
at: usize,
|
||||
width: usize,
|
||||
length: usize,
|
||||
max: usize,
|
||||
what: &'static str,
|
||||
) -> Result<(), EncodeError> {
|
||||
if length > max {
|
||||
return Err(EncodeError::TooLong {
|
||||
what,
|
||||
len: length,
|
||||
max,
|
||||
});
|
||||
}
|
||||
let bytes = length.to_be_bytes();
|
||||
out[at..at + width].copy_from_slice(&bytes[bytes.len() - width..]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Fills in the field of `width` octets reserved at `at` with how much
|
||||
/// has been written after it, if that is no more than `max`: for a length
|
||||
/// that is known only once what it counts has been written. `what` names
|
||||
/// the field in the error.
|
||||
pub(crate) fn fill_length(
|
||||
out: &mut BytesMut,
|
||||
at: usize,
|
||||
width: usize,
|
||||
max: usize,
|
||||
what: &'static str,
|
||||
) -> Result<(), EncodeError> {
|
||||
let length = out.len() - at - width;
|
||||
put_length(out, at, width, length, max, what)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn a_length_is_filled_in_after_what_it_counts() {
|
||||
let mut out = BytesMut::new();
|
||||
out.put_u8(0xaa);
|
||||
let at = out.len();
|
||||
out.put_u16(0);
|
||||
out.put_slice(&[1, 2, 3]);
|
||||
fill_length(&mut out, at, 2, u16::MAX.into(), "field").unwrap();
|
||||
assert_eq!(&out[..], [0xaa, 0, 3, 1, 2, 3]);
|
||||
|
||||
let mut out = BytesMut::new();
|
||||
out.put_u8(0);
|
||||
out.put_slice(&[0; 300]);
|
||||
assert_eq!(
|
||||
fill_length(&mut out, 0, 1, u8::MAX.into(), "field"),
|
||||
Err(EncodeError::TooLong {
|
||||
what: "field",
|
||||
len: 300,
|
||||
max: 255,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn records_are_a_whole_number_of_them_and_at_least_one() {
|
||||
assert_eq!(
|
||||
records::<4>(&[1, 2, 3, 4, 5, 6, 7, 8], "X").unwrap().len(),
|
||||
2
|
||||
);
|
||||
for value in [&[][..], &[1, 2, 3][..], &[1, 2, 3, 4, 5][..]] {
|
||||
let error = records::<4>(value, "X").unwrap_err();
|
||||
assert_eq!(
|
||||
crate::message::describe(&error),
|
||||
format!(
|
||||
"X is {} octets long, not a non-empty multiple of 4",
|
||||
value.len()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,9 +25,9 @@ use bgp_packet::message::{
|
||||
PathAttributeFlags, RouteRefreshMessage, UpdateMessage,
|
||||
};
|
||||
use bgp_packet::open::{
|
||||
AddPathEntry, BgpRole, BgpSecHeader, Capability, ExtendedNextHopEntry, GracefulRestartEntry,
|
||||
LongLivedGracefulRestartEntry, MultiLabelEntry, OpenMessage, OpenOption, OrfFamily, OrfType,
|
||||
OutboundRouteFilterEntry, SendReceive,
|
||||
AddPathEntry, AddPathSendRecv, BgpRole, BgpSecHeader, Capability, ExtendedNextHop,
|
||||
GracefulRestartEntry, LongLivedGracefulRestart, MultiLabelEntry, OpenMessage, OpenOption,
|
||||
OrfFamily, OrfSendRecv, OrfType, OutboundRouteFilterEntry,
|
||||
};
|
||||
use bgp_primitives::constants::{Afi, AfiSafi, IpVersion, Safi};
|
||||
use bgp_primitives::ip_prefix::{IpPrefix, Nlri};
|
||||
@@ -137,6 +137,19 @@ fn extended_community() -> impl Strategy<Value = ExtendedCommunity> {
|
||||
|
||||
fn mp_reach(session: Session) -> impl Strategy<Value = MpReachNlriPathAttribute> {
|
||||
let ip_safi = || prop_oneof![Just(Safi::UNICAST), Just(Safi::MULTICAST)];
|
||||
let v4 = ip_safi()
|
||||
.prop_flat_map(move |safi| {
|
||||
(
|
||||
Just(safi),
|
||||
any::<[u8; 4]>(),
|
||||
nlri(session, IpVersion::Ipv4, safi),
|
||||
)
|
||||
})
|
||||
.prop_map(|(safi, next_hop, prefixes)| MpReachNlriPathAttribute {
|
||||
family: AfiSafi::new(Afi::IPV4, safi),
|
||||
next_hop: NlriNextHop::Ipv4(Ipv4Addr::from(next_hop)),
|
||||
prefixes,
|
||||
});
|
||||
let v6_next_hop = || {
|
||||
prop_oneof![
|
||||
any::<[u8; 16]>().prop_map(|a| NlriNextHop::Ipv6(Ipv6Addr::from(a))),
|
||||
@@ -148,27 +161,6 @@ fn mp_reach(session: Session) -> impl Strategy<Value = MpReachNlriPathAttribute>
|
||||
}),
|
||||
]
|
||||
};
|
||||
// IPv4 routes have an IPv4 next hop, or an IPv6 one over a link that
|
||||
// has only IPv6 addresses (RFC 8950).
|
||||
let v4_next_hop = move || {
|
||||
prop_oneof![
|
||||
3 => any::<[u8; 4]>().prop_map(|a| NlriNextHop::Ipv4(Ipv4Addr::from(a))),
|
||||
1 => v6_next_hop(),
|
||||
]
|
||||
};
|
||||
let v4 = ip_safi()
|
||||
.prop_flat_map(move |safi| {
|
||||
(
|
||||
Just(safi),
|
||||
v4_next_hop(),
|
||||
nlri(session, IpVersion::Ipv4, safi),
|
||||
)
|
||||
})
|
||||
.prop_map(|(safi, next_hop, prefixes)| MpReachNlriPathAttribute {
|
||||
family: AfiSafi::new(Afi::IPV4, safi),
|
||||
next_hop,
|
||||
prefixes,
|
||||
});
|
||||
let v6 = ip_safi()
|
||||
.prop_flat_map(move |safi| {
|
||||
(
|
||||
@@ -197,7 +189,7 @@ fn mp_unreach(session: Session) -> impl Strategy<Value = MpUnreachNlriPathAttrib
|
||||
|
||||
/// Type codes whose definition the decoder knows; an unknown attribute has
|
||||
/// none of these.
|
||||
const DEFINED_ATTRIBUTES: [u8; 17] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 14, 15, 16, 17, 18, 32, 35];
|
||||
const DEFINED_ATTRIBUTES: [u8; 16] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 14, 15, 16, 17, 18, 32];
|
||||
|
||||
/// Unknown attributes, each with a type code of its own: a repeated
|
||||
/// attribute is dropped by the decoder.
|
||||
@@ -220,7 +212,7 @@ fn unknown_attributes() -> impl Strategy<Value = Vec<PathAttribute>> {
|
||||
if payload.len() > 255 {
|
||||
flags |= 0x10;
|
||||
}
|
||||
PathAttribute::Unknown {
|
||||
PathAttribute::UnknownPathAttribute {
|
||||
flags: PathAttributeFlags(flags),
|
||||
type_code,
|
||||
payload,
|
||||
@@ -299,40 +291,11 @@ fn path_attributes(session: Session) -> impl Strategy<Value = Vec<PathAttribute>
|
||||
.collect(),
|
||||
})
|
||||
})),
|
||||
proptest::option::of(any::<u32>().prop_map(PathAttribute::OnlyToCustomer)),
|
||||
// Route reflection's, which only an internal peer sends.
|
||||
proptest::option::of((any::<[u8; 4]>(), vec(any::<[u8; 4]>(), 1..4))).prop_map(
|
||||
move |reflected| {
|
||||
reflected
|
||||
.filter(|_| session.ibgp)
|
||||
.map(|(originator, clusters)| {
|
||||
vec![
|
||||
PathAttribute::OriginatorId(Ipv4Addr::from(originator)),
|
||||
PathAttribute::ClusterList(
|
||||
clusters.into_iter().map(Ipv4Addr::from).collect(),
|
||||
),
|
||||
]
|
||||
})
|
||||
.unwrap_or_default()
|
||||
},
|
||||
),
|
||||
unknown_attributes(),
|
||||
);
|
||||
(mandatory, optional).prop_map(|(mut attributes, optional)| {
|
||||
let (
|
||||
med,
|
||||
atomic,
|
||||
aggregator,
|
||||
communities,
|
||||
reach,
|
||||
unreach,
|
||||
extended,
|
||||
as4,
|
||||
large,
|
||||
otc,
|
||||
reflected,
|
||||
unknown,
|
||||
) = optional;
|
||||
let (med, atomic, aggregator, communities, reach, unreach, extended, as4, large, unknown) =
|
||||
optional;
|
||||
attributes.extend(
|
||||
[
|
||||
med,
|
||||
@@ -344,12 +307,10 @@ fn path_attributes(session: Session) -> impl Strategy<Value = Vec<PathAttribute>
|
||||
extended,
|
||||
as4,
|
||||
large,
|
||||
otc,
|
||||
]
|
||||
.into_iter()
|
||||
.flatten(),
|
||||
);
|
||||
attributes.extend(reflected);
|
||||
attributes.extend(unknown);
|
||||
attributes
|
||||
})
|
||||
@@ -377,16 +338,16 @@ fn capability() -> impl Strategy<Value = Capability> {
|
||||
let orf = (
|
||||
any::<u8>().prop_map(OrfType),
|
||||
prop_oneof![
|
||||
Just(SendReceive::Receive),
|
||||
Just(SendReceive::Send),
|
||||
Just(SendReceive::Both)
|
||||
Just(OrfSendRecv::Receive),
|
||||
Just(OrfSendRecv::Send),
|
||||
Just(OrfSendRecv::Both)
|
||||
],
|
||||
)
|
||||
.prop_map(|(r#type, send_recv)| OutboundRouteFilterEntry { r#type, send_recv });
|
||||
let add_path = prop_oneof![
|
||||
Just(SendReceive::Receive),
|
||||
Just(SendReceive::Send),
|
||||
Just(SendReceive::Both)
|
||||
Just(AddPathSendRecv::Receive),
|
||||
Just(AddPathSendRecv::Send),
|
||||
Just(AddPathSendRecv::Both)
|
||||
];
|
||||
prop_oneof![
|
||||
(afi(), safi()).prop_map(|(afi, safi)| Capability::MultiProtocol { afi, safi }),
|
||||
@@ -402,7 +363,7 @@ fn capability() -> impl Strategy<Value = Capability> {
|
||||
vec((afi(), safi(), afi()), 1..4).prop_map(|entries| Capability::ExtendedNextHop {
|
||||
entries: entries
|
||||
.into_iter()
|
||||
.map(|(afi, safi, nh_afi)| ExtendedNextHopEntry { afi, safi, nh_afi })
|
||||
.map(|(afi, safi, nh_afi)| ExtendedNextHop { afi, safi, nh_afi })
|
||||
.collect(),
|
||||
}),
|
||||
Just(Capability::ExtendedMessage),
|
||||
@@ -411,7 +372,7 @@ fn capability() -> impl Strategy<Value = Capability> {
|
||||
afi,
|
||||
}),
|
||||
vec((afi(), safi(), any::<u8>()), 1..4).prop_map(|entries| {
|
||||
Capability::MultiLabel {
|
||||
Capability::MultiLabelCompat {
|
||||
entries: entries
|
||||
.into_iter()
|
||||
.map(|(afi, safi, count)| MultiLabelEntry { afi, safi, count })
|
||||
@@ -422,15 +383,13 @@ fn capability() -> impl Strategy<Value = Capability> {
|
||||
role: BgpRole::from(role),
|
||||
}),
|
||||
(
|
||||
any::<bool>(),
|
||||
any::<bool>(),
|
||||
0u16..4096,
|
||||
vec((afi(), safi(), any::<bool>()), 0..4)
|
||||
)
|
||||
.prop_map(|(restart_state, notification, restart_time, entries)| {
|
||||
.prop_map(|(restart_state, restart_time, entries)| {
|
||||
Capability::GracefulRestart {
|
||||
restart_state,
|
||||
notification,
|
||||
restart_time,
|
||||
entries: entries
|
||||
.into_iter()
|
||||
@@ -458,14 +417,12 @@ fn capability() -> impl Strategy<Value = Capability> {
|
||||
Capability::LongLivedGracefulRestart(
|
||||
entries
|
||||
.into_iter()
|
||||
.map(
|
||||
|(afi, safi, flags, stale_time)| LongLivedGracefulRestartEntry {
|
||||
afi,
|
||||
safi,
|
||||
flags,
|
||||
stale_time,
|
||||
},
|
||||
)
|
||||
.map(|(afi, safi, flags, stale_time)| LongLivedGracefulRestart {
|
||||
afi,
|
||||
safi,
|
||||
flags,
|
||||
stale_time,
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}),
|
||||
@@ -475,7 +432,7 @@ fn capability() -> impl Strategy<Value = Capability> {
|
||||
}),
|
||||
vec(any::<u8>(), 0..20)
|
||||
)
|
||||
.prop_map(|(type_code, value)| Capability::Unknown { type_code, value }),
|
||||
.prop_map(|(type_code, value)| Capability::UnknownCapability { type_code, value }),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -702,9 +659,7 @@ proptest! {
|
||||
check_decoding(&ctx, &damaged(&wire, &damage))?;
|
||||
}
|
||||
|
||||
/// The parsers below the message level are public too. What they
|
||||
/// accept must write back as it was read, where the format leaves
|
||||
/// nothing for the reader to normalise.
|
||||
/// The parsers below the message level are public too.
|
||||
#[test]
|
||||
fn noise_is_never_more_than_an_error_for_the_parts(
|
||||
session in session(),
|
||||
@@ -712,52 +667,8 @@ proptest! {
|
||||
) {
|
||||
let ctx = session.ctx();
|
||||
let _ = OpenMessage::from_wire(&bytes);
|
||||
let _ = Capability::from_wire(&bytes);
|
||||
let _ = UpdateMessage::decode(&ctx, &bytes);
|
||||
|
||||
if let Ok((rest, capability)) = Capability::from_wire(&bytes) {
|
||||
let consumed = &bytes[..bytes.len() - rest.len()];
|
||||
let mut rewritten = BytesMut::new();
|
||||
capability.to_wire(&mut rewritten)
|
||||
.map_err(|e| TestCaseError::fail(format!("accepted but cannot be written: {e}")))?;
|
||||
// ADD-PATH families with a Send/Receive value nobody defined
|
||||
// are left out (RFC 7911 §4), and the reserved octet of
|
||||
// Multiprotocol and ORF and the reserved bits of Graceful
|
||||
// Restart are read as zero whatever they were. Everything else
|
||||
// is written as it was read.
|
||||
let normalised = matches!(
|
||||
capability,
|
||||
Capability::AddPath { .. }
|
||||
| Capability::MultiProtocol { .. }
|
||||
| Capability::OutboundRouteFilter(_)
|
||||
| Capability::GracefulRestart { .. }
|
||||
);
|
||||
if !normalised {
|
||||
prop_assert_eq!(&rewritten[..], consumed, "{:?}", capability);
|
||||
}
|
||||
let again = Capability::from_wire(&rewritten)
|
||||
.map_err(|e| TestCaseError::fail(format!("written but cannot be read: {e:?}")))?;
|
||||
prop_assert_eq!(again, (&[][..], capability));
|
||||
}
|
||||
|
||||
if let Ok((rest, attribute)) = PathAttribute::from_wire(&ctx, &bytes) {
|
||||
let consumed = &bytes[..bytes.len() - rest.len()];
|
||||
let mut rewritten = BytesMut::new();
|
||||
attribute.to_wire(&ctx, &mut rewritten)
|
||||
.map_err(|e| TestCaseError::fail(format!("accepted but cannot be written: {e}")))?;
|
||||
// The flags are not checked against the attribute's definition
|
||||
// here, and are written as defined; and the bits of a prefix
|
||||
// past its length are cleared. With the flags as defined and
|
||||
// no prefixes, the attribute is written as it was read.
|
||||
let names_prefixes = matches!(
|
||||
attribute,
|
||||
PathAttribute::MpReachNlri(_) | PathAttribute::MpUnreachNlri(_)
|
||||
);
|
||||
if rewritten[0] == consumed[0] && !names_prefixes {
|
||||
prop_assert_eq!(&rewritten[..], consumed, "{:?}", attribute);
|
||||
}
|
||||
let again = PathAttribute::from_wire(&ctx, &rewritten)
|
||||
.map_err(|e| TestCaseError::fail(format!("written but cannot be read: {e:?}")))?;
|
||||
prop_assert_eq!(again, (&[][..], attribute));
|
||||
}
|
||||
let _ = PathAttribute::from_wire(&ctx, &bytes);
|
||||
}
|
||||
}
|
||||
@@ -9,11 +9,13 @@ repository.workspace = true
|
||||
version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
bitfield.workspace = true
|
||||
bytes.workspace = true
|
||||
nom.workspace = true
|
||||
serde.workspace = true
|
||||
serde_repr.workspace = true
|
||||
thiserror.workspace = true
|
||||
strum.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
proptest.workspace = true
|
||||
|
||||
@@ -11,7 +11,7 @@ pub struct UnknownAfi(pub u16);
|
||||
|
||||
/// The IP version of a prefix. For the address family field of a capability
|
||||
/// or an MP_REACH_NLRI, which can name families that are not IP, see [`Afi`].
|
||||
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize_repr)]
|
||||
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, Serialize_repr)]
|
||||
#[repr(u16)]
|
||||
pub enum IpVersion {
|
||||
Ipv4 = 1,
|
||||
|
||||
@@ -31,7 +31,7 @@ pub enum PrefixError {
|
||||
/// value. A prefix has one representation: the bits past its length are
|
||||
/// always zero, so two prefixes are equal exactly when they are the same
|
||||
/// prefix.
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash, Serialize)]
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, Serialize)]
|
||||
pub struct IpPrefix {
|
||||
address_family: IpVersion,
|
||||
length: u8,
|
||||
@@ -64,7 +64,7 @@ impl IpPrefix {
|
||||
let mut address = [0u8; 16];
|
||||
address[..needed].copy_from_slice(&prefix[..needed]);
|
||||
if let (Some(last), rem @ 1..) = (address[..needed].last_mut(), length % 8) {
|
||||
*last &= high_bits_mask(rem);
|
||||
*last &= u8::MAX << (8 - rem);
|
||||
}
|
||||
Ok(Self {
|
||||
address_family,
|
||||
@@ -88,25 +88,6 @@ impl IpPrefix {
|
||||
&self.address[..usize::from(self.length.div_ceil(8))]
|
||||
}
|
||||
|
||||
/// The prefix of `length` bits that covers this one: itself, if it is
|
||||
/// no longer than that.
|
||||
pub fn shortened(&self, length: u8) -> Self {
|
||||
if length >= self.length {
|
||||
return *self;
|
||||
}
|
||||
let mut address = [0u8; 16];
|
||||
let whole = usize::from(length / 8);
|
||||
address[..whole].copy_from_slice(&self.address[..whole]);
|
||||
if !length.is_multiple_of(8) {
|
||||
address[whole] = self.address[whole] & high_bits_mask(length % 8);
|
||||
}
|
||||
Self {
|
||||
address_family: self.address_family,
|
||||
length,
|
||||
address,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if `other` is equal to or more specific than `self`, i.e.
|
||||
/// `self` covers `other`. Prefixes of different address families never
|
||||
/// contain each other.
|
||||
@@ -122,18 +103,11 @@ impl IpPrefix {
|
||||
if rem_bits == 0 {
|
||||
return true;
|
||||
}
|
||||
let mask = high_bits_mask(rem_bits);
|
||||
let mask = u8::MAX << (8 - rem_bits);
|
||||
(self.address[full_bytes] & mask) == (other.address[full_bytes] & mask)
|
||||
}
|
||||
}
|
||||
|
||||
/// The mask that keeps the first `bits` bits of an octet and clears the
|
||||
/// rest, for `bits` from 1 to 7: what the last octet of a prefix whose
|
||||
/// length is not a whole number of octets is masked with.
|
||||
fn high_bits_mask(bits: u8) -> u8 {
|
||||
u8::MAX << (8 - bits)
|
||||
}
|
||||
|
||||
/// A prefix as an UPDATE names it: the prefix and, on a session where
|
||||
/// ADD-PATH is in use for the address family (RFC 7911), the identifier
|
||||
/// that tells apart the several routes a peer may announce for it.
|
||||
@@ -151,6 +125,11 @@ impl Nlri {
|
||||
pub fn plain(prefixes: impl IntoIterator<Item = IpPrefix>) -> Vec<Self> {
|
||||
prefixes.into_iter().map(Self::from).collect()
|
||||
}
|
||||
|
||||
/// The bytes this takes on the wire, with or without its identifier.
|
||||
pub fn wire_len(&self, with_path_id: bool) -> usize {
|
||||
1 + self.prefix.bytes().len() + if with_path_id { 4 } else { 0 }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<IpPrefix> for Nlri {
|
||||
@@ -365,21 +344,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// A shortened prefix is the same as one made from the same
|
||||
/// address with the shorter length, and covers the original.
|
||||
#[test]
|
||||
fn shortening_keeps_the_leading_bits(
|
||||
version: bool, bytes: [u8; 16], length in 0u8..=128, shorter in 0u8..=128,
|
||||
) {
|
||||
let version = if version { IpVersion::Ipv6 } else { IpVersion::Ipv4 };
|
||||
let length = length.min(version.bits());
|
||||
let prefix = IpPrefix::new(version, bytes, length).unwrap();
|
||||
let shortened = prefix.shortened(shorter);
|
||||
let expected = IpPrefix::new(version, bytes, shorter.min(length)).unwrap();
|
||||
prop_assert_eq!(shortened, expected);
|
||||
prop_assert!(shortened.contains(&prefix));
|
||||
}
|
||||
|
||||
/// `contains` agrees with comparing the leading bits by hand.
|
||||
#[test]
|
||||
fn contains_is_a_comparison_of_leading_bits(
|
||||
|
||||
@@ -16,6 +16,7 @@ hashbrown.workspace = true
|
||||
serde.workspace = true
|
||||
thiserror.workspace = true
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
tracing.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
proptest.workspace = true
|
||||
|
||||
+26
-357
@@ -13,7 +13,7 @@
|
||||
use std::hash::BuildHasher;
|
||||
use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
|
||||
|
||||
use bgp_packet::message::{NlriNextHop, PathAttribute, RawAttribute, attr_type, split_attribute};
|
||||
use bgp_packet::message::{NlriNextHop, PathAttribute, attr_type, split_attribute};
|
||||
use bgp_primitives::parser::{EncodeError, ParserContext};
|
||||
use bytes::{BufMut, BytesMut};
|
||||
use foldhash::fast::RandomState;
|
||||
@@ -21,10 +21,6 @@ use hashbrown::HashTable;
|
||||
|
||||
const OPTIONAL: u8 = 0x80;
|
||||
|
||||
/// The type of an AS_PATH segment whose AS numbers are in order; the other
|
||||
/// type, AS_SET, is an unordered handful (RFC 4271 §4.3).
|
||||
const AS_SEQUENCE: u8 = 2;
|
||||
|
||||
/// What route selection reads from a set of attributes (RFC 4271 §9.1.2.2).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct SelectionKey {
|
||||
@@ -41,26 +37,12 @@ pub struct SelectionKey {
|
||||
pub as_path_len: u16,
|
||||
/// ORIGIN: 0 for IGP, 1 for EGP, 2 for INCOMPLETE.
|
||||
pub origin: u8,
|
||||
/// The AS the route originates from: the last AS of the path. Zero,
|
||||
/// which is no AS's number, if the path is empty or ends in a set and
|
||||
/// so has no one origin. Not part of selection; kept here because it
|
||||
/// is what a route's validity is asked about (RFC 6811), for every
|
||||
/// route, when the validation data changes.
|
||||
pub origin_as: u32,
|
||||
}
|
||||
|
||||
/// Reads the selection key straight off attributes in the form an
|
||||
/// [`AttrSet`] keeps, without building anything. `None` if the bytes are
|
||||
/// not a well-formed list of attributes.
|
||||
///
|
||||
/// A list with the same attribute twice is not well formed (RFC 4271
|
||||
/// §5). The decoder keeps only the first of such, so none comes from a
|
||||
/// session; were one to get in some other way, the readers of a set, of
|
||||
/// which this is one, could each take a different one of the two for the
|
||||
/// route's path or next hop.
|
||||
fn scan(mut wire: &[u8]) -> Option<SelectionKey> {
|
||||
// One bit for each type of attribute there can be.
|
||||
let mut seen = [0u64; 4];
|
||||
let mut key = SelectionKey {
|
||||
local_pref: None,
|
||||
med: None,
|
||||
@@ -69,29 +51,15 @@ fn scan(mut wire: &[u8]) -> Option<SelectionKey> {
|
||||
// A route without ORIGIN does not get this far; if one did, it
|
||||
// would be the least preferred.
|
||||
origin: 2,
|
||||
origin_as: 0,
|
||||
};
|
||||
let number = |value: &[u8]| Some(u32::from_be_bytes(<[u8; 4]>::try_from(value).ok()?));
|
||||
while !wire.is_empty() {
|
||||
let (attribute, rest) = split_attribute(wire).ok()?;
|
||||
wire = rest;
|
||||
let value = attribute.value;
|
||||
let (word, bit) = (
|
||||
usize::from(attribute.type_code / 64),
|
||||
1u64 << (attribute.type_code % 64),
|
||||
);
|
||||
if seen[word] & bit != 0 {
|
||||
return None;
|
||||
}
|
||||
seen[word] |= bit;
|
||||
match attribute.type_code {
|
||||
attr_type::ORIGIN => key.origin = *value.first()?,
|
||||
attr_type::AS_PATH => {
|
||||
let path = scan_as_path(value)?;
|
||||
key.as_path_len = path.hops;
|
||||
key.neighbor_as = path.first;
|
||||
key.origin_as = path.last;
|
||||
}
|
||||
attr_type::AS_PATH => (key.as_path_len, key.neighbor_as) = scan_as_path(value)?,
|
||||
attr_type::MULTI_EXIT_DISC => key.med = Some(number(value)?),
|
||||
attr_type::LOCAL_PREF => key.local_pref = Some(number(value)?),
|
||||
_ => {}
|
||||
@@ -100,70 +68,29 @@ fn scan(mut wire: &[u8]) -> Option<SelectionKey> {
|
||||
Some(key)
|
||||
}
|
||||
|
||||
/// What the selection key takes from an AS path.
|
||||
struct PathScan {
|
||||
/// The length as selection counts it.
|
||||
hops: u16,
|
||||
/// The first AS, if the path begins with a sequence; else zero.
|
||||
first: u32,
|
||||
/// The last AS, if the path ends in a sequence; else zero.
|
||||
last: u32,
|
||||
}
|
||||
|
||||
impl SelectionKey {
|
||||
/// The AS the route originates from: the last of its path. `None` if
|
||||
/// the path is empty or ends in a set, which has no one last AS.
|
||||
pub fn origin(&self) -> Option<u32> {
|
||||
Some(self.origin_as).filter(|origin| *origin != 0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads an AS_PATH value with four-octet AS numbers.
|
||||
fn scan_as_path(mut segments: &[u8]) -> Option<PathScan> {
|
||||
/// The length of an AS_PATH as selection counts it, and the neighbouring
|
||||
/// AS, from the attribute's value with four-octet AS numbers.
|
||||
fn scan_as_path(mut segments: &[u8]) -> Option<(u16, u32)> {
|
||||
const AS_SEQUENCE: u8 = 2;
|
||||
let mut hops = 0usize;
|
||||
let (mut first, mut last) = (0, 0);
|
||||
let mut at_start = true;
|
||||
let mut neighbor_as = 0;
|
||||
let mut first = true;
|
||||
while let [kind, count, rest @ ..] = segments {
|
||||
let (asns, rest) = rest.split_at_checked(usize::from(*count) * 4)?;
|
||||
let ordered = *kind == AS_SEQUENCE;
|
||||
hops += if ordered { usize::from(*count) } else { 1 };
|
||||
let asn = |bytes: Option<&[u8; 4]>| bytes.map_or(0, |asn| u32::from_be_bytes(*asn));
|
||||
if at_start && ordered {
|
||||
first = asn(asns.first_chunk::<4>());
|
||||
if first && ordered {
|
||||
neighbor_as = asns
|
||||
.first_chunk::<4>()
|
||||
.map_or(0, |asn| u32::from_be_bytes(*asn));
|
||||
}
|
||||
last = if ordered {
|
||||
asn(asns.last_chunk::<4>())
|
||||
} else {
|
||||
0
|
||||
};
|
||||
at_start = false;
|
||||
first = false;
|
||||
segments = rest;
|
||||
}
|
||||
if !segments.is_empty() {
|
||||
return None;
|
||||
}
|
||||
Some(PathScan {
|
||||
hops: u16::try_from(hops).unwrap_or(u16::MAX),
|
||||
first,
|
||||
last,
|
||||
})
|
||||
}
|
||||
|
||||
/// Appends the AS numbers of an AS_PATH or AS4_PATH value with four-octet
|
||||
/// AS numbers to `out`, and returns whether any segment is not a sequence.
|
||||
/// Stops at what is not a segment.
|
||||
fn read_as_path(mut segments: &[u8], out: &mut Vec<u32>) -> bool {
|
||||
let mut has_set = false;
|
||||
while let [kind, count, rest @ ..] = segments {
|
||||
let Some((asns, rest)) = rest.split_at_checked(usize::from(*count) * 4) else {
|
||||
break;
|
||||
};
|
||||
has_set |= *kind != AS_SEQUENCE;
|
||||
let (asns, _) = asns.as_chunks::<4>();
|
||||
out.extend(asns.iter().map(|asn| u32::from_be_bytes(*asn)));
|
||||
segments = rest;
|
||||
}
|
||||
has_set
|
||||
Some((u16::try_from(hops).unwrap_or(u16::MAX), neighbor_as))
|
||||
}
|
||||
|
||||
/// One distinct list of path attributes, with the next hop that goes with
|
||||
@@ -189,77 +116,6 @@ impl AttrSet {
|
||||
&self.wire
|
||||
}
|
||||
|
||||
/// The attributes one by one, as they lie in the stored bytes. The
|
||||
/// bytes were checked when the set was made, so this is all of them;
|
||||
/// it stops at the first that does not split as an attribute all the
|
||||
/// same, rather than go on from the middle of one.
|
||||
fn raw_attributes(&self) -> impl Iterator<Item = RawAttribute<'_>> {
|
||||
let mut wire = &*self.wire;
|
||||
std::iter::from_fn(move || {
|
||||
let (attribute, rest) = split_attribute(wire).ok()?;
|
||||
wire = rest;
|
||||
Some(attribute)
|
||||
})
|
||||
}
|
||||
|
||||
/// The value of the attribute of type `type_code`, if the set has one.
|
||||
fn attribute(&self, type_code: u8) -> Option<&[u8]> {
|
||||
self.raw_attributes()
|
||||
.find(|attribute| attribute.type_code == type_code)
|
||||
.map(|attribute| attribute.value)
|
||||
}
|
||||
|
||||
/// The route's next hop: the one that came in MP_REACH_NLRI, or else
|
||||
/// the NEXT_HOP attribute. Read off the stored bytes without building
|
||||
/// anything else, for those who ask it of every route.
|
||||
pub fn next_hop(&self) -> Option<NlriNextHop> {
|
||||
let mut plain = None;
|
||||
for attribute in self.raw_attributes() {
|
||||
match attribute.type_code {
|
||||
attr_type::MP_REACH_NLRI => return decode_next_hop(attribute.value),
|
||||
attr_type::NEXT_HOP => plain = NlriNextHop::from_bytes(attribute.value),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
plain
|
||||
}
|
||||
|
||||
/// Whether the route carries the OTC attribute (RFC 9234): it came
|
||||
/// down from a provider or across from a peer, and is to go on down
|
||||
/// only. Read off the stored bytes without building anything else,
|
||||
/// for those who ask it of every route: a session to a peer or a
|
||||
/// provider asks it of most of the table, to pass over them.
|
||||
pub fn is_only_to_customer(&self) -> bool {
|
||||
self.attribute(attr_type::ONLY_TO_CUSTOMER).is_some()
|
||||
}
|
||||
|
||||
/// Whether the route carries the BLACKHOLE community (RFC 7999), which
|
||||
/// asks for the packets to its prefix to be dropped. Read off the
|
||||
/// stored bytes without building anything else, for those who ask it
|
||||
/// of every route.
|
||||
pub fn is_blackhole(&self) -> bool {
|
||||
const BLACKHOLE: [u8; 4] = [0xFF, 0xFF, 0x02, 0x9A];
|
||||
self.attribute(attr_type::COMMUNITIES)
|
||||
.is_some_and(|communities| communities.as_chunks::<4>().0.contains(&BLACKHOLE))
|
||||
}
|
||||
|
||||
/// Puts the AS numbers of the route's path in `out`, in place of what
|
||||
/// was there: in the order of the path, the neighbor's first and the
|
||||
/// origin's last, sets and all. Returns whether the path has a set in
|
||||
/// it.
|
||||
///
|
||||
/// This is the whole path: what a session with two-octet AS numbers
|
||||
/// brought in AS4_PATH was put in its place when the UPDATE was read
|
||||
/// (RFC 6793 §4.2.3). It reads the stored bytes and builds nothing
|
||||
/// else, for those who ask it of every route.
|
||||
pub fn as_path(&self, out: &mut Vec<u32>) -> bool {
|
||||
out.clear();
|
||||
match self.attribute(attr_type::AS_PATH) {
|
||||
Some(segments) => read_as_path(segments, out),
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// The attributes, parsed, and the next hop if it came in
|
||||
/// MP_REACH_NLRI.
|
||||
///
|
||||
@@ -304,8 +160,7 @@ fn encode(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Writes a next hop as an MP_REACH_NLRI that holds nothing else: the
|
||||
/// next hop's length, and the next hop.
|
||||
/// Writes a next hop as an MP_REACH_NLRI that holds nothing else.
|
||||
fn encode_next_hop(next_hop: &NlriNextHop, out: &mut BytesMut) {
|
||||
out.put_u8(OPTIONAL);
|
||||
out.put_u8(attr_type::MP_REACH_NLRI);
|
||||
@@ -314,13 +169,6 @@ fn encode_next_hop(next_hop: &NlriNextHop, out: &mut BytesMut) {
|
||||
next_hop.to_wire(out);
|
||||
}
|
||||
|
||||
/// Reads the next hop out of the value of an MP_REACH_NLRI as
|
||||
/// [`encode_next_hop`] writes it. `None` if the value is not one.
|
||||
fn decode_next_hop(value: &[u8]) -> Option<NlriNextHop> {
|
||||
let (length, address) = value.split_first()?;
|
||||
NlriNextHop::from_bytes(address).filter(|_| usize::from(*length) == address.len())
|
||||
}
|
||||
|
||||
/// Parses path attributes in the form [`AttrSet`] keeps and table dumps
|
||||
/// use (RFC 6396 §4.3.4): four-octet AS numbers, and an MP_REACH_NLRI that
|
||||
/// holds only a next hop. `None` if they are malformed.
|
||||
@@ -331,7 +179,11 @@ fn parse(mut wire: &[u8]) -> Option<(Vec<PathAttribute>, Option<NlriNextHop>)> {
|
||||
while !wire.is_empty() {
|
||||
let (raw, rest) = split_attribute(wire).ok()?;
|
||||
if raw.type_code == attr_type::MP_REACH_NLRI {
|
||||
next_hop = Some(decode_next_hop(raw.value)?);
|
||||
let (length, address) = raw.value.split_first()?;
|
||||
if usize::from(*length) != address.len() {
|
||||
return None;
|
||||
}
|
||||
next_hop = Some(NlriNextHop::from_bytes(address)?);
|
||||
} else {
|
||||
let (_, attribute) = PathAttribute::from_wire(&ctx, raw.wire).ok()?;
|
||||
attributes.push(attribute);
|
||||
@@ -415,28 +267,13 @@ impl AttrStore {
|
||||
wire: &[u8],
|
||||
next_hop: Option<&NlriNextHop>,
|
||||
) -> Option<Arc<AttrSet>> {
|
||||
self.intern_received_with(wire, &[], next_hop)
|
||||
}
|
||||
|
||||
/// As [`AttrStore::intern_received`], with attributes of our own after
|
||||
/// the peer's: `added` is one or more whole attributes in the same
|
||||
/// form, of types that `wire` does not have.
|
||||
pub fn intern_received_with(
|
||||
&self,
|
||||
wire: &[u8],
|
||||
added: &[u8],
|
||||
next_hop: Option<&NlriNextHop>,
|
||||
) -> Option<Arc<AttrSet>> {
|
||||
if added.is_empty() && next_hop.is_none() {
|
||||
let Some(next_hop) = next_hop else {
|
||||
return self.intern_wire(wire);
|
||||
}
|
||||
let mut whole = BytesMut::with_capacity(wire.len() + added.len() + 36);
|
||||
whole.put_slice(wire);
|
||||
whole.put_slice(added);
|
||||
if let Some(next_hop) = next_hop {
|
||||
encode_next_hop(next_hop, &mut whole);
|
||||
}
|
||||
self.intern_wire(&whole)
|
||||
};
|
||||
let mut with_next_hop = BytesMut::with_capacity(wire.len() + 36);
|
||||
with_next_hop.put_slice(wire);
|
||||
encode_next_hop(next_hop, &mut with_next_hop);
|
||||
self.intern_wire(&with_next_hop)
|
||||
}
|
||||
|
||||
/// How many distinct sets are stored.
|
||||
@@ -559,8 +396,6 @@ mod tests {
|
||||
neighbor_as: 65001,
|
||||
as_path_len: 3,
|
||||
origin: 2,
|
||||
// The path ends in a set: it has no one origin.
|
||||
origin_as: 0,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -568,171 +403,6 @@ mod tests {
|
||||
assert_eq!(store.intern(&attrs, None).unwrap().key().med, Some(7));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_next_hop_is_read_off_a_set() {
|
||||
let store = AttrStore::new();
|
||||
let plain = store.intern(&attributes(vec![3333]), None).unwrap();
|
||||
assert_eq!(
|
||||
plain.next_hop(),
|
||||
Some(NlriNextHop::Ipv4(Ipv4Addr::new(192, 0, 2, 1)))
|
||||
);
|
||||
// The one that came with the routes counts, where there is one.
|
||||
let with_link_local = NlriNextHop::Ipv6WithLl {
|
||||
global: "2001:db8::1".parse().unwrap(),
|
||||
link_local: "fe80::1".parse().unwrap(),
|
||||
};
|
||||
let v6 = store
|
||||
.intern(&attributes(vec![3333]), Some(&with_link_local))
|
||||
.unwrap();
|
||||
assert_eq!(v6.next_hop(), Some(with_link_local));
|
||||
// A route of our own making may have none.
|
||||
let none = store
|
||||
.intern(&[PathAttribute::Origin(OriginPathAttribute::IGP)], None)
|
||||
.unwrap();
|
||||
assert_eq!(none.next_hop(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_path_is_read_off_a_set_as_it_really_is() {
|
||||
use bgp_packet::message::{AsPathAttribute, AsPathSegment};
|
||||
let store = AttrStore::new();
|
||||
let mut path = Vec::new();
|
||||
|
||||
let set = store
|
||||
.intern(&attributes(vec![3333, 3333, 1299]), None)
|
||||
.unwrap();
|
||||
assert!(!set.as_path(&mut path));
|
||||
assert_eq!(path, [3333, 3333, 1299]);
|
||||
|
||||
// A set is said to be there.
|
||||
let sequence = |path: Vec<u32>| AsPathSegment {
|
||||
ordered: true,
|
||||
path,
|
||||
};
|
||||
let mut with_set = attributes(vec![]);
|
||||
with_set[1] = PathAttribute::AsPath(AsPathAttribute {
|
||||
segments: vec![
|
||||
sequence(vec![3333]),
|
||||
AsPathSegment {
|
||||
ordered: false,
|
||||
path: vec![64500, 64501],
|
||||
},
|
||||
],
|
||||
});
|
||||
let set = store.intern(&with_set, None).unwrap();
|
||||
assert!(set.as_path(&mut path));
|
||||
assert_eq!(path, [3333, 64500, 64501]);
|
||||
|
||||
// No path at all.
|
||||
let set = store.intern(&attributes(vec![]), None).unwrap();
|
||||
assert!(!set.as_path(&mut path));
|
||||
assert!(path.is_empty());
|
||||
}
|
||||
|
||||
/// The BLACKHOLE community is read off the stored bytes.
|
||||
#[test]
|
||||
fn a_blackhole_request_is_told_from_the_bytes() {
|
||||
let store = AttrStore::new();
|
||||
let with = |communities: Vec<(u16, u16)>| {
|
||||
let mut attributes = attributes(vec![3333]);
|
||||
attributes.retain(|attribute| !matches!(attribute, PathAttribute::Communities(_)));
|
||||
attributes.push(PathAttribute::Communities(CommunitiesPathAttribute(
|
||||
communities,
|
||||
)));
|
||||
store.intern(&attributes, None).unwrap()
|
||||
};
|
||||
assert!(with(vec![(3333, 100), (0xFFFF, 0x029A)]).is_blackhole());
|
||||
assert!(!with(vec![(3333, 666), (0xFFFF, 0xFF01)]).is_blackhole());
|
||||
assert!(
|
||||
!store
|
||||
.intern(&attributes(vec![3333]), None)
|
||||
.unwrap()
|
||||
.is_blackhole()
|
||||
);
|
||||
}
|
||||
|
||||
/// The OTC attribute is read off the stored bytes, wherever in them it
|
||||
/// lies.
|
||||
#[test]
|
||||
fn otc_is_told_from_the_bytes() {
|
||||
let store = AttrStore::new();
|
||||
let without = store.intern(&attributes(vec![3333]), None).unwrap();
|
||||
assert!(!without.is_only_to_customer());
|
||||
|
||||
let mut with = attributes(vec![3333]);
|
||||
with.insert(1, PathAttribute::OnlyToCustomer(3333));
|
||||
assert!(store.intern(&with, None).unwrap().is_only_to_customer());
|
||||
|
||||
// After COMMUNITIES, which is the last of the usual attributes.
|
||||
let mut after = attributes(vec![3333]);
|
||||
after.push(PathAttribute::OnlyToCustomer(3333));
|
||||
assert!(store.intern(&after, None).unwrap().is_only_to_customer());
|
||||
}
|
||||
|
||||
/// What a session received, with attributes of our own added after it,
|
||||
/// is one set with the parsed form of the whole; the bytes are not
|
||||
/// parsed to find that out, so the bytes must say the same thing.
|
||||
#[test]
|
||||
fn received_attributes_with_ours_added_are_the_same_set_as_the_parsed_form() {
|
||||
let store = AttrStore::new();
|
||||
let ctx = four_octet();
|
||||
let received = attributes(vec![65001, 65002]);
|
||||
let mut wire = BytesMut::new();
|
||||
encode(&received, None, &mut wire).unwrap();
|
||||
let ours = [
|
||||
PathAttribute::OnlyToCustomer(65001),
|
||||
PathAttribute::LocalPref(LocalPrefPathAttribute(150)),
|
||||
];
|
||||
let mut added = BytesMut::new();
|
||||
for attribute in &ours {
|
||||
attribute.to_wire(&ctx, &mut added).unwrap();
|
||||
}
|
||||
let next_hop = NlriNextHop::Ipv6("2001:db8::1".parse().unwrap());
|
||||
|
||||
let mut whole = received.clone();
|
||||
whole.extend(ours.iter().cloned());
|
||||
let parsed = store.intern(&whole, Some(&next_hop)).unwrap();
|
||||
let from_bytes = store
|
||||
.intern_received_with(&wire, &added, Some(&next_hop))
|
||||
.unwrap();
|
||||
assert!(Arc::ptr_eq(&parsed, &from_bytes));
|
||||
assert!(from_bytes.is_only_to_customer());
|
||||
assert_eq!(from_bytes.next_hop(), Some(next_hop));
|
||||
|
||||
// Without a next hop too.
|
||||
let parsed = store.intern(&whole, None).unwrap();
|
||||
let from_bytes = store.intern_received_with(&wire, &added, None).unwrap();
|
||||
assert!(Arc::ptr_eq(&parsed, &from_bytes));
|
||||
|
||||
// An attribute of ours of a type the peer sent is not a set.
|
||||
let mut repeated = BytesMut::new();
|
||||
PathAttribute::MultiExitDisc(MultiExitDiscPathAttribute(1))
|
||||
.to_wire(&ctx, &mut repeated)
|
||||
.unwrap();
|
||||
assert!(store.intern_received_with(&wire, &repeated, None).is_none());
|
||||
}
|
||||
|
||||
/// Two of the same attribute are not a set: its readers could each
|
||||
/// take a different one.
|
||||
#[test]
|
||||
fn the_same_attribute_twice_is_not_a_set() {
|
||||
let store = AttrStore::new();
|
||||
let origin = [0x40, 1, 1, 0];
|
||||
let path = |asn: u8| [0x40, 2, 6, 2, 1, 0, 0, 0, asn];
|
||||
let once = [&origin[..], &path(1)].concat();
|
||||
assert!(store.intern_wire(&once).is_some());
|
||||
let twice = [&origin[..], &path(1), &path(2)].concat();
|
||||
assert!(store.intern_wire(&twice).is_none());
|
||||
// Whatever the type, known here or not.
|
||||
let unknown = [0xc0, 200, 1, 7];
|
||||
assert!(store.intern_wire(&[&once[..], &unknown].concat()).is_some());
|
||||
assert!(
|
||||
store
|
||||
.intern_wire(&[&once[..], &unknown, &unknown].concat())
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attributes_on_the_wire_are_interned_without_parsing_them() {
|
||||
let store = AttrStore::new();
|
||||
@@ -752,7 +422,6 @@ mod tests {
|
||||
assert_eq!(fresh.key(), parsed.key());
|
||||
assert_eq!(fresh.key().as_path_len, 3);
|
||||
assert_eq!(fresh.key().neighbor_as, 65001);
|
||||
assert_eq!(fresh.key().origin_as, 65003);
|
||||
assert_eq!(fresh.attributes(), Some((attrs, Some(next_hop))));
|
||||
|
||||
// Bytes that are not attributes are refused.
|
||||
|
||||
+179
-374
@@ -87,15 +87,10 @@ impl Default for Limits {
|
||||
struct Entry {
|
||||
seq: u64,
|
||||
prefix: IpPrefix,
|
||||
/// When, by the table's clock: the same for every change one call
|
||||
/// made, in whatever shard. A reader gives out all the changes up to
|
||||
/// a time or none of them, so that what was changed together is read
|
||||
/// together.
|
||||
stamp: u64,
|
||||
}
|
||||
|
||||
/// One journal of one shard.
|
||||
pub(super) struct Journal {
|
||||
struct Journal {
|
||||
kind: Kind,
|
||||
/// The sequence number of the latest change. Counts every change this
|
||||
/// journal is for, whether or not anyone was reading.
|
||||
@@ -116,8 +111,6 @@ pub(super) struct Journal {
|
||||
/// How many reader slots are in use.
|
||||
readers: usize,
|
||||
limits: Limits,
|
||||
/// The time the entries written now are given; see [`Entry::stamp`].
|
||||
stamp: u64,
|
||||
}
|
||||
|
||||
impl Journal {
|
||||
@@ -131,7 +124,6 @@ impl Journal {
|
||||
gone: HashMap::default(),
|
||||
cursors: Vec::new(),
|
||||
readers: 0,
|
||||
stamp: 0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,7 +135,6 @@ impl Journal {
|
||||
self.entries.push_back(Entry {
|
||||
seq: self.seq,
|
||||
prefix,
|
||||
stamp: self.stamp,
|
||||
});
|
||||
}
|
||||
self.seq
|
||||
@@ -165,41 +156,30 @@ impl Journal {
|
||||
self.entries.push_back(Entry {
|
||||
seq: self.seq,
|
||||
prefix,
|
||||
stamp: self.stamp,
|
||||
});
|
||||
self.gone.insert(prefix, self.seq);
|
||||
}
|
||||
}
|
||||
|
||||
/// Voids the note that `entry`'s prefix had gone, if `entry` is what
|
||||
/// said so: an entry that is dropped must not leave the note behind,
|
||||
/// or the prefix would be given out as gone on the strength of a later
|
||||
/// entry that says something else.
|
||||
fn void_gone_note(gone: &mut HashMap<IpPrefix, u64, RandomState>, entry: &Entry) {
|
||||
/// Forgets that `entry`'s prefix had gone, if `entry` is what said so.
|
||||
fn drop_entry(gone: &mut HashMap<IpPrefix, u64, RandomState>, entry: &Entry) {
|
||||
if gone.get(&entry.prefix) == Some(&entry.seq) {
|
||||
gone.remove(&entry.prefix);
|
||||
}
|
||||
}
|
||||
|
||||
/// What `entry` says: something only if it is the latest change of its
|
||||
/// prefix.
|
||||
fn says<'a>(&self, entry: &Entry, rows: &'a HashTable<Row>, hasher: &RandomState) -> Says<'a> {
|
||||
/// What `entry` says, if it still says something: it is the latest
|
||||
/// change of its prefix. The row, or `None` for a prefix that has gone.
|
||||
fn live<'a>(
|
||||
&self,
|
||||
entry: &Entry,
|
||||
rows: &'a HashTable<Row>,
|
||||
hasher: &RandomState,
|
||||
) -> Option<Option<&'a Row>> {
|
||||
let hash = hasher.hash_one(entry.prefix);
|
||||
match rows.find(hash, |row| row.prefix == entry.prefix) {
|
||||
Some(row) if *row.seqs.of(self.kind) == entry.seq => Says::Row(row),
|
||||
None if self.gone.get(&entry.prefix) == Some(&entry.seq) => Says::Gone,
|
||||
_ => Says::Nothing,
|
||||
}
|
||||
}
|
||||
|
||||
/// Where a reader whose cursor is `cursor` is to read from.
|
||||
fn place_of(&self, cursor: u64) -> Place {
|
||||
if cursor < self.floor {
|
||||
Place::Lagged
|
||||
} else if cursor == self.seq {
|
||||
Place::Nothing
|
||||
} else {
|
||||
Place::From(self.entries.partition_point(|entry| entry.seq <= cursor))
|
||||
Some(row) => (row.seqs.of(self.kind) == entry.seq).then_some(Some(row)),
|
||||
None => (self.gone.get(&entry.prefix) == Some(&entry.seq)).then_some(None),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,26 +189,7 @@ impl Journal {
|
||||
if self.entries.len() <= self.limits.tidy_at {
|
||||
return;
|
||||
}
|
||||
self.drop_passed();
|
||||
// Entries for prefixes that have a row are at most as many as the
|
||||
// rows once the dead ones are out. Entries for prefixes that have
|
||||
// gone are not counted in: they are what a reader that has stopped
|
||||
// reading makes pile up. Twice that, so that the pass over every
|
||||
// entry that finding the dead ones takes is made only once the
|
||||
// journal has doubled, and the cost per change stays small.
|
||||
let limit = 2 * (rows.len() + self.limits.slack);
|
||||
if self.entries.len() <= limit {
|
||||
return;
|
||||
}
|
||||
self.drop_dead(rows, hasher);
|
||||
if self.entries.len() <= limit {
|
||||
return;
|
||||
}
|
||||
self.drop_oldest_half();
|
||||
}
|
||||
|
||||
/// Drops what every reader has passed.
|
||||
fn drop_passed(&mut self) {
|
||||
// What every reader has passed can go.
|
||||
let passed = self
|
||||
.cursors
|
||||
.iter()
|
||||
@@ -239,57 +200,40 @@ impl Journal {
|
||||
if front.seq > passed {
|
||||
break;
|
||||
}
|
||||
Self::void_gone_note(&mut self.gone, front);
|
||||
Self::drop_entry(&mut self.gone, front);
|
||||
self.entries.pop_front();
|
||||
}
|
||||
}
|
||||
|
||||
/// Drops the dead entries: those whose prefix has changed again since,
|
||||
/// so that a later entry says what they said. This looks every entry
|
||||
/// up.
|
||||
fn drop_dead(&mut self, rows: &HashTable<Row>, hasher: &RandomState) {
|
||||
// Entries for prefixes that have a row are at most as many as the
|
||||
// rows once the dead ones are out. Entries for prefixes that have
|
||||
// gone are not counted in: they are what a reader that has stopped
|
||||
// reading makes pile up.
|
||||
let limit = 2 * (rows.len() + self.limits.slack);
|
||||
if self.entries.len() <= limit {
|
||||
return;
|
||||
}
|
||||
// Dead entries: their prefix has changed again since. This looks
|
||||
// every entry up, but only once the journal has doubled, so the
|
||||
// cost per change stays small.
|
||||
let mut entries = std::mem::take(&mut self.entries);
|
||||
entries.retain(|entry| !matches!(self.says(entry, rows, hasher), Says::Nothing));
|
||||
entries.retain(|entry| self.live(entry, rows, hasher).is_some());
|
||||
self.entries = entries;
|
||||
}
|
||||
|
||||
/// Drops the older half of the entries without regard to the readers,
|
||||
/// for a journal that is too long even without its dead entries: a
|
||||
/// reader is not reading while prefixes come and go, or more prefixes
|
||||
/// went at once than are left. A reader that had not read what goes
|
||||
/// loses its place. Half, so that readers that are only a little
|
||||
/// behind keep theirs, and so that getting a long journal down to size
|
||||
/// takes few passes.
|
||||
fn drop_oldest_half(&mut self) {
|
||||
if self.entries.len() <= limit {
|
||||
return;
|
||||
}
|
||||
// Still too long: a reader is not reading while prefixes come and
|
||||
// go, or more prefixes went at once than are left. The older half
|
||||
// goes, and a reader that had not read it loses its place. Half,
|
||||
// so that readers that are only a little behind keep theirs, and
|
||||
// so that getting a long journal down to size takes few passes.
|
||||
for entry in self.entries.drain(..self.entries.len() / 2) {
|
||||
Self::void_gone_note(&mut self.gone, &entry);
|
||||
Self::drop_entry(&mut self.gone, &entry);
|
||||
self.floor = entry.seq;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// What an entry says; see [`Journal::says`].
|
||||
enum Says<'a> {
|
||||
/// The prefix's row, whose latest change the entry is.
|
||||
Row(&'a Row),
|
||||
/// The prefix has no route any more, and the entry is what says so.
|
||||
Gone,
|
||||
/// Nothing: the prefix has changed again since, and a later entry has
|
||||
/// it.
|
||||
Nothing,
|
||||
}
|
||||
|
||||
/// Where a reader is to read a journal from; see [`Journal::place_of`].
|
||||
enum Place {
|
||||
/// Entries the reader had not read were dropped; it has lost its
|
||||
/// place.
|
||||
Lagged,
|
||||
/// The reader has read everything up to the latest change.
|
||||
Nothing,
|
||||
/// The index of the first entry the reader has not read.
|
||||
From(usize),
|
||||
}
|
||||
|
||||
/// Which of a shard's two journals.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(super) enum Kind {
|
||||
@@ -299,36 +243,29 @@ pub(super) enum Kind {
|
||||
All,
|
||||
}
|
||||
|
||||
/// One of something for each [`Kind`] of journal.
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub(super) struct ByKind<T> {
|
||||
best: T,
|
||||
all: T,
|
||||
}
|
||||
|
||||
impl<T> ByKind<T> {
|
||||
pub(super) fn of(&self, kind: Kind) -> &T {
|
||||
match kind {
|
||||
Kind::Best => &self.best,
|
||||
Kind::All => &self.all,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn of_mut(&mut self, kind: Kind) -> &mut T {
|
||||
match kind {
|
||||
Kind::Best => &mut self.best,
|
||||
Kind::All => &mut self.all,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The sequence numbers a row remembers, one for each journal: of the
|
||||
/// latest change of its best route, and of any of its routes.
|
||||
pub(super) type Seqs = ByKind<u64>;
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub(super) struct Seqs {
|
||||
best: u64,
|
||||
all: u64,
|
||||
}
|
||||
|
||||
impl Seqs {
|
||||
fn of(self, kind: Kind) -> u64 {
|
||||
match kind {
|
||||
Kind::Best => self.best,
|
||||
Kind::All => self.all,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The two journals of a shard, written together: every change to a row is
|
||||
/// a change of one of its routes, and some are a change of its best route.
|
||||
pub(super) type Journals = ByKind<Journal>;
|
||||
pub(super) struct Journals {
|
||||
best: Journal,
|
||||
all: Journal,
|
||||
}
|
||||
|
||||
impl Journals {
|
||||
pub(super) fn new(limits: Limits) -> Self {
|
||||
@@ -338,20 +275,18 @@ impl Journals {
|
||||
}
|
||||
}
|
||||
|
||||
/// Copies the journals' sequence numbers to `latest`, which the table
|
||||
/// keeps outside the shard's lock for readers to look at
|
||||
/// ([`RibTable::latest_seq`]). Called with the lock held, after every
|
||||
/// change to the shard.
|
||||
pub(super) fn publish(&self, latest: &ByKind<AtomicU64>) {
|
||||
latest.best.store(self.best.seq, Ordering::Release);
|
||||
latest.all.store(self.all.seq, Ordering::Release);
|
||||
fn of(&self, kind: Kind) -> &Journal {
|
||||
match kind {
|
||||
Kind::Best => &self.best,
|
||||
Kind::All => &self.all,
|
||||
}
|
||||
}
|
||||
|
||||
/// The time by the table's clock that the changes made from now on
|
||||
/// are given; see [`Entry::stamp`].
|
||||
pub(super) fn set_stamp(&mut self, stamp: u64) {
|
||||
self.best.stamp = stamp;
|
||||
self.all.stamp = stamp;
|
||||
fn of_mut(&mut self, kind: Kind) -> &mut Journal {
|
||||
match kind {
|
||||
Kind::Best => &mut self.best,
|
||||
Kind::All => &mut self.all,
|
||||
}
|
||||
}
|
||||
|
||||
/// Notes that `prefix` has its first route, and so a row. Returns the
|
||||
@@ -410,11 +345,18 @@ pub(super) struct Mark(u64, u64);
|
||||
pub struct Routes<'a>(&'a Row);
|
||||
|
||||
impl<'a> Routes<'a> {
|
||||
/// The routes: at least one, since a prefix with none has no row, and
|
||||
/// is given out as [`Event::Withdrawn`] instead.
|
||||
pub fn iter(&self) -> impl Iterator<Item = RouteRef<'a>> + use<'a> {
|
||||
self.0.routes()
|
||||
}
|
||||
|
||||
/// How many there are: at least one.
|
||||
pub fn len(&self) -> usize {
|
||||
self.0.paths.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.len() == 0
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Routes<'_> {
|
||||
@@ -428,8 +370,9 @@ impl std::fmt::Debug for Routes<'_> {
|
||||
pub enum Event<'a> {
|
||||
/// Forget what you know. The whole table follows, as [`Event::Best`]
|
||||
/// or [`Event::Routes`] for every prefix, and then [`Event::Synced`].
|
||||
/// The first thing a new reader is given, and what a reader that fell
|
||||
/// too far behind is given in place of what it missed.
|
||||
/// The first thing a new
|
||||
/// reader is given, and what a reader that fell too far behind is
|
||||
/// given in place of what it missed.
|
||||
Reset,
|
||||
/// This is now the best route for `prefix`. Given to a follower of the
|
||||
/// best routes.
|
||||
@@ -465,30 +408,19 @@ pub struct Progress {
|
||||
}
|
||||
|
||||
enum Phase {
|
||||
/// Giving out the whole table; `next` is the shard to do next. `upto`
|
||||
/// is the time on the table's clock the table is given out as of, and
|
||||
/// is taken when [`Event::Reset`] is given, so `None` until then: the
|
||||
/// shards are gone through one at a time while the table changes, and
|
||||
/// the place taken in each shard's journal is as of that one time, so
|
||||
/// that what follows is consistent from shard to shard. A change after
|
||||
/// it that a later shard's rows already show is read again from the
|
||||
/// journal; that is harmless.
|
||||
Resync { next: usize, upto: Option<u64> },
|
||||
/// Following the journals; `next` is the shard to read next, and
|
||||
/// `upto` the time by the table's clock up to which this pass reads
|
||||
/// (see [`Entry::stamp`]).
|
||||
Follow { next: usize, upto: u64 },
|
||||
/// Giving out the whole table; `next` is the shard to do next, and
|
||||
/// `announced` whether [`Event::Reset`] has been given.
|
||||
Resync { next: usize, announced: bool },
|
||||
/// Following the journals; `next` is the shard to read next.
|
||||
Follow { next: usize },
|
||||
}
|
||||
|
||||
impl Phase {
|
||||
/// The beginning of a resynchronisation.
|
||||
const RESYNC: Self = Self::Resync {
|
||||
next: 0,
|
||||
upto: None,
|
||||
announced: false,
|
||||
};
|
||||
|
||||
/// The beginning of a pass over the journals.
|
||||
const FOLLOW: Self = Self::Follow { next: 0, upto: 0 };
|
||||
}
|
||||
|
||||
/// Follows the routes of a table: the best of each prefix, or all of them.
|
||||
@@ -505,10 +437,7 @@ pub struct Reader {
|
||||
/// is for the shard's benefit.
|
||||
cursors: Vec<u64>,
|
||||
phase: Phase,
|
||||
/// The table's version as it was when the current pass over the
|
||||
/// journals began. [`Reader::changed`] waits while the version is still
|
||||
/// this one: whatever was written since the pass began has moved it
|
||||
/// on, and is there to be read.
|
||||
/// The table's version when the reader was last found caught up.
|
||||
seen: u64,
|
||||
caught_up: bool,
|
||||
}
|
||||
@@ -524,7 +453,7 @@ impl RibTable {
|
||||
/// those who want the news and not the table. If it falls too far
|
||||
/// behind it is reset like any other, and is then given the table.
|
||||
pub fn reader_from_now(self: &Arc<Self>) -> Reader {
|
||||
let mut reader = self.register(Kind::Best, Phase::FOLLOW);
|
||||
let mut reader = self.register(Kind::Best, Phase::Follow { next: 0 });
|
||||
for (index, shard) in self.shards.iter().enumerate() {
|
||||
let shard = read(shard);
|
||||
reader.set_cursor(index, &shard, shard.journals.best.seq);
|
||||
@@ -618,14 +547,8 @@ impl Drop for Reader {
|
||||
enum Drained {
|
||||
/// Entries were dropped that this reader had not read.
|
||||
Lagged,
|
||||
/// `events` were given out; `consumed` if the reader's place in the
|
||||
/// journal moved at all; `more` if entries were left past the time
|
||||
/// read up to.
|
||||
Read {
|
||||
events: usize,
|
||||
consumed: bool,
|
||||
more: bool,
|
||||
},
|
||||
/// `events` were given out; `more` if the budget ran out first.
|
||||
Read { events: usize, more: bool },
|
||||
}
|
||||
|
||||
impl Reader {
|
||||
@@ -640,128 +563,77 @@ impl Reader {
|
||||
pub fn poll(&mut self, budget: usize, mut visit: impl FnMut(Event<'_>)) -> Progress {
|
||||
// A call that may give nothing out would never get anywhere.
|
||||
let budget = budget.max(1);
|
||||
// Held once for the call: a count taken on the table for every
|
||||
// shard of every pass, by every reader, was the most of what a
|
||||
// router with many peers did.
|
||||
let table = Arc::clone(&self.table);
|
||||
let shards = self.table.shards.len();
|
||||
let mut events = 0;
|
||||
loop {
|
||||
let done = match self.phase {
|
||||
Phase::Resync { next, upto } => {
|
||||
self.resync_step(&table, next, upto, budget, &mut events, &mut visit)
|
||||
match self.phase {
|
||||
Phase::Resync { next, announced } => {
|
||||
if !announced {
|
||||
self.forget_place();
|
||||
visit(Event::Reset);
|
||||
events += 1;
|
||||
}
|
||||
if next == shards {
|
||||
visit(Event::Synced);
|
||||
events += 1;
|
||||
self.phase = Phase::Follow { next: 0 };
|
||||
continue;
|
||||
}
|
||||
events += self.scan(next, &mut visit);
|
||||
self.phase = Phase::Resync {
|
||||
next: next + 1,
|
||||
announced: true,
|
||||
};
|
||||
if events >= budget {
|
||||
return self.progress(events, false);
|
||||
}
|
||||
}
|
||||
Phase::Follow { next, upto } => {
|
||||
self.follow_pass(&table, next, upto, budget, &mut events, &mut visit)
|
||||
}
|
||||
};
|
||||
if let Some(progress) = done {
|
||||
return progress;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One step of a resynchronisation: the shard `next`, or the end of it
|
||||
/// once every shard has been given out. Returns what to tell the
|
||||
/// caller if the budget is spent, and `None` to go on.
|
||||
fn resync_step(
|
||||
&mut self,
|
||||
table: &RibTable,
|
||||
next: usize,
|
||||
upto: Option<u64>,
|
||||
budget: usize,
|
||||
events: &mut usize,
|
||||
visit: &mut impl FnMut(Event<'_>),
|
||||
) -> Option<Progress> {
|
||||
let upto = upto.unwrap_or_else(|| {
|
||||
self.forget_place();
|
||||
visit(Event::Reset);
|
||||
*events += 1;
|
||||
table.settled()
|
||||
});
|
||||
if next == table.shards.len() {
|
||||
visit(Event::Synced);
|
||||
*events += 1;
|
||||
self.phase = Phase::FOLLOW;
|
||||
return None;
|
||||
}
|
||||
*events += self.scan(table, next, upto, visit);
|
||||
self.phase = Phase::Resync {
|
||||
next: next + 1,
|
||||
upto: Some(upto),
|
||||
};
|
||||
(*events >= budget).then(|| self.progress(*events, false))
|
||||
}
|
||||
|
||||
/// A pass over the journals from shard `next` on, or the rest of one
|
||||
/// that an earlier call cut short. Returns what to tell the caller if
|
||||
/// the pass is to be the call's last, and `None` to go round again.
|
||||
fn follow_pass(
|
||||
&mut self,
|
||||
table: &RibTable,
|
||||
next: usize,
|
||||
upto: u64,
|
||||
budget: usize,
|
||||
events: &mut usize,
|
||||
visit: &mut impl FnMut(Event<'_>),
|
||||
) -> Option<Progress> {
|
||||
// Read before the pass: a change made during it shows as a newer
|
||||
// version, and `changed` then returns at once. And the pass reads
|
||||
// up to one time in every shard alike, chosen so that about the
|
||||
// budget is read.
|
||||
let (version, upto) = if next == 0 {
|
||||
(
|
||||
table.version.load(Ordering::Acquire),
|
||||
self.time_for(budget - *events),
|
||||
)
|
||||
} else {
|
||||
(self.seen, upto)
|
||||
};
|
||||
self.seen = version;
|
||||
let mut consumed_any = false;
|
||||
let mut more_somewhere = false;
|
||||
for shard in next..table.shards.len() {
|
||||
if *events >= budget {
|
||||
self.phase = Phase::Follow { next: shard, upto };
|
||||
return Some(self.progress(*events, false));
|
||||
}
|
||||
// Nothing new in this shard, by the number that is read
|
||||
// without its lock: it is left alone, and so are the sessions
|
||||
// writing to it.
|
||||
if self.cursors[shard] == table.latest_seq(shard, self.kind) {
|
||||
continue;
|
||||
}
|
||||
match self.drain(table, shard, upto, visit) {
|
||||
Drained::Lagged => {
|
||||
self.phase = Phase::RESYNC;
|
||||
return None;
|
||||
}
|
||||
Drained::Read {
|
||||
events: read,
|
||||
consumed,
|
||||
more,
|
||||
} => {
|
||||
*events += read;
|
||||
consumed_any |= consumed;
|
||||
more_somewhere |= more;
|
||||
Phase::Follow { next } => {
|
||||
// Read before the pass: a change made during it shows as
|
||||
// a newer version, and `changed` then returns at once.
|
||||
let version = if next == 0 {
|
||||
self.table.version.load(Ordering::Acquire)
|
||||
} else {
|
||||
self.seen
|
||||
};
|
||||
self.seen = version;
|
||||
let mut shard = next;
|
||||
let mut lagged = false;
|
||||
while shard < shards {
|
||||
if events >= budget {
|
||||
self.phase = Phase::Follow { next: shard };
|
||||
return self.progress(events, false);
|
||||
}
|
||||
match self.drain(shard, budget - events, &mut visit) {
|
||||
Drained::Lagged => {
|
||||
lagged = true;
|
||||
break;
|
||||
}
|
||||
Drained::Read { events: read, more } => {
|
||||
events += read;
|
||||
if more {
|
||||
self.phase = Phase::Follow { next: shard };
|
||||
return self.progress(events, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
shard += 1;
|
||||
}
|
||||
if lagged {
|
||||
self.phase = Phase::RESYNC;
|
||||
continue;
|
||||
}
|
||||
self.phase = Phase::Follow { next: 0 };
|
||||
if next != 0 {
|
||||
// This pass took up where an earlier call stopped.
|
||||
// The shards before that point have not been looked
|
||||
// at in this call, so go round again from the first.
|
||||
continue;
|
||||
}
|
||||
return self.progress(events, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.phase = Phase::FOLLOW;
|
||||
if next == 0 && !consumed_any && more_somewhere {
|
||||
// A whole pass got nowhere, though there is more: it is all
|
||||
// beyond the earliest change still under way, which no pass
|
||||
// can read past until it ends. Asking again at once would spin
|
||||
// until then. The change wakes the readers when it ends.
|
||||
return Some(self.progress(*events, true));
|
||||
}
|
||||
if next != 0 || more_somewhere {
|
||||
// This pass took up where an earlier call stopped, so the
|
||||
// shards before that point have not been looked at in this
|
||||
// call; or some shard had more than its share, or changes
|
||||
// later than this pass's time. Go round again from the first.
|
||||
return (*events >= budget).then(|| self.progress(*events, false));
|
||||
}
|
||||
Some(self.progress(*events, true))
|
||||
}
|
||||
|
||||
fn progress(&mut self, events: usize, caught_up: bool) -> Progress {
|
||||
@@ -769,11 +641,6 @@ impl Reader {
|
||||
Progress { events, caught_up }
|
||||
}
|
||||
|
||||
/// Whether the last [`Reader::poll`] read everything there was.
|
||||
pub fn caught_up(&self) -> bool {
|
||||
self.caught_up
|
||||
}
|
||||
|
||||
/// Waits until there may be something to read. Returns at once if the
|
||||
/// last [`Reader::poll`] did not catch up.
|
||||
pub async fn changed(&mut self) {
|
||||
@@ -824,109 +691,52 @@ impl Reader {
|
||||
shard.journals.of(self.kind).cursors[self.slot].store(cursor, Ordering::Release);
|
||||
}
|
||||
|
||||
/// Gives out every prefix in a shard and takes a place in its journal
|
||||
/// as of `upto` on the table's clock, in one hold of the shard: what
|
||||
/// changed after that time is in the journal after that place, whether
|
||||
/// or not the rows given out show it already.
|
||||
fn scan(
|
||||
&mut self,
|
||||
table: &RibTable,
|
||||
index: usize,
|
||||
upto: u64,
|
||||
visit: &mut impl FnMut(Event<'_>),
|
||||
) -> usize {
|
||||
/// Gives out every prefix in a shard and takes a place at the end of
|
||||
/// its journal, in one hold of the shard: what changes afterwards is in
|
||||
/// the journal after that place.
|
||||
fn scan(&mut self, index: usize, visit: &mut impl FnMut(Event<'_>)) -> usize {
|
||||
let table = Arc::clone(&self.table);
|
||||
let shard = read(&table.shards[index]);
|
||||
for row in &shard.rows {
|
||||
visit(self.event_for(row));
|
||||
}
|
||||
let journal = shard.journals.of(self.kind);
|
||||
let beyond = journal.entries.partition_point(|entry| entry.stamp <= upto);
|
||||
let cursor = match (beyond.checked_sub(1), journal.entries.get(beyond)) {
|
||||
// Nothing in the journal is later than `upto`.
|
||||
(_, None) => journal.seq,
|
||||
// The last entry that is not later; or, if every entry is
|
||||
// later, just before the first.
|
||||
(Some(last), Some(_)) => journal.entries[last].seq,
|
||||
(None, Some(first)) => first.seq - 1,
|
||||
};
|
||||
self.set_cursor(index, &shard, cursor);
|
||||
self.set_cursor(index, &shard, shard.journals.of(self.kind).seq);
|
||||
shard.rows.len()
|
||||
}
|
||||
|
||||
/// The time on the table's clock up to which a pass over the shards
|
||||
/// is to read, for about `budget` events in all: changes made together
|
||||
/// are read together, in whatever shards they are (see
|
||||
/// [`Entry::stamp`]). Taken shard by shard to the budget, the first
|
||||
/// shards would be read far ahead of the last, and the routes of one
|
||||
/// UPDATE, spread over the shards by their prefixes, given out far
|
||||
/// apart.
|
||||
fn time_for(&self, budget: usize) -> u64 {
|
||||
let shards = self.table.shards.len();
|
||||
let share = (budget / shards).max(1);
|
||||
// No further than the changes that are in every shard already.
|
||||
let settled = self.table.settled();
|
||||
let mut upto = settled;
|
||||
// The earliest change not yet read: at least that one is, however
|
||||
// many changes were made with it.
|
||||
let mut earliest = None;
|
||||
for (index, shard) in self.table.shards.iter().enumerate() {
|
||||
let cursor = self.cursors[index];
|
||||
if cursor == self.table.latest_seq(index, self.kind) {
|
||||
continue;
|
||||
}
|
||||
let shard = read(shard);
|
||||
let journal = shard.journals.of(self.kind);
|
||||
let Place::From(start) = journal.place_of(cursor) else {
|
||||
continue;
|
||||
};
|
||||
if let Some(next) = journal.entries.get(start) {
|
||||
earliest = Some(earliest.map_or(next.stamp, |e: u64| e.min(next.stamp)));
|
||||
}
|
||||
// Everything before the change that would go over the share.
|
||||
if let Some(beyond) = journal.entries.get(start + share) {
|
||||
upto = upto.min(beyond.stamp - 1);
|
||||
}
|
||||
}
|
||||
upto.max(earliest.unwrap_or(upto)).min(settled)
|
||||
}
|
||||
|
||||
/// Reads a shard's journal from this reader's place, no further than
|
||||
/// the changes made by `upto` on the table's clock.
|
||||
fn drain(
|
||||
&mut self,
|
||||
table: &RibTable,
|
||||
index: usize,
|
||||
upto: u64,
|
||||
visit: &mut impl FnMut(Event<'_>),
|
||||
) -> Drained {
|
||||
/// Reads a shard's journal from this reader's place, for at most `max`
|
||||
/// events.
|
||||
fn drain(&mut self, index: usize, max: usize, visit: &mut impl FnMut(Event<'_>)) -> Drained {
|
||||
let table = Arc::clone(&self.table);
|
||||
let shard = read(&table.shards[index]);
|
||||
let journal = shard.journals.of(self.kind);
|
||||
let mut cursor = self.cursors[index];
|
||||
let start = match journal.place_of(cursor) {
|
||||
Place::Lagged => return Drained::Lagged,
|
||||
Place::Nothing => {
|
||||
return Drained::Read {
|
||||
events: 0,
|
||||
consumed: false,
|
||||
more: false,
|
||||
};
|
||||
}
|
||||
Place::From(start) => start,
|
||||
};
|
||||
if cursor < journal.floor {
|
||||
return Drained::Lagged;
|
||||
}
|
||||
if cursor == journal.seq {
|
||||
return Drained::Read {
|
||||
events: 0,
|
||||
more: false,
|
||||
};
|
||||
}
|
||||
|
||||
let start = journal.entries.partition_point(|entry| entry.seq <= cursor);
|
||||
let mut events = 0;
|
||||
let mut more = false;
|
||||
for entry in journal.entries.range(start..) {
|
||||
if entry.stamp > upto {
|
||||
if events == max {
|
||||
more = true;
|
||||
break;
|
||||
}
|
||||
cursor = entry.seq;
|
||||
let event = match journal.says(entry, &shard.rows, &shard.hasher) {
|
||||
Says::Row(row) => self.event_for(row),
|
||||
Says::Gone => Event::Withdrawn {
|
||||
let event = match journal.live(entry, &shard.rows, &shard.hasher) {
|
||||
Some(Some(row)) => self.event_for(row),
|
||||
Some(None) => Event::Withdrawn {
|
||||
prefix: entry.prefix,
|
||||
},
|
||||
Says::Nothing => continue,
|
||||
// The prefix has changed again; a later entry has it.
|
||||
None => continue,
|
||||
};
|
||||
visit(event);
|
||||
events += 1;
|
||||
@@ -935,13 +745,8 @@ impl Reader {
|
||||
// Past the last entry there is nothing up to the latest change.
|
||||
cursor = journal.seq;
|
||||
}
|
||||
let consumed = cursor != self.cursors[index];
|
||||
self.set_cursor(index, &shard, cursor);
|
||||
Drained::Read {
|
||||
events,
|
||||
consumed,
|
||||
more,
|
||||
}
|
||||
Drained::Read { events, more }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ use proptest::prelude::*;
|
||||
|
||||
use super::super::tests::{Fixture, Route, external, internal, prefix, route};
|
||||
use super::super::{AttrSet, PeerIndex, PeerInfo, RibTable};
|
||||
use super::{Event, Kind, Limits, Reader, read};
|
||||
use super::{Event, Limits, Reader};
|
||||
|
||||
/// What a follower would keep: its own copy of the best routes, brought
|
||||
/// up to date from the events it is given.
|
||||
@@ -53,10 +53,7 @@ impl Mirror {
|
||||
}
|
||||
Event::Routes { prefix, routes } => {
|
||||
self.routes_events += 1;
|
||||
assert!(
|
||||
routes.iter().next().is_some(),
|
||||
"a prefix with no routes is withdrawn"
|
||||
);
|
||||
assert!(!routes.is_empty(), "a prefix with no routes is withdrawn");
|
||||
self.routes.insert(
|
||||
prefix,
|
||||
sorted(
|
||||
@@ -118,6 +115,9 @@ impl Mirror {
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Mirror {
|
||||
/// For the mirror of a follower of all routes: it holds every route in
|
||||
/// the table, and knows which is best.
|
||||
#[track_caller]
|
||||
@@ -213,107 +213,6 @@ fn a_reader_from_now_is_given_the_changes_and_not_the_table() {
|
||||
assert_eq!((best, withdrawn), (vec![new], vec![old]));
|
||||
}
|
||||
|
||||
/// The number a reader checks without a shard's lock must be the journal's
|
||||
/// own after every kind of change, whether or not anyone is reading: a
|
||||
/// reader skips a shard on the strength of it, and a shard with news that
|
||||
/// said it had none would never be read until the next change.
|
||||
#[test]
|
||||
fn the_lock_free_number_is_the_journals_after_every_kind_of_change() {
|
||||
let mut fixture = Fixture::new(&[external(1), external(2)]);
|
||||
let prefixes: Vec<IpPrefix> = (0..16).map(nth_prefix).collect();
|
||||
let agree = |table: &RibTable| {
|
||||
for (index, shard) in table.shards.iter().enumerate() {
|
||||
let shard = read(shard);
|
||||
for kind in [Kind::Best, Kind::All] {
|
||||
assert_eq!(
|
||||
table.latest_seq(index, kind),
|
||||
shard.journals.of(kind).seq,
|
||||
"shard {index}, {kind:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Without a reader, with one of each kind, and with both.
|
||||
for readers in 0..4 {
|
||||
let _best = (readers & 1 != 0).then(|| fixture.table.reader());
|
||||
let _all = (readers & 2 != 0).then(|| fixture.table.reader_of_all_routes());
|
||||
agree(&fixture.table);
|
||||
|
||||
for p in &prefixes {
|
||||
fixture.announce(0, p, &Route::via(&[1]));
|
||||
}
|
||||
agree(&fixture.table);
|
||||
// The same prefixes again, with other attributes.
|
||||
for p in &prefixes {
|
||||
fixture.announce(0, p, &Route::via(&[1, 2]));
|
||||
}
|
||||
agree(&fixture.table);
|
||||
// A second route for each, then the last route of one taken away.
|
||||
for p in &prefixes {
|
||||
fixture.announce(1, p, &Route::via(&[2, 3]));
|
||||
}
|
||||
agree(&fixture.table);
|
||||
fixture.withdraw(0, &prefixes[0]);
|
||||
agree(&fixture.table);
|
||||
fixture.withdraw(1, &prefixes[0]);
|
||||
agree(&fixture.table);
|
||||
|
||||
fixture.table.remove_peer(fixture.peers[1]);
|
||||
agree(&fixture.table);
|
||||
fixture.peers[1] = fixture.table.add_peer(external(2)).unwrap();
|
||||
|
||||
fixture
|
||||
.table
|
||||
.mark_stale(fixture.peers[0], &[IpVersion::Ipv4]);
|
||||
agree(&fixture.table);
|
||||
fixture
|
||||
.table
|
||||
.sweep_stale(fixture.peers[0], &[IpVersion::Ipv4]);
|
||||
agree(&fixture.table);
|
||||
assert!(
|
||||
fixture.table.is_empty(),
|
||||
"round {readers} left routes behind"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A shard that had nothing new for a reader is passed over without its
|
||||
/// lock. The next change to it must bring the reader back.
|
||||
#[test]
|
||||
fn a_shard_passed_over_is_read_again_once_it_has_news() {
|
||||
let fixture = Fixture::on(RibTable::new(1), &[external(1)]);
|
||||
let mut reader = fixture.table.reader();
|
||||
let mut mirror = Mirror::default();
|
||||
mirror.catch_up(&mut reader, 100);
|
||||
assert_eq!(reader.poll(100, |_| panic!("no event expected")).events, 0);
|
||||
|
||||
fixture.announce(0, &prefix("10.0.0.0/8"), &Route::via(&[1]));
|
||||
let mut events = Vec::new();
|
||||
let progress = reader.poll(100, |event| events.push(format!("{event:?}")));
|
||||
assert!(progress.caught_up);
|
||||
assert_eq!(events.len(), 1, "{events:?}");
|
||||
assert!(events[0].starts_with("Best"), "{events:?}");
|
||||
}
|
||||
|
||||
/// As [`a_shard_passed_over_is_read_again_once_it_has_news`], for a reader
|
||||
/// that begins where the journals are rather than with the table.
|
||||
#[test]
|
||||
fn a_shard_passed_over_by_a_reader_from_now_is_read_again_once_it_has_news() {
|
||||
let fixture = Fixture::on(RibTable::new(1), &[external(1)]);
|
||||
fixture.announce(0, &prefix("192.0.2.0/24"), &Route::via(&[1]));
|
||||
let mut reader = fixture.table.reader_from_now();
|
||||
assert!(reader.poll(100, |_| panic!("no event expected")).caught_up);
|
||||
assert_eq!(reader.poll(100, |_| panic!("no event expected")).events, 0);
|
||||
|
||||
fixture.announce(0, &prefix("10.0.0.0/8"), &Route::via(&[1]));
|
||||
let mut events = Vec::new();
|
||||
let progress = reader.poll(100, |event| events.push(format!("{event:?}")));
|
||||
assert!(progress.caught_up);
|
||||
assert_eq!(events.len(), 1, "{events:?}");
|
||||
assert!(events[0].starts_with("Best"), "{events:?}");
|
||||
}
|
||||
|
||||
/// A change to a route that is not the best is nothing to a follower of
|
||||
/// the best routes, and is what a follower of all routes is there for.
|
||||
#[test]
|
||||
@@ -618,45 +517,6 @@ async fn a_waiting_reader_is_woken_by_a_change_of_best_route() {
|
||||
.expect("the change was already there");
|
||||
}
|
||||
|
||||
/// A reader does not read past a change that is still under way, which
|
||||
/// a change to every shard (a peer's whole table going) is for a while.
|
||||
/// Changes made by others meanwhile are beyond it. A reader that finds
|
||||
/// only those must say it is caught up and wait, not ask again at once
|
||||
/// for as long as the change takes; and when the change ends, it is
|
||||
/// woken, though the change itself wrote nothing for it.
|
||||
#[tokio::test]
|
||||
async fn a_change_under_way_does_not_keep_a_reader_spinning() {
|
||||
let fixture = Fixture::new(&[external(1), external(2)]);
|
||||
let mut reader = fixture.table.reader();
|
||||
let mut mirror = Mirror::default();
|
||||
mirror.catch_up(&mut reader, 100);
|
||||
|
||||
// A long change begins, and another writer announces meanwhile.
|
||||
let change = fixture.table.begin_change();
|
||||
fixture.announce(1, &prefix("10.0.0.0/8"), &Route::via(&[2]));
|
||||
|
||||
let progress = reader.poll(100, |_| {});
|
||||
assert_eq!(progress.events, 0, "nothing can be read past the change");
|
||||
assert!(
|
||||
progress.caught_up,
|
||||
"and the reader is to wait, not ask again"
|
||||
);
|
||||
let waiting = tokio::time::timeout(std::time::Duration::from_millis(50), reader.changed());
|
||||
assert!(
|
||||
waiting.await.is_err(),
|
||||
"nothing to be had until the change ends"
|
||||
);
|
||||
|
||||
// The change ends without writing anything itself.
|
||||
drop(change);
|
||||
tokio::time::timeout(std::time::Duration::from_secs(5), reader.changed())
|
||||
.await
|
||||
.expect("woken when the change ended");
|
||||
mirror.catch_up(&mut reader, 100);
|
||||
mirror.assert_matches(&fixture.table);
|
||||
assert_eq!(mirror.best.len(), 1);
|
||||
}
|
||||
|
||||
/// Writers on several threads, a reader on another, reading while they
|
||||
/// write. When the writers are done and the reader has caught up, it has
|
||||
/// what the table has.
|
||||
@@ -851,56 +711,3 @@ proptest! {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Routes that were announced together, with one set of attributes, are
|
||||
/// spread over the shards by their prefixes. A reader that takes its
|
||||
/// budget from the shards evenly is given them together all the same,
|
||||
/// which is what lets whoever passes them on send them in one message.
|
||||
#[test]
|
||||
fn changes_made_together_are_read_together() {
|
||||
let fixture = Fixture::on(RibTable::new(128), &[external(1)]);
|
||||
let mut reader = fixture.table.reader();
|
||||
assert!(reader.poll(10, |_| {}).caught_up);
|
||||
|
||||
// 6000 UPDATEs of six prefixes each, every one with attributes of its
|
||||
// own.
|
||||
let groups = 6000u32;
|
||||
for group in 0..groups {
|
||||
let attrs = fixture
|
||||
.table
|
||||
.attrs()
|
||||
.intern(&Route::via(&[1, 100_000 + group]).attributes(), None)
|
||||
.unwrap();
|
||||
let prefixes: Vec<IpPrefix> = (0..6).map(|n| nth_prefix(group * 6 + n)).collect();
|
||||
fixture.table.announce(fixture.peers[0], &attrs, &prefixes);
|
||||
}
|
||||
|
||||
// Read in batches a tenth of that size: each should hold whole groups,
|
||||
// but for the ones cut by its edges.
|
||||
let mut batches = 0;
|
||||
let mut split_groups = 0;
|
||||
let mut seen_total = 0;
|
||||
while seen_total < groups * 6 {
|
||||
let mut groups_in_batch: HashMap<usize, usize> = HashMap::new();
|
||||
let progress = reader.poll(3600, |event| {
|
||||
if let Event::Best { attrs, .. } = event {
|
||||
*groups_in_batch
|
||||
.entry(Arc::as_ptr(attrs) as usize)
|
||||
.or_default() += 1;
|
||||
}
|
||||
});
|
||||
seen_total += groups_in_batch.values().sum::<usize>() as u32;
|
||||
split_groups += groups_in_batch.values().filter(|n| **n < 6).count();
|
||||
batches += 1;
|
||||
if progress.events == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert_eq!(seen_total, groups * 6);
|
||||
// A few are, where a batch's time falls inside one: the reader cannot
|
||||
// know what was written together until it is all there.
|
||||
assert!(
|
||||
split_groups < 100,
|
||||
"{split_groups} of {groups} groups were split over {batches} batches"
|
||||
);
|
||||
}
|
||||
+59
-384
@@ -22,7 +22,7 @@
|
||||
//! - **Readers do not stop the table.** Reading takes a shard's lock for
|
||||
//! reading, one shard at a time, and looks at the rows in place.
|
||||
//! - **Followers go at their own pace.** What acts on the best routes reads
|
||||
//! the changes from a `journal`, and a slow reader holds nothing up.
|
||||
//! the changes from a [`journal`], and a slow reader holds nothing up.
|
||||
|
||||
pub mod attrs;
|
||||
mod journal;
|
||||
@@ -30,13 +30,11 @@ mod select;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
use std::hash::BuildHasher;
|
||||
use std::net::{IpAddr, Ipv4Addr};
|
||||
use std::sync::atomic::{AtomicU64, AtomicUsize};
|
||||
use std::sync::{Arc, Mutex, PoisonError, RwLock, RwLockReadGuard, RwLockWriteGuard};
|
||||
|
||||
use bgp_primitives::constants::IpVersion;
|
||||
use bgp_primitives::ip_prefix::{IpPrefix, Nlri};
|
||||
use foldhash::fast::RandomState;
|
||||
use hashbrown::HashTable;
|
||||
@@ -53,15 +51,8 @@ pub struct PeerInfo {
|
||||
/// The peer's BGP Identifier.
|
||||
pub router_id: Ipv4Addr,
|
||||
pub address: IpAddr,
|
||||
/// The number of the interface the peer is on, if its address is a
|
||||
/// link-local one; zero otherwise.
|
||||
pub interface: u32,
|
||||
/// Whether the peer is in our own AS.
|
||||
pub ibgp: bool,
|
||||
/// Whether routes learned from our other routers are passed on to the
|
||||
/// peer, and its routes to them: whether it is a route reflector
|
||||
/// client of ours (RFC 4456).
|
||||
pub reflector_client: bool,
|
||||
}
|
||||
|
||||
/// A peer's number in a table, given out by [`RibTable::add_peer`]. Small,
|
||||
@@ -90,9 +81,6 @@ pub struct Outcome {
|
||||
pub removed: usize,
|
||||
/// Prefixes whose best route is now another one, or none.
|
||||
pub best_changed: usize,
|
||||
/// Stale routes that are no longer: replaced or withdrawn by their
|
||||
/// peer (graceful restart; see [`RibTable::mark_stale`]).
|
||||
pub stale_cleared: usize,
|
||||
}
|
||||
|
||||
impl std::ops::AddAssign for Outcome {
|
||||
@@ -101,31 +89,20 @@ impl std::ops::AddAssign for Outcome {
|
||||
self.replaced += other.replaced;
|
||||
self.removed += other.removed;
|
||||
self.best_changed += other.best_changed;
|
||||
self.stale_cleared += other.stale_cleared;
|
||||
}
|
||||
}
|
||||
|
||||
/// One route of a prefix: sixteen bytes.
|
||||
struct Path {
|
||||
/// Three things in one word. The high 48 bits are the first steps of
|
||||
/// Two things in one word. The high 48 bits are the first steps of
|
||||
/// route selection as one number, the larger the better; see
|
||||
/// [`select::preference`]. The low 16 are the peer the route is from.
|
||||
/// And one bit that the number never uses marks the route stale; see
|
||||
/// [`STALE`].
|
||||
word: u64,
|
||||
attrs: Arc<AttrSet>,
|
||||
}
|
||||
|
||||
/// The bit of a [`Path`]'s word that marks a route stale: kept from a
|
||||
/// session that ended, until the peer announces it again or it is swept
|
||||
/// away (graceful restart, RFC 4724 §4.2). It is the lowest bit of the
|
||||
/// preference, which is always zero ([`select::preference`]), and is left
|
||||
/// out of it: a stale route is chosen like any other, as the RFC has it.
|
||||
const STALE: u64 = 1 << 16;
|
||||
|
||||
impl Path {
|
||||
fn new(preference: u64, peer: PeerIndex, attrs: &Arc<AttrSet>) -> Self {
|
||||
debug_assert_eq!(preference & 1, 0, "the bit for stale routes is used");
|
||||
Self {
|
||||
word: (preference << 16) | u64::from(peer.0),
|
||||
attrs: Arc::clone(attrs),
|
||||
@@ -133,11 +110,7 @@ impl Path {
|
||||
}
|
||||
|
||||
fn preference(&self) -> u64 {
|
||||
(self.word & !STALE) >> 16
|
||||
}
|
||||
|
||||
fn is_stale(&self) -> bool {
|
||||
self.word & STALE != 0
|
||||
self.word >> 16
|
||||
}
|
||||
|
||||
fn peer(&self) -> PeerIndex {
|
||||
@@ -193,16 +166,18 @@ impl Row {
|
||||
.position(|(index, path)| path.peer() == peer && self.path_id(index) == path_id)
|
||||
}
|
||||
|
||||
/// Adds a route. `peers` is how many peers the table has: a prefix
|
||||
/// that one of them announces, the others mostly announce too, so a
|
||||
/// row is given room for that many at its first growth.
|
||||
fn push(&mut self, path: Path, path_id: u32, peers: usize) {
|
||||
/// Where a route from `peer` is, whatever its identifier.
|
||||
fn position_of_peer(&self, peer: PeerIndex) -> Option<usize> {
|
||||
self.paths.iter().position(|path| path.peer() == peer)
|
||||
}
|
||||
|
||||
fn push(&mut self, path: Path, path_id: u32) {
|
||||
if path_id != 0 && self.path_ids.is_empty() {
|
||||
self.path_ids = vec![0; self.paths.len()];
|
||||
}
|
||||
push_tight(&mut self.paths, path, peers);
|
||||
push_tight(&mut self.paths, path);
|
||||
if !self.path_ids.is_empty() {
|
||||
push_tight(&mut self.path_ids, path_id, peers);
|
||||
push_tight(&mut self.path_ids, path_id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,7 +207,6 @@ impl Row {
|
||||
path_id: self.path_id(index),
|
||||
attrs: &path.attrs,
|
||||
best: index == self.best(),
|
||||
stale: path.is_stale(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,23 +215,6 @@ impl Row {
|
||||
}
|
||||
}
|
||||
|
||||
/// A prefix in the table with its routes, looked at in place.
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct PrefixRef<'a> {
|
||||
row: &'a Row,
|
||||
}
|
||||
|
||||
impl<'a> PrefixRef<'a> {
|
||||
pub fn prefix(&self) -> &'a IpPrefix {
|
||||
&self.row.prefix
|
||||
}
|
||||
|
||||
/// The routes there are for the prefix: at least one.
|
||||
pub fn routes(&self) -> impl Iterator<Item = RouteRef<'a>> + use<'a> {
|
||||
self.row.routes()
|
||||
}
|
||||
}
|
||||
|
||||
/// A route in the table, looked at in place.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct RouteRef<'a> {
|
||||
@@ -270,9 +227,6 @@ pub struct RouteRef<'a> {
|
||||
pub attrs: &'a Arc<AttrSet>,
|
||||
/// Whether it is the route chosen for its prefix.
|
||||
pub best: bool,
|
||||
/// Whether it is kept from a session of the peer's that ended, and has
|
||||
/// not been announced again since (graceful restart).
|
||||
pub stale: bool,
|
||||
}
|
||||
|
||||
impl RouteRef<'_> {
|
||||
@@ -284,7 +238,6 @@ impl RouteRef<'_> {
|
||||
path_id: self.path_id,
|
||||
attrs: Arc::clone(self.attrs),
|
||||
best: self.best,
|
||||
stale: self.stale,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -297,7 +250,6 @@ pub struct Route {
|
||||
pub path_id: u32,
|
||||
pub attrs: Arc<AttrSet>,
|
||||
pub best: bool,
|
||||
pub stale: bool,
|
||||
}
|
||||
|
||||
struct Shard {
|
||||
@@ -313,21 +265,10 @@ struct Shard {
|
||||
journals: journal::Journals,
|
||||
}
|
||||
|
||||
/// A shard, and beside its lock what is read without taking it.
|
||||
struct ShardSlot {
|
||||
lock: RwLock<Shard>,
|
||||
/// The sequence number of each of the shard's journals as of the last
|
||||
/// change to the shard, stored by the change while it still holds the
|
||||
/// lock ([`RibTable::change`]). A reader looks at it first, and passes
|
||||
/// over a shard with nothing new for it without stopping the shard's
|
||||
/// writers.
|
||||
latest: journal::ByKind<AtomicU64>,
|
||||
}
|
||||
|
||||
/// The routing table. Shared between threads by reference: nothing takes
|
||||
/// `&mut self`, and the readers ([`Reader`]) keep an `Arc` of their own.
|
||||
/// The routing table. Shared between threads by reference; every method
|
||||
/// takes `&self`.
|
||||
pub struct RibTable {
|
||||
shards: Box<[ShardSlot]>,
|
||||
shards: Box<[RwLock<Shard>]>,
|
||||
hasher: RandomState,
|
||||
attrs: AttrStore,
|
||||
/// Which peer numbers are in use. Changing the peer list also takes
|
||||
@@ -340,13 +281,6 @@ pub struct RibTable {
|
||||
/// Goes up whenever something is written to a journal that has
|
||||
/// readers.
|
||||
version: AtomicU64,
|
||||
/// Counts the calls that change the table, so that a change can say
|
||||
/// when it was made relative to others (see the journal's `Entry`).
|
||||
clock: AtomicU64,
|
||||
/// The changes being made right now, by their time on the clock: a
|
||||
/// reader does not read past the earliest of them, which may not have
|
||||
/// reached every shard yet.
|
||||
in_progress: Mutex<BTreeSet<u64>>,
|
||||
wake: Notify,
|
||||
}
|
||||
|
||||
@@ -368,16 +302,15 @@ impl RibTable {
|
||||
let hasher = RandomState::default();
|
||||
Self {
|
||||
shards: (0..shards.max(1))
|
||||
.map(|_| ShardSlot {
|
||||
lock: RwLock::new(Shard {
|
||||
.map(|_| {
|
||||
RwLock::new(Shard {
|
||||
rows: HashTable::new(),
|
||||
routes: 0,
|
||||
peers: Vec::new(),
|
||||
hasher: hasher.clone(),
|
||||
scratch: select::Scratch::default(),
|
||||
journals: journal::Journals::new(limits),
|
||||
}),
|
||||
latest: journal::ByKind::default(),
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
hasher,
|
||||
@@ -386,8 +319,6 @@ impl RibTable {
|
||||
reader_slots: Mutex::default(),
|
||||
readers: AtomicUsize::new(0),
|
||||
version: AtomicU64::new(0),
|
||||
clock: AtomicU64::new(0),
|
||||
in_progress: Mutex::default(),
|
||||
wake: Notify::new(),
|
||||
}
|
||||
}
|
||||
@@ -397,13 +328,6 @@ impl RibTable {
|
||||
&self.attrs
|
||||
}
|
||||
|
||||
/// What is known of the peer with this number, if it is in use. Not to
|
||||
/// be called from a visitor that is given routes with a shard held.
|
||||
pub fn peer(&self, index: PeerIndex) -> Option<PeerInfo> {
|
||||
let peers = self.peers.lock().unwrap_or_else(PoisonError::into_inner);
|
||||
peers.get(index.as_usize()).copied().flatten()
|
||||
}
|
||||
|
||||
/// Makes room for a peer's routes and returns the number to announce
|
||||
/// them under.
|
||||
///
|
||||
@@ -443,105 +367,30 @@ impl RibTable {
|
||||
/// held while it does, so other peers can come and go meanwhile.
|
||||
pub fn remove_peer(&self, peer: PeerIndex) -> Outcome {
|
||||
let mut outcome = Outcome::default();
|
||||
self.change(self.shards.iter().map(|shard| (shard, ())), |shard, ()| {
|
||||
let mut news = false;
|
||||
for shard in &self.shards {
|
||||
let mut shard = write(shard);
|
||||
let before = shard.journals.mark();
|
||||
outcome += shard.remove_peer(peer);
|
||||
// From here on the shard takes no more routes from the peer.
|
||||
if let Some(slot) = shard.peers.get_mut(peer.as_usize()) {
|
||||
*slot = None;
|
||||
}
|
||||
});
|
||||
shard.tidy_journals();
|
||||
news |= shard.journals.has_news_since(before);
|
||||
}
|
||||
// Only now can the number be given out again: no shard knows it.
|
||||
let mut peers = self.peers.lock().unwrap_or_else(PoisonError::into_inner);
|
||||
if let Some(slot) = peers.get_mut(peer.as_usize()) {
|
||||
*slot = None;
|
||||
}
|
||||
drop(peers);
|
||||
if news {
|
||||
self.wake_readers();
|
||||
}
|
||||
outcome
|
||||
}
|
||||
|
||||
/// Gives a peer number that is in use to a new session of the same
|
||||
/// peer, whose identifier or address may have changed: what route
|
||||
/// selection knows of the peer is replaced. Returns whether the number
|
||||
/// was in use.
|
||||
///
|
||||
/// This is how a peer that restarts finds the routes kept from its last
|
||||
/// session (graceful restart): they are still under its number.
|
||||
pub fn rebind_peer(&self, peer: PeerIndex, info: PeerInfo) -> bool {
|
||||
let mut peers = self.peers.lock().unwrap_or_else(PoisonError::into_inner);
|
||||
match peers.get_mut(peer.as_usize()) {
|
||||
Some(slot @ Some(_)) => *slot = Some(info),
|
||||
_ => return false,
|
||||
}
|
||||
for shard in &self.shards {
|
||||
if let Some(slot) = write(shard).peers.get_mut(peer.as_usize()) {
|
||||
*slot = Some(info);
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Marks every route of `peer` for prefixes of the IP versions in
|
||||
/// `versions` stale, and returns how many there are. They stay in the
|
||||
/// table and are chosen as before; announcing one again makes it fresh,
|
||||
/// and [`RibTable::sweep_stale`] takes out those that are still stale.
|
||||
///
|
||||
/// For graceful restart (RFC 4724 §4.2): the routes of a peer that is
|
||||
/// restarting are kept while it does. Like [`RibTable::remove_peer`]
|
||||
/// this looks at every prefix in the table. It changes no route, so
|
||||
/// the readers are not told of anything.
|
||||
pub fn mark_stale(&self, peer: PeerIndex, versions: &[IpVersion]) -> usize {
|
||||
let mut marked = 0;
|
||||
for shard in &self.shards {
|
||||
let mut shard = write(shard);
|
||||
for row in shard.rows.iter_mut() {
|
||||
if !versions.contains(&row.prefix.address_family()) {
|
||||
continue;
|
||||
}
|
||||
for path in &mut row.paths {
|
||||
if path.peer() == peer {
|
||||
path.word |= STALE;
|
||||
marked += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
marked
|
||||
}
|
||||
|
||||
/// Takes out the routes of `peer` for prefixes of the IP versions in
|
||||
/// `versions` that are still stale; see [`RibTable::mark_stale`]. The
|
||||
/// peer keeps its number.
|
||||
///
|
||||
/// Safe to run while the peer announces routes: one that is announced
|
||||
/// again, before or during this, is fresh and stays.
|
||||
pub fn sweep_stale(&self, peer: PeerIndex, versions: &[IpVersion]) -> Outcome {
|
||||
let mut outcome = Outcome::default();
|
||||
self.change(self.shards.iter().map(|shard| (shard, ())), |shard, ()| {
|
||||
outcome += shard.remove_where(|row, path| {
|
||||
path.peer() == peer
|
||||
&& path.is_stale()
|
||||
&& versions.contains(&row.prefix.address_family())
|
||||
});
|
||||
});
|
||||
outcome
|
||||
}
|
||||
|
||||
/// How many routes of `peer` are stale; see [`RibTable::mark_stale`].
|
||||
/// Looks at every prefix in the table.
|
||||
#[cfg(test)]
|
||||
fn count_stale(&self, peer: PeerIndex) -> usize {
|
||||
self.shards
|
||||
.iter()
|
||||
.map(|shard| {
|
||||
read(shard)
|
||||
.rows
|
||||
.iter()
|
||||
.flat_map(|row| &row.paths)
|
||||
.filter(|path| path.peer() == peer && path.is_stale())
|
||||
.count()
|
||||
})
|
||||
.sum()
|
||||
}
|
||||
|
||||
/// Announces `routes` from `peer`, all with the attributes `attrs`:
|
||||
/// what one UPDATE does. A route is known by its prefix and path
|
||||
/// identifier together ([`Nlri`]; a plain [`IpPrefix`] is the route
|
||||
@@ -554,11 +403,11 @@ impl RibTable {
|
||||
{
|
||||
let mut outcome = Outcome::default();
|
||||
self.by_shard(routes, |shard, placed| {
|
||||
let Some(_) = shard.peers.get(peer.as_usize()).copied().flatten() else {
|
||||
let Some(info) = shard.peers.get(peer.as_usize()).copied().flatten() else {
|
||||
// The peer was removed while this was on its way.
|
||||
return;
|
||||
};
|
||||
let preference = select::preference(attrs.key());
|
||||
let preference = select::preference(attrs.key(), &info);
|
||||
for (hash, nlri) in placed {
|
||||
outcome += shard.announce(*hash, *nlri, peer, preference, attrs);
|
||||
}
|
||||
@@ -588,7 +437,7 @@ impl RibTable {
|
||||
/// Calls `apply` once for each shard that some of `routes` fall in,
|
||||
/// with that shard locked and those routes. Wakes the readers if a
|
||||
/// journal one of them follows was written to.
|
||||
fn by_shard<N>(&self, routes: &[N], apply: impl FnMut(&mut Shard, &[(u64, Nlri)]))
|
||||
fn by_shard<N>(&self, routes: &[N], mut apply: impl FnMut(&mut Shard, &[(u64, Nlri)]))
|
||||
where
|
||||
N: Copy + Into<Nlri>,
|
||||
{
|
||||
@@ -614,81 +463,19 @@ impl RibTable {
|
||||
};
|
||||
placed.sort_unstable_by_key(|(hash, _)| self.shard_of(*hash));
|
||||
|
||||
let runs = placed
|
||||
.chunk_by(|a, b| self.shard_of(a.0) == self.shard_of(b.0))
|
||||
.map(|run| (&self.shards[self.shard_of(run[0].0)], run));
|
||||
self.change(runs, apply);
|
||||
}
|
||||
|
||||
/// Makes one change to the table, part by part: `apply` is called with
|
||||
/// each shard that `parts` names locked for writing, and the part's
|
||||
/// own item. All the parts are one change, at one time on the clock.
|
||||
///
|
||||
/// Readers are woken once the change is complete in every shard, if a
|
||||
/// journal one of them follows was written to.
|
||||
fn change<'a, T>(
|
||||
&'a self,
|
||||
parts: impl IntoIterator<Item = (&'a ShardSlot, T)>,
|
||||
mut apply: impl FnMut(&mut Shard, T),
|
||||
) {
|
||||
let change = self.begin_change();
|
||||
let mut news = false;
|
||||
for (slot, part) in parts {
|
||||
let mut shard = write(slot);
|
||||
shard.journals.set_stamp(change.stamp);
|
||||
for run in placed.chunk_by(|a, b| self.shard_of(a.0) == self.shard_of(b.0)) {
|
||||
let mut shard = write(&self.shards[self.shard_of(run[0].0)]);
|
||||
let before = shard.journals.mark();
|
||||
apply(&mut shard, part);
|
||||
apply(&mut shard, run);
|
||||
shard.tidy_journals();
|
||||
news |= shard.journals.has_news_since(before);
|
||||
// Once per change, not once per route; and before the lock is
|
||||
// let go, so that a reader that sees the new number and takes
|
||||
// the lock finds the entries behind it.
|
||||
shard.journals.publish(&slot.latest);
|
||||
}
|
||||
drop(change);
|
||||
if news {
|
||||
self.wake_readers();
|
||||
}
|
||||
}
|
||||
|
||||
/// Takes a time on the clock for a change that is about to be made,
|
||||
/// and notes that it is under way until the guard drops.
|
||||
fn begin_change(&self) -> Change<'_> {
|
||||
let version = self.version.load(std::sync::atomic::Ordering::Acquire);
|
||||
let stamp = self.clock.fetch_add(1, std::sync::atomic::Ordering::AcqRel) + 1;
|
||||
self.in_progress
|
||||
.lock()
|
||||
.unwrap_or_else(PoisonError::into_inner)
|
||||
.insert(stamp);
|
||||
Change {
|
||||
table: self,
|
||||
stamp,
|
||||
version,
|
||||
}
|
||||
}
|
||||
|
||||
/// The sequence number of shard `shard`'s journal of `kind` as of the
|
||||
/// last change to the shard, without the shard's lock.
|
||||
fn latest_seq(&self, shard: usize, kind: journal::Kind) -> u64 {
|
||||
self.shards[shard]
|
||||
.latest
|
||||
.of(kind)
|
||||
.load(std::sync::atomic::Ordering::Acquire)
|
||||
}
|
||||
|
||||
/// The time on the clock up to which every change is complete in every
|
||||
/// shard: everything before the earliest one still under way.
|
||||
fn settled(&self) -> u64 {
|
||||
let in_progress = self
|
||||
.in_progress
|
||||
.lock()
|
||||
.unwrap_or_else(PoisonError::into_inner);
|
||||
match in_progress.first() {
|
||||
Some(earliest) => earliest - 1,
|
||||
None => self.clock.load(std::sync::atomic::Ordering::Acquire),
|
||||
}
|
||||
}
|
||||
|
||||
/// How many routes the table holds, from all peers.
|
||||
pub fn len(&self) -> usize {
|
||||
self.shards.iter().map(|shard| read(shard).routes).sum()
|
||||
@@ -698,31 +485,17 @@ impl RibTable {
|
||||
self.len() == 0
|
||||
}
|
||||
|
||||
/// Makes room for `prefixes` prefixes, spread over the shards, so that
|
||||
/// a table of that size goes in without the shards growing as it
|
||||
/// does. Growing a shard moves every row it has into a bigger table;
|
||||
/// over a full table that was a seventh of what a session spent
|
||||
/// taking it in. Room that is already there is kept.
|
||||
pub fn expect_prefixes(&self, prefixes: usize) {
|
||||
let each = prefixes.div_ceil(self.shards.len());
|
||||
for shard in &self.shards {
|
||||
let mut shard = write(shard);
|
||||
let hasher = shard.hasher.clone();
|
||||
let additional = each.saturating_sub(shard.rows.len());
|
||||
shard
|
||||
.rows
|
||||
.reserve(additional, |row| hasher.hash_one(row.prefix));
|
||||
}
|
||||
}
|
||||
|
||||
/// How many prefixes have at least one route.
|
||||
pub fn prefix_count(&self) -> usize {
|
||||
self.shards.iter().map(|shard| read(shard).rows.len()).sum()
|
||||
}
|
||||
|
||||
/// The best route for `prefix`: who it is from, and its attributes.
|
||||
pub fn best(&self, prefix: &IpPrefix) -> Option<Route> {
|
||||
self.with_row(prefix, |row| row.route(row.best()).to_owned())
|
||||
pub fn best(&self, prefix: &IpPrefix) -> Option<(PeerIndex, Arc<AttrSet>)> {
|
||||
self.with_row(prefix, |row| {
|
||||
let best = &row.paths[row.best()];
|
||||
(best.peer(), Arc::clone(&best.attrs))
|
||||
})
|
||||
}
|
||||
|
||||
/// Every route for `prefix`.
|
||||
@@ -731,23 +504,6 @@ impl RibTable {
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Every route for `prefix`, from the best down: after the best, the
|
||||
/// route that would be chosen were the ones before it gone, and so on.
|
||||
pub fn ranked_routes(&self, prefix: &IpPrefix) -> Vec<Route> {
|
||||
let hash = self.hasher.hash_one(prefix);
|
||||
let shard = read(&self.shards[self.shard_of(hash)]);
|
||||
shard
|
||||
.rows
|
||||
.find(hash, |row| row.prefix == *prefix)
|
||||
.map(|row| {
|
||||
select::ranked(row, &shard.peers)
|
||||
.into_iter()
|
||||
.map(|index| row.route(index).to_owned())
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn with_row<T>(&self, prefix: &IpPrefix, read_row: impl FnOnce(&Row) -> T) -> Option<T> {
|
||||
let hash = self.hasher.hash_one(prefix);
|
||||
let shard = read(&self.shards[self.shard_of(hash)]);
|
||||
@@ -773,16 +529,14 @@ impl RibTable {
|
||||
}
|
||||
}
|
||||
|
||||
/// Up to `limit` of the routes that `wanted` picks, in no particular
|
||||
/// order. This looks at routes until it has enough, which for a choosy
|
||||
/// `wanted` is every route in the table: it is for looking into the
|
||||
/// table by hand, not for anything done often. `wanted` is called with
|
||||
/// a shard held and must not call into the table.
|
||||
pub fn routes_where(&self, limit: usize, wanted: impl Fn(&RouteRef<'_>) -> bool) -> Vec<Route> {
|
||||
/// Up to `limit` routes: every route of as many prefixes as fit, in no
|
||||
/// particular order. For showing some of a table that is too large to
|
||||
/// show all of.
|
||||
pub fn some_routes(&self, limit: usize) -> Vec<Route> {
|
||||
let mut routes = Vec::new();
|
||||
for shard in &self.shards {
|
||||
for row in &read(shard).rows {
|
||||
for route in row.routes().filter(&wanted) {
|
||||
for route in row.routes() {
|
||||
if routes.len() == limit {
|
||||
return routes;
|
||||
}
|
||||
@@ -793,21 +547,6 @@ impl RibTable {
|
||||
routes
|
||||
}
|
||||
|
||||
/// Calls `visit` with every prefix that has routes, in no particular
|
||||
/// order. As [`RibTable::for_each_best`]: the table is not stopped,
|
||||
/// and `visit` must not call into it.
|
||||
///
|
||||
/// The prefix's routes are there to be looked at, and cost nothing if
|
||||
/// they are not: this is how to pick a few prefixes out of a table
|
||||
/// with a great many routes for each.
|
||||
pub fn for_each_prefix(&self, mut visit: impl FnMut(PrefixRef<'_>)) {
|
||||
for shard in &self.shards {
|
||||
for row in &read(shard).rows {
|
||||
visit(PrefixRef { row });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Calls `visit` with every route in the table. As
|
||||
/// [`RibTable::for_each_best`]: the table is not stopped, and `visit`
|
||||
/// must not call into it.
|
||||
@@ -820,44 +559,14 @@ impl RibTable {
|
||||
}
|
||||
}
|
||||
|
||||
fn read(shard: &ShardSlot) -> RwLockReadGuard<'_, Shard> {
|
||||
fn read(shard: &RwLock<Shard>) -> RwLockReadGuard<'_, Shard> {
|
||||
// A panic under the lock cannot leave a row half changed in a way that
|
||||
// is unsafe to read; the poison is ignored, as for the server's tables.
|
||||
shard.lock.read().unwrap_or_else(PoisonError::into_inner)
|
||||
shard.read().unwrap_or_else(PoisonError::into_inner)
|
||||
}
|
||||
|
||||
fn write(shard: &ShardSlot) -> RwLockWriteGuard<'_, Shard> {
|
||||
shard.lock.write().unwrap_or_else(PoisonError::into_inner)
|
||||
}
|
||||
|
||||
/// A change to the table that is under way; see [`RibTable::begin_change`].
|
||||
struct Change<'a> {
|
||||
table: &'a RibTable,
|
||||
stamp: u64,
|
||||
/// The readers' version when it began, to tell whether they were
|
||||
/// woken while it was under way.
|
||||
version: u64,
|
||||
}
|
||||
|
||||
impl Drop for Change<'_> {
|
||||
fn drop(&mut self) {
|
||||
self.table
|
||||
.in_progress
|
||||
.lock()
|
||||
.unwrap_or_else(PoisonError::into_inner)
|
||||
.remove(&self.stamp);
|
||||
// A reader does not read past a change still under way. One that
|
||||
// another change woke meanwhile found nothing it could read yet,
|
||||
// and is woken again now that it can.
|
||||
if self
|
||||
.table
|
||||
.version
|
||||
.load(std::sync::atomic::Ordering::Acquire)
|
||||
!= self.version
|
||||
{
|
||||
self.table.wake_readers();
|
||||
}
|
||||
}
|
||||
fn write(shard: &RwLock<Shard>) -> RwLockWriteGuard<'_, Shard> {
|
||||
shard.write().unwrap_or_else(PoisonError::into_inner)
|
||||
}
|
||||
|
||||
/// Picks one of `count` buckets by a hash, for spreading keys over shards
|
||||
@@ -865,45 +574,20 @@ impl Drop for Change<'_> {
|
||||
///
|
||||
/// That table takes a key's slot from the low bits of the hash and, from
|
||||
/// the top seven, the tag it compares before it looks at the key itself
|
||||
/// (hashbrown). Taken from either end, the keys of one bucket would all
|
||||
/// share those bits, and the table inside would lose them. Nor is it
|
||||
/// enough to take the bits in between: the hash is one multiplication,
|
||||
/// which for keys that are numbers in a row (prefixes are) can leave
|
||||
/// neighbouring bits alike. So the hash is stirred once more, and the
|
||||
/// bucket taken from the top of that, which every bit of the hash has a
|
||||
/// part in.
|
||||
/// (hashbrown). The bucket is taken from bits in between: taken from
|
||||
/// either end, the keys of one bucket would all share those bits, and the
|
||||
/// table inside would lose them.
|
||||
fn spread(hash: u64, count: usize) -> usize {
|
||||
// 2^64 divided by the golden ratio: the multiplier of Fibonacci
|
||||
// hashing.
|
||||
const STIR: u64 = 0x9E37_79B9_7F4A_7C15;
|
||||
let stirred = hash.wrapping_mul(STIR) >> 32;
|
||||
((stirred * count as u64) >> 32) as usize
|
||||
let middle = (hash << 7) >> 32;
|
||||
((middle * count as u64) >> 32) as usize
|
||||
}
|
||||
|
||||
/// The most room a row is given at its first growth, however many peers
|
||||
/// there are: a route server's clients do not all announce every prefix.
|
||||
const FIRST_ROOM: usize = 8;
|
||||
|
||||
/// Adds to a vector without the doubling that `push` does: a row of a few
|
||||
/// hundred routes should not hold room for as many again. The first
|
||||
/// growth is to `expected` (at most [`FIRST_ROOM`]), since with several
|
||||
/// full tables nearly every row gets that many routes, and growing a row
|
||||
/// one route at a time was a twelfth of what taking in four tables cost.
|
||||
/// After that it grows by a quarter, which keeps adding cheap on average.
|
||||
fn push_tight<T>(vec: &mut Vec<T>, value: T, expected: usize) {
|
||||
/// hundred routes should not hold room for as many again. Grows by a
|
||||
/// quarter, which keeps adding cheap on average.
|
||||
fn push_tight<T>(vec: &mut Vec<T>, value: T) {
|
||||
if vec.len() == vec.capacity() {
|
||||
let first = expected.min(FIRST_ROOM);
|
||||
let more = if vec.capacity() < first {
|
||||
first - vec.len()
|
||||
} else if vec.capacity() < FIRST_ROOM {
|
||||
// A peer that sends several routes for a prefix (ADD-PATH)
|
||||
// takes a row past the peers' count: doubled while small,
|
||||
// and from there by a quarter.
|
||||
vec.capacity()
|
||||
} else {
|
||||
(vec.len() / 4).max(1)
|
||||
};
|
||||
vec.reserve_exact(more);
|
||||
vec.reserve_exact((vec.len() / 4).max(1));
|
||||
}
|
||||
vec.push(value);
|
||||
}
|
||||
@@ -945,7 +629,7 @@ impl Shard {
|
||||
|
||||
let (outcome, best_changed) = match row.position(peer, path_id) {
|
||||
None => {
|
||||
row.push(path, path_id, self.peers.len());
|
||||
row.push(path, path_id);
|
||||
self.routes += 1;
|
||||
let outcome = Outcome {
|
||||
added: 1,
|
||||
@@ -960,7 +644,6 @@ impl Shard {
|
||||
let before = std::mem::replace(&mut row.paths[at], path);
|
||||
let outcome = Outcome {
|
||||
replaced: 1,
|
||||
stale_cleared: usize::from(before.is_stale()),
|
||||
..Outcome::default()
|
||||
};
|
||||
if Arc::ptr_eq(&before.attrs, attrs) && before.preference() == preference {
|
||||
@@ -994,14 +677,12 @@ impl Shard {
|
||||
return Outcome::default();
|
||||
};
|
||||
self.routes -= 1;
|
||||
let stale_cleared = usize::from(row.paths[at].is_stale());
|
||||
if row.paths.len() == 1 {
|
||||
entry.remove();
|
||||
self.journals.disappeared(prefix);
|
||||
return Outcome {
|
||||
removed: 1,
|
||||
best_changed: 1,
|
||||
stale_cleared,
|
||||
..Outcome::default()
|
||||
};
|
||||
}
|
||||
@@ -1010,17 +691,11 @@ impl Shard {
|
||||
Outcome {
|
||||
removed: 1,
|
||||
best_changed: best_changed.into(),
|
||||
stale_cleared,
|
||||
..Outcome::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_peer(&mut self, peer: PeerIndex) -> Outcome {
|
||||
self.remove_where(|_, path| path.peer() == peer)
|
||||
}
|
||||
|
||||
/// Takes out every route that `doomed` picks, given its row.
|
||||
fn remove_where(&mut self, doomed: impl Fn(&Row, &Path) -> bool) -> Outcome {
|
||||
let mut outcome = Outcome::default();
|
||||
let peers = &self.peers;
|
||||
let scratch = &mut self.scratch;
|
||||
@@ -1030,7 +705,7 @@ impl Shard {
|
||||
let mut best_changed = false;
|
||||
// A peer that sends several routes for a prefix has several
|
||||
// here.
|
||||
while let Some(at) = row.paths.iter().position(|path| doomed(row, path)) {
|
||||
while let Some(at) = row.position_of_peer(peer) {
|
||||
removed += 1;
|
||||
if row.paths.len() == 1 {
|
||||
outcome.removed += removed;
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
//! of all its routes.
|
||||
|
||||
use super::attrs::SelectionKey;
|
||||
use super::{DEFAULT_LOCAL_PREF, Path, PeerInfo, Row};
|
||||
use super::{DEFAULT_LOCAL_PREF, PeerInfo, Row};
|
||||
|
||||
/// Steps 1 to 3 as one number of 48 bits: the larger, the better.
|
||||
///
|
||||
@@ -35,12 +35,14 @@ use super::{DEFAULT_LOCAL_PREF, Path, PeerInfo, Row};
|
||||
/// 12 bits and the ORIGIN in 2, each turned upside down so that shorter and
|
||||
/// lower compare as larger. Twelve bits are enough for any path: a BGP
|
||||
/// message of 4096 octets has room for about a thousand AS numbers.
|
||||
pub(super) fn preference(key: &SelectionKey) -> u64 {
|
||||
// LOCAL_PREF is a matter between routers of one AS. What an external
|
||||
// peer sent as one never gets as far as the table (the decoder drops
|
||||
// it, RFC 7606 §7.5); a route from such a peer that has one was given
|
||||
// it by our own import policy, which is who is to say.
|
||||
let local_pref = key.local_pref.unwrap_or(DEFAULT_LOCAL_PREF);
|
||||
pub(super) fn preference(key: &SelectionKey, peer: &PeerInfo) -> u64 {
|
||||
// LOCAL_PREF is a matter between routers of one AS. A route from an
|
||||
// external peer has the default until import policy gives it another.
|
||||
let local_pref = if peer.ibgp {
|
||||
key.local_pref.unwrap_or(DEFAULT_LOCAL_PREF)
|
||||
} else {
|
||||
DEFAULT_LOCAL_PREF
|
||||
};
|
||||
const LONGEST: u16 = (1 << 12) - 1;
|
||||
(u64::from(local_pref) << 16)
|
||||
| (u64::from(LONGEST - key.as_path_len.min(LONGEST)) << 4)
|
||||
@@ -187,40 +189,6 @@ fn choose(row: &mut Row, peers: &[Option<PeerInfo>], scratch: &mut Scratch) -> b
|
||||
row.best != before
|
||||
}
|
||||
|
||||
/// The routes of `row` from the best down, as indices into its routes:
|
||||
/// after the best, the one that would be chosen were the best gone, and
|
||||
/// so on. What an operator asks when looking at a prefix: which route
|
||||
/// would take over.
|
||||
pub(super) fn ranked(row: &Row, peers: &[Option<PeerInfo>]) -> Vec<usize> {
|
||||
// A copy to take the winners out of one by one, and where each of its
|
||||
// routes is in the row itself.
|
||||
let mut rest = Row {
|
||||
prefix: row.prefix,
|
||||
best: 0,
|
||||
seqs: row.seqs,
|
||||
paths: row
|
||||
.paths
|
||||
.iter()
|
||||
.map(|path| Path {
|
||||
word: path.word,
|
||||
attrs: std::sync::Arc::clone(&path.attrs),
|
||||
})
|
||||
.collect(),
|
||||
path_ids: row.path_ids.clone(),
|
||||
};
|
||||
let mut places: Vec<usize> = (0..row.paths.len()).collect();
|
||||
let mut scratch = Scratch::default();
|
||||
let mut order = Vec::with_capacity(row.paths.len());
|
||||
while !rest.paths.is_empty() {
|
||||
choose(&mut rest, peers, &mut scratch);
|
||||
let at = rest.best();
|
||||
order.push(places[at]);
|
||||
rest.swap_remove(at);
|
||||
places.swap_remove(at);
|
||||
}
|
||||
order
|
||||
}
|
||||
|
||||
/// For a route just added at `added` that ties with the best in steps 1 to
|
||||
/// 3: whether it is the new best, if that can be said from the two routes
|
||||
/// alone. `None` if the whole row has to be looked at.
|
||||
|
||||
+18
-291
@@ -61,9 +61,7 @@ pub(super) fn external(number: u8) -> PeerInfo {
|
||||
asn: 65000 + u32::from(number),
|
||||
router_id: Ipv4Addr::new(10, 0, 0, number),
|
||||
address: IpAddr::from(Ipv4Addr::new(192, 0, 2, number)),
|
||||
interface: 0,
|
||||
ibgp: false,
|
||||
reflector_client: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +69,6 @@ pub(super) fn internal(number: u8) -> PeerInfo {
|
||||
PeerInfo {
|
||||
asn: 64999,
|
||||
ibgp: true,
|
||||
reflector_client: false,
|
||||
..external(number)
|
||||
}
|
||||
}
|
||||
@@ -162,7 +159,7 @@ impl Fixture {
|
||||
|
||||
/// Which peer (by order added) the best route for `prefix` is from.
|
||||
fn best(&self, prefix: &IpPrefix) -> Option<usize> {
|
||||
let peer = self.table.best(prefix)?.peer;
|
||||
let (peer, _) = self.table.best(prefix)?;
|
||||
self.peers.iter().position(|p| *p == peer)
|
||||
}
|
||||
}
|
||||
@@ -172,7 +169,7 @@ fn each_step_of_selection_decides_when_the_ones_before_tie() {
|
||||
let p = prefix("10.0.0.0/8");
|
||||
// Peer 0 has the lower identifier, so it wins whenever nothing else
|
||||
// decides; each case makes peer 1's route better in one step.
|
||||
let cases: [(&str, [PeerInfo; 2], Route, Route); 7] = [
|
||||
let cases: [(&str, [PeerInfo; 2], Route, Route); 6] = [
|
||||
(
|
||||
"higher LOCAL_PREF, despite a longer path",
|
||||
[internal(1), internal(2)],
|
||||
@@ -185,18 +182,6 @@ fn each_step_of_selection_decides_when_the_ones_before_tie() {
|
||||
..Route::via(&[1, 2, 3])
|
||||
},
|
||||
),
|
||||
(
|
||||
"higher LOCAL_PREF from an external peer too, where policy set one",
|
||||
[external(1), external(2)],
|
||||
Route {
|
||||
local_pref: Some(100),
|
||||
..Route::via(&[1])
|
||||
},
|
||||
Route {
|
||||
local_pref: Some(200),
|
||||
..Route::via(&[1, 2, 3])
|
||||
},
|
||||
),
|
||||
(
|
||||
"shorter AS path",
|
||||
[external(1), external(2)],
|
||||
@@ -261,62 +246,6 @@ fn each_step_of_selection_decides_when_the_ones_before_tie() {
|
||||
assert_eq!(fixture.best(&p), Some(1));
|
||||
}
|
||||
|
||||
/// The ranking of a prefix's routes is selection run again without the
|
||||
/// winners: each route in it is what would be chosen were the ones before
|
||||
/// it gone.
|
||||
#[test]
|
||||
fn routes_are_ranked_as_they_would_be_chosen_one_after_another() {
|
||||
let p = prefix("10.0.0.0/8");
|
||||
// Peer 0 would win on identifier alone; peer 2 has the highest
|
||||
// LOCAL_PREF, peer 1 the shortest path of the rest, peer 3 a MED
|
||||
// that loses to peer 1's within their shared neighbouring AS.
|
||||
let peers = [external(1), external(2), internal(3), external(4)];
|
||||
let fixture = Fixture::new(&peers);
|
||||
fixture.announce(0, &p, &Route::via(&[7, 1, 2]));
|
||||
fixture.announce(1, &p, &Route::via(&[7, 3]));
|
||||
fixture.announce(
|
||||
2,
|
||||
&p,
|
||||
&Route {
|
||||
local_pref: Some(200),
|
||||
..Route::via(&[8, 1, 2, 3])
|
||||
},
|
||||
);
|
||||
fixture.announce(
|
||||
3,
|
||||
&p,
|
||||
&Route {
|
||||
med: Some(5),
|
||||
..Route::via(&[7, 4])
|
||||
},
|
||||
);
|
||||
let ranked: Vec<usize> = fixture
|
||||
.table
|
||||
.ranked_routes(&p)
|
||||
.iter()
|
||||
.map(|route| {
|
||||
fixture
|
||||
.peers
|
||||
.iter()
|
||||
.position(|peer| *peer == route.peer)
|
||||
.unwrap()
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(ranked, vec![2, 1, 3, 0]);
|
||||
assert_eq!(fixture.best(&p), Some(2));
|
||||
// Taking the best away makes the second the best, and so on down.
|
||||
fixture.withdraw(2, &p);
|
||||
assert_eq!(fixture.best(&p), Some(1));
|
||||
fixture.withdraw(1, &p);
|
||||
assert_eq!(fixture.best(&p), Some(3));
|
||||
assert!(
|
||||
fixture
|
||||
.table
|
||||
.ranked_routes(&prefix("10.1.0.0/16"))
|
||||
.is_empty()
|
||||
);
|
||||
}
|
||||
|
||||
/// MEDs are compared only within a neighbouring AS. Taken pair by pair
|
||||
/// that is not an order: A beats B, B beats C, C beats A. Whatever the
|
||||
/// order of arrival, the answer must be the same.
|
||||
@@ -503,129 +432,6 @@ fn removing_a_peer_takes_its_routes_and_frees_its_number() {
|
||||
assert_eq!(fixture.table.add_peer(external(3)), Ok(fixture.peers[0]));
|
||||
}
|
||||
|
||||
/// Graceful restart (RFC 4724 §4.2): a restarting peer's routes are kept,
|
||||
/// marked stale, and used as before; what it announces again is fresh, and
|
||||
/// what it does not is swept away at the end.
|
||||
#[test]
|
||||
fn stale_routes_are_chosen_as_before_and_swept_unless_announced_again() {
|
||||
let fixture = Fixture::new(&[external(1), external(2)]);
|
||||
let v4 = [
|
||||
prefix("10.1.0.0/16"),
|
||||
prefix("10.2.0.0/16"),
|
||||
prefix("10.3.0.0/16"),
|
||||
];
|
||||
let v6 = prefix("2001:db8::/32");
|
||||
for p in v4.iter().chain([&v6]) {
|
||||
fixture.announce(0, p, &Route::via(&[1]));
|
||||
}
|
||||
// Peer 1 has a worse route for the first prefix, and loses.
|
||||
fixture.announce(1, &v4[0], &Route::via(&[2, 3]));
|
||||
let mut reader = fixture.table.reader_from_now();
|
||||
|
||||
let peer = fixture.peers[0];
|
||||
let both = [IpVersion::Ipv4, IpVersion::Ipv6];
|
||||
assert_eq!(fixture.table.mark_stale(peer, &both), 4);
|
||||
assert_eq!(fixture.table.count_stale(peer), 4);
|
||||
// Nothing changed for anyone who follows the table, and the stale
|
||||
// route is still the best.
|
||||
let mut events = 0;
|
||||
reader.poll(usize::MAX, |_| events += 1);
|
||||
assert_eq!(events, 0);
|
||||
assert_eq!(fixture.best(&v4[0]), Some(0));
|
||||
assert!(fixture.table.routes(&v4[0]).iter().any(|r| r.stale));
|
||||
|
||||
// The peer comes back and announces two of them again, one with the
|
||||
// attributes it had: they are fresh.
|
||||
assert_eq!(
|
||||
fixture.announce(0, &v4[0], &Route::via(&[1])).stale_cleared,
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
fixture
|
||||
.announce(0, &v4[1], &Route::via(&[1, 4]))
|
||||
.stale_cleared,
|
||||
1
|
||||
);
|
||||
// Again, and it was not stale any more.
|
||||
assert_eq!(
|
||||
fixture
|
||||
.announce(0, &v4[1], &Route::via(&[1, 4]))
|
||||
.stale_cleared,
|
||||
0
|
||||
);
|
||||
assert_eq!(fixture.table.count_stale(peer), 2);
|
||||
|
||||
// Its End-of-RIB for IPv4: the IPv4 route it did not announce goes,
|
||||
// the IPv6 one waits for the IPv6 marker.
|
||||
let outcome = fixture.table.sweep_stale(peer, &[IpVersion::Ipv4]);
|
||||
assert_eq!((outcome.removed, outcome.best_changed), (1, 1));
|
||||
assert_eq!(fixture.best(&v4[2]), None);
|
||||
assert_eq!(fixture.best(&v4[0]), Some(0));
|
||||
assert_eq!(fixture.table.count_stale(peer), 1);
|
||||
let outcome = fixture.table.sweep_stale(peer, &[IpVersion::Ipv6]);
|
||||
assert_eq!(outcome.removed, 1);
|
||||
assert_eq!(fixture.table.count_stale(peer), 0);
|
||||
assert_eq!(fixture.table.len(), 3);
|
||||
// Sweeping again finds nothing.
|
||||
assert_eq!(fixture.table.sweep_stale(peer, &both), Outcome::default());
|
||||
}
|
||||
|
||||
/// Stale routes are swept from one peer only, and only for the versions
|
||||
/// asked: the other peer's routes, stale or not, stay.
|
||||
#[test]
|
||||
fn sweeping_takes_only_the_stale_routes_of_one_peer() {
|
||||
let fixture = Fixture::new(&[external(1), external(2)]);
|
||||
let p = prefix("10.0.0.0/8");
|
||||
fixture.announce(0, &p, &Route::via(&[1]));
|
||||
fixture.announce(1, &p, &Route::via(&[2, 3]));
|
||||
fixture
|
||||
.table
|
||||
.mark_stale(fixture.peers[1], &[IpVersion::Ipv4]);
|
||||
assert_eq!(
|
||||
fixture
|
||||
.table
|
||||
.sweep_stale(fixture.peers[0], &[IpVersion::Ipv4]),
|
||||
Outcome::default()
|
||||
);
|
||||
assert_eq!(
|
||||
fixture
|
||||
.table
|
||||
.sweep_stale(fixture.peers[1], &[IpVersion::Ipv6]),
|
||||
Outcome::default()
|
||||
);
|
||||
assert_eq!(fixture.held(&p), vec![(0, 0), (1, 0)]);
|
||||
let outcome = fixture
|
||||
.table
|
||||
.sweep_stale(fixture.peers[1], &[IpVersion::Ipv4]);
|
||||
assert_eq!((outcome.removed, outcome.best_changed), (1, 0));
|
||||
assert_eq!(fixture.held(&p), vec![(0, 0)]);
|
||||
}
|
||||
|
||||
/// A new session of a restarting peer takes over its number, with what
|
||||
/// selection knows of it brought up to date: here a new identifier, which
|
||||
/// is what breaks the tie.
|
||||
#[test]
|
||||
fn a_peer_number_can_be_given_to_the_next_session_of_the_peer() {
|
||||
let fixture = Fixture::new(&[external(1), external(2)]);
|
||||
let p = prefix("10.0.0.0/8");
|
||||
fixture.announce(0, &p, &Route::via(&[1]));
|
||||
fixture.announce(1, &p, &Route::via(&[2]));
|
||||
assert_eq!(fixture.best(&p), Some(0));
|
||||
let renumbered = PeerInfo {
|
||||
router_id: Ipv4Addr::new(10, 0, 0, 9),
|
||||
..external(1)
|
||||
};
|
||||
assert!(fixture.table.rebind_peer(fixture.peers[0], renumbered));
|
||||
assert_eq!(fixture.table.peer(fixture.peers[0]), Some(renumbered));
|
||||
// Chosen again the next time the row changes.
|
||||
fixture.announce(1, &p, &Route::via(&[5]));
|
||||
assert_eq!(fixture.best(&p), Some(1));
|
||||
|
||||
// A number nobody has cannot be taken over.
|
||||
fixture.table.remove_peer(fixture.peers[1]);
|
||||
assert!(!fixture.table.rebind_peer(fixture.peers[1], external(3)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn readers_see_every_route_and_which_is_best() {
|
||||
let fixture = Fixture::new(&[external(1), external(2)]);
|
||||
@@ -727,9 +533,13 @@ fn chosen_plainly(
|
||||
routes: &BTreeMap<(usize, u32), Route>,
|
||||
peers: &[PeerInfo],
|
||||
) -> Option<(usize, u32)> {
|
||||
// Whoever the route is from: an external peer's own LOCAL_PREF never
|
||||
// gets into a table, and one that is there was put there by policy.
|
||||
let local_pref = |_peer: usize, route: &Route| route.local_pref.unwrap_or(DEFAULT_LOCAL_PREF);
|
||||
let local_pref = |peer: usize, route: &Route| {
|
||||
if peers[peer].ibgp {
|
||||
route.local_pref.unwrap_or(DEFAULT_LOCAL_PREF)
|
||||
} else {
|
||||
DEFAULT_LOCAL_PREF
|
||||
}
|
||||
};
|
||||
let mut left: Vec<((usize, u32), &Route)> =
|
||||
routes.iter().map(|(k, route)| (*k, route)).collect();
|
||||
let most = left.iter().map(|(k, r)| local_pref(k.0, r)).max()?;
|
||||
@@ -888,35 +698,12 @@ proptest! {
|
||||
/// differ where that table looks.
|
||||
#[test]
|
||||
fn keys_of_one_shard_differ_in_the_bits_their_table_uses() {
|
||||
// With keys of the hash that are the same every time: a test that
|
||||
// fails one run in a thousand tells nobody anything.
|
||||
for seed in 0..32u64 {
|
||||
keys_of_one_shard_differ(foldhash::fast::FixedState::with_seed(seed));
|
||||
}
|
||||
// Always one of the buckets there are.
|
||||
for hash in [0, 1, u64::MAX, u64::MAX / 2, 1 << 63] {
|
||||
for count in [1, 3, 64, 1000] {
|
||||
assert!(spread(hash, count) < count, "{hash} of {count}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn keys_of_one_shard_differ(hasher: impl std::hash::BuildHasher) {
|
||||
let hasher = RandomState::default();
|
||||
let shards = 64;
|
||||
let mut counts = vec![0usize; shards];
|
||||
let mut tags = vec![std::collections::HashSet::new(); shards];
|
||||
// Keys as the table has them, and as regular as they come: every /24
|
||||
// of a /8.
|
||||
for n in 0..64_000u32 {
|
||||
let [_, a, b, c] = n.to_be_bytes();
|
||||
let key: Nlri = IpPrefix::new(
|
||||
bgp_primitives::constants::IpVersion::Ipv4,
|
||||
[100 + a, b, c, 0],
|
||||
24,
|
||||
)
|
||||
.unwrap()
|
||||
.into();
|
||||
let hash = hasher.hash_one(key);
|
||||
let hash = hasher.hash_one(n);
|
||||
let shard = spread(hash, shards);
|
||||
counts[shard] += 1;
|
||||
// The seven bits hashbrown keeps as a tag.
|
||||
@@ -928,14 +715,10 @@ fn keys_of_one_shard_differ(hasher: impl std::hash::BuildHasher) {
|
||||
"shard {shard} has {} of 64000 keys",
|
||||
counts[shard]
|
||||
);
|
||||
// Nearly all of the 128 there are; taken from the bits the shard
|
||||
// is picked by, there would be two.
|
||||
assert!(
|
||||
tags[shard].len() > 100,
|
||||
"shard {shard} has {} tags",
|
||||
tags[shard].len()
|
||||
);
|
||||
assert_eq!(tags[shard].len(), 128, "shard {shard}");
|
||||
}
|
||||
assert_eq!(spread(u64::MAX, 3), 2);
|
||||
assert_eq!(spread(0, 3), 0);
|
||||
}
|
||||
|
||||
/// A peer that sends several routes for a prefix can send more than fit a
|
||||
@@ -950,20 +733,21 @@ fn a_row_can_hold_more_routes_than_a_sixteen_bit_index_counts() {
|
||||
let info = external(1);
|
||||
let peers = [Some(info)];
|
||||
let peer = PeerIndex(0);
|
||||
let path = |attrs: &Arc<AttrSet>| Path::new(select::preference(attrs.key()), peer, attrs);
|
||||
let path =
|
||||
|attrs: &Arc<AttrSet>| Path::new(select::preference(attrs.key(), &info), peer, attrs);
|
||||
let seqs = journal::Journals::new(journal::Limits::default()).appeared(prefix("10.0.0.0/8"));
|
||||
|
||||
let mut row = Row::new(prefix("10.0.0.0/8"), path(&worse), 1, seqs);
|
||||
let mut scratch = select::Scratch::default();
|
||||
let count = usize::from(u16::MAX) + 10;
|
||||
for path_id in 2..=count as u32 {
|
||||
row.push(path(&worse), path_id, peers.len());
|
||||
row.push(path(&worse), path_id);
|
||||
}
|
||||
// Equal routes from one peer: the lowest identifier is the best.
|
||||
assert!(!select::after_add(&mut row, &peers, &mut scratch));
|
||||
assert_eq!(row.best(), 0);
|
||||
|
||||
row.push(path(&better), count as u32 + 1, peers.len());
|
||||
row.push(path(&better), count as u32 + 1);
|
||||
assert!(select::after_add(&mut row, &peers, &mut scratch));
|
||||
assert_eq!(row.best(), count);
|
||||
assert_eq!(row.route(row.best()).path_id, count as u32 + 1);
|
||||
@@ -973,60 +757,3 @@ fn a_row_can_hold_more_routes_than_a_sixteen_bit_index_counts() {
|
||||
assert!(select::after_remove(&mut row, count, &peers, &mut scratch));
|
||||
assert_eq!(row.route(row.best()).path_id, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn routes_can_be_picked_out_of_the_table() {
|
||||
let fixture = Fixture::new(&[external(1), external(2)]);
|
||||
for n in 0..50u8 {
|
||||
let p = prefix(&format!("10.{n}.0.0/16"));
|
||||
fixture.announce(0, &p, &Route::via(&[1]));
|
||||
if n % 2 == 0 {
|
||||
fixture.announce(1, &p, &Route::via(&[2, 3]));
|
||||
}
|
||||
}
|
||||
let table = &fixture.table;
|
||||
let second = fixture.peers[1];
|
||||
assert_eq!(table.routes_where(1000, |_| true).len(), 75);
|
||||
assert_eq!(
|
||||
table.routes_where(1000, |route| route.peer == second).len(),
|
||||
25
|
||||
);
|
||||
assert_eq!(table.routes_where(1000, |route| route.best).len(), 50);
|
||||
let inside = prefix("10.16.0.0/12");
|
||||
let found = table.routes_where(1000, |route| inside.contains(&route.prefix));
|
||||
assert_eq!(found.len(), 16 + 8);
|
||||
// No more than asked for, however many there are.
|
||||
assert_eq!(
|
||||
table.routes_where(10, |route| route.peer == second).len(),
|
||||
10
|
||||
);
|
||||
assert!(table.routes_where(0, |_| true).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_table_told_what_to_expect_has_the_room_before_the_routes_come() {
|
||||
let table = RibTable::new(8);
|
||||
table.expect_prefixes(100_000);
|
||||
let room: usize = table
|
||||
.shards
|
||||
.iter()
|
||||
.map(|shard| read(shard).rows.capacity())
|
||||
.sum();
|
||||
assert!(room >= 100_000, "room for {room} prefixes");
|
||||
// Asking again for less, or for the same, takes nothing away.
|
||||
table.expect_prefixes(10);
|
||||
let still: usize = table
|
||||
.shards
|
||||
.iter()
|
||||
.map(|shard| read(shard).rows.capacity())
|
||||
.sum();
|
||||
assert_eq!(still, room);
|
||||
// And it is a table like any other.
|
||||
let peer = table.add_peer(external(1)).unwrap();
|
||||
let attrs = table
|
||||
.attrs()
|
||||
.intern(&Route::via(&[64512]).attributes(), None)
|
||||
.unwrap();
|
||||
table.announce(peer, &attrs, &[Nlri::from(prefix("192.0.2.0/24"))]);
|
||||
assert_eq!(table.prefix_count(), 1);
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
[package]
|
||||
name = "bgp-rpki"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
homepage.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
bgp-primitives.workspace = true
|
||||
bytes.workspace = true
|
||||
foldhash.workspace = true
|
||||
thiserror.workspace = true
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
tracing.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
proptest.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -1,426 +0,0 @@
|
||||
//! Autonomous System Provider Authorisations, and what they say about a
|
||||
//! route's AS path (draft-ietf-sidrops-aspa-verification).
|
||||
//!
|
||||
//! An ASPA is a signed statement by an AS of who its providers are: the
|
||||
//! networks it buys transit from. With those statements a router can tell
|
||||
//! a path that goes the way traffic is paid to go (up from the origin
|
||||
//! through providers, across at most one peering, and down through
|
||||
//! customers) from one that does not. A path that climbs again after it
|
||||
//! has come down, or crosses a second peering, is a route leak or a forged
|
||||
//! path, and this is the check that finds them.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// What the ASPAs say about a path.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum PathValidity {
|
||||
/// Some AS on the path has no ASPA, and nothing on the path is known
|
||||
/// to be wrong. Most paths are in this state while few networks have
|
||||
/// an ASPA, and are used.
|
||||
Unknown,
|
||||
/// Every hop that has to be from a customer to its provider is
|
||||
/// attested to be one.
|
||||
Valid,
|
||||
/// Some AS on the path has said who its providers are, and the path
|
||||
/// went from it to an AS that is not one of them where only a provider
|
||||
/// could come next.
|
||||
Invalid,
|
||||
}
|
||||
|
||||
impl PathValidity {
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Unknown => "unknown",
|
||||
Self::Valid => "valid",
|
||||
Self::Invalid => "invalid",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Which of the two procedures verifies a path. That depends on what the
|
||||
/// neighbor it came from is to us.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum Algorithm {
|
||||
/// For a route from a customer, a peer, or across a route server: all
|
||||
/// of its path must lead upwards, from each AS to a provider of it
|
||||
/// (§6.1 of the draft).
|
||||
Upstream,
|
||||
/// For a route from a provider: its path may lead up from the origin
|
||||
/// and then down to us, and nothing else (§6.2).
|
||||
Downstream,
|
||||
}
|
||||
|
||||
/// Whether one AS is a provider of another, as far as the ASPAs say.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum Hop {
|
||||
/// The customer has no ASPA.
|
||||
NoAttestation,
|
||||
/// The customer's ASPA lists the other AS.
|
||||
Provider,
|
||||
/// The customer has an ASPA, and the other AS is not on it.
|
||||
NotProvider,
|
||||
}
|
||||
|
||||
/// The ASPAs there are: for each customer AS that has one, its providers.
|
||||
///
|
||||
/// Cloning copies the list of customers and shares the lists of providers.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct AspaTable {
|
||||
providers: HashMap<u32, Arc<[u32]>, foldhash::fast::FixedState>,
|
||||
}
|
||||
|
||||
impl AspaTable {
|
||||
/// How many customer ASes have an ASPA.
|
||||
pub fn len(&self) -> usize {
|
||||
self.providers.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.providers.is_empty()
|
||||
}
|
||||
|
||||
/// Records that `customer`'s providers are `providers`, in place of
|
||||
/// what was recorded for it before, which is returned.
|
||||
pub fn set(
|
||||
&mut self,
|
||||
customer: u32,
|
||||
providers: impl IntoIterator<Item = u32>,
|
||||
) -> Option<Arc<[u32]>> {
|
||||
let mut providers: Vec<u32> = providers.into_iter().collect();
|
||||
providers.sort_unstable();
|
||||
providers.dedup();
|
||||
self.providers.insert(customer, providers.into())
|
||||
}
|
||||
|
||||
/// Forgets `customer`'s ASPA, and returns the providers it listed.
|
||||
pub fn remove(&mut self, customer: u32) -> Option<Arc<[u32]>> {
|
||||
self.providers.remove(&customer)
|
||||
}
|
||||
|
||||
/// Puts back what [`AspaTable::set`] or [`AspaTable::remove`] returned.
|
||||
pub(crate) fn restore(&mut self, customer: u32, providers: Option<Arc<[u32]>>) {
|
||||
match providers {
|
||||
Some(providers) => self.providers.insert(customer, providers),
|
||||
None => self.providers.remove(&customer),
|
||||
};
|
||||
}
|
||||
|
||||
/// The providers `customer` lists, in increasing order, if it has an
|
||||
/// ASPA.
|
||||
pub fn providers(&self, customer: u32) -> Option<&[u32]> {
|
||||
self.providers.get(&customer).map(|providers| &**providers)
|
||||
}
|
||||
|
||||
/// The customers and their providers, in no particular order.
|
||||
pub fn iter(&self) -> impl Iterator<Item = (u32, &[u32])> {
|
||||
self.providers
|
||||
.iter()
|
||||
.map(|(customer, providers)| (*customer, &**providers))
|
||||
}
|
||||
|
||||
fn hop(&self, customer: u32, provider: u32) -> Hop {
|
||||
match self.providers.get(&customer) {
|
||||
None => Hop::NoAttestation,
|
||||
Some(providers) if providers.binary_search(&provider).is_ok() => Hop::Provider,
|
||||
Some(_) => Hop::NotProvider,
|
||||
}
|
||||
}
|
||||
|
||||
/// Verifies `path`: the AS numbers of an AS_PATH in the order they are
|
||||
/// in it, the neighbor's first and the origin's last, with whatever
|
||||
/// repeats it has.
|
||||
///
|
||||
/// This is the part of the draft's procedures that the ASPAs decide.
|
||||
/// What comes before it there is the caller's to check: that the path
|
||||
/// has no AS_SET and begins with the neighbor's AS. An empty path is
|
||||
/// invalid.
|
||||
pub fn verify(&self, path: &[u32], algorithm: Algorithm) -> PathValidity {
|
||||
// The draft numbers the path's distinct ASes from the origin,
|
||||
// A(1), to the neighbor, A(N). Here they are `hops[0]` to
|
||||
// `hops[n - 1]`.
|
||||
let mut hops: Vec<u32> = Vec::with_capacity(path.len());
|
||||
for asn in path.iter().rev() {
|
||||
if hops.last() != Some(asn) {
|
||||
hops.push(*asn);
|
||||
}
|
||||
}
|
||||
let n = hops.len();
|
||||
if n == 0 {
|
||||
return PathValidity::Invalid;
|
||||
}
|
||||
|
||||
// How far up from the origin the path can be a chain of customers
|
||||
// and their providers at most (as far as nothing says otherwise),
|
||||
// and at least (as far as it is attested).
|
||||
let (mut max_up, mut min_up) = (n, n);
|
||||
for (at, pair) in hops.windows(2).enumerate() {
|
||||
match self.hop(pair[0], pair[1]) {
|
||||
Hop::Provider => continue,
|
||||
Hop::NoAttestation => min_up = min_up.min(at + 1),
|
||||
Hop::NotProvider => {
|
||||
min_up = min_up.min(at + 1);
|
||||
max_up = at + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if algorithm == Algorithm::Upstream {
|
||||
return match (max_up < n, min_up < n) {
|
||||
(true, _) => PathValidity::Invalid,
|
||||
(false, true) => PathValidity::Unknown,
|
||||
(false, false) => PathValidity::Valid,
|
||||
};
|
||||
}
|
||||
|
||||
// The same from the neighbor's end, looking down: each AS is to be
|
||||
// the customer of the one after it.
|
||||
let (mut max_down, mut min_down) = (n, n);
|
||||
for (at, pair) in hops.windows(2).rev().enumerate() {
|
||||
match self.hop(pair[1], pair[0]) {
|
||||
Hop::Provider => continue,
|
||||
Hop::NoAttestation => min_down = min_down.min(at + 1),
|
||||
Hop::NotProvider => {
|
||||
min_down = min_down.min(at + 1);
|
||||
max_down = at + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// The way up and the way down have to meet, or overlap.
|
||||
if max_up + max_down < n {
|
||||
PathValidity::Invalid
|
||||
} else if min_up + min_down < n {
|
||||
PathValidity::Unknown
|
||||
} else {
|
||||
PathValidity::Valid
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromIterator<(u32, Vec<u32>)> for AspaTable {
|
||||
fn from_iter<T: IntoIterator<Item = (u32, Vec<u32>)>>(aspas: T) -> Self {
|
||||
let mut table = Self::default();
|
||||
for (customer, providers) in aspas {
|
||||
table.set(customer, providers);
|
||||
}
|
||||
table
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use proptest::prelude::*;
|
||||
|
||||
use super::Algorithm::{Downstream, Upstream};
|
||||
use super::PathValidity::{Invalid, Unknown, Valid};
|
||||
use super::*;
|
||||
|
||||
fn table(aspas: &[(u32, &[u32])]) -> AspaTable {
|
||||
aspas
|
||||
.iter()
|
||||
.map(|(customer, providers)| (*customer, providers.to_vec()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// A small Internet. 10 and 20 are the large networks, which peer and
|
||||
/// have no providers (an ASPA that lists AS 0). 100 buys from 10, 200
|
||||
/// from 20, 300 from both. 1000 buys from 100; 2000 from 200 and has
|
||||
/// no ASPA.
|
||||
fn internet() -> AspaTable {
|
||||
table(&[
|
||||
(10, &[0]),
|
||||
(20, &[0]),
|
||||
(100, &[10]),
|
||||
(200, &[20]),
|
||||
(300, &[10, 20]),
|
||||
(1000, &[100]),
|
||||
])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_path_that_only_climbs_is_valid_from_a_customer_or_peer() {
|
||||
let aspas = internet();
|
||||
// 1000 -> 100 -> 10, as 10 hears it from its customer 100.
|
||||
assert_eq!(aspas.verify(&[100, 1000], Upstream), Valid);
|
||||
// As 20 hears it from its peer 10.
|
||||
assert_eq!(aspas.verify(&[10, 100, 1000], Upstream), Valid);
|
||||
// A route the neighbor originates is as far up as it has got.
|
||||
assert_eq!(aspas.verify(&[1000], Upstream), Valid);
|
||||
// Prepending changes nothing.
|
||||
assert_eq!(
|
||||
aspas.verify(&[10, 10, 100, 100, 100, 1000, 1000], Upstream),
|
||||
Valid
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_leak_is_invalid_from_a_customer_or_peer() {
|
||||
let aspas = internet();
|
||||
// 300 buys from 10 and 20, and passes 10's routes on to 20: the
|
||||
// classic leak. 20 hears the path 300 10 100 1000 from its
|
||||
// customer; 10 says it has no providers, so 300 is not one.
|
||||
assert_eq!(aspas.verify(&[300, 10, 100, 1000], Upstream), Invalid);
|
||||
// 20 passes a route of its peer 10's on to another peer: 20 is not
|
||||
// a provider of 10.
|
||||
assert_eq!(aspas.verify(&[20, 10, 100, 1000], Upstream), Invalid);
|
||||
// An origin that is not where the path says: 1000 lists only 100.
|
||||
assert_eq!(aspas.verify(&[200, 1000], Upstream), Invalid);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_hop_nobody_has_attested_leaves_the_path_unknown() {
|
||||
let aspas = internet();
|
||||
// 2000 has no ASPA.
|
||||
assert_eq!(aspas.verify(&[200, 2000], Upstream), Unknown);
|
||||
assert_eq!(aspas.verify(&[20, 200, 2000], Upstream), Unknown);
|
||||
// But what is known to be wrong is invalid whatever else is not
|
||||
// known.
|
||||
assert_eq!(aspas.verify(&[300, 20, 200, 2000], Upstream), Invalid);
|
||||
// With no ASPAs at all, nothing is known.
|
||||
assert_eq!(AspaTable::default().verify(&[1, 2, 3], Upstream), Unknown);
|
||||
assert_eq!(AspaTable::default().verify(&[1, 2, 3], Downstream), Unknown);
|
||||
// Except of the path that is no path.
|
||||
assert_eq!(AspaTable::default().verify(&[], Upstream), Invalid);
|
||||
assert_eq!(AspaTable::default().verify(&[], Downstream), Invalid);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_a_provider_a_path_may_climb_and_then_descend() {
|
||||
let aspas = internet();
|
||||
// 1000's route, as 200 hears it from its provider 20: up through
|
||||
// 100 to 10, across to 20.
|
||||
assert_eq!(aspas.verify(&[20, 10, 100, 1000], Downstream), Valid);
|
||||
// As 300 hears it from 10: only up.
|
||||
assert_eq!(aspas.verify(&[10, 100, 1000], Downstream), Valid);
|
||||
// As 1000 would hear 300's route from 100: up to 10, down to 100.
|
||||
assert_eq!(aspas.verify(&[100, 10, 300], Downstream), Valid);
|
||||
// Short paths have no room to be wrong in.
|
||||
assert_eq!(aspas.verify(&[20, 1000], Downstream), Valid);
|
||||
assert_eq!(aspas.verify(&[20], Downstream), Valid);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_a_provider_a_path_that_descends_and_climbs_again_is_invalid() {
|
||||
let aspas = internet();
|
||||
// 300 leaks 10's routes to 20, and 20 passes them down to 200,
|
||||
// which hears the path 20 300 10 100 1000. The way up from 1000
|
||||
// ends at 10, three ASes in: 300 is not a provider of 10. The way
|
||||
// down to 200 begins no earlier than 20, one AS: 20 has no
|
||||
// providers, so it did not get the route from one. Three and one
|
||||
// do not make five.
|
||||
assert_eq!(aspas.verify(&[20, 300, 10, 100, 1000], Downstream), Invalid);
|
||||
// Two peerings in a row: 30 is another large network, which hands
|
||||
// its customer a route it got from its peer 20, which got it from
|
||||
// its peer 10.
|
||||
let mut aspas = internet();
|
||||
aspas.set(30, [0]);
|
||||
assert_eq!(aspas.verify(&[30, 20, 10, 100, 1000], Downstream), Invalid);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_a_provider_what_is_not_attested_leaves_the_path_unknown() {
|
||||
let aspas = internet();
|
||||
// 2000 (no ASPA) up to 200, to 20, across to 10, down to 100: the
|
||||
// first hop is not attested either way.
|
||||
assert_eq!(aspas.verify(&[10, 20, 200, 2000], Downstream), Unknown);
|
||||
// With 2000's ASPA it is valid.
|
||||
let mut with = aspas.clone();
|
||||
with.set(2000, [200]);
|
||||
assert_eq!(with.verify(&[10, 20, 200, 2000], Downstream), Valid);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_aspa_is_replaced_and_removed_whole() {
|
||||
let mut aspas = internet();
|
||||
assert_eq!(aspas.providers(300), Some(&[10, 20][..]));
|
||||
assert_eq!(
|
||||
aspas.set(300, [20, 10, 20, 5]).as_deref(),
|
||||
Some(&[10, 20][..])
|
||||
);
|
||||
assert_eq!(aspas.providers(300), Some(&[5, 10, 20][..]));
|
||||
assert_eq!(aspas.remove(300).as_deref(), Some(&[5, 10, 20][..]));
|
||||
assert_eq!(aspas.providers(300), None);
|
||||
assert_eq!(aspas.remove(300), None);
|
||||
assert_eq!(aspas.len(), 5);
|
||||
}
|
||||
|
||||
/// The procedures as the draft words them, with its numbering: A(1) is
|
||||
/// the origin, A(N) the neighbor.
|
||||
fn by_the_draft(aspas: &AspaTable, path: &[u32], algorithm: Algorithm) -> PathValidity {
|
||||
let mut a: Vec<u32> = path.iter().rev().copied().collect();
|
||||
a.dedup();
|
||||
let n = a.len();
|
||||
if n == 0 {
|
||||
return Invalid;
|
||||
}
|
||||
// One-based, as written.
|
||||
let hop = |i: usize, j: usize| aspas.hop(a[i - 1], a[j - 1]);
|
||||
let first_up = |wanted: &[Hop]| {
|
||||
(1..n)
|
||||
.find(|i| wanted.contains(&hop(*i, *i + 1)))
|
||||
.unwrap_or(n)
|
||||
};
|
||||
let last_down = |wanted: &[Hop]| {
|
||||
(2..=n)
|
||||
.rev()
|
||||
.find(|j| wanted.contains(&hop(*j, *j - 1)))
|
||||
.map_or(n, |j| n - j + 1)
|
||||
};
|
||||
let not = [Hop::NotProvider];
|
||||
let not_attested = [Hop::NoAttestation, Hop::NotProvider];
|
||||
let (max_up, min_up) = (first_up(¬), first_up(¬_attested));
|
||||
match algorithm {
|
||||
Upstream if max_up < n => Invalid,
|
||||
Upstream if min_up < n => Unknown,
|
||||
Upstream => Valid,
|
||||
Downstream => {
|
||||
let (max_down, min_down) = (last_down(¬), last_down(¬_attested));
|
||||
if max_up + max_down < n {
|
||||
Invalid
|
||||
} else if min_up + min_down < n {
|
||||
Unknown
|
||||
} else {
|
||||
Valid
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
proptest! {
|
||||
/// Over a handful of AS numbers, so that paths and ASPAs meet.
|
||||
#[test]
|
||||
fn verification_agrees_with_the_draft_as_written(
|
||||
aspas in proptest::collection::vec(
|
||||
(0u32..8, proptest::collection::vec(0u32..8, 1..4)), 0..8),
|
||||
path in proptest::collection::vec(0u32..8, 0..10),
|
||||
downstream: bool,
|
||||
) {
|
||||
let aspas: AspaTable = aspas.into_iter().collect();
|
||||
let algorithm = if downstream { Downstream } else { Upstream };
|
||||
prop_assert_eq!(
|
||||
aspas.verify(&path, algorithm),
|
||||
by_the_draft(&aspas, &path, algorithm)
|
||||
);
|
||||
}
|
||||
|
||||
/// What is invalid from a provider is invalid from anyone, and
|
||||
/// what is valid from a customer is valid from anyone: the
|
||||
/// procedure for providers allows more.
|
||||
#[test]
|
||||
fn the_procedure_for_providers_is_the_more_lenient(
|
||||
aspas in proptest::collection::vec(
|
||||
(0u32..8, proptest::collection::vec(0u32..8, 1..4)), 0..8),
|
||||
path in proptest::collection::vec(0u32..8, 1..10),
|
||||
) {
|
||||
let aspas: AspaTable = aspas.into_iter().collect();
|
||||
let (up, down) = (aspas.verify(&path, Upstream), aspas.verify(&path, Downstream));
|
||||
if down == Invalid {
|
||||
prop_assert_eq!(up, Invalid);
|
||||
}
|
||||
if up == Valid {
|
||||
prop_assert_eq!(down, Valid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,953 +0,0 @@
|
||||
//! Keeps the VRPs and ASPAs in step with a validator, over the
|
||||
//! RPKI-to-Router protocol (RFC 8210 and draft-ietf-sidrops-8210bis).
|
||||
//!
|
||||
//! The client connects to a cache (the validator's RTR server), asks for
|
||||
//! everything once, and from then on for what has changed: when the cache
|
||||
//! says there is something new, and otherwise every so often. Each time the
|
||||
//! data changes an [`Update`] is published for whoever validates routes.
|
||||
//!
|
||||
//! The newest version of the protocol is asked for first, and an older one
|
||||
//! if that is what the cache speaks. Only version 2 carries ASPAs: with an
|
||||
//! older cache there are none, and paths are not verified.
|
||||
//!
|
||||
//! A cache that cannot be reached is tried again, and the other caches in
|
||||
//! turn. Meanwhile the data last received is used, but not for ever: a list
|
||||
//! that has not been confirmed for the expire interval is dropped, and
|
||||
//! routes are then neither valid nor invalid. Using stale data would be
|
||||
//! worse: it would go on refusing routes whose holders have since
|
||||
//! authorised them.
|
||||
|
||||
use std::io;
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::{Arc, Mutex, PoisonError};
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use bgp_primitives::constants::IpVersion;
|
||||
use bytes::BytesMut;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::sync::watch;
|
||||
use tokio::task::JoinHandle;
|
||||
use tokio::time::{Instant, sleep_until, timeout};
|
||||
use tracing::{info, warn};
|
||||
|
||||
use crate::aspa::AspaTable;
|
||||
use crate::rtr::{Pdu, RtrError, Timers, VERSION_0, VERSION_2, error_code};
|
||||
use crate::vrp::{Vrp, VrpTable};
|
||||
|
||||
// Hashed by the hundred thousand with every answer, and told apart from
|
||||
// what there was with every reset: a quick hash matters here.
|
||||
type HashSet<K> = std::collections::HashSet<K, foldhash::fast::RandomState>;
|
||||
|
||||
const CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
|
||||
/// How long a cache may take to send the next part of an answer.
|
||||
const RESPONSE_TIMEOUT: Duration = Duration::from_secs(60);
|
||||
|
||||
/// How long a whole answer may take. Everything a validator has takes it
|
||||
/// seconds to send.
|
||||
const ANSWER_TIMEOUT: Duration = Duration::from_secs(600);
|
||||
|
||||
/// The most records taken in one answer: several times what the whole
|
||||
/// RPKI holds. A cache that sends more is not sending the RPKI, and
|
||||
/// would otherwise be listened to until there is no memory left.
|
||||
const MAX_RECORDS: usize = 8_000_000;
|
||||
|
||||
/// How long to wait before trying the next cache after one has failed.
|
||||
const NEXT_CACHE_DELAY: Duration = Duration::from_secs(1);
|
||||
|
||||
/// How long to wait before asking a cache again that said it has no data
|
||||
/// yet, rather than the retry interval: it is up and validating, which
|
||||
/// takes minutes after it starts, and the retry interval is ten unless it
|
||||
/// has said otherwise, which it has not had the chance to.
|
||||
const NO_DATA_RETRY: Duration = Duration::from_secs(30);
|
||||
|
||||
/// How long a cache that is being left is given to take the reason.
|
||||
const ERROR_REPORT_TIMEOUT: Duration = Duration::from_secs(1);
|
||||
|
||||
/// Where the VRPs come from, and how patiently.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct RpkiConfig {
|
||||
/// The caches, in order of preference. One is used at a time; the next
|
||||
/// is tried when it fails.
|
||||
pub caches: Vec<SocketAddr>,
|
||||
/// If set, used in place of what the cache says (or of the defaults of
|
||||
/// RFC 8210 §6, with a cache that says nothing).
|
||||
pub refresh: Option<Duration>,
|
||||
pub retry: Option<Duration>,
|
||||
pub expire: Option<Duration>,
|
||||
}
|
||||
|
||||
/// The VRPs and ASPAs as they are after a change.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Update {
|
||||
/// Goes up by one with every update.
|
||||
pub version: u64,
|
||||
pub vrps: Arc<VrpTable>,
|
||||
pub aspas: Arc<AspaTable>,
|
||||
/// What was added or removed to get here from the version before.
|
||||
/// `None` if any route's validity may have changed.
|
||||
pub changed: Option<Changed>,
|
||||
}
|
||||
|
||||
/// What differs between one [`Update`] and the next.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Changed {
|
||||
/// The VRPs that came or went: only routes that these cover can have
|
||||
/// another origin validity than before.
|
||||
pub vrps: Arc<VrpTable>,
|
||||
/// The customer ASes whose ASPA came, went or lists other providers:
|
||||
/// only paths with one of these on them can verify otherwise than
|
||||
/// before.
|
||||
pub aspa_customers: Arc<HashSet<u32>>,
|
||||
}
|
||||
|
||||
/// How things stand with the caches.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum CacheState {
|
||||
/// No cache has been reached yet, or the one in use was lost.
|
||||
Connecting,
|
||||
/// Connected and waiting for the data.
|
||||
Synchronising,
|
||||
/// The data is complete as of the cache's serial.
|
||||
Synchronised,
|
||||
}
|
||||
|
||||
/// What there is to say about the client, for showing.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Status {
|
||||
/// The cache in use, or last tried.
|
||||
pub cache: Option<SocketAddr>,
|
||||
pub state: CacheState,
|
||||
/// The cache's session and the serial the data is at.
|
||||
pub session_id: Option<u16>,
|
||||
pub serial: Option<u32>,
|
||||
pub protocol_version: u8,
|
||||
pub vrps_ipv4: usize,
|
||||
pub vrps_ipv6: usize,
|
||||
/// How many ASes have an ASPA. None do if the cache speaks a version
|
||||
/// of the protocol before 2.
|
||||
pub aspas: usize,
|
||||
/// When the data was last confirmed by a cache.
|
||||
pub updated_at: Option<SystemTime>,
|
||||
/// When it will be dropped unless confirmed again.
|
||||
pub expires_at: Option<SystemTime>,
|
||||
/// Why the last attempt failed, if it did.
|
||||
pub last_error: Option<String>,
|
||||
}
|
||||
|
||||
struct Shared {
|
||||
updates: watch::Sender<Update>,
|
||||
/// The version of the latest update, for a look that costs nothing.
|
||||
version: AtomicU64,
|
||||
status: Mutex<Status>,
|
||||
}
|
||||
|
||||
/// Handle to the VRPs and to the client that keeps them. Cheap to clone.
|
||||
#[derive(Clone)]
|
||||
pub struct Rpki {
|
||||
shared: Arc<Shared>,
|
||||
}
|
||||
|
||||
impl Rpki {
|
||||
/// Starts a client for `config` and returns the handle to its data,
|
||||
/// with the task that runs it. Until the first cache has answered
|
||||
/// there are no VRPs and no ASPAs: every route's origin is
|
||||
/// [`crate::Validity::NotFound`] and every path
|
||||
/// [`crate::PathValidity::Unknown`].
|
||||
pub fn spawn(config: RpkiConfig) -> (Self, JoinHandle<()>) {
|
||||
let (updates, _) = watch::channel(Update {
|
||||
version: 0,
|
||||
vrps: Arc::default(),
|
||||
aspas: Arc::default(),
|
||||
changed: None,
|
||||
});
|
||||
let rpki = Self {
|
||||
shared: Arc::new(Shared {
|
||||
updates,
|
||||
version: AtomicU64::new(0),
|
||||
status: Mutex::new(Status {
|
||||
cache: None,
|
||||
state: CacheState::Connecting,
|
||||
session_id: None,
|
||||
serial: None,
|
||||
protocol_version: VERSION_2,
|
||||
vrps_ipv4: 0,
|
||||
vrps_ipv6: 0,
|
||||
aspas: 0,
|
||||
updated_at: None,
|
||||
expires_at: None,
|
||||
last_error: None,
|
||||
}),
|
||||
}),
|
||||
};
|
||||
let client = Client {
|
||||
shared: Arc::clone(&rpki.shared),
|
||||
config,
|
||||
data: Data::default(),
|
||||
place: None,
|
||||
protocol_version: VERSION_2,
|
||||
timers: Timers::default(),
|
||||
expires: None,
|
||||
};
|
||||
(rpki, tokio::spawn(client.run()))
|
||||
}
|
||||
|
||||
/// The VRPs and ASPAs as they are now.
|
||||
pub fn current(&self) -> Update {
|
||||
self.shared.updates.borrow().clone()
|
||||
}
|
||||
|
||||
/// The version of the latest [`Update`]. Costs a load of one number,
|
||||
/// for those who ask before every use whether theirs is still current.
|
||||
pub fn version(&self) -> u64 {
|
||||
self.shared.version.load(Ordering::Acquire)
|
||||
}
|
||||
|
||||
/// Follows the updates. A follower that is busy sees the latest only;
|
||||
/// [`Update::version`] tells it whether it skipped any.
|
||||
pub fn subscribe(&self) -> watch::Receiver<Update> {
|
||||
self.shared.updates.subscribe()
|
||||
}
|
||||
|
||||
pub fn status(&self) -> Status {
|
||||
self.shared
|
||||
.status
|
||||
.lock()
|
||||
.unwrap_or_else(PoisonError::into_inner)
|
||||
.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// Why a session with a cache ended.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
enum SessionError {
|
||||
#[error("{0}")]
|
||||
Io(#[from] io::Error),
|
||||
#[error("the cache sent something that is not RTR: {0}")]
|
||||
Malformed(#[from] RtrError),
|
||||
#[error("the cache closed the connection")]
|
||||
Closed,
|
||||
#[error("the cache did not accept the connection within {0:?}")]
|
||||
ConnectTimeout(Duration),
|
||||
#[error("the cache sent nothing for {0:?} in the middle of an answer")]
|
||||
ResponseTimeout(Duration),
|
||||
#[error("the cache did not finish its answer within {0:?}")]
|
||||
AnswerTimeout(Duration),
|
||||
#[error("the cache confirmed nothing for so long that the data expired")]
|
||||
Expired,
|
||||
#[error("the cache reported an error: {text} (code {code})")]
|
||||
Reported { code: u16, text: String },
|
||||
#[error("the cache sent {0} where it has no place")]
|
||||
Unexpected(&'static str),
|
||||
#[error("the cache {0}")]
|
||||
Inconsistent(Inconsistency),
|
||||
/// Not a failure of the cache: it speaks an older version, and is to
|
||||
/// be asked again in that.
|
||||
#[error("the cache speaks protocol version {0}")]
|
||||
Downgrade(u8),
|
||||
}
|
||||
|
||||
impl SessionError {
|
||||
/// The error code that tells the cache why it is being left, if that
|
||||
/// is something it did and can be told (RFC 8210 §10). An Error Report
|
||||
/// is never answered with another.
|
||||
fn report_code(&self) -> Option<u16> {
|
||||
match self {
|
||||
Self::Malformed(e) => Some(e.code()),
|
||||
Self::Unexpected(_) => Some(error_code::CORRUPT_DATA),
|
||||
Self::Inconsistent(problem) => Some(problem.code),
|
||||
Self::Io(_)
|
||||
| Self::Closed
|
||||
| Self::ConnectTimeout(_)
|
||||
| Self::ResponseTimeout(_)
|
||||
| Self::AnswerTimeout(_)
|
||||
| Self::Expired
|
||||
| Self::Reported { .. }
|
||||
| Self::Downgrade(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Something a cache said that does not fit with what it said before.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
struct Inconsistency {
|
||||
what: &'static str,
|
||||
/// The error code it is reported to the cache with.
|
||||
code: u16,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Inconsistency {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(self.what)
|
||||
}
|
||||
}
|
||||
|
||||
const fn inconsistent(what: &'static str, code: u16) -> Inconsistency {
|
||||
Inconsistency { what, code }
|
||||
}
|
||||
|
||||
const DUPLICATE_VRP: Inconsistency =
|
||||
inconsistent("announced a VRP twice", error_code::DUPLICATE_ANNOUNCEMENT);
|
||||
const UNKNOWN_VRP: Inconsistency = inconsistent(
|
||||
"withdrew a VRP it had not announced",
|
||||
error_code::WITHDRAWAL_OF_UNKNOWN_RECORD,
|
||||
);
|
||||
const UNKNOWN_ASPA: Inconsistency = inconsistent(
|
||||
"withdrew an ASPA it had not announced",
|
||||
error_code::WITHDRAWAL_OF_UNKNOWN_RECORD,
|
||||
);
|
||||
const OTHER_SESSION: Inconsistency = inconsistent(
|
||||
"ended an answer in another session than it began it",
|
||||
error_code::CORRUPT_DATA,
|
||||
);
|
||||
const OTHER_VERSION: Inconsistency = inconsistent(
|
||||
"changed protocol version in the middle of a session",
|
||||
error_code::UNEXPECTED_VERSION,
|
||||
);
|
||||
const INTERNAL: Inconsistency = inconsistent(
|
||||
"could not be followed (a bug here)",
|
||||
error_code::INTERNAL_ERROR,
|
||||
);
|
||||
|
||||
/// Where the data is at: which of the cache's sessions, and which serial.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
struct Place {
|
||||
cache: SocketAddr,
|
||||
/// The version of the protocol the data came in. What another version
|
||||
/// carries is not the same data.
|
||||
protocol_version: u8,
|
||||
session_id: u16,
|
||||
serial: u32,
|
||||
}
|
||||
|
||||
/// What a cache has given: the data validation rests on.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
struct Data {
|
||||
vrps: HashSet<Vrp>,
|
||||
aspas: AspaTable,
|
||||
}
|
||||
|
||||
/// One thing an answer says: a VRP or an ASPA, come or gone.
|
||||
#[derive(Debug, Clone)]
|
||||
enum Record {
|
||||
Vrp {
|
||||
announce: bool,
|
||||
vrp: Vrp,
|
||||
},
|
||||
/// With providers, the ASPA that is now `customer`'s; without, that
|
||||
/// it has none any more.
|
||||
Aspa {
|
||||
customer: u32,
|
||||
providers: Option<Vec<u32>>,
|
||||
},
|
||||
}
|
||||
|
||||
struct Client {
|
||||
shared: Arc<Shared>,
|
||||
config: RpkiConfig,
|
||||
/// The data as last received.
|
||||
data: Data,
|
||||
/// Where `data` is at, if it came from a cache that can be asked for
|
||||
/// what has changed since.
|
||||
place: Option<Place>,
|
||||
protocol_version: u8,
|
||||
/// As the cache last gave them, or the defaults.
|
||||
timers: Timers,
|
||||
/// When `data` is to be dropped unless a cache confirms it first.
|
||||
expires: Option<Instant>,
|
||||
}
|
||||
|
||||
/// A connection to a cache, read a PDU at a time.
|
||||
struct Connection {
|
||||
stream: TcpStream,
|
||||
buf: BytesMut,
|
||||
}
|
||||
|
||||
impl Connection {
|
||||
async fn send(&mut self, version: u8, pdu: &Pdu) -> io::Result<()> {
|
||||
let mut out = BytesMut::new();
|
||||
pdu.encode(version, &mut out);
|
||||
self.stream.write_all(&out).await
|
||||
}
|
||||
|
||||
/// The next PDU, and the protocol version it was sent with.
|
||||
/// Cancel-safe: what has arrived of a PDU stays in the buffer.
|
||||
async fn next(&mut self) -> Result<(u8, Pdu), SessionError> {
|
||||
loop {
|
||||
if let Some((version, pdu, used)) = Pdu::decode(&self.buf)? {
|
||||
let _ = self.buf.split_to(used);
|
||||
return Ok((version, pdu));
|
||||
}
|
||||
if self.stream.read_buf(&mut self.buf).await? == 0 {
|
||||
return Err(SessionError::Closed);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Client {
|
||||
fn status(&self, change: impl FnOnce(&mut Status)) {
|
||||
change(
|
||||
&mut self
|
||||
.shared
|
||||
.status
|
||||
.lock()
|
||||
.unwrap_or_else(PoisonError::into_inner),
|
||||
);
|
||||
}
|
||||
|
||||
fn refresh(&self) -> Duration {
|
||||
self.config.refresh.unwrap_or(Duration::from_secs(
|
||||
self.timers.refresh.clamp(1, 86400).into(),
|
||||
))
|
||||
}
|
||||
|
||||
fn retry(&self) -> Duration {
|
||||
self.config
|
||||
.retry
|
||||
.unwrap_or(Duration::from_secs(self.timers.retry.clamp(1, 7200).into()))
|
||||
}
|
||||
|
||||
/// Never sooner than a refresh that failed and was tried once more
|
||||
/// would take, whatever the cache or the configuration says: data that
|
||||
/// ran out between two refreshes would leave nothing validated for a
|
||||
/// part of every round (RFC 8210 §6 wants it longer than either).
|
||||
fn expire(&self) -> Duration {
|
||||
self.config
|
||||
.expire
|
||||
.unwrap_or(Duration::from_secs(
|
||||
self.timers.expire.clamp(600, 172_800).into(),
|
||||
))
|
||||
.max(self.refresh() + self.retry())
|
||||
}
|
||||
|
||||
async fn run(mut self) {
|
||||
if self.config.caches.is_empty() {
|
||||
return std::future::pending().await;
|
||||
}
|
||||
let mut attempt = 0usize;
|
||||
loop {
|
||||
let cache = self.config.caches[attempt % self.config.caches.len()];
|
||||
self.status(|status| {
|
||||
status.cache = Some(cache);
|
||||
status.state = CacheState::Connecting;
|
||||
});
|
||||
let ended = self.session(cache).await;
|
||||
if let SessionError::Downgrade(version) = ended {
|
||||
info!(%cache, version, "RPKI cache speaks an older RTR version; asking again in that");
|
||||
self.protocol_version = version;
|
||||
continue;
|
||||
}
|
||||
let no_data = matches!(
|
||||
ended,
|
||||
SessionError::Reported {
|
||||
code: error_code::NO_DATA_AVAILABLE,
|
||||
..
|
||||
}
|
||||
);
|
||||
if no_data {
|
||||
info!(%cache, "RPKI cache has no data yet; asking again soon");
|
||||
} else {
|
||||
warn!(%cache, err = %ended, "RPKI cache session ended");
|
||||
}
|
||||
self.status(|status| {
|
||||
status.state = CacheState::Connecting;
|
||||
status.last_error = Some(ended.to_string());
|
||||
});
|
||||
|
||||
attempt += 1;
|
||||
// The next cache, or this one when it is back, may speak a
|
||||
// newer version.
|
||||
self.protocol_version = VERSION_2;
|
||||
let all_tried = attempt.is_multiple_of(self.config.caches.len());
|
||||
self.wait(rest(all_tried, no_data, self.retry())).await;
|
||||
}
|
||||
}
|
||||
|
||||
/// Waits for `duration`, dropping the data on the way if its time runs
|
||||
/// out.
|
||||
async fn wait(&mut self, duration: Duration) {
|
||||
let until = Instant::now() + duration;
|
||||
if let Some(expires) = self.expires
|
||||
&& expires <= until
|
||||
{
|
||||
sleep_until(expires).await;
|
||||
self.expired();
|
||||
}
|
||||
sleep_until(until).await;
|
||||
}
|
||||
|
||||
/// The data has gone unconfirmed for too long to be used.
|
||||
fn expired(&mut self) {
|
||||
warn!(
|
||||
vrps = self.data.vrps.len(),
|
||||
aspas = self.data.aspas.len(),
|
||||
"RPKI data has expired without a cache to confirm it; dropping it"
|
||||
);
|
||||
self.data = Data::default();
|
||||
self.place = None;
|
||||
self.expires = None;
|
||||
self.publish(Arc::default(), None);
|
||||
self.status(|status| {
|
||||
status.session_id = None;
|
||||
status.serial = None;
|
||||
status.expires_at = None;
|
||||
});
|
||||
}
|
||||
|
||||
/// Runs a session with `cache` until it ends, which it does only by
|
||||
/// failing.
|
||||
async fn session(&mut self, cache: SocketAddr) -> SessionError {
|
||||
let stream = match timeout(CONNECT_TIMEOUT, TcpStream::connect(cache)).await {
|
||||
Ok(Ok(stream)) => stream,
|
||||
Ok(Err(e)) => return e.into(),
|
||||
Err(_) => return SessionError::ConnectTimeout(CONNECT_TIMEOUT),
|
||||
};
|
||||
let mut connection = Connection {
|
||||
stream,
|
||||
buf: BytesMut::with_capacity(64 * 1024),
|
||||
};
|
||||
info!(%cache, "Connected to RPKI cache");
|
||||
self.status(|status| status.state = CacheState::Synchronising);
|
||||
|
||||
loop {
|
||||
if let Err(e) = self.synchronise(cache, &mut connection).await {
|
||||
return self.leave(&mut connection, e).await;
|
||||
}
|
||||
// Until the cache says there is something new, or it is time
|
||||
// to ask anyway. If the data runs out first, it is the cache
|
||||
// that has stopped confirming it.
|
||||
let ask = Instant::now() + self.refresh();
|
||||
let expires = self.expires.unwrap_or(ask);
|
||||
tokio::select! {
|
||||
pdu = connection.next() => {
|
||||
let problem = match pdu {
|
||||
Ok((_, Pdu::ErrorReport { code, text })) => {
|
||||
SessionError::Reported { code, text }
|
||||
}
|
||||
Ok((version, _)) if version != self.protocol_version => {
|
||||
SessionError::Inconsistent(OTHER_VERSION)
|
||||
}
|
||||
Ok((_, Pdu::SerialNotify { .. })) => continue,
|
||||
Ok(_) => SessionError::Unexpected("an answer nobody asked for"),
|
||||
Err(e) => e,
|
||||
};
|
||||
return self.leave(&mut connection, problem).await;
|
||||
}
|
||||
() = sleep_until(ask) => {}
|
||||
() = sleep_until(expires), if expires < ask => {
|
||||
self.expired();
|
||||
return SessionError::Expired;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Tells the cache why the session is ending, if it is for something
|
||||
/// the cache did, and gives the reason back.
|
||||
async fn leave(&self, connection: &mut Connection, why: SessionError) -> SessionError {
|
||||
if let Some(code) = why.report_code() {
|
||||
let report = Pdu::ErrorReport {
|
||||
code,
|
||||
text: why.to_string(),
|
||||
};
|
||||
// If it cannot be told, it cannot.
|
||||
let _ = timeout(
|
||||
ERROR_REPORT_TIMEOUT,
|
||||
connection.send(self.protocol_version, &report),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
why
|
||||
}
|
||||
|
||||
/// Asks the cache for what has changed, or for everything, and takes
|
||||
/// the answer.
|
||||
async fn synchronise(
|
||||
&mut self,
|
||||
cache: SocketAddr,
|
||||
connection: &mut Connection,
|
||||
) -> Result<(), SessionError> {
|
||||
// Changes can be asked for only of the cache the data came from,
|
||||
// in the version of the protocol it came in.
|
||||
let mut since = self.place.filter(|place| {
|
||||
place.cache == cache && place.protocol_version == self.protocol_version
|
||||
});
|
||||
loop {
|
||||
let query = match since {
|
||||
Some(place) => Pdu::SerialQuery {
|
||||
session_id: place.session_id,
|
||||
serial: place.serial,
|
||||
},
|
||||
None => Pdu::ResetQuery,
|
||||
};
|
||||
connection.send(self.protocol_version, &query).await?;
|
||||
match self.answer(cache, connection, since).await? {
|
||||
Answer::Complete => return Ok(()),
|
||||
// The cache cannot say what changed: ask for everything.
|
||||
Answer::Reset => since = None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads the answer to a query. `since` is the place a Serial Query
|
||||
/// asked about; `None` after a Reset Query.
|
||||
async fn answer(
|
||||
&mut self,
|
||||
cache: SocketAddr,
|
||||
connection: &mut Connection,
|
||||
since: Option<Place>,
|
||||
) -> Result<Answer, SessionError> {
|
||||
// Each PDU in good time, and all of them too: a cache that sends
|
||||
// one a minute would keep the old data in use for ever.
|
||||
let deadline = Instant::now() + ANSWER_TIMEOUT;
|
||||
let mut next = async || {
|
||||
let left = deadline.saturating_duration_since(Instant::now());
|
||||
match timeout(RESPONSE_TIMEOUT.min(left), connection.next()).await {
|
||||
Ok(pdu) => pdu,
|
||||
Err(_) if left <= RESPONSE_TIMEOUT => {
|
||||
Err(SessionError::AnswerTimeout(ANSWER_TIMEOUT))
|
||||
}
|
||||
Err(_) => Err(SessionError::ResponseTimeout(RESPONSE_TIMEOUT)),
|
||||
}
|
||||
};
|
||||
|
||||
// The answer begins with a Cache Response, or is a Cache Reset.
|
||||
let session_id = loop {
|
||||
let (version, pdu) = next().await?;
|
||||
if version < self.protocol_version {
|
||||
// An older cache answers in its own version, with an error
|
||||
// or as if it had been asked in that; the query has to be
|
||||
// put again in that version (RFC 8210 §7).
|
||||
return Err(SessionError::Downgrade(version));
|
||||
}
|
||||
if version > self.protocol_version {
|
||||
return Err(SessionError::Inconsistent(OTHER_VERSION));
|
||||
}
|
||||
match pdu {
|
||||
Pdu::CacheResponse { session_id } => break session_id,
|
||||
Pdu::CacheReset if since.is_some() => return Ok(Answer::Reset),
|
||||
// News of what is being asked for anyway.
|
||||
Pdu::SerialNotify { .. } => {}
|
||||
// A cache that refuses the version in the version it
|
||||
// refuses: the next one down, then.
|
||||
Pdu::ErrorReport { code, .. }
|
||||
if code == error_code::UNSUPPORTED_VERSION
|
||||
&& self.protocol_version != VERSION_0 =>
|
||||
{
|
||||
return Err(SessionError::Downgrade(self.protocol_version - 1));
|
||||
}
|
||||
Pdu::ErrorReport { code, text } => {
|
||||
return Err(SessionError::Reported { code, text });
|
||||
}
|
||||
_ => return Err(SessionError::Unexpected("data before a Cache Response")),
|
||||
}
|
||||
};
|
||||
// The cache has answered in the version asked for: that is the
|
||||
// version of the session.
|
||||
let protocol_version = self.protocol_version;
|
||||
self.status(|status| status.protocol_version = protocol_version);
|
||||
if since.is_some_and(|place| place.session_id != session_id) {
|
||||
// The cache has started over; its serials mean something else
|
||||
// now.
|
||||
return Ok(Answer::Reset);
|
||||
}
|
||||
|
||||
// Then the VRPs and ASPAs, and End of Data. Nothing takes effect
|
||||
// until the answer is complete (RFC 8210 §8.1), so what it says is
|
||||
// kept aside in the order it came.
|
||||
let mut records: Vec<Record> = Vec::new();
|
||||
let (serial, timers) = loop {
|
||||
let (version, pdu) = next().await?;
|
||||
if version != self.protocol_version && !matches!(pdu, Pdu::ErrorReport { .. }) {
|
||||
return Err(SessionError::Inconsistent(OTHER_VERSION));
|
||||
}
|
||||
if records.len() >= MAX_RECORDS {
|
||||
return Err(SessionError::Unexpected("more data than any cache has"));
|
||||
}
|
||||
match pdu {
|
||||
Pdu::Prefix { announce, vrp } => records.push(Record::Vrp { announce, vrp }),
|
||||
Pdu::Aspa {
|
||||
announce,
|
||||
customer,
|
||||
providers,
|
||||
} => records.push(Record::Aspa {
|
||||
customer,
|
||||
providers: announce.then_some(providers),
|
||||
}),
|
||||
Pdu::RouterKey => {}
|
||||
Pdu::EndOfData {
|
||||
session_id: ended,
|
||||
serial,
|
||||
timers,
|
||||
} => {
|
||||
if ended != session_id {
|
||||
return Err(SessionError::Inconsistent(OTHER_SESSION));
|
||||
}
|
||||
break (serial, timers);
|
||||
}
|
||||
Pdu::ErrorReport { code, text } => {
|
||||
return Err(SessionError::Reported { code, text });
|
||||
}
|
||||
_ => {
|
||||
return Err(SessionError::Unexpected(
|
||||
"a PDU that is not part of an answer",
|
||||
));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// The first data there is can change any route's validity; after
|
||||
// that, only where something came or went.
|
||||
let first = self.place.is_none() && self.expires.is_none();
|
||||
let (before, replaced) = match since {
|
||||
// Changes, to what there is.
|
||||
Some(_) => (std::mem::take(&mut self.data), None),
|
||||
// Everything anew.
|
||||
None => (Data::default(), Some(std::mem::take(&mut self.data))),
|
||||
};
|
||||
let vrps = Arc::clone(&self.shared.updates.borrow().vrps);
|
||||
// Building the tables is work for a thread that may take its time.
|
||||
let built = tokio::task::spawn_blocking(move || apply(before, replaced, &records, vrps))
|
||||
.await
|
||||
.map_err(|_| SessionError::Inconsistent(INTERNAL))?;
|
||||
let built = match built {
|
||||
Ok(built) => built,
|
||||
Err((data, problem)) => {
|
||||
// What there was stays as it was.
|
||||
self.data = data;
|
||||
return Err(SessionError::Inconsistent(problem));
|
||||
}
|
||||
};
|
||||
|
||||
self.data = built.data;
|
||||
self.place = Some(Place {
|
||||
cache,
|
||||
protocol_version: self.protocol_version,
|
||||
session_id,
|
||||
serial,
|
||||
});
|
||||
self.timers = timers.unwrap_or_default();
|
||||
let expires_in = self.expire();
|
||||
self.expires = Some(Instant::now() + expires_in);
|
||||
let changes = built.changed.vrps.len() + built.changed.aspa_customers.len();
|
||||
if first || changes > 0 {
|
||||
self.publish(built.vrps, (!first).then_some(built.changed));
|
||||
}
|
||||
info!(
|
||||
%cache,
|
||||
serial,
|
||||
vrps = self.data.vrps.len(),
|
||||
aspas = self.data.aspas.len(),
|
||||
changes,
|
||||
"RPKI data is up to date"
|
||||
);
|
||||
let updated_at = SystemTime::now();
|
||||
self.status(|status| {
|
||||
status.state = CacheState::Synchronised;
|
||||
status.session_id = Some(session_id);
|
||||
status.serial = Some(serial);
|
||||
status.updated_at = Some(updated_at);
|
||||
status.expires_at = Some(updated_at + expires_in);
|
||||
status.last_error = None;
|
||||
});
|
||||
Ok(Answer::Complete)
|
||||
}
|
||||
|
||||
/// Publishes the data as it now is. `vrps` holds its VRPs.
|
||||
fn publish(&self, vrps: Arc<VrpTable>, changed: Option<Changed>) {
|
||||
let count = |version| {
|
||||
self.data
|
||||
.vrps
|
||||
.iter()
|
||||
.filter(|vrp| vrp.prefix.address_family() == version)
|
||||
.count()
|
||||
};
|
||||
let (ipv4, ipv6) = (count(IpVersion::Ipv4), count(IpVersion::Ipv6));
|
||||
self.status(|status| {
|
||||
status.vrps_ipv4 = ipv4;
|
||||
status.vrps_ipv6 = ipv6;
|
||||
status.aspas = self.data.aspas.len();
|
||||
});
|
||||
let aspas = Arc::new(self.data.aspas.clone());
|
||||
self.shared.updates.send_modify(|update| {
|
||||
update.version += 1;
|
||||
update.vrps = vrps;
|
||||
update.aspas = aspas;
|
||||
update.changed = changed;
|
||||
self.shared.version.store(update.version, Ordering::Release);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// The data after an answer, ready to be used.
|
||||
struct Built {
|
||||
data: Data,
|
||||
/// The VRPs of `data`, arranged for validating.
|
||||
vrps: Arc<VrpTable>,
|
||||
changed: Changed,
|
||||
}
|
||||
|
||||
/// Applies the `records` of a complete answer, in order, to `data`.
|
||||
/// `replaced` is the data that an answer to a Reset Query takes the place
|
||||
/// of, and `vrps` the table of the VRPs as they were before the answer,
|
||||
/// which is kept if the answer changes none.
|
||||
///
|
||||
/// If the answer contradicts itself or what there was, what there was is
|
||||
/// given back with the reason.
|
||||
fn apply(
|
||||
mut data: Data,
|
||||
replaced: Option<Data>,
|
||||
records: &[Record],
|
||||
vrps: Arc<VrpTable>,
|
||||
) -> Result<Built, (Data, Inconsistency)> {
|
||||
// What each record displaced, to put back if a later one cannot be
|
||||
// applied.
|
||||
let mut displaced: Vec<Option<Arc<[u32]>>> = Vec::new();
|
||||
let mut problem = None;
|
||||
for record in records {
|
||||
let ok = match record {
|
||||
Record::Vrp {
|
||||
announce: true,
|
||||
vrp,
|
||||
} => data.vrps.insert(*vrp).then_some(None).ok_or(DUPLICATE_VRP),
|
||||
Record::Vrp {
|
||||
announce: false,
|
||||
vrp,
|
||||
} => data.vrps.remove(vrp).then_some(None).ok_or(UNKNOWN_VRP),
|
||||
// An ASPA that is announced takes the place of the one there
|
||||
// was (draft-ietf-sidrops-8210bis §5.12).
|
||||
Record::Aspa {
|
||||
customer,
|
||||
providers: Some(providers),
|
||||
} => Ok(data.aspas.set(*customer, providers.iter().copied())),
|
||||
Record::Aspa {
|
||||
customer,
|
||||
providers: None,
|
||||
} => data.aspas.remove(*customer).map(Some).ok_or(UNKNOWN_ASPA),
|
||||
};
|
||||
match ok {
|
||||
Ok(was) => displaced.push(was),
|
||||
Err(e) => {
|
||||
problem = Some(e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(problem) = problem {
|
||||
return Err(match replaced {
|
||||
Some(replaced) => (replaced, problem),
|
||||
None => {
|
||||
// Undo, last first.
|
||||
for (record, was) in records.iter().zip(displaced).rev() {
|
||||
match record {
|
||||
Record::Vrp {
|
||||
announce: true,
|
||||
vrp,
|
||||
} => {
|
||||
data.vrps.remove(vrp);
|
||||
}
|
||||
Record::Vrp {
|
||||
announce: false,
|
||||
vrp,
|
||||
} => {
|
||||
data.vrps.insert(*vrp);
|
||||
}
|
||||
Record::Aspa { customer, .. } => data.aspas.restore(*customer, was),
|
||||
}
|
||||
}
|
||||
(data, problem)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// What came or went: against the data replaced, or else whatever the
|
||||
// answer touched.
|
||||
let (changed_vrps, aspa_customers): (Vec<Vrp>, HashSet<u32>) = match &replaced {
|
||||
Some(replaced) => (
|
||||
data.vrps
|
||||
.symmetric_difference(&replaced.vrps)
|
||||
.copied()
|
||||
.collect(),
|
||||
differing_customers(&data.aspas, &replaced.aspas),
|
||||
),
|
||||
None => (
|
||||
records
|
||||
.iter()
|
||||
.filter_map(|record| match record {
|
||||
Record::Vrp { vrp, .. } => Some(*vrp),
|
||||
Record::Aspa { .. } => None,
|
||||
})
|
||||
.collect(),
|
||||
records
|
||||
.iter()
|
||||
.filter_map(|record| match record {
|
||||
Record::Aspa { customer, .. } => Some(*customer),
|
||||
Record::Vrp { .. } => None,
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
};
|
||||
Ok(Built {
|
||||
vrps: match changed_vrps.is_empty() {
|
||||
true => vrps,
|
||||
false => Arc::new(VrpTable::new(data.vrps.iter().copied())),
|
||||
},
|
||||
changed: Changed {
|
||||
vrps: Arc::new(VrpTable::new(changed_vrps)),
|
||||
aspa_customers: Arc::new(aspa_customers),
|
||||
},
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/// The customers whose ASPA is not the same in `a` as in `b`.
|
||||
fn differing_customers(a: &AspaTable, b: &AspaTable) -> HashSet<u32> {
|
||||
let differs = |(customer, providers): (u32, &[u32]), other: &AspaTable| {
|
||||
(other.providers(customer) != Some(providers)).then_some(customer)
|
||||
};
|
||||
a.iter()
|
||||
.filter_map(|aspa| differs(aspa, b))
|
||||
.chain(b.iter().filter_map(|aspa| differs(aspa, a)))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// How a query was answered.
|
||||
enum Answer {
|
||||
/// With data, which is now in use.
|
||||
Complete,
|
||||
/// With a request to ask for everything instead.
|
||||
Reset,
|
||||
}
|
||||
|
||||
/// How long to rest after a session ended: the next cache at once; when
|
||||
/// all have been tried, the retry interval, or less if the last one is
|
||||
/// only still validating.
|
||||
fn rest(all_tried: bool, no_data: bool, retry: Duration) -> Duration {
|
||||
match (all_tried, no_data) {
|
||||
(false, _) => NEXT_CACHE_DELAY,
|
||||
(true, true) => NO_DATA_RETRY.min(retry),
|
||||
(true, false) => retry,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// A cache that is still validating after it started is asked again
|
||||
/// in half a minute, not the ten the retry interval is by default.
|
||||
#[test]
|
||||
fn a_cache_without_data_yet_is_asked_again_soon() {
|
||||
let ten_minutes = Duration::from_secs(600);
|
||||
assert_eq!(rest(true, true, ten_minutes), NO_DATA_RETRY);
|
||||
assert_eq!(rest(true, false, ten_minutes), ten_minutes);
|
||||
// Never later than the retry interval says.
|
||||
let short = Duration::from_secs(5);
|
||||
assert_eq!(rest(true, true, short), short);
|
||||
// Another cache first, whatever this one said.
|
||||
assert_eq!(rest(false, true, ten_minutes), NEXT_CACHE_DELAY);
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
//! Route origin validation (RFC 6811) and AS path verification
|
||||
//! (draft-ietf-sidrops-aspa-verification) with the RPKI.
|
||||
//!
|
||||
//! The holder of an address block can sign a statement of which AS may
|
||||
//! originate routes for it. Validators collect and check those statements
|
||||
//! and hand routers the result, a list of validated ROA payloads (VRPs),
|
||||
//! over the RPKI-to-Router protocol (RFC 8210). With that list a router can
|
||||
//! tell a route whose origin is authorised from one whose origin is not:
|
||||
//! the commonest kind of hijack and of mistake.
|
||||
//!
|
||||
//! The same way, a network can sign a statement of who its providers are
|
||||
//! (an ASPA). With those a router can tell a path that could have come
|
||||
//! about by networks passing routes on as they are paid to from one that
|
||||
//! could not: a route leak, or a path made up to get past origin
|
||||
//! validation.
|
||||
//!
|
||||
//! - [`vrp`] holds the VRPs and answers whether a route's origin is valid.
|
||||
//! - [`aspa`] holds the ASPAs and answers whether a path is.
|
||||
//! - [`rtr`] is the protocol's messages.
|
||||
//! - [`client`] keeps both in step with a validator.
|
||||
|
||||
pub mod aspa;
|
||||
pub mod client;
|
||||
pub mod rtr;
|
||||
pub mod vrp;
|
||||
|
||||
pub use aspa::{Algorithm, AspaTable, PathValidity};
|
||||
pub use client::{CacheState, Changed, Rpki, RpkiConfig, Status, Update};
|
||||
pub use vrp::{Validity, Vrp, VrpTable};
|
||||
@@ -1,674 +0,0 @@
|
||||
//! The messages of the RPKI-to-Router protocol, as bytes and back: version
|
||||
//! 0 (RFC 6810), version 1 (RFC 8210) and version 2
|
||||
//! (draft-ietf-sidrops-8210bis). No I/O here.
|
||||
//!
|
||||
//! Every message (a PDU) begins with the same eight octets: the protocol
|
||||
//! version, the type, a field whose meaning depends on the type, and the
|
||||
//! length of the whole PDU.
|
||||
|
||||
use bgp_primitives::constants::IpVersion;
|
||||
use bgp_primitives::ip_prefix::IpPrefix;
|
||||
use bytes::{BufMut, BytesMut};
|
||||
|
||||
use crate::vrp::Vrp;
|
||||
|
||||
/// The newest protocol version, which this client asks for first. Version
|
||||
/// 2 added ASPAs.
|
||||
pub const VERSION_2: u8 = 2;
|
||||
/// Version 1 added the timers in End of Data and router keys.
|
||||
pub const VERSION_1: u8 = 1;
|
||||
/// What the oldest validators speak.
|
||||
pub const VERSION_0: u8 = 0;
|
||||
|
||||
const HEADER_LEN: usize = 8;
|
||||
|
||||
/// The longest PDU this client reads. Every PDU but Error Report, Router
|
||||
/// Key and ASPA has a fixed, small length. An ASPA of this length lists
|
||||
/// sixteen thousand providers, more than validators let an AS have.
|
||||
pub const MAX_PDU_LEN: usize = 64 * 1024;
|
||||
|
||||
mod pdu_type {
|
||||
pub const SERIAL_NOTIFY: u8 = 0;
|
||||
pub const SERIAL_QUERY: u8 = 1;
|
||||
pub const RESET_QUERY: u8 = 2;
|
||||
pub const CACHE_RESPONSE: u8 = 3;
|
||||
pub const IPV4_PREFIX: u8 = 4;
|
||||
pub const IPV6_PREFIX: u8 = 6;
|
||||
pub const END_OF_DATA: u8 = 7;
|
||||
pub const CACHE_RESET: u8 = 8;
|
||||
pub const ROUTER_KEY: u8 = 9;
|
||||
pub const ERROR_REPORT: u8 = 10;
|
||||
pub const ASPA: u8 = 11;
|
||||
}
|
||||
|
||||
/// Error codes of an Error Report (RFC 8210 §12; from 9 on, version 2).
|
||||
pub mod error_code {
|
||||
pub const CORRUPT_DATA: u16 = 0;
|
||||
pub const INTERNAL_ERROR: u16 = 1;
|
||||
pub const NO_DATA_AVAILABLE: u16 = 2;
|
||||
pub const INVALID_REQUEST: u16 = 3;
|
||||
pub const UNSUPPORTED_VERSION: u16 = 4;
|
||||
pub const UNSUPPORTED_PDU_TYPE: u16 = 5;
|
||||
pub const WITHDRAWAL_OF_UNKNOWN_RECORD: u16 = 6;
|
||||
pub const DUPLICATE_ANNOUNCEMENT: u16 = 7;
|
||||
pub const UNEXPECTED_VERSION: u16 = 8;
|
||||
pub const ASPA_PROVIDER_LIST: u16 = 9;
|
||||
pub const TRANSPORT_FAILURE: u16 = 10;
|
||||
pub const ORDERING: u16 = 11;
|
||||
}
|
||||
|
||||
/// The timers a validator tells its routers to use (RFC 8210 §6), in
|
||||
/// seconds.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Timers {
|
||||
/// How long to wait before asking for changes unprompted.
|
||||
pub refresh: u32,
|
||||
/// How long to wait before trying again after a failure.
|
||||
pub retry: u32,
|
||||
/// How long the data may be used without a successful update.
|
||||
pub expire: u32,
|
||||
}
|
||||
|
||||
impl Default for Timers {
|
||||
/// What RFC 8210 §6 recommends, and what a version 0 session uses.
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
refresh: 3600,
|
||||
retry: 600,
|
||||
expire: 7200,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One PDU.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Pdu {
|
||||
/// The validator has new data, up to `serial`.
|
||||
SerialNotify { session_id: u16, serial: u32 },
|
||||
/// Send what has changed since `serial`.
|
||||
SerialQuery { session_id: u16, serial: u32 },
|
||||
/// Send everything.
|
||||
ResetQuery,
|
||||
/// The answer to a query begins: prefixes follow, then End of Data.
|
||||
CacheResponse { session_id: u16 },
|
||||
/// A VRP, announced or withdrawn.
|
||||
Prefix { announce: bool, vrp: Vrp },
|
||||
/// The answer is complete, and the data is now at `serial`. `timers`
|
||||
/// are there from version 1 on.
|
||||
EndOfData {
|
||||
session_id: u16,
|
||||
serial: u32,
|
||||
timers: Option<Timers>,
|
||||
},
|
||||
/// The validator cannot say what changed since the serial asked about;
|
||||
/// ask for everything.
|
||||
CacheReset,
|
||||
/// A BGPsec router key. Not used here; read so as to be skipped.
|
||||
RouterKey,
|
||||
/// The ASPA of `customer`, whole (version 2). Announced, it takes the
|
||||
/// place of any the cache gave for that customer before, and lists at
|
||||
/// least one provider; withdrawn, it lists none.
|
||||
Aspa {
|
||||
announce: bool,
|
||||
customer: u32,
|
||||
providers: Vec<u32>,
|
||||
},
|
||||
/// Something went wrong at the sender's end, or with what we sent.
|
||||
ErrorReport { code: u16, text: String },
|
||||
}
|
||||
|
||||
/// Bytes that are not a PDU this client can read.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
|
||||
pub enum RtrError {
|
||||
#[error("PDU of type {pdu_type} has length {length}, which is not valid for it")]
|
||||
Length { pdu_type: u8, length: u32 },
|
||||
#[error("unknown PDU type {0}")]
|
||||
UnknownType(u8),
|
||||
#[error("protocol version {0} is not supported")]
|
||||
Version(u8),
|
||||
#[error("a prefix PDU with prefix length {length} and maximum length {max_length}")]
|
||||
Prefix { length: u8, max_length: u8 },
|
||||
#[error("an ASPA of AS{customer} is announced with no providers")]
|
||||
AspaWithoutProviders { customer: u32 },
|
||||
}
|
||||
|
||||
impl RtrError {
|
||||
/// The error code that reports this to the sender (RFC 8210 §12).
|
||||
pub fn code(&self) -> u16 {
|
||||
match self {
|
||||
Self::Length { .. } | Self::Prefix { .. } => error_code::CORRUPT_DATA,
|
||||
Self::UnknownType(_) => error_code::UNSUPPORTED_PDU_TYPE,
|
||||
Self::Version(_) => error_code::UNSUPPORTED_VERSION,
|
||||
Self::AspaWithoutProviders { .. } => error_code::ASPA_PROVIDER_LIST,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn be_u16(bytes: &[u8]) -> u16 {
|
||||
u16::from_be_bytes([bytes[0], bytes[1]])
|
||||
}
|
||||
|
||||
fn be_u32(bytes: &[u8]) -> u32 {
|
||||
u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])
|
||||
}
|
||||
|
||||
impl Pdu {
|
||||
/// Reads the PDU at the front of `buf`, if all of it has arrived:
|
||||
/// the protocol version it was sent with, the PDU, and how many octets
|
||||
/// it took. `Ok(None)` means more octets are needed.
|
||||
pub fn decode(buf: &[u8]) -> Result<Option<(u8, Self, usize)>, RtrError> {
|
||||
let Some(header) = buf.first_chunk::<HEADER_LEN>() else {
|
||||
return Ok(None);
|
||||
};
|
||||
let (version, pdu_type, flags) = (header[0], header[1], header[2]);
|
||||
let field = be_u16(&header[2..]);
|
||||
let length = be_u32(&header[4..]);
|
||||
let bad_length = || RtrError::Length { pdu_type, length };
|
||||
|
||||
if version > VERSION_2 {
|
||||
return Err(RtrError::Version(version));
|
||||
}
|
||||
let total = usize::try_from(length).map_err(|_| bad_length())?;
|
||||
if !(HEADER_LEN..=MAX_PDU_LEN).contains(&total) {
|
||||
return Err(bad_length());
|
||||
}
|
||||
// The fixed lengths are checked before waiting for the rest, so
|
||||
// that a wrong one is not waited for.
|
||||
let fixed = match pdu_type {
|
||||
pdu_type::SERIAL_NOTIFY | pdu_type::SERIAL_QUERY => Some(12),
|
||||
pdu_type::RESET_QUERY | pdu_type::CACHE_RESPONSE | pdu_type::CACHE_RESET => Some(8),
|
||||
pdu_type::IPV4_PREFIX => Some(20),
|
||||
pdu_type::IPV6_PREFIX => Some(32),
|
||||
pdu_type::END_OF_DATA => Some(if version == VERSION_0 { 12 } else { 24 }),
|
||||
pdu_type::ROUTER_KEY | pdu_type::ERROR_REPORT => None,
|
||||
pdu_type::ASPA if version >= VERSION_2 => {
|
||||
// The customer, and whole AS numbers after it; none of
|
||||
// them in a withdrawal.
|
||||
let announce = flags & 1 == 1;
|
||||
if total < 12 || !total.is_multiple_of(4) || (!announce && total != 12) {
|
||||
return Err(bad_length());
|
||||
}
|
||||
None
|
||||
}
|
||||
other => return Err(RtrError::UnknownType(other)),
|
||||
};
|
||||
if fixed.is_some_and(|fixed| fixed != total) {
|
||||
return Err(bad_length());
|
||||
}
|
||||
let Some(body) = buf.get(HEADER_LEN..total) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let pdu = match pdu_type {
|
||||
pdu_type::SERIAL_NOTIFY => Self::SerialNotify {
|
||||
session_id: field,
|
||||
serial: be_u32(body),
|
||||
},
|
||||
pdu_type::SERIAL_QUERY => Self::SerialQuery {
|
||||
session_id: field,
|
||||
serial: be_u32(body),
|
||||
},
|
||||
pdu_type::RESET_QUERY => Self::ResetQuery,
|
||||
pdu_type::CACHE_RESPONSE => Self::CacheResponse { session_id: field },
|
||||
pdu_type::IPV4_PREFIX | pdu_type::IPV6_PREFIX => {
|
||||
let version = if pdu_type == pdu_type::IPV4_PREFIX {
|
||||
IpVersion::Ipv4
|
||||
} else {
|
||||
IpVersion::Ipv6
|
||||
};
|
||||
let (flags, length, max_length) = (body[0], body[1], body[2]);
|
||||
let address = &body[4..body.len() - 4];
|
||||
let bad_prefix = || RtrError::Prefix { length, max_length };
|
||||
if max_length < length || max_length > version.bits() {
|
||||
return Err(bad_prefix());
|
||||
}
|
||||
Self::Prefix {
|
||||
announce: flags & 1 == 1,
|
||||
vrp: Vrp {
|
||||
prefix: IpPrefix::new(version, address, length)
|
||||
.map_err(|_| bad_prefix())?,
|
||||
max_length,
|
||||
asn: be_u32(&body[body.len() - 4..]),
|
||||
},
|
||||
}
|
||||
}
|
||||
pdu_type::END_OF_DATA => Self::EndOfData {
|
||||
session_id: field,
|
||||
serial: be_u32(body),
|
||||
timers: (version != VERSION_0).then(|| Timers {
|
||||
refresh: be_u32(&body[4..]),
|
||||
retry: be_u32(&body[8..]),
|
||||
expire: be_u32(&body[12..]),
|
||||
}),
|
||||
},
|
||||
pdu_type::CACHE_RESET => Self::CacheReset,
|
||||
pdu_type::ROUTER_KEY => Self::RouterKey,
|
||||
pdu_type::ASPA => {
|
||||
let announce = flags & 1 == 1;
|
||||
let (customer, providers) = body.split_at(4);
|
||||
let customer = be_u32(customer);
|
||||
if announce && providers.is_empty() {
|
||||
return Err(RtrError::AspaWithoutProviders { customer });
|
||||
}
|
||||
Self::Aspa {
|
||||
announce,
|
||||
customer,
|
||||
providers: providers
|
||||
.as_chunks::<4>()
|
||||
.0
|
||||
.iter()
|
||||
.map(|asn| u32::from_be_bytes(*asn))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
pdu_type::ERROR_REPORT => {
|
||||
// The length of the PDU that was at fault, that PDU, the
|
||||
// length of the text, the text.
|
||||
let text = (|| {
|
||||
let pdu_len = usize::try_from(be_u32(body.get(..4)?)).ok()?;
|
||||
let rest = body.get(4usize.checked_add(pdu_len)?..)?;
|
||||
let text_len = usize::try_from(be_u32(rest.get(..4)?)).ok()?;
|
||||
let text = rest.get(4..)?;
|
||||
(text.len() == text_len).then(|| String::from_utf8_lossy(text).into_owned())
|
||||
})()
|
||||
.ok_or_else(bad_length)?;
|
||||
Self::ErrorReport { code: field, text }
|
||||
}
|
||||
_ => unreachable!("unknown types were refused above"),
|
||||
};
|
||||
Ok(Some((version, pdu, total)))
|
||||
}
|
||||
|
||||
/// Writes the PDU, for a session that speaks protocol version
|
||||
/// `version`, to the end of `out`. Whether that version has such a PDU
|
||||
/// is the caller's to know.
|
||||
pub fn encode(&self, version: u8, out: &mut BytesMut) {
|
||||
let start = out.len();
|
||||
let header = |out: &mut BytesMut, pdu_type: u8, field: u16| {
|
||||
out.put_u8(version);
|
||||
out.put_u8(pdu_type);
|
||||
out.put_u16(field);
|
||||
// The length, filled in below.
|
||||
out.put_u32(0);
|
||||
};
|
||||
match self {
|
||||
Self::SerialNotify { session_id, serial } => {
|
||||
header(out, pdu_type::SERIAL_NOTIFY, *session_id);
|
||||
out.put_u32(*serial);
|
||||
}
|
||||
Self::SerialQuery { session_id, serial } => {
|
||||
header(out, pdu_type::SERIAL_QUERY, *session_id);
|
||||
out.put_u32(*serial);
|
||||
}
|
||||
Self::ResetQuery => header(out, pdu_type::RESET_QUERY, 0),
|
||||
Self::CacheResponse { session_id } => {
|
||||
header(out, pdu_type::CACHE_RESPONSE, *session_id);
|
||||
}
|
||||
Self::Prefix { announce, vrp } => {
|
||||
let (pdu_type, address_len) = match vrp.prefix.address_family() {
|
||||
IpVersion::Ipv4 => (pdu_type::IPV4_PREFIX, 4),
|
||||
IpVersion::Ipv6 => (pdu_type::IPV6_PREFIX, 16),
|
||||
};
|
||||
header(out, pdu_type, 0);
|
||||
out.put_u8(u8::from(*announce));
|
||||
out.put_u8(vrp.prefix.length());
|
||||
out.put_u8(vrp.max_length);
|
||||
out.put_u8(0);
|
||||
let mut address = [0u8; 16];
|
||||
address[..vrp.prefix.bytes().len()].copy_from_slice(vrp.prefix.bytes());
|
||||
out.put_slice(&address[..address_len]);
|
||||
out.put_u32(vrp.asn);
|
||||
}
|
||||
Self::EndOfData {
|
||||
session_id,
|
||||
serial,
|
||||
timers,
|
||||
} => {
|
||||
header(out, pdu_type::END_OF_DATA, *session_id);
|
||||
out.put_u32(*serial);
|
||||
if version != VERSION_0 {
|
||||
let timers = timers.unwrap_or_default();
|
||||
out.put_u32(timers.refresh);
|
||||
out.put_u32(timers.retry);
|
||||
out.put_u32(timers.expire);
|
||||
}
|
||||
}
|
||||
Self::CacheReset => header(out, pdu_type::CACHE_RESET, 0),
|
||||
Self::RouterKey => header(out, pdu_type::ROUTER_KEY, 0),
|
||||
Self::Aspa {
|
||||
announce,
|
||||
customer,
|
||||
providers,
|
||||
} => {
|
||||
header(out, pdu_type::ASPA, u16::from(*announce) << 8);
|
||||
out.put_u32(*customer);
|
||||
for provider in providers {
|
||||
out.put_u32(*provider);
|
||||
}
|
||||
}
|
||||
Self::ErrorReport { code, text } => {
|
||||
header(out, pdu_type::ERROR_REPORT, *code);
|
||||
// No copy of the PDU at fault.
|
||||
out.put_u32(0);
|
||||
out.put_u32(text.len() as u32);
|
||||
out.put_slice(text.as_bytes());
|
||||
}
|
||||
}
|
||||
let length = (out.len() - start) as u32;
|
||||
out[start + 4..start + 8].copy_from_slice(&length.to_be_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use proptest::prelude::*;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn decode_one(bytes: &[u8]) -> (u8, Pdu) {
|
||||
let (version, pdu, used) = Pdu::decode(bytes).unwrap().expect("a whole PDU");
|
||||
assert_eq!(used, bytes.len());
|
||||
(version, pdu)
|
||||
}
|
||||
|
||||
/// The PDUs as RFC 8210 §5 draws them.
|
||||
#[test]
|
||||
fn pdus_are_read_as_the_rfc_lays_them_out() {
|
||||
#[rustfmt::skip]
|
||||
let ipv4 = [
|
||||
1, 4, 0, 0, 0, 0, 0, 20,
|
||||
1, 21, 24, 0,
|
||||
193, 0, 0, 0,
|
||||
0, 0, 0x0d, 0x05,
|
||||
];
|
||||
assert_eq!(
|
||||
decode_one(&ipv4),
|
||||
(
|
||||
1,
|
||||
Pdu::Prefix {
|
||||
announce: true,
|
||||
vrp: Vrp {
|
||||
prefix: IpPrefix::try_from("193.0.0.0/21").unwrap(),
|
||||
max_length: 24,
|
||||
asn: 3333,
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
#[rustfmt::skip]
|
||||
let ipv6 = [
|
||||
1, 6, 0, 0, 0, 0, 0, 32,
|
||||
0, 48, 48, 0,
|
||||
0x20, 0x01, 0x06, 0x7c, 0x02, 0xe8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0x0d, 0x05,
|
||||
];
|
||||
assert_eq!(
|
||||
decode_one(&ipv6),
|
||||
(
|
||||
1,
|
||||
Pdu::Prefix {
|
||||
announce: false,
|
||||
vrp: Vrp {
|
||||
prefix: IpPrefix::try_from("2001:67c:2e8::/48").unwrap(),
|
||||
max_length: 48,
|
||||
asn: 3333,
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
#[rustfmt::skip]
|
||||
let end_of_data = [
|
||||
1, 7, 0x12, 0x34, 0, 0, 0, 24,
|
||||
0, 0, 0, 99,
|
||||
0, 0, 0x0e, 0x10, 0, 0, 0x02, 0x58, 0, 0, 0x1c, 0x20,
|
||||
];
|
||||
assert_eq!(
|
||||
decode_one(&end_of_data),
|
||||
(
|
||||
1,
|
||||
Pdu::EndOfData {
|
||||
session_id: 0x1234,
|
||||
serial: 99,
|
||||
timers: Some(Timers::default()),
|
||||
}
|
||||
)
|
||||
);
|
||||
// Version 0 has no timers.
|
||||
assert_eq!(
|
||||
decode_one(&[0, 7, 0x12, 0x34, 0, 0, 0, 12, 0, 0, 0, 99]),
|
||||
(
|
||||
0,
|
||||
Pdu::EndOfData {
|
||||
session_id: 0x1234,
|
||||
serial: 99,
|
||||
timers: None,
|
||||
}
|
||||
)
|
||||
);
|
||||
// Version 2's ASPA, as draft-ietf-sidrops-8210bis §5.12 draws it.
|
||||
#[rustfmt::skip]
|
||||
let aspa = [
|
||||
2, 11, 1, 0, 0, 0, 0, 20,
|
||||
0, 0, 0x0d, 0x05,
|
||||
0, 0, 0x04, 0x4d, 0, 0, 0x0c, 0xb9,
|
||||
];
|
||||
assert_eq!(
|
||||
decode_one(&aspa),
|
||||
(
|
||||
2,
|
||||
Pdu::Aspa {
|
||||
announce: true,
|
||||
customer: 3333,
|
||||
providers: vec![1101, 3257],
|
||||
}
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
decode_one(&[2, 11, 0, 0, 0, 0, 0, 12, 0, 0, 0x0d, 0x05]),
|
||||
(
|
||||
2,
|
||||
Pdu::Aspa {
|
||||
announce: false,
|
||||
customer: 3333,
|
||||
providers: vec![],
|
||||
}
|
||||
)
|
||||
);
|
||||
#[rustfmt::skip]
|
||||
let error = [
|
||||
1, 10, 0, 2, 0, 0, 0, 30,
|
||||
0, 0, 0, 8, 1, 1, 0, 0, 0, 0, 0, 12,
|
||||
0, 0, 0, 6, b'n', b'o', b' ', b'd', b'a', b't',
|
||||
];
|
||||
assert_eq!(
|
||||
decode_one(&error),
|
||||
(
|
||||
1,
|
||||
Pdu::ErrorReport {
|
||||
code: error_code::NO_DATA_AVAILABLE,
|
||||
text: "no dat".into(),
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_pdu_that_has_not_all_arrived_is_waited_for() {
|
||||
let mut wire = BytesMut::new();
|
||||
Pdu::SerialNotify {
|
||||
session_id: 7,
|
||||
serial: 42,
|
||||
}
|
||||
.encode(VERSION_1, &mut wire);
|
||||
for cut in 0..wire.len() {
|
||||
assert_eq!(Pdu::decode(&wire[..cut]), Ok(None), "{cut} octets");
|
||||
}
|
||||
assert!(Pdu::decode(&wire).unwrap().is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn what_is_not_a_pdu_is_refused_from_its_header() {
|
||||
// A version from the future.
|
||||
assert_eq!(
|
||||
Pdu::decode(&[3, 2, 0, 0, 0, 0, 0, 8]),
|
||||
Err(RtrError::Version(3))
|
||||
);
|
||||
assert_eq!(
|
||||
Pdu::decode(&[1, 99, 0, 0, 0, 0, 0, 8]),
|
||||
Err(RtrError::UnknownType(99))
|
||||
);
|
||||
// A Reset Query that claims to be long, and a length no PDU has:
|
||||
// refused without waiting for the octets.
|
||||
for length in [9u32, 7, 0, 1 << 30] {
|
||||
let mut header = vec![1, 2, 0, 0];
|
||||
header.extend_from_slice(&length.to_be_bytes());
|
||||
assert_eq!(
|
||||
Pdu::decode(&header),
|
||||
Err(RtrError::Length {
|
||||
pdu_type: 2,
|
||||
length
|
||||
}),
|
||||
);
|
||||
}
|
||||
// A maximum length shorter than the prefix, and longer than an
|
||||
// address.
|
||||
for (length, max_length) in [(24, 21), (24, 33)] {
|
||||
#[rustfmt::skip]
|
||||
let prefix = [
|
||||
1, 4, 0, 0, 0, 0, 0, 20,
|
||||
1, length, max_length, 0, 193, 0, 0, 0, 0, 0, 0, 1,
|
||||
];
|
||||
assert_eq!(
|
||||
Pdu::decode(&prefix),
|
||||
Err(RtrError::Prefix { length, max_length })
|
||||
);
|
||||
}
|
||||
// An ASPA before version 2 had them.
|
||||
assert_eq!(
|
||||
Pdu::decode(&[1, 11, 0, 0, 0, 0, 0, 12, 0, 0, 0, 1]),
|
||||
Err(RtrError::UnknownType(11))
|
||||
);
|
||||
// ASPAs with half an AS number, without a customer, withdrawn
|
||||
// with providers: refused from the header.
|
||||
for (flags, length) in [(1u8, 18u32), (1, 8), (0, 16)] {
|
||||
let mut header = vec![2, 11, flags, 0];
|
||||
header.extend_from_slice(&length.to_be_bytes());
|
||||
assert_eq!(
|
||||
Pdu::decode(&header),
|
||||
Err(RtrError::Length {
|
||||
pdu_type: 11,
|
||||
length
|
||||
}),
|
||||
);
|
||||
}
|
||||
// Announced with no providers.
|
||||
assert_eq!(
|
||||
Pdu::decode(&[2, 11, 1, 0, 0, 0, 0, 12, 0, 0, 0xfd, 0xe8]),
|
||||
Err(RtrError::AspaWithoutProviders { customer: 65000 })
|
||||
);
|
||||
// An Error Report whose inner lengths do not add up.
|
||||
#[rustfmt::skip]
|
||||
let error = [1, 10, 0, 2, 0, 0, 0, 16, 0, 0, 0, 9, 0, 0, 0, 0];
|
||||
assert!(matches!(Pdu::decode(&error), Err(RtrError::Length { .. })));
|
||||
}
|
||||
|
||||
fn any_prefix() -> impl Strategy<Value = Vrp> {
|
||||
(
|
||||
any::<bool>(),
|
||||
any::<[u8; 16]>(),
|
||||
0u8..=128,
|
||||
0u8..=128,
|
||||
any::<u32>(),
|
||||
)
|
||||
.prop_map(|(v6, bytes, length, extra, asn)| {
|
||||
let version = if v6 { IpVersion::Ipv6 } else { IpVersion::Ipv4 };
|
||||
let length = length.min(version.bits());
|
||||
Vrp {
|
||||
prefix: IpPrefix::new(version, bytes, length).unwrap(),
|
||||
max_length: length.saturating_add(extra).min(version.bits()),
|
||||
asn,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn any_pdu() -> impl Strategy<Value = Pdu> {
|
||||
prop_oneof![
|
||||
(any::<u16>(), any::<u32>())
|
||||
.prop_map(|(session_id, serial)| Pdu::SerialNotify { session_id, serial }),
|
||||
(any::<u16>(), any::<u32>())
|
||||
.prop_map(|(session_id, serial)| Pdu::SerialQuery { session_id, serial }),
|
||||
Just(Pdu::ResetQuery),
|
||||
any::<u16>().prop_map(|session_id| Pdu::CacheResponse { session_id }),
|
||||
(any::<bool>(), any_prefix()).prop_map(|(announce, vrp)| Pdu::Prefix { announce, vrp }),
|
||||
(any::<u16>(), any::<u32>(), any::<[u32; 3]>()).prop_map(
|
||||
|(session_id, serial, [refresh, retry, expire])| Pdu::EndOfData {
|
||||
session_id,
|
||||
serial,
|
||||
timers: Some(Timers {
|
||||
refresh,
|
||||
retry,
|
||||
expire
|
||||
}),
|
||||
}
|
||||
),
|
||||
Just(Pdu::CacheReset),
|
||||
(any::<u16>(), "[ -~]{0,60}").prop_map(|(code, text)| Pdu::ErrorReport { code, text }),
|
||||
]
|
||||
}
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
fn a_pdu_that_is_written_reads_back_equal(pdu in any_pdu(), version in 0u8..=2) {
|
||||
let mut wire = BytesMut::new();
|
||||
pdu.encode(version, &mut wire);
|
||||
// Version 0 does not carry the timers.
|
||||
let expected = match (&pdu, version) {
|
||||
(Pdu::EndOfData { session_id, serial, .. }, VERSION_0) => Pdu::EndOfData {
|
||||
session_id: *session_id,
|
||||
serial: *serial,
|
||||
timers: None,
|
||||
},
|
||||
_ => pdu.clone(),
|
||||
};
|
||||
prop_assert_eq!(Pdu::decode(&wire), Ok(Some((version, expected, wire.len()))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_aspa_that_is_written_reads_back_equal(
|
||||
customer: u32,
|
||||
providers in proptest::collection::vec(any::<u32>(), 0..40),
|
||||
) {
|
||||
let pdu = Pdu::Aspa { announce: !providers.is_empty(), customer, providers };
|
||||
let mut wire = BytesMut::new();
|
||||
pdu.encode(VERSION_2, &mut wire);
|
||||
prop_assert_eq!(Pdu::decode(&wire), Ok(Some((VERSION_2, pdu, wire.len()))));
|
||||
}
|
||||
|
||||
/// Whatever arrives, decoding returns: a PDU, a wait for more, or
|
||||
/// an error. What it reads is within what arrived.
|
||||
#[test]
|
||||
fn noise_is_never_more_than_an_error(bytes in proptest::collection::vec(any::<u8>(), 0..80)) {
|
||||
if let Ok(Some((_, _, used))) = Pdu::decode(&bytes) {
|
||||
prop_assert!(used <= bytes.len());
|
||||
}
|
||||
}
|
||||
|
||||
/// Noise behind a header that could be a PDU's, to get past it.
|
||||
#[test]
|
||||
fn noise_behind_a_plausible_header_is_never_more_than_an_error(
|
||||
version in 0u8..=2,
|
||||
pdu_type in 0u8..13,
|
||||
field: u16,
|
||||
length in 8u32..64,
|
||||
body in proptest::collection::vec(any::<u8>(), 0..64),
|
||||
) {
|
||||
let mut bytes = vec![version, pdu_type];
|
||||
bytes.extend_from_slice(&field.to_be_bytes());
|
||||
bytes.extend_from_slice(&length.to_be_bytes());
|
||||
bytes.extend_from_slice(&body);
|
||||
if let Ok(Some((_, _, used))) = Pdu::decode(&bytes) {
|
||||
prop_assert!(used <= bytes.len());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,429 +0,0 @@
|
||||
//! The validated ROA payloads, and what they say about a route.
|
||||
|
||||
use bgp_primitives::constants::IpVersion;
|
||||
use bgp_primitives::ip_prefix::IpPrefix;
|
||||
|
||||
/// A validated ROA payload: `asn` may originate `prefix`, and any prefix
|
||||
/// inside it of at most `max_length` bits.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub struct Vrp {
|
||||
pub prefix: IpPrefix,
|
||||
pub max_length: u8,
|
||||
pub asn: u32,
|
||||
}
|
||||
|
||||
/// What the VRPs say about a route (RFC 6811 §2).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum Validity {
|
||||
/// No VRP covers the route's prefix: nothing is known about it. Most
|
||||
/// of the Internet's routes are in this state, and are used.
|
||||
NotFound,
|
||||
/// A VRP covers the prefix and allows this origin at this length.
|
||||
Valid,
|
||||
/// VRPs cover the prefix and none of them allows this origin at this
|
||||
/// length: the holder of the addresses has said who may announce
|
||||
/// them, and this is not it.
|
||||
Invalid,
|
||||
}
|
||||
|
||||
impl Validity {
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::NotFound => "not found",
|
||||
Self::Valid => "valid",
|
||||
Self::Invalid => "invalid",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A set of VRPs, arranged for asking about routes: this is asked for
|
||||
/// every route received.
|
||||
///
|
||||
/// It is built once and not changed. When the VRPs change, another is built
|
||||
/// and takes its place.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct VrpTable {
|
||||
v4: Trie,
|
||||
v6: Trie,
|
||||
len: usize,
|
||||
}
|
||||
|
||||
/// A binary trie over prefixes: a node for each prefix that is a VRP's or
|
||||
/// lies on the way to one.
|
||||
#[derive(Debug)]
|
||||
struct Trie {
|
||||
nodes: Vec<Node>,
|
||||
/// What the VRPs of each node allow, in runs by node: (maximum length,
|
||||
/// origin AS).
|
||||
allowed: Vec<(u8, u32)>,
|
||||
/// A short cut past the first sixteen levels, which every lookup of a
|
||||
/// longer prefix would otherwise walk: for each value of an address's
|
||||
/// first sixteen bits, the node those bits lead to, or zero if there
|
||||
/// is none. [`WALK`] is set where a VRP lies on the way there, which
|
||||
/// is rare; such lookups walk from the root.
|
||||
jump: Box<[u32]>,
|
||||
}
|
||||
|
||||
/// How many leading bits [`Trie::jump`] is indexed by.
|
||||
const JUMP_BITS: u8 = 16;
|
||||
/// Set in a [`Trie::jump`] entry: walk from the root.
|
||||
const WALK: u32 = 1 << 31;
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
struct Node {
|
||||
/// The nodes for this prefix extended by a zero bit and by a one bit;
|
||||
/// zero where there is none (the root is nobody's child).
|
||||
children: [u32; 2],
|
||||
/// Where in `allowed` this prefix's VRPs are, and how many.
|
||||
first: u32,
|
||||
count: u32,
|
||||
}
|
||||
|
||||
impl Default for Trie {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
nodes: vec![Node::default()],
|
||||
allowed: Vec::new(),
|
||||
jump: vec![0; 1 << JUMP_BITS].into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The bit of `bytes` at `index`, counting from the most significant bit
|
||||
/// of the first byte.
|
||||
fn bit(bytes: &[u8], index: u8) -> usize {
|
||||
usize::from(bytes[usize::from(index / 8)] >> (7 - index % 8) & 1)
|
||||
}
|
||||
|
||||
impl Trie {
|
||||
/// `vrps` must be sorted, so that those of one prefix are together.
|
||||
fn build(vrps: &[Vrp]) -> Self {
|
||||
let mut trie = Self::default();
|
||||
for group in vrps.chunk_by(|a, b| a.prefix == b.prefix) {
|
||||
let prefix = &group[0].prefix;
|
||||
let mut at = 0usize;
|
||||
for index in 0..prefix.length() {
|
||||
let side = bit(prefix.bytes(), index);
|
||||
at = match trie.nodes[at].children[side] {
|
||||
0 => {
|
||||
trie.nodes.push(Node::default());
|
||||
let new = trie.nodes.len() - 1;
|
||||
trie.nodes[at].children[side] = new as u32;
|
||||
new
|
||||
}
|
||||
child => child as usize,
|
||||
};
|
||||
}
|
||||
trie.nodes[at].first = trie.allowed.len() as u32;
|
||||
trie.nodes[at].count = group.len() as u32;
|
||||
trie.allowed
|
||||
.extend(group.iter().map(|vrp| (vrp.max_length, vrp.asn)));
|
||||
}
|
||||
assert!(
|
||||
trie.nodes.len() < WALK as usize,
|
||||
"more VRPs than a table can hold"
|
||||
);
|
||||
for slot in 0..=u16::MAX {
|
||||
trie.jump[usize::from(slot)] = trie.jump_for(slot);
|
||||
}
|
||||
trie
|
||||
}
|
||||
|
||||
/// The [`Trie::jump`] entry for addresses that begin with `bits`.
|
||||
fn jump_for(&self, bits: u16) -> u32 {
|
||||
let bytes = bits.to_be_bytes();
|
||||
let mut at = 0usize;
|
||||
for index in 0..JUMP_BITS {
|
||||
if self.nodes[at].count > 0 {
|
||||
return WALK;
|
||||
}
|
||||
match self.nodes[at].children[bit(&bytes, index)] {
|
||||
0 => return 0,
|
||||
child => at = child as usize,
|
||||
}
|
||||
}
|
||||
at as u32
|
||||
}
|
||||
|
||||
/// Calls `visit` with what the VRPs of each prefix that covers `prefix`
|
||||
/// allow, from the shortest; stops when `visit` returns true, and
|
||||
/// returns whether it did.
|
||||
fn covering(&self, prefix: &IpPrefix, mut visit: impl FnMut(&[(u8, u32)]) -> bool) -> bool {
|
||||
let bytes = prefix.bytes();
|
||||
let mut at = 0usize;
|
||||
let mut index = 0;
|
||||
if let [first, second, ..] = bytes
|
||||
&& prefix.length() >= JUMP_BITS
|
||||
{
|
||||
match self.jump[usize::from(u16::from_be_bytes([*first, *second]))] {
|
||||
WALK => {}
|
||||
// Nothing covers an address that begins like this.
|
||||
0 => return false,
|
||||
node => (at, index) = (node as usize, JUMP_BITS),
|
||||
}
|
||||
}
|
||||
loop {
|
||||
let node = &self.nodes[at];
|
||||
if node.count > 0 {
|
||||
let first = node.first as usize;
|
||||
if visit(&self.allowed[first..first + node.count as usize]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if index == prefix.length() {
|
||||
return false;
|
||||
}
|
||||
match node.children[bit(bytes, index)] {
|
||||
0 => return false,
|
||||
child => at = child as usize,
|
||||
}
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl VrpTable {
|
||||
/// A table of `vrps`. Listing one more than once is the same as listing
|
||||
/// it once.
|
||||
pub fn new(vrps: impl IntoIterator<Item = Vrp>) -> Self {
|
||||
let mut vrps: Vec<Vrp> = vrps.into_iter().collect();
|
||||
vrps.sort_unstable();
|
||||
vrps.dedup();
|
||||
let v6_from = vrps.partition_point(|vrp| vrp.prefix.address_family() == IpVersion::Ipv4);
|
||||
debug_assert!(
|
||||
vrps[..v6_from]
|
||||
.iter()
|
||||
.all(|vrp| vrp.prefix.address_family() == IpVersion::Ipv4)
|
||||
);
|
||||
Self {
|
||||
v4: Trie::build(&vrps[..v6_from]),
|
||||
v6: Trie::build(&vrps[v6_from..]),
|
||||
len: vrps.len(),
|
||||
}
|
||||
}
|
||||
|
||||
/// How many VRPs the table holds.
|
||||
pub fn len(&self) -> usize {
|
||||
self.len
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.len == 0
|
||||
}
|
||||
|
||||
fn trie(&self, prefix: &IpPrefix) -> &Trie {
|
||||
match prefix.address_family() {
|
||||
IpVersion::Ipv4 => &self.v4,
|
||||
IpVersion::Ipv6 => &self.v6,
|
||||
}
|
||||
}
|
||||
|
||||
/// The validity of a route for `prefix` that originates from `origin`
|
||||
/// (RFC 6811 §2). `None` is a route with no one origin, as when its AS
|
||||
/// path ends in a set: no VRP can allow it.
|
||||
pub fn validate(&self, prefix: &IpPrefix, origin: Option<u32>) -> Validity {
|
||||
let mut covered = false;
|
||||
let valid = self.trie(prefix).covering(prefix, |allowed| {
|
||||
covered = true;
|
||||
// AS 0 in a VRP says that nobody may originate the prefix
|
||||
// (RFC 6483 §4); it is the origin of no route.
|
||||
origin.is_some_and(|origin| {
|
||||
origin != 0
|
||||
&& allowed
|
||||
.iter()
|
||||
.any(|(max_length, asn)| *asn == origin && prefix.length() <= *max_length)
|
||||
})
|
||||
});
|
||||
match (valid, covered) {
|
||||
(true, _) => Validity::Valid,
|
||||
(false, true) => Validity::Invalid,
|
||||
(false, false) => Validity::NotFound,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether some VRP covers `prefix`: whether the VRPs say anything
|
||||
/// about routes for it.
|
||||
pub fn covers(&self, prefix: &IpPrefix) -> bool {
|
||||
self.trie(prefix).covering(prefix, |_| true)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use proptest::prelude::*;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn prefix(s: &str) -> IpPrefix {
|
||||
IpPrefix::try_from(s).unwrap()
|
||||
}
|
||||
|
||||
fn vrp(p: &str, max_length: u8, asn: u32) -> Vrp {
|
||||
Vrp {
|
||||
prefix: prefix(p),
|
||||
max_length,
|
||||
asn,
|
||||
}
|
||||
}
|
||||
|
||||
/// The cases of RFC 6811 §2.
|
||||
#[test]
|
||||
fn a_route_is_valid_invalid_or_not_found() {
|
||||
let table = VrpTable::new([
|
||||
vrp("193.0.0.0/21", 24, 3333),
|
||||
vrp("193.0.0.0/21", 21, 2121),
|
||||
vrp("2001:67c:2e8::/48", 48, 3333),
|
||||
]);
|
||||
let validate = |p: &str, origin| table.validate(&prefix(p), Some(origin));
|
||||
|
||||
// Covered, by the right AS, no longer than allowed.
|
||||
assert_eq!(validate("193.0.0.0/21", 3333), Validity::Valid);
|
||||
assert_eq!(validate("193.0.4.0/24", 3333), Validity::Valid);
|
||||
assert_eq!(validate("193.0.0.0/21", 2121), Validity::Valid);
|
||||
assert_eq!(validate("2001:67c:2e8::/48", 3333), Validity::Valid);
|
||||
// The right AS, but more specific than it may announce.
|
||||
assert_eq!(validate("193.0.4.0/24", 2121), Validity::Invalid);
|
||||
assert_eq!(validate("193.0.4.0/25", 3333), Validity::Invalid);
|
||||
// Someone else's AS: a hijack, or a mistake.
|
||||
assert_eq!(validate("193.0.0.0/21", 15169), Validity::Invalid);
|
||||
assert_eq!(validate("2001:67c:2e8::/48", 15169), Validity::Invalid);
|
||||
// Nothing covers these: a shorter prefix, another block, another
|
||||
// family.
|
||||
assert_eq!(validate("193.0.0.0/20", 3333), Validity::NotFound);
|
||||
assert_eq!(validate("194.0.0.0/21", 3333), Validity::NotFound);
|
||||
assert_eq!(validate("2001:67c:2e9::/48", 3333), Validity::NotFound);
|
||||
|
||||
// A route with no one origin matches no VRP.
|
||||
assert_eq!(
|
||||
table.validate(&prefix("193.0.0.0/21"), None),
|
||||
Validity::Invalid
|
||||
);
|
||||
assert_eq!(
|
||||
table.validate(&prefix("194.0.0.0/21"), None),
|
||||
Validity::NotFound
|
||||
);
|
||||
assert_eq!(table.len(), 3);
|
||||
assert!(table.covers(&prefix("193.0.7.0/24")));
|
||||
assert!(!table.covers(&prefix("193.0.8.0/24")));
|
||||
}
|
||||
|
||||
/// A VRP for AS 0 says the prefix is not to be routed at all.
|
||||
#[test]
|
||||
fn as_zero_allows_nobody() {
|
||||
let table = VrpTable::new([vrp("203.0.113.0/24", 24, 0)]);
|
||||
assert_eq!(
|
||||
table.validate(&prefix("203.0.113.0/24"), Some(0)),
|
||||
Validity::Invalid
|
||||
);
|
||||
assert_eq!(
|
||||
table.validate(&prefix("203.0.113.0/24"), Some(64500)),
|
||||
Validity::Invalid
|
||||
);
|
||||
}
|
||||
|
||||
/// A VRP covers a route however many other VRPs cover it too: one that
|
||||
/// allows it is enough.
|
||||
#[test]
|
||||
fn any_covering_vrp_can_make_a_route_valid() {
|
||||
let table = VrpTable::new([
|
||||
vrp("10.0.0.0/8", 8, 1),
|
||||
vrp("10.1.0.0/16", 24, 2),
|
||||
vrp("0.0.0.0/0", 0, 3),
|
||||
]);
|
||||
assert_eq!(
|
||||
table.validate(&prefix("10.1.2.0/24"), Some(2)),
|
||||
Validity::Valid
|
||||
);
|
||||
assert_eq!(
|
||||
table.validate(&prefix("10.1.2.0/24"), Some(1)),
|
||||
Validity::Invalid
|
||||
);
|
||||
assert_eq!(
|
||||
table.validate(&prefix("10.0.0.0/8"), Some(1)),
|
||||
Validity::Valid
|
||||
);
|
||||
assert_eq!(
|
||||
table.validate(&prefix("0.0.0.0/0"), Some(3)),
|
||||
Validity::Valid
|
||||
);
|
||||
// The VRP for the default route covers everything.
|
||||
assert_eq!(
|
||||
table.validate(&prefix("192.0.2.0/24"), Some(9)),
|
||||
Validity::Invalid
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_empty_table_knows_nothing() {
|
||||
let table = VrpTable::default();
|
||||
assert!(table.is_empty());
|
||||
assert_eq!(
|
||||
table.validate(&prefix("193.0.0.0/21"), Some(3333)),
|
||||
Validity::NotFound
|
||||
);
|
||||
assert_eq!(table.validate(&prefix("::/0"), None), Validity::NotFound);
|
||||
}
|
||||
|
||||
fn any_vrp() -> impl Strategy<Value = Vrp> {
|
||||
// Few distinct addresses and AS numbers, so that routes and VRPs
|
||||
// meet.
|
||||
(any::<bool>(), 0u8..4, 0u8..=24, 0u8..=8, 0u32..4).prop_map(
|
||||
|(v6, address, length, extra, asn)| {
|
||||
let version = if v6 { IpVersion::Ipv6 } else { IpVersion::Ipv4 };
|
||||
let bytes = [
|
||||
address << 6 | address,
|
||||
address,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
];
|
||||
Vrp {
|
||||
prefix: IpPrefix::new(version, bytes, length).unwrap(),
|
||||
max_length: length + extra,
|
||||
asn,
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
proptest! {
|
||||
/// The table agrees with reading RFC 6811 out over the list.
|
||||
#[test]
|
||||
fn the_table_agrees_with_looking_through_the_list(
|
||||
vrps in proptest::collection::vec(any_vrp(), 0..30),
|
||||
routes in proptest::collection::vec((any_vrp(), any::<bool>()), 1..40),
|
||||
) {
|
||||
let table = VrpTable::new(vrps.iter().copied());
|
||||
for (route, has_origin) in routes {
|
||||
let origin = has_origin.then_some(route.asn);
|
||||
let covering: Vec<&Vrp> = vrps
|
||||
.iter()
|
||||
.filter(|vrp| vrp.prefix.contains(&route.prefix))
|
||||
.collect();
|
||||
let expected = if covering.is_empty() {
|
||||
Validity::NotFound
|
||||
} else if covering.iter().any(|vrp| {
|
||||
origin.is_some_and(|origin| origin != 0 && origin == vrp.asn)
|
||||
&& route.prefix.length() <= vrp.max_length
|
||||
}) {
|
||||
Validity::Valid
|
||||
} else {
|
||||
Validity::Invalid
|
||||
};
|
||||
prop_assert_eq!(table.validate(&route.prefix, origin), expected, "{:?}", route);
|
||||
prop_assert_eq!(table.covers(&route.prefix), !covering.is_empty());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,737 +0,0 @@
|
||||
//! The RTR client against a cache played by the test: what it asks for,
|
||||
//! what it makes of the answers, and what it does when the cache misbehaves
|
||||
//! or goes away.
|
||||
|
||||
use std::net::{Ipv4Addr, SocketAddr};
|
||||
use std::time::Duration;
|
||||
|
||||
use bgp_primitives::ip_prefix::IpPrefix;
|
||||
use bgp_rpki::rtr::{Pdu, Timers, error_code};
|
||||
use bgp_rpki::{Algorithm, CacheState, PathValidity, Rpki, RpkiConfig, Update, Validity, Vrp};
|
||||
use bytes::BytesMut;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::{TcpListener, TcpStream};
|
||||
use tokio::sync::watch;
|
||||
|
||||
const TIMEOUT: Duration = Duration::from_secs(5);
|
||||
const SESSION: u16 = 0x1234;
|
||||
/// The version of the protocol the client asks for first.
|
||||
const V: u8 = 2;
|
||||
|
||||
fn prefix(s: &str) -> IpPrefix {
|
||||
IpPrefix::try_from(s).unwrap()
|
||||
}
|
||||
|
||||
fn vrp(p: &str, max_length: u8, asn: u32) -> Vrp {
|
||||
Vrp {
|
||||
prefix: prefix(p),
|
||||
max_length,
|
||||
asn,
|
||||
}
|
||||
}
|
||||
|
||||
fn announce(vrp: Vrp) -> Pdu {
|
||||
Pdu::Prefix {
|
||||
announce: true,
|
||||
vrp,
|
||||
}
|
||||
}
|
||||
|
||||
fn withdraw(vrp: Vrp) -> Pdu {
|
||||
Pdu::Prefix {
|
||||
announce: false,
|
||||
vrp,
|
||||
}
|
||||
}
|
||||
|
||||
fn end_of_data(serial: u32) -> Pdu {
|
||||
Pdu::EndOfData {
|
||||
session_id: SESSION,
|
||||
serial,
|
||||
timers: Some(Timers::default()),
|
||||
}
|
||||
}
|
||||
|
||||
/// A cache played by the test.
|
||||
struct Cache {
|
||||
listener: TcpListener,
|
||||
}
|
||||
|
||||
/// One connection from the client to the cache.
|
||||
struct Connection {
|
||||
stream: TcpStream,
|
||||
buf: BytesMut,
|
||||
}
|
||||
|
||||
impl Cache {
|
||||
async fn start() -> Self {
|
||||
Self {
|
||||
listener: TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await.unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
fn address(&self) -> SocketAddr {
|
||||
self.listener.local_addr().unwrap()
|
||||
}
|
||||
|
||||
async fn accept(&self) -> Connection {
|
||||
let (stream, _) = tokio::time::timeout(TIMEOUT, self.listener.accept())
|
||||
.await
|
||||
.expect("the client connects")
|
||||
.unwrap();
|
||||
Connection {
|
||||
stream,
|
||||
buf: BytesMut::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Connection {
|
||||
/// The next PDU from the client and the version it was sent in.
|
||||
async fn received(&mut self) -> (u8, Pdu) {
|
||||
let reading = async {
|
||||
loop {
|
||||
if let Some((version, pdu, used)) = Pdu::decode(&self.buf).unwrap() {
|
||||
let _ = self.buf.split_to(used);
|
||||
return (version, pdu);
|
||||
}
|
||||
assert_ne!(
|
||||
self.stream.read_buf(&mut self.buf).await.unwrap(),
|
||||
0,
|
||||
"the client closed the connection"
|
||||
);
|
||||
}
|
||||
};
|
||||
tokio::time::timeout(TIMEOUT, reading)
|
||||
.await
|
||||
.expect("the client sends something")
|
||||
}
|
||||
|
||||
async fn send(&mut self, version: u8, pdus: &[Pdu]) {
|
||||
let mut out = BytesMut::new();
|
||||
for pdu in pdus {
|
||||
pdu.encode(version, &mut out);
|
||||
}
|
||||
self.stream.write_all(&out).await.unwrap();
|
||||
}
|
||||
|
||||
/// Answers a query with everything: `vrps`, at `serial`.
|
||||
async fn send_all(&mut self, vrps: &[Vrp], serial: u32) {
|
||||
let mut pdus = vec![Pdu::CacheResponse {
|
||||
session_id: SESSION,
|
||||
}];
|
||||
pdus.extend(vrps.iter().copied().map(announce));
|
||||
pdus.push(end_of_data(serial));
|
||||
self.send(V, &pdus).await;
|
||||
}
|
||||
}
|
||||
|
||||
fn client_of(caches: &[&Cache]) -> (Rpki, watch::Receiver<Update>) {
|
||||
let (rpki, _task) = Rpki::spawn(RpkiConfig {
|
||||
caches: caches.iter().map(|cache| cache.address()).collect(),
|
||||
retry: Some(Duration::from_millis(50)),
|
||||
..RpkiConfig::default()
|
||||
});
|
||||
let updates = rpki.subscribe();
|
||||
(rpki, updates)
|
||||
}
|
||||
|
||||
async fn next_update(updates: &mut watch::Receiver<Update>) -> Update {
|
||||
tokio::time::timeout(TIMEOUT, updates.changed())
|
||||
.await
|
||||
.expect("the VRPs change")
|
||||
.unwrap();
|
||||
updates.borrow_and_update().clone()
|
||||
}
|
||||
|
||||
/// The ordinary life of a session: everything once, then what changes.
|
||||
#[tokio::test]
|
||||
async fn the_client_takes_everything_once_and_then_the_changes() {
|
||||
let cache = Cache::start().await;
|
||||
let (rpki, mut updates) = client_of(&[&cache]);
|
||||
// Nothing is known until a cache has answered.
|
||||
assert_eq!(rpki.version(), 0);
|
||||
assert!(rpki.current().vrps.is_empty());
|
||||
|
||||
let mut connection = cache.accept().await;
|
||||
assert_eq!(connection.received().await, (V, Pdu::ResetQuery));
|
||||
let ripe = vrp("193.0.0.0/21", 24, 3333);
|
||||
let other = vrp("2001:67c:2e8::/48", 48, 3333);
|
||||
connection.send_all(&[ripe, other], 7).await;
|
||||
|
||||
let update = next_update(&mut updates).await;
|
||||
assert_eq!(update.version, 1);
|
||||
assert_eq!(rpki.version(), 1);
|
||||
assert!(
|
||||
update.changed.is_none(),
|
||||
"the first data can change anything"
|
||||
);
|
||||
assert_eq!(update.vrps.len(), 2);
|
||||
assert_eq!(
|
||||
update.vrps.validate(&prefix("193.0.4.0/24"), Some(3333)),
|
||||
Validity::Valid
|
||||
);
|
||||
assert_eq!(
|
||||
update.vrps.validate(&prefix("193.0.4.0/24"), Some(15169)),
|
||||
Validity::Invalid
|
||||
);
|
||||
let status = rpki.status();
|
||||
assert_eq!(status.state, CacheState::Synchronised);
|
||||
assert_eq!((status.session_id, status.serial), (Some(SESSION), Some(7)));
|
||||
assert_eq!((status.vrps_ipv4, status.vrps_ipv6), (1, 1));
|
||||
assert_eq!(status.cache, Some(cache.address()));
|
||||
|
||||
// The cache has news: the client asks for what changed since serial 7.
|
||||
connection
|
||||
.send(
|
||||
V,
|
||||
&[Pdu::SerialNotify {
|
||||
session_id: SESSION,
|
||||
serial: 8,
|
||||
}],
|
||||
)
|
||||
.await;
|
||||
assert_eq!(
|
||||
connection.received().await,
|
||||
(
|
||||
V,
|
||||
Pdu::SerialQuery {
|
||||
session_id: SESSION,
|
||||
serial: 7
|
||||
}
|
||||
)
|
||||
);
|
||||
let new = vrp("8.8.8.0/24", 24, 15169);
|
||||
connection
|
||||
.send(
|
||||
V,
|
||||
&[
|
||||
Pdu::CacheResponse {
|
||||
session_id: SESSION,
|
||||
},
|
||||
announce(new),
|
||||
withdraw(other),
|
||||
end_of_data(8),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
|
||||
let update = next_update(&mut updates).await;
|
||||
assert_eq!(update.version, 2);
|
||||
assert_eq!(update.vrps.len(), 2);
|
||||
assert_eq!(
|
||||
update.vrps.validate(&prefix("8.8.8.0/24"), Some(15169)),
|
||||
Validity::Valid
|
||||
);
|
||||
assert_eq!(
|
||||
update
|
||||
.vrps
|
||||
.validate(&prefix("2001:67c:2e8::/48"), Some(3333)),
|
||||
Validity::NotFound,
|
||||
"withdrawn"
|
||||
);
|
||||
// Only routes that what came and went covers can have changed.
|
||||
let changed = update.changed.expect("a change to what there was");
|
||||
assert!(changed.vrps.covers(&prefix("8.8.8.0/24")));
|
||||
assert!(changed.vrps.covers(&prefix("2001:67c:2e8::/48")));
|
||||
assert!(!changed.vrps.covers(&prefix("193.0.0.0/21")));
|
||||
assert_eq!(rpki.status().serial, Some(8));
|
||||
}
|
||||
|
||||
/// A cache that cannot say what changed tells the client to start over.
|
||||
/// What the client then publishes is the difference to what it had.
|
||||
#[tokio::test]
|
||||
async fn a_cache_reset_makes_the_client_ask_for_everything() {
|
||||
let cache = Cache::start().await;
|
||||
let (rpki, mut updates) = client_of(&[&cache]);
|
||||
let mut connection = cache.accept().await;
|
||||
connection.received().await;
|
||||
let kept = vrp("193.0.0.0/21", 24, 3333);
|
||||
let dropped = vrp("8.8.8.0/24", 24, 15169);
|
||||
connection.send_all(&[kept, dropped], 7).await;
|
||||
next_update(&mut updates).await;
|
||||
|
||||
connection
|
||||
.send(
|
||||
V,
|
||||
&[Pdu::SerialNotify {
|
||||
session_id: SESSION,
|
||||
serial: 20,
|
||||
}],
|
||||
)
|
||||
.await;
|
||||
assert!(matches!(
|
||||
connection.received().await.1,
|
||||
Pdu::SerialQuery { serial: 7, .. }
|
||||
));
|
||||
connection.send(V, &[Pdu::CacheReset]).await;
|
||||
assert_eq!(connection.received().await.1, Pdu::ResetQuery);
|
||||
let added = vrp("1.1.1.0/24", 24, 13335);
|
||||
connection.send_all(&[kept, added], 20).await;
|
||||
|
||||
let update = next_update(&mut updates).await;
|
||||
assert_eq!(update.vrps.len(), 2);
|
||||
let changed = update.changed.expect("known what changed");
|
||||
assert!(changed.vrps.covers(&prefix("8.8.8.0/24")));
|
||||
assert!(changed.vrps.covers(&prefix("1.1.1.0/24")));
|
||||
assert!(!changed.vrps.covers(&prefix("193.0.0.0/21")), "it stayed");
|
||||
assert_eq!(rpki.status().serial, Some(20));
|
||||
}
|
||||
|
||||
/// An answer takes effect whole or not at all: one that contradicts itself
|
||||
/// changes nothing, and the session is given up.
|
||||
#[tokio::test]
|
||||
async fn an_answer_that_contradicts_itself_changes_nothing() {
|
||||
let cache = Cache::start().await;
|
||||
let (rpki, mut updates) = client_of(&[&cache]);
|
||||
let mut connection = cache.accept().await;
|
||||
connection.received().await;
|
||||
let held = vrp("193.0.0.0/21", 24, 3333);
|
||||
connection.send_all(&[held], 7).await;
|
||||
next_update(&mut updates).await;
|
||||
|
||||
connection
|
||||
.send(
|
||||
V,
|
||||
&[Pdu::SerialNotify {
|
||||
session_id: SESSION,
|
||||
serial: 8,
|
||||
}],
|
||||
)
|
||||
.await;
|
||||
connection.received().await;
|
||||
// A good change, then the withdrawal of something never announced.
|
||||
connection
|
||||
.send(
|
||||
V,
|
||||
&[
|
||||
Pdu::CacheResponse {
|
||||
session_id: SESSION,
|
||||
},
|
||||
announce(vrp("1.1.1.0/24", 24, 13335)),
|
||||
withdraw(vrp("9.9.9.0/24", 24, 19281)),
|
||||
end_of_data(8),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
|
||||
// The client says what was wrong,
|
||||
assert!(matches!(
|
||||
connection.received().await,
|
||||
(
|
||||
V,
|
||||
Pdu::ErrorReport {
|
||||
code: error_code::WITHDRAWAL_OF_UNKNOWN_RECORD,
|
||||
..
|
||||
}
|
||||
)
|
||||
));
|
||||
// gives the session up and comes back; what it has is what it had,
|
||||
// and it asks from where it was.
|
||||
let mut again = cache.accept().await;
|
||||
assert_eq!(
|
||||
again.received().await.1,
|
||||
Pdu::SerialQuery {
|
||||
session_id: SESSION,
|
||||
serial: 7
|
||||
}
|
||||
);
|
||||
let current = rpki.current();
|
||||
assert_eq!(current.version, 1);
|
||||
assert_eq!(current.vrps.len(), 1);
|
||||
assert!(
|
||||
rpki.status()
|
||||
.last_error
|
||||
.is_some_and(|e| e.contains("withdrew a VRP it had not announced"))
|
||||
);
|
||||
}
|
||||
|
||||
fn aspa(customer: u32, providers: &[u32]) -> Pdu {
|
||||
Pdu::Aspa {
|
||||
announce: true,
|
||||
customer,
|
||||
providers: providers.to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
/// ASPAs come with the VRPs, in version 2. One that is announced again
|
||||
/// takes the place of the one before.
|
||||
#[tokio::test]
|
||||
async fn aspas_are_taken_replaced_and_withdrawn() {
|
||||
let cache = Cache::start().await;
|
||||
let (rpki, mut updates) = client_of(&[&cache]);
|
||||
let mut connection = cache.accept().await;
|
||||
assert_eq!(connection.received().await, (V, Pdu::ResetQuery));
|
||||
connection
|
||||
.send(
|
||||
V,
|
||||
&[
|
||||
Pdu::CacheResponse {
|
||||
session_id: SESSION,
|
||||
},
|
||||
announce(vrp("193.0.0.0/21", 24, 3333)),
|
||||
aspa(3333, &[1101, 3257]),
|
||||
aspa(1101, &[0]),
|
||||
end_of_data(7),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
let update = next_update(&mut updates).await;
|
||||
assert_eq!(update.vrps.len(), 1);
|
||||
assert_eq!(update.aspas.len(), 2);
|
||||
assert_eq!(update.aspas.providers(3333), Some(&[1101, 3257][..]));
|
||||
assert_eq!(rpki.status().aspas, 2);
|
||||
assert_eq!(rpki.status().protocol_version, 2);
|
||||
assert_eq!(
|
||||
update.aspas.verify(&[174, 3333], Algorithm::Upstream),
|
||||
PathValidity::Invalid
|
||||
);
|
||||
|
||||
connection
|
||||
.send(
|
||||
V,
|
||||
&[Pdu::SerialNotify {
|
||||
session_id: SESSION,
|
||||
serial: 8,
|
||||
}],
|
||||
)
|
||||
.await;
|
||||
connection.received().await;
|
||||
connection
|
||||
.send(
|
||||
V,
|
||||
&[
|
||||
Pdu::CacheResponse {
|
||||
session_id: SESSION,
|
||||
},
|
||||
// Another list for 3333, a new ASPA, and one gone.
|
||||
aspa(3333, &[174, 1101]),
|
||||
aspa(2121, &[3333]),
|
||||
Pdu::Aspa {
|
||||
announce: false,
|
||||
customer: 1101,
|
||||
providers: vec![],
|
||||
},
|
||||
end_of_data(8),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
let update = next_update(&mut updates).await;
|
||||
assert_eq!(update.aspas.len(), 2);
|
||||
assert_eq!(update.aspas.providers(3333), Some(&[174, 1101][..]));
|
||||
assert_eq!(update.aspas.providers(1101), None);
|
||||
assert_eq!(
|
||||
update.aspas.verify(&[174, 3333], Algorithm::Upstream),
|
||||
PathValidity::Valid
|
||||
);
|
||||
let changed = update.changed.expect("a change to what there was");
|
||||
assert_eq!(
|
||||
*changed.aspa_customers,
|
||||
[3333, 2121, 1101].into_iter().collect()
|
||||
);
|
||||
assert!(changed.vrps.is_empty(), "no VRP came or went");
|
||||
assert_eq!(update.vrps.len(), 1);
|
||||
|
||||
// The withdrawal of an ASPA there is not is a contradiction: nothing
|
||||
// of the answer is used.
|
||||
connection
|
||||
.send(
|
||||
V,
|
||||
&[Pdu::SerialNotify {
|
||||
session_id: SESSION,
|
||||
serial: 9,
|
||||
}],
|
||||
)
|
||||
.await;
|
||||
connection.received().await;
|
||||
connection
|
||||
.send(
|
||||
V,
|
||||
&[
|
||||
Pdu::CacheResponse {
|
||||
session_id: SESSION,
|
||||
},
|
||||
aspa(3333, &[6939]),
|
||||
Pdu::Aspa {
|
||||
announce: false,
|
||||
customer: 1101,
|
||||
providers: vec![],
|
||||
},
|
||||
end_of_data(9),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
assert!(matches!(
|
||||
connection.received().await.1,
|
||||
Pdu::ErrorReport {
|
||||
code: error_code::WITHDRAWAL_OF_UNKNOWN_RECORD,
|
||||
..
|
||||
}
|
||||
));
|
||||
let mut again = cache.accept().await;
|
||||
assert!(matches!(
|
||||
again.received().await.1,
|
||||
Pdu::SerialQuery { serial: 8, .. }
|
||||
));
|
||||
let current = rpki.current();
|
||||
assert_eq!(current.version, 2);
|
||||
assert_eq!(current.aspas.providers(3333), Some(&[174, 1101][..]));
|
||||
}
|
||||
|
||||
/// A cache that speaks version 1 is spoken to in it. It has no ASPAs to
|
||||
/// give: those that came from a newer cache go when its data takes their
|
||||
/// place.
|
||||
#[tokio::test]
|
||||
async fn a_cache_that_speaks_version_one_has_no_aspas() {
|
||||
let cache = Cache::start().await;
|
||||
let (rpki, mut updates) = client_of(&[&cache]);
|
||||
let mut connection = cache.accept().await;
|
||||
connection.received().await;
|
||||
let ripe = vrp("193.0.0.0/21", 24, 3333);
|
||||
connection
|
||||
.send(
|
||||
V,
|
||||
&[
|
||||
Pdu::CacheResponse {
|
||||
session_id: SESSION,
|
||||
},
|
||||
announce(ripe),
|
||||
aspa(3333, &[1101]),
|
||||
end_of_data(7),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
assert_eq!(next_update(&mut updates).await.aspas.len(), 1);
|
||||
|
||||
// The validator is replaced by an older one at the same address.
|
||||
drop(connection);
|
||||
let mut connection = cache.accept().await;
|
||||
assert!(matches!(
|
||||
connection.received().await,
|
||||
(V, Pdu::SerialQuery { serial: 7, .. })
|
||||
));
|
||||
connection
|
||||
.send(
|
||||
1,
|
||||
&[Pdu::ErrorReport {
|
||||
code: error_code::UNSUPPORTED_VERSION,
|
||||
text: "version 1 at most".into(),
|
||||
}],
|
||||
)
|
||||
.await;
|
||||
// Asked again in version 1, and for everything: the serial was of
|
||||
// another version's data.
|
||||
let mut connection = cache.accept().await;
|
||||
assert_eq!(connection.received().await, (1, Pdu::ResetQuery));
|
||||
connection
|
||||
.send(
|
||||
1,
|
||||
&[
|
||||
Pdu::CacheResponse {
|
||||
session_id: SESSION,
|
||||
},
|
||||
announce(ripe),
|
||||
end_of_data(3),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
let update = next_update(&mut updates).await;
|
||||
assert!(update.aspas.is_empty());
|
||||
assert_eq!(update.vrps.len(), 1);
|
||||
let changed = update.changed.expect("the difference to what there was");
|
||||
assert_eq!(*changed.aspa_customers, [3333].into_iter().collect());
|
||||
assert!(changed.vrps.is_empty());
|
||||
let status = rpki.status();
|
||||
assert_eq!((status.protocol_version, status.aspas), (1, 0));
|
||||
|
||||
// An ASPA in a version that has none is not RTR.
|
||||
connection
|
||||
.stream
|
||||
.write_all(&[1, 11, 1, 0, 0, 0, 0, 16, 0, 0, 0, 1, 0, 0, 0, 2])
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(matches!(
|
||||
connection.received().await,
|
||||
(
|
||||
1,
|
||||
Pdu::ErrorReport {
|
||||
code: error_code::UNSUPPORTED_PDU_TYPE,
|
||||
..
|
||||
}
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
/// A validator that only speaks version 0 says so, by an error or by
|
||||
/// answering in version 0. The client asks again in version 0.
|
||||
#[tokio::test]
|
||||
async fn a_cache_that_speaks_version_zero_is_spoken_to_in_it() {
|
||||
for refuses_with_an_error in [true, false] {
|
||||
let cache = Cache::start().await;
|
||||
let (rpki, mut updates) = client_of(&[&cache]);
|
||||
let mut connection = cache.accept().await;
|
||||
assert_eq!(connection.received().await, (V, Pdu::ResetQuery));
|
||||
if refuses_with_an_error {
|
||||
connection
|
||||
.send(
|
||||
0,
|
||||
&[Pdu::ErrorReport {
|
||||
code: error_code::UNSUPPORTED_VERSION,
|
||||
text: "version 0 only".into(),
|
||||
}],
|
||||
)
|
||||
.await;
|
||||
} else {
|
||||
connection
|
||||
.send(
|
||||
0,
|
||||
&[Pdu::CacheResponse {
|
||||
session_id: SESSION,
|
||||
}],
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
let mut connection = cache.accept().await;
|
||||
assert_eq!(connection.received().await, (0, Pdu::ResetQuery));
|
||||
connection
|
||||
.send(
|
||||
0,
|
||||
&[
|
||||
Pdu::CacheResponse {
|
||||
session_id: SESSION,
|
||||
},
|
||||
announce(vrp("193.0.0.0/21", 24, 3333)),
|
||||
Pdu::EndOfData {
|
||||
session_id: SESSION,
|
||||
serial: 3,
|
||||
timers: None,
|
||||
},
|
||||
],
|
||||
)
|
||||
.await;
|
||||
assert_eq!(next_update(&mut updates).await.vrps.len(), 1);
|
||||
assert_eq!(rpki.status().protocol_version, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// When the cache goes away the data is kept and used while the client
|
||||
/// tries again, and the next cache is tried. A cache the data did not come
|
||||
/// from is asked for everything.
|
||||
#[tokio::test]
|
||||
async fn a_lost_cache_is_replaced_by_the_next_and_the_data_is_kept() {
|
||||
let first = Cache::start().await;
|
||||
let second = Cache::start().await;
|
||||
let (rpki, mut updates) = client_of(&[&first, &second]);
|
||||
|
||||
let mut connection = first.accept().await;
|
||||
connection.received().await;
|
||||
connection
|
||||
.send_all(&[vrp("193.0.0.0/21", 24, 3333)], 7)
|
||||
.await;
|
||||
next_update(&mut updates).await;
|
||||
|
||||
drop(connection);
|
||||
drop(first);
|
||||
let mut connection = second.accept().await;
|
||||
assert_eq!(connection.received().await.1, Pdu::ResetQuery);
|
||||
// Still in use meanwhile.
|
||||
assert_eq!(rpki.current().vrps.len(), 1);
|
||||
|
||||
connection
|
||||
.send_all(
|
||||
&[vrp("193.0.0.0/21", 24, 3333), vrp("1.1.1.0/24", 24, 13335)],
|
||||
100,
|
||||
)
|
||||
.await;
|
||||
let update = next_update(&mut updates).await;
|
||||
assert_eq!(update.vrps.len(), 2);
|
||||
let changed = update.changed.expect("the difference to what there was");
|
||||
assert!(changed.vrps.covers(&prefix("1.1.1.0/24")));
|
||||
assert!(!changed.vrps.covers(&prefix("193.0.0.0/21")));
|
||||
assert_eq!(rpki.status().cache, Some(second.address()));
|
||||
}
|
||||
|
||||
/// Data that no cache has confirmed for the expire interval is dropped:
|
||||
/// routes are then neither valid nor invalid.
|
||||
#[tokio::test]
|
||||
async fn data_that_is_not_confirmed_in_time_is_dropped() {
|
||||
let cache = Cache::start().await;
|
||||
let (rpki, _task) = Rpki::spawn(RpkiConfig {
|
||||
caches: vec![cache.address()],
|
||||
// Data never expires sooner than a refresh and a retry take.
|
||||
refresh: Some(Duration::from_millis(200)),
|
||||
retry: Some(Duration::from_millis(50)),
|
||||
expire: Some(Duration::from_millis(300)),
|
||||
});
|
||||
let mut updates = rpki.subscribe();
|
||||
let mut connection = cache.accept().await;
|
||||
connection.received().await;
|
||||
connection
|
||||
.send_all(&[vrp("193.0.0.0/21", 24, 3333)], 7)
|
||||
.await;
|
||||
assert_eq!(next_update(&mut updates).await.vrps.len(), 1);
|
||||
|
||||
// The cache goes and does not come back.
|
||||
drop(connection);
|
||||
drop(cache);
|
||||
let update = next_update(&mut updates).await;
|
||||
assert!(update.vrps.is_empty());
|
||||
assert!(update.changed.is_none(), "anything may have changed");
|
||||
assert_eq!(
|
||||
update.vrps.validate(&prefix("193.0.0.0/21"), Some(15169)),
|
||||
Validity::NotFound
|
||||
);
|
||||
let status = rpki.status();
|
||||
assert_eq!(status.state, CacheState::Connecting);
|
||||
assert_eq!(status.serial, None);
|
||||
}
|
||||
|
||||
/// A cache that reports an error, or sends what is not RTR, is given up
|
||||
/// on and tried again; nothing of what it sent is used.
|
||||
#[tokio::test]
|
||||
async fn a_cache_that_misbehaves_is_given_up_on() {
|
||||
let cache = Cache::start().await;
|
||||
let (rpki, _updates) = client_of(&[&cache]);
|
||||
|
||||
let mut connection = cache.accept().await;
|
||||
connection.received().await;
|
||||
connection
|
||||
.send(
|
||||
V,
|
||||
&[Pdu::ErrorReport {
|
||||
code: error_code::NO_DATA_AVAILABLE,
|
||||
text: "still validating".into(),
|
||||
}],
|
||||
)
|
||||
.await;
|
||||
let mut connection = cache.accept().await;
|
||||
assert_eq!(connection.received().await.1, Pdu::ResetQuery);
|
||||
assert!(
|
||||
rpki.status()
|
||||
.last_error
|
||||
.is_some_and(|e| e.contains("still validating"))
|
||||
);
|
||||
|
||||
// Half an answer, then noise.
|
||||
connection
|
||||
.send(
|
||||
V,
|
||||
&[
|
||||
Pdu::CacheResponse {
|
||||
session_id: SESSION,
|
||||
},
|
||||
announce(vrp("193.0.0.0/21", 24, 3333)),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
connection
|
||||
.stream
|
||||
.write_all(&[1, 99, 0, 0, 0, 0, 0, 8])
|
||||
.await
|
||||
.unwrap();
|
||||
let mut connection = cache.accept().await;
|
||||
assert_eq!(connection.received().await.1, Pdu::ResetQuery);
|
||||
assert!(rpki.current().vrps.is_empty());
|
||||
assert_eq!(rpki.version(), 0);
|
||||
}
|
||||
@@ -8,21 +8,19 @@ repository.workspace = true
|
||||
version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
foldhash.workspace = true
|
||||
bgp-packet.workspace = true
|
||||
bgp-primitives.workspace = true
|
||||
bgp-rib.workspace = true
|
||||
bgp-fib.workspace = true
|
||||
bgp-rpki.workspace = true
|
||||
bytes.workspace = true
|
||||
clap.workspace = true
|
||||
futures.workspace = true
|
||||
prometheus.workspace = true
|
||||
socket2.workspace = true
|
||||
tokio.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
tracing.workspace = true
|
||||
protobuf.workspace = true
|
||||
thiserror.workspace = true
|
||||
nix.workspace = true
|
||||
|
||||
[build-dependencies]
|
||||
protobuf-codegen.workspace = true
|
||||
|
||||
@@ -37,237 +37,10 @@ message BgpConfig {
|
||||
// restarted take over from a session we have not yet noticed is dead,
|
||||
// instead of being refused until the hold timer expires.
|
||||
bool collision_detect_established_state = 8;
|
||||
|
||||
// Connections are accepted only from the addresses of configured peers;
|
||||
// from anywhere else they are closed before a byte is read. When true,
|
||||
// any host that can reach a listening endpoint may open a session, as
|
||||
// any AS it cares to name, and have its routes taken. That is for
|
||||
// testing on a closed network and nothing else.
|
||||
bool accept_unconfigured_peers = 9;
|
||||
|
||||
// Route origin validation (RFC 6811) and AS path verification with the
|
||||
// RPKI. Left out, routes are not validated.
|
||||
Rpki rpki = 10;
|
||||
|
||||
// Put the best route of every prefix in the kernel's routing table, so
|
||||
// that the machine forwards packets by them. Left out, the daemon only
|
||||
// speaks BGP and the machine's forwarding is not touched.
|
||||
Kernel kernel = 11;
|
||||
|
||||
// What is announced, and how, to every peer that has no `export` of its
|
||||
// own.
|
||||
ExportPolicy export = 12;
|
||||
|
||||
// Let routes say, with communities, which peers they are passed on to
|
||||
// and how far away we look to them, as the route servers of Internet
|
||||
// exchanges and transit networks do. With L our own AS number, P a
|
||||
// peer's, and G a group of sessions (`groups` of a peer):
|
||||
//
|
||||
// 0:P or L:0:P not to the peer with AS P
|
||||
// L:P or L:1:P to the peer with AS P, and then only to peers
|
||||
// named so
|
||||
// 0:L or L:0:0 to nobody
|
||||
// L:11:P with our AS once more on the path to the peer
|
||||
// L:12:P with AS P; twice, three times. P 0 is every
|
||||
// L:13:P peer in another AS.
|
||||
// L:20:G not to the sessions of group G
|
||||
// L:21:G to the sessions of group G, and then only to
|
||||
// sessions named so, by group or AS
|
||||
// L:31:G with our AS once more on the path to the
|
||||
// L:32:G sessions of group G; twice, three times
|
||||
// L:33:G
|
||||
//
|
||||
// A route with none of these goes to everyone it otherwise would. "Not"
|
||||
// wins over "to". Of several prepends that apply, the most counts, on
|
||||
// top of the session's export.as_path_prepend. The two-part form is a
|
||||
// standard community, with room for AS numbers below 65536; the
|
||||
// three-part forms are large communities and work for any.
|
||||
//
|
||||
// They are acted on where a route leaves our AS, and taken off there:
|
||||
// to a peer in our own AS a route goes with them, for that router to act
|
||||
// on. They are honoured on routes we originate too (`communities` and
|
||||
// `large_communities` of `originate`), on what import.add_communities
|
||||
// puts on, and on what a peer in our own AS, or one with
|
||||
// import.accept_control_communities, sends. From any other peer the
|
||||
// communities of our own AS are taken off as its routes come in, so that
|
||||
// nobody outside can say what is done with our routes.
|
||||
//
|
||||
// With this on, standard communities L:n are taken for "to the peer
|
||||
// with AS n": mark routes with something else (large communities
|
||||
// L:100:n and up, say; functions 2 to 9 are left for marks too).
|
||||
bool control_communities = 13;
|
||||
|
||||
// Graceful restart, for every peer that has no `graceful_restart` of its
|
||||
// own. Left out, it is on, as the defaults below have it.
|
||||
GracefulRestart graceful_restart = 14;
|
||||
|
||||
// Where this router passes on routes from one router of our AS to
|
||||
// another (Peer.route_reflector_client), the cluster it does so for, as
|
||||
// a dotted-quad: what it puts on such routes, and what it does not take
|
||||
// back (RFC 4456). Give routers that serve the same clients the same
|
||||
// cluster ID. Defaults to router_id.
|
||||
string cluster_id = 15;
|
||||
|
||||
// Limits on what goes to other networks that are not our customers: to
|
||||
// every peer whose role is PROVIDER, PEER or ROUTE_SERVER, and to one
|
||||
// in another AS that has no role. Not to customers, route server
|
||||
// clients or routers of our own AS. A peer's own `export_limits` takes
|
||||
// the place of these.
|
||||
//
|
||||
// They hold whatever the routes are and wherever they came from, and
|
||||
// whatever `export` says: a check on everything else, in case it goes
|
||||
// wrong. With the roles, a route learned from a provider or a peer is
|
||||
// passed on to customers only; these keep it from going further even
|
||||
// if a route is not marked as it should be.
|
||||
ExportLimits export_limits = 16;
|
||||
|
||||
// Names for communities, for the status page to show in place of the
|
||||
// numbers: what the marks on routes mean to a reader. A name for one
|
||||
// community ("64601:100:2" is "comrack"), or for a kind of them, with
|
||||
// `*` for the part left open ("64601:102:*" is "interconnection"), which
|
||||
// is shown with the open part after the name. The daemon itself does
|
||||
// nothing with them.
|
||||
repeated CommunityName community_names = 17;
|
||||
}
|
||||
|
||||
// A name for a community, or for a kind of them.
|
||||
message CommunityName {
|
||||
// The community, standard ("64601:100") or large ("64601:100:2"), or a
|
||||
// pattern of them with `*` for a part left open ("64601:102:*").
|
||||
string community = 1;
|
||||
// What it is shown as.
|
||||
string name = 2;
|
||||
}
|
||||
|
||||
// Limits on what is announced to a peer, which hold whatever `export`
|
||||
// and the routes say.
|
||||
message ExportLimits {
|
||||
// Only routes for these prefixes are announced: our own address space
|
||||
// and our customers', say, with max_length as long as the longest
|
||||
// prefix announced from it. Routes for other prefixes, learned or
|
||||
// originated, are not. Empty allows any.
|
||||
repeated PrefixRange prefixes = 1;
|
||||
|
||||
// The most routes that are announced to the peer at a time. If more
|
||||
// were to be, none of them is: the session is ended with a Cease /
|
||||
// Administrative Shutdown NOTIFICATION that says why (RFC 9003), and the
|
||||
// peer is kept down for its import.hold_down_seconds, as when it sends
|
||||
// us too many. Set it somewhat above what is expected. Zero is no
|
||||
// limit.
|
||||
uint32 max_prefixes = 2;
|
||||
}
|
||||
|
||||
// Graceful restart (RFC 4724): BGP restarting without the routes going.
|
||||
//
|
||||
// When the daemon is restarted in good order for an upgrade (on SIGUSR1),
|
||||
// it leaves its routes in the kernel, and its peers keep the routes it
|
||||
// announced to them, for as long as they were told a restart takes. When a
|
||||
// peer restarts, its routes are kept and used as before, marked stale,
|
||||
// until it has sent them again; those it does not send again are removed.
|
||||
// Either way, nothing is withdrawn across the network for a restart.
|
||||
//
|
||||
// It is in use with a peer that offers it too. A session that ends with a
|
||||
// Hard Reset (RFC 8538), or because of an error, has its routes removed at
|
||||
// once, as without graceful restart.
|
||||
message GracefulRestart {
|
||||
// True does not offer graceful restart: a peer's routes are removed as
|
||||
// soon as its session ends, and it removes ours as soon as it sees ours
|
||||
// end.
|
||||
optional bool disabled = 1;
|
||||
|
||||
// How long, in seconds, peers are told a restart of this daemon takes:
|
||||
// how long they keep our routes after our session with them ends. At
|
||||
// most 4095. Defaults to 120.
|
||||
optional uint32 restart_time_seconds = 2;
|
||||
|
||||
// The longest time, in seconds, a restarting peer's routes are kept
|
||||
// stale: while it is gone, whatever restart time it gave, and again once
|
||||
// it is back, until it has sent all its routes. Zero keeps none.
|
||||
// Defaults to 120.
|
||||
optional uint32 stale_routes_seconds = 3;
|
||||
}
|
||||
|
||||
// Installing routes in the kernel. Linux only; the daemon needs the
|
||||
// CAP_NET_ADMIN capability.
|
||||
//
|
||||
// The best route of a prefix is installed if its next hop is a neighbor:
|
||||
// an address in the network of one of the machine's interfaces. Routes
|
||||
// are marked as BGP's ("proto bgp"), and every route with that mark, the
|
||||
// table and the metric below is taken to be this daemon's: what it finds
|
||||
// at startup and no longer wants, it removes. When the daemon stops in
|
||||
// good order it removes its routes.
|
||||
message Kernel {
|
||||
// The routing table. Defaults to 254, the main table, which is the one
|
||||
// packets are forwarded by unless rules say otherwise.
|
||||
uint32 table = 1;
|
||||
|
||||
// The metric of the routes. Defaults to 20. Of several routes for a
|
||||
// prefix the kernel uses the one with the lowest metric.
|
||||
optional uint32 metric = 2;
|
||||
|
||||
// By default, where the kernel has nexthop objects (Linux 5.3 and
|
||||
// later), a gateway is put there once as such an object and routes name
|
||||
// it, which makes installing a full table about a third faster. `ip
|
||||
// route` then shows routes with "nhid". True has every route carry its
|
||||
// own gateway, as on older kernels.
|
||||
bool without_nexthop_objects = 3;
|
||||
|
||||
// How long, after the daemon starts, routes of its own that an earlier
|
||||
// run left in the kernel are kept though no peer has announced them
|
||||
// (again). Defaults to 120. The daemon's table is empty until its peers
|
||||
// have sent theirs; removing the old routes at once would stop the
|
||||
// machine forwarding for as long as that takes. After this time, what
|
||||
// was not announced again is removed.
|
||||
// Zero removes them at once.
|
||||
optional uint32 stale_seconds = 4;
|
||||
|
||||
// The kernel's table whose routes say how a next hop that is not a
|
||||
// neighbour is reached: its connected and static routes, and an IGP's
|
||||
// if there is one. Defaults to 254, the main table. A route whose next
|
||||
// hop is covered by such a route is installed with that route's gateway
|
||||
// and interface, the kernel not looking gateways up in turn. Routes put
|
||||
// in the table by BGP, this daemon's or another's, never count: a next
|
||||
// hop reached only by way of a BGP route rests on what may itself rest
|
||||
// on a next hop.
|
||||
uint32 resolution_table = 5;
|
||||
|
||||
// Whether the default route in that table counts as the way to a next
|
||||
// hop. Off by default: on a router the default route points at a
|
||||
// transit provider, and a next hop "reached" only by sending the
|
||||
// packets to the Internet at large is not reached.
|
||||
bool resolve_via_default_route = 6;
|
||||
}
|
||||
|
||||
// Where to get the validated ROA payloads that say which AS may originate
|
||||
// which prefixes. With this set, a route whose origin is not authorised
|
||||
// for its prefix ("invalid") is refused, from every peer that is not in
|
||||
// our own AS and does not have accept_rpki_invalid set. Routes that
|
||||
// nothing is known about are taken as before.
|
||||
//
|
||||
// A validator that speaks version 2 of the protocol also gives the ASPAs,
|
||||
// in which networks say who their providers are. With those, a route
|
||||
// whose AS path cannot have come about by networks passing routes on as
|
||||
// they should (a route leak, or a made-up path) is refused, from every
|
||||
// peer that has a role set and does not have accept_aspa_invalid set.
|
||||
message Rpki {
|
||||
// The RPKI-to-Router servers (RFC 8210, draft-ietf-sidrops-8210bis) of
|
||||
// one or more validators, such as Routinator, rpki-client with StayRTR,
|
||||
// or FORT, in order of preference. The port is required. One is used at
|
||||
// a time, over plain TCP: keep it on a network you trust.
|
||||
repeated TcpEndpoint caches = 1;
|
||||
|
||||
// How often to ask the cache for changes unprompted, how long to wait
|
||||
// before trying again after a failure, and how long the data may be used
|
||||
// without a cache confirming it, after which nothing is invalid any
|
||||
// more. Each defaults to what the cache says, or to 3600, 600 and 7200.
|
||||
uint32 refresh_seconds = 2;
|
||||
uint32 retry_seconds = 3;
|
||||
uint32 expire_seconds = 4;
|
||||
}
|
||||
|
||||
// A route originated by this router. It is announced with an AS_PATH of the
|
||||
// local AS (empty, to a peer in our own AS) and, on each session, that
|
||||
// session's local address as next hop.
|
||||
// local AS and, on each session, that session's local address as next hop.
|
||||
message OriginatedRoute {
|
||||
// The prefix in CIDR notation, e.g. "192.0.2.0/24" or "2001:db8::/32".
|
||||
string prefix = 1;
|
||||
@@ -291,8 +64,7 @@ message OriginatedRoute {
|
||||
|
||||
// An IP address and port pair.
|
||||
message TcpEndpoint {
|
||||
// IP address (V4 or V6). A peer at a link-local IPv6 address has the
|
||||
// interface it is on after it: "fe80::1%eth0".
|
||||
// IP address (V4 or V6).
|
||||
string address = 1;
|
||||
// TCP port. Defaults to 179 for BGP when unset.
|
||||
uint32 port = 2;
|
||||
@@ -310,9 +82,8 @@ message Peer {
|
||||
uint32 remote_asn = 3;
|
||||
|
||||
// Hold time to advertise in the OPEN message, in seconds (RFC 4271 §4.2).
|
||||
// At least 3, or 0 for a session without keepalives that is never timed
|
||||
// out. Defaults to 90 when unset.
|
||||
optional uint32 hold_time_seconds = 4;
|
||||
// Must be 0 or >= 3. Defaults to 90 when unset.
|
||||
uint32 hold_time_seconds = 4;
|
||||
|
||||
// Source IP address for connections we open to this peer. It is also the
|
||||
// next hop of the routes we send it. Required when the peer identifies us
|
||||
@@ -332,298 +103,6 @@ message Peer {
|
||||
// direction and address family, only if the peer agrees. Defaults to
|
||||
// off: one route for each prefix, as in plain BGP.
|
||||
AddPath add_path = 7;
|
||||
|
||||
// Which of the peer's routes are taken. Left out, the general checks
|
||||
// apply and there is no limit and no list.
|
||||
ImportPolicy import = 8;
|
||||
|
||||
// What the peer is to us. Not for a peer in our own AS. With it set:
|
||||
//
|
||||
// - Our OPEN says what we are to the peer, and a peer that says it is
|
||||
// something else than configured here is refused (RFC 9234). One side
|
||||
// having the other down as a customer and the other as a peer is how
|
||||
// leaks come about.
|
||||
// - Routes are marked "only to customer" as they come in from a provider,
|
||||
// a peer or a route server, and as they go out to a customer, a peer
|
||||
// or a route server client. A route with that mark that comes back up
|
||||
// from a customer, or across from a second peer, is a leak and is
|
||||
// refused. This cannot be turned off.
|
||||
// - Where rpki is configured, the AS paths of the peer's routes are
|
||||
// verified against the ASPAs: a route from a customer, a peer or a
|
||||
// route server must have come up through providers all the way; one
|
||||
// from a provider may have come up and then down.
|
||||
//
|
||||
// Left out, none of this is done for the peer.
|
||||
Role role = 9;
|
||||
|
||||
// Refuse the peer unless its OPEN says what it is. By default a peer
|
||||
// that says nothing (one that does not implement RFC 9234) is taken at
|
||||
// this configuration's word for it. Needs role.
|
||||
bool require_role = 10;
|
||||
|
||||
// What is announced to the peer, and how. Left out, the top-level
|
||||
// `export` holds, and without that the routes we originate are
|
||||
// announced as they are.
|
||||
ExportPolicy export = 11;
|
||||
|
||||
// Graceful restart with this peer. What is set here takes the place of
|
||||
// what the top-level `graceful_restart` says.
|
||||
GracefulRestart graceful_restart = 12;
|
||||
|
||||
// The groups the session is in, by number, for control communities to
|
||||
// name sessions by (see control_communities): those at one Internet
|
||||
// exchange, say, or those over one link, whatever AS the peers are. Use
|
||||
// the same numbers on all routers of the AS: a route marked on one is
|
||||
// acted on by another. From 1.
|
||||
repeated uint32 groups = 13;
|
||||
|
||||
// Pass on to this peer, a router of our own AS, the routes learned from
|
||||
// our other routers, and pass on the routes it sends to all of them: we
|
||||
// are its route reflector (RFC 4456). Without it, as BGP has it, a route
|
||||
// learned from one router of our AS goes to no other, and every router
|
||||
// needs a session with every other. Routes go with this router as next
|
||||
// hop, so that the peer can reach what lies behind another router
|
||||
// through this one. Only for a peer in our own AS: other networks are
|
||||
// told of learned routes by `export` and the roles alone. The peer's
|
||||
// `export` still has to pass on learned routes.
|
||||
bool route_reflector_client = 14;
|
||||
|
||||
// Limits on what is announced to the peer, in the place of the
|
||||
// top-level export_limits. They apply to the same peers as those do: to
|
||||
// one whose role is PROVIDER, PEER or ROUTE_SERVER, or that is in
|
||||
// another AS and has no role.
|
||||
ExportLimits export_limits = 15;
|
||||
|
||||
// Do not open connections to the peer: wait for it to connect. The
|
||||
// session is configured in full, and comes up when the peer connects
|
||||
// from `endpoint`'s address; the port of `endpoint` is not used. For a
|
||||
// new peer whose side is not ready yet, or one that is to open the
|
||||
// connections itself.
|
||||
bool passive = 16;
|
||||
|
||||
// About how many routes the peer sends when all is well: a full table,
|
||||
// a few hundred from an exchange peer, one from a customer. Nothing is
|
||||
// decided by it. The status page compares the routes the peer has in
|
||||
// the table with it, and shows an established session with fewer than
|
||||
// half as needing attention: a peer that is up but has sent next to
|
||||
// nothing looks, without this, exactly like one that is fine. Zero, the
|
||||
// default, compares nothing. The routing table is also made with room
|
||||
// for the largest of these from the start, so that it need not grow,
|
||||
// and so use twice its memory for a moment, while the tables come in.
|
||||
uint32 expected_routes = 17;
|
||||
}
|
||||
|
||||
// What is announced to a peer.
|
||||
//
|
||||
// The routes we originate are always announced. Routes learned from other
|
||||
// peers are passed on only if learned_routes says so, and then only as
|
||||
// far as these let them:
|
||||
//
|
||||
// - Never back to the peer a route came from, nor to a peer whose AS is
|
||||
// on its path already.
|
||||
// - A route from a peer in our own AS is not passed to another such peer,
|
||||
// unless one of them is a route reflector client of ours
|
||||
// (Peer.route_reflector_client).
|
||||
// - Where peers have roles (RFC 9234): a route learned from a provider, a
|
||||
// peer or a route server goes on to customers only. So with roles on
|
||||
// all peers, turning this on makes us transit for our customers and for
|
||||
// nobody else. Without roles there is no such rule, and every route
|
||||
// goes to every peer: do not turn this on for a peer without a role
|
||||
// unless that is what is wanted.
|
||||
// - The communities NO_EXPORT and NO_ADVERTISE, and control_communities.
|
||||
//
|
||||
// A route is passed on 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 left as it is and LOCAL_PREF goes along. To a peer whose role
|
||||
// is ROUTE_SERVER_CLIENT the route goes as a route server sends it
|
||||
// (RFC 7947): path, MED and, where the client can use it, next hop
|
||||
// unchanged.
|
||||
message ExportPolicy {
|
||||
// Pass on the best routes learned from other peers.
|
||||
bool learned_routes = 1;
|
||||
|
||||
// Pass on every route learned from other peers, not only the best of
|
||||
// each prefix, each with a path identifier (ADD-PATH, RFC 7911). For a
|
||||
// route collector, which wants to see every path. Needs learned_routes,
|
||||
// and the peer's add_path to offer sending; for a family the peer did
|
||||
// not agree to receive several routes for, the best goes as usual. The
|
||||
// export policy and the limits apply to every route alike.
|
||||
bool all_paths = 6;
|
||||
|
||||
// Communities put on every route announced to the peer: which router
|
||||
// it went through, say. "64601:100", or a well-known name.
|
||||
repeated string add_communities = 2;
|
||||
|
||||
// Large communities put on, as "64601:1:2".
|
||||
repeated string add_large_communities = 3;
|
||||
|
||||
// Communities taken off first: one ("64601:100"), all of an AS
|
||||
// ("64601:*"), or large ones with three parts ("64601:1:*").
|
||||
repeated string remove_communities = 4;
|
||||
|
||||
// How many more times our AS is put on the path of every route
|
||||
// announced to the peer, to make them look further away. At most 16.
|
||||
uint32 as_path_prepend = 5;
|
||||
}
|
||||
|
||||
// What a peer is to us.
|
||||
enum Role {
|
||||
ROLE_UNSPECIFIED = 0;
|
||||
// We buy transit from the peer.
|
||||
PROVIDER = 1;
|
||||
// The peer buys transit from us.
|
||||
CUSTOMER = 2;
|
||||
// The peer and we exchange our own and our customers' routes.
|
||||
PEER = 3;
|
||||
// The peer is an Internet exchange's route server.
|
||||
ROUTE_SERVER = 4;
|
||||
// We are a route server and the peer is one of our clients.
|
||||
ROUTE_SERVER_CLIENT = 5;
|
||||
}
|
||||
|
||||
// What is taken from a peer. Every route is judged before it goes into
|
||||
// the routing table. One that is refused is dropped, and so is the route
|
||||
// it would have replaced.
|
||||
//
|
||||
// A route whose AS path holds our own AS number is always refused.
|
||||
message ImportPolicy {
|
||||
// The most routes the peer may have at a time. One more ends the session
|
||||
// with Cease / Maximum Number of Prefixes Reached, and the peer is kept
|
||||
// down for hold_down_seconds. Set it somewhat above what the peer is
|
||||
// expected to send. Zero is no limit.
|
||||
uint32 max_prefixes = 1;
|
||||
|
||||
// How long the peer is kept down after going over max_prefixes. Neither
|
||||
// side's connections are taken meanwhile. Defaults to 600.
|
||||
uint32 hold_down_seconds = 2;
|
||||
|
||||
// By default, prefixes that are not routed on the Internet are refused
|
||||
// (private, loopback, link-local, documentation, multicast and reserved
|
||||
// space), and so are routes with a reserved, private or documentation AS
|
||||
// number anywhere in their path. True takes them.
|
||||
bool accept_bogons = 3;
|
||||
|
||||
// By default a default route (0.0.0.0/0 or ::/0) is refused. True takes
|
||||
// one, as from an upstream that is asked to send it.
|
||||
bool accept_default_route = 4;
|
||||
|
||||
// The longest prefixes taken. Defaults to 24 and 48, the longest that are
|
||||
// routed between networks.
|
||||
uint32 max_ipv4_prefix_length = 5;
|
||||
uint32 max_ipv6_prefix_length = 6;
|
||||
|
||||
// By default the AS path of a route has to begin with the peer's own AS
|
||||
// number. True takes any, as is needed for an Internet exchange's route
|
||||
// server, which does not put its AS on the path.
|
||||
bool accept_any_first_as = 7;
|
||||
|
||||
// The longest AS path taken, counted in AS numbers. Defaults to 64.
|
||||
uint32 max_as_path_length = 8;
|
||||
|
||||
// The prefixes the peer may announce. Empty allows any. This is the
|
||||
// list generated for a peer from the routing registries (with bgpq4,
|
||||
// say).
|
||||
repeated PrefixRange prefixes = 9;
|
||||
|
||||
// The AS numbers the peer's routes may originate from: the last AS of
|
||||
// the path. Empty allows any.
|
||||
repeated uint32 origin_asns = 10;
|
||||
|
||||
// By default, where route origin validation is configured (rpki), a
|
||||
// route that is invalid is refused. True takes it all the same.
|
||||
bool accept_rpki_invalid = 11;
|
||||
|
||||
// By default, where the validator gives ASPAs and the peer has a role,
|
||||
// a route whose AS path they find invalid is refused. True takes it all
|
||||
// the same.
|
||||
bool accept_aspa_invalid = 12;
|
||||
|
||||
// By default a route whose AS path has an AS_SET in it is refused: sets
|
||||
// are deprecated (RFC 9774), and with one nobody can say where a route
|
||||
// comes from. True takes such routes.
|
||||
bool accept_as_sets = 13;
|
||||
|
||||
// By default the next hop of a route from another network has to be the
|
||||
// peer itself, so that a peer can only draw traffic to itself and not
|
||||
// send it to a third party. True takes any next hop packets can be sent
|
||||
// to. Not needed for a peer whose role is ROUTE_SERVER, whose routes
|
||||
// have its clients as next hops.
|
||||
bool accept_third_party_next_hop = 14;
|
||||
|
||||
// Communities put on every route taken from the peer: where it was
|
||||
// learned, say, for other routers of ours and for `export` to go by.
|
||||
// "64601:100", or a well-known name.
|
||||
repeated string add_communities = 15;
|
||||
|
||||
// Large communities put on, as "64601:1:2".
|
||||
repeated string add_large_communities = 16;
|
||||
|
||||
// Communities taken off first: one ("64601:100"), all of an AS
|
||||
// ("64601:*"), or large ones with three parts ("64601:1:*"). Marks that
|
||||
// mean something to us are not to be taken on a peer's word.
|
||||
repeated string remove_communities = 17;
|
||||
|
||||
// The LOCAL_PREF of the peer's routes: of two routes for a prefix the
|
||||
// one with the higher is chosen, whatever else they have. Left out, 100.
|
||||
optional uint32 local_pref = 18;
|
||||
|
||||
// By default the communities of our own AS (L:n and L:n:m; and 0:n,
|
||||
// with control_communities on) are taken off the routes of a peer in
|
||||
// another AS as they come in, before add_communities puts ours on: what
|
||||
// they ask of us is not for others to ask. True keeps them, for a
|
||||
// customer that is to steer its own routes with control communities, or
|
||||
// a route server's client. Not for a peer in our own AS, whose routes
|
||||
// keep them in any case.
|
||||
bool accept_control_communities = 19;
|
||||
|
||||
// What is done to some of the routes, beyond what is done to all of
|
||||
// them above. Each route that is taken is looked at by the rules in
|
||||
// turn, and the first that holds for it is applied; the rest are not
|
||||
// looked at. A rule sees the route as the statements above leave it:
|
||||
// with our communities taken off and ours put on.
|
||||
repeated ImportRule rules = 20;
|
||||
|
||||
// By default a route with the BLACKHOLE community (65535:666, RFC
|
||||
// 7999), which asks for the traffic to its prefix to be dropped, is
|
||||
// refused ("blackhole request"). True takes such a route if it is for
|
||||
// one address (a /32 or /128), whatever the longest prefix taken
|
||||
// otherwise, and inside what the peer may announce; its next hop is
|
||||
// not looked at. The traffic to the address is then dropped by this
|
||||
// router (a blackhole route in the kernel), and the route goes on to
|
||||
// the peers the export policy sends it to, community and all, for them
|
||||
// to do the same. For a customer under attack, and for a route server
|
||||
// whose blackhole routes are to be honoured.
|
||||
bool accept_blackholes = 21;
|
||||
}
|
||||
|
||||
// What is done to the routes with some communities, or for some prefixes:
|
||||
// those learned at one exchange, say, by the mark another router of ours
|
||||
// put on them, to be preferred less; or one prefix of a customer's, to be
|
||||
// marked for a prepend.
|
||||
message ImportRule {
|
||||
// The rule holds for routes with one of these communities: one
|
||||
// ("64601:100"), all of an AS ("64601:*"), or large ones with three
|
||||
// parts ("64601:101:*"). Left out, for routes with any.
|
||||
repeated string match_communities = 1;
|
||||
|
||||
// And for routes for a prefix in these. Left out, for any prefix.
|
||||
repeated PrefixRange match_prefixes = 2;
|
||||
|
||||
// The LOCAL_PREF of the routes, in place of import.local_pref.
|
||||
optional uint32 local_pref = 3;
|
||||
|
||||
// Communities put on the routes.
|
||||
repeated string add_communities = 4;
|
||||
repeated string add_large_communities = 5;
|
||||
}
|
||||
|
||||
// A prefix, and optionally the more specific prefixes inside it.
|
||||
message PrefixRange {
|
||||
// In CIDR notation, e.g. "193.0.0.0/21".
|
||||
string prefix = 1;
|
||||
// The longest prefix inside it that is also allowed. Left out, only the
|
||||
// prefix itself is.
|
||||
uint32 max_length = 2;
|
||||
}
|
||||
|
||||
// The directions ADD-PATH can be offered for.
|
||||
@@ -631,8 +110,7 @@ enum AddPath {
|
||||
ADD_PATH_OFF = 0;
|
||||
// Accept several routes for a prefix from the peer.
|
||||
ADD_PATH_RECEIVE = 1;
|
||||
// Offer to send the peer several routes for a prefix. As yet only the
|
||||
// best is sent all the same.
|
||||
// Send the peer several routes for a prefix.
|
||||
ADD_PATH_SEND = 2;
|
||||
ADD_PATH_BOTH = 3;
|
||||
}
|
||||
|
||||
@@ -1,366 +0,0 @@
|
||||
//! Where a peer is.
|
||||
//!
|
||||
//! Most peers are told apart by their IP address alone. A link-local
|
||||
//! address (`fe80::/10`) means something only on one link, and the same
|
||||
//! one is often used on every link a router has, so such a peer is the
|
||||
//! address together with the interface it is reached on: `fe80::1%eth0`.
|
||||
//!
|
||||
//! The kernel knows an interface by a number that changes when the
|
||||
//! interface is made anew, as a tunnel is when it is restarted. So the
|
||||
//! configuration keeps the name, and the number is looked up whenever it
|
||||
//! is needed.
|
||||
|
||||
use std::fmt;
|
||||
use std::io;
|
||||
use std::net::{IpAddr, Ipv6Addr, SocketAddr, SocketAddrV6};
|
||||
|
||||
/// The longest name the kernel gives an interface.
|
||||
const MAX_INTERFACE_NAME: usize = 15;
|
||||
|
||||
/// A peer's IP address, and for a link-local address the interface it is
|
||||
/// on.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub struct PeerAddress {
|
||||
pub ip: IpAddr,
|
||||
/// The number of the interface, for a link-local address; zero for
|
||||
/// any other.
|
||||
pub interface: u32,
|
||||
}
|
||||
|
||||
impl From<SocketAddr> for PeerAddress {
|
||||
fn from(address: SocketAddr) -> Self {
|
||||
let interface = match address {
|
||||
SocketAddr::V6(v6) if v6.ip().is_unicast_link_local() => v6.scope_id(),
|
||||
_ => 0,
|
||||
};
|
||||
Self {
|
||||
ip: address.ip(),
|
||||
interface,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PeerAddress {
|
||||
/// Whether `text` is this address as someone might type it: as it is
|
||||
/// written out, or without the interface.
|
||||
pub fn is_called(&self, text: &str) -> bool {
|
||||
text.parse() == Ok(self.ip) || self.to_string() == text
|
||||
}
|
||||
}
|
||||
|
||||
impl From<IpAddr> for PeerAddress {
|
||||
fn from(ip: IpAddr) -> Self {
|
||||
Self { ip, interface: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether `wanted`, as someone typed it, names the peer at `address`
|
||||
/// that is configured as `name`, if it is: by that name, or by the address
|
||||
/// as [`PeerAddress::is_called`] reads it. The one rule for every place a
|
||||
/// peer is asked for by name.
|
||||
pub fn names_peer(wanted: &str, name: Option<&str>, address: PeerAddress) -> bool {
|
||||
name == Some(wanted) || address.is_called(wanted)
|
||||
}
|
||||
|
||||
/// The address, and the interface by name if there is one: `fe80::1%eth0`.
|
||||
/// An interface that is no longer there goes by its number.
|
||||
impl fmt::Display for PeerAddress {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.ip)?;
|
||||
match self.interface {
|
||||
0 => Ok(()),
|
||||
index => match interface_name(index) {
|
||||
Some(name) => write!(f, "%{name}"),
|
||||
None => write!(f, "%{index}"),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `address` as text, with an interface by its name: `[fe80::1%eth0]:179`.
|
||||
pub fn socket_text(address: SocketAddr) -> String {
|
||||
match PeerAddress::from(address) {
|
||||
PeerAddress { interface: 0, ip } => SocketAddr::new(ip, address.port()).to_string(),
|
||||
scoped => format!("[{scoped}]:{}", address.port()),
|
||||
}
|
||||
}
|
||||
|
||||
/// The number of the interface called `name`, if there is one now.
|
||||
pub fn interface_index(name: &str) -> Option<u32> {
|
||||
nix::net::if_::if_nametoindex(name).ok()
|
||||
}
|
||||
|
||||
/// The name of the interface with this number, if there is one now.
|
||||
pub fn interface_name(index: u32) -> Option<String> {
|
||||
nix::net::if_::if_indextoname(index)
|
||||
.ok()?
|
||||
.into_string()
|
||||
.ok()
|
||||
}
|
||||
|
||||
/// A peer's address as the configuration has it.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Endpoint {
|
||||
pub ip: IpAddr,
|
||||
pub port: u16,
|
||||
/// The interface a link-local address is on; `None` for any other
|
||||
/// address.
|
||||
pub interface: Option<String>,
|
||||
}
|
||||
|
||||
/// What is wrong with a peer's address.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
|
||||
pub enum EndpointProblem {
|
||||
#[error("{0:?} is not an IP address")]
|
||||
NotAnAddress(String),
|
||||
#[error(
|
||||
"{0} is a link-local address, which needs the interface it is on: {0}%eth0, for example"
|
||||
)]
|
||||
NoInterface(Ipv6Addr),
|
||||
#[error("{0} is not a link-local address, and takes no interface")]
|
||||
NotLinkLocal(IpAddr),
|
||||
#[error("{0:?} is not the name of an interface")]
|
||||
InterfaceName(String),
|
||||
}
|
||||
|
||||
impl Endpoint {
|
||||
/// Reads `text`: an IP address, with `%` and the name of an interface
|
||||
/// after it if, and only if, it is link-local.
|
||||
pub fn parse(text: &str, port: u16) -> Result<Self, EndpointProblem> {
|
||||
let (address, interface) = match text.split_once('%') {
|
||||
Some((address, interface)) => (address, Some(interface)),
|
||||
None => (text, None),
|
||||
};
|
||||
let ip: IpAddr = address
|
||||
.parse()
|
||||
.map_err(|_| EndpointProblem::NotAnAddress(text.to_string()))?;
|
||||
let link_local = match ip {
|
||||
IpAddr::V6(v6) if v6.is_unicast_link_local() => Some(v6),
|
||||
_ => None,
|
||||
};
|
||||
let interface = match (link_local, interface) {
|
||||
(None, None) => None,
|
||||
(Some(v6), None) => return Err(EndpointProblem::NoInterface(v6)),
|
||||
(None, Some(_)) => return Err(EndpointProblem::NotLinkLocal(ip)),
|
||||
(Some(_), Some(name)) => {
|
||||
let fine = !name.is_empty()
|
||||
&& name.len() <= MAX_INTERFACE_NAME
|
||||
&& name
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_graphic() && c != '/' && c != '%');
|
||||
if !fine {
|
||||
return Err(EndpointProblem::InterfaceName(name.to_string()));
|
||||
}
|
||||
Some(name.to_string())
|
||||
}
|
||||
};
|
||||
Ok(Self {
|
||||
ip,
|
||||
port,
|
||||
interface,
|
||||
})
|
||||
}
|
||||
|
||||
/// The number of the peer's interface as things are now: zero if the
|
||||
/// address needs none, and `None` if the interface is not there.
|
||||
fn scope(&self) -> Option<u32> {
|
||||
match &self.interface {
|
||||
None => Some(0),
|
||||
Some(name) => interface_index(name),
|
||||
}
|
||||
}
|
||||
|
||||
/// What a connection from the peer comes from, as things are now.
|
||||
/// `None` if the peer's interface is not there.
|
||||
pub fn address(&self) -> Option<PeerAddress> {
|
||||
Some(PeerAddress {
|
||||
ip: self.ip,
|
||||
interface: self.scope()?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Whether a connection from `address` is one from this peer.
|
||||
pub fn is_at(&self, address: PeerAddress) -> bool {
|
||||
// The interface is looked up only for a peer whose address it is.
|
||||
self.ip == address.ip && self.scope() == Some(address.interface)
|
||||
}
|
||||
|
||||
/// Where to connect to reach the peer.
|
||||
pub fn to_dial(&self) -> io::Result<SocketAddr> {
|
||||
self.socket(self.ip, self.port)
|
||||
}
|
||||
|
||||
/// `ip` and `port` as an address on the peer's link: for the peer
|
||||
/// itself, and for our own address there.
|
||||
pub fn socket(&self, ip: IpAddr, port: u16) -> io::Result<SocketAddr> {
|
||||
let scope = self.scope().ok_or_else(|| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::NotFound,
|
||||
format!(
|
||||
"there is no interface {}",
|
||||
self.interface.as_deref().unwrap_or_default()
|
||||
),
|
||||
)
|
||||
})?;
|
||||
Ok(match ip {
|
||||
IpAddr::V6(v6) if v6.is_unicast_link_local() => {
|
||||
SocketAddrV6::new(v6, port, 0, scope).into()
|
||||
}
|
||||
ip => SocketAddr::new(ip, port),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// The address and the interface: `fe80::1%eth0`. Without the port.
|
||||
impl fmt::Display for Endpoint {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match &self.interface {
|
||||
None => write!(f, "{}", self.ip),
|
||||
Some(interface) => write!(f, "{}%{interface}", self.ip),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Endpoint {
|
||||
/// The address and the port, as [`SocketAddr`] is written:
|
||||
/// `[fe80::1%eth0]:179`.
|
||||
pub fn with_port(&self) -> String {
|
||||
match self.ip {
|
||||
IpAddr::V4(v4) => format!("{v4}:{}", self.port),
|
||||
IpAddr::V6(_) => format!("[{self}]:{}", self.port),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SocketAddr> for Endpoint {
|
||||
/// An address that needs no interface. A link-local one keeps none
|
||||
/// and so reaches nothing; those are read with [`Endpoint::parse`].
|
||||
fn from(address: SocketAddr) -> Self {
|
||||
Self {
|
||||
ip: address.ip(),
|
||||
port: address.port(),
|
||||
interface: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// An interface every machine has, and its number.
|
||||
fn loopback() -> (String, u32) {
|
||||
["lo", "lo0"]
|
||||
.iter()
|
||||
.find_map(|name| Some((name.to_string(), interface_index(name)?)))
|
||||
.expect("a loopback interface")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_link_local_address_is_read_with_its_interface() {
|
||||
let peer = Endpoint::parse("fe80:1234::1%eno3", 179).unwrap();
|
||||
assert_eq!(peer.ip, "fe80:1234::1".parse::<IpAddr>().unwrap());
|
||||
assert_eq!(peer.interface.as_deref(), Some("eno3"));
|
||||
assert_eq!(peer.to_string(), "fe80:1234::1%eno3");
|
||||
assert_eq!(peer.with_port(), "[fe80:1234::1%eno3]:179");
|
||||
|
||||
let plain = Endpoint::parse("2001:db8::1", 1179).unwrap();
|
||||
assert_eq!(plain.interface, None);
|
||||
assert_eq!(plain.with_port(), "[2001:db8::1]:1179");
|
||||
assert_eq!(
|
||||
Endpoint::parse("192.0.2.1", 179).unwrap().with_port(),
|
||||
"192.0.2.1:179"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_address_and_an_interface_that_do_not_go_together_are_refused() {
|
||||
let problem = |text: &str| Endpoint::parse(text, 179).unwrap_err();
|
||||
assert_eq!(
|
||||
problem("fe80::1"),
|
||||
EndpointProblem::NoInterface("fe80::1".parse().unwrap())
|
||||
);
|
||||
assert_eq!(
|
||||
problem("febf::1"),
|
||||
EndpointProblem::NoInterface("febf::1".parse().unwrap())
|
||||
);
|
||||
assert_eq!(
|
||||
problem("2001:db8::1%eth0"),
|
||||
EndpointProblem::NotLinkLocal("2001:db8::1".parse().unwrap())
|
||||
);
|
||||
assert_eq!(
|
||||
problem("192.0.2.1%eth0"),
|
||||
EndpointProblem::NotLinkLocal("192.0.2.1".parse().unwrap())
|
||||
);
|
||||
// fec0::/10 was site-local: not link-local.
|
||||
assert!(Endpoint::parse("fec0::1", 179).is_ok());
|
||||
for name in ["", "a/b", "an interface", "sixteen-letters-", "a%b"] {
|
||||
assert_eq!(
|
||||
problem(&format!("fe80::1%{name}")),
|
||||
EndpointProblem::InterfaceName(name.to_string()),
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
problem("example"),
|
||||
EndpointProblem::NotAnAddress("example".into())
|
||||
);
|
||||
}
|
||||
|
||||
/// The interface's number is whatever it is when it is asked for.
|
||||
#[test]
|
||||
fn the_interface_is_looked_up_when_it_is_needed() {
|
||||
let (name, index) = loopback();
|
||||
let peer = Endpoint::parse(&format!("fe80::1%{name}"), 179).unwrap();
|
||||
let ip: IpAddr = "fe80::1".parse().unwrap();
|
||||
let there = PeerAddress {
|
||||
ip,
|
||||
interface: index,
|
||||
};
|
||||
assert_eq!(peer.address(), Some(there));
|
||||
assert!(peer.is_at(there));
|
||||
// The same address on another link is another peer.
|
||||
assert!(!peer.is_at(PeerAddress {
|
||||
ip,
|
||||
interface: index + 1000
|
||||
}));
|
||||
assert!(!peer.is_at(ip.into()));
|
||||
match peer.to_dial().unwrap() {
|
||||
SocketAddr::V6(v6) => assert_eq!((v6.scope_id(), v6.port()), (index, 179)),
|
||||
SocketAddr::V4(_) => panic!("an IPv6 peer"),
|
||||
}
|
||||
assert_eq!(there.to_string(), format!("fe80::1%{name}"));
|
||||
assert!(there.is_called(&format!("fe80::1%{name}")));
|
||||
assert!(there.is_called("fe80::1"));
|
||||
assert!(!there.is_called("fe80::1%other"));
|
||||
assert!(!there.is_called("fe80::2"));
|
||||
// By its configured name as well, where it has one.
|
||||
assert!(names_peer("upstream", Some("upstream"), there));
|
||||
assert!(names_peer("fe80::1", Some("upstream"), there));
|
||||
assert!(names_peer("fe80::1", None, there));
|
||||
assert!(!names_peer("upstream", None, there));
|
||||
assert!(!names_peer("other", Some("upstream"), there));
|
||||
assert_eq!(
|
||||
socket_text(peer.to_dial().unwrap()),
|
||||
format!("[fe80::1%{name}]:179")
|
||||
);
|
||||
|
||||
let gone = Endpoint::parse("fe80::1%not-there", 179).unwrap();
|
||||
assert_eq!(gone.address(), None);
|
||||
assert!(!gone.is_at(there));
|
||||
assert_eq!(
|
||||
gone.to_dial().unwrap_err().to_string(),
|
||||
"there is no interface not-there"
|
||||
);
|
||||
}
|
||||
|
||||
/// Only a link-local address keeps the interface it came in on.
|
||||
#[test]
|
||||
fn only_a_link_local_address_has_an_interface() {
|
||||
let global: SocketAddr =
|
||||
SocketAddrV6::new("2001:db8::1".parse().unwrap(), 179, 0, 7).into();
|
||||
assert_eq!(PeerAddress::from(global).interface, 0);
|
||||
let local: SocketAddr = SocketAddrV6::new("fe80::1".parse().unwrap(), 179, 0, 7).into();
|
||||
assert_eq!(PeerAddress::from(local).interface, 7);
|
||||
assert_eq!(socket_text(global), "[2001:db8::1]:179");
|
||||
}
|
||||
}
|
||||
+102
-2385
File diff suppressed because it is too large.
Load diff
@@ -24,13 +24,12 @@
|
||||
//! by the same side the newer wins: the initiator has abandoned the older.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::net::Ipv4Addr;
|
||||
use std::net::{IpAddr, Ipv4Addr};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use tokio::sync::oneshot;
|
||||
|
||||
use crate::address::PeerAddress;
|
||||
use crate::metrics::COLLISIONS;
|
||||
use crate::sync::IgnorePoison;
|
||||
|
||||
@@ -84,7 +83,7 @@ fn new_loses(
|
||||
|
||||
/// Who a connection is with: the address at the other end, and the BGP
|
||||
/// Identifier announced from it.
|
||||
type Peer = (PeerAddress, Ipv4Addr);
|
||||
type Peer = (IpAddr, Ipv4Addr);
|
||||
|
||||
/// Shared by every connection of one server.
|
||||
#[derive(Clone)]
|
||||
@@ -103,33 +102,19 @@ impl CollisionTable {
|
||||
}
|
||||
}
|
||||
|
||||
/// Call when a connection receives the peer's OPEN. `one_identifier`
|
||||
/// says that the address is that of one router, which holds for a
|
||||
/// configured peer. `None` means this connection lost and must be
|
||||
/// closed with Cease / Connection Collision Resolution. Otherwise any
|
||||
/// rival has been told to close and this connection holds the slot
|
||||
/// until the guard drops.
|
||||
/// Call when a connection receives the peer's OPEN. `None` means this
|
||||
/// connection lost and must be closed with Cease / Connection Collision
|
||||
/// Resolution. Otherwise any rival has been told to close and this
|
||||
/// connection holds the slot until the guard drops.
|
||||
pub fn admit(
|
||||
&self,
|
||||
local_id: Ipv4Addr,
|
||||
remote_addr: PeerAddress,
|
||||
remote_addr: IpAddr,
|
||||
remote_id: Ipv4Addr,
|
||||
direction: Direction,
|
||||
one_identifier: bool,
|
||||
) -> Option<ConnectionGuard> {
|
||||
let peer = (remote_addr, remote_id);
|
||||
let mut entries = self.entries.locked();
|
||||
// A configured peer is one router, with one identifier. A second
|
||||
// connection from its address under another would be a second
|
||||
// session, with a second allowance of routes.
|
||||
if one_identifier
|
||||
&& entries
|
||||
.keys()
|
||||
.any(|(address, id)| *address == remote_addr && *id != remote_id)
|
||||
{
|
||||
COLLISIONS.with_label_values(&["new"]).inc();
|
||||
return None;
|
||||
}
|
||||
if let Some(existing) = entries.get_mut(&peer) {
|
||||
if new_loses(
|
||||
local_id,
|
||||
@@ -143,7 +128,6 @@ impl CollisionTable {
|
||||
}
|
||||
if let Some(kill) = existing.kill.take() {
|
||||
COLLISIONS.with_label_values(&["existing"]).inc();
|
||||
// Fails only if the rival has gone already.
|
||||
let _ = kill.send(());
|
||||
}
|
||||
}
|
||||
@@ -279,34 +263,26 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn table_signals_loser_and_releases_slots() {
|
||||
let table = CollisionTable::new(false);
|
||||
let addr = PeerAddress::from(std::net::IpAddr::from(Ipv4Addr::new(192, 0, 2, 1)));
|
||||
let addr: IpAddr = Ipv4Addr::new(192, 0, 2, 1).into();
|
||||
// We are LOW: outbound first, then the peer's inbound wins.
|
||||
let mut out = table.admit(LOW, addr, HIGH, Outbound, false).unwrap();
|
||||
let inb = table.admit(LOW, addr, HIGH, Inbound, false).unwrap();
|
||||
let mut out = table.admit(LOW, addr, HIGH, Outbound).unwrap();
|
||||
let inb = table.admit(LOW, addr, HIGH, Inbound).unwrap();
|
||||
assert!(out.killed.try_recv().is_ok());
|
||||
// The loser's guard dropping must not free the winner's slot.
|
||||
drop(out);
|
||||
inb.set_established();
|
||||
assert!(table.admit(LOW, addr, HIGH, Outbound, false).is_none());
|
||||
assert!(table.admit(LOW, addr, HIGH, Outbound).is_none());
|
||||
|
||||
// A different identifier never collides.
|
||||
assert!(
|
||||
table
|
||||
.admit(LOW, addr, Ipv4Addr::new(192, 0, 2, 2), Inbound, false)
|
||||
.admit(LOW, addr, Ipv4Addr::new(192, 0, 2, 2), Inbound)
|
||||
.is_some()
|
||||
);
|
||||
|
||||
// Unless the address is one router's, which has one identifier: a
|
||||
// second would be a second session, with its own limit of routes.
|
||||
assert!(
|
||||
table
|
||||
.admit(LOW, addr, Ipv4Addr::new(192, 0, 2, 3), Inbound, true)
|
||||
.is_none()
|
||||
);
|
||||
|
||||
// Once the survivor goes away the identifier is free again.
|
||||
drop(inb);
|
||||
assert!(table.admit(LOW, addr, HIGH, Outbound, false).is_some());
|
||||
assert!(table.admit(LOW, addr, HIGH, Outbound).is_some());
|
||||
}
|
||||
|
||||
/// A neighbour reached over IPv4 and over IPv6 announces the same
|
||||
@@ -316,12 +292,12 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn the_same_identifier_from_another_address_is_another_peer() {
|
||||
let table = CollisionTable::new(true);
|
||||
let v4 = PeerAddress::from(std::net::IpAddr::from(Ipv4Addr::new(192, 0, 2, 1)));
|
||||
let v6 = PeerAddress::from("2001:db8::1".parse::<std::net::IpAddr>().unwrap());
|
||||
let v4: IpAddr = Ipv4Addr::new(192, 0, 2, 1).into();
|
||||
let v6: IpAddr = "2001:db8::1".parse().unwrap();
|
||||
|
||||
let mut over_v4 = table.admit(LOW, v4, HIGH, Outbound, false).unwrap();
|
||||
let mut over_v4 = table.admit(LOW, v4, HIGH, Outbound).unwrap();
|
||||
over_v4.set_established();
|
||||
let over_v6 = table.admit(LOW, v6, HIGH, Inbound, false);
|
||||
let over_v6 = table.admit(LOW, v6, HIGH, Inbound);
|
||||
assert!(over_v6.is_some());
|
||||
assert!(over_v4.killed.try_recv().is_err(), "the first is untouched");
|
||||
}
|
||||
|
||||
+126
-1215
File diff suppressed because it is too large.
Load diff
+217
-1458
File diff suppressed because it is too large.
Load diff
+40
-95
@@ -8,7 +8,6 @@
|
||||
//! connection is the one closed, the next pass finds the surviving session
|
||||
//! and goes idle.
|
||||
|
||||
use std::hash::{BuildHasher, Hasher, RandomState};
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
@@ -32,38 +31,26 @@ pub fn spawn_dialers(
|
||||
) -> Vec<JoinHandle<()>> {
|
||||
peers
|
||||
.iter()
|
||||
// A passive peer is left to connect to us.
|
||||
.filter(|peer| !peer.passive)
|
||||
.map(|peer| tokio::spawn(dial_loop(peer.clone(), Arc::clone(server), connect_retry)))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Opens the TCP connection, from the configured source address if any.
|
||||
async fn connect(peer: &PeerConfig) -> std::io::Result<TcpStream> {
|
||||
// Worked out for each attempt: an interface that was made anew has a
|
||||
// new number.
|
||||
let endpoint = peer.endpoint.to_dial()?;
|
||||
let Some(local) = peer.local_address else {
|
||||
return TcpStream::connect(endpoint).await;
|
||||
return TcpStream::connect(peer.endpoint).await;
|
||||
};
|
||||
let socket = if endpoint.is_ipv4() {
|
||||
let socket = if peer.endpoint.is_ipv4() {
|
||||
TcpSocket::new_v4()?
|
||||
} else {
|
||||
TcpSocket::new_v6()?
|
||||
};
|
||||
// A link-local address of our own is on the peer's link.
|
||||
socket.bind(peer.endpoint.socket(local, 0)?).map_err(|e| {
|
||||
std::io::Error::new(e.kind(), format!("cannot use local_address {local}: {e}"))
|
||||
})?;
|
||||
socket.connect(endpoint).await
|
||||
}
|
||||
|
||||
/// `wait`, less up to a quarter of it, by chance (RFC 4271 §10).
|
||||
fn jittered(wait: Duration) -> Duration {
|
||||
// The standard library's source of chance, which is what keys its
|
||||
// hash tables: good enough to tell dialers apart.
|
||||
let chance = RandomState::new().build_hasher().finish();
|
||||
wait.mul_f64(0.75 + 0.25 * (chance as f64 / u64::MAX as f64))
|
||||
socket
|
||||
.bind(std::net::SocketAddr::new(local, 0))
|
||||
.map_err(|e| {
|
||||
std::io::Error::new(e.kind(), format!("cannot use local_address {local}: {e}"))
|
||||
})?;
|
||||
socket.connect(peer.endpoint).await
|
||||
}
|
||||
|
||||
async fn dial_loop(peer: PeerConfig, server: Arc<ServerContext>, connect_retry: Duration) {
|
||||
@@ -74,98 +61,56 @@ async fn dial_loop(peer: PeerConfig, server: Arc<ServerContext>, connect_retry:
|
||||
if registry.is_shutting_down() {
|
||||
return;
|
||||
}
|
||||
// Where the peer is now; nowhere if its interface is not there.
|
||||
let address = peer.endpoint.address();
|
||||
if address.is_some_and(|address| registry.find(address, peer.remote_asn).is_some()) {
|
||||
if registry.find(peer.endpoint.ip(), peer.remote_asn).is_some() {
|
||||
// Peer dialled us; nothing to do until that session goes away.
|
||||
registry.set_dial_state(&peer.name, DialState::Idle);
|
||||
tokio::time::sleep(connect_retry).await;
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some((remaining, why)) =
|
||||
address.and_then(|address| server.hold_downs.remaining(address))
|
||||
{
|
||||
registry.set_dial_state(
|
||||
&peer.name,
|
||||
DialState::Backoff {
|
||||
until: SystemTime::now() + remaining,
|
||||
last_error: why.as_str().into(),
|
||||
},
|
||||
);
|
||||
tokio::time::sleep(remaining).await;
|
||||
// The wait starts over: the peer has had time to be put right.
|
||||
backoff = connect_retry;
|
||||
continue;
|
||||
}
|
||||
|
||||
registry.set_dial_state(&peer.name, DialState::Connecting);
|
||||
info!(peer = %peer.name, endpoint = %peer.endpoint, "Connecting to peer");
|
||||
|
||||
// Why the attempt ended, and whether that is a failure worth a
|
||||
// line in the log.
|
||||
let (reason, failed) = match tokio::time::timeout(CONNECT_TIMEOUT, connect(&peer)).await {
|
||||
Ok(Ok(stream)) => match stream.peer_addr() {
|
||||
Ok(peer_addr) => {
|
||||
info!(peer = %peer.name, endpoint = %peer.endpoint, "TCP connected, starting session");
|
||||
let end = run_session(
|
||||
stream,
|
||||
peer_addr,
|
||||
Direction::Outbound,
|
||||
Arc::clone(&server),
|
||||
PeerSettings::from(&peer),
|
||||
)
|
||||
.await;
|
||||
// A session that came up is a peer worth dialling again
|
||||
// soon. One that connects and is then refused, for its AS
|
||||
// number say, will be refused again: it is retried ever
|
||||
// more slowly, like one that cannot be reached at all.
|
||||
if end.established {
|
||||
backoff = connect_retry;
|
||||
}
|
||||
let failed = end.result.is_err();
|
||||
(end.reason(), failed)
|
||||
let ended = match tokio::time::timeout(CONNECT_TIMEOUT, connect(&peer)).await {
|
||||
Ok(Ok(stream)) => {
|
||||
info!(peer = %peer.name, endpoint = %peer.endpoint, "TCP connected, starting session");
|
||||
let end = run_session(
|
||||
stream,
|
||||
peer.endpoint,
|
||||
Direction::Outbound,
|
||||
Arc::clone(&server),
|
||||
PeerSettings::from(&peer),
|
||||
)
|
||||
.await;
|
||||
// A session that came up is a peer worth dialling again
|
||||
// soon. One that connects and is then refused, for its AS
|
||||
// number say, will be refused again: it is retried ever
|
||||
// more slowly, like one that cannot be reached at all.
|
||||
if end.established {
|
||||
backoff = connect_retry;
|
||||
}
|
||||
Err(e) => (format!("connection lost at once: {e}"), true),
|
||||
},
|
||||
Ok(Err(e)) => (format!("connect failed: {e}"), true),
|
||||
Err(_) => (format!("connect timed out after {CONNECT_TIMEOUT:?}"), true),
|
||||
end.result.map_err(|e| e.to_string())
|
||||
}
|
||||
Ok(Err(e)) => Err(format!("connect failed: {e}")),
|
||||
Err(_) => Err(format!("connect timed out after {CONNECT_TIMEOUT:?}")),
|
||||
};
|
||||
|
||||
let reason = match ended {
|
||||
Ok(()) => "session closed at our request".to_string(),
|
||||
Err(reason) => {
|
||||
warn!(peer = %peer.name, endpoint = %peer.endpoint, err = %reason, "Session attempt ended");
|
||||
reason
|
||||
}
|
||||
};
|
||||
if failed {
|
||||
warn!(peer = %peer.name, endpoint = %peer.endpoint, err = %reason, "Session attempt ended");
|
||||
}
|
||||
|
||||
// Not all at the same moment, when many peers went away together.
|
||||
let wait = jittered(backoff);
|
||||
registry.set_dial_state(
|
||||
&peer.name,
|
||||
DialState::Backoff {
|
||||
until: SystemTime::now() + wait,
|
||||
until: SystemTime::now() + backoff,
|
||||
last_error: reason,
|
||||
},
|
||||
);
|
||||
tokio::time::sleep(wait).await;
|
||||
tokio::time::sleep(backoff).await;
|
||||
backoff = (backoff * 2).min(MAX_BACKOFF);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// The wait before dialling again is shortened by up to a quarter, by
|
||||
/// chance, so that peers that went away together are not all dialled
|
||||
/// at the same moment (RFC 4271 §10).
|
||||
#[test]
|
||||
fn the_wait_is_jittered_by_up_to_a_quarter() {
|
||||
let wait = Duration::from_secs(100);
|
||||
let mut shortest = wait;
|
||||
for _ in 0..200 {
|
||||
let jittered = jittered(wait);
|
||||
assert!(jittered <= wait, "{jittered:?}");
|
||||
assert!(jittered >= wait.mul_f64(0.75), "{jittered:?}");
|
||||
shortest = shortest.min(jittered);
|
||||
}
|
||||
assert!(shortest < wait, "the wait varies");
|
||||
}
|
||||
}
|
||||
+5
-203
@@ -2,13 +2,10 @@
|
||||
|
||||
use std::io;
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use crate::address::EndpointProblem;
|
||||
use std::time::Duration;
|
||||
|
||||
use bgp_packet::errors::{BgpError, CeaseSubcode, DecodeError, FsmSubcode, OpenMsgSubcode};
|
||||
use bgp_packet::message::NotificationMessage;
|
||||
use bgp_packet::open::BgpRole;
|
||||
use bgp_primitives::parser::EncodeError;
|
||||
use bgp_rib::RibStopped;
|
||||
use bgp_rib::table::TooManyPeers;
|
||||
@@ -16,21 +13,6 @@ use thiserror::Error;
|
||||
|
||||
use crate::family::FamilySet;
|
||||
use crate::fsm::State;
|
||||
use crate::policy::PeerRole;
|
||||
|
||||
/// Says what is wrong between what a peer is configured as and what it
|
||||
/// says it is.
|
||||
fn role_mismatch(configured: Option<PeerRole>, theirs: Option<BgpRole>) -> String {
|
||||
let configured = configured.map_or("no role is configured".to_string(), |role| {
|
||||
format!("the peer is configured as our {}", role.as_str())
|
||||
});
|
||||
match theirs {
|
||||
Some(theirs) => format!("role mismatch: {configured} and says it is {theirs:?}"),
|
||||
None => format!(
|
||||
"role mismatch: {configured} and does not say what it is, or says more than one thing"
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Why a session ended abnormally.
|
||||
#[derive(Debug, Error)]
|
||||
@@ -41,9 +23,6 @@ pub enum SessionError {
|
||||
Decode(#[from] DecodeError),
|
||||
#[error("could not encode a message: {0}")]
|
||||
Encode(#[from] EncodeError),
|
||||
/// A received UPDATE's attributes could not be kept in the table.
|
||||
#[error(transparent)]
|
||||
Unstorable(#[from] crate::routes::Unstorable),
|
||||
/// The peer sent nothing for as long as the pre-negotiation hold time.
|
||||
#[error("timed out waiting for a message in the {0} state")]
|
||||
Timeout(State),
|
||||
@@ -61,13 +40,6 @@ pub enum SessionError {
|
||||
OpenRefused(BgpError),
|
||||
#[error("peer announced AS{actual} but AS{expected} was configured")]
|
||||
WrongPeerAs { expected: u32, actual: u32 },
|
||||
/// RFC 9234 §4.2. `theirs` is `None` if the peer did not say, or said
|
||||
/// more than one thing.
|
||||
#[error("{}", role_mismatch(*.configured, *.theirs))]
|
||||
RoleMismatch {
|
||||
configured: Option<PeerRole>,
|
||||
theirs: Option<BgpRole>,
|
||||
},
|
||||
#[error("closed by connection collision resolution")]
|
||||
Collision,
|
||||
/// The routing table has no room for another peer.
|
||||
@@ -81,17 +53,6 @@ pub enum SessionError {
|
||||
/// the session could carry nothing.
|
||||
#[error("no address family in common: we offer {ours}, the peer offers {theirs}")]
|
||||
NoCommonAddressFamily { ours: FamilySet, theirs: FamilySet },
|
||||
/// The peer has more routes than it is allowed (RFC 4486). It is kept
|
||||
/// down for a while after.
|
||||
#[error("the peer went over its limit of {limit} routes")]
|
||||
PrefixLimitExceeded { limit: u32 },
|
||||
/// We were to announce the peer more routes than its export limit
|
||||
/// allows, and did not. It is kept down for a while after.
|
||||
#[error(
|
||||
"{routes} routes were to be advertised to the peer, over its export limit of {limit}; \
|
||||
none of them was"
|
||||
)]
|
||||
ExportLimitExceeded { limit: u32, routes: usize },
|
||||
/// The peer took nothing we sent for this long (RFC 9687): it has
|
||||
/// stopped reading, and there is no telling it so.
|
||||
#[error("the peer accepted no data for {0:?}")]
|
||||
@@ -104,10 +65,9 @@ impl SessionError {
|
||||
pub fn notification(&self) -> Option<NotificationMessage> {
|
||||
let error = match self {
|
||||
Self::Io(_) | Self::SendHoldTimerExpired(_) | Self::NotifiedByPeer(_) => return None,
|
||||
// Something of ours could not be written out, or the peer's
|
||||
// routes could not be kept, which is a bug here: as for a
|
||||
// panic, the session is reset.
|
||||
Self::Encode(_) | Self::Unstorable(_) => BgpError::Cease(CeaseSubcode::AdminReset),
|
||||
// Something of ours could not be written out, which is a bug
|
||||
// here: as for a panic, the session is reset.
|
||||
Self::Encode(_) => BgpError::Cease(CeaseSubcode::AdminReset),
|
||||
Self::Decode(e) => {
|
||||
return Some(NotificationMessage {
|
||||
error: e.notification.clone(),
|
||||
@@ -124,21 +84,8 @@ impl SessionError {
|
||||
}),
|
||||
Self::OpenRefused(error) => error.clone(),
|
||||
Self::WrongPeerAs { .. } => BgpError::OpenMsg(OpenMsgSubcode::BadPeerAs),
|
||||
Self::RoleMismatch { .. } => BgpError::OpenMsg(OpenMsgSubcode::RoleMismatch),
|
||||
Self::Collision => BgpError::Cease(CeaseSubcode::ConnCollisionResolution),
|
||||
Self::TableFull(_) => BgpError::Cease(CeaseSubcode::OutOfResources),
|
||||
Self::PrefixLimitExceeded { .. } => BgpError::Cease(CeaseSubcode::MaxPrefixes),
|
||||
// Nothing is wrong at the peer's end, as far as we know; its
|
||||
// operators are told what is at ours.
|
||||
Self::ExportLimitExceeded { limit, .. } => {
|
||||
return Some(NotificationMessage {
|
||||
error: BgpError::Cease(CeaseSubcode::AdminShutdown),
|
||||
data: shutdown_communication(&format!(
|
||||
"export limit of {limit} routes reached; no more routes were sent, \
|
||||
and the session is held down for a while"
|
||||
)),
|
||||
});
|
||||
}
|
||||
// The session will be set up again; nothing is wrong at the
|
||||
// peer's end.
|
||||
Self::Panicked(_) => BgpError::Cease(CeaseSubcode::AdminReset),
|
||||
@@ -158,19 +105,6 @@ impl SessionError {
|
||||
}
|
||||
}
|
||||
|
||||
/// A Shutdown Communication (RFC 9003 §2): text for the peer's operators,
|
||||
/// at most 255 octets of UTF-8, after its length.
|
||||
fn shutdown_communication(text: &str) -> Vec<u8> {
|
||||
let mut end = text.len().min(255);
|
||||
while !text.is_char_boundary(end) {
|
||||
end -= 1;
|
||||
}
|
||||
let mut data = Vec::with_capacity(end + 1);
|
||||
data.push(end as u8);
|
||||
data.extend_from_slice(&text.as_bytes()[..end]);
|
||||
data
|
||||
}
|
||||
|
||||
/// Why the server could not start, or could not go on.
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ServerError {
|
||||
@@ -189,9 +123,6 @@ pub enum ServerError {
|
||||
/// they panic.
|
||||
#[error("the {task} task stopped unexpectedly: {reason}")]
|
||||
TaskStopped { task: &'static str, reason: String },
|
||||
/// Routes were to be installed in the kernel, and cannot be.
|
||||
#[error(transparent)]
|
||||
Kernel(#[from] bgp_fib::FibError),
|
||||
}
|
||||
|
||||
/// Why a configuration was rejected.
|
||||
@@ -199,41 +130,14 @@ pub enum ServerError {
|
||||
pub enum ConfigError {
|
||||
#[error("invalid router_id {0:?}: must be a dotted-quad IPv4 address other than 0.0.0.0")]
|
||||
RouterId(String),
|
||||
#[error("invalid cluster_id {0:?}: must be a dotted-quad IPv4 address other than 0.0.0.0")]
|
||||
ClusterId(String),
|
||||
#[error("local_asn must be non-zero")]
|
||||
LocalAsn,
|
||||
#[error("invalid IP address {0:?}")]
|
||||
Address(String),
|
||||
#[error(transparent)]
|
||||
Endpoint(#[from] EndpointProblem),
|
||||
#[error("port {0} is out of range")]
|
||||
Port(u32),
|
||||
#[error("http_endpoint.port is required")]
|
||||
HttpPort,
|
||||
#[error("rpki: no caches are listed")]
|
||||
RpkiNoCaches,
|
||||
#[error("export: {0}")]
|
||||
Export(#[from] ExportProblem),
|
||||
#[error("export_limits: {0}")]
|
||||
ExportLimits(ImportProblem),
|
||||
#[error("community_names {community:?}: {problem}")]
|
||||
CommunityName {
|
||||
community: String,
|
||||
problem: crate::marks::MarksProblem,
|
||||
},
|
||||
#[error("community_names {community:?}: the name is empty")]
|
||||
CommunityNameEmpty { community: String },
|
||||
#[error(
|
||||
"accept_unconfigured_peers is set and export.learned_routes passes learned routes on: \
|
||||
a peer that is not configured has no role, so the routes it sends would go to every \
|
||||
peer, and every route to it"
|
||||
)]
|
||||
UnconfiguredPeersWithoutRole,
|
||||
#[error("rpki: the port of cache {0:?} is required")]
|
||||
RpkiCachePort(String),
|
||||
#[error("graceful_restart: {0}")]
|
||||
GracefulRestart(#[from] GracefulRestartProblem),
|
||||
#[error("peer {peer:?}: {problem}")]
|
||||
Peer { peer: String, problem: PeerProblem },
|
||||
#[error("originate {prefix:?}: {problem}")]
|
||||
@@ -266,82 +170,6 @@ pub enum PeerProblem {
|
||||
AddressFamily,
|
||||
#[error("add_path has a value that is not one of its settings")]
|
||||
AddPath,
|
||||
#[error("role is set for a peer in our own AS; roles are between networks")]
|
||||
RoleInOwnAs,
|
||||
#[error("require_role is set without a role")]
|
||||
RequireRoleWithoutRole,
|
||||
#[error("role has a value that is not one of its settings")]
|
||||
Role,
|
||||
#[error("groups has a 0 in it; groups are numbered from 1")]
|
||||
Group,
|
||||
#[error("route_reflector_client is set for a peer in another AS; it is for routers of our own")]
|
||||
ReflectorClientOutsideAs,
|
||||
#[error(
|
||||
"role is not set, and learned routes are passed on (export.learned_routes): without a \
|
||||
role, the routes the peer sends would go to every other peer, and it would be told of \
|
||||
every route. Set role to PROVIDER, CUSTOMER, PEER, ROUTE_SERVER or ROUTE_SERVER_CLIENT"
|
||||
)]
|
||||
NoRole,
|
||||
#[error("export_limits: {0}")]
|
||||
ExportLimits(ImportProblem),
|
||||
#[error("import: {0}")]
|
||||
Import(#[from] ImportProblem),
|
||||
#[error("export: {0}")]
|
||||
Export(#[from] ExportProblem),
|
||||
#[error("graceful_restart: {0}")]
|
||||
GracefulRestart(#[from] GracefulRestartProblem),
|
||||
}
|
||||
|
||||
/// What is wrong with a `graceful_restart`.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Error)]
|
||||
pub enum GracefulRestartProblem {
|
||||
#[error("restart_time_seconds is {0}; it is at most {max}", max = crate::restart::MAX_RESTART_TIME)]
|
||||
RestartTime(u32),
|
||||
}
|
||||
|
||||
/// What is wrong with a peer's `import` policy.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Error)]
|
||||
pub enum ImportProblem {
|
||||
#[error("prefix {prefix:?}: {problem}")]
|
||||
Prefix {
|
||||
prefix: String,
|
||||
problem: bgp_primitives::ip_prefix::PrefixError,
|
||||
},
|
||||
#[error(
|
||||
"prefix {prefix:?}: max_length {max_length} is not between its own length and {longest}"
|
||||
)]
|
||||
MaxLength {
|
||||
prefix: String,
|
||||
max_length: u32,
|
||||
longest: u8,
|
||||
},
|
||||
#[error("{field} of {value} is longer than an address of {longest} bits")]
|
||||
PrefixLength {
|
||||
field: &'static str,
|
||||
value: u32,
|
||||
longest: u8,
|
||||
},
|
||||
#[error("max_as_path_length of {0} is more than a path can hold")]
|
||||
PathLength(u32),
|
||||
#[error(transparent)]
|
||||
Marks(#[from] crate::marks::MarksProblem),
|
||||
#[error("rule {number}: {problem}")]
|
||||
Rule {
|
||||
/// From 1, as an operator counts.
|
||||
number: usize,
|
||||
problem: Box<ImportProblem>,
|
||||
},
|
||||
}
|
||||
|
||||
/// What is wrong with an `export` policy.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Error)]
|
||||
pub enum ExportProblem {
|
||||
#[error(transparent)]
|
||||
Marks(#[from] crate::marks::MarksProblem),
|
||||
#[error("as_path_prepend of {value} is more than {max}")]
|
||||
Prepend { value: u32, max: u32 },
|
||||
#[error("all_paths needs learned_routes")]
|
||||
AllPathsWithoutLearnedRoutes,
|
||||
}
|
||||
|
||||
/// What is wrong with one `originate` entry.
|
||||
@@ -351,8 +179,8 @@ pub enum OriginateProblem {
|
||||
Prefix(#[from] bgp_primitives::ip_prefix::PrefixError),
|
||||
#[error("origin {0:?} must be IGP, EGP or INCOMPLETE")]
|
||||
Origin(String),
|
||||
#[error("as_path_prepend of {value} is more than {max}")]
|
||||
Prepend { value: u32, max: u32 },
|
||||
#[error("as_path_prepend must be at most {max}")]
|
||||
Prepend { max: u32 },
|
||||
#[error("{0}")]
|
||||
Community(#[from] bgp_packet::community::CommunityError),
|
||||
#[error("listed more than once")]
|
||||
@@ -416,31 +244,5 @@ mod tests {
|
||||
notification(SessionError::Io(io::ErrorKind::BrokenPipe.into())),
|
||||
None
|
||||
);
|
||||
// Going over an export limit is ours to explain: an Administrative
|
||||
// Shutdown, with the reason for the peer's operators (RFC 9003).
|
||||
let (error, data) = notification(SessionError::ExportLimitExceeded {
|
||||
limit: 50,
|
||||
routes: 900_000,
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(error, BgpError::Cease(CeaseSubcode::AdminShutdown));
|
||||
let text = std::str::from_utf8(&data[1..]).unwrap();
|
||||
assert_eq!(usize::from(data[0]), text.len());
|
||||
assert!(
|
||||
text.starts_with("export limit of 50 routes reached"),
|
||||
"{text}"
|
||||
);
|
||||
}
|
||||
|
||||
/// A Shutdown Communication is at most 255 octets, and whole
|
||||
/// characters (RFC 9003 §2).
|
||||
#[test]
|
||||
fn a_shutdown_communication_is_cut_to_fit() {
|
||||
let long = "é".repeat(200);
|
||||
let data = shutdown_communication(&long);
|
||||
assert_eq!(usize::from(data[0]), data.len() - 1);
|
||||
assert_eq!(data[0], 254);
|
||||
assert!(std::str::from_utf8(&data[1..]).is_ok());
|
||||
assert_eq!(shutdown_communication(""), [0]);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
+25
-616
@@ -14,18 +14,13 @@
|
||||
use std::net::Ipv4Addr;
|
||||
use std::time::Duration;
|
||||
|
||||
use bgp_packet::errors::{BgpError, CeaseSubcode, DecodeError, OpenMsgSubcode};
|
||||
use bgp_packet::message::{AS_TRANS, BGP4_VERSION, Message, NotificationMessage, UpdateMessage};
|
||||
use bgp_packet::open::{
|
||||
AddPathEntry, Capability, ExtendedNextHopEntry, OpenMessage, OpenOption, SendReceive,
|
||||
};
|
||||
use bgp_primitives::constants::{Afi, AfiSafi, Safi};
|
||||
use bgp_packet::errors::{BgpError, DecodeError, OpenMsgSubcode};
|
||||
use bgp_packet::message::{AS_TRANS, BGP4_VERSION, Message, UpdateMessage};
|
||||
use bgp_packet::open::{AddPathEntry, AddPathSendRecv, Capability, OpenMessage, OpenOption};
|
||||
|
||||
use crate::error::SessionError;
|
||||
use crate::family::FamilySet;
|
||||
use crate::metrics::message_type_name;
|
||||
use crate::policy::PeerRole;
|
||||
use crate::restart::{self, Agreed, Leave, Offer};
|
||||
|
||||
/// How long to wait for the peer's OPEN, and for its first KEEPALIVE when
|
||||
/// the negotiated hold time is zero. RFC 4271 §8.2.2 suggests four minutes.
|
||||
@@ -69,19 +64,7 @@ pub struct FsmConfig {
|
||||
pub families: FamilySet,
|
||||
/// The directions to offer ADD-PATH (RFC 7911) for, on each of
|
||||
/// `families`; `None` for neither.
|
||||
pub add_path: Option<SendReceive>,
|
||||
/// What the peer is to us, if that is configured. Our OPEN then says
|
||||
/// what we are to it, and the peer's must not say otherwise (RFC 9234).
|
||||
/// Not for a peer in our own AS.
|
||||
pub role: Option<PeerRole>,
|
||||
/// Refuse a peer that does not say what it is (RFC 9234 §4.2, "strict
|
||||
/// mode"). Means nothing without `role`.
|
||||
pub require_role: bool,
|
||||
/// Offer to exchange IPv4 routes with IPv6 next hops (RFC 8950): for a
|
||||
/// session over a link that has only IPv6 addresses.
|
||||
pub extended_next_hop: bool,
|
||||
/// What graceful restart (RFC 4724) is offered as, if it is.
|
||||
pub graceful_restart: Option<Offer>,
|
||||
pub add_path: Option<AddPathSendRecv>,
|
||||
}
|
||||
|
||||
/// What the two OPENs settled.
|
||||
@@ -95,8 +78,6 @@ pub struct Negotiated {
|
||||
pub hold_time_secs: u16,
|
||||
/// Whether AS numbers are four octets wide on this session (RFC 6793).
|
||||
pub four_octet_asn: bool,
|
||||
/// Whether the peer can be asked to send its routes again (RFC 2918).
|
||||
pub route_refresh: bool,
|
||||
/// The address families both sides offered: the ones routes may be
|
||||
/// exchanged for. Never empty.
|
||||
pub families: FamilySet,
|
||||
@@ -107,14 +88,6 @@ pub struct Negotiated {
|
||||
/// The families for which we send the peer path identifiers: we
|
||||
/// offered to send and the peer to receive.
|
||||
pub add_path_send: FamilySet,
|
||||
/// Whether the peer said what it is to us, and it is what we have it
|
||||
/// configured as (RFC 9234).
|
||||
pub role_confirmed: bool,
|
||||
/// Whether IPv4 routes go with IPv6 next hops on this session
|
||||
/// (RFC 8950): both sides offered it, and the session carries IPv4.
|
||||
pub extended_next_hop: bool,
|
||||
/// Graceful restart (RFC 4724), if both sides offered it.
|
||||
pub graceful_restart: Option<Agreed>,
|
||||
}
|
||||
|
||||
/// A message from the peer, with what the decoder found out besides.
|
||||
@@ -125,9 +98,6 @@ pub struct Received {
|
||||
/// decoder kept them (`DecodedUpdate::attributes_wire`). They go with
|
||||
/// the UPDATE to whoever stores its routes.
|
||||
pub attributes_wire: Option<Vec<u8>>,
|
||||
/// For an UPDATE: whether the decoder changed it because of errors in
|
||||
/// it (RFC 7606). An UPDATE emptied that way is no End-of-RIB marker.
|
||||
pub repaired: bool,
|
||||
}
|
||||
|
||||
impl From<Message> for Received {
|
||||
@@ -135,7 +105,6 @@ impl From<Message> for Received {
|
||||
Self {
|
||||
message,
|
||||
attributes_wire: None,
|
||||
repaired: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -158,8 +127,6 @@ pub enum Event {
|
||||
/// table has no room for the peer's routes, say. The peer is told as
|
||||
/// the error has it.
|
||||
LocalFailure(SessionError),
|
||||
/// The daemon is going, and the session with it.
|
||||
Leave(Leave),
|
||||
}
|
||||
|
||||
/// Something the driver must do, in the order given.
|
||||
@@ -181,8 +148,6 @@ pub enum Action {
|
||||
/// The peer sent this many routes of address families that were not
|
||||
/// negotiated. They have been left out of the update that follows.
|
||||
RoutesIgnored(usize),
|
||||
/// The peer has sent all its routes of this family once (RFC 4724 §2).
|
||||
EndOfRib(AfiSafi),
|
||||
/// Routes arrived; put them in the routing table.
|
||||
DeliverUpdate {
|
||||
update: UpdateMessage,
|
||||
@@ -265,7 +230,6 @@ impl Fsm {
|
||||
_ => vec![],
|
||||
},
|
||||
Event::Outbound(msg) => self.on_outbound(msg),
|
||||
Event::Leave(leave) => self.leave(leave),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,7 +237,6 @@ impl Fsm {
|
||||
let Received {
|
||||
message,
|
||||
attributes_wire,
|
||||
repaired,
|
||||
} = received;
|
||||
match (&self.phase, message) {
|
||||
// A NOTIFICATION ends the session in any state, and is not
|
||||
@@ -299,14 +262,6 @@ impl Fsm {
|
||||
(Phase::Established(negotiated), Message::KeepAlive) => heard_from_peer(negotiated),
|
||||
(Phase::Established(negotiated), Message::Update(mut update)) => {
|
||||
let mut actions = heard_from_peer(negotiated);
|
||||
// Before the families are restricted, which can leave an
|
||||
// UPDATE that looks like one.
|
||||
if let Some(family) = update.end_of_rib_family().filter(|_| !repaired) {
|
||||
if negotiated.families.contains(family) {
|
||||
actions.push(Action::EndOfRib(family));
|
||||
}
|
||||
return actions;
|
||||
}
|
||||
// A route of a family the session does not carry is ignored
|
||||
// rather than treated as an error: it harms nothing, and
|
||||
// resetting the session over it would.
|
||||
@@ -352,48 +307,42 @@ impl Fsm {
|
||||
});
|
||||
}
|
||||
|
||||
let role_confirmed = match self.negotiate_role(&open) {
|
||||
Ok(confirmed) => confirmed,
|
||||
Err(e) => return self.fail(e),
|
||||
};
|
||||
|
||||
let ours = self.config.families.clone();
|
||||
let theirs = FamilySet::offered_by(&open);
|
||||
let families = ours.intersection(&theirs);
|
||||
if families.is_empty() {
|
||||
return self.fail(SessionError::NoCommonAddressFamily { ours, theirs });
|
||||
}
|
||||
let (add_path_receive, add_path_send) = self.negotiate_add_path(&open, &families);
|
||||
|
||||
// RFC 4724 §3: in use if both offered it.
|
||||
let graceful_restart = self
|
||||
.config
|
||||
.graceful_restart
|
||||
.and_then(|_| Agreed::of(&open, &families));
|
||||
|
||||
// RFC 8950 §4: in use only if both said so.
|
||||
let extended_next_hop = self.config.extended_next_hop
|
||||
&& families.contains(AfiSafi::IPV4_UNICAST)
|
||||
&& open.capabilities().any(|capability| {
|
||||
matches!(capability, Capability::ExtendedNextHop { entries }
|
||||
if entries.contains(&IPV4_WITH_IPV6_NEXT_HOP))
|
||||
// ADD-PATH is in use in a direction, for a family the session
|
||||
// carries, when one side offered to send and the other to receive
|
||||
// (RFC 7911 §4).
|
||||
let (mut add_path_receive, mut add_path_send) =
|
||||
(FamilySet::default(), FamilySet::default());
|
||||
if let Some(ours) = self.config.add_path {
|
||||
let theirs = open.capabilities().flat_map(|capability| match capability {
|
||||
Capability::AddPath { entries } => entries.as_slice(),
|
||||
_ => &[],
|
||||
});
|
||||
for entry in theirs.filter(|entry| families.contains(entry.family())) {
|
||||
if ours.receives() && entry.send_recv.sends() {
|
||||
add_path_receive.insert(entry.family());
|
||||
}
|
||||
if ours.sends() && entry.send_recv.receives() {
|
||||
add_path_send.insert(entry.family());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let negotiated = Negotiated {
|
||||
extended_next_hop,
|
||||
peer_asn,
|
||||
ibgp: peer_asn == self.config.local_asn,
|
||||
peer_identifier: open.identifier,
|
||||
hold_time_secs: self.config.hold_time_secs.min(open.hold_time),
|
||||
four_octet_asn: capability_asn.is_some(),
|
||||
route_refresh: open
|
||||
.capabilities()
|
||||
.any(|capability| matches!(capability, Capability::RouteRefresh)),
|
||||
families,
|
||||
add_path_receive,
|
||||
add_path_send,
|
||||
role_confirmed,
|
||||
graceful_restart,
|
||||
};
|
||||
// With a hold time of zero the timer will not run once Established,
|
||||
// but a peer that never confirms must still be given up on.
|
||||
@@ -422,21 +371,9 @@ impl Fsm {
|
||||
|
||||
/// Ends the session because of `error`, telling the peer with the
|
||||
/// NOTIFICATION that goes with it if there is one.
|
||||
///
|
||||
/// With graceful restart agreed for NOTIFICATIONs too (RFC 8538), one
|
||||
/// that goes for an error is a Hard Reset: the peer is to drop our
|
||||
/// routes, and not keep them for a restart. Not for an expired hold
|
||||
/// timer, which RFC 8538 §4 has the peer treat as a restart; nor for a
|
||||
/// collision, where another connection takes over at once.
|
||||
fn fail(&mut self, error: SessionError) -> Vec<Action> {
|
||||
let for_good = !matches!(
|
||||
error,
|
||||
SessionError::HoldTimerExpired | SessionError::Collision
|
||||
);
|
||||
let notification = error
|
||||
let mut actions: Vec<Action> = error
|
||||
.notification()
|
||||
.map(|notification| self.as_sent(notification, for_good));
|
||||
let mut actions: Vec<Action> = notification
|
||||
.map(|notification| Action::Send(Message::Notification(notification)))
|
||||
.into_iter()
|
||||
.collect();
|
||||
@@ -444,123 +381,12 @@ impl Fsm {
|
||||
actions
|
||||
}
|
||||
|
||||
/// Whether graceful restart applies to sessions that end with a
|
||||
/// NOTIFICATION, other than a Hard Reset (RFC 8538).
|
||||
fn notifications_restart(&self) -> bool {
|
||||
match &self.phase {
|
||||
Phase::OpenConfirm(negotiated) | Phase::Established(negotiated) => negotiated
|
||||
.graceful_restart
|
||||
.as_ref()
|
||||
.is_some_and(|agreed| agreed.notification),
|
||||
Phase::OpenSent | Phase::Closed => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Ends the session because the daemon is going: for good, with a
|
||||
/// NOTIFICATION that has the peer drop our routes now; or to come back
|
||||
/// at once, by closing the connection without one where graceful
|
||||
/// restart is agreed, which has the peer keep them (RFC 4724 §4.2).
|
||||
fn leave(&mut self, leave: Leave) -> Vec<Action> {
|
||||
let agreed = match &self.phase {
|
||||
Phase::Established(negotiated) => negotiated.graceful_restart.is_some(),
|
||||
_ => false,
|
||||
};
|
||||
if leave == Leave::Restart && agreed {
|
||||
return self.close(Ok(()));
|
||||
}
|
||||
let shutdown = NotificationMessage {
|
||||
error: BgpError::Cease(CeaseSubcode::AdminShutdown),
|
||||
data: vec![],
|
||||
};
|
||||
let notification = self.as_sent(shutdown, true);
|
||||
let mut actions = vec![Action::Send(Message::Notification(notification))];
|
||||
actions.extend(self.close(Ok(())));
|
||||
actions
|
||||
}
|
||||
|
||||
/// What the peer says it is to us, held against what is configured
|
||||
/// (RFC 9234 §4.2): whether the two agree, or an error if they do not.
|
||||
/// A peer that does not say is taken at our word for it, unless it is
|
||||
/// required to; what a peer says of itself means nothing where nothing
|
||||
/// is configured to hold it against.
|
||||
fn negotiate_role(&self, open: &OpenMessage) -> Result<bool, SessionError> {
|
||||
let mut their_roles = open
|
||||
.capabilities()
|
||||
.filter_map(|capability| match capability {
|
||||
Capability::BgpRole { role } => Some(*role),
|
||||
_ => None,
|
||||
});
|
||||
let their_role = their_roles.next();
|
||||
let mismatch = |theirs| SessionError::RoleMismatch {
|
||||
configured: self.config.role,
|
||||
theirs,
|
||||
};
|
||||
// Saying it twice is saying it once; saying two things is saying
|
||||
// nothing that can be relied on.
|
||||
if their_roles.any(|role| Some(role) != their_role) {
|
||||
return Err(mismatch(None));
|
||||
}
|
||||
match (self.config.role, their_role) {
|
||||
(Some(configured), Some(theirs)) if theirs == configured.theirs() => Ok(true),
|
||||
(Some(_), Some(theirs)) => Err(mismatch(Some(theirs))),
|
||||
(Some(_), None) if self.config.require_role => Err(mismatch(None)),
|
||||
(Some(_), None) | (None, _) => Ok(false),
|
||||
}
|
||||
}
|
||||
|
||||
/// The families the peer sends us several routes for a prefix in, and
|
||||
/// those we send it several in: ADD-PATH is in use in a direction, for
|
||||
/// a family the session carries, when one side offered to send and the
|
||||
/// other to receive (RFC 7911 §4).
|
||||
fn negotiate_add_path(
|
||||
&self,
|
||||
open: &OpenMessage,
|
||||
families: &FamilySet,
|
||||
) -> (FamilySet, FamilySet) {
|
||||
let (mut receive, mut send) = (FamilySet::default(), FamilySet::default());
|
||||
let Some(ours) = self.config.add_path else {
|
||||
return (receive, send);
|
||||
};
|
||||
let theirs = open.capabilities().flat_map(|capability| match capability {
|
||||
Capability::AddPath { entries } => entries.as_slice(),
|
||||
_ => &[],
|
||||
});
|
||||
for entry in theirs.filter(|entry| families.contains(entry.family())) {
|
||||
if ours.receives() && entry.send_recv.sends() {
|
||||
receive.insert(entry.family());
|
||||
}
|
||||
if ours.sends() && entry.send_recv.receives() {
|
||||
send.insert(entry.family());
|
||||
}
|
||||
}
|
||||
(receive, send)
|
||||
}
|
||||
|
||||
/// `notification` as it goes to the peer: where NOTIFICATIONs count as
|
||||
/// restarts (RFC 8538) and this one is `for_good`, as a Hard Reset, so
|
||||
/// that the peer drops our routes rather than keeping them.
|
||||
fn as_sent(&self, notification: NotificationMessage, for_good: bool) -> NotificationMessage {
|
||||
if for_good && self.notifications_restart() {
|
||||
restart::hard_reset(notification)
|
||||
} else {
|
||||
notification
|
||||
}
|
||||
}
|
||||
|
||||
fn close(&mut self, result: Result<(), SessionError>) -> Vec<Action> {
|
||||
self.phase = Phase::Closed;
|
||||
vec![Action::Close(result)]
|
||||
}
|
||||
}
|
||||
|
||||
/// IPv4 unicast routes with next hops that are IPv6 addresses, as the
|
||||
/// Extended Next Hop capability names it (RFC 8950 §3).
|
||||
const IPV4_WITH_IPV6_NEXT_HOP: ExtendedNextHopEntry = ExtendedNextHopEntry {
|
||||
afi: Afi::IPV4,
|
||||
safi: Safi::UNICAST,
|
||||
nh_afi: Afi::IPV6,
|
||||
};
|
||||
|
||||
/// Any message from the peer shows it is alive (RFC 4271 §4.4).
|
||||
fn heard_from_peer(negotiated: &Negotiated) -> Vec<Action> {
|
||||
hold_time(negotiated)
|
||||
@@ -588,17 +414,6 @@ fn build_open(config: &FsmConfig) -> Message {
|
||||
// not accept routes of a family that was not offered (RFC 4760 §8).
|
||||
capabilities.extend(config.families.iter().map(Capability::multiprotocol));
|
||||
capabilities.push(Capability::RouteRefresh);
|
||||
if let Some(role) = config.role {
|
||||
capabilities.push(Capability::BgpRole { role: role.ours() });
|
||||
}
|
||||
if config.extended_next_hop && config.families.contains(AfiSafi::IPV4_UNICAST) {
|
||||
capabilities.push(Capability::ExtendedNextHop {
|
||||
entries: vec![IPV4_WITH_IPV6_NEXT_HOP],
|
||||
});
|
||||
}
|
||||
if let Some(offer) = &config.graceful_restart {
|
||||
capabilities.push(restart::capability(offer, &config.families));
|
||||
}
|
||||
if let Some(send_recv) = config.add_path {
|
||||
capabilities.push(Capability::AddPath {
|
||||
entries: config
|
||||
@@ -648,8 +463,6 @@ mod tests {
|
||||
|
||||
use bgp_rib::table::TooManyPeers;
|
||||
|
||||
use bgp_packet::open::BgpRole;
|
||||
|
||||
use super::*;
|
||||
use crate::family::SUPPORTED;
|
||||
|
||||
@@ -664,10 +477,6 @@ mod tests {
|
||||
expected_asn: None,
|
||||
families: FamilySet::supported(),
|
||||
add_path: None,
|
||||
role: None,
|
||||
require_role: false,
|
||||
extended_next_hop: false,
|
||||
graceful_restart: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -763,14 +572,10 @@ mod tests {
|
||||
peer_identifier: PEER_ID,
|
||||
hold_time_secs: 30,
|
||||
four_octet_asn: false,
|
||||
route_refresh: false,
|
||||
// The peer listed no families, so it speaks IPv4 unicast.
|
||||
families: only(AfiSafi::IPV4_UNICAST),
|
||||
add_path_receive: FamilySet::default(),
|
||||
add_path_send: FamilySet::default(),
|
||||
role_confirmed: false,
|
||||
extended_next_hop: false,
|
||||
graceful_restart: None,
|
||||
};
|
||||
assert!(matches!(
|
||||
&actions[..],
|
||||
@@ -900,7 +705,8 @@ mod tests {
|
||||
#[test]
|
||||
fn add_path_is_in_use_where_one_side_sends_and_the_other_receives() {
|
||||
use bgp_packet::open::SendReceive::{Both, Receive, Send};
|
||||
let negotiated = |ours: Option<SendReceive>, theirs: Vec<(AfiSafi, SendReceive)>| {
|
||||
let negotiated = |ours: Option<AddPathSendRecv>,
|
||||
theirs: Vec<(AfiSafi, AddPathSendRecv)>| {
|
||||
let (mut fsm, first) = Fsm::new(FsmConfig {
|
||||
add_path: ours,
|
||||
..config()
|
||||
@@ -1000,197 +806,6 @@ mod tests {
|
||||
));
|
||||
}
|
||||
|
||||
/// What becomes of a session with a peer configured as `role`, whose
|
||||
/// OPEN carries the Role capabilities `theirs`: whether the peer
|
||||
/// confirmed its role, or the error the session ended with.
|
||||
fn roles_meet(
|
||||
role: Option<PeerRole>,
|
||||
require_role: bool,
|
||||
theirs: &[BgpRole],
|
||||
) -> Result<bool, BgpError> {
|
||||
let (mut fsm, _) = Fsm::new(FsmConfig {
|
||||
role,
|
||||
require_role,
|
||||
..config()
|
||||
});
|
||||
let open = OpenMessage {
|
||||
options: vec![OpenOption::Capabilities(
|
||||
theirs
|
||||
.iter()
|
||||
.map(|role| Capability::BgpRole { role: *role })
|
||||
.collect(),
|
||||
)],
|
||||
..peer_open(90)
|
||||
};
|
||||
let actions = fsm.handle(received(Message::Open(open)));
|
||||
match &actions[..] {
|
||||
[Action::Admit(negotiated), ..] => Ok(negotiated.role_confirmed),
|
||||
_ => Err(ending(&fsm, actions).0.expect("a NOTIFICATION")),
|
||||
}
|
||||
}
|
||||
|
||||
/// RFC 8950: IPv4 routes with IPv6 next hops are in use only if both
|
||||
/// sides offer it, and we offer it only where asked to.
|
||||
#[test]
|
||||
fn ipv4_with_ipv6_next_hops_is_in_use_only_if_both_offer_it() {
|
||||
let offer = Capability::ExtendedNextHop {
|
||||
entries: vec![IPV4_WITH_IPV6_NEXT_HOP],
|
||||
};
|
||||
let outcome = |ours: bool, theirs: Vec<Capability>| {
|
||||
let (mut fsm, first) = Fsm::new(FsmConfig {
|
||||
extended_next_hop: ours,
|
||||
..config()
|
||||
});
|
||||
let Action::Send(open) = &first[0] else {
|
||||
panic!("expected an OPEN first, got {first:?}");
|
||||
};
|
||||
assert_eq!(capabilities(open).contains(&&offer), ours);
|
||||
let mut offered = vec![
|
||||
Capability::multiprotocol(AfiSafi::IPV4_UNICAST),
|
||||
Capability::multiprotocol(AfiSafi::IPV6_UNICAST),
|
||||
];
|
||||
offered.extend(theirs);
|
||||
let open = OpenMessage {
|
||||
options: vec![OpenOption::Capabilities(offered)],
|
||||
..peer_open(90)
|
||||
};
|
||||
match &fsm.handle(received(Message::Open(open)))[..] {
|
||||
[Action::Admit(negotiated), ..] => negotiated.extended_next_hop,
|
||||
other => panic!("expected the session to go on, got {other:?}"),
|
||||
}
|
||||
};
|
||||
assert!(outcome(true, vec![offer.clone()]));
|
||||
assert!(!outcome(true, vec![]));
|
||||
assert!(!outcome(false, vec![offer.clone()]));
|
||||
// Offered for something else: IPv6 routes with IPv4 next hops,
|
||||
// say.
|
||||
let other = Capability::ExtendedNextHop {
|
||||
entries: vec![ExtendedNextHopEntry {
|
||||
afi: Afi::IPV6,
|
||||
safi: Safi::UNICAST,
|
||||
nh_afi: Afi::IPV4,
|
||||
}],
|
||||
};
|
||||
assert!(!outcome(true, vec![other]));
|
||||
}
|
||||
|
||||
/// RFC 9234 §4.1: our OPEN says what we are to the peer, which is the
|
||||
/// other half of what the peer is to us.
|
||||
#[test]
|
||||
fn our_open_says_what_we_are_to_the_peer() {
|
||||
for (peer_is, we_are) in [
|
||||
(PeerRole::Provider, BgpRole::Customer),
|
||||
(PeerRole::Customer, BgpRole::Provider),
|
||||
(PeerRole::Peer, BgpRole::Peer),
|
||||
(PeerRole::RouteServer, BgpRole::RouteServerClient),
|
||||
(PeerRole::RouteServerClient, BgpRole::RouteServer),
|
||||
] {
|
||||
let (_, actions) = Fsm::new(FsmConfig {
|
||||
role: Some(peer_is),
|
||||
..config()
|
||||
});
|
||||
let Action::Send(open) = &actions[0] else {
|
||||
panic!("expected an OPEN first, got {actions:?}");
|
||||
};
|
||||
let roles: Vec<_> = capabilities(open)
|
||||
.into_iter()
|
||||
.filter(|capability| matches!(capability, Capability::BgpRole { .. }))
|
||||
.collect();
|
||||
assert_eq!(
|
||||
roles,
|
||||
[&Capability::BgpRole { role: we_are }],
|
||||
"{peer_is:?}"
|
||||
);
|
||||
}
|
||||
// Nothing is said where nothing is configured.
|
||||
let (_, actions) = Fsm::new(config());
|
||||
let Action::Send(open) = &actions[0] else {
|
||||
panic!("expected an OPEN first, got {actions:?}");
|
||||
};
|
||||
assert!(
|
||||
!capabilities(open)
|
||||
.iter()
|
||||
.any(|capability| matches!(capability, Capability::BgpRole { .. }))
|
||||
);
|
||||
}
|
||||
|
||||
/// RFC 9234 §4.2.
|
||||
#[test]
|
||||
fn a_peer_that_says_it_is_something_else_than_configured_is_refused() {
|
||||
let mismatch = Err(BgpError::OpenMsg(OpenMsgSubcode::RoleMismatch));
|
||||
let all = [
|
||||
BgpRole::Provider,
|
||||
BgpRole::RouteServer,
|
||||
BgpRole::RouteServerClient,
|
||||
BgpRole::Customer,
|
||||
BgpRole::Peer,
|
||||
];
|
||||
for configured in [
|
||||
PeerRole::Provider,
|
||||
PeerRole::Customer,
|
||||
PeerRole::Peer,
|
||||
PeerRole::RouteServer,
|
||||
PeerRole::RouteServerClient,
|
||||
] {
|
||||
for theirs in all {
|
||||
let expected = if theirs == configured.theirs() {
|
||||
Ok(true)
|
||||
} else {
|
||||
mismatch.clone()
|
||||
};
|
||||
assert_eq!(
|
||||
roles_meet(Some(configured), false, &[theirs]),
|
||||
expected,
|
||||
"configured as {configured:?}, says {theirs:?}"
|
||||
);
|
||||
}
|
||||
// A role nobody has defined is not the one configured.
|
||||
assert_eq!(
|
||||
roles_meet(Some(configured), false, &[BgpRole::Unknown(9)]),
|
||||
mismatch
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_peer_that_does_not_say_what_it_is_is_refused_only_if_it_must() {
|
||||
let mismatch = Err(BgpError::OpenMsg(OpenMsgSubcode::RoleMismatch));
|
||||
// A peer that knows nothing of roles is taken at our word for it,
|
||||
assert_eq!(roles_meet(Some(PeerRole::Customer), false, &[]), Ok(false));
|
||||
// unless it is required to say.
|
||||
assert_eq!(roles_meet(Some(PeerRole::Customer), true, &[]), mismatch);
|
||||
assert_eq!(
|
||||
roles_meet(Some(PeerRole::Customer), true, &[BgpRole::Customer]),
|
||||
Ok(true)
|
||||
);
|
||||
// What a peer says where nothing is configured confirms nothing.
|
||||
assert_eq!(roles_meet(None, false, &[BgpRole::Provider]), Ok(false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_peer_that_says_two_things_is_refused() {
|
||||
let mismatch = Err(BgpError::OpenMsg(OpenMsgSubcode::RoleMismatch));
|
||||
// The same thing twice is the same thing.
|
||||
assert_eq!(
|
||||
roles_meet(Some(PeerRole::Peer), false, &[BgpRole::Peer, BgpRole::Peer]),
|
||||
Ok(true)
|
||||
);
|
||||
// Two things, one of them right, is not.
|
||||
assert_eq!(
|
||||
roles_meet(
|
||||
Some(PeerRole::Peer),
|
||||
false,
|
||||
&[BgpRole::Peer, BgpRole::Customer]
|
||||
),
|
||||
mismatch
|
||||
);
|
||||
// Not even where we have nothing configured to compare with.
|
||||
assert_eq!(
|
||||
roles_meet(None, false, &[BgpRole::Peer, BgpRole::Customer]),
|
||||
mismatch
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_unacceptable_open_is_refused_with_the_matching_subcode() {
|
||||
let refused = |open: OpenMessage, config: FsmConfig| {
|
||||
@@ -1342,29 +957,7 @@ mod tests {
|
||||
let actions = fsm.handle(received(Message::KeepAlive));
|
||||
assert!(matches!(&actions[..], [Action::ResetHoldTimer(d)] if *d == hold));
|
||||
|
||||
let withdrawal = UpdateMessage {
|
||||
withdrawn_routes: vec![IpPrefix::try_from("192.0.2.0/24").unwrap().into()],
|
||||
..empty_update()
|
||||
};
|
||||
let actions = fsm.handle(received(Message::Update(withdrawal)));
|
||||
assert!(matches!(
|
||||
&actions[..],
|
||||
[Action::ResetHoldTimer(d), Action::DeliverUpdate { .. }] if *d == hold
|
||||
));
|
||||
|
||||
// An UPDATE with nothing in it is the End-of-RIB marker for IPv4
|
||||
// (RFC 4724 §2), and has no routes to deliver.
|
||||
let actions = fsm.handle(received(Message::Update(empty_update())));
|
||||
assert!(matches!(
|
||||
&actions[..],
|
||||
[Action::ResetHoldTimer(d), Action::EndOfRib(AfiSafi::IPV4_UNICAST)] if *d == hold
|
||||
));
|
||||
// Unless the decoder emptied it for errors in it (RFC 7606).
|
||||
let actions = fsm.handle(Event::Message(Received {
|
||||
message: Message::Update(empty_update()),
|
||||
attributes_wire: None,
|
||||
repaired: true,
|
||||
}));
|
||||
assert!(matches!(
|
||||
&actions[..],
|
||||
[Action::ResetHoldTimer(d), Action::DeliverUpdate { .. }] if *d == hold
|
||||
@@ -1438,188 +1031,4 @@ mod tests {
|
||||
assert!(fsm.handle(event).is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
// ---- graceful restart (RFC 4724, RFC 8538) ----------------------------
|
||||
|
||||
const OFFER: Offer = Offer {
|
||||
restart_time: 120,
|
||||
restarted: true,
|
||||
forwarding_preserved: false,
|
||||
};
|
||||
|
||||
/// A peer's capability: its restart time, N bit and the families with
|
||||
/// their F bits.
|
||||
fn peer_restart(notification: bool, entries: &[(AfiSafi, bool)]) -> Capability {
|
||||
Capability::GracefulRestart {
|
||||
restart_state: false,
|
||||
restart_time: 90,
|
||||
notification,
|
||||
entries: entries
|
||||
.iter()
|
||||
.map(
|
||||
|(family, forwarding)| bgp_packet::open::GracefulRestartEntry {
|
||||
afi: family.afi,
|
||||
safi: family.safi,
|
||||
preserve_forwarding: *forwarding,
|
||||
},
|
||||
)
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
/// A machine offering graceful restart, Established with a peer that
|
||||
/// speaks both families and offers it as `restart` says, if at all.
|
||||
fn restarting_fsm(restart: Option<Capability>) -> Fsm {
|
||||
let (mut fsm, _) = Fsm::new(FsmConfig {
|
||||
graceful_restart: Some(OFFER),
|
||||
..config()
|
||||
});
|
||||
let mut capabilities: Vec<Capability> = SUPPORTED
|
||||
.iter()
|
||||
.map(|family| Capability::multiprotocol(*family))
|
||||
.collect();
|
||||
capabilities.extend(restart);
|
||||
fsm.handle(received(Message::Open(OpenMessage {
|
||||
options: vec![OpenOption::Capabilities(capabilities)],
|
||||
..peer_open(90)
|
||||
})));
|
||||
fsm.handle(received(Message::KeepAlive));
|
||||
assert_eq!(fsm.state(), State::Established);
|
||||
fsm
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn graceful_restart_is_offered_with_notifications_for_every_family() {
|
||||
let (_, actions) = Fsm::new(FsmConfig {
|
||||
graceful_restart: Some(OFFER),
|
||||
..config()
|
||||
});
|
||||
let Action::Send(open) = &actions[0] else {
|
||||
panic!("the OPEN comes first: {actions:?}");
|
||||
};
|
||||
let offered = capabilities(open)
|
||||
.into_iter()
|
||||
.find(|c| matches!(c, Capability::GracefulRestart { .. }))
|
||||
.cloned();
|
||||
assert_eq!(
|
||||
offered,
|
||||
Some(Capability::GracefulRestart {
|
||||
restart_state: true,
|
||||
restart_time: 120,
|
||||
notification: true,
|
||||
entries: SUPPORTED
|
||||
.iter()
|
||||
.map(|family| bgp_packet::open::GracefulRestartEntry {
|
||||
afi: family.afi,
|
||||
safi: family.safi,
|
||||
preserve_forwarding: false,
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
);
|
||||
// Not offered where it is not configured.
|
||||
let (_, actions) = Fsm::new(config());
|
||||
let Action::Send(open) = &actions[0] else {
|
||||
panic!("the OPEN comes first: {actions:?}");
|
||||
};
|
||||
assert!(
|
||||
!capabilities(open)
|
||||
.iter()
|
||||
.any(|c| matches!(c, Capability::GracefulRestart { .. }))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn graceful_restart_is_agreed_for_the_families_the_peer_lists() {
|
||||
let fsm = restarting_fsm(Some(peer_restart(
|
||||
true,
|
||||
&[
|
||||
(AfiSafi::IPV6_UNICAST, true),
|
||||
(AfiSafi::new(Afi::L2VPN, Safi::EVPN), true),
|
||||
],
|
||||
)));
|
||||
let agreed = fsm.negotiated().unwrap().graceful_restart.clone().unwrap();
|
||||
assert_eq!(agreed.restart_time, 90);
|
||||
assert!(agreed.notification);
|
||||
// A family the session does not carry is left out.
|
||||
assert_eq!(agreed.families, only(AfiSafi::IPV6_UNICAST));
|
||||
assert_eq!(agreed.forwarding, only(AfiSafi::IPV6_UNICAST));
|
||||
|
||||
// Not agreed where only one side offers it.
|
||||
let fsm = restarting_fsm(None);
|
||||
assert_eq!(fsm.negotiated().unwrap().graceful_restart, None);
|
||||
let mut fsm = fsm_in(State::OpenSent, 90);
|
||||
fsm.handle(received(Message::Open(OpenMessage {
|
||||
options: vec![OpenOption::Capabilities(vec![peer_restart(true, &[])])],
|
||||
..peer_open(90)
|
||||
})));
|
||||
assert_eq!(fsm.negotiated().unwrap().graceful_restart, None);
|
||||
}
|
||||
|
||||
/// Leaving to restart is closing the connection without a word, which
|
||||
/// a peer that agreed takes as a restart; without the agreement, and
|
||||
/// when leaving for good, the peer is told, with a Hard Reset where it
|
||||
/// would otherwise keep our routes.
|
||||
#[test]
|
||||
fn leaving_says_goodbye_as_graceful_restart_has_it() {
|
||||
let shutdown = BgpError::Cease(CeaseSubcode::AdminShutdown);
|
||||
let hard_reset = BgpError::Cease(CeaseSubcode::HardReset);
|
||||
let both = &[(AfiSafi::IPV4_UNICAST, true), (AfiSafi::IPV6_UNICAST, true)];
|
||||
for (restart, leave, expected) in [
|
||||
(Some(peer_restart(true, both)), Leave::Restart, None),
|
||||
(Some(peer_restart(false, both)), Leave::Restart, None),
|
||||
(None, Leave::Restart, Some(shutdown.clone())),
|
||||
(
|
||||
Some(peer_restart(true, both)),
|
||||
Leave::Stop,
|
||||
Some(hard_reset.clone()),
|
||||
),
|
||||
(
|
||||
Some(peer_restart(false, both)),
|
||||
Leave::Stop,
|
||||
Some(shutdown.clone()),
|
||||
),
|
||||
(None, Leave::Stop, Some(shutdown.clone())),
|
||||
] {
|
||||
let mut fsm = restarting_fsm(restart.clone());
|
||||
let actions = fsm.handle(Event::Leave(leave));
|
||||
let (sent, result) = ending(&fsm, actions);
|
||||
assert_eq!(sent, expected, "{leave:?} with {restart:?}");
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
// Before Established there is nothing to keep.
|
||||
let mut fsm = fsm_in(State::OpenConfirm, 90);
|
||||
let actions = fsm.handle(Event::Leave(Leave::Restart));
|
||||
assert_eq!(ending(&fsm, actions).0, Some(shutdown));
|
||||
}
|
||||
|
||||
/// With NOTIFICATIONs covered (RFC 8538), one for an error of ours is a
|
||||
/// Hard Reset, with the error in its data; an expired hold timer is
|
||||
/// not.
|
||||
#[test]
|
||||
fn errors_end_the_session_with_a_hard_reset_where_notifications_restart() {
|
||||
let both = &[(AfiSafi::IPV4_UNICAST, true), (AfiSafi::IPV6_UNICAST, true)];
|
||||
let mut fsm = restarting_fsm(Some(peer_restart(true, both)));
|
||||
let actions = fsm.handle(Event::LocalFailure(SessionError::PrefixLimitExceeded {
|
||||
limit: 10,
|
||||
}));
|
||||
let Action::Send(Message::Notification(sent)) = &actions[0] else {
|
||||
panic!("a NOTIFICATION first: {actions:?}");
|
||||
};
|
||||
assert_eq!(sent.error, BgpError::Cease(CeaseSubcode::HardReset));
|
||||
assert_eq!(sent.data, [6, 1]);
|
||||
|
||||
let mut fsm = restarting_fsm(Some(peer_restart(true, both)));
|
||||
let actions = fsm.handle(Event::HoldTimerExpired);
|
||||
assert_eq!(ending(&fsm, actions).0, Some(BgpError::HoldTimer));
|
||||
|
||||
let mut fsm = restarting_fsm(Some(peer_restart(false, both)));
|
||||
let actions = fsm.handle(Event::LocalFailure(SessionError::PrefixLimitExceeded {
|
||||
limit: 10,
|
||||
}));
|
||||
assert_eq!(
|
||||
ending(&fsm, actions).0,
|
||||
Some(BgpError::Cease(CeaseSubcode::MaxPrefixes))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,174 +0,0 @@
|
||||
//! Peers that are being kept down for a while: those that went over a
|
||||
//! limit, ours or theirs, and those whose sessions keep failing.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Mutex;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use crate::address::PeerAddress;
|
||||
use crate::sync::IgnorePoison;
|
||||
|
||||
/// Why a peer is being kept down.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum HeldDown {
|
||||
/// It went over its limit of routes.
|
||||
OverLimit,
|
||||
/// We were to announce it more routes than its export limit allows.
|
||||
OverExportLimit,
|
||||
/// Its sessions keep coming up and failing.
|
||||
Flapping,
|
||||
}
|
||||
|
||||
impl HeldDown {
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::OverLimit => "held down after going over its limit of routes",
|
||||
Self::OverExportLimit => {
|
||||
"held down after more routes were to be advertised to it than its export limit allows"
|
||||
}
|
||||
Self::Flapping => "held down because its sessions keep failing",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A session that fails sooner than this after coming up counts as a
|
||||
/// flap.
|
||||
const FLAP_WITHIN: Duration = Duration::from_secs(60);
|
||||
/// This many flaps in a row are let pass: a router being restarted, a
|
||||
/// cable put back.
|
||||
const FLAPS_ALLOWED: u32 = 2;
|
||||
/// How long a peer is kept down after the first flap too many; twice as
|
||||
/// long after each one more, up to `FLAP_HOLD_MAX`.
|
||||
const FLAP_HOLD: Duration = Duration::from_secs(1);
|
||||
const FLAP_HOLD_MAX: Duration = Duration::from_secs(300);
|
||||
|
||||
/// Peers that are being kept down, and until when: those that went over
|
||||
/// their limit, and those whose sessions keep failing. Shared by the
|
||||
/// sessions, which put peers here, and by the dialer and the listeners,
|
||||
/// which leave them alone meanwhile.
|
||||
///
|
||||
/// Every session that comes up and goes costs a look at every route in
|
||||
/// the table, and tells every other peer of the changes. A peer that
|
||||
/// does so over and over, by fault or by design, is slowed down.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct HoldDowns {
|
||||
held: Mutex<HashMap<PeerAddress, (Instant, HeldDown)>>,
|
||||
/// How many times in a row each peer's session has failed soon after
|
||||
/// coming up.
|
||||
flaps: Mutex<HashMap<PeerAddress, u32>>,
|
||||
}
|
||||
|
||||
impl HoldDowns {
|
||||
/// Keeps the peer at `address` down for `duration` from now.
|
||||
pub fn hold(&self, address: PeerAddress, duration: Duration, why: HeldDown) {
|
||||
let until = Instant::now() + duration;
|
||||
let mut held = self.held.locked();
|
||||
// Not for less long than it already is.
|
||||
if held
|
||||
.get(&address)
|
||||
.is_none_or(|(already, _)| *already < until)
|
||||
{
|
||||
held.insert(address, (until, why));
|
||||
}
|
||||
}
|
||||
|
||||
/// How much longer the peer at `address` is kept down, if it is, and
|
||||
/// why.
|
||||
pub fn remaining(&self, address: PeerAddress) -> Option<(Duration, HeldDown)> {
|
||||
let mut held = self.held.locked();
|
||||
let (until, why) = *held.get(&address)?;
|
||||
let remaining = until.saturating_duration_since(Instant::now());
|
||||
if remaining.is_zero() {
|
||||
held.remove(&address);
|
||||
return None;
|
||||
}
|
||||
Some((remaining, why))
|
||||
}
|
||||
|
||||
/// A session with the peer at `address` that had come up has ended,
|
||||
/// `lasted` after it did; `failed` if not at our own wish. Returns for
|
||||
/// how long the peer is kept down for it, if it is.
|
||||
pub fn session_ended(
|
||||
&self,
|
||||
address: PeerAddress,
|
||||
lasted: Duration,
|
||||
failed: bool,
|
||||
) -> Option<Duration> {
|
||||
let mut flaps = self.flaps.locked();
|
||||
if !failed || lasted >= FLAP_WITHIN {
|
||||
flaps.remove(&address);
|
||||
return None;
|
||||
}
|
||||
let count = flaps.entry(address).or_insert(0);
|
||||
*count += 1;
|
||||
let over = count.checked_sub(FLAPS_ALLOWED + 1)?;
|
||||
let hold = FLAP_HOLD
|
||||
.checked_mul(1u32.checked_shl(over).unwrap_or(u32::MAX))
|
||||
.map_or(FLAP_HOLD_MAX, |hold| hold.min(FLAP_HOLD_MAX));
|
||||
drop(flaps);
|
||||
self.hold(address, hold, HeldDown::Flapping);
|
||||
Some(hold)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn a_peer_is_held_down_for_as_long_as_was_said() {
|
||||
let held = HoldDowns::default();
|
||||
let address = PeerAddress::from("192.0.2.1".parse::<std::net::IpAddr>().unwrap());
|
||||
let other = PeerAddress::from("192.0.2.2".parse::<std::net::IpAddr>().unwrap());
|
||||
|
||||
// Two failures soon after coming up are let pass; from the third
|
||||
// the peer is kept down, twice as long each time, up to a limit.
|
||||
let flaps = HoldDowns::default();
|
||||
let soon = Duration::from_secs(5);
|
||||
assert_eq!(flaps.session_ended(address, soon, true), None);
|
||||
assert_eq!(flaps.session_ended(address, soon, true), None);
|
||||
assert_eq!(flaps.session_ended(address, soon, true), Some(FLAP_HOLD));
|
||||
assert_eq!(
|
||||
flaps.session_ended(address, soon, true),
|
||||
Some(2 * FLAP_HOLD)
|
||||
);
|
||||
assert_eq!(
|
||||
flaps.remaining(address).map(|(_, why)| why),
|
||||
Some(HeldDown::Flapping)
|
||||
);
|
||||
assert_eq!(flaps.remaining(other), None, "each peer for itself");
|
||||
for _ in 0..40 {
|
||||
flaps.session_ended(address, soon, true);
|
||||
}
|
||||
assert_eq!(
|
||||
flaps.session_ended(address, soon, true),
|
||||
Some(FLAP_HOLD_MAX)
|
||||
);
|
||||
// A session that lasted, or that we ended ourselves, starts the
|
||||
// count over.
|
||||
assert_eq!(flaps.session_ended(address, FLAP_WITHIN, true), None);
|
||||
assert_eq!(flaps.session_ended(address, soon, true), None);
|
||||
assert_eq!(flaps.session_ended(address, soon, false), None);
|
||||
assert_eq!(flaps.session_ended(address, soon, true), None);
|
||||
assert_eq!(held.remaining(address), None);
|
||||
|
||||
held.hold(address, Duration::from_secs(600), HeldDown::OverLimit);
|
||||
let (remaining, why) = held.remaining(address).expect("held");
|
||||
assert!(remaining > Duration::from_secs(590), "{remaining:?}");
|
||||
assert_eq!(why, HeldDown::OverLimit);
|
||||
assert_eq!(held.remaining(other), None);
|
||||
// Not made shorter by a lesser reason.
|
||||
held.hold(address, Duration::from_secs(1), HeldDown::Flapping);
|
||||
let (remaining, why) = held.remaining(address).expect("held");
|
||||
assert!(remaining > Duration::from_secs(590), "{remaining:?}");
|
||||
assert_eq!(why, HeldDown::OverLimit);
|
||||
|
||||
held.hold(other, Duration::ZERO, HeldDown::OverLimit);
|
||||
assert_eq!(held.remaining(other), None);
|
||||
assert_eq!(
|
||||
held.held.locked().len(),
|
||||
1,
|
||||
"a hold that is over is forgotten"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,18 @@
|
||||
pub mod address;
|
||||
pub mod advertise;
|
||||
pub mod collision;
|
||||
pub mod config;
|
||||
mod connection;
|
||||
mod dialer;
|
||||
pub mod connection;
|
||||
pub mod dialer;
|
||||
pub mod error;
|
||||
pub mod export;
|
||||
pub mod family;
|
||||
mod fsm;
|
||||
pub(crate) mod holddown;
|
||||
pub mod marks;
|
||||
pub mod fsm;
|
||||
pub mod metrics;
|
||||
mod panic;
|
||||
pub mod policy;
|
||||
mod reach;
|
||||
pub mod rejections;
|
||||
pub mod restart;
|
||||
mod routes;
|
||||
pub mod rpki;
|
||||
pub mod server;
|
||||
pub mod session;
|
||||
mod sync;
|
||||
mod throttle;
|
||||
pub mod tracing;
|
||||
|
||||
pub mod proto {
|
||||
include!(concat!(env!("OUT_DIR"), "/protos/mod.rs"));
|
||||
|
||||
Loaded 100 of 202 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user