diff --git a/.github/buildomat/jobs/build-interop.sh b/.github/buildomat/jobs/build-interop.sh index 5ac573b1..a1e1911f 100755 --- a/.github/buildomat/jobs/build-interop.sh +++ b/.github/buildomat/jobs/build-interop.sh @@ -12,7 +12,6 @@ #: "=/work/testbed.tar.gz", #: "=/work/dhcp-server", #: ] -#: set -x set -e diff --git a/.github/buildomat/jobs/build.sh b/.github/buildomat/jobs/build.sh index 1a894585..f4fbd8bb 100755 --- a/.github/buildomat/jobs/build.sh +++ b/.github/buildomat/jobs/build.sh @@ -20,6 +20,11 @@ #: from_output = "/work/release/ddmadm" #: #: [[publish]] +#: series = "release" +#: name = "falcon-lab" +#: from_output = "/work/release/falcon-lab" +#: +#: [[publish]] #: series = "debug" #: name = "ddmd" #: from_output = "/work/debug/ddmd" @@ -29,6 +34,26 @@ #: name = "ddmadm" #: from_output = "/work/debug/ddmadm" #: +#: [[publish]] +#: series = "debug" +#: name = "mgd" +#: from_output = "/work/debug/mgd" +#: +#: [[publish]] +#: series = "debug" +#: name = "mgadm" +#: from_output = "/work/debug/mgadm" +#: +#: [[publish]] +#: series = "release" +#: name = "mgd" +#: from_output = "/work/release/mgd" +#: +#: [[publish]] +#: series = "release" +#: name = "mgadm" +#: from_output = "/work/release/mgadm" +#: set -o errexit set -o pipefail @@ -41,6 +66,7 @@ rustc --version banner "check" cargo fmt -- --check cargo clippy --all-targets -- --deny warnings +cargo xtask openapi check banner "build" ptime -m cargo build @@ -51,4 +77,8 @@ do mkdir -p /work/$x cp target/$x/ddmd /work/$x/ddmd cp target/$x/ddmadm /work/$x/ddmadm + cp target/$x/mgd /work/$x/mgd + cp target/$x/mgadm /work/$x/mgadm done + +cp target/release/falcon-lab /work/release/falcon-lab diff --git a/.github/buildomat/jobs/falcon-lab.sh b/.github/buildomat/jobs/falcon-lab.sh new file mode 100644 index 00000000..b1c69d5c --- /dev/null +++ b/.github/buildomat/jobs/falcon-lab.sh @@ -0,0 +1,58 @@ +#!/bin/bash +#: +#: name = "falcon" +#: variety = "basic" +#: target = "lab-2.0-gimlet" +#: skip_clone = true +#: +#: [dependencies.build-interop] +#: job = "build-interop" +#: +#: [dependencies.build] +#: job = "build" +#: + +set -x +set -e + +banner 'zpool' + +# pick the largest disk available +DISK=$(pfexec diskinfo -pH | sort -k8 -n -r | head -1 | awk '{print $2}') +export DISK +pfexec zpool create -o ashift=12 -f cpool "$DISK" +pfexec zfs create -o mountpoint=/ci cpool/ci + +if [[ $(curl -s http://catacomb.eng.oxide.computer:12346/trim-me) =~ "true" ]]; then + pfexec zpool trim cpool + while [[ ! $(zpool status -t cpool) =~ "100%" ]]; do sleep 10; done +fi + +pfexec chown "$UID" /ci +cd /ci +export FALCON_DATASET="cpool/falcon" + +banner 'setup' + +cp /input/build-interop/work/dhcp-server . +cp /input/build/work/release/falcon-lab . +cp /input/build/work/release/mgd . +cp /input/build/work/release/ddmd . + +chmod +x dhcp-server falcon-lab mgd ddmd + +mkdir -p cargo-bay +mv mgd cargo-bay/ +mv ddmd cargo-bay/ + +export EXT_INTERFACE=${EXT_INTERFACE:-igb0} + +first=$(bmat address ls -f extra -Ho first) +last=$(bmat address ls -f extra -Ho last) +gw=$(bmat address ls -f extra -Ho gateway) +server=$(ipadm show-addr "$EXT_INTERFACE"/dhcp -po ADDR | sed 's#/.*##g') +pfexec ./dhcp-server "$first" "$last" "$gw" "$server" &> /work/dhcp-server.log & + +RUST_LOG=debug pfexec ./falcon-lab run \ + --dendrite-commit 0c2ab6c341bf9e3802c688961b3bc687b941a144 \ + trio-unnumbered diff --git a/.github/buildomat/jobs/test-interop.sh b/.github/buildomat/jobs/test-interop.sh index aee6c8f2..b7f48d8e 100755 --- a/.github/buildomat/jobs/test-interop.sh +++ b/.github/buildomat/jobs/test-interop.sh @@ -7,13 +7,14 @@ #: output_rules = [ #: "/work/*", #: ] +# +#: enable = false #: #: [dependencies.build-interop] #: job = "build-interop" #: #: [dependencies.image] #: job = "image" -#: set -x set -e diff --git a/Cargo.lock b/Cargo.lock index 368d5fd8..aa7fb10a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,7 +29,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "version_check", - "zerocopy 0.8.31", + "zerocopy 0.8.33", ] [[package]] @@ -115,19 +115,22 @@ checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "api_identity" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#79b29e0d001efc324ce0ba851379db0545ca7356" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#63d8904b88de3ca37f17450d01c59dc2167f0ebe" dependencies = [ "omicron-workspace-hack", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] name = "arc-swap" -version = "1.7.1" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" +checksum = "51d03449bb8ca2cc2ef70869af31463d1ae5ccc8fa3e334b307203fbf815207e" +dependencies = [ + "rustversion", +] [[package]] name = "argon2" @@ -161,7 +164,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -183,7 +186,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -194,7 +197,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -265,9 +268,9 @@ checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" [[package]] name = "base16ct" -version = "0.3.0" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8b59d472eab27ade8d770dcb11da7201c11234bef9f82ce7aa517be028d462b" +checksum = "fd307490d624467aa6f74b0eabb77633d1f758a7b25f12bceb0b22e08d9726f6" [[package]] name = "base64" @@ -283,9 +286,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.8.1" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e050f626429857a27ddccb31e0aca21356bfa709c04041aefddac081a8f068a" +checksum = "7d809780667f4410e7c41b07f52439b94d2bdf8528eeedc287fa38d3b7f95d82" [[package]] name = "bcs" @@ -307,7 +310,7 @@ dependencies = [ "pretty_assertions", "rand 0.8.5", "rdb", - "schemars", + "schemars 0.8.22", "serde", "slog", "slog-async", @@ -335,7 +338,7 @@ dependencies = [ "rand 0.8.5", "rdb", "rhai", - "schemars", + "schemars 0.8.22", "serde", "serial_test", "slog", @@ -347,7 +350,7 @@ dependencies = [ [[package]] name = "bhyve_api" version = "0.0.0" -source = "git+https://github.com/oxidecomputer/propolis?rev=3f1752e6cee9a2f8ecdce6e2ad3326781182e2d9#3f1752e6cee9a2f8ecdce6e2ad3326781182e2d9" +source = "git+https://github.com/oxidecomputer/propolis?rev=2dc643742f82d2e072a1281dab23ba2bfdcee440#2dc643742f82d2e072a1281dab23ba2bfdcee440" dependencies = [ "bhyve_api_sys", "libc", @@ -357,7 +360,7 @@ dependencies = [ [[package]] name = "bhyve_api_sys" version = "0.0.0" -source = "git+https://github.com/oxidecomputer/propolis?rev=3f1752e6cee9a2f8ecdce6e2ad3326781182e2d9#3f1752e6cee9a2f8ecdce6e2ad3326781182e2d9" +source = "git+https://github.com/oxidecomputer/propolis?rev=2dc643742f82d2e072a1281dab23ba2bfdcee440#2dc643742f82d2e072a1281dab23ba2bfdcee440" dependencies = [ "libc", "strum 0.26.3", @@ -404,15 +407,16 @@ dependencies = [ [[package]] name = "blake3" -version = "1.8.2" +version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3888aaa89e4b2a40fca9848e400f6a658a5a3978de7be858e209cafa8be9a4a0" +checksum = "2468ef7d57b3fb7e16b576e8377cdbde2320c60e1491e961d11da40fc4f02a2d" dependencies = [ "arrayref", "arrayvec", "cc", "cfg-if", "constant_time_eq", + "cpufeatures", "memmap2", "rayon-core", ] @@ -429,7 +433,7 @@ dependencies = [ [[package]] name = "bootstore" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#79b29e0d001efc324ce0ba851379db0545ca7356" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#63d8904b88de3ca37f17450d01c59dc2167f0ebe" dependencies = [ "bytes", "camino", @@ -532,14 +536,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" dependencies = [ "serde", - "toml 0.9.9+spec-1.0.0", + "toml 0.9.11+spec-1.1.0", ] [[package]] name = "cc" -version = "1.2.49" +version = "1.2.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215" +checksum = "cd4932aefd12402b36c60956a4fe0035421f544799057659ff86f923657aada3" dependencies = [ "find-msvc-tools", "jobserver", @@ -678,7 +682,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -690,7 +694,16 @@ checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" [[package]] name = "clickhouse-admin-types" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#79b29e0d001efc324ce0ba851379db0545ca7356" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#63d8904b88de3ca37f17450d01c59dc2167f0ebe" +dependencies = [ + "clickhouse-admin-types-versions", + "omicron-workspace-hack", +] + +[[package]] +name = "clickhouse-admin-types-versions" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#63d8904b88de3ca37f17450d01c59dc2167f0ebe" dependencies = [ "anyhow", "atomicwrites", @@ -703,7 +716,7 @@ dependencies = [ "itertools 0.14.0", "omicron-common", "omicron-workspace-hack", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "slog", @@ -718,7 +731,7 @@ dependencies = [ "camino", "clap", "derive_more", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "thiserror 1.0.69", @@ -746,13 +759,23 @@ dependencies = [ [[package]] name = "cockroach-admin-types" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#79b29e0d001efc324ce0ba851379db0545ca7356" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#63d8904b88de3ca37f17450d01c59dc2167f0ebe" +dependencies = [ + "cockroach-admin-types-versions", + "omicron-workspace-hack", + "serde", +] + +[[package]] +name = "cockroach-admin-types-versions" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#63d8904b88de3ca37f17450d01c59dc2167f0ebe" dependencies = [ "chrono", "csv", - "omicron-common", + "omicron-uuid-kinds", "omicron-workspace-hack", - "schemars", + "schemars 0.8.22", "serde", "thiserror 2.0.17", ] @@ -775,14 +798,14 @@ dependencies = [ [[package]] name = "common" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/dendrite?branch=main#ab30fa91227fd478bfe0e023310ca83dec0bc22b" +source = "git+https://github.com/oxidecomputer/dendrite?branch=ry%2Fv4-over-v6-routes#f486ffe91f5bd6e767891b3af018436219edf5f2" dependencies = [ "anyhow", "chrono", "oximeter", "oxnet", "rand 0.9.2", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "slog", @@ -862,9 +885,9 @@ dependencies = [ [[package]] name = "constant_time_eq" -version = "0.3.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" [[package]] name = "convert_case" @@ -969,7 +992,7 @@ source = "git+https://github.com/oxidecomputer/crucible?rev=7103cd3a3d7b0112d294 dependencies = [ "base64 0.22.1", "crucible-workspace-hack", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "uuid", @@ -1078,7 +1101,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -1102,7 +1125,7 @@ checksum = "27c6a4a4003df965e441d13b2a7044efa44334b567c984701f8a2773f815c5e2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -1126,7 +1149,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -1137,7 +1160,7 @@ checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ "darling_core", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -1164,13 +1187,13 @@ dependencies = [ "libnet", "mg-common", "omicron-common", - "opte-ioctl 0.1.0 (git+https://github.com/oxidecomputer/opte?rev=0f048374110d75ae61743ae3ec0de96960a2848d)", - "oxide-vpc 0.1.0 (git+https://github.com/oxidecomputer/opte?rev=0f048374110d75ae61743ae3ec0de96960a2848d)", + "opte-ioctl 0.1.0 (git+https://github.com/oxidecomputer/opte?rev=4bd8a40c0f5c05de7bb29b5f592f2dc99b4fd1d7)", + "oxide-vpc 0.1.0 (git+https://github.com/oxidecomputer/opte?rev=4bd8a40c0f5c05de7bb29b5f592f2dc99b4fd1d7)", "oximeter", "oximeter-producer", "oxnet", "pretty_assertions", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "sled", @@ -1202,7 +1225,7 @@ dependencies = [ "dropshot-api-manager-types", "mg-common", "oxnet", - "schemars", + "schemars 0.8.22", "serde", "uuid", ] @@ -1213,7 +1236,7 @@ version = "0.1.0" dependencies = [ "mg-common", "oxnet", - "schemars", + "schemars 0.8.22", "serde", "serde_repr", ] @@ -1292,7 +1315,7 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -1311,6 +1334,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" dependencies = [ "powerfmt", + "serde_core", ] [[package]] @@ -1322,7 +1346,7 @@ dependencies = [ "proc-macro2", "quote", "structmeta", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -1333,7 +1357,7 @@ checksum = "ef941ded77d15ca19b40374869ac6000af1c9f2a4c0f3d4c70926287e6364a8f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -1346,7 +1370,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version 0.4.1", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -1374,13 +1398,13 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] name = "dlpi" version = "0.2.0" -source = "git+https://github.com/oxidecomputer/dlpi-sys#2f5c441a8c0902d547e55a72346054709252cb18" +source = "git+https://github.com/oxidecomputer/dlpi-sys#42b2bfeefdfb8c7b96fc6cfa9ec45ef4554c2714" dependencies = [ "libc", "libdlpi-sys", @@ -1414,13 +1438,13 @@ dependencies = [ "serde", "serde_json", "thiserror 2.0.17", - "zerocopy 0.8.31", + "zerocopy 0.8.33", ] [[package]] name = "dpd-client" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/dendrite?branch=main#ab30fa91227fd478bfe0e023310ca83dec0bc22b" +source = "git+https://github.com/oxidecomputer/dendrite?branch=ry%2Fv4-over-v6-routes#f486ffe91f5bd6e767891b3af018436219edf5f2" dependencies = [ "async-trait", "chrono", @@ -1432,7 +1456,7 @@ dependencies = [ "progenitor 0.11.2", "regress", "reqwest", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "slog", @@ -1448,7 +1472,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9ba64b39d5fd68e09169e63c8e82b7a50c9b6082f2c44f52db2a11e3b9d7dd4" dependencies = [ "anyhow", - "indexmap", + "indexmap 2.13.0", "openapiv3", "regex", "serde", @@ -1476,14 +1500,14 @@ dependencies = [ "http-body-util", "hyper", "hyper-util", - "indexmap", + "indexmap 2.13.0", "multer", "openapiv3", "paste", "percent-encoding", "rustls 0.22.4", "rustls-pemfile", - "schemars", + "schemars 0.8.22", "scopeguard", "semver 1.0.27", "serde", @@ -1499,7 +1523,7 @@ dependencies = [ "thiserror 2.0.17", "tokio", "tokio-rustls 0.25.0", - "toml 0.9.9+spec-1.0.0", + "toml 0.9.11+spec-1.1.0", "usdt 0.6.0", "uuid", "version_check", @@ -1561,7 +1585,7 @@ dependencies = [ "semver 1.0.27", "serde", "serde_tokenstream", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -1658,7 +1682,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -1679,12 +1703,12 @@ dependencies = [ [[package]] name = "ereport-types" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#79b29e0d001efc324ce0ba851379db0545ca7356" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#63d8904b88de3ca37f17450d01c59dc2167f0ebe" dependencies = [ "dropshot", "omicron-uuid-kinds", "omicron-workspace-hack", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "thiserror 2.0.17", @@ -1712,6 +1736,27 @@ dependencies = [ "similar", ] +[[package]] +name = "falcon-lab" +version = "0.1.0" +dependencies = [ + "anyhow", + "bgp", + "clap", + "colored", + "ddm-admin-client", + "dpd-client", + "libfalcon", + "mg-admin-client 0.1.0", + "oxide-tokio-rt", + "oxnet", + "rdb-types 0.1.0", + "serde", + "serde_json", + "slog", + "tokio", +] + [[package]] name = "fastrand" version = "2.3.0" @@ -1748,9 +1793,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.5" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" +checksum = "f449e6c6c08c865631d4890cfacf252b3d396c9bcc83adb6623cdb02a8336c41" [[package]] name = "fixedbitset" @@ -1825,7 +1870,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -1851,9 +1896,9 @@ dependencies = [ [[package]] name = "fs-err" -version = "3.2.1" +version = "3.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "824f08d01d0f496b3eca4f001a13cf17690a6ee930043d20817f547455fd98f8" +checksum = "baf68cef89750956493a66a10f512b9e58d9db21f2a573c079c0bdf1207a54a7" dependencies = [ "autocfg", ] @@ -1930,7 +1975,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -1975,7 +2020,7 @@ dependencies = [ [[package]] name = "gateway-client" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#79b29e0d001efc324ce0ba851379db0545ca7356" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#63d8904b88de3ca37f17450d01c59dc2167f0ebe" dependencies = [ "base64 0.22.1", "chrono", @@ -1988,7 +2033,7 @@ dependencies = [ "progenitor 0.10.0", "rand 0.9.2", "reqwest", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "slog", @@ -2011,13 +2056,13 @@ dependencies = [ "strum 0.27.2", "strum_macros 0.27.2", "uuid", - "zerocopy 0.8.31", + "zerocopy 0.8.33", ] [[package]] name = "gateway-types" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#79b29e0d001efc324ce0ba851379db0545ca7356" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#63d8904b88de3ca37f17450d01c59dc2167f0ebe" dependencies = [ "gateway-types-versions", "omicron-workspace-hack", @@ -2026,7 +2071,7 @@ dependencies = [ [[package]] name = "gateway-types-versions" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#79b29e0d001efc324ce0ba851379db0545ca7356" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#63d8904b88de3ca37f17450d01c59dc2167f0ebe" dependencies = [ "daft", "dropshot", @@ -2034,7 +2079,7 @@ dependencies = [ "hex", "omicron-uuid-kinds", "omicron-workspace-hack", - "schemars", + "schemars 0.8.22", "serde", "thiserror 2.0.17", "tufaceous-artifact", @@ -2089,6 +2134,22 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "gfss" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#63d8904b88de3ca37f17450d01c59dc2167f0ebe" +dependencies = [ + "digest", + "omicron-workspace-hack", + "rand 0.9.2", + "schemars 0.8.22", + "secrecy", + "serde", + "subtle", + "thiserror 2.0.17", + "zeroize", +] + [[package]] name = "glob" version = "0.3.3" @@ -2143,9 +2204,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" dependencies = [ "atomic-waker", "bytes", @@ -2153,7 +2214,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap", + "indexmap 2.13.0", "slab", "tokio", "tokio-util", @@ -2168,7 +2229,7 @@ checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" dependencies = [ "cfg-if", "crunchy", - "zerocopy 0.8.31", + "zerocopy 0.8.33", ] [[package]] @@ -2180,6 +2241,12 @@ dependencies = [ "byteorder", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.15.5" @@ -2478,7 +2545,7 @@ dependencies = [ "http", "hyper", "hyper-util", - "rustls 0.23.35", + "rustls 0.23.36", "rustls-pki-types", "tokio", "tokio-rustls 0.26.4", @@ -2631,7 +2698,7 @@ dependencies = [ "hashbrown 0.16.1", "ref-cast", "rustc-hash", - "schemars", + "schemars 0.8.22", "serde_core", "serde_json", ] @@ -2676,7 +2743,7 @@ dependencies = [ [[package]] name = "illumos-sys-hdrs" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/opte?rev=0f048374110d75ae61743ae3ec0de96960a2848d#0f048374110d75ae61743ae3ec0de96960a2848d" +source = "git+https://github.com/oxidecomputer/opte?rev=4bd8a40c0f5c05de7bb29b5f592f2dc99b4fd1d7#4bd8a40c0f5c05de7bb29b5f592f2dc99b4fd1d7" dependencies = [ "bitflags 2.10.0", ] @@ -2692,7 +2759,7 @@ dependencies = [ [[package]] name = "illumos-utils" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#79b29e0d001efc324ce0ba851379db0545ca7356" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#63d8904b88de3ca37f17450d01c59dc2167f0ebe" dependencies = [ "anyhow", "async-trait", @@ -2701,6 +2768,7 @@ dependencies = [ "camino", "camino-tempfile", "cfg-if", + "chrono", "crucible-smf", "debug-ignore", "dropshot", @@ -2718,10 +2786,12 @@ dependencies = [ "oxide-vpc 0.1.0 (git+https://github.com/oxidecomputer/opte?rev=795a1e0aeefb7a2c6fe4139779fdf66930d09b80)", "oxlog", "oxnet", - "schemars", + "schemars 0.8.22", "serde", "slog", + "slog-async", "slog-error-chain", + "slog-term", "smf 0.2.3", "thiserror 2.0.17", "tofino", @@ -2739,9 +2809,20 @@ checksum = "0cfe9645a18782869361d9c8732246be7b410ad4e919d3609ebabdac00ba12c3" [[package]] name = "indexmap" -version = "2.12.1" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", "hashbrown 0.16.1", @@ -2773,7 +2854,7 @@ dependencies = [ "ingot-types", "macaddr", "serde", - "zerocopy 0.8.31", + "zerocopy 0.8.33", ] [[package]] @@ -2787,7 +2868,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -2798,7 +2879,7 @@ checksum = "2d0d55db2f1de52564cc3781ffd5a7ebb7f2c6e1888841c2fa54231a9498db5f" dependencies = [ "ingot-macros", "macaddr", - "zerocopy 0.8.31", + "zerocopy 0.8.33", ] [[package]] @@ -2822,7 +2903,7 @@ dependencies = [ [[package]] name = "internal-dns-resolver" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#79b29e0d001efc324ce0ba851379db0545ca7356" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#63d8904b88de3ca37f17450d01c59dc2167f0ebe" dependencies = [ "futures", "hickory-proto 0.25.2", @@ -2840,18 +2921,38 @@ dependencies = [ [[package]] name = "internal-dns-types" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#79b29e0d001efc324ce0ba851379db0545ca7356" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#63d8904b88de3ca37f17450d01c59dc2167f0ebe" dependencies = [ "anyhow", "chrono", + "internal-dns-types-versions", "omicron-common", "omicron-uuid-kinds", "omicron-workspace-hack", - "schemars", + "schemars 0.8.22", "serde", "strum 0.27.2", ] +[[package]] +name = "internal-dns-types-versions" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#63d8904b88de3ca37f17450d01c59dc2167f0ebe" +dependencies = [ + "anyhow", + "chrono", + "omicron-common", + "omicron-workspace-hack", + "schemars 0.8.22", + "serde", +] + +[[package]] +name = "internet-checksum" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6d6206008e25125b1f97fbe5d309eb7b85141cf9199d52dbd3729a1584dd16" + [[package]] name = "ipconfig" version = "0.3.2" @@ -2876,15 +2977,15 @@ version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf370abdafd54d13e54a620e8c3e1145f28e46cc9d704bc6d94414559df41763" dependencies = [ - "schemars", + "schemars 0.8.22", "serde", ] [[package]] name = "iri-string" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" dependencies = [ "memchr", "serde", @@ -2941,15 +3042,15 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "jiff" -version = "0.2.16" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49cce2b81f2098e7e3efc35bc2e0a6b7abec9d34128283d7a26fa8f32a6dbb35" +checksum = "e67e8da4c49d6d9909fe03361f9b620f58898859f5c7aded68351e85e71ecf50" dependencies = [ "jiff-static", "jiff-tzdb-platform", @@ -2962,13 +3063,13 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.16" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "980af8b43c3ad5d8d349ace167ec8170839f753a42d233ba19e08afe1850fa69" +checksum = "e0c84ee7f197eca9a86c6fd6cb771e55eb991632f15f2bc3ca6ec838929e6e78" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -3018,10 +3119,10 @@ dependencies = [ [[package]] name = "kstat-macro" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/opte?rev=0f048374110d75ae61743ae3ec0de96960a2848d#0f048374110d75ae61743ae3ec0de96960a2848d" +source = "git+https://github.com/oxidecomputer/opte?rev=4bd8a40c0f5c05de7bb29b5f592f2dc99b4fd1d7#4bd8a40c0f5c05de7bb29b5f592f2dc99b4fd1d7" dependencies = [ "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -3030,7 +3131,7 @@ version = "0.1.0" source = "git+https://github.com/oxidecomputer/opte?rev=795a1e0aeefb7a2c6fe4139779fdf66930d09b80#795a1e0aeefb7a2c6fe4139779fdf66930d09b80" dependencies = [ "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -3049,23 +3150,23 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.179" +version = "0.2.180" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5a2d376baa530d1238d133232d15e239abad80d05838b4b59354e5268af431f" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" [[package]] name = "libdlpi-sys" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/dlpi-sys#2f5c441a8c0902d547e55a72346054709252cb18" +source = "git+https://github.com/oxidecomputer/dlpi-sys#42b2bfeefdfb8c7b96fc6cfa9ec45ef4554c2714" [[package]] name = "libfalcon" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/falcon?branch=main#45a8b502e3c151e02f6b4acc4ffcdd3f2152c3e7" +source = "git+https://github.com/oxidecomputer/falcon?branch=main#e6e4c1c3db3859d3d91a37c2618be2ee4f003012" dependencies = [ "anstyle", "anyhow", - "base16ct 0.3.0", + "base16ct 1.0.0", "camino", "cargo_toml", "clap", @@ -3089,12 +3190,12 @@ dependencies = [ "slog-envlogger", "slog-term", "smf 0.2.3", - "syn 2.0.111", + "syn 2.0.114", "tabwriter", "thiserror 1.0.69", "tokio", "tokio-tungstenite", - "toml 0.9.9+spec-1.0.0", + "toml 0.9.11+spec-1.1.0", "uuid", "xz2", "zone 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -3109,7 +3210,7 @@ checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] name = "libnet" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/netadm-sys?branch=main#763dbb28fe66eb726f43872b5d979c58eb15de6b" +source = "git+https://github.com/oxidecomputer/netadm-sys?branch=main#6c94b3c4fa494b065d065b32b6186360b0517908" dependencies = [ "anyhow", "cfg-if", @@ -3130,13 +3231,13 @@ dependencies = [ [[package]] name = "libredox" -version = "0.1.11" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df15f6eac291ed1cf25865b1ee60399f57e7c227e7f51bdbd4c5270396a9ed50" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" dependencies = [ "bitflags 2.10.0", "libc", - "redox_syscall 0.6.0", + "redox_syscall 0.7.0", ] [[package]] @@ -3317,7 +3418,7 @@ dependencies = [ "progenitor 0.11.2", "rdb-types 0.1.0", "reqwest", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "slog", @@ -3328,14 +3429,14 @@ dependencies = [ [[package]] name = "mg-admin-client" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/maghemite?rev=0df320d42b356e689a3c7a7600eec9b16770237a#0df320d42b356e689a3c7a7600eec9b16770237a" +source = "git+https://github.com/oxidecomputer/maghemite?rev=205b3ccf75b527ac7a565285fdcc0c78f4fcee95#205b3ccf75b527ac7a565285fdcc0c78f4fcee95" dependencies = [ "chrono", "colored", "progenitor 0.11.2", - "rdb-types 0.1.0 (git+https://github.com/oxidecomputer/maghemite?rev=0df320d42b356e689a3c7a7600eec9b16770237a)", + "rdb-types 0.1.0 (git+https://github.com/oxidecomputer/maghemite?rev=205b3ccf75b527ac7a565285fdcc0c78f4fcee95)", "reqwest", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "slog", @@ -3352,7 +3453,7 @@ dependencies = [ "dropshot", "dropshot-api-manager-types", "rdb", - "schemars", + "schemars 0.8.22", "serde", ] @@ -3369,7 +3470,7 @@ dependencies = [ "oximeter", "oximeter-producer", "oxnet", - "schemars", + "schemars 0.8.22", "serde", "slog", "slog-async", @@ -3407,6 +3508,7 @@ dependencies = [ "thiserror 1.0.69", "tokio", "util", + "uuid", ] [[package]] @@ -3473,6 +3575,8 @@ dependencies = [ "mg-api", "mg-common", "mg-lower", + "ndp", + "network-interface", "omicron-common", "oxide-tokio-rt", "oximeter", @@ -3523,9 +3627,9 @@ dependencies = [ [[package]] name = "moka" -version = "0.12.11" +version = "0.12.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8261cd88c312e0004c1d51baad2980c66528dfdb2bee62003e643a4d8f86b077" +checksum = "a3dec6bd31b08944e08b58fd99373893a6c17054d6f3ea5006cc894f4f4eee2a" dependencies = [ "crossbeam-channel", "crossbeam-epoch", @@ -3533,7 +3637,6 @@ dependencies = [ "equivalent", "parking_lot 0.12.5", "portable-atomic", - "rustc_version 0.4.1", "smallvec", "tagptr", "uuid", @@ -3556,6 +3659,33 @@ dependencies = [ "version_check", ] +[[package]] +name = "ndp" +version = "0.1.0" +dependencies = [ + "internet-checksum", + "ispf", + "libc", + "mg-common", + "network-interface", + "oxnet", + "serde", + "slog", + "socket2 0.5.10", + "thiserror 1.0.69", +] + +[[package]] +name = "network-interface" +version = "0.1.7" +source = "git+https://github.com/oxidecomputer/network-interface?branch=illumos#5a696e910333bdc50ef56cebe9cdd78e40127d87" +dependencies = [ + "cc", + "libc", + "thiserror 1.0.69", + "winapi", +] + [[package]] name = "newline-converter" version = "0.3.0" @@ -3571,7 +3701,7 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c012d14ef788ab066a347d19e3dda699916c92293b05b85ba2c76b8c82d2830" dependencies = [ - "schemars", + "schemars 0.8.22", "serde", "serde_json", "uuid", @@ -3588,7 +3718,7 @@ dependencies = [ "quote", "serde", "serde_tokenstream", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -3603,7 +3733,7 @@ dependencies = [ [[package]] name = "nexus-client" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#79b29e0d001efc324ce0ba851379db0545ca7356" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#63d8904b88de3ca37f17450d01c59dc2167f0ebe" dependencies = [ "chrono", "futures", @@ -3616,7 +3746,7 @@ dependencies = [ "progenitor 0.10.0", "regress", "reqwest", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "slog", @@ -3626,7 +3756,7 @@ dependencies = [ [[package]] name = "nexus-types" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#79b29e0d001efc324ce0ba851379db0545ca7356" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#63d8904b88de3ca37f17450d01c59dc2167f0ebe" dependencies = [ "anyhow", "api_identity", @@ -3651,6 +3781,7 @@ dependencies = [ "illumos-utils", "indent_write", "internal-dns-types", + "ipnet", "ipnetwork", "itertools 0.14.0", "newtype-uuid", @@ -3665,11 +3796,12 @@ dependencies = [ "oxql-types", "parse-display", "regex", - "schemars", + "schemars 0.8.22", "semver 1.0.27", "serde", "serde_json", "serde_with", + "sled-agent-types", "sled-agent-types-versions", "sled-hardware-types", "slog", @@ -3683,6 +3815,8 @@ dependencies = [ "thiserror 2.0.17", "tokio", "tough", + "trust-quorum-protocol", + "trust-quorum-types", "tufaceous-artifact", "unicode-width 0.1.14", "update-engine", @@ -3769,7 +3903,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -3837,7 +3971,7 @@ version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799" dependencies = [ - "proc-macro-crate", + "proc-macro-crate 1.3.1", "proc-macro2", "quote", "syn 1.0.109", @@ -3849,10 +3983,10 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" dependencies = [ - "proc-macro-crate", + "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -3893,7 +4027,7 @@ dependencies = [ [[package]] name = "omicron-common" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#79b29e0d001efc324ce0ba851379db0545ca7356" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#63d8904b88de3ca37f17450d01c59dc2167f0ebe" dependencies = [ "anyhow", "api_identity", @@ -3910,7 +4044,7 @@ dependencies = [ "ipnetwork", "itertools 0.14.0", "macaddr", - "mg-admin-client 0.1.0 (git+https://github.com/oxidecomputer/maghemite?rev=0df320d42b356e689a3c7a7600eec9b16770237a)", + "mg-admin-client 0.1.0 (git+https://github.com/oxidecomputer/maghemite?rev=205b3ccf75b527ac7a565285fdcc0c78f4fcee95)", "omicron-uuid-kinds", "omicron-workspace-hack", "oxnet", @@ -3920,7 +4054,7 @@ dependencies = [ "rand 0.9.2", "regress", "reqwest", - "schemars", + "schemars 0.8.22", "semver 1.0.27", "serde", "serde_human_bytes", @@ -3938,12 +4072,12 @@ dependencies = [ [[package]] name = "omicron-passwords" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#79b29e0d001efc324ce0ba851379db0545ca7356" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#63d8904b88de3ca37f17450d01c59dc2167f0ebe" dependencies = [ "argon2", "omicron-workspace-hack", "rand 0.9.2", - "schemars", + "schemars 0.8.22", "secrecy", "serde", "serde_with", @@ -3953,7 +4087,7 @@ dependencies = [ [[package]] name = "omicron-rpaths" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#79b29e0d001efc324ce0ba851379db0545ca7356" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#63d8904b88de3ca37f17450d01c59dc2167f0ebe" dependencies = [ "omicron-workspace-hack", ] @@ -3961,13 +4095,13 @@ dependencies = [ [[package]] name = "omicron-uuid-kinds" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#79b29e0d001efc324ce0ba851379db0545ca7356" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#63d8904b88de3ca37f17450d01c59dc2167f0ebe" dependencies = [ "daft", "newtype-uuid", "newtype-uuid-macros", "paste", - "schemars", + "schemars 0.8.22", ] [[package]] @@ -4036,7 +4170,7 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c8d427828b22ae1fff2833a03d8486c2c881367f1c336349f307f321e7f4d05" dependencies = [ - "indexmap", + "indexmap 2.13.0", "serde", "serde_json", ] @@ -4064,7 +4198,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -4082,20 +4216,20 @@ dependencies = [ [[package]] name = "opte" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/opte?rev=0f048374110d75ae61743ae3ec0de96960a2848d#0f048374110d75ae61743ae3ec0de96960a2848d" +source = "git+https://github.com/oxidecomputer/opte?rev=4bd8a40c0f5c05de7bb29b5f592f2dc99b4fd1d7#4bd8a40c0f5c05de7bb29b5f592f2dc99b4fd1d7" dependencies = [ "bitflags 2.10.0", "dyn-clone", - "illumos-sys-hdrs 0.1.0 (git+https://github.com/oxidecomputer/opte?rev=0f048374110d75ae61743ae3ec0de96960a2848d)", + "illumos-sys-hdrs 0.1.0 (git+https://github.com/oxidecomputer/opte?rev=4bd8a40c0f5c05de7bb29b5f592f2dc99b4fd1d7)", "ingot", - "kstat-macro 0.1.0 (git+https://github.com/oxidecomputer/opte?rev=0f048374110d75ae61743ae3ec0de96960a2848d)", - "opte-api 0.1.0 (git+https://github.com/oxidecomputer/opte?rev=0f048374110d75ae61743ae3ec0de96960a2848d)", + "kstat-macro 0.1.0 (git+https://github.com/oxidecomputer/opte?rev=4bd8a40c0f5c05de7bb29b5f592f2dc99b4fd1d7)", + "opte-api 0.1.0 (git+https://github.com/oxidecomputer/opte?rev=4bd8a40c0f5c05de7bb29b5f592f2dc99b4fd1d7)", "postcard", "ref-cast", "serde", "tabwriter", "version_check", - "zerocopy 0.8.31", + "zerocopy 0.8.33", ] [[package]] @@ -4114,15 +4248,15 @@ dependencies = [ "serde", "tabwriter", "version_check", - "zerocopy 0.8.31", + "zerocopy 0.8.33", ] [[package]] name = "opte-api" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/opte?rev=0f048374110d75ae61743ae3ec0de96960a2848d#0f048374110d75ae61743ae3ec0de96960a2848d" +source = "git+https://github.com/oxidecomputer/opte?rev=4bd8a40c0f5c05de7bb29b5f592f2dc99b4fd1d7#4bd8a40c0f5c05de7bb29b5f592f2dc99b4fd1d7" dependencies = [ - "illumos-sys-hdrs 0.1.0 (git+https://github.com/oxidecomputer/opte?rev=0f048374110d75ae61743ae3ec0de96960a2848d)", + "illumos-sys-hdrs 0.1.0 (git+https://github.com/oxidecomputer/opte?rev=4bd8a40c0f5c05de7bb29b5f592f2dc99b4fd1d7)", "ingot", "ipnetwork", "postcard", @@ -4146,12 +4280,12 @@ dependencies = [ [[package]] name = "opte-ioctl" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/opte?rev=0f048374110d75ae61743ae3ec0de96960a2848d#0f048374110d75ae61743ae3ec0de96960a2848d" +source = "git+https://github.com/oxidecomputer/opte?rev=4bd8a40c0f5c05de7bb29b5f592f2dc99b4fd1d7#4bd8a40c0f5c05de7bb29b5f592f2dc99b4fd1d7" dependencies = [ "libc", "libnet", - "opte 0.1.0 (git+https://github.com/oxidecomputer/opte?rev=0f048374110d75ae61743ae3ec0de96960a2848d)", - "oxide-vpc 0.1.0 (git+https://github.com/oxidecomputer/opte?rev=0f048374110d75ae61743ae3ec0de96960a2848d)", + "opte 0.1.0 (git+https://github.com/oxidecomputer/opte?rev=4bd8a40c0f5c05de7bb29b5f592f2dc99b4fd1d7)", + "oxide-vpc 0.1.0 (git+https://github.com/oxidecomputer/opte?rev=4bd8a40c0f5c05de7bb29b5f592f2dc99b4fd1d7)", "postcard", "serde", "thiserror 2.0.17", @@ -4191,15 +4325,15 @@ dependencies = [ [[package]] name = "oxide-vpc" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/opte?rev=0f048374110d75ae61743ae3ec0de96960a2848d#0f048374110d75ae61743ae3ec0de96960a2848d" +source = "git+https://github.com/oxidecomputer/opte?rev=4bd8a40c0f5c05de7bb29b5f592f2dc99b4fd1d7#4bd8a40c0f5c05de7bb29b5f592f2dc99b4fd1d7" dependencies = [ "cfg-if", - "illumos-sys-hdrs 0.1.0 (git+https://github.com/oxidecomputer/opte?rev=0f048374110d75ae61743ae3ec0de96960a2848d)", - "opte 0.1.0 (git+https://github.com/oxidecomputer/opte?rev=0f048374110d75ae61743ae3ec0de96960a2848d)", + "illumos-sys-hdrs 0.1.0 (git+https://github.com/oxidecomputer/opte?rev=4bd8a40c0f5c05de7bb29b5f592f2dc99b4fd1d7)", + "opte 0.1.0 (git+https://github.com/oxidecomputer/opte?rev=4bd8a40c0f5c05de7bb29b5f592f2dc99b4fd1d7)", "serde", "tabwriter", "uuid", - "zerocopy 0.8.31", + "zerocopy 0.8.33", ] [[package]] @@ -4213,13 +4347,13 @@ dependencies = [ "serde", "tabwriter", "uuid", - "zerocopy 0.8.31", + "zerocopy 0.8.33", ] [[package]] name = "oximeter" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#79b29e0d001efc324ce0ba851379db0545ca7356" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#63d8904b88de3ca37f17450d01c59dc2167f0ebe" dependencies = [ "anyhow", "chrono", @@ -4230,7 +4364,7 @@ dependencies = [ "oximeter-timeseries-macro", "oximeter-types", "prettyplease", - "syn 2.0.111", + "syn 2.0.114", "toml 0.8.23", "uuid", ] @@ -4238,7 +4372,7 @@ dependencies = [ [[package]] name = "oximeter-db" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#79b29e0d001efc324ce0ba851379db0545ca7356" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#63d8904b88de3ca37f17450d01c59dc2167f0ebe" dependencies = [ "anyhow", "async-recursion", @@ -4257,7 +4391,7 @@ dependencies = [ "gethostname", "highway", "iana-time-zone", - "indexmap", + "indexmap 2.13.0", "libc", "nom", "num", @@ -4271,7 +4405,7 @@ dependencies = [ "quote", "regex", "reqwest", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "slog", @@ -4291,18 +4425,18 @@ dependencies = [ [[package]] name = "oximeter-macro-impl" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#79b29e0d001efc324ce0ba851379db0545ca7356" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#63d8904b88de3ca37f17450d01c59dc2167f0ebe" dependencies = [ "omicron-workspace-hack", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] name = "oximeter-producer" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#79b29e0d001efc324ce0ba851379db0545ca7356" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#63d8904b88de3ca37f17450d01c59dc2167f0ebe" dependencies = [ "chrono", "dropshot", @@ -4312,7 +4446,7 @@ dependencies = [ "omicron-common", "omicron-workspace-hack", "oximeter", - "schemars", + "schemars 0.8.22", "serde", "slog", "slog-dtrace", @@ -4324,7 +4458,7 @@ dependencies = [ [[package]] name = "oximeter-schema" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#79b29e0d001efc324ce0ba851379db0545ca7356" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#63d8904b88de3ca37f17450d01c59dc2167f0ebe" dependencies = [ "anyhow", "chrono", @@ -4335,30 +4469,30 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "schemars", + "schemars 0.8.22", "serde", "slog-error-chain", - "syn 2.0.111", + "syn 2.0.114", "toml 0.8.23", ] [[package]] name = "oximeter-timeseries-macro" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#79b29e0d001efc324ce0ba851379db0545ca7356" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#63d8904b88de3ca37f17450d01c59dc2167f0ebe" dependencies = [ "omicron-workspace-hack", "oximeter-schema", "oximeter-types", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] name = "oximeter-types" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#79b29e0d001efc324ce0ba851379db0545ca7356" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#63d8904b88de3ca37f17450d01c59dc2167f0ebe" dependencies = [ "bytes", "chrono", @@ -4369,7 +4503,7 @@ dependencies = [ "oximeter-types-versions", "parse-display", "regex", - "schemars", + "schemars 0.8.22", "serde", "strum 0.27.2", "thiserror 2.0.17", @@ -4379,12 +4513,12 @@ dependencies = [ [[package]] name = "oximeter-types-versions" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#79b29e0d001efc324ce0ba851379db0545ca7356" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#63d8904b88de3ca37f17450d01c59dc2167f0ebe" dependencies = [ "chrono", "omicron-common", "omicron-workspace-hack", - "schemars", + "schemars 0.8.22", "serde", "uuid", ] @@ -4392,7 +4526,7 @@ dependencies = [ [[package]] name = "oxlog" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#79b29e0d001efc324ce0ba851379db0545ca7356" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#63d8904b88de3ca37f17450d01c59dc2167f0ebe" dependencies = [ "anyhow", "camino", @@ -4413,7 +4547,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dc6fb07ecd6d2a17ff1431bc5b3ce11036c0b6dd93a3c4904db5b910817b162" dependencies = [ "ipnetwork", - "schemars", + "schemars 0.8.22", "serde", "serde_json", ] @@ -4421,7 +4555,7 @@ dependencies = [ [[package]] name = "oxql-types" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#79b29e0d001efc324ce0ba851379db0545ca7356" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#63d8904b88de3ca37f17450d01c59dc2167f0ebe" dependencies = [ "anyhow", "chrono", @@ -4429,7 +4563,7 @@ dependencies = [ "num", "omicron-workspace-hack", "oximeter-types", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "uuid", @@ -4527,7 +4661,7 @@ dependencies = [ "regex", "regex-syntax", "structmeta", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -4565,9 +4699,9 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" -version = "2.8.4" +version = "2.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbcfd20a6d4eeba40179f05735784ad32bdaef05ce8e8af05f180d45bb3e7e22" +checksum = "2c9eb05c21a464ea704b53158d358a31e6425db2f63a1a7312268b05fe2b75f7" dependencies = [ "memchr", "ucd-trie", @@ -4575,9 +4709,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.8.4" +version = "2.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51f72981ade67b1ca6adc26ec221be9f463f2b5839c7508998daa17c23d94d7f" +checksum = "68f9dbced329c441fa79d80472764b1a2c7e57123553b8519b36663a2fb234ed" dependencies = [ "pest", "pest_generator", @@ -4585,22 +4719,22 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.8.4" +version = "2.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dee9efd8cdb50d719a80088b76f81aec7c41ed6d522ee750178f83883d271625" +checksum = "3bb96d5051a78f44f43c8f712d8e810adb0ebf923fc9ed2655a7f66f63ba8ee5" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] name = "pest_meta" -version = "2.8.4" +version = "2.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf1d70880e76bdc13ba52eafa6239ce793d85c8e43896507e43dd8984ff05b82" +checksum = "602113b5b5e8621770cfd490cfd90b9f84ab29bd2b0e49ad83eb6d186cef2365" dependencies = [ "pest", "sha2", @@ -4613,7 +4747,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset 0.4.2", - "indexmap", + "indexmap 2.13.0", "serde", "serde_derive", ] @@ -4626,7 +4760,7 @@ checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" dependencies = [ "fixedbitset 0.5.7", "hashbrown 0.15.5", - "indexmap", + "indexmap 2.13.0", "serde", ] @@ -4665,7 +4799,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -4705,9 +4839,9 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.11.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" [[package]] name = "portable-atomic-util" @@ -4751,7 +4885,7 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ - "zerocopy 0.8.31", + "zerocopy 0.8.33", ] [[package]] @@ -4788,7 +4922,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -4801,6 +4935,15 @@ dependencies = [ "toml_edit 0.19.15", ] +[[package]] +name = "proc-macro-crate" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit 0.23.10+spec-1.0.0", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -4844,14 +4987,14 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] name = "proc-macro2" -version = "1.0.103" +version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" dependencies = [ "unicode-ident", ] @@ -4916,15 +5059,15 @@ checksum = "b17e5363daa50bf1cccfade6b0fb970d2278758fd5cfa9ab69f25028e4b1afa3" dependencies = [ "heck 0.5.0", "http", - "indexmap", + "indexmap 2.13.0", "openapiv3", "proc-macro2", "quote", "regex", - "schemars", + "schemars 0.8.22", "serde", "serde_json", - "syn 2.0.111", + "syn 2.0.114", "thiserror 2.0.17", "typify", "unicode-ident", @@ -4938,15 +5081,15 @@ checksum = "90f6d9109b04e005bbdec84cacec7e81cc15533f2b5dc505f0defc212d270c15" dependencies = [ "heck 0.5.0", "http", - "indexmap", + "indexmap 2.13.0", "openapiv3", "proc-macro2", "quote", "regex", - "schemars", + "schemars 0.8.22", "serde", "serde_json", - "syn 2.0.111", + "syn 2.0.114", "thiserror 2.0.17", "typify", "unicode-ident", @@ -4962,12 +5105,12 @@ dependencies = [ "proc-macro2", "progenitor-impl 0.10.0", "quote", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "serde_tokenstream", "serde_yaml", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -4980,18 +5123,18 @@ dependencies = [ "proc-macro2", "progenitor-impl 0.11.2", "quote", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "serde_tokenstream", "serde_yaml", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] name = "propolis-client" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/propolis?rev=30d32c418804cb094751673cabd86ee99bda2d66#30d32c418804cb094751673cabd86ee99bda2d66" +source = "git+https://github.com/oxidecomputer/propolis?rev=2dc643742f82d2e072a1281dab23ba2bfdcee440#2dc643742f82d2e072a1281dab23ba2bfdcee440" dependencies = [ "async-trait", "base64 0.21.7", @@ -4999,10 +5142,10 @@ dependencies = [ "futures", "progenitor 0.10.0", "progenitor-client 0.10.0", - "propolis_api_types 0.0.0 (git+https://github.com/oxidecomputer/propolis?rev=30d32c418804cb094751673cabd86ee99bda2d66)", + "propolis_api_types", "rand 0.9.2", "reqwest", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "slog", @@ -5015,24 +5158,11 @@ dependencies = [ [[package]] name = "propolis_api_types" version = "0.0.0" -source = "git+https://github.com/oxidecomputer/propolis?rev=30d32c418804cb094751673cabd86ee99bda2d66#30d32c418804cb094751673cabd86ee99bda2d66" +source = "git+https://github.com/oxidecomputer/propolis?rev=2dc643742f82d2e072a1281dab23ba2bfdcee440#2dc643742f82d2e072a1281dab23ba2bfdcee440" dependencies = [ "crucible-client-types", - "propolis_types 0.0.0 (git+https://github.com/oxidecomputer/propolis?rev=30d32c418804cb094751673cabd86ee99bda2d66)", - "schemars", - "serde", - "thiserror 1.0.69", - "uuid", -] - -[[package]] -name = "propolis_api_types" -version = "0.0.0" -source = "git+https://github.com/oxidecomputer/propolis?rev=3f1752e6cee9a2f8ecdce6e2ad3326781182e2d9#3f1752e6cee9a2f8ecdce6e2ad3326781182e2d9" -dependencies = [ - "crucible-client-types", - "propolis_types 0.0.0 (git+https://github.com/oxidecomputer/propolis?rev=3f1752e6cee9a2f8ecdce6e2ad3326781182e2d9)", - "schemars", + "propolis_types", + "schemars 0.8.22", "serde", "thiserror 1.0.69", "uuid", @@ -5041,18 +5171,9 @@ dependencies = [ [[package]] name = "propolis_types" version = "0.0.0" -source = "git+https://github.com/oxidecomputer/propolis?rev=30d32c418804cb094751673cabd86ee99bda2d66#30d32c418804cb094751673cabd86ee99bda2d66" +source = "git+https://github.com/oxidecomputer/propolis?rev=2dc643742f82d2e072a1281dab23ba2bfdcee440#2dc643742f82d2e072a1281dab23ba2bfdcee440" dependencies = [ - "schemars", - "serde", -] - -[[package]] -name = "propolis_types" -version = "0.0.0" -source = "git+https://github.com/oxidecomputer/propolis?rev=3f1752e6cee9a2f8ecdce6e2ad3326781182e2d9#3f1752e6cee9a2f8ecdce6e2ad3326781182e2d9" -dependencies = [ - "schemars", + "schemars 0.8.22", "serde", ] @@ -5081,7 +5202,7 @@ version = "0.1.0" source = "git+https://github.com/oxidecomputer/lldp#61479b6922f9112fbe1e722414d2b8055212cb12" dependencies = [ "anyhow", - "schemars", + "schemars 0.8.22", "serde", "thiserror 1.0.69", ] @@ -5125,7 +5246,7 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash", - "rustls 0.23.35", + "rustls 0.23.36", "socket2 0.6.1", "thiserror 2.0.17", "tokio", @@ -5145,7 +5266,7 @@ dependencies = [ "rand 0.9.2", "ring", "rustc-hash", - "rustls 0.23.35", + "rustls 0.23.36", "rustls-pki-types", "slab", "thiserror 2.0.17", @@ -5170,9 +5291,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.42" +version = "1.0.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" dependencies = [ "proc-macro2", ] @@ -5281,10 +5402,11 @@ dependencies = [ "clap", "itertools 0.14.0", "mg-common", + "ndp", "oxnet", "proptest", "rdb-types 0.1.0", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "sled", @@ -5298,17 +5420,17 @@ version = "0.1.0" dependencies = [ "clap", "oxnet", - "schemars", + "schemars 0.8.22", "serde", ] [[package]] name = "rdb-types" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/maghemite?rev=0df320d42b356e689a3c7a7600eec9b16770237a#0df320d42b356e689a3c7a7600eec9b16770237a" +source = "git+https://github.com/oxidecomputer/maghemite?rev=205b3ccf75b527ac7a565285fdcc0c78f4fcee95#205b3ccf75b527ac7a565285fdcc0c78f4fcee95" dependencies = [ "oxnet", - "schemars", + "schemars 0.8.22", "serde", ] @@ -5332,9 +5454,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec96166dafa0886eb81fe1c0a388bece180fbef2135f97c1e2cf8302e74b43b5" +checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27" dependencies = [ "bitflags 2.10.0", ] @@ -5356,7 +5478,7 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -5400,9 +5522,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.12.26" +version = "0.12.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b4c14b2d9afca6a60277086b0cc6a6ae0b568f6f7916c943a8cdc79f8be240f" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ "base64 0.22.1", "bytes", @@ -5419,7 +5541,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.35", + "rustls 0.23.36", "rustls-pki-types", "serde", "serde_json", @@ -5473,7 +5595,7 @@ checksum = "d4322a2a4e8cf30771dd9f27f7f37ca9ac8fe812dddd811096a98483080dabe6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -5543,9 +5665,9 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" dependencies = [ "bitflags 2.10.0", "errno", @@ -5570,9 +5692,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.35" +version = "0.23.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" +checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" dependencies = [ "aws-lc-rs", "log", @@ -5664,9 +5786,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.20" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" [[package]] name = "same-file" @@ -5703,6 +5825,30 @@ dependencies = [ "uuid", ] +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54e910108742c57a770f492731f99be216a52fadd361b06c8fb59d74ccc267d2" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + [[package]] name = "schemars_derive" version = "0.8.22" @@ -5712,7 +5858,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -5747,7 +5893,7 @@ checksum = "1783eabc414609e28a5ba76aee5ddd52199f7107a0b24c2e9746a1ecc34a683d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -5758,7 +5904,7 @@ checksum = "ed76efe62313ab6610570951494bdaa81568026e0318eaa55f167de70eeea67d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -5828,7 +5974,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -5839,7 +5985,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -5853,9 +5999,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.148" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3084b546a1dd6289475996f182a22aba973866ea8e8b02c51d9f46b1336a22da" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", @@ -5892,7 +6038,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -5922,7 +6068,7 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -5946,6 +6092,11 @@ dependencies = [ "base64 0.22.1", "chrono", "hex", + "indexmap 1.9.3", + "indexmap 2.13.0", + "schemars 0.8.22", + "schemars 0.9.0", + "schemars 1.2.0", "serde_core", "serde_json", "serde_with_macros", @@ -5961,7 +6112,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -5970,7 +6121,7 @@ version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap", + "indexmap 2.13.0", "itoa", "ryu", "serde", @@ -5979,11 +6130,12 @@ dependencies = [ [[package]] name = "serial_test" -version = "3.2.0" +version = "3.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b258109f244e1d6891bf1053a55d63a5cd4f8f4c30cf9a1280989f80e7a1fa9" +checksum = "0d0b343e184fc3b7bb44dff0705fffcf4b3756ba6aff420dddd8b24ca145e555" dependencies = [ - "futures", + "futures-executor", + "futures-util", "log", "once_cell", "parking_lot 0.12.5", @@ -5993,13 +6145,13 @@ dependencies = [ [[package]] name = "serial_test_derive" -version = "3.2.0" +version = "3.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d69265a08751de7844521fd15003ae0a888e035773ba05695c5c759a6f89eef" +checksum = "6f50427f258fb77356e4cd4aa0e87e2bd2c66dbcee41dc405282cae2bfc26c83" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -6042,10 +6194,11 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.7" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] @@ -6101,10 +6254,42 @@ dependencies = [ "parking_lot 0.11.2", ] +[[package]] +name = "sled-agent-types" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#63d8904b88de3ca37f17450d01c59dc2167f0ebe" +dependencies = [ + "anyhow", + "async-trait", + "bootstore", + "camino", + "chrono", + "daft", + "iddqd", + "omicron-common", + "omicron-uuid-kinds", + "omicron-workspace-hack", + "oxnet", + "schemars 0.8.22", + "serde", + "serde_human_bytes", + "serde_json", + "sled-agent-types-versions", + "sled-hardware-types", + "slog", + "slog-error-chain", + "strum 0.27.2", + "swrite", + "thiserror 2.0.17", + "toml 0.8.23", + "tufaceous-artifact", + "uuid", +] + [[package]] name = "sled-agent-types-versions" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#79b29e0d001efc324ce0ba851379db0545ca7356" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#63d8904b88de3ca37f17450d01c59dc2167f0ebe" dependencies = [ "async-trait", "bootstore", @@ -6119,15 +6304,17 @@ dependencies = [ "omicron-uuid-kinds", "omicron-workspace-hack", "oxnet", - "propolis_api_types 0.0.0 (git+https://github.com/oxidecomputer/propolis?rev=3f1752e6cee9a2f8ecdce6e2ad3326781182e2d9)", - "schemars", + "propolis_api_types", + "schemars 0.8.22", "serde", "serde_json", + "serde_with", "sha3", "sled-hardware-types", "slog", "strum 0.27.2", "thiserror 2.0.17", + "trust-quorum-types-versions", "tufaceous-artifact", "uuid", ] @@ -6135,13 +6322,16 @@ dependencies = [ [[package]] name = "sled-hardware-types" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#79b29e0d001efc324ce0ba851379db0545ca7356" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#63d8904b88de3ca37f17450d01c59dc2167f0ebe" dependencies = [ + "daft", "illumos-utils", "omicron-common", "omicron-workspace-hack", - "schemars", + "schemars 0.8.22", "serde", + "slog", + "thiserror 2.0.17", ] [[package]] @@ -6225,7 +6415,7 @@ source = "git+https://github.com/oxidecomputer/slog-error-chain?branch=main#15f6 dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -6368,7 +6558,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -6428,7 +6618,7 @@ dependencies = [ "lazy_static", "newtype_derive", "petgraph 0.6.5", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "slog", @@ -6452,7 +6642,7 @@ dependencies = [ "proc-macro2", "quote", "structmeta-derive", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -6463,7 +6653,7 @@ checksum = "152a0b65a590ff6c3da95cabe2353ee04e6167c896b28e3b14478c2636c922fc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -6494,7 +6684,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -6506,7 +6696,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -6543,9 +6733,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.111" +version = "2.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" dependencies = [ "proc-macro2", "quote", @@ -6569,7 +6759,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -6638,7 +6828,7 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -6675,14 +6865,14 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.23.0" +version = "3.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" dependencies = [ "fastrand", "getrandom 0.3.4", "once_cell", - "rustix 1.1.2", + "rustix 1.1.3", "windows-sys 0.61.2", ] @@ -6701,7 +6891,7 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" dependencies = [ - "rustix 1.1.2", + "rustix 1.1.3", "windows-sys 0.60.2", ] @@ -6721,7 +6911,7 @@ dependencies = [ "proc-macro2", "quote", "structmeta", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -6780,7 +6970,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -6791,7 +6981,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -6922,9 +7112,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.48.0" +version = "1.49.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" dependencies = [ "bytes", "libc", @@ -6956,7 +7146,7 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -6976,15 +7166,15 @@ version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ - "rustls 0.23.35", + "rustls 0.23.36", "tokio", ] [[package]] name = "tokio-stream" -version = "0.1.17" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" dependencies = [ "futures-core", "pin-project-lite", @@ -7006,9 +7196,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.17" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ "bytes", "futures-core", @@ -7044,14 +7234,14 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.9+spec-1.0.0" +version = "0.9.11+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb5238e643fc34a1d5d7e753e1532a91912d74b63b92b3ea51fde8d1b7bc79dd" +checksum = "f3afc9a848309fe1aaffaed6e1546a7a14de1f935dc9d89d32afd9a44bab7c46" dependencies = [ - "indexmap", + "indexmap 2.13.0", "serde_core", "serde_spanned 1.0.4", - "toml_datetime 0.7.4+spec-1.0.0", + "toml_datetime 0.7.5+spec-1.1.0", "toml_parser", "toml_writer", "winnow 0.7.14", @@ -7068,9 +7258,9 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.7.4+spec-1.0.0" +version = "0.7.5+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe3cea6b2aa3b910092f6abd4053ea464fab5f9c170ba5e9a6aead16ec4af2b6" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" dependencies = [ "serde_core", ] @@ -7081,7 +7271,7 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap", + "indexmap 2.13.0", "serde", "serde_spanned 0.6.9", "toml_datetime 0.6.11", @@ -7094,7 +7284,7 @@ version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap", + "indexmap 2.13.0", "serde", "serde_spanned 0.6.9", "toml_datetime 0.6.11", @@ -7102,11 +7292,23 @@ dependencies = [ "winnow 0.7.14", ] +[[package]] +name = "toml_edit" +version = "0.23.10+spec-1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" +dependencies = [ + "indexmap 2.13.0", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "winnow 0.7.14", +] + [[package]] name = "toml_parser" -version = "1.0.5+spec-1.0.0" +version = "1.0.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c03bee5ce3696f31250db0bbaff18bc43301ce0e8db2ed1f07cbb2acf89984c" +checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" dependencies = [ "winnow 0.7.14", ] @@ -7119,9 +7321,9 @@ checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" [[package]] name = "toml_writer" -version = "1.0.5+spec-1.0.0" +version = "1.0.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9cd6190959dce0994aa8970cd32ab116d1851ead27e866039acaf2524ce44fa" +checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" [[package]] name = "topological-sort" @@ -7150,7 +7352,7 @@ dependencies = [ "pem", "percent-encoding", "reqwest", - "rustls 0.23.35", + "rustls 0.23.36", "serde", "serde_json", "serde_plain", @@ -7211,9 +7413,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.43" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "pin-project-lite", "tracing-attributes", @@ -7228,14 +7430,14 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] name = "tracing-core" -version = "0.1.35" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", ] @@ -7243,14 +7445,14 @@ dependencies = [ [[package]] name = "transceiver-controller" version = "0.1.1" -source = "git+https://github.com/oxidecomputer/transceiver-control?branch=main#7cd1a05eff3b1a480ed0d573124f6784d24999b0" +source = "git+https://github.com/oxidecomputer/transceiver-control?branch=main#5f48c09e112a91ec8ff770daad359a144ff9f8f5" dependencies = [ "anyhow", "clap", "hubpack", "itertools 0.14.0", "nix", - "schemars", + "schemars 0.8.22", "serde", "slog", "slog-async", @@ -7267,9 +7469,9 @@ dependencies = [ [[package]] name = "transceiver-decode" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/transceiver-control?branch=main#7cd1a05eff3b1a480ed0d573124f6784d24999b0" +source = "git+https://github.com/oxidecomputer/transceiver-control?branch=main#5f48c09e112a91ec8ff770daad359a144ff9f8f5" dependencies = [ - "schemars", + "schemars 0.8.22", "serde", "static_assertions", "thiserror 2.0.17", @@ -7279,16 +7481,82 @@ dependencies = [ [[package]] name = "transceiver-messages" version = "0.1.1" -source = "git+https://github.com/oxidecomputer/transceiver-control?branch=main#7cd1a05eff3b1a480ed0d573124f6784d24999b0" +source = "git+https://github.com/oxidecomputer/transceiver-control?branch=main#5f48c09e112a91ec8ff770daad359a144ff9f8f5" dependencies = [ "bitflags 2.10.0", "clap", "hubpack", - "schemars", + "schemars 0.8.22", "serde", "thiserror 2.0.17", ] +[[package]] +name = "trust-quorum-protocol" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#63d8904b88de3ca37f17450d01c59dc2167f0ebe" +dependencies = [ + "bootstore", + "bytes", + "camino", + "chacha20poly1305", + "ciborium", + "daft", + "derive_more", + "gfss", + "hex", + "hkdf", + "iddqd", + "omicron-uuid-kinds", + "omicron-workspace-hack", + "rand 0.9.2", + "secrecy", + "serde", + "serde_with", + "sha3", + "sled-agent-types", + "sled-hardware-types", + "slog", + "slog-error-chain", + "static_assertions", + "subtle", + "thiserror 2.0.17", + "trust-quorum-types", + "uuid", + "zeroize", +] + +[[package]] +name = "trust-quorum-types" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#63d8904b88de3ca37f17450d01c59dc2167f0ebe" +dependencies = [ + "omicron-workspace-hack", + "trust-quorum-types-versions", +] + +[[package]] +name = "trust-quorum-types-versions" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#63d8904b88de3ca37f17450d01c59dc2167f0ebe" +dependencies = [ + "daft", + "derive_more", + "gfss", + "iddqd", + "omicron-uuid-kinds", + "omicron-workspace-hack", + "rand 0.9.2", + "schemars 0.8.22", + "serde", + "serde_human_bytes", + "serde_with", + "sled-hardware-types", + "slog", + "slog-error-chain", + "thiserror 2.0.17", +] + [[package]] name = "try-lock" version = "0.2.5" @@ -7303,7 +7571,7 @@ dependencies = [ "daft", "hex", "proptest", - "schemars", + "schemars 0.8.22", "semver 1.0.27", "serde", "serde_human_bytes", @@ -7370,11 +7638,11 @@ dependencies = [ "proc-macro2", "quote", "regress", - "schemars", + "schemars 0.8.22", "semver 1.0.27", "serde", "serde_json", - "syn 2.0.111", + "syn 2.0.114", "thiserror 2.0.17", "unicode-ident", ] @@ -7387,12 +7655,12 @@ checksum = "9708a3ceb6660ba3f8d2b8f0567e7d4b8b198e2b94d093b8a6077a751425de9e" dependencies = [ "proc-macro2", "quote", - "schemars", + "schemars 0.8.22", "semver 1.0.27", "serde", "serde_json", "serde_tokenstream", - "syn 2.0.111", + "syn 2.0.114", "typify-impl", ] @@ -7490,7 +7758,7 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "update-engine" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#79b29e0d001efc324ce0ba851379db0545ca7356" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#63d8904b88de3ca37f17450d01c59dc2167f0ebe" dependencies = [ "anyhow", "cancel-safe-futures", @@ -7500,13 +7768,13 @@ dependencies = [ "either", "futures", "indent_write", - "indexmap", + "indexmap 2.13.0", "libsw", "linear-map", "omicron-workspace-hack", "owo-colors", "petgraph 0.8.3", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "serde_with", @@ -7519,14 +7787,15 @@ dependencies = [ [[package]] name = "url" -version = "2.5.7" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", "idna", "percent-encoding", "serde", + "serde_derive", ] [[package]] @@ -7570,7 +7839,7 @@ dependencies = [ "proc-macro2", "quote", "serde_tokenstream", - "syn 2.0.111", + "syn 2.0.114", "usdt-impl 0.5.0", ] @@ -7584,7 +7853,7 @@ dependencies = [ "proc-macro2", "quote", "serde_tokenstream", - "syn 2.0.111", + "syn 2.0.114", "usdt-impl 0.6.0", ] @@ -7602,7 +7871,7 @@ dependencies = [ "quote", "serde", "serde_json", - "syn 2.0.111", + "syn 2.0.114", "thiserror 1.0.69", "thread-id 4.2.2", "version_check", @@ -7622,7 +7891,7 @@ dependencies = [ "quote", "serde", "serde_json", - "syn 2.0.111", + "syn 2.0.114", "thiserror 2.0.17", "thread-id 5.0.0", ] @@ -7637,7 +7906,7 @@ dependencies = [ "proc-macro2", "quote", "serde_tokenstream", - "syn 2.0.111", + "syn 2.0.114", "usdt-impl 0.5.0", ] @@ -7651,7 +7920,7 @@ dependencies = [ "proc-macro2", "quote", "serde_tokenstream", - "syn 2.0.111", + "syn 2.0.114", "usdt-impl 0.6.0", ] @@ -7830,7 +8099,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", "wasm-bindgen-shared", ] @@ -7878,9 +8147,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" +checksum = "12bed680863276c63889429bfd6cab3b99943659923822de1c8a39c49e4d722c" dependencies = [ "rustls-pki-types", ] @@ -7954,7 +8223,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -7965,7 +8234,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -8281,7 +8550,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" dependencies = [ "libc", - "rustix 1.1.2", + "rustix 1.1.3", ] [[package]] @@ -8327,7 +8596,7 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", "synstructure", ] @@ -8343,11 +8612,11 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.31" +version = "0.8.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" +checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd" dependencies = [ - "zerocopy-derive 0.8.31", + "zerocopy-derive 0.8.33", ] [[package]] @@ -8358,18 +8627,18 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] name = "zerocopy-derive" -version = "0.8.31" +version = "0.8.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" +checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -8389,7 +8658,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", "synstructure", ] @@ -8404,13 +8673,13 @@ dependencies = [ [[package]] name = "zeroize_derive" -version = "1.4.2" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -8443,14 +8712,14 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] name = "zmij" -version = "1.0.7" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de9211a9f64b825911bdf0240f58b7a8dac217fe260fc61f080a07f61372fbd5" +checksum = "2fc5a66a20078bf1251bde995aa2fdcc4b800c70b5d92dd2c62abc5c60f679f8" [[package]] name = "zone" @@ -8502,7 +8771,7 @@ dependencies = [ [[package]] name = "ztest" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/falcon?branch=main#45a8b502e3c151e02f6b4acc4ffcdd3f2152c3e7" +source = "git+https://github.com/oxidecomputer/falcon?branch=main#f5f8fd52ea72167b6a46eff34dc7b46b87d3b5f9" dependencies = [ "anyhow", "libnet", diff --git a/Cargo.toml b/Cargo.toml index e2164bae..79168482 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,9 @@ default-members = [ "mgd", "mg-lower", "mg-common", + "ndp", "xtask", + "falcon-lab", ] members = [ @@ -48,11 +50,27 @@ members = [ "mgd", "mg-lower", "mg-common", + "ndp", "xtask", + "falcon-lab", ] [workspace.dependencies] -slog = { version = "2.8.2", features = ["max_level_trace", "release_max_level_debug"] } +# Local +mg-api = { path = "mg-api" } +mg-common = { path = "mg-common" } +rdb-types = { path = "rdb-types" } +ndp = { path = "ndp" } +bgp = { path = "bgp" } +bfd = { path = "bfd" } +mg-admin-client = { path = "mg-admin-client" } +ddm-admin-client = { path = "ddm-admin-client" } +rdb = { path = "rdb", features = ["clap"] } +ddm-api = { path = "ddm-api" } +ddm-types = { path = "ddm-types" } + +# External +slog = { version = "2.7.0", features = ["max_level_trace", "release_max_level_debug"] } slog-term = "2.9" slog-envlogger = "2.2" slog-async = "2.8" @@ -77,10 +95,11 @@ libnet = { git = "https://github.com/oxidecomputer/netadm-sys", branch = "main" progenitor = "0.11" progenitor-client = "0.11" reqwest = { version = "0.12", default-features = false, features = ["json", "stream", "rustls-tls"] } -clap = { version = "4.5.54", features = ["derive", "unstable-styles", "env"] } +clap = { version = "4.5.53", features = ["derive", "unstable-styles", "env"] } tabwriter = { version = "1", features = ["ansi_formatting"] } colored = "3.0" ztest = { git = "https://github.com/oxidecomputer/falcon", branch = "main" } +libfalcon = { git = "https://github.com/oxidecomputer/falcon", branch = "main" } anstyle = "1.0.13" nom = "7.1" num_enum = "0.7.5" @@ -95,16 +114,13 @@ http-body-util = "0.1" humantime = "2.1" rand = "0.8.5" backoff = "0.4" -mg-api = { path = "mg-api" } -mg-common = { path = "mg-common" } -bgp = { path = "bgp" } -rdb-types = { path = "rdb-types" } chrono = { version = "0.4.42", features = ["serde"] } oxide-tokio-rt = "0.1.2" oximeter = { git = "https://github.com/oxidecomputer/omicron", branch = "main"} oximeter-producer = { git = "https://github.com/oxidecomputer/omicron", branch = "main"} oxnet = { version = "0.1.4", default-features = false, features = ["schemars", "serde"] } omicron-common = { git = "https://github.com/oxidecomputer/omicron", branch = "main"} +gateway-client = { git = "https://github.com/oxidecomputer/omicron", branch = "main" } uuid = { version = "1.8", features = ["serde", "v4"] } smf = { git = "https://github.com/illumos/smf-rs", branch = "main" } libc = "0.2" @@ -113,19 +129,21 @@ rhai = { version = "1", features = ["metadata", "sync"] } semver = "1.0" proptest = "1.4" serial_test = "3.2" -ddm-api = { path = "ddm-api" } -ddm-types = { path = "ddm-types" } -gateway-client = { git = "https://github.com/oxidecomputer/omicron", branch = "main" } +internet-checksum = "0.2.1" +network-interface = { git = "https://github.com/oxidecomputer/network-interface", branch = "illumos" } + [workspace.dependencies.opte-ioctl] git = "https://github.com/oxidecomputer/opte" -rev = "0f048374110d75ae61743ae3ec0de96960a2848d" +rev = "4bd8a40c0f5c05de7bb29b5f592f2dc99b4fd1d7" [workspace.dependencies.oxide-vpc] git = "https://github.com/oxidecomputer/opte" -rev = "0f048374110d75ae61743ae3ec0de96960a2848d" +rev = "4bd8a40c0f5c05de7bb29b5f592f2dc99b4fd1d7" [workspace.dependencies.dpd-client] +#path = "/home/ry/src/dendrite/dpd-client" git = "https://github.com/oxidecomputer/dendrite" -branch = "main" +branch = "ry/v4-over-v6-routes" +#branch = "main" package = "dpd-client" diff --git a/bgp/src/error.rs b/bgp/src/error.rs index d739ecdc..eff442cb 100644 --- a/bgp/src/error.rs +++ b/bgp/src/error.rs @@ -157,7 +157,7 @@ pub enum Error { #[error("Enforce-first-AS check failed: expected: {0}, found: {1:?}")] EnforceAsFirst(u32, Vec), - #[error("Invalid address")] + #[error("Invalid address: {0}")] InvalidAddress(String), #[error("Datastore error: {0}")] diff --git a/bgp/src/messages.rs b/bgp/src/messages.rs index c6a6bf23..c278910a 100644 --- a/bgp/src/messages.rs +++ b/bgp/src/messages.rs @@ -575,26 +575,56 @@ pub struct OpenMessage { impl OpenMessage { /// Create a new open message for a sender with a 2-byte ASN - pub fn new2(asn: u16, hold_time: u16, id: u32) -> OpenMessage { + pub fn new2( + asn: u16, + hold_time: u16, + id: u32, + extended_nexthop: bool, + ) -> OpenMessage { + let parameters = if extended_nexthop { + let caps = BTreeSet::from([Capability::ExtendedNextHopEncoding { + elements: vec![ExtendedNexthopElement { + afi: Afi::Ipv4.into(), + safi: u8::from(Safi::Unicast).into(), + nh_afi: Afi::Ipv6.into(), + }], + }]); + vec![OptionalParameter::Capabilities(caps)] + } else { + Vec::default() + }; OpenMessage { version: BGP4, asn, hold_time, id, - parameters: Vec::new(), + parameters, } } /// Create a new open message for a sender with a 4-byte ASN - pub fn new4(asn: u32, hold_time: u16, id: u32) -> OpenMessage { + pub fn new4( + asn: u32, + hold_time: u16, + id: u32, + extended_nexthop: bool, + ) -> OpenMessage { + let mut params = BTreeSet::from([Capability::FourOctetAs { asn }]); + if extended_nexthop { + params.insert(Capability::ExtendedNextHopEncoding { + elements: vec![ExtendedNexthopElement { + afi: Afi::Ipv4.into(), + safi: u8::from(Safi::Unicast).into(), + nh_afi: Afi::Ipv6.into(), + }], + }); + } OpenMessage { version: BGP4, asn: u16::try_from(asn).unwrap_or(AS_TRANS), hold_time, id, - parameters: vec![OptionalParameter::Capabilities(BTreeSet::from( - [Capability::FourOctetAs { asn }], - ))], + parameters: vec![OptionalParameter::Capabilities(params)], } } @@ -613,12 +643,13 @@ impl OpenMessage { } pub fn get_capabilities(&self) -> BTreeSet { + let mut result = BTreeSet::new(); for p in self.parameters.iter() { if let OptionalParameter::Capabilities(caps) = p { - return caps.clone(); + result.extend(caps.clone().into_iter()); } } - BTreeSet::new() + result } pub fn has_capability(&self, code: CapabilityCode) -> bool { @@ -2818,19 +2849,18 @@ impl BgpNexthop { // SAFETY: The length check above guarantees nh_bytes.len() == nh_len. // Each match arm below only matches when nh_len equals the exact size // needed for copy_from_slice, so all slice operations are bounds-safe. - // XXX: extended nexthop support match (afi, nh_len) { (Afi::Ipv4, 4) => { let mut bytes = [0u8; 4]; bytes.copy_from_slice(nh_bytes); Ok(BgpNexthop::Ipv4(Ipv4Addr::from(bytes))) } - (Afi::Ipv6, 16) => { + (Afi::Ipv4 | Afi::Ipv6, 16) => { let mut bytes = [0u8; 16]; bytes.copy_from_slice(nh_bytes); Ok(BgpNexthop::Ipv6Single(Ipv6Addr::from(bytes))) } - (Afi::Ipv6, 32) => { + (Afi::Ipv4 | Afi::Ipv6, 32) => { let mut bytes1 = [0u8; 16]; let mut bytes2 = [0u8; 16]; bytes1.copy_from_slice(&nh_bytes[..16]); @@ -4090,7 +4120,60 @@ impl Display for AddPathElement { write!( f, "AddPathElement {{ afi: {}, safi: {}, send_receive: {} }}", - self.afi, self.safi, self.send_receive + match Afi::try_from_primitive(self.afi) { + Ok(x) => x.to_string(), + _ => self.afi.to_string(), + }, + match Safi::try_from_primitive(self.safi) { + Ok(x) => x.to_string(), + _ => self.safi.to_string(), + }, + self.send_receive + ) + } +} + +#[derive( + Debug, + PartialEq, + Eq, + Clone, + Copy, + Serialize, + Deserialize, + JsonSchema, + PartialOrd, + Ord, +)] +pub struct ExtendedNexthopElement { + pub afi: u16, + pub safi: u16, + pub nh_afi: u16, +} + +impl ExtendedNexthopElement { + fn is_v4_over_v6(&self) -> bool { + self == &ExtendedNexthopElement { + afi: Afi::Ipv4.into(), + safi: u8::from(Safi::Unicast).into(), + nh_afi: Afi::Ipv6.into(), + } + } + fn is_v6_over_v4(&self) -> bool { + self == &ExtendedNexthopElement { + afi: Afi::Ipv6.into(), + safi: u8::from(Safi::Unicast).into(), + nh_afi: Afi::Ipv4.into(), + } + } +} + +impl Display for ExtendedNexthopElement { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "safi={}/afi={}/nh_afi={}", + self.afi, self.safi, self.nh_afi ) } } @@ -4131,10 +4214,13 @@ pub enum Capability { /// (deprecated). Note this capability is not yet implemented. MultipleRoutesToDestination {}, - //TODO - /// Multiple nexthop encoding capability as defined in RFC 8950. Note this - /// capability is not yet implemented. - ExtendedNextHopEncoding {}, + /// Multiple nexthop encoding capability as defined in RFC 8950. + ExtendedNextHopEncoding { + //XXX trying to avoid a version bump on 86 billion data structures + // right now. + #[schemars(skip)] + elements: Vec, + }, //TODO /// Extended message capability as defined in RFC 8654. Note this @@ -4262,8 +4348,13 @@ impl Display for Capability { Capability::MultipleRoutesToDestination {} => { write!(f, "Multiple Routes to Destination") } - Capability::ExtendedNextHopEncoding {} => { - write!(f, "Extended Next Hop Encoding") + Capability::ExtendedNextHopEncoding { elements } => { + let elements = elements + .iter() + .map(|x| x.to_string()) + .collect::>() + .join(", "); + write!(f, "Extended Next Hop Encoding {elements}") } Capability::BGPExtendedMessage {} => { write!(f, "BGP Extended Message") @@ -4384,6 +4475,18 @@ impl Capability { let buf = vec![CapabilityCode::EnhancedRouteRefresh.into(), 0]; Ok(buf) } + Self::ExtendedNextHopEncoding { elements } => { + let mut buf = vec![ + CapabilityCode::ExtendedNextHopEncoding as u8, + (elements.len() * 6) as u8, + ]; + for e in elements { + buf.extend_from_slice(&e.afi.to_be_bytes()); + buf.extend_from_slice(&e.safi.to_be_bytes()); + buf.extend_from_slice(&e.nh_afi.to_be_bytes()); + } + Ok(buf) + } Self::Experimental { code: _ } => Err(Error::Experimental), Self::Unassigned { code } => Err(Error::Unassigned(*code)), Self::Reserved { code: _ } => Err(Error::ReservedCapability), @@ -4409,303 +4512,282 @@ impl Capability { return Ok((&input[len..], Capability::Unassigned { code })); } }; - let mut input = input; + let (cap_data, remaining) = input.split_at(len); + let mut input = cap_data; - match code { + let cap = match code { CapabilityCode::MultiprotocolExtensions => { let (input, afi) = be_u16(input)?; let (input, _) = be_u8(input)?; - let (input, safi) = be_u8(input)?; - Ok((input, Capability::MultiprotocolExtensions { afi, safi })) + let (_, safi) = be_u8(input)?; + Capability::MultiprotocolExtensions { afi, safi } } - CapabilityCode::RouteRefresh => { - Ok((&input[len..], Capability::RouteRefresh {})) - } - + CapabilityCode::RouteRefresh => Capability::RouteRefresh {}, CapabilityCode::GracefulRestart => { //TODO handle for real - Ok((&input[len..], Capability::GracefulRestart {})) + Capability::GracefulRestart {} } CapabilityCode::FourOctetAs => { - let (input, asn) = be_u32(input)?; - Ok((input, Capability::FourOctetAs { asn })) + let (_, asn) = be_u32(input)?; + Capability::FourOctetAs { asn } } CapabilityCode::AddPath => { let mut elements = BTreeSet::new(); while !input.is_empty() { - let (remaining, afi) = be_u16(input)?; - let (remaining, safi) = be_u8(remaining)?; - let (remaining, send_receive) = be_u8(remaining)?; + let (rem, afi) = be_u16(input)?; + let (rem, safi) = be_u8(rem)?; + let (rem, send_receive) = be_u8(rem)?; elements.insert(AddPathElement { afi, safi, send_receive, }); - input = remaining; + input = rem; } - Ok((input, Capability::AddPath { elements })) + Capability::AddPath { elements } } CapabilityCode::EnhancedRouteRefresh => { //TODO handle for real - Ok((&input[len..], Capability::EnhancedRouteRefresh {})) + Capability::EnhancedRouteRefresh {} } - CapabilityCode::Fqdn => { //TODO handle for real - Ok((&input[len..], Capability::Fqdn {})) + Capability::Fqdn {} } - CapabilityCode::PrestandardRouteRefresh => { //TODO handle for real - Ok((&input[len..], Capability::PrestandardRouteRefresh {})) + Capability::PrestandardRouteRefresh {} } - CapabilityCode::BGPExtendedMessage => { //TODO handle for real - Ok((&input[len..], Capability::BGPExtendedMessage {})) + Capability::BGPExtendedMessage {} } - CapabilityCode::LongLivedGracefulRestart => { //TODO handle for real - Ok((&input[len..], Capability::LongLivedGracefulRestart {})) + Capability::LongLivedGracefulRestart {} } - CapabilityCode::MultipleRoutesToDestination => { //TODO handle for real - Ok((&input[len..], Capability::MultipleRoutesToDestination {})) + Capability::MultipleRoutesToDestination {} } - CapabilityCode::ExtendedNextHopEncoding => { - //TODO handle for real - Ok((&input[len..], Capability::ExtendedNextHopEncoding {})) + let mut elements = Vec::new(); + while !input.is_empty() { + let (rem, afi) = be_u16(input)?; + let (rem, safi) = be_u16(rem)?; + let (rem, nh_afi) = be_u16(rem)?; + elements.push(ExtendedNexthopElement { afi, safi, nh_afi }); + input = rem; + } + Capability::ExtendedNextHopEncoding { elements } } - CapabilityCode::OutboundRouteFiltering => { //TODO handle for real - Ok((&input[len..], Capability::OutboundRouteFiltering {})) + Capability::OutboundRouteFiltering {} } - CapabilityCode::BgpSec => { //TODO handle for real - Ok((&input[len..], Capability::BgpSec {})) + Capability::BgpSec {} } - CapabilityCode::MultipleLabels => { //TODO handle for real - Ok((&input[len..], Capability::MultipleLabels {})) + Capability::MultipleLabels {} } - CapabilityCode::BgpRole => { //TODO handle for real - Ok((&input[len..], Capability::BgpRole {})) + Capability::BgpRole {} } - CapabilityCode::DynamicCapability => { //TODO handle for real - Ok((&input[len..], Capability::DynamicCapability {})) + Capability::DynamicCapability {} } - CapabilityCode::MultisessionBgp => { //TODO handle for real - Ok((&input[len..], Capability::MultisessionBgp {})) + Capability::MultisessionBgp {} } - CapabilityCode::RoutingPolicyDistribution => { //TODO handle for real - Ok((&input[len..], Capability::RoutingPolicyDistribution {})) + Capability::RoutingPolicyDistribution {} } - CapabilityCode::PrestandardOrfAndPd => { //TODO handle for real - Ok((&input[len..], Capability::PrestandardOrfAndPd {})) + Capability::PrestandardOrfAndPd {} } - CapabilityCode::PrestandardOutboundRouteFiltering => { //TODO handle for real - Ok(( - &input[len..], - Capability::PrestandardOutboundRouteFiltering {}, - )) + Capability::PrestandardOutboundRouteFiltering {} } - CapabilityCode::PrestandardMultisession => { //TODO handle for real - Ok((&input[len..], Capability::PrestandardMultisession {})) + Capability::PrestandardMultisession {} } - CapabilityCode::PrestandardFqdn => { //TODO handle for real - Ok((&input[len..], Capability::PrestandardFqdn {})) + Capability::PrestandardFqdn {} } - CapabilityCode::PrestandardOperationalMessage => { //TODO handle for real - Ok(( - &input[len..], - Capability::PrestandardOperationalMessage {}, - )) + Capability::PrestandardOperationalMessage {} } - CapabilityCode::Experimental0 => { - Ok((&input[len..], Capability::Experimental { code: 0 })) + Capability::Experimental { code: 0 } } CapabilityCode::Experimental1 => { - Ok((&input[len..], Capability::Experimental { code: 1 })) + Capability::Experimental { code: 1 } } CapabilityCode::Experimental2 => { - Ok((&input[len..], Capability::Experimental { code: 2 })) + Capability::Experimental { code: 2 } } CapabilityCode::Experimental3 => { - Ok((&input[len..], Capability::Experimental { code: 3 })) + Capability::Experimental { code: 3 } } CapabilityCode::Experimental4 => { - Ok((&input[len..], Capability::Experimental { code: 4 })) + Capability::Experimental { code: 4 } } CapabilityCode::Experimental5 => { - Ok((&input[len..], Capability::Experimental { code: 5 })) + Capability::Experimental { code: 5 } } CapabilityCode::Experimental6 => { - Ok((&input[len..], Capability::Experimental { code: 6 })) + Capability::Experimental { code: 6 } } CapabilityCode::Experimental7 => { - Ok((&input[len..], Capability::Experimental { code: 7 })) + Capability::Experimental { code: 7 } } CapabilityCode::Experimental8 => { - Ok((&input[len..], Capability::Experimental { code: 8 })) + Capability::Experimental { code: 8 } } CapabilityCode::Experimental9 => { - Ok((&input[len..], Capability::Experimental { code: 9 })) + Capability::Experimental { code: 9 } } CapabilityCode::Experimental10 => { - Ok((&input[len..], Capability::Experimental { code: 10 })) + Capability::Experimental { code: 10 } } CapabilityCode::Experimental11 => { - Ok((&input[len..], Capability::Experimental { code: 11 })) + Capability::Experimental { code: 11 } } CapabilityCode::Experimental12 => { - Ok((&input[len..], Capability::Experimental { code: 12 })) + Capability::Experimental { code: 12 } } CapabilityCode::Experimental13 => { - Ok((&input[len..], Capability::Experimental { code: 13 })) + Capability::Experimental { code: 13 } } CapabilityCode::Experimental14 => { - Ok((&input[len..], Capability::Experimental { code: 14 })) + Capability::Experimental { code: 14 } } CapabilityCode::Experimental15 => { - Ok((&input[len..], Capability::Experimental { code: 15 })) + Capability::Experimental { code: 15 } } CapabilityCode::Experimental16 => { - Ok((&input[len..], Capability::Experimental { code: 16 })) + Capability::Experimental { code: 16 } } CapabilityCode::Experimental17 => { - Ok((&input[len..], Capability::Experimental { code: 17 })) + Capability::Experimental { code: 17 } } CapabilityCode::Experimental18 => { - Ok((&input[len..], Capability::Experimental { code: 18 })) + Capability::Experimental { code: 18 } } CapabilityCode::Experimental19 => { - Ok((&input[len..], Capability::Experimental { code: 19 })) + Capability::Experimental { code: 19 } } CapabilityCode::Experimental20 => { - Ok((&input[len..], Capability::Experimental { code: 20 })) + Capability::Experimental { code: 20 } } CapabilityCode::Experimental21 => { - Ok((&input[len..], Capability::Experimental { code: 21 })) + Capability::Experimental { code: 21 } } CapabilityCode::Experimental22 => { - Ok((&input[len..], Capability::Experimental { code: 22 })) + Capability::Experimental { code: 22 } } CapabilityCode::Experimental23 => { - Ok((&input[len..], Capability::Experimental { code: 23 })) + Capability::Experimental { code: 23 } } CapabilityCode::Experimental24 => { - Ok((&input[len..], Capability::Experimental { code: 24 })) + Capability::Experimental { code: 24 } } CapabilityCode::Experimental25 => { - Ok((&input[len..], Capability::Experimental { code: 25 })) + Capability::Experimental { code: 25 } } CapabilityCode::Experimental26 => { - Ok((&input[len..], Capability::Experimental { code: 26 })) + Capability::Experimental { code: 26 } } CapabilityCode::Experimental27 => { - Ok((&input[len..], Capability::Experimental { code: 27 })) + Capability::Experimental { code: 27 } } CapabilityCode::Experimental28 => { - Ok((&input[len..], Capability::Experimental { code: 28 })) + Capability::Experimental { code: 28 } } CapabilityCode::Experimental29 => { - Ok((&input[len..], Capability::Experimental { code: 29 })) + Capability::Experimental { code: 29 } } CapabilityCode::Experimental30 => { - Ok((&input[len..], Capability::Experimental { code: 30 })) + Capability::Experimental { code: 30 } } CapabilityCode::Experimental31 => { - Ok((&input[len..], Capability::Experimental { code: 31 })) + Capability::Experimental { code: 31 } } CapabilityCode::Experimental32 => { - Ok((&input[len..], Capability::Experimental { code: 32 })) + Capability::Experimental { code: 32 } } CapabilityCode::Experimental33 => { - Ok((&input[len..], Capability::Experimental { code: 33 })) + Capability::Experimental { code: 33 } } CapabilityCode::Experimental34 => { - Ok((&input[len..], Capability::Experimental { code: 34 })) + Capability::Experimental { code: 34 } } CapabilityCode::Experimental35 => { - Ok((&input[len..], Capability::Experimental { code: 35 })) + Capability::Experimental { code: 35 } } CapabilityCode::Experimental36 => { - Ok((&input[len..], Capability::Experimental { code: 36 })) + Capability::Experimental { code: 36 } } CapabilityCode::Experimental37 => { - Ok((&input[len..], Capability::Experimental { code: 37 })) + Capability::Experimental { code: 37 } } CapabilityCode::Experimental38 => { - Ok((&input[len..], Capability::Experimental { code: 38 })) + Capability::Experimental { code: 38 } } CapabilityCode::Experimental39 => { - Ok((&input[len..], Capability::Experimental { code: 39 })) + Capability::Experimental { code: 39 } } CapabilityCode::Experimental40 => { - Ok((&input[len..], Capability::Experimental { code: 40 })) + Capability::Experimental { code: 40 } } CapabilityCode::Experimental41 => { - Ok((&input[len..], Capability::Experimental { code: 41 })) + Capability::Experimental { code: 41 } } CapabilityCode::Experimental42 => { - Ok((&input[len..], Capability::Experimental { code: 42 })) + Capability::Experimental { code: 42 } } CapabilityCode::Experimental43 => { - Ok((&input[len..], Capability::Experimental { code: 43 })) + Capability::Experimental { code: 43 } } CapabilityCode::Experimental44 => { - Ok((&input[len..], Capability::Experimental { code: 44 })) + Capability::Experimental { code: 44 } } CapabilityCode::Experimental45 => { - Ok((&input[len..], Capability::Experimental { code: 45 })) + Capability::Experimental { code: 45 } } CapabilityCode::Experimental46 => { - Ok((&input[len..], Capability::Experimental { code: 46 })) + Capability::Experimental { code: 46 } } CapabilityCode::Experimental47 => { - Ok((&input[len..], Capability::Experimental { code: 47 })) + Capability::Experimental { code: 47 } } CapabilityCode::Experimental48 => { - Ok((&input[len..], Capability::Experimental { code: 48 })) + Capability::Experimental { code: 48 } } CapabilityCode::Experimental49 => { - Ok((&input[len..], Capability::Experimental { code: 49 })) + Capability::Experimental { code: 49 } } CapabilityCode::Experimental50 => { - Ok((&input[len..], Capability::Experimental { code: 50 })) + Capability::Experimental { code: 50 } } CapabilityCode::Experimental51 => { - Ok((&input[len..], Capability::Experimental { code: 51 })) - } - CapabilityCode::Reserved => { - Ok((&input[len..], Capability::Reserved { code: 0 })) + Capability::Experimental { code: 51 } } - } + CapabilityCode::Reserved => Capability::Reserved { code: 0 }, + }; + Ok((remaining, cap)) } /// Helper function to generate an IPv4 Unicast MP-BGP capability. @@ -4723,6 +4805,22 @@ impl Capability { safi: Safi::Unicast.into(), } } + + pub fn extended_nh_v4_over_v6(&self) -> bool { + if let Self::ExtendedNextHopEncoding { elements } = self { + elements.iter().any(|x| x.is_v4_over_v6()) + } else { + false + } + } + + pub fn extended_nh_v6_over_v4(&self) -> bool { + if let Self::ExtendedNextHopEncoding { elements } = self { + elements.iter().any(|x| x.is_v6_over_v4()) + } else { + false + } + } } /// The set of capability codes supported by this BGP implementation @@ -4874,7 +4972,7 @@ impl From for CapabilityCode { Capability::MultipleRoutesToDestination {} => { CapabilityCode::MultipleRoutesToDestination } - Capability::ExtendedNextHopEncoding {} => { + Capability::ExtendedNextHopEncoding { elements: _ } => { CapabilityCode::ExtendedNextHopEncoding } Capability::BGPExtendedMessage {} => { @@ -5902,7 +6000,18 @@ mod tests { #[test] fn open_round_trip() { - let om0 = OpenMessage::new4(395849, 0x1234, 0xaabbccdd); + let om0 = OpenMessage::new4(395849, 0x1234, 0xaabbccdd, false); + + let buf = om0.to_wire().expect("open message to wire"); + println!("buf: {}", buf.hex_dump()); + + let om1 = OpenMessage::from_wire(&buf).expect("open message from wire"); + assert_eq!(om0, om1); + } + + #[test] + fn open_round_trip_extended_nexthop() { + let om0 = OpenMessage::new4(395849, 0x1234, 0xaabbccdd, true); let buf = om0.to_wire().expect("open message to wire"); println!("buf: {}", buf.hex_dump()); diff --git a/bgp/src/params.rs b/bgp/src/params.rs index 2a038c43..818428cb 100644 --- a/bgp/src/params.rs +++ b/bgp/src/params.rs @@ -15,7 +15,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::{ collections::{BTreeMap, HashMap}, - net::{IpAddr, SocketAddr}, + net::{IpAddr, Ipv6Addr, SocketAddr, SocketAddrV6}, sync::atomic::Ordering, time::Duration, }; @@ -158,36 +158,18 @@ pub struct Ipv6UnicastConfig { pub struct Neighbor { pub asn: u32, pub name: String, - pub host: SocketAddr, - pub hold_time: u64, - pub idle_hold_time: u64, - pub delay_open: u64, - pub connect_retry: u64, - pub keepalive: u64, - pub resolution: u64, pub group: String, - pub passive: bool, - pub remote_asn: Option, - pub min_ttl: Option, - pub md5_auth_key: Option, - pub multi_exit_discriminator: Option, - pub communities: Vec, - pub local_pref: Option, - pub enforce_first_as: bool, - /// IPv4 Unicast address family configuration (None = disabled) - pub ipv4_unicast: Option, - /// IPv6 Unicast address family configuration (None = disabled) - pub ipv6_unicast: Option, - pub vlan_id: Option, - pub connect_retry_jitter: Option, - pub idle_hold_jitter: Option, - pub deterministic_collision_resolution: bool, + pub host: SocketAddr, + #[serde(flatten)] + pub parameters: BgpPeerParameters, } impl Neighbor { /// Validate that at least one address family is enabled pub fn validate_address_families(&self) -> Result<(), String> { - if self.ipv4_unicast.is_none() && self.ipv6_unicast.is_none() { + if self.parameters.ipv4_unicast.is_none() + && self.parameters.ipv6_unicast.is_none() + { return Err("at least one address family must be enabled".into()); } Ok(()) @@ -201,7 +183,7 @@ impl Neighbor { /// - IPv4 Unicast enabled for IPv6 peer requires configured IPv4 nexthop /// - IPv6 Unicast enabled for IPv4 peer requires configured IPv6 nexthop pub fn validate_nexthop(&self) -> Result<(), String> { - if let Some(cfg) = &self.ipv4_unicast { + if let Some(cfg) = &self.parameters.ipv4_unicast { if let Some(nh) = cfg.nexthop { if !nh.is_ipv4() { return Err(format!( @@ -217,7 +199,7 @@ impl Neighbor { } } - if let Some(cfg) = &self.ipv6_unicast { + if let Some(cfg) = &self.parameters.ipv6_unicast { if let Some(nh) = cfg.nexthop { if !nh.is_ipv6() { return Err(format!( @@ -242,25 +224,27 @@ impl Neighbor { pub struct NeighborV1 { pub asn: u32, pub name: String, + pub group: String, pub host: SocketAddr, - pub hold_time: u64, - pub idle_hold_time: u64, - pub delay_open: u64, - pub connect_retry: u64, - pub keepalive: u64, - pub resolution: u64, + #[serde(flatten)] + pub parameters: BgpPeerParametersV1, +} + +#[derive(Debug, Deserialize, Serialize, JsonSchema, Clone, PartialEq)] +pub struct UnnumberedNeighbor { + pub asn: u32, + pub name: String, pub group: String, - pub passive: bool, - pub remote_asn: Option, - pub min_ttl: Option, - pub md5_auth_key: Option, - pub multi_exit_discriminator: Option, - pub communities: Vec, - pub local_pref: Option, - pub enforce_first_as: bool, - pub allow_import: ImportExportPolicyV1, - pub allow_export: ImportExportPolicyV1, - pub vlan_id: Option, + pub interface: String, + pub act_as_a_default_ipv6_router: u16, + #[serde(flatten)] + pub parameters: BgpPeerParameters, +} + +#[derive(Debug, Deserialize, Serialize, JsonSchema, Clone, PartialEq)] +pub struct PendingUnnumberedNeighbor { + pub interface: String, + pub local_addr: Ipv6Addr, } impl From for PeerConfig { @@ -269,12 +253,12 @@ impl From for PeerConfig { name: rq.name.clone(), group: rq.group.clone(), host: rq.host, - hold_time: rq.hold_time, - idle_hold_time: rq.idle_hold_time, - delay_open: rq.delay_open, - connect_retry: rq.connect_retry, - keepalive: rq.keepalive, - resolution: rq.resolution, + hold_time: rq.parameters.hold_time, + idle_hold_time: rq.parameters.idle_hold_time, + delay_open: rq.parameters.delay_open, + connect_retry: rq.parameters.connect_retry, + keepalive: rq.parameters.keepalive, + resolution: rq.parameters.resolution, } } } @@ -285,12 +269,12 @@ impl From for PeerConfig { name: rq.name.clone(), group: rq.group.clone(), host: rq.host, - hold_time: rq.hold_time, - idle_hold_time: rq.idle_hold_time, - delay_open: rq.delay_open, - connect_retry: rq.connect_retry, - keepalive: rq.keepalive, - resolution: rq.resolution, + hold_time: rq.parameters.hold_time, + idle_hold_time: rq.parameters.idle_hold_time, + delay_open: rq.parameters.delay_open, + connect_retry: rq.parameters.connect_retry, + keepalive: rq.parameters.keepalive, + resolution: rq.parameters.resolution, } } } @@ -303,59 +287,117 @@ impl NeighborV1 { ) -> Self { Self { asn, - remote_asn: rq.remote_asn, - min_ttl: rq.min_ttl, - name: rq.name.clone(), - host: rq.host, - hold_time: rq.hold_time, - idle_hold_time: rq.idle_hold_time, - delay_open: rq.delay_open, - connect_retry: rq.connect_retry, - keepalive: rq.keepalive, - resolution: rq.resolution, - passive: rq.passive, group: group.clone(), - md5_auth_key: rq.md5_auth_key, - multi_exit_discriminator: rq.multi_exit_discriminator, - communities: rq.communities, - local_pref: rq.local_pref, - enforce_first_as: rq.enforce_first_as, - allow_import: rq.allow_import, - allow_export: rq.allow_export, - vlan_id: rq.vlan_id, + host: rq.host, + name: rq.name.clone(), + parameters: rq.parameters.clone(), } } pub fn from_rdb_neighbor_info(asn: u32, rq: &rdb::BgpNeighborInfo) -> Self { Self { asn, - remote_asn: rq.remote_asn, - min_ttl: rq.min_ttl, + group: rq.group.clone(), name: rq.name.clone(), host: rq.host, - hold_time: rq.hold_time, - idle_hold_time: rq.idle_hold_time, - delay_open: rq.delay_open, - connect_retry: rq.connect_retry, - keepalive: rq.keepalive, - resolution: rq.resolution, - passive: rq.passive, + parameters: BgpPeerParametersV1 { + remote_asn: rq.parameters.remote_asn, + min_ttl: rq.parameters.min_ttl, + hold_time: rq.parameters.hold_time, + idle_hold_time: rq.parameters.idle_hold_time, + delay_open: rq.parameters.delay_open, + connect_retry: rq.parameters.connect_retry, + keepalive: rq.parameters.keepalive, + resolution: rq.parameters.resolution, + passive: rq.parameters.passive, + md5_auth_key: rq.parameters.md5_auth_key.clone(), + multi_exit_discriminator: rq + .parameters + .multi_exit_discriminator, + communities: rq.parameters.communities.clone(), + local_pref: rq.parameters.local_pref, + enforce_first_as: rq.parameters.enforce_first_as, + allow_import: ImportExportPolicyV1::from_per_af_policies( + &rq.parameters.allow_import4, + &rq.parameters.allow_import6, + ), + allow_export: ImportExportPolicyV1::from_per_af_policies( + &rq.parameters.allow_export4, + &rq.parameters.allow_export6, + ), + vlan_id: rq.parameters.vlan_id, + }, + } + } +} + +impl UnnumberedNeighbor { + pub fn from_bgp_peer_config( + asn: u32, + group: String, + rq: UnnumberedBgpPeerConfig, + ) -> Self { + Self { + asn, + group: group.clone(), + interface: rq.interface.clone(), + name: rq.name.clone(), + act_as_a_default_ipv6_router: rq.router_lifetime, + parameters: rq.parameters.clone(), + } + } + + pub fn to_peer_config(&self, addr: SocketAddrV6) -> PeerConfig { + PeerConfig { + name: self.name.clone(), + host: addr.into(), + group: self.group.clone(), + hold_time: self.parameters.hold_time, + idle_hold_time: self.parameters.idle_hold_time, + delay_open: self.parameters.delay_open, + connect_retry: self.parameters.connect_retry, + keepalive: self.parameters.keepalive, + resolution: self.parameters.resolution, + } + } + + pub fn from_rdb_neighbor_info( + asn: u32, + rq: &rdb::BgpUnnumberedNeighborInfo, + ) -> Self { + Self { + asn, group: rq.group.clone(), - md5_auth_key: rq.md5_auth_key.clone(), - multi_exit_discriminator: rq.multi_exit_discriminator, - communities: rq.communities.clone(), - local_pref: rq.local_pref, - enforce_first_as: rq.enforce_first_as, - // Combine per-AF policies into legacy format for API compatibility - allow_import: ImportExportPolicyV1::from_per_af_policies( - &rq.allow_import4, - &rq.allow_import6, - ), - allow_export: ImportExportPolicyV1::from_per_af_policies( - &rq.allow_export4, - &rq.allow_export6, - ), - vlan_id: rq.vlan_id, + name: rq.name.clone(), + interface: rq.interface.clone(), + act_as_a_default_ipv6_router: rq.router_lifetime, + parameters: BgpPeerParameters { + remote_asn: rq.parameters.remote_asn, + min_ttl: rq.parameters.min_ttl, + hold_time: rq.parameters.hold_time, + idle_hold_time: rq.parameters.idle_hold_time, + delay_open: rq.parameters.delay_open, + connect_retry: rq.parameters.connect_retry, + keepalive: rq.parameters.keepalive, + resolution: rq.parameters.resolution, + passive: rq.parameters.passive, + md5_auth_key: rq.parameters.md5_auth_key.clone(), + multi_exit_discriminator: rq + .parameters + .multi_exit_discriminator, + communities: rq.parameters.communities.clone(), + local_pref: rq.parameters.local_pref, + enforce_first_as: rq.parameters.enforce_first_as, + vlan_id: rq.parameters.vlan_id, + ipv4_unicast: None, + ipv6_unicast: None, + deterministic_collision_resolution: false, + idle_hold_jitter: None, + connect_retry_jitter: Some(JitterRange { + min: 0.75, + max: 1.0, + }), + }, } } } @@ -372,50 +414,30 @@ impl Neighbor { ) -> Self { Self { asn, - remote_asn: rq.remote_asn, - min_ttl: rq.min_ttl, name: rq.name.clone(), host: rq.host, - hold_time: rq.hold_time, - idle_hold_time: rq.idle_hold_time, - delay_open: rq.delay_open, - connect_retry: rq.connect_retry, - keepalive: rq.keepalive, - resolution: rq.resolution, - passive: rq.passive, group: group.clone(), - md5_auth_key: rq.md5_auth_key, - multi_exit_discriminator: rq.multi_exit_discriminator, - communities: rq.communities, - local_pref: rq.local_pref, - enforce_first_as: rq.enforce_first_as, - ipv4_unicast: rq.ipv4_unicast, - ipv6_unicast: rq.ipv6_unicast, - vlan_id: rq.vlan_id, - connect_retry_jitter: rq.connect_retry_jitter, - idle_hold_jitter: rq.idle_hold_jitter, - deterministic_collision_resolution: rq - .deterministic_collision_resolution, + parameters: rq.parameters.clone(), } } pub fn from_rdb_neighbor_info(asn: u32, rq: &rdb::BgpNeighborInfo) -> Self { // Use explicit enablement flags from the database - let ipv4_unicast = if rq.ipv4_enabled { + let ipv4_unicast = if rq.parameters.ipv4_enabled { Some(Ipv4UnicastConfig { - nexthop: rq.nexthop4, - import_policy: rq.allow_import4.clone(), - export_policy: rq.allow_export4.clone(), + nexthop: rq.parameters.nexthop4, + import_policy: rq.parameters.allow_import4.clone(), + export_policy: rq.parameters.allow_export4.clone(), }) } else { None }; - let ipv6_unicast = if rq.ipv6_enabled { + let ipv6_unicast = if rq.parameters.ipv6_enabled { Some(Ipv6UnicastConfig { - nexthop: rq.nexthop6, - import_policy: rq.allow_import6.clone(), - export_policy: rq.allow_export6.clone(), + nexthop: rq.parameters.nexthop6, + import_policy: rq.parameters.allow_import6.clone(), + export_policy: rq.parameters.allow_export6.clone(), }) } else { None @@ -423,32 +445,36 @@ impl Neighbor { Self { asn, - remote_asn: rq.remote_asn, - min_ttl: rq.min_ttl, name: rq.name.clone(), host: rq.host, - hold_time: rq.hold_time, - idle_hold_time: rq.idle_hold_time, - delay_open: rq.delay_open, - connect_retry: rq.connect_retry, - keepalive: rq.keepalive, - resolution: rq.resolution, - passive: rq.passive, group: rq.group.clone(), - md5_auth_key: rq.md5_auth_key.clone(), - multi_exit_discriminator: rq.multi_exit_discriminator, - communities: rq.communities.clone(), - local_pref: rq.local_pref, - enforce_first_as: rq.enforce_first_as, - ipv4_unicast, - ipv6_unicast, - vlan_id: rq.vlan_id, - connect_retry_jitter: Some(JitterRange { - min: 0.75, - max: 1.0, - }), - idle_hold_jitter: None, - deterministic_collision_resolution: false, + parameters: BgpPeerParameters { + remote_asn: rq.parameters.remote_asn, + min_ttl: rq.parameters.min_ttl, + hold_time: rq.parameters.hold_time, + idle_hold_time: rq.parameters.idle_hold_time, + delay_open: rq.parameters.delay_open, + connect_retry: rq.parameters.connect_retry, + keepalive: rq.parameters.keepalive, + resolution: rq.parameters.resolution, + passive: rq.parameters.passive, + md5_auth_key: rq.parameters.md5_auth_key.clone(), + multi_exit_discriminator: rq + .parameters + .multi_exit_discriminator, + communities: rq.parameters.communities.clone(), + local_pref: rq.parameters.local_pref, + enforce_first_as: rq.parameters.enforce_first_as, + ipv4_unicast, + ipv6_unicast, + vlan_id: rq.parameters.vlan_id, + connect_retry_jitter: Some(JitterRange { + min: 0.75, + max: 1.0, + }), + idle_hold_jitter: None, + deterministic_collision_resolution: false, + }, } } } @@ -848,6 +874,29 @@ pub struct ApplyRequestV1 { pub struct BgpPeerConfigV1 { pub host: SocketAddr, pub name: String, + #[serde(flatten)] + pub parameters: BgpPeerParametersV1, +} + +#[derive(Debug, Deserialize, Serialize, JsonSchema, Clone, PartialEq)] +pub struct BgpPeerConfig { + pub host: SocketAddr, + pub name: String, + #[serde(flatten)] + pub parameters: BgpPeerParameters, +} + +#[derive(Debug, Deserialize, Serialize, JsonSchema, Clone, PartialEq)] +pub struct UnnumberedBgpPeerConfig { + pub interface: String, + pub name: String, + pub router_lifetime: u16, + #[serde(flatten)] + pub parameters: BgpPeerParameters, +} + +#[derive(Debug, Deserialize, Serialize, JsonSchema, Clone, PartialEq)] +pub struct BgpPeerParameters { pub hold_time: u64, pub idle_hold_time: u64, pub delay_open: u64, @@ -862,16 +911,30 @@ pub struct BgpPeerConfigV1 { pub communities: Vec, pub local_pref: Option, pub enforce_first_as: bool, - pub allow_import: ImportExportPolicyV1, - pub allow_export: ImportExportPolicyV1, pub vlan_id: Option, + + // new stuff after v1 + /// IPv4 Unicast address family configuration (None = disabled) + pub ipv4_unicast: Option, + /// IPv6 Unicast address family configuration (None = disabled) + pub ipv6_unicast: Option, + /// Enable deterministic collision resolution in Established state. + /// When true, uses BGP-ID comparison per RFC 4271 §6.8 for collision + /// resolution even when one connection is already in Established state. + /// When false, Established connection always wins (timing-based resolution). + pub deterministic_collision_resolution: bool, + /// Jitter range for idle hold timer. When used, the idle hold timer is + /// multiplied by a random value within the (min, max) range supplied. + /// Useful to help break repeated synchronization of connection collisions. + pub idle_hold_jitter: Option, + /// Jitter range for connect_retry timer. When used, the connect_retry timer + /// is multiplied by a random value within the (min, max) range supplied. + /// Useful to help break repeated synchronization of connection collisions. + pub connect_retry_jitter: Option, } -/// BGP peer configuration (current version with per-address-family policies). -#[derive(Debug, Deserialize, Serialize, JsonSchema, Clone)] -pub struct BgpPeerConfig { - pub host: SocketAddr, - pub name: String, +#[derive(Debug, Deserialize, Serialize, JsonSchema, Clone, PartialEq)] +pub struct BgpPeerParametersV1 { pub hold_time: u64, pub idle_hold_time: u64, pub delay_open: u64, @@ -886,24 +949,9 @@ pub struct BgpPeerConfig { pub communities: Vec, pub local_pref: Option, pub enforce_first_as: bool, - /// IPv4 Unicast address family configuration (None = disabled) - pub ipv4_unicast: Option, - /// IPv6 Unicast address family configuration (None = disabled) - pub ipv6_unicast: Option, + pub allow_import: ImportExportPolicyV1, + pub allow_export: ImportExportPolicyV1, pub vlan_id: Option, - /// Jitter range for connect_retry timer. When used, the connect_retry timer - /// is multiplied by a random value within the (min, max) range supplied. - /// Useful to help break repeated synchronization of connection collisions. - pub connect_retry_jitter: Option, - /// Jitter range for idle hold timer. When used, the idle hold timer is - /// multiplied by a random value within the (min, max) range supplied. - /// Useful to help break repeated synchronization of connection collisions. - pub idle_hold_jitter: Option, - /// Enable deterministic collision resolution in Established state. - /// When true, uses BGP-ID comparison per RFC 4271 §6.8 for collision - /// resolution even when one connection is already in Established state. - /// When false, Established connection always wins (timing-based resolution). - pub deterministic_collision_resolution: bool, } impl From for BgpPeerConfig { @@ -912,33 +960,37 @@ impl From for BgpPeerConfig { Self { host: cfg.host, name: cfg.name, - hold_time: cfg.hold_time, - idle_hold_time: cfg.idle_hold_time, - delay_open: cfg.delay_open, - connect_retry: cfg.connect_retry, - keepalive: cfg.keepalive, - resolution: cfg.resolution, - passive: cfg.passive, - remote_asn: cfg.remote_asn, - min_ttl: cfg.min_ttl, - md5_auth_key: cfg.md5_auth_key, - multi_exit_discriminator: cfg.multi_exit_discriminator, - communities: cfg.communities, - local_pref: cfg.local_pref, - enforce_first_as: cfg.enforce_first_as, - ipv4_unicast: Some(Ipv4UnicastConfig { - nexthop: None, - import_policy: cfg.allow_import.as_ipv4_policy(), - export_policy: cfg.allow_export.as_ipv4_policy(), - }), - ipv6_unicast: None, - vlan_id: cfg.vlan_id, - connect_retry_jitter: Some(JitterRange { - min: 0.75, - max: 1.0, - }), - idle_hold_jitter: None, - deterministic_collision_resolution: false, + parameters: BgpPeerParameters { + hold_time: cfg.parameters.hold_time, + idle_hold_time: cfg.parameters.idle_hold_time, + delay_open: cfg.parameters.delay_open, + connect_retry: cfg.parameters.connect_retry, + keepalive: cfg.parameters.keepalive, + resolution: cfg.parameters.resolution, + passive: cfg.parameters.passive, + remote_asn: cfg.parameters.remote_asn, + min_ttl: cfg.parameters.min_ttl, + md5_auth_key: cfg.parameters.md5_auth_key, + multi_exit_discriminator: cfg + .parameters + .multi_exit_discriminator, + communities: cfg.parameters.communities, + local_pref: cfg.parameters.local_pref, + enforce_first_as: cfg.parameters.enforce_first_as, + ipv4_unicast: Some(Ipv4UnicastConfig { + nexthop: None, + import_policy: cfg.parameters.allow_import.as_ipv4_policy(), + export_policy: cfg.parameters.allow_export.as_ipv4_policy(), + }), + ipv6_unicast: None, + vlan_id: cfg.parameters.vlan_id, + connect_retry_jitter: Some(JitterRange { + min: 0.75, + max: 1.0, + }), + idle_hold_jitter: None, + deterministic_collision_resolution: false, + }, } } } @@ -1069,6 +1121,9 @@ pub struct ApplyRequest { pub shaper: Option, /// Lists of peers indexed by peer group. pub peers: HashMap>, + /// Lists of unnumbered peers indexed by peer group. + #[serde(default)] + pub unnumbered_peers: HashMap>, } impl From for ApplyRequest { @@ -1085,6 +1140,7 @@ impl From for ApplyRequest { (k, v.into_iter().map(BgpPeerConfig::from).collect()) }) .collect(), + unnumbered_peers: HashMap::default(), } } } diff --git a/bgp/src/policy.rs b/bgp/src/policy.rs index e2f7a492..c956b182 100644 --- a/bgp/src/policy.rs +++ b/bgp/src/policy.rs @@ -424,7 +424,7 @@ mod test { // check that open messages without the 4-octet AS capability code get dropped let asn = 47; let addr = "198.51.100.1".parse().unwrap(); - let m = OpenMessage::new2(asn, 30, 1701); + let m = OpenMessage::new2(asn, 30, 1701, false); let source = std::fs::read_to_string("../bgp/policy/policy-check0.rhai") .unwrap(); @@ -435,7 +435,7 @@ mod test { assert_eq!(result, CheckerResult::Drop); // check that open messages with the 4-octet AS capability code get accepted - let m = OpenMessage::new4(asn.into(), 30, 1701); + let m = OpenMessage::new4(asn.into(), 30, 1701, false); let result = check_incoming_open(m, &ast, asn.into(), addr, init_logger()) .unwrap(); @@ -475,7 +475,7 @@ mod test { // check that open messages without the 4-octet AS capability code get dropped let asn = 100; let addr = "198.51.100.1".parse().unwrap(); - let mut m = OpenMessage::new2(asn, 30, 1701); + let mut m = OpenMessage::new2(asn, 30, 1701, false); let source = std::fs::read_to_string("../bgp/policy/policy-shape0.rhai") .unwrap(); diff --git a/bgp/src/session.rs b/bgp/src/session.rs index 809213e2..383a4eb4 100644 --- a/bgp/src/session.rs +++ b/bgp/src/session.rs @@ -19,9 +19,9 @@ use crate::{ Safi, UpdateMessage, }, params::{ - BgpCapability, DynamicTimerInfo, Ipv4UnicastConfig, Ipv6UnicastConfig, - JitterRange, PeerCounters, PeerInfo, PeerTimers, StaticTimerInfo, - TimerConfig, + BgpCapability, BgpPeerParameters, BgpPeerParametersV1, + DynamicTimerInfo, Ipv4UnicastConfig, Ipv6UnicastConfig, JitterRange, + PeerCounters, PeerInfo, PeerTimers, StaticTimerInfo, TimerConfig, }, policy::{CheckerResult, ShaperResult}, recv_event_loop, recv_event_return, @@ -219,6 +219,7 @@ fn select_nexthop( nlri_afi: Afi, local_ip: IpAddr, configured_nexthop: Option, + caps: &BTreeSet, ) -> Result { // Canonicalize the local_ip to handle IPv4-mapped IPv6 addresses let local_ip = local_ip.to_canonical(); @@ -228,16 +229,8 @@ fn select_nexthop( return match (nlri_afi, nexthop) { (Afi::Ipv4, IpAddr::V4(ipv4)) => Ok(BgpNexthop::Ipv4(ipv4)), (Afi::Ipv6, IpAddr::V6(ipv6)) => Ok(BgpNexthop::Ipv6Single(ipv6)), - // XXX: Extended Next-Hop - (Afi::Ipv4, IpAddr::V6(_)) => Err(Error::InvalidAddress( - "IPv4 routes require IPv4 next-hop (configured mismatch)" - .into(), - )), - // XXX: Extended Next-Hop - (Afi::Ipv6, IpAddr::V4(_)) => Err(Error::InvalidAddress( - "IPv6 routes require IPv6 next-hop (configured mismatch)" - .into(), - )), + (Afi::Ipv4, IpAddr::V6(ipv6)) => v4_over_v6_nexthop(caps, ipv6), + (Afi::Ipv6, IpAddr::V4(ipv4)) => v6_over_v4_nexthop(caps, ipv4), }; } @@ -245,16 +238,36 @@ fn select_nexthop( match (nlri_afi, local_ip) { (Afi::Ipv4, IpAddr::V4(ipv4)) => Ok(BgpNexthop::Ipv4(ipv4)), (Afi::Ipv6, IpAddr::V6(ipv6)) => Ok(BgpNexthop::Ipv6Single(ipv6)), - (Afi::Ipv4, IpAddr::V6(_)) => { - Err(Error::InvalidAddress( - "IPv4 routes require IPv4 next-hop (Extended Next-Hop not negotiated)".into() - )) - } - (Afi::Ipv6, IpAddr::V4(_)) => { - Err(Error::InvalidAddress( - "IPv6 routes require IPv6 next-hop".into() - )) - } + (Afi::Ipv4, IpAddr::V6(ipv6)) => v4_over_v6_nexthop(caps, ipv6), + (Afi::Ipv6, IpAddr::V4(ipv4)) => v6_over_v4_nexthop(caps, ipv4), + } +} + +fn v4_over_v6_nexthop( + caps: &BTreeSet, + nexthop: Ipv6Addr, +) -> Result { + let v4_over_v6 = caps.iter().any(|x| x.extended_nh_v4_over_v6()); + if v4_over_v6 { + Ok(BgpNexthop::Ipv6Single(nexthop)) + } else { + Err(Error::InvalidAddress(format!( + "Ipv6 nexthop {nexthop} without extended NH v4 over v6 negotiated" + ))) + } +} + +fn v6_over_v4_nexthop( + caps: &BTreeSet, + nexthop: Ipv4Addr, +) -> Result { + let v6_over_v4 = caps.iter().any(|x| x.extended_nh_v6_over_v4()); + if v6_over_v4 { + Ok(BgpNexthop::Ipv4(nexthop)) + } else { + Err(Error::InvalidAddress(format!( + "Ipv4 nexthop {nexthop} without extended NH v6 over v4 negotiated" + ))) } } @@ -896,6 +909,72 @@ impl SessionInfo { } } +impl From<&BgpPeerParameters> for SessionInfo { + fn from(value: &BgpPeerParameters) -> Self { + SessionInfo { + passive_tcp_establishment: value.passive, + remote_asn: value.remote_asn, + min_ttl: value.min_ttl, + md5_auth_key: value.md5_auth_key.clone(), + multi_exit_discriminator: value.multi_exit_discriminator, + communities: value.communities.clone().into_iter().collect(), + local_pref: value.local_pref, + enforce_first_as: value.enforce_first_as, + vlan_id: value.vlan_id, + remote_id: None, + bind_addr: None, + connect_retry_time: Duration::from_secs(value.connect_retry), + keepalive_time: Duration::from_secs(value.keepalive), + hold_time: Duration::from_secs(value.hold_time), + idle_hold_time: Duration::from_secs(value.idle_hold_time), + delay_open_time: Duration::from_secs(value.delay_open), + resolution: Duration::from_millis(value.resolution), + idle_hold_jitter: value.idle_hold_jitter, + connect_retry_jitter: value.connect_retry_jitter, + deterministic_collision_resolution: value + .deterministic_collision_resolution, + ipv4_unicast: value.ipv4_unicast.clone(), + ipv6_unicast: value.ipv6_unicast.clone(), + } + } +} + +impl From<&BgpPeerParametersV1> for SessionInfo { + fn from(value: &BgpPeerParametersV1) -> Self { + SessionInfo { + passive_tcp_establishment: value.passive, + remote_asn: value.remote_asn, + min_ttl: value.min_ttl, + md5_auth_key: value.md5_auth_key.clone(), + multi_exit_discriminator: value.multi_exit_discriminator, + communities: value.communities.clone().into_iter().collect(), + local_pref: value.local_pref, + enforce_first_as: value.enforce_first_as, + vlan_id: value.vlan_id, + remote_id: None, + bind_addr: None, + connect_retry_time: Duration::from_secs(value.connect_retry), + keepalive_time: Duration::from_secs(value.keepalive), + hold_time: Duration::from_secs(value.hold_time), + idle_hold_time: Duration::from_secs(value.idle_hold_time), + delay_open_time: Duration::from_secs(value.delay_open), + resolution: Duration::from_millis(value.resolution), + idle_hold_jitter: None, + connect_retry_jitter: Some(JitterRange { + min: 0.75, + max: 1.0, + }), + deterministic_collision_resolution: false, + ipv4_unicast: Some(Ipv4UnicastConfig { + nexthop: None, + import_policy: value.allow_import.as_ipv4_policy().clone(), + export_policy: value.allow_export.as_ipv4_policy().clone(), + }), + ipv6_unicast: None, + } + } +} + /// Information about a neighbor (peer). #[derive(Debug, Clone)] pub struct NeighborInfo { @@ -7159,13 +7238,23 @@ impl SessionRunner { let capabilities = lock!(self.caps_tx).clone(); // pull hold_time from config, not the clock let hold_time = lock!(self.session).hold_time; + let extended_nexthop = match self.get_peer_info().remote_ip { + IpAddr::V6(addr) => addr.is_unicast_link_local(), + _ => false, + }; let mut msg = match self.asn { - Asn::FourOctet(asn) => { - OpenMessage::new4(asn, hold_time.as_secs() as u16, self.id) - } - Asn::TwoOctet(asn) => { - OpenMessage::new2(asn, hold_time.as_secs() as u16, self.id) - } + Asn::FourOctet(asn) => OpenMessage::new4( + asn, + hold_time.as_secs() as u16, + self.id, + extended_nexthop, + ), + Asn::TwoOctet(asn) => OpenMessage::new2( + asn, + hold_time.as_secs() as u16, + self.id, + extended_nexthop, + ), }; msg.add_capabilities(&capabilities); @@ -7320,7 +7409,12 @@ impl SessionRunner { .and_then(|cfg| cfg.nexthop), }; - select_nexthop(nlri_afi, pc.conn.local().ip(), configured_nexthop) + select_nexthop( + nlri_afi, + pc.conn.local().ip(), + configured_nexthop, + &pc.caps, + ) } /// Add peer-specific path attributes to an UPDATE message. @@ -7431,23 +7525,34 @@ impl SessionRunner { // Each RouteUpdate is either an announcement OR withdrawal, never both. let mut update = match route_update { RouteUpdate::V4(RouteUpdate4::Announce(nlri)) => { - let nh4 = match self.derive_nexthop(Afi::Ipv4, pc)? { - BgpNexthop::Ipv4(addr) => addr, - _ => { - return Err(Error::InvalidAddress( - "IPv4 routes require IPv4 next-hop".into(), - )); + match self.derive_nexthop(Afi::Ipv4, pc)? { + BgpNexthop::Ipv4(nh4) => { + let mut path_attributes = self.router.base_attributes(); + path_attributes + .push(PathAttributeValue::NextHop(nh4).into()); + + UpdateMessage { + withdrawn: vec![], + path_attributes, + nlri, + ..Default::default() + } } - }; - - let mut path_attributes = self.router.base_attributes(); - path_attributes.push(PathAttributeValue::NextHop(nh4).into()); + nh6 @ BgpNexthop::Ipv6Single(_) + | nh6 @ BgpNexthop::Ipv6Double(_) => { + let mut path_attrs = self.router.base_attributes(); + let reach = MpReachNlri::ipv4_unicast(nh6, nlri); + path_attrs.push( + PathAttributeValue::MpReachNlri(reach).into(), + ); - UpdateMessage { - withdrawn: vec![], - path_attributes, - nlri, - ..Default::default() + UpdateMessage { + withdrawn: vec![], + path_attributes: path_attrs, + nlri: vec![], + ..Default::default() + } + } } } RouteUpdate::V4(RouteUpdate4::Withdraw(withdrawn)) => { @@ -7965,8 +8070,10 @@ impl SessionRunner { self, warn, pc.conn, - "MP_REACH_NLRI for unnegotiated AFI/SAFI: {}/{}", - afi, safi; + "MP_REACH_NLRI for unnegotiated AFI/SAFI: {}/{}: {:?}", + afi, + safi, + afi_state; ); self.counters @@ -9184,7 +9291,12 @@ mod tests { let configured_nh = ip!("10.0.0.1"); let local_ip = ip!("10.0.0.2"); - let result = select_nexthop(Afi::Ipv4, local_ip, Some(configured_nh)); + let result = select_nexthop( + Afi::Ipv4, + local_ip, + Some(configured_nh), + &BTreeSet::default(), + ); assert!(result.is_ok()); match result.unwrap() { BgpNexthop::Ipv4(addr) => { @@ -9201,7 +9313,12 @@ mod tests { let configured_nh = ip!("2001:db8::1"); let local_ip = ip!("2001:db8::2"); - let result = select_nexthop(Afi::Ipv6, local_ip, Some(configured_nh)); + let result = select_nexthop( + Afi::Ipv6, + local_ip, + Some(configured_nh), + &BTreeSet::default(), + ); assert!(result.is_ok()); match result.unwrap() { BgpNexthop::Ipv6Single(addr) => { @@ -9217,7 +9334,8 @@ mod tests { // No nexthop configured, pure IPv4 local_ip should be used for IPv4 routes let local_ip = ip!("10.0.0.1"); - let result = select_nexthop(Afi::Ipv4, local_ip, None); + let result = + select_nexthop(Afi::Ipv4, local_ip, None, &BTreeSet::default()); assert!(result.is_ok()); match result.unwrap() { BgpNexthop::Ipv4(addr) => { @@ -9235,7 +9353,8 @@ mod tests { // [::]:179 with v6_only=false. let mapped = ip!("::ffff:10.0.0.1"); - let result = select_nexthop(Afi::Ipv4, mapped, None); + let result = + select_nexthop(Afi::Ipv4, mapped, None, &BTreeSet::default()); assert!(result.is_ok()); match result.unwrap() { BgpNexthop::Ipv4(addr) => { @@ -9251,7 +9370,8 @@ mod tests { // No nexthop configured, pure IPv6 local_ip should be used for IPv6 routes let local_ip = ip!("2001:db8::1"); - let result = select_nexthop(Afi::Ipv6, local_ip, None); + let result = + select_nexthop(Afi::Ipv6, local_ip, None, &BTreeSet::default()); assert!(result.is_ok()); match result.unwrap() { BgpNexthop::Ipv6Single(addr) => { @@ -9268,7 +9388,12 @@ mod tests { let nexthop = ip!("2001:db8::1"); let local_ip = ip!("10.0.0.1"); - let result = select_nexthop(Afi::Ipv4, local_ip, Some(nexthop)); + let result = select_nexthop( + Afi::Ipv4, + local_ip, + Some(nexthop), + &BTreeSet::default(), + ); // Should error because IPv4 route needs IPv4 nexthop assert!(result.is_err()); } @@ -9279,7 +9404,12 @@ mod tests { let nexthop = ip!("10.0.0.1"); let local_ip = ip!("2001:db8::1"); - let result = select_nexthop(Afi::Ipv6, local_ip, Some(nexthop)); + let result = select_nexthop( + Afi::Ipv6, + local_ip, + Some(nexthop), + &BTreeSet::default(), + ); // Should error because IPv6 route needs IPv6 nexthop assert!(result.is_err()); } @@ -9289,7 +9419,8 @@ mod tests { // IPv4 route with pure IPv6 local_ip and no configured nexthop = error let local_ip = ip!("2001:db8::1"); - let result = select_nexthop(Afi::Ipv4, local_ip, None); + let result = + select_nexthop(Afi::Ipv4, local_ip, None, &BTreeSet::default()); // Should error because cannot derive IPv4 nexthop from IPv6 connection assert!(result.is_err()); } @@ -9299,7 +9430,8 @@ mod tests { // IPv6 route with pure IPv4 local_ip and no configured nexthop = error let local_ip = ip!("10.0.0.1"); - let result = select_nexthop(Afi::Ipv6, local_ip, None); + let result = + select_nexthop(Afi::Ipv6, local_ip, None, &BTreeSet::default()); // Should error because cannot derive IPv6 nexthop from IPv4 connection assert!(result.is_err()); } diff --git a/falcon-lab/Cargo.toml b/falcon-lab/Cargo.toml new file mode 100644 index 00000000..199f0a37 --- /dev/null +++ b/falcon-lab/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "falcon-lab" +version = "0.1.0" +edition = "2024" + +[dependencies] +libfalcon.workspace = true +anyhow.workspace = true +serde.workspace = true +serde_json.workspace = true +mg-admin-client.workspace = true +ddm-admin-client.workspace = true +tokio.workspace = true +slog.workspace = true +dpd-client.workspace = true +clap.workspace = true +oxnet.workspace = true +oxide-tokio-rt.workspace = true +bgp.workspace = true +colored.workspace = true +rdb-types.workspace = true diff --git a/falcon-lab/src/bgp.rs b/falcon-lab/src/bgp.rs new file mode 100644 index 00000000..aa3bf451 --- /dev/null +++ b/falcon-lab/src/bgp.rs @@ -0,0 +1,50 @@ +//! BGP utilities + +use mg_admin_client::types::{ + ImportExportPolicy4, ImportExportPolicy6, Ipv4UnicastConfig, + Ipv6UnicastConfig, UnnumberedNeighbor, +}; + +pub fn basic_unnumbered_neighbor( + name: &str, + group: &str, + interface: &str, + local_asn: u32, + act_as_a_default_ipv6_router: u16, +) -> UnnumberedNeighbor { + UnnumberedNeighbor { + asn: local_asn, + act_as_a_default_ipv6_router, + communities: Vec::default(), + connect_retry: 5, + delay_open: 0, + enforce_first_as: false, + group: group.to_owned(), + hold_time: 6, + idle_hold_time: 0, + interface: interface.to_string(), + keepalive: 2, + local_pref: None, + md5_auth_key: None, + min_ttl: None, + multi_exit_discriminator: None, + name: name.to_string(), + passive: false, + remote_asn: None, + resolution: 100, + vlan_id: None, + ipv4_unicast: Some(Ipv4UnicastConfig { + import_policy: ImportExportPolicy4::NoFiltering, + export_policy: ImportExportPolicy4::NoFiltering, + nexthop: None, + }), + ipv6_unicast: Some(Ipv6UnicastConfig { + import_policy: ImportExportPolicy6::NoFiltering, + export_policy: ImportExportPolicy6::NoFiltering, + nexthop: None, + }), + connect_retry_jitter: None, + deterministic_collision_resolution: false, + idle_hold_jitter: None, + } +} diff --git a/falcon-lab/src/ddm.rs b/falcon-lab/src/ddm.rs new file mode 100644 index 00000000..2b52d92d --- /dev/null +++ b/falcon-lab/src/ddm.rs @@ -0,0 +1,36 @@ +//! DDM machinery + +#![allow(dead_code)] + +use crate::{dendrite::DendriteNode, illumos::IllumosNode}; +use anyhow::Result; +use ddm_admin_client::Client; +use libfalcon::{NodeRef, Runner}; +use std::net::IpAddr; + +#[derive(Copy, Clone)] +pub struct DdmNode(pub NodeRef); + +impl DdmNode { + pub async fn run_ddm(&self, d: &Runner) -> Result<()> { + d.exec( + self.0, + "chmod +x /opt/cargo-bay/ddmd && \ + /opt/cargo-bay/ddmd &> /tmp/ddm.log &", + ) + .await?; + Ok(()) + } + + pub async fn client(&self, d: &Runner, addr: IpAddr) -> Result { + Ok(Client::new(&format!("http://{addr}:8000"), d.log.clone())) + } + + pub fn illumos(&self) -> IllumosNode { + IllumosNode(self.0) + } + + pub fn dendrite(&self) -> DendriteNode { + DendriteNode(self.0) + } +} diff --git a/falcon-lab/src/dendrite.rs b/falcon-lab/src/dendrite.rs new file mode 100644 index 00000000..aa540a57 --- /dev/null +++ b/falcon-lab/src/dendrite.rs @@ -0,0 +1,122 @@ +//! Dendrite machinery + +#![allow(dead_code)] + +use crate::illumos::IllumosNode; +use anyhow::{Result, anyhow}; +use dpd_client::{ + Client, + types::{LinkCreate, LinkId, PortId, PortSpeed}, +}; +use libfalcon::{NodeRef, Runner}; +use slog::{Logger, debug, info}; +use std::{net::IpAddr, sync::Arc, time::Duration}; +use tokio::time::{Instant, sleep}; + +#[derive(Copy, Clone)] +pub struct DendriteNode(pub NodeRef); + +impl DendriteNode { + pub fn name(&self, d: &Runner) -> String { + d.get_node(self.0).name.clone() + } + + pub async fn client(&self, d: &Runner, addr: IpAddr) -> Result { + let client_state = dpd_client::ClientState { + tag: String::default(), + log: d.log.clone(), + }; + Ok(Client::new( + &format!("http://{addr}:{}", dpd_client::default_port()), + client_state, + )) + } + + pub async fn npuvm( + self, + d: Arc, + front_ports: usize, + rear_ports: usize, + npuvm_commit: String, + dendrite_commit: Option, + sidecar_lite_commit: Option, + ) -> Result<()> { + const BUILDOMAT_URL: &str = + "https://buildomat.eng.oxide.computer/public/file/oxidecomputer/"; + info!(d.log, "{}: setting up npuvm", self.name(&d)); + d.exec( + self.0, + &format!( + "curl --retry 5 -OL \ + {BUILDOMAT_URL}/softnpu/image/{npuvm_commit}/npuvm" + ), + ) + .await?; + d.exec(self.0, "chmod +x npuvm").await?; + d.exec( + self.0, + &format!( + "./npuvm install \ + --front-ports {front_ports} \ + --rear-ports {rear_ports} \ + --pkt-source vioif0 \ + {} {}", + dendrite_commit + .map(|x| format!("--dendrite-commit {x}")) + .unwrap_or_default(), + sidecar_lite_commit + .map(|x| format!("--sidecar-lite-commit {x}")) + .unwrap_or_default(), + ), + ) + .await?; + d.exec( + self.0, + "/root/scadm propolis load-program /root/libsidecar_lite.so", + ) + .await?; + Ok(()) + } + + pub fn illumos(&self) -> IllumosNode { + IllumosNode(self.0) + } +} + +pub async fn softnpu_link_create(c: &Client, name: &str) -> Result<()> { + let port = PortId::Qsfp(name.parse()?); + let link = LinkId(0); + c.link_create( + &port, + &LinkCreate { + autoneg: false, + fec: None, + kr: false, + lane: Some(link), + speed: PortSpeed::Speed100G, + tx_eq: None, + }, + ) + .await?; + c.link_enabled_set(&port, &link, true).await?; + Ok(()) +} + +pub async fn wait_for_dpd( + c: &Client, + timeout: Duration, + log: &Logger, +) -> Result<()> { + let start = Instant::now(); + loop { + match c.dpd_uptime().await { + Ok(_) => return Ok(()), + Err(e) => debug!(log, "wait for dpd: {e}"), + } + if start.elapsed() >= timeout { + break; + } + sleep(Duration::from_secs(1)).await + } + Err(anyhow!("timeout waiting for dpd")) +} diff --git a/falcon-lab/src/eos.rs b/falcon-lab/src/eos.rs new file mode 100644 index 00000000..410bf91d --- /dev/null +++ b/falcon-lab/src/eos.rs @@ -0,0 +1,167 @@ +//! Arista EOS machinery + +#![allow(dead_code)] + +use crate::linux::LinuxNode; +use anyhow::{Result, anyhow}; +use colored::Colorize; +use libfalcon::{NodeRef, Runner}; +use oxnet::{Ipv4Net, Ipv6Net}; +use serde::Deserialize; +use slog::info; +use std::collections::HashMap; + +#[derive(Copy, Clone)] +pub struct EosNode(pub NodeRef); + +impl EosNode { + pub fn name(&self, d: &Runner) -> String { + d.get_node(self.0).name.clone() + } + + pub async fn wait_for_init(&self, d: &Runner) -> Result<()> { + info!(d.log, "waiting for ceos to initialize"); + let mut retries = 60usize; + loop { + if retries == 0 { + break; + } + retries = retries.saturating_sub(1); + let status = d + .exec( + self.0, + "docker inspect ceos --format '{{.State.Status}}'", + ) + .await?; + + let version = self.shell(d, "show version").await?; + + if status.contains("running") && version.contains("Arista cEOSLab") + { + return Ok(()); + } + } + Err(anyhow!("ceos wait for init timeout")) + } + + pub async fn shell(&self, d: &Runner, script: &str) -> Result { + info!( + d.log, + "{}: executing eos script {}", + self.name(d), + script.dimmed() + ); + + let response = d + .exec(self.0, &format!("docker exec ceos Cli -c '{script}'")) + .await?; + + Ok(response) + } + + pub fn linux(&self) -> LinuxNode { + LinuxNode(self.0) + } + + /// Get BGP IPv4 imported prefixes from EOS. + pub async fn bgp_ipv4_imported( + &self, + d: &Runner, + ) -> Result { + let output = self.shell(d, "show ip bgp | json").await?; + let response: BgpIpv4Response = serde_json::from_str(&output)?; + Ok(response) + } + + /// Get BGP IPv6 imported prefixes from EOS. + pub async fn bgp_ipv6_imported( + &self, + d: &Runner, + ) -> Result { + let output = self.shell(d, "show ipv6 bgp | json").await?; + let response: BgpIpv6Response = serde_json::from_str(&output)?; + Ok(response) + } +} + +/// Minimal representation of `show ip bgp | json` output. +/// Only captures destination prefix and nexthop. +#[derive(Debug, Deserialize)] +pub struct BgpIpv4Response { + pub vrfs: HashMap, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BgpIpv4Vrf { + pub bgp_route_entries: HashMap, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BgpIpv4RouteEntry { + pub bgp_route_paths: Vec, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BgpIpv4RoutePath { + #[serde(default)] + pub next_hop: String, +} + +impl BgpIpv4Response { + /// Returns all imported routes (those with a non-empty nexthop) from all VRFs. + pub fn all(&self) -> impl Iterator { + self.vrfs.values().flat_map(|vrf| { + vrf.bgp_route_entries.iter().flat_map(|(prefix, entry)| { + entry + .bgp_route_paths + .iter() + .filter(|path| !path.next_hop.is_empty()) + .map(move |path| (prefix, path)) + }) + }) + } +} + +/// Minimal representation of `show ipv6 bgp | json` output. +/// Only captures destination prefix and nexthop. +#[derive(Debug, Deserialize)] +pub struct BgpIpv6Response { + pub vrfs: HashMap, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BgpIpv6Vrf { + pub bgp_route_entries: HashMap, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BgpIpv6RouteEntry { + pub bgp_route_paths: Vec, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BgpIpv6RoutePath { + #[serde(default)] + pub next_hop: String, +} + +impl BgpIpv6Response { + /// Returns all imported routes (those with a non-empty nexthop) from all VRFs. + pub fn all(&self) -> impl Iterator { + self.vrfs.values().flat_map(|vrf| { + vrf.bgp_route_entries.iter().flat_map(|(prefix, entry)| { + entry + .bgp_route_paths + .iter() + .filter(|path| !path.next_hop.is_empty()) + .map(move |path| (prefix, path)) + }) + }) + } +} diff --git a/falcon-lab/src/frr.rs b/falcon-lab/src/frr.rs new file mode 100644 index 00000000..066f3da8 --- /dev/null +++ b/falcon-lab/src/frr.rs @@ -0,0 +1,148 @@ +//! FRR machinery + +#![allow(dead_code)] + +use crate::linux::LinuxNode; +use anyhow::{Context, Result}; +use colored::Colorize; +use libfalcon::{NodeRef, Runner}; +use oxnet::{Ipv4Net, Ipv6Net}; +use serde::Deserialize; +use slog::info; +use std::collections::HashMap; +use std::net::IpAddr; +use std::time::Duration; +use tokio::time::sleep; + +#[derive(Copy, Clone)] +pub struct FrrNode(pub NodeRef); + +impl FrrNode { + pub fn name(&self, d: &Runner) -> String { + d.get_node(self.0).name.clone() + } + + pub async fn enable_daemons( + &self, + d: &Runner, + daemons: &[&str], + ) -> Result<()> { + for name in daemons { + info!(d.log, "{}: enabling frr daemon {name}", self.name(d)); + d.exec( + self.0, + &format!("sed -i 's/{name}=no/{name}=yes/g' /etc/frr/daemons"), + ) + .await?; + } + d.exec(self.0, "systemctl restart frr").await?; + // XXX do better than arbitrary wait + sleep(Duration::from_secs(5)).await; + Ok(()) + } + + pub async fn install(&self, d: &Runner) -> Result<()> { + info!(d.log, "{}: installing frr", self.name(d)); + d.exec(self.0, "apt-get -y update && apt-get -y install frr") + .await + .context("apt install frr failed")?; + Ok(()) + } + + pub fn linux(&self) -> LinuxNode { + LinuxNode(self.0) + } + + /// Execute a vtysh command and return the output. + pub async fn shell(&self, d: &Runner, script: &str) -> Result { + info!( + d.log, + "{}: executing frr script {}", + self.name(d), + script.dimmed() + ); + let args = script + .lines() + .map(|l| format!("-c '{l}'")) + .collect::>() + .join(" "); + let output = d + .exec(self.0, &format!("vtysh {args}")) + .await + .context("vtysh shell failed")?; + Ok(output) + } + + /// Get BGP IPv4 imported prefixes from FRR. + pub async fn bgp_ipv4_imported( + &self, + d: &Runner, + ) -> Result { + let output = self.shell(d, "show ip bgp json").await?; + let response: FrrBgpIpv4Response = serde_json::from_str(&output)?; + Ok(response) + } + + /// Get BGP IPv6 imported prefixes from FRR. + pub async fn bgp_ipv6_imported( + &self, + d: &Runner, + ) -> Result { + let output = self.shell(d, "show bgp json").await?; + let response: FrrBgpIpv6Response = serde_json::from_str(&output)?; + Ok(response) + } +} + +/// Minimal representation of FRR `show ip bgp json` output. +/// Only captures destination prefix and nexthop. +#[derive(Debug, Deserialize)] +pub struct FrrBgpIpv4Response { + pub routes: HashMap>, +} + +impl FrrBgpIpv4Response { + /// Returns all imported routes (those with a non-unspecified nexthop) from the response. + pub fn all(&self) -> impl Iterator { + self.routes.iter().flat_map(|(prefix, paths)| { + paths.iter().flat_map(move |path| { + path.nexthops + .iter() + .filter(|nh| !nh.ip.is_unspecified()) + .map(move |nh| (prefix, nh)) + }) + }) + } +} + +/// Minimal representation of FRR `show bgp json` output (IPv6). +/// Only captures destination prefix and nexthop. +#[derive(Debug, Deserialize)] +pub struct FrrBgpIpv6Response { + pub routes: HashMap>, +} + +impl FrrBgpIpv6Response { + /// Returns all imported routes (those with a non-unspecified nexthop) from the response. + pub fn all(&self) -> impl Iterator { + self.routes.iter().flat_map(|(prefix, paths)| { + paths.iter().flat_map(move |path| { + path.nexthops + .iter() + .filter(|nh| !nh.ip.is_unspecified()) + .map(move |nh| (prefix, nh)) + }) + }) + } +} + +#[derive(Debug, Deserialize)] +pub struct FrrBgpRoutePath { + #[serde(default)] + pub nexthops: Vec, +} + +#[derive(Debug, Deserialize)] +pub struct FrrBgpNexthop { + pub ip: IpAddr, +} diff --git a/falcon-lab/src/illumos.rs b/falcon-lab/src/illumos.rs new file mode 100644 index 00000000..fdb819db --- /dev/null +++ b/falcon-lab/src/illumos.rs @@ -0,0 +1,100 @@ +//! illumos machinery + +use anyhow::{Result, anyhow}; +use libfalcon::{NodeRef, Runner}; +use slog::{debug, error}; +use std::{net::IpAddr, time::Duration}; +use tokio::time::{Instant, sleep}; + +#[derive(Copy, Clone)] +pub struct IllumosNode(pub NodeRef); + +impl IllumosNode { + pub async fn ip(&self, d: &Runner, addrobj: &str) -> Result { + let ip = d + .exec(self.0, &format!("ipadm show-addr {addrobj} -p -o addr")) + .await?; + // handle link locals with percent scopes + if let Some((ip, _)) = ip.split_once("%") { + return ip.parse().map_err(|e| anyhow!("invalid ip: {ip}: {e}")); + } + let ipnet: oxnet::IpNet = + ip.parse().map_err(|e| anyhow!("invalid ip: {ip}: {e}"))?; + Ok(ipnet.addr()) + } + + pub async fn dhcp(&self, d: &Runner, addrobj: &str) -> Result { + d.exec(self.0, &format!("ipadm create-addr -T dhcp {addrobj}")) + .await?; + d.exec(self.0, "echo 'nameserver 1.1.1.1' > /etc/resolv.conf") + .await?; + let mut retries = 10usize; + loop { + match self.ip(d, addrobj).await { + Ok(addr) => return Ok(addr), + Err(e) => { + if retries > 0 { + debug!(d.log, "error waiting for dhcp address: {e}"); + retries = retries.saturating_sub(1); + sleep(Duration::from_secs(1)).await; + } else { + error!(d.log, "error waiting for dhcp address: {e}"); + break; + } + } + } + } + Err(anyhow!("dhcp timed out")) + } + + pub async fn addrconf(&self, d: &Runner, addrobj: &str) -> Result { + d.exec(self.0, &format!("ipadm create-addr -T addrconf {addrobj}")) + .await?; + let mut retries = 10usize; + loop { + match self.ip(d, addrobj).await { + Ok(addr) => return Ok(addr), + Err(e) => { + if retries > 0 { + debug!( + d.log, + "error waiting for addrconf address: {e}" + ); + retries = retries.saturating_sub(1); + sleep(Duration::from_secs(1)).await; + } else { + error!( + d.log, + "error waiting for addrconf address: {e}" + ); + break; + } + } + } + } + Err(anyhow!("addrconf timed out")) + } + + pub async fn wait_for_link( + &self, + d: &Runner, + name: &str, + timeout: Duration, + ) -> Result<()> { + let start = Instant::now(); + loop { + let result = d + .exec(self.0, &format!("dladm show-link {name} -p -o link")) + .await + .map_err(|e| anyhow!("error showing link {name}: {e}"))?; + if result.as_str() == name { + return Ok(()); + } + if start.elapsed() >= timeout { + break; + } + sleep(Duration::from_secs(1)).await + } + Err(anyhow!("timeout waiting for link {name}")) + } +} diff --git a/falcon-lab/src/linux.rs b/falcon-lab/src/linux.rs new file mode 100644 index 00000000..bac85aef --- /dev/null +++ b/falcon-lab/src/linux.rs @@ -0,0 +1,31 @@ +//! Linux machinery + +#![allow(dead_code)] + +use anyhow::{Context, Result}; +use libfalcon::{NodeRef, Runner}; +use serde::Deserialize; +use std::net::IpAddr; + +#[derive(Copy, Clone)] +pub struct LinuxNode(pub NodeRef); + +impl LinuxNode { + pub async fn ip(&self, d: &Runner, link: &str) -> Result> { + let json = d.exec(self.0, &format!("ip j addr show {link}")).await?; + let ipaddr: IpAddrInfo = + serde_json::from_str(&json).context("parse ip addr json")?; + + Ok(ipaddr.addr_info.iter().map(|x| x.local).collect()) + } +} + +#[derive(Deserialize)] +struct IpAddrInfo { + addr_info: Vec, +} + +#[derive(Deserialize)] +struct AddrInfo { + local: IpAddr, +} diff --git a/falcon-lab/src/main.rs b/falcon-lab/src/main.rs new file mode 100644 index 00000000..e590b7a9 --- /dev/null +++ b/falcon-lab/src/main.rs @@ -0,0 +1,91 @@ +//! Falcon test lab + +use crate::test::{cleanup_unnumbered_test, run_trio_unnumbered_test}; +use clap::{Parser, Subcommand}; + +mod bgp; +mod ddm; +mod dendrite; +mod eos; +mod frr; +mod illumos; +mod linux; +mod mgd; +mod test; +mod topo; +mod util; + +#[derive(Debug, Parser)] +struct Cli { + #[clap(subcommand)] + command: Command, +} + +#[derive(Debug, Subcommand)] +enum Command { + Run(Run), + Cleanup(Cleanup), + Serial(Serial), +} + +#[derive(Debug, Parser)] +struct Run { + #[clap(subcommand)] + command: TestCommand, + + #[clap(long)] + no_cleanup: bool, + + #[clap(long, default_value = "fd2c726815cdb03c2687e1bf2912a9184905557b")] + npuvm_commit: String, + + #[clap(long)] + dendrite_commit: Option, + + #[clap(long)] + sidecar_lite_commit: Option, +} + +#[derive(Debug, Parser)] +struct Cleanup { + #[clap(subcommand)] + command: TestCommand, +} + +#[derive(Debug, Parser)] +struct Serial { + node: String, +} + +#[derive(Debug, Subcommand)] +enum TestCommand { + TrioUnnumbered, +} + +fn main() -> anyhow::Result<()> { + oxide_tokio_rt::run(run()) +} + +async fn run() -> anyhow::Result<()> { + let cli = Cli::parse(); + match cli.command { + Command::Run(cmd) => match cmd.command { + TestCommand::TrioUnnumbered => { + run_trio_unnumbered_test( + cmd.no_cleanup, + cmd.npuvm_commit.clone(), + cmd.dendrite_commit, + cmd.sidecar_lite_commit, + ) + .await? + } + }, + Command::Cleanup(cmd) => match cmd.command { + TestCommand::TrioUnnumbered => cleanup_unnumbered_test().await?, + }, + Command::Serial(cmd) => { + libfalcon::cli::console(&cmd.node, ".falcon".into()).await? + } + } + Ok(()) +} diff --git a/falcon-lab/src/mgd.rs b/falcon-lab/src/mgd.rs new file mode 100644 index 00000000..ed432b86 --- /dev/null +++ b/falcon-lab/src/mgd.rs @@ -0,0 +1,59 @@ +//! MGD machinery + +use crate::{ddm::DdmNode, dendrite::DendriteNode, illumos::IllumosNode}; +use anyhow::{Result, anyhow}; +use libfalcon::{NodeRef, Runner}; +use mg_admin_client::Client; +use slog::{Logger, debug}; +use std::{net::IpAddr, time::Duration}; +use tokio::time::{Instant, sleep}; + +#[derive(Copy, Clone)] +pub struct MgdNode(pub NodeRef); + +impl MgdNode { + pub async fn run_mgd(&self, d: &Runner) -> Result<()> { + d.exec( + self.0, + "chmod +x /opt/cargo-bay/mgd && \ + /opt/cargo-bay/mgd run &> /tmp/mgd.log &", + ) + .await?; + Ok(()) + } + + pub async fn client(&self, d: &Runner, addr: IpAddr) -> Result { + Ok(Client::new(&format!("http://{addr}:4676"), d.log.clone())) + } + + pub fn illumos(&self) -> IllumosNode { + IllumosNode(self.0) + } + + pub fn dendrite(&self) -> DendriteNode { + DendriteNode(self.0) + } + + pub fn ddm(&self) -> DdmNode { + DdmNode(self.0) + } +} + +pub async fn wait_for_mgd( + c: &Client, + timeout: Duration, + log: &Logger, +) -> Result<()> { + let start = Instant::now(); + loop { + match c.read_routers().await { + Ok(_) => return Ok(()), + Err(e) => debug!(log, "wait for mgd: {e}"), + } + if start.elapsed() >= timeout { + break; + } + sleep(Duration::from_secs(1)).await + } + Err(anyhow!("timeout waiting for mgd")) +} diff --git a/falcon-lab/src/test.rs b/falcon-lab/src/test.rs new file mode 100644 index 00000000..6871e228 --- /dev/null +++ b/falcon-lab/src/test.rs @@ -0,0 +1,322 @@ +//! Tests + +#![allow(clippy::iter_nth_zero)] + +use crate::{ + bgp::basic_unnumbered_neighbor, + dendrite::{softnpu_link_create, wait_for_dpd}, + eos::EosNode, + frr::FrrNode, + mgd::wait_for_mgd, + topo::{Trio, trio}, + wait_for_eq, +}; +use anyhow::{Context, Result}; +use libfalcon::Runner; +use mg_admin_client::types::{FsmStateKind, Origin4, Origin6, Router}; +use rdb_types::AddressFamily; +use slog::info; +use std::{sync::Arc, time::Duration}; + +const TRIO_UNNUMBERED_TOPO_NAME: &str = "mgtriou"; +const OP_TIMEOUT: Duration = Duration::from_secs(10); + +pub async fn cleanup_unnumbered_test() -> Result<()> { + // dropping this with out persistent set will destroy + // the topo + let _topo = trio(TRIO_UNNUMBERED_TOPO_NAME)?; + Ok(()) +} + +pub async fn run_trio_unnumbered_test( + persistent: bool, + npuvm_commit: String, + dendrite_commit: Option, + sidecar_lite_commit: Option, +) -> Result<()> { + let Trio { + mut d, + ox, + cr1, + cr2, + } = trio(TRIO_UNNUMBERED_TOPO_NAME)?; + d.persistent = persistent; + + d.launch().await.context("launch failed")?; + + let ad = std::sync::Arc::new(d); + + let addr = ox.illumos().dhcp(&ad, "vioif1/dhcp").await?; + + // These take a minute, knock them out concurrently + let mut js = tokio::task::JoinSet::new(); + js.spawn(frr_setup(cr1, ad.clone())); + js.spawn(eos_setup(cr2, ad.clone())); + js.spawn(ox.dendrite().npuvm( + ad.clone(), + 2, + 0, + npuvm_commit, + dendrite_commit, + sidecar_lite_commit, + )); + for result in js.join_all().await.into_iter() { + result?; + } + + let mgd = ox.client(&ad, addr).await?; + let dpd = ox.dendrite().client(&ad, addr).await?; + + // Wait for dpd to start + wait_for_dpd(&dpd, OP_TIMEOUT, &ad.log).await?; + + for link in ["qsfp0", "qsfp1"] { + softnpu_link_create(&dpd, link) + .await + .context(format!("create {link}"))?; + } + + for link in ["tfportqsfp0_0", "tfportqsfp1_0"] { + ox.illumos().wait_for_link(&ad, link, OP_TIMEOUT).await?; + let addr = format!("{link}/ll"); + ox.illumos() + .addrconf(&ad, &addr) + .await + .context(format!("create {addr}"))?; + } + + ox.run_mgd(&ad).await?; + ox.ddm().run_ddm(&ad).await?; + + // Wait for mgd to start + wait_for_mgd(&mgd, OP_TIMEOUT, &ad.log).await?; + + let local_asn: u32 = 33; + + info!(ad.log, "adding BGP router to mgd"); + + mgd.create_router(&Router { + asn: local_asn, + graceful_shutdown: false, + id: 33, + listen: "[::]:179".to_owned(), + }) + .await + .context("mgd: create router")?; + + mgd.create_unnumbered_neighbor(&basic_unnumbered_neighbor( + "cr1", + "test", + "tfportqsfp0_0", + 33, + 0, + )) + .await + .context("mgd: create cr1 unnumbered neighbor")?; + + mgd.create_unnumbered_neighbor(&basic_unnumbered_neighbor( + "cr2", + "test", + "tfportqsfp1_0", + 33, + 1800, + )) + .await + .context("mgd: create cr2 unnumbered neighbor")?; + + mgd.create_origin4(&Origin4 { + asn: 33, + prefixes: vec!["4.5.6.0/24".parse().expect("parse ipv4 origin")], + }) + .await + .context("announce v4 prefix")?; + + mgd.create_origin6(&Origin6 { + asn: 33, + prefixes: vec!["fdee::/64".parse().expect("parse ipv6 origin")], + }) + .await + .context("announce v6 prefix")?; + + wait_for_eq!( + mgd.get_neighbors_v3(local_asn) + .await + .map(|x| x.len()) + .unwrap_or(0), + 2, + "neighbors" + ); + + wait_for_eq!( + mgd.get_neighbors_v3(local_asn) + .await + .map(|x| x.values().nth(0).map(|y| y.fsm_state)) + .unwrap_or(None), + Some(FsmStateKind::Established), + "first neighbor established" + ); + + wait_for_eq!( + mgd.get_rib_imported(Some(&AddressFamily::Ipv4), None) + .await + .map(|x| x.len()) + .unwrap_or(0), + 1, + "imported ipv4 route" + ); + + wait_for_eq!( + mgd.get_rib_imported(Some(&AddressFamily::Ipv4), None) + .await + .map(|x| x.values().nth(0).map(|x| x.len())) + .unwrap_or(None), + Some(2), + "ipv4 paths" + ); + + wait_for_eq!( + dpd.route_ipv4_list(None, None) + .await + .map(|x| x.items.len()) + .unwrap_or(0), + 1, + "dpd ipv4 routes" + ); + + wait_for_eq!( + mgd.get_rib_imported(Some(&AddressFamily::Ipv6), None) + .await + .map(|x| x.len()) + .unwrap_or(0), + 1, + "imported ipv6 route" + ); + + wait_for_eq!( + mgd.get_rib_imported(Some(&AddressFamily::Ipv6), None) + .await + .map(|x| x.values().nth(0).map(|x| x.len())) + .unwrap_or(None), + Some(2), + "ipv6 paths" + ); + + wait_for_eq!( + dpd.route_ipv6_list(None, None) + .await + .map(|x| x.items.len()) + .unwrap_or(0), + 1, + "dpd ipv6 routes" + ); + + wait_for_eq!( + cr1.bgp_ipv4_imported(&ad) + .await + .map(|x| x.all().count()) + .unwrap_or(0), + 1, + "cr1 imported ipv4 routes" + ); + + wait_for_eq!( + cr1.bgp_ipv6_imported(&ad) + .await + .map(|x| x.all().count()) + .unwrap_or(0), + 1, + "cr1 imported ipv6 routes" + ); + + wait_for_eq!( + cr2.bgp_ipv4_imported(&ad) + .await + .map(|x| x.all().count()) + .unwrap_or(0), + 1, + "cr2 imported ipv4 routes" + ); + + wait_for_eq!( + cr2.bgp_ipv6_imported(&ad) + .await + .map(|x| x.all().count()) + .unwrap_or(0), + 1, + "cr2 imported ipv6 routes" + ); + + info!(ad.log, "trio bgp unnumbered test passed 🎉"); + + Ok(()) +} + +async fn frr_setup(r: FrrNode, d: Arc) -> Result<()> { + const BASE_CONFIG: &str = " + configure + ip forwarding + ipv6 forwarding + ip route 1.2.3.0/24 null0 + ipv6 route fd99::/64 null0 + route-map PERMIT-ALL permit 10 + router bgp 44 + timers bgp 2 6 + neighbor enp0s8 interface remote-as external + neighbor enp0s8 timers connect 1 + address-family ipv4 unicast + network 1.2.3.0/24 + neighbor enp0s8 activate + neighbor enp0s8 route-map PERMIT-ALL out + neighbor enp0s8 route-map PERMIT-ALL in + exit-address-family + address-family ipv6 unicast + network fd99::/64 + neighbor enp0s8 activate + neighbor enp0s8 route-map PERMIT-ALL out + neighbor enp0s8 route-map PERMIT-ALL in + exit-address-family + exit + "; + + r.install(&d).await?; + r.enable_daemons(&d, &["bgpd"]).await?; + r.shell(&d, BASE_CONFIG).await?; + Ok(()) +} + +async fn eos_setup(r: EosNode, d: Arc) -> Result<()> { + const BASE_CONFIG: &str = " + enable + configure + ipv6 unicast-routing + ip routing ipv6 interfaces + ip routing + ip route 1.2.3.0/24 null0 + ipv6 route fd99::/64 null0 + interface et1 + no switchport + ipv6 enable + + router bgp 45 + router-id 1.2.3.1 + no bgp default ipv4-unicast + timers bgp 2 6 + neighbor ebgp peer group + neighbor ebgp remote-as 33 + neighbor interface Et1 peer-group ebgp + address-family ipv4 + neighbor ebgp activate + neighbor ebgp next-hop address-family ipv6 originate + network 1.2.3.0/24 + exit + address-family ipv6 + neighbor ebgp activate + neighbor ebgp next-hop address-family ipv6 originate + network fd99::/64 + exit + exit + "; + r.wait_for_init(&d).await?; + r.shell(&d, BASE_CONFIG).await?; + Ok(()) +} diff --git a/falcon-lab/src/topo.rs b/falcon-lab/src/topo.rs new file mode 100644 index 00000000..360ef716 --- /dev/null +++ b/falcon-lab/src/topo.rs @@ -0,0 +1,47 @@ +//! Testing topologies + +use anyhow::Result; +use libfalcon::{Runner, node, unit::gb}; + +use crate::{eos::EosNode, frr::FrrNode, mgd::MgdNode}; + +pub struct Trio { + pub d: Runner, + pub ox: MgdNode, + pub cr1: FrrNode, + pub cr2: EosNode, +} + +pub fn trio(name: &str) -> Result { + let mut d = Runner::new(name); + + // nodes + node!(d, ox, "helios-2.9", 4, gb(4)); + node!(d, cr1, "debian-13.2", 4, gb(4)); + node!(d, cr2, "eos-4.35", 4, gb(4)); + + // links + let mut mac_counter = 0; + let mut new_mac = || { + mac_counter += 1; + format!("a8:40:25:00:00:{mac_counter:02}") + }; + + d.softnpu_link(ox, cr1, Some(new_mac()), None); + d.softnpu_link(ox, cr2, Some(new_mac()), None); + + d.default_ext_link(ox)?; + d.default_ext_link(cr1)?; + d.default_ext_link(cr2)?; + + d.mount("cargo-bay", "/opt/cargo-bay", ox)?; + d.mount("cargo-bay", "/opt/cargo-bay", cr1)?; + d.mount("cargo-bay", "/opt/cargo-bay", cr2)?; + + Ok(Trio { + d, + ox: MgdNode(ox), + cr1: FrrNode(cr1), + cr2: EosNode(cr2), + }) +} diff --git a/falcon-lab/src/util.rs b/falcon-lab/src/util.rs new file mode 100644 index 00000000..12bb60c0 --- /dev/null +++ b/falcon-lab/src/util.rs @@ -0,0 +1,24 @@ +#[macro_export] +macro_rules! wait_for_eq { + ($measure:expr, $expect:expr, $period:expr, $count:expr, $desc:expr) => { + for i in 0..$count { + let measured = $measure; + let expected = $expect; + if measured == expected { + break; + } + if i == $count - 1 { + anyhow::bail!( + "{}: expected {:?}, got {:?}", + $desc, + expected, + measured + ); + } + tokio::time::sleep(Duration::from_secs($period)).await; + } + }; + ($measure:expr, $expect:expr, $desc:expr) => { + wait_for_eq!($measure, $expect, 1, 20, $desc); + }; +} diff --git a/mg-api/src/lib.rs b/mg-api/src/lib.rs index 1f2e2528..b7e80095 100644 --- a/mg-api/src/lib.rs +++ b/mg-api/src/lib.rs @@ -13,7 +13,8 @@ use bgp::{ params::{ ApplyRequest, ApplyRequestV1, CheckerSource, Neighbor, NeighborResetOp, NeighborResetOpV1, NeighborV1, Origin4, Origin6, PeerInfo, PeerInfoV1, - PeerInfoV2, Router, ShaperSource, + PeerInfoV2, PendingUnnumberedNeighbor, Router, ShaperSource, + UnnumberedNeighbor, }, session::{FsmEventRecord, MessageHistory, MessageHistoryV1}, }; @@ -41,6 +42,7 @@ api_versions!([ // | example for the next person. // v // (next_int, IDENT), + (5, UNNUMBERED), (4, MP_BGP), (3, SWITCH_IDENTIFIERS), (2, IPV6_BASIC), @@ -116,6 +118,9 @@ pub trait MgAdminApi { ) -> Result; // V1/V2 API - legacy Neighbor type with combined import/export policies + + // Neighbors ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + #[endpoint { method = PUT, path = "/bgp/config/neighbor", versions = ..VERSION_MP_BGP }] async fn create_neighbor( rqctx: RequestContext, @@ -191,6 +196,80 @@ pub trait MgAdminApi { request: TypedBody, ) -> Result; + // Unnumbered neighbors ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + #[endpoint { + method = GET, + path = "/bgp/config/unnumbered-pending", + versions = VERSION_UNNUMBERED.., + }] + async fn read_pending_unnumbered_neighbors( + rqctx: RequestContext, + request: Query, + ) -> Result>, HttpError>; + + #[endpoint { + method = GET, + path = "/bgp/config/unnumbered-neighbors", + versions = VERSION_UNNUMBERED.., + }] + async fn read_unnumbered_neighbors( + rqctx: RequestContext, + request: Query, + ) -> Result>, HttpError>; + + #[endpoint { + method = PUT, + path = "/bgp/config/unnumbered-neighbor", + versions = VERSION_UNNUMBERED.., + }] + async fn create_unnumbered_neighbor( + rqctx: RequestContext, + request: TypedBody, + ) -> Result; + + #[endpoint { + method = GET, + path = "/bgp/config/unnumbered-neighbor", + versions = VERSION_UNNUMBERED.., + }] + async fn read_unnumbered_neighbor( + rqctx: RequestContext, + request: Query, + ) -> Result, HttpError>; + + #[endpoint { + method = POST, + path = "/bgp/config/unnumbered-neighbor", + versions = VERSION_UNNUMBERED.., + }] + async fn update_unnumbered_neighbor( + rqctx: RequestContext, + request: TypedBody, + ) -> Result; + + #[endpoint { + method = DELETE, + path = "/bgp/config/unnumbered-neighbor", + versions = VERSION_UNNUMBERED.., + }] + async fn delete_unnumbered_neighbor( + rqctx: RequestContext, + request: Query, + ) -> Result; + + #[endpoint { + method = POST, + path = "/bgp/clear/unnumbered-neighbor", + versions = VERSION_UNNUMBERED.., + }] + async fn clear_unnumbered_neighbor( + rqctx: RequestContext, + request: TypedBody, + ) -> Result; + + // IPv4 origin ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + #[endpoint { method = PUT, path = "/bgp/config/origin4" }] async fn create_origin4( rqctx: RequestContext, @@ -483,6 +562,12 @@ pub struct NeighborResetRequestV1 { pub op: NeighborResetOpV1, } +#[derive(Debug, Deserialize, Serialize, JsonSchema, Clone)] +pub struct UnnumberedNeighborSelector { + pub asn: u32, + pub interface: String, +} + #[derive(Debug, Deserialize, Serialize, JsonSchema, Clone)] pub struct NeighborResetRequest { pub asn: u32, @@ -510,6 +595,13 @@ impl From for NeighborResetRequest { } } +#[derive(Debug, Deserialize, Serialize, JsonSchema, Clone)] +pub struct UnnumberedNeighborResetRequest { + pub asn: u32, + pub interface: String, + pub op: NeighborResetOp, +} + #[derive(Debug, Deserialize, Serialize, JsonSchema)] pub struct DeleteNeighborRequest { pub asn: u32, diff --git a/mg-lower/Cargo.toml b/mg-lower/Cargo.toml index 89659eeb..a2ee97f7 100644 --- a/mg-lower/Cargo.toml +++ b/mg-lower/Cargo.toml @@ -5,6 +5,7 @@ edition = "2024" [dev-dependencies] util = { path = "../util" } +uuid.workspace = true [dependencies] ddm-admin-client = { path = "../ddm-admin-client" } diff --git a/mg-lower/src/dendrite.rs b/mg-lower/src/dendrite.rs index 100ece5d..06755bdb 100644 --- a/mg-lower/src/dendrite.rs +++ b/mg-lower/src/dendrite.rs @@ -9,7 +9,7 @@ use crate::{ }; use dpd_client::{Client as DpdClient, types, types::LinkState}; use oxnet::{IpNet, Ipv4Net, Ipv6Net}; -use rdb::{Path, Prefix}; +use rdb::{Db, Path, Prefix}; use slog::Logger; use std::{ collections::{BTreeSet, HashSet}, @@ -57,8 +57,9 @@ impl RouteHash { sw: &impl SwitchZone, prefix: Prefix, path: Path, + db: &Db, ) -> Result { - let (port_id, link_id) = get_port_and_link(sw, path.nexthop)?; + let (port_id, link_id) = get_port_and_link(sw, path.nexthop, db)?; let rh = RouteHash { cidr: match prefix { @@ -249,8 +250,32 @@ where return Err(e.into()); } } + (IpNet::V4(c), IpAddr::V6(tgt_ip)) => { + let target = types::Ipv6Route { + tag, + port_id, + link_id, + tgt_ip, + vlan_id, + }; + + let update = types::Ipv4OverIpv6RouteUpdate { + cidr: c, + target, + replace: false, + }; + if let Err(e) = rt.block_on(async { + dpd.route_ipv4_over_ipv6_add(&update).await + }) { + dpd_log!(log, + error, + "failed to create route {r:?} {e}"; + "error" => format!("{e}") + ); + return Err(e.into()); + } + } _ => { - // XXX: re-evaluate for RFC 8950 (BGP unnumbered) support dpd_log!(log, error, "mismatched address-family for subnet {} and target {}", r.cidr, r.nexthop; @@ -357,6 +382,32 @@ fn test_tfport_parser() { fn get_port_and_link( sw: &impl SwitchZone, nexthop: IpAddr, + db: &Db, +) -> Result<(types::PortId, types::LinkId), Error> { + if let IpAddr::V6(nh6) = nexthop + && nh6.is_unicast_link_local() + && let Some(ifx) = db.get_interface_for_unnumbered_nexthop(nh6) + { + let (port, link, _vlan) = parse_tfport_name(&ifx.name)?; + let port_name = format!("qsfp{port}"); + let port_id = types::Qsfp::try_from(&port_name) + .map(types::PortId::Qsfp) + .map_err(|e| { + Error::Tfport(format!( + "bad port name ifname: {} port name: {port_name}: {e}", + ifx.name + )) + })?; + // TODO breakout considerations + let link_id = types::LinkId(link); + return Ok((port_id, link_id)); + } + resolve_port_and_link(sw, nexthop) +} + +fn resolve_port_and_link( + sw: &impl SwitchZone, + nexthop: IpAddr, ) -> Result<(types::PortId, types::LinkId), Error> { let prefix = IpNet::host_net(nexthop); let sys_route = sw.get_route(prefix, Some(Duration::from_secs(1)))?; @@ -409,6 +460,10 @@ pub(crate) fn get_routes_for_prefix( let mut result: Vec = Vec::new(); for r in dpd_routes.iter() { + let dpd_client::types::Route::V4(r) = r else { + // TODO v6 nexthop? + continue; + }; if r.tag != MG_LOWER_TAG { continue; } diff --git a/mg-lower/src/lib.rs b/mg-lower/src/lib.rs index 3b374b0b..279e4d23 100644 --- a/mg-lower/src/lib.rs +++ b/mg-lower/src/lib.rs @@ -171,7 +171,7 @@ fn full_sync( // Compute the bestpath for each prefix and synchronize the ASIC routing // tables with the chosen paths. for (prefix, _paths) in rib_in.iter() { - sync_prefix(tep, &rib_loc, prefix, dpd, ddm, sw, log, &rt)?; + sync_prefix(tep, &rib_loc, prefix, dpd, ddm, sw, db, log, &rt)?; } Ok(()) @@ -192,7 +192,7 @@ fn handle_change( let rib_loc = db.loc_rib(None); for prefix in notification.changed.iter() { - sync_prefix(tep, &rib_loc, prefix, dpd, ddm, sw, log, &rt)? + sync_prefix(tep, &rib_loc, prefix, dpd, ddm, sw, db, log, &rt)? } Ok(()) @@ -206,6 +206,7 @@ pub(crate) fn sync_prefix( dpd: &impl Dpd, ddm: &impl Ddm, sw: &impl SwitchZone, + db: &Db, log: &Logger, rt: &Arc, ) -> Result<(), Error> { @@ -225,7 +226,12 @@ pub(crate) fn sync_prefix( let mut best: HashSet = HashSet::new(); if let Some(paths) = rib_loc.get(prefix) { for path in paths { - best.insert(RouteHash::for_prefix_path(sw, *prefix, path.clone())?); + best.insert(RouteHash::for_prefix_path( + sw, + *prefix, + path.clone(), + db, + )?); } } diff --git a/mg-lower/src/platform.rs b/mg-lower/src/platform.rs index 650740c7..41c64767 100644 --- a/mg-lower/src/platform.rs +++ b/mg-lower/src/platform.rs @@ -19,7 +19,7 @@ pub(crate) trait Dpd { &self, cidr: &Ipv4Net, ) -> Result< - dpd_client::ResponseValue>, + dpd_client::ResponseValue>, progenitor_client::Error, >; async fn route_ipv6_get( @@ -79,6 +79,11 @@ pub(crate) trait Dpd { body: &'a Ipv4RouteUpdate, ) -> Result, progenitor_client::Error>; + async fn route_ipv4_over_ipv6_add<'a>( + &'a self, + body: &'a Ipv4OverIpv6RouteUpdate, + ) -> Result, progenitor_client::Error>; + async fn route_ipv6_add<'a>( &'a self, body: &'a Ipv6RouteUpdate, @@ -157,7 +162,7 @@ impl Dpd for ProductionDpd { &self, cidr: &Ipv4Net, ) -> Result< - dpd_client::ResponseValue>, + dpd_client::ResponseValue>, progenitor_client::Error, > { self.client.route_ipv4_get(cidr).await @@ -240,6 +245,14 @@ impl Dpd for ProductionDpd { self.client.route_ipv4_add(body).await } + async fn route_ipv4_over_ipv6_add<'a>( + &'a self, + body: &'a Ipv4OverIpv6RouteUpdate, + ) -> Result, progenitor_client::Error> + { + self.client.route_ipv4_over_ipv6_add(body).await + } + async fn route_ipv6_add<'a>( &'a self, body: &'a Ipv6RouteUpdate, @@ -368,7 +381,7 @@ pub(crate) mod test { /// useful for tests. pub(crate) struct TestDpd { pub(crate) links: Mutex>, - pub(crate) v4_routes: Mutex>>, + pub(crate) v4_routes: Mutex>>, pub(crate) v6_routes: Mutex>>, pub(crate) v4_addrs: HashMap>, pub(crate) v6_addrs: HashMap>, @@ -412,7 +425,7 @@ pub(crate) mod test { &self, cidr: &Ipv4Net, ) -> Result< - dpd_client::ResponseValue>, + dpd_client::ResponseValue>, progenitor_client::Error, > { let result = self @@ -528,10 +541,35 @@ pub(crate) mod test { let mut routes = self.v4_routes.lock().unwrap(); match routes.get_mut(&body.cidr) { Some(targets) => { - targets.push(body.target.clone()); + targets.push(Route::V4(body.target.clone())); } None => { - routes.insert(body.cidr, vec![body.target.clone()]); + routes.insert( + body.cidr, + vec![Route::V4(body.target.clone())], + ); + } + } + Ok(dpd_response_ok!(())) + } + + async fn route_ipv4_over_ipv6_add<'a>( + &'a self, + body: &'a Ipv4OverIpv6RouteUpdate, + ) -> Result< + dpd_client::ResponseValue<()>, + progenitor_client::Error, + > { + let mut routes = self.v4_routes.lock().unwrap(); + match routes.get_mut(&body.cidr) { + Some(targets) => { + targets.push(Route::V6(body.target.clone())); + } + None => { + routes.insert( + body.cidr, + vec![Route::V6(body.target.clone())], + ); } } Ok(dpd_response_ok!(())) @@ -569,9 +607,13 @@ pub(crate) mod test { let mut routes = self.v4_routes.lock().unwrap(); if let Some(targets) = routes.get_mut(cidr) { targets.retain(|x| { - !(x.tgt_ip == *tgt_ip - && x.link_id == *link_id - && x.port_id == *port_id) + if let Route::V4(x) = x { + !(x.tgt_ip == *tgt_ip + && x.link_id == *link_id + && x.port_id == *port_id) + } else { + false + } }); } routes.retain(|_, v| !v.is_empty()); diff --git a/mg-lower/src/test.rs b/mg-lower/src/test.rs index 7ccf6f16..610e099e 100644 --- a/mg-lower/src/test.rs +++ b/mg-lower/src/test.rs @@ -4,7 +4,7 @@ use ddm_admin_client::types::TunnelOrigin; use dpd_client::types::{ Ipv4Route, LinkId, LinkState, PortId, PortMedia, PortPrbsMode, PortSpeed, }; -use rdb::{Path, Prefix4, db::Rib}; +use rdb::{Path, Prefix4, db::Rib, test::get_test_db}; use crate::platform::test::{TestDdm, TestDpd, TestSwitchZone}; @@ -42,6 +42,7 @@ async fn sync_prefix_test() { ); let log = util::test::logger(); + let db = get_test_db("sync_prefix_test", log.clone()).expect("new db"); crate::sync_prefix( tep, @@ -50,6 +51,7 @@ async fn sync_prefix_test() { &dpd, &ddm, &sw, + &db, &log, &rt, ) @@ -90,6 +92,11 @@ async fn sync_link_down_test() { let log = util::test::logger(); let mut rib = Rib::default(); + let db = rdb::Db::new( + &format!("/tmp/{}.db", uuid::Uuid::new_v4()), + log.clone(), + ) + .expect("new db"); test_setup(tep, &dpd, &ddm, &mut rib); @@ -101,6 +108,7 @@ async fn sync_link_down_test() { &dpd, &ddm, &sw, + &db, &log, &rt, ) @@ -181,33 +189,33 @@ fn test_setup(tep: Ipv6Addr, dpd: &TestDpd, ddm: &TestDdm, rib: &mut Rib) { // Add three initial prefixes to dpd dpd.v4_routes.lock().unwrap().insert( "1.0.0.0/24".parse().unwrap(), - vec![Ipv4Route { + vec![dpd_client::types::Route::V4(Ipv4Route { link_id: LinkId(0), port_id: PortId::Qsfp("qsfp0".parse().unwrap()), tag: String::from("mg_lower_test"), tgt_ip: "1.0.0.1".parse().unwrap(), vlan_id: None, - }], + })], ); dpd.v4_routes.lock().unwrap().insert( "2.0.0.0/24".parse().unwrap(), - vec![Ipv4Route { + vec![dpd_client::types::Route::V4(Ipv4Route { link_id: LinkId(0), port_id: PortId::Qsfp("qsfp0".parse().unwrap()), tag: String::from("mg_lower_test"), tgt_ip: "2.0.0.1".parse().unwrap(), vlan_id: None, - }], + })], ); dpd.v4_routes.lock().unwrap().insert( "3.0.0.0/24".parse().unwrap(), - vec![Ipv4Route { + vec![dpd_client::types::Route::V4(Ipv4Route { link_id: LinkId(0), port_id: PortId::Qsfp("qsfp1".parse().unwrap()), tag: String::from("mg_lower_test"), tgt_ip: "3.0.0.1".parse().unwrap(), vlan_id: None, - }], + })], ); // Add three initial prefixes to ddm diff --git a/mgadm/Cargo.toml b/mgadm/Cargo.toml index 7e50fd7f..4fc9037a 100644 --- a/mgadm/Cargo.toml +++ b/mgadm/Cargo.toml @@ -4,9 +4,9 @@ version = "0.1.0" edition = "2024" [dependencies] -bgp = { path = "../bgp", features = ["clap"] } -mg-common = { path = "../mg-common" } -mg-admin-client = { path = "../mg-admin-client" } +bgp = { workspace = true, features = ["clap"] } +mg-common.workspace = true +mg-admin-client.workspace = true clap.workspace = true anyhow.workspace = true oxide-tokio-rt.workspace = true @@ -19,4 +19,4 @@ humantime.workspace = true serde_json.workspace = true oxnet.workspace = true tabwriter.workspace = true -rdb = { version = "0.1.0", path = "../rdb", features = ["clap"] } +rdb.workspace = true diff --git a/mgadm/src/bgp.rs b/mgadm/src/bgp.rs index 1cb7c4e1..d2490e1f 100644 --- a/mgadm/src/bgp.rs +++ b/mgadm/src/bgp.rs @@ -32,6 +32,7 @@ use std::{ fs::read_to_string, io::{Write, stdout}, net::{IpAddr, Ipv4Addr, SocketAddr}, + ops::{Deref, DerefMut}, time::Duration, }; use tabwriter::TabWriter; @@ -102,6 +103,11 @@ pub enum StatusCmd { mode: NeighborDisplayMode, }, + PendingNeighbors { + #[clap(env)] + asn: u32, + }, + /// Get the prefixes exported by a BGP router. Exported { #[clap(env)] @@ -291,10 +297,18 @@ pub enum NeighborCmd { #[clap(env)] asn: u32, }, + /// List the unnumbered neighbors of a given router. + ListUnnumbered { + #[clap(env)] + asn: u32, + }, /// Create a neighbor configuration. Create(Neighbor), + /// Create an unnumbered neighbor configuration. + CreateUnnumbered(UnnumberedNeighbor), + /// Read a neighbor configuration. Read { addr: IpAddr, @@ -302,15 +316,32 @@ pub enum NeighborCmd { asn: u32, }, + /// Read an unnumbered neighbor configuration. + ReadUnnumbered { + interface: String, + #[clap(env)] + asn: u32, + }, + /// Update a neighbor's configuration. Update(Neighbor), + /// Update an unnumbered neighbor's configuration. + UpdateUnnumbered(UnnumberedNeighbor), + /// Delete a neighbor configuration Delete { addr: IpAddr, #[clap(env)] asn: u32, }, + + /// Delete an unnumbered neighbor configuration + DeleteUnnumbered { + interface: String, + #[clap(env)] + asn: u32, + }, } #[derive(Args, Debug)] @@ -532,12 +563,57 @@ pub struct Withdraw4 { #[derive(Args, Debug)] pub struct Neighbor { - /// Name for this neighbor - name: String, - /// Neighbor address addr: IpAddr, + #[command(flatten)] + common: NeighborCommon, +} + +impl Deref for Neighbor { + type Target = NeighborCommon; + fn deref(&self) -> &Self::Target { + &self.common + } +} + +impl DerefMut for Neighbor { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.common + } +} + +#[derive(Args, Debug)] +pub struct UnnumberedNeighbor { + /// Neighbor address + interface: String, + + /// Some routers such as those from Arista require that we act as a default + /// router in order to peer over BGP unnumbered. + act_as_a_default_ipv6_router: u16, + + #[command(flatten)] + common: NeighborCommon, +} + +impl Deref for UnnumberedNeighbor { + type Target = NeighborCommon; + fn deref(&self) -> &Self::Target { + &self.common + } +} + +impl DerefMut for UnnumberedNeighbor { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.common + } +} + +#[derive(Args, Debug)] +pub struct NeighborCommon { + /// Name for this neighbor + name: String, + /// Peer group to add the neighbor to. group: String, @@ -658,13 +734,13 @@ impl From for types::Neighbor { fn from(n: Neighbor) -> types::Neighbor { // Build IPv4 unicast config if enabled let ipv4_unicast = if n.enable_ipv4 { - let import_policy = match n.allow_import4 { + let import_policy = match n.allow_import4.clone() { Some(prefixes) => { ImportExportPolicy4::Allow(prefixes.into_iter().collect()) } None => ImportExportPolicy4::NoFiltering, }; - let export_policy = match n.allow_export4 { + let export_policy = match n.allow_export4.clone() { Some(prefixes) => { ImportExportPolicy4::Allow(prefixes.into_iter().collect()) } @@ -681,13 +757,13 @@ impl From for types::Neighbor { // Build IPv6 unicast config if enabled let ipv6_unicast = if n.enable_ipv6 { - let import_policy = match n.allow_import6 { + let import_policy = match n.allow_import6.clone() { Some(prefixes) => { ImportExportPolicy6::Allow(prefixes.into_iter().collect()) } None => ImportExportPolicy6::NoFiltering, }; - let export_policy = match n.allow_export6 { + let export_policy = match n.allow_export6.clone() { Some(prefixes) => { ImportExportPolicy6::Allow(prefixes.into_iter().collect()) } @@ -706,7 +782,7 @@ impl From for types::Neighbor { asn: n.asn, remote_asn: n.remote_asn, min_ttl: n.min_ttl, - name: n.name, + name: n.name.clone(), host: SocketAddr::new(n.addr, n.port).to_string(), hold_time: n.hold_time, idle_hold_time: n.idle_hold_time, @@ -714,11 +790,90 @@ impl From for types::Neighbor { keepalive: n.keepalive_time, delay_open: n.delay_open_time, resolution: n.clock_resolution, - group: n.group, + group: n.group.clone(), passive: n.passive_connection, md5_auth_key: n.md5_auth_key.clone(), multi_exit_discriminator: n.med, - communities: n.communities, + communities: n.communities.clone(), + local_pref: n.local_pref, + enforce_first_as: n.enforce_first_as, + ipv4_unicast, + ipv6_unicast, + vlan_id: n.vlan_id, + connect_retry_jitter: n + .connect_retry_jitter + .map(jitter_range_to_api), + idle_hold_jitter: n.idle_hold_jitter.map(jitter_range_to_api), + deterministic_collision_resolution: n + .deterministic_collision_resolution, + } + } +} + +impl From for types::UnnumberedNeighbor { + fn from(n: UnnumberedNeighbor) -> types::UnnumberedNeighbor { + let ipv4_unicast = if n.enable_ipv4 { + let import_policy = match n.allow_import4.clone() { + Some(prefixes) => { + ImportExportPolicy4::Allow(prefixes.into_iter().collect()) + } + None => ImportExportPolicy4::NoFiltering, + }; + let export_policy = match n.allow_export4.clone() { + Some(prefixes) => { + ImportExportPolicy4::Allow(prefixes.into_iter().collect()) + } + None => ImportExportPolicy4::NoFiltering, + }; + Some(Ipv4UnicastConfig { + nexthop: n.nexthop4, + import_policy, + export_policy, + }) + } else { + None + }; + + // Build IPv6 unicast config if enabled + let ipv6_unicast = if n.enable_ipv6 { + let import_policy = match n.allow_import6.clone() { + Some(prefixes) => { + ImportExportPolicy6::Allow(prefixes.into_iter().collect()) + } + None => ImportExportPolicy6::NoFiltering, + }; + let export_policy = match n.allow_export6.clone() { + Some(prefixes) => { + ImportExportPolicy6::Allow(prefixes.into_iter().collect()) + } + None => ImportExportPolicy6::NoFiltering, + }; + Some(Ipv6UnicastConfig { + nexthop: n.nexthop6, + import_policy, + export_policy, + }) + } else { + None + }; + types::UnnumberedNeighbor { + asn: n.asn, + remote_asn: n.remote_asn, + min_ttl: n.min_ttl, + act_as_a_default_ipv6_router: n.act_as_a_default_ipv6_router, + name: n.name.clone(), + interface: n.interface.clone(), + hold_time: n.hold_time, + idle_hold_time: n.idle_hold_time, + connect_retry: n.connect_retry_time, + keepalive: n.keepalive_time, + delay_open: n.delay_open_time, + resolution: n.clock_resolution, + group: n.group.clone(), + passive: n.passive_connection, + md5_auth_key: n.md5_auth_key.clone(), + multi_exit_discriminator: n.med, + communities: n.communities.clone(), local_pref: n.local_pref, enforce_first_as: n.enforce_first_as, ipv4_unicast, @@ -755,6 +910,21 @@ pub async fn commands(command: Commands, c: Client) -> Result<()> { NeighborCmd::Delete { asn, addr } => { delete_nbr(asn, addr, c).await? } + NeighborCmd::ListUnnumbered { asn } => { + list_unnumbered_nbr(asn, c).await? + } + NeighborCmd::CreateUnnumbered(nbr) => { + create_unnumbered_nbr(nbr, c).await? + } + NeighborCmd::ReadUnnumbered { asn, interface } => { + read_unnumbered_nbr(asn, interface, c).await? + } + NeighborCmd::UpdateUnnumbered(nbr) => { + update_unnumbered_nbr(nbr, c).await? + } + NeighborCmd::DeleteUnnumbered { asn, interface } => { + delete_unnumbered_nbr(asn, interface, c).await? + } }, ConfigCmd::Origin(cmd) => match cmd.command { @@ -812,6 +982,9 @@ pub async fn commands(command: Commands, c: Client) -> Result<()> { StatusCmd::Neighbors { asn, mode } => { get_neighbors(c, asn, mode).await? } + StatusCmd::PendingNeighbors { asn } => { + list_unnumbered_pending(asn, c).await? + } StatusCmd::Exported { asn } => get_exported(c, asn).await?, }, @@ -1175,6 +1348,56 @@ async fn delete_nbr(asn: u32, addr: IpAddr, c: Client) -> Result<()> { Ok(()) } +async fn list_unnumbered_pending(asn: u32, c: Client) -> Result<()> { + let pending = c.read_pending_unnumbered_neighbors(asn).await?; + println!("{pending:#?}"); + Ok(()) +} + +async fn list_unnumbered_nbr(asn: u32, c: Client) -> Result<()> { + let nbrs = c.read_unnumbered_neighbors(asn).await?; + println!("{nbrs:#?}"); + Ok(()) +} + +async fn create_unnumbered_nbr( + nbr: UnnumberedNeighbor, + c: Client, +) -> Result<()> { + c.create_unnumbered_neighbor(&nbr.into()).await?; + Ok(()) +} + +async fn read_unnumbered_nbr( + asn: u32, + interface: String, + c: Client, +) -> Result<()> { + let nbr = c + .read_unnumbered_neighbor(asn, &interface) + .await? + .into_inner(); + println!("{nbr:#?}"); + Ok(()) +} + +async fn update_unnumbered_nbr( + nbr: UnnumberedNeighbor, + c: Client, +) -> Result<()> { + c.update_unnumbered_neighbor(&nbr.into()).await?; + Ok(()) +} + +async fn delete_unnumbered_nbr( + asn: u32, + interface: String, + c: Client, +) -> Result<()> { + c.delete_unnumbered_neighbor(asn, &interface).await?; + Ok(()) +} + async fn clear_nbr( asn: u32, addr: IpAddr, diff --git a/mgd/Cargo.toml b/mgd/Cargo.toml index ddda43e3..e2d35c87 100644 --- a/mgd/Cargo.toml +++ b/mgd/Cargo.toml @@ -4,12 +4,12 @@ version = "0.1.0" edition = "2024" [dependencies] -mg-api.workspace = true mg-lower = { path = "../mg-lower", optional = true } -mg-common = { path = "../mg-common", default-features = false} -bfd = { path = "../bfd" } -bgp = { path = "../bgp" } -rdb = { path = "../rdb" } +mg-api.workspace = true +mg-common.workspace = true +bfd.workspace = true +bgp.workspace = true +rdb.workspace = true anyhow.workspace = true clap.workspace = true slog.workspace = true @@ -27,6 +27,8 @@ hostname.workspace = true uuid.workspace = true smf.workspace = true gateway-client.workspace = true +ndp.workspace = true +network-interface.workspace = true [dev-dependencies] tempfile = "3" diff --git a/mgd/src/admin.rs b/mgd/src/admin.rs index 3320076d..58371017 100644 --- a/mgd/src/admin.rs +++ b/mgd/src/admin.rs @@ -149,6 +149,8 @@ impl MgAdminApi for MgAdminApiImpl { bgp_admin::read_neighbors(ctx, request).await } + // Neighbors ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + async fn create_neighbor( ctx: RequestContext, request: TypedBody, @@ -226,6 +228,59 @@ impl MgAdminApi for MgAdminApiImpl { bgp_admin::clear_neighbor_v2(ctx, request).await } + // Unnumbered neighbors ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + async fn read_pending_unnumbered_neighbors( + rqctx: RequestContext, + request: Query, + ) -> Result>, HttpError> { + bgp_admin::read_pending_unnumbered_neighbors(rqctx, request).await + } + + async fn read_unnumbered_neighbors( + rqctx: RequestContext, + request: Query, + ) -> Result>, HttpError> { + bgp_admin::read_unnumbered_neighbors(rqctx, request).await + } + + async fn create_unnumbered_neighbor( + rqctx: RequestContext, + request: TypedBody, + ) -> Result { + bgp_admin::create_unnumbered_neighbor(rqctx, request).await + } + + async fn read_unnumbered_neighbor( + rqctx: RequestContext, + request: Query, + ) -> Result, HttpError> { + bgp_admin::read_unnumbered_neighbor(rqctx, request).await + } + + async fn update_unnumbered_neighbor( + rqctx: RequestContext, + request: TypedBody, + ) -> Result { + bgp_admin::update_unnumbered_neighbor(rqctx, request).await + } + + async fn delete_unnumbered_neighbor( + rqctx: RequestContext, + request: Query, + ) -> Result { + bgp_admin::delete_unnumbered_neighbor(rqctx, request).await + } + + async fn clear_unnumbered_neighbor( + rqctx: RequestContext, + request: TypedBody, + ) -> Result { + bgp_admin::clear_unnumbered_neighbor(rqctx, request).await + } + + // IPv4 origin ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + async fn create_origin4( ctx: RequestContext, request: TypedBody, diff --git a/mgd/src/bgp_admin.rs b/mgd/src/bgp_admin.rs index 166e6bbe..2b4cdb1c 100644 --- a/mgd/src/bgp_admin.rs +++ b/mgd/src/bgp_admin.rs @@ -3,6 +3,7 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. #![allow(clippy::type_complexity)] +use crate::unnumbered_manager::UnnumberedNeighborManager; use crate::validation::{validate_prefixes_v4, validate_prefixes_v6}; use crate::{admin::HandlerContext, error::Error, log::bgp_log}; use bgp::{ @@ -27,11 +28,15 @@ use mg_api::{ FsmHistoryRequest, FsmHistoryResponse, MessageDirection, MessageHistoryRequest, MessageHistoryRequestV1, MessageHistoryResponse, MessageHistoryResponseV1, NeighborResetRequest, NeighborResetRequestV1, - NeighborSelector, Rib, + NeighborSelector, Rib, UnnumberedNeighborResetRequest, + UnnumberedNeighborSelector, }; use mg_common::lock; -use rdb::{AddressFamily, Asn, BgpRouterInfo, ImportExportPolicyV1, Prefix}; -use rdb::{ImportExportPolicy4, ImportExportPolicy6}; +use rdb::{ + AddressFamily, Asn, BgpRouterInfo, ImportExportPolicy4, + ImportExportPolicy6, ImportExportPolicyV1, Prefix, +}; +use slog::Logger; use std::collections::{BTreeMap, HashMap, HashSet}; use std::hash::{Hash, Hasher}; use std::net::{IpAddr, Ipv6Addr, SocketAddr, SocketAddrV6}; @@ -39,7 +44,6 @@ use std::sync::{ Arc, Mutex, mpsc::{Sender, channel}, }; -use std::time::Duration; const UNIT_BGP: &str = "bgp"; const DEFAULT_BGP_LISTEN: SocketAddr = @@ -50,6 +54,7 @@ pub struct BgpContext { pub(crate) router: Arc>>>>, addr_to_session: Arc>>>, + pub(crate) unnumbered_manager: Arc, } impl BgpContext { @@ -57,10 +62,16 @@ impl BgpContext { addr_to_session: Arc< Mutex>>, >, + db: rdb::Db, + log: Logger, ) -> Self { + let router = Arc::new(Mutex::new(BTreeMap::new())); + let unnumbered_manager = + UnnumberedNeighborManager::new(router.clone(), db, log); Self { - router: Arc::new(Mutex::new(BTreeMap::new())), + router, addr_to_session, + unnumbered_manager, } } } @@ -166,6 +177,29 @@ pub async fn delete_router( Ok(HttpResponseUpdatedNoContent()) } +// Neighbors ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +pub async fn read_neighbors_v2( + ctx: RequestContext>, + request: Query, +) -> Result>, HttpError> { + let rq = request.into_inner(); + let ctx = ctx.context(); + + let nbrs = ctx + .db + .get_bgp_neighbors() + .map_err(|e| HttpError::for_internal_error(e.to_string()))?; + + let result = nbrs + .into_iter() + .filter(|x| x.asn == rq.asn) + .map(|x| Neighbor::from_rdb_neighbor_info(rq.asn, &x)) + .collect(); + + Ok(HttpResponseOk(result)) +} + pub async fn read_neighbors( ctx: RequestContext>, request: Query, @@ -257,28 +291,6 @@ pub async fn clear_neighbor_v2( Ok(helpers::reset_neighbor(ctx.clone(), rq).await?) } -// V3 API handlers (new Neighbor type with optional per-AF configs) -pub async fn read_neighbors_v2( - ctx: RequestContext>, - request: Query, -) -> Result>, HttpError> { - let rq = request.into_inner(); - let ctx = ctx.context(); - - let nbrs = ctx - .db - .get_bgp_neighbors() - .map_err(|e| HttpError::for_internal_error(e.to_string()))?; - - let result = nbrs - .into_iter() - .filter(|x| x.asn == rq.asn) - .map(|x| Neighbor::from_rdb_neighbor_info(rq.asn, &x)) - .collect(); - - Ok(HttpResponseOk(result)) -} - pub async fn create_neighbor_v2( ctx: RequestContext>, request: TypedBody, @@ -328,6 +340,126 @@ pub async fn delete_neighbor_v2( Ok(helpers::remove_neighbor(ctx.clone(), rq.asn, rq.addr).await?) } +// Unnumbered neighbors ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +pub async fn read_pending_unnumbered_neighbors( + rqctx: RequestContext>, + request: Query, +) -> Result>, HttpError> { + let rq = request.into_inner(); + let ctx = rqctx.context(); + + let pending = ctx.bgp.unnumbered_manager.get_pending(); + let mut result = Vec::default(); + + for k in pending.keys() { + if k.asn != rq.asn { + continue; + } + result.push(PendingUnnumberedNeighbor { + interface: k.interface.name.clone(), + local_addr: k.interface.ip, + }); + } + + Ok(HttpResponseOk(result)) +} + +pub async fn read_unnumbered_neighbors( + rqctx: RequestContext>, + request: Query, +) -> Result>, HttpError> { + let rq = request.into_inner(); + let ctx = rqctx.context(); + + let nbrs = ctx + .db + .get_unnumbered_bgp_neighbors() + .map_err(|e| HttpError::for_internal_error(e.to_string()))?; + + let result = nbrs + .into_iter() + .filter(|x| x.asn == rq.asn) + .map(|x| UnnumberedNeighbor::from_rdb_neighbor_info(rq.asn, &x)) + .collect(); + + Ok(HttpResponseOk(result)) +} + +pub async fn create_unnumbered_neighbor( + rqctx: RequestContext>, + request: TypedBody, +) -> Result { + let rq = request.into_inner(); + let ctx = rqctx.context(); + helpers::add_unnumbered_neighbor(ctx.clone(), rq)?; + Ok(HttpResponseUpdatedNoContent()) +} + +pub async fn read_unnumbered_neighbor( + rqctx: RequestContext>, + request: Query, +) -> Result, HttpError> { + let rq = request.into_inner(); + let db_neighbors = rqctx + .context() + .db + .get_unnumbered_bgp_neighbors() + .map_err(|e| { + HttpError::for_internal_error(format!("get neighbors kv tree: {e}")) + })?; + let neighbor_info = db_neighbors + .iter() + .find(|n| n.interface == rq.interface) + .ok_or(HttpError::for_not_found( + None, + format!("neighbor {} not found in db", rq.interface), + ))?; + + let result = + UnnumberedNeighbor::from_rdb_neighbor_info(rq.asn, neighbor_info); + Ok(HttpResponseOk(result)) +} + +pub async fn update_unnumbered_neighbor( + rqctx: RequestContext>, + request: TypedBody, +) -> Result { + let rq = request.into_inner(); + let ctx = rqctx.context(); + helpers::add_unnumbered_neighbor(ctx.clone(), rq)?; + Ok(HttpResponseUpdatedNoContent()) +} + +pub async fn delete_unnumbered_neighbor( + rqctx: RequestContext>, + request: Query, +) -> Result { + let rq = request.into_inner(); + let ctx = rqctx.context(); + Ok( + helpers::remove_unnumbered_neighbor(ctx.clone(), rq.asn, &rq.interface) + .await?, + ) +} + +pub async fn clear_unnumbered_neighbor( + rqctx: RequestContext>, + request: TypedBody, +) -> Result { + let rq = request.into_inner(); + let ctx = rqctx.context(); + Ok(helpers::reset_unnumbered_neighbor( + ctx.clone(), + rq.asn, + &rq.interface, + rq.op, + ) + .await?) +} + +// IPv4 origin ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + pub async fn create_origin4( ctx: RequestContext>, request: TypedBody, @@ -503,8 +635,8 @@ pub async fn get_exported( // Combine per-AF export policies into legacy format for filtering let allow_export = ImportExportPolicyV1::from_per_af_policies( - &n.allow_export4, - &n.allow_export6, + &n.parameters.allow_export4, + &n.parameters.allow_export6, ); let mut exported_routes: Vec = match allow_export { ImportExportPolicyV1::NoFiltering => orig_routes, @@ -739,6 +871,24 @@ async fn do_bgp_apply( } } + #[derive(Debug, Eq)] + struct Unbr { + interface: String, + asn: u32, + } + + impl Hash for Unbr { + fn hash(&self, state: &mut H) { + self.interface.hash(state); + } + } + + impl PartialEq for Unbr { + fn eq(&self, other: &Unbr) -> bool { + self.interface.eq(&other.interface) + } + } + let groups = ctx .db .get_bgp_neighbors() @@ -756,6 +906,120 @@ async fn do_bgp_apply( peers.insert(g.clone(), Vec::default()); } } + let mut upeers = rq.unnumbered_peers.clone(); + for g in &groups { + if !upeers.contains_key(g) { + upeers.insert(g.clone(), Vec::default()); + } + } + + helpers::ensure_router( + ctx.clone(), + bgp::params::Router { + asn: rq.asn, + id: rq.asn, + listen: DEFAULT_BGP_LISTEN.to_string(), //TODO as parameter + graceful_shutdown: false, // TODO as parameter + }, + ) + .await?; + + for (group, peers) in &upeers { + let current: Vec = ctx + .db + .get_unnumbered_bgp_neighbors() + .map_err(Error::Db)? + .into_iter() + .filter(|x| &x.group == group) + .collect(); + + let current_unbr_ifxs: HashSet = current + .iter() + .map(|x| Unbr { + interface: x.interface.clone(), + asn: x.asn, + }) + .collect(); + + let specified_unbr_ifxs: HashSet = peers + .iter() + .map(|x| Unbr { + interface: x.interface.clone(), + asn: rq.asn, + }) + .collect(); + + let to_delete = current_unbr_ifxs.difference(&specified_unbr_ifxs); + let to_add = specified_unbr_ifxs.difference(¤t_unbr_ifxs); + let to_modify = current_unbr_ifxs.intersection(&specified_unbr_ifxs); + + bgp_log!(log, info, "unbr: current {current:#?}"); + bgp_log!(log, info, "unbr: adding {to_add:#?}"); + bgp_log!(log, info, "unbr: removing {to_delete:#?}"); + + let mut nbr_config = Vec::new(); + for nbr in to_add { + let cfg = peers + .iter() + .find(|x| x.interface == nbr.interface) + .ok_or(Error::NotFound(nbr.interface.clone()))?; + nbr_config.push((nbr, cfg)); + } + + for nbr in to_modify { + let spec = peers + .iter() + .find(|x| x.interface == nbr.interface) + .ok_or(Error::NotFound(nbr.interface.clone()))?; + + let tgt = UnnumberedNeighbor::from_bgp_peer_config( + nbr.asn, + group.clone(), + spec.clone(), + ); + + let curr = UnnumberedNeighbor::from_rdb_neighbor_info( + nbr.asn, + current + .iter() + .find(|x| x.interface == nbr.interface) + .ok_or(Error::NotFound(nbr.interface.clone()))?, + ); + + if tgt != curr { + nbr_config.push((nbr, spec)); + } + } + + for (nbr, cfg) in nbr_config { + helpers::add_unnumbered_neighbor( + ctx.clone(), + UnnumberedNeighbor::from_bgp_peer_config( + nbr.asn, + group.clone(), + cfg.clone(), + ), + )?; + } + + for nbr in to_delete { + helpers::remove_unnumbered_neighbor( + ctx.clone(), + nbr.asn, + &nbr.interface, + ) + .await?; + + let mut routers = lock!(ctx.bgp.router); + let mut remove = false; + if let Some(r) = routers.get(&nbr.asn) { + remove = lock!(r.sessions).is_empty(); + } + if remove && let Some(r) = routers.remove(&nbr.asn) { + r.shutdown() + }; + } + } for (group, peers) in &peers { let current: Vec = ctx @@ -827,17 +1091,6 @@ async fn do_bgp_apply( // TODO all the db modification that happens below needs to happen in a // transaction. - helpers::ensure_router( - ctx.clone(), - bgp::params::Router { - asn: rq.asn, - id: rq.asn, - listen: DEFAULT_BGP_LISTEN.to_string(), //TODO as parameter - graceful_shutdown: false, // TODO as parameter - }, - ) - .await?; - for (nbr, cfg) in nbr_config { helpers::add_neighbor( ctx.clone(), @@ -1123,6 +1376,7 @@ pub async fn update_bestpath_fanout( pub(crate) mod helpers { use bgp::router::{EnsureSessionResult, UnloadPolicyError}; + use rdb::BgpNeighborParameters; use super::*; @@ -1153,6 +1407,34 @@ pub(crate) mod helpers { Ok(HttpResponseDeleted()) } + pub(crate) async fn remove_unnumbered_neighbor( + ctx: Arc, + asn: u32, + interface: &str, + ) -> Result { + bgp_log!( + ctx.log, + info, + "remove unnumbered neighbor (interface {interface}, asn {asn})" + ); + + // If the unnumbered neighbor has a session, delete it first. + if let Some(addr) = + ctx.bgp.unnumbered_manager.get_neighbor_addr(interface)? + { + get_router!(&ctx, asn)?.delete_session(addr.into()); + } + + // If the neighbor manager is running for this interface, remove + // the neighbor + ctx.bgp.unnumbered_manager.remove_neighbor(asn, interface)?; + + // And now clear out the top level database entry + ctx.db.remove_unnumbered_bgp_neighbor(interface)?; + + Ok(HttpResponseDeleted()) + } + pub(crate) fn add_neighbor_v1( ctx: Arc, rq: NeighborV1, @@ -1166,44 +1448,10 @@ pub(crate) mod helpers { let (event_tx, event_rx) = channel(); // V1 API is IPv4-only; extract only IPv4 policies - let allow_import4 = rq.allow_import.as_ipv4_policy(); - let allow_export4 = rq.allow_export.as_ipv4_policy(); - - // XXX: Do we really want both rq and info? - // SessionInfo and Neighbor types could probably be merged. - let info = SessionInfo { - passive_tcp_establishment: rq.passive, - remote_asn: rq.remote_asn, - min_ttl: rq.min_ttl, - md5_auth_key: rq.md5_auth_key.clone(), - multi_exit_discriminator: rq.multi_exit_discriminator, - communities: rq.communities.clone().into_iter().collect(), - local_pref: rq.local_pref, - enforce_first_as: rq.enforce_first_as, - // V1 API is IPv4-only; IPv6 support didn't exist in legacy API - ipv4_unicast: Some(Ipv4UnicastConfig { - nexthop: None, - import_policy: allow_import4.clone(), - export_policy: allow_export4.clone(), - }), - ipv6_unicast: None, - vlan_id: rq.vlan_id, - remote_id: None, - bind_addr: None, - connect_retry_time: Duration::from_secs(rq.connect_retry), - keepalive_time: Duration::from_secs(rq.keepalive), - hold_time: Duration::from_secs(rq.hold_time), - idle_hold_time: Duration::from_secs(rq.idle_hold_time), - delay_open_time: Duration::from_secs(rq.delay_open), - resolution: Duration::from_millis(rq.resolution), - // insert default values for fields not present in the v1 API - idle_hold_jitter: None, - connect_retry_jitter: Some(JitterRange { - min: 0.75, - max: 1.0, - }), - deterministic_collision_resolution: false, - }; + let allow_import4 = rq.parameters.allow_import.as_ipv4_policy(); + let allow_export4 = rq.parameters.allow_export.as_ipv4_policy(); + + let info = SessionInfo::from(&rq.parameters); let start_session = if ensure { match get_router!(&ctx, rq.asn)?.ensure_session( @@ -1229,34 +1477,38 @@ pub(crate) mod helpers { ctx.db.add_bgp_neighbor(rdb::BgpNeighborInfo { asn: rq.asn, - remote_asn: rq.remote_asn, - min_ttl: rq.min_ttl, name: rq.name.clone(), - host: rq.host, - hold_time: rq.hold_time, - idle_hold_time: rq.idle_hold_time, - delay_open: rq.delay_open, - connect_retry: rq.connect_retry, - keepalive: rq.keepalive, - resolution: rq.resolution, group: rq.group.clone(), - passive: rq.passive, - md5_auth_key: rq.md5_auth_key, - multi_exit_discriminator: rq.multi_exit_discriminator, - communities: rq.communities, - local_pref: rq.local_pref, - enforce_first_as: rq.enforce_first_as, - allow_import4, - allow_export4, - vlan_id: rq.vlan_id, - - // V1 API is IPv4-only and doesn't support nexthop override - ipv4_enabled: true, - ipv6_enabled: false, - allow_import6: ImportExportPolicy6::NoFiltering, - allow_export6: ImportExportPolicy6::NoFiltering, - nexthop4: None, - nexthop6: None, + host: rq.host, + parameters: BgpNeighborParameters { + hold_time: rq.parameters.hold_time, + idle_hold_time: rq.parameters.idle_hold_time, + delay_open: rq.parameters.delay_open, + passive: rq.parameters.passive, + connect_retry: rq.parameters.connect_retry, + keepalive: rq.parameters.keepalive, + resolution: rq.parameters.resolution, + remote_asn: rq.parameters.remote_asn, + min_ttl: rq.parameters.min_ttl, + md5_auth_key: rq.parameters.md5_auth_key, + multi_exit_discriminator: rq + .parameters + .multi_exit_discriminator, + communities: rq.parameters.communities, + local_pref: rq.parameters.local_pref, + enforce_first_as: rq.parameters.enforce_first_as, + allow_import4, + allow_export4, + vlan_id: rq.parameters.vlan_id, + + // V1 API is IPv4-only and doesn't support nexthop override + ipv4_enabled: true, + ipv6_enabled: false, + allow_import6: ImportExportPolicy6::NoFiltering, + allow_export6: ImportExportPolicy6::NoFiltering, + nexthop4: None, + nexthop6: None, + }, })?; if start_session { @@ -1285,32 +1537,7 @@ pub(crate) mod helpers { let (event_tx, event_rx) = channel(); - // Build SessionInfo with optional per-AF config directly from the new Neighbor type - let info = SessionInfo { - passive_tcp_establishment: rq.passive, - remote_asn: rq.remote_asn, - min_ttl: rq.min_ttl, - md5_auth_key: rq.md5_auth_key.clone(), - multi_exit_discriminator: rq.multi_exit_discriminator, - communities: rq.communities.clone().into_iter().collect(), - local_pref: rq.local_pref, - enforce_first_as: rq.enforce_first_as, - ipv4_unicast: rq.ipv4_unicast.clone(), - ipv6_unicast: rq.ipv6_unicast.clone(), - vlan_id: rq.vlan_id, - remote_id: None, - bind_addr: None, - connect_retry_time: Duration::from_secs(rq.connect_retry), - keepalive_time: Duration::from_secs(rq.keepalive), - hold_time: Duration::from_secs(rq.hold_time), - idle_hold_time: Duration::from_secs(rq.idle_hold_time), - delay_open_time: Duration::from_secs(rq.delay_open), - resolution: Duration::from_millis(rq.resolution), - idle_hold_jitter: rq.idle_hold_jitter, - connect_retry_jitter: rq.connect_retry_jitter, - deterministic_collision_resolution: rq - .deterministic_collision_resolution, - }; + let info = SessionInfo::from(&rq.parameters); let start_session = if ensure { match get_router!(&ctx, rq.asn)?.ensure_session( @@ -1335,61 +1562,66 @@ pub(crate) mod helpers { }; // Extract per-AF policies and nexthop for database storage - let (allow_import4, allow_export4, nexthop4) = match &rq.ipv4_unicast { - Some(cfg) => ( - cfg.import_policy.clone(), - cfg.export_policy.clone(), - cfg.nexthop, - ), - None => ( - ImportExportPolicy4::NoFiltering, - ImportExportPolicy4::NoFiltering, - None, - ), - }; + let (allow_import4, allow_export4, nexthop4) = + match &rq.parameters.ipv4_unicast { + Some(cfg) => ( + cfg.import_policy.clone(), + cfg.export_policy.clone(), + cfg.nexthop, + ), + None => ( + ImportExportPolicy4::NoFiltering, + ImportExportPolicy4::NoFiltering, + None, + ), + }; - let (allow_import6, allow_export6, nexthop6) = match &rq.ipv6_unicast { - Some(cfg) => ( - cfg.import_policy.clone(), - cfg.export_policy.clone(), - cfg.nexthop, - ), - None => ( - ImportExportPolicy6::NoFiltering, - ImportExportPolicy6::NoFiltering, - None, - ), - }; + let (allow_import6, allow_export6, nexthop6) = + match &rq.parameters.ipv6_unicast { + Some(cfg) => ( + cfg.import_policy.clone(), + cfg.export_policy.clone(), + cfg.nexthop, + ), + None => ( + ImportExportPolicy6::NoFiltering, + ImportExportPolicy6::NoFiltering, + None, + ), + }; ctx.db.add_bgp_neighbor(rdb::BgpNeighborInfo { asn: rq.asn, - remote_asn: rq.remote_asn, - min_ttl: rq.min_ttl, + group: rq.group.clone(), name: rq.name.clone(), host: rq.host, - hold_time: rq.hold_time, - idle_hold_time: rq.idle_hold_time, - delay_open: rq.delay_open, - connect_retry: rq.connect_retry, - keepalive: rq.keepalive, - resolution: rq.resolution, - group: rq.group.clone(), - passive: rq.passive, - md5_auth_key: rq.md5_auth_key, - multi_exit_discriminator: rq.multi_exit_discriminator, - communities: rq.communities, - local_pref: rq.local_pref, - enforce_first_as: rq.enforce_first_as, - // Derive enablement from whether the AF config is present - ipv4_enabled: rq.ipv4_unicast.is_some(), - ipv6_enabled: rq.ipv6_unicast.is_some(), - allow_import4, - allow_export4, - allow_import6, - allow_export6, - nexthop4, - nexthop6, - vlan_id: rq.vlan_id, + parameters: BgpNeighborParameters { + remote_asn: rq.parameters.remote_asn, + min_ttl: rq.parameters.min_ttl, + hold_time: rq.parameters.hold_time, + idle_hold_time: rq.parameters.idle_hold_time, + delay_open: rq.parameters.delay_open, + connect_retry: rq.parameters.connect_retry, + keepalive: rq.parameters.keepalive, + resolution: rq.parameters.resolution, + passive: rq.parameters.passive, + md5_auth_key: rq.parameters.md5_auth_key, + multi_exit_discriminator: rq + .parameters + .multi_exit_discriminator, + communities: rq.parameters.communities, + local_pref: rq.parameters.local_pref, + enforce_first_as: rq.parameters.enforce_first_as, + allow_import4, + allow_import6, + allow_export4, + allow_export6, + ipv4_enabled: rq.parameters.ipv4_unicast.is_some(), + ipv6_enabled: rq.parameters.ipv6_unicast.is_some(), + nexthop4, + nexthop6, + vlan_id: rq.parameters.vlan_id, + }, })?; if start_session { @@ -1399,6 +1631,92 @@ pub(crate) mod helpers { Ok(()) } + pub(crate) fn add_unnumbered_neighbor( + ctx: Arc, + rq: UnnumberedNeighbor, + ) -> Result<(), Error> { + let log = &ctx.log; + bgp_log!(log, info, "add unnumbered neighbor {}", rq.interface; + "params" => format!("{rq:#?}") + ); + + let info = SessionInfo::from(&rq.parameters); + + // Extract per-AF policies and nexthop for database storage + let (allow_import4, allow_export4, nexthop4) = + match &rq.parameters.ipv4_unicast { + Some(cfg) => ( + cfg.import_policy.clone(), + cfg.export_policy.clone(), + cfg.nexthop, + ), + None => ( + ImportExportPolicy4::NoFiltering, + ImportExportPolicy4::NoFiltering, + None, + ), + }; + + let (allow_import6, allow_export6, nexthop6) = + match &rq.parameters.ipv6_unicast { + Some(cfg) => ( + cfg.import_policy.clone(), + cfg.export_policy.clone(), + cfg.nexthop, + ), + None => ( + ImportExportPolicy6::NoFiltering, + ImportExportPolicy6::NoFiltering, + None, + ), + }; + + ctx.db + .add_unnumbered_bgp_neighbor(rdb::BgpUnnumberedNeighborInfo { + asn: rq.asn, + name: rq.name.clone(), + group: rq.group.clone(), + interface: rq.interface.clone(), + router_lifetime: rq.act_as_a_default_ipv6_router, + parameters: BgpNeighborParameters { + remote_asn: rq.parameters.remote_asn, + min_ttl: rq.parameters.min_ttl, + hold_time: rq.parameters.hold_time, + idle_hold_time: rq.parameters.idle_hold_time, + delay_open: rq.parameters.delay_open, + connect_retry: rq.parameters.connect_retry, + keepalive: rq.parameters.keepalive, + resolution: rq.parameters.resolution, + passive: rq.parameters.passive, + md5_auth_key: rq.parameters.md5_auth_key.clone(), + multi_exit_discriminator: rq + .parameters + .multi_exit_discriminator, + communities: rq.parameters.communities.clone(), + local_pref: rq.parameters.local_pref, + enforce_first_as: rq.parameters.enforce_first_as, + allow_import4, + allow_import6, + allow_export4, + allow_export6, + ipv4_enabled: rq.parameters.ipv4_unicast.is_some(), + ipv6_enabled: rq.parameters.ipv6_unicast.is_some(), + nexthop4, + nexthop6, + vlan_id: rq.parameters.vlan_id, + }, + })?; + + ctx.bgp.unnumbered_manager.add_neighbor( + rq.asn, + &rq.interface, + info, + rq.clone(), + )?; + + Ok(()) + } + pub(crate) async fn reset_neighbor( ctx: Arc, rq: NeighborResetRequest, @@ -1483,6 +1801,34 @@ pub(crate) mod helpers { } } } + Ok(HttpResponseUpdatedNoContent()) + } + + pub(crate) async fn reset_unnumbered_neighbor( + ctx: Arc, + asn: u32, + interface: &str, + op: NeighborResetOp, + ) -> Result { + bgp_log!(ctx.log, info, "clear unnumbered neighbor {interface}, asn {asn}"; + "op" => format!("{op:?}") + ); + + if let Some(session) = ctx + .bgp + .unnumbered_manager + .get_neighbor_session(asn, interface)? + { + reset_neighbor( + ctx, + NeighborResetRequest { + asn, + addr: session.neighbor.host.ip(), + op, + }, + ) + .await?; + } Ok(HttpResponseUpdatedNoContent()) } @@ -1645,9 +1991,9 @@ mod tests { use crate::{ admin::HandlerContext, bfd_admin::BfdContext, bgp_admin::BgpContext, }; - use bgp::params::{ApplyRequestV1, BgpPeerConfigV1}; + use bgp::params::{ApplyRequestV1, BgpPeerConfigV1, BgpPeerParametersV1}; use mg_common::stats::MgLowerStats; - use rdb::Db; + use rdb::test::get_test_db; use std::{ collections::{BTreeMap, HashMap}, env::temp_dir, @@ -1668,16 +2014,20 @@ mod tests { } create_dir_all(&tmpdir).unwrap(); println!("tmpdir is {tmpdir}"); - let dbdir = format!("{tmpdir}/test.db"); let log = mg_common::log::init_file_logger("apply_remove_entire_group.log"); + let db = get_test_db("apply_remove_entire_group", log.clone()).unwrap(); let ctx = Arc::new(HandlerContext { tep: Ipv6Addr::UNSPECIFIED, - bgp: BgpContext::new(Arc::new(Mutex::new(BTreeMap::new()))), + bgp: BgpContext::new( + Arc::new(Mutex::new(BTreeMap::new())), + (*db).clone(), + log.clone(), + ), bfd: BfdContext::new(log.clone()), log: log.clone(), - db: Db::new(dbdir.as_str(), log.clone()).unwrap(), + db: (*db).clone(), mg_lower_stats: Arc::new(MgLowerStats::default()), stats_server_running: Mutex::new(false), oximeter_port: 0, @@ -1689,23 +2039,25 @@ mod tests { vec![BgpPeerConfigV1 { host: SocketAddr::new("203.0.113.1".parse().unwrap(), 179), name: String::from("bob"), - hold_time: 3, - idle_hold_time: 1, - delay_open: 1, - connect_retry: 1, - keepalive: 1, - resolution: 1, - passive: false, - remote_asn: None, - min_ttl: None, - md5_auth_key: None, - multi_exit_discriminator: None, - communities: Vec::default(), - local_pref: None, - enforce_first_as: false, - allow_import: rdb::ImportExportPolicyV1::NoFiltering, - allow_export: rdb::ImportExportPolicyV1::NoFiltering, - vlan_id: None, + parameters: BgpPeerParametersV1 { + hold_time: 3, + idle_hold_time: 1, + delay_open: 1, + connect_retry: 1, + keepalive: 1, + resolution: 1, + passive: false, + remote_asn: None, + min_ttl: None, + md5_auth_key: None, + multi_exit_discriminator: None, + communities: Vec::default(), + local_pref: None, + enforce_first_as: false, + allow_import: rdb::ImportExportPolicyV1::NoFiltering, + allow_export: rdb::ImportExportPolicyV1::NoFiltering, + vlan_id: None, + }, }], ); peers.insert( @@ -1713,23 +2065,25 @@ mod tests { vec![BgpPeerConfigV1 { host: SocketAddr::new("203.0.113.2".parse().unwrap(), 179), name: String::from("alice"), - hold_time: 3, - idle_hold_time: 1, - delay_open: 1, - connect_retry: 1, - keepalive: 1, - resolution: 1, - passive: false, - remote_asn: None, - min_ttl: None, - md5_auth_key: None, - multi_exit_discriminator: None, - communities: Vec::default(), - local_pref: None, - enforce_first_as: false, - allow_import: rdb::ImportExportPolicyV1::NoFiltering, - allow_export: rdb::ImportExportPolicyV1::NoFiltering, - vlan_id: None, + parameters: BgpPeerParametersV1 { + hold_time: 3, + idle_hold_time: 1, + delay_open: 1, + connect_retry: 1, + keepalive: 1, + resolution: 1, + passive: false, + remote_asn: None, + min_ttl: None, + md5_auth_key: None, + multi_exit_discriminator: None, + communities: Vec::default(), + local_pref: None, + enforce_first_as: false, + allow_import: rdb::ImportExportPolicyV1::NoFiltering, + allow_export: rdb::ImportExportPolicyV1::NoFiltering, + vlan_id: None, + }, }], ); diff --git a/mgd/src/error.rs b/mgd/src/error.rs index 4637b57b..e184c980 100644 --- a/mgd/src/error.rs +++ b/mgd/src/error.rs @@ -2,6 +2,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. +use crate::unnumbered_manager::{AddNeighborError, ResolveNeighborError}; use dropshot::{ClientErrorStatusCode, HttpError}; #[derive(thiserror::Error, Debug)] @@ -18,6 +19,12 @@ pub enum Error { #[error("bgp error: {0}")] Bgp(#[from] bgp::error::Error), + #[error("error adding an unnumbered error: {0}")] + AddUnnumberedNeighbor(#[from] AddNeighborError), + + #[error("error resolving a neighbor: {0}")] + ResolveNeighbor(#[from] ResolveNeighborError), + #[error("internal communication error: {0}")] InternalCommunication(String), @@ -43,6 +50,35 @@ impl From for HttpError { } _ => Self::for_internal_error(value.to_string()), }, + Error::AddUnnumberedNeighbor(ref err) => match err { + AddNeighborError::Resolve(e) => match e { + ResolveNeighborError::NoSuchInterface + | ResolveNeighborError::NotIpv6Interface => { + Self::for_client_error_with_status( + Some(err.to_string()), + ClientErrorStatusCode::BAD_REQUEST, + ) + } + ResolveNeighborError::System(e) => { + Self::for_internal_error(e.to_string()) + } + }, + AddNeighborError::NdpManager(e) => { + Self::for_internal_error(e.to_string()) + } + }, + Error::ResolveNeighbor(ref err) => match err { + ResolveNeighborError::NoSuchInterface + | ResolveNeighborError::NotIpv6Interface => { + Self::for_client_error_with_status( + Some(err.to_string()), + ClientErrorStatusCode::BAD_REQUEST, + ) + } + ResolveNeighborError::System(e) => { + Self::for_internal_error(e.to_string()) + } + }, Error::InternalCommunication(_) => { Self::for_internal_error(value.to_string()) } diff --git a/mgd/src/main.rs b/mgd/src/main.rs index 8a687b34..b2224f7e 100644 --- a/mgd/src/main.rs +++ b/mgd/src/main.rs @@ -7,6 +7,7 @@ use crate::bfd_admin::BfdContext; use crate::bgp_admin::BgpContext; use crate::log::dlog; use bgp::connection_tcp::{BgpConnectionTcp, BgpListenerTcp}; +use bgp::params::BgpPeerParametersV1; use clap::{Parser, Subcommand}; use mg_common::cli::oxide_cli_style; use mg_common::lock; @@ -36,6 +37,7 @@ mod rib_admin; mod signal; mod smf; mod static_admin; +mod unnumbered_manager; mod validation; #[derive(Parser, Debug)] @@ -109,9 +111,9 @@ async fn run(args: RunArgs) { .await .expect("set up refresh signal handler"); - let bgp = init_bgp(&args, &log); let db = rdb::Db::new(&format!("{}/rdb", args.data_dir), log.clone()) .expect("open datastore file"); + let bgp = init_bgp(&args, db.clone(), &log); let tep_ula = get_tunnel_endpoint_ula(&db); let bfd = BfdContext::new(log.clone()); @@ -250,7 +252,7 @@ fn detect_switch_slot( rt.spawn(task()); } -fn init_bgp(args: &RunArgs, log: &Logger) -> BgpContext { +fn init_bgp(args: &RunArgs, db: rdb::Db, log: &Logger) -> BgpContext { let addr_to_session = Arc::new(Mutex::new(BTreeMap::new())); if !args.no_bgp_dispatcher { let bgp_dispatcher = @@ -268,7 +270,7 @@ fn init_bgp(args: &RunArgs, log: &Logger) -> BgpContext { .spawn(move || bgp_dispatcher.run::()) .expect("failed to start {listener_str}"); } - BgpContext::new(addr_to_session) + BgpContext::new(addr_to_session, db, log.clone()) } fn start_bgp_routers( @@ -298,33 +300,38 @@ fn start_bgp_routers( context.clone(), bgp::params::NeighborV1 { asn: nbr.asn, - remote_asn: nbr.remote_asn, - min_ttl: nbr.min_ttl, + group: nbr.group.clone(), name: nbr.name.clone(), host: nbr.host, - hold_time: nbr.hold_time, - idle_hold_time: nbr.idle_hold_time, - delay_open: nbr.delay_open, - connect_retry: nbr.connect_retry, - keepalive: nbr.keepalive, - resolution: nbr.resolution, - group: nbr.group.clone(), - passive: nbr.passive, - md5_auth_key: nbr.md5_auth_key.clone(), - multi_exit_discriminator: nbr.multi_exit_discriminator, - communities: nbr.communities.clone(), - local_pref: nbr.local_pref, - enforce_first_as: nbr.enforce_first_as, - // Combine per-AF policies into legacy format for API compatibility - allow_import: rdb::ImportExportPolicyV1::from_per_af_policies( - &nbr.allow_import4, - &nbr.allow_import6, - ), - allow_export: rdb::ImportExportPolicyV1::from_per_af_policies( - &nbr.allow_export4, - &nbr.allow_export6, - ), - vlan_id: nbr.vlan_id, + parameters: BgpPeerParametersV1 { + remote_asn: nbr.parameters.remote_asn, + min_ttl: nbr.parameters.min_ttl, + hold_time: nbr.parameters.hold_time, + idle_hold_time: nbr.parameters.idle_hold_time, + delay_open: nbr.parameters.delay_open, + connect_retry: nbr.parameters.connect_retry, + keepalive: nbr.parameters.keepalive, + resolution: nbr.parameters.resolution, + passive: nbr.parameters.passive, + md5_auth_key: nbr.parameters.md5_auth_key.clone(), + multi_exit_discriminator: nbr + .parameters + .multi_exit_discriminator, + communities: nbr.parameters.communities.clone(), + local_pref: nbr.parameters.local_pref, + enforce_first_as: nbr.parameters.enforce_first_as, + allow_import: + rdb::ImportExportPolicyV1::from_per_af_policies( + &nbr.parameters.allow_import4, + &nbr.parameters.allow_import6, + ), + allow_export: + rdb::ImportExportPolicyV1::from_per_af_policies( + &nbr.parameters.allow_export4, + &nbr.parameters.allow_export6, + ), + vlan_id: nbr.parameters.vlan_id, + }, }, true, ) diff --git a/mgd/src/unnumbered_manager.rs b/mgd/src/unnumbered_manager.rs new file mode 100644 index 00000000..7065949c --- /dev/null +++ b/mgd/src/unnumbered_manager.rs @@ -0,0 +1,281 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use bgp::{ + connection_tcp::BgpConnectionTcp, + params::UnnumberedNeighbor, + router::Router, + session::{SessionInfo, SessionRunner}, +}; +use mg_common::lock; +use ndp::{Ipv6NetworkInterface, NdpManager, NewInterfaceNdpManagerError}; +use network_interface::{NetworkInterface, NetworkInterfaceConfig}; +use rdb::Db; +use slog::{Logger, error, info, o, warn}; +use std::{ + collections::{BTreeMap, HashMap}, + net::{IpAddr, Ipv6Addr, SocketAddrV6}, + sync::{Arc, Mutex, mpsc::channel}, + thread::{sleep, spawn}, + time::Duration, +}; + +pub const MOD_UNNUMBERED_MANAGER: &str = "unnumbered manager"; + +pub struct UnnumberedNeighborManager { + routers: Arc>>>>, + ndp_mgr: Arc, + pending_sessions: Mutex>, + db: Db, + log: Logger, +} + +#[derive(Debug, PartialEq, Eq, Hash, Clone)] +pub struct NbrKey { + pub asn: u32, + pub interface: Ipv6NetworkInterface, +} + +#[derive(Clone)] +pub struct NbrInfo { + pub nbr: UnnumberedNeighbor, + pub session: SessionInfo, +} + +#[derive(Debug, thiserror::Error)] +pub enum ResolveNeighborError { + #[error("No such interface")] + NoSuchInterface, + #[error("Interface has no IPv6 link local address")] + NotIpv6Interface, + #[error("Could not get system interfaces: {0}")] + System(#[from] network_interface::Error), +} + +#[derive(Debug, thiserror::Error)] +pub enum AddNeighborError { + #[error("resolve neighbor error: {0}")] + Resolve(#[from] ResolveNeighborError), + + #[error("add interface error: {0}")] + NdpManager(#[from] NewInterfaceNdpManagerError), +} + +impl UnnumberedNeighborManager { + pub fn new( + routers: Arc>>>>, + db: Db, + log: Logger, + ) -> Arc { + let log = log.new(o!( + "component" => crate::COMPONENT_MGD, + "unit" => crate::UNIT_DAEMON, + "module" => MOD_UNNUMBERED_MANAGER, + )); + + let s = Arc::new(Self { + routers, + pending_sessions: Mutex::new(HashMap::default()), + ndp_mgr: NdpManager::new(log.clone()), + db, + log: log.clone(), + }); + + { + let s = s.clone(); + let log = log.clone(); + // TODO(609) considermanaged threading appraoch + // https://github.com/oxidecomputer/maghemite/issues/609 + spawn(move || s.run(log)); + } + + s + } + + pub fn add_neighbor( + self: &Arc, + asn: u32, + interface: impl AsRef, + info: SessionInfo, + nbr: UnnumberedNeighbor, + ) -> Result<(), AddNeighborError> { + let ifx = Self::get_interface(interface.as_ref(), &self.log)?; + self.ndp_mgr + .add_interface(ifx.clone(), nbr.act_as_a_default_ipv6_router)?; + + lock!(self.pending_sessions).insert( + NbrKey { + asn, + interface: ifx, + }, + NbrInfo { session: info, nbr }, + ); + Ok(()) + } + + pub fn remove_neighbor( + self: &Arc, + asn: u32, + interface: impl AsRef, + ) -> Result<(), ResolveNeighborError> { + let ifx = Self::get_interface(interface.as_ref(), &self.log)?; + self.ndp_mgr.remove_interface(ifx.clone()); + self.db.remove_unnumbered_nexthop_for_interface(&ifx); + lock!(self.pending_sessions).remove(&NbrKey { + asn, + interface: ifx, + }); + + Ok(()) + } + pub fn get_neighbor_addr( + self: &Arc, + interface: impl AsRef, + ) -> Result, ResolveNeighborError> { + let ifx = Self::get_interface(interface.as_ref(), &self.log)?; + Ok(self.ndp_mgr.get_peer(&ifx)) + } + + pub fn get_neighbor_session( + self: &Arc, + asn: u32, + interface: impl AsRef, + ) -> Result< + Option>>, + ResolveNeighborError, + > { + if let Some(addr) = self.get_neighbor_addr(interface)? + && let Some(rtr) = lock!(self.routers).get(&asn) + && let Some(session) = rtr.get_session(addr.into()) + { + return Ok(Some(session)); + }; + Ok(None) + } + + fn get_interface( + name: &str, + log: &Logger, + ) -> Result { + let candidates: Vec<_> = NetworkInterface::show()? + .into_iter() + .filter(|x| x.name == name) + .collect(); + + if candidates.is_empty() { + return Err(ResolveNeighborError::NoSuchInterface); + } + + let mut local: Vec<_> = candidates + .into_iter() + .filter_map(|x| x.addr.map(|addr| (addr, x.index))) + .filter_map(|(addr, idx)| match addr.ip() { + IpAddr::V6(ip) if ip.is_unicast_link_local() => Some((ip, idx)), + _ => None, + }) + .collect(); + + let Some((addr, index)) = local.pop() else { + return Err(ResolveNeighborError::NotIpv6Interface); + }; + + if !local.is_empty() { + warn!( + log, + "more than 1 link local address for interface"; + "using" => addr.to_string(), + "also found" => local + .into_iter() + .map(|x| x.0.to_string()) + .collect::>() + .join(","), + ); + } + + Ok(Ipv6NetworkInterface { + name: name.to_owned(), + ip: addr, + index, + }) + } + + pub fn get_pending(self: &Arc) -> HashMap { + lock!(self.pending_sessions).clone() + } + + fn run(self: Arc, log: Logger) { + info!(log, "unnumbered manager loop starting"); + const RUN_LOOP_INTERVAL: Duration = Duration::from_secs(1); + loop { + // clone the pending list so we don't hold the lock + // + // NOTE: sessions must be a named variable, the RHS of this + // statement cannot go into the for loop directly as that will + // cause the guard to be held across the for loop creating a + // deadlock with start_session on pending_sessions. This is known + // as temporary lifetime extension. + let sessions = self.get_pending(); + + for (key, session) in sessions.into_iter() { + if let Some(peer_addr) = self.ndp_mgr.get_peer(&key.interface) { + self.start_session( + key, + session.session, + session.nbr, + peer_addr, + &log, + ); + } + } + sleep(RUN_LOOP_INTERVAL); + } + } + + fn start_session( + self: &Arc, + key: NbrKey, + session: SessionInfo, + neighbor: UnnumberedNeighbor, + peer_addr: Ipv6Addr, + log: &Logger, + ) { + let router_guard = lock!(self.routers); + let Some(router) = router_guard.get(&key.asn) else { + warn!( + log, + "session configured for asn {}, but no router is running", + key.asn + ); + return; + }; + + let (event_tx, event_rx) = channel(); + + let host = SocketAddrV6::new(peer_addr, 0, 0, key.interface.index); + + if let Err(e) = router.ensure_session( + neighbor.to_peer_config(host), + None, + event_tx.clone(), + event_rx, + session, + ) { + error!( + log, + "error starting unnumbered session"; + "error" => e.to_string(), + "interface" => &neighbor.interface, + ); + return; + } + + drop(router_guard); + + self.db + .add_unnumbered_nexthop(peer_addr, key.interface.clone()); + + // if we are here the session has started, remove it from pending + lock!(self.pending_sessions).remove(&key); + } +} diff --git a/ndp/Cargo.toml b/ndp/Cargo.toml new file mode 100644 index 00000000..d4224cf5 --- /dev/null +++ b/ndp/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "ndp" +version = "0.1.0" +edition = "2024" + +[dependencies] +ispf.workspace = true +serde.workspace = true +oxnet.workspace = true +socket2.workspace = true +slog.workspace = true +internet-checksum.workspace = true +network-interface.workspace = true +thiserror.workspace = true +mg-common.workspace = true +libc.workspace = true diff --git a/ndp/src/lib.rs b/ndp/src/lib.rs new file mode 100644 index 00000000..d39396ea --- /dev/null +++ b/ndp/src/lib.rs @@ -0,0 +1,13 @@ +//! Neighbor discovery protocol support crate + +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/ +// +// Copyright 2026 Oxide Computer Company + +mod manager; +mod packet; +mod util; + +pub use manager::*; diff --git a/ndp/src/manager.rs b/ndp/src/manager.rs new file mode 100644 index 00000000..6742fc4b --- /dev/null +++ b/ndp/src/manager.rs @@ -0,0 +1,304 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/ +// +// Copyright 2026 Oxide Computer Company + +use crate::packet::{Icmp6RouterAdvertisement, Icmp6RouterSolicitation}; +use crate::util::{ + DropSleep, ListeningSocketError, ReceivedAdvertisement, create_socket, + send_ra, send_rs, +}; +use mg_common::{lock, read_lock, write_lock}; +use slog::{Logger, error}; +use socket2::Socket; +use std::mem::MaybeUninit; +use std::net::Ipv6Addr; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Mutex, RwLock}; +use std::thread::{JoinHandle, sleep, spawn}; +use std::time::{Duration, Instant}; + +/// The `NdpManager` performs router discovery for a provided set of interfaces. +/// +/// Use `add_interface` and `remove_interface` to manage discovery interfaces and +/// use `get_peer` to determine if a router peer has been discovered for a given +/// interface. +#[derive(Debug)] +pub struct NdpManager { + /// Individual interface-level NDP managers. + interfaces: RwLock>>, + log: Logger, +} + +impl NdpManager { + /// Create a new NDP manager. + pub fn new(log: Logger) -> Arc { + Arc::new(Self { + interfaces: RwLock::new(Vec::default()), + log, + }) + } + + /// Add an interface to the NDP manager. Discovery starts immediately. + pub fn add_interface( + &self, + ifx: Ipv6NetworkInterface, + router_lifetime: u16, + ) -> Result<(), NewInterfaceNdpManagerError> { + write_lock!(self.interfaces).push(InterfaceNdpManager::new( + ifx, + router_lifetime, + self.log.clone(), + )?); + Ok(()) + } + + /// Remove an interface from the NDP manager. Discovery is stopped when + /// the interface is removed. + pub fn remove_interface(&self, ifx: Ipv6NetworkInterface) -> bool { + let mut ifxs_guard = write_lock!(self.interfaces); + let Some(pos) = ifxs_guard.iter().position(|x| x.inner.ifx == ifx) + else { + return false; + }; + + ifxs_guard.remove(pos); + true + } + + /// Get a router peer, if any, that has been discovered for the given interface. + pub fn get_peer(&self, ifx: &Ipv6NetworkInterface) -> Option { + let ifxs_guard = read_lock!(self.interfaces); + let interface = ifxs_guard.iter().find(|x| &x.inner.ifx == ifx)?; + let nbr_guard = lock!(interface.inner.neighbor_router); + let neighbor_router = nbr_guard.as_ref()?; + + if neighbor_router.expired() { + None + } else { + Some(neighbor_router.sender) + } + } +} + +/// The `InterfaceNdpManager` runs router discovery for an individual interface. +/// +/// Discovery is started on construction and stopped when the interface manager +/// object is dropped. +#[derive(Debug)] +pub struct InterfaceNdpManager { + // TODO(609) considermanaged threading appraoch + // https://github.com/oxidecomputer/maghemite/issues/609 + tx_thread: Option>, + rx_thread: Option>, + inner: InterfaceNdpManagerInner, +} + +#[derive(Debug, Clone)] +struct InterfaceNdpManagerInner { + ifx: Ipv6NetworkInterface, + neighbor_router: Arc>>, + stop: Arc, + router_lifetime: u16, + log: Logger, +} + +#[derive(Debug, thiserror::Error)] +pub enum NewInterfaceNdpManagerError { + #[error("socket create: {0}")] + SocketCreate(ListeningSocketError), + + #[error("socket clone: {0}")] + SocketClone(std::io::Error), +} + +impl InterfaceNdpManager { + /// Create a new interface manager for a given interface. + pub fn new( + ifx: Ipv6NetworkInterface, + router_lifetime: u16, + log: Logger, + ) -> Result, NewInterfaceNdpManagerError> { + let sk = create_socket(ifx.index) + .map_err(NewInterfaceNdpManagerError::SocketCreate)?; + + let inner = InterfaceNdpManagerInner { + ifx, + neighbor_router: Arc::new(Mutex::new(None)), + stop: Arc::new(AtomicBool::new(false)), + router_lifetime, + log, + }; + + let tx_thread = Some({ + let sk = sk + .try_clone() + .map_err(NewInterfaceNdpManagerError::SocketClone)?; + let s = inner.clone(); + spawn(move || s.tx_loop(sk)) + }); + + let rx_thread = Some({ + let sk = sk + .try_clone() + .map_err(NewInterfaceNdpManagerError::SocketClone)?; + let s = inner.clone(); + spawn(move || s.rx_loop(sk)) + }); + + Ok(Arc::new(Self { + rx_thread, + tx_thread, + inner, + })) + } +} + +impl InterfaceNdpManagerInner { + /// Run the interface NDP manager receive loop. Advertisements are used to + /// set the current peer address. Advertisements are sent in response to + /// solicitations. + /// + /// A read timeout of 1 second is used. When the time out hits instead of + /// receiving a advertisement or solicitation packet, the current neighbor + /// (if any) is checked for expiration. + pub fn rx_loop(&self, s: Socket) { + const INTERVAL: Duration = Duration::from_secs(1); + loop { + if self.stop.load(Ordering::SeqCst) { + break; + } + let _ds = DropSleep(INTERVAL); + + let mut buf: [MaybeUninit; 1024] = + [const { MaybeUninit::uninit() }; 1024]; + + match s.recv_from(&mut buf) { + Ok((len, src)) => { + let buf: &[u8] = unsafe { + std::slice::from_raw_parts(buf.as_ptr().cast(), len) + }; + let Some(src) = src.as_socket_ipv6().map(|x| *x.ip()) + else { + continue; + }; + if let Ok(ra) = Icmp6RouterAdvertisement::from_wire(buf) { + self.handle_ra(ra, src); + } + if let Ok(rs) = Icmp6RouterSolicitation::from_wire(buf) { + self.handle_rs(&s, rs, src); + } + } + Err(e) => { + if e.kind() == std::io::ErrorKind::WouldBlock { + self.check_expired(); + continue; + } + error!(self.log, "rx: {e}"); + } + } + } + } + + /// Start the transmit loop, periodically sending out announcements. + pub fn tx_loop(&self, sk: Socket) { + const INTERVAL: Duration = Duration::from_secs(5); + loop { + if self.stop.load(Ordering::SeqCst) { + break; + } + send_ra( + &sk, + self.ifx.ip, + None, + self.ifx.index, + self.router_lifetime, + &self.log, + ); + send_rs(&sk, self.ifx.ip, None, self.ifx.index, &self.log); + sleep(INTERVAL); + } + } + + /// Handle a router advertisement. On reception the neighbor source address + /// is updated as well as the time of reception and the stored advertisement + /// containing the reachable time. + fn handle_ra(&self, ra: Icmp6RouterAdvertisement, src: Ipv6Addr) { + let mut guard = lock!(self.neighbor_router); + let nbr = &mut *guard; + *nbr = Some(ReceivedAdvertisement { + when: Instant::now(), + adv: ra, + sender: src, + }); + } + + /// Handle a router solicitation by sending an announcement to the + /// sender. + fn handle_rs( + &self, + sk: &Socket, + // Don't really care what's in the solicitation for now, just + // care that it parses as a valid RS. + _rs: Icmp6RouterSolicitation, + src: Ipv6Addr, + ) { + send_ra( + sk, + self.ifx.ip, + Some(src), + self.ifx.index, + self.router_lifetime, + &self.log, + ); + } + + /// Check to see if the reachable time for our current peer (if any) + /// is expired. If so, remove the peer. + fn check_expired(&self) { + let mut guard = lock!(self.neighbor_router); + let Some(expired) = guard.as_ref().map(|nbr| nbr.expired()) else { + return; + }; + if expired { + *guard = None; + } + } +} + +/// When an `InterfaceNdpManager` is dropped, the tx and rx loops are stopped. +impl Drop for InterfaceNdpManager { + fn drop(&mut self) { + self.inner.stop.store(true, Ordering::SeqCst); + if let Some(t) = self.rx_thread.take() + && let Err(_) = t.join() + { + error!( + self.inner.log, + "interface ndp mgr join on drop: rx thread panicked"; + "interface" => &self.inner.ifx.name, + ); + } + if let Some(t) = self.tx_thread.take() + && let Err(_) = t.join() + { + error!( + self.inner.log, + "interface ndp mgr join on drop: tx thread panicked"; + "interface" => &self.inner.ifx.name, + ); + } + } +} + +/// Information about a network interface managed by the NDP manager. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Ipv6NetworkInterface { + /// Interface's name + pub name: String, + /// Interface's address + pub ip: Ipv6Addr, + /// Interface's index + pub index: u32, +} diff --git a/ndp/src/packet.rs b/ndp/src/packet.rs new file mode 100644 index 00000000..84aa0964 --- /dev/null +++ b/ndp/src/packet.rs @@ -0,0 +1,151 @@ +//! Neighbor discovery protocol support crate + +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/ +// +// Copyright 2026 Oxide Computer Company + +use serde::{Deserialize, Serialize}; +use std::time::Duration; + +/// ICMP6 router advertisement +/// +/// 0 1 2 3 +/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | Type | Code | Checksum | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | Cur Hop Limit |M|O| Reserved | Router Lifetime | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | Reachable Time | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | Retrans Timer | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// +#[derive(Debug, Serialize, Deserialize, Clone, Copy)] +pub struct Icmp6RouterAdvertisement { + pub typ: u8, + pub code: u8, + pub checksum: u16, + pub hop_limit: u8, + pub flags: u8, + pub lifetime: u16, + pub reachable_time: u32, + pub retrans_timer: u32, +} + +impl Icmp6RouterAdvertisement { + const TYPE: u8 = 134; + const CODE: u8 = 0; + const DEFAULT_HOPLIMIT: u8 = 255; + + pub fn from_wire(buf: &[u8]) -> Result { + let s: Self = ispf::from_bytes_be(buf)?; + if s.typ != Self::TYPE { + return Err(Icmp6RaFromWireError::WrongType(s.typ)); + } + if s.code != Self::CODE { + return Err(Icmp6RaFromWireError::WrongCode(s.code)); + } + Ok(s) + } + + // While RFC 4861 says 0 means unspecified, it's not clear how to interpret + // that from the perspective of a discovery engine. One interpretation may + // be that the reachable time is forever, another may be that reachable time + // is zero. Ten seconds is double our solicit interval, so if we get to a + // place where we don't have RAs inside 10 seconds, something has gone + // sideways. + pub fn effective_reachable_time(&self) -> Duration { + if self.reachable_time == 0 { + Duration::from_secs(10) + } else { + Duration::from_millis(self.reachable_time.into()) + } + } +} + +impl Default for Icmp6RouterAdvertisement { + fn default() -> Self { + Self { + typ: Self::TYPE, + code: Self::CODE, + checksum: 0, + hop_limit: Self::DEFAULT_HOPLIMIT, + flags: 0, + lifetime: 0, //indicates this is not a default router + reachable_time: 0, + retrans_timer: 0, + } + } +} + +#[derive(Debug, thiserror::Error)] +pub enum Icmp6RaFromWireError { + #[error("deserialization error: {0}")] + Ispf(#[from] ispf::Error), + + #[error("wrong type: expected {}, got {0}", Icmp6RouterAdvertisement::TYPE)] + WrongType(u8), + + #[error("wrong code: expected {}, got {0}", Icmp6RouterAdvertisement::CODE)] + WrongCode(u8), +} + +/// ICMP6 router solicitation +/// +/// 0 1 2 3 +/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | Type | Code | Checksum | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | Reserved | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// +#[derive(Debug, Serialize, Deserialize, Clone, Copy)] +pub struct Icmp6RouterSolicitation { + pub typ: u8, + pub code: u8, + pub checksum: u16, + pub reserved: u32, +} + +impl Icmp6RouterSolicitation { + const TYPE: u8 = 133; + const CODE: u8 = 0; + + pub fn from_wire(buf: &[u8]) -> Result { + let s: Self = ispf::from_bytes_be(buf)?; + if s.typ != Self::TYPE { + return Err(Icmp6RsFromWireError::WrongType(s.typ)); + } + if s.code != Self::CODE { + return Err(Icmp6RsFromWireError::WrongCode(s.code)); + } + Ok(s) + } +} + +impl Default for Icmp6RouterSolicitation { + fn default() -> Self { + Self { + typ: Self::TYPE, + code: Self::CODE, + checksum: 0, + reserved: 0, + } + } +} + +#[derive(Debug, thiserror::Error)] +pub enum Icmp6RsFromWireError { + #[error("deserialization error: {0}")] + Ispf(#[from] ispf::Error), + + #[error("wrong type: expected {}, got {0}", Icmp6RouterSolicitation::TYPE)] + WrongType(u8), + + #[error("wrong code: expected {}, got {0}", Icmp6RouterSolicitation::CODE)] + WrongCode(u8), +} diff --git a/ndp/src/util.rs b/ndp/src/util.rs new file mode 100644 index 00000000..be8d52f6 --- /dev/null +++ b/ndp/src/util.rs @@ -0,0 +1,229 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/ +// +// Copyright 2026 Oxide Computer Company + +use crate::packet::{Icmp6RouterAdvertisement, Icmp6RouterSolicitation}; +use libc::{c_int, socklen_t}; +use slog::{Logger, error}; +use socket2::{Domain, Protocol, Socket, Type}; +use std::{ + ffi::c_void, + net::{Ipv6Addr, SocketAddrV6}, + os::fd::AsRawFd, + thread::sleep, + time::{Duration, Instant}, +}; + +pub const ALL_NODES_MCAST: Ipv6Addr = + Ipv6Addr::new(0xff02, 0, 0, 0, 0, 0, 0, 1); + +pub const ALL_ROUTERS_MCAST: Ipv6Addr = + Ipv6Addr::new(0xff02, 0, 0, 0, 0, 0, 0, 2); + +const ICMP6_RA_ULP_LEN: u32 = 16; +const ICMP6_RS_ULP_LEN: u32 = 8; + +#[derive(Debug, Clone)] +pub struct ReceivedAdvertisement { + pub when: Instant, + pub adv: Icmp6RouterAdvertisement, + pub sender: Ipv6Addr, +} + +impl ReceivedAdvertisement { + pub fn expired(&self) -> bool { + self.when.elapsed() > self.adv.effective_reachable_time() + } +} + +#[derive(Debug, thiserror::Error)] +pub enum ListeningSocketError { + #[error("new socket error: {0}")] + NewSocketError(std::io::Error), + + #[error("reuse address error: {0}")] + ReuseAddress(std::io::Error), + + #[error("set multicast interface error: {0}")] + SetMulticastIf(std::io::Error), + + #[error("set multicast hops v6: {0}")] + SetMulticastHopsV6(std::io::Error), + + #[error("bind error: {0}")] + Bind(std::io::Error), + + #[error("set multicast loop error: {0}")] + SetMulticastLoop(std::io::Error), + + #[error("join all-nodes multicast group error: {0}")] + JoinAllNodesMulticast(std::io::Error), + + #[error("join all-routers multicast group error: {0}")] + JoinAllRoutersMulticast(std::io::Error), + + #[error("set read timeout error: {0}")] + SetReadTimeoutError(std::io::Error), + + #[error("failed to set ipv6 min hop count: {0}")] + SetIpv6MinHopCount(std::io::Error), +} + +pub fn send_ra( + s: &Socket, + src: Ipv6Addr, + dst: Option, + ifindex: u32, + router_lifetime: u16, + log: &Logger, +) { + let pkt = Icmp6RouterAdvertisement { + lifetime: router_lifetime, + ..Default::default() + }; + + let mut out = match ispf::to_bytes_be(&pkt) { + Ok(data) => data, + Err(e) => { + error!(log, "send_ra: serialize packet: {e}"); + return; + } + }; + cksum(src, dst, ICMP6_RA_ULP_LEN, &mut out); + + let dst = SocketAddrV6::new( + match dst { + Some(d) => d, + None => ALL_NODES_MCAST, + }, + 0, + 0, + ifindex, + ); + if let Err(e) = s.send_to(&out, &dst.into()) { + error!(log, "send_ra: send: {e}"); + } +} + +pub fn send_rs( + s: &Socket, + src: Ipv6Addr, + dst: Option, + ifindex: u32, + log: &Logger, +) { + let pkt = Icmp6RouterSolicitation::default(); + let mut out = match ispf::to_bytes_be(&pkt) { + Ok(data) => data, + Err(e) => { + error!(log, "send_rs: serialize packet: {e}"); + return; + } + }; + cksum(src, dst, ICMP6_RS_ULP_LEN, &mut out); + + let dst = SocketAddrV6::new( + match dst { + Some(d) => d, + None => ALL_ROUTERS_MCAST, + }, + 0, + 0, + ifindex, + ); + if let Err(e) = s.send_to(&out, &dst.into()) { + error!(log, "send_rs: send: {e}"); + } +} + +pub fn cksum( + src: Ipv6Addr, + dst: Option, + ulp_len: u32, + data: &mut [u8], +) { + // IP Protocol number for ICMP6 + const ICMP6_NEXT_HDR: u8 = 58; + + let mut ck = internet_checksum::Checksum::new(); + ck.add_bytes(&src.octets()); + ck.add_bytes( + &match dst { + Some(d) => d, + None => ALL_NODES_MCAST, + } + .octets(), + ); + ck.add_bytes(&ulp_len.to_be_bytes()); + ck.add_bytes(&[0, 0, 0, ICMP6_NEXT_HDR]); + ck.add_bytes(data); + let sum = ck.checksum(); + + // Checksum is the third octet of the ICMP packet. + data[2] = sum[0]; + data[3] = sum[1]; +} + +pub struct DropSleep(pub Duration); + +impl Drop for DropSleep { + fn drop(&mut self) { + sleep(self.0); + } +} + +/// Create a listening socket for solicitations and advertisements. This +/// socket listens on the unspecified address to pick up both unicast +/// and multicast solicitations and advertisements. +pub fn create_socket(index: u32) -> Result { + use ListeningSocketError as E; + const READ_TIMEOUT: Duration = Duration::from_secs(1); + + let s = Socket::new(Domain::IPV6, Type::RAW, Some(Protocol::ICMPV6)) + .map_err(E::NewSocketError)?; + + let sa = SocketAddrV6::new(Ipv6Addr::UNSPECIFIED, 0, 0, index).into(); + + s.set_reuse_address(true).map_err(E::ReuseAddress)?; + + s.set_multicast_if_v6(index).map_err(E::SetMulticastIf)?; + + s.set_multicast_hops_v6(255) + .map_err(E::SetMulticastHopsV6)?; + + s.set_multicast_loop_v6(false) + .map_err(E::SetMulticastLoop)?; + + s.join_multicast_v6(&ALL_NODES_MCAST, index) + .map_err(E::JoinAllNodesMulticast)?; + + s.join_multicast_v6(&ALL_ROUTERS_MCAST, index) + .map_err(E::JoinAllRoutersMulticast)?; + + s.bind(&sa).map_err(ListeningSocketError::Bind)?; + + s.set_read_timeout(Some(READ_TIMEOUT)) + .map_err(E::SetReadTimeoutError)?; + + unsafe { + // from + const IPV6_MINHOPCOUNT: c_int = 0x2f; + let min_hops: c_int = 255; + let rc = libc::setsockopt( + s.as_raw_fd(), + libc::IPPROTO_IPV6, + IPV6_MINHOPCOUNT, + &min_hops as *const _ as *const c_void, + std::mem::size_of::() as socklen_t, + ); + if rc < 0 { + return Err(ListeningSocketError::SetIpv6MinHopCount( + std::io::Error::last_os_error(), + )); + } + } + + Ok(s) +} diff --git a/openapi/mg-admin/mg-admin-4.0.0-9d15bb.json b/openapi/mg-admin/mg-admin-4.0.0-dbb252.json similarity index 95% rename from openapi/mg-admin/mg-admin-4.0.0-9d15bb.json rename to openapi/mg-admin/mg-admin-4.0.0-dbb252.json index 3332678a..fdda473c 100644 --- a/openapi/mg-admin/mg-admin-4.0.0-9d15bb.json +++ b/openapi/mg-admin/mg-admin-4.0.0-dbb252.json @@ -1508,6 +1508,17 @@ "$ref": "#/components/schemas/ShaperSource" } ] + }, + "unnumbered_peers": { + "description": "Lists of unnumbered peers indexed by peer group.", + "default": {}, + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UnnumberedBgpPeerConfig" + } + } } }, "required": [ @@ -1875,7 +1886,6 @@ ] }, "BgpPeerConfig": { - "description": "BGP peer configuration (current version with per-address-family policies).", "type": "object", "properties": { "communities": { @@ -2090,7 +2100,7 @@ "additionalProperties": false }, { - "description": "Multiple nexthop encoding capability as defined in RFC 8950. Note this capability is not yet implemented.", + "description": "Multiple nexthop encoding capability as defined in RFC 8950.", "type": "object", "properties": { "extended_next_hop_encoding": { @@ -3398,6 +3408,7 @@ }, "connect_retry_jitter": { "nullable": true, + "description": "Jitter range for connect_retry timer. When used, the connect_retry timer is multiplied by a random value within the (min, max) range supplied. Useful to help break repeated synchronization of connection collisions.", "allOf": [ { "$ref": "#/components/schemas/JitterRange" @@ -3410,6 +3421,7 @@ "minimum": 0 }, "deterministic_collision_resolution": { + "description": "Enable deterministic collision resolution in Established state. When true, uses BGP-ID comparison per RFC 4271 §6.8 for collision resolution even when one connection is already in Established state. When false, Established connection always wins (timing-based resolution).", "type": "boolean" }, "enforce_first_as": { @@ -3428,6 +3440,7 @@ }, "idle_hold_jitter": { "nullable": true, + "description": "Jitter range for idle hold timer. When used, the idle hold timer is multiplied by a random value within the (min, max) range supplied. Useful to help break repeated synchronization of connection collisions.", "allOf": [ { "$ref": "#/components/schemas/JitterRange" @@ -4793,6 +4806,155 @@ } } }, + "UnnumberedBgpPeerConfig": { + "type": "object", + "properties": { + "communities": { + "type": "array", + "items": { + "type": "integer", + "format": "uint32", + "minimum": 0 + } + }, + "connect_retry": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "connect_retry_jitter": { + "nullable": true, + "description": "Jitter range for connect_retry timer. When used, the connect_retry timer is multiplied by a random value within the (min, max) range supplied. Useful to help break repeated synchronization of connection collisions.", + "allOf": [ + { + "$ref": "#/components/schemas/JitterRange" + } + ] + }, + "delay_open": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "deterministic_collision_resolution": { + "description": "Enable deterministic collision resolution in Established state. When true, uses BGP-ID comparison per RFC 4271 §6.8 for collision resolution even when one connection is already in Established state. When false, Established connection always wins (timing-based resolution).", + "type": "boolean" + }, + "enforce_first_as": { + "type": "boolean" + }, + "hold_time": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "idle_hold_jitter": { + "nullable": true, + "description": "Jitter range for idle hold timer. When used, the idle hold timer is multiplied by a random value within the (min, max) range supplied. Useful to help break repeated synchronization of connection collisions.", + "allOf": [ + { + "$ref": "#/components/schemas/JitterRange" + } + ] + }, + "idle_hold_time": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "interface": { + "type": "string" + }, + "ipv4_unicast": { + "nullable": true, + "description": "IPv4 Unicast address family configuration (None = disabled)", + "allOf": [ + { + "$ref": "#/components/schemas/Ipv4UnicastConfig" + } + ] + }, + "ipv6_unicast": { + "nullable": true, + "description": "IPv6 Unicast address family configuration (None = disabled)", + "allOf": [ + { + "$ref": "#/components/schemas/Ipv6UnicastConfig" + } + ] + }, + "keepalive": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "local_pref": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "md5_auth_key": { + "nullable": true, + "type": "string" + }, + "min_ttl": { + "nullable": true, + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "multi_exit_discriminator": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "name": { + "type": "string" + }, + "passive": { + "type": "boolean" + }, + "remote_asn": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "resolution": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "router_lifetime": { + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "vlan_id": { + "nullable": true, + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + "required": [ + "communities", + "connect_retry", + "delay_open", + "deterministic_collision_resolution", + "enforce_first_as", + "hold_time", + "idle_hold_time", + "interface", + "keepalive", + "name", + "passive", + "resolution", + "router_lifetime" + ] + }, "UpdateErrorSubcode": { "description": "Update message error subcode types", "type": "string", diff --git a/openapi/mg-admin/mg-admin-5.0.0-ca4879.json b/openapi/mg-admin/mg-admin-5.0.0-ca4879.json new file mode 100644 index 00000000..da238344 --- /dev/null +++ b/openapi/mg-admin/mg-admin-5.0.0-ca4879.json @@ -0,0 +1,5483 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "Maghemite Admin", + "contact": { + "url": "https://oxide.computer", + "email": "api@oxide.computer" + }, + "version": "5.0.0" + }, + "paths": { + "/bfd/peers": { + "get": { + "summary": "Get all the peers and their associated BFD state. Peers are identified by IP", + "description": "address.", + "operationId": "get_bfd_peers", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_BfdPeerInfo", + "type": "array", + "items": { + "$ref": "#/components/schemas/BfdPeerInfo" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "summary": "Add a new peer to the daemon. A session for the specified peer will start", + "description": "immediately.", + "operationId": "add_bfd_peer", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BfdPeerConfig" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/bfd/peers/{addr}": { + "delete": { + "summary": "Remove the specified peer from the daemon. The associated peer session will", + "description": "be stopped immediately.", + "operationId": "remove_bfd_peer", + "parameters": [ + { + "in": "path", + "name": "addr", + "description": "Address of the peer to remove.", + "required": true, + "schema": { + "type": "string", + "format": "ip" + } + } + ], + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/bgp/clear/neighbor": { + "post": { + "operationId": "clear_neighbor_v2", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NeighborResetRequest" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/bgp/clear/unnumbered-neighbor": { + "post": { + "operationId": "clear_unnumbered_neighbor", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnnumberedNeighborResetRequest" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/bgp/config/checker": { + "get": { + "operationId": "read_checker", + "parameters": [ + { + "in": "query", + "name": "asn", + "description": "ASN of the router to get imported prefixes from.", + "required": true, + "schema": { + "type": "integer", + "format": "uint32", + "minimum": 0 + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CheckerSource" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "operationId": "create_checker", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CheckerSource" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "post": { + "operationId": "update_checker", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CheckerSource" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "operationId": "delete_checker", + "parameters": [ + { + "in": "query", + "name": "asn", + "description": "ASN of the router to get imported prefixes from.", + "required": true, + "schema": { + "type": "integer", + "format": "uint32", + "minimum": 0 + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/bgp/config/neighbor": { + "get": { + "operationId": "read_neighbor_v2", + "parameters": [ + { + "in": "query", + "name": "addr", + "required": true, + "schema": { + "type": "string", + "format": "ip" + } + }, + { + "in": "query", + "name": "asn", + "required": true, + "schema": { + "type": "integer", + "format": "uint32", + "minimum": 0 + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Neighbor" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "operationId": "create_neighbor_v2", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Neighbor" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "post": { + "operationId": "update_neighbor_v2", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Neighbor" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "operationId": "delete_neighbor_v2", + "parameters": [ + { + "in": "query", + "name": "addr", + "required": true, + "schema": { + "type": "string", + "format": "ip" + } + }, + { + "in": "query", + "name": "asn", + "required": true, + "schema": { + "type": "integer", + "format": "uint32", + "minimum": 0 + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/bgp/config/neighbors": { + "get": { + "operationId": "read_neighbors_v2", + "parameters": [ + { + "in": "query", + "name": "asn", + "description": "ASN of the router to get imported prefixes from.", + "required": true, + "schema": { + "type": "integer", + "format": "uint32", + "minimum": 0 + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_Neighbor", + "type": "array", + "items": { + "$ref": "#/components/schemas/Neighbor" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/bgp/config/origin4": { + "get": { + "operationId": "read_origin4", + "parameters": [ + { + "in": "query", + "name": "asn", + "description": "ASN of the router to get imported prefixes from.", + "required": true, + "schema": { + "type": "integer", + "format": "uint32", + "minimum": 0 + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Origin4" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "operationId": "create_origin4", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Origin4" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "post": { + "operationId": "update_origin4", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Origin4" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "operationId": "delete_origin4", + "parameters": [ + { + "in": "query", + "name": "asn", + "description": "ASN of the router to get imported prefixes from.", + "required": true, + "schema": { + "type": "integer", + "format": "uint32", + "minimum": 0 + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/bgp/config/origin6": { + "get": { + "operationId": "read_origin6", + "parameters": [ + { + "in": "query", + "name": "asn", + "description": "ASN of the router to get imported prefixes from.", + "required": true, + "schema": { + "type": "integer", + "format": "uint32", + "minimum": 0 + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Origin6" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "operationId": "create_origin6", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Origin6" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "post": { + "operationId": "update_origin6", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Origin6" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "operationId": "delete_origin6", + "parameters": [ + { + "in": "query", + "name": "asn", + "description": "ASN of the router to get imported prefixes from.", + "required": true, + "schema": { + "type": "integer", + "format": "uint32", + "minimum": 0 + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/bgp/config/router": { + "get": { + "operationId": "read_router", + "parameters": [ + { + "in": "query", + "name": "asn", + "description": "ASN of the router to get imported prefixes from.", + "required": true, + "schema": { + "type": "integer", + "format": "uint32", + "minimum": 0 + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Router" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "operationId": "create_router", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Router" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "post": { + "operationId": "update_router", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Router" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "operationId": "delete_router", + "parameters": [ + { + "in": "query", + "name": "asn", + "description": "ASN of the router to get imported prefixes from.", + "required": true, + "schema": { + "type": "integer", + "format": "uint32", + "minimum": 0 + } + } + ], + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/bgp/config/routers": { + "get": { + "operationId": "read_routers", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_Router", + "type": "array", + "items": { + "$ref": "#/components/schemas/Router" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/bgp/config/shaper": { + "get": { + "operationId": "read_shaper", + "parameters": [ + { + "in": "query", + "name": "asn", + "description": "ASN of the router to get imported prefixes from.", + "required": true, + "schema": { + "type": "integer", + "format": "uint32", + "minimum": 0 + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ShaperSource" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "operationId": "create_shaper", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ShaperSource" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "post": { + "operationId": "update_shaper", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ShaperSource" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "operationId": "delete_shaper", + "parameters": [ + { + "in": "query", + "name": "asn", + "description": "ASN of the router to get imported prefixes from.", + "required": true, + "schema": { + "type": "integer", + "format": "uint32", + "minimum": 0 + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/bgp/config/unnumbered-neighbor": { + "get": { + "operationId": "read_unnumbered_neighbor", + "parameters": [ + { + "in": "query", + "name": "asn", + "required": true, + "schema": { + "type": "integer", + "format": "uint32", + "minimum": 0 + } + }, + { + "in": "query", + "name": "interface", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnnumberedNeighbor" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "operationId": "create_unnumbered_neighbor", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnnumberedNeighbor" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "post": { + "operationId": "update_unnumbered_neighbor", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnnumberedNeighbor" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "operationId": "delete_unnumbered_neighbor", + "parameters": [ + { + "in": "query", + "name": "asn", + "required": true, + "schema": { + "type": "integer", + "format": "uint32", + "minimum": 0 + } + }, + { + "in": "query", + "name": "interface", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/bgp/config/unnumbered-neighbors": { + "get": { + "operationId": "read_unnumbered_neighbors", + "parameters": [ + { + "in": "query", + "name": "asn", + "description": "ASN of the router to get imported prefixes from.", + "required": true, + "schema": { + "type": "integer", + "format": "uint32", + "minimum": 0 + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_UnnumberedNeighbor", + "type": "array", + "items": { + "$ref": "#/components/schemas/UnnumberedNeighbor" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/bgp/config/unnumbered-pending": { + "get": { + "operationId": "read_pending_unnumbered_neighbors", + "parameters": [ + { + "in": "query", + "name": "asn", + "description": "ASN of the router to get imported prefixes from.", + "required": true, + "schema": { + "type": "integer", + "format": "uint32", + "minimum": 0 + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_PendingUnnumberedNeighbor", + "type": "array", + "items": { + "$ref": "#/components/schemas/PendingUnnumberedNeighbor" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/bgp/history/fsm": { + "get": { + "operationId": "fsm_history", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FsmHistoryRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FsmHistoryResponse" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/bgp/history/message": { + "get": { + "operationId": "message_history_v2", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MessageHistoryRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MessageHistoryResponse" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/bgp/omicron/apply": { + "post": { + "operationId": "bgp_apply_v2", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApplyRequest" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/bgp/status/exported": { + "get": { + "operationId": "get_exported", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AsnSelector" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Map_of_Array_of_Prefix", + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Prefix" + } + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/bgp/status/neighbors": { + "get": { + "operationId": "get_neighbors_v3", + "parameters": [ + { + "in": "query", + "name": "asn", + "description": "ASN of the router to get imported prefixes from.", + "required": true, + "schema": { + "type": "integer", + "format": "uint32", + "minimum": 0 + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Map_of_PeerInfo", + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/PeerInfo" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/rib/config/bestpath/fanout": { + "get": { + "operationId": "read_rib_bestpath_fanout", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BestpathFanoutResponse" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "post": { + "operationId": "update_rib_bestpath_fanout", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BestpathFanoutRequest" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/rib/status/imported": { + "get": { + "operationId": "get_rib_imported", + "parameters": [ + { + "in": "query", + "name": "address_family", + "description": "Filter by address family (None means all families)", + "schema": { + "$ref": "#/components/schemas/AddressFamily" + } + }, + { + "in": "query", + "name": "protocol", + "description": "Filter by protocol (optional)", + "schema": { + "$ref": "#/components/schemas/ProtocolFilter" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Rib" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/rib/status/selected": { + "get": { + "operationId": "get_rib_selected", + "parameters": [ + { + "in": "query", + "name": "address_family", + "description": "Filter by address family (None means all families)", + "schema": { + "$ref": "#/components/schemas/AddressFamily" + } + }, + { + "in": "query", + "name": "protocol", + "description": "Filter by protocol (optional)", + "schema": { + "$ref": "#/components/schemas/ProtocolFilter" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Rib" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/static/route4": { + "get": { + "operationId": "static_list_v4_routes", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Map_of_Set_of_Path", + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Path" + }, + "uniqueItems": true + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "operationId": "static_add_v4_route", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AddStaticRoute4Request" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "operationId": "static_remove_v4_route", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteStaticRoute4Request" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/static/route6": { + "get": { + "operationId": "static_list_v6_routes", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Map_of_Set_of_Path", + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Path" + }, + "uniqueItems": true + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "operationId": "static_add_v6_route", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AddStaticRoute6Request" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "operationId": "static_remove_v6_route", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteStaticRoute6Request" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/switch/identifiers": { + "get": { + "operationId": "switch_identifiers", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SwitchIdentifiers" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + } + }, + "components": { + "schemas": { + "AddPathElement": { + "description": "The add path element comes as a BGP capability extension as described in RFC 7911.", + "type": "object", + "properties": { + "afi": { + "description": "Address family identifier. ", + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "safi": { + "description": "Subsequent address family identifier. There are a large pile of these ", + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "send_receive": { + "description": "This field indicates whether the sender is (a) able to receive multiple paths from its peer (value 1), (b) able to send multiple paths to its peer (value 2), or (c) both (value 3) for the .", + "type": "integer", + "format": "uint8", + "minimum": 0 + } + }, + "required": [ + "afi", + "safi", + "send_receive" + ] + }, + "AddStaticRoute4Request": { + "type": "object", + "properties": { + "routes": { + "$ref": "#/components/schemas/StaticRoute4List" + } + }, + "required": [ + "routes" + ] + }, + "AddStaticRoute6Request": { + "type": "object", + "properties": { + "routes": { + "$ref": "#/components/schemas/StaticRoute6List" + } + }, + "required": [ + "routes" + ] + }, + "Afi": { + "description": "Address families supported by Maghemite BGP.", + "oneOf": [ + { + "description": "Internet protocol version 4", + "type": "string", + "enum": [ + "Ipv4" + ] + }, + { + "description": "Internet protocol version 6", + "type": "string", + "enum": [ + "Ipv6" + ] + } + ] + }, + "AfiSafi": { + "type": "object", + "properties": { + "afi": { + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "safi": { + "type": "integer", + "format": "uint8", + "minimum": 0 + } + }, + "required": [ + "afi", + "safi" + ] + }, + "Aggregator": { + "description": "AGGREGATOR path attribute (RFC 4271 §5.1.8)\n\nThe AGGREGATOR attribute is an optional transitive attribute that contains the AS number and IP address of the last BGP speaker that formed the aggregate route.", + "type": "object", + "properties": { + "address": { + "description": "IP address of the BGP speaker that formed the aggregate", + "type": "string", + "format": "ipv4" + }, + "asn": { + "description": "Autonomous System Number that formed the aggregate (2-octet)", + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + "required": [ + "address", + "asn" + ] + }, + "ApplyRequest": { + "description": "Apply changes to an ASN (current version with per-AF policies).", + "type": "object", + "properties": { + "asn": { + "description": "ASN to apply changes to.", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "checker": { + "nullable": true, + "description": "Checker rhai code to apply to ingress open and update messages.", + "allOf": [ + { + "$ref": "#/components/schemas/CheckerSource" + } + ] + }, + "originate": { + "description": "Complete set of prefixes to originate.", + "type": "array", + "items": { + "$ref": "#/components/schemas/Prefix4" + } + }, + "peers": { + "description": "Lists of peers indexed by peer group.", + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BgpPeerConfig" + } + } + }, + "shaper": { + "nullable": true, + "description": "Checker rhai code to apply to egress open and update messages.", + "allOf": [ + { + "$ref": "#/components/schemas/ShaperSource" + } + ] + }, + "unnumbered_peers": { + "description": "Lists of unnumbered peers indexed by peer group.", + "default": {}, + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UnnumberedBgpPeerConfig" + } + } + } + }, + "required": [ + "asn", + "originate", + "peers" + ] + }, + "As4Aggregator": { + "description": "AS4_AGGREGATOR path attribute (RFC 6793)\n\nThe AS4_AGGREGATOR attribute is an optional transitive attribute with the same semantics as AGGREGATOR, but carries a 4-octet AS number instead of 2-octet.", + "type": "object", + "properties": { + "address": { + "description": "IP address of the BGP speaker that formed the aggregate", + "type": "string", + "format": "ipv4" + }, + "asn": { + "description": "Autonomous System Number that formed the aggregate (4-octet)", + "type": "integer", + "format": "uint32", + "minimum": 0 + } + }, + "required": [ + "address", + "asn" + ] + }, + "As4PathSegment": { + "type": "object", + "properties": { + "typ": { + "$ref": "#/components/schemas/AsPathType" + }, + "value": { + "type": "array", + "items": { + "type": "integer", + "format": "uint32", + "minimum": 0 + } + } + }, + "required": [ + "typ", + "value" + ] + }, + "AsPathType": { + "description": "Enumeration describes possible AS path types", + "oneOf": [ + { + "description": "The path is to be interpreted as a set", + "type": "string", + "enum": [ + "as_set" + ] + }, + { + "description": "The path is to be interpreted as a sequence", + "type": "string", + "enum": [ + "as_sequence" + ] + } + ] + }, + "AsnSelector": { + "type": "object", + "properties": { + "asn": { + "description": "ASN of the router to get imported prefixes from.", + "type": "integer", + "format": "uint32", + "minimum": 0 + } + }, + "required": [ + "asn" + ] + }, + "BestpathFanoutRequest": { + "type": "object", + "properties": { + "fanout": { + "description": "Maximum number of equal-cost paths for ECMP forwarding", + "type": "integer", + "format": "uint8", + "minimum": 1 + } + }, + "required": [ + "fanout" + ] + }, + "BestpathFanoutResponse": { + "type": "object", + "properties": { + "fanout": { + "description": "Current maximum number of equal-cost paths for ECMP forwarding", + "type": "integer", + "format": "uint8", + "minimum": 1 + } + }, + "required": [ + "fanout" + ] + }, + "BfdPeerConfig": { + "type": "object", + "properties": { + "detection_threshold": { + "description": "Detection threshold for connectivity as a multipler to required_rx", + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "listen": { + "description": "Address to listen on for control messages from the peer.", + "type": "string", + "format": "ip" + }, + "mode": { + "description": "Mode is single-hop (RFC 5881) or multi-hop (RFC 5883).", + "allOf": [ + { + "$ref": "#/components/schemas/SessionMode" + } + ] + }, + "peer": { + "description": "Address of the peer to add.", + "type": "string", + "format": "ip" + }, + "required_rx": { + "description": "Acceptable time between control messages in microseconds.", + "type": "integer", + "format": "uint64", + "minimum": 0 + } + }, + "required": [ + "detection_threshold", + "listen", + "mode", + "peer", + "required_rx" + ] + }, + "BfdPeerInfo": { + "type": "object", + "properties": { + "config": { + "$ref": "#/components/schemas/BfdPeerConfig" + }, + "state": { + "$ref": "#/components/schemas/BfdPeerState" + } + }, + "required": [ + "config", + "state" + ] + }, + "BfdPeerState": { + "description": "The possible peer states. See the `State` trait implementations `Down`, `Init`, and `Up` for detailed semantics. Data representation is u8 as this enum is used as a part of the BFD wire protocol.", + "oneOf": [ + { + "description": "A stable down state. Non-responsive to incoming messages.", + "type": "string", + "enum": [ + "AdminDown" + ] + }, + { + "description": "The initial state.", + "type": "string", + "enum": [ + "Down" + ] + }, + { + "description": "The peer has detected a remote peer in the down state.", + "type": "string", + "enum": [ + "Init" + ] + }, + { + "description": "The peer has detected a remote peer in the up or init state while in the init state.", + "type": "string", + "enum": [ + "Up" + ] + } + ] + }, + "BgpCapability": { + "oneOf": [ + { + "type": "string", + "enum": [ + "RouteRefresh" + ] + }, + { + "type": "object", + "properties": { + "MultiprotocolExtensions": { + "$ref": "#/components/schemas/AfiSafi" + } + }, + "required": [ + "MultiprotocolExtensions" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "FourOctetAsn": { + "type": "integer", + "format": "uint32", + "minimum": 0 + } + }, + "required": [ + "FourOctetAsn" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "AddPath": { + "type": "object", + "properties": { + "elements": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AfiSafi" + } + } + }, + "required": [ + "elements" + ] + } + }, + "required": [ + "AddPath" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "Unknown": { + "type": "integer", + "format": "uint8", + "minimum": 0 + } + }, + "required": [ + "Unknown" + ], + "additionalProperties": false + } + ] + }, + "BgpNexthop": { + "description": "A BGP next-hop address in one of three formats: IPv4, IPv6 single, or IPv6 double.", + "oneOf": [ + { + "type": "object", + "properties": { + "Ipv4": { + "type": "string", + "format": "ipv4" + } + }, + "required": [ + "Ipv4" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "Ipv6Single": { + "type": "string", + "format": "ipv6" + } + }, + "required": [ + "Ipv6Single" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "Ipv6Double": { + "$ref": "#/components/schemas/Ipv6DoubleNexthop" + } + }, + "required": [ + "Ipv6Double" + ], + "additionalProperties": false + } + ] + }, + "BgpPathProperties": { + "type": "object", + "properties": { + "as_path": { + "type": "array", + "items": { + "type": "integer", + "format": "uint32", + "minimum": 0 + } + }, + "id": { + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "local_pref": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "med": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "origin_as": { + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "peer": { + "type": "string", + "format": "ip" + }, + "stale": { + "nullable": true, + "type": "string", + "format": "date-time" + } + }, + "required": [ + "as_path", + "id", + "origin_as", + "peer" + ] + }, + "BgpPeerConfig": { + "type": "object", + "properties": { + "communities": { + "type": "array", + "items": { + "type": "integer", + "format": "uint32", + "minimum": 0 + } + }, + "connect_retry": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "connect_retry_jitter": { + "nullable": true, + "description": "Jitter range for connect_retry timer. When used, the connect_retry timer is multiplied by a random value within the (min, max) range supplied. Useful to help break repeated synchronization of connection collisions.", + "allOf": [ + { + "$ref": "#/components/schemas/JitterRange" + } + ] + }, + "delay_open": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "deterministic_collision_resolution": { + "description": "Enable deterministic collision resolution in Established state. When true, uses BGP-ID comparison per RFC 4271 §6.8 for collision resolution even when one connection is already in Established state. When false, Established connection always wins (timing-based resolution).", + "type": "boolean" + }, + "enforce_first_as": { + "type": "boolean" + }, + "hold_time": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "host": { + "type": "string" + }, + "idle_hold_jitter": { + "nullable": true, + "description": "Jitter range for idle hold timer. When used, the idle hold timer is multiplied by a random value within the (min, max) range supplied. Useful to help break repeated synchronization of connection collisions.", + "allOf": [ + { + "$ref": "#/components/schemas/JitterRange" + } + ] + }, + "idle_hold_time": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "ipv4_unicast": { + "nullable": true, + "description": "IPv4 Unicast address family configuration (None = disabled)", + "allOf": [ + { + "$ref": "#/components/schemas/Ipv4UnicastConfig" + } + ] + }, + "ipv6_unicast": { + "nullable": true, + "description": "IPv6 Unicast address family configuration (None = disabled)", + "allOf": [ + { + "$ref": "#/components/schemas/Ipv6UnicastConfig" + } + ] + }, + "keepalive": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "local_pref": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "md5_auth_key": { + "nullable": true, + "type": "string" + }, + "min_ttl": { + "nullable": true, + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "multi_exit_discriminator": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "name": { + "type": "string" + }, + "passive": { + "type": "boolean" + }, + "remote_asn": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "resolution": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "vlan_id": { + "nullable": true, + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + "required": [ + "communities", + "connect_retry", + "delay_open", + "deterministic_collision_resolution", + "enforce_first_as", + "hold_time", + "host", + "idle_hold_time", + "keepalive", + "name", + "passive", + "resolution" + ] + }, + "Capability": { + "description": "Optional capabilities supported by a BGP implementation.", + "oneOf": [ + { + "description": "Multiprotocol extensions as defined in RFC 2858", + "type": "object", + "properties": { + "multiprotocol_extensions": { + "type": "object", + "properties": { + "afi": { + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "safi": { + "type": "integer", + "format": "uint8", + "minimum": 0 + } + }, + "required": [ + "afi", + "safi" + ] + } + }, + "required": [ + "multiprotocol_extensions" + ], + "additionalProperties": false + }, + { + "description": "Route refresh capability as defined in RFC 2918.", + "type": "object", + "properties": { + "route_refresh": { + "type": "object" + } + }, + "required": [ + "route_refresh" + ], + "additionalProperties": false + }, + { + "description": "Outbound filtering capability as defined in RFC 5291. Note this capability is not yet implemented.", + "type": "object", + "properties": { + "outbound_route_filtering": { + "type": "object" + } + }, + "required": [ + "outbound_route_filtering" + ], + "additionalProperties": false + }, + { + "description": "Multiple routes to destination capability as defined in RFC 8277 (deprecated). Note this capability is not yet implemented.", + "type": "object", + "properties": { + "multiple_routes_to_destination": { + "type": "object" + } + }, + "required": [ + "multiple_routes_to_destination" + ], + "additionalProperties": false + }, + { + "description": "Multiple nexthop encoding capability as defined in RFC 8950.", + "type": "object", + "properties": { + "extended_next_hop_encoding": { + "type": "object" + } + }, + "required": [ + "extended_next_hop_encoding" + ], + "additionalProperties": false + }, + { + "description": "Extended message capability as defined in RFC 8654. Note this capability is not yet implemented.", + "type": "object", + "properties": { + "b_g_p_extended_message": { + "type": "object" + } + }, + "required": [ + "b_g_p_extended_message" + ], + "additionalProperties": false + }, + { + "description": "BGPSec as defined in RFC 8205. Note this capability is not yet implemented.", + "type": "object", + "properties": { + "bgp_sec": { + "type": "object" + } + }, + "required": [ + "bgp_sec" + ], + "additionalProperties": false + }, + { + "description": "Multiple label support as defined in RFC 8277. Note this capability is not yet implemented.", + "type": "object", + "properties": { + "multiple_labels": { + "type": "object" + } + }, + "required": [ + "multiple_labels" + ], + "additionalProperties": false + }, + { + "description": "BGP role capability as defined in RFC 9234. Note this capability is not yet implemented.", + "type": "object", + "properties": { + "bgp_role": { + "type": "object" + } + }, + "required": [ + "bgp_role" + ], + "additionalProperties": false + }, + { + "description": "Graceful restart as defined in RFC 4724. Note this capability is not yet implemented.", + "type": "object", + "properties": { + "graceful_restart": { + "type": "object" + } + }, + "required": [ + "graceful_restart" + ], + "additionalProperties": false + }, + { + "description": "Four octet AS numbers as defined in RFC 6793.", + "type": "object", + "properties": { + "four_octet_as": { + "type": "object", + "properties": { + "asn": { + "type": "integer", + "format": "uint32", + "minimum": 0 + } + }, + "required": [ + "asn" + ] + } + }, + "required": [ + "four_octet_as" + ], + "additionalProperties": false + }, + { + "description": "Dynamic capabilities as defined in draft-ietf-idr-dynamic-cap. Note this capability is not yet implemented.", + "type": "object", + "properties": { + "dynamic_capability": { + "type": "object" + } + }, + "required": [ + "dynamic_capability" + ], + "additionalProperties": false + }, + { + "description": "Multi session support as defined in draft-ietf-idr-bgp-multisession. Note this capability is not yet supported.", + "type": "object", + "properties": { + "multisession_bgp": { + "type": "object" + } + }, + "required": [ + "multisession_bgp" + ], + "additionalProperties": false + }, + { + "description": "Add path capability as defined in RFC 7911.", + "type": "object", + "properties": { + "add_path": { + "type": "object", + "properties": { + "elements": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AddPathElement" + }, + "uniqueItems": true + } + }, + "required": [ + "elements" + ] + } + }, + "required": [ + "add_path" + ], + "additionalProperties": false + }, + { + "description": "Enhanced route refresh as defined in RFC 7313. Note this capability is not yet supported.", + "type": "object", + "properties": { + "enhanced_route_refresh": { + "type": "object" + } + }, + "required": [ + "enhanced_route_refresh" + ], + "additionalProperties": false + }, + { + "description": "Long-lived graceful restart as defined in draft-uttaro-idr-bgp-persistence. Note this capability is not yet supported.", + "type": "object", + "properties": { + "long_lived_graceful_restart": { + "type": "object" + } + }, + "required": [ + "long_lived_graceful_restart" + ], + "additionalProperties": false + }, + { + "description": "Routing policy distribution as defined indraft-ietf-idr-rpd-04. Note this capability is not yet supported.", + "type": "object", + "properties": { + "routing_policy_distribution": { + "type": "object" + } + }, + "required": [ + "routing_policy_distribution" + ], + "additionalProperties": false + }, + { + "description": "Fully qualified domain names as defined intdraft-walton-bgp-hostname-capability. Note this capability is not yet supported.", + "type": "object", + "properties": { + "fqdn": { + "type": "object" + } + }, + "required": [ + "fqdn" + ], + "additionalProperties": false + }, + { + "description": "Pre-standard route refresh as defined in RFC 8810 (deprecated). Note this capability is not yet supported.", + "type": "object", + "properties": { + "prestandard_route_refresh": { + "type": "object" + } + }, + "required": [ + "prestandard_route_refresh" + ], + "additionalProperties": false + }, + { + "description": "Pre-standard prefix-based outbound route filtering as defined in RFC 8810 (deprecated). Note this is not yet implemented.", + "type": "object", + "properties": { + "prestandard_orf_and_pd": { + "type": "object" + } + }, + "required": [ + "prestandard_orf_and_pd" + ], + "additionalProperties": false + }, + { + "description": "Pre-standard outbound route filtering as defined in RFC 8810 (deprecated). Note this is not yet implemented.", + "type": "object", + "properties": { + "prestandard_outbound_route_filtering": { + "type": "object" + } + }, + "required": [ + "prestandard_outbound_route_filtering" + ], + "additionalProperties": false + }, + { + "description": "Pre-standard multisession as defined in RFC 8810 (deprecated). Note this is not yet implemented.", + "type": "object", + "properties": { + "prestandard_multisession": { + "type": "object" + } + }, + "required": [ + "prestandard_multisession" + ], + "additionalProperties": false + }, + { + "description": "Pre-standard fully qualified domain names as defined in RFC 8810 (deprecated). Note this is not yet implemented.", + "type": "object", + "properties": { + "prestandard_fqdn": { + "type": "object" + } + }, + "required": [ + "prestandard_fqdn" + ], + "additionalProperties": false + }, + { + "description": "Pre-standard operational messages as defined in RFC 8810 (deprecated). Note this is not yet implemented.", + "type": "object", + "properties": { + "prestandard_operational_message": { + "type": "object" + } + }, + "required": [ + "prestandard_operational_message" + ], + "additionalProperties": false + }, + { + "description": "Experimental capability as defined in RFC 8810.", + "type": "object", + "properties": { + "experimental": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "uint8", + "minimum": 0 + } + }, + "required": [ + "code" + ] + } + }, + "required": [ + "experimental" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "unassigned": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "uint8", + "minimum": 0 + } + }, + "required": [ + "code" + ] + } + }, + "required": [ + "unassigned" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "reserved": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "uint8", + "minimum": 0 + } + }, + "required": [ + "code" + ] + } + }, + "required": [ + "reserved" + ], + "additionalProperties": false + } + ] + }, + "CeaseErrorSubcode": { + "description": "Cease error subcode types from RFC 4486", + "type": "string", + "enum": [ + "unspecific", + "maximum_numberof_prefixes_reached", + "administrative_shutdown", + "peer_deconfigured", + "administrative_reset", + "connection_rejected", + "other_configuration_change", + "connection_collision_resolution", + "out_of_resources" + ] + }, + "CheckerSource": { + "type": "object", + "properties": { + "asn": { + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "code": { + "type": "string" + } + }, + "required": [ + "asn", + "code" + ] + }, + "Community": { + "description": "BGP community value", + "oneOf": [ + { + "description": "All routes received carrying a communities attribute containing this value MUST NOT be advertised outside a BGP confederation boundary (a stand-alone autonomous system that is not part of a confederation should be considered a confederation itself)", + "type": "string", + "enum": [ + "no_export" + ] + }, + { + "description": "All routes received carrying a communities attribute containing this value MUST NOT be advertised to other BGP peers.", + "type": "string", + "enum": [ + "no_advertise" + ] + }, + { + "description": "All routes received carrying a communities attribute containing this value MUST NOT be advertised to external BGP peers (this includes peers in other members autonomous systems inside a BGP confederation).", + "type": "string", + "enum": [ + "no_export_sub_confed" + ] + }, + { + "description": "All routes received carrying a communities attribute containing this value must set the local preference for the received routes to a low value, preferably zero.", + "type": "string", + "enum": [ + "graceful_shutdown" + ] + }, + { + "description": "A user defined community", + "type": "object", + "properties": { + "user_defined": { + "type": "integer", + "format": "uint32", + "minimum": 0 + } + }, + "required": [ + "user_defined" + ], + "additionalProperties": false + } + ] + }, + "ConnectionId": { + "description": "Unique identifier for a BGP connection instance", + "type": "object", + "properties": { + "local": { + "description": "Local socket address for this connection", + "type": "string" + }, + "remote": { + "description": "Remote socket address for this connection", + "type": "string" + }, + "uuid": { + "description": "Unique identifier for this connection instance", + "type": "string", + "format": "uuid" + } + }, + "required": [ + "local", + "remote", + "uuid" + ] + }, + "DeleteStaticRoute4Request": { + "type": "object", + "properties": { + "routes": { + "$ref": "#/components/schemas/StaticRoute4List" + } + }, + "required": [ + "routes" + ] + }, + "DeleteStaticRoute6Request": { + "type": "object", + "properties": { + "routes": { + "$ref": "#/components/schemas/StaticRoute6List" + } + }, + "required": [ + "routes" + ] + }, + "Duration": { + "type": "object", + "properties": { + "nanos": { + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "secs": { + "type": "integer", + "format": "uint64", + "minimum": 0 + } + }, + "required": [ + "nanos", + "secs" + ] + }, + "DynamicTimerInfo": { + "type": "object", + "properties": { + "configured": { + "$ref": "#/components/schemas/Duration" + }, + "negotiated": { + "$ref": "#/components/schemas/Duration" + }, + "remaining": { + "$ref": "#/components/schemas/Duration" + } + }, + "required": [ + "configured", + "negotiated", + "remaining" + ] + }, + "Error": { + "description": "Error information from a response.", + "type": "object", + "properties": { + "error_code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "request_id": { + "type": "string" + } + }, + "required": [ + "message", + "request_id" + ] + }, + "ErrorCode": { + "description": "This enumeration contains possible notification error codes.", + "type": "string", + "enum": [ + "header", + "open", + "update", + "hold_timer_expired", + "fsm", + "cease" + ] + }, + "ErrorSubcode": { + "description": "This enumeration contains possible notification error subcodes.", + "oneOf": [ + { + "type": "object", + "properties": { + "header": { + "$ref": "#/components/schemas/HeaderErrorSubcode" + } + }, + "required": [ + "header" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "open": { + "$ref": "#/components/schemas/OpenErrorSubcode" + } + }, + "required": [ + "open" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "update": { + "$ref": "#/components/schemas/UpdateErrorSubcode" + } + }, + "required": [ + "update" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "hold_time": { + "type": "integer", + "format": "uint8", + "minimum": 0 + } + }, + "required": [ + "hold_time" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "fsm": { + "type": "integer", + "format": "uint8", + "minimum": 0 + } + }, + "required": [ + "fsm" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "cease": { + "$ref": "#/components/schemas/CeaseErrorSubcode" + } + }, + "required": [ + "cease" + ], + "additionalProperties": false + } + ] + }, + "FsmEventBuffer": { + "oneOf": [ + { + "description": "All FSM events (high frequency, includes all timers)", + "type": "string", + "enum": [ + "all" + ] + }, + { + "description": "Major events only (state transitions, admin, new connections)", + "type": "string", + "enum": [ + "major" + ] + } + ] + }, + "FsmEventCategory": { + "description": "Category of FSM event for filtering and display purposes", + "type": "string", + "enum": [ + "Admin", + "Connection", + "Session", + "StateTransition" + ] + }, + "FsmEventRecord": { + "description": "Serializable record of an FSM event with full context", + "type": "object", + "properties": { + "connection_id": { + "nullable": true, + "description": "Connection ID if event is connection-specific", + "allOf": [ + { + "$ref": "#/components/schemas/ConnectionId" + } + ] + }, + "current_state": { + "description": "FSM state at time of event", + "allOf": [ + { + "$ref": "#/components/schemas/FsmStateKind" + } + ] + }, + "details": { + "nullable": true, + "description": "Additional event details (e.g., \"Received OPEN\", \"Admin command\")", + "type": "string" + }, + "event_category": { + "description": "High-level event category", + "allOf": [ + { + "$ref": "#/components/schemas/FsmEventCategory" + } + ] + }, + "event_type": { + "description": "Specific event type as string (e.g., \"ManualStart\", \"HoldTimerExpires\")", + "type": "string" + }, + "previous_state": { + "nullable": true, + "description": "Previous state if this caused a transition", + "allOf": [ + { + "$ref": "#/components/schemas/FsmStateKind" + } + ] + }, + "timestamp": { + "description": "UTC timestamp when event occurred", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "current_state", + "event_category", + "event_type", + "timestamp" + ] + }, + "FsmHistoryRequest": { + "type": "object", + "properties": { + "asn": { + "description": "ASN of the BGP router", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "buffer": { + "nullable": true, + "description": "Which buffer to retrieve - if None, returns major buffer", + "allOf": [ + { + "$ref": "#/components/schemas/FsmEventBuffer" + } + ] + }, + "peer": { + "nullable": true, + "description": "Optional peer filter - if None, returns history for all peers", + "type": "string", + "format": "ip" + } + }, + "required": [ + "asn" + ] + }, + "FsmHistoryResponse": { + "type": "object", + "properties": { + "by_peer": { + "description": "Events organized by peer address Each peer's value contains only the events from the requested buffer", + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FsmEventRecord" + } + } + } + }, + "required": [ + "by_peer" + ] + }, + "FsmStateKind": { + "description": "Simplified representation of a BGP state without having to carry a connection.", + "oneOf": [ + { + "description": "Initial state. Refuse all incomming BGP connections. No resources allocated to peer.", + "type": "string", + "enum": [ + "Idle" + ] + }, + { + "description": "Waiting for the TCP connection to be completed.", + "type": "string", + "enum": [ + "Connect" + ] + }, + { + "description": "Trying to acquire peer by listening for and accepting a TCP connection.", + "type": "string", + "enum": [ + "Active" + ] + }, + { + "description": "Waiting for open message from peer.", + "type": "string", + "enum": [ + "OpenSent" + ] + }, + { + "description": "Waiting for keepalive or notification from peer.", + "type": "string", + "enum": [ + "OpenConfirm" + ] + }, + { + "description": "Handler for Connection Collisions (RFC 4271 6.8)", + "type": "string", + "enum": [ + "ConnectionCollision" + ] + }, + { + "description": "Sync up with peers.", + "type": "string", + "enum": [ + "SessionSetup" + ] + }, + { + "description": "Able to exchange update, notification and keepliave messages with peers.", + "type": "string", + "enum": [ + "Established" + ] + } + ] + }, + "HeaderErrorSubcode": { + "description": "Header error subcode types", + "type": "string", + "enum": [ + "unspecific", + "connection_not_synchronized", + "bad_message_length", + "bad_message_type" + ] + }, + "ImportExportPolicy4": { + "description": "Import/Export policy for IPv4 prefixes only.", + "oneOf": [ + { + "type": "string", + "enum": [ + "NoFiltering" + ] + }, + { + "type": "object", + "properties": { + "Allow": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Prefix4" + }, + "uniqueItems": true + } + }, + "required": [ + "Allow" + ], + "additionalProperties": false + } + ] + }, + "ImportExportPolicy6": { + "description": "Import/Export policy for IPv6 prefixes only.", + "oneOf": [ + { + "type": "string", + "enum": [ + "NoFiltering" + ] + }, + { + "type": "object", + "properties": { + "Allow": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Prefix6" + }, + "uniqueItems": true + } + }, + "required": [ + "Allow" + ], + "additionalProperties": false + } + ] + }, + "Ipv4UnicastConfig": { + "description": "Per-address-family configuration for IPv4 Unicast", + "type": "object", + "properties": { + "export_policy": { + "$ref": "#/components/schemas/ImportExportPolicy4" + }, + "import_policy": { + "$ref": "#/components/schemas/ImportExportPolicy4" + }, + "nexthop": { + "nullable": true, + "type": "string", + "format": "ip" + } + }, + "required": [ + "export_policy", + "import_policy" + ] + }, + "Ipv6DoubleNexthop": { + "description": "IPv6 double nexthop: global unicast address + link-local address. Per RFC 2545, when advertising IPv6 routes, both addresses may be present.", + "type": "object", + "properties": { + "global": { + "description": "Global unicast address", + "type": "string", + "format": "ipv6" + }, + "link_local": { + "description": "Link-local address", + "type": "string", + "format": "ipv6" + } + }, + "required": [ + "global", + "link_local" + ] + }, + "Ipv6UnicastConfig": { + "description": "Per-address-family configuration for IPv6 Unicast", + "type": "object", + "properties": { + "export_policy": { + "$ref": "#/components/schemas/ImportExportPolicy6" + }, + "import_policy": { + "$ref": "#/components/schemas/ImportExportPolicy6" + }, + "nexthop": { + "nullable": true, + "type": "string", + "format": "ip" + } + }, + "required": [ + "export_policy", + "import_policy" + ] + }, + "JitterRange": { + "description": "Jitter range with minimum and maximum multiplier values. When applied to a timer, the timer duration is multiplied by a random value within [min, max] to help break synchronization patterns.", + "type": "object", + "properties": { + "max": { + "description": "Maximum jitter multiplier (typically 1.0 or similar)", + "type": "number", + "format": "double" + }, + "min": { + "description": "Minimum jitter multiplier (typically 0.75 or similar)", + "type": "number", + "format": "double" + } + }, + "required": [ + "max", + "min" + ] + }, + "Message": { + "description": "Holds a BGP message. May be an Open, Update, Notification or Keep Alive message.", + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "open" + ] + }, + "value": { + "$ref": "#/components/schemas/OpenMessage" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "update" + ] + }, + "value": { + "$ref": "#/components/schemas/UpdateMessage" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "notification" + ] + }, + "value": { + "$ref": "#/components/schemas/NotificationMessage" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "keep_alive" + ] + } + }, + "required": [ + "type" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "route_refresh" + ] + }, + "value": { + "$ref": "#/components/schemas/RouteRefreshMessage" + } + }, + "required": [ + "type", + "value" + ] + } + ] + }, + "MessageDirection": { + "type": "string", + "enum": [ + "sent", + "received" + ] + }, + "MessageHistory": { + "description": "Message history for a BGP session", + "type": "object", + "properties": { + "received": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MessageHistoryEntry" + } + }, + "sent": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MessageHistoryEntry" + } + } + }, + "required": [ + "received", + "sent" + ] + }, + "MessageHistoryEntry": { + "description": "A message history entry is a BGP message with an associated timestamp and connection ID", + "type": "object", + "properties": { + "connection_id": { + "$ref": "#/components/schemas/ConnectionId" + }, + "message": { + "$ref": "#/components/schemas/Message" + }, + "timestamp": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "connection_id", + "message", + "timestamp" + ] + }, + "MessageHistoryRequest": { + "type": "object", + "properties": { + "asn": { + "description": "ASN of the BGP router", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "direction": { + "nullable": true, + "description": "Optional direction filter - if None, returns both sent and received", + "allOf": [ + { + "$ref": "#/components/schemas/MessageDirection" + } + ] + }, + "peer": { + "nullable": true, + "description": "Optional peer filter - if None, returns history for all peers", + "type": "string", + "format": "ip" + } + }, + "required": [ + "asn" + ] + }, + "MessageHistoryResponse": { + "type": "object", + "properties": { + "by_peer": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/MessageHistory" + } + } + }, + "required": [ + "by_peer" + ] + }, + "MpReachNlri": { + "description": "MP_REACH_NLRI path attribute\n\nEach variant represents a specific AFI+SAFI combination, providing compile-time guarantees about the address family of routes being announced.\n\n```text 3. Multiprotocol Reachable NLRI - MP_REACH_NLRI (Type Code 14):\n\nThis is an optional non-transitive attribute that can be used for the following purposes:\n\n(a) to advertise a feasible route to a peer\n\n(b) to permit a router to advertise the Network Layer address of the router that should be used as the next hop to the destinations listed in the Network Layer Reachability Information field of the MP_NLRI attribute.\n\nThe attribute is encoded as shown below:\n\n+---------------------------------------------------------+ | Address Family Identifier (2 octets) | +---------------------------------------------------------+ ```", + "oneOf": [ + { + "description": "IPv4 Unicast routes (AFI=1, SAFI=1)", + "type": "object", + "properties": { + "afi_safi": { + "type": "string", + "enum": [ + "ipv4_unicast" + ] + }, + "nexthop": { + "description": "Next-hop for IPv4 routes.\n\nCurrently must be `BgpNexthop::Ipv4`, but will support IPv6 nexthops when extended next-hop capability (RFC 8950) is implemented.", + "allOf": [ + { + "$ref": "#/components/schemas/BgpNexthop" + } + ] + }, + "nlri": { + "description": "IPv4 prefixes being announced", + "type": "array", + "items": { + "$ref": "#/components/schemas/Prefix4" + } + }, + "reserved": { + "description": "Reserved byte from RFC 4760 §3 (historically \"Number of SNPAs\" in RFC 2858). MUST be 0 per RFC 4760, but MUST be ignored by receiver. Stored for validation logging in session layer. This field is positioned before NLRI to match the wire format encoding.", + "type": "integer", + "format": "uint8", + "minimum": 0 + } + }, + "required": [ + "afi_safi", + "nexthop", + "nlri", + "reserved" + ] + }, + { + "description": "IPv6 Unicast routes (AFI=2, SAFI=1)", + "type": "object", + "properties": { + "afi_safi": { + "type": "string", + "enum": [ + "ipv6_unicast" + ] + }, + "nexthop": { + "description": "Next-hop for IPv6 routes.\n\nCan be `BgpNexthop::Ipv6Single` (16 bytes) or `BgpNexthop::Ipv6Double` (32 bytes with link-local address).", + "allOf": [ + { + "$ref": "#/components/schemas/BgpNexthop" + } + ] + }, + "nlri": { + "description": "IPv6 prefixes being announced", + "type": "array", + "items": { + "$ref": "#/components/schemas/Prefix6" + } + }, + "reserved": { + "description": "Reserved byte from RFC 4760 §3 (historically \"Number of SNPAs\" in RFC 2858). MUST be 0 per RFC 4760, but MUST be ignored by receiver. Stored for validation logging in session layer. This field is positioned before NLRI to match the wire format encoding.", + "type": "integer", + "format": "uint8", + "minimum": 0 + } + }, + "required": [ + "afi_safi", + "nexthop", + "nlri", + "reserved" + ] + } + ] + }, + "MpUnreachNlri": { + "description": "MP_UNREACH_NLRI path attribute\n\nEach variant represents a specific AFI+SAFI combination, providing compile-time guarantees about the address family of routes being withdrawn.\n\n```text 4. Multiprotocol Unreachable NLRI - MP_UNREACH_NLRI (Type Code 15):\n\nThis is an optional non-transitive attribute that can be used for the purpose of withdrawing multiple unfeasible routes from service.\n\nThe attribute is encoded as shown below:\n\n+---------------------------------------------------------+ | Address Family Identifier (2 octets) | +---------------------------------------------------------+ | Subsequent Address Family Identifier (1 octet) | +---------------------------------------------------------+ | Withdrawn Routes (variable) | +---------------------------------------------------------+ ```", + "oneOf": [ + { + "description": "IPv4 Unicast routes being withdrawn (AFI=1, SAFI=1)", + "type": "object", + "properties": { + "afi_safi": { + "type": "string", + "enum": [ + "ipv4_unicast" + ] + }, + "withdrawn": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Prefix4" + } + } + }, + "required": [ + "afi_safi", + "withdrawn" + ] + }, + { + "description": "IPv6 Unicast routes being withdrawn (AFI=2, SAFI=1)", + "type": "object", + "properties": { + "afi_safi": { + "type": "string", + "enum": [ + "ipv6_unicast" + ] + }, + "withdrawn": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Prefix6" + } + } + }, + "required": [ + "afi_safi", + "withdrawn" + ] + } + ] + }, + "Neighbor": { + "description": "Neighbor configuration with explicit per-address-family enablement (v3 API)", + "type": "object", + "properties": { + "asn": { + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "communities": { + "type": "array", + "items": { + "type": "integer", + "format": "uint32", + "minimum": 0 + } + }, + "connect_retry": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "connect_retry_jitter": { + "nullable": true, + "description": "Jitter range for connect_retry timer. When used, the connect_retry timer is multiplied by a random value within the (min, max) range supplied. Useful to help break repeated synchronization of connection collisions.", + "allOf": [ + { + "$ref": "#/components/schemas/JitterRange" + } + ] + }, + "delay_open": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "deterministic_collision_resolution": { + "description": "Enable deterministic collision resolution in Established state. When true, uses BGP-ID comparison per RFC 4271 §6.8 for collision resolution even when one connection is already in Established state. When false, Established connection always wins (timing-based resolution).", + "type": "boolean" + }, + "enforce_first_as": { + "type": "boolean" + }, + "group": { + "type": "string" + }, + "hold_time": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "host": { + "type": "string" + }, + "idle_hold_jitter": { + "nullable": true, + "description": "Jitter range for idle hold timer. When used, the idle hold timer is multiplied by a random value within the (min, max) range supplied. Useful to help break repeated synchronization of connection collisions.", + "allOf": [ + { + "$ref": "#/components/schemas/JitterRange" + } + ] + }, + "idle_hold_time": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "ipv4_unicast": { + "nullable": true, + "description": "IPv4 Unicast address family configuration (None = disabled)", + "allOf": [ + { + "$ref": "#/components/schemas/Ipv4UnicastConfig" + } + ] + }, + "ipv6_unicast": { + "nullable": true, + "description": "IPv6 Unicast address family configuration (None = disabled)", + "allOf": [ + { + "$ref": "#/components/schemas/Ipv6UnicastConfig" + } + ] + }, + "keepalive": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "local_pref": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "md5_auth_key": { + "nullable": true, + "type": "string" + }, + "min_ttl": { + "nullable": true, + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "multi_exit_discriminator": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "name": { + "type": "string" + }, + "passive": { + "type": "boolean" + }, + "remote_asn": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "resolution": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "vlan_id": { + "nullable": true, + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + "required": [ + "asn", + "communities", + "connect_retry", + "delay_open", + "deterministic_collision_resolution", + "enforce_first_as", + "group", + "hold_time", + "host", + "idle_hold_time", + "keepalive", + "name", + "passive", + "resolution" + ] + }, + "NeighborResetOp": { + "description": "V2 API neighbor reset operations with per-AF support", + "oneOf": [ + { + "description": "Hard reset - closes TCP connection and restarts session", + "type": "string", + "enum": [ + "Hard" + ] + }, + { + "description": "Soft inbound reset - sends route refresh for specified AF(s) None means all negotiated AFs", + "type": "object", + "properties": { + "SoftInbound": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/Afi" + } + ] + } + }, + "required": [ + "SoftInbound" + ], + "additionalProperties": false + }, + { + "description": "Soft outbound reset - re-advertises routes for specified AF(s) None means all negotiated AFs", + "type": "object", + "properties": { + "SoftOutbound": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/Afi" + } + ] + } + }, + "required": [ + "SoftOutbound" + ], + "additionalProperties": false + } + ] + }, + "NeighborResetRequest": { + "type": "object", + "properties": { + "addr": { + "type": "string", + "format": "ip" + }, + "asn": { + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "op": { + "$ref": "#/components/schemas/NeighborResetOp" + } + }, + "required": [ + "addr", + "asn", + "op" + ] + }, + "NotificationMessage": { + "description": "Notification messages are exchanged between BGP peers when an exceptional event has occurred.", + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0 + } + }, + "error_code": { + "description": "Error code associated with the notification", + "allOf": [ + { + "$ref": "#/components/schemas/ErrorCode" + } + ] + }, + "error_subcode": { + "description": "Error subcode associated with the notification", + "allOf": [ + { + "$ref": "#/components/schemas/ErrorSubcode" + } + ] + } + }, + "required": [ + "data", + "error_code", + "error_subcode" + ] + }, + "OpenErrorSubcode": { + "description": "Open message error subcode types", + "type": "string", + "enum": [ + "unspecific", + "unsupported_version_number", + "bad_peer_a_s", + "bad_bgp_identifier", + "unsupported_optional_parameter", + "deprecated", + "unacceptable_hold_time", + "unsupported_capability" + ] + }, + "OpenMessage": { + "description": "The first message sent by each side once a TCP connection is established.\n\n```text 0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Version | My Autonomous System | Hold Time : +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ : | BGP Identifier : +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ : | Opt Parm Len | Optional Parameters : +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ : : : Optional Parameters (cont, variable) : : | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ ```\n\nRef: RFC 4271 §4.2", + "type": "object", + "properties": { + "asn": { + "description": "Autonomous system number of the sender. When 4-byte ASNs are in use this value is set to AS_TRANS which has a value of 23456.\n\nRef: RFC 4893 §7", + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "hold_time": { + "description": "Number of seconds the sender proposes for the hold timer.", + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "id": { + "description": "BGP identifier of the sender", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "parameters": { + "description": "A list of optional parameters.", + "type": "array", + "items": { + "$ref": "#/components/schemas/OptionalParameter" + } + }, + "version": { + "description": "BGP protocol version.", + "type": "integer", + "format": "uint8", + "minimum": 0 + } + }, + "required": [ + "asn", + "hold_time", + "id", + "parameters", + "version" + ] + }, + "OptionalParameter": { + "description": "The IANA/IETF currently defines the following optional parameter types.", + "oneOf": [ + { + "description": "Code 0", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "reserved" + ] + } + }, + "required": [ + "type" + ] + }, + { + "description": "Code 1: RFC 4217, RFC 5492 (deprecated)", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "authentication" + ] + } + }, + "required": [ + "type" + ] + }, + { + "description": "Code 2: RFC 5492", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "capabilities" + ] + }, + "value": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Capability" + }, + "uniqueItems": true + } + }, + "required": [ + "type", + "value" + ] + }, + { + "description": "Unassigned", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "unassigned" + ] + } + }, + "required": [ + "type" + ] + }, + { + "description": "Code 255: RFC 9072", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "extended_length" + ] + } + }, + "required": [ + "type" + ] + } + ] + }, + "Origin4": { + "type": "object", + "properties": { + "asn": { + "description": "ASN of the router to originate from.", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "prefixes": { + "description": "Set of prefixes to originate.", + "type": "array", + "items": { + "$ref": "#/components/schemas/Prefix4" + } + } + }, + "required": [ + "asn", + "prefixes" + ] + }, + "Origin6": { + "type": "object", + "properties": { + "asn": { + "description": "ASN of the router to originate from.", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "prefixes": { + "description": "Set of prefixes to originate.", + "type": "array", + "items": { + "$ref": "#/components/schemas/Prefix6" + } + } + }, + "required": [ + "asn", + "prefixes" + ] + }, + "Path": { + "type": "object", + "properties": { + "bgp": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/BgpPathProperties" + } + ] + }, + "nexthop": { + "type": "string", + "format": "ip" + }, + "rib_priority": { + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "shutdown": { + "type": "boolean" + }, + "vlan_id": { + "nullable": true, + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + "required": [ + "nexthop", + "rib_priority", + "shutdown" + ] + }, + "PathAttribute": { + "description": "A self-describing BGP path attribute", + "type": "object", + "properties": { + "typ": { + "description": "Type encoding for the attribute", + "allOf": [ + { + "$ref": "#/components/schemas/PathAttributeType" + } + ] + }, + "value": { + "description": "Value of the attribute", + "allOf": [ + { + "$ref": "#/components/schemas/PathAttributeValue" + } + ] + } + }, + "required": [ + "typ", + "value" + ] + }, + "PathAttributeType": { + "description": "Type encoding for a path attribute.", + "type": "object", + "properties": { + "flags": { + "description": "Flags may include, Optional, Transitive, Partial and Extended Length.", + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "type_code": { + "description": "Type code for the path attribute.", + "allOf": [ + { + "$ref": "#/components/schemas/PathAttributeTypeCode" + } + ] + } + }, + "required": [ + "flags", + "type_code" + ] + }, + "PathAttributeTypeCode": { + "description": "An enumeration describing available path attribute type codes.", + "oneOf": [ + { + "type": "string", + "enum": [ + "as_path", + "next_hop", + "multi_exit_disc", + "local_pref", + "atomic_aggregate", + "aggregator", + "communities", + "mp_unreach_nlri", + "as4_aggregator" + ] + }, + { + "description": "RFC 4271", + "type": "string", + "enum": [ + "origin" + ] + }, + { + "description": "RFC 4760", + "type": "string", + "enum": [ + "mp_reach_nlri" + ] + }, + { + "description": "RFC 6793", + "type": "string", + "enum": [ + "as4_path" + ] + } + ] + }, + "PathAttributeValue": { + "description": "The value encoding of a path attribute.", + "oneOf": [ + { + "description": "The type of origin associated with a path", + "type": "object", + "properties": { + "origin": { + "$ref": "#/components/schemas/PathOrigin" + } + }, + "required": [ + "origin" + ], + "additionalProperties": false + }, + { + "description": "The AS set associated with a path", + "type": "object", + "properties": { + "as_path": { + "type": "array", + "items": { + "$ref": "#/components/schemas/As4PathSegment" + } + } + }, + "required": [ + "as_path" + ], + "additionalProperties": false + }, + { + "description": "The nexthop associated with a path (IPv4 only for traditional BGP)", + "type": "object", + "properties": { + "next_hop": { + "type": "string", + "format": "ipv4" + } + }, + "required": [ + "next_hop" + ], + "additionalProperties": false + }, + { + "description": "A metric used for external (inter-AS) links to discriminate among multiple entry or exit points.", + "type": "object", + "properties": { + "multi_exit_disc": { + "type": "integer", + "format": "uint32", + "minimum": 0 + } + }, + "required": [ + "multi_exit_disc" + ], + "additionalProperties": false + }, + { + "description": "Local pref is included in update messages sent to internal peers and indicates a degree of preference.", + "type": "object", + "properties": { + "local_pref": { + "type": "integer", + "format": "uint32", + "minimum": 0 + } + }, + "required": [ + "local_pref" + ], + "additionalProperties": false + }, + { + "description": "AGGREGATOR: AS number and IP address of the last aggregating BGP speaker (2-octet ASN)", + "type": "object", + "properties": { + "aggregator": { + "$ref": "#/components/schemas/Aggregator" + } + }, + "required": [ + "aggregator" + ], + "additionalProperties": false + }, + { + "description": "Indicates communities associated with a path.", + "type": "object", + "properties": { + "communities": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Community" + } + } + }, + "required": [ + "communities" + ], + "additionalProperties": false + }, + { + "description": "Indicates this route was formed via aggregation (RFC 4271 §5.1.7)", + "type": "string", + "enum": [ + "atomic_aggregate" + ] + }, + { + "description": "The 4-byte encoded AS set associated with a path", + "type": "object", + "properties": { + "as4_path": { + "type": "array", + "items": { + "$ref": "#/components/schemas/As4PathSegment" + } + } + }, + "required": [ + "as4_path" + ], + "additionalProperties": false + }, + { + "description": "AS4_AGGREGATOR: AS number and IP address of the last aggregating BGP speaker (4-octet ASN)", + "type": "object", + "properties": { + "as4_aggregator": { + "$ref": "#/components/schemas/As4Aggregator" + } + }, + "required": [ + "as4_aggregator" + ], + "additionalProperties": false + }, + { + "description": "Carries reachable MP-BGP NLRI and Next-hop (advertisement).", + "type": "object", + "properties": { + "mp_reach_nlri": { + "$ref": "#/components/schemas/MpReachNlri" + } + }, + "required": [ + "mp_reach_nlri" + ], + "additionalProperties": false + }, + { + "description": "Carries unreachable MP-BGP NLRI (withdrawal).", + "type": "object", + "properties": { + "mp_unreach_nlri": { + "$ref": "#/components/schemas/MpUnreachNlri" + } + }, + "required": [ + "mp_unreach_nlri" + ], + "additionalProperties": false + } + ] + }, + "PathOrigin": { + "description": "An enumeration indicating the origin type of a path.", + "oneOf": [ + { + "description": "Interior gateway protocol", + "type": "string", + "enum": [ + "igp" + ] + }, + { + "description": "Exterior gateway protocol", + "type": "string", + "enum": [ + "egp" + ] + }, + { + "description": "Incomplete path origin", + "type": "string", + "enum": [ + "incomplete" + ] + } + ] + }, + "PeerCounters": { + "description": "Session-level counters that persist across connection changes These serve as aggregate counters across all connections for the session", + "type": "object", + "properties": { + "active_connections_accepted": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "active_connections_declined": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "connection_retries": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "connector_panics": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "hold_timer_expirations": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "idle_hold_timer_expirations": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "keepalive_send_failure": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "keepalives_received": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "keepalives_sent": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "md5_auth_failures": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "notification_send_failure": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "notifications_received": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "notifications_sent": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "open_handle_failures": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "open_send_failure": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "opens_received": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "opens_sent": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "passive_connections_accepted": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "passive_connections_declined": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "prefixes_advertised": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "prefixes_imported": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "route_refresh_received": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "route_refresh_send_failure": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "route_refresh_sent": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "tcp_connection_failure": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "transitions_to_active": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "transitions_to_connect": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "transitions_to_connection_collision": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "transitions_to_established": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "transitions_to_idle": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "transitions_to_open_confirm": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "transitions_to_open_sent": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "transitions_to_session_setup": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "unexpected_keepalive_message": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "unexpected_notification_message": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "unexpected_open_message": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "unexpected_route_refresh_message": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "unexpected_update_message": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "unnegotiated_address_family": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "update_nexhop_missing": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "update_send_failure": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "updates_received": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "updates_sent": { + "type": "integer", + "format": "uint64", + "minimum": 0 + } + }, + "required": [ + "active_connections_accepted", + "active_connections_declined", + "connection_retries", + "connector_panics", + "hold_timer_expirations", + "idle_hold_timer_expirations", + "keepalive_send_failure", + "keepalives_received", + "keepalives_sent", + "md5_auth_failures", + "notification_send_failure", + "notifications_received", + "notifications_sent", + "open_handle_failures", + "open_send_failure", + "opens_received", + "opens_sent", + "passive_connections_accepted", + "passive_connections_declined", + "prefixes_advertised", + "prefixes_imported", + "route_refresh_received", + "route_refresh_send_failure", + "route_refresh_sent", + "tcp_connection_failure", + "transitions_to_active", + "transitions_to_connect", + "transitions_to_connection_collision", + "transitions_to_established", + "transitions_to_idle", + "transitions_to_open_confirm", + "transitions_to_open_sent", + "transitions_to_session_setup", + "unexpected_keepalive_message", + "unexpected_notification_message", + "unexpected_open_message", + "unexpected_route_refresh_message", + "unexpected_update_message", + "unnegotiated_address_family", + "update_nexhop_missing", + "update_send_failure", + "updates_received", + "updates_sent" + ] + }, + "PeerInfo": { + "type": "object", + "properties": { + "asn": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "counters": { + "$ref": "#/components/schemas/PeerCounters" + }, + "fsm_state": { + "$ref": "#/components/schemas/FsmStateKind" + }, + "fsm_state_duration": { + "$ref": "#/components/schemas/Duration" + }, + "id": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "ipv4_unicast": { + "$ref": "#/components/schemas/Ipv4UnicastConfig" + }, + "ipv6_unicast": { + "$ref": "#/components/schemas/Ipv6UnicastConfig" + }, + "local_ip": { + "type": "string", + "format": "ip" + }, + "local_tcp_port": { + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "name": { + "type": "string" + }, + "peer_group": { + "type": "string" + }, + "received_capabilities": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BgpCapability" + } + }, + "remote_ip": { + "type": "string", + "format": "ip" + }, + "remote_tcp_port": { + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "timers": { + "$ref": "#/components/schemas/PeerTimers" + } + }, + "required": [ + "counters", + "fsm_state", + "fsm_state_duration", + "ipv4_unicast", + "ipv6_unicast", + "local_ip", + "local_tcp_port", + "name", + "peer_group", + "received_capabilities", + "remote_ip", + "remote_tcp_port", + "timers" + ] + }, + "PeerTimers": { + "type": "object", + "properties": { + "connect_retry": { + "$ref": "#/components/schemas/StaticTimerInfo" + }, + "connect_retry_jitter": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/JitterRange" + } + ] + }, + "delay_open": { + "$ref": "#/components/schemas/StaticTimerInfo" + }, + "hold": { + "$ref": "#/components/schemas/DynamicTimerInfo" + }, + "idle_hold": { + "$ref": "#/components/schemas/StaticTimerInfo" + }, + "idle_hold_jitter": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/JitterRange" + } + ] + }, + "keepalive": { + "$ref": "#/components/schemas/DynamicTimerInfo" + } + }, + "required": [ + "connect_retry", + "delay_open", + "hold", + "idle_hold", + "keepalive" + ] + }, + "PendingUnnumberedNeighbor": { + "type": "object", + "properties": { + "interface": { + "type": "string" + }, + "local_addr": { + "type": "string", + "format": "ipv6" + } + }, + "required": [ + "interface", + "local_addr" + ] + }, + "Prefix": { + "oneOf": [ + { + "type": "object", + "properties": { + "V4": { + "$ref": "#/components/schemas/Prefix4" + } + }, + "required": [ + "V4" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "V6": { + "$ref": "#/components/schemas/Prefix6" + } + }, + "required": [ + "V6" + ], + "additionalProperties": false + } + ] + }, + "Prefix4": { + "type": "object", + "properties": { + "length": { + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "value": { + "type": "string", + "format": "ipv4" + } + }, + "required": [ + "length", + "value" + ] + }, + "Prefix6": { + "type": "object", + "properties": { + "length": { + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "value": { + "type": "string", + "format": "ipv6" + } + }, + "required": [ + "length", + "value" + ] + }, + "Rib": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Path" + }, + "uniqueItems": true + } + }, + "RouteRefreshMessage": { + "type": "object", + "properties": { + "afi": { + "description": "Address family identifier.", + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "safi": { + "description": "Subsequent address family identifier.", + "type": "integer", + "format": "uint8", + "minimum": 0 + } + }, + "required": [ + "afi", + "safi" + ] + }, + "Router": { + "type": "object", + "properties": { + "asn": { + "description": "Autonomous system number for this router", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "graceful_shutdown": { + "description": "Gracefully shut this router down.", + "type": "boolean" + }, + "id": { + "description": "Id for this router", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "listen": { + "description": "Listening address :", + "type": "string" + } + }, + "required": [ + "asn", + "graceful_shutdown", + "id", + "listen" + ] + }, + "SessionMode": { + "type": "string", + "enum": [ + "SingleHop", + "MultiHop" + ] + }, + "ShaperSource": { + "type": "object", + "properties": { + "asn": { + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "code": { + "type": "string" + } + }, + "required": [ + "asn", + "code" + ] + }, + "StaticRoute4": { + "type": "object", + "properties": { + "nexthop": { + "type": "string", + "format": "ipv4" + }, + "prefix": { + "$ref": "#/components/schemas/Prefix4" + }, + "rib_priority": { + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "vlan_id": { + "nullable": true, + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + "required": [ + "nexthop", + "prefix", + "rib_priority" + ] + }, + "StaticRoute4List": { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/components/schemas/StaticRoute4" + } + } + }, + "required": [ + "list" + ] + }, + "StaticRoute6": { + "type": "object", + "properties": { + "nexthop": { + "type": "string", + "format": "ipv6" + }, + "prefix": { + "$ref": "#/components/schemas/Prefix6" + }, + "rib_priority": { + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "vlan_id": { + "nullable": true, + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + "required": [ + "nexthop", + "prefix", + "rib_priority" + ] + }, + "StaticRoute6List": { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/components/schemas/StaticRoute6" + } + } + }, + "required": [ + "list" + ] + }, + "StaticTimerInfo": { + "description": "Timer information for static (non-negotiated) timers", + "type": "object", + "properties": { + "configured": { + "$ref": "#/components/schemas/Duration" + }, + "remaining": { + "$ref": "#/components/schemas/Duration" + } + }, + "required": [ + "configured", + "remaining" + ] + }, + "SwitchIdentifiers": { + "description": "Identifiers for a switch.", + "type": "object", + "properties": { + "slot": { + "nullable": true, + "description": "The slot number of the switch being managed.\n\nMGS uses u16 for this internally.", + "type": "integer", + "format": "uint16", + "minimum": 0 + } + } + }, + "UnnumberedBgpPeerConfig": { + "type": "object", + "properties": { + "communities": { + "type": "array", + "items": { + "type": "integer", + "format": "uint32", + "minimum": 0 + } + }, + "connect_retry": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "connect_retry_jitter": { + "nullable": true, + "description": "Jitter range for connect_retry timer. When used, the connect_retry timer is multiplied by a random value within the (min, max) range supplied. Useful to help break repeated synchronization of connection collisions.", + "allOf": [ + { + "$ref": "#/components/schemas/JitterRange" + } + ] + }, + "delay_open": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "deterministic_collision_resolution": { + "description": "Enable deterministic collision resolution in Established state. When true, uses BGP-ID comparison per RFC 4271 §6.8 for collision resolution even when one connection is already in Established state. When false, Established connection always wins (timing-based resolution).", + "type": "boolean" + }, + "enforce_first_as": { + "type": "boolean" + }, + "hold_time": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "idle_hold_jitter": { + "nullable": true, + "description": "Jitter range for idle hold timer. When used, the idle hold timer is multiplied by a random value within the (min, max) range supplied. Useful to help break repeated synchronization of connection collisions.", + "allOf": [ + { + "$ref": "#/components/schemas/JitterRange" + } + ] + }, + "idle_hold_time": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "interface": { + "type": "string" + }, + "ipv4_unicast": { + "nullable": true, + "description": "IPv4 Unicast address family configuration (None = disabled)", + "allOf": [ + { + "$ref": "#/components/schemas/Ipv4UnicastConfig" + } + ] + }, + "ipv6_unicast": { + "nullable": true, + "description": "IPv6 Unicast address family configuration (None = disabled)", + "allOf": [ + { + "$ref": "#/components/schemas/Ipv6UnicastConfig" + } + ] + }, + "keepalive": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "local_pref": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "md5_auth_key": { + "nullable": true, + "type": "string" + }, + "min_ttl": { + "nullable": true, + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "multi_exit_discriminator": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "name": { + "type": "string" + }, + "passive": { + "type": "boolean" + }, + "remote_asn": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "resolution": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "router_lifetime": { + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "vlan_id": { + "nullable": true, + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + "required": [ + "communities", + "connect_retry", + "delay_open", + "deterministic_collision_resolution", + "enforce_first_as", + "hold_time", + "idle_hold_time", + "interface", + "keepalive", + "name", + "passive", + "resolution", + "router_lifetime" + ] + }, + "UnnumberedNeighbor": { + "type": "object", + "properties": { + "act_as_a_default_ipv6_router": { + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "asn": { + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "communities": { + "type": "array", + "items": { + "type": "integer", + "format": "uint32", + "minimum": 0 + } + }, + "connect_retry": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "connect_retry_jitter": { + "nullable": true, + "description": "Jitter range for connect_retry timer. When used, the connect_retry timer is multiplied by a random value within the (min, max) range supplied. Useful to help break repeated synchronization of connection collisions.", + "allOf": [ + { + "$ref": "#/components/schemas/JitterRange" + } + ] + }, + "delay_open": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "deterministic_collision_resolution": { + "description": "Enable deterministic collision resolution in Established state. When true, uses BGP-ID comparison per RFC 4271 §6.8 for collision resolution even when one connection is already in Established state. When false, Established connection always wins (timing-based resolution).", + "type": "boolean" + }, + "enforce_first_as": { + "type": "boolean" + }, + "group": { + "type": "string" + }, + "hold_time": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "idle_hold_jitter": { + "nullable": true, + "description": "Jitter range for idle hold timer. When used, the idle hold timer is multiplied by a random value within the (min, max) range supplied. Useful to help break repeated synchronization of connection collisions.", + "allOf": [ + { + "$ref": "#/components/schemas/JitterRange" + } + ] + }, + "idle_hold_time": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "interface": { + "type": "string" + }, + "ipv4_unicast": { + "nullable": true, + "description": "IPv4 Unicast address family configuration (None = disabled)", + "allOf": [ + { + "$ref": "#/components/schemas/Ipv4UnicastConfig" + } + ] + }, + "ipv6_unicast": { + "nullable": true, + "description": "IPv6 Unicast address family configuration (None = disabled)", + "allOf": [ + { + "$ref": "#/components/schemas/Ipv6UnicastConfig" + } + ] + }, + "keepalive": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "local_pref": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "md5_auth_key": { + "nullable": true, + "type": "string" + }, + "min_ttl": { + "nullable": true, + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "multi_exit_discriminator": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "name": { + "type": "string" + }, + "passive": { + "type": "boolean" + }, + "remote_asn": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "resolution": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "vlan_id": { + "nullable": true, + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + "required": [ + "act_as_a_default_ipv6_router", + "asn", + "communities", + "connect_retry", + "delay_open", + "deterministic_collision_resolution", + "enforce_first_as", + "group", + "hold_time", + "idle_hold_time", + "interface", + "keepalive", + "name", + "passive", + "resolution" + ] + }, + "UnnumberedNeighborResetRequest": { + "type": "object", + "properties": { + "asn": { + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "interface": { + "type": "string" + }, + "op": { + "$ref": "#/components/schemas/NeighborResetOp" + } + }, + "required": [ + "asn", + "interface", + "op" + ] + }, + "UpdateErrorSubcode": { + "description": "Update message error subcode types", + "type": "string", + "enum": [ + "unspecific", + "malformed_attribute_list", + "unrecognized_well_known_attribute", + "missing_well_known_attribute", + "attribute_flags", + "attribute_length", + "invalid_origin_attribute", + "deprecated", + "invalid_nexthop_attribute", + "optional_attribute", + "invalid_network_field", + "malformed_as_path" + ] + }, + "UpdateMessage": { + "description": "An update message is used to advertise feasible routes that share common path attributes to a peer, or to withdraw multiple unfeasible routes from service.\n\n```text 0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Witdrawn Length | Withdrawn Routes : +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ : : : Withdrawn Routes (cont, variable) : : | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Path Attribute Length | Path Attributes : +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ : : : Path Attributes (cont, variable) : : | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ : : : Network Layer Reachability Information (variable) : : | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ ```\n\nRef: RFC 4271 §4.3", + "type": "object", + "properties": { + "nlri": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Prefix4" + } + }, + "path_attributes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PathAttribute" + } + }, + "withdrawn": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Prefix4" + } + } + }, + "required": [ + "nlri", + "path_attributes", + "withdrawn" + ] + }, + "AddressFamily": { + "description": "Represents the address family (protocol version) for network routes.\n\nThis is the canonical source of truth for address family definitions across the entire codebase. All routing-related components (RIB operations, BGP messages, API filtering, CLI tools) use this single enum rather than defining their own.\n\n# Semantics\n\nWhen used in filtering contexts (e.g., database queries or API parameters), `Option` is preferred: - `None` = no filter (match all address families) - `Some(Ipv4)` = IPv4 routes only - `Some(Ipv6)` = IPv6 routes only\n\n# Examples\n\n``` use rdb_types::AddressFamily;\n\nlet ipv4 = AddressFamily::Ipv4; let ipv6 = AddressFamily::Ipv6;\n\n// For filtering, use Option let filter: Option = Some(AddressFamily::Ipv4); let no_filter: Option = None; // matches all families ```", + "oneOf": [ + { + "description": "Internet Protocol Version 4 (IPv4)", + "type": "string", + "enum": [ + "Ipv4" + ] + }, + { + "description": "Internet Protocol Version 6 (IPv6)", + "type": "string", + "enum": [ + "Ipv6" + ] + } + ] + }, + "ProtocolFilter": { + "oneOf": [ + { + "description": "BGP routes only", + "type": "string", + "enum": [ + "Bgp" + ] + }, + { + "description": "Static routes only", + "type": "string", + "enum": [ + "Static" + ] + } + ] + } + }, + "responses": { + "Error": { + "description": "Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } +} diff --git a/openapi/mg-admin/mg-admin-latest.json b/openapi/mg-admin/mg-admin-latest.json index bb139baa..b10ce4c9 120000 --- a/openapi/mg-admin/mg-admin-latest.json +++ b/openapi/mg-admin/mg-admin-latest.json @@ -1 +1 @@ -mg-admin-4.0.0-9d15bb.json \ No newline at end of file +mg-admin-5.0.0-ca4879.json \ No newline at end of file diff --git a/rdb/Cargo.toml b/rdb/Cargo.toml index b4c228e2..7cce9782 100644 --- a/rdb/Cargo.toml +++ b/rdb/Cargo.toml @@ -18,6 +18,7 @@ chrono.workspace = true clap = { workspace = true, optional = true } oxnet.workspace = true rdb-types = { workspace = true, features = ["clap"] } +ndp.workspace = true [dev-dependencies] proptest.workspace = true diff --git a/rdb/src/db.rs b/rdb/src/db.rs index 6738479b..0ed237fa 100644 --- a/rdb/src/db.rs +++ b/rdb/src/db.rs @@ -15,10 +15,11 @@ use crate::log::rdb_log; use crate::types::*; use chrono::Utc; use mg_common::{lock, read_lock, write_lock}; +use ndp::Ipv6NetworkInterface; use sled::Tree; use slog::{Logger, error}; use std::cmp::Ordering as CmpOrdering; -use std::collections::{BTreeMap, BTreeSet}; +use std::collections::{BTreeMap, BTreeSet, HashMap}; use std::net::{IpAddr, Ipv6Addr}; use std::num::NonZeroU8; use std::sync::atomic::{AtomicU64, Ordering}; @@ -45,6 +46,10 @@ const BGP_ROUTER: &str = "bgp_router"; /// information. const BGP_NEIGHBOR: &str = "bgp_neighbor"; +/// The handle used to open a persistent key-value tree for BGP neighbor +/// information. +const BGP_UNNUMBERED_NEIGHBOR: &str = "bgp_unnumbered_neighbor"; + /// The handle used to open a persistent key-value tree for settings /// information. const SETTINGS: &str = "settings"; @@ -101,13 +106,16 @@ pub struct Db { /// A set of watchers that are notified when changes to the data store occur. watchers: Arc>>, - /// Reaps expired routes from the local RIB + /// Reaps expired routes from the local RIB. reaper: Arc, /// Switch slot reported from MGS. /// Information is not available until first successful communication with MGS. slot: Arc>>, + /// A map from peer addresses to the corresponding local interface. + unnumbered_nexthop: Arc>>, + log: Logger, } unsafe impl Sync for Db {} @@ -134,6 +142,7 @@ impl Db { watchers: Arc::new(RwLock::new(Vec::new())), reaper: Reaper::new(rib_loc), slot: Arc::new(RwLock::new(None)), + unnumbered_nexthop: Arc::new(Mutex::new(HashMap::new())), log, }) } @@ -318,6 +327,28 @@ impl Db { Ok(()) } + pub fn add_unnumbered_bgp_neighbor( + &self, + nbr: BgpUnnumberedNeighborInfo, + ) -> Result<(), Error> { + let tree = self.persistent.open_tree(BGP_UNNUMBERED_NEIGHBOR)?; + let key = nbr.interface.clone(); + let value = serde_json::to_string(&nbr)?; + tree.insert(key.as_str(), value.as_str())?; + tree.flush()?; + Ok(()) + } + + pub fn remove_unnumbered_bgp_neighbor( + &self, + interface: &str, + ) -> Result<(), Error> { + let tree = self.persistent.open_tree(BGP_UNNUMBERED_NEIGHBOR)?; + tree.remove(interface)?; + tree.flush()?; + Ok(()) + } + pub fn remove_bgp_neighbor(&self, addr: IpAddr) -> Result<(), Error> { let tree = self.persistent.open_tree(BGP_NEIGHBOR)?; let key = addr.to_string(); @@ -363,6 +394,45 @@ impl Db { Ok(result) } + pub fn get_unnumbered_bgp_neighbors( + &self, + ) -> Result, Error> { + let tree = self.persistent.open_tree(BGP_UNNUMBERED_NEIGHBOR)?; + let result = tree + .scan_prefix(vec![]) + .filter_map(|item| { + let (_key, value) = match item { + Ok(item) => item, + Err(ref e) => { + rdb_log!( + self, + error, + "error fetching unnumbered bgp neighbor entry {item:?}: {e}"; + "unit" => UNIT_PERSISTENT + ); + return None; + } + }; + let value = String::from_utf8_lossy(&value); + let value: BgpUnnumberedNeighborInfo = match serde_json::from_str(&value) + { + Ok(item) => item, + Err(ref e) => { + rdb_log!( + self, + error, + "error parsing unnumbered bgp neighbor entry value {value:?}: {e}"; + "unit" => UNIT_PERSISTENT + ); + return None; + } + }; + Some(value) + }) + .collect(); + Ok(result) + } + pub fn add_bfd_neighbor(&self, cfg: BfdPeerConfig) -> Result<(), Error> { let tree = self.persistent.open_tree(BFD_NEIGHBOR)?; let key = cfg.peer.to_string(); @@ -1293,6 +1363,38 @@ impl Db { AddressFamily::Ipv6 => self.mark_bgp_peer_stale6(peer), } } + + pub fn add_unnumbered_nexthop( + &self, + nexthop: Ipv6Addr, + interface: Ipv6NetworkInterface, + ) { + self.unnumbered_nexthop + .lock() + .unwrap() + .insert(nexthop, interface); + } + + pub fn remove_unnumbered_nexthop_for_interface( + &self, + interface: &Ipv6NetworkInterface, + ) { + self.unnumbered_nexthop + .lock() + .unwrap() + .retain(|_k, v| v != interface); + } + + pub fn get_interface_for_unnumbered_nexthop( + &self, + nexthop: Ipv6Addr, + ) -> Option { + self.unnumbered_nexthop + .lock() + .unwrap() + .get(&nexthop) + .cloned() + } } struct Reaper { diff --git a/rdb/src/proptest.rs b/rdb/src/proptest.rs index c9ee02c7..a4bba03f 100644 --- a/rdb/src/proptest.rs +++ b/rdb/src/proptest.rs @@ -8,9 +8,12 @@ //! correctness and consistency of prefix operations (excluding wire format //! tests, which are in bgp/src/proptest.rs since they test BgpWireFormat). -use crate::types::{ - BgpNeighborInfo, ImportExportPolicy4, ImportExportPolicy6, Prefix, Prefix4, - Prefix6, StaticRouteKey, +use crate::{ + BgpNeighborParameters, + types::{ + BgpNeighborInfo, ImportExportPolicy4, ImportExportPolicy6, Prefix, + Prefix4, Prefix6, StaticRouteKey, + }, }; use proptest::{prelude::*, strategy::Just}; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; @@ -168,30 +171,32 @@ fn bgp_neighbor_info_strategy() -> impl Strategy { asn, name, host, - hold_time: 90, - idle_hold_time: 60, - delay_open: 0, - connect_retry: 30, - keepalive: 30, - resolution: 1000, group: "test".into(), - passive: false, - remote_asn: Some(65001), - min_ttl: Some(1), - md5_auth_key: Some("password".to_string()), - multi_exit_discriminator: Some(100), - communities: vec![], - local_pref: Some(100), - enforce_first_as: false, - ipv4_enabled: true, - ipv6_enabled: true, - allow_import4, - allow_export4, - allow_import6, - allow_export6, - nexthop4, - nexthop6, - vlan_id: Some(1), + parameters: BgpNeighborParameters { + hold_time: 90, + idle_hold_time: 60, + delay_open: 0, + connect_retry: 30, + keepalive: 30, + resolution: 1000, + passive: false, + remote_asn: Some(65001), + min_ttl: Some(1), + md5_auth_key: Some("password".to_string()), + multi_exit_discriminator: Some(100), + communities: vec![], + local_pref: Some(100), + enforce_first_as: false, + ipv4_enabled: true, + ipv6_enabled: true, + allow_import4, + allow_export4, + allow_import6, + allow_export6, + nexthop4, + nexthop6, + vlan_id: Some(1), + }, } }, ) @@ -470,47 +475,47 @@ proptest! { "Host should survive serialization round-trip" ); prop_assert_eq!( - deserialized.nexthop4, neighbor.nexthop4, + deserialized.parameters.nexthop4, neighbor.parameters.nexthop4, "IPv4 nexthop should survive serialization round-trip" ); prop_assert_eq!( - deserialized.nexthop6, neighbor.nexthop6, + deserialized.parameters.nexthop6, neighbor.parameters.nexthop6, "IPv6 nexthop should survive serialization round-trip" ); prop_assert_eq!( - deserialized.ipv4_enabled, neighbor.ipv4_enabled, + deserialized.parameters.ipv4_enabled, neighbor.parameters.ipv4_enabled, "IPv4 enabled flag should survive serialization round-trip" ); prop_assert_eq!( - deserialized.ipv6_enabled, neighbor.ipv6_enabled, + deserialized.parameters.ipv6_enabled, neighbor.parameters.ipv6_enabled, "IPv6 enabled flag should survive serialization round-trip" ); prop_assert_eq!( - deserialized.multi_exit_discriminator, neighbor.multi_exit_discriminator, + deserialized.parameters.multi_exit_discriminator, neighbor.parameters.multi_exit_discriminator, "MED should survive serialization round-trip" ); prop_assert_eq!( - deserialized.local_pref, neighbor.local_pref, + deserialized.parameters.local_pref, neighbor.parameters.local_pref, "Local preference should survive serialization round-trip" ); prop_assert_eq!( - deserialized.remote_asn, neighbor.remote_asn, + deserialized.parameters.remote_asn, neighbor.parameters.remote_asn, "Remote ASN should survive serialization round-trip" ); prop_assert_eq!( - deserialized.allow_import4, neighbor.allow_import4, + deserialized.parameters.allow_import4, neighbor.parameters.allow_import4, "IPv4 import policy should survive serialization round-trip" ); prop_assert_eq!( - deserialized.allow_export4, neighbor.allow_export4, + deserialized.parameters.allow_export4, neighbor.parameters.allow_export4, "IPv4 export policy should survive serialization round-trip" ); prop_assert_eq!( - deserialized.allow_import6, neighbor.allow_import6, + deserialized.parameters.allow_import6, neighbor.parameters.allow_import6, "IPv6 import policy should survive serialization round-trip" ); prop_assert_eq!( - deserialized.allow_export6, neighbor.allow_export6, + deserialized.parameters.allow_export6, neighbor.parameters.allow_export6, "IPv6 export policy should survive serialization round-trip" ); } diff --git a/rdb/src/types.rs b/rdb/src/types.rs index 3454d694..1cc6d43a 100644 --- a/rdb/src/types.rs +++ b/rdb/src/types.rs @@ -527,14 +527,29 @@ pub enum ImportExportPolicy { pub struct BgpNeighborInfo { pub asn: u32, pub name: String, + pub group: String, pub host: SocketAddr, + pub parameters: BgpNeighborParameters, +} + +#[derive(Debug, Serialize, Deserialize, Clone, JsonSchema)] +pub struct BgpUnnumberedNeighborInfo { + pub asn: u32, + pub name: String, + pub group: String, + pub interface: String, + pub router_lifetime: u16, + pub parameters: BgpNeighborParameters, +} + +#[derive(Debug, Serialize, Deserialize, Clone, JsonSchema)] +pub struct BgpNeighborParameters { pub hold_time: u64, pub idle_hold_time: u64, pub delay_open: u64, pub connect_retry: u64, pub keepalive: u64, pub resolution: u64, - pub group: String, pub passive: bool, pub remote_asn: Option, pub min_ttl: Option,