Compare commits
38
Commits
main
..
netsec_2026
No files matched your search
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"version": "0.0.1",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "slides",
|
||||
"runtimeExecutable": "python3",
|
||||
"runtimeArgs": ["slides/serve.py", "8765"],
|
||||
"port": 8765
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,7 +1,2 @@
|
||||
target
|
||||
.git
|
||||
.claude
|
||||
fuzz/target
|
||||
fuzz/corpus
|
||||
fuzz/artifacts
|
||||
crates/bench/data
|
||||
@@ -1,99 +0,0 @@
|
||||
# Runs on every push and pull request. Gitea Actions reads this file too
|
||||
# (it falls back to .github/workflows), so one definition serves both.
|
||||
name: ci
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
# Warnings fail the build here, and only here: locally they stay warnings
|
||||
# so that work in progress compiles.
|
||||
RUSTFLAGS: -D warnings
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: rustfmt, clippy
|
||||
- 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
|
||||
- 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
|
||||
|
||||
# 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
|
||||
# fuzz/README.md.
|
||||
fuzz:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@nightly
|
||||
- name: Install cargo-fuzz
|
||||
run: cargo install cargo-fuzz --locked
|
||||
- name: Fuzz
|
||||
working-directory: fuzz
|
||||
# cargo-fuzz sets its own flags; the workspace's -D warnings would
|
||||
# replace them.
|
||||
env:
|
||||
RUSTFLAGS: ""
|
||||
run: |
|
||||
for target in message message_body attrs prefix_text config rtr netlink; do
|
||||
mkdir -p "corpus/$target"
|
||||
cargo fuzz run "$target" "corpus/$target" "seeds/$target" -- -max_total_time=60
|
||||
done
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
/target
|
||||
__pycache__/
|
||||
slides/reveal.js/
|
||||
slides/dist/
|
||||
.DS_Store
|
||||
.claude/
|
||||
/docs/public
|
||||
/docs/static/giallo.css
|
||||
Generated
+21
-397
@@ -2,12 +2,6 @@
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "adler2"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.4"
|
||||
@@ -17,12 +11,6 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "allocator-api2"
|
||||
version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.21"
|
||||
@@ -157,63 +145,24 @@ 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"
|
||||
dependencies = [
|
||||
"bgp-integration-tests",
|
||||
"bgp-packet",
|
||||
"bgp-primitives",
|
||||
"bgp-rib",
|
||||
"bgp-server",
|
||||
"bytes",
|
||||
"flate2",
|
||||
"stats_alloc",
|
||||
"tikv-jemallocator",
|
||||
"tokio",
|
||||
"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.0"
|
||||
dependencies = [
|
||||
"bgp-fib",
|
||||
"bgp-packet",
|
||||
"bgp-primitives",
|
||||
"bgp-rib",
|
||||
"bgp-rpki",
|
||||
"bgp-server",
|
||||
"bgp-web",
|
||||
"bytes",
|
||||
@@ -226,14 +175,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "bgp-packet"
|
||||
version = "0.2.6"
|
||||
version = "0.0.2"
|
||||
dependencies = [
|
||||
"bgp-primitives",
|
||||
"bitfield",
|
||||
"bytes",
|
||||
"eyre",
|
||||
"nom",
|
||||
"proptest",
|
||||
"serde",
|
||||
"serde_repr",
|
||||
"strum",
|
||||
@@ -242,103 +190,72 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "bgp-primitives"
|
||||
version = "0.2.6"
|
||||
version = "0.0.2"
|
||||
dependencies = [
|
||||
"bitfield",
|
||||
"bytes",
|
||||
"eyre",
|
||||
"nom",
|
||||
"proptest",
|
||||
"serde",
|
||||
"serde_repr",
|
||||
"strum",
|
||||
"thiserror 2.0.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bgp-rib"
|
||||
version = "0.2.6"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"bgp-packet",
|
||||
"bgp-primitives",
|
||||
"bytes",
|
||||
"foldhash",
|
||||
"hashbrown 0.17.1",
|
||||
"proptest",
|
||||
"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.0"
|
||||
dependencies = [
|
||||
"bgp-fib",
|
||||
"bgp-packet",
|
||||
"bgp-primitives",
|
||||
"bgp-rib",
|
||||
"bgp-rpki",
|
||||
"bytes",
|
||||
"foldhash",
|
||||
"clap",
|
||||
"eyre",
|
||||
"futures",
|
||||
"nix",
|
||||
"libc",
|
||||
"prometheus",
|
||||
"proptest",
|
||||
"protobuf",
|
||||
"protobuf-codegen",
|
||||
"socket2",
|
||||
"thiserror 2.0.12",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bgp-web"
|
||||
version = "0.2.6"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"bgp-fib",
|
||||
"bgp-packet",
|
||||
"bgp-primitives",
|
||||
"bgp-rib",
|
||||
"bgp-rpki",
|
||||
"bgp-server",
|
||||
"bytes",
|
||||
"eyre",
|
||||
"futures",
|
||||
"nix",
|
||||
"prometheus",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"tikv-jemalloc-ctl",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bit-set"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
|
||||
dependencies = [
|
||||
"bit-vec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bit-vec"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
|
||||
|
||||
[[package]]
|
||||
name = "bitfield"
|
||||
version = "0.19.0"
|
||||
@@ -361,9 +278,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.13.2"
|
||||
version = "2.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
|
||||
checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd"
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
@@ -377,28 +294,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"
|
||||
@@ -445,15 +346,6 @@ version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "displaydoc"
|
||||
version = "0.2.7"
|
||||
@@ -503,35 +395,12 @@ 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"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"miniz_oxide",
|
||||
"zlib-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
|
||||
|
||||
[[package]]
|
||||
name = "form_urlencoded"
|
||||
version = "1.2.2"
|
||||
@@ -647,17 +516,6 @@ version = "0.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||
dependencies = [
|
||||
"allocator-api2",
|
||||
"equivalent",
|
||||
"foldhash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
@@ -879,7 +737,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.16.1",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -978,31 +836,12 @@ 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"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c"
|
||||
dependencies = [
|
||||
"adler2",
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.2.3"
|
||||
@@ -1014,32 +853,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"
|
||||
@@ -1058,15 +871,6 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.1"
|
||||
@@ -1102,12 +906,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"
|
||||
@@ -1129,15 +927,6 @@ dependencies = [
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
|
||||
dependencies = [
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.94"
|
||||
@@ -1162,25 +951,6 @@ dependencies = [
|
||||
"thiserror 2.0.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proptest"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744"
|
||||
dependencies = [
|
||||
"bit-set",
|
||||
"bit-vec",
|
||||
"bitflags",
|
||||
"num-traits",
|
||||
"rand",
|
||||
"rand_chacha",
|
||||
"rand_xorshift",
|
||||
"regex-syntax",
|
||||
"rusty-fork",
|
||||
"tempfile",
|
||||
"unarray",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "protobuf"
|
||||
version = "3.7.2"
|
||||
@@ -1232,12 +1002,6 @@ dependencies = [
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-error"
|
||||
version = "1.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.47"
|
||||
@@ -1253,44 +1017,6 @@ version = "5.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41"
|
||||
dependencies = [
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_xorshift"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a"
|
||||
dependencies = [
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.10"
|
||||
@@ -1392,18 +1118,6 @@ version = "1.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
|
||||
|
||||
[[package]]
|
||||
name = "rusty-fork"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2"
|
||||
dependencies = [
|
||||
"fnv",
|
||||
"quick-error",
|
||||
"tempfile",
|
||||
"wait-timeout",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.23"
|
||||
@@ -1502,12 +1216,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"
|
||||
@@ -1517,12 +1225,6 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simd-adler32"
|
||||
version = "0.3.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea"
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.12"
|
||||
@@ -1551,12 +1253,6 @@ version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
|
||||
|
||||
[[package]]
|
||||
name = "stats_alloc"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c0e04424e733e69714ca1bbb9204c1a57f09f5493439520f9f68c132ad25eec"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
@@ -1689,37 +1385,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"
|
||||
@@ -1897,12 +1562,6 @@ version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||
|
||||
[[package]]
|
||||
name = "unarray"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.18"
|
||||
@@ -1939,15 +1598,6 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
|
||||
|
||||
[[package]]
|
||||
name = "wait-timeout"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "want"
|
||||
version = "0.3.1"
|
||||
@@ -2172,26 +1822,6 @@ dependencies = [
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.59"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6df92bf3d9227be3d53173901ddbffac2babc27ae50f397776ffd6dc33f800cb"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.8.59"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac4f328cf2f05d084e496c3e9c3f33ed0a183656a16e1fcec4d464d8373aec82"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerofrom"
|
||||
version = "0.1.8"
|
||||
@@ -2246,12 +1876,6 @@ dependencies = [
|
||||
"syn 3.0.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zlib-rs"
|
||||
version = "0.6.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112"
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.23"
|
||||
|
||||
+4
-34
@@ -1,8 +1,6 @@
|
||||
[workspace]
|
||||
default-members = ["bin/bgp"]
|
||||
members = ["bin/*", "crates/*"]
|
||||
# Fuzz targets need a nightly toolchain; see fuzz/README.md.
|
||||
exclude = ["fuzz"]
|
||||
resolver = "3"
|
||||
|
||||
[workspace.package]
|
||||
@@ -11,31 +9,18 @@ description = "A Border Gateway Protocol implementation"
|
||||
edition = "2024"
|
||||
homepage = "https://rayhaan.ch"
|
||||
license = "Apache-2.0"
|
||||
repository = "https://git.rayhaan.net/rayhaan/ferrix"
|
||||
version = "0.2.6"
|
||||
|
||||
# Every crate opts in with `[lints] workspace = true`. CI turns warnings into
|
||||
# errors, so a lint added here is enforced.
|
||||
[workspace.lints.rust]
|
||||
unsafe_code = "forbid"
|
||||
|
||||
[workspace.lints.clippy]
|
||||
all = { level = "warn", priority = -1 }
|
||||
dbg_macro = "warn"
|
||||
todo = "warn"
|
||||
unimplemented = "warn"
|
||||
name = "bgp"
|
||||
repository = "https://git.rayhaan.net/rayhaan/bgp"
|
||||
version = "0.0.2"
|
||||
|
||||
[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" }
|
||||
bgp-bench = { path = "crates/bench" }
|
||||
|
||||
# --- General ---
|
||||
bitfield = "0.19.0"
|
||||
@@ -43,8 +28,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"
|
||||
thiserror = "2.0.12"
|
||||
@@ -53,24 +36,11 @@ strum = { version = "0.27.1", features = ["derive"] }
|
||||
tracing = "0.1.44"
|
||||
tracing-subscriber = { version = "0.3.22", features = ["env-filter"] }
|
||||
prometheus = "0.14.0"
|
||||
proptest = "1.7"
|
||||
protobuf = { package = "protobuf", version = "3.7.2" }
|
||||
protobuf-codegen = { package = "protobuf-codegen", version = "3.7.2" }
|
||||
clap = { version = "4.5.53", features = ["derive"] }
|
||||
foldhash = "0.2"
|
||||
futures = "0.3"
|
||||
hashbrown = "0.17"
|
||||
libc = "0.2"
|
||||
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"
|
||||
+2
-3
@@ -1,9 +1,8 @@
|
||||
# Builds the daemon into a small image. Mount a configuration file at
|
||||
# /etc/ferrix/server.textpb, or pass another path as the first argument.
|
||||
# Builds the ferrix_bgp daemon for the lab. See docs/lab/README.md.
|
||||
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,176 +0,0 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
@@ -1,193 +1,29 @@
|
||||
# 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 Systms.
|
||||
|
||||
## Status
|
||||
The aim of this project is to provide a modern, high performance, secure and programatic 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 |
|
||||
|
||||
## Development
|
||||
|
||||
CI runs these 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
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
`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.
|
||||
|
||||
## License
|
||||
|
||||
Apache License 2.0; see [`LICENSE`](LICENSE).
|
||||
- ☐ 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)
|
||||
- ✅ Locally originated routes with communities, announced to all peers
|
||||
- ✅ HTTP API, live status page and Prometheus metrics
|
||||
- ☐ Implement an efficient data structure for storing prefixes (e.g. Poptrie)
|
||||
- ☐ Filtering for peer RIB in / out
|
||||
- ☐ Main / auxillary RIBs for exchanging routes between peers
|
||||
+5
-27
@@ -1,12 +1,10 @@
|
||||
[package]
|
||||
name = "bgp"
|
||||
description = "A Border Gateway Protocol implementation"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
description = "A Border Gateway Protocol implementation"
|
||||
edition.workspace = true
|
||||
homepage.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
version.workspace = true
|
||||
license.workspace = true
|
||||
name = "bgp"
|
||||
version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
bgp-server.workspace = true
|
||||
@@ -16,23 +14,3 @@ 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
|
||||
+57
-11
@@ -1,17 +1,63 @@
|
||||
# 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 HTTP API.
|
||||
|
||||
```
|
||||
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. When `http_endpoint` is set the
|
||||
binary also serves:
|
||||
|
||||
- [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)
|
||||
| Path | Purpose |
|
||||
|---|---|
|
||||
| `GET /statusz` | Status page: peers, announce form, local and received routes, live event log |
|
||||
| `GET /api/status` | Router ID, ASN, uptime and table sizes |
|
||||
| `GET /api/peers` | Established sessions |
|
||||
| `GET /api/routes/local` | Locally originated routes |
|
||||
| `POST /api/routes/local` | Announce a route (JSON body, see below) |
|
||||
| `DELETE /api/routes/local` | Withdraw a route (JSON body: `prefix`, optional `owner`) |
|
||||
| `GET /api/routes/local/wire?prefix=…` | The exact UPDATE bytes sent to each peer |
|
||||
| `GET /api/routes/received` | Adj-RIB-In contents |
|
||||
| `GET /api/communities/well-known` | Names accepted in place of numeric communities |
|
||||
| `GET /api/events` | Server-sent events for session and route changes |
|
||||
| `GET /metricz` | Prometheus metrics |
|
||||
|
||||
Announce body:
|
||||
|
||||
```json
|
||||
{
|
||||
"prefix": "10.100.1.0/24",
|
||||
"owner": "alice",
|
||||
"origin": "IGP",
|
||||
"prepend": 2,
|
||||
"med": 100,
|
||||
"communities": ["65000:100", "NO_EXPORT"],
|
||||
"large_communities": ["65000:1:2"]
|
||||
}
|
||||
```
|
||||
|
||||
Only `prefix` and `owner` are required. Routes are subject to the
|
||||
`local_route_policy` in the configuration.
|
||||
|
||||
## 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 locally announced 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(())
|
||||
}
|
||||
}
|
||||
+35
-129
@@ -1,77 +1,44 @@
|
||||
use std::path::PathBuf;
|
||||
use std::process::ExitCode;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use bgp_server::config::Config;
|
||||
use bgp_server::ping::PingMonitor;
|
||||
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::BgpServerBuilder;
|
||||
use bgp_server::tracing::TracingArgs;
|
||||
use bgp_web::{AppState, RouterInfo};
|
||||
use clap::Parser;
|
||||
use eyre::{Context, Result};
|
||||
use tracing::{error, info};
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
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()?;
|
||||
log_panics();
|
||||
args.tracing.install()?;
|
||||
|
||||
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?;
|
||||
let server = BgpServerBuilder::new(config).bind().await?;
|
||||
|
||||
let pings = match &server.config().ping_monitor {
|
||||
Some(prefixes) => PingMonitor::start(prefixes.clone(), server.local_rib.clone()),
|
||||
None => PingMonitor::disabled(),
|
||||
};
|
||||
|
||||
if let Some(http_endpoint) = server.config().http_endpoint {
|
||||
// Bound here, so that an address that cannot be bound stops the
|
||||
// daemon from starting, as it does for the BGP listeners.
|
||||
let listener = tokio::net::TcpListener::bind(http_endpoint)
|
||||
.await
|
||||
.wrap_err_with(|| format!("Failed to bind HTTP listener on {http_endpoint}"))?;
|
||||
let state = AppState {
|
||||
info: Arc::new(
|
||||
RouterInfo::new(
|
||||
@@ -79,30 +46,15 @@ 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),
|
||||
adj_rib_in: server.adj_rib_in.clone(),
|
||||
pings,
|
||||
};
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = bgp_web::serve_on(state, listener).await {
|
||||
if let Err(e) = bgp_web::serve(state, http_endpoint).await {
|
||||
error!(err = %e, "HTTP server exited");
|
||||
}
|
||||
});
|
||||
@@ -111,80 +63,34 @@ 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() => result,
|
||||
signal = shutdown_signal() => {
|
||||
info!(signal, "Shutting down, closing BGP sessions");
|
||||
let left = registry.shutdown(Duration::from_secs(2)).await;
|
||||
if left > 0 {
|
||||
warn!(sessions = left, "Some sessions did not close in time");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends panics to the log, with where they happened. A panic is a bug, and
|
||||
/// the default report goes to stderr in a format of its own, apart from
|
||||
/// everything else the daemon says. The task that panicked still unwinds;
|
||||
/// the server deals with what it leaves behind.
|
||||
fn log_panics() {
|
||||
std::panic::set_hook(Box::new(|info| {
|
||||
let location = info
|
||||
.location()
|
||||
.map(|l| format!("{}:{}", l.file(), l.line()))
|
||||
.unwrap_or_else(|| "an unknown location".into());
|
||||
let message = info
|
||||
.payload()
|
||||
.downcast_ref::<&str>()
|
||||
.map(|s| s.to_string())
|
||||
.or_else(|| info.payload().downcast_ref::<String>().cloned())
|
||||
.unwrap_or_else(|| "no message".into());
|
||||
// Captured only if RUST_BACKTRACE is set.
|
||||
let backtrace = std::backtrace::Backtrace::capture();
|
||||
error!(%location, %message, %backtrace, "Panic: this is a bug");
|
||||
}));
|
||||
}
|
||||
|
||||
/// 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,52 +0,0 @@
|
||||
[package]
|
||||
name = "bgp-bench"
|
||||
description = "Benchmarks at the scale of the Internet routing table"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
homepage.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
version.workspace = true
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
bgp-packet.workspace = true
|
||||
bgp-primitives.workspace = true
|
||||
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-subscriber.workspace = true
|
||||
|
||||
[features]
|
||||
# Counts every allocation, to report how much memory a table holds. The
|
||||
# counters are shared by all threads and slow them down, so timings are
|
||||
# taken without this and memory is measured in a run of its own.
|
||||
count-memory = ["dep:stats_alloc"]
|
||||
|
||||
[[bench]]
|
||||
name = "full_table"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "real_table"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "transit"
|
||||
harness = false
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[[bench]]
|
||||
name = "mesh"
|
||||
harness = false
|
||||
@@ -1,202 +0,0 @@
|
||||
//! What it costs to take in the Internet routing table, from the bytes on
|
||||
//! the wire to a chosen best route.
|
||||
//!
|
||||
//! 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
|
||||
//! the crate documentation); `real_table` runs on a real one.
|
||||
//!
|
||||
//! This is a stopwatch around whole operations at full size, not a
|
||||
//! statistical micro-benchmark: the questions are "how long until a full
|
||||
//! table is in" and "how much memory does it take", and their answers are
|
||||
//! seconds and gigabytes.
|
||||
|
||||
use std::hint::black_box;
|
||||
use std::net::Ipv4Addr;
|
||||
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_integration_tests::test_peer::TestPeer;
|
||||
use bgp_integration_tests::test_server::TestServer;
|
||||
use bgp_packet::message::{Message, PathAttribute};
|
||||
use bgp_primitives::ip_prefix::IpPrefix;
|
||||
use bgp_rib::table::{PeerIndex, PeerInfo, RibTable};
|
||||
use bytes::BytesMut;
|
||||
|
||||
fn main() {
|
||||
let peers = env("BENCH_PEERS", 4) as u32;
|
||||
let shape = Shape::INTERNET.scaled_down(env("BENCH_SCALE_DOWN", 1));
|
||||
let threads = std::thread::available_parallelism().map_or(1, |n| n.get());
|
||||
|
||||
println!(
|
||||
"== Table: {} IPv4 + {} IPv6 prefixes, from each of {peers} peers",
|
||||
shape.ipv4_prefixes, shape.ipv6_prefixes
|
||||
);
|
||||
println!(
|
||||
"(IpPrefix is {} bytes, PathAttribute {})",
|
||||
size_of::<IpPrefix>(),
|
||||
size_of::<PathAttribute>()
|
||||
);
|
||||
let prefixes = Prefixes::generate(shape);
|
||||
let wire: Vec<Vec<BytesMut>> = (1..=peers)
|
||||
.map(|peer| encoded(&announcements(&prefixes, shape, peer)))
|
||||
.collect();
|
||||
let updates: usize = wire.iter().map(Vec::len).sum();
|
||||
let routes = shape.prefixes() * peers as usize;
|
||||
let wire_bytes: usize = wire.iter().flatten().map(BytesMut::len).sum();
|
||||
println!(
|
||||
"{updates} UPDATE messages, {:.1} prefixes each, {:.1} MiB on the wire, {routes} routes",
|
||||
routes as f64 / updates as f64,
|
||||
mib(wire_bytes),
|
||||
);
|
||||
|
||||
println!("\n== Reading the messages (one thread)");
|
||||
let ctx = wire_context();
|
||||
let started = Instant::now();
|
||||
for message in wire.iter().flatten() {
|
||||
black_box(Message::decode(&ctx, message).expect("what we wrote is well formed"));
|
||||
}
|
||||
let elapsed = started.elapsed();
|
||||
row(
|
||||
"decode UPDATEs",
|
||||
elapsed,
|
||||
format!(
|
||||
"{} routes, {} messages, {:.0} MiB/s",
|
||||
per_second(routes, elapsed),
|
||||
per_second(updates, elapsed),
|
||||
mib(wire_bytes) / elapsed.as_secs_f64()
|
||||
),
|
||||
);
|
||||
|
||||
println!("\n== From bytes to best routes, without the network");
|
||||
for threads in [1, threads] {
|
||||
into_table(&wire, routes, threads);
|
||||
}
|
||||
|
||||
println!("\n== End to end: {peers} peers send the table over TCP at once");
|
||||
let runtime = tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.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 mut senders = Vec::new();
|
||||
for (index, messages) in wire.iter().enumerate() {
|
||||
let peer = index as u32 + 1;
|
||||
let mut session = TestPeer::connect(server.addr).await.expect("a connection");
|
||||
session
|
||||
.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));
|
||||
}
|
||||
let started = Instant::now();
|
||||
let sending: Vec<_> = senders
|
||||
.into_iter()
|
||||
.map(|(mut session, stream)| {
|
||||
tokio::spawn(async move {
|
||||
session.send_raw(&stream).await.expect("the server reads");
|
||||
// Held open until the table is in.
|
||||
session
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
while server.rib.len() < routes {
|
||||
tokio::time::sleep(Duration::from_millis(2)).await;
|
||||
}
|
||||
let elapsed = started.elapsed();
|
||||
row(
|
||||
"sessions, state machine, table",
|
||||
elapsed,
|
||||
format!(
|
||||
"{} routes; {} prefixes with a best route",
|
||||
per_second(routes, elapsed),
|
||||
server.rib.prefix_count()
|
||||
),
|
||||
);
|
||||
for session in sending {
|
||||
drop(session.await);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// What a session does with each message, without the session: decode it,
|
||||
/// find its attribute set, put its routes in the table, choose the best.
|
||||
/// `threads` threads each take whole peers.
|
||||
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
|
||||
.add_peer(PeerInfo {
|
||||
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")
|
||||
})
|
||||
.collect();
|
||||
let ctx = wire_context();
|
||||
let next = AtomicUsize::new(0);
|
||||
let started = Instant::now();
|
||||
std::thread::scope(|scope| {
|
||||
for _ in 0..threads {
|
||||
scope.spawn(|| {
|
||||
loop {
|
||||
let at = next.fetch_add(1, Ordering::Relaxed);
|
||||
let Some(messages) = wire.get(at) else {
|
||||
break;
|
||||
};
|
||||
for message in messages {
|
||||
let decoded =
|
||||
Message::decode_with_report(&ctx, message).expect("well formed");
|
||||
let Message::Update(update) = decoded.message else {
|
||||
continue;
|
||||
};
|
||||
let wire = decoded.attributes_wire.as_deref().expect("kept");
|
||||
if !update.announced_routes.is_empty() {
|
||||
let attrs = table.attrs().intern_received(wire, None).expect("valid");
|
||||
table.announce(peers[at], &attrs, &update.announced_routes);
|
||||
}
|
||||
for attribute in &update.path_attributes {
|
||||
if let PathAttribute::MpReachNlri(mp) = attribute {
|
||||
let attrs = table
|
||||
.attrs()
|
||||
.intern_received(wire, Some(&mp.next_hop))
|
||||
.expect("valid");
|
||||
table.announce(peers[at], &attrs, &mp.prefixes);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
let elapsed = started.elapsed();
|
||||
assert_eq!(table.len(), routes);
|
||||
row(
|
||||
&format!("decode, intern, insert, choose: {threads} thread(s)"),
|
||||
elapsed,
|
||||
format!(
|
||||
"{} routes; {}",
|
||||
per_second(routes, elapsed),
|
||||
describe(region.held(), routes, "route")
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -1,423 +0,0 @@
|
||||
//! The same questions as `full_table`, asked of a real table dump: every
|
||||
//! route that the peers of a RIPE RIS route collector had announced to it.
|
||||
//!
|
||||
//! Needs a dump in `crates/bench/data/` (see the README there) or at the
|
||||
//! path in `BENCH_MRT`. Without one it says so and does nothing, so that
|
||||
//! `cargo bench` works on a fresh checkout. `BENCH_PEERS` limits the run to
|
||||
//! the peers with the most routes (default: all of them).
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::net::Ipv4Addr;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||
use std::time::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)
|
||||
}
|
||||
|
||||
fn dump_path() -> Option<PathBuf> {
|
||||
if let Ok(path) = std::env::var("BENCH_MRT") {
|
||||
return Some(path.into());
|
||||
}
|
||||
let data = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("data");
|
||||
let mut dumps: Vec<PathBuf> = std::fs::read_dir(data)
|
||||
.ok()?
|
||||
.filter_map(|entry| Some(entry.ok()?.path()))
|
||||
.filter(|path| path.extension().is_some_and(|e| e == "gz"))
|
||||
.collect();
|
||||
dumps.sort();
|
||||
dumps.pop()
|
||||
}
|
||||
|
||||
type Attributes = (Arc<[PathAttribute]>, Option<NlriNextHop>);
|
||||
|
||||
fn main() {
|
||||
if Region::begin().held().is_none() {
|
||||
println!(
|
||||
"Memory is not counted in this run: sizes print as 0. For sizes (and slower\ntimings) add --features count-memory.\n"
|
||||
);
|
||||
}
|
||||
let Some(path) = dump_path() else {
|
||||
println!("No table dump found; see crates/bench/data/README.md. Nothing to do.");
|
||||
return;
|
||||
};
|
||||
println!("== Reading {}", path.display());
|
||||
let started = Instant::now();
|
||||
let dump = Dump::read(&path).expect("a readable dump");
|
||||
println!(
|
||||
"{} peers, {} routes, {} distinct attribute sets, read in {:.0} s",
|
||||
dump.peers.len(),
|
||||
dump.route_count(),
|
||||
dump.attribute_sets.len(),
|
||||
started.elapsed().as_secs_f64()
|
||||
);
|
||||
|
||||
let limit = std::env::var("BENCH_PEERS")
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(dump.peers.len());
|
||||
let peers = dump.largest_peers(limit);
|
||||
let peers: Vec<usize> = peers
|
||||
.into_iter()
|
||||
.filter(|&peer| !dump.routes[peer].is_empty())
|
||||
.collect();
|
||||
describe(&dump, &peers);
|
||||
|
||||
// Parsed once, outside the timings: reading messages is measured by
|
||||
// `full_table`, and here the attributes arrive already parsed.
|
||||
let region = Region::begin();
|
||||
let parsed: Vec<Option<Attributes>> = dump
|
||||
.attribute_sets
|
||||
.iter()
|
||||
.map(|wire| parse_attributes(wire).map(|(a, next_hop)| (Arc::from(a), next_hop)))
|
||||
.collect();
|
||||
let parsed_bytes = retained(®ion);
|
||||
println!(
|
||||
"\nattribute sets, parsed (Arc<[PathAttribute]>) {:.0} MiB = {:.0} bytes a set",
|
||||
mib(parsed_bytes),
|
||||
parsed_bytes as f64 / parsed.len() as f64
|
||||
);
|
||||
let unparsed = parsed.iter().filter(|a| a.is_none()).count();
|
||||
if unparsed > 0 {
|
||||
println!("{unparsed} attribute sets could not be parsed; their routes are left out");
|
||||
}
|
||||
|
||||
let threads = std::thread::available_parallelism().map_or(1, |n| n.get());
|
||||
sharded_table(&dump, &peers, &parsed, 1);
|
||||
sharded_table(&dump, &peers, &parsed, threads);
|
||||
followed_table(&dump, &peers, &parsed, threads);
|
||||
}
|
||||
|
||||
/// What the table looks like, as far as it decides what a RIB costs.
|
||||
fn describe(dump: &Dump, peers: &[usize]) {
|
||||
let routes: usize = peers.iter().map(|&p| dump.routes[p].len()).sum();
|
||||
let full = peers
|
||||
.iter()
|
||||
.filter(|&&p| dump.routes[p].len() > 800_000)
|
||||
.count();
|
||||
let prefixes: HashSet<IpPrefix> = peers
|
||||
.iter()
|
||||
.flat_map(|&p| dump.routes[p].iter().map(|(prefix, _)| *prefix))
|
||||
.collect();
|
||||
let mut sets_used = HashSet::new();
|
||||
let mut sets_per_peer = 0usize;
|
||||
for &peer in peers {
|
||||
let own: HashSet<u32> = dump.routes[peer].iter().map(|(_, set)| *set).collect();
|
||||
sets_per_peer += own.len();
|
||||
sets_used.extend(own);
|
||||
}
|
||||
let attribute_bytes: usize = sets_used
|
||||
.iter()
|
||||
.map(|&set| dump.attribute_sets[set as usize].len())
|
||||
.sum();
|
||||
println!("\n== The table ({} peers used)", peers.len());
|
||||
println!("routes {routes:>12}");
|
||||
println!("peers with more than 800,000 routes {full:>12}");
|
||||
println!(
|
||||
"distinct prefixes {:>12}",
|
||||
prefixes.len()
|
||||
);
|
||||
println!(
|
||||
"routes per prefix {:>12.1}",
|
||||
routes as f64 / prefixes.len() as f64
|
||||
);
|
||||
println!(
|
||||
"distinct attribute sets {:>12}",
|
||||
sets_used.len()
|
||||
);
|
||||
println!(
|
||||
" if each peer's were kept apart {sets_per_peer:>12} ({:.2}x: sets shared between peers)",
|
||||
sets_per_peer as f64 / sets_used.len() as f64
|
||||
);
|
||||
println!(
|
||||
"routes per attribute set {:>12.1}",
|
||||
routes as f64 / sets_used.len() as f64
|
||||
);
|
||||
println!(
|
||||
"attribute set on the wire, mean {:>12.0} bytes",
|
||||
attribute_bytes as f64 / sets_used.len() as f64
|
||||
);
|
||||
}
|
||||
|
||||
/// The sharded table, filled by `threads` threads. Each takes whole peers,
|
||||
/// as sessions would, and for every UPDATE interns the attributes and then
|
||||
/// announces the prefixes: everything between a decoded message and a
|
||||
/// chosen best route.
|
||||
fn sharded_table(dump: &Dump, peers: &[usize], parsed: &[Option<Attributes>], threads: usize) {
|
||||
println!("\n== The sharded table, {threads} thread(s)");
|
||||
let updates = grouped(dump, peers, parsed);
|
||||
let routes: usize = updates
|
||||
.iter()
|
||||
.flatten()
|
||||
.map(|(_, prefixes)| prefixes.len())
|
||||
.sum();
|
||||
|
||||
let region = Region::begin();
|
||||
let table = Arc::new(RibTable::new(threads * 16));
|
||||
let indices = add_peers(&table, dump, peers);
|
||||
let started = Instant::now();
|
||||
let best_changes = load(&table, dump, &updates, &indices, threads);
|
||||
let elapsed = started.elapsed();
|
||||
assert_eq!(table.len(), routes);
|
||||
let held = retained(®ion);
|
||||
row(
|
||||
"intern attributes, insert, choose best",
|
||||
elapsed,
|
||||
format!(
|
||||
"{} routes; {:.0} MiB = {:.0} bytes a route, attribute sets included",
|
||||
per_second(routes, elapsed),
|
||||
mib(held),
|
||||
held as f64 / routes as f64
|
||||
),
|
||||
);
|
||||
println!(
|
||||
" {} prefixes, {} attribute sets, best route changed {} times",
|
||||
table.prefix_count(),
|
||||
table.attrs().len(),
|
||||
best_changes
|
||||
);
|
||||
|
||||
let started = Instant::now();
|
||||
let mut best = 0usize;
|
||||
table.for_each_best(|prefix, _, _| {
|
||||
best += usize::from(std::hint::black_box(prefix).length() > 0)
|
||||
});
|
||||
row(
|
||||
"read every best route",
|
||||
started.elapsed(),
|
||||
format!("{best} of them, in place, table not stopped"),
|
||||
);
|
||||
|
||||
// Reading has to look at each route for the timing to mean anything.
|
||||
let started = Instant::now();
|
||||
let mut all = 0usize;
|
||||
let mut hops = 0u64;
|
||||
table.for_each_route(|route| {
|
||||
all += 1;
|
||||
hops += u64::from(route.attrs.key().as_path_len);
|
||||
});
|
||||
row(
|
||||
"read every route and its attributes",
|
||||
started.elapsed(),
|
||||
format!(
|
||||
"{all} of them, mean AS path {:.1}",
|
||||
hops as f64 / all as f64
|
||||
),
|
||||
);
|
||||
|
||||
let started = Instant::now();
|
||||
let outcome = table.remove_peer(indices[0]);
|
||||
let elapsed = started.elapsed();
|
||||
row(
|
||||
"remove one peer (session down)",
|
||||
elapsed,
|
||||
format!(
|
||||
"{} routes; best route changed for {} prefixes",
|
||||
per_second(outcome.removed, elapsed),
|
||||
outcome.best_changed
|
||||
),
|
||||
);
|
||||
let started = Instant::now();
|
||||
let swept = table.attrs().sweep();
|
||||
row(
|
||||
"drop attribute sets no longer used",
|
||||
started.elapsed(),
|
||||
format!("{swept} dropped"),
|
||||
);
|
||||
}
|
||||
|
||||
/// A peer's routes grouped as UPDATEs: the attribute set and the prefixes
|
||||
/// announced with it.
|
||||
type Updates = Vec<Vec<(u32, Vec<IpPrefix>)>>;
|
||||
|
||||
fn grouped(dump: &Dump, peers: &[usize], parsed: &[Option<Attributes>]) -> Updates {
|
||||
peers
|
||||
.iter()
|
||||
.map(|&peer| {
|
||||
let mut routes = dump.routes[peer].clone();
|
||||
routes.sort_by_key(|(_, set)| *set);
|
||||
routes
|
||||
.chunk_by(|a, b| a.1 == b.1)
|
||||
.filter(|group| parsed[group[0].1 as usize].is_some())
|
||||
.map(|group| {
|
||||
(
|
||||
group[0].1,
|
||||
group.iter().map(|(prefix, _)| *prefix).collect(),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn add_peers(table: &RibTable, dump: &Dump, peers: &[usize]) -> Vec<PeerIndex> {
|
||||
peers
|
||||
.iter()
|
||||
.map(|&peer| {
|
||||
table
|
||||
.add_peer(PeerInfo {
|
||||
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")
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Announces everything in `updates` on `threads` threads, each taking
|
||||
/// whole peers. Returns how many times a best route changed.
|
||||
fn load(
|
||||
table: &RibTable,
|
||||
dump: &Dump,
|
||||
updates: &Updates,
|
||||
indices: &[PeerIndex],
|
||||
threads: usize,
|
||||
) -> usize {
|
||||
let next = AtomicUsize::new(0);
|
||||
let best_changes = AtomicUsize::new(0);
|
||||
std::thread::scope(|scope| {
|
||||
for _ in 0..threads {
|
||||
scope.spawn(|| {
|
||||
let mut changes = 0;
|
||||
loop {
|
||||
let at = next.fetch_add(1, Ordering::Relaxed);
|
||||
let Some(updates) = updates.get(at) else {
|
||||
break;
|
||||
};
|
||||
for (set, prefixes) in updates {
|
||||
// From the bytes, as a dump has them and as a
|
||||
// session could hand them over without parsing.
|
||||
let Some(attrs) = table
|
||||
.attrs()
|
||||
.intern_wire(&dump.attribute_sets[*set as usize])
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
changes += table.announce(indices[at], &attrs, prefixes).best_changed;
|
||||
}
|
||||
}
|
||||
best_changes.fetch_add(changes, Ordering::Relaxed);
|
||||
});
|
||||
}
|
||||
});
|
||||
best_changes.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// The same load with someone following the best routes through the
|
||||
/// journal: once reading as fast as it can while the table fills, once not
|
||||
/// reading at all until the table is full.
|
||||
fn followed_table(dump: &Dump, peers: &[usize], parsed: &[Option<Attributes>], threads: usize) {
|
||||
println!("\n== The sharded table with a follower, {threads} thread(s)");
|
||||
let updates = grouped(dump, peers, parsed);
|
||||
let routes: usize = updates
|
||||
.iter()
|
||||
.flatten()
|
||||
.map(|(_, prefixes)| prefixes.len())
|
||||
.sum();
|
||||
let count = |counts: &mut (usize, usize, usize), event: Event<'_>| match event {
|
||||
Event::Best { attrs, .. } => {
|
||||
// Look at the route, as a follower would.
|
||||
counts.0 += usize::from(attrs.key().as_path_len > 0);
|
||||
}
|
||||
Event::Withdrawn { .. } => counts.1 += 1,
|
||||
Event::Reset | Event::Synced => counts.2 += 1,
|
||||
// Not given to a follower of the best routes.
|
||||
Event::Routes { .. } => {}
|
||||
};
|
||||
|
||||
// Reading while the table fills.
|
||||
let table = Arc::new(RibTable::new(threads * 16));
|
||||
let indices = add_peers(&table, dump, peers);
|
||||
let done = AtomicBool::new(false);
|
||||
let started = Instant::now();
|
||||
let (best_changes, counts, longest, after_load) = std::thread::scope(|scope| {
|
||||
let follower = scope.spawn(|| {
|
||||
let mut reader = table.reader();
|
||||
let mut counts = (0, 0, 0);
|
||||
let mut longest = 0;
|
||||
while !done.load(Ordering::Acquire) {
|
||||
reader.poll(4096, |event| count(&mut counts, event));
|
||||
longest = longest.max(table.journal_len());
|
||||
}
|
||||
let finishing = Instant::now();
|
||||
while !reader
|
||||
.poll(4096, |event| count(&mut counts, event))
|
||||
.caught_up
|
||||
{}
|
||||
(counts, longest, finishing.elapsed())
|
||||
});
|
||||
let best_changes = load(&table, dump, &updates, &indices, threads);
|
||||
done.store(true, Ordering::Release);
|
||||
let (counts, longest, after_load) = follower.join().expect("the follower runs");
|
||||
(best_changes, counts, longest, after_load)
|
||||
});
|
||||
let elapsed = started.elapsed();
|
||||
assert_eq!(table.len(), routes);
|
||||
row(
|
||||
"load, follower reading throughout",
|
||||
elapsed,
|
||||
format!(
|
||||
"{} routes; caught up {:.3} s after the last route",
|
||||
per_second(routes, elapsed),
|
||||
after_load.as_secs_f64()
|
||||
),
|
||||
);
|
||||
println!(
|
||||
" best route changed {best_changes} times; the follower was given {} events for it; \n journal at most {longest} entries for {} prefixes",
|
||||
counts.0 + counts.1,
|
||||
table.prefix_count()
|
||||
);
|
||||
drop(table);
|
||||
|
||||
// Not reading: the journal has to stay small on its own account.
|
||||
let table = Arc::new(RibTable::new(threads * 16));
|
||||
let indices = add_peers(&table, dump, peers);
|
||||
let mut reader = table.reader();
|
||||
let mut counts = (0, 0, 0);
|
||||
reader.poll(usize::MAX, |event| count(&mut counts, event));
|
||||
let started = Instant::now();
|
||||
load(&table, dump, &updates, &indices, threads);
|
||||
let elapsed = started.elapsed();
|
||||
row(
|
||||
"load, follower not reading",
|
||||
elapsed,
|
||||
format!(
|
||||
"{} routes; journal holds {} entries for {} prefixes",
|
||||
per_second(routes, elapsed),
|
||||
table.journal_len(),
|
||||
table.prefix_count()
|
||||
),
|
||||
);
|
||||
let started = Instant::now();
|
||||
let mut counts = (0, 0, 0);
|
||||
while !reader
|
||||
.poll(4096, |event| count(&mut counts, event))
|
||||
.caught_up
|
||||
{}
|
||||
let elapsed = started.elapsed();
|
||||
row(
|
||||
"the follower then catches up",
|
||||
elapsed,
|
||||
format!(
|
||||
"{} events, {} of them a reset",
|
||||
counts.0 + counts.1,
|
||||
counts.2
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -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,4 +0,0 @@
|
||||
# Table dumps downloaded for benchmarks; see README.md.
|
||||
*
|
||||
!.gitignore
|
||||
!README.md
|
||||
@@ -1,18 +0,0 @@
|
||||
# Table dumps for the benchmarks
|
||||
|
||||
`cargo bench -p bgp-bench --bench real_table` runs on a real routing table:
|
||||
a dump of every route that the peers of a route collector had announced to
|
||||
it. The dumps are large and are not checked in. Fetch one into this
|
||||
directory:
|
||||
|
||||
```bash
|
||||
curl -O https://data.ris.ripe.net/rrc03/2026.09/bview.20260915.0000.gz
|
||||
```
|
||||
|
||||
That is RIPE RIS collector `rrc03`, which peers at AMS-IX: about 340 MB,
|
||||
3.4 GB uncompressed. Any `bview` file from <https://data.ris.ripe.net/> or
|
||||
a RouteViews RIB works; the most recent `.gz` here is used, or the one named
|
||||
by `BENCH_MRT`.
|
||||
|
||||
The benchmark needs memory in proportion to the dump. `BENCH_PEERS=8`
|
||||
limits it to the eight peers with the most routes.
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,378 +0,0 @@
|
||||
//! 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:
|
||||
//!
|
||||
//! - about a million IPv4 prefixes, most of them /24s, and about a quarter
|
||||
//! of a million IPv6 prefixes, most of them /48s;
|
||||
//! - prefixes that share their attributes are announced together, a handful
|
||||
//! to an UPDATE, as routers do;
|
||||
//! - AS paths a few hops long, and communities on most routes;
|
||||
//! - several peers announce the same prefixes, each with attributes of its
|
||||
//! own.
|
||||
|
||||
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};
|
||||
|
||||
use bgp_packet::message::{
|
||||
AsPathAttribute, AsPathSegment, CommunitiesPathAttribute, LargeCommunitiesPathAttribute,
|
||||
LargeCommunity, Message, MpReachNlriPathAttribute, MultiExitDiscPathAttribute,
|
||||
NextHopPathAttribute, NlriNextHop, OriginPathAttribute, PathAttribute, UpdateMessage,
|
||||
};
|
||||
use bgp_primitives::constants::{AfiSafi, IpVersion};
|
||||
use bgp_primitives::ip_prefix::{IpPrefix, Nlri};
|
||||
use bgp_primitives::parser::ParserContext;
|
||||
use bytes::BytesMut;
|
||||
|
||||
/// How big a table to make.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Shape {
|
||||
pub ipv4_prefixes: usize,
|
||||
pub ipv6_prefixes: usize,
|
||||
/// How many prefixes share one set of attributes, on average.
|
||||
pub prefixes_per_attribute_set: usize,
|
||||
}
|
||||
|
||||
impl Shape {
|
||||
/// Roughly the default-free zone in 2026.
|
||||
pub const INTERNET: Self = Self {
|
||||
ipv4_prefixes: 1_000_000,
|
||||
ipv6_prefixes: 230_000,
|
||||
prefixes_per_attribute_set: 6,
|
||||
};
|
||||
|
||||
pub fn prefixes(&self) -> usize {
|
||||
self.ipv4_prefixes + self.ipv6_prefixes
|
||||
}
|
||||
|
||||
/// The same shape, `factor` times smaller.
|
||||
pub fn scaled_down(&self, factor: usize) -> Self {
|
||||
Self {
|
||||
ipv4_prefixes: self.ipv4_prefixes / factor,
|
||||
ipv6_prefixes: self.ipv6_prefixes / factor,
|
||||
..*self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A small deterministic generator (SplitMix64); no need for a dependency.
|
||||
pub struct Rng(u64);
|
||||
|
||||
impl Rng {
|
||||
pub fn new(seed: u64) -> Self {
|
||||
Self(seed)
|
||||
}
|
||||
|
||||
pub fn next_u64(&mut self) -> u64 {
|
||||
self.0 = self.0.wrapping_add(0x9e37_79b9_7f4a_7c15);
|
||||
let mut z = self.0;
|
||||
z = (z ^ (z >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
|
||||
z = (z ^ (z >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
|
||||
z ^ (z >> 31)
|
||||
}
|
||||
|
||||
/// A number below `bound`.
|
||||
pub fn below(&mut self, bound: u64) -> u64 {
|
||||
self.next_u64() % bound
|
||||
}
|
||||
|
||||
/// An entry of `weighted`, picked with probability proportional to its
|
||||
/// weight.
|
||||
fn pick<T: Copy>(&mut self, weighted: &[(T, u64)]) -> T {
|
||||
let total: u64 = weighted.iter().map(|(_, w)| w).sum();
|
||||
let mut at = self.below(total);
|
||||
for (value, weight) in weighted {
|
||||
if at < *weight {
|
||||
return *value;
|
||||
}
|
||||
at -= weight;
|
||||
}
|
||||
weighted[weighted.len() - 1].0
|
||||
}
|
||||
}
|
||||
|
||||
/// The prefixes of the table. Every peer announces these same prefixes.
|
||||
pub struct Prefixes {
|
||||
pub ipv4: Vec<IpPrefix>,
|
||||
pub ipv6: Vec<IpPrefix>,
|
||||
}
|
||||
|
||||
impl Prefixes {
|
||||
pub fn generate(shape: Shape) -> Self {
|
||||
let mut rng = Rng::new(0x1234_5678);
|
||||
// Shares of prefix lengths, close to what the table looks like.
|
||||
let v4_lengths: [(u8, u64); 9] = [
|
||||
(24, 60),
|
||||
(23, 10),
|
||||
(22, 11),
|
||||
(21, 5),
|
||||
(20, 4),
|
||||
(19, 3),
|
||||
(18, 2),
|
||||
(16, 4),
|
||||
(15, 1),
|
||||
];
|
||||
let v6_lengths: [(u8, u64); 7] = [
|
||||
(48, 48),
|
||||
(32, 14),
|
||||
(40, 10),
|
||||
(44, 10),
|
||||
(36, 6),
|
||||
(29, 6),
|
||||
(47, 6),
|
||||
];
|
||||
|
||||
let mut seen = HashSet::new();
|
||||
let mut ipv4 = Vec::with_capacity(shape.ipv4_prefixes);
|
||||
while ipv4.len() < shape.ipv4_prefixes {
|
||||
let length = rng.pick(&v4_lengths);
|
||||
// Unicast space, 1.0.0.0 to 223.255.255.255.
|
||||
let address = 0x0100_0000 + (rng.next_u64() as u32) % 0xdf00_0000;
|
||||
let prefix = IpPrefix::new(IpVersion::Ipv4, address.to_be_bytes(), length)
|
||||
.expect("four bytes cover any IPv4 prefix");
|
||||
if seen.insert(prefix) {
|
||||
ipv4.push(prefix);
|
||||
}
|
||||
}
|
||||
let mut ipv6 = Vec::with_capacity(shape.ipv6_prefixes);
|
||||
while ipv6.len() < shape.ipv6_prefixes {
|
||||
let length = rng.pick(&v6_lengths);
|
||||
// Global unicast, 2000::/3.
|
||||
let address = (0x2000_u128 << 112) | (u128::from(rng.next_u64()) << 61 >> 3);
|
||||
let prefix = IpPrefix::new(IpVersion::Ipv6, address.to_be_bytes(), length)
|
||||
.expect("sixteen bytes cover any IPv6 prefix");
|
||||
if seen.insert(prefix) {
|
||||
ipv6.push(prefix);
|
||||
}
|
||||
}
|
||||
Self { ipv4, ipv6 }
|
||||
}
|
||||
}
|
||||
|
||||
/// The UPDATE messages with which the peer numbered `peer` announces the
|
||||
/// whole table. The peer's AS is `64512 + peer`.
|
||||
pub fn announcements(prefixes: &Prefixes, shape: Shape, peer: u32) -> Vec<UpdateMessage> {
|
||||
let mut rng = Rng::new(0x9999 + u64::from(peer));
|
||||
let peer_asn = 64512 + peer;
|
||||
let attributes = |rng: &mut Rng| {
|
||||
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![
|
||||
PathAttribute::Origin(OriginPathAttribute::IGP),
|
||||
PathAttribute::AsPath(AsPathAttribute {
|
||||
segments: vec![AsPathSegment {
|
||||
ordered: true,
|
||||
path,
|
||||
}],
|
||||
}),
|
||||
];
|
||||
let mut later = Vec::new();
|
||||
if rng.below(4) == 0 {
|
||||
later.push(PathAttribute::MultiExitDisc(MultiExitDiscPathAttribute(
|
||||
rng.below(1000) as u32,
|
||||
)));
|
||||
}
|
||||
let communities = rng.below(7) as usize;
|
||||
if communities > 0 {
|
||||
later.push(PathAttribute::Communities(CommunitiesPathAttribute(
|
||||
(0..communities)
|
||||
.map(|_| (rng.below(65000) as u16, rng.below(1000) as u16))
|
||||
.collect(),
|
||||
)));
|
||||
}
|
||||
let large = if rng.below(5) == 0 {
|
||||
Some(PathAttribute::LargeCommunities(
|
||||
LargeCommunitiesPathAttribute {
|
||||
communities: vec![LargeCommunity {
|
||||
global_admin: peer_asn,
|
||||
data_1: rng.below(100) as u32,
|
||||
data_2: rng.below(100) as u32,
|
||||
}],
|
||||
},
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
(attributes, later, large)
|
||||
};
|
||||
|
||||
let mut updates = Vec::new();
|
||||
// Prefixes that share attributes travel together. A set's prefixes are a
|
||||
// run of the (randomly ordered) list, a few long.
|
||||
let mut grouped = |list: &[IpPrefix], rng: &mut Rng, ipv6: bool| {
|
||||
let mut rest = list;
|
||||
while !rest.is_empty() {
|
||||
let size = (1 + rng.below(2 * shape.prefixes_per_attribute_set as u64 - 1) as usize)
|
||||
.min(rest.len());
|
||||
let (group, after) = rest.split_at(size);
|
||||
rest = after;
|
||||
|
||||
let (mut path_attributes, later, large) = attributes(rng);
|
||||
let update = if ipv6 {
|
||||
// In order of type code: MED and COMMUNITIES come before
|
||||
// MP_REACH_NLRI.
|
||||
path_attributes.extend(later);
|
||||
path_attributes.push(PathAttribute::MpReachNlri(MpReachNlriPathAttribute {
|
||||
family: AfiSafi::IPV6_UNICAST,
|
||||
next_hop: NlriNextHop::Ipv6(Ipv6Addr::new(
|
||||
0x2001,
|
||||
0xdb8,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
peer as u16,
|
||||
)),
|
||||
prefixes: Nlri::plain(group.iter().copied()),
|
||||
}));
|
||||
path_attributes.extend(large);
|
||||
UpdateMessage {
|
||||
withdrawn_routes: vec![],
|
||||
path_attributes,
|
||||
announced_routes: vec![],
|
||||
}
|
||||
} else {
|
||||
path_attributes.push(PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::new(
|
||||
192, 0, 2, peer as u8,
|
||||
))));
|
||||
path_attributes.extend(later);
|
||||
path_attributes.extend(large);
|
||||
UpdateMessage {
|
||||
withdrawn_routes: vec![],
|
||||
path_attributes,
|
||||
announced_routes: Nlri::plain(group.iter().copied()),
|
||||
}
|
||||
};
|
||||
updates.push(update);
|
||||
}
|
||||
};
|
||||
grouped(&prefixes.ipv4, &mut rng, false);
|
||||
grouped(&prefixes.ipv6, &mut rng, true);
|
||||
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> {
|
||||
let ctx = wire_context();
|
||||
updates
|
||||
.iter()
|
||||
.map(|update| {
|
||||
let mut wire = BytesMut::new();
|
||||
Message::Update(update.clone())
|
||||
.to_wire(&ctx, &mut wire)
|
||||
.expect("a few prefixes fit one message");
|
||||
wire
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The context of the sessions the benchmarks pretend to have: an external
|
||||
/// peer with four-octet AS numbers.
|
||||
pub fn wire_context() -> ParserContext {
|
||||
ParserContext {
|
||||
four_octet_asn: true,
|
||||
ibgp: false,
|
||||
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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
//! Counting the memory a benchmark holds.
|
||||
//!
|
||||
//! With the `count-memory` feature every allocation is counted, and
|
||||
//! [`Region::held`] says how many bytes were allocated and not freed since
|
||||
//! the region began. Without it nothing is counted and the answer is
|
||||
//! `None`: the counters are shared by all threads, and counting slows down
|
||||
//! exactly the code whose speed is being measured.
|
||||
|
||||
#[cfg(feature = "count-memory")]
|
||||
mod counting {
|
||||
use std::alloc::System;
|
||||
|
||||
use stats_alloc::{INSTRUMENTED_SYSTEM, StatsAlloc};
|
||||
|
||||
#[global_allocator]
|
||||
static GLOBAL: &StatsAlloc<System> = &INSTRUMENTED_SYSTEM;
|
||||
|
||||
pub struct Region(stats_alloc::Region<'static, System>);
|
||||
|
||||
impl Region {
|
||||
pub fn begin() -> Self {
|
||||
Self(stats_alloc::Region::new(GLOBAL))
|
||||
}
|
||||
|
||||
pub fn held(&self) -> Option<i64> {
|
||||
let stats = self.0.change();
|
||||
Some(stats.bytes_allocated as i64 - stats.bytes_deallocated as i64)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "count-memory"))]
|
||||
mod counting {
|
||||
pub struct Region;
|
||||
|
||||
impl Region {
|
||||
pub fn begin() -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
pub fn held(&self) -> Option<i64> {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub use counting::Region;
|
||||
|
||||
/// "`bytes` MiB = n bytes a `unit`", or a note that memory is not being
|
||||
/// counted.
|
||||
pub fn describe(held: Option<i64>, count: usize, unit: &str) -> String {
|
||||
match held {
|
||||
Some(bytes) => format!(
|
||||
"{:.0} MiB = {:.0} bytes a {unit}",
|
||||
bytes as f64 / (1024.0 * 1024.0),
|
||||
bytes as f64 / count as f64
|
||||
),
|
||||
None => "memory not counted (run with --features count-memory)".into(),
|
||||
}
|
||||
}
|
||||
@@ -1,227 +0,0 @@
|
||||
//! Reads a table dump in MRT format (RFC 6396, TABLE_DUMP_V2), as published
|
||||
//! by RIPE RIS and RouteViews: the routes that every peer of a route
|
||||
//! collector had announced to it at one moment.
|
||||
//!
|
||||
//! Only what the benchmarks need is read: the IPv4 and IPv6 unicast tables.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::fs::File;
|
||||
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_primitives::constants::IpVersion;
|
||||
use bgp_primitives::ip_prefix::IpPrefix;
|
||||
use flate2::read::MultiGzDecoder;
|
||||
|
||||
use crate::wire_context;
|
||||
|
||||
const TABLE_DUMP_V2: u16 = 13;
|
||||
const PEER_INDEX_TABLE: u16 = 1;
|
||||
const RIB_IPV4_UNICAST: u16 = 2;
|
||||
const RIB_IPV6_UNICAST: u16 = 4;
|
||||
|
||||
/// A peer of the collector.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DumpPeer {
|
||||
pub address: IpAddr,
|
||||
pub asn: u32,
|
||||
}
|
||||
|
||||
/// The contents of a table dump.
|
||||
pub struct Dump {
|
||||
/// When the dump was taken, in seconds since the Unix epoch.
|
||||
pub timestamp: u32,
|
||||
pub peers: Vec<DumpPeer>,
|
||||
/// Every distinct list of path attributes in the dump, as it is on the
|
||||
/// wire. Routes refer to these by index.
|
||||
pub attribute_sets: Vec<Box<[u8]>>,
|
||||
/// For each peer (by index into `peers`), its routes: a prefix and the
|
||||
/// index of its attributes.
|
||||
pub routes: Vec<Vec<(IpPrefix, u32)>>,
|
||||
}
|
||||
|
||||
impl Dump {
|
||||
/// Reads the dump at `path`, gzip-compressed as it is published.
|
||||
pub fn read(path: &Path) -> io::Result<Self> {
|
||||
let mut input = BufReader::with_capacity(
|
||||
1 << 20,
|
||||
MultiGzDecoder::new(BufReader::new(File::open(path)?)),
|
||||
);
|
||||
let mut dump = Dump {
|
||||
timestamp: 0,
|
||||
peers: Vec::new(),
|
||||
attribute_sets: Vec::new(),
|
||||
routes: Vec::new(),
|
||||
};
|
||||
let mut interned: HashMap<Box<[u8]>, u32> = HashMap::new();
|
||||
let mut body = Vec::new();
|
||||
loop {
|
||||
let mut header = [0u8; 12];
|
||||
match input.read_exact(&mut header) {
|
||||
Ok(()) => {}
|
||||
Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => break,
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
let timestamp = u32::from_be_bytes([header[0], header[1], header[2], header[3]]);
|
||||
let kind = u16::from_be_bytes([header[4], header[5]]);
|
||||
let subtype = u16::from_be_bytes([header[6], header[7]]);
|
||||
let length = u32::from_be_bytes([header[8], header[9], header[10], header[11]]);
|
||||
body.resize(length as usize, 0);
|
||||
input.read_exact(&mut body)?;
|
||||
if kind != TABLE_DUMP_V2 {
|
||||
continue;
|
||||
}
|
||||
let mut record = Cursor(&body);
|
||||
let malformed = || io::Error::new(io::ErrorKind::InvalidData, "truncated MRT record");
|
||||
match subtype {
|
||||
PEER_INDEX_TABLE => {
|
||||
dump.timestamp = timestamp;
|
||||
dump.peers = peer_index_table(&mut record).ok_or_else(malformed)?;
|
||||
dump.routes = vec![Vec::new(); dump.peers.len()];
|
||||
}
|
||||
RIB_IPV4_UNICAST | RIB_IPV6_UNICAST => {
|
||||
let version = if subtype == RIB_IPV4_UNICAST {
|
||||
IpVersion::Ipv4
|
||||
} else {
|
||||
IpVersion::Ipv6
|
||||
};
|
||||
rib_entries(&mut record, version, &mut dump, &mut interned)
|
||||
.ok_or_else(malformed)?;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Ok(dump)
|
||||
}
|
||||
|
||||
pub fn route_count(&self) -> usize {
|
||||
self.routes.iter().map(Vec::len).sum()
|
||||
}
|
||||
|
||||
/// The peers with the most routes, most first: the ones that send a
|
||||
/// full table.
|
||||
pub fn largest_peers(&self, count: usize) -> Vec<usize> {
|
||||
let mut peers: Vec<usize> = (0..self.peers.len()).collect();
|
||||
peers.sort_by_key(|&peer| std::cmp::Reverse(self.routes[peer].len()));
|
||||
peers.truncate(count);
|
||||
peers
|
||||
}
|
||||
}
|
||||
|
||||
struct Cursor<'a>(&'a [u8]);
|
||||
|
||||
impl<'a> Cursor<'a> {
|
||||
fn take(&mut self, n: usize) -> Option<&'a [u8]> {
|
||||
if self.0.len() < n {
|
||||
return None;
|
||||
}
|
||||
let (taken, rest) = self.0.split_at(n);
|
||||
self.0 = rest;
|
||||
Some(taken)
|
||||
}
|
||||
fn u8(&mut self) -> Option<u8> {
|
||||
self.take(1).map(|b| b[0])
|
||||
}
|
||||
fn u16(&mut self) -> Option<u16> {
|
||||
self.take(2).map(|b| u16::from_be_bytes([b[0], b[1]]))
|
||||
}
|
||||
fn u32(&mut self) -> Option<u32> {
|
||||
self.take(4)
|
||||
.map(|b| u32::from_be_bytes([b[0], b[1], b[2], b[3]]))
|
||||
}
|
||||
}
|
||||
|
||||
fn peer_index_table(record: &mut Cursor<'_>) -> Option<Vec<DumpPeer>> {
|
||||
record.u32()?; // collector's BGP identifier
|
||||
let name = record.u16()?;
|
||||
record.take(name.into())?;
|
||||
let count = record.u16()?;
|
||||
let mut peers = Vec::with_capacity(count.into());
|
||||
for _ in 0..count {
|
||||
let kind = record.u8()?;
|
||||
record.u32()?; // peer's BGP identifier
|
||||
let address = if kind & 1 == 1 {
|
||||
let bytes: [u8; 16] = record.take(16)?.try_into().ok()?;
|
||||
IpAddr::from(Ipv6Addr::from(bytes))
|
||||
} else {
|
||||
let bytes: [u8; 4] = record.take(4)?.try_into().ok()?;
|
||||
IpAddr::from(Ipv4Addr::from(bytes))
|
||||
};
|
||||
let asn = if kind & 2 == 2 {
|
||||
record.u32()?
|
||||
} else {
|
||||
record.u16()?.into()
|
||||
};
|
||||
peers.push(DumpPeer { address, asn });
|
||||
}
|
||||
Some(peers)
|
||||
}
|
||||
|
||||
fn rib_entries(
|
||||
record: &mut Cursor<'_>,
|
||||
version: IpVersion,
|
||||
dump: &mut Dump,
|
||||
interned: &mut HashMap<Box<[u8]>, u32>,
|
||||
) -> Option<()> {
|
||||
record.u32()?; // sequence number
|
||||
let length = record.u8()?;
|
||||
let bytes = record.take(usize::from(length.div_ceil(8)))?;
|
||||
// A prefix the collector should not have had is not ours to fix.
|
||||
let prefix = IpPrefix::new(version, bytes, length).ok();
|
||||
let count = record.u16()?;
|
||||
for _ in 0..count {
|
||||
let peer = usize::from(record.u16()?);
|
||||
record.u32()?; // when the route was received
|
||||
let attributes_len = record.u16()?;
|
||||
let attributes = record.take(attributes_len.into())?;
|
||||
let (Some(prefix), Some(routes)) = (prefix, dump.routes.get_mut(peer)) else {
|
||||
continue;
|
||||
};
|
||||
let id = match interned.get(attributes) {
|
||||
Some(id) => *id,
|
||||
None => {
|
||||
let id = u32::try_from(dump.attribute_sets.len()).ok()?;
|
||||
dump.attribute_sets.push(attributes.into());
|
||||
interned.insert(attributes.into(), id);
|
||||
id
|
||||
}
|
||||
};
|
||||
routes.push((prefix, id));
|
||||
}
|
||||
Some(())
|
||||
}
|
||||
|
||||
/// The path attributes of a route in a dump, parsed.
|
||||
///
|
||||
/// In a dump the AS numbers are always four octets wide, and
|
||||
/// MP_REACH_NLRI holds only the next hop (RFC 6396 §4.3.4): the prefix is in
|
||||
/// the record. The next hop is returned beside the other attributes, as the
|
||||
/// RIB keeps it. `None` if the attributes cannot be parsed.
|
||||
pub fn parse_attributes(mut wire: &[u8]) -> Option<(Vec<PathAttribute>, Option<NlriNextHop>)> {
|
||||
let ctx = wire_context();
|
||||
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;
|
||||
};
|
||||
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(&[]);
|
||||
}
|
||||
}
|
||||
@@ -1,30 +1,17 @@
|
||||
[package]
|
||||
name = "bgp-integration-tests"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
homepage.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
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
|
||||
@@ -1,14 +1,13 @@
|
||||
//! A minimal stand-in for a neighbouring router: connects to the daemon,
|
||||
//! A minimal stand-in for a lab router: connects to a ferrix_bgp instance,
|
||||
//! completes the handshake, optionally announces some prefixes, and prints
|
||||
//! every UPDATE it receives. Useful for trying things out when no real
|
||||
//! router is around.
|
||||
//! every UPDATE it receives. Useful for demos when no real router is around.
|
||||
//!
|
||||
//! cargo run -p bgp-integration-tests --example fake_peer -- \
|
||||
//! --server 127.0.0.1:1790 --asn 65001 --announce 203.0.113.0/24
|
||||
//!
|
||||
//! With `--echo`, every prefix received is re-announced back to the server
|
||||
//! with this peer's ASN prepended, which lets a single daemon show its own
|
||||
//! routes "coming back" in the received table.
|
||||
//! with this peer's ASN prepended, which lets a single ferrix instance show
|
||||
//! its own routes "coming back" in the received table.
|
||||
//!
|
||||
//! With `--listen ADDR` it instead waits for the server to dial it (one
|
||||
//! connection at a time), which exercises the server's active side.
|
||||
@@ -16,9 +15,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;
|
||||
@@ -26,7 +27,7 @@ use tokio::net::TcpListener;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
struct Args {
|
||||
/// Address of the daemon's BGP listener to connect to.
|
||||
/// Address of the ferrix_bgp listener to connect to.
|
||||
#[arg(long, default_value = "127.0.0.1:1790", conflicts_with = "listen")]
|
||||
server: String,
|
||||
/// Instead of connecting, listen here and accept connections from the
|
||||
@@ -76,18 +77,13 @@ async fn main() -> Result<()> {
|
||||
}
|
||||
|
||||
async fn run(args: &Args, mut peer: TestPeer, addr: std::net::SocketAddr) -> Result<()> {
|
||||
let open = peer
|
||||
.handshake(args.asn, args.router_id, args.hold_time)
|
||||
.await?;
|
||||
println!(
|
||||
"established with {} (AS{} router-id {})",
|
||||
addr, open.asn, open.identifier
|
||||
);
|
||||
let open = peer.handshake(args.asn, args.router_id, args.hold_time).await?;
|
||||
println!("established with {} (AS{} router-id {})", addr, open.asn, open.identifier);
|
||||
|
||||
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.clone(), vec![args.asn], vec![(args.asn as u16, 1)]);
|
||||
peer.send(&Message::Update(update)).await?;
|
||||
println!("announced {prefix}");
|
||||
}
|
||||
|
||||
@@ -96,8 +92,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 +115,16 @@ 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] }
|
||||
}
|
||||
|
||||
async fn echo_back(peer: &mut TestPeer, u: &UpdateMessage, asn: u32) -> Result<()> {
|
||||
@@ -131,9 +132,7 @@ async fn echo_back(peer: &mut TestPeer, u: &UpdateMessage, asn: u32) -> Result<(
|
||||
let mut communities = vec![];
|
||||
for attr in &u.path_attributes {
|
||||
match attr {
|
||||
PathAttribute::AsPath(p) => {
|
||||
as_path.extend(p.segments.iter().flat_map(|s| s.path.iter().copied()))
|
||||
}
|
||||
PathAttribute::AsPath(p) => as_path.extend(p.segments.iter().flat_map(|s| s.path.iter().copied())),
|
||||
PathAttribute::Communities(c) => communities = c.0.clone(),
|
||||
_ => {}
|
||||
}
|
||||
@@ -142,22 +141,18 @@ async fn echo_back(peer: &mut TestPeer, u: &UpdateMessage, asn: u32) -> Result<(
|
||||
println!(" not echoing: NO_EXPORT");
|
||||
return Ok(());
|
||||
}
|
||||
for nlri in &u.announced_routes {
|
||||
let prefix = nlri.prefix;
|
||||
peer.announce(announce(prefix, as_path.clone(), communities.clone()))
|
||||
.await?;
|
||||
for prefix in &u.announced_routes {
|
||||
peer.send(&Message::Update(announce(prefix.clone(), 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?;
|
||||
println!(
|
||||
" echoed withdraw of {:?}",
|
||||
u.withdrawn_routes
|
||||
.iter()
|
||||
.map(|p| p.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
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.iter().map(|p| p.to_string()).collect::<Vec<_>>());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -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,69 +1,19 @@
|
||||
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_primitives::constants::{Afi, AfiSafi, Safi};
|
||||
use bgp_packet::message::{BGP_HEADER_LEN, Message, UpdateMessage};
|
||||
use bgp_packet::open::{Capability, OpenMessage, OpenOption};
|
||||
use bgp_primitives::constants::{AddressFamilyId, SubsequentAfi};
|
||||
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.
|
||||
pub struct TestPeer {
|
||||
stream: TcpStream,
|
||||
pub ctx: ParserContext,
|
||||
/// The address families for which this peer sends path identifiers
|
||||
/// with its prefixes (ADD-PATH, RFC 7911). What it expects to be sent
|
||||
/// is `ctx.add_path`. A test sets both to match the capabilities it
|
||||
/// exchanged.
|
||||
pub send_add_path: Vec<AfiSafi>,
|
||||
/// Whether the OPEN we sent offered four-octet AS numbers. They are in
|
||||
/// use only if the server's did too.
|
||||
offered_four_octet_asn: bool,
|
||||
}
|
||||
|
||||
impl TestPeer {
|
||||
@@ -72,99 +22,22 @@ 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 {
|
||||
Self {
|
||||
stream,
|
||||
ctx: ParserContext::default(),
|
||||
send_add_path: Vec::new(),
|
||||
offered_four_octet_asn: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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()?)
|
||||
Self { stream, ctx: ParserContext::default() }
|
||||
}
|
||||
|
||||
/// Send a BGP message.
|
||||
pub async fn send(&mut self, msg: &Message) -> Result<()> {
|
||||
let send_ctx = ParserContext {
|
||||
add_path: self.send_add_path.clone(),
|
||||
..self.ctx.clone()
|
||||
};
|
||||
let mut buf = BytesMut::new();
|
||||
msg.to_wire(&send_ctx, &mut buf)?;
|
||||
msg.to_wire(&self.ctx, &mut buf)?;
|
||||
self.stream.write_all(&buf).await?;
|
||||
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?;
|
||||
|
||||
@@ -175,11 +48,11 @@ impl TestPeer {
|
||||
|
||||
let mut buf = vec![0u8; total_len];
|
||||
buf[..BGP_HEADER_LEN as usize].copy_from_slice(&header);
|
||||
self.stream
|
||||
.read_exact(&mut buf[BGP_HEADER_LEN as usize..])
|
||||
.await?;
|
||||
self.stream.read_exact(&mut buf[BGP_HEADER_LEN as usize..]).await?;
|
||||
|
||||
Message::decode(&self.ctx, &buf).map_err(|e| eyre!("Failed to parse BGP message: {e}"))
|
||||
let (_, msg) = Message::from_wire(&self.ctx, &buf)
|
||||
.map_err(|e| eyre!("Failed to parse BGP message: {e:?}"))?;
|
||||
Ok(msg)
|
||||
}
|
||||
|
||||
/// Read messages until an UPDATE arrives, skipping KEEPALIVEs. Any other
|
||||
@@ -189,19 +62,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,74 +71,39 @@ 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 { .. })))
|
||||
});
|
||||
self.ctx.four_octet_asn = four_byte && self.offered_four_octet_asn;
|
||||
self.ctx.four_octet_asn = Some(four_byte);
|
||||
Ok(o)
|
||||
}
|
||||
other => Err(eyre::eyre!("Expected OPEN, got {:?}", other)),
|
||||
other => bail!("Expected OPEN, got {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
/// Send an OPEN message with the given parameters and the capabilities
|
||||
/// of a typical dual-stack speaker: four-octet AS numbers, IPv4 and IPv6
|
||||
/// unicast, route refresh.
|
||||
/// Send an OPEN message with the given parameters and standard capabilities.
|
||||
pub async fn send_open(
|
||||
&mut self,
|
||||
local_asn: u32,
|
||||
router_id: Ipv4Addr,
|
||||
hold_time: u16,
|
||||
) -> Result<()> {
|
||||
let capabilities = vec![
|
||||
Capability::FourByteAsn { asn: local_asn },
|
||||
Capability::MultiProtocol {
|
||||
afi: Afi::IPV4,
|
||||
safi: Safi::UNICAST,
|
||||
},
|
||||
Capability::MultiProtocol {
|
||||
afi: Afi::IPV6,
|
||||
safi: Safi::UNICAST,
|
||||
},
|
||||
Capability::RouteRefresh,
|
||||
];
|
||||
self.send_open_with(local_asn, router_id, hold_time, capabilities)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Send an OPEN message carrying exactly `capabilities`.
|
||||
pub async fn send_open_with(
|
||||
&mut self,
|
||||
local_asn: u32,
|
||||
router_id: Ipv4Addr,
|
||||
hold_time: u16,
|
||||
capabilities: Vec<Capability>,
|
||||
) -> Result<()> {
|
||||
self.offered_four_octet_asn = capabilities
|
||||
.iter()
|
||||
.any(|c| matches!(c, Capability::FourByteAsn { .. }));
|
||||
// If the server's OPEN has been read already, what was settled
|
||||
// then still depends on what is offered now.
|
||||
self.ctx.four_octet_asn &= self.offered_four_octet_asn;
|
||||
let asn_field = if local_asn > u16::MAX as u32 {
|
||||
23456
|
||||
} else {
|
||||
local_asn as u16
|
||||
};
|
||||
let asn_field = if local_asn > u16::MAX as u32 { 23456 } else { local_asn as u16 };
|
||||
let open = Message::Open(OpenMessage {
|
||||
version: 4,
|
||||
asn: asn_field,
|
||||
hold_time,
|
||||
identifier: router_id,
|
||||
options: if capabilities.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
vec![OpenOption::Capabilities(capabilities)]
|
||||
},
|
||||
options: vec![OpenOption::Capabilities(vec![
|
||||
Capability::FourByteAsn { asn: local_asn },
|
||||
Capability::MultiProtocol {
|
||||
afi: AddressFamilyId::Ipv4,
|
||||
safi: SubsequentAfi::Unicast,
|
||||
},
|
||||
Capability::RouteRefresh,
|
||||
])],
|
||||
});
|
||||
self.send(&open).await
|
||||
}
|
||||
@@ -291,67 +117,29 @@ impl TestPeer {
|
||||
hold_time: u16,
|
||||
) -> Result<OpenMessage> {
|
||||
self.send_open(local_asn, router_id, hold_time).await?;
|
||||
self.finish_handshake().await
|
||||
}
|
||||
|
||||
/// As [`TestPeer::handshake`], with an OPEN carrying exactly
|
||||
/// `capabilities`.
|
||||
pub async fn handshake_with(
|
||||
&mut self,
|
||||
local_asn: u32,
|
||||
router_id: Ipv4Addr,
|
||||
hold_time: u16,
|
||||
capabilities: Vec<Capability>,
|
||||
) -> Result<OpenMessage> {
|
||||
self.send_open_with(local_asn, router_id, hold_time, capabilities)
|
||||
.await?;
|
||||
self.finish_handshake().await
|
||||
}
|
||||
|
||||
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_rib::{AdjRibIn, LocalRib, LocalRibPolicy};
|
||||
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::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::config::Config;
|
||||
use bgp_server::ping::PingMonitor;
|
||||
use bgp_server::server::BgpServerBuilder;
|
||||
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>>,
|
||||
handle: JoinHandle<Result<()>>,
|
||||
/// 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 Adj-RIB-In for test introspection.
|
||||
pub adj_rib_in: AdjRibIn,
|
||||
/// 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>,
|
||||
/// Ping monitor without a socket, watching 2001:db8::/32; tests feed it
|
||||
/// through `record`.
|
||||
pub pings: PingMonitor,
|
||||
pub local_asn: u32,
|
||||
}
|
||||
|
||||
@@ -57,7 +37,20 @@ impl TestServer {
|
||||
|
||||
/// Start a server with an explicit hold time (useful for timer tests).
|
||||
pub async fn start_with_hold_time(local_asn: u32, hold_time_secs: u16) -> Result<Self> {
|
||||
Self::start_with(local_asn, |c| c.hold_time_secs = hold_time_secs).await
|
||||
Self::start_inner(local_asn, hold_time_secs, LocalRibPolicy::default()).await
|
||||
}
|
||||
|
||||
/// Start a server with a policy restricting locally originated routes.
|
||||
pub async fn start_with_policy(local_asn: u32, policy: LocalRibPolicy) -> Result<Self> {
|
||||
Self::start_inner(local_asn, 90, policy).await
|
||||
}
|
||||
|
||||
async fn start_inner(local_asn: u32, hold_time_secs: u16, policy: LocalRibPolicy) -> Result<Self> {
|
||||
Self::start_with(local_asn, |c| {
|
||||
c.hold_time_secs = hold_time_secs;
|
||||
c.local_route_policy = policy;
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// Start a server after letting `customize` edit the default test config
|
||||
@@ -65,277 +58,39 @@ 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![],
|
||||
local_route_policy: LocalRibPolicy::default(),
|
||||
http_endpoint: None,
|
||||
rpki: None,
|
||||
kernel: None,
|
||||
export: Default::default(),
|
||||
export_limits: None,
|
||||
community_names: Vec::new(),
|
||||
control: None,
|
||||
graceful_restart: Default::default(),
|
||||
ping_monitor: None,
|
||||
};
|
||||
customize(&mut config);
|
||||
|
||||
let bound = BgpServer::bind(config).await?;
|
||||
let addrs = bound.local_addrs();
|
||||
let addr = *addrs.first().expect("at least one listener");
|
||||
let rib = Arc::clone(&bound.rib);
|
||||
let bound = BgpServerBuilder::new(config).bind().await?;
|
||||
let addr = bound
|
||||
.local_addrs()
|
||||
.into_iter()
|
||||
.next()
|
||||
.expect("at least one listener");
|
||||
let adj_rib_in = bound.adj_rib_in.clone();
|
||||
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 pings = PingMonitor::without_socket(
|
||||
vec![IpPrefix::try_from("2001:db8::/32").expect("valid prefix")],
|
||||
Some(local_rib.clone()),
|
||||
);
|
||||
|
||||
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.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ReceivedRoute {
|
||||
pub prefix: IpPrefix,
|
||||
pub peer: PeerIndex,
|
||||
/// The peer's identifier for the route; zero without ADD-PATH.
|
||||
pub path_id: u32,
|
||||
/// The route's attributes, without MP_REACH_NLRI.
|
||||
pub attributes: Vec<PathAttribute>,
|
||||
/// The next hop, if the route came in MP_REACH_NLRI.
|
||||
pub next_hop: Option<NlriNextHop>,
|
||||
/// Whether it is the best route for its prefix.
|
||||
pub best: bool,
|
||||
}
|
||||
|
||||
/// A change of a prefix's best route.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum RouteChange {
|
||||
/// The prefix has a best route, or another one than before.
|
||||
Announced { prefix: IpPrefix, peer: PeerIndex },
|
||||
/// The prefix has no route any more.
|
||||
Withdrawn { prefix: IpPrefix },
|
||||
}
|
||||
|
||||
/// The changes of best route in a server's table from the moment this was
|
||||
/// made, read from the table's journal.
|
||||
pub struct RouteChanges {
|
||||
reader: Reader,
|
||||
pending: std::collections::VecDeque<RouteChange>,
|
||||
}
|
||||
|
||||
impl RouteChanges {
|
||||
/// The next change, waiting for one if need be. Changes to one prefix
|
||||
/// that happen before they are read arrive as one.
|
||||
pub async fn recv(&mut self) -> Result<RouteChange> {
|
||||
loop {
|
||||
if let Some(change) = self.pending.pop_front() {
|
||||
return Ok(change);
|
||||
}
|
||||
self.reader.changed().await;
|
||||
self.read();
|
||||
}
|
||||
}
|
||||
|
||||
fn read(&mut self) {
|
||||
let pending = &mut self.pending;
|
||||
let mut read = |event: Event<'_>| match event {
|
||||
Event::Best { prefix, peer, .. } => {
|
||||
pending.push_back(RouteChange::Announced { prefix, peer })
|
||||
}
|
||||
Event::Withdrawn { prefix } => pending.push_back(RouteChange::Withdrawn { prefix }),
|
||||
// Not given to a follower of the best routes.
|
||||
Event::Routes { .. } => {}
|
||||
Event::Reset | Event::Synced => {}
|
||||
};
|
||||
while !self.reader.poll(1024, &mut read).caught_up {}
|
||||
Ok(Self { handle, addr, adj_rib_in, local_rib, registry, pings, local_asn })
|
||||
}
|
||||
}
|
||||
|
||||
impl TestServer {
|
||||
/// Every route in the server's table.
|
||||
pub fn received_routes(&self) -> Vec<ReceivedRoute> {
|
||||
let mut routes = Vec::new();
|
||||
self.rib.for_each_route(|route| {
|
||||
let (attributes, next_hop) = route.attrs.attributes().expect("stored attributes parse");
|
||||
routes.push(ReceivedRoute {
|
||||
prefix: route.prefix,
|
||||
peer: route.peer,
|
||||
path_id: route.path_id,
|
||||
attributes,
|
||||
next_hop,
|
||||
best: route.best,
|
||||
});
|
||||
});
|
||||
routes
|
||||
}
|
||||
|
||||
/// Follows the changes of best route from now on.
|
||||
pub fn route_changes(&self) -> RouteChanges {
|
||||
let mut changes = RouteChanges {
|
||||
reader: self.rib.reader(),
|
||||
pending: Default::default(),
|
||||
};
|
||||
// A new reader is first given the table as it is. That is not a
|
||||
// change.
|
||||
changes.read();
|
||||
changes.pending.clear();
|
||||
changes
|
||||
}
|
||||
|
||||
/// Waits for the server to stop by itself and returns why. A healthy
|
||||
/// server never does.
|
||||
pub async fn stopped(&mut self) -> Result<(), ServerError> {
|
||||
(&mut self.handle)
|
||||
.await
|
||||
.expect("the server task does not panic")
|
||||
}
|
||||
|
||||
/// Starts the HTTP API on an ephemeral loopback port and returns its
|
||||
/// base URL (e.g. `http://127.0.0.1:12345`). The API task is aborted
|
||||
/// when the returned guard drops.
|
||||
@@ -349,11 +104,8 @@ 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),
|
||||
adj_rib_in: self.adj_rib_in.clone(),
|
||||
pings: self.pings.clone(),
|
||||
};
|
||||
let handle = tokio::spawn(bgp_web::serve_on(state, listener));
|
||||
Ok((base_url, HttpGuard { handle }))
|
||||
@@ -362,7 +114,7 @@ impl TestServer {
|
||||
|
||||
/// Aborts the HTTP API task when dropped.
|
||||
pub struct HttpGuard {
|
||||
handle: JoinHandle<std::io::Result<()>>,
|
||||
handle: JoinHandle<Result<()>>,
|
||||
}
|
||||
|
||||
impl Drop for HttpGuard {
|
||||
|
||||
@@ -1,569 +0,0 @@
|
||||
//! 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::time::Duration;
|
||||
|
||||
use bgp_integration_tests::announcement::{Announcement, withdrawal};
|
||||
use bgp_integration_tests::test_peer::{TestPeer, collector_capabilities};
|
||||
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,
|
||||
};
|
||||
use bgp_packet::open::{AddPathEntry, Capability, OpenMessage, SendReceive};
|
||||
use bgp_primitives::constants::AfiSafi;
|
||||
use bgp_primitives::ip_prefix::Nlri;
|
||||
use bgp_rib::{LocalRoute, LocalRouteAttrs};
|
||||
use bgp_server::config::PeerConfig;
|
||||
use bgp_server::family::SUPPORTED;
|
||||
use bgp_server::policy::{ExportPolicy, ImportPolicy};
|
||||
use eyre::Result;
|
||||
use serde_json::Value;
|
||||
|
||||
const LOCAL_ASN: u32 = 65000;
|
||||
const PEER_ASN: u32 = 65001;
|
||||
|
||||
/// 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> {
|
||||
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 {
|
||||
add_path,
|
||||
import: ImportPolicy::permissive(),
|
||||
..peer_config("route-server-client", at, PEER_ASN)
|
||||
}];
|
||||
c.originate = originate;
|
||||
// Keep the dialer out of the way.
|
||||
c.connect_retry = Duration::from_secs(3600);
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// A session from a peer whose OPEN offers ADD-PATH as `offer` says, for
|
||||
/// IPv4 unicast. Returns the peer and the server's OPEN.
|
||||
async fn peer_offering(
|
||||
server: &TestServer,
|
||||
offer: Option<SendReceive>,
|
||||
) -> Result<(TestPeer, OpenMessage)> {
|
||||
let mut capabilities = vec![Capability::FourByteAsn { asn: PEER_ASN }];
|
||||
capabilities.extend(SUPPORTED.into_iter().map(Capability::multiprotocol));
|
||||
if let Some(send_recv) = offer {
|
||||
capabilities.push(Capability::AddPath {
|
||||
entries: vec![AddPathEntry {
|
||||
afi: AfiSafi::IPV4_UNICAST.afi,
|
||||
safi: AfiSafi::IPV4_UNICAST.safi,
|
||||
send_recv,
|
||||
}],
|
||||
});
|
||||
}
|
||||
let mut peer = TestPeer::connect(server.addr).await?;
|
||||
let open = peer
|
||||
.handshake_with(PEER_ASN, Ipv4Addr::new(10, 0, 0, 1), 90, capabilities)
|
||||
.await?;
|
||||
server.wait_for_session().await?;
|
||||
Ok((peer, open))
|
||||
}
|
||||
|
||||
fn add_path_offered(open: &OpenMessage) -> Vec<(AfiSafi, SendReceive)> {
|
||||
open.capabilities()
|
||||
.filter_map(|capability| match capability {
|
||||
Capability::AddPath { entries } => Some(entries),
|
||||
_ => None,
|
||||
})
|
||||
.flatten()
|
||||
.map(|entry| (entry.family(), entry.send_recv))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn announcement(nlri: Nlri, as_path: &[u32]) -> Announcement {
|
||||
Announcement::of([nlri], as_path)
|
||||
}
|
||||
|
||||
/// The routes the server holds, as (path identifier, AS path, best).
|
||||
fn held(server: &TestServer) -> Vec<(u32, Vec<u32>, bool)> {
|
||||
let mut held: Vec<(u32, Vec<u32>, bool)> = server
|
||||
.received_routes()
|
||||
.into_iter()
|
||||
.map(|route| {
|
||||
let path = route
|
||||
.attributes
|
||||
.iter()
|
||||
.find_map(|attribute| match attribute {
|
||||
PathAttribute::AsPath(path) => Some(path.segments[0].path.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or_default();
|
||||
(route.path_id, path, route.best)
|
||||
})
|
||||
.collect();
|
||||
held.sort();
|
||||
held
|
||||
}
|
||||
|
||||
/// A peer that sends several routes for one prefix: each is kept, the best
|
||||
/// 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 (base, _http) = server.serve_http().await?;
|
||||
let (mut peer, open) = peer_offering(&server, Some(SendReceive::Send)).await?;
|
||||
|
||||
// The server offered to receive, for every family it offers.
|
||||
assert_eq!(
|
||||
add_path_offered(&open),
|
||||
SUPPORTED.map(|family| (family, SendReceive::Receive))
|
||||
);
|
||||
// In use for the one family the peer sends for, and in that direction.
|
||||
let session = &server.registry.list()[0];
|
||||
assert_eq!(
|
||||
session.add_path_receive,
|
||||
[AfiSafi::IPV4_UNICAST].into_iter().collect()
|
||||
);
|
||||
assert!(session.add_path_send.is_empty());
|
||||
peer.send_add_path = vec![AfiSafi::IPV4_UNICAST];
|
||||
|
||||
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]))
|
||||
.await?;
|
||||
peer.announce(announcement(path(2), &[PEER_ASN, 64998]))
|
||||
.await?;
|
||||
server.wait_for_routes(2).await?;
|
||||
assert_eq!(
|
||||
held(&server),
|
||||
[
|
||||
(1, vec![PEER_ASN, 64999, 64998], false),
|
||||
(2, vec![PEER_ASN, 64998], true),
|
||||
]
|
||||
);
|
||||
assert_eq!(server.rib.prefix_count(), 1);
|
||||
|
||||
// The API tells them apart too.
|
||||
let routes: Value = reqwest::get(format!("{base}/api/routes/received"))
|
||||
.await?
|
||||
.json()
|
||||
.await?;
|
||||
let mut ids: Vec<u64> = routes["routes"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|route| route["path_id"].as_u64().unwrap())
|
||||
.collect();
|
||||
ids.sort_unstable();
|
||||
assert_eq!(ids, [1, 2]);
|
||||
let peers: Value = reqwest::get(format!("{base}/api/peers"))
|
||||
.await?
|
||||
.json()
|
||||
.await?;
|
||||
assert_eq!(
|
||||
peers[0]["add_path_receive"],
|
||||
serde_json::json!(["IPv4 unicast"])
|
||||
);
|
||||
|
||||
// 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?;
|
||||
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?;
|
||||
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]))
|
||||
.await?;
|
||||
server.wait_for_routes(2).await?;
|
||||
drop(peer);
|
||||
server.wait_for_routes(0).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// ADD-PATH changes how prefixes are written. It must be in use only when
|
||||
/// both sides said so; a peer that offers it to a server that does not is
|
||||
/// spoken to, and listened to, in plain BGP.
|
||||
#[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?;
|
||||
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?;
|
||||
assert_eq!(update.announced_routes, vec![prefix("10.100.0.0/16")]);
|
||||
peer.announce(announcement(prefix("203.0.113.0/24").into(), &[PEER_ASN]))
|
||||
.await?;
|
||||
server.wait_for_routes(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 (mut peer, open) = peer_offering(&server, None).await?;
|
||||
assert_eq!(add_path_offered(&open).len(), SUPPORTED.len());
|
||||
let update = peer.recv_update().await?;
|
||||
assert_eq!(update.announced_routes, vec![prefix("10.100.0.0/16")]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Where the peer offered to receive and the server to send, the server's
|
||||
/// prefixes carry a path identifier, and a peer that reads them so
|
||||
/// 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 session = &server.registry.list()[0];
|
||||
assert_eq!(
|
||||
session.add_path_send,
|
||||
[AfiSafi::IPV4_UNICAST].into_iter().collect()
|
||||
);
|
||||
assert!(session.add_path_receive.is_empty());
|
||||
|
||||
// Read as agreed: with identifiers.
|
||||
peer.ctx.add_path = vec![AfiSafi::IPV4_UNICAST];
|
||||
let update = 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]))
|
||||
.await?;
|
||||
server.wait_for_routes(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,36 +4,54 @@
|
||||
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_integration_tests::test_server::TestServer;
|
||||
use bgp_packet::message::{
|
||||
AsPathAttribute, CommunitiesPathAttribute, LargeCommunitiesPathAttribute, LargeCommunity,
|
||||
LocalPrefPathAttribute, NextHopPathAttribute, OriginPathAttribute, PathAttribute,
|
||||
UpdateMessage,
|
||||
NextHopPathAttribute, OriginPathAttribute, PathAttribute, UpdateMessage,
|
||||
};
|
||||
use bgp_rib::{LocalRoute, LocalRouteAttrs};
|
||||
use bgp_primitives::ip_prefix::IpPrefix;
|
||||
use bgp_rib::{LocalRibError, LocalRibPolicy, 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)
|
||||
LocalRoute::new(prefix(p), "alice", attrs)
|
||||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -49,9 +67,7 @@ async fn test_registry_tracks_session_lifecycle() -> Result<()> {
|
||||
let peer = established_peer(&server).await?;
|
||||
|
||||
let up = tokio::time::timeout(TIMEOUT, events.recv()).await??;
|
||||
let SessionEvent::Up(handle) = up else {
|
||||
panic!("expected Up, got {up:?}")
|
||||
};
|
||||
let SessionEvent::Up(handle) = up else { panic!("expected Up, got {up:?}") };
|
||||
assert_eq!(handle.peer.asn, PEER_ASN);
|
||||
assert_eq!(handle.local_addr, server.addr);
|
||||
assert!(handle.four_octet_asn);
|
||||
@@ -60,10 +76,7 @@ async fn test_registry_tracks_session_lifecycle() -> Result<()> {
|
||||
drop(peer);
|
||||
|
||||
let down = tokio::time::timeout(TIMEOUT, events.recv()).await??;
|
||||
assert!(
|
||||
matches!(down, SessionEvent::Down(p) if p == handle.peer),
|
||||
"got {down:?}"
|
||||
);
|
||||
assert!(matches!(down, SessionEvent::Down(p) if p == handle.peer), "got {down:?}");
|
||||
assert!(server.registry.list().is_empty());
|
||||
Ok(())
|
||||
}
|
||||
@@ -78,7 +91,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
|
||||
@@ -89,11 +102,7 @@ async fn test_announce_after_session_up() -> Result<()> {
|
||||
prepend: 0,
|
||||
med: None,
|
||||
communities: vec![(65000, 100), (65535, 65281)],
|
||||
large_communities: vec![LargeCommunity {
|
||||
global_admin: 65000,
|
||||
data_1: 7,
|
||||
data_2: 8,
|
||||
}],
|
||||
large_communities: vec![LargeCommunity { global_admin: 65000, data_1: 7, data_2: 8 }],
|
||||
},
|
||||
))
|
||||
.await?;
|
||||
@@ -107,16 +116,9 @@ async fn test_announce_after_session_up() -> Result<()> {
|
||||
PathAttribute::Origin(OriginPathAttribute::IGP),
|
||||
AsPathAttribute::from_asns(vec![LOCAL_ASN]),
|
||||
PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::LOCALHOST)),
|
||||
PathAttribute::Communities(CommunitiesPathAttribute(vec![
|
||||
(65000, 100),
|
||||
(65535, 65281)
|
||||
])),
|
||||
PathAttribute::Communities(CommunitiesPathAttribute(vec![(65000, 100), (65535, 65281)])),
|
||||
PathAttribute::LargeCommunities(LargeCommunitiesPathAttribute {
|
||||
communities: vec![LargeCommunity {
|
||||
global_admin: 65000,
|
||||
data_1: 7,
|
||||
data_2: 8
|
||||
}],
|
||||
communities: vec![LargeCommunity { global_admin: 65000, data_1: 7, data_2: 8 }],
|
||||
}),
|
||||
]
|
||||
);
|
||||
@@ -128,14 +130,8 @@ async fn test_announce_after_session_up() -> Result<()> {
|
||||
#[tokio::test]
|
||||
async fn test_routes_replayed_on_session_up() -> Result<()> {
|
||||
let server = TestServer::start(LOCAL_ASN).await?;
|
||||
server
|
||||
.local_rib
|
||||
.announce(route("10.100.1.0/24", LocalRouteAttrs::default()))
|
||||
.await?;
|
||||
server
|
||||
.local_rib
|
||||
.announce(route("10.100.2.0/24", LocalRouteAttrs::default()))
|
||||
.await?;
|
||||
server.local_rib.announce(route("10.100.1.0/24", LocalRouteAttrs::default())).await?;
|
||||
server.local_rib.announce(route("10.100.2.0/24", LocalRouteAttrs::default())).await?;
|
||||
|
||||
let mut peer = established_peer(&server).await?;
|
||||
|
||||
@@ -155,12 +151,9 @@ 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
|
||||
.announce(route("10.100.1.0/24", LocalRouteAttrs::default()))
|
||||
.await?;
|
||||
server.local_rib.announce(route("10.100.1.0/24", LocalRouteAttrs::default())).await?;
|
||||
|
||||
for peer in [&mut a, &mut b] {
|
||||
let update = next_update(peer).await?;
|
||||
@@ -173,15 +166,12 @@ 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
|
||||
.announce(route("10.100.1.0/24", LocalRouteAttrs::default()))
|
||||
.await?;
|
||||
server.local_rib.announce(route("10.100.1.0/24", LocalRouteAttrs::default())).await?;
|
||||
let _ = next_update(&mut peer).await?;
|
||||
|
||||
server.local_rib.withdraw(prefix("10.100.1.0/24")).await?;
|
||||
server.local_rib.withdraw(prefix("10.100.1.0/24"), Some("alice")).await?;
|
||||
let update = next_update(&mut peer).await?;
|
||||
assert_eq!(update.withdrawn_routes, vec![prefix("10.100.1.0/24")]);
|
||||
assert!(update.announced_routes.is_empty());
|
||||
@@ -194,41 +184,26 @@ 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
|
||||
.announce(route("10.100.1.0/24", LocalRouteAttrs::default()))
|
||||
.await?;
|
||||
server.local_rib.announce(route("10.100.1.0/24", LocalRouteAttrs::default())).await?;
|
||||
let first = next_update(&mut peer).await?;
|
||||
assert_eq!(
|
||||
first.path_attributes[1],
|
||||
AsPathAttribute::from_asns(vec![LOCAL_ASN])
|
||||
);
|
||||
assert_eq!(first.path_attributes[1], AsPathAttribute::from_asns(vec![LOCAL_ASN]));
|
||||
|
||||
server
|
||||
.local_rib
|
||||
.announce(route(
|
||||
"10.100.1.0/24",
|
||||
LocalRouteAttrs {
|
||||
prepend: 3,
|
||||
med: Some(42),
|
||||
..LocalRouteAttrs::default()
|
||||
},
|
||||
LocalRouteAttrs { prepend: 3, med: Some(42), ..LocalRouteAttrs::default() },
|
||||
))
|
||||
.await?;
|
||||
let second = next_update(&mut peer).await?;
|
||||
assert_eq!(second.announced_routes, vec![prefix("10.100.1.0/24")]);
|
||||
assert_eq!(
|
||||
second.path_attributes[1],
|
||||
AsPathAttribute::from_asns(vec![LOCAL_ASN; 4])
|
||||
);
|
||||
assert!(
|
||||
second
|
||||
.path_attributes
|
||||
.iter()
|
||||
.any(|a| matches!(a, PathAttribute::MultiExitDisc(m) if m.0 == 42))
|
||||
);
|
||||
assert_eq!(second.path_attributes[1], AsPathAttribute::from_asns(vec![LOCAL_ASN; 4]));
|
||||
assert!(second
|
||||
.path_attributes
|
||||
.iter()
|
||||
.any(|a| matches!(a, PathAttribute::MultiExitDisc(m) if m.0 == 42)));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -237,14 +212,10 @@ async fn test_reannounce_with_prepend_and_med() -> Result<()> {
|
||||
async fn test_keepalives_continue_alongside_updates() -> Result<()> {
|
||||
let server = TestServer::start_with_hold_time(LOCAL_ASN, 3).await?;
|
||||
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?;
|
||||
peer.handshake(PEER_ASN, Ipv4Addr::new(10, 0, 0, 1), 3).await?;
|
||||
wait_for_sessions(&server, 1).await?;
|
||||
|
||||
server
|
||||
.local_rib
|
||||
.announce(route("10.100.1.0/24", LocalRouteAttrs::default()))
|
||||
.await?;
|
||||
server.local_rib.announce(route("10.100.1.0/24", LocalRouteAttrs::default())).await?;
|
||||
|
||||
let mut saw_update = false;
|
||||
let mut saw_keepalive = false;
|
||||
@@ -267,34 +238,39 @@ async fn test_keepalives_continue_alongside_updates() -> Result<()> {
|
||||
#[tokio::test]
|
||||
async fn test_received_routes_removed_when_session_closes() -> Result<()> {
|
||||
let server = TestServer::start(LOCAL_ASN).await?;
|
||||
let mut rib_events = server.route_changes();
|
||||
let mut rib_events = server.adj_rib_in.subscribe();
|
||||
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"), prefix("198.51.100.0/24")],
|
||||
}))
|
||||
.await?;
|
||||
for _ in 0..2 {
|
||||
let ev = tokio::time::timeout(TIMEOUT, rib_events.recv()).await??;
|
||||
assert!(matches!(ev, RouteChange::Announced { .. }), "{ev:?}");
|
||||
assert!(matches!(ev, bgp_rib::RibEvent::Announced { .. }), "{ev:?}");
|
||||
}
|
||||
assert_eq!(server.received_routes().len(), 2);
|
||||
assert_eq!(server.adj_rib_in.snapshot().await.len(), 2);
|
||||
|
||||
drop(peer);
|
||||
|
||||
let mut withdrawn = vec![];
|
||||
for _ in 0..2 {
|
||||
match tokio::time::timeout(TIMEOUT, rib_events.recv()).await?? {
|
||||
RouteChange::Withdrawn { prefix, .. } => withdrawn.push(prefix.to_string()),
|
||||
bgp_rib::RibEvent::Withdrawn { prefix, .. } => withdrawn.push(prefix.to_string()),
|
||||
other => panic!("expected Withdrawn, got {other:?}"),
|
||||
}
|
||||
}
|
||||
withdrawn.sort();
|
||||
assert_eq!(withdrawn, vec!["198.51.100.0/24", "203.0.113.0/24"]);
|
||||
assert!(server.received_routes().is_empty());
|
||||
assert!(server.adj_rib_in.snapshot().await.is_empty());
|
||||
|
||||
// Withdrawals precede the Down event.
|
||||
let _up = tokio::time::timeout(TIMEOUT, session_events.recv()).await??;
|
||||
@@ -322,16 +298,10 @@ 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
|
||||
.announce(route("10.100.1.0/24", LocalRouteAttrs::default()))
|
||||
.await?;
|
||||
server
|
||||
.local_rib
|
||||
.announce(route("10.100.2.0/24", LocalRouteAttrs::default()))
|
||||
.await?;
|
||||
server.local_rib.announce(route("10.100.1.0/24", LocalRouteAttrs::default())).await?;
|
||||
server.local_rib.announce(route("10.100.2.0/24", LocalRouteAttrs::default())).await?;
|
||||
let _ = next_update(&mut peer).await?;
|
||||
let _ = next_update(&mut peer).await?;
|
||||
|
||||
@@ -344,11 +314,7 @@ async fn test_route_refresh_resends_local_routes() -> Result<()> {
|
||||
.collect::<Vec<_>>();
|
||||
resent.sort();
|
||||
assert_eq!(resent, vec!["10.100.1.0/24", "10.100.2.0/24"]);
|
||||
assert_eq!(
|
||||
server.registry.list().len(),
|
||||
1,
|
||||
"session must survive a route refresh"
|
||||
);
|
||||
assert_eq!(server.registry.list().len(), 1, "session must survive a route refresh");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -357,108 +323,58 @@ 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?;
|
||||
assert!(
|
||||
matches!(notif.error, BgpError::Cease(CeaseSubcode::AdminShutdown)),
|
||||
"{notif:?}"
|
||||
);
|
||||
assert!(peer.closed_within(TIMEOUT).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.is_closed().await);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Routes from the configuration
|
||||
// Policy
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Routes listed under `originate` are in the table before any peer
|
||||
/// connects, and each session is sent them when it comes up.
|
||||
#[tokio::test]
|
||||
async fn test_configured_routes_are_announced_to_every_new_session() -> Result<()> {
|
||||
let attrs = LocalRouteAttrs {
|
||||
med: Some(7),
|
||||
communities: vec![(65000, 100)],
|
||||
..Default::default()
|
||||
};
|
||||
let configured = vec![
|
||||
route("10.100.1.0/24", attrs),
|
||||
route("10.100.2.0/24", LocalRouteAttrs::default()),
|
||||
];
|
||||
let server = TestServer::start_with(LOCAL_ASN, |c| c.originate = configured).await?;
|
||||
assert_eq!(server.local_rib.snapshot().await.unwrap().len(), 2);
|
||||
async fn test_policy_rejects_prefix_outside_allowlist() -> Result<()> {
|
||||
let server = TestServer::start_with_policy(
|
||||
LOCAL_ASN,
|
||||
LocalRibPolicy { allowed_prefixes: vec![prefix("10.100.0.0/16")], max_routes_per_owner: 2 },
|
||||
)
|
||||
.await?;
|
||||
let mut peer = established_peer(&server).await?;
|
||||
wait_for_sessions(&server, 1).await?;
|
||||
|
||||
for router_id in [Ipv4Addr::new(10, 0, 0, 1), Ipv4Addr::new(10, 0, 0, 2)] {
|
||||
let mut peer = established_peer_with_id(&server, router_id).await?;
|
||||
let mut seen = [next_update(&mut peer).await?, next_update(&mut peer).await?];
|
||||
seen.sort_by_key(|u| u.announced_routes[0].to_string());
|
||||
assert_eq!(seen[0].announced_routes, vec![prefix("10.100.1.0/24")]);
|
||||
assert!(
|
||||
seen[0]
|
||||
.path_attributes
|
||||
.contains(&PathAttribute::Communities(CommunitiesPathAttribute(vec![
|
||||
(65000, 100)
|
||||
])))
|
||||
);
|
||||
assert_eq!(seen[1].announced_routes, vec![prefix("10.100.2.0/24")]);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A peer in our own AS is sent a route we originate with an empty AS path
|
||||
/// and a LOCAL_PREF, whatever prepending is configured: our AS number goes
|
||||
/// on the path only as the route leaves the AS. It used to be sent as to
|
||||
/// an external peer, which an internal one refuses.
|
||||
#[tokio::test]
|
||||
async fn test_a_peer_in_our_own_as_gets_an_empty_path_and_a_local_pref() -> Result<()> {
|
||||
let server = TestServer::start(LOCAL_ASN).await?;
|
||||
let mut peer = TestPeer::connect(server.addr).await?;
|
||||
peer.handshake(LOCAL_ASN, Ipv4Addr::new(10, 0, 0, 1), 90)
|
||||
.await?;
|
||||
// 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?;
|
||||
assert!(server.registry.list()[0].ibgp);
|
||||
|
||||
server
|
||||
let err = server
|
||||
.local_rib
|
||||
.announce(route(
|
||||
"10.100.1.0/24",
|
||||
LocalRouteAttrs {
|
||||
prepend: 2,
|
||||
..LocalRouteAttrs::default()
|
||||
},
|
||||
))
|
||||
.await?;
|
||||
.announce(route("192.0.2.0/24", LocalRouteAttrs::default()))
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert_eq!(err, LocalRibError::NotAllowed(prefix("192.0.2.0/24")));
|
||||
|
||||
// An allowed prefix still goes through on the same session.
|
||||
server.local_rib.announce(route("10.100.9.0/24", LocalRouteAttrs::default())).await?;
|
||||
let update = next_update(&mut peer).await?;
|
||||
assert_eq!(update.announced_routes, vec![prefix("10.100.1.0/24")]);
|
||||
assert!(
|
||||
update
|
||||
.path_attributes
|
||||
.contains(&PathAttribute::AsPath(AsPathAttribute { segments: vec![] })),
|
||||
"{:?}",
|
||||
update.path_attributes
|
||||
);
|
||||
assert!(
|
||||
update
|
||||
.path_attributes
|
||||
.contains(&PathAttribute::LocalPref(LocalPrefPathAttribute(100))),
|
||||
"{:?}",
|
||||
update.path_attributes
|
||||
);
|
||||
assert_eq!(update.announced_routes, vec![prefix("10.100.9.0/24")]);
|
||||
Ok(())
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -2,40 +2,17 @@ use std::net::Ipv4Addr;
|
||||
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::errors::{BgpError, CeaseSubcode};
|
||||
use bgp_packet::message::{
|
||||
AsPathAttribute, Message, MpReachNlriPathAttribute, MpUnreachNlriPathAttribute,
|
||||
NextHopPathAttribute, NlriNextHop, NotificationMessage, OriginPathAttribute, PathAttribute,
|
||||
UpdateMessage,
|
||||
Message, MpReachNlriPathAttribute, MpUnreachNlriPathAttribute, NlriNextHop,
|
||||
NotificationMessage, PathAttribute, UpdateMessage,
|
||||
};
|
||||
use bgp_primitives::constants::{AfiSafi, IpVersion};
|
||||
use bgp_primitives::constants::{AddressFamilyId, SubsequentAfi};
|
||||
use bgp_primitives::ip_prefix::IpPrefix;
|
||||
use bgp_rib::RibEvent;
|
||||
use eyre::Result;
|
||||
|
||||
/// 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> {
|
||||
let mut attributes = with_origin_and_as_path(PathAttribute::NextHop(NextHopPathAttribute(
|
||||
Ipv4Addr::new(192, 0, 2, 1),
|
||||
)));
|
||||
// In order of type code: ORIGIN, AS_PATH, NEXT_HOP.
|
||||
attributes.rotate_left(1);
|
||||
attributes
|
||||
}
|
||||
|
||||
/// `first` followed by the attributes every announcement must carry.
|
||||
/// Routes in MP_REACH_NLRI have their next hop there.
|
||||
fn with_origin_and_as_path(first: PathAttribute) -> Vec<PathAttribute> {
|
||||
vec![
|
||||
first,
|
||||
PathAttribute::Origin(OriginPathAttribute::IGP),
|
||||
AsPathAttribute::from_asns(vec![65001]),
|
||||
]
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Connectivity
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -97,130 +74,6 @@ async fn test_bad_marker_does_not_crash_server() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 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];
|
||||
buf.extend_from_slice(&length.to_be_bytes());
|
||||
buf.push(msg_type);
|
||||
buf.extend_from_slice(body);
|
||||
buf
|
||||
}
|
||||
|
||||
/// What the peer sent decides the NOTIFICATION it gets back (RFC 4271 §6),
|
||||
/// in every state, and the session ends.
|
||||
#[tokio::test]
|
||||
async fn test_malformed_messages_are_answered_with_the_matching_notification() -> Result<()> {
|
||||
let server = TestServer::start(65000).await?;
|
||||
let header = |subcode| BgpError::MsgHeader(subcode);
|
||||
let mut bad_marker = framed(19, 4, &[]);
|
||||
bad_marker[0] = 0;
|
||||
|
||||
let cases: Vec<(&str, Vec<u8>, BgpError, Vec<u8>)> = vec![
|
||||
(
|
||||
"length beyond the maximum",
|
||||
framed(5000, 2, &[]),
|
||||
header(MsgHeaderSubcode::BadMessageLength),
|
||||
5000u16.to_be_bytes().to_vec(),
|
||||
),
|
||||
(
|
||||
"length below a header",
|
||||
framed(5, 4, &[]),
|
||||
header(MsgHeaderSubcode::BadMessageLength),
|
||||
vec![0, 5],
|
||||
),
|
||||
(
|
||||
"unknown message type",
|
||||
framed(19, 9, &[]),
|
||||
header(MsgHeaderSubcode::BadMessageType),
|
||||
vec![9],
|
||||
),
|
||||
(
|
||||
"marker not all ones",
|
||||
bad_marker,
|
||||
header(MsgHeaderSubcode::ConnNotSynchronized),
|
||||
vec![],
|
||||
),
|
||||
(
|
||||
"UPDATE with attributes running past its end",
|
||||
framed(23, 2, &[0, 0, 0, 40]),
|
||||
BgpError::UpdateMsg(UpdateMsgSubcode::MalformedAttrs),
|
||||
vec![],
|
||||
),
|
||||
];
|
||||
|
||||
for (i, (what, bytes, error, data)) in cases.into_iter().enumerate() {
|
||||
// Established: a distinct router ID each time, so that a session
|
||||
// still closing does not collide with the next one.
|
||||
let mut peer = TestPeer::connect(server.addr).await?;
|
||||
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?;
|
||||
assert_eq!(
|
||||
(notification.error, notification.data),
|
||||
(error.clone(), data.clone()),
|
||||
"{what}, Established"
|
||||
);
|
||||
|
||||
// OpenSent: the same bytes instead of an OPEN.
|
||||
let mut peer = TestPeer::connect(server.addr).await?;
|
||||
let _ = peer.recv_open().await?;
|
||||
peer.send_raw(&bytes).await?;
|
||||
let notification = peer.expect_notification().await?;
|
||||
assert_eq!(
|
||||
(notification.error, notification.data),
|
||||
(error, data),
|
||||
"{what}, OpenSent"
|
||||
);
|
||||
}
|
||||
|
||||
// A well-formed message at the wrong time is an FSM error, not a
|
||||
// message error.
|
||||
let mut peer = TestPeer::connect(server.addr).await?;
|
||||
let _ = peer.recv_open().await?;
|
||||
peer.send(&Message::KeepAlive).await?;
|
||||
let notification = peer.expect_notification().await?;
|
||||
assert_eq!(
|
||||
notification.error,
|
||||
BgpError::FsmError(FsmSubcode::UnexpectedOpenSent)
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A message may arrive in pieces, with the server's own timers firing in
|
||||
/// between. The server once threw away the piece it had already read when
|
||||
/// that happened, so the message was silently lost (and the stream was only
|
||||
/// back in step if the remainder happened to be thrown away as well).
|
||||
#[tokio::test]
|
||||
async fn test_message_split_across_a_keepalive_tick_is_still_one_message() -> Result<()> {
|
||||
// Hold time 3 s: the server sends a KEEPALIVE every second.
|
||||
let server = TestServer::start_with_hold_time(65000, 3).await?;
|
||||
let mut peer = TestPeer::connect(server.addr).await?;
|
||||
peer.handshake(65001, Ipv4Addr::new(10, 0, 0, 1), 3).await?;
|
||||
|
||||
let prefix = IpPrefix::new(IpVersion::Ipv4, vec![10], 8)?;
|
||||
let mut update = bytes::BytesMut::new();
|
||||
Message::Update(UpdateMessage {
|
||||
withdrawn_routes: vec![],
|
||||
path_attributes: mandatory_attributes(),
|
||||
announced_routes: vec![prefix.into()],
|
||||
})
|
||||
.to_wire(&peer.ctx, &mut update)?;
|
||||
|
||||
// The first half, a pause long enough for the server's keepalive timer
|
||||
// to fire, then the rest.
|
||||
peer.send_raw(&update[..10]).await?;
|
||||
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)
|
||||
})
|
||||
.await?;
|
||||
assert_eq!(server.registry.list().len(), 1, "session was dropped");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// OpenConfirm → Established
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -248,7 +101,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 +121,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 +154,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(())
|
||||
}
|
||||
|
||||
@@ -310,9 +173,9 @@ async fn test_server_accepts_update() -> Result<()> {
|
||||
|
||||
let update = Message::Update(UpdateMessage {
|
||||
withdrawn_routes: vec![],
|
||||
path_attributes: mandatory_attributes(),
|
||||
path_attributes: vec![],
|
||||
// 10.0.0.0/8 — one byte of prefix for a /8.
|
||||
announced_routes: vec![IpPrefix::new(IpVersion::Ipv4, vec![10], 8)?.into()],
|
||||
announced_routes: vec![IpPrefix::new(AddressFamilyId::Ipv4, vec![10], 8)?],
|
||||
});
|
||||
peer.send(&update).await?;
|
||||
|
||||
@@ -330,16 +193,15 @@ async fn test_server_accepts_ipv6_update() -> Result<()> {
|
||||
.await?;
|
||||
|
||||
// 2001:db8::/32
|
||||
let prefix = IpPrefix::new(IpVersion::Ipv6, vec![0x20, 0x01, 0x0d, 0xb8], 32)?;
|
||||
let prefix = IpPrefix::new(AddressFamilyId::Ipv6, vec![0x20, 0x01, 0x0d, 0xb8], 32)?;
|
||||
let update = Message::Update(UpdateMessage {
|
||||
withdrawn_routes: vec![],
|
||||
path_attributes: with_origin_and_as_path(PathAttribute::MpReachNlri(
|
||||
MpReachNlriPathAttribute {
|
||||
family: AfiSafi::IPV6_UNICAST,
|
||||
next_hop: NlriNextHop::Ipv6("2001:db8::1".parse().unwrap()),
|
||||
prefixes: vec![prefix.into()],
|
||||
},
|
||||
)),
|
||||
path_attributes: vec![PathAttribute::MpReachNlri(MpReachNlriPathAttribute {
|
||||
afi: AddressFamilyId::Ipv6,
|
||||
safi: SubsequentAfi::Unicast,
|
||||
next_hop: NlriNextHop::Ipv6("2001:db8::1".parse().unwrap()),
|
||||
prefixes: vec![prefix],
|
||||
})],
|
||||
announced_routes: vec![],
|
||||
});
|
||||
peer.send(&update).await?;
|
||||
@@ -357,27 +219,26 @@ async fn test_server_accepts_ipv6_update() -> Result<()> {
|
||||
#[tokio::test]
|
||||
async fn test_rib_records_ipv4_announce() -> Result<()> {
|
||||
let server = TestServer::start(65000).await?;
|
||||
let mut events = server.route_changes();
|
||||
let mut events = server.adj_rib_in.subscribe();
|
||||
|
||||
let mut peer = TestPeer::connect(server.addr).await?;
|
||||
peer.handshake(65001, Ipv4Addr::new(10, 0, 0, 1), 90)
|
||||
.await?;
|
||||
peer.handshake(65001, Ipv4Addr::new(10, 0, 0, 1), 90).await?;
|
||||
|
||||
let prefix = IpPrefix::new(IpVersion::Ipv4, vec![10], 8)?;
|
||||
let prefix = IpPrefix::new(AddressFamilyId::Ipv4, vec![10], 8)?;
|
||||
peer.send(&Message::Update(UpdateMessage {
|
||||
withdrawn_routes: vec![],
|
||||
path_attributes: mandatory_attributes(),
|
||||
announced_routes: vec![prefix.into()],
|
||||
path_attributes: vec![],
|
||||
announced_routes: vec![prefix.clone()],
|
||||
}))
|
||||
.await?;
|
||||
|
||||
let event = tokio::time::timeout(Duration::from_secs(1), events.recv()).await??;
|
||||
assert!(
|
||||
matches!(&event, RouteChange::Announced { prefix: p, .. } if p == &prefix),
|
||||
matches!(&event, RibEvent::Announced { prefix: p, .. } if p == &prefix),
|
||||
"Expected Announced for 10.0.0.0/8, got {event:?}",
|
||||
);
|
||||
|
||||
let snapshot = server.received_routes();
|
||||
let snapshot = server.adj_rib_in.snapshot().await;
|
||||
assert!(
|
||||
snapshot.iter().any(|e| e.prefix == prefix),
|
||||
"10.0.0.0/8 missing from RIB snapshot",
|
||||
@@ -389,26 +250,25 @@ async fn test_rib_records_ipv4_announce() -> Result<()> {
|
||||
#[tokio::test]
|
||||
async fn test_rib_records_ipv4_withdraw() -> Result<()> {
|
||||
let server = TestServer::start(65000).await?;
|
||||
let mut events = server.route_changes();
|
||||
let mut events = server.adj_rib_in.subscribe();
|
||||
|
||||
let mut peer = TestPeer::connect(server.addr).await?;
|
||||
peer.handshake(65001, Ipv4Addr::new(10, 0, 0, 1), 90)
|
||||
.await?;
|
||||
peer.handshake(65001, Ipv4Addr::new(10, 0, 0, 1), 90).await?;
|
||||
|
||||
let prefix = IpPrefix::new(IpVersion::Ipv4, vec![10], 8)?;
|
||||
let prefix = IpPrefix::new(AddressFamilyId::Ipv4, vec![10], 8)?;
|
||||
|
||||
// Announce first.
|
||||
peer.send(&Message::Update(UpdateMessage {
|
||||
withdrawn_routes: vec![],
|
||||
path_attributes: mandatory_attributes(),
|
||||
announced_routes: vec![prefix.into()],
|
||||
path_attributes: vec![],
|
||||
announced_routes: vec![prefix.clone()],
|
||||
}))
|
||||
.await?;
|
||||
tokio::time::timeout(Duration::from_secs(1), events.recv()).await??;
|
||||
|
||||
// Now withdraw.
|
||||
peer.send(&Message::Update(UpdateMessage {
|
||||
withdrawn_routes: vec![prefix.into()],
|
||||
withdrawn_routes: vec![prefix.clone()],
|
||||
path_attributes: vec![],
|
||||
announced_routes: vec![],
|
||||
}))
|
||||
@@ -416,11 +276,11 @@ async fn test_rib_records_ipv4_withdraw() -> Result<()> {
|
||||
|
||||
let event = tokio::time::timeout(Duration::from_secs(1), events.recv()).await??;
|
||||
assert!(
|
||||
matches!(&event, RouteChange::Withdrawn { prefix: p, .. } if p == &prefix),
|
||||
matches!(&event, RibEvent::Withdrawn { prefix: p, .. } if p == &prefix),
|
||||
"Expected Withdrawn for 10.0.0.0/8, got {event:?}",
|
||||
);
|
||||
|
||||
let snapshot = server.received_routes();
|
||||
let snapshot = server.adj_rib_in.snapshot().await;
|
||||
assert!(
|
||||
!snapshot.iter().any(|e| e.prefix == prefix),
|
||||
"10.0.0.0/8 should have been removed from RIB snapshot",
|
||||
@@ -432,33 +292,31 @@ async fn test_rib_records_ipv4_withdraw() -> Result<()> {
|
||||
#[tokio::test]
|
||||
async fn test_rib_records_ipv6_announce() -> Result<()> {
|
||||
let server = TestServer::start(65000).await?;
|
||||
let mut events = server.route_changes();
|
||||
let mut events = server.adj_rib_in.subscribe();
|
||||
|
||||
let mut peer = TestPeer::connect(server.addr).await?;
|
||||
peer.handshake(65001, Ipv4Addr::new(10, 0, 0, 1), 90)
|
||||
.await?;
|
||||
peer.handshake(65001, Ipv4Addr::new(10, 0, 0, 1), 90).await?;
|
||||
|
||||
let prefix = IpPrefix::new(IpVersion::Ipv6, vec![0x20, 0x01, 0x0d, 0xb8], 32)?;
|
||||
let prefix = IpPrefix::new(AddressFamilyId::Ipv6, vec![0x20, 0x01, 0x0d, 0xb8], 32)?;
|
||||
peer.send(&Message::Update(UpdateMessage {
|
||||
withdrawn_routes: vec![],
|
||||
path_attributes: with_origin_and_as_path(PathAttribute::MpReachNlri(
|
||||
MpReachNlriPathAttribute {
|
||||
family: AfiSafi::IPV6_UNICAST,
|
||||
next_hop: NlriNextHop::Ipv6("2001:db8::1".parse().unwrap()),
|
||||
prefixes: vec![prefix.into()],
|
||||
},
|
||||
)),
|
||||
path_attributes: vec![PathAttribute::MpReachNlri(MpReachNlriPathAttribute {
|
||||
afi: AddressFamilyId::Ipv6,
|
||||
safi: SubsequentAfi::Unicast,
|
||||
next_hop: NlriNextHop::Ipv6("2001:db8::1".parse().unwrap()),
|
||||
prefixes: vec![prefix.clone()],
|
||||
})],
|
||||
announced_routes: vec![],
|
||||
}))
|
||||
.await?;
|
||||
|
||||
let event = tokio::time::timeout(Duration::from_secs(1), events.recv()).await??;
|
||||
assert!(
|
||||
matches!(&event, RouteChange::Announced { prefix: p, .. } if p == &prefix),
|
||||
matches!(&event, RibEvent::Announced { prefix: p, .. } if p == &prefix),
|
||||
"Expected Announced for 2001:db8::/32, got {event:?}",
|
||||
);
|
||||
|
||||
let snapshot = server.received_routes();
|
||||
let snapshot = server.adj_rib_in.snapshot().await;
|
||||
assert!(
|
||||
snapshot.iter().any(|e| e.prefix == prefix),
|
||||
"2001:db8::/32 missing from RIB snapshot",
|
||||
@@ -470,24 +328,22 @@ async fn test_rib_records_ipv6_announce() -> Result<()> {
|
||||
#[tokio::test]
|
||||
async fn test_rib_records_ipv6_withdraw() -> Result<()> {
|
||||
let server = TestServer::start(65000).await?;
|
||||
let mut events = server.route_changes();
|
||||
let mut events = server.adj_rib_in.subscribe();
|
||||
|
||||
let mut peer = TestPeer::connect(server.addr).await?;
|
||||
peer.handshake(65001, Ipv4Addr::new(10, 0, 0, 1), 90)
|
||||
.await?;
|
||||
peer.handshake(65001, Ipv4Addr::new(10, 0, 0, 1), 90).await?;
|
||||
|
||||
let prefix = IpPrefix::new(IpVersion::Ipv6, vec![0x20, 0x01, 0x0d, 0xb8], 32)?;
|
||||
let prefix = IpPrefix::new(AddressFamilyId::Ipv6, vec![0x20, 0x01, 0x0d, 0xb8], 32)?;
|
||||
|
||||
// Announce first.
|
||||
peer.send(&Message::Update(UpdateMessage {
|
||||
withdrawn_routes: vec![],
|
||||
path_attributes: with_origin_and_as_path(PathAttribute::MpReachNlri(
|
||||
MpReachNlriPathAttribute {
|
||||
family: AfiSafi::IPV6_UNICAST,
|
||||
next_hop: NlriNextHop::Ipv6("2001:db8::1".parse().unwrap()),
|
||||
prefixes: vec![prefix.into()],
|
||||
},
|
||||
)),
|
||||
path_attributes: vec![PathAttribute::MpReachNlri(MpReachNlriPathAttribute {
|
||||
afi: AddressFamilyId::Ipv6,
|
||||
safi: SubsequentAfi::Unicast,
|
||||
next_hop: NlriNextHop::Ipv6("2001:db8::1".parse().unwrap()),
|
||||
prefixes: vec![prefix.clone()],
|
||||
})],
|
||||
announced_routes: vec![],
|
||||
}))
|
||||
.await?;
|
||||
@@ -497,8 +353,9 @@ async fn test_rib_records_ipv6_withdraw() -> Result<()> {
|
||||
peer.send(&Message::Update(UpdateMessage {
|
||||
withdrawn_routes: vec![],
|
||||
path_attributes: vec![PathAttribute::MpUnreachNlri(MpUnreachNlriPathAttribute {
|
||||
family: AfiSafi::IPV6_UNICAST,
|
||||
prefixes: vec![prefix.into()],
|
||||
afi: AddressFamilyId::Ipv6,
|
||||
safi: SubsequentAfi::Unicast,
|
||||
prefixes: vec![prefix.clone()],
|
||||
})],
|
||||
announced_routes: vec![],
|
||||
}))
|
||||
@@ -506,11 +363,11 @@ async fn test_rib_records_ipv6_withdraw() -> Result<()> {
|
||||
|
||||
let event = tokio::time::timeout(Duration::from_secs(1), events.recv()).await??;
|
||||
assert!(
|
||||
matches!(&event, RouteChange::Withdrawn { prefix: p, .. } if p == &prefix),
|
||||
matches!(&event, RibEvent::Withdrawn { prefix: p, .. } if p == &prefix),
|
||||
"Expected Withdrawn for 2001:db8::/32, got {event:?}",
|
||||
);
|
||||
|
||||
let snapshot = server.received_routes();
|
||||
let snapshot = server.adj_rib_in.snapshot().await;
|
||||
assert!(
|
||||
!snapshot.iter().any(|e| e.prefix == prefix),
|
||||
"2001:db8::/32 should have been removed from RIB snapshot",
|
||||
|
||||
@@ -1,210 +0,0 @@
|
||||
//! End-to-end tests of route selection: routes for one prefix from several
|
||||
//! peers, which of them the server chooses, and how that is shown.
|
||||
|
||||
use std::net::Ipv4Addr;
|
||||
|
||||
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_primitives::ip_prefix::{IpPrefix, Nlri};
|
||||
use eyre::Result;
|
||||
use serde_json::Value;
|
||||
|
||||
const LOCAL_ASN: u32 = 65000;
|
||||
|
||||
/// 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
|
||||
}
|
||||
|
||||
fn announcement(prefixes: &[IpPrefix], as_path: &[u32]) -> Announcement {
|
||||
Announcement::of(Nlri::plain(prefixes.iter().copied()), as_path)
|
||||
}
|
||||
|
||||
fn withdrawal(prefixes: &[IpPrefix]) -> UpdateMessage {
|
||||
withdrawal_of(Nlri::plain(prefixes.iter().copied()))
|
||||
}
|
||||
|
||||
/// The AS path of the route the server holds as best for `prefix`.
|
||||
fn best_path(server: &TestServer, prefix: &IpPrefix) -> Option<Vec<u32>> {
|
||||
let best = server
|
||||
.received_routes()
|
||||
.into_iter()
|
||||
.find(|route| route.prefix == *prefix && route.best)?;
|
||||
best.attributes
|
||||
.iter()
|
||||
.find_map(|attribute| match attribute {
|
||||
PathAttribute::AsPath(path) => Some(path.segments[0].path.clone()),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Two peers offer the same prefix. The route with the shorter AS path is
|
||||
/// chosen; when it goes, the other is.
|
||||
#[tokio::test]
|
||||
async fn test_the_best_route_is_chosen_and_replaced_when_it_goes() -> Result<()> {
|
||||
let server = TestServer::start(LOCAL_ASN).await?;
|
||||
let (base, _http) = server.serve_http().await?;
|
||||
let client = reqwest::Client::new();
|
||||
let mut changes = server.route_changes();
|
||||
let p = prefix("203.0.113.0/24");
|
||||
|
||||
let mut far = peer(&server, 1).await?;
|
||||
far.announce(announcement(&[p], &[65001, 64999, 64998]))
|
||||
.await?;
|
||||
server.wait_for_routes(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?;
|
||||
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 best: Vec<&Value> = routes
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.filter(|route| route["best"] == true)
|
||||
.collect();
|
||||
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?;
|
||||
assert_eq!(best_path(&server, &p), Some(vec![65001, 64999, 64998]));
|
||||
|
||||
far.announce(withdrawal(&[p])).await?;
|
||||
server.wait_for_routes(0).await?;
|
||||
assert_eq!(best_path(&server, &p), None);
|
||||
|
||||
// A follower of the best routes is told of each change: the first
|
||||
// route, the better one, back to the first, none.
|
||||
let mut seen = Vec::new();
|
||||
while seen.last() != Some(&RouteChange::Withdrawn { prefix: p }) {
|
||||
seen.push(tokio::time::timeout(TIMEOUT, changes.recv()).await??);
|
||||
}
|
||||
assert!(
|
||||
seen.iter()
|
||||
.all(|change| matches!(change, RouteChange::Announced { prefix, .. } | RouteChange::Withdrawn { prefix } if *prefix == p)),
|
||||
"{seen:?}"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The event stream reports changes of best route, and only those.
|
||||
#[tokio::test]
|
||||
async fn test_changes_of_best_route_are_on_the_event_stream() -> Result<()> {
|
||||
let server = TestServer::start(LOCAL_ASN).await?;
|
||||
let (base, _http) = server.serve_http().await?;
|
||||
let p = prefix("203.0.113.0/24");
|
||||
|
||||
// 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]))
|
||||
.await?;
|
||||
server.wait_for_routes(1).await?;
|
||||
|
||||
let mut stream = reqwest::Client::new()
|
||||
.get(format!("{base}/api/events"))
|
||||
.send()
|
||||
.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?;
|
||||
|
||||
let mut text = String::new();
|
||||
let mut events: Vec<Value> = Vec::new();
|
||||
tokio::time::timeout(TIMEOUT, async {
|
||||
while !events.iter().any(|event| event["type"] == "best_withdrawn") {
|
||||
let chunk = stream
|
||||
.chunk()
|
||||
.await?
|
||||
.ok_or_else(|| eyre::eyre!("stream ended"))?;
|
||||
text.push_str(std::str::from_utf8(&chunk)?);
|
||||
events = text
|
||||
.lines()
|
||||
.filter_map(|line| line.strip_prefix("data:"))
|
||||
.filter_map(|data| serde_json::from_str(data.trim()).ok())
|
||||
.collect();
|
||||
}
|
||||
Ok::<_, eyre::Error>(())
|
||||
})
|
||||
.await??;
|
||||
|
||||
let about_routes: Vec<&Value> = events
|
||||
.iter()
|
||||
.filter(|event| {
|
||||
event["type"]
|
||||
.as_str()
|
||||
.is_some_and(|t| t.starts_with("best_"))
|
||||
})
|
||||
.collect();
|
||||
// The announcement and the withdrawal may have been read as one change
|
||||
// or as two; either way nothing is said of the route that was there
|
||||
// from the start, and the last word is that the prefix is gone.
|
||||
assert!(about_routes.len() <= 2, "{about_routes:?}");
|
||||
for event in &about_routes {
|
||||
let named = event["prefix"]
|
||||
.as_str()
|
||||
.or(event["route"]["prefix"].as_str());
|
||||
assert_eq!(named, Some("203.0.113.0/24"), "{event}");
|
||||
}
|
||||
assert_eq!(about_routes.last().unwrap()["type"], "best_withdrawn");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The table can be far too large to send whole: the API gives a part of
|
||||
/// it, and says how much there is.
|
||||
#[tokio::test]
|
||||
async fn test_received_routes_are_returned_up_to_a_limit() -> Result<()> {
|
||||
let server = TestServer::start(LOCAL_ASN).await?;
|
||||
let (base, _http) = server.serve_http().await?;
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
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?;
|
||||
|
||||
let count = |page: Value| page["routes"].as_array().unwrap().len();
|
||||
let url = format!("{base}/api/routes/received");
|
||||
assert_eq!(count(get_json(&client, &url).await?), 40);
|
||||
assert_eq!(
|
||||
count(get_json(&client, &format!("{url}?limit=7")).await?),
|
||||
7
|
||||
);
|
||||
let status = get_json(&client, &format!("{base}/api/status")).await?;
|
||||
assert_eq!(status["received_routes"], 40);
|
||||
Ok(())
|
||||
}
|
||||
@@ -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,37 @@
|
||||
//! 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 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: "lab".into(), endpoint, remote_asn: PEER_ASN, hold_time_secs: 90, local_address: None }];
|
||||
})
|
||||
.await
|
||||
}
|
||||
@@ -44,61 +48,78 @@ 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,
|
||||
BgpError::Cease(CeaseSubcode::ConnCollisionResolution)
|
||||
)
|
||||
matches!(n.error, BgpError::Cease(CeaseSubcode::ConnCollisionResolution))
|
||||
}
|
||||
|
||||
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"));
|
||||
assert_eq!(session.name.as_deref(), Some("lab"));
|
||||
|
||||
// The survivor is fully functional.
|
||||
server
|
||||
.local_rib
|
||||
.announce(LocalRoute::new(
|
||||
prefix("10.100.1.0/24"),
|
||||
LocalRouteAttrs::default(),
|
||||
))
|
||||
.announce(LocalRoute::new(IpPrefix::try_from("10.100.1.0/24")?, "alice", 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.
|
||||
let extra = tokio::time::timeout(Duration::from_millis(700), listener.accept()).await;
|
||||
assert!(
|
||||
extra.is_err(),
|
||||
"dialer opened a second connection despite an established session"
|
||||
);
|
||||
assert!(extra.is_err(), "dialer opened a second connection despite an established session");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -106,20 +127,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 +151,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,23 +179,20 @@ 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.
|
||||
assert_eq!(server.registry.list().len(), 1);
|
||||
server
|
||||
.local_rib
|
||||
.announce(LocalRoute::new(
|
||||
prefix("10.100.1.0/24"),
|
||||
LocalRouteAttrs::default(),
|
||||
))
|
||||
.announce(LocalRoute::new(IpPrefix::try_from("10.100.1.0/24")?, "alice", 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(())
|
||||
}
|
||||
@@ -180,37 +202,48 @@ async fn test_established_session_wins_by_default() -> Result<()> {
|
||||
/// old session are flushed.
|
||||
#[tokio::test]
|
||||
async fn test_restarted_peer_replaces_stale_session_when_enabled() -> Result<()> {
|
||||
let server =
|
||||
TestServer::start_with(LOCAL_ASN, |c| c.collision_detect_established_state = true).await?;
|
||||
let server = TestServer::start_with(LOCAL_ASN, |c| c.collision_detect_established_state = true).await?;
|
||||
|
||||
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")?],
|
||||
}))
|
||||
.await?;
|
||||
server.wait_for_routes(1).await?;
|
||||
tokio::time::timeout(TIMEOUT, async {
|
||||
while server.adj_rib_in.snapshot().await.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!(
|
||||
server.received_routes().is_empty(),
|
||||
"stale routes were not flushed"
|
||||
);
|
||||
assert!(server.adj_rib_in.snapshot().await.is_empty(), "stale routes were not flushed");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -222,7 +255,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 +265,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,10 +281,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?;
|
||||
assert!(
|
||||
matches!(notif.error, BgpError::OpenMsg(OpenMsgSubcode::BadBgpId)),
|
||||
"{notif:?}"
|
||||
);
|
||||
let notif = expect_notification(&mut peer).await?;
|
||||
assert!(matches!(notif.error, BgpError::OpenMsg(OpenMsgSubcode::BadBgpId)), "{notif:?}");
|
||||
Ok(())
|
||||
}
|
||||
@@ -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,120 +5,134 @@ 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::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 "lab".
|
||||
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: "lab".into(), endpoint, remote_asn, hold_time_secs: 90, local_address: 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?;
|
||||
assert_eq!(listed.name.as_deref(), Some("r1"));
|
||||
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("lab"));
|
||||
assert_eq!(listed.peer.asn, PEER_ASN);
|
||||
assert_eq!(listed.peer.addr, listener.local_addr()?);
|
||||
|
||||
// Configured view reflects it; the dialer is not in backoff.
|
||||
let (cfg, state) = server
|
||||
.registry
|
||||
.configured_peers()
|
||||
.into_iter()
|
||||
.next()
|
||||
.unwrap();
|
||||
assert_eq!(cfg.name, "r1");
|
||||
assert!(
|
||||
matches!(state, DialState::Connecting | DialState::Idle),
|
||||
"{state:?}"
|
||||
);
|
||||
let (cfg, state) = server.registry.configured_peers().into_iter().next().unwrap();
|
||||
assert_eq!(cfg.name, "lab");
|
||||
assert!(matches!(state, DialState::Connecting | DialState::Idle), "{state:?}");
|
||||
|
||||
// Local routes flow over the dialled session too.
|
||||
server
|
||||
.local_rib
|
||||
.announce(LocalRoute::new(
|
||||
prefix("10.100.1.0/24"),
|
||||
LocalRouteAttrs::default(),
|
||||
))
|
||||
.announce(LocalRoute::new(IpPrefix::try_from("10.100.1.0/24")?, "alice", 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?;
|
||||
let (_, state) = server
|
||||
.registry
|
||||
.configured_peers()
|
||||
.into_iter()
|
||||
.next()
|
||||
.unwrap();
|
||||
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().into_iter().next().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?;
|
||||
assert!(
|
||||
matches!(notif.error, BgpError::OpenMsg(OpenMsgSubcode::BadPeerAs)),
|
||||
"{notif:?}"
|
||||
);
|
||||
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,29 +144,36 @@ 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: "lab".into(),
|
||||
endpoint: SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 1),
|
||||
remote_asn: PEER_ASN,
|
||||
hold_time_secs: 90,
|
||||
local_address: 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?;
|
||||
assert!(
|
||||
matches!(&msg, Message::Notification(n) if matches!(n.error, BgpError::OpenMsg(OpenMsgSubcode::BadPeerAs))),
|
||||
"{msg:?}"
|
||||
);
|
||||
let msg = tokio::time::timeout(TIMEOUT, bad.recv()).await??;
|
||||
assert!(matches!(&msg, Message::Notification(n) if matches!(n.error, BgpError::OpenMsg(OpenMsgSubcode::BadPeerAs))), "{msg:?}");
|
||||
|
||||
// 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?;
|
||||
assert_eq!(listed.name.as_deref(), Some("r1"));
|
||||
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("lab"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -161,124 +182,43 @@ 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: "lab".into(),
|
||||
endpoint,
|
||||
remote_asn: PEER_ASN,
|
||||
hold_time_secs: 90,
|
||||
local_address: Some(Ipv4Addr::LOCALHOST.into()),
|
||||
}];
|
||||
})
|
||||
.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: "lab".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)
|
||||
}];
|
||||
})
|
||||
.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?;
|
||||
assert!(reason.contains("local_address 192.0.2.1"), "{reason}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A peer that takes the connection and then refuses the session, here
|
||||
/// because it is not the AS we are configured to expect, will refuse it
|
||||
/// again. It is dialled ever more slowly, like a peer that cannot be
|
||||
/// reached: connecting used to count as success and reset the wait, so
|
||||
/// 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 mut waits = Vec::new();
|
||||
for _ in 0..3 {
|
||||
let mut peer = TestPeer::accept(&listener).await?;
|
||||
peer.send_open(65099, PEER_ID, 90).await?;
|
||||
let _ = peer.recv_open().await?;
|
||||
let refusal = peer.recv().await?;
|
||||
assert!(
|
||||
matches!(&refusal, Message::Notification(n) if n.error == BgpError::OpenMsg(OpenMsgSubcode::BadPeerAs)),
|
||||
"{refusal:?}"
|
||||
);
|
||||
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 })) => {
|
||||
assert!(last_error.contains("AS65099"), "{last_error}");
|
||||
Some(
|
||||
until
|
||||
.duration_since(std::time::SystemTime::now())
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
waits.push(wait);
|
||||
}
|
||||
// 200 ms, then 400, then 800, less what passed before each was read.
|
||||
assert!(
|
||||
waits[2] > Duration::from_millis(500),
|
||||
"the waits were {waits:?}"
|
||||
);
|
||||
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(())
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
//! What the server does when a part of it that it cannot do without fails.
|
||||
//!
|
||||
//! The local RIB's actor stops only by panicking. Before, nothing noticed:
|
||||
//! sessions stayed up, nothing was announced any more, and the status page
|
||||
//! showed an empty table. Now the server tells its peers it is going and
|
||||
//! stops with an error, for whatever supervises it to restart it.
|
||||
|
||||
use std::net::Ipv4Addr;
|
||||
|
||||
use bgp_integration_tests::TIMEOUT;
|
||||
use bgp_integration_tests::test_server::TestServer;
|
||||
use bgp_packet::errors::{BgpError, CeaseSubcode};
|
||||
use bgp_rib::RibStopped;
|
||||
use bgp_server::error::ServerError;
|
||||
use eyre::Result;
|
||||
|
||||
const LOCAL_ASN: u32 = 65000;
|
||||
const PEER_ASN: u32 = 65001;
|
||||
|
||||
#[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?;
|
||||
|
||||
server.local_rib.stop_for_testing();
|
||||
|
||||
let outcome = tokio::time::timeout(TIMEOUT, server.stopped()).await?;
|
||||
assert!(
|
||||
matches!(outcome, Err(ServerError::RibStopped(RibStopped { rib })) if rib == "local RIB"),
|
||||
"{outcome:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
peer.expect_notification().await?.error,
|
||||
BgpError::Cease(CeaseSubcode::AdminShutdown)
|
||||
);
|
||||
|
||||
// The status interface says the table cannot be read, where it used to
|
||||
// show an empty one.
|
||||
let client = reqwest::Client::new();
|
||||
for path in ["/api/status", "/api/routes/local", "/metricz"] {
|
||||
let response = client.get(format!("{base}{path}")).send().await?;
|
||||
assert_eq!(response.status(), 503, "{path}");
|
||||
assert_eq!(
|
||||
response.text().await?,
|
||||
"the local RIB has stopped",
|
||||
"{path}"
|
||||
);
|
||||
}
|
||||
// What does not need that table still answers.
|
||||
let response = client
|
||||
.get(format!("{base}/api/routes/received"))
|
||||
.send()
|
||||
.await?;
|
||||
assert_eq!(response.status(), 200);
|
||||
Ok(())
|
||||
}
|
||||
@@ -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,40 @@
|
||||
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_rib::{LocalRoute, LocalRouteAttrs};
|
||||
use bgp_packet::message::{
|
||||
AsPathAttribute, CommunitiesPathAttribute, Message, NextHopPathAttribute,
|
||||
OriginPathAttribute, PathAttribute, UpdateMessage,
|
||||
};
|
||||
use bgp_primitives::ip_prefix::IpPrefix;
|
||||
use bgp_primitives::parser::ParserContext;
|
||||
use bgp_rib::LocalRibPolicy;
|
||||
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]
|
||||
@@ -48,99 +66,138 @@ async fn test_status_and_peers() -> Result<()> {
|
||||
assert_eq!(peers[0]["peer_asn"], PEER_ASN);
|
||||
assert_eq!(peers[0]["local_addr"], server.addr.to_string());
|
||||
assert_eq!(peers[0]["four_octet_asn"], true);
|
||||
assert_eq!(
|
||||
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(())
|
||||
}
|
||||
|
||||
/// Originated routes are listed with their attributes as text.
|
||||
/// The full loop: POST an announcement, the BGP peer receives the UPDATE,
|
||||
/// the local table lists it, the wire view matches the bytes, DELETE
|
||||
/// withdraws it.
|
||||
#[tokio::test]
|
||||
async fn test_originated_routes_are_listed() -> Result<()> {
|
||||
let attrs = LocalRouteAttrs {
|
||||
prepend: 1,
|
||||
med: Some(10),
|
||||
communities: vec![(65000, 100), (65535, 65281)],
|
||||
large_communities: vec![LargeCommunity {
|
||||
global_admin: 65000,
|
||||
data_1: 1,
|
||||
data_2: 2,
|
||||
}],
|
||||
..Default::default()
|
||||
};
|
||||
let server = TestServer::start_with(LOCAL_ASN, |c| {
|
||||
c.originate = vec![LocalRoute::new(prefix("10.100.1.0/24"), attrs)];
|
||||
})
|
||||
.await?;
|
||||
let (base, _http) = server.serve_http().await?;
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let local = get_json(&client, &format!("{base}/api/routes/local")).await?;
|
||||
assert_eq!(local.as_array().unwrap().len(), 1);
|
||||
assert_eq!(local[0]["prefix"], "10.100.1.0/24");
|
||||
assert_eq!(local[0]["origin"], "IGP");
|
||||
assert_eq!(local[0]["prepend"], 1);
|
||||
assert_eq!(local[0]["med"], 10);
|
||||
assert_eq!(local[0]["communities"], json!(["65000:100", "65535:65281"]));
|
||||
assert_eq!(local[0]["large_communities"], json!(["65000:1:2"]));
|
||||
assert_eq!(
|
||||
get_json(&client, &format!("{base}/api/status")).await?["local_routes"],
|
||||
1
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The API only reports. Nothing reachable over HTTP changes what the
|
||||
/// router announces.
|
||||
#[tokio::test]
|
||||
async fn test_api_is_read_only() -> Result<()> {
|
||||
async fn test_announce_over_http_reaches_peer() -> Result<()> {
|
||||
let server = TestServer::start(LOCAL_ASN).await?;
|
||||
let (base, _http) = server.serve_http().await?;
|
||||
let client = reqwest::Client::new();
|
||||
let mut peer = established_peer(&server).await?;
|
||||
|
||||
let body = json!({ "prefix": "10.100.1.0/24" });
|
||||
for path in [
|
||||
"/api/routes/local",
|
||||
"/api/routes/received",
|
||||
"/api/peers",
|
||||
"/api/status",
|
||||
] {
|
||||
let post = client
|
||||
.post(format!("{base}{path}"))
|
||||
.json(&body)
|
||||
.send()
|
||||
.await?;
|
||||
assert_eq!(post.status(), 405, "POST {path}");
|
||||
let delete = client
|
||||
.delete(format!("{base}{path}"))
|
||||
.json(&body)
|
||||
.send()
|
||||
.await?;
|
||||
assert_eq!(delete.status(), 405, "DELETE {path}");
|
||||
}
|
||||
assert!(server.local_rib.snapshot().await.unwrap().is_empty());
|
||||
let resp = client
|
||||
.post(format!("{base}/api/routes/local"))
|
||||
.json(&json!({
|
||||
"prefix": "10.100.1.0/24",
|
||||
"owner": "alice",
|
||||
"prepend": 1,
|
||||
"med": 10,
|
||||
"communities": ["65000:100", "NO_EXPORT"],
|
||||
"large_communities": ["65000:1:2"],
|
||||
}))
|
||||
.send()
|
||||
.await?;
|
||||
assert_eq!(resp.status(), 200, "{}", resp.text().await?);
|
||||
let body: Value = resp.json().await?;
|
||||
assert_eq!(body["prefix"], "10.100.1.0/24");
|
||||
assert_eq!(body["origin"], "IGP");
|
||||
assert_eq!(body["communities"], json!(["65000:100", "65535:65281"]));
|
||||
assert_eq!(body["large_communities"], json!(["65000:1:2"]));
|
||||
|
||||
// The peer sees the UPDATE.
|
||||
let update: UpdateMessage = tokio::time::timeout(TIMEOUT, peer.recv_update()).await??;
|
||||
assert_eq!(update.announced_routes, vec![prefix("10.100.1.0/24")]);
|
||||
assert_eq!(update.path_attributes[1], AsPathAttribute::from_asns(vec![LOCAL_ASN, LOCAL_ASN]));
|
||||
assert_eq!(update.path_attributes[2], PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::LOCALHOST)));
|
||||
assert!(update.path_attributes.contains(&PathAttribute::Communities(CommunitiesPathAttribute(vec![
|
||||
(65000, 100),
|
||||
(65535, 65281)
|
||||
]))));
|
||||
|
||||
// Local table lists it.
|
||||
let local = get_json(&client, &format!("{base}/api/routes/local")).await?;
|
||||
assert_eq!(local.as_array().unwrap().len(), 1);
|
||||
assert_eq!(local[0]["owner"], "alice");
|
||||
|
||||
// Wire view reproduces exactly what went over the socket.
|
||||
let wire = get_json(&client, &format!("{base}/api/routes/local/wire?prefix=10.100.1.0/24")).await?;
|
||||
assert_eq!(wire.as_array().unwrap().len(), 1);
|
||||
assert_eq!(wire[0]["peer_asn"], PEER_ASN);
|
||||
assert_eq!(wire[0]["four_octet_asn"], true);
|
||||
assert_eq!(wire[0]["summary"]["as_path"], json!([LOCAL_ASN, LOCAL_ASN]));
|
||||
let hex = wire[0]["hex"].as_str().unwrap();
|
||||
let ctx = ParserContext { four_octet_asn: peer.ctx.four_octet_asn, address_family: None };
|
||||
let mut expected = bytes::BytesMut::new();
|
||||
Message::Update(update).to_wire(&ctx, &mut expected)?;
|
||||
let expected_hex: String = expected.iter().map(|b| format!("{b:02x}")).collect();
|
||||
assert_eq!(hex, expected_hex);
|
||||
assert!(hex.starts_with(&"ff".repeat(16)), "marker missing");
|
||||
|
||||
// Withdraw.
|
||||
let resp = client
|
||||
.delete(format!("{base}/api/routes/local"))
|
||||
.json(&json!({ "prefix": "10.100.1.0/24", "owner": "alice" }))
|
||||
.send()
|
||||
.await?;
|
||||
assert_eq!(resp.status(), 200);
|
||||
let withdrawn: UpdateMessage = tokio::time::timeout(TIMEOUT, peer.recv_update()).await??;
|
||||
assert_eq!(withdrawn.withdrawn_routes, vec![prefix("10.100.1.0/24")]);
|
||||
|
||||
let local = get_json(&client, &format!("{base}/api/routes/local")).await?;
|
||||
assert!(local.as_array().unwrap().is_empty());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_validation_and_policy_status_codes() -> Result<()> {
|
||||
let server = TestServer::start_with_policy(
|
||||
LOCAL_ASN,
|
||||
LocalRibPolicy { allowed_prefixes: vec![prefix("10.100.0.0/16")], max_routes_per_owner: 1 },
|
||||
)
|
||||
.await?;
|
||||
let (base, _http) = server.serve_http().await?;
|
||||
let client = reqwest::Client::new();
|
||||
let url = format!("{base}/api/routes/local");
|
||||
|
||||
let post = |body: Value| client.post(&url).json(&body).send();
|
||||
|
||||
// Malformed input → 400 with an error message.
|
||||
let resp = post(json!({ "prefix": "not-a-prefix", "owner": "alice" })).await?;
|
||||
assert_eq!(resp.status(), 400);
|
||||
assert!(resp.json::<Value>().await?["error"].as_str().unwrap().contains("invalid prefix"));
|
||||
|
||||
let resp = post(json!({ "prefix": "10.100.1.0/24", "owner": "" })).await?;
|
||||
assert_eq!(resp.status(), 400);
|
||||
|
||||
let resp = post(json!({ "prefix": "10.100.1.0/24", "owner": "alice", "communities": ["bogus"] })).await?;
|
||||
assert_eq!(resp.status(), 400);
|
||||
|
||||
let resp = post(json!({ "prefix": "10.100.1.0/24", "owner": "alice", "origin": "weird" })).await?;
|
||||
assert_eq!(resp.status(), 400);
|
||||
|
||||
// Outside allowlist → 403.
|
||||
let resp = post(json!({ "prefix": "192.0.2.0/24", "owner": "alice" })).await?;
|
||||
assert_eq!(resp.status(), 403);
|
||||
|
||||
// First allowed route is fine.
|
||||
assert_eq!(post(json!({ "prefix": "10.100.1.0/24", "owner": "alice" })).await?.status(), 200);
|
||||
|
||||
// Per-owner limit → 422.
|
||||
let resp = post(json!({ "prefix": "10.100.2.0/24", "owner": "alice" })).await?;
|
||||
assert_eq!(resp.status(), 422);
|
||||
|
||||
// Someone else's prefix → 409, for both replace and withdraw.
|
||||
let resp = post(json!({ "prefix": "10.100.1.0/24", "owner": "bob" })).await?;
|
||||
assert_eq!(resp.status(), 409);
|
||||
let resp = client
|
||||
.delete(&url)
|
||||
.json(&json!({ "prefix": "10.100.1.0/24", "owner": "bob" }))
|
||||
.send()
|
||||
.await?;
|
||||
assert_eq!(resp.status(), 409);
|
||||
|
||||
// Withdrawing an unknown prefix is a 200 with null.
|
||||
let resp = client.delete(&url).json(&json!({ "prefix": "10.100.7.0/24" })).send().await?;
|
||||
assert_eq!(resp.status(), 200);
|
||||
assert_eq!(resp.json::<Value>().await?, Value::Null);
|
||||
|
||||
// Wire view for an unannounced prefix → 404.
|
||||
let resp = client.get(format!("{base}/api/routes/local/wire?prefix=10.100.7.0/24")).send().await?;
|
||||
assert_eq!(resp.status(), 404);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -152,17 +209,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")],
|
||||
}))
|
||||
.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);
|
||||
}
|
||||
@@ -185,21 +246,13 @@ async fn test_event_stream() -> Result<()> {
|
||||
let (base, _http) = server.serve_http().await?;
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let mut resp = client
|
||||
.get(format!("{base}/api/events"))
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?;
|
||||
assert!(
|
||||
resp.headers()["content-type"]
|
||||
.to_str()?
|
||||
.starts_with("text/event-stream")
|
||||
);
|
||||
let mut resp = client.get(format!("{base}/api/events")).send().await?.error_for_status()?;
|
||||
assert!(resp.headers()["content-type"].to_str()?.starts_with("text/event-stream"));
|
||||
|
||||
let _peer = established_peer(&server).await?;
|
||||
server
|
||||
.local_rib
|
||||
.announce(LocalRoute::new(prefix("10.100.1.0/24"), Default::default()))
|
||||
.announce(bgp_rib::LocalRoute::new(prefix("10.100.1.0/24"), "alice", Default::default()))
|
||||
.await?;
|
||||
|
||||
// Collect SSE frames until we have seen both events.
|
||||
@@ -208,18 +261,13 @@ async fn test_event_stream() -> Result<()> {
|
||||
let mut seen_announce = false;
|
||||
tokio::time::timeout(TIMEOUT, async {
|
||||
while !(seen_up && seen_announce) {
|
||||
let chunk = resp
|
||||
.chunk()
|
||||
.await?
|
||||
.ok_or_else(|| eyre::eyre!("stream ended"))?;
|
||||
let chunk = resp.chunk().await?.ok_or_else(|| eyre::eyre!("stream ended"))?;
|
||||
buf.push_str(std::str::from_utf8(&chunk)?);
|
||||
for line in buf.lines().filter_map(|l| l.strip_prefix("data:")) {
|
||||
let ev: Value = serde_json::from_str(line.trim())?;
|
||||
match ev["type"].as_str() {
|
||||
Some("session_up") if ev["peer"]["peer_asn"] == PEER_ASN => seen_up = true,
|
||||
Some("local_announced") if ev["route"]["prefix"] == "10.100.1.0/24" => {
|
||||
seen_announce = true
|
||||
}
|
||||
Some("local_announced") if ev["route"]["prefix"] == "10.100.1.0/24" => seen_announce = true,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -238,19 +286,13 @@ async fn test_statusz_page_is_served() -> Result<()> {
|
||||
|
||||
let resp = client.get(format!("{base}/statusz")).send().await?;
|
||||
assert_eq!(resp.status(), 200);
|
||||
assert!(
|
||||
resp.headers()["content-type"]
|
||||
.to_str()?
|
||||
.starts_with("text/html")
|
||||
);
|
||||
assert!(resp.headers()["content-type"].to_str()?.starts_with("text/html"));
|
||||
let body = resp.text().await?;
|
||||
assert!(body.contains("<title>Status for ferrix_bgp</title>"));
|
||||
assert!(body.contains("/api/events"));
|
||||
|
||||
// The root redirects to the page.
|
||||
let no_redirect = reqwest::Client::builder()
|
||||
.redirect(reqwest::redirect::Policy::none())
|
||||
.build()?;
|
||||
let no_redirect = reqwest::Client::builder().redirect(reqwest::redirect::Policy::none()).build()?;
|
||||
let resp = no_redirect.get(format!("{base}/")).send().await?;
|
||||
assert_eq!(resp.status(), 307);
|
||||
assert_eq!(resp.headers()["location"], "/statusz");
|
||||
@@ -258,19 +300,13 @@ async fn test_statusz_page_is_served() -> Result<()> {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_metricz() -> Result<()> {
|
||||
async fn test_metricz_and_well_known() -> Result<()> {
|
||||
let server = TestServer::start(LOCAL_ASN).await?;
|
||||
let (base, _http) = server.serve_http().await?;
|
||||
let client = reqwest::Client::new();
|
||||
let _peer = established_peer(&server).await?;
|
||||
|
||||
let text = client
|
||||
.get(format!("{base}/metricz"))
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?
|
||||
.text()
|
||||
.await?;
|
||||
let text = client.get(format!("{base}/metricz")).send().await?.error_for_status()?.text().await?;
|
||||
// Metrics are process-global, so other tests in this binary contribute
|
||||
// too; assert on presence and a lower bound rather than exact values.
|
||||
let metric = |name: &str| -> Option<i64> {
|
||||
@@ -278,19 +314,13 @@ async fn test_metricz() -> Result<()> {
|
||||
.find(|l| l.starts_with(name) && l[name.len()..].starts_with(' '))
|
||||
.and_then(|l| l.rsplit(' ').next()?.parse().ok())
|
||||
};
|
||||
assert!(
|
||||
metric("bgp_sessions_established").unwrap_or(0) >= 1,
|
||||
"{text}"
|
||||
);
|
||||
assert!(
|
||||
metric("bgp_messages_sent_total{type=\"open\"}").unwrap_or(0) >= 1,
|
||||
"{text}"
|
||||
);
|
||||
assert!(
|
||||
metric("bgp_messages_received_total{type=\"keepalive\"}").unwrap_or(0) >= 1,
|
||||
"{text}"
|
||||
);
|
||||
assert!(metric("bgp_sessions_established").unwrap_or(0) >= 1, "{text}");
|
||||
assert!(metric("bgp_messages_sent_total{type=\"open\"}").unwrap_or(0) >= 1, "{text}");
|
||||
assert!(metric("bgp_messages_received_total{type=\"keepalive\"}").unwrap_or(0) >= 1, "{text}");
|
||||
assert!(metric("bgp_local_routes").is_some(), "{text}");
|
||||
assert!(metric("bgp_rib_routes").is_some(), "{text}");
|
||||
assert!(metric("bgp_adj_rib_in_routes").is_some(), "{text}");
|
||||
|
||||
let wk = get_json(&client, &format!("{base}/api/communities/well-known")).await?;
|
||||
assert!(wk.as_array().unwrap().iter().any(|c| c["name"] == "NO_EXPORT" && c["value"] == "65535:65281"));
|
||||
Ok(())
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -7,11 +7,13 @@ use bgp_integration_tests::test_peer::TestPeer;
|
||||
use bgp_integration_tests::test_server::TestServer;
|
||||
use bgp_packet::message::{Message, PathAttribute};
|
||||
use bgp_packet::open::{Capability, OpenOption};
|
||||
use bgp_primitives::constants::{Afi, AfiSafi, Safi};
|
||||
use bgp_primitives::constants::{AddressFamilyId, SubsequentAfi};
|
||||
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
|
||||
@@ -59,9 +61,9 @@ async fn test_server_advertises_ipv4_and_ipv6_unicast() -> Result<()> {
|
||||
OpenOption::Capabilities(c) => c.iter(),
|
||||
})
|
||||
.collect();
|
||||
for afi in [Afi::IPV4, Afi::IPV6] {
|
||||
for afi in [AddressFamilyId::Ipv4, AddressFamilyId::Ipv6] {
|
||||
assert!(
|
||||
caps.iter().any(|c| matches!(c, Capability::MultiProtocol { afi: a, safi: Safi::UNICAST } if *a == afi)),
|
||||
caps.iter().any(|c| matches!(c, Capability::MultiProtocol { afi: a, safi: SubsequentAfi::Unicast } if *a == afi)),
|
||||
"missing MP capability for {afi:?}: {caps:?}"
|
||||
);
|
||||
}
|
||||
@@ -75,68 +77,43 @@ 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?;
|
||||
assert_eq!(
|
||||
session.peer.asn, 210036,
|
||||
"4-octet ASN must come from the capability, not AS_TRANS"
|
||||
);
|
||||
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");
|
||||
assert!(session.four_octet_asn);
|
||||
|
||||
// End-of-RIB must neither break the session nor create a route.
|
||||
peer.send_raw(IPV6_END_OF_RIB).await?;
|
||||
peer.send(&Message::KeepAlive).await?;
|
||||
tokio::time::sleep(Duration::from_millis(200)).await;
|
||||
assert_eq!(
|
||||
server.registry.list().len(),
|
||||
1,
|
||||
"session dropped after End-of-RIB"
|
||||
);
|
||||
assert!(server.received_routes().is_empty());
|
||||
assert_eq!(server.registry.list().len(), 1, "session dropped after End-of-RIB");
|
||||
assert!(server.adj_rib_in.snapshot().await.is_empty());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Over an IPv4 test transport an IPv6 route cannot be sent (no IPv6 next
|
||||
/// hop), so this only checks the builder the advertiser uses: the UPDATE for
|
||||
/// an IPv6 prefix carries MP_REACH_NLRI with an IPv6 next hop and no NEXT_HOP.
|
||||
/// a lab /48 carries MP_REACH_NLRI with an IPv6 next hop and no NEXT_HOP.
|
||||
#[tokio::test]
|
||||
async fn test_ipv6_prefix_update_shape() -> Result<()> {
|
||||
let route = LocalRoute::new(
|
||||
IpPrefix::try_from("2001:db8:1::/48")?,
|
||||
LocalRouteAttrs::default(),
|
||||
);
|
||||
async fn test_ipv6_lab_prefix_update_shape() -> Result<()> {
|
||||
let route = LocalRoute::new(IpPrefix::try_from("2a0d:d741:1::/48")?, "student1", LocalRouteAttrs::default());
|
||||
let nh: std::net::Ipv6Addr = "fd00:179::2".parse()?;
|
||||
let update = bgp_server::advertise::build_announce(
|
||||
&route,
|
||||
&bgp_server::advertise::Towards {
|
||||
local_asn: 4200000001,
|
||||
next_hop: nh.into(),
|
||||
four_octet_asn: true,
|
||||
ibgp: false,
|
||||
role: None,
|
||||
extended_next_hop: false,
|
||||
},
|
||||
)
|
||||
.expect("ipv6 over ipv6");
|
||||
let update = bgp_server::advertise::build_announce(&route, 4200000001, nh.into()).expect("ipv6 over ipv6");
|
||||
assert!(update.announced_routes.is_empty());
|
||||
assert!(
|
||||
!update
|
||||
.path_attributes
|
||||
.iter()
|
||||
.any(|a| matches!(a, PathAttribute::NextHop(_)))
|
||||
);
|
||||
assert!(
|
||||
update
|
||||
.path_attributes
|
||||
.iter()
|
||||
.any(|a| matches!(a, PathAttribute::MpReachNlri(m)
|
||||
if m.family == AfiSafi::IPV6_UNICAST && m.prefixes[0].to_string() == "2001:db8:1::/48"))
|
||||
);
|
||||
assert!(!update.path_attributes.iter().any(|a| matches!(a, PathAttribute::NextHop(_))));
|
||||
assert!(update.path_attributes.iter().any(|a| matches!(a, PathAttribute::MpReachNlri(m)
|
||||
if m.afi == AddressFamilyId::Ipv6 && m.prefixes[0].to_string() == "2a0d:d741:1::/48")));
|
||||
let _ = Ipv4Addr::LOCALHOST;
|
||||
Ok(())
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -1,279 +0,0 @@
|
||||
//! End-to-end tests for capability negotiation: what the two OPENs agree on
|
||||
//! decides which routes are sent and accepted, and how they are encoded.
|
||||
|
||||
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_packet::errors::{BgpError, OpenMsgSubcode};
|
||||
use bgp_packet::message::{
|
||||
AsPathAttribute, Message, MpReachNlriPathAttribute, NlriNextHop, PathAttribute, UpdateMessage,
|
||||
};
|
||||
use bgp_packet::open::Capability;
|
||||
use bgp_primitives::constants::{Afi, AfiSafi, Safi};
|
||||
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);
|
||||
|
||||
fn only(family: AfiSafi) -> FamilySet {
|
||||
[family].into_iter().collect()
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
.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()
|
||||
}
|
||||
|
||||
/// The session carries the families both OPENs offered, and the server's
|
||||
/// OPEN offers every family by default.
|
||||
#[tokio::test]
|
||||
async fn test_families_are_the_ones_both_sides_offered() -> Result<()> {
|
||||
let server = TestServer::start(LOCAL_ASN).await?;
|
||||
let mut peer = TestPeer::connect(server.addr).await?;
|
||||
let open = peer
|
||||
.handshake_with(
|
||||
PEER_ASN,
|
||||
PEER_ID,
|
||||
90,
|
||||
vec![
|
||||
four_octet(),
|
||||
Capability::multiprotocol(AfiSafi::IPV4_UNICAST),
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
|
||||
for family in SUPPORTED {
|
||||
assert!(
|
||||
open.capabilities()
|
||||
.any(|c| *c == Capability::multiprotocol(family)),
|
||||
"the server's OPEN offers {family}"
|
||||
);
|
||||
}
|
||||
let session = server.wait_for_session().await?;
|
||||
assert_eq!(session.families, only(AfiSafi::IPV4_UNICAST));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A route of a family the peer did not offer is not accepted from it. The
|
||||
/// rest of the UPDATE is, and the session stays up.
|
||||
#[tokio::test]
|
||||
async fn test_routes_of_a_family_not_negotiated_are_not_accepted() -> Result<()> {
|
||||
let server = TestServer::start(LOCAL_ASN).await?;
|
||||
let mut peer = TestPeer::connect(server.addr).await?;
|
||||
peer.handshake_with(
|
||||
PEER_ASN,
|
||||
PEER_ID,
|
||||
90,
|
||||
vec![
|
||||
four_octet(),
|
||||
Capability::multiprotocol(AfiSafi::IPV4_UNICAST),
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
server.wait_for_session().await?;
|
||||
|
||||
peer.announce(dual_stack_update()).await?;
|
||||
|
||||
let routes = some_routes(&server).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"));
|
||||
assert_eq!(server.registry.list().len(), 1, "the session survives");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A route of a family the peer did not offer is not sent to it.
|
||||
#[tokio::test]
|
||||
async fn test_routes_of_a_family_not_negotiated_are_not_sent() -> Result<()> {
|
||||
let server = TestServer::start_with(LOCAL_ASN, |c| {
|
||||
c.originate = vec![LocalRoute::new(
|
||||
prefix("10.100.0.0/16"),
|
||||
LocalRouteAttrs::default(),
|
||||
)];
|
||||
})
|
||||
.await?;
|
||||
|
||||
// This peer wants IPv6 only; the server has an IPv4 route.
|
||||
let mut v6_only = TestPeer::connect(server.addr).await?;
|
||||
v6_only
|
||||
.handshake_with(
|
||||
PEER_ASN,
|
||||
PEER_ID,
|
||||
90,
|
||||
vec![
|
||||
four_octet(),
|
||||
Capability::multiprotocol(AfiSafi::IPV6_UNICAST),
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
let session = server.wait_for_session().await?;
|
||||
assert_eq!(session.families, only(AfiSafi::IPV6_UNICAST));
|
||||
|
||||
// A second peer that takes IPv4 is sent the route. The advertiser
|
||||
// handles sessions in the order they came up, so by the time this one
|
||||
// has its route the first has been dealt with.
|
||||
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?;
|
||||
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;
|
||||
assert!(nothing.is_err(), "got {nothing:?}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A configured peer is offered only the families configured for it, and a
|
||||
/// peer that shares none of them is refused with the capability we needed.
|
||||
#[tokio::test]
|
||||
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 {
|
||||
families: only(AfiSafi::IPV6_UNICAST),
|
||||
import: ImportPolicy::permissive(),
|
||||
..peer_config("v6-only", at, PEER_ASN)
|
||||
}];
|
||||
// Keep the dialer out of the way.
|
||||
c.connect_retry = Duration::from_secs(3600);
|
||||
})
|
||||
.await?;
|
||||
|
||||
let mut peer = TestPeer::connect(server.addr).await?;
|
||||
peer.send_open_with(
|
||||
PEER_ASN,
|
||||
PEER_ID,
|
||||
90,
|
||||
vec![
|
||||
four_octet(),
|
||||
Capability::multiprotocol(AfiSafi::IPV4_UNICAST),
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
let open = peer.recv_open().await?;
|
||||
let offered: Vec<_> = open
|
||||
.capabilities()
|
||||
.filter(|c| matches!(c, Capability::MultiProtocol { .. }))
|
||||
.collect();
|
||||
assert_eq!(offered, [&Capability::multiprotocol(AfiSafi::IPV6_UNICAST)]);
|
||||
|
||||
match peer.recv().await? {
|
||||
Message::Notification(n) => {
|
||||
assert_eq!(
|
||||
n.error,
|
||||
BgpError::OpenMsg(OpenMsgSubcode::UnsupportedCapability)
|
||||
);
|
||||
// The Multiprotocol capability for IPv6 unicast.
|
||||
assert_eq!(n.data, [1, 4, 0, 2, 0, 1]);
|
||||
}
|
||||
other => bail!("expected NOTIFICATION, got {other:?}"),
|
||||
}
|
||||
assert!(server.registry.list().is_empty());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Real routers offer families we do not support. That must not stop
|
||||
/// the families we do share from working.
|
||||
#[tokio::test]
|
||||
async fn test_a_peer_offering_an_unsupported_family_is_still_accepted() -> Result<()> {
|
||||
let server = TestServer::start(LOCAL_ASN).await?;
|
||||
let mut peer = TestPeer::connect(server.addr).await?;
|
||||
peer.handshake_with(
|
||||
PEER_ASN,
|
||||
PEER_ID,
|
||||
90,
|
||||
vec![
|
||||
four_octet(),
|
||||
Capability::multiprotocol(AfiSafi::new(Afi::L2VPN, Safi::EVPN)),
|
||||
Capability::multiprotocol(AfiSafi::IPV4_UNICAST),
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
|
||||
let session = server.wait_for_session().await?;
|
||||
assert_eq!(session.families, only(AfiSafi::IPV4_UNICAST));
|
||||
assert!(
|
||||
session.four_octet_asn,
|
||||
"capabilities after it are still read"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A peer that offers no capabilities at all is a plain RFC 4271 speaker:
|
||||
/// IPv4 unicast, two-octet AS numbers. Our four-octet AS number reaches it
|
||||
/// as AS_TRANS in AS_PATH and in full in AS4_PATH (RFC 6793 §4.2.2).
|
||||
#[tokio::test]
|
||||
async fn test_a_peer_without_capabilities_gets_two_octet_as_paths() -> Result<()> {
|
||||
const WIDE_ASN: u32 = 4_200_000_001;
|
||||
let server = TestServer::start_with(WIDE_ASN, |c| {
|
||||
c.originate = vec![LocalRoute::new(
|
||||
prefix("10.100.0.0/16"),
|
||||
LocalRouteAttrs::default(),
|
||||
)];
|
||||
})
|
||||
.await?;
|
||||
|
||||
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?;
|
||||
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?;
|
||||
assert_eq!(update.announced_routes, vec![prefix("10.100.0.0/16")]);
|
||||
assert!(
|
||||
update
|
||||
.path_attributes
|
||||
.contains(&AsPathAttribute::from_asns(vec![23456])),
|
||||
"{update:?}"
|
||||
);
|
||||
let as4_path = update.path_attributes.iter().find_map(|a| match a {
|
||||
PathAttribute::As4Path(path) => Some(path.segments[0].path.clone()),
|
||||
_ => None,
|
||||
});
|
||||
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?;
|
||||
assert!(
|
||||
routes[0]
|
||||
.attributes
|
||||
.contains(&AsPathAttribute::from_asns(vec![PEER_ASN, 64512]))
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
//! The ping monitor as seen through the HTTP API. The socket is left out
|
||||
//! (it needs privileges); observations are fed in directly.
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use bgp_integration_tests::test_server::TestServer;
|
||||
use bgp_primitives::ip_prefix::IpPrefix;
|
||||
use bgp_rib::LocalRoute;
|
||||
use bgp_server::ping::PingObservation;
|
||||
use eyre::Result;
|
||||
use serde_json::Value;
|
||||
|
||||
const TIMEOUT: Duration = Duration::from_secs(5);
|
||||
|
||||
fn ping(src: &str, dst: &str, hop_limit: u8, seq: u16) -> PingObservation {
|
||||
PingObservation {
|
||||
src: src.parse().unwrap(),
|
||||
dst: dst.parse().unwrap(),
|
||||
interface: Some("swissix0".into()),
|
||||
hop_limit: Some(hop_limit),
|
||||
ident: 1,
|
||||
seq,
|
||||
bytes: 64,
|
||||
packet: [&[128, 0, 0xab, 0xcd, 0, 1][..], &seq.to_be_bytes(), b"hi"].concat(),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn pings_are_listed_with_the_route_they_were_sent_to() -> Result<()> {
|
||||
let server = TestServer::start(65000).await?;
|
||||
let (base, _http) = server.serve_http().await?;
|
||||
let client = reqwest::Client::new();
|
||||
server
|
||||
.local_rib
|
||||
.announce(LocalRoute::new(IpPrefix::try_from("2001:db8:1a::/48").unwrap(), "alice", Default::default()))
|
||||
.await?;
|
||||
|
||||
server.pings.record(ping("2001:67c:10ec::5", "2001:db8:1a::1", 57, 0)).await;
|
||||
server.pings.record(ping("2001:67c:10ec::5", "2001:db8:1a::1", 57, 1)).await;
|
||||
server.pings.record(ping("2a02:1210::9", "2001:db8:ff::1", 243, 0)).await;
|
||||
server.pings.record(ping("2a02:1210::9", "2a0d:d740::1", 243, 0)).await; // not a watched prefix
|
||||
|
||||
let view: Value = client.get(format!("{base}/api/pings")).send().await?.error_for_status()?.json().await?;
|
||||
assert_eq!(view["status"], "listening");
|
||||
assert_eq!(view["prefixes"][0], "2001:db8::/32");
|
||||
assert_eq!(view["total_flows"], 2);
|
||||
|
||||
let flows = view["flows"].as_array().unwrap();
|
||||
let alice = flows.iter().find(|f| f["dst"] == "2001:db8:1a::1").unwrap();
|
||||
assert_eq!(alice["src"], "2001:67c:10ec::5");
|
||||
assert_eq!(alice["prefix"], "2001:db8:1a::/48");
|
||||
assert_eq!(alice["owner"], "alice");
|
||||
assert_eq!(alice["interface"], "swissix0");
|
||||
assert_eq!((alice["hop_limit"].as_u64(), alice["hops"].as_u64()), (Some(57), Some(7)));
|
||||
assert_eq!((alice["count"].as_u64(), alice["seq"].as_u64()), (Some(2), Some(1)));
|
||||
assert_eq!(alice["hex"], "8000abcd000100016869");
|
||||
assert!(alice["last_seen_unix"].as_u64().unwrap() > 1_700_000_000);
|
||||
|
||||
let stray = flows.iter().find(|f| f["dst"] == "2001:db8:ff::1").unwrap();
|
||||
assert!(stray["prefix"].is_null() && stray["owner"].is_null());
|
||||
assert_eq!(stray["hops"], 12);
|
||||
|
||||
let limited: Value = client.get(format!("{base}/api/pings?limit=1")).send().await?.json().await?;
|
||||
assert_eq!(limited["total_flows"], 2);
|
||||
assert_eq!(limited["flows"].as_array().unwrap().len(), 1);
|
||||
|
||||
let metrics = client.get(format!("{base}/metricz")).send().await?.text().await?;
|
||||
assert!(metrics.contains("ping_monitor_echo_requests_total"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn pings_appear_on_the_event_stream() -> Result<()> {
|
||||
let server = TestServer::start(65000).await?;
|
||||
let (base, _http) = server.serve_http().await?;
|
||||
let mut resp = reqwest::Client::new().get(format!("{base}/api/events")).send().await?.error_for_status()?;
|
||||
|
||||
// The subscription exists once the response has started; a ping recorded
|
||||
// before that would be missed, so keep pinging until one shows up.
|
||||
let pings = server.pings.clone();
|
||||
let pinger = tokio::spawn(async move {
|
||||
for seq in 0.. {
|
||||
pings.record(ping("2001:67c:10ec::5", "2001:db8:1a::1", 57, seq)).await;
|
||||
tokio::time::sleep(Duration::from_millis(50)).await;
|
||||
}
|
||||
});
|
||||
|
||||
let mut buf = String::new();
|
||||
let event = tokio::time::timeout(TIMEOUT, async {
|
||||
loop {
|
||||
let chunk = resp.chunk().await?.ok_or_else(|| eyre::eyre!("stream ended"))?;
|
||||
buf.push_str(std::str::from_utf8(&chunk)?);
|
||||
for line in buf.lines().filter_map(|l| l.strip_prefix("data:")) {
|
||||
let ev: Value = serde_json::from_str(line.trim())?;
|
||||
if ev["type"] == "ping_seen" {
|
||||
return Ok::<_, eyre::Error>(ev);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.await??;
|
||||
pinger.abort();
|
||||
|
||||
assert_eq!(event["ping"]["src"], "2001:67c:10ec::5");
|
||||
assert_eq!(event["ping"]["dst"], "2001:db8:1a::1");
|
||||
assert_eq!(event["ping"]["interface"], "swissix0");
|
||||
assert!(event["new_burst"].is_boolean());
|
||||
Ok(())
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -1,207 +0,0 @@
|
||||
//! End-to-end tests for errors in UPDATE messages (RFC 7606): most of them
|
||||
//! cost the routes in the UPDATE, not the session.
|
||||
|
||||
use std::net::Ipv4Addr;
|
||||
|
||||
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_primitives::ip_prefix::IpPrefix;
|
||||
use bgp_server::metrics::UPDATE_ERRORS;
|
||||
use eyre::Result;
|
||||
|
||||
const LOCAL_ASN: u32 = 65000;
|
||||
const PEER_ASN: u32 = 65001;
|
||||
|
||||
const WELL_KNOWN: u8 = 0x40;
|
||||
const OPTIONAL_TRANSITIVE: u8 = 0xc0;
|
||||
|
||||
fn attr(flags: u8, type_code: u8, value: &[u8]) -> Vec<u8> {
|
||||
let mut wire = vec![flags, type_code, value.len() as u8];
|
||||
wire.extend_from_slice(value);
|
||||
wire
|
||||
}
|
||||
|
||||
fn origin() -> Vec<u8> {
|
||||
attr(WELL_KNOWN, 1, &[0])
|
||||
}
|
||||
|
||||
/// AS_SEQUENCE of the peer's AS, four octets wide.
|
||||
fn as_path() -> Vec<u8> {
|
||||
attr(WELL_KNOWN, 2, &[2, 1, 0, 0, 0xfd, 0xe9])
|
||||
}
|
||||
|
||||
fn next_hop() -> Vec<u8> {
|
||||
attr(WELL_KNOWN, 3, &[192, 0, 2, 1])
|
||||
}
|
||||
|
||||
/// A whole UPDATE message announcing `nlri` with `attributes`, as bytes, so
|
||||
/// that it can be wrong in ways the encoder would not write.
|
||||
fn update(attributes: &[Vec<u8>], nlri: &[u8]) -> Vec<u8> {
|
||||
let attributes = attributes.concat();
|
||||
let mut body = vec![0, 0];
|
||||
body.extend_from_slice(&(attributes.len() as u16).to_be_bytes());
|
||||
body.extend_from_slice(&attributes);
|
||||
body.extend_from_slice(nlri);
|
||||
|
||||
let mut wire = vec![0xff; 16];
|
||||
wire.extend_from_slice(&((19 + body.len()) as u16).to_be_bytes());
|
||||
wire.push(2);
|
||||
wire.extend_from_slice(&body);
|
||||
wire
|
||||
}
|
||||
|
||||
/// 10.1.0.0/16 and 10.2.0.0/16.
|
||||
const ROUTE_A: [u8; 3] = [16, 10, 1];
|
||||
const ROUTE_B: [u8; 3] = [16, 10, 2];
|
||||
|
||||
async fn established(server: &TestServer, asn: u32) -> Result<TestPeer> {
|
||||
server.established(asn, Ipv4Addr::new(10, 0, 0, 1)).await
|
||||
}
|
||||
|
||||
/// 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)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// The situation RFC 7606 exists for: a route is in the table, and then an
|
||||
/// UPDATE for it arrives with a broken attribute. The route goes; the
|
||||
/// session, and the other routes learned over it, stay.
|
||||
#[tokio::test]
|
||||
async fn test_a_malformed_attribute_withdraws_the_route_and_keeps_the_session() -> Result<()> {
|
||||
let server = TestServer::start(LOCAL_ASN).await?;
|
||||
let mut peer = established(&server, PEER_ASN).await?;
|
||||
let handled_before = UPDATE_ERRORS
|
||||
.with_label_values(&["treat-as-withdraw"])
|
||||
.get();
|
||||
|
||||
let good = [origin(), as_path(), next_hop()];
|
||||
peer.send_raw(&update(&good, &[ROUTE_A, ROUTE_B].concat()))
|
||||
.await?;
|
||||
wait_for_routes(&server, &["10.1.0.0/16", "10.2.0.0/16"]).await?;
|
||||
|
||||
// ORIGIN with a value that is not defined.
|
||||
let bad = [attr(WELL_KNOWN, 1, &[7]), as_path(), next_hop()];
|
||||
peer.send_raw(&update(&bad, &ROUTE_A)).await?;
|
||||
wait_for_routes(&server, &["10.2.0.0/16"]).await?;
|
||||
|
||||
// The session goes on: the route can be announced again.
|
||||
peer.send_raw(&update(&good, &ROUTE_A)).await?;
|
||||
wait_for_routes(&server, &["10.1.0.0/16", "10.2.0.0/16"]).await?;
|
||||
assert_eq!(server.registry.list().len(), 1);
|
||||
assert!(
|
||||
UPDATE_ERRORS
|
||||
.with_label_values(&["treat-as-withdraw"])
|
||||
.get()
|
||||
> handled_before
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// An announcement without a mandatory attribute is not a route we can use.
|
||||
#[tokio::test]
|
||||
async fn test_a_route_without_as_path_is_not_accepted() -> Result<()> {
|
||||
let server = TestServer::start(LOCAL_ASN).await?;
|
||||
let mut peer = established(&server, PEER_ASN).await?;
|
||||
|
||||
peer.send_raw(&update(&[origin(), next_hop()], &ROUTE_A))
|
||||
.await?;
|
||||
// A well-formed one after it shows that the first was dealt with.
|
||||
peer.send_raw(&update(&[origin(), as_path(), next_hop()], &ROUTE_B))
|
||||
.await?;
|
||||
|
||||
wait_for_routes(&server, &["10.2.0.0/16"]).await?;
|
||||
assert_eq!(server.registry.list().len(), 1);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A broken attribute that plays no part in choosing a route is dropped,
|
||||
/// and the route is kept.
|
||||
#[tokio::test]
|
||||
async fn test_a_malformed_aggregator_is_dropped_and_the_route_kept() -> Result<()> {
|
||||
let server = TestServer::start(LOCAL_ASN).await?;
|
||||
let mut peer = established(&server, PEER_ASN).await?;
|
||||
|
||||
let attributes = [
|
||||
origin(),
|
||||
as_path(),
|
||||
next_hop(),
|
||||
// AGGREGATOR is eight octets on this session.
|
||||
attr(OPTIONAL_TRANSITIVE, 7, &[0, 0, 0xfd, 0xe9, 192]),
|
||||
];
|
||||
peer.send_raw(&update(&attributes, &ROUTE_A)).await?;
|
||||
|
||||
let routes = wait_for_routes(&server, &["10.1.0.0/16"]).await?;
|
||||
assert_eq!(routes[0].attributes.len(), 3);
|
||||
assert!(
|
||||
!routes[0]
|
||||
.attributes
|
||||
.iter()
|
||||
.any(|a| matches!(a, PathAttribute::Aggregator(_)))
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// LOCAL_PREF is a matter between routers of one AS. From an external peer
|
||||
/// it is dropped; from an internal one it is kept.
|
||||
#[tokio::test]
|
||||
async fn test_local_pref_is_kept_only_from_an_internal_peer() -> Result<()> {
|
||||
let local_pref = attr(WELL_KNOWN, 5, &[0, 0, 0, 200]);
|
||||
let has_local_pref = |routes: &[ReceivedRoute]| {
|
||||
routes[0]
|
||||
.attributes
|
||||
.contains(&PathAttribute::LocalPref(LocalPrefPathAttribute(200)))
|
||||
};
|
||||
|
||||
let server = TestServer::start(LOCAL_ASN).await?;
|
||||
let mut external = established(&server, PEER_ASN).await?;
|
||||
let attributes = [origin(), as_path(), next_hop(), local_pref.clone()];
|
||||
external.send_raw(&update(&attributes, &ROUTE_A)).await?;
|
||||
let routes = wait_for_routes(&server, &["10.1.0.0/16"]).await?;
|
||||
assert!(!has_local_pref(&routes));
|
||||
|
||||
let server = TestServer::start(LOCAL_ASN).await?;
|
||||
let mut internal = established(&server, LOCAL_ASN).await?;
|
||||
// An internal peer has not added its AS to the path.
|
||||
let attributes = [origin(), attr(WELL_KNOWN, 2, &[]), next_hop(), local_pref];
|
||||
internal.send_raw(&update(&attributes, &ROUTE_A)).await?;
|
||||
let routes = wait_for_routes(&server, &["10.1.0.0/16"]).await?;
|
||||
assert!(has_local_pref(&routes));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// When the prefixes themselves cannot be read there is nothing to
|
||||
/// withdraw, and the session is reset as before.
|
||||
#[tokio::test]
|
||||
async fn test_an_unreadable_prefix_still_resets_the_session() -> Result<()> {
|
||||
let server = TestServer::start(LOCAL_ASN).await?;
|
||||
let mut peer = established(&server, PEER_ASN).await?;
|
||||
|
||||
// An IPv4 prefix of length 33.
|
||||
peer.send_raw(&update(
|
||||
&[origin(), as_path(), next_hop()],
|
||||
&[33, 10, 1, 2, 3, 4],
|
||||
))
|
||||
.await?;
|
||||
|
||||
let notification = peer.expect_notification().await?;
|
||||
assert_eq!(
|
||||
notification.error,
|
||||
BgpError::UpdateMsg(UpdateMsgSubcode::InvalidNetworkField)
|
||||
);
|
||||
server.wait_for_sessions(0).await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "bgp-packet"
|
||||
description = "Parsing and serialisation of BGP messages"
|
||||
authors.workspace = true
|
||||
|
||||
description.workspace = true
|
||||
edition.workspace = true
|
||||
homepage.workspace = true
|
||||
license.workspace = true
|
||||
@@ -11,16 +11,10 @@ version.workspace = true
|
||||
[dependencies]
|
||||
bitfield.workspace = true
|
||||
bytes.workspace = true
|
||||
eyre.workspace = true
|
||||
nom.workspace = true
|
||||
serde.workspace = true
|
||||
serde_repr.workspace = true
|
||||
thiserror.workspace = true
|
||||
strum.workspace = true
|
||||
bgp-primitives.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
eyre.workspace = true
|
||||
proptest.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
+96
-74
@@ -1,79 +1,68 @@
|
||||
use serde::Serialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum::FromRepr;
|
||||
use thiserror::Error;
|
||||
|
||||
/// A field holds a number that is not one of the values defined for it.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
|
||||
#[error("invalid {what} value {value}")]
|
||||
pub struct InvalidValue {
|
||||
pub what: &'static str,
|
||||
pub value: u32,
|
||||
#[derive(Debug, Error)]
|
||||
#[error("Unknown BGP error code {code} subcode {subcode}")]
|
||||
pub struct UnknownErrorCode {
|
||||
pub code: u8,
|
||||
pub subcode: u8,
|
||||
}
|
||||
|
||||
/// A message that could not be decoded, together with the NOTIFICATION that
|
||||
/// reports the problem to the peer (RFC 4271 §6).
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Error)]
|
||||
#[error("{detail}")]
|
||||
pub struct DecodeError {
|
||||
/// The error code and subcode to send.
|
||||
pub notification: BgpError,
|
||||
/// The Data field of that NOTIFICATION, where the RFC defines one.
|
||||
pub data: Vec<u8>,
|
||||
/// What was wrong, for the log.
|
||||
pub detail: String,
|
||||
}
|
||||
|
||||
/// The error a NOTIFICATION reports: its code and subcode (RFC 4271 §4.5).
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Error, Serialize)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Error, Serialize, Deserialize)]
|
||||
#[repr(u8)]
|
||||
pub enum BgpError {
|
||||
#[error("Message header error: {0}")]
|
||||
MsgHeader(MsgHeaderSubcode),
|
||||
#[error("Message header error")]
|
||||
MsgHeader(MsgHeaderSubcode) = 1,
|
||||
|
||||
#[error("Open message error: {0}")]
|
||||
OpenMsg(OpenMsgSubcode),
|
||||
#[error("Open message error")]
|
||||
OpenMsg(OpenMsgSubcode) = 2,
|
||||
|
||||
#[error("Update message error: {0}")]
|
||||
UpdateMsg(UpdateMsgSubcode),
|
||||
#[error("Update message error")]
|
||||
UpdateMsg(UpdateMsgSubcode) = 3,
|
||||
|
||||
#[error("Hold timer expired")]
|
||||
HoldTimer,
|
||||
HoldTimer = 4,
|
||||
|
||||
#[error("Finite state machine error: {0}")]
|
||||
FsmError(FsmSubcode),
|
||||
#[error("Finite state machine error")]
|
||||
FsmError(FsmSubcode) = 5,
|
||||
|
||||
#[error("Cease: {0}")]
|
||||
Cease(CeaseSubcode),
|
||||
#[error("Cease")]
|
||||
Cease(CeaseSubcode) = 6,
|
||||
|
||||
#[error("Route refresh message error: {0}")]
|
||||
RouteRefresh(RouteRefreshSubcode),
|
||||
#[error("Route refresh message error")]
|
||||
RouteRefresh(RouteRefreshSubcode) = 7,
|
||||
|
||||
#[error("Send hold timer expired")]
|
||||
SendHoldTimer,
|
||||
|
||||
/// A code or subcode this implementation has no name for. A peer may
|
||||
/// send any: new ones are assigned, and zero ("unspecific") is always
|
||||
/// allowed. The session ends the same way, and the numbers are kept
|
||||
/// for the log.
|
||||
#[error("error code {code} subcode {subcode}")]
|
||||
Other { code: u8, subcode: u8 },
|
||||
SendHoldTimer = 8,
|
||||
}
|
||||
|
||||
impl BgpError {
|
||||
/// The error for a code and subcode off the wire. Every pair has one,
|
||||
/// so that a peer's NOTIFICATION can always be read.
|
||||
pub fn from_code_subcode(code: u8, subcode: u8) -> Self {
|
||||
let known = match code {
|
||||
1 => MsgHeaderSubcode::from_repr(subcode).map(Self::MsgHeader),
|
||||
2 => OpenMsgSubcode::from_repr(subcode).map(Self::OpenMsg),
|
||||
3 => UpdateMsgSubcode::from_repr(subcode).map(Self::UpdateMsg),
|
||||
4 => (subcode == 0).then_some(Self::HoldTimer),
|
||||
5 => FsmSubcode::from_repr(subcode).map(Self::FsmError),
|
||||
6 => CeaseSubcode::from_repr(subcode).map(Self::Cease),
|
||||
7 => RouteRefreshSubcode::from_repr(subcode).map(Self::RouteRefresh),
|
||||
8 => (subcode == 0).then_some(Self::SendHoldTimer),
|
||||
_ => None,
|
||||
};
|
||||
known.unwrap_or(Self::Other { code, subcode })
|
||||
pub fn from_code_subcode(code: u8, subcode: u8) -> Result<Self, UnknownErrorCode> {
|
||||
let err = || UnknownErrorCode { code, subcode };
|
||||
match code {
|
||||
1 => Ok(Self::MsgHeader(
|
||||
MsgHeaderSubcode::try_from(subcode).map_err(|_| err())?,
|
||||
)),
|
||||
2 => Ok(Self::OpenMsg(
|
||||
OpenMsgSubcode::try_from(subcode).map_err(|_| err())?,
|
||||
)),
|
||||
3 => Ok(Self::UpdateMsg(
|
||||
UpdateMsgSubcode::try_from(subcode).map_err(|_| err())?,
|
||||
)),
|
||||
4 => Ok(Self::HoldTimer),
|
||||
5 => Ok(Self::FsmError(
|
||||
FsmSubcode::try_from(subcode).map_err(|_| err())?,
|
||||
)),
|
||||
6 => Ok(Self::Cease(
|
||||
CeaseSubcode::try_from(subcode).map_err(|_| err())?,
|
||||
)),
|
||||
7 => Ok(Self::RouteRefresh(
|
||||
RouteRefreshSubcode::try_from(subcode).map_err(|_| err())?,
|
||||
)),
|
||||
8 => Ok(Self::SendHoldTimer),
|
||||
_ => Err(err()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn code_subcode(&self) -> (u8, u8) {
|
||||
@@ -86,12 +75,11 @@ impl BgpError {
|
||||
Self::Cease(s) => (6, *s as u8),
|
||||
Self::RouteRefresh(s) => (7, *s as u8),
|
||||
Self::SendHoldTimer => (8, 0),
|
||||
Self::Other { code, subcode } => (*code, *subcode),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error, Serialize, FromRepr)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error, Serialize, Deserialize, FromRepr)]
|
||||
#[repr(u8)]
|
||||
pub enum MsgHeaderSubcode {
|
||||
#[error("Connection not synchronized")]
|
||||
@@ -104,12 +92,16 @@ pub enum MsgHeaderSubcode {
|
||||
BadMessageType = 3,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error, Serialize, FromRepr)]
|
||||
impl TryFrom<u8> for MsgHeaderSubcode {
|
||||
type Error = ();
|
||||
fn try_from(v: u8) -> Result<Self, ()> {
|
||||
Self::from_repr(v).ok_or(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error, Serialize, Deserialize, FromRepr)]
|
||||
#[repr(u8)]
|
||||
pub enum OpenMsgSubcode {
|
||||
#[error("Unspecific")]
|
||||
Unspecific = 0,
|
||||
|
||||
#[error("Unsupported BGP version number")]
|
||||
UnsupportedVersion = 1,
|
||||
|
||||
@@ -128,12 +120,18 @@ 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)]
|
||||
impl TryFrom<u8> for OpenMsgSubcode {
|
||||
type Error = ();
|
||||
fn try_from(v: u8) -> Result<Self, ()> {
|
||||
Self::from_repr(v).ok_or(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error, Serialize, Deserialize, FromRepr)]
|
||||
#[repr(u8)]
|
||||
pub enum UpdateMsgSubcode {
|
||||
#[error("Malformed attribute list")]
|
||||
@@ -167,13 +165,16 @@ pub enum UpdateMsgSubcode {
|
||||
MalformedAsPath = 11,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error, Serialize, FromRepr)]
|
||||
impl TryFrom<u8> for UpdateMsgSubcode {
|
||||
type Error = ();
|
||||
fn try_from(v: u8) -> Result<Self, ()> {
|
||||
Self::from_repr(v).ok_or(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error, Serialize, Deserialize, 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,
|
||||
|
||||
@@ -184,7 +185,14 @@ pub enum FsmSubcode {
|
||||
UnexpectedEstablished = 3,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error, Serialize, FromRepr)]
|
||||
impl TryFrom<u8> for FsmSubcode {
|
||||
type Error = ();
|
||||
fn try_from(v: u8) -> Result<Self, ()> {
|
||||
Self::from_repr(v).ok_or(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error, Serialize, Deserialize, FromRepr)]
|
||||
#[repr(u8)]
|
||||
pub enum CeaseSubcode {
|
||||
#[error("Maximum number of prefixes reached")]
|
||||
@@ -218,9 +226,23 @@ pub enum CeaseSubcode {
|
||||
BfdDown = 10,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error, Serialize, FromRepr)]
|
||||
impl TryFrom<u8> for CeaseSubcode {
|
||||
type Error = ();
|
||||
fn try_from(v: u8) -> Result<Self, ()> {
|
||||
Self::from_repr(v).ok_or(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error, Serialize, Deserialize, FromRepr)]
|
||||
#[repr(u8)]
|
||||
pub enum RouteRefreshSubcode {
|
||||
#[error("Invalid message length")]
|
||||
InvalidLength = 1,
|
||||
}
|
||||
|
||||
impl TryFrom<u8> for RouteRefreshSubcode {
|
||||
type Error = ();
|
||||
fn try_from(v: u8) -> Result<Self, ()> {
|
||||
Self::from_repr(v).ok_or(())
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,3 @@
|
||||
pub mod attributes;
|
||||
pub mod community;
|
||||
pub mod errors;
|
||||
pub mod message;
|
||||
pub mod open;
|
||||
pub mod update_decode;
|
||||
mod wire;
|
||||
+1493
-872
File diff suppressed because it is too large.
Load diff
+637
-869
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -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()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,763 +0,0 @@
|
||||
//! Property tests for the wire codec.
|
||||
//!
|
||||
//! Two things are checked for every kind of message:
|
||||
//!
|
||||
//! - **Round trip.** A message that can be written can be read back, comes
|
||||
//! back equal.
|
||||
//! - **Hostile input.** Whatever bytes arrive, decoding returns a message or
|
||||
//! an error: it never panics. What it does accept can be shown, written
|
||||
//! again, and read back as the same message.
|
||||
//!
|
||||
//! The second is what a peer can do to us, so it is run on three kinds of
|
||||
//! input: plain noise, noise behind a valid header (so it reaches the body
|
||||
//! parsers), and valid messages with a few bytes damaged (so it reaches deep
|
||||
//! into them).
|
||||
|
||||
use std::net::{Ipv4Addr, Ipv6Addr};
|
||||
|
||||
use bgp_packet::errors::BgpError;
|
||||
use bgp_packet::message::{
|
||||
AggregatorPathAttribute, AsPathAttribute, AsPathSegment, AtomicAggregatePathAttribute,
|
||||
BGP_HEADER_LEN, BGP_MAX_MSG_LEN, CommunitiesPathAttribute, ExtendedCommunitiesPathAttribute,
|
||||
ExtendedCommunity, LargeCommunitiesPathAttribute, LargeCommunity, LocalPrefPathAttribute,
|
||||
Message, MpReachNlriPathAttribute, MpUnreachNlriPathAttribute, MultiExitDiscPathAttribute,
|
||||
NextHopPathAttribute, NlriNextHop, NotificationMessage, OriginPathAttribute, PathAttribute,
|
||||
PathAttributeFlags, RouteRefreshMessage, UpdateMessage,
|
||||
};
|
||||
use bgp_packet::open::{
|
||||
AddPathEntry, BgpRole, BgpSecHeader, Capability, ExtendedNextHopEntry, GracefulRestartEntry,
|
||||
LongLivedGracefulRestartEntry, MultiLabelEntry, OpenMessage, OpenOption, OrfFamily, OrfType,
|
||||
OutboundRouteFilterEntry, SendReceive,
|
||||
};
|
||||
use bgp_primitives::constants::{Afi, AfiSafi, IpVersion, Safi};
|
||||
use bgp_primitives::ip_prefix::{IpPrefix, Nlri};
|
||||
use bgp_primitives::parser::{EncodeError, ParserContext};
|
||||
use bytes::BytesMut;
|
||||
use proptest::collection::vec;
|
||||
use proptest::prelude::*;
|
||||
|
||||
/// What a session settled that the codec depends on.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct Session {
|
||||
four_octet_asn: bool,
|
||||
ibgp: bool,
|
||||
/// Whether prefixes carry path identifiers (RFC 7911), for IPv4 and
|
||||
/// for IPv6 unicast.
|
||||
add_path: [bool; 2],
|
||||
}
|
||||
|
||||
impl Session {
|
||||
fn ctx(self) -> ParserContext {
|
||||
ParserContext {
|
||||
four_octet_asn: self.four_octet_asn,
|
||||
ibgp: self.ibgp,
|
||||
add_path: [AfiSafi::IPV4_UNICAST, AfiSafi::IPV6_UNICAST]
|
||||
.into_iter()
|
||||
.zip(self.add_path)
|
||||
.filter_map(|(family, in_use)| in_use.then_some(family))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn has_add_path(self, version: IpVersion, safi: Safi) -> bool {
|
||||
safi == Safi::UNICAST
|
||||
&& match version {
|
||||
IpVersion::Ipv4 => self.add_path[0],
|
||||
IpVersion::Ipv6 => self.add_path[1],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn session() -> impl Strategy<Value = Session> {
|
||||
any::<(bool, bool, [bool; 2])>().prop_map(|(four_octet_asn, ibgp, add_path)| Session {
|
||||
four_octet_asn,
|
||||
ibgp,
|
||||
add_path,
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Generators
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn prefix(version: IpVersion) -> impl Strategy<Value = IpPrefix> {
|
||||
let max_len = match version {
|
||||
IpVersion::Ipv4 => 32u8,
|
||||
IpVersion::Ipv6 => 128,
|
||||
};
|
||||
(0..=max_len, any::<[u8; 16]>()).prop_map(move |(length, bytes)| {
|
||||
IpPrefix::new(version, bytes, length).expect("16 bytes are enough for any prefix")
|
||||
})
|
||||
}
|
||||
|
||||
/// Prefixes as an UPDATE names them. Without ADD-PATH for the family there
|
||||
/// is no identifier on the wire, and it reads back as zero.
|
||||
fn nlri(session: Session, version: IpVersion, safi: Safi) -> impl Strategy<Value = Vec<Nlri>> {
|
||||
let with_path_id = session.has_add_path(version, safi);
|
||||
vec(
|
||||
(prefix(version), any::<u32>()).prop_map(move |(prefix, path_id)| Nlri {
|
||||
prefix,
|
||||
path_id: if with_path_id { path_id } else { 0 },
|
||||
}),
|
||||
0..5,
|
||||
)
|
||||
}
|
||||
|
||||
fn afi() -> impl Strategy<Value = Afi> {
|
||||
any::<u16>().prop_map(Afi)
|
||||
}
|
||||
|
||||
fn safi() -> impl Strategy<Value = Safi> {
|
||||
any::<u8>().prop_map(Safi)
|
||||
}
|
||||
|
||||
/// AS numbers that fit the session: any on a four-octet one, two-octet ones
|
||||
/// otherwise (wider ones are replaced by AS_TRANS on the wire, by design).
|
||||
fn asn(four_octet_asn: bool) -> BoxedStrategy<u32> {
|
||||
if four_octet_asn {
|
||||
any::<u32>().boxed()
|
||||
} else {
|
||||
any::<u16>().prop_map(u32::from).boxed()
|
||||
}
|
||||
}
|
||||
|
||||
fn as_path(asn: BoxedStrategy<u32>) -> impl Strategy<Value = AsPathAttribute> {
|
||||
// A segment has at least one AS number.
|
||||
vec((any::<bool>(), vec(asn, 1..6)), 0..4).prop_map(|segments| AsPathAttribute {
|
||||
segments: segments
|
||||
.into_iter()
|
||||
.map(|(ordered, path)| AsPathSegment { ordered, path })
|
||||
.collect(),
|
||||
})
|
||||
}
|
||||
|
||||
fn extended_community() -> impl Strategy<Value = ExtendedCommunity> {
|
||||
any::<[u8; 8]>().prop_map(ExtendedCommunity)
|
||||
}
|
||||
|
||||
fn mp_reach(session: Session) -> impl Strategy<Value = MpReachNlriPathAttribute> {
|
||||
let ip_safi = || prop_oneof![Just(Safi::UNICAST), Just(Safi::MULTICAST)];
|
||||
let v6_next_hop = || {
|
||||
prop_oneof![
|
||||
any::<[u8; 16]>().prop_map(|a| NlriNextHop::Ipv6(Ipv6Addr::from(a))),
|
||||
(any::<[u8; 16]>(), any::<[u8; 16]>()).prop_map(|(global, link_local)| {
|
||||
NlriNextHop::Ipv6WithLl {
|
||||
global: Ipv6Addr::from(global),
|
||||
link_local: Ipv6Addr::from(link_local),
|
||||
}
|
||||
}),
|
||||
]
|
||||
};
|
||||
// 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| {
|
||||
(
|
||||
Just(safi),
|
||||
v6_next_hop(),
|
||||
nlri(session, IpVersion::Ipv6, safi),
|
||||
)
|
||||
})
|
||||
.prop_map(|(safi, next_hop, prefixes)| MpReachNlriPathAttribute {
|
||||
family: AfiSafi::new(Afi::IPV6, safi),
|
||||
next_hop,
|
||||
prefixes,
|
||||
});
|
||||
prop_oneof![v4, v6]
|
||||
}
|
||||
|
||||
fn mp_unreach(session: Session) -> impl Strategy<Value = MpUnreachNlriPathAttribute> {
|
||||
let of = move |version: IpVersion| {
|
||||
nlri(session, version, Safi::UNICAST).prop_map(move |prefixes| MpUnreachNlriPathAttribute {
|
||||
family: AfiSafi::new(version.into(), Safi::UNICAST),
|
||||
prefixes,
|
||||
})
|
||||
};
|
||||
prop_oneof![of(IpVersion::Ipv4), of(IpVersion::Ipv6)]
|
||||
}
|
||||
|
||||
/// 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];
|
||||
|
||||
/// Unknown attributes, each with a type code of its own: a repeated
|
||||
/// attribute is dropped by the decoder.
|
||||
fn unknown_attributes() -> impl Strategy<Value = Vec<PathAttribute>> {
|
||||
proptest::collection::btree_map(
|
||||
any::<u8>().prop_filter("a type code without a definition", |code| {
|
||||
!DEFINED_ATTRIBUTES.contains(code)
|
||||
}),
|
||||
(any::<u8>(), vec(any::<u8>(), 0..300)),
|
||||
0..3,
|
||||
)
|
||||
.prop_map(|attributes| {
|
||||
attributes
|
||||
.into_iter()
|
||||
.map(|(type_code, (flags, payload))| {
|
||||
// Optional (bit 7): an unknown well-known attribute is an
|
||||
// error. Bit 4 selects the two-octet length, which a payload
|
||||
// this long needs.
|
||||
let mut flags = flags | 0x80;
|
||||
if payload.len() > 255 {
|
||||
flags |= 0x10;
|
||||
}
|
||||
PathAttribute::Unknown {
|
||||
flags: PathAttributeFlags(flags),
|
||||
type_code,
|
||||
payload,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
/// The attributes of a well-formed UPDATE on `session`: the mandatory ones,
|
||||
/// and any of the others at most once.
|
||||
fn path_attributes(session: Session) -> impl Strategy<Value = Vec<PathAttribute>> {
|
||||
let mandatory = (
|
||||
prop_oneof![
|
||||
Just(OriginPathAttribute::IGP),
|
||||
Just(OriginPathAttribute::EGP),
|
||||
Just(OriginPathAttribute::INCOMPLETE)
|
||||
],
|
||||
as_path(asn(session.four_octet_asn)),
|
||||
any::<[u8; 4]>(),
|
||||
any::<u32>(),
|
||||
)
|
||||
.prop_map(move |(origin, as_path, next_hop, local_pref)| {
|
||||
let mut attributes = vec![
|
||||
PathAttribute::Origin(origin),
|
||||
PathAttribute::AsPath(as_path),
|
||||
PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::from(next_hop))),
|
||||
];
|
||||
// Mandatory between internal peers, dropped from external ones.
|
||||
if session.ibgp {
|
||||
attributes.push(PathAttribute::LocalPref(LocalPrefPathAttribute(local_pref)));
|
||||
}
|
||||
attributes
|
||||
});
|
||||
let optional = (
|
||||
proptest::option::of(
|
||||
any::<u32>().prop_map(|v| PathAttribute::MultiExitDisc(MultiExitDiscPathAttribute(v))),
|
||||
),
|
||||
proptest::option::of(Just(PathAttribute::AtomicAggregate(
|
||||
AtomicAggregatePathAttribute {},
|
||||
))),
|
||||
proptest::option::of((asn(session.four_octet_asn), any::<[u8; 4]>()).prop_map(
|
||||
|(asn, ip)| {
|
||||
PathAttribute::Aggregator(AggregatorPathAttribute {
|
||||
asn,
|
||||
ip: Ipv4Addr::from(ip),
|
||||
})
|
||||
},
|
||||
)),
|
||||
proptest::option::of(
|
||||
vec(any::<(u16, u16)>(), 1..8)
|
||||
.prop_map(|c| PathAttribute::Communities(CommunitiesPathAttribute(c))),
|
||||
),
|
||||
proptest::option::of(mp_reach(session).prop_map(PathAttribute::MpReachNlri)),
|
||||
proptest::option::of(mp_unreach(session).prop_map(PathAttribute::MpUnreachNlri)),
|
||||
proptest::option::of(
|
||||
vec(extended_community(), 1..6).prop_map(|extended_communities| {
|
||||
PathAttribute::ExtendedCommunities(ExtendedCommunitiesPathAttribute {
|
||||
extended_communities,
|
||||
})
|
||||
}),
|
||||
),
|
||||
// AS4_PATH is four octets wide, and only a two-octet speaker sends
|
||||
// it.
|
||||
proptest::option::of(as_path(any::<u32>().boxed()).prop_map(PathAttribute::As4Path))
|
||||
.prop_map(move |as4_path| as4_path.filter(|_| !session.four_octet_asn)),
|
||||
proptest::option::of(vec(any::<(u32, u32, u32)>(), 1..6).prop_map(|communities| {
|
||||
PathAttribute::LargeCommunities(LargeCommunitiesPathAttribute {
|
||||
communities: communities
|
||||
.into_iter()
|
||||
.map(|(global_admin, data_1, data_2)| LargeCommunity {
|
||||
global_admin,
|
||||
data_1,
|
||||
data_2,
|
||||
})
|
||||
.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;
|
||||
attributes.extend(
|
||||
[
|
||||
med,
|
||||
atomic,
|
||||
aggregator,
|
||||
communities,
|
||||
reach,
|
||||
unreach,
|
||||
extended,
|
||||
as4,
|
||||
large,
|
||||
otc,
|
||||
]
|
||||
.into_iter()
|
||||
.flatten(),
|
||||
);
|
||||
attributes.extend(reflected);
|
||||
attributes.extend(unknown);
|
||||
attributes
|
||||
})
|
||||
}
|
||||
|
||||
fn update(session: Session) -> impl Strategy<Value = UpdateMessage> {
|
||||
(
|
||||
nlri(session, IpVersion::Ipv4, Safi::UNICAST),
|
||||
path_attributes(session),
|
||||
nlri(session, IpVersion::Ipv4, Safi::UNICAST),
|
||||
)
|
||||
.prop_map(
|
||||
|(withdrawn_routes, path_attributes, announced_routes)| UpdateMessage {
|
||||
withdrawn_routes,
|
||||
path_attributes,
|
||||
announced_routes,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Capability codes that have their own variant.
|
||||
const KNOWN_CAPABILITIES: [u8; 13] = [1, 2, 3, 5, 6, 7, 8, 9, 64, 65, 69, 70, 71];
|
||||
|
||||
fn capability() -> impl Strategy<Value = Capability> {
|
||||
let orf = (
|
||||
any::<u8>().prop_map(OrfType),
|
||||
prop_oneof![
|
||||
Just(SendReceive::Receive),
|
||||
Just(SendReceive::Send),
|
||||
Just(SendReceive::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)
|
||||
];
|
||||
prop_oneof![
|
||||
(afi(), safi()).prop_map(|(afi, safi)| Capability::MultiProtocol { afi, safi }),
|
||||
Just(Capability::RouteRefresh),
|
||||
vec((afi(), safi(), vec(orf, 0..4)), 0..3).prop_map(|families| {
|
||||
Capability::OutboundRouteFilter(
|
||||
families
|
||||
.into_iter()
|
||||
.map(|(afi, safi, orfs)| OrfFamily { afi, safi, orfs })
|
||||
.collect(),
|
||||
)
|
||||
}),
|
||||
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 })
|
||||
.collect(),
|
||||
}),
|
||||
Just(Capability::ExtendedMessage),
|
||||
(any::<u8>(), afi()).prop_map(|(header, afi)| Capability::BgpSec {
|
||||
header: BgpSecHeader(header),
|
||||
afi,
|
||||
}),
|
||||
vec((afi(), safi(), any::<u8>()), 1..4).prop_map(|entries| {
|
||||
Capability::MultiLabel {
|
||||
entries: entries
|
||||
.into_iter()
|
||||
.map(|(afi, safi, count)| MultiLabelEntry { afi, safi, count })
|
||||
.collect(),
|
||||
}
|
||||
}),
|
||||
any::<u8>().prop_map(|role| Capability::BgpRole {
|
||||
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)| {
|
||||
Capability::GracefulRestart {
|
||||
restart_state,
|
||||
notification,
|
||||
restart_time,
|
||||
entries: entries
|
||||
.into_iter()
|
||||
.map(|(afi, safi, preserve_forwarding)| GracefulRestartEntry {
|
||||
afi,
|
||||
safi,
|
||||
preserve_forwarding,
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}),
|
||||
any::<u32>().prop_map(|asn| Capability::FourByteAsn { asn }),
|
||||
vec((afi(), safi(), add_path), 1..4).prop_map(|entries| Capability::AddPath {
|
||||
entries: entries
|
||||
.into_iter()
|
||||
.map(|(afi, safi, send_recv)| AddPathEntry {
|
||||
afi,
|
||||
safi,
|
||||
send_recv,
|
||||
})
|
||||
.collect(),
|
||||
}),
|
||||
Just(Capability::EnhancedRouteRefresh),
|
||||
vec((afi(), safi(), any::<u8>(), 0u32..(1 << 24)), 0..4).prop_map(|entries| {
|
||||
Capability::LongLivedGracefulRestart(
|
||||
entries
|
||||
.into_iter()
|
||||
.map(
|
||||
|(afi, safi, flags, stale_time)| LongLivedGracefulRestartEntry {
|
||||
afi,
|
||||
safi,
|
||||
flags,
|
||||
stale_time,
|
||||
},
|
||||
)
|
||||
.collect(),
|
||||
)
|
||||
}),
|
||||
(
|
||||
any::<u8>().prop_filter("a code without a variant", |code| {
|
||||
!KNOWN_CAPABILITIES.contains(code)
|
||||
}),
|
||||
vec(any::<u8>(), 0..20)
|
||||
)
|
||||
.prop_map(|(type_code, value)| Capability::Unknown { type_code, value }),
|
||||
]
|
||||
}
|
||||
|
||||
fn open() -> impl Strategy<Value = OpenMessage> {
|
||||
(
|
||||
any::<u16>(),
|
||||
any::<u16>(),
|
||||
any::<[u8; 4]>(),
|
||||
// Enough to go past 255 octets of options now and then, which
|
||||
// switches to the extended length of RFC 9072.
|
||||
vec(vec(capability(), 0..10), 0..8),
|
||||
)
|
||||
.prop_map(|(asn, hold_time, identifier, options)| OpenMessage {
|
||||
version: 4,
|
||||
asn,
|
||||
hold_time,
|
||||
identifier: Ipv4Addr::from(identifier),
|
||||
options: options.into_iter().map(OpenOption::Capabilities).collect(),
|
||||
})
|
||||
}
|
||||
|
||||
fn notification() -> impl Strategy<Value = NotificationMessage> {
|
||||
// Any code and subcode: a peer may send ones we have no name for.
|
||||
(any::<u8>(), any::<u8>(), vec(any::<u8>(), 0..40)).prop_map(|(code, subcode, data)| {
|
||||
NotificationMessage {
|
||||
error: BgpError::from_code_subcode(code, subcode),
|
||||
data,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn message(session: Session) -> impl Strategy<Value = Message> {
|
||||
prop_oneof![
|
||||
open().prop_map(Message::Open),
|
||||
update(session).prop_map(Message::Update),
|
||||
notification().prop_map(Message::Notification),
|
||||
Just(Message::KeepAlive),
|
||||
(afi(), any::<u8>(), safi()).prop_map(|(afi, subtype, safi)| {
|
||||
Message::RouteRefresh(RouteRefreshMessage { afi, subtype, safi })
|
||||
}),
|
||||
]
|
||||
}
|
||||
|
||||
/// A session, and a well-formed message that fits it.
|
||||
fn session_and_message() -> impl Strategy<Value = (Session, Message)> {
|
||||
session().prop_flat_map(|session| (Just(session), message(session)))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Properties
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Decodes `wire` and checks what must hold for anything the decoder
|
||||
/// accepts: it can be shown and written, and unless the decoder had to
|
||||
/// repair it, it reads back as itself.
|
||||
fn check_decoding(ctx: &ParserContext, wire: &[u8]) -> Result<(), TestCaseError> {
|
||||
let Ok(decoded) = Message::decode_with_report(ctx, wire) else {
|
||||
return Ok(());
|
||||
};
|
||||
let (msg, repairs) = (decoded.message, decoded.update_errors);
|
||||
// Display of what a peer sent must not be able to panic: it is logged.
|
||||
let _ = format!("{msg:?} {repairs:?}");
|
||||
if let Message::Update(update) = &msg {
|
||||
for prefix in update
|
||||
.withdrawn_routes
|
||||
.iter()
|
||||
.chain(&update.announced_routes)
|
||||
{
|
||||
let _ = prefix.to_string();
|
||||
}
|
||||
for attribute in &update.path_attributes {
|
||||
match attribute {
|
||||
PathAttribute::MpReachNlri(mp) => {
|
||||
mp.prefixes.iter().for_each(|p| drop(p.to_string()))
|
||||
}
|
||||
PathAttribute::MpUnreachNlri(mp) => {
|
||||
mp.prefixes.iter().for_each(|p| drop(p.to_string()))
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut rewritten = BytesMut::new();
|
||||
match msg.to_wire(ctx, &mut rewritten) {
|
||||
Ok(()) => {}
|
||||
// Withdrawing what a full-size UPDATE announced can need more room
|
||||
// than one message has. The result is acted on, never sent.
|
||||
Err(EncodeError::TooLong { .. }) if !repairs.is_empty() => return Ok(()),
|
||||
Err(e) => {
|
||||
return Err(TestCaseError::fail(format!(
|
||||
"accepted but cannot be written: {e}"
|
||||
)));
|
||||
}
|
||||
}
|
||||
if repairs.is_empty() {
|
||||
let again = Message::decode_with_report(ctx, &rewritten)
|
||||
.map_err(|e| TestCaseError::fail(format!("written but cannot be read: {e}")))?;
|
||||
prop_assert!(again.update_errors.is_empty(), "{:?}", again.update_errors);
|
||||
prop_assert_eq!(again.message, msg);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Writes a generated message. `None` when it is too big for one BGP
|
||||
/// message, or some part of it for its length field, which the generators
|
||||
/// can produce and the encoder must refuse.
|
||||
fn written(ctx: &ParserContext, msg: &Message) -> Option<BytesMut> {
|
||||
let mut wire = BytesMut::new();
|
||||
match msg.to_wire(ctx, &mut wire) {
|
||||
Ok(()) => Some(wire),
|
||||
Err(EncodeError::TooLong { .. }) => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn framed(msg_type: u8, body: &[u8]) -> Vec<u8> {
|
||||
let mut wire = vec![0xff; 16];
|
||||
let length = u16::try_from(usize::from(BGP_HEADER_LEN) + body.len()).unwrap();
|
||||
wire.extend_from_slice(&length.to_be_bytes());
|
||||
wire.push(msg_type);
|
||||
wire.extend_from_slice(body);
|
||||
wire
|
||||
}
|
||||
|
||||
/// Ways to damage a message.
|
||||
#[derive(Debug, Clone)]
|
||||
enum Damage {
|
||||
Set { at: prop::sample::Index, to: u8 },
|
||||
Remove { at: prop::sample::Index },
|
||||
Insert { at: prop::sample::Index, byte: u8 },
|
||||
Truncate { to: prop::sample::Index },
|
||||
}
|
||||
|
||||
fn damage() -> impl Strategy<Value = Damage> {
|
||||
prop_oneof![
|
||||
(any::<prop::sample::Index>(), any::<u8>()).prop_map(|(at, to)| Damage::Set { at, to }),
|
||||
any::<prop::sample::Index>().prop_map(|at| Damage::Remove { at }),
|
||||
(any::<prop::sample::Index>(), any::<u8>())
|
||||
.prop_map(|(at, byte)| Damage::Insert { at, byte }),
|
||||
any::<prop::sample::Index>().prop_map(|to| Damage::Truncate { to }),
|
||||
]
|
||||
}
|
||||
|
||||
/// Applies `damage` to the body of `wire` and makes the header's length
|
||||
/// right again, so that the damage is the body parsers' problem.
|
||||
fn damaged(wire: &[u8], damage: &[Damage]) -> Vec<u8> {
|
||||
let header = usize::from(BGP_HEADER_LEN);
|
||||
let msg_type = wire[header - 1];
|
||||
let mut body = wire[header..].to_vec();
|
||||
for d in damage {
|
||||
match d {
|
||||
Damage::Set { at, to } if !body.is_empty() => {
|
||||
let i = at.index(body.len());
|
||||
body[i] = *to;
|
||||
}
|
||||
Damage::Remove { at } if !body.is_empty() => {
|
||||
body.remove(at.index(body.len()));
|
||||
}
|
||||
Damage::Insert { at, byte } => body.insert(at.index(body.len() + 1), *byte),
|
||||
Damage::Truncate { to } if !body.is_empty() => body.truncate(to.index(body.len())),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
body.truncate(usize::from(BGP_MAX_MSG_LEN - BGP_HEADER_LEN));
|
||||
framed(msg_type, &body)
|
||||
}
|
||||
|
||||
/// 2000 cases a property, which takes about a second. For a longer hunt
|
||||
/// set `PROPTEST_CASES`, e.g. `PROPTEST_CASES=1000000 cargo test --release
|
||||
/// -p bgp-packet --test properties`.
|
||||
fn config() -> ProptestConfig {
|
||||
let cases = std::env::var("PROPTEST_CASES")
|
||||
.ok()
|
||||
.and_then(|cases| cases.parse().ok())
|
||||
.unwrap_or(2000);
|
||||
ProptestConfig {
|
||||
cases,
|
||||
// Failures are not saved to a file: one that is found becomes a
|
||||
// named test next to the code it broke.
|
||||
failure_persistence: None,
|
||||
..ProptestConfig::default()
|
||||
}
|
||||
}
|
||||
|
||||
proptest! {
|
||||
#![proptest_config(config())]
|
||||
|
||||
#[test]
|
||||
fn a_message_that_can_be_written_reads_back_equal((session, msg) in session_and_message()) {
|
||||
let ctx = session.ctx();
|
||||
let Some(wire) = written(&ctx, &msg) else {
|
||||
return Ok(());
|
||||
};
|
||||
let decoded = Message::decode_with_report(&ctx, &wire)
|
||||
.map_err(|e| TestCaseError::fail(format!("{e}")))?;
|
||||
// What we write is well formed: nothing for the decoder to repair.
|
||||
prop_assert!(decoded.update_errors.is_empty(), "{:?}", decoded.update_errors);
|
||||
prop_assert_eq!(&decoded.message, &msg);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn noise_is_never_more_than_an_error(session in session(), bytes in vec(any::<u8>(), 0..200)) {
|
||||
check_decoding(&session.ctx(), &bytes)?;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn noise_behind_a_valid_header_is_never_more_than_an_error(
|
||||
session in session(),
|
||||
msg_type in 0u8..7,
|
||||
body in vec(any::<u8>(), 0..300),
|
||||
) {
|
||||
check_decoding(&session.ctx(), &framed(msg_type, &body))?;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_damaged_message_is_never_more_than_an_error(
|
||||
(session, msg) in session_and_message(),
|
||||
damage in vec(damage(), 1..4),
|
||||
) {
|
||||
let ctx = session.ctx();
|
||||
let Some(wire) = written(&ctx, &msg) else {
|
||||
return Ok(());
|
||||
};
|
||||
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.
|
||||
#[test]
|
||||
fn noise_is_never_more_than_an_error_for_the_parts(
|
||||
session in session(),
|
||||
bytes in vec(any::<u8>(), 0..120),
|
||||
) {
|
||||
let ctx = session.ctx();
|
||||
let _ = OpenMessage::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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "bgp-primitives"
|
||||
|
||||
description = "Primitive types for working with routing information"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
homepage.workspace = true
|
||||
license.workspace = true
|
||||
@@ -9,14 +9,11 @@ repository.workspace = true
|
||||
version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
bitfield.workspace = true
|
||||
bytes.workspace = true
|
||||
eyre.workspace = true
|
||||
nom.workspace = true
|
||||
serde.workspace = true
|
||||
serde_repr.workspace = true
|
||||
thiserror.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
proptest.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
strum.workspace = true
|
||||
@@ -1,174 +1,80 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
use serde::Serialize;
|
||||
use serde_repr::Serialize_repr;
|
||||
use thiserror::Error;
|
||||
use eyre::bail;
|
||||
use serde_repr::{Deserialize_repr, Serialize_repr};
|
||||
|
||||
/// An address family number this implementation has no name for.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
|
||||
#[error("unknown address family identifier {0}")]
|
||||
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, Deserialize_repr)]
|
||||
#[repr(u16)]
|
||||
pub enum IpVersion {
|
||||
pub enum AddressFamilyId {
|
||||
Ipv4 = 1,
|
||||
Ipv6 = 2,
|
||||
}
|
||||
|
||||
impl IpVersion {
|
||||
/// How many bits an address of this version has.
|
||||
pub fn bits(self) -> u8 {
|
||||
match self {
|
||||
Self::Ipv4 => 32,
|
||||
Self::Ipv6 => 128,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<u16> for IpVersion {
|
||||
type Error = UnknownAfi;
|
||||
impl TryFrom<u16> for AddressFamilyId {
|
||||
type Error = eyre::Error;
|
||||
|
||||
fn try_from(value: u16) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
x if x == Self::Ipv4 as u16 => Ok(Self::Ipv4),
|
||||
x if x == Self::Ipv6 as u16 => Ok(Self::Ipv6),
|
||||
other => Err(UnknownAfi(other)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for IpVersion {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
IpVersion::Ipv4 => write!(f, "Ipv4"),
|
||||
IpVersion::Ipv6 => write!(f, "Ipv6"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An Address Family Identifier as it appears on the wire (RFC 4760).
|
||||
///
|
||||
/// Any number is a valid value: a peer may name a family this implementation
|
||||
/// has never heard of, and that must be readable so it can be ignored. Which
|
||||
/// families are *supported* is a separate question, answered where routes
|
||||
/// are stored.
|
||||
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct Afi(pub u16);
|
||||
|
||||
impl Afi {
|
||||
pub const IPV4: Self = Self(1);
|
||||
pub const IPV6: Self = Self(2);
|
||||
pub const L2VPN: Self = Self(25);
|
||||
pub const BGP_LS: Self = Self(16388);
|
||||
}
|
||||
|
||||
impl From<IpVersion> for Afi {
|
||||
fn from(version: IpVersion) -> Self {
|
||||
Self(version as u16)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Afi> for IpVersion {
|
||||
type Error = UnknownAfi;
|
||||
|
||||
fn try_from(afi: Afi) -> Result<Self, Self::Error> {
|
||||
Self::try_from(afi.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// A Subsequent Address Family Identifier as it appears on the wire. Like
|
||||
/// [`Afi`], any number is a valid value.
|
||||
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct Safi(pub u8);
|
||||
|
||||
impl Safi {
|
||||
pub const UNICAST: Self = Self(1);
|
||||
pub const MULTICAST: Self = Self(2);
|
||||
pub const MPLS_LABEL: Self = Self(4);
|
||||
pub const EVPN: Self = Self(70);
|
||||
pub const BGP_LS: Self = Self(71);
|
||||
pub const MPLS_VPN: Self = Self(128);
|
||||
pub const MULTICAST_MPLS_VPN: Self = Self(129);
|
||||
pub const FLOWSPEC: Self = Self(133);
|
||||
}
|
||||
|
||||
/// An address family: the pair that says what kind of routes a capability
|
||||
/// or an MP_REACH_NLRI is about.
|
||||
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize)]
|
||||
pub struct AfiSafi {
|
||||
pub afi: Afi,
|
||||
pub safi: Safi,
|
||||
}
|
||||
|
||||
impl AfiSafi {
|
||||
pub const IPV4_UNICAST: Self = Self::new(Afi::IPV4, Safi::UNICAST);
|
||||
pub const IPV6_UNICAST: Self = Self::new(Afi::IPV6, Safi::UNICAST);
|
||||
|
||||
pub const fn new(afi: Afi, safi: Safi) -> Self {
|
||||
Self { afi, safi }
|
||||
}
|
||||
|
||||
/// The unicast family of an IP version.
|
||||
pub fn unicast(version: IpVersion) -> Self {
|
||||
Self::new(version.into(), Safi::UNICAST)
|
||||
}
|
||||
|
||||
/// The usual name of the family, if it has one here.
|
||||
fn name(self) -> Option<&'static str> {
|
||||
Some(match (self.afi, self.safi) {
|
||||
(Afi::IPV4, Safi::UNICAST) => "IPv4 unicast",
|
||||
(Afi::IPV6, Safi::UNICAST) => "IPv6 unicast",
|
||||
(Afi::IPV4, Safi::MULTICAST) => "IPv4 multicast",
|
||||
(Afi::IPV6, Safi::MULTICAST) => "IPv6 multicast",
|
||||
(Afi::IPV4, Safi::MPLS_LABEL) => "IPv4 labeled unicast",
|
||||
(Afi::IPV6, Safi::MPLS_LABEL) => "IPv6 labeled unicast",
|
||||
(Afi::IPV4, Safi::MPLS_VPN) => "VPNv4 unicast",
|
||||
(Afi::IPV6, Safi::MPLS_VPN) => "VPNv6 unicast",
|
||||
(Afi::IPV4, Safi::FLOWSPEC) => "IPv4 flowspec",
|
||||
(Afi::IPV6, Safi::FLOWSPEC) => "IPv6 flowspec",
|
||||
(Afi::L2VPN, Safi::EVPN) => "L2VPN EVPN",
|
||||
(Afi::BGP_LS, Safi::BGP_LS) => "BGP-LS",
|
||||
_ => return None,
|
||||
Ok(match value {
|
||||
x if x == Self::Ipv4 as u16 => Self::Ipv4,
|
||||
x if x == Self::Ipv6 as u16 => Self::Ipv6,
|
||||
other => bail!("Unknown AddressFamily: {}", other),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for AfiSafi {
|
||||
impl Display for AddressFamilyId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self.name() {
|
||||
Some(name) => f.write_str(name),
|
||||
None => write!(f, "AFI {} SAFI {}", self.afi.0, self.safi.0),
|
||||
match self {
|
||||
AddressFamilyId::Ipv4 => write!(f, "Ipv4"),
|
||||
AddressFamilyId::Ipv6 => write!(f, "Ipv6"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
/// Represents a Subsequent Address Family Identifier.
|
||||
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, Serialize_repr, Deserialize_repr)]
|
||||
#[repr(u8)]
|
||||
pub enum SubsequentAfi {
|
||||
Unicast = 1,
|
||||
Multicast = 2,
|
||||
NlriWithMpls = 4,
|
||||
MplsLabelledVpn = 128,
|
||||
MulticastMplsVpn = 129,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn any_family_is_a_value_and_the_common_ones_have_names() {
|
||||
assert_eq!(AfiSafi::IPV6_UNICAST.to_string(), "IPv6 unicast");
|
||||
assert_eq!(
|
||||
AfiSafi::new(Afi::L2VPN, Safi::EVPN).to_string(),
|
||||
"L2VPN EVPN"
|
||||
);
|
||||
assert_eq!(
|
||||
AfiSafi::new(Afi(999), Safi(7)).to_string(),
|
||||
"AFI 999 SAFI 7"
|
||||
);
|
||||
assert_eq!(AfiSafi::unicast(IpVersion::Ipv4), AfiSafi::IPV4_UNICAST);
|
||||
}
|
||||
impl TryFrom<u8> for SubsequentAfi {
|
||||
type Error = eyre::Error;
|
||||
|
||||
#[test]
|
||||
fn only_ipv4_and_ipv6_are_ip_versions() {
|
||||
assert_eq!(IpVersion::try_from(Afi::IPV6), Ok(IpVersion::Ipv6));
|
||||
assert_eq!(IpVersion::try_from(Afi::L2VPN), Err(UnknownAfi(25)));
|
||||
assert_eq!(IpVersion::try_from(3u16), Err(UnknownAfi(3)));
|
||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||
Ok(match value {
|
||||
x if x == Self::Unicast as u8 => Self::Unicast,
|
||||
x if x == Self::Multicast as u8 => Self::Multicast,
|
||||
x if x == Self::NlriWithMpls as u8 => Self::NlriWithMpls,
|
||||
x if x == Self::MplsLabelledVpn as u8 => Self::MplsLabelledVpn,
|
||||
x if x == Self::MulticastMplsVpn as u8 => Self::MulticastMplsVpn,
|
||||
other => bail!("Unknown SubsequentAfi: {}", other),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// The implementation try_from for u16 just reuses the u8 implementation since
|
||||
/// we don't have any cases where the SAFI is longer than u8::MAX yet.
|
||||
impl TryFrom<u16> for SubsequentAfi {
|
||||
type Error = eyre::Error;
|
||||
|
||||
fn try_from(value: u16) -> Result<Self, Self::Error> {
|
||||
TryFrom::<u8>::try_from(value as u8)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for SubsequentAfi {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
SubsequentAfi::Unicast => write!(f, "Unicast"),
|
||||
SubsequentAfi::Multicast => write!(f, "Multicast"),
|
||||
SubsequentAfi::NlriWithMpls => write!(f, "NlriWithMpls"),
|
||||
SubsequentAfi::MplsLabelledVpn => write!(f, "MplsLabelledVpn"),
|
||||
SubsequentAfi::MulticastMplsVpn => write!(f, "MulticastMplsVpn"),
|
||||
}
|
||||
}
|
||||
}
|
||||
+117
-281
@@ -2,111 +2,38 @@ use std::fmt::Display;
|
||||
use std::net::{Ipv4Addr, Ipv6Addr};
|
||||
use std::str::FromStr;
|
||||
|
||||
use serde::Serialize;
|
||||
use thiserror::Error;
|
||||
use bytes::{Buf, BufMut, BytesMut};
|
||||
use eyre::{Result, bail};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::constants::IpVersion;
|
||||
use crate::constants::AddressFamilyId;
|
||||
use crate::parser::{ParserContext, ToWireError};
|
||||
|
||||
/// Why something is not a valid prefix.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Error)]
|
||||
pub enum PrefixError {
|
||||
#[error("prefix {0:?} is not in address/length form")]
|
||||
Format(String),
|
||||
#[error("prefix {0:?} has an address that is neither IPv4 nor IPv6")]
|
||||
Address(String),
|
||||
#[error("prefix length {length} is longer than the {max} bits of an address")]
|
||||
LengthTooLong { length: u8, max: u8 },
|
||||
#[error("prefix length {length} needs {needed} address bytes, got {got}")]
|
||||
TooFewBytes {
|
||||
length: u8,
|
||||
needed: usize,
|
||||
got: usize,
|
||||
},
|
||||
}
|
||||
|
||||
/// An IPv4 or IPv6 prefix.
|
||||
///
|
||||
/// It is a small value with no allocation behind it, cheap to copy, compare
|
||||
/// and hash: a routing table holds millions of them and looks them up by
|
||||
/// 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)]
|
||||
/// IpPrefix represents some IP address prefix, for a specific AddressFamilyId.
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Hash, Serialize, Deserialize)]
|
||||
pub struct IpPrefix {
|
||||
address_family: IpVersion,
|
||||
length: u8,
|
||||
/// The address, left-aligned, zero past `length` bits.
|
||||
address: [u8; 16],
|
||||
pub address_family: AddressFamilyId,
|
||||
pub prefix: Vec<u8>,
|
||||
pub length: u8,
|
||||
}
|
||||
|
||||
impl IpPrefix {
|
||||
/// Makes a prefix from the leading bytes of its address. `prefix` must
|
||||
/// hold at least the bytes that `length` bits cover; anything past
|
||||
/// those bits is ignored (RFC 4271 §4.3: such bits are irrelevant).
|
||||
pub fn new(
|
||||
address_family: IpVersion,
|
||||
prefix: impl AsRef<[u8]>,
|
||||
length: u8,
|
||||
) -> Result<Self, PrefixError> {
|
||||
let prefix = prefix.as_ref();
|
||||
let max = address_family.bits();
|
||||
if length > max {
|
||||
return Err(PrefixError::LengthTooLong { length, max });
|
||||
}
|
||||
let needed = usize::from(length.div_ceil(8));
|
||||
if prefix.len() < needed {
|
||||
return Err(PrefixError::TooFewBytes {
|
||||
length,
|
||||
needed,
|
||||
got: prefix.len(),
|
||||
});
|
||||
}
|
||||
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);
|
||||
pub fn new(address_family: AddressFamilyId, prefix: Vec<u8>, length: u8) -> Result<Self> {
|
||||
// Ensure that the prefix we are given contains the right number of bytes corresponding to the prefix length.
|
||||
if prefix.len() < ((length + 7) / 8).into() {
|
||||
bail!(
|
||||
"Mismatched prefix {:?} for given prefix length: {}",
|
||||
prefix,
|
||||
length
|
||||
);
|
||||
}
|
||||
Ok(Self {
|
||||
address_family,
|
||||
prefix,
|
||||
length,
|
||||
address,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn address_family(&self) -> IpVersion {
|
||||
self.address_family
|
||||
}
|
||||
|
||||
/// The prefix length in bits.
|
||||
pub fn length(&self) -> u8 {
|
||||
self.length
|
||||
}
|
||||
|
||||
/// The bytes the prefix length covers, as they go on the wire: none for
|
||||
/// a default route, three for a /24.
|
||||
pub fn bytes(&self) -> &[u8] {
|
||||
&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.
|
||||
@@ -114,96 +41,110 @@ impl IpPrefix {
|
||||
if self.address_family != other.address_family || other.length < self.length {
|
||||
return false;
|
||||
}
|
||||
let full_bytes = usize::from(self.length / 8);
|
||||
if self.address[..full_bytes] != other.address[..full_bytes] {
|
||||
let full_bytes = (self.length / 8) as usize;
|
||||
if self.prefix[..full_bytes] != other.prefix[..full_bytes] {
|
||||
return false;
|
||||
}
|
||||
let rem_bits = self.length % 8;
|
||||
if rem_bits == 0 {
|
||||
return true;
|
||||
}
|
||||
let mask = high_bits_mask(rem_bits);
|
||||
(self.address[full_bytes] & mask) == (other.address[full_bytes] & mask)
|
||||
let mask = u8::MAX << (8 - rem_bits);
|
||||
(self.prefix[full_bytes] & mask) == (other.prefix[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.
|
||||
///
|
||||
/// Whether the identifier is on the wire is a matter of what the session
|
||||
/// negotiated, not of its value. Where it is not, it is zero here.
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, Serialize)]
|
||||
pub struct Nlri {
|
||||
pub prefix: IpPrefix,
|
||||
pub path_id: u32,
|
||||
}
|
||||
|
||||
impl Nlri {
|
||||
/// `prefixes` as they are named on a session without ADD-PATH.
|
||||
pub fn plain(prefixes: impl IntoIterator<Item = IpPrefix>) -> Vec<Self> {
|
||||
prefixes.into_iter().map(Self::from).collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<IpPrefix> for Nlri {
|
||||
fn from(prefix: IpPrefix) -> Self {
|
||||
Self { prefix, path_id: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
/// So that a list of these compares with a list of plain prefixes.
|
||||
impl PartialEq<IpPrefix> for Nlri {
|
||||
fn eq(&self, prefix: &IpPrefix) -> bool {
|
||||
self.path_id == 0 && self.prefix == *prefix
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Nlri {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self.path_id {
|
||||
0 => write!(f, "{}", self.prefix),
|
||||
id => write!(f, "{} (path {id})", self.prefix),
|
||||
pub fn to_wire(&self, _: &ParserContext, out: &mut BytesMut) -> Result<(), ToWireError> {
|
||||
// Verify that there is enough space to write the IpPrefix.
|
||||
if out.remaining() < (self.prefix.len() + 1) {
|
||||
Err(ToWireError::OutBufferOverflow)?;
|
||||
}
|
||||
|
||||
// Write length and prefix.
|
||||
out.put_u8(self.length);
|
||||
out.put(self.prefix.as_slice());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for IpPrefix {
|
||||
type Error = PrefixError;
|
||||
type Error = eyre::Error;
|
||||
|
||||
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
||||
let format = || PrefixError::Format(value.to_string());
|
||||
let address = || PrefixError::Address(value.to_string());
|
||||
let (addr, length) = value.split_once('/').ok_or_else(format)?;
|
||||
let length: u8 = length.parse().map_err(|_| format())?;
|
||||
let parts: Vec<&str> = value.split("/").collect();
|
||||
|
||||
let (afi, octets) = if addr.contains(':') {
|
||||
let addr = Ipv6Addr::from_str(addr).map_err(|_| address())?;
|
||||
(IpVersion::Ipv6, addr.octets().to_vec())
|
||||
if parts.len() != 2 {
|
||||
bail!(
|
||||
"Expected IpPrefix in format prefix/length but got: {}",
|
||||
value
|
||||
);
|
||||
}
|
||||
|
||||
let length: u8 = u8::from_str_radix(parts[1], 10).map_err(eyre::Error::from)?;
|
||||
let mut octets;
|
||||
let afi: AddressFamilyId;
|
||||
|
||||
if parts[0].contains(":") {
|
||||
afi = AddressFamilyId::Ipv6;
|
||||
let addr: Ipv6Addr = Ipv6Addr::from_str(parts[0]).map_err(eyre::Error::from)?;
|
||||
octets = addr.octets().to_vec();
|
||||
} else if parts[0].contains(".") {
|
||||
afi = AddressFamilyId::Ipv4;
|
||||
let addr: Ipv4Addr = Ipv4Addr::from_str(parts[0]).map_err(eyre::Error::from)?;
|
||||
octets = addr.octets().to_vec();
|
||||
} else {
|
||||
let addr = Ipv4Addr::from_str(addr).map_err(|_| address())?;
|
||||
(IpVersion::Ipv4, addr.octets().to_vec())
|
||||
};
|
||||
IpPrefix::new(afi, octets, length)
|
||||
bail!("Could not figure out address type")
|
||||
}
|
||||
|
||||
// Truncate the octets we have to the right number of bytes to match the prefix length..
|
||||
if length % 8 == 0 {
|
||||
// We can cleanly truncate the number of bytes since we are at a byte boundary.
|
||||
octets.truncate((length / 8).into());
|
||||
} else {
|
||||
// We need to keep length % 8 bits of the last byte.
|
||||
let num_bytes = (length / 8) + 1;
|
||||
let mask = u8::MAX << (8 - (length % 8));
|
||||
octets.truncate(num_bytes.into());
|
||||
// Fix up the last byte.
|
||||
let last_pos = octets.len() - 1;
|
||||
octets[last_pos] &= mask;
|
||||
}
|
||||
|
||||
Ok(IpPrefix {
|
||||
address_family: afi,
|
||||
prefix: octets,
|
||||
length,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for IpPrefix {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self.address_family {
|
||||
IpVersion::Ipv4 => {
|
||||
let [a, b, c, d, ..] = self.address;
|
||||
write!(f, "{}/{}", Ipv4Addr::new(a, b, c, d), self.length)
|
||||
AddressFamilyId::Ipv4 => {
|
||||
// Pad the prefix with 0 bytes if it's less than 4 bytes long.
|
||||
let bytes = &mut self.prefix.clone();
|
||||
if bytes.len() < 4 {
|
||||
bytes.extend(std::iter::repeat(0).take(4 - bytes.len()));
|
||||
}
|
||||
let four_bytes: [u8; 4] = bytes
|
||||
.as_slice()
|
||||
.try_into()
|
||||
.map_err(|_| std::fmt::Error {})?;
|
||||
let ipv4_addr = Ipv4Addr::from(four_bytes);
|
||||
write!(f, "{}/{}", ipv4_addr, self.length)
|
||||
}
|
||||
IpVersion::Ipv6 => {
|
||||
write!(f, "{}/{}", Ipv6Addr::from(self.address), self.length)
|
||||
AddressFamilyId::Ipv6 => {
|
||||
let bytes = &mut self.prefix.clone();
|
||||
if bytes.len() < 16 {
|
||||
bytes.extend(std::iter::repeat(0).take(16 - bytes.len()));
|
||||
}
|
||||
let sixteen_bytes: [u8; 16] = bytes
|
||||
.as_slice()
|
||||
.try_into()
|
||||
.map_err(|_| std::fmt::Error {})?;
|
||||
let ipv6_addr = Ipv6Addr::from(sixteen_bytes);
|
||||
write!(f, "{}/{}", ipv6_addr, self.length)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -211,11 +152,9 @@ impl Display for IpPrefix {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::constants::IpVersion;
|
||||
use crate::constants::AddressFamilyId;
|
||||
|
||||
use proptest::prelude::*;
|
||||
|
||||
use super::{IpPrefix, PrefixError};
|
||||
use super::IpPrefix;
|
||||
|
||||
macro_rules! verify_roundtrip {
|
||||
($name:ident, $prefix_str:expr, $afi:expr, $bytes:expr, $length:expr) => {
|
||||
@@ -223,9 +162,9 @@ mod tests {
|
||||
fn $name() {
|
||||
let ip_prefix = IpPrefix::try_from($prefix_str).unwrap();
|
||||
|
||||
assert_eq!(ip_prefix.address_family(), $afi);
|
||||
assert_eq!(ip_prefix.bytes(), $bytes);
|
||||
assert_eq!(ip_prefix.length(), $length);
|
||||
assert_eq!(ip_prefix.address_family, $afi);
|
||||
assert_eq!(ip_prefix.prefix, $bytes);
|
||||
assert_eq!(ip_prefix.length, $length);
|
||||
|
||||
let to_str: &str = &ip_prefix.to_string();
|
||||
assert_eq!(IpPrefix::try_from(to_str).unwrap(), ip_prefix);
|
||||
@@ -236,7 +175,7 @@ mod tests {
|
||||
verify_roundtrip!(
|
||||
verify_roundtrip_ipv4_24,
|
||||
"10.1.2.0/24",
|
||||
IpVersion::Ipv4,
|
||||
AddressFamilyId::Ipv4,
|
||||
vec![10, 1, 2],
|
||||
24
|
||||
);
|
||||
@@ -245,7 +184,7 @@ mod tests {
|
||||
verify_roundtrip!(
|
||||
verify_roundtrip_ipv4_19,
|
||||
"10.245.123.0/19",
|
||||
IpVersion::Ipv4,
|
||||
AddressFamilyId::Ipv4,
|
||||
vec![10, 245, 96],
|
||||
19
|
||||
);
|
||||
@@ -254,7 +193,7 @@ mod tests {
|
||||
verify_roundtrip!(
|
||||
verify_roundtrip_ipv4_3,
|
||||
"192.168.1.0/3",
|
||||
IpVersion::Ipv4,
|
||||
AddressFamilyId::Ipv4,
|
||||
vec![192],
|
||||
3
|
||||
);
|
||||
@@ -263,7 +202,7 @@ mod tests {
|
||||
verify_roundtrip!(
|
||||
verify_roundtrip_ipv4_0,
|
||||
"0.0.0.0/0",
|
||||
IpVersion::Ipv4,
|
||||
AddressFamilyId::Ipv4,
|
||||
vec![],
|
||||
0
|
||||
);
|
||||
@@ -271,7 +210,7 @@ mod tests {
|
||||
verify_roundtrip!(
|
||||
verify_roundtrip_ipv6_48,
|
||||
"2001:db8:cafe::/48",
|
||||
IpVersion::Ipv6,
|
||||
AddressFamilyId::Ipv6,
|
||||
vec![32, 1, 13, 184, 202, 254],
|
||||
48
|
||||
);
|
||||
@@ -280,120 +219,11 @@ mod tests {
|
||||
verify_roundtrip!(
|
||||
verify_roundtrip_ipv6_32,
|
||||
"2001:db8:cafe::/32",
|
||||
IpVersion::Ipv6,
|
||||
AddressFamilyId::Ipv6,
|
||||
vec![32, 1, 13, 184],
|
||||
32
|
||||
);
|
||||
|
||||
#[test]
|
||||
fn text_that_is_not_a_prefix_is_rejected_with_the_reason() {
|
||||
for (text, expected) in [
|
||||
("10.0.0.0", PrefixError::Format("10.0.0.0".into())),
|
||||
("10.0.0.0/x", PrefixError::Format("10.0.0.0/x".into())),
|
||||
("10.0.0/8", PrefixError::Address("10.0.0/8".into())),
|
||||
("nonsense/8", PrefixError::Address("nonsense/8".into())),
|
||||
(
|
||||
"10.0.0.0/33",
|
||||
PrefixError::LengthTooLong {
|
||||
length: 33,
|
||||
max: 32,
|
||||
},
|
||||
),
|
||||
(
|
||||
"2001:db8::/129",
|
||||
PrefixError::LengthTooLong {
|
||||
length: 129,
|
||||
max: 128,
|
||||
},
|
||||
),
|
||||
] {
|
||||
assert_eq!(IpPrefix::try_from(text), Err(expected), "{text}");
|
||||
}
|
||||
assert!(IpPrefix::try_from("0.0.0.0/0").is_ok());
|
||||
assert!(IpPrefix::try_from("2001:db8::1/128").is_ok());
|
||||
assert_eq!(
|
||||
IpPrefix::new(IpVersion::Ipv4, vec![10], 16),
|
||||
Err(PrefixError::TooFewBytes {
|
||||
length: 16,
|
||||
needed: 2,
|
||||
got: 1
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_prefix_has_one_representation() {
|
||||
// Extra bytes and stray bits past the length are dropped.
|
||||
let noisy = IpPrefix::new(IpVersion::Ipv4, vec![10, 0xff, 3, 4], 12).unwrap();
|
||||
assert_eq!(noisy, IpPrefix::try_from("10.240.0.0/12").unwrap());
|
||||
assert_eq!(noisy.bytes(), [10, 0xf0]);
|
||||
assert_eq!(
|
||||
IpPrefix::new(IpVersion::Ipv4, vec![0xff; 13], 99),
|
||||
Err(PrefixError::LengthTooLong {
|
||||
length: 99,
|
||||
max: 32
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_prefix_is_a_small_value() {
|
||||
assert!(size_of::<IpPrefix>() <= 20);
|
||||
let prefix = IpPrefix::try_from("10.1.2.0/24").unwrap();
|
||||
let copy = prefix;
|
||||
assert_eq!(copy, prefix);
|
||||
assert_eq!(prefix.bytes(), [10, 1, 2]);
|
||||
assert_eq!(IpPrefix::try_from("0.0.0.0/0").unwrap().bytes(), [0u8; 0]);
|
||||
}
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
fn text_round_trips(version: bool, bytes: [u8; 16], length 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 text = prefix.to_string();
|
||||
prop_assert_eq!(IpPrefix::try_from(text.as_str()), Ok(prefix));
|
||||
prop_assert!(prefix.contains(&prefix));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_that_is_noise_is_an_error_or_a_prefix_that_round_trips(text in "\\PC{0,40}") {
|
||||
if let Ok(prefix) = IpPrefix::try_from(text.as_str()) {
|
||||
let shown = prefix.to_string();
|
||||
prop_assert_eq!(IpPrefix::try_from(shown.as_str()), Ok(prefix));
|
||||
}
|
||||
}
|
||||
|
||||
/// 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(
|
||||
a: [u8; 4], a_len in 0u8..=32, b: [u8; 4], b_len in 0u8..=32,
|
||||
) {
|
||||
let outer = IpPrefix::new(IpVersion::Ipv4, a, a_len).unwrap();
|
||||
let inner = IpPrefix::new(IpVersion::Ipv4, b, b_len).unwrap();
|
||||
let mask = if a_len == 0 { 0 } else { u32::MAX << (32 - a_len) };
|
||||
let expected = b_len >= a_len
|
||||
&& (u32::from_be_bytes(a) & mask) == (u32::from_be_bytes(b) & mask);
|
||||
prop_assert_eq!(outer.contains(&inner), expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn contains_covers_more_specifics() {
|
||||
let p = |s: &str| IpPrefix::try_from(s).unwrap();
|
||||
@@ -410,5 +240,11 @@ mod tests {
|
||||
assert!(p("2001:db8::/32").contains(&p("2001:db8:cafe::/48")));
|
||||
}
|
||||
|
||||
verify_roundtrip!(verify_roundtrip_ipv6_0, "::/0", IpVersion::Ipv6, vec![], 0);
|
||||
verify_roundtrip!(
|
||||
verify_roundtrip_ipv6_0,
|
||||
"::/0",
|
||||
AddressFamilyId::Ipv6,
|
||||
vec![],
|
||||
0
|
||||
);
|
||||
}
|
||||
@@ -1,43 +1,35 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
use nom::error::{ErrorKind, ParseError};
|
||||
|
||||
use crate::constants::AfiSafi;
|
||||
use thiserror::Error;
|
||||
use crate::constants::AddressFamilyId;
|
||||
|
||||
/// What reading or writing a message has to know about the session it
|
||||
/// belongs to. The default is a session before its OPENs are exchanged;
|
||||
/// only an UPDATE depends on anything here, and there are none until then.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ParserContext {
|
||||
/// Whether AS numbers are four octets wide on this session (RFC 6793).
|
||||
pub four_octet_asn: bool,
|
||||
/// Whether the peer is in our own AS. Some attributes mean something
|
||||
/// only between internal peers and are dropped when an external one
|
||||
/// sends them (RFC 7606 §7).
|
||||
pub ibgp: bool,
|
||||
/// The address families whose prefixes carry a path identifier in the
|
||||
/// direction this context is for (RFC 7911). What a session sends and
|
||||
/// what it receives are negotiated separately, so a session has one
|
||||
/// context for reading and one for writing.
|
||||
pub add_path: Vec<AfiSafi>,
|
||||
/// Whether thi parser is being run with a peer that is RFC6793 compliant.
|
||||
pub four_octet_asn: Option<bool>,
|
||||
/// Which address family should be parsed by default with this parser.
|
||||
pub address_family: Option<AddressFamilyId>,
|
||||
}
|
||||
|
||||
impl ParserContext {
|
||||
pub fn has_add_path(&self, family: AfiSafi) -> bool {
|
||||
self.add_path.contains(&family)
|
||||
#[derive(Debug)]
|
||||
pub enum ToWireError {
|
||||
/// There was not enough space in the output buffer to serialize the data into.
|
||||
OutBufferOverflow,
|
||||
/// Another error.
|
||||
Other(eyre::Error),
|
||||
}
|
||||
|
||||
impl Display for ToWireError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
ToWireError::OutBufferOverflow => write!(f, "OutBufferOverflow"),
|
||||
ToWireError::Other(report) => report.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Why a value could not be written to the wire.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Error)]
|
||||
pub enum EncodeError {
|
||||
/// A length does not fit the field that has to carry it.
|
||||
#[error("{what} is too long to encode: {len} exceeds {max}")]
|
||||
TooLong {
|
||||
what: &'static str,
|
||||
len: usize,
|
||||
max: usize,
|
||||
},
|
||||
}
|
||||
impl std::error::Error for ToWireError {}
|
||||
|
||||
// Custom error type for the parser.
|
||||
#[derive(Debug)]
|
||||
@@ -45,8 +37,8 @@ pub enum BgpParserError<I> {
|
||||
/// ProtocolError encapsulates protocol level semantics problems that require
|
||||
/// action from the caller of the parser.
|
||||
ProtocolError(ProtocolErrorValues),
|
||||
/// The input does not follow the format; the text says how.
|
||||
Malformed(String),
|
||||
CustomText(&'static str),
|
||||
Eyre(eyre::ErrReport),
|
||||
Nom(I, ErrorKind),
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,3 @@
|
||||
//! A Poptrie (Asai and Ohara, SIGCOMM 2015): a compressed multiway trie for
|
||||
//! longest-prefix matching.
|
||||
//!
|
||||
//! Work in progress. Nothing in the daemon uses it yet, and the construction
|
||||
//! side has no public entry point, hence the lint expectation below.
|
||||
#![expect(dead_code, reason = "not yet wired into a RIB")]
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -82,8 +75,7 @@ impl<T: Clone> Poptrie<T> {
|
||||
result
|
||||
}
|
||||
|
||||
/// Fills the trie from a routing table keyed by (address, prefix length).
|
||||
fn build(&mut self, rt: BTreeMap<(u32, u8), T>) {
|
||||
fn new(&mut self, rt: BTreeMap<(u32, u8), T>) {
|
||||
const DEPTH: u8 = 6;
|
||||
let mut forest = vec![(0, rt)];
|
||||
|
||||
@@ -176,10 +168,9 @@ mod tests {
|
||||
leaves: vec![],
|
||||
};
|
||||
|
||||
poptrie.build(map);
|
||||
assert!(
|
||||
!poptrie.inners.is_empty(),
|
||||
"building from a non-empty table yields nodes"
|
||||
);
|
||||
poptrie.new(map);
|
||||
println!("Success");
|
||||
|
||||
dbg!(poptrie);
|
||||
}
|
||||
}
|
||||
+1
-14
@@ -1,24 +1,11 @@
|
||||
[package]
|
||||
name = "bgp-rib"
|
||||
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
|
||||
bytes.workspace = true
|
||||
foldhash.workspace = true
|
||||
hashbrown.workspace = true
|
||||
serde.workspace = true
|
||||
thiserror.workspace = true
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
|
||||
[dev-dependencies]
|
||||
proptest.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
tracing.workspace = true
|
||||
@@ -1,7 +0,0 @@
|
||||
# Seeds for failure cases proptest has generated in the past. It is
|
||||
# automatically read and these particular cases re-run before any
|
||||
# novel cases are generated.
|
||||
#
|
||||
# It is recommended to check this file in to source control so that
|
||||
# everyone who runs the test benefits from these saved cases.
|
||||
cc 7107cf5580fe7bb95e138f1fead386d89273532f83aa1d11808aa9586aa9da63 # shrinks to ops = [Withdraw { peer: 0, prefix: 1 }, Withdraw { peer: 0, prefix: 1 }, Poll { reader: 2, budget: 2 }, Withdraw { peer: 0, prefix: 10 }, Poll { reader: 2, budget: 4 }, Announce { peer: 2, prefix: 7, route: Route { local_pref: None, path: [1, 2], origin: 0, med: Some(0) } }, Announce { peer: 3, prefix: 6, route: Route { local_pref: None, path: [1], origin: 0, med: None } }, Announce { peer: 3, prefix: 6, route: Route { local_pref: None, path: [1, 1], origin: 0, med: None } }, Withdraw { peer: 1, prefix: 5 }, Withdraw { peer: 0, prefix: 11 }, Withdraw { peer: 3, prefix: 6 }, Announce { peer: 0, prefix: 3, route: Route { local_pref: None, path: [1, 2], origin: 1, med: None } }, Poll { reader: 2, budget: 3 }, Withdraw { peer: 0, prefix: 8 }, Flap { peer: 3 }, Withdraw { peer: 1, prefix: 1 }, Announce { peer: 2, prefix: 5, route: Route { local_pref: None, path: [1], origin: 1, med: Some(0) } }, Announce { peer: 0, prefix: 1, route: Route { local_pref: None, path: [1, 1], origin: 1, med: None } }, Withdraw { peer: 0, prefix: 1 }, Withdraw { peer: 2, prefix: 8 }, Poll { reader: 1, budget: 2 }, Flap { peer: 1 }, Poll { reader: 2, budget: 2 }, Announce { peer: 0, prefix: 9, route: Route { local_pref: None, path: [1], origin: 0, med: Some(1) } }, Announce { peer: 2, prefix: 2, route: Route { local_pref: Some(100), path: [2], origin: 0, med: Some(1) } }, Poll { reader: 1, budget: 3 }, Withdraw { peer: 3, prefix: 1 }, Announce { peer: 1, prefix: 4, route: Route { local_pref: None, path: [2], origin: 1, med: None } }, Announce { peer: 3, prefix: 7, route: Route { local_pref: None, path: [2], origin: 0, med: None } }, Announce { peer: 1, prefix: 11, route: Route { local_pref: None, path: [1], origin: 1, med: Some(1) } }, Withdraw { peer: 0, prefix: 6 }, Withdraw { peer: 0, prefix: 4 }, Announce { peer: 3, prefix: 10, route: Route { local_pref: Some(100), path: [2, 2], origin: 1, med: None } }, Announce { peer: 1, prefix: 2, route: Route { local_pref: None, path: [2], origin: 0, med: Some(2) } }, Announce { peer: 1, prefix: 3, route: Route { local_pref: None, path: [1], origin: 1, med: None } }, Withdraw { peer: 0, prefix: 2 }, Announce { peer: 1, prefix: 4, route: Route { local_pref: None, path: [1, 1], origin: 0, med: None } }, Poll { reader: 1, budget: 2 }, Poll { reader: 1, budget: 4 }, Poll { reader: 2, budget: 1 }, Poll { reader: 1, budget: 3 }, Poll { reader: 1, budget: 5 }, Poll { reader: 1, budget: 1 }, Withdraw { peer: 3, prefix: 5 }, Withdraw { peer: 3, prefix: 9 }, Withdraw { peer: 2, prefix: 10 }], shards = 3
|
||||
@@ -1,7 +0,0 @@
|
||||
# Seeds for failure cases proptest has generated in the past. It is
|
||||
# automatically read and these particular cases re-run before any
|
||||
# novel cases are generated.
|
||||
#
|
||||
# It is recommended to check this file in to source control so that
|
||||
# everyone who runs the test benefits from these saved cases.
|
||||
cc 6bcfc317c976928c2fbb29e943f385127b91770a76f18e7061b80b31f4276955 # shrinks to ops = [Announce { peer: 1, prefix: 1, route: Route { local_pref: None, path: [2], origin: 0, med: None } }, Announce { peer: 2, prefix: 1, route: Route { local_pref: None, path: [1], origin: 0, med: Some(2) } }, Announce { peer: 3, prefix: 1, route: Route { local_pref: None, path: [1], origin: 0, med: None } }, Announce { peer: 3, prefix: 1, route: Route { local_pref: None, path: [1], origin: 1, med: None } }]
|
||||
@@ -0,0 +1,112 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use bgp_primitives::ip_prefix::IpPrefix;
|
||||
use tokio::sync::{broadcast, mpsc, oneshot};
|
||||
use tracing::warn;
|
||||
|
||||
use crate::entry::{PeerId, RibEntry, SnapshotEntry};
|
||||
use crate::event::RibEvent;
|
||||
|
||||
const EVENT_CHANNEL_CAPACITY: usize = 1024;
|
||||
const CMD_CHANNEL_CAPACITY: usize = 256;
|
||||
|
||||
enum RibCommand {
|
||||
Announce { peer: PeerId, prefix: IpPrefix, entry: RibEntry },
|
||||
Withdraw { peer: PeerId, prefix: IpPrefix },
|
||||
/// Drop everything learned from `peer`, e.g. because its session ended.
|
||||
RemovePeer { peer: PeerId, reply: oneshot::Sender<usize> },
|
||||
Snapshot { reply: oneshot::Sender<Vec<SnapshotEntry>> },
|
||||
}
|
||||
|
||||
/// Handle to the Adj-RIB-In actor. Cheap to clone — all clones share the same
|
||||
/// backing task. The actor is stopped when all handles are dropped.
|
||||
#[derive(Clone)]
|
||||
pub struct AdjRibIn {
|
||||
tx: mpsc::Sender<RibCommand>,
|
||||
events: broadcast::Sender<RibEvent>,
|
||||
}
|
||||
|
||||
impl AdjRibIn {
|
||||
/// Spawns the backing actor and returns a handle.
|
||||
pub fn spawn() -> Self {
|
||||
let (tx, rx) = mpsc::channel(CMD_CHANNEL_CAPACITY);
|
||||
let (events, _) = broadcast::channel(EVENT_CHANNEL_CAPACITY);
|
||||
let events_clone = events.clone();
|
||||
tokio::spawn(run_actor(rx, events_clone));
|
||||
Self { tx, events }
|
||||
}
|
||||
|
||||
pub async fn announce(&self, peer: PeerId, prefix: IpPrefix, entry: RibEntry) {
|
||||
let _ = self.tx.send(RibCommand::Announce { peer, prefix, entry }).await;
|
||||
}
|
||||
|
||||
pub async fn withdraw(&self, peer: PeerId, prefix: IpPrefix) {
|
||||
let _ = self.tx.send(RibCommand::Withdraw { peer, prefix }).await;
|
||||
}
|
||||
|
||||
/// Removes every route learned from `peer`, emitting a
|
||||
/// [`RibEvent::Withdrawn`] for each (RFC 4271 §8.2.2: routes from a
|
||||
/// terminated session are deleted). Returns how many were removed.
|
||||
pub async fn remove_peer(&self, peer: PeerId) -> usize {
|
||||
let (reply_tx, reply_rx) = oneshot::channel();
|
||||
let _ = self.tx.send(RibCommand::RemovePeer { peer, reply: reply_tx }).await;
|
||||
reply_rx.await.unwrap_or(0)
|
||||
}
|
||||
|
||||
/// Returns a point-in-time snapshot of all routes in the RIB.
|
||||
pub async fn snapshot(&self) -> Vec<SnapshotEntry> {
|
||||
let (reply_tx, reply_rx) = oneshot::channel();
|
||||
let _ = self.tx.send(RibCommand::Snapshot { reply: reply_tx }).await;
|
||||
reply_rx.await.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Subscribe to a stream of RIB mutation events.
|
||||
///
|
||||
/// The receiver will see every [`RibEvent`] from the point of subscription.
|
||||
/// If the consumer falls behind by more than [`EVENT_CHANNEL_CAPACITY`]
|
||||
/// events it will receive a [`broadcast::error::RecvError::Lagged`] error.
|
||||
pub fn subscribe(&self) -> broadcast::Receiver<RibEvent> {
|
||||
self.events.subscribe()
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_actor(mut rx: mpsc::Receiver<RibCommand>, events: broadcast::Sender<RibEvent>) {
|
||||
let mut table: HashMap<(PeerId, IpPrefix), RibEntry> = HashMap::new();
|
||||
|
||||
while let Some(cmd) = rx.recv().await {
|
||||
match cmd {
|
||||
// Send errors below mean there are no subscribers, which is fine.
|
||||
RibCommand::Announce { peer, prefix, entry } => {
|
||||
table.insert((peer, prefix.clone()), entry.clone());
|
||||
let _ = events.send(RibEvent::Announced { peer, prefix, entry });
|
||||
}
|
||||
RibCommand::Withdraw { peer, prefix } => {
|
||||
if table.remove(&(peer, prefix.clone())).is_some() {
|
||||
let _ = events.send(RibEvent::Withdrawn { peer, prefix });
|
||||
}
|
||||
}
|
||||
RibCommand::RemovePeer { peer, reply } => {
|
||||
let prefixes: Vec<IpPrefix> =
|
||||
table.keys().filter(|(p, _)| *p == peer).map(|(_, prefix)| prefix.clone()).collect();
|
||||
for prefix in &prefixes {
|
||||
table.remove(&(peer, prefix.clone()));
|
||||
let _ = events.send(RibEvent::Withdrawn { peer, prefix: prefix.clone() });
|
||||
}
|
||||
let _ = reply.send(prefixes.len());
|
||||
}
|
||||
RibCommand::Snapshot { reply } => {
|
||||
let snap = table
|
||||
.iter()
|
||||
.map(|((peer, prefix), entry)| SnapshotEntry {
|
||||
peer: *peer,
|
||||
prefix: prefix.clone(),
|
||||
entry: entry.clone(),
|
||||
})
|
||||
.collect();
|
||||
if reply.send(snap).is_err() {
|
||||
warn!("Snapshot reply channel closed before send");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
use std::net::SocketAddr;
|
||||
use std::time::SystemTime;
|
||||
|
||||
use bgp_packet::message::PathAttribute;
|
||||
use bgp_primitives::ip_prefix::IpPrefix;
|
||||
|
||||
/// Identifies a BGP peer by its TCP address and negotiated ASN.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct PeerId {
|
||||
pub addr: SocketAddr,
|
||||
pub asn: u32,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for PeerId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}(AS{})", self.addr, self.asn)
|
||||
}
|
||||
}
|
||||
|
||||
/// A single route stored in the Adj-RIB-In.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RibEntry {
|
||||
pub path_attributes: Vec<PathAttribute>,
|
||||
pub received_at: SystemTime,
|
||||
}
|
||||
|
||||
impl RibEntry {
|
||||
pub fn new(path_attributes: Vec<PathAttribute>) -> Self {
|
||||
Self { path_attributes, received_at: SystemTime::now() }
|
||||
}
|
||||
}
|
||||
|
||||
/// A flattened row returned by [`crate::AdjRibIn::snapshot`].
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SnapshotEntry {
|
||||
pub peer: PeerId,
|
||||
pub prefix: IpPrefix,
|
||||
pub entry: RibEntry,
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
/// A RIB's actor is no longer running, so the table can be neither read nor
|
||||
/// changed.
|
||||
///
|
||||
/// The actor runs for as long as a handle to it exists; it stops early only
|
||||
/// by panicking. That is a bug, and one the server cannot work around: it
|
||||
/// could no longer say what it originates. Callers pass this up
|
||||
/// until the process exits and is restarted, rather than carry on with a
|
||||
/// table that looks empty.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
|
||||
#[error("the {rib} has stopped")]
|
||||
pub struct RibStopped {
|
||||
/// Which table.
|
||||
pub rib: &'static str,
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
use bgp_primitives::ip_prefix::IpPrefix;
|
||||
|
||||
use crate::entry::{PeerId, RibEntry};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum RibEvent {
|
||||
Announced { peer: PeerId, prefix: IpPrefix, entry: RibEntry },
|
||||
Withdrawn { peer: PeerId, prefix: IpPrefix },
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
//! The router's tables: the routes it has learned and chosen
|
||||
//! ([`table::RibTable`]) and the routes it originates ([`LocalRib`]).
|
||||
|
||||
mod error;
|
||||
mod adj_rib_in;
|
||||
mod entry;
|
||||
mod event;
|
||||
mod local_rib;
|
||||
pub mod table;
|
||||
|
||||
pub use error::RibStopped;
|
||||
pub use local_rib::{LocalRib, LocalRibEvent, LocalRoute, LocalRouteAttrs};
|
||||
pub use adj_rib_in::AdjRibIn;
|
||||
pub use entry::{PeerId, RibEntry, SnapshotEntry};
|
||||
pub use event::RibEvent;
|
||||
pub use local_rib::{LocalRib, LocalRibError, LocalRibEvent, LocalRibPolicy, LocalRoute, LocalRouteAttrs};
|
||||
+179
-136
@@ -1,26 +1,24 @@
|
||||
//! Locally originated routes (the "Loc-RIB" of routes this router announces
|
||||
//! on its own behalf, as opposed to routes learned from peers).
|
||||
//!
|
||||
//! The actor stores one route per prefix together with its attributes. It
|
||||
//! does not know about peers or sessions: the server subscribes to
|
||||
//! [`LocalRibEvent`]s and turns them into UPDATEs.
|
||||
//! The actor stores one route per prefix together with the attributes chosen
|
||||
//! by whoever announced it. It does not know about peers or sessions: the
|
||||
//! server subscribes to [`LocalRibEvent`]s and turns them into UPDATEs.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::time::SystemTime;
|
||||
|
||||
use bgp_packet::message::{LargeCommunity, OriginPathAttribute};
|
||||
use bgp_primitives::ip_prefix::IpPrefix;
|
||||
use serde::Serialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::sync::{broadcast, mpsc, oneshot};
|
||||
use tokio::task::AbortHandle;
|
||||
|
||||
use crate::error::RibStopped;
|
||||
use tracing::warn;
|
||||
|
||||
const EVENT_CHANNEL_CAPACITY: usize = 1024;
|
||||
const CMD_CHANNEL_CAPACITY: usize = 256;
|
||||
|
||||
/// Attributes of a locally originated route.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
/// User-selectable attributes for a locally originated route.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct LocalRouteAttrs {
|
||||
pub origin: OriginPathAttribute,
|
||||
/// Number of *additional* copies of the local ASN to prepend to the
|
||||
@@ -46,155 +44,197 @@ impl Default for LocalRouteAttrs {
|
||||
}
|
||||
|
||||
/// A route originated by this router.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct LocalRoute {
|
||||
pub prefix: IpPrefix,
|
||||
/// Free-form identity of whoever announced the route (e.g. a student
|
||||
/// name). Used to stop one owner replacing or withdrawing another's route.
|
||||
pub owner: String,
|
||||
pub attrs: LocalRouteAttrs,
|
||||
pub announced_at: SystemTime,
|
||||
}
|
||||
|
||||
impl LocalRoute {
|
||||
pub fn new(prefix: IpPrefix, attrs: LocalRouteAttrs) -> Self {
|
||||
Self {
|
||||
prefix,
|
||||
attrs,
|
||||
announced_at: SystemTime::now(),
|
||||
}
|
||||
pub fn new(prefix: IpPrefix, owner: impl Into<String>, attrs: LocalRouteAttrs) -> Self {
|
||||
Self { prefix, owner: owner.into(), attrs, announced_at: SystemTime::now() }
|
||||
}
|
||||
}
|
||||
|
||||
const STOPPED: RibStopped = RibStopped { rib: "local RIB" };
|
||||
/// Limits on what may be announced. Enforced inside the actor so every caller
|
||||
/// (HTTP API, tests, future CLI) goes through the same checks.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct LocalRibPolicy {
|
||||
/// A route is accepted only if it is covered by at least one of these
|
||||
/// prefixes. Empty means no restriction.
|
||||
pub allowed_prefixes: Vec<IpPrefix>,
|
||||
/// Maximum number of routes a single owner may hold. `0` means unlimited.
|
||||
pub max_routes_per_owner: usize,
|
||||
}
|
||||
|
||||
impl LocalRibPolicy {
|
||||
fn allows(&self, prefix: &IpPrefix) -> bool {
|
||||
self.allowed_prefixes.is_empty() || self.allowed_prefixes.iter().any(|p| p.contains(prefix))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
|
||||
pub enum LocalRibError {
|
||||
#[error("prefix {0} is not covered by the allowed prefixes")]
|
||||
NotAllowed(IpPrefix),
|
||||
#[error("owner {owner:?} already has {limit} routes")]
|
||||
OwnerLimit { owner: String, limit: usize },
|
||||
#[error("prefix {prefix} is owned by {owner:?}")]
|
||||
OwnedByOther { prefix: IpPrefix, owner: String },
|
||||
#[error("local RIB actor has stopped")]
|
||||
Closed,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum LocalRibEvent {
|
||||
/// A route was added or its attributes replaced.
|
||||
Announced(LocalRoute),
|
||||
Withdrawn {
|
||||
prefix: IpPrefix,
|
||||
},
|
||||
Withdrawn { prefix: IpPrefix },
|
||||
}
|
||||
|
||||
enum Command {
|
||||
Announce {
|
||||
route: LocalRoute,
|
||||
reply: oneshot::Sender<()>,
|
||||
},
|
||||
Announce { route: LocalRoute, reply: oneshot::Sender<Result<(), LocalRibError>> },
|
||||
Withdraw {
|
||||
prefix: IpPrefix,
|
||||
reply: oneshot::Sender<Option<LocalRoute>>,
|
||||
},
|
||||
Snapshot {
|
||||
reply: oneshot::Sender<Vec<LocalRoute>>,
|
||||
},
|
||||
Len {
|
||||
reply: oneshot::Sender<usize>,
|
||||
requester: Option<String>,
|
||||
reply: oneshot::Sender<Result<Option<LocalRoute>, LocalRibError>>,
|
||||
},
|
||||
Snapshot { reply: oneshot::Sender<Vec<LocalRoute>> },
|
||||
}
|
||||
|
||||
/// Handle to the local RIB actor. Cheap to clone; all clones share one table.
|
||||
///
|
||||
/// Every operation fails with [`RibStopped`] if the actor is gone; see
|
||||
/// there for what that means.
|
||||
#[derive(Clone)]
|
||||
pub struct LocalRib {
|
||||
tx: mpsc::Sender<Command>,
|
||||
events: broadcast::Sender<LocalRibEvent>,
|
||||
actor: AbortHandle,
|
||||
}
|
||||
|
||||
impl LocalRib {
|
||||
pub fn spawn() -> Self {
|
||||
pub fn spawn(policy: LocalRibPolicy) -> Self {
|
||||
let (tx, rx) = mpsc::channel(CMD_CHANNEL_CAPACITY);
|
||||
let (events, _) = broadcast::channel(EVENT_CHANNEL_CAPACITY);
|
||||
let actor = tokio::spawn(run_actor(rx, events.clone())).abort_handle();
|
||||
Self { tx, events, actor }
|
||||
tokio::spawn(run_actor(rx, events.clone(), policy));
|
||||
Self { tx, events }
|
||||
}
|
||||
|
||||
/// Adds a route, or replaces the existing route for the same prefix.
|
||||
pub async fn announce(&self, route: LocalRoute) -> Result<(), RibStopped> {
|
||||
self.ask(|reply| Command::Announce { route, reply }).await
|
||||
/// Adds a route, or replaces the attributes of an existing route with the
|
||||
/// same prefix and owner.
|
||||
pub async fn announce(&self, route: LocalRoute) -> Result<(), LocalRibError> {
|
||||
let (reply, rx) = oneshot::channel();
|
||||
self.tx
|
||||
.send(Command::Announce { route, reply })
|
||||
.await
|
||||
.map_err(|_| LocalRibError::Closed)?;
|
||||
rx.await.map_err(|_| LocalRibError::Closed)?
|
||||
}
|
||||
|
||||
/// Removes a route. Returns the removed route, or `None` if the prefix
|
||||
/// was not present.
|
||||
pub async fn withdraw(&self, prefix: IpPrefix) -> Result<Option<LocalRoute>, RibStopped> {
|
||||
self.ask(|reply| Command::Withdraw { prefix, reply }).await
|
||||
/// Removes a route. `requester` must match the route's owner; pass `None`
|
||||
/// to withdraw regardless of owner (administrative override). Returns the
|
||||
/// removed route, or `None` if the prefix was not present.
|
||||
pub async fn withdraw(
|
||||
&self,
|
||||
prefix: IpPrefix,
|
||||
requester: Option<&str>,
|
||||
) -> Result<Option<LocalRoute>, LocalRibError> {
|
||||
let (reply, rx) = oneshot::channel();
|
||||
self.tx
|
||||
.send(Command::Withdraw { prefix, requester: requester.map(str::to_owned), reply })
|
||||
.await
|
||||
.map_err(|_| LocalRibError::Closed)?;
|
||||
rx.await.map_err(|_| LocalRibError::Closed)?
|
||||
}
|
||||
|
||||
/// Point-in-time copy of every route, in no particular order.
|
||||
pub async fn snapshot(&self) -> Result<Vec<LocalRoute>, RibStopped> {
|
||||
self.ask(|reply| Command::Snapshot { reply }).await
|
||||
pub async fn snapshot(&self) -> Vec<LocalRoute> {
|
||||
let (reply, rx) = oneshot::channel();
|
||||
let _ = self.tx.send(Command::Snapshot { reply }).await;
|
||||
rx.await.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// How many routes the RIB holds, without copying them.
|
||||
pub async fn len(&self) -> Result<usize, RibStopped> {
|
||||
self.ask(|reply| Command::Len { reply }).await
|
||||
}
|
||||
|
||||
/// Whether the RIB holds no routes.
|
||||
pub async fn is_empty(&self) -> Result<bool, RibStopped> {
|
||||
Ok(self.len().await? == 0)
|
||||
}
|
||||
|
||||
/// Sends a command and waits for its answer.
|
||||
async fn ask<T>(
|
||||
&self,
|
||||
command: impl FnOnce(oneshot::Sender<T>) -> Command,
|
||||
) -> Result<T, RibStopped> {
|
||||
let (reply, answer) = oneshot::channel();
|
||||
self.tx.send(command(reply)).await.map_err(|_| STOPPED)?;
|
||||
answer.await.map_err(|_| STOPPED)
|
||||
}
|
||||
|
||||
/// Resolves when the actor has stopped. It never does in a healthy
|
||||
/// process; whoever owns the RIB waits on this to notice a failure even
|
||||
/// when nothing happens to be using the table.
|
||||
pub async fn stopped(&self) {
|
||||
self.tx.closed().await
|
||||
}
|
||||
|
||||
/// Stops the actor, as a panic in it would. For tests of how the rest
|
||||
/// of the server copes.
|
||||
#[doc(hidden)]
|
||||
pub fn stop_for_testing(&self) {
|
||||
self.actor.abort();
|
||||
}
|
||||
|
||||
/// Subscribe to mutations. A subscriber that falls too far behind is
|
||||
/// told so with [`broadcast::error::RecvError::Lagged`].
|
||||
/// Subscribe to mutations. See [`crate::AdjRibIn::subscribe`] for lag
|
||||
/// semantics.
|
||||
pub fn subscribe(&self) -> broadcast::Receiver<LocalRibEvent> {
|
||||
self.events.subscribe()
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_actor(mut rx: mpsc::Receiver<Command>, events: broadcast::Sender<LocalRibEvent>) {
|
||||
async fn run_actor(
|
||||
mut rx: mpsc::Receiver<Command>,
|
||||
events: broadcast::Sender<LocalRibEvent>,
|
||||
policy: LocalRibPolicy,
|
||||
) {
|
||||
let mut table: HashMap<IpPrefix, LocalRoute> = HashMap::new();
|
||||
|
||||
while let Some(cmd) = rx.recv().await {
|
||||
match cmd {
|
||||
Command::Announce { route, reply } => {
|
||||
table.insert(route.prefix, route.clone());
|
||||
let _ = events.send(LocalRibEvent::Announced(route));
|
||||
let _ = reply.send(());
|
||||
let result = check_announce(&table, &policy, &route).map(|()| {
|
||||
table.insert(route.prefix.clone(), route.clone());
|
||||
let _ = events.send(LocalRibEvent::Announced(route));
|
||||
});
|
||||
let _ = reply.send(result);
|
||||
}
|
||||
Command::Withdraw { prefix, reply } => {
|
||||
let removed = table.remove(&prefix);
|
||||
if removed.is_some() {
|
||||
let _ = events.send(LocalRibEvent::Withdrawn { prefix });
|
||||
}
|
||||
let _ = reply.send(removed);
|
||||
Command::Withdraw { prefix, requester, reply } => {
|
||||
let result = match (table.get(&prefix), requester) {
|
||||
(Some(existing), Some(req)) if existing.owner != req => {
|
||||
Err(LocalRibError::OwnedByOther {
|
||||
prefix: prefix.clone(),
|
||||
owner: existing.owner.clone(),
|
||||
})
|
||||
}
|
||||
_ => {
|
||||
let removed = table.remove(&prefix);
|
||||
if removed.is_some() {
|
||||
let _ = events.send(LocalRibEvent::Withdrawn { prefix });
|
||||
}
|
||||
Ok(removed)
|
||||
}
|
||||
};
|
||||
let _ = reply.send(result);
|
||||
}
|
||||
Command::Snapshot { reply } => {
|
||||
// A failed reply means the asker stopped waiting.
|
||||
let _ = reply.send(table.values().cloned().collect());
|
||||
}
|
||||
Command::Len { reply } => {
|
||||
let _ = reply.send(table.len());
|
||||
if reply.send(table.values().cloned().collect()).is_err() {
|
||||
warn!("Snapshot reply channel closed before send");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn check_announce(
|
||||
table: &HashMap<IpPrefix, LocalRoute>,
|
||||
policy: &LocalRibPolicy,
|
||||
route: &LocalRoute,
|
||||
) -> Result<(), LocalRibError> {
|
||||
if !policy.allows(&route.prefix) {
|
||||
return Err(LocalRibError::NotAllowed(route.prefix.clone()));
|
||||
}
|
||||
if let Some(existing) = table.get(&route.prefix) {
|
||||
if existing.owner != route.owner {
|
||||
return Err(LocalRibError::OwnedByOther {
|
||||
prefix: route.prefix.clone(),
|
||||
owner: existing.owner.clone(),
|
||||
});
|
||||
}
|
||||
// Replacing our own route never changes the per-owner count.
|
||||
return Ok(());
|
||||
}
|
||||
if policy.max_routes_per_owner > 0 {
|
||||
let held = table.values().filter(|r| r.owner == route.owner).count();
|
||||
if held >= policy.max_routes_per_owner {
|
||||
return Err(LocalRibError::OwnerLimit {
|
||||
owner: route.owner.clone(),
|
||||
limit: policy.max_routes_per_owner,
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -203,60 +243,63 @@ mod tests {
|
||||
IpPrefix::try_from(s).unwrap()
|
||||
}
|
||||
|
||||
fn route(p: &str) -> LocalRoute {
|
||||
LocalRoute::new(prefix(p), LocalRouteAttrs::default())
|
||||
fn route(p: &str, owner: &str) -> LocalRoute {
|
||||
LocalRoute::new(prefix(p), owner, LocalRouteAttrs::default())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn announce_and_withdraw_emit_events() {
|
||||
let rib = LocalRib::spawn();
|
||||
let rib = LocalRib::spawn(LocalRibPolicy::default());
|
||||
let mut events = rib.subscribe();
|
||||
|
||||
rib.announce(route("10.0.0.0/24")).await.unwrap();
|
||||
assert!(
|
||||
matches!(events.recv().await.unwrap(), LocalRibEvent::Announced(r) if r.prefix == prefix("10.0.0.0/24"))
|
||||
);
|
||||
assert_eq!(rib.len().await, Ok(1));
|
||||
rib.announce(route("10.0.0.0/24", "alice")).await.unwrap();
|
||||
assert!(matches!(events.recv().await.unwrap(), LocalRibEvent::Announced(r) if r.prefix == prefix("10.0.0.0/24")));
|
||||
assert_eq!(rib.snapshot().await.len(), 1);
|
||||
|
||||
let removed = rib.withdraw(prefix("10.0.0.0/24")).await.unwrap();
|
||||
assert_eq!(removed.unwrap().prefix, prefix("10.0.0.0/24"));
|
||||
assert!(
|
||||
matches!(events.recv().await.unwrap(), LocalRibEvent::Withdrawn { prefix: p } if p == prefix("10.0.0.0/24"))
|
||||
);
|
||||
assert_eq!(rib.is_empty().await, Ok(true));
|
||||
let removed = rib.withdraw(prefix("10.0.0.0/24"), Some("alice")).await.unwrap();
|
||||
assert_eq!(removed.unwrap().owner, "alice");
|
||||
assert!(matches!(events.recv().await.unwrap(), LocalRibEvent::Withdrawn { prefix: p } if p == prefix("10.0.0.0/24")));
|
||||
assert!(rib.snapshot().await.is_empty());
|
||||
|
||||
// Withdrawing an absent prefix is a no-op, not an error.
|
||||
assert_eq!(rib.withdraw(prefix("10.0.0.0/24")).await.unwrap(), None);
|
||||
assert_eq!(rib.withdraw(prefix("10.0.0.0/24"), None).await.unwrap(), None);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn announcing_a_prefix_again_replaces_its_attributes() {
|
||||
let rib = LocalRib::spawn();
|
||||
rib.announce(route("10.0.0.0/24")).await.unwrap();
|
||||
let attrs = LocalRouteAttrs {
|
||||
med: Some(50),
|
||||
..Default::default()
|
||||
};
|
||||
rib.announce(LocalRoute::new(prefix("10.0.0.0/24"), attrs.clone()))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let table = rib.snapshot().await.unwrap();
|
||||
assert_eq!(table.len(), 1);
|
||||
assert_eq!(table[0].attrs, attrs);
|
||||
async fn policy_allowlist_is_enforced() {
|
||||
let rib = LocalRib::spawn(LocalRibPolicy {
|
||||
allowed_prefixes: vec![prefix("10.100.0.0/16")],
|
||||
max_routes_per_owner: 0,
|
||||
});
|
||||
rib.announce(route("10.100.3.0/24", "alice")).await.unwrap();
|
||||
let err = rib.announce(route("192.0.2.0/24", "alice")).await.unwrap_err();
|
||||
assert_eq!(err, LocalRibError::NotAllowed(prefix("192.0.2.0/24")));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_stopped_actor_is_an_error_and_not_an_empty_table() {
|
||||
let rib = LocalRib::spawn();
|
||||
rib.announce(route("10.0.0.0/24")).await.unwrap();
|
||||
async fn owner_limit_ignores_replacements() {
|
||||
let rib = LocalRib::spawn(LocalRibPolicy { allowed_prefixes: vec![], max_routes_per_owner: 1 });
|
||||
rib.announce(route("10.0.0.0/24", "alice")).await.unwrap();
|
||||
// Replacing the same prefix is fine.
|
||||
rib.announce(route("10.0.0.0/24", "alice")).await.unwrap();
|
||||
let err = rib.announce(route("10.0.1.0/24", "alice")).await.unwrap_err();
|
||||
assert!(matches!(err, LocalRibError::OwnerLimit { limit: 1, .. }));
|
||||
// Other owners are unaffected.
|
||||
rib.announce(route("10.0.1.0/24", "bob")).await.unwrap();
|
||||
}
|
||||
|
||||
rib.stop_for_testing();
|
||||
rib.stopped().await;
|
||||
#[tokio::test]
|
||||
async fn other_owner_cannot_replace_or_withdraw() {
|
||||
let rib = LocalRib::spawn(LocalRibPolicy::default());
|
||||
rib.announce(route("10.0.0.0/24", "alice")).await.unwrap();
|
||||
|
||||
assert_eq!(rib.announce(route("10.0.1.0/24")).await, Err(STOPPED));
|
||||
assert_eq!(rib.withdraw(prefix("10.0.0.0/24")).await, Err(STOPPED));
|
||||
assert_eq!(rib.snapshot().await, Err(STOPPED));
|
||||
assert_eq!(rib.len().await, Err(STOPPED));
|
||||
let err = rib.announce(route("10.0.0.0/24", "bob")).await.unwrap_err();
|
||||
assert!(matches!(err, LocalRibError::OwnedByOther { .. }));
|
||||
|
||||
let err = rib.withdraw(prefix("10.0.0.0/24"), Some("bob")).await.unwrap_err();
|
||||
assert!(matches!(err, LocalRibError::OwnedByOther { .. }));
|
||||
|
||||
// Administrative override works.
|
||||
assert!(rib.withdraw(prefix("10.0.0.0/24"), None).await.unwrap().is_some());
|
||||
}
|
||||
}
|
||||
@@ -1,780 +0,0 @@
|
||||
//! Attribute sets, stored once.
|
||||
//!
|
||||
//! A full table has millions of routes and far fewer distinct lists of path
|
||||
//! attributes: a peer announces prefixes that share their attributes
|
||||
//! together, about six to an UPDATE on the Internet today. A route in the
|
||||
//! table is therefore a pointer to an [`AttrSet`], and each distinct set
|
||||
//! exists once, in the [`AttrStore`].
|
||||
//!
|
||||
//! A set is kept as its bytes on the wire, which is a fifth the size of the
|
||||
//! parsed form. What choosing between routes needs from it is worked out
|
||||
//! once, when the set is stored, and kept beside the bytes.
|
||||
|
||||
use std::hash::BuildHasher;
|
||||
use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
|
||||
|
||||
use bgp_packet::message::{NlriNextHop, PathAttribute, RawAttribute, attr_type, split_attribute};
|
||||
use bgp_primitives::parser::{EncodeError, ParserContext};
|
||||
use bytes::{BufMut, BytesMut};
|
||||
use foldhash::fast::RandomState;
|
||||
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 {
|
||||
/// LOCAL_PREF, if the set has one.
|
||||
pub local_pref: Option<u32>,
|
||||
/// MULTI_EXIT_DISC, if the set has one.
|
||||
pub med: Option<u32>,
|
||||
/// The AS that the route was learned from: the first AS of the path, or
|
||||
/// zero if the path is empty or begins with a set. MEDs are comparable
|
||||
/// only between routes with the same one.
|
||||
pub neighbor_as: u32,
|
||||
/// The length of the AS path as selection counts it: an AS_SET is one
|
||||
/// hop however many AS numbers it holds.
|
||||
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,
|
||||
neighbor_as: 0,
|
||||
as_path_len: 0,
|
||||
// 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::MULTI_EXIT_DISC => key.med = Some(number(value)?),
|
||||
attr_type::LOCAL_PREF => key.local_pref = Some(number(value)?),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
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> {
|
||||
let mut hops = 0usize;
|
||||
let (mut first, mut last) = (0, 0);
|
||||
let mut at_start = 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>());
|
||||
}
|
||||
last = if ordered {
|
||||
asn(asns.last_chunk::<4>())
|
||||
} else {
|
||||
0
|
||||
};
|
||||
at_start = 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
|
||||
}
|
||||
|
||||
/// One distinct list of path attributes, with the next hop that goes with
|
||||
/// it. Made by an [`AttrStore`]; two routes have the same attributes
|
||||
/// exactly when they point to the same `AttrSet`.
|
||||
#[derive(Debug)]
|
||||
pub struct AttrSet {
|
||||
hash: u64,
|
||||
key: SelectionKey,
|
||||
/// The attributes as on the wire, with four-octet AS numbers. A next
|
||||
/// hop that arrived in MP_REACH_NLRI is kept as an MP_REACH_NLRI that
|
||||
/// holds nothing else, the form RFC 6396 §4.3.4 uses in table dumps.
|
||||
wire: Box<[u8]>,
|
||||
}
|
||||
|
||||
impl AttrSet {
|
||||
pub fn key(&self) -> &SelectionKey {
|
||||
&self.key
|
||||
}
|
||||
|
||||
/// The attributes as they are on the wire.
|
||||
pub fn wire(&self) -> &[u8] {
|
||||
&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.
|
||||
///
|
||||
/// `None` if they do not parse. Sets made from parsed attributes or
|
||||
/// from an UPDATE the decoder accepted always do; one made from bytes
|
||||
/// of another source ([`AttrStore::intern_wire`]) was checked only as
|
||||
/// far as selection reads it.
|
||||
pub fn attributes(&self) -> Option<(Vec<PathAttribute>, Option<NlriNextHop>)> {
|
||||
parse(&self.wire)
|
||||
}
|
||||
}
|
||||
|
||||
/// How the attributes of a set are written: with four-octet AS numbers.
|
||||
fn four_octet() -> ParserContext {
|
||||
ParserContext {
|
||||
four_octet_asn: true,
|
||||
ibgp: true,
|
||||
add_path: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Writes `attributes` and `next_hop` in the form an [`AttrSet`] keeps.
|
||||
fn encode(
|
||||
attributes: &[PathAttribute],
|
||||
next_hop: Option<&NlriNextHop>,
|
||||
out: &mut BytesMut,
|
||||
) -> Result<(), EncodeError> {
|
||||
let ctx = four_octet();
|
||||
for attribute in attributes {
|
||||
// These name prefixes, which are not a property of the route.
|
||||
if matches!(
|
||||
attribute,
|
||||
PathAttribute::MpReachNlri(_) | PathAttribute::MpUnreachNlri(_)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
attribute.to_wire(&ctx, out)?;
|
||||
}
|
||||
if let Some(next_hop) = next_hop {
|
||||
encode_next_hop(next_hop, out);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Writes a next hop as an MP_REACH_NLRI that holds nothing else: the
|
||||
/// next hop's length, and the next hop.
|
||||
fn encode_next_hop(next_hop: &NlriNextHop, out: &mut BytesMut) {
|
||||
out.put_u8(OPTIONAL);
|
||||
out.put_u8(attr_type::MP_REACH_NLRI);
|
||||
out.put_u8(1 + next_hop.wire_len());
|
||||
out.put_u8(next_hop.wire_len());
|
||||
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.
|
||||
fn parse(mut wire: &[u8]) -> Option<(Vec<PathAttribute>, Option<NlriNextHop>)> {
|
||||
let ctx = four_octet();
|
||||
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 {
|
||||
next_hop = Some(decode_next_hop(raw.value)?);
|
||||
} else {
|
||||
let (_, attribute) = PathAttribute::from_wire(&ctx, raw.wire).ok()?;
|
||||
attributes.push(attribute);
|
||||
}
|
||||
wire = rest;
|
||||
}
|
||||
Some((attributes, next_hop))
|
||||
}
|
||||
|
||||
/// How many locks the store is split over. Sessions intern concurrently,
|
||||
/// once per UPDATE; this many locks keep them out of each other's way.
|
||||
const STRIPES: usize = 64;
|
||||
|
||||
/// The one place attribute sets are kept. Safe to share between threads.
|
||||
pub struct AttrStore {
|
||||
stripes: Box<[Mutex<HashTable<Arc<AttrSet>>>]>,
|
||||
hasher: RandomState,
|
||||
}
|
||||
|
||||
impl Default for AttrStore {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl AttrStore {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
stripes: (0..STRIPES).map(|_| Mutex::default()).collect(),
|
||||
hasher: RandomState::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The stored set equal to `attributes` and `next_hop`, added if it was
|
||||
/// not there. `next_hop` is the one from MP_REACH_NLRI; routes whose
|
||||
/// next hop is the NEXT_HOP attribute pass `None`.
|
||||
///
|
||||
/// Fails only if the attributes cannot be encoded, which attributes that
|
||||
/// were decoded from a message always can.
|
||||
pub fn intern(
|
||||
&self,
|
||||
attributes: &[PathAttribute],
|
||||
next_hop: Option<&NlriNextHop>,
|
||||
) -> Result<Arc<AttrSet>, EncodeError> {
|
||||
let mut wire = BytesMut::with_capacity(256);
|
||||
encode(attributes, next_hop, &mut wire)?;
|
||||
Ok(self
|
||||
.intern_wire(&wire)
|
||||
.expect("attributes written here are well formed"))
|
||||
}
|
||||
|
||||
/// As [`AttrStore::intern`], for attributes that are already in the
|
||||
/// form a set keeps: on the wire, with four-octet AS numbers, no
|
||||
/// MP_UNREACH_NLRI, and an MP_REACH_NLRI (if any) that holds only the
|
||||
/// next hop. That is how a table dump has them (RFC 6396 §4.3.4).
|
||||
///
|
||||
/// Nothing is parsed: finding a set that is already stored costs a hash
|
||||
/// of the bytes and a comparison. `None` if the bytes are not a
|
||||
/// well-formed list of attributes.
|
||||
pub fn intern_wire(&self, wire: &[u8]) -> Option<Arc<AttrSet>> {
|
||||
let hash = self.hasher.hash_one(wire);
|
||||
let mut stripe = self.stripe(hash);
|
||||
if let Some(existing) = stripe.find(hash, |set| *set.wire == *wire) {
|
||||
return Some(Arc::clone(existing));
|
||||
}
|
||||
let set = Arc::new(AttrSet {
|
||||
hash,
|
||||
key: scan(wire)?,
|
||||
wire: wire.into(),
|
||||
});
|
||||
stripe.insert_unique(hash, Arc::clone(&set), |set| set.hash);
|
||||
Some(set)
|
||||
}
|
||||
|
||||
/// The set for attributes as a session received them: `wire` is what
|
||||
/// the decoder kept of the UPDATE's attributes
|
||||
/// (`DecodedUpdate::attributes_wire`), and `next_hop` the next hop of
|
||||
/// the routes if they came in MP_REACH_NLRI.
|
||||
pub fn intern_received(
|
||||
&self,
|
||||
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() {
|
||||
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)
|
||||
}
|
||||
|
||||
/// How many distinct sets are stored.
|
||||
pub fn len(&self) -> usize {
|
||||
(0..STRIPES).map(|i| self.stripe_at(i).len()).sum()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.len() == 0
|
||||
}
|
||||
|
||||
/// Drops the sets that no route refers to any more, and returns how
|
||||
/// many that was. Sets are not dropped as their last route goes, since
|
||||
/// that would mean a lock here for every route removed; this is called
|
||||
/// now and then instead.
|
||||
pub fn sweep(&self) -> usize {
|
||||
let mut dropped = 0;
|
||||
for i in 0..STRIPES {
|
||||
let mut stripe = self.stripe_at(i);
|
||||
let before = stripe.len();
|
||||
// The store's own reference is the only one left. Nobody can
|
||||
// be taking another: that is done under this lock.
|
||||
stripe.retain(|set| Arc::strong_count(set) > 1);
|
||||
dropped += before - stripe.len();
|
||||
}
|
||||
dropped
|
||||
}
|
||||
|
||||
fn stripe(&self, hash: u64) -> MutexGuard<'_, HashTable<Arc<AttrSet>>> {
|
||||
self.stripe_at(super::spread(hash, STRIPES))
|
||||
}
|
||||
|
||||
fn stripe_at(&self, index: usize) -> MutexGuard<'_, HashTable<Arc<AttrSet>>> {
|
||||
// A panic while holding the lock cannot leave the table half
|
||||
// changed in a way that matters: see `IgnorePoison` in the server.
|
||||
self.stripes[index]
|
||||
.lock()
|
||||
.unwrap_or_else(PoisonError::into_inner)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::net::Ipv4Addr;
|
||||
|
||||
use bgp_packet::message::{
|
||||
AsPathAttribute, AsPathSegment, CommunitiesPathAttribute, LocalPrefPathAttribute,
|
||||
MultiExitDiscPathAttribute, NextHopPathAttribute, OriginPathAttribute,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn attributes(path: Vec<u32>) -> Vec<PathAttribute> {
|
||||
vec![
|
||||
PathAttribute::Origin(OriginPathAttribute::EGP),
|
||||
AsPathAttribute::from_asns(path),
|
||||
PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::new(192, 0, 2, 1))),
|
||||
PathAttribute::MultiExitDisc(MultiExitDiscPathAttribute(50)),
|
||||
PathAttribute::Communities(CommunitiesPathAttribute(vec![(65000, 1)])),
|
||||
]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn equal_attributes_are_one_set() {
|
||||
let store = AttrStore::new();
|
||||
let a = store.intern(&attributes(vec![65001, 65002]), None).unwrap();
|
||||
let b = store.intern(&attributes(vec![65001, 65002]), None).unwrap();
|
||||
let c = store.intern(&attributes(vec![65001, 65003]), None).unwrap();
|
||||
assert!(Arc::ptr_eq(&a, &b));
|
||||
assert!(!Arc::ptr_eq(&a, &c));
|
||||
assert_eq!(store.len(), 2);
|
||||
|
||||
// The next hop is part of what a set is.
|
||||
let next_hop = NlriNextHop::Ipv6("2001:db8::1".parse().unwrap());
|
||||
let d = store
|
||||
.intern(&attributes(vec![65001, 65002]), Some(&next_hop))
|
||||
.unwrap();
|
||||
assert!(!Arc::ptr_eq(&a, &d));
|
||||
assert_eq!(store.len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_set_gives_back_what_went_in() {
|
||||
let store = AttrStore::new();
|
||||
let next_hop = NlriNextHop::Ipv6WithLl {
|
||||
global: "2001:db8::1".parse().unwrap(),
|
||||
link_local: "fe80::1".parse().unwrap(),
|
||||
};
|
||||
let original = attributes(vec![65001, 4_200_000_001]);
|
||||
let set = store.intern(&original, Some(&next_hop)).unwrap();
|
||||
assert_eq!(set.attributes(), Some((original, Some(next_hop))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_selection_key_is_read_off_the_attributes() {
|
||||
let store = AttrStore::new();
|
||||
let mut attrs = vec![
|
||||
PathAttribute::Origin(OriginPathAttribute::INCOMPLETE),
|
||||
PathAttribute::AsPath(AsPathAttribute {
|
||||
segments: vec![
|
||||
AsPathSegment {
|
||||
ordered: true,
|
||||
path: vec![65001, 65002],
|
||||
},
|
||||
// A set counts as one hop.
|
||||
AsPathSegment {
|
||||
ordered: false,
|
||||
path: vec![65003, 65004, 65005],
|
||||
},
|
||||
],
|
||||
}),
|
||||
PathAttribute::LocalPref(LocalPrefPathAttribute(200)),
|
||||
];
|
||||
let key = *store.intern(&attrs, None).unwrap().key();
|
||||
assert_eq!(
|
||||
key,
|
||||
SelectionKey {
|
||||
local_pref: Some(200),
|
||||
med: None,
|
||||
neighbor_as: 65001,
|
||||
as_path_len: 3,
|
||||
origin: 2,
|
||||
// The path ends in a set: it has no one origin.
|
||||
origin_as: 0,
|
||||
}
|
||||
);
|
||||
|
||||
attrs.push(PathAttribute::MultiExitDisc(MultiExitDiscPathAttribute(7)));
|
||||
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();
|
||||
let next_hop = NlriNextHop::Ipv6("2001:db8::1".parse().unwrap());
|
||||
let mut attrs = attributes(vec![65001, 65002, 65003]);
|
||||
attrs.push(PathAttribute::LocalPref(LocalPrefPathAttribute(300)));
|
||||
let parsed = store.intern(&attrs, Some(&next_hop)).unwrap();
|
||||
|
||||
// The same set by its bytes is the same set, key and all.
|
||||
let by_wire = store.intern_wire(parsed.wire()).unwrap();
|
||||
assert!(Arc::ptr_eq(&parsed, &by_wire));
|
||||
|
||||
// And one that arrives as bytes first gets the same key as it would
|
||||
// have had from the parsed form.
|
||||
let other = AttrStore::new();
|
||||
let fresh = other.intern_wire(parsed.wire()).unwrap();
|
||||
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.
|
||||
assert!(other.intern_wire(&[0x40, 0x01, 0x05, 0x00]).is_none());
|
||||
assert!(
|
||||
other
|
||||
.intern_wire(&[0x40, 0x02, 0x03, 0x02, 0x01, 0x00])
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sweeping_drops_only_sets_nobody_holds() {
|
||||
let store = AttrStore::new();
|
||||
let kept = store.intern(&attributes(vec![65001]), None).unwrap();
|
||||
drop(store.intern(&attributes(vec![65002]), None).unwrap());
|
||||
assert_eq!(store.len(), 2);
|
||||
|
||||
assert_eq!(store.sweep(), 1);
|
||||
assert_eq!(store.len(), 1);
|
||||
// The one that is left is still the one that is found.
|
||||
let again = store.intern(&attributes(vec![65001]), None).unwrap();
|
||||
assert!(Arc::ptr_eq(&kept, &again));
|
||||
}
|
||||
}
|
||||
@@ -1,950 +0,0 @@
|
||||
//! The journal: how the rest of the router follows the best routes.
|
||||
//!
|
||||
//! Whatever acts on the table's choices (the peers that are sent the best
|
||||
//! routes, the forwarding table, the status page) has to learn when a best
|
||||
//! route changes, at its own pace, without the table waiting for it. Each
|
||||
//! shard keeps a journal for that, and each follower reads it through a
|
||||
//! [`Reader`].
|
||||
//!
|
||||
//! # What the journal holds
|
||||
//!
|
||||
//! It records *that* a prefix's best route changed, not what it changed to.
|
||||
//! A reader that comes to an entry looks the prefix up and is given the best
|
||||
//! route as it is at that moment. A follower does not want the history; it
|
||||
//! wants to end up with what the table has now.
|
||||
//!
|
||||
//! This is what keeps the journal small. If a prefix changes again before a
|
||||
//! reader has got to its entry, the older entry no longer says anything: the
|
||||
//! newer one will deliver the current route. Each row remembers the
|
||||
//! sequence number of its latest change, an entry with any other number is
|
||||
//! dead, and dead entries are thrown out when the journal is tidied. So a
|
||||
//! prefix that flaps a thousand times costs a reader one event, and the
|
||||
//! journal never has more live entries than the table has prefixes.
|
||||
//!
|
||||
//! # Readers that fall behind
|
||||
//!
|
||||
//! Entries are dropped once every reader has passed them. A reader that
|
||||
//! stops reading would hold them all back, so there is a limit: when the
|
||||
//! journal is too long even after tidying, its oldest half is dropped
|
||||
//! regardless. A reader that was behind that point is told so the next time
|
||||
//! it reads ([`Event::Reset`]) and is given the whole table again. Reading
|
||||
//! too slowly costs the slow reader a resynchronisation; it costs the
|
||||
//! table, and the other readers, nothing.
|
||||
//!
|
||||
//! A new reader starts the same way: `Reset`, every best route, `Synced`,
|
||||
//! and changes from then on.
|
||||
//!
|
||||
//! # All routes, or only the best
|
||||
//!
|
||||
//! There are two journals in each shard. One records changes of best route,
|
||||
//! for followers that want one route for each prefix: the forwarding table,
|
||||
//! and peers that are sent the best route. The other records every change
|
||||
//! to any route, for followers that want them all: peers that are sent
|
||||
//! several routes for a prefix (ADD-PATH, RFC 7911), or a choice made for
|
||||
//! them alone. A [`Reader`] follows one or the other. The second journal
|
||||
//! costs nothing while nobody follows it.
|
||||
//!
|
||||
//! # Order
|
||||
//!
|
||||
//! For any one prefix, a reader is given its states in the order they came
|
||||
//! about, and always, in the end, the latest. Between prefixes there is no
|
||||
//! order worth relying on.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
use std::hash::BuildHasher;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
|
||||
use bgp_primitives::ip_prefix::IpPrefix;
|
||||
use foldhash::fast::RandomState;
|
||||
use hashbrown::{HashMap, HashTable};
|
||||
|
||||
use super::{AttrSet, PeerIndex, RibTable, RouteRef, Row, Shard, read, write};
|
||||
|
||||
/// A cursor that holds nothing back: a reader slot that is free, or a
|
||||
/// reader that has not read this shard yet.
|
||||
const NO_CURSOR: u64 = u64::MAX;
|
||||
|
||||
/// When a journal is tidied, and when it is too long.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub(super) struct Limits {
|
||||
/// Below this many entries a journal is not worth tidying.
|
||||
pub(super) tidy_at: usize,
|
||||
/// Room allowed beyond the number of prefixes before the journal is
|
||||
/// thought too long.
|
||||
pub(super) slack: usize,
|
||||
}
|
||||
|
||||
impl Default for Limits {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
tidy_at: 1024,
|
||||
slack: 64,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
kind: Kind,
|
||||
/// The sequence number of the latest change. Counts every change this
|
||||
/// journal is for, whether or not anyone was reading.
|
||||
seq: u64,
|
||||
/// Entries up to and including this number were dropped without regard
|
||||
/// to the readers. A reader whose cursor is below it missed something.
|
||||
floor: u64,
|
||||
/// In order of sequence number. Not every number is here: dead entries
|
||||
/// are removed, and nothing is recorded while there are no readers.
|
||||
entries: VecDeque<Entry>,
|
||||
/// Prefixes that have no route any more and whose disappearance is
|
||||
/// still to be read, with the sequence number of the entry that says
|
||||
/// so. A row keeps this number itself; a prefix without a row cannot.
|
||||
gone: HashMap<IpPrefix, u64, RandomState>,
|
||||
/// Where each reader has read to, by reader slot. Written by the
|
||||
/// readers themselves while they hold the shard for reading.
|
||||
cursors: Vec<AtomicU64>,
|
||||
/// 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 {
|
||||
fn new(kind: Kind, limits: Limits) -> Self {
|
||||
Self {
|
||||
kind,
|
||||
limits,
|
||||
seq: 0,
|
||||
floor: 0,
|
||||
entries: VecDeque::new(),
|
||||
gone: HashMap::default(),
|
||||
cursors: Vec::new(),
|
||||
readers: 0,
|
||||
stamp: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Notes a change to `prefix`, which has a row. Returns the sequence
|
||||
/// number for the row to remember.
|
||||
fn changed(&mut self, prefix: IpPrefix) -> u64 {
|
||||
self.seq += 1;
|
||||
if self.readers > 0 {
|
||||
self.entries.push_back(Entry {
|
||||
seq: self.seq,
|
||||
prefix,
|
||||
stamp: self.stamp,
|
||||
});
|
||||
}
|
||||
self.seq
|
||||
}
|
||||
|
||||
/// As [`Journal::changed`], for a prefix whose row was just made:
|
||||
/// if it had gone and is back, the note that it had gone is void.
|
||||
fn appeared(&mut self, prefix: IpPrefix) -> u64 {
|
||||
if !self.gone.is_empty() {
|
||||
self.gone.remove(&prefix);
|
||||
}
|
||||
self.changed(prefix)
|
||||
}
|
||||
|
||||
/// Notes that `prefix` lost its last route and has no row any more.
|
||||
fn disappeared(&mut self, prefix: IpPrefix) {
|
||||
self.seq += 1;
|
||||
if self.readers > 0 {
|
||||
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) {
|
||||
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> {
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
/// Keeps the journal from growing. Called after every change to the
|
||||
/// shard; does nothing until there is something worth doing.
|
||||
fn tidy(&mut self, rows: &HashTable<Row>, hasher: &RandomState) {
|
||||
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) {
|
||||
let passed = self
|
||||
.cursors
|
||||
.iter()
|
||||
.map(|cursor| cursor.load(Ordering::Acquire))
|
||||
.min()
|
||||
.unwrap_or(NO_CURSOR);
|
||||
while let Some(front) = self.entries.front() {
|
||||
if front.seq > passed {
|
||||
break;
|
||||
}
|
||||
Self::void_gone_note(&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) {
|
||||
let mut entries = std::mem::take(&mut self.entries);
|
||||
entries.retain(|entry| !matches!(self.says(entry, rows, hasher), Says::Nothing));
|
||||
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) {
|
||||
for entry in self.entries.drain(..self.entries.len() / 2) {
|
||||
Self::void_gone_note(&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 {
|
||||
/// Changes of best route.
|
||||
Best,
|
||||
/// Changes of any route.
|
||||
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>;
|
||||
|
||||
/// 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>;
|
||||
|
||||
impl Journals {
|
||||
pub(super) fn new(limits: Limits) -> Self {
|
||||
Self {
|
||||
best: Journal::new(Kind::Best, limits),
|
||||
all: Journal::new(Kind::All, limits),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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);
|
||||
}
|
||||
|
||||
/// 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;
|
||||
}
|
||||
|
||||
/// Notes that `prefix` has its first route, and so a row. Returns the
|
||||
/// sequence numbers for the new row.
|
||||
pub(super) fn appeared(&mut self, prefix: IpPrefix) -> Seqs {
|
||||
Seqs {
|
||||
best: self.best.appeared(prefix),
|
||||
all: self.all.appeared(prefix),
|
||||
}
|
||||
}
|
||||
|
||||
/// Notes that a route of `row` changed, and whether its best route did.
|
||||
pub(super) fn changed(&mut self, row: &mut Row, best_changed: bool) {
|
||||
if best_changed {
|
||||
row.seqs.best = self.best.changed(row.prefix);
|
||||
}
|
||||
row.seqs.all = self.all.changed(row.prefix);
|
||||
}
|
||||
|
||||
/// Notes that `prefix` lost its last route and has no row any more.
|
||||
pub(super) fn disappeared(&mut self, prefix: IpPrefix) {
|
||||
self.best.disappeared(prefix);
|
||||
self.all.disappeared(prefix);
|
||||
}
|
||||
|
||||
/// Keeps the journals from growing; see [`Journal::tidy`].
|
||||
pub(super) fn tidy(&mut self, rows: &HashTable<Row>, hasher: &RandomState) {
|
||||
self.best.tidy(rows, hasher);
|
||||
self.all.tidy(rows, hasher);
|
||||
}
|
||||
|
||||
/// Where the journals are now, to ask [`Journals::has_news_since`]
|
||||
/// about later.
|
||||
pub(super) fn mark(&self) -> Mark {
|
||||
Mark(self.best.seq, self.all.seq)
|
||||
}
|
||||
|
||||
/// Whether anything was written since `mark` that some reader is there
|
||||
/// to read.
|
||||
pub(super) fn has_news_since(&self, mark: Mark) -> bool {
|
||||
(self.best.readers > 0 && self.best.seq != mark.0)
|
||||
|| (self.all.readers > 0 && self.all.seq != mark.1)
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
self.best.entries.len() + self.all.entries.len()
|
||||
}
|
||||
}
|
||||
|
||||
/// A point in a shard's journals; see [`Journals::mark`].
|
||||
#[derive(Clone, Copy)]
|
||||
pub(super) struct Mark(u64, u64);
|
||||
|
||||
/// The routes of one prefix, as a follower of all routes is given them.
|
||||
#[derive(Clone, Copy)]
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Routes<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_list().entries(self.iter()).finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// What a [`Reader`] is given.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
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.
|
||||
Reset,
|
||||
/// This is now the best route for `prefix`. Given to a follower of the
|
||||
/// best routes.
|
||||
Best {
|
||||
prefix: IpPrefix,
|
||||
peer: PeerIndex,
|
||||
/// The peer's identifier for the route: zero unless the peer
|
||||
/// sends several routes for a prefix (ADD-PATH, RFC 7911).
|
||||
path_id: u32,
|
||||
attrs: &'a Arc<AttrSet>,
|
||||
},
|
||||
/// These are now all the routes for `prefix`: whatever was known of its
|
||||
/// routes before is replaced. Given to a follower of all routes.
|
||||
Routes {
|
||||
prefix: IpPrefix,
|
||||
routes: Routes<'a>,
|
||||
},
|
||||
/// `prefix` has no route any more.
|
||||
Withdrawn { prefix: IpPrefix },
|
||||
/// The table that began with [`Event::Reset`] is complete: a prefix
|
||||
/// not heard of since the reset has no route.
|
||||
Synced,
|
||||
}
|
||||
|
||||
/// What a call to [`Reader::poll`] got done.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Progress {
|
||||
/// How many events were given out.
|
||||
pub events: usize,
|
||||
/// Whether the reader has now been given everything there was when the
|
||||
/// call began. If not, call again.
|
||||
pub caught_up: bool,
|
||||
}
|
||||
|
||||
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 },
|
||||
}
|
||||
|
||||
impl Phase {
|
||||
/// The beginning of a resynchronisation.
|
||||
const RESYNC: Self = Self::Resync {
|
||||
next: 0,
|
||||
upto: None,
|
||||
};
|
||||
|
||||
/// 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.
|
||||
/// Made by [`RibTable::reader`] and its siblings.
|
||||
///
|
||||
/// A reader holds its place in every shard's journal. Dropping it gives
|
||||
/// the place up, so that the journals need not keep entries for it.
|
||||
pub struct Reader {
|
||||
table: Arc<RibTable>,
|
||||
/// Which journal it follows.
|
||||
kind: Kind,
|
||||
slot: usize,
|
||||
/// Where this reader has read to in each shard; the copy in the shard
|
||||
/// 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.
|
||||
seen: u64,
|
||||
caught_up: bool,
|
||||
}
|
||||
|
||||
impl RibTable {
|
||||
/// A new follower of this table's best routes. It is given the whole
|
||||
/// table first and every change after.
|
||||
pub fn reader(self: &Arc<Self>) -> Reader {
|
||||
self.register(Kind::Best, Phase::RESYNC)
|
||||
}
|
||||
|
||||
/// A new follower that is given only the changes from now on, for
|
||||
/// 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);
|
||||
for (index, shard) in self.shards.iter().enumerate() {
|
||||
let shard = read(shard);
|
||||
reader.set_cursor(index, &shard, shard.journals.best.seq);
|
||||
}
|
||||
reader
|
||||
}
|
||||
|
||||
/// A new follower of every route in the table, not only the best: it
|
||||
/// is told whenever any route of a prefix changes, with all the
|
||||
/// prefix's routes ([`Event::Routes`]). The whole table first, like
|
||||
/// [`RibTable::reader`].
|
||||
pub fn reader_of_all_routes(self: &Arc<Self>) -> Reader {
|
||||
self.register(Kind::All, Phase::RESYNC)
|
||||
}
|
||||
|
||||
fn register(self: &Arc<Self>, kind: Kind, phase: Phase) -> Reader {
|
||||
let mut slots = self
|
||||
.reader_slots
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let slot = match slots.iter().position(|used| !used) {
|
||||
Some(free) => free,
|
||||
None => {
|
||||
slots.push(false);
|
||||
slots.len() - 1
|
||||
}
|
||||
};
|
||||
slots[slot] = true;
|
||||
for shard in &self.shards {
|
||||
let mut shard = write(shard);
|
||||
let journal = shard.journals.of_mut(kind);
|
||||
while journal.cursors.len() <= slot {
|
||||
journal.cursors.push(AtomicU64::new(NO_CURSOR));
|
||||
}
|
||||
journal.cursors[slot].store(NO_CURSOR, Ordering::Release);
|
||||
journal.readers += 1;
|
||||
}
|
||||
self.readers.fetch_add(1, Ordering::AcqRel);
|
||||
Reader {
|
||||
table: Arc::clone(self),
|
||||
kind,
|
||||
slot,
|
||||
cursors: vec![NO_CURSOR; self.shards.len()],
|
||||
phase,
|
||||
seen: 0,
|
||||
caught_up: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// How many entries the journals hold, over all shards. For tests and
|
||||
/// for watching: it stays bounded whatever the readers do.
|
||||
pub fn journal_len(&self) -> usize {
|
||||
self.shards
|
||||
.iter()
|
||||
.map(|shard| read(shard).journals.len())
|
||||
.sum()
|
||||
}
|
||||
|
||||
/// Tells readers that are waiting that there is something to read.
|
||||
pub(super) fn wake_readers(&self) {
|
||||
if self.readers.load(Ordering::Acquire) > 0 {
|
||||
self.version.fetch_add(1, Ordering::AcqRel);
|
||||
self.wake.notify_waiters();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Reader {
|
||||
fn drop(&mut self) {
|
||||
for shard in &self.table.shards {
|
||||
let mut shard = write(shard);
|
||||
let journal = shard.journals.of_mut(self.kind);
|
||||
journal.cursors[self.slot].store(NO_CURSOR, Ordering::Release);
|
||||
journal.readers -= 1;
|
||||
if journal.readers == 0 {
|
||||
// Nobody left to read it.
|
||||
journal.entries.clear();
|
||||
journal.gone.clear();
|
||||
}
|
||||
}
|
||||
self.table.readers.fetch_sub(1, Ordering::AcqRel);
|
||||
let mut slots = self
|
||||
.table
|
||||
.reader_slots
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
slots[self.slot] = false;
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
}
|
||||
|
||||
impl Reader {
|
||||
/// Gives `visit` what has changed since the last call, in up to about
|
||||
/// `budget` events (a shard's part of a resynchronisation is not cut
|
||||
/// short, so it can be more).
|
||||
///
|
||||
/// `visit` is called with a shard of the table held for reading. It
|
||||
/// should be quick, and it must not call into the table, even to read:
|
||||
/// taking the shard's lock again can wait for ever on a writer that is
|
||||
/// waiting for this hold to end.
|
||||
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 mut events = 0;
|
||||
loop {
|
||||
let done = match self.phase {
|
||||
Phase::Resync { next, upto } => {
|
||||
self.resync_step(&table, next, upto, budget, &mut events, &mut visit)
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
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 {
|
||||
self.caught_up = caught_up;
|
||||
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) {
|
||||
loop {
|
||||
// Registered before the check, so that a change between the
|
||||
// check and the wait is not missed.
|
||||
let woken = self.table.wake.notified();
|
||||
tokio::pin!(woken);
|
||||
woken.as_mut().enable();
|
||||
if !self.caught_up || self.table.version.load(Ordering::Acquire) != self.seen {
|
||||
return;
|
||||
}
|
||||
woken.await;
|
||||
}
|
||||
}
|
||||
|
||||
/// What this reader is told about a row.
|
||||
fn event_for<'a>(&self, row: &'a Row) -> Event<'a> {
|
||||
match self.kind {
|
||||
Kind::Best => {
|
||||
let best = row.route(row.best());
|
||||
Event::Best {
|
||||
prefix: row.prefix,
|
||||
peer: best.peer,
|
||||
path_id: best.path_id,
|
||||
attrs: best.attrs,
|
||||
}
|
||||
}
|
||||
Kind::All => Event::Routes {
|
||||
prefix: row.prefix,
|
||||
routes: Routes(row),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Gives up this reader's place in every journal, before a
|
||||
/// resynchronisation takes a new one.
|
||||
fn forget_place(&mut self) {
|
||||
for (index, shard) in self.table.shards.iter().enumerate() {
|
||||
read(shard).journals.of(self.kind).cursors[self.slot]
|
||||
.store(NO_CURSOR, Ordering::Release);
|
||||
self.cursors[index] = NO_CURSOR;
|
||||
}
|
||||
}
|
||||
|
||||
fn set_cursor(&mut self, index: usize, shard: &Shard, cursor: u64) {
|
||||
self.cursors[index] = cursor;
|
||||
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 {
|
||||
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);
|
||||
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 {
|
||||
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,
|
||||
};
|
||||
let mut events = 0;
|
||||
let mut more = false;
|
||||
for entry in journal.entries.range(start..) {
|
||||
if entry.stamp > upto {
|
||||
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 {
|
||||
prefix: entry.prefix,
|
||||
},
|
||||
Says::Nothing => continue,
|
||||
};
|
||||
visit(event);
|
||||
events += 1;
|
||||
}
|
||||
if !more {
|
||||
// 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "journal_tests.rs"]
|
||||
mod tests;
|
||||
@@ -1,906 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
use bgp_primitives::constants::IpVersion;
|
||||
use bgp_primitives::ip_prefix::IpPrefix;
|
||||
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};
|
||||
|
||||
/// What a follower would keep: its own copy of the best routes, brought
|
||||
/// up to date from the events it is given.
|
||||
#[derive(Default)]
|
||||
struct Mirror {
|
||||
best: HashMap<IpPrefix, (PeerIndex, Arc<AttrSet>)>,
|
||||
/// What was held before a reset and has not been heard of since.
|
||||
stale: HashMap<IpPrefix, (PeerIndex, Arc<AttrSet>)>,
|
||||
/// For a follower of all routes: every route of every prefix, as
|
||||
/// (peer, path identifier, attributes, whether best).
|
||||
routes: HashMap<IpPrefix, Vec<Held>>,
|
||||
resets: usize,
|
||||
synced: usize,
|
||||
best_events: usize,
|
||||
routes_events: usize,
|
||||
withdrawn_events: usize,
|
||||
}
|
||||
|
||||
/// A route as a mirror holds it. The attributes are held, so that the
|
||||
/// address used to compare them stays theirs.
|
||||
type Held = (PeerIndex, u32, Arc<AttrSet>, bool);
|
||||
|
||||
fn same_routes(a: &[Held], b: &[Held]) -> bool {
|
||||
a.len() == b.len()
|
||||
&& a.iter()
|
||||
.zip(b)
|
||||
.all(|(a, b)| (a.0, a.1, a.3) == (b.0, b.1, b.3) && Arc::ptr_eq(&a.2, &b.2))
|
||||
}
|
||||
|
||||
fn sorted(mut routes: Vec<Held>) -> Vec<Held> {
|
||||
routes.sort_by_key(|(peer, path_id, _, _)| (*peer, *path_id));
|
||||
routes
|
||||
}
|
||||
|
||||
impl Mirror {
|
||||
fn apply(&mut self, event: Event<'_>) {
|
||||
match event {
|
||||
Event::Reset => {
|
||||
self.resets += 1;
|
||||
self.stale = std::mem::take(&mut self.best);
|
||||
self.routes.clear();
|
||||
}
|
||||
Event::Routes { prefix, routes } => {
|
||||
self.routes_events += 1;
|
||||
assert!(
|
||||
routes.iter().next().is_some(),
|
||||
"a prefix with no routes is withdrawn"
|
||||
);
|
||||
self.routes.insert(
|
||||
prefix,
|
||||
sorted(
|
||||
routes
|
||||
.iter()
|
||||
.map(|r| (r.peer, r.path_id, Arc::clone(r.attrs), r.best))
|
||||
.collect(),
|
||||
),
|
||||
);
|
||||
}
|
||||
Event::Best {
|
||||
prefix,
|
||||
peer,
|
||||
attrs,
|
||||
..
|
||||
} => {
|
||||
self.best_events += 1;
|
||||
self.stale.remove(&prefix);
|
||||
self.best.insert(prefix, (peer, Arc::clone(attrs)));
|
||||
}
|
||||
Event::Withdrawn { prefix } => {
|
||||
self.withdrawn_events += 1;
|
||||
self.best.remove(&prefix);
|
||||
self.routes.remove(&prefix);
|
||||
}
|
||||
Event::Synced => {
|
||||
self.synced += 1;
|
||||
self.stale.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads until there is nothing more, in steps of `budget`.
|
||||
fn catch_up(&mut self, reader: &mut Reader, budget: usize) {
|
||||
while !reader.poll(budget, |event| self.apply(event)).caught_up {}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn assert_matches(&self, table: &RibTable) {
|
||||
let mut expected = HashMap::new();
|
||||
table.for_each_best(|prefix, peer, attrs| {
|
||||
expected.insert(*prefix, (peer, Arc::clone(attrs)));
|
||||
});
|
||||
assert_eq!(
|
||||
self.best.len(),
|
||||
expected.len(),
|
||||
"the mirror has {} prefixes, the table {}",
|
||||
self.best.len(),
|
||||
expected.len()
|
||||
);
|
||||
for (prefix, (peer, attrs)) in &expected {
|
||||
let Some((mirrored_peer, mirrored_attrs)) = self.best.get(prefix) else {
|
||||
panic!("{prefix} is in the table and not in the mirror");
|
||||
};
|
||||
assert_eq!(mirrored_peer, peer, "{prefix}: from the wrong peer");
|
||||
assert!(
|
||||
Arc::ptr_eq(mirrored_attrs, attrs),
|
||||
"{prefix}: wrong attributes"
|
||||
);
|
||||
}
|
||||
}
|
||||
/// For the mirror of a follower of all routes: it holds every route in
|
||||
/// the table, and knows which is best.
|
||||
#[track_caller]
|
||||
fn assert_has_all_routes_of(&self, table: &RibTable) {
|
||||
let mut expected: HashMap<IpPrefix, Vec<Held>> = HashMap::new();
|
||||
table.for_each_route(|r| {
|
||||
expected.entry(r.prefix).or_default().push((
|
||||
r.peer,
|
||||
r.path_id,
|
||||
Arc::clone(r.attrs),
|
||||
r.best,
|
||||
));
|
||||
});
|
||||
assert_eq!(self.routes.len(), expected.len(), "prefixes");
|
||||
for (prefix, routes) in expected {
|
||||
let mirrored = self
|
||||
.routes
|
||||
.get(&prefix)
|
||||
.unwrap_or_else(|| panic!("{prefix} is in the table and not in the mirror"));
|
||||
assert!(
|
||||
same_routes(mirrored, &sorted(routes)),
|
||||
"{prefix}: the mirror's routes are not the table's"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn nth_prefix(n: u32) -> IpPrefix {
|
||||
IpPrefix::new(IpVersion::Ipv4, (n << 8).to_be_bytes(), 24).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_new_reader_is_given_the_table_and_then_the_changes() {
|
||||
let fixture = Fixture::new(&[external(1), external(2)]);
|
||||
let (a, b) = (prefix("10.0.0.0/8"), prefix("2001:db8::/32"));
|
||||
fixture.announce(0, &a, &Route::via(&[1, 2]));
|
||||
fixture.announce(0, &b, &Route::via(&[1, 2]));
|
||||
|
||||
let mut reader = fixture.table.reader();
|
||||
let mut mirror = Mirror::default();
|
||||
let mut events = Vec::new();
|
||||
let progress = reader.poll(100, |event| {
|
||||
events.push(
|
||||
format!("{event:?}")
|
||||
.split([' ', '{'])
|
||||
.next()
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
);
|
||||
mirror.apply(event);
|
||||
});
|
||||
assert!(progress.caught_up);
|
||||
assert_eq!(events, ["Reset", "Best", "Best", "Synced"]);
|
||||
mirror.assert_matches(&fixture.table);
|
||||
|
||||
// Nothing has happened: nothing to read.
|
||||
assert_eq!(reader.poll(100, |_| panic!("no event expected")).events, 0);
|
||||
|
||||
// A change that does not change the best route is not an event.
|
||||
fixture.announce(1, &a, &Route::via(&[3, 4, 5]));
|
||||
assert_eq!(reader.poll(100, |_| panic!("no event expected")).events, 0);
|
||||
|
||||
fixture.announce(1, &a, &Route::via(&[3]));
|
||||
fixture.withdraw(0, &b);
|
||||
mirror.catch_up(&mut reader, 100);
|
||||
assert_eq!((mirror.best_events, mirror.withdrawn_events), (3, 1));
|
||||
assert_eq!((mirror.resets, mirror.synced), (1, 1));
|
||||
mirror.assert_matches(&fixture.table);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_reader_from_now_is_given_the_changes_and_not_the_table() {
|
||||
let fixture = Fixture::new(&[external(1)]);
|
||||
let (old, new) = (prefix("10.0.0.0/8"), prefix("10.1.0.0/16"));
|
||||
fixture.announce(0, &old, &Route::via(&[1]));
|
||||
|
||||
let mut reader = fixture.table.reader_from_now();
|
||||
assert_eq!(
|
||||
reader
|
||||
.poll(100, |event| panic!("unexpected {event:?}"))
|
||||
.events,
|
||||
0
|
||||
);
|
||||
|
||||
fixture.announce(0, &new, &Route::via(&[1]));
|
||||
fixture.withdraw(0, &old);
|
||||
let (mut best, mut withdrawn) = (Vec::new(), Vec::new());
|
||||
reader.poll(100, |event| match event {
|
||||
Event::Best { prefix, .. } => best.push(prefix),
|
||||
Event::Withdrawn { prefix } => withdrawn.push(prefix),
|
||||
other => panic!("unexpected {other:?}"),
|
||||
});
|
||||
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]
|
||||
fn a_follower_of_all_routes_is_told_of_every_change() {
|
||||
let fixture = Fixture::new(&[external(1), external(2)]);
|
||||
let p = prefix("10.0.0.0/8");
|
||||
fixture.announce(0, &p, &Route::via(&[1]));
|
||||
|
||||
let mut best = fixture.table.reader();
|
||||
let mut all = fixture.table.reader_of_all_routes();
|
||||
let (mut best_mirror, mut all_mirror) = (Mirror::default(), Mirror::default());
|
||||
best_mirror.catch_up(&mut best, 100);
|
||||
all_mirror.catch_up(&mut all, 100);
|
||||
assert_eq!((best_mirror.best_events, all_mirror.routes_events), (1, 1));
|
||||
|
||||
// A worse route from another peer, then two more from that peer under
|
||||
// other path identifiers, one of them changed, one withdrawn.
|
||||
fixture.announce(1, &p, &Route::via(&[2, 3, 4]));
|
||||
fixture.announce_path(1, &p, 7, &Route::via(&[2, 3, 5]));
|
||||
fixture.announce_path(1, &p, 8, &Route::via(&[2, 3, 6]));
|
||||
fixture.announce_path(1, &p, 7, &Route::via(&[2, 9, 5]));
|
||||
fixture.withdraw_path(1, &p, 8);
|
||||
|
||||
assert_eq!(
|
||||
best.poll(100, |event| panic!("unexpected {event:?}"))
|
||||
.events,
|
||||
0
|
||||
);
|
||||
all_mirror.catch_up(&mut all, 100);
|
||||
// All of it between two reads is one event, with the routes as they
|
||||
// are now.
|
||||
assert_eq!(all_mirror.routes_events, 2);
|
||||
assert_eq!(all_mirror.routes[&p].len(), 3);
|
||||
all_mirror.assert_has_all_routes_of(&fixture.table);
|
||||
|
||||
// The last route going is a withdrawal for both.
|
||||
fixture.table.remove_peer(fixture.peers[1]);
|
||||
fixture.withdraw(0, &p);
|
||||
best_mirror.catch_up(&mut best, 100);
|
||||
all_mirror.catch_up(&mut all, 100);
|
||||
assert_eq!(
|
||||
(best_mirror.withdrawn_events, all_mirror.withdrawn_events),
|
||||
(1, 1)
|
||||
);
|
||||
all_mirror.assert_has_all_routes_of(&fixture.table);
|
||||
|
||||
// Nobody following all routes: that journal keeps nothing. A change
|
||||
// that is not of the best route is then recorded nowhere.
|
||||
drop(all);
|
||||
fixture.announce(0, &p, &Route::via(&[1]));
|
||||
fixture.announce(1, &p, &Route::via(&[2, 3]));
|
||||
let before = fixture.table.journal_len();
|
||||
fixture.announce(1, &p, &Route::via(&[2, 4]));
|
||||
fixture.announce_path(1, &p, 7, &Route::via(&[2, 5]));
|
||||
assert_eq!(fixture.table.journal_len(), before);
|
||||
}
|
||||
|
||||
/// A follower wants what the table has now, not how it got there.
|
||||
#[test]
|
||||
fn many_changes_to_a_prefix_between_reads_are_one_event() {
|
||||
let fixture = Fixture::new(&[external(1), external(2)]);
|
||||
let p = prefix("10.0.0.0/8");
|
||||
let mut reader = fixture.table.reader();
|
||||
let mut mirror = Mirror::default();
|
||||
mirror.catch_up(&mut reader, 100);
|
||||
|
||||
for round in 0..50 {
|
||||
fixture.announce(0, &p, &Route::via(&[1, 2, 3]));
|
||||
fixture.announce(1, &p, &Route::via(&[4, 5]));
|
||||
fixture.withdraw(1, &p);
|
||||
if round % 2 == 0 {
|
||||
fixture.withdraw(0, &p);
|
||||
}
|
||||
}
|
||||
// It ended with peer 0's route in place.
|
||||
mirror.catch_up(&mut reader, 100);
|
||||
assert_eq!((mirror.best_events, mirror.withdrawn_events), (1, 0));
|
||||
mirror.assert_matches(&fixture.table);
|
||||
|
||||
// And a prefix that came and went again is, at most, a withdrawal.
|
||||
let q = prefix("10.1.0.0/16");
|
||||
for _ in 0..50 {
|
||||
fixture.announce(0, &q, &Route::via(&[1]));
|
||||
fixture.withdraw(0, &q);
|
||||
}
|
||||
mirror.catch_up(&mut reader, 100);
|
||||
assert_eq!((mirror.best_events, mirror.withdrawn_events), (1, 1));
|
||||
mirror.assert_matches(&fixture.table);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reading_in_small_steps_gives_the_same_as_reading_at_once() {
|
||||
let fixture = Fixture::new(&[external(1)]);
|
||||
for n in 0..500 {
|
||||
fixture.announce(0, &nth_prefix(n), &Route::via(&[1, n % 7 + 1]));
|
||||
}
|
||||
let mut reader = fixture.table.reader();
|
||||
let mut mirror = Mirror::default();
|
||||
mirror.catch_up(&mut reader, 7);
|
||||
mirror.assert_matches(&fixture.table);
|
||||
|
||||
for n in 0..500 {
|
||||
if n % 3 == 0 {
|
||||
fixture.withdraw(0, &nth_prefix(n));
|
||||
} else {
|
||||
fixture.announce(0, &nth_prefix(n), &Route::via(&[2, n % 5 + 1]));
|
||||
}
|
||||
}
|
||||
let mut steps = 0;
|
||||
while !reader.poll(7, |event| mirror.apply(event)).caught_up {
|
||||
steps += 1;
|
||||
}
|
||||
assert!(steps > 50, "the budget was kept to: {steps} steps");
|
||||
mirror.assert_matches(&fixture.table);
|
||||
assert_eq!(mirror.resets, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_no_reader_nothing_is_kept() {
|
||||
let fixture = Fixture::new(&[external(1)]);
|
||||
for n in 0..5000 {
|
||||
fixture.announce(0, &nth_prefix(n), &Route::via(&[1]));
|
||||
fixture.withdraw(0, &nth_prefix(n));
|
||||
}
|
||||
assert_eq!(fixture.table.journal_len(), 0);
|
||||
|
||||
// And what a reader was holding back goes when the reader does.
|
||||
let reader = fixture.table.reader();
|
||||
for n in 0..100 {
|
||||
fixture.announce(0, &nth_prefix(n), &Route::via(&[1]));
|
||||
}
|
||||
assert_eq!(fixture.table.journal_len(), 100);
|
||||
drop(reader);
|
||||
assert_eq!(fixture.table.journal_len(), 0);
|
||||
}
|
||||
|
||||
/// A reader that stops reading must not make the table keep a growing
|
||||
/// journal for it. It is the one that pays: it is given the table again.
|
||||
#[test]
|
||||
fn a_reader_that_stops_reading_is_reset_and_holds_nothing_up() {
|
||||
// One shard, so that the journal's length is that shard's.
|
||||
let fixture = Fixture::on(RibTable::new(1), &[external(1)]);
|
||||
fixture.announce(0, &prefix("192.0.2.0/24"), &Route::via(&[1]));
|
||||
|
||||
let mut stuck = fixture.table.reader();
|
||||
let mut stuck_mirror = Mirror::default();
|
||||
stuck_mirror.catch_up(&mut stuck, 100);
|
||||
let mut diligent = fixture.table.reader();
|
||||
let mut diligent_mirror = Mirror::default();
|
||||
|
||||
// Prefixes come and go, each a different one, so no entry is made
|
||||
// dead by a later one for the same prefix.
|
||||
let limits = Limits::default();
|
||||
let mut longest = 0;
|
||||
for n in 0..20_000 {
|
||||
fixture.announce(0, &nth_prefix(n), &Route::via(&[1]));
|
||||
fixture.withdraw(0, &nth_prefix(n));
|
||||
longest = longest.max(fixture.table.journal_len());
|
||||
if n % 100 == 0 {
|
||||
diligent_mirror.catch_up(&mut diligent, 1000);
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
longest <= limits.tidy_at.max(2 * (1 + limits.slack)) + 2,
|
||||
"the journal reached {longest} entries"
|
||||
);
|
||||
|
||||
// The one that kept up never noticed.
|
||||
diligent_mirror.catch_up(&mut diligent, 1000);
|
||||
assert_eq!(
|
||||
diligent_mirror.resets, 1,
|
||||
"only the reset every reader starts with"
|
||||
);
|
||||
diligent_mirror.assert_matches(&fixture.table);
|
||||
|
||||
// The one that did not is told, and ends up right all the same.
|
||||
stuck_mirror.catch_up(&mut stuck, 1000);
|
||||
assert_eq!(stuck_mirror.resets, 2);
|
||||
assert_eq!(stuck_mirror.synced, 2);
|
||||
stuck_mirror.assert_matches(&fixture.table);
|
||||
}
|
||||
|
||||
/// When a peer that was the only source of most prefixes goes, more
|
||||
/// prefixes disappear at once than are left. The journal still comes down
|
||||
/// to its bound, and a reader ends up with what the table has.
|
||||
#[test]
|
||||
fn many_prefixes_going_at_once_leaves_the_journal_bounded() {
|
||||
let fixture = Fixture::on(RibTable::new(1), &[external(1), external(2)]);
|
||||
for n in 0..5000 {
|
||||
fixture.announce(0, &nth_prefix(n), &Route::via(&[1]));
|
||||
}
|
||||
fixture.announce(1, &nth_prefix(0), &Route::via(&[2, 3]));
|
||||
let mut reader = fixture.table.reader();
|
||||
let mut mirror = Mirror::default();
|
||||
mirror.catch_up(&mut reader, 1000);
|
||||
|
||||
fixture.table.remove_peer(fixture.peers[0]);
|
||||
// Some later change, as there always is, lets the journal be tidied.
|
||||
for _ in 0..8 {
|
||||
fixture.announce(1, &nth_prefix(1), &Route::via(&[2]));
|
||||
fixture.withdraw(1, &nth_prefix(1));
|
||||
}
|
||||
let limits = Limits::default();
|
||||
assert!(
|
||||
fixture.table.journal_len() <= limits.tidy_at.max(2 * (1 + limits.slack)) + 2,
|
||||
"the journal holds {} entries",
|
||||
fixture.table.journal_len()
|
||||
);
|
||||
mirror.catch_up(&mut reader, 1000);
|
||||
mirror.assert_matches(&fixture.table);
|
||||
assert_eq!(mirror.best.len(), 1);
|
||||
}
|
||||
|
||||
/// Readers are woken to read. An announcement that changes nothing, or
|
||||
/// nothing a reader follows, writes nothing for them and must not wake
|
||||
/// them: each would look through every shard to find that out.
|
||||
#[test]
|
||||
fn readers_are_not_woken_when_nothing_was_written_for_them() {
|
||||
let fixture = Fixture::new(&[external(1), external(2)]);
|
||||
let p = prefix("10.0.0.0/8");
|
||||
let version = || fixture.table.version.load(Ordering::Acquire);
|
||||
|
||||
// Nobody reading: nobody to wake.
|
||||
fixture.announce(0, &p, &Route::via(&[1]));
|
||||
assert_eq!(version(), 0);
|
||||
|
||||
let _best = fixture.table.reader();
|
||||
let before = version();
|
||||
// The same route again.
|
||||
fixture.announce(0, &p, &Route::via(&[1]));
|
||||
assert_eq!(version(), before);
|
||||
// A worse route from another peer: the best route stands.
|
||||
fixture.announce(1, &p, &Route::via(&[2, 3, 4]));
|
||||
assert_eq!(version(), before);
|
||||
// A better one.
|
||||
fixture.announce(1, &p, &Route::via(&[]));
|
||||
assert_eq!(version(), before + 1);
|
||||
|
||||
// A follower of all routes is told of any change to a route.
|
||||
let _all = fixture.table.reader_of_all_routes();
|
||||
let before = version();
|
||||
fixture.announce(0, &p, &Route::via(&[1, 1, 1]));
|
||||
assert_eq!(version(), before + 1);
|
||||
fixture.announce(0, &p, &Route::via(&[1, 1, 1]));
|
||||
assert_eq!(version(), before + 1);
|
||||
}
|
||||
|
||||
/// A budget of nothing would let a reader ask for ever without getting
|
||||
/// anywhere; it is taken as the smallest that does.
|
||||
#[test]
|
||||
fn a_budget_of_nothing_still_makes_progress() {
|
||||
let fixture = Fixture::new(&[external(1)]);
|
||||
for n in 0..20 {
|
||||
fixture.announce(0, &nth_prefix(n), &Route::via(&[1]));
|
||||
}
|
||||
let mut reader = fixture.table.reader();
|
||||
let mut mirror = Mirror::default();
|
||||
mirror.catch_up(&mut reader, 0);
|
||||
for n in 0..20 {
|
||||
fixture.withdraw(0, &nth_prefix(n));
|
||||
}
|
||||
mirror.catch_up(&mut reader, 0);
|
||||
mirror.assert_matches(&fixture.table);
|
||||
assert!(mirror.best.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_waiting_reader_is_woken_by_a_change_of_best_route() {
|
||||
let fixture = Fixture::new(&[external(1), external(2)]);
|
||||
let p = prefix("10.0.0.0/8");
|
||||
let mut reader = fixture.table.reader();
|
||||
let mut mirror = Mirror::default();
|
||||
|
||||
// Not caught up yet: no waiting.
|
||||
reader.changed().await;
|
||||
mirror.catch_up(&mut reader, 100);
|
||||
|
||||
let waiting = tokio::time::timeout(std::time::Duration::from_millis(50), reader.changed());
|
||||
assert!(waiting.await.is_err(), "nothing changed, so it waits");
|
||||
|
||||
let table = Arc::clone(&fixture.table);
|
||||
let attrs = table
|
||||
.attrs()
|
||||
.intern(&Route::via(&[1]).attributes(), None)
|
||||
.unwrap();
|
||||
let peer = fixture.peers[0];
|
||||
let writer = tokio::spawn(async move {
|
||||
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
|
||||
table.announce(peer, &attrs, &[p]);
|
||||
});
|
||||
tokio::time::timeout(std::time::Duration::from_secs(5), reader.changed())
|
||||
.await
|
||||
.expect("woken by the announcement");
|
||||
writer.await.unwrap();
|
||||
mirror.catch_up(&mut reader, 100);
|
||||
mirror.assert_matches(&fixture.table);
|
||||
|
||||
// A change made before the wait began is not missed either.
|
||||
fixture.withdraw(0, &p);
|
||||
tokio::time::timeout(std::time::Duration::from_secs(5), reader.changed())
|
||||
.await
|
||||
.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.
|
||||
#[test]
|
||||
fn a_reader_keeps_up_with_writers_on_other_threads() {
|
||||
const WRITERS: usize = 4;
|
||||
const PREFIXES: u32 = 300;
|
||||
let infos: Vec<PeerInfo> = (0..WRITERS).map(|i| external(i as u8 + 1)).collect();
|
||||
// Small journals, so that tidying and falling behind happen too.
|
||||
let fixture = Fixture::on(
|
||||
RibTable::with_journal_limits(
|
||||
8,
|
||||
Limits {
|
||||
tidy_at: 16,
|
||||
slack: 4,
|
||||
},
|
||||
),
|
||||
&infos,
|
||||
);
|
||||
let table = &fixture.table;
|
||||
let done = AtomicBool::new(false);
|
||||
let mut mirror = Mirror::default();
|
||||
|
||||
std::thread::scope(|scope| {
|
||||
let writers: Vec<_> = (0..WRITERS)
|
||||
.map(|writer| {
|
||||
let peer = fixture.peers[writer];
|
||||
scope.spawn(move || {
|
||||
// A simple generator; each writer its own sequence.
|
||||
let mut state = 0x9e37_79b9_7f4a_7c15u64.wrapping_mul(writer as u64 + 1);
|
||||
let mut next = || {
|
||||
state ^= state << 13;
|
||||
state ^= state >> 7;
|
||||
state ^= state << 17;
|
||||
state
|
||||
};
|
||||
for _ in 0..30_000 {
|
||||
let p = nth_prefix((next() % u64::from(PREFIXES)) as u32);
|
||||
if next() % 3 == 0 {
|
||||
table.withdraw(peer, &[p]);
|
||||
} else {
|
||||
let route = Route {
|
||||
med: Some((next() % 3) as u32),
|
||||
..Route::via(
|
||||
&[(next() % 3) as u32 + 1, 9][..(next() % 2 + 1) as usize],
|
||||
)
|
||||
};
|
||||
let attrs = table.attrs().intern(&route.attributes(), None).unwrap();
|
||||
table.announce(peer, &attrs, &[p]);
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mirror = &mut mirror;
|
||||
let done = &done;
|
||||
let reading = scope.spawn(move || {
|
||||
let mut reader = table.reader();
|
||||
while !done.load(Ordering::Acquire) {
|
||||
reader.poll(50, |event| mirror.apply(event));
|
||||
}
|
||||
mirror.catch_up(&mut reader, 50);
|
||||
});
|
||||
for writer in writers {
|
||||
writer.join().unwrap();
|
||||
}
|
||||
done.store(true, Ordering::Release);
|
||||
reading.join().unwrap();
|
||||
});
|
||||
|
||||
mirror.assert_matches(table);
|
||||
assert!(mirror.best_events > 0);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Readers against the table, under any interleaving
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum Op {
|
||||
Announce {
|
||||
peer: usize,
|
||||
prefix: usize,
|
||||
path_id: u32,
|
||||
route: Route,
|
||||
},
|
||||
Withdraw {
|
||||
peer: usize,
|
||||
prefix: usize,
|
||||
path_id: u32,
|
||||
},
|
||||
Flap {
|
||||
peer: usize,
|
||||
},
|
||||
Poll {
|
||||
reader: usize,
|
||||
budget: usize,
|
||||
},
|
||||
/// The reader is dropped and a new one takes its place.
|
||||
Replace {
|
||||
reader: usize,
|
||||
},
|
||||
}
|
||||
|
||||
const PEERS: usize = 4;
|
||||
const PREFIXES: usize = 12;
|
||||
// The first three follow the best routes, the others all routes.
|
||||
const READERS: usize = 5;
|
||||
const OF_BEST: usize = 3;
|
||||
|
||||
fn op() -> impl Strategy<Value = Op> {
|
||||
prop_oneof![
|
||||
8 => (0..PEERS, 0..PREFIXES, 0u32..3, route()).prop_map(
|
||||
|(peer, prefix, path_id, route)| Op::Announce { peer, prefix, path_id, route }
|
||||
),
|
||||
5 => (0..PEERS, 0..PREFIXES, 0u32..3)
|
||||
.prop_map(|(peer, prefix, path_id)| Op::Withdraw { peer, prefix, path_id }),
|
||||
1 => (0..PEERS).prop_map(|peer| Op::Flap { peer }),
|
||||
// Reader 0 is polled rarely: it is the one that falls behind.
|
||||
4 => (1..READERS, 1usize..6).prop_map(|(reader, budget)| Op::Poll { reader, budget }),
|
||||
1 => (0..READERS, 1usize..6).prop_map(|(reader, budget)| Op::Poll { reader, budget }),
|
||||
1 => (1..READERS).prop_map(|reader| Op::Replace { reader }),
|
||||
]
|
||||
}
|
||||
|
||||
proptest! {
|
||||
#![proptest_config(ProptestConfig::with_cases(1500))]
|
||||
|
||||
/// However changes and reads are interleaved, and however small the
|
||||
/// journals: a reader that has caught up holds exactly the table's best
|
||||
/// routes, and every reader gets there in the end.
|
||||
#[test]
|
||||
fn a_reader_that_has_caught_up_has_what_the_table_has(
|
||||
ops in proptest::collection::vec(op(), 1..150),
|
||||
shards in 1usize..4,
|
||||
) {
|
||||
let infos: Vec<PeerInfo> = (0..PEERS)
|
||||
.map(|i| if i == 0 { internal(9) } else { external(i as u8) })
|
||||
.collect();
|
||||
// Journals that are tidied, and found too long, almost at once.
|
||||
let limits = Limits { tidy_at: 3, slack: 1 };
|
||||
let mut fixture = Fixture::on(RibTable::with_journal_limits(shards, limits), &infos);
|
||||
let prefixes: Vec<IpPrefix> = (0..PREFIXES as u32).map(nth_prefix).collect();
|
||||
let new_reader = |table: &Arc<RibTable>, index: usize| {
|
||||
if index < OF_BEST { table.reader() } else { table.reader_of_all_routes() }
|
||||
};
|
||||
let check = |index: usize, mirror: &Mirror, table: &RibTable| {
|
||||
if index < OF_BEST {
|
||||
mirror.assert_matches(table);
|
||||
} else {
|
||||
mirror.assert_has_all_routes_of(table);
|
||||
}
|
||||
};
|
||||
let mut readers: Vec<(Reader, Mirror)> = (0..READERS)
|
||||
.map(|index| (new_reader(&fixture.table, index), Mirror::default()))
|
||||
.collect();
|
||||
|
||||
for op in ops {
|
||||
match op {
|
||||
Op::Announce { peer, prefix, path_id, route } => {
|
||||
fixture.announce_path(peer, &prefixes[prefix], path_id, &route);
|
||||
}
|
||||
Op::Withdraw { peer, prefix, path_id } => {
|
||||
fixture.withdraw_path(peer, &prefixes[prefix], path_id);
|
||||
}
|
||||
Op::Flap { peer } => {
|
||||
fixture.table.remove_peer(fixture.peers[peer]);
|
||||
fixture.peers[peer] = fixture.table.add_peer(infos[peer]).unwrap();
|
||||
}
|
||||
Op::Poll { reader: index, budget } => {
|
||||
let (reader, mirror) = &mut readers[index];
|
||||
let progress = reader.poll(budget, |event| mirror.apply(event));
|
||||
if progress.caught_up {
|
||||
check(index, mirror, &fixture.table);
|
||||
}
|
||||
}
|
||||
Op::Replace { reader } => {
|
||||
readers[reader] = (new_reader(&fixture.table, reader), Mirror::default());
|
||||
}
|
||||
}
|
||||
// Never much longer than the table, whatever the readers do.
|
||||
// Two journals to a shard.
|
||||
let bound = 2 * shards * (2 * (PREFIXES + limits.slack) + limits.tidy_at + PREFIXES);
|
||||
prop_assert!(fixture.table.journal_len() <= bound);
|
||||
}
|
||||
|
||||
for (index, (reader, mirror)) in readers.iter_mut().enumerate() {
|
||||
mirror.catch_up(reader, 5);
|
||||
check(index, mirror, &fixture.table);
|
||||
prop_assert_eq!(mirror.resets, mirror.synced);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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"
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,281 +0,0 @@
|
||||
//! Choosing the best route for a prefix (RFC 4271 §9.1.2.2).
|
||||
//!
|
||||
//! The steps, in order. A route wins at the first step where it differs:
|
||||
//!
|
||||
//! 1. highest LOCAL_PREF;
|
||||
//! 2. shortest AS path;
|
||||
//! 3. lowest ORIGIN;
|
||||
//! 4. lowest MULTI_EXIT_DISC, among routes from the same neighbouring AS;
|
||||
//! 5. a route from an external peer over one from an internal peer;
|
||||
//! 6. the peer with the lowest BGP Identifier;
|
||||
//! 7. the peer with the lowest address.
|
||||
//!
|
||||
//! (The RFC's step between 5 and 6, the interior cost to the next hop, has
|
||||
//! nothing to compare until there is an IGP to ask.)
|
||||
//!
|
||||
//! Steps 1 to 3 are put into one number per route when it is stored,
|
||||
//! [`preference`]. Most of the time that number alone settles it: a new
|
||||
//! route with a larger one than the best is the new best, with a smaller
|
||||
//! one changes nothing, and neither needs a look at any other route. Only a
|
||||
//! tie goes on to [`choose`], which looks at the whole row, and that begins
|
||||
//! with a pass over the numbers to find the routes still in the running.
|
||||
//!
|
||||
//! [`after_add`], [`after_replace`] and [`after_remove`] are what the table
|
||||
//! calls when a row has changed. Each says whether the best route is now
|
||||
//! another, and takes the short way where there is one. They rely on, and
|
||||
//! keep, one fact about a row: its best route has the highest preference
|
||||
//! of all its routes.
|
||||
|
||||
use super::attrs::SelectionKey;
|
||||
use super::{DEFAULT_LOCAL_PREF, Path, PeerInfo, Row};
|
||||
|
||||
/// Steps 1 to 3 as one number of 48 bits: the larger, the better.
|
||||
///
|
||||
/// LOCAL_PREF fills the high 32 bits. Below it come the AS path length in
|
||||
/// 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);
|
||||
const LONGEST: u16 = (1 << 12) - 1;
|
||||
(u64::from(local_pref) << 16)
|
||||
| (u64::from(LONGEST - key.as_path_len.min(LONGEST)) << 4)
|
||||
| (u64::from(3 - key.origin.min(3)) << 2)
|
||||
}
|
||||
|
||||
/// Buffers that [`choose`] reuses.
|
||||
#[derive(Default)]
|
||||
pub(super) struct Scratch {
|
||||
/// Routes still in the running: (neighbouring AS, MED, index in row).
|
||||
candidates: Vec<(u32, u32, u32)>,
|
||||
}
|
||||
|
||||
/// The best route changed after a route was added at the end of `row`?
|
||||
pub(super) fn after_add(row: &mut Row, peers: &[Option<PeerInfo>], scratch: &mut Scratch) -> bool {
|
||||
let added = row.paths.len() - 1;
|
||||
let preference = row.paths[added].preference();
|
||||
let best = row.paths[row.best()].preference();
|
||||
// Better than the best in the first steps of selection is better than
|
||||
// everything; worse than it changes nothing. A tie has to be settled by
|
||||
// the later steps.
|
||||
if preference > best {
|
||||
row.set_best(added);
|
||||
true
|
||||
} else if preference < best {
|
||||
false
|
||||
} else if let Some(wins) = settles_tie(row, added, peers) {
|
||||
if wins {
|
||||
row.set_best(added);
|
||||
}
|
||||
wins
|
||||
} else {
|
||||
choose(row, peers, scratch)
|
||||
}
|
||||
}
|
||||
|
||||
/// The best route changed after the route at `at` was replaced by another
|
||||
/// from the same peer? `before` is the preference of the route it replaced.
|
||||
/// The two are known to differ.
|
||||
pub(super) fn after_replace(
|
||||
row: &mut Row,
|
||||
at: usize,
|
||||
before: u64,
|
||||
peers: &[Option<PeerInfo>],
|
||||
scratch: &mut Scratch,
|
||||
) -> bool {
|
||||
let preference = row.paths[at].preference();
|
||||
if at == row.best() {
|
||||
// The best route itself changed. If it got better in the first
|
||||
// steps it is still the best, but it is a different route now.
|
||||
if preference <= before {
|
||||
choose(row, peers, scratch);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
let best = row.paths[row.best()].preference();
|
||||
if preference > best {
|
||||
row.set_best(at);
|
||||
true
|
||||
} else if preference < best && before < best {
|
||||
// It was not in the running and still is not.
|
||||
false
|
||||
} else {
|
||||
choose(row, peers, scratch)
|
||||
}
|
||||
}
|
||||
|
||||
/// Takes the route at `at` out of `row`, which has others, and returns
|
||||
/// whether the best route changed.
|
||||
pub(super) fn after_remove(
|
||||
row: &mut Row,
|
||||
at: usize,
|
||||
peers: &[Option<PeerInfo>],
|
||||
scratch: &mut Scratch,
|
||||
) -> bool {
|
||||
let best = row.best();
|
||||
let best_preference = row.paths[best].preference();
|
||||
let last = row.paths.len() - 1;
|
||||
let removed = row.swap_remove(at);
|
||||
if at == best {
|
||||
choose(row, peers, scratch);
|
||||
return true;
|
||||
}
|
||||
// The last route moved into the gap.
|
||||
if best == last {
|
||||
row.set_best(at);
|
||||
}
|
||||
// A route that tied with the best in the first steps may have been
|
||||
// what kept another from being best, by its MED.
|
||||
removed.preference() == best_preference && choose(row, peers, scratch)
|
||||
}
|
||||
|
||||
/// Chooses the best route of `row` from all its routes, records it in the
|
||||
/// row, and returns whether it is another route than before.
|
||||
///
|
||||
/// The row must have at least one route.
|
||||
fn choose(row: &mut Row, peers: &[Option<PeerInfo>], scratch: &mut Scratch) -> bool {
|
||||
let before = row.best;
|
||||
let top = row
|
||||
.paths
|
||||
.iter()
|
||||
.map(|path| path.preference())
|
||||
.max()
|
||||
.unwrap_or(0);
|
||||
|
||||
let candidates = &mut scratch.candidates;
|
||||
candidates.clear();
|
||||
for (index, path) in row.paths.iter().enumerate() {
|
||||
if path.preference() == top {
|
||||
let key = path.attrs.key();
|
||||
// A route without a MED is taken to have the lowest (RFC 4271
|
||||
// §9.1.2.2 c).
|
||||
candidates.push((key.neighbor_as, key.med.unwrap_or(0), index as u32));
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4. MEDs are compared only between routes from the same
|
||||
// neighbouring AS, so "lower MED" is not an order over all the routes,
|
||||
// and comparing them pair by pair can go round in circles. Instead:
|
||||
// within each neighbouring AS only the routes with its lowest MED stay
|
||||
// in, and the steps after are an order over what is left.
|
||||
candidates.sort_unstable();
|
||||
let mut best: Option<(TieBreak, u32)> = None;
|
||||
let mut group = None;
|
||||
for &(neighbor_as, med, index) in candidates.iter() {
|
||||
// Sorted by AS and then MED: the first of each AS has its lowest.
|
||||
let lowest = match group {
|
||||
Some((group_as, lowest)) if group_as == neighbor_as => lowest,
|
||||
_ => {
|
||||
group = Some((neighbor_as, med));
|
||||
med
|
||||
}
|
||||
};
|
||||
if med != lowest {
|
||||
continue;
|
||||
}
|
||||
let tie_break = TieBreak::of(row, index as usize, peers);
|
||||
if best.as_ref().is_none_or(|(least, _)| tie_break < *least) {
|
||||
best = Some((tie_break, index));
|
||||
}
|
||||
}
|
||||
|
||||
row.best = best.map_or(0, |(_, index)| index);
|
||||
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.
|
||||
///
|
||||
/// It can be said when MED cannot knock either of them out. The new route
|
||||
/// has the lowest MED there is, so nothing knocks it out. It knocks out
|
||||
/// only routes of its own neighbouring AS with a higher MED, and if the
|
||||
/// best is not one of those, the best is still the best of all the others.
|
||||
/// So the better of the two is the best of all. This is the usual case:
|
||||
/// most routes have no MED.
|
||||
fn settles_tie(row: &Row, added: usize, peers: &[Option<PeerInfo>]) -> Option<bool> {
|
||||
let new = row.paths[added].attrs.key();
|
||||
let best = row.paths[row.best()].attrs.key();
|
||||
if new.med.unwrap_or(0) != 0 {
|
||||
return None;
|
||||
}
|
||||
if best.neighbor_as == new.neighbor_as && best.med.unwrap_or(0) != 0 {
|
||||
return None;
|
||||
}
|
||||
Some(TieBreak::of(row, added, peers) < TieBreak::of(row, row.best(), peers))
|
||||
}
|
||||
|
||||
/// Steps 5 to 7, then the peer's number, and then the path identifier for
|
||||
/// a peer that sent several routes: so that the choice never depends on the
|
||||
/// order routes arrived in. The least wins.
|
||||
#[derive(PartialEq, Eq, PartialOrd, Ord)]
|
||||
struct TieBreak {
|
||||
internal: bool,
|
||||
router_id: u32,
|
||||
address: std::net::IpAddr,
|
||||
peer: u16,
|
||||
path_id: u32,
|
||||
}
|
||||
|
||||
impl TieBreak {
|
||||
fn of(row: &Row, index: usize, peers: &[Option<PeerInfo>]) -> Self {
|
||||
let peer = row.paths[index].peer();
|
||||
let path_id = row.path_id(index);
|
||||
match peers.get(peer.as_usize()).copied().flatten() {
|
||||
Some(info) => Self {
|
||||
internal: info.ibgp,
|
||||
router_id: info.router_id.into(),
|
||||
address: info.address,
|
||||
peer: peer.0,
|
||||
path_id,
|
||||
},
|
||||
// Not expected: a peer is forgotten only once its routes are
|
||||
// gone. If it happened, anything known would be preferred.
|
||||
None => Self {
|
||||
internal: true,
|
||||
router_id: u32::MAX,
|
||||
address: std::net::Ipv6Addr::from(u128::MAX).into(),
|
||||
peer: peer.0,
|
||||
path_id,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -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);
|
||||
}
|
||||
}
|
||||
Loaded 100 of 310 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user