From 025389ff39d594bf2b815377e2c1dc4dd23b1f96 Mon Sep 17 00:00:00 2001 From: Ryan Goodfellow Date: Fri, 3 May 2024 17:25:50 -0700 Subject: [PATCH] 0xPop (#199) --- Cargo.lock | 1070 ++++++++++++------------ Cargo.toml | 14 +- bfd/src/sm.rs | 20 +- bgp/Cargo.toml | 3 + bgp/policy/policy-check0.rhai | 14 + bgp/policy/policy-shape0.rhai | 13 + bgp/policy/shape-prefix0.rhai | 11 + bgp/src/clock.rs | 40 +- bgp/src/connection.rs | 19 + bgp/src/connection_channel.rs | 16 +- bgp/src/connection_tcp.rs | 263 +++++- bgp/src/dispatcher.rs | 8 +- bgp/src/error.rs | 68 +- bgp/src/lib.rs | 3 + bgp/src/log.rs | 10 +- bgp/src/messages.rs | 700 ++++++++++++++-- bgp/src/policy.rs | 550 +++++++++++++ bgp/src/rhai_integration.rs | 193 +++++ bgp/src/router.rs | 228 +++++- bgp/src/session.rs | 1235 +++++++++++++++++++++++----- bgp/src/test.rs | 11 +- clab/.gitignore | 2 + clab/cdn-set-config.sh | 4 + clab/cdn.json | 152 ++++ clab/diagram.svg | 3 + clab/get-bgp.sh | 17 + clab/get-imported.sh | 17 + clab/get-rib.sh | 17 + clab/lab-init.sh | 7 + clab/mgadm | 1 + clab/mgd-addr.sh | 5 + clab/mgd-setup-bad-asn.sh | 11 + clab/mgd-setup-ibgp.sh | 17 + clab/mgd-setup.sh | 61 ++ clab/oxpop.clab.yml | 36 + clab/pceast-set-config.sh | 4 + clab/pceast.json | 128 +++ clab/pcwest-set-config.sh | 4 + clab/pcwest.json | 128 +++ clab/run-mgd.sh | 11 + clab/shaper.rhai | 17 + clab/srlinux-set-med-cli.txt | 1 + clab/terraform/.gitignore | 3 + clab/terraform/checker.rhai | 14 + clab/terraform/main.tf | 162 ++++ clab/terraform/shaper.rhai | 17 + clab/transit-ibgp.json | 96 +++ clab/transit-set-config-ibgp.sh | 4 + clab/transit-set-config.sh | 4 + clab/transit.json | 152 ++++ ddm-admin-client/build.rs | 3 + ddm/src/oxstats.rs | 16 +- ddm/src/sys.rs | 40 +- interop-lab/add-images.sh | 4 + interop-lab/create-containers.sh | 36 + interop-lab/create-network.sh | 11 + interop-lab/destroy-network.sh | 5 + interop-lab/install-docker.sh | 15 + interop-lab/remove.sh | 6 + interop-lab/run-containers.sh | 6 + interop-lab/stop.sh | 6 + mg-admin-client/build.rs | 3 + mg-admin-client/src/lib.rs | 24 +- mg-common/src/nexus.rs | 24 +- mg-lower/src/ddm.rs | 97 ++- mg-lower/src/dendrite.rs | 359 +++++--- mg-lower/src/error.rs | 9 + mg-lower/src/lib.rs | 187 ++--- mgadm/Cargo.toml | 1 + mgadm/src/bgp.rs | 796 ++++++++++++++---- mgadm/src/main.rs | 12 +- mgadm/src/static_routing.rs | 3 + mgd/src/admin.rs | 29 +- mgd/src/bfd_admin.rs | 9 +- mgd/src/bgp_admin.rs | 1129 +++++++++++++++----------- mgd/src/bgp_param.rs | 329 ++++++++ mgd/src/main.rs | 35 +- mgd/src/oxstats.rs | 23 +- mgd/src/static_admin.rs | 57 +- openapi/mg-admin.json | 1306 +++++++++++++++++++++++------- rdb/Cargo.toml | 2 + rdb/src/bestpath.rs | 193 +++++ rdb/src/db.rs | 566 ++++++------- rdb/src/error.rs | 3 + rdb/src/lib.rs | 3 +- rdb/src/types.rs | 285 +++++-- 86 files changed, 8734 insertions(+), 2482 deletions(-) create mode 100644 bgp/policy/policy-check0.rhai create mode 100644 bgp/policy/policy-shape0.rhai create mode 100644 bgp/policy/shape-prefix0.rhai create mode 100644 bgp/src/policy.rs create mode 100644 bgp/src/rhai_integration.rs create mode 100644 clab/.gitignore create mode 100755 clab/cdn-set-config.sh create mode 100644 clab/cdn.json create mode 100644 clab/diagram.svg create mode 100755 clab/get-bgp.sh create mode 100755 clab/get-imported.sh create mode 100755 clab/get-rib.sh create mode 100755 clab/lab-init.sh create mode 120000 clab/mgadm create mode 100755 clab/mgd-addr.sh create mode 100755 clab/mgd-setup-bad-asn.sh create mode 100755 clab/mgd-setup-ibgp.sh create mode 100755 clab/mgd-setup.sh create mode 100644 clab/oxpop.clab.yml create mode 100755 clab/pceast-set-config.sh create mode 100644 clab/pceast.json create mode 100755 clab/pcwest-set-config.sh create mode 100644 clab/pcwest.json create mode 100755 clab/run-mgd.sh create mode 100644 clab/shaper.rhai create mode 100644 clab/srlinux-set-med-cli.txt create mode 100644 clab/terraform/.gitignore create mode 100644 clab/terraform/checker.rhai create mode 100644 clab/terraform/main.tf create mode 100644 clab/terraform/shaper.rhai create mode 100644 clab/transit-ibgp.json create mode 100755 clab/transit-set-config-ibgp.sh create mode 100755 clab/transit-set-config.sh create mode 100644 clab/transit.json create mode 100644 ddm-admin-client/build.rs create mode 100755 interop-lab/add-images.sh create mode 100755 interop-lab/create-containers.sh create mode 100755 interop-lab/create-network.sh create mode 100755 interop-lab/destroy-network.sh create mode 100755 interop-lab/install-docker.sh create mode 100755 interop-lab/remove.sh create mode 100755 interop-lab/run-containers.sh create mode 100755 interop-lab/stop.sh create mode 100644 mg-admin-client/build.rs create mode 100644 mgd/src/bgp_param.rs create mode 100644 rdb/src/bestpath.rs diff --git a/Cargo.lock b/Cargo.lock index 4921c751..8d5f86a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -24,6 +24,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", + "const-random", + "getrandom", "once_cell", "version_check", "zerocopy 0.7.32", @@ -40,9 +42,9 @@ dependencies = [ [[package]] name = "allocator-api2" -version = "0.2.16" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" +checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" [[package]] name = "android-tzdata" @@ -61,47 +63,48 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.13" +version = "0.6.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb" +checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", + "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" +checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" [[package]] name = "anstyle-parse" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5" dependencies = [ "windows-sys 0.52.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.2" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" dependencies = [ "anstyle", "windows-sys 0.52.0", @@ -116,12 +119,12 @@ checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" [[package]] name = "api_identity" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#2349feb36d39804e38539e134a9a3a4462e7d516" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#ee901b00c20a2ccaeabbd1bf27ddb60c95b334b5" dependencies = [ "omicron-workspace-hack", "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.60", ] [[package]] @@ -154,12 +157,6 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" -[[package]] -name = "ascii" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" - [[package]] name = "async-stream" version = "0.3.5" @@ -179,18 +176,18 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.60", ] [[package]] name = "async-trait" -version = "0.1.79" +version = "0.1.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507401cad91ec6a857ed5513a2073c82a9b9048762b885bb98655b306964681" +checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.60", ] [[package]] @@ -199,22 +196,11 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" -[[package]] -name = "atty" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" -dependencies = [ - "hermit-abi 0.1.19", - "libc", - "winapi", -] - [[package]] name = "autocfg" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" [[package]] name = "backoff" @@ -259,9 +245,9 @@ checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" [[package]] name = "base64" -version = "0.22.0" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" @@ -292,40 +278,24 @@ dependencies = [ "anyhow", "chrono", "lazy_static", + "libc", "mg-common", "nom", "num_enum 0.7.2", "pretty-hex 0.4.1", "pretty_assertions", "rdb", + "rhai", "schemars", "serde", "sled", "slog", "slog-async", "slog-bunyan", + "socket2 0.5.7", "thiserror", ] -[[package]] -name = "bhyve_api" -version = "0.0.0" -source = "git+https://github.com/oxidecomputer/propolis?rev=6dceb9ef69c217cb78a2018bbedafbc19f6ec1af#6dceb9ef69c217cb78a2018bbedafbc19f6ec1af" -dependencies = [ - "bhyve_api_sys", - "libc", - "strum", -] - -[[package]] -name = "bhyve_api_sys" -version = "0.0.0" -source = "git+https://github.com/oxidecomputer/propolis?rev=6dceb9ef69c217cb78a2018bbedafbc19f6ec1af#6dceb9ef69c217cb78a2018bbedafbc19f6ec1af" -dependencies = [ - "libc", - "strum", -] - [[package]] name = "bitflags" version = "1.3.2" @@ -338,26 +308,6 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" -[[package]] -name = "bitstruct" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1b10c3912af09af44ea1dafe307edb5ed374b2a32658eb610e372270c9017b4" -dependencies = [ - "bitstruct_derive", -] - -[[package]] -name = "bitstruct_derive" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35fd19022c2b750d14eb9724c204d08ab7544570105b3b466d8a9f2f3feded27" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "blake2" version = "0.10.6" @@ -393,9 +343,15 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.15.4" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "bytecount" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ce89b21cab1437276d2650d57e971f9d548a2d9037cc231abdc0562b97498ce" [[package]] name = "byteorder" @@ -433,9 +389,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.90" +version = "1.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" +checksum = "065a29261d53ba54260972629f9ca6bffa69bac13cd1fed61420f7fa68b9f8bd" [[package]] name = "cfg-if" @@ -455,7 +411,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-targets 0.52.4", + "windows-targets 0.52.5", ] [[package]] @@ -504,7 +460,8 @@ dependencies = [ "anstream", "anstyle", "clap_lex", - "strsim 0.11.0", + "strsim 0.11.1", + "terminal_size", ] [[package]] @@ -516,7 +473,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.60", ] [[package]] @@ -533,9 +490,9 @@ checksum = "67ba02a97a2bd10f4b59b25c7973101c79642302776489e030cd13cdab09ed15" [[package]] name = "colorchoice" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" [[package]] name = "colored" @@ -550,7 +507,7 @@ dependencies = [ [[package]] name = "common" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/dendrite?branch=main#c2a9f29f70b1e05d891c713997577be53826e1bb" +source = "git+https://github.com/oxidecomputer/dendrite?branch=main#3b84ea6516cafb4595a6f2a668df16c1a501b687" dependencies = [ "anyhow", "chrono", @@ -566,6 +523,39 @@ dependencies = [ "thiserror", ] +[[package]] +name = "console" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "unicode-width", + "windows-sys 0.52.0", +] + +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom", + "once_cell", + "tiny-keccak", +] + [[package]] name = "constant_time_eq" version = "0.3.0" @@ -600,9 +590,8 @@ dependencies = [ [[package]] name = "cpuid_profile_config" version = "0.0.0" -source = "git+https://github.com/oxidecomputer/propolis?rev=6dceb9ef69c217cb78a2018bbedafbc19f6ec1af#6dceb9ef69c217cb78a2018bbedafbc19f6ec1af" +source = "git+https://github.com/oxidecomputer/propolis?rev=d6fc6d458e08e7ae1008aaa2d505a6523a4e3538#d6fc6d458e08e7ae1008aaa2d505a6523a4e3538" dependencies = [ - "propolis", "serde", "serde_derive", "thiserror", @@ -705,7 +694,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.10.0", - "syn 2.0.55", + "syn 2.0.60", ] [[package]] @@ -716,14 +705,14 @@ checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" dependencies = [ "darling_core", "quote", - "syn 2.0.55", + "syn 2.0.60", ] [[package]] name = "data-encoding" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" +checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" [[package]] name = "ddm" @@ -734,10 +723,10 @@ dependencies = [ "common", "dpd-client", "dropshot", - "hostname", + "hostname 0.3.1", "hyper", "ispf", - "libnet 0.1.0 (git+https://github.com/oxidecomputer/netadm-sys?branch=main)", + "libnet", "mg-common", "omicron-common", "opte-ioctl", @@ -751,7 +740,7 @@ dependencies = [ "serde_repr", "sled", "slog", - "socket2 0.5.6", + "socket2 0.5.7", "thiserror", "tokio", "uuid 1.8.0", @@ -798,8 +787,8 @@ dependencies = [ "clap", "ddm", "dpd-client", - "hostname", - "libnet 0.1.0 (git+https://github.com/oxidecomputer/netadm-sys?branch=main)", + "hostname 0.3.1", + "libnet", "mg-common", "slog", "slog-async", @@ -835,7 +824,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.60", ] [[package]] @@ -860,12 +849,12 @@ dependencies = [ [[package]] name = "derror-macro" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/opte?branch=master#4cc823b50d3e4a629cdfaab2b3d3382514174ba9" +source = "git+https://github.com/oxidecomputer/opte?rev=7ee353a470ea59529ee1b34729681da887aa88ce#7ee353a470ea59529ee1b34729681da887aa88ce" dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.60", ] [[package]] @@ -906,15 +895,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "dladm" -version = "0.0.0" -source = "git+https://github.com/oxidecomputer/propolis?rev=6dceb9ef69c217cb78a2018bbedafbc19f6ec1af#6dceb9ef69c217cb78a2018bbedafbc19f6ec1af" -dependencies = [ - "libc", - "strum", -] - [[package]] name = "dlpi" version = "0.2.0" @@ -931,10 +911,11 @@ dependencies = [ [[package]] name = "dns-service-client" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#2349feb36d39804e38539e134a9a3a4462e7d516" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#ee901b00c20a2ccaeabbd1bf27ddb60c95b334b5" dependencies = [ "anyhow", "chrono", + "expectorate", "http 0.2.12", "omicron-workspace-hack", "progenitor", @@ -961,7 +942,7 @@ dependencies = [ [[package]] name = "dpd-client" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/dendrite?branch=main#c2a9f29f70b1e05d891c713997577be53826e1bb" +source = "git+https://github.com/oxidecomputer/dendrite?branch=main#3b84ea6516cafb4595a6f2a668df16c1a501b687" dependencies = [ "async-trait", "chrono", @@ -970,7 +951,7 @@ dependencies = [ "futures", "http 0.2.12", "progenitor", - "regress 0.6.0", + "regress", "reqwest", "schemars", "serde", @@ -983,11 +964,11 @@ dependencies = [ [[package]] name = "dropshot" version = "0.10.1-dev" -source = "git+https://github.com/oxidecomputer/dropshot?branch=main#e6691c1818fb952cd29af02f2fe57e77fc591c94" +source = "git+https://github.com/oxidecomputer/dropshot?branch=main#33e158ba518a9b42aa036a0f6b605d73f11d8b23" dependencies = [ "async-stream", "async-trait", - "base64 0.22.0", + "base64 0.22.1", "bytes", "camino", "chrono", @@ -995,7 +976,7 @@ dependencies = [ "dropshot_endpoint", "form_urlencoded", "futures", - "hostname", + "hostname 0.4.0", "http 0.2.12", "hyper", "indexmap 2.2.6", @@ -1003,8 +984,8 @@ dependencies = [ "openapiv3", "paste", "percent-encoding", - "rustls 0.22.2", - "rustls-pemfile 2.1.1", + "rustls 0.22.4", + "rustls-pemfile 2.1.2", "schemars", "serde", "serde_json", @@ -1028,13 +1009,13 @@ dependencies = [ [[package]] name = "dropshot_endpoint" version = "0.10.1-dev" -source = "git+https://github.com/oxidecomputer/dropshot?branch=main#e6691c1818fb952cd29af02f2fe57e77fc591c94" +source = "git+https://github.com/oxidecomputer/dropshot?branch=main#33e158ba518a9b42aa036a0f6b605d73f11d8b23" dependencies = [ "proc-macro2", "quote", "serde", "serde_tokenstream", - "syn 2.0.55", + "syn 2.0.60", ] [[package]] @@ -1056,9 +1037,9 @@ checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" [[package]] name = "either" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" +checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" [[package]] name = "embedded-io" @@ -1066,11 +1047,17 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + [[package]] name = "encoding_rs" -version = "0.8.33" +version = "0.8.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" dependencies = [ "cfg-if", ] @@ -1087,34 +1074,12 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "env_logger" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a12e6657c4c97ebab115a42dcee77225f7f482cdd841cf7088c657a42e9e00e7" -dependencies = [ - "atty", - "humantime", - "log", - "regex", - "termcolor", -] - [[package]] name = "equivalent" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" -[[package]] -name = "erased-serde" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c138974f9d5e7fe373eb04df7cae98833802ae4b11c24ac7039a21d5af4b26c" -dependencies = [ - "serde", -] - [[package]] name = "errno" version = "0.3.8" @@ -1125,11 +1090,22 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "expectorate" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de6f19b25bdfa2747ae775f37cd109c31f1272d4e4c83095be0727840aa1d75f" +dependencies = [ + "console", + "newline-converter", + "similar", +] + [[package]] name = "fastrand" -version = "2.0.2" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984" +checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" [[package]] name = "filetime" @@ -1151,9 +1127,9 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flate2" -version = "1.0.28" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" dependencies = [ "crc32fast", "miniz_oxide", @@ -1192,7 +1168,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.60", ] [[package]] @@ -1282,7 +1258,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.60", ] [[package]] @@ -1327,9 +1303,9 @@ dependencies = [ [[package]] name = "gateway-client" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#2349feb36d39804e38539e134a9a3a4462e7d516" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#ee901b00c20a2ccaeabbd1bf27ddb60c95b334b5" dependencies = [ - "base64 0.22.0", + "base64 0.22.1", "chrono", "gateway-messages", "omicron-workspace-hack", @@ -1379,9 +1355,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.12" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" +checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" dependencies = [ "cfg-if", "libc", @@ -1396,9 +1372,9 @@ checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] name = "goblin" -version = "0.8.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb07a4ffed2093b118a525b1d8f5204ae274faed5604537caf7135d0f18d9887" +checksum = "1b363a30c165f666402fe6a3024d3bec7ebc898f96a4a23bd1c99f8dbf3f4f47" dependencies = [ "log", "plain", @@ -1407,9 +1383,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.25" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fbd2820c5e49886948654ab546d0688ff24530286bdcf8fca3cefb16d4618eb" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" dependencies = [ "bytes", "fnv", @@ -1426,9 +1402,9 @@ dependencies = [ [[package]] name = "half" -version = "2.4.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5eceaaeec696539ddaf7b333340f1af35a5aa87ae3e4f3ead0532f72affab2e" +checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" dependencies = [ "cfg-if", "crunchy", @@ -1451,18 +1427,9 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" -version = "0.13.2" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" -dependencies = [ - "ahash", -] - -[[package]] -name = "hashbrown" -version = "0.14.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ "ahash", "allocator-api2", @@ -1499,15 +1466,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "hermit-abi" -version = "0.1.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" -dependencies = [ - "libc", -] - [[package]] name = "hermit-abi" version = "0.3.9" @@ -1534,6 +1492,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "hostname" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9c7c7c8ac16c798734b8a24560c1362120597c40d5e1459f09498f8f6c8f2ba" +dependencies = [ + "cfg-if", + "libc", + "windows", +] + [[package]] name = "http" version = "0.2.12" @@ -1623,7 +1592,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2 0.5.6", + "socket2 0.5.7", "tokio", "tower-service", "tracing", @@ -1639,7 +1608,7 @@ dependencies = [ "futures-util", "http 0.2.12", "hyper", - "rustls 0.21.10", + "rustls 0.21.12", "tokio", "tokio-rustls 0.24.1", ] @@ -1710,7 +1679,7 @@ dependencies = [ [[package]] name = "illumos-sys-hdrs" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/opte?branch=master#4cc823b50d3e4a629cdfaab2b3d3382514174ba9" +source = "git+https://github.com/oxidecomputer/opte?rev=7ee353a470ea59529ee1b34729681da887aa88ce#7ee353a470ea59529ee1b34729681da887aa88ce" [[package]] name = "indexmap" @@ -1730,7 +1699,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", - "hashbrown 0.14.3", + "hashbrown 0.14.5", "serde", ] @@ -1746,7 +1715,7 @@ dependencies = [ [[package]] name = "internal-dns" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#2349feb36d39804e38539e134a9a3a4462e7d516" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#ee901b00c20a2ccaeabbd1bf27ddb60c95b334b5" dependencies = [ "anyhow", "chrono", @@ -1754,6 +1723,7 @@ dependencies = [ "futures", "hyper", "omicron-common", + "omicron-uuid-kinds", "omicron-workspace-hack", "reqwest", "slog", @@ -1768,7 +1738,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" dependencies = [ - "socket2 0.5.6", + "socket2 0.5.7", "widestring", "windows-sys 0.48.0", "winreg", @@ -1796,11 +1766,17 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" dependencies = [ - "hermit-abi 0.3.9", + "hermit-abi", "libc", "windows-sys 0.52.0", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" + [[package]] name = "ispf" version = "0.1.0" @@ -1829,9 +1805,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "js-sys" @@ -1845,10 +1821,10 @@ dependencies = [ [[package]] name = "kstat-macro" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/opte?branch=master#4cc823b50d3e4a629cdfaab2b3d3382514174ba9" +source = "git+https://github.com/oxidecomputer/opte?rev=7ee353a470ea59529ee1b34729681da887aa88ce#7ee353a470ea59529ee1b34729681da887aa88ce" dependencies = [ "quote", - "syn 2.0.55", + "syn 2.0.60", ] [[package]] @@ -1868,9 +1844,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.153" +version = "0.2.154" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346" [[package]] name = "libdlpi-sys" @@ -1880,7 +1856,7 @@ source = "git+https://github.com/oxidecomputer/dlpi-sys#1d587ea98cf2d36f1b1624b0 [[package]] name = "libfalcon" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/falcon?branch=main#e69694a1f7cc9fe31fab27f321017280531fb5f7" +source = "git+https://github.com/oxidecomputer/falcon?branch=main#34609623da1d5aa5b6e52ba52e3348e13f61bb26" dependencies = [ "anstyle", "anyhow", @@ -1889,7 +1865,7 @@ dependencies = [ "colored", "futures", "libc", - "libnet 0.1.0 (git+https://github.com/oxidecomputer/netadm-sys?branch=main)", + "libnet", "portpicker", "propolis-client", "propolis-server-config", @@ -1921,26 +1897,7 @@ checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" [[package]] name = "libnet" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/netadm-sys?branch=main#1d75565d35765c57dcf1c1a34b56cf5024086fba" -dependencies = [ - "anyhow", - "cfg-if", - "colored", - "dlpi", - "libc", - "num_enum 0.5.11", - "nvpair", - "nvpair-sys", - "rusty-doors", - "socket2 0.4.10", - "thiserror", - "tracing", -] - -[[package]] -name = "libnet" -version = "0.1.0" -source = "git+https://github.com/oxidecomputer/netadm-sys#1d75565d35765c57dcf1c1a34b56cf5024086fba" +source = "git+https://www.github.com/oxidecomputer/netadm-sys.git?branch=main#4ceaf96e02acb8258ea4aa403326c08932324835" dependencies = [ "anyhow", "cfg-if", @@ -1958,13 +1915,12 @@ dependencies = [ [[package]] name = "libredox" -version = "0.0.1" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ "bitflags 2.5.0", "libc", - "redox_syscall 0.4.1", ] [[package]] @@ -1991,9 +1947,9 @@ checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" [[package]] name = "lock_api" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", @@ -2043,9 +1999,9 @@ checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" [[package]] name = "memchr" -version = "2.7.1" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" [[package]] name = "memmap" @@ -2084,7 +2040,7 @@ dependencies = [ [[package]] name = "mg-admin-client" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/maghemite?rev=de065a84831e66c829603d9a098e237e8f5faaa1#de065a84831e66c829603d9a098e237e8f5faaa1" +source = "git+https://github.com/oxidecomputer/maghemite?rev=8207cb9c90cd7144c3f351823bfb2ae3e221ad10#8207cb9c90cd7144c3f351823bfb2ae3e221ad10" dependencies = [ "anyhow", "chrono", @@ -2106,7 +2062,7 @@ dependencies = [ "backoff", "clap", "internal-dns", - "libnet 0.1.0 (git+https://github.com/oxidecomputer/netadm-sys?branch=main)", + "libnet", "omicron-common", "oximeter", "oximeter-producer", @@ -2139,7 +2095,7 @@ dependencies = [ "ddm-admin-client", "dpd-client", "http 0.2.12", - "libnet 0.1.0 (git+https://github.com/oxidecomputer/netadm-sys?branch=main)", + "libnet", "mg-common", "rdb", "slog", @@ -2163,7 +2119,7 @@ version = "0.1.0" dependencies = [ "anyhow", "ddm-admin-client", - "libnet 0.1.0 (git+https://github.com/oxidecomputer/netadm-sys?branch=main)", + "libnet", "slog", "slog-async", "slog-envlogger", @@ -2179,6 +2135,7 @@ name = "mgadm" version = "0.1.0" dependencies = [ "anyhow", + "bgp", "clap", "colored", "humantime", @@ -2206,7 +2163,7 @@ dependencies = [ "clap", "colored", "dropshot", - "hostname", + "hostname 0.3.1", "http 0.2.12", "mg-common", "mg-lower", @@ -2294,11 +2251,20 @@ dependencies = [ "tempfile", ] +[[package]] +name = "newline-converter" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b6b097ecb1cbfed438542d16e84fd7ad9b0c76c8a65b7f9039212a3d14dc7f" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "newtype-uuid" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a5ff2b31594942586c1520da8f1e5c705729ec67b3c2ad0fe459f0b576e4d9a" +checksum = "3526cb7c660872e401beaf3297f95f548ce3b4b4bdd8121b7c0713771d7c4a6e" dependencies = [ "schemars", "serde", @@ -2317,7 +2283,7 @@ dependencies = [ [[package]] name = "nexus-client" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#2349feb36d39804e38539e134a9a3a4462e7d516" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#ee901b00c20a2ccaeabbd1bf27ddb60c95b334b5" dependencies = [ "chrono", "futures", @@ -2328,7 +2294,7 @@ dependencies = [ "omicron-uuid-kinds", "omicron-workspace-hack", "progenitor", - "regress 0.9.0", + "regress", "reqwest", "schemars", "serde", @@ -2340,16 +2306,19 @@ dependencies = [ [[package]] name = "nexus-types" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#2349feb36d39804e38539e134a9a3a4462e7d516" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#ee901b00c20a2ccaeabbd1bf27ddb60c95b334b5" dependencies = [ "anyhow", "api_identity", - "base64 0.22.0", + "base64 0.22.1", "chrono", + "clap", "dns-service-client", "futures", "gateway-client", "humantime", + "ipnetwork", + "newtype-uuid", "omicron-common", "omicron-passwords", "omicron-uuid-kinds", @@ -2361,8 +2330,11 @@ dependencies = [ "serde_json", "serde_with", "sled-agent-client", + "slog", + "slog-error-chain", "steno", "strum", + "tabled", "thiserror", "uuid 1.8.0", ] @@ -2379,9 +2351,9 @@ dependencies = [ [[package]] name = "num" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05180d69e3da0e530ba2a1dae5110317e49e3b7f3d41be227dc5f92e49ee7af" +checksum = "3135b08af27d103b0a51f2ae0f8632117b7b185ccf931445affa8df530576a41" dependencies = [ "num-complex", "num-integer", @@ -2463,7 +2435,7 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ - "hermit-abi 0.3.9", + "hermit-abi", "libc", ] @@ -2491,7 +2463,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", @@ -2503,10 +2475,10 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b" dependencies = [ - "proc-macro-crate", + "proc-macro-crate 3.1.0", "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.60", ] [[package]] @@ -2545,7 +2517,7 @@ dependencies = [ [[package]] name = "omicron-common" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#2349feb36d39804e38539e134a9a3a4462e7d516" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#ee901b00c20a2ccaeabbd1bf27ddb60c95b334b5" dependencies = [ "anyhow", "api_identity", @@ -2559,7 +2531,7 @@ dependencies = [ "http 0.2.12", "ipnetwork", "macaddr", - "mg-admin-client 0.1.0 (git+https://github.com/oxidecomputer/maghemite?rev=de065a84831e66c829603d9a098e237e8f5faaa1)", + "mg-admin-client 0.1.0 (git+https://github.com/oxidecomputer/maghemite?rev=8207cb9c90cd7144c3f351823bfb2ae3e221ad10)", "omicron-uuid-kinds", "omicron-workspace-hack", "once_cell", @@ -2567,7 +2539,7 @@ dependencies = [ "progenitor", "progenitor-client", "rand", - "regress 0.9.0", + "regress", "reqwest", "schemars", "semver 1.0.22", @@ -2586,7 +2558,7 @@ dependencies = [ [[package]] name = "omicron-passwords" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#2349feb36d39804e38539e134a9a3a4462e7d516" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#ee901b00c20a2ccaeabbd1bf27ddb60c95b334b5" dependencies = [ "argon2", "omicron-workspace-hack", @@ -2600,9 +2572,10 @@ dependencies = [ [[package]] name = "omicron-uuid-kinds" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#2349feb36d39804e38539e134a9a3a4462e7d516" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#ee901b00c20a2ccaeabbd1bf27ddb60c95b334b5" dependencies = [ "newtype-uuid", + "paste", "schemars", ] @@ -2684,7 +2657,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.60", ] [[package]] @@ -2695,9 +2668,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.101" +version = "0.9.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dda2b0f344e78efc2facf7d195d098df0dd72151b26ab98da807afc26c198dff" +checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2" dependencies = [ "cc", "libc", @@ -2708,7 +2681,7 @@ dependencies = [ [[package]] name = "opte" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/opte?branch=master#4cc823b50d3e4a629cdfaab2b3d3382514174ba9" +source = "git+https://github.com/oxidecomputer/opte?rev=7ee353a470ea59529ee1b34729681da887aa88ce#7ee353a470ea59529ee1b34729681da887aa88ce" dependencies = [ "cfg-if", "derror-macro", @@ -2726,7 +2699,7 @@ dependencies = [ [[package]] name = "opte-api" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/opte?branch=master#4cc823b50d3e4a629cdfaab2b3d3382514174ba9" +source = "git+https://github.com/oxidecomputer/opte?rev=7ee353a470ea59529ee1b34729681da887aa88ce#7ee353a470ea59529ee1b34729681da887aa88ce" dependencies = [ "illumos-sys-hdrs", "ipnetwork", @@ -2738,10 +2711,10 @@ dependencies = [ [[package]] name = "opte-ioctl" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/opte?branch=master#4cc823b50d3e4a629cdfaab2b3d3382514174ba9" +source = "git+https://github.com/oxidecomputer/opte?rev=7ee353a470ea59529ee1b34729681da887aa88ce#7ee353a470ea59529ee1b34729681da887aa88ce" dependencies = [ "libc", - "libnet 0.1.0 (git+https://github.com/oxidecomputer/netadm-sys)", + "libnet", "opte", "oxide-vpc", "postcard", @@ -2752,7 +2725,7 @@ dependencies = [ [[package]] name = "oxide-vpc" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/opte?branch=master#4cc823b50d3e4a629cdfaab2b3d3382514174ba9" +source = "git+https://github.com/oxidecomputer/opte?rev=7ee353a470ea59529ee1b34729681da887aa88ce#7ee353a470ea59529ee1b34729681da887aa88ce" dependencies = [ "cfg-if", "illumos-sys-hdrs", @@ -2767,7 +2740,7 @@ dependencies = [ [[package]] name = "oximeter" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#2349feb36d39804e38539e134a9a3a4462e7d516" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#ee901b00c20a2ccaeabbd1bf27ddb60c95b334b5" dependencies = [ "bytes", "chrono", @@ -2787,21 +2760,22 @@ dependencies = [ [[package]] name = "oximeter-macro-impl" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#2349feb36d39804e38539e134a9a3a4462e7d516" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#ee901b00c20a2ccaeabbd1bf27ddb60c95b334b5" dependencies = [ "omicron-workspace-hack", "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.60", ] [[package]] name = "oximeter-producer" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#2349feb36d39804e38539e134a9a3a4462e7d516" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#ee901b00c20a2ccaeabbd1bf27ddb60c95b334b5" dependencies = [ "chrono", "dropshot", + "internal-dns", "nexus-client", "omicron-common", "omicron-workspace-hack", @@ -2815,6 +2789,17 @@ dependencies = [ "uuid 1.8.0", ] +[[package]] +name = "papergrid" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ad43c07024ef767f9160710b3a6773976194758c7919b17e63b863db0bdf7fb" +dependencies = [ + "bytecount", + "fnv", + "unicode-width", +] + [[package]] name = "parking_lot" version = "0.11.2" @@ -2828,12 +2813,12 @@ dependencies = [ [[package]] name = "parking_lot" -version = "0.12.1" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +checksum = "7e4af0ca4f6caed20e900d564c242b8e5d4903fdacf31d3daf527b66fe6f42fb" dependencies = [ "lock_api", - "parking_lot_core 0.9.9", + "parking_lot_core 0.9.10", ] [[package]] @@ -2852,15 +2837,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.9" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.4.1", + "redox_syscall 0.5.1", "smallvec", - "windows-targets 0.48.5", + "windows-targets 0.52.5", ] [[package]] @@ -2885,7 +2870,7 @@ dependencies = [ "regex", "regex-syntax", "structmeta", - "syn 2.0.55", + "syn 2.0.60", ] [[package]] @@ -2913,9 +2898,9 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pest" -version = "2.7.8" +version = "2.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f8023d0fb78c8e03784ea1c7f3fa36e68a723138990b8d5a47d916b651e7a8" +checksum = "560131c633294438da9f7c4b08189194b20946c8274c6b9e38881a7874dc8ee8" dependencies = [ "memchr", "thiserror", @@ -2924,9 +2909,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.7.8" +version = "2.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0d24f72393fd16ab6ac5738bc33cdb6a9aa73f8b902e8fe29cf4e67d7dd1026" +checksum = "26293c9193fbca7b1a3bf9b79dc1e388e927e6cacaa78b4a3ab705a1d3d41459" dependencies = [ "pest", "pest_generator", @@ -2934,22 +2919,22 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.7.8" +version = "2.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdc17e2a6c7d0a492f0158d7a4bd66cc17280308bbaff78d5bef566dca35ab80" +checksum = "3ec22af7d3fb470a85dd2ca96b7c577a1eb4ef6f1683a9fe9a8c16e136c04687" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.60", ] [[package]] name = "pest_meta" -version = "2.7.8" +version = "2.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "934cd7631c050f4674352a6e835d5f6711ffbfb9345c2fc0107155ac495ae293" +checksum = "d7a240022f37c361ec1878d646fc5b7d7c4d28d5946e1a80ad5a7a4f4ca0bdcd" dependencies = [ "once_cell", "pest", @@ -2970,9 +2955,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" [[package]] name = "pin-utils" @@ -3061,6 +3046,15 @@ dependencies = [ "toml_edit 0.19.15", ] +[[package]] +name = "proc-macro-crate" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284" +dependencies = [ + "toml_edit 0.21.1", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -3087,9 +3081,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.79" +version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" +checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" dependencies = [ "unicode-ident", ] @@ -3097,7 +3091,7 @@ dependencies = [ [[package]] name = "progenitor" version = "0.6.0" -source = "git+https://github.com/oxidecomputer/progenitor?branch=main#090dd361fc530f0a6bfe238773239b78aaed91e1" +source = "git+https://github.com/oxidecomputer/progenitor?branch=main#0f0b1062f471f33d4d42273c03c4079f6ebf4ad9" dependencies = [ "progenitor-client", "progenitor-impl", @@ -3108,7 +3102,7 @@ dependencies = [ [[package]] name = "progenitor-client" version = "0.6.0" -source = "git+https://github.com/oxidecomputer/progenitor?branch=main#090dd361fc530f0a6bfe238773239b78aaed91e1" +source = "git+https://github.com/oxidecomputer/progenitor?branch=main#0f0b1062f471f33d4d42273c03c4079f6ebf4ad9" dependencies = [ "bytes", "futures-core", @@ -3122,10 +3116,10 @@ dependencies = [ [[package]] name = "progenitor-impl" version = "0.6.0" -source = "git+https://github.com/oxidecomputer/progenitor?branch=main#090dd361fc530f0a6bfe238773239b78aaed91e1" +source = "git+https://github.com/oxidecomputer/progenitor?branch=main#0f0b1062f471f33d4d42273c03c4079f6ebf4ad9" dependencies = [ "getopts", - "heck 0.4.1", + "heck 0.5.0", "http 0.2.12", "indexmap 2.2.6", "openapiv3", @@ -3135,7 +3129,7 @@ dependencies = [ "schemars", "serde", "serde_json", - "syn 2.0.55", + "syn 2.0.60", "thiserror", "typify", "unicode-ident", @@ -3144,7 +3138,7 @@ dependencies = [ [[package]] name = "progenitor-macro" version = "0.6.0" -source = "git+https://github.com/oxidecomputer/progenitor?branch=main#090dd361fc530f0a6bfe238773239b78aaed91e1" +source = "git+https://github.com/oxidecomputer/progenitor?branch=main#0f0b1062f471f33d4d42273c03c4079f6ebf4ad9" dependencies = [ "openapiv3", "proc-macro2", @@ -3155,43 +3149,13 @@ dependencies = [ "serde_json", "serde_tokenstream", "serde_yaml", - "syn 2.0.55", -] - -[[package]] -name = "propolis" -version = "0.1.0" -source = "git+https://github.com/oxidecomputer/propolis?rev=6dceb9ef69c217cb78a2018bbedafbc19f6ec1af#6dceb9ef69c217cb78a2018bbedafbc19f6ec1af" -dependencies = [ - "anyhow", - "bhyve_api", - "bitflags 2.5.0", - "bitstruct", - "byteorder", - "dladm", - "erased-serde", - "futures", - "lazy_static", - "libc", - "pin-project-lite", - "propolis_types", - "rfb", - "serde", - "serde_arrays", - "serde_json", - "slog", - "strum", - "thiserror", - "tokio", - "usdt", - "uuid 1.8.0", - "viona_api", + "syn 2.0.60", ] [[package]] name = "propolis-client" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/propolis?rev=6dceb9ef69c217cb78a2018bbedafbc19f6ec1af#6dceb9ef69c217cb78a2018bbedafbc19f6ec1af" +source = "git+https://github.com/oxidecomputer/propolis?rev=d6fc6d458e08e7ae1008aaa2d505a6523a4e3538#d6fc6d458e08e7ae1008aaa2d505a6523a4e3538" dependencies = [ "async-trait", "base64 0.21.7", @@ -3212,7 +3176,7 @@ dependencies = [ [[package]] name = "propolis-server-config" version = "0.0.0" -source = "git+https://github.com/oxidecomputer/propolis?rev=6dceb9ef69c217cb78a2018bbedafbc19f6ec1af#6dceb9ef69c217cb78a2018bbedafbc19f6ec1af" +source = "git+https://github.com/oxidecomputer/propolis?rev=d6fc6d458e08e7ae1008aaa2d505a6523a4e3538#d6fc6d458e08e7ae1008aaa2d505a6523a4e3538" dependencies = [ "cpuid_profile_config", "serde", @@ -3221,15 +3185,6 @@ dependencies = [ "toml 0.7.8", ] -[[package]] -name = "propolis_types" -version = "0.0.0" -source = "git+https://github.com/oxidecomputer/propolis?rev=6dceb9ef69c217cb78a2018bbedafbc19f6ec1af#6dceb9ef69c217cb78a2018bbedafbc19f6ec1af" -dependencies = [ - "schemars", - "serde", -] - [[package]] name = "quick-error" version = "1.2.3" @@ -3238,9 +3193,9 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quote" -version = "1.0.35" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] @@ -3300,7 +3255,9 @@ name = "rdb" version = "0.1.0" dependencies = [ "anyhow", + "chrono", "ciborium", + "itertools 0.12.1", "mg-common", "schemars", "serde", @@ -3328,11 +3285,20 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_syscall" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e" +dependencies = [ + "bitflags 2.5.0", +] + [[package]] name = "redox_users" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" +checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" dependencies = [ "getrandom", "libredox", @@ -3364,27 +3330,17 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" [[package]] name = "regress" -version = "0.6.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82a9ecfa0cb04d0b04dddb99b8ccf4f66bc8dfd23df694b398570bd8ae3a50fb" +checksum = "0eae2a1ebfecc58aff952ef8ccd364329abe627762f5bf09ff42eb9d98522479" dependencies = [ - "hashbrown 0.13.2", - "memchr", -] - -[[package]] -name = "regress" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d06f9a1f7cd8473611ba1a480cf35f9c5cffc2954336ba90a982fdb7e7d7f51e" -dependencies = [ - "hashbrown 0.14.3", + "hashbrown 0.14.5", "memchr", ] @@ -3413,7 +3369,7 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", - "rustls 0.21.10", + "rustls 0.21.12", "rustls-pemfile 1.0.4", "serde", "serde_json", @@ -3440,23 +3396,38 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52e44394d2086d010551b14b53b1f24e31647570cd1deb0379e2c21b329aba00" dependencies = [ - "hostname", + "hostname 0.3.1", "quick-error", ] [[package]] -name = "rfb" -version = "0.1.0" -source = "git+https://github.com/oxidecomputer/rfb?rev=0cac8d9c25eb27acfa35df80f3b9d371de98ab3b#0cac8d9c25eb27acfa35df80f3b9d371de98ab3b" +name = "rhai" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a7d88770120601ba1e548bb6bc2a05019e54ff01b51479e38e64ec3b59d4759" dependencies = [ - "ascii", - "async-trait", - "bitflags 1.3.2", - "env_logger", - "futures", - "log", - "thiserror", - "tokio", + "ahash", + "bitflags 2.5.0", + "instant", + "num-traits", + "once_cell", + "rhai_codegen", + "serde", + "serde_json", + "smallvec", + "smartstring", + "thin-vec", +] + +[[package]] +name = "rhai_codegen" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59aecf17969c04b9c0c5d21f6bc9da9fec9dd4980e64d1871443a476589d8c86" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.60", ] [[package]] @@ -3517,9 +3488,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.32" +version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ "bitflags 2.5.0", "errno", @@ -3530,9 +3501,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.21.10" +version = "0.21.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" dependencies = [ "log", "ring 0.17.8", @@ -3542,14 +3513,14 @@ dependencies = [ [[package]] name = "rustls" -version = "0.22.2" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e87c9956bd9807afa1f77e0f7594af32566e830e088a5576d27c5b6f30f49d41" +checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" dependencies = [ "log", "ring 0.17.8", "rustls-pki-types", - "rustls-webpki 0.102.2", + "rustls-webpki 0.102.3", "subtle", "zeroize", ] @@ -3565,19 +3536,19 @@ dependencies = [ [[package]] name = "rustls-pemfile" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f48172685e6ff52a556baa527774f61fcaa884f59daf3375c62a3f1cd2549dab" +checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" dependencies = [ - "base64 0.21.7", + "base64 0.22.1", "rustls-pki-types", ] [[package]] name = "rustls-pki-types" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "868e20fada228fefaf6b652e00cc73623d54f8171e7352c18bb281571f2d92da" +checksum = "beb461507cee2c2ff151784c52762cf4d9ff6a61f3e80968600ed24fa837fa54" [[package]] name = "rustls-webpki" @@ -3591,9 +3562,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.102.2" +version = "0.102.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faaa0a62740bedb9b2ef5afa303da42764c012f743917351dc9a237ea1663610" +checksum = "f3bce581c0dd41bce533ce695a1437fa16a7ab5ac3ccfa99fe1a620a7885eabf" dependencies = [ "ring 0.17.8", "rustls-pki-types", @@ -3602,9 +3573,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" +checksum = "80af6f9131f277a45a3fba6ce8e2258037bb0477a67e610d3c1fe046ab31de47" [[package]] name = "rusty-doors" @@ -3650,9 +3621,9 @@ dependencies = [ [[package]] name = "schemars" -version = "0.8.16" +version = "0.8.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45a28f4c49489add4ce10783f7911893516f15afe45d015608d41faca6bc4d29" +checksum = "7f55c82c700538496bdc329bb4918a81f87cc8888811bd123cf325a0f2f8d309" dependencies = [ "bytes", "chrono", @@ -3666,14 +3637,14 @@ dependencies = [ [[package]] name = "schemars_derive" -version = "0.8.16" +version = "0.8.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c767fd6fa65d9ccf9cf026122c1b555f2ef9a4f0cea69da4d7dbc3e258d30967" +checksum = "83263746fe5e32097f06356968a077f96089739c927a61450efa069905eec108" dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 1.0.109", + "syn 2.0.60", ] [[package]] @@ -3699,7 +3670,7 @@ checksum = "7f81c2fde025af7e69b1d1420531c8a8811ca898919db177141a85313b1cb932" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.60", ] [[package]] @@ -3714,9 +3685,9 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.9.2" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +checksum = "770452e37cad93e0a50d5abc3990d2bc351c36d0328f86cefec2f2fb206eaef6" dependencies = [ "bitflags 1.3.2", "core-foundation", @@ -3727,9 +3698,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.9.1" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +checksum = "41f3cc463c0ef97e11c3461a9d3787412d30e8e7eb907c79180c4a57bf7c04ef" dependencies = [ "core-foundation-sys", "libc", @@ -3752,42 +3723,33 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.199" +version = "1.0.200" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c9f6e76df036c77cd94996771fb40db98187f096dd0b9af39c6c6e452ba966a" +checksum = "ddc6f9cc94d67c0e21aaf7eda3a010fd3af78ebf6e096aa6e2e13c79749cce4f" dependencies = [ "serde_derive", ] -[[package]] -name = "serde_arrays" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38636132857f68ec3d5f3eb121166d2af33cb55174c4d5ff645db6165cbef0fd" -dependencies = [ - "serde", -] - [[package]] name = "serde_derive" -version = "1.0.199" +version = "1.0.200" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11bd257a6541e141e42ca6d24ae26f7714887b47e89aa739099104c7e4d3b7fc" +checksum = "856f046b9400cee3c8c94ed572ecdb752444c24528c035cd35882aad6f492bcb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.60", ] [[package]] name = "serde_derive_internals" -version = "0.26.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85bf8229e7920a9f636479437026331ce11aa132b4dde37d121944a44d6e5f3c" +checksum = "330f01ce65a3a5fe59a60c82f3c9a024b573b8a6e875bd233fe5f934e71d54e3" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.60", ] [[package]] @@ -3828,7 +3790,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.60", ] [[package]] @@ -3849,7 +3811,7 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.55", + "syn 2.0.60", ] [[package]] @@ -3866,11 +3828,11 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.7.0" +version = "3.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee80b0e361bbf88fd2f6e242ccd19cfda072cb0faa6ae694ecee08199938569a" +checksum = "0ad483d2ab0149d5a5ebcd9972a3852711e0153d863bf5a5d0391d28883c4a20" dependencies = [ - "base64 0.21.7", + "base64 0.22.1", "chrono", "hex", "indexmap 1.9.3", @@ -3884,14 +3846,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.7.0" +version = "3.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6561dc161a9224638a31d876ccdfefbc1df91d3f3a8342eddb35f055d48c7655" +checksum = "65569b702f41443e8bc8bbb1c5779bd0450bbe723b56198980e80ec45780bce2" dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.60", ] [[package]] @@ -3931,13 +3893,19 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" dependencies = [ "libc", ] +[[package]] +name = "similar" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa42c91313f1d05da9b26f267f931cf178d4aba455b4c4622dd7355eb80c6640" + [[package]] name = "slab" version = "0.4.9" @@ -3966,16 +3934,17 @@ dependencies = [ [[package]] name = "sled-agent-client" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#2349feb36d39804e38539e134a9a3a4462e7d516" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#ee901b00c20a2ccaeabbd1bf27ddb60c95b334b5" dependencies = [ "anyhow", "async-trait", "chrono", "ipnetwork", "omicron-common", + "omicron-uuid-kinds", "omicron-workspace-hack", "progenitor", - "regress 0.9.0", + "regress", "reqwest", "schemars", "serde", @@ -4007,7 +3976,7 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcaaf6e68789d3f0411f1e72bc443214ef252a1038b6e344836e50442541f190" dependencies = [ - "hostname", + "hostname 0.3.1", "slog", "slog-json", "time", @@ -4058,7 +4027,7 @@ source = "git+https://github.com/oxidecomputer/slog-error-chain?branch=main#15f6 dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.60", ] [[package]] @@ -4113,6 +4082,21 @@ name = "smallvec" version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +dependencies = [ + "serde", +] + +[[package]] +name = "smartstring" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29" +dependencies = [ + "autocfg", + "serde", + "static_assertions", + "version_check", +] [[package]] name = "smf" @@ -4160,9 +4144,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.6" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" dependencies = [ "libc", "windows-sys 0.52.0", @@ -4222,9 +4206,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "strsim" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "structmeta" @@ -4235,7 +4219,7 @@ dependencies = [ "proc-macro2", "quote", "structmeta-derive", - "syn 2.0.55", + "syn 2.0.60", ] [[package]] @@ -4246,7 +4230,7 @@ checksum = "152a0b65a590ff6c3da95cabe2353ee04e6167c896b28e3b14478c2636c922fc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.60", ] [[package]] @@ -4268,7 +4252,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.55", + "syn 2.0.60", ] [[package]] @@ -4281,7 +4265,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.55", + "syn 2.0.60", ] [[package]] @@ -4303,9 +4287,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.55" +version = "2.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "002a1b3dbf967edfafc32655d0f377ab0bb7b994aa1d32c8cc7e9b8bf3ebb8f0" +checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" dependencies = [ "proc-macro2", "quote", @@ -4339,6 +4323,30 @@ dependencies = [ "libc", ] +[[package]] +name = "tabled" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c998b0c8b921495196a48aabaf1901ff28be0760136e31604f7967b0792050e" +dependencies = [ + "papergrid", + "tabled_derive", + "unicode-width", +] + +[[package]] +name = "tabled_derive" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c138f99377e5d653a371cdad263615634cfc8467685dfe8e73e2b8e98f44b17" +dependencies = [ + "heck 0.4.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "tabwriter" version = "1.4.0" @@ -4389,12 +4397,22 @@ dependencies = [ ] [[package]] -name = "termcolor" -version = "1.4.1" +name = "terminal_size" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" dependencies = [ - "winapi-util", + "rustix", + "windows-sys 0.48.0", +] + +[[package]] +name = "thin-vec" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a38c90d48152c236a3ab59271da4f4ae63d678c5d7ad6b7714d7cb9760be5e4b" +dependencies = [ + "serde", ] [[package]] @@ -4414,7 +4432,7 @@ checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.60", ] [[package]] @@ -4439,9 +4457,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.34" +version = "0.3.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ "deranged", "itoa", @@ -4462,14 +4480,23 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.17" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" dependencies = [ "num-conv", "time-core", ] +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -4496,10 +4523,10 @@ dependencies = [ "libc", "mio", "num_cpus", - "parking_lot 0.12.1", + "parking_lot 0.12.2", "pin-project-lite", "signal-hook-registry", - "socket2 0.5.6", + "socket2 0.5.7", "tokio-macros", "windows-sys 0.48.0", ] @@ -4512,7 +4539,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.60", ] [[package]] @@ -4531,7 +4558,7 @@ version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" dependencies = [ - "rustls 0.21.10", + "rustls 0.21.12", "tokio", ] @@ -4541,7 +4568,7 @@ version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" dependencies = [ - "rustls 0.22.2", + "rustls 0.22.4", "rustls-pki-types", "tokio", ] @@ -4605,7 +4632,7 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.22.9", + "toml_edit 0.22.12", ] [[package]] @@ -4632,15 +4659,26 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.9" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e40bb779c5187258fd7aad0eb68cb8706a0a81fa712fbea808ab43c4b8374c4" +checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" +dependencies = [ + "indexmap 2.2.6", + "toml_datetime", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.22.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3328d4f68a705b2a4498da1d580585d39a6510f98318a2cec3018a7ec61ddef" dependencies = [ "indexmap 2.2.6", "serde", "serde_spanned", "toml_datetime", - "winnow 0.6.5", + "winnow 0.6.7", ] [[package]] @@ -4674,7 +4712,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.60", ] [[package]] @@ -4722,7 +4760,7 @@ dependencies = [ "ipconfig", "lazy_static", "lru-cache", - "parking_lot 0.12.1", + "parking_lot 0.12.2", "resolv-conf", "smallvec", "thiserror", @@ -4784,7 +4822,7 @@ checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "typify" version = "0.0.16" -source = "git+https://github.com/oxidecomputer/typify#0d30d24c33ca7a5595b592d3b3d3908e87ae54d5" +source = "git+https://github.com/oxidecomputer/typify#08a53a7ef9da9b81838002332fa414d433f10067" dependencies = [ "typify-impl", "typify-macro", @@ -4793,16 +4831,16 @@ dependencies = [ [[package]] name = "typify-impl" version = "0.0.16" -source = "git+https://github.com/oxidecomputer/typify#0d30d24c33ca7a5595b592d3b3d3908e87ae54d5" +source = "git+https://github.com/oxidecomputer/typify#08a53a7ef9da9b81838002332fa414d433f10067" dependencies = [ - "heck 0.4.1", + "heck 0.5.0", "log", "proc-macro2", "quote", - "regress 0.9.0", + "regress", "schemars", "serde_json", - "syn 2.0.55", + "syn 2.0.60", "thiserror", "unicode-ident", ] @@ -4810,7 +4848,7 @@ dependencies = [ [[package]] name = "typify-macro" version = "0.0.16" -source = "git+https://github.com/oxidecomputer/typify#0d30d24c33ca7a5595b592d3b3d3908e87ae54d5" +source = "git+https://github.com/oxidecomputer/typify#08a53a7ef9da9b81838002332fa414d433f10067" dependencies = [ "proc-macro2", "quote", @@ -4818,7 +4856,7 @@ dependencies = [ "serde", "serde_json", "serde_tokenstream", - "syn 2.0.55", + "syn 2.0.60", "typify-impl", ] @@ -4857,9 +4895,9 @@ checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" [[package]] name = "unicode-width" -version = "0.1.11" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" +checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6" [[package]] name = "unsafe-libyaml" @@ -4916,7 +4954,7 @@ dependencies = [ "proc-macro2", "quote", "serde_tokenstream", - "syn 2.0.55", + "syn 2.0.60", "usdt-impl", ] @@ -4934,7 +4972,7 @@ dependencies = [ "quote", "serde", "serde_json", - "syn 2.0.55", + "syn 2.0.60", "thiserror", "thread-id", "version_check", @@ -4950,7 +4988,7 @@ dependencies = [ "proc-macro2", "quote", "serde_tokenstream", - "syn 2.0.55", + "syn 2.0.60", "usdt-impl", ] @@ -4971,12 +5009,12 @@ name = "util" version = "0.1.0" dependencies = [ "anyhow", - "libnet 0.1.0 (git+https://github.com/oxidecomputer/netadm-sys?branch=main)", + "libnet", "slog", "slog-async", "slog-envlogger", "slog-term", - "socket2 0.5.6", + "socket2 0.5.7", ] [[package]] @@ -5007,23 +5045,6 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" -[[package]] -name = "viona_api" -version = "0.0.0" -source = "git+https://github.com/oxidecomputer/propolis?rev=6dceb9ef69c217cb78a2018bbedafbc19f6ec1af#6dceb9ef69c217cb78a2018bbedafbc19f6ec1af" -dependencies = [ - "libc", - "viona_api_sys", -] - -[[package]] -name = "viona_api_sys" -version = "0.0.0" -source = "git+https://github.com/oxidecomputer/propolis?rev=6dceb9ef69c217cb78a2018bbedafbc19f6ec1af#6dceb9ef69c217cb78a2018bbedafbc19f6ec1af" -dependencies = [ - "libc", -] - [[package]] name = "waitgroup" version = "0.1.2" @@ -5079,7 +5100,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.60", "wasm-bindgen-shared", ] @@ -5113,7 +5134,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.60", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -5155,9 +5176,9 @@ checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" [[package]] name = "widestring" -version = "1.0.2" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "653f141f39ec16bba3c5abe400a0c60da7468261cc2cbf36805022876bc721a8" +checksum = "7219d36b6eac893fa81e84ebe06485e7dcbb616177469b142df14f1f4deb1311" [[package]] name = "winapi" @@ -5177,11 +5198,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" dependencies = [ - "winapi", + "windows-sys 0.52.0", ] [[package]] @@ -5190,13 +5211,23 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" +dependencies = [ + "windows-core", + "windows-targets 0.52.5", +] + [[package]] name = "windows-core" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.52.4", + "windows-targets 0.52.5", ] [[package]] @@ -5214,7 +5245,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.4", + "windows-targets 0.52.5", ] [[package]] @@ -5234,17 +5265,18 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" dependencies = [ - "windows_aarch64_gnullvm 0.52.4", - "windows_aarch64_msvc 0.52.4", - "windows_i686_gnu 0.52.4", - "windows_i686_msvc 0.52.4", - "windows_x86_64_gnu 0.52.4", - "windows_x86_64_gnullvm 0.52.4", - "windows_x86_64_msvc 0.52.4", + "windows_aarch64_gnullvm 0.52.5", + "windows_aarch64_msvc 0.52.5", + "windows_i686_gnu 0.52.5", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.5", + "windows_x86_64_gnu 0.52.5", + "windows_x86_64_gnullvm 0.52.5", + "windows_x86_64_msvc 0.52.5", ] [[package]] @@ -5255,9 +5287,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" [[package]] name = "windows_aarch64_msvc" @@ -5267,9 +5299,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" [[package]] name = "windows_i686_gnu" @@ -5279,9 +5311,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.4" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" [[package]] name = "windows_i686_msvc" @@ -5291,9 +5329,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" [[package]] name = "windows_x86_64_gnu" @@ -5303,9 +5341,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" [[package]] name = "windows_x86_64_gnullvm" @@ -5315,9 +5353,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" [[package]] name = "windows_x86_64_msvc" @@ -5327,9 +5365,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" [[package]] name = "winnow" @@ -5342,9 +5380,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.6.5" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dffa400e67ed5a4dd237983829e66475f0a4a26938c4b04c21baede6262215b8" +checksum = "14b9415ee827af173ebb3f15f9083df5a122eb93572ec28741fb153356ea2578" dependencies = [ "memchr", ] @@ -5404,7 +5442,7 @@ checksum = "125139de3f6b9d625c39e2efdd73d41bdac468ccd556556440e322be0e1bbd91" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.60", ] [[package]] @@ -5415,7 +5453,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.60", ] [[package]] @@ -5473,10 +5511,10 @@ dependencies = [ [[package]] name = "ztest" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/falcon?branch=main#e69694a1f7cc9fe31fab27f321017280531fb5f7" +source = "git+https://github.com/oxidecomputer/falcon?branch=main#34609623da1d5aa5b6e52ba52e3348e13f61bb26" dependencies = [ "anyhow", - "libnet 0.1.0 (git+https://github.com/oxidecomputer/netadm-sys?branch=main)", + "libnet", "tokio", "zone 0.3.0", ] diff --git a/Cargo.toml b/Cargo.toml index 79dfe019..4389b168 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,11 +57,11 @@ serde_repr = "0.1" anyhow = "1.0.82" hyper = "0.14.28" serde_json = "1.0.116" -libnet = { git = "https://github.com/oxidecomputer/netadm-sys", branch = "main" } percent-encoding = "2.3.1" +libnet = { git = "https://github.com/oxidecomputer/netadm-sys", branch = "compile-macos" } reqwest = { version = "0.11", features = ["json", "stream", "rustls-tls"] } progenitor = { git = "https://github.com/oxidecomputer/progenitor", branch = "main" } -clap = { version = "4.5.4", features = ["derive", "unstable-styles"] } +clap = { version = "4.5.4", features = ["derive", "unstable-styles", "env"] } tabwriter = { version = "1", features = ["ansi_formatting"] } colored = "2.1" ctrlc = { version = "3.4.4", features = ["termination"] } @@ -86,14 +86,17 @@ omicron-common = { git = "https://github.com/oxidecomputer/omicron", branch= "ma internal-dns = { 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" +itertools = "0.12" +rhai = { version = "1", features = ["metadata", "sync"] } [workspace.dependencies.opte-ioctl] git = "https://github.com/oxidecomputer/opte" -branch = "master" +rev = "7ee353a470ea59529ee1b34729681da887aa88ce" [workspace.dependencies.oxide-vpc] git = "https://github.com/oxidecomputer/opte" -branch = "master" +rev = "7ee353a470ea59529ee1b34729681da887aa88ce" [workspace.dependencies.dpd-client] git = "https://github.com/oxidecomputer/dendrite" @@ -104,3 +107,6 @@ package = "dpd-client" git = "https://github.com/oxidecomputer/dendrite" branch = "main" package = "common" + +[patch."https://github.com/oxidecomputer/netadm-sys"] +libnet = { git = "https://www.github.com/oxidecomputer/netadm-sys.git", branch = "main" } diff --git a/bfd/src/sm.rs b/bfd/src/sm.rs index 42844c6f..ed23a049 100644 --- a/bfd/src/sm.rs +++ b/bfd/src/sm.rs @@ -438,15 +438,7 @@ impl State for Down { db: rdb::Db, counters: Arc, ) -> Result<(Box, BfdEndpoint)> { - match self.peer { - IpAddr::V4(addr) => db.disable_nexthop4(addr), - IpAddr::V6(addr) => { - warn!( - self.log, - "{addr} is down but active mode ipv6 not implemented yet" - ) - } - } + db.disable_nexthop(self.peer); loop { // Get an incoming message let (_addr, msg) = match self.recv( @@ -605,15 +597,7 @@ impl State for Up { db: rdb::Db, counters: Arc, ) -> Result<(Box, BfdEndpoint)> { - match self.peer { - IpAddr::V4(addr) => db.enable_nexthop4(addr), - IpAddr::V6(addr) => { - warn!( - self.log, - "{addr} is up but active mode ipv6 not implemented yet" - ) - } - } + db.enable_nexthop(self.peer); loop { // Get an incoming message let (_addr, msg) = match self.recv( diff --git a/bgp/Cargo.toml b/bgp/Cargo.toml index ec38fcc4..8fcf7499 100644 --- a/bgp/Cargo.toml +++ b/bgp/Cargo.toml @@ -17,6 +17,9 @@ sled.workspace = true anyhow.workspace = true mg-common.workspace = true chrono.workspace = true +libc.workspace = true +socket2.workspace = true +rhai.workspace = true [dev-dependencies] pretty-hex.workspace = true diff --git a/bgp/policy/policy-check0.rhai b/bgp/policy/policy-check0.rhai new file mode 100644 index 00000000..d47a3d4d --- /dev/null +++ b/bgp/policy/policy-check0.rhai @@ -0,0 +1,14 @@ +fn open(message, asn, addr) { + if !message.has_capability(CapabilityCode::FourOctetAs) { + return CheckerResult::Drop; + } + CheckerResult::Accept +} + +fn update(message, asn, addr) { + // drop no-export community + if message.has_community(0xFFFFFF01) { + return CheckerResult::Drop; + } + CheckerResult::Accept +} diff --git a/bgp/policy/policy-shape0.rhai b/bgp/policy/policy-shape0.rhai new file mode 100644 index 00000000..b923d1ed --- /dev/null +++ b/bgp/policy/policy-shape0.rhai @@ -0,0 +1,13 @@ +fn open(message, asn, addr) { + if asn == 100 { + message.add_four_octet_as(74); + } + message.emit() +} + +fn update(message, asn, addr) { + if asn == 100 { + message.add_community(1701); + } + message.emit() +} diff --git a/bgp/policy/shape-prefix0.rhai b/bgp/policy/shape-prefix0.rhai new file mode 100644 index 00000000..c209fe92 --- /dev/null +++ b/bgp/policy/shape-prefix0.rhai @@ -0,0 +1,11 @@ +fn open(message, asn, addr) { + message.emit() +} + +fn update(message, asn, addr) { + if asn == 65402 { + // Apply a filter on both NLRI elements and withdraw elements + message.prefix_filter(|prefix| prefix.within("10.128.0.0/16")); + } + message.emit() +} diff --git a/bgp/src/clock.rs b/bgp/src/clock.rs index 5010d96c..6003b8ec 100644 --- a/bgp/src/clock.rs +++ b/bgp/src/clock.rs @@ -23,20 +23,28 @@ pub struct Clock { pub struct ClockTimers { /// How long to wait between connection attempts. - pub connect_retry_timer: Timer, + pub connect_retry_timer: Mutex, + + /// Configured keepliave timer interval. May be distinct from actual + /// keepalive interval depending on session parameter negotiation. + pub keepalive_configured_interval: Mutex, /// Time between sending keepalive messages. - pub keepalive_timer: Timer, + pub keepalive_timer: Mutex, + + /// Configured hold timer interval. May be distinct from actual keepalive + /// interval depending on session parameter negotiation. + pub hold_configured_interval: Mutex, /// How long to keep a session alive between keepalive, update and/or /// notification messages. - pub hold_timer: Timer, + pub hold_timer: Mutex, /// Amount of time that a peer is held in the idle state. - pub idle_hold_timer: Timer, + pub idle_hold_timer: Mutex, /// Interval to wait before sending out an open message. - pub delay_open_timer: Timer, + pub delay_open_timer: Mutex, } impl Clock { @@ -53,11 +61,13 @@ impl Clock { ) -> Self { let shutdown = Arc::new(AtomicBool::new(false)); let timers = Arc::new(ClockTimers { - connect_retry_timer: Timer::new(connect_retry_interval), - keepalive_timer: Timer::new(keepalive_interval), - hold_timer: Timer::new(hold_interval), - idle_hold_timer: Timer::new(idle_hold_interval), - delay_open_timer: Timer::new(delay_open_interval), + connect_retry_timer: Mutex::new(Timer::new(connect_retry_interval)), + keepalive_configured_interval: Mutex::new(keepalive_interval), + keepalive_timer: Mutex::new(Timer::new(keepalive_interval)), + hold_configured_interval: Mutex::new(hold_interval), + hold_timer: Mutex::new(Timer::new(hold_interval)), + idle_hold_timer: Mutex::new(Timer::new(idle_hold_interval)), + delay_open_timer: Mutex::new(Timer::new(delay_open_interval)), }); let join_handle = Arc::new(Self::run( resolution, @@ -98,35 +108,35 @@ impl Clock { ) { Self::step( resolution, - &timers.connect_retry_timer, + &timers.connect_retry_timer.lock().unwrap(), FsmEvent::ConnectRetryTimerExpires, s.clone(), &log, ); Self::step( resolution, - &timers.keepalive_timer, + &timers.keepalive_timer.lock().unwrap(), FsmEvent::KeepaliveTimerExpires, s.clone(), &log, ); Self::step( resolution, - &timers.hold_timer, + &timers.hold_timer.lock().unwrap(), FsmEvent::HoldTimerExpires, s.clone(), &log, ); Self::step( resolution, - &timers.idle_hold_timer, + &timers.idle_hold_timer.lock().unwrap(), FsmEvent::IdleHoldTimerExpires, s.clone(), &log, ); Self::step( resolution, - &timers.delay_open_timer, + &timers.delay_open_timer.lock().unwrap(), FsmEvent::DelayOpenTimerExpires, s.clone(), &log, diff --git a/bgp/src/connection.rs b/bgp/src/connection.rs index 89f89942..d57704cb 100644 --- a/bgp/src/connection.rs +++ b/bgp/src/connection.rs @@ -12,6 +12,15 @@ use std::sync::mpsc::Sender; use std::sync::{Arc, Mutex}; use std::time::Duration; +#[cfg(target_os = "linux")] +pub const MAX_MD5SIG_KEYLEN: usize = libc::TCP_MD5SIG_MAXKEYLEN; + +#[cfg(target_os = "illumos")] +pub const MAX_MD5SIG_KEYLEN: usize = 80; + +#[cfg(target_os = "macos")] +pub const MAX_MD5SIG_KEYLEN: usize = 80; + /// Implementors of this trait listen to and accept BGP connections. pub trait BgpListener { /// Bind to an address and listen for connections. @@ -48,6 +57,8 @@ pub trait BgpConnection: Send + Clone { &self, event_tx: Sender>, timeout: Duration, + min_ttl: Option, + md5_key: Option, ) -> Result<(), Error> where Self: Sized; @@ -61,4 +72,12 @@ pub trait BgpConnection: Send + Clone { // Return the local address being used for the connection. fn local(&self) -> Option; + + fn set_min_ttl(&self, ttl: u8) -> Result<(), Error>; + + fn set_md5_sig( + &self, + keylen: u16, + key: [u8; MAX_MD5SIG_KEYLEN], + ) -> Result<(), Error>; } diff --git a/bgp/src/connection_channel.rs b/bgp/src/connection_channel.rs index f7f66952..0cb3b5c3 100644 --- a/bgp/src/connection_channel.rs +++ b/bgp/src/connection_channel.rs @@ -7,7 +7,7 @@ /// code in this file is to implement BgpListener and BgpConnection such that /// the core functionality of the BGP upper-half in `session.rs` may be tested /// rapidly using a simulated network. -use crate::connection::{BgpConnection, BgpListener}; +use crate::connection::{BgpConnection, BgpListener, MAX_MD5SIG_KEYLEN}; use crate::error::Error; use crate::messages::Message; use crate::session::FsmEvent; @@ -159,6 +159,8 @@ impl BgpConnection for BgpConnectionChannel { &self, event_tx: Sender>, timeout: Duration, + _ttl_sec: Option, + _md5_key: Option, ) -> Result<(), Error> { debug!(self.log, "[{}] connecting", self.peer); let (local, remote) = channel(); @@ -209,6 +211,18 @@ impl BgpConnection for BgpConnectionChannel { fn local(&self) -> Option { Some(self.addr) } + + fn set_min_ttl(&self, _ttl: u8) -> Result<(), Error> { + Ok(()) + } + + fn set_md5_sig( + &self, + _keylen: u16, + _key: [u8; MAX_MD5SIG_KEYLEN], + ) -> Result<(), Error> { + Ok(()) + } } impl BgpConnectionChannel { diff --git a/bgp/src/connection_tcp.rs b/bgp/src/connection_tcp.rs index 742c4aa7..d1426162 100644 --- a/bgp/src/connection_tcp.rs +++ b/bgp/src/connection_tcp.rs @@ -2,26 +2,38 @@ // 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::connection::{BgpConnection, BgpListener}; +use crate::connection::{BgpConnection, BgpListener, MAX_MD5SIG_KEYLEN}; use crate::error::Error; use crate::messages::{ ErrorCode, ErrorSubcode, Header, Message, MessageType, NotificationMessage, - OpenMessage, UpdateMessage, + OpenMessage, RouteRefreshMessage, UpdateMessage, }; use crate::session::FsmEvent; use crate::to_canonical; +use libc::{c_int, sockaddr_storage}; +#[cfg(any(target_os = "linux", target_os = "illumos"))] +use libc::{c_void, IPPROTO_IP, IPPROTO_IPV6, IPPROTO_TCP}; +#[cfg(target_os = "linux")] +use libc::{IP_MINTTL, TCP_MD5SIG}; + use mg_common::lock; use slog::{error, trace, warn, Logger}; use std::collections::BTreeMap; use std::io::Read; use std::io::Write; use std::net::{IpAddr, SocketAddr, TcpListener, TcpStream, ToSocketAddrs}; +use std::os::fd::AsRawFd; use std::sync::atomic::AtomicBool; use std::sync::mpsc::Sender; use std::sync::{Arc, Mutex}; use std::thread::spawn; use std::time::Duration; +#[cfg(target_os = "illumos")] +const IP_MINTTL: i32 = 0x1c; +#[cfg(target_os = "illumos")] +const TCP_MD5SIG: i32 = 0x27; + pub struct BgpListenerTcp { addr: SocketAddr, listener: TcpListener, @@ -88,14 +100,55 @@ impl BgpConnection for BgpConnectionTcp { } } + #[allow(unused_variables)] fn connect( &self, event_tx: Sender>, timeout: Duration, + min_ttl: Option, + md5_key: Option, ) -> Result<(), Error> { - match TcpStream::connect_timeout(&self.peer, timeout) { - Ok(new_conn) => { + let s = match self.peer { + SocketAddr::V4(_) => socket2::Socket::new( + socket2::Domain::IPV4, + socket2::Type::STREAM, + None, + )?, + SocketAddr::V6(_) => socket2::Socket::new( + socket2::Domain::IPV6, + socket2::Type::STREAM, + None, + )?, + }; + + #[cfg(target_os = "linux")] + if let Some(key) = md5_key { + slog::info!(self.log, "setting md5 key: {:?}", key); + let mut keyval = [0u8; MAX_MD5SIG_KEYLEN]; + let len = key.len(); + keyval[..len].copy_from_slice(key.as_bytes()); + if let Err(e) = + set_md5_sig_fd(s.as_raw_fd(), len as u16, keyval, self.peer) + { + error!(self.log, "set md5 key for tcp conn failed: {e}"); + return Err(e); + } + } + + #[cfg(target_os = "illumos")] + if let Some(key) = md5_key { + // TODO This will come later for illumos + return Err(Error::FeatureNotSupported); + } + + let sa: socket2::SockAddr = self.peer.into(); + match s.connect_timeout(&sa, timeout) { + Ok(()) => { + let new_conn: TcpStream = s.into(); lock!(self.conn).replace(new_conn.try_clone()?); + if let Some(ttl) = min_ttl { + self.set_min_ttl(ttl)?; + } Self::recv( self.peer, event_tx.clone(), @@ -150,6 +203,94 @@ impl BgpConnection for BgpConnectionTcp { }; Some(sockaddr) } + + #[allow(unused_variables)] + fn set_min_ttl(&self, ttl: u8) -> Result<(), Error> { + let conn = self.conn.lock().unwrap(); + match conn.as_ref() { + None => Err(Error::NotConnected), + Some(conn) => { + conn.set_ttl(ttl.into())?; + let fd = conn.as_raw_fd(); + let min_ttl = ttl as u32; + #[cfg(any(target_os = "linux", target_os = "illumos"))] + unsafe { + if self.peer().is_ipv4() + && libc::setsockopt( + fd, + IPPROTO_IP, + IP_MINTTL, + &min_ttl as *const u32 as *const c_void, + std::mem::size_of::() as u32, + ) != 0 + { + return Err(Error::Io(std::io::Error::last_os_error())); + } + if self.peer().is_ipv6() + && libc::setsockopt( + fd, + IPPROTO_IPV6, + IP_MINTTL, + &min_ttl as *const u32 as *const c_void, + std::mem::size_of::() as u32, + ) != 0 + { + return Err(Error::Io(std::io::Error::last_os_error())); + } + } + Ok(()) + } + } + } + + #[cfg(target_os = "linux")] + fn set_md5_sig( + &self, + keylen: u16, + key: [u8; MAX_MD5SIG_KEYLEN], + ) -> Result<(), Error> { + slog::info!(self.log, "setting md5 auth for {}", self.peer); + let conn = self.conn.lock().unwrap(); + let fd = match conn.as_ref() { + None => return Err(Error::NotConnected), + Some(c) => c.as_raw_fd(), + }; + + set_md5_sig_fd(fd, keylen, key, self.peer) + } + + #[cfg(target_os = "illumos")] + fn set_md5_sig( + &self, + keylen: u16, + key: [u8; MAX_MD5SIG_KEYLEN], + ) -> Result<(), Error> { + slog::info!(self.log, "setting md5 auth for {}", self.peer); + let conn = self.conn.lock().unwrap(); + match conn.as_ref() { + None => return Err(Error::NotConnected), + Some(c) => { + let local = c.local_addr()?; + let peer = c.peer_addr()?; + let s = String::from_utf8_lossy(&key[..keylen as usize]) + .to_string(); + if let Err(e) = set_md5_sig_fd(c.as_raw_fd(), &s, local, peer) { + error!(self.log, "set md5 key for tcp conn failed: {e}"); + return Err(e); + } + } + }; + Ok(()) + } + + #[cfg(target_os = "macos")] + fn set_md5_sig( + &self, + _keylen: u16, + _key: [u8; MAX_MD5SIG_KEYLEN], + ) -> Result<(), Error> { + return Err(Error::FeatureNotSupported); + } } impl Drop for BgpConnectionTcp { @@ -322,6 +463,17 @@ impl BgpConnectionTcp { } } MessageType::KeepAlive => return Ok(Message::KeepAlive), + MessageType::RouteRefresh => { + match RouteRefreshMessage::from_wire(&msgbuf) { + Ok(m) => m.into(), + Err(_) => { + return Err(std::io::Error::new( + std::io::ErrorKind::Other, + "route refresh message error", + )) + } + } + } }; Ok(msg) @@ -369,3 +521,106 @@ impl BgpConnectionTcp { ) } } + +#[cfg(target_os = "linux")] +fn set_md5_sig_fd( + fd: i32, + keylen: u16, + key: [u8; MAX_MD5SIG_KEYLEN], + peer: SocketAddr, +) -> Result<(), Error> { + let mut sig = TcpMd5Sig { + tcpm_keylen: keylen, + tcpm_key: key, + ..Default::default() + }; + let x = socket2::SockAddr::from(peer); + let x = x.as_storage(); + sig.tcpm_addr = x; + unsafe { + if libc::setsockopt( + fd, + IPPROTO_TCP, + TCP_MD5SIG, + &sig as *const TcpMd5Sig as *const c_void, + std::mem::size_of::() as u32, + ) != 0 + { + return Err(Error::Io(std::io::Error::last_os_error())); + } + } + + Ok(()) +} + +// TODO use actual kernel interfaces instead of this puddle of perl pretending +// to be rust. +#[cfg(target_os = "illumos")] +fn set_md5_sig_fd( + fd: i32, + key: &str, + local: SocketAddr, + peer: SocketAddr, +) -> Result<(), Error> { + { + use std::{ + io::BufWriter, + process::{Command, Stdio}, + }; + + let tcpkey = Command::new("/usr/sbin/tcpkey") + .stdin(Stdio::piped()) + .spawn()?; + let mut tcpkey_in = tcpkey.stdin.unwrap(); + let mut writer = BufWriter::new(&mut tcpkey_in); + + writeln!( + writer, + "add src {} dst {} dport {} authalg md5 authstring {}\n", + local.ip(), + peer.ip(), + peer.port(), + key + )?; + writeln!( + writer, + "add src {} dst {} sport {} authalg md5 authstring {}", + local.ip(), + peer.ip(), + peer.port(), + key + )?; + } + + let yes = true; + unsafe { + if libc::setsockopt( + fd, + IPPROTO_TCP, + TCP_MD5SIG, + &yes as *const bool as *const c_void, + std::mem::size_of::() as u32, + ) != 0 + { + return Err(Error::Io(std::io::Error::last_os_error())); + } + } + + Ok(()) +} + +#[repr(C)] +struct TcpMd5Sig { + tcpm_addr: sockaddr_storage, + tcpm_flags: u8, + tcpm_prefixlen: u8, + tcpm_keylen: u16, + tcpm_ifindex: c_int, + tcpm_key: [u8; MAX_MD5SIG_KEYLEN], +} + +impl Default for TcpMd5Sig { + fn default() -> Self { + unsafe { std::mem::zeroed() } + } +} diff --git a/bgp/src/dispatcher.rs b/bgp/src/dispatcher.rs index 56e39cfb..0ea373e8 100644 --- a/bgp/src/dispatcher.rs +++ b/bgp/src/dispatcher.rs @@ -51,7 +51,7 @@ impl Dispatcher { continue; } }; - let conn = match listener.accept( + let accepted = match listener.accept( self.log.clone(), self.addr_to_session.clone(), Duration::from_millis(100), @@ -65,13 +65,13 @@ impl Dispatcher { continue; } }; - let addr = conn.peer().ip(); + let addr = accepted.peer().ip(); match lock!(self.addr_to_session).get(&addr) { Some(tx) => { - if let Err(e) = tx.send(FsmEvent::Connected(conn)) { + if let Err(e) = tx.send(FsmEvent::Connected(accepted)) { slog::error!( self.log, - "failed to send connected envent to session: {e}", + "failed to send connected event to session: {e}", ); continue; } diff --git a/bgp/src/error.rs b/bgp/src/error.rs index 16c30073..103d1b70 100644 --- a/bgp/src/error.rs +++ b/bgp/src/error.rs @@ -2,9 +2,10 @@ // 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 std::net::IpAddr; +use std::{fmt::Display, net::IpAddr}; use num_enum::TryFromPrimitiveError; +use rdb::Prefix4; #[derive(thiserror::Error, Debug)] pub enum Error { @@ -101,12 +102,14 @@ pub enum Error { #[from] TryFromPrimitiveError, ), + #[error("Cease error subcode")] + CeaseSubcode( + #[from] TryFromPrimitiveError, + ), + #[error("Path origin error")] PathOrigin(#[from] TryFromPrimitiveError), - #[error("Community value error")] - Community(#[from] TryFromPrimitiveError), - #[error("message parse error")] Parse(nom::Err<(Vec, nom::error::ErrorKind)>), @@ -143,6 +146,15 @@ pub enum Error { #[error("Self loop detected")] SelfLoopDetected, + #[error("AS path missing")] + MissingAsPath, + + #[error("AS path is empty")] + EmptyAsPath, + + #[error("Enforce-first-AS check failed: expected: {0}, found: {1:?}")] + EnforceAsFirst(u32, Vec), + #[error("Invalid address")] InvalidAddress(String), @@ -151,6 +163,54 @@ pub enum Error { #[error("Internal communication error {0}")] InternalCommunication(String), + + #[error("Unexpected ASN {0}")] + UnexpectedAsn(ExpectationMismatch), + + #[error("Hold time too small")] + HoldTimeTooSmall, + + #[error("Invalid NLRI prefix")] + InvalidNlriPrefix(Prefix4), + + #[error("Nexthop cannot equal prefix")] + NexthopSelf(IpAddr), + + #[error("Nexthop missing")] + MissingNexthop, + + #[error("Drop due to user defined policy")] + PolicyCheckFailed, + + #[error("Policy error {0}")] + PolicyError(#[from] crate::policy::Error), + + #[error("Message conversion: {0}")] + MessageConversion(#[from] crate::messages::MessageConvertError), + + #[error("Changing peer address is not supported. Delete and recreate.")] + PeerAddressUpdate, + + #[error("Failed to send event: {0}")] + EventSend(String), + + #[error("Invalid keepalive time, must be smaller than hold time")] + KeepaliveLargerThanHoldTime, + + #[error("Feature not yet supported")] + FeatureNotSupported, +} + +#[derive(Debug)] +pub struct ExpectationMismatch { + pub expected: T, + pub got: T, +} + +impl Display for ExpectationMismatch { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "expected: {} got: {}", self.expected, self.got) + } } impl<'a> From> for Error { diff --git a/bgp/src/lib.rs b/bgp/src/lib.rs index 6e36004f..e80f96d9 100644 --- a/bgp/src/lib.rs +++ b/bgp/src/lib.rs @@ -13,9 +13,12 @@ pub mod error; pub mod fanout; pub mod log; pub mod messages; +pub mod policy; pub mod router; pub mod session; +mod rhai_integration; + #[cfg(test)] #[macro_use] extern crate lazy_static; diff --git a/bgp/src/log.rs b/bgp/src/log.rs index 210bee55..e3ea03a9 100644 --- a/bgp/src/log.rs +++ b/bgp/src/log.rs @@ -29,7 +29,7 @@ macro_rules! trc { slog::trace!( $self.log, "[{}] {}", - $self.neighbor.name, + lock!($self.neighbor.name), format!($($args)+) ) } @@ -41,7 +41,7 @@ macro_rules! dbg { slog::debug!( $self.log, "[{}] {}", - $self.neighbor.name, + lock!($self.neighbor.name), format!($($args)+) ) } @@ -53,7 +53,7 @@ macro_rules! inf { slog::info!( $self.log, "[{}] {}", - $self.neighbor.name, + lock!($self.neighbor.name), format!($($args)+) ) } @@ -65,7 +65,7 @@ macro_rules! wrn { slog::warn!( $self.log, "[{}] {}", - $self.neighbor.name, + lock!($self.neighbor.name), format!($($args)+) ) } @@ -77,7 +77,7 @@ macro_rules! err { slog::error!( $self.log, "[{}] {}", - $self.neighbor.name, + lock!($self.neighbor.name), format!($($args)+) ) } diff --git a/bgp/src/messages.rs b/bgp/src/messages.rs index b3605c03..859bb414 100644 --- a/bgp/src/messages.rs +++ b/bgp/src/messages.rs @@ -7,10 +7,14 @@ use nom::{ bytes::complete::{tag, take}, number::complete::{be_u16, be_u32, be_u8, u8 as parse_u8}, }; +use num_enum::FromPrimitive; use num_enum::{IntoPrimitive, TryFromPrimitive}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use std::net::{IpAddr, Ipv4Addr}; +use std::{ + collections::BTreeSet, + net::{IpAddr, Ipv4Addr, Ipv6Addr}, +}; pub const MAX_MESSAGE_SIZE: usize = 4096; @@ -41,6 +45,12 @@ pub enum MessageType { /// /// RFC 4271 §4.4 KeepAlive = 4, + + /// When this message is received from a peer, we send that peer all + /// current outbound routes. + /// + /// RFC 2918 + RouteRefresh = 5, } impl From<&Message> for MessageType { @@ -50,6 +60,7 @@ impl From<&Message> for MessageType { Message::Update(_) => Self::Update, Message::Notification(_) => Self::Notification, Message::KeepAlive => Self::KeepAlive, + Message::RouteRefresh(_) => Self::RouteRefresh, } } } @@ -63,6 +74,7 @@ pub enum Message { Update(UpdateMessage), Notification(NotificationMessage), KeepAlive, + RouteRefresh(RouteRefreshMessage), } impl Message { @@ -72,6 +84,7 @@ impl Message { Self::Update(m) => m.to_wire(), Self::Notification(m) => m.to_wire(), Self::KeepAlive => Ok(Vec::new()), + Self::RouteRefresh(m) => m.to_wire(), } } } @@ -94,6 +107,74 @@ impl From for Message { } } +impl From for Message { + fn from(m: RouteRefreshMessage) -> Message { + Message::RouteRefresh(m) + } +} + +#[derive(Debug, thiserror::Error)] +pub enum MessageConvertError { + #[error("not an update")] + NotAnUpdate, + + #[error("not a notification")] + NotANotification, + + #[error("not an open")] + NotAnOpen, + + #[error("not a keepalive")] + NotAKeepalive, + + #[error("not a route refresh")] + NotARouteRefresh, +} + +impl TryFrom for OpenMessage { + type Error = MessageConvertError; + fn try_from(value: Message) -> Result { + if let Message::Open(msg) = value { + Ok(msg) + } else { + Err(MessageConvertError::NotAnOpen) + } + } +} + +impl TryFrom for UpdateMessage { + type Error = MessageConvertError; + fn try_from(value: Message) -> Result { + if let Message::Update(msg) = value { + Ok(msg) + } else { + Err(MessageConvertError::NotAnUpdate) + } + } +} + +impl TryFrom for NotificationMessage { + type Error = MessageConvertError; + fn try_from(value: Message) -> Result { + if let Message::Notification(msg) = value { + Ok(msg) + } else { + Err(MessageConvertError::NotANotification) + } + } +} + +impl TryFrom for RouteRefreshMessage { + type Error = MessageConvertError; + fn try_from(value: Message) -> Result { + if let Message::RouteRefresh(msg) = value { + Ok(msg) + } else { + Err(MessageConvertError::NotARouteRefresh) + } + } +} + /// Each BGP message has a fixed sized header. /// /// ```text @@ -231,24 +312,39 @@ impl OpenMessage { asn: AS_TRANS, hold_time, id, - parameters: vec![OptionalParameter::Capabilities(vec![ - Capability::FourOctetAs { asn }, - ])], + parameters: vec![OptionalParameter::Capabilities(BTreeSet::from( + [Capability::FourOctetAs { asn }], + ))], } } - pub fn add_capabilities(&mut self, capabilities: &[Capability]) { + pub fn add_capabilities(&mut self, capabilities: &BTreeSet) { if capabilities.is_empty() { return; } for p in &mut self.parameters { if let OptionalParameter::Capabilities(cs) = p { - cs.extend_from_slice(capabilities); + cs.extend(capabilities.iter().cloned()); return; } } self.parameters - .push(OptionalParameter::Capabilities(capabilities.into())); + .push(OptionalParameter::Capabilities(capabilities.clone())); + } + + pub fn get_capabilities(&self) -> BTreeSet { + for p in self.parameters.iter() { + if let OptionalParameter::Capabilities(caps) = p { + return caps.clone(); + } + } + BTreeSet::new() + } + + pub fn has_capability(&self, code: CapabilityCode) -> bool { + self.get_capabilities() + .into_iter() + .any(|x| CapabilityCode::from(x) == code) } /// Serialize an open message to wire format. @@ -483,37 +579,110 @@ impl UpdateMessage { pub fn nexthop4(&self) -> Option { for a in &self.path_attributes { - match a.value { - PathAttributeValue::NextHop(IpAddr::V4(addr)) => { - return Some(addr); - } - _ => continue, + if let PathAttributeValue::NextHop(IpAddr::V4(addr)) = a.value { + return Some(addr); } } None } pub fn graceful_shutdown(&self) -> bool { + self.has_community(Community::GracefulShutdown) + } + + pub fn multi_exit_discriminator(&self) -> Option { + for a in &self.path_attributes { + if let PathAttributeValue::MultiExitDisc(med) = &a.value { + return Some(*med); + } + } + None + } + + pub fn local_pref(&self) -> Option { + for a in &self.path_attributes { + if let PathAttributeValue::LocalPref(value) = &a.value { + return Some(*value); + } + } + None + } + + pub fn set_local_pref(&mut self, value: u32) { + for a in &mut self.path_attributes { + if let PathAttributeValue::LocalPref(current) = &mut a.value { + *current = value; + return; + } + } + self.path_attributes + .push(PathAttributeValue::LocalPref(value).into()); + } + + pub fn clear_local_pref(&mut self) { + self.path_attributes + .retain(|a| a.typ.type_code != PathAttributeTypeCode::LocalPref); + } + + pub fn as_path(&self) -> Option> { + for a in &self.path_attributes { + if let PathAttributeValue::AsPath(path) = &a.value { + return Some(path.clone()); + } + if let PathAttributeValue::As4Path(path) = &a.value { + return Some(path.clone()); + } + } + None + } + + pub fn path_len(&self) -> Option { + self.as_path() + .map(|p| p.iter().fold(0, |a, b| a + b.value.len())) + } + + pub fn has_community(&self, community: Community) -> bool { for a in &self.path_attributes { - match &a.value { - PathAttributeValue::Communities(communities) => { - for c in communities { - if *c == Community::GracefulShutdown { - return true; - } + if let PathAttributeValue::Communities(communities) = &a.value { + for c in communities { + if *c == community { + return true; } } - _ => continue, } } false } + + pub fn add_community(&mut self, community: Community) { + for a in &mut self.path_attributes { + if let PathAttributeValue::Communities(ref mut communities) = + &mut a.value + { + communities.push(community); + return; + } + } + self.path_attributes + .push(PathAttributeValue::Communities(vec![community]).into()); + } } /// This data structure captures a network prefix as it's laid out in a BGP /// message. There is a prefix length followed by a variable number of bytes. /// Just enough bytes to express the prefix. -#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, JsonSchema)] +#[derive( + Debug, + PartialEq, + Eq, + Hash, + Clone, + Serialize, + Deserialize, + JsonSchema, + Ord, + PartialOrd, +)] pub struct Prefix { pub length: u8, pub value: Vec, @@ -541,6 +710,176 @@ impl Prefix { }, )) } + + pub fn as_prefix4(&self) -> rdb::Prefix4 { + let v = &self.value; + match self.length { + 0 => rdb::Prefix4 { + value: Ipv4Addr::UNSPECIFIED, + length: 0, + }, + x if x <= 8 => rdb::Prefix4 { + value: Ipv4Addr::from([v[0], 0, 0, 0]), + length: x, + }, + x if x <= 16 => rdb::Prefix4 { + value: Ipv4Addr::from([v[0], v[1], 0, 0]), + length: x, + }, + x if x <= 24 => rdb::Prefix4 { + value: Ipv4Addr::from([v[0], v[1], v[2], 0]), + length: x, + }, + x => rdb::Prefix4 { + value: Ipv4Addr::from([v[0], v[1], v[2], v[3]]), + length: x, + }, + } + } + + pub fn as_prefix6(&self) -> rdb::Prefix6 { + let v = &self.value; + match self.length { + 0 => rdb::Prefix6 { + value: Ipv6Addr::UNSPECIFIED, + length: 0, + }, + x if x <= 8 => rdb::Prefix6 { + value: Ipv6Addr::from([ + v[0], 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]), + length: x, + }, + x if x <= 16 => rdb::Prefix6 { + value: Ipv6Addr::from([ + v[0], v[1], 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]), + length: x, + }, + x if x <= 24 => rdb::Prefix6 { + value: Ipv6Addr::from([ + v[0], v[1], v[2], 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]), + length: x, + }, + x if x <= 32 => rdb::Prefix6 { + value: Ipv6Addr::from([ + v[0], v[1], v[2], v[3], 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]), + length: x, + }, + x if x <= 40 => rdb::Prefix6 { + value: Ipv6Addr::from([ + v[0], v[1], v[2], v[3], v[4], 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, + ]), + length: x, + }, + x if x <= 48 => rdb::Prefix6 { + value: Ipv6Addr::from([ + v[0], v[1], v[2], v[3], v[4], v[5], 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, + ]), + length: x, + }, + x if x <= 56 => rdb::Prefix6 { + value: Ipv6Addr::from([ + v[0], v[1], v[2], v[3], v[4], v[5], v[6], 0, 0, 0, 0, 0, 0, + 0, 0, 0, + ]), + length: x, + }, + x if x <= 64 => rdb::Prefix6 { + value: Ipv6Addr::from([ + v[0], v[1], v[2], v[3], v[4], v[5], v[6], v[7], 0, 0, 0, 0, + 0, 0, 0, 0, + ]), + length: x, + }, + x if x <= 72 => rdb::Prefix6 { + value: Ipv6Addr::from([ + v[0], v[1], v[2], v[3], v[4], v[5], v[6], v[7], v[8], 0, 0, + 0, 0, 0, 0, 0, + ]), + length: x, + }, + x if x <= 80 => rdb::Prefix6 { + value: Ipv6Addr::from([ + v[0], v[1], v[2], v[3], v[4], v[5], v[6], v[7], v[8], v[9], + 0, 0, 0, 0, 0, 0, + ]), + length: x, + }, + x if x <= 88 => rdb::Prefix6 { + value: Ipv6Addr::from([ + v[0], v[1], v[2], v[3], v[4], v[5], v[6], v[7], v[8], v[9], + v[10], 0, 0, 0, 0, 0, + ]), + length: x, + }, + x if x <= 96 => rdb::Prefix6 { + value: Ipv6Addr::from([ + v[0], v[1], v[2], v[3], v[4], v[5], v[6], v[7], v[8], v[9], + v[10], v[11], 0, 0, 0, 0, + ]), + length: x, + }, + x if x <= 104 => rdb::Prefix6 { + value: Ipv6Addr::from([ + v[0], v[1], v[2], v[3], v[4], v[5], v[6], v[7], v[8], v[9], + v[10], v[11], v[12], 0, 0, 0, + ]), + length: x, + }, + x if x <= 112 => rdb::Prefix6 { + value: Ipv6Addr::from([ + v[0], v[1], v[2], v[3], v[4], v[5], v[6], v[7], v[8], v[9], + v[10], v[11], v[12], v[13], 0, 0, + ]), + length: x, + }, + x if x <= 120 => rdb::Prefix6 { + value: Ipv6Addr::from([ + v[0], v[1], v[2], v[3], v[4], v[5], v[6], v[7], v[8], v[9], + v[10], v[11], v[12], v[13], v[14], 0, + ]), + length: x, + }, + x => rdb::Prefix6 { + value: Ipv6Addr::from([ + v[0], v[1], v[2], v[3], v[4], v[5], v[6], v[7], v[8], v[9], + v[10], v[11], v[12], v[13], v[14], v[15], + ]), + length: x, + }, + } + } + + pub fn within(&self, x: &Prefix) -> bool { + if self.length > 128 || x.length > 128 { + return false; + } + if self.length < x.length { + return false; + } + if self.value.len() > 16 || x.value.len() > 16 { + return false; + } + + let mut a = self.value.clone(); + a.resize(16, 0); + let mut a = u128::from_le_bytes(a.try_into().unwrap()); + + let mut b = x.value.clone(); + b.resize(16, 0); + let mut b = u128::from_le_bytes(b.try_into().unwrap()); + + let mask = (1u128 << x.length) - 1; + a &= mask; + b &= mask; + + a == b + } } impl std::str::FromStr for Prefix { @@ -567,36 +906,6 @@ impl std::str::FromStr for Prefix { } } -/// The BGP prefix format only contains enough bytes to describe the prefix -/// so we need to be careful about transferring into fixed width IP addresses. -impl From<&Prefix> for rdb::Prefix4 { - fn from(p: &Prefix) -> Self { - let v = &p.value; - match p.length { - 0 => rdb::Prefix4 { - value: Ipv4Addr::UNSPECIFIED, - length: 0, - }, - x if x <= 8 => rdb::Prefix4 { - value: Ipv4Addr::from([v[0], 0, 0, 0]), - length: x, - }, - x if x <= 16 => rdb::Prefix4 { - value: Ipv4Addr::from([v[0], v[1], 0, 0]), - length: x, - }, - x if x <= 24 => rdb::Prefix4 { - value: Ipv4Addr::from([v[0], v[1], v[2], 0]), - length: x, - }, - x => rdb::Prefix4 { - value: Ipv4Addr::from([v[0], v[1], v[2], v[3]]), - length: x, - }, - } - } -} - impl From for Prefix { fn from(p: rdb::Prefix4) -> Self { Self { @@ -623,6 +932,9 @@ impl From for PathAttribute { PathAttributeValue::AsPath(_) => path_attribute_flags::TRANSITIVE, PathAttributeValue::As4Path(_) => path_attribute_flags::TRANSITIVE, PathAttributeValue::NextHop(_) => path_attribute_flags::TRANSITIVE, + PathAttributeValue::LocalPref(_) => { + path_attribute_flags::TRANSITIVE + } PathAttributeValue::Communities(_) => { path_attribute_flags::OPTIONAL | path_attribute_flags::TRANSITIVE @@ -837,6 +1149,8 @@ impl PathAttributeValue { } Ok(buf) } + Self::LocalPref(value) => Ok(value.to_be_bytes().into()), + Self::MultiExitDisc(value) => Ok(value.to_be_bytes().into()), x => Err(Error::UnsupportedPathAttributeValue(x.clone())), } } @@ -891,24 +1205,28 @@ impl PathAttributeValue { break; } let (out, v) = be_u32(input)?; - communities.push(Community::try_from(v)?); + communities.push(Community::from(v)); input = out; } Ok(PathAttributeValue::Communities(communities)) } + PathAttributeTypeCode::LocalPref => { + let (_input, v) = be_u32(input)?; + Ok(PathAttributeValue::LocalPref(v)) + } x => Err(Error::UnsupportedPathAttributeTypeCode(x)), } } } -/// BGP communities recognized by this BGP implementation. +/// BGP community value #[derive( Debug, PartialEq, Eq, Clone, Copy, - TryFromPrimitive, + FromPrimitive, IntoPrimitive, Serialize, Deserialize, @@ -939,6 +1257,10 @@ pub enum Community { /// containing this value must set the local preference for /// the received routes to a low value, preferably zero. GracefulShutdown = 0xFFFF0000, + + /// A user defined community + #[num_enum(catch_all)] + UserDefined(u32), } /// An enumeration indicating the origin type of a path. @@ -1163,7 +1485,9 @@ impl NotificationMessage { ErrorSubcode::HoldTime(error_subcode) } ErrorCode::Fsm => ErrorSubcode::Fsm(error_subcode), - ErrorCode::Cease => ErrorSubcode::Cease(error_subcode), + ErrorCode::Cease => { + CeaseErrorSubcode::try_from(error_subcode)?.into() + } }; Ok(NotificationMessage { error_code, @@ -1173,6 +1497,32 @@ impl NotificationMessage { } } +// A message sent between peers to ask for re-advertisement of all outbound +// routes. Defined in RFC 2918. +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, JsonSchema)] +pub struct RouteRefreshMessage { + /// Address family identifier. + pub afi: u16, + /// Subsequent address family identifier. + pub safi: u8, +} + +impl RouteRefreshMessage { + pub fn to_wire(&self) -> Result, Error> { + let mut buf = Vec::new(); + buf.extend_from_slice(&self.afi.to_be_bytes()); + buf.push(0); // reserved + buf.push(self.safi); + Ok(buf) + } + pub fn from_wire(input: &[u8]) -> Result { + let (input, afi) = be_u16(input)?; + let (input, _reserved) = parse_u8(input)?; + let (_, safi) = parse_u8(input)?; + Ok(RouteRefreshMessage { afi, safi }) + } +} + /// This enumeration contains possible notification error codes. #[derive( Debug, @@ -1205,7 +1555,7 @@ pub enum ErrorSubcode { Update(UpdateErrorSubcode), HoldTime(u8), Fsm(u8), - Cease(u8), + Cease(CeaseErrorSubcode), } impl From for ErrorSubcode { @@ -1226,6 +1576,12 @@ impl From for ErrorSubcode { } } +impl From for ErrorSubcode { + fn from(x: CeaseErrorSubcode) -> ErrorSubcode { + ErrorSubcode::Cease(x) + } +} + impl ErrorSubcode { fn as_u8(&self) -> u8 { match self { @@ -1234,7 +1590,7 @@ impl ErrorSubcode { Self::Update(u) => *u as u8, Self::HoldTime(x) => *x, Self::Fsm(x) => *x, - Self::Cease(x) => *x, + Self::Cease(x) => *x as u8, } } } @@ -1314,6 +1670,32 @@ pub enum UpdateErrorSubcode { MalformedAsPath, } +/// Cease error subcode types from RFC 4486 +#[derive( + Debug, + PartialEq, + Eq, + Clone, + Copy, + TryFromPrimitive, + Serialize, + Deserialize, + JsonSchema, +)] +#[repr(u8)] +#[serde(rename_all = "snake_case")] +pub enum CeaseErrorSubcode { + Unspecific = 0, + MaximumNumberofPrefixesReached, + AdministrativeShutdown, + PeerDeconfigured, + AdministrativeReset, + ConnectionRejected, + OtherConfigurationChange, + ConnectionCollisionResolution, + OutOfResources, +} + /// The IANA/IETF currently defines the following optional parameter types. #[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, JsonSchema)] #[serde(tag = "type", content = "value", rename_all = "snake_case")] @@ -1325,7 +1707,7 @@ pub enum OptionalParameter { Authentication, //TODO /// Code 2: RFC 5492 - Capabilities(Vec), + Capabilities(BTreeSet), /// Unassigned Unassigned, @@ -1376,10 +1758,10 @@ impl OptionalParameter { Err(Error::ReservedOptionalParameter) } OptionalParameterCode::Capabilities => { - let mut result = Vec::new(); + let mut result = BTreeSet::new(); while !cap_input.is_empty() { let (out, cap) = Capability::from_wire(cap_input)?; - result.push(cap); + result.insert(cap); cap_input = out; } Ok((input, OptionalParameter::Capabilities(result))) @@ -1391,7 +1773,17 @@ impl OptionalParameter { /// The add path element comes as a BGP capability extension as described in /// RFC 7911. -#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, JsonSchema)] +#[derive( + Debug, + PartialEq, + Eq, + Clone, + Serialize, + Deserialize, + JsonSchema, + PartialOrd, + Ord, +)] pub struct AddPathElement { /// Address family identifier. /// @@ -1409,7 +1801,17 @@ pub struct AddPathElement { // /// Optional capabilities supported by a BGP implementation. -#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, JsonSchema)] +#[derive( + Debug, + PartialEq, + Eq, + Clone, + Serialize, + Deserialize, + JsonSchema, + PartialOrd, + Ord, +)] #[serde(rename_all = "snake_case")] pub enum Capability { /// Multiprotocol extensions as defined in RFC 2858 @@ -1418,9 +1820,7 @@ pub enum Capability { safi: u8, }, - //TODO - /// Route refresh capability as defined in RFC 2918. Note this capability - /// is not yet implemented. + /// Route refresh capability as defined in RFC 2918. RouteRefresh {}, //TODO @@ -1480,7 +1880,7 @@ pub enum Capability { /// Add path capability as defined in RFC 7911. AddPath { - elements: Vec, + elements: BTreeSet, }, //TODO @@ -1561,7 +1961,6 @@ impl Capability { Ok(buf) } Self::RouteRefresh {} => { - //TODO audit let buf = vec![CapabilityCode::RouteRefresh as u8, 0]; Ok(buf) } @@ -1617,7 +2016,6 @@ impl Capability { Ok((input, Capability::MultiprotocolExtensions { afi, safi })) } CapabilityCode::RouteRefresh => { - //TODO handle for real, needed for arista Ok((&input[len..], Capability::RouteRefresh {})) } @@ -1630,12 +2028,12 @@ impl Capability { Ok((input, Capability::FourOctetAs { asn })) } CapabilityCode::AddPath => { - let mut elements = Vec::new(); + 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)?; - elements.push(AddPathElement { + elements.insert(AddPathElement { afi, safi, send_receive, @@ -1909,7 +2307,7 @@ impl Capability { } /// The set of capability codes supported by this BGP implementation -#[derive(Debug, Eq, PartialEq, TryFromPrimitive)] +#[derive(Debug, Eq, PartialEq, TryFromPrimitive, Copy, Clone)] #[repr(u8)] pub enum CapabilityCode { /// RFC 5492 @@ -2042,6 +2440,138 @@ pub enum CapabilityCode { Experimental51, } +impl From for CapabilityCode { + fn from(value: Capability) -> Self { + match value { + Capability::MultiprotocolExtensions { afi: _, safi: _ } => { + CapabilityCode::MultiprotocolExtensions + } + Capability::RouteRefresh {} => CapabilityCode::RouteRefresh, + Capability::OutboundRouteFiltering {} => { + CapabilityCode::OutboundRouteFiltering + } + Capability::MultipleRoutesToDestination {} => { + CapabilityCode::MultipleRoutesToDestination + } + Capability::ExtendedNextHopEncoding {} => { + CapabilityCode::ExtendedNextHopEncoding + } + Capability::BGPExtendedMessage {} => { + CapabilityCode::BGPExtendedMessage + } + Capability::BgpSec {} => CapabilityCode::BgpSec, + Capability::MultipleLabels {} => CapabilityCode::MultipleLabels, + Capability::BgpRole {} => CapabilityCode::BgpRole, + Capability::GracefulRestart {} => CapabilityCode::GracefulRestart, + Capability::FourOctetAs { asn: _ } => CapabilityCode::FourOctetAs, + Capability::DynamicCapability {} => { + CapabilityCode::DynamicCapability + } + Capability::MultisessionBgp {} => CapabilityCode::MultisessionBgp, + Capability::AddPath { elements: _ } => CapabilityCode::AddPath, + Capability::EnhancedRouteRefresh {} => { + CapabilityCode::EnhancedRouteRefresh + } + Capability::LongLivedGracefulRestart {} => { + CapabilityCode::LongLivedGracefulRestart + } + Capability::RoutingPolicyDistribution {} => { + CapabilityCode::RoutingPolicyDistribution + } + Capability::Fqdn {} => CapabilityCode::Fqdn, + Capability::PrestandardRouteRefresh {} => { + CapabilityCode::PrestandardRouteRefresh + } + Capability::PrestandardOrfAndPd {} => { + CapabilityCode::PrestandardOrfAndPd + } + Capability::PrestandardOutboundRouteFiltering {} => { + CapabilityCode::PrestandardOutboundRouteFiltering + } + Capability::PrestandardMultisession {} => { + CapabilityCode::PrestandardMultisession + } + Capability::PrestandardFqdn {} => CapabilityCode::PrestandardFqdn, + Capability::PrestandardOperationalMessage {} => { + CapabilityCode::PrestandardOperationalMessage + } + Capability::Experimental { code } => match code { + 0 => CapabilityCode::Experimental0, + 1 => CapabilityCode::Experimental1, + 2 => CapabilityCode::Experimental2, + 3 => CapabilityCode::Experimental3, + 4 => CapabilityCode::Experimental4, + 5 => CapabilityCode::Experimental5, + 6 => CapabilityCode::Experimental6, + 7 => CapabilityCode::Experimental7, + 8 => CapabilityCode::Experimental8, + 9 => CapabilityCode::Experimental9, + 10 => CapabilityCode::Experimental10, + 11 => CapabilityCode::Experimental11, + 12 => CapabilityCode::Experimental12, + 13 => CapabilityCode::Experimental13, + 14 => CapabilityCode::Experimental14, + 15 => CapabilityCode::Experimental15, + 16 => CapabilityCode::Experimental16, + 17 => CapabilityCode::Experimental17, + 18 => CapabilityCode::Experimental18, + 19 => CapabilityCode::Experimental19, + 20 => CapabilityCode::Experimental20, + 21 => CapabilityCode::Experimental21, + 22 => CapabilityCode::Experimental22, + 23 => CapabilityCode::Experimental23, + 24 => CapabilityCode::Experimental24, + 25 => CapabilityCode::Experimental25, + 26 => CapabilityCode::Experimental26, + 27 => CapabilityCode::Experimental27, + 28 => CapabilityCode::Experimental28, + 29 => CapabilityCode::Experimental29, + 30 => CapabilityCode::Experimental30, + 31 => CapabilityCode::Experimental31, + 32 => CapabilityCode::Experimental32, + 33 => CapabilityCode::Experimental33, + 34 => CapabilityCode::Experimental34, + 35 => CapabilityCode::Experimental35, + 36 => CapabilityCode::Experimental36, + 37 => CapabilityCode::Experimental37, + 38 => CapabilityCode::Experimental38, + 39 => CapabilityCode::Experimental39, + 40 => CapabilityCode::Experimental40, + 41 => CapabilityCode::Experimental41, + 42 => CapabilityCode::Experimental42, + 43 => CapabilityCode::Experimental43, + 44 => CapabilityCode::Experimental44, + 45 => CapabilityCode::Experimental45, + 46 => CapabilityCode::Experimental46, + 47 => CapabilityCode::Experimental47, + 48 => CapabilityCode::Experimental48, + 49 => CapabilityCode::Experimental49, + 50 => CapabilityCode::Experimental50, + 51 => CapabilityCode::Experimental51, + _ => CapabilityCode::Experimental0, + }, + Capability::Unassigned { code: _ } => CapabilityCode::Reserved, + Capability::Reserved { code: _ } => CapabilityCode::Reserved, + } + } +} + +/// Address families supported by Maghemite BGP. +#[repr(u16)] +pub enum Afi { + /// Internet protocol version 4 + Ipv4 = 1, + /// Internet protocol version 6 + Ipv6 = 2, +} + +/// Subsequent address families supported by Maghemite BGP. +#[repr(u8)] +pub enum Safi { + /// Network Layer Reachability Information used for unicast forwarding + NlriUnicast = 1, +} + #[cfg(test)] mod tests { use super::*; @@ -2120,4 +2650,34 @@ mod tests { UpdateMessage::from_wire(&buf).expect("update message from wire"); assert_eq!(um0, um1); } + + #[test] + fn prefix_within() { + let prefixes: &[Prefix] = &[ + "10.10.10.10/32".parse().unwrap(), + "10.10.10.0/24".parse().unwrap(), + "10.10.0.0/16".parse().unwrap(), + "10.0.0.0/8".parse().unwrap(), + ]; + + for i in 0..prefixes.len() { + for j in i..prefixes.len() { + // shorter prefixes contain longer or equal + assert!(prefixes[i].within(&prefixes[j])); + if i != j { + // longer prefixes should not contain shorter + assert!(!prefixes[j].within(&prefixes[i])) + } + } + } + + let a: Prefix = "10.10.0.0/16".parse().unwrap(); + let b: Prefix = "10.20.0.0/16".parse().unwrap(); + assert!(!a.within(&b)); + let a: Prefix = "10.10.0.0/24".parse().unwrap(); + assert!(!a.within(&b)); + + let b: Prefix = "0.0.0.0/0".parse().unwrap(); + assert!(a.within(&b)); + } } diff --git a/bgp/src/policy.rs b/bgp/src/policy.rs new file mode 100644 index 00000000..19432e25 --- /dev/null +++ b/bgp/src/policy.rs @@ -0,0 +1,550 @@ +// 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/. + +//! This file contains the Maghemite BGP policy engine. Policies fall into the +//! following categories. +//! +//! - Outgoing open message shaping. +//! - Outgoing update message shaping. +//! - Incoming open message checking. +//! - Incoming update message checking. +//! +//! Outgoing message shaping provides an opportunity for policy scripts to +//! modify and filter outgoing open and update messages. Incomming message +//! checking provides an opportunity for policy scripts to ensure open and +//! update messages meet policy requirements and can prevent them from being +//! accepted. Generally speaking shaping an incomming message does not make +//! sense, with one notable exception: local preference. Check scripts may +//! modify the local preference of incoming update messages to influence how +//! the Maghemite bestpath algorithm selects routes for data plane +//! installation. +//! +//! Shape and check scripts have full access to the message they are operating +//! over, as well as information about the peer the message is being exchanged +//! with. +//! +//! Policy scripts are operator defined and written in Rhai. + +use crate::messages::{ + CapabilityCode, Message, OpenMessage, Prefix, UpdateMessage, +}; +use crate::rhai_integration::*; +use rhai::{ + Dynamic, Engine, EvalAltResult, FnPtr, NativeCallContext, ParseError, + Scope, AST, +}; +use slog::{debug, info, Logger}; +use std::collections::HashSet; +use std::net::IpAddr; + +#[derive(Debug, Clone, Copy)] +pub enum Direction { + Incoming, + Outgoing, +} + +#[derive(Debug, Clone, Copy)] +pub struct PeerInfo { + pub asn: u32, + pub address: IpAddr, +} + +#[derive(Debug, Clone)] +pub struct PolicyContext { + pub direction: Direction, + pub message: Message, + pub peer: PeerInfo, +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum ShaperResult { + Emit(Message), + Drop, +} + +// TODO this is too general, we really only need to perform differences on +// updates +impl ShaperResult { + pub fn difference(&self, other: &ShaperResult) -> ShaperResult { + match (self, other) { + (ShaperResult::Drop, ShaperResult::Drop) => ShaperResult::Drop, + (ShaperResult::Drop, b @ ShaperResult::Emit(_)) => b.clone(), + (ShaperResult::Emit(a), ShaperResult::Drop) => { + ShaperResult::Emit(Self::diff_emit_to_drop(a)) + } + (ShaperResult::Emit(a), ShaperResult::Emit(b)) => { + ShaperResult::Emit(Self::diff_emit_to_emit(a, b)) + } + } + } + + fn diff_emit_to_drop(b: &Message) -> Message { + match b { + Message::Open(m) => Self::diff_emit_to_drop_open(m).into(), + Message::Update(m) => Self::diff_emit_to_drop_update(m).into(), + m @ Message::Notification(_) => m.clone(), + m @ Message::KeepAlive => m.clone(), + m @ Message::RouteRefresh(_) => m.clone(), + } + } + + fn diff_emit_to_drop_open(b: &OpenMessage) -> OpenMessage { + b.clone() + } + + fn diff_emit_to_drop_update(b: &UpdateMessage) -> UpdateMessage { + // if we were emitting before and dropping now, that means all nlris + // need to be sent out as withdraws. + let mut new = b.clone(); + new.withdrawn = new.nlri.clone(); + new.nlri.clear(); + new + } + + fn diff_emit_to_emit(a: &Message, b: &Message) -> Message { + match (a, b) { + (Message::Update(a), Message::Update(b)) => { + Self::diff_emit_to_emit_update(a, b).into() + } + (Message::Open(_), m @ Message::Open(_)) => m.clone(), + // See todo above on this entire impl. The programmable policy + // framework is not yet accessible from omicron so this code is + // not reachable. + _ => todo!(), + } + } + + fn diff_emit_to_emit_update( + a: &UpdateMessage, + b: &UpdateMessage, + ) -> UpdateMessage { + // anything that was previously being announced that is no longer + // being announced, must be withdrawn + let previous: HashSet = + a.nlri.iter().cloned().collect(); + + let current: HashSet = + b.nlri.iter().cloned().collect(); + + let mut new = b.clone(); + new.withdrawn = previous.difference(¤t).cloned().collect(); + + new + } +} + +impl ShaperResult { + pub fn unwrap(self) -> Message { + match self { + Self::Drop => panic!("unwrap dropped shaper result"), + Self::Emit(message) => message, + } + } +} + +#[derive(PartialEq, Eq, Copy, Clone, Debug)] +pub enum CheckerResult { + Accept, + Drop, +} + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("Policy eval error: {0}")] + RhaiEval(#[from] Box), + + #[error("Rhai parser error: {0}")] + RhaiParse(#[from] ParseError), + + #[error("Incorrect open signature: expected 3 arguments, found {0}")] + BadOpenSignature(usize), + + #[error("Open function missing")] + MissingOpenFunction, + + #[error("Incorrect update signature: expected 3 arguments, found {0}")] + BadUpdateSignature(usize), + + #[error("Update function missing")] + MissingUpdateFunction, +} + +pub fn new_rhai_engine() -> Engine { + let mut engine = Engine::new(); + engine.set_max_expr_depths(50, 50); + + engine + .register_type_with_name::("CapabilityCode") + .register_static_module( + "CapabilityCode", + rhai::exported_module!(capability_code_module).into(), + ); + + engine + .register_type_with_name::("CheckerResult") + .register_static_module( + "CheckerResult", + rhai::exported_module!(checker_result_module).into(), + ); + + engine + .register_type_with_name::("ShaperResult") + .register_static_module( + "ShaperResult", + rhai::exported_module!(shaper_result_module).into(), + ); + + engine + .register_type_with_name::("OpenMessage") + .register_fn("has_capability", OpenMessage::rhai_has_capability) + .register_fn("add_four_octet_as", OpenMessage::add_four_octet_as) + .register_fn("emit", OpenMessage::emit); + + engine + .register_type_with_name::("UpdateMessage") + .register_fn("has_community", UpdateMessage::rhai_has_community) + .register_fn("add_community", UpdateMessage::rhai_add_community) + .register_fn("emit", UpdateMessage::emit) + .register_raw_fn( + "prefix_filter", + [ + std::any::TypeId::of::(), + std::any::TypeId::of::(), + ], + |context: NativeCallContext, args: &mut [&'_ mut Dynamic]| { + // get the passed in function + let fp = args[1].take().cast::(); + let mut msg = args[0].write_lock::().unwrap(); + msg.prefix_filter(|p| { + fp.call_raw(&context, None, [Dynamic::from(p.clone())]) + .unwrap() + .cast::() + }); + Ok(()) + }, + ); + + engine + .register_type_with_name::("Prefix") + .register_fn("within", Prefix::within_rhai); + + #[cfg(debug_assertions)] + { + println!("Functions registered:"); + engine + .gen_fn_signatures(false) + .into_iter() + .for_each(|func| println!("{func}")); + println!(); + } + + engine +} + +fn set_engine_logger( + engine: &mut Engine, + log: Logger, + component: &str, + asn: u32, +) { + //TODO have a log scraper ship these to somewhere the user can get at them + let info_log = + log.new(slog::o!("component" => component.to_string(), "asn" => asn)); + engine.on_print(move |s| { + info!(info_log, "{}", s); + }); + + let debug_log = + log.new(slog::o!("component" => component.to_string(), "asn" => asn)); + engine.on_debug(move |s, src, pos| { + debug!(debug_log, "[{src:?}:{pos}] {}", s); + }); +} + +fn new_rhai_scope(ctx: &PolicyContext) -> Scope { + let mut scope = Scope::new(); + scope.push("direction", ctx.direction); + scope.push("message", ctx.message.clone()); + scope.push("peer", ctx.peer); + scope +} + +pub fn check_incoming_open( + m: OpenMessage, + checker: &AST, + asn: u32, + address: IpAddr, + log: Logger, +) -> Result { + let ctx = PolicyContext { + direction: Direction::Incoming, + message: m.clone().into(), + peer: PeerInfo { asn, address }, + }; + + let mut scope = new_rhai_scope(&ctx); + let mut engine = new_rhai_engine(); + set_engine_logger(&mut engine, log, "checker", asn); + + Ok(engine.call_fn::( + &mut scope, + checker, + "open", + (m, asn, address), + )?) +} + +pub fn check_incoming_update( + m: UpdateMessage, + checker: &AST, + asn: u32, + address: IpAddr, + log: Logger, +) -> Result { + let ctx = PolicyContext { + direction: Direction::Incoming, + message: m.clone().into(), + peer: PeerInfo { asn, address }, + }; + + let mut scope = new_rhai_scope(&ctx); + let mut engine = new_rhai_engine(); + set_engine_logger(&mut engine, log, "checker", asn); + + Ok(engine.call_fn::( + &mut scope, + checker, + "update", + (m, asn, address), + )?) +} + +pub fn shape_outgoing_open( + m: OpenMessage, + shaper: &AST, + asn: u32, + address: IpAddr, + log: Logger, +) -> Result { + let ctx = PolicyContext { + direction: Direction::Incoming, + message: m.clone().into(), + peer: PeerInfo { asn, address }, + }; + + let mut scope = new_rhai_scope(&ctx); + let mut engine = new_rhai_engine(); + set_engine_logger(&mut engine, log, "checker", asn); + + Ok(engine.call_fn::( + &mut scope, + shaper, + "open", + (m.clone(), asn as i64, address), + )?) +} + +pub fn shape_outgoing_update( + m: UpdateMessage, + shaper: &AST, + asn: u32, + address: IpAddr, + log: Logger, +) -> Result { + let ctx = PolicyContext { + direction: Direction::Incoming, + message: m.clone().into(), + peer: PeerInfo { asn, address }, + }; + + let mut scope = new_rhai_scope(&ctx); + let mut engine = new_rhai_engine(); + set_engine_logger(&mut engine, log, "checker", asn); + + Ok(engine.call_fn::( + &mut scope, + shaper, + "update", + (m.clone(), asn as i64, address), + )?) +} + +pub fn load_shaper(program_source: &str) -> Result { + // same as checker for now + load_checker(program_source) +} + +pub fn load_checker(program_source: &str) -> Result { + let engine = new_rhai_engine(); + let mut ast = engine.compile(program_source)?; + ast.set_source(program_source); + + match ast.iter_functions().find(|f| f.name == "open") { + Some(open) => { + if open.params.len() != 3 { + return Err(Error::BadOpenSignature(open.params.len())); + } + } + None => return Err(Error::MissingOpenFunction), + } + + match ast.iter_functions().find(|f| f.name == "update") { + Some(update) => { + if update.params.len() != 3 { + return Err(Error::BadUpdateSignature(update.params.len())); + } + } + None => return Err(Error::MissingUpdateFunction), + } + + Ok(ast) +} + +#[cfg(test)] +mod test { + use slog::Drain; + + use crate::messages::{ + Community, PathAttribute, PathAttributeType, PathAttributeTypeCode, + PathAttributeValue, + }; + + use super::*; + + fn log() -> Logger { + let drain = slog_bunyan::new(std::io::stdout()).build().fuse(); + let drain = slog_async::Async::new(drain) + .chan_size(0x8000) + .build() + .fuse(); + slog::Logger::root(drain, slog::o!()) + } + + #[test] + fn open_require_4byte_as() { + // 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 source = + std::fs::read_to_string("../bgp/policy/policy-check0.rhai") + .unwrap(); + let ast = load_checker(&source).unwrap(); + let result = + check_incoming_open(m, &ast, asn.into(), addr, log()).unwrap(); + 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 result = + check_incoming_open(m, &ast, asn.into(), addr, log()).unwrap(); + assert_eq!(result, CheckerResult::Accept); + } + + #[test] + fn update_drop_on_no_export() { + // check that messages with the no-export community are dropped + let asn = 47; + let addr = "198.51.100.1".parse().unwrap(); + let mut m = UpdateMessage::default(); + m.path_attributes.push(PathAttribute { + typ: PathAttributeType { + flags: 0, + type_code: PathAttributeTypeCode::Communities, + }, + value: PathAttributeValue::Communities(vec![Community::NoExport]), + }); + let source = + std::fs::read_to_string("../bgp/policy/policy-check0.rhai") + .unwrap(); + let ast = load_checker(&source).unwrap(); + let result = check_incoming_update(m, &ast, asn, addr, log()).unwrap(); + assert_eq!(result, CheckerResult::Drop); + + // check that messages without the no-export community are accepted + let m = UpdateMessage::default(); + let result = check_incoming_update(m, &ast, asn, addr, log()).unwrap(); + assert_eq!(result, CheckerResult::Accept); + } + + #[test] + fn open_add_4byte_as() { + // 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 source = + std::fs::read_to_string("../bgp/policy/policy-shape0.rhai") + .unwrap(); + let ast = load_shaper(&source).unwrap(); + let result = + shape_outgoing_open(m.clone(), &ast, asn.into(), addr, log()) + .unwrap(); + m.add_four_octet_as(74); + assert_eq!(result, ShaperResult::Emit(m.into())); + } + + #[test] + fn update_shape_community() { + // check that messages with the no-export community are dropped + let asn = 100; + let addr = "198.51.100.1".parse().unwrap(); + let mut m = UpdateMessage::default(); + m.path_attributes.push(PathAttribute { + typ: PathAttributeType { + flags: 0, + type_code: PathAttributeTypeCode::Communities, + }, + value: PathAttributeValue::Communities(vec![Community::NoExport]), + }); + let source = + std::fs::read_to_string("../bgp/policy/policy-shape0.rhai") + .unwrap(); + let ast = load_shaper(&source).unwrap(); + let result = + shape_outgoing_update(m.clone(), &ast, asn, addr, log()).unwrap(); + m.add_community(Community::UserDefined(1701)); + assert_eq!(result, ShaperResult::Emit(m.into())); + } + + #[test] + fn shape_update_prefixes() { + let addr = "198.51.100.1".parse().unwrap(); + let originated = UpdateMessage { + nlri: vec![ + "10.10.0.0/16".parse().unwrap(), + "10.128.0.0/16".parse().unwrap(), + ], + ..Default::default() + }; + let filtered = UpdateMessage { + nlri: vec!["10.128.0.0/16".parse().unwrap()], + ..Default::default() + }; + let source = + std::fs::read_to_string("../bgp/policy/shape-prefix0.rhai") + .unwrap(); + let ast = load_shaper(&source).unwrap(); + + // ASN 100 should not have any changes + let result: UpdateMessage = + shape_outgoing_update(originated.clone(), &ast, 100, addr, log()) + .unwrap() + .unwrap() + .try_into() + .unwrap(); + + assert_eq!(result, originated.clone()); + + // ASN 65402 should have only the 10.128./16 prefix + let result: UpdateMessage = + shape_outgoing_update(originated, &ast, 65402, addr, log()) + .unwrap() + .unwrap() + .try_into() + .unwrap(); + + assert_eq!(result, filtered); + } +} diff --git a/bgp/src/rhai_integration.rs b/bgp/src/rhai_integration.rs new file mode 100644 index 00000000..dea0f83f --- /dev/null +++ b/bgp/src/rhai_integration.rs @@ -0,0 +1,193 @@ +// 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 std::collections::BTreeSet; + +use crate::{ + messages::{ + Capability, CapabilityCode, Community, Message, OpenMessage, Prefix, + UpdateMessage, + }, + policy::{CheckerResult, ShaperResult}, +}; +use rhai::{export_module, plugin::*, Module}; + +macro_rules! create_enum_module { + ($module:ident : $typ:ty => $($variant:ident),+) => { + #[rhai::export_module] + pub mod $module { + $( + #[allow(non_upper_case_globals)] + pub const $variant: $typ = <$typ>::$variant; + )* + } + }; +} + +create_enum_module! { + capability_code_module: CapabilityCode => + Reserved, + MultiprotocolExtensions, + RouteRefresh, + OutboundRouteFiltering, + MultipleRoutesToDestination, + ExtendedNextHopEncoding, + BGPExtendedMessage, + BgpSec, + MultipleLabels, + BgpRole, + GracefulRestart, + FourOctetAs, + DynamicCapability, + MultisessionBgp, + AddPath, + EnhancedRouteRefresh, + LongLivedGracefulRestart, + RoutingPolicyDistribution, + Fqdn, + PrestandardRouteRefresh, + PrestandardOrfAndPd, + PrestandardOutboundRouteFiltering, + PrestandardMultisession, + PrestandardFqdn, + PrestandardOperationalMessage +} + +create_enum_module! { + checker_result_module: CheckerResult => Accept, Drop +} + +// Rhai needs methods to be &mut self and not just &self, so the following +// methods are to accomplish that and a bit of type translation in cases +// where complex rust types would be difficult to deal with in rhai. +impl OpenMessage { + pub fn rhai_has_capability(&mut self, code: CapabilityCode) -> bool { + self.has_capability(code) + } + pub fn add_four_octet_as(&mut self, asn: i64) { + let asn = match asn.try_into() { + Ok(asn) => asn, + Err(_) => return, //TODO something better? + }; + self.add_capabilities(&BTreeSet::from([Capability::FourOctetAs { + asn, + }])); + } + pub fn emit(&mut self) -> ShaperResult { + ShaperResult::Emit(Message::Open(self.clone())) + } +} + +impl UpdateMessage { + pub fn rhai_has_community(&mut self, community: i64) -> bool { + // rhai integers are of type i64, so if we get something bigger, the + // answer is no, as communities out of the 32 bit range are not defined + let c: u32 = match community.try_into() { + Ok(c) => c, + Err(_) => return false, + }; + self.has_community(Community::from(c)) + } + + pub fn rhai_add_community(&mut self, community: i64) { + let c: u32 = match community.try_into() { + Ok(c) => c, + Err(_) => return, //TODO something better + }; + self.add_community(Community::from(c)); + } + + pub fn emit(&mut self) -> ShaperResult { + ShaperResult::Emit(Message::Update(self.clone())) + } + + pub fn prefix_filter(&mut self, f: F) + where + F: Clone + Fn(&Prefix) -> bool, + { + self.withdrawn.retain(f.clone()); + self.nlri.retain(f); + } + + pub fn get_nlri(&mut self) -> Vec { + self.nlri.clone() + } + + pub fn set_nlri(&mut self, value: Vec) { + self.nlri = value; + } +} + +// Create a plugin module with functions constructing the 'ShaperResult' variants +#[export_module] +pub mod shaper_result_module { + + use crate::{messages::Message, policy::ShaperResult}; + use rhai::Dynamic; + + // Constructors for 'ShaperResult' variants + #[allow(non_upper_case_globals)] + pub const Drop: ShaperResult = ShaperResult::Drop; + + #[allow(non_snake_case)] + pub fn Emit(value: Message) -> ShaperResult { + ShaperResult::Emit(value) + } + + /// Return the current variant of `ShaperResult`. + #[rhai_fn(global, get = "enum_type", pure)] + pub fn get_type(sr: &mut ShaperResult) -> String { + match sr { + ShaperResult::Drop => "Drop".to_string(), + ShaperResult::Emit(_) => "Emit".to_string(), + } + } + + /// Return the inner value. + #[rhai_fn(global, get = "value", pure)] + pub fn get_value(sr: &mut ShaperResult) -> Dynamic { + match sr { + ShaperResult::Drop => Dynamic::UNIT, + ShaperResult::Emit(x) => Dynamic::from(x.clone()), + } + } + + // Access to inner values by position + + /// Return the value kept in the first position of `ShaperResult`. + #[rhai_fn(global, get = "field_0", pure)] + pub fn get_field_0(sr: &mut ShaperResult) -> Dynamic { + match sr { + ShaperResult::Drop => Dynamic::UNIT, + ShaperResult::Emit(x) => Dynamic::from(x.clone()), + } + } + + // Printing + #[rhai_fn(global, name = "to_string", name = "to_debug", pure)] + pub fn to_string(sr: &mut ShaperResult) -> String { + format!("{sr:?}") + } + + // '==' and '!=' operators + #[rhai_fn(global, name = "==", pure)] + pub fn eq(sr: &mut ShaperResult, sr2: ShaperResult) -> bool { + sr == &sr2 + } + #[rhai_fn(global, name = "!=", pure)] + pub fn neq(sr: &mut ShaperResult, sr2: ShaperResult) -> bool { + sr != &sr2 + } +} + +impl Prefix { + pub fn within_rhai(&mut self, x: &str) -> bool { + let x: Prefix = match x.parse() { + Ok(p) => p, + Err(_) => return false, + }; + let s = self.clone(); + s.within(&x) + } +} diff --git a/bgp/src/router.rs b/bgp/src/router.rs index 93f5bd8e..70dbd779 100644 --- a/bgp/src/router.rs +++ b/bgp/src/router.rs @@ -7,20 +7,27 @@ use crate::config::RouterConfig; use crate::connection::BgpConnection; use crate::error::Error; use crate::fanout::{Egress, Fanout}; +use crate::messages::PathOrigin; use crate::messages::{ As4PathSegment, AsPathType, Community, PathAttribute, PathAttributeValue, - PathOrigin, Prefix, UpdateMessage, + Prefix, UpdateMessage, }; +use crate::policy::load_checker; +use crate::policy::load_shaper; use crate::session::{FsmEvent, NeighborInfo, SessionInfo, SessionRunner}; use mg_common::{lock, read_lock, write_lock}; +use rdb::Prefix4; use rdb::{Asn, Db}; +use rhai::AST; use slog::Logger; use std::collections::BTreeMap; +use std::collections::BTreeSet; use std::net::IpAddr; use std::net::SocketAddr; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::mpsc::Receiver; use std::sync::mpsc::Sender; +use std::sync::MutexGuard; use std::sync::{Arc, Mutex, RwLock}; use std::thread::spawn; use std::time::Duration; @@ -37,6 +44,9 @@ pub struct Router { /// A set of BGP session runners indexed by peer IP address. pub sessions: Mutex>>>, + /// Compiled policy programs. + pub policy: Policy, + /// The logger used by this router. log: Logger, @@ -78,6 +88,7 @@ impl Router { db, sessions: Mutex::new(BTreeMap::new()), fanout: Arc::new(RwLock::new(Fanout::default())), + policy: Policy::default(), } } @@ -119,6 +130,26 @@ impl Router { fanout.remove_egress(peer); } + pub fn ensure_session( + self: &Arc, + peer: PeerConfig, + bind_addr: SocketAddr, + event_tx: Sender>, + event_rx: Receiver>, + info: SessionInfo, + ) -> Result, Error> { + let a2s = lock!(self.addr_to_session); + if a2s.contains_key(&peer.host.ip()) { + Ok(EnsureSessionResult::Updated( + self.update_session(peer, info)?, + )) + } else { + Ok(EnsureSessionResult::New(self.new_session_locked( + a2s, peer, bind_addr, event_tx, event_rx, info, + )?)) + } + } + pub fn new_session( self: &Arc, peer: PeerConfig, @@ -127,16 +158,30 @@ impl Router { event_rx: Receiver>, info: SessionInfo, ) -> Result>, Error> { - let mut a2s = lock!(self.addr_to_session); + let a2s = lock!(self.addr_to_session); if a2s.contains_key(&peer.host.ip()) { - return Err(Error::PeerExists); + Err(Error::PeerExists) + } else { + self.new_session_locked( + a2s, peer, bind_addr, event_tx, event_rx, info, + ) } + } + pub fn new_session_locked( + self: &Arc, + mut a2s: MutexGuard>>>, + peer: PeerConfig, + bind_addr: SocketAddr, + event_tx: Sender>, + event_rx: Receiver>, + info: SessionInfo, + ) -> Result>, Error> { a2s.insert(peer.host.ip(), event_tx.clone()); drop(a2s); let neighbor = NeighborInfo { - name: peer.name.clone(), + name: Arc::new(Mutex::new(peer.name.clone())), host: peer.host, }; @@ -146,7 +191,7 @@ impl Router { Duration::from_secs(peer.hold_time), Duration::from_secs(peer.idle_hold_time), Duration::from_secs(peer.delay_open), - Arc::new(Mutex::new(info)), + Arc::new(Mutex::new(info.clone())), event_rx, event_tx.clone(), neighbor.clone(), @@ -174,6 +219,21 @@ impl Router { Ok(runner) } + pub fn update_session( + self: &Arc, + peer: PeerConfig, + info: SessionInfo, + ) -> Result>, Error> { + let session = match lock!(self.sessions).get(&peer.host.ip()) { + None => return Err(Error::UnknownPeer(peer.host.ip())), + Some(s) => s.clone(), + }; + + session.update_session_parameters(peer, info)?; + + Ok(session) + } + pub fn delete_session(&self, addr: IpAddr) { lock!(self.addr_to_session).remove(&addr); self.remove_fanout(addr); @@ -189,47 +249,78 @@ impl Router { Ok(()) } - pub fn originate4(&self, prefixes: Vec) -> Result<(), Error> { + pub fn create_origin4(&self, prefixes: Vec) -> Result<(), Error> { + let prefix4: Vec = + prefixes.iter().cloned().map(|x| x.as_prefix4()).collect(); + self.db.create_origin4(&prefix4)?; + self.announce_origin4(&prefixes); + Ok(()) + } + + pub fn set_origin4(&self, prefixes: Vec) -> Result<(), Error> { + let origin4 = self.db.get_origin4()?; + let current: BTreeSet<&Prefix4> = origin4.iter().collect(); + + let prefix4: Vec = + prefixes.iter().cloned().map(|x| x.as_prefix4()).collect(); + + let new: BTreeSet<&Prefix4> = prefix4.iter().collect(); + + let to_withdraw: Vec<_> = + current.difference(&new).map(|x| (**x).into()).collect(); + + let to_announce: Vec<_> = + new.difference(¤t).map(|x| (**x).into()).collect(); + + self.db.set_origin4(&prefix4)?; + + self.withdraw_origin4(&to_withdraw); + self.announce_origin4(&to_announce); + Ok(()) + } + + pub fn clear_origin4(&self) -> Result<(), Error> { + let current = self.db.get_origin4()?; + let prefix: Vec = + current.iter().cloned().map(Into::into).collect(); + self.withdraw_origin4(&prefix); + self.db.clear_origin4()?; + Ok(()) + } + + fn announce_origin4(&self, prefixes: &Vec) { let mut update = UpdateMessage { path_attributes: self.base_attributes(), ..Default::default() }; - for p in &prefixes { + for p in prefixes { update.nlri.push(p.clone()); - self.db.add_origin4(p.into())?; } if !update.nlri.is_empty() { read_lock!(self.fanout).send_all(&update); } - - Ok(()) } - pub fn withdraw4(&self, prefixes: Vec) -> Result<(), Error> { + pub fn withdraw_origin4(&self, prefixes: &Vec) { let mut update = UpdateMessage { path_attributes: self.base_attributes(), ..Default::default() }; - for p in &prefixes { + for p in prefixes { update.withdrawn.push(p.clone()); - self.db.remove_origin4(p.into())?; } if !update.withdrawn.is_empty() { read_lock!(self.fanout).send_all(&update); } - - Ok(()) } pub fn base_attributes(&self) -> Vec { - let mut path_attributes = vec![ - //TODO hardcode - PathAttributeValue::Origin(PathOrigin::Egp).into(), - ]; + let mut path_attributes = + vec![PathAttributeValue::Origin(PathOrigin::Igp).into()]; if self.graceful_shutdown.load(Ordering::Relaxed) { path_attributes.push( @@ -276,8 +367,11 @@ impl Router { } pub fn graceful_shutdown(&self, enabled: bool) -> Result<(), Error> { - self.graceful_shutdown.store(enabled, Ordering::Relaxed); - self.announce_all() + if enabled != self.graceful_shutdown.load(Ordering::Relaxed) { + self.graceful_shutdown.store(enabled, Ordering::Relaxed); + self.announce_all()?; + } + Ok(()) } pub fn in_graceful_shutdown(&self) -> bool { @@ -285,7 +379,7 @@ impl Router { } fn announce_all(&self) -> Result<(), Error> { - let originated = self.db.get_originated4()?; + let originated = self.db.get_origin4()?; let mut update = UpdateMessage { path_attributes: self.base_attributes(), @@ -293,10 +387,98 @@ impl Router { }; for p in &originated { update.nlri.push((*p).into()); - self.db.add_origin4(*p)?; } read_lock!(self.fanout).send_all(&update); Ok(()) } } + +pub enum EnsureSessionResult { + New(Arc>), + Updated(Arc>), +} + +#[derive(Default, Clone)] +pub struct Policy { + pub shaper: Arc>>, + pub checker: Arc>>, +} + +#[derive(Debug, thiserror::Error)] +pub enum LoadPolicyError { + #[error("Policy program compilation error: {0}")] + Compilation(String), + + #[error("Policy program already exists")] + Confilct, +} + +#[derive(Debug, thiserror::Error)] +pub enum UnloadPolicyError { + #[error("Policy program not loaded")] + NotFound, +} + +impl Policy { + // Load a shaper and return the previously loaded shaper (if any). + pub fn load_shaper( + &self, + program_source: &str, + overwrite: bool, + ) -> Result, LoadPolicyError> { + let mut current = self.shaper.write().unwrap(); + if current.is_some() && !overwrite { + return Err(LoadPolicyError::Confilct); + } + let ast = load_shaper(program_source) + .map_err(|e| LoadPolicyError::Compilation(e.to_string()))?; + Ok(current.replace(ast)) + } + + pub fn unload_shaper(&self) -> Result { + let mut current = self.shaper.write().unwrap(); + if current.is_none() { + return Err(UnloadPolicyError::NotFound); + } + Ok(current.take().unwrap()) + } + + pub fn shaper_source(&self) -> Option { + self.shaper + .read() + .unwrap() + .clone() + .and_then(|ast| ast.source().map(|s| s.to_owned())) + } + + pub fn load_checker( + &self, + program_source: &str, + overwrite: bool, + ) -> Result, LoadPolicyError> { + let mut current = self.checker.write().unwrap(); + if current.is_some() && !overwrite { + return Err(LoadPolicyError::Confilct); + } + let ast = load_checker(program_source) + .map_err(|e| LoadPolicyError::Compilation(e.to_string()))?; + Ok(current.replace(ast)) + } + + pub fn unload_checker(&self) -> Result { + let mut current = self.checker.write().unwrap(); + if current.is_none() { + return Err(UnloadPolicyError::NotFound); + } + Ok(current.take().unwrap()) + } + + pub fn checker_source(&self) -> Option { + self.checker + .read() + .unwrap() + .clone() + .and_then(|ast| ast.source().map(|s| s.to_owned())) + } +} diff --git a/bgp/src/session.rs b/bgp/src/session.rs index 12dbf419..9785a4b8 100644 --- a/bgp/src/session.rs +++ b/bgp/src/session.rs @@ -3,25 +3,27 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. use crate::clock::Clock; -use crate::connection::BgpConnection; -use crate::error::Error; +use crate::config::PeerConfig; +use crate::connection::{BgpConnection, MAX_MD5SIG_KEYLEN}; +use crate::error::{Error, ExpectationMismatch}; use crate::fanout::Fanout; use crate::messages::{ - AddPathElement, Capability, ErrorCode, ErrorSubcode, Message, - NotificationMessage, OpenMessage, OptionalParameter, PathAttributeValue, - UpdateMessage, + AddPathElement, Afi, Capability, Community, ErrorCode, ErrorSubcode, + Message, NotificationMessage, OpenMessage, OptionalParameter, + PathAttributeValue, RouteRefreshMessage, Safi, UpdateMessage, }; +use crate::policy::{CheckerResult, ShaperResult}; use crate::router::Router; use crate::{dbg, err, inf, to_canonical, trc, wrn}; use mg_common::{lock, read_lock, write_lock}; pub use rdb::DEFAULT_ROUTE_PRIORITY; -use rdb::{Asn, Db, Prefix4}; +use rdb::{Asn, BgpPathProperties, Db, ImportExportPolicy, Prefix, Prefix4}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use slog::Logger; -use std::collections::{BTreeMap, VecDeque}; +use std::collections::{BTreeSet, VecDeque}; use std::fmt::{self, Display, Formatter}; -use std::net::{Ipv4Addr, SocketAddr}; +use std::net::SocketAddr; use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::sync::mpsc::{Receiver, Sender}; use std::sync::{Arc, Mutex, RwLock}; @@ -156,6 +158,20 @@ pub enum FsmEvent { // Instructs peer to announce the update Announce(UpdateMessage), + // The shaper for the router has changed. Event contains previous checker. + // Current shaper is available in the router policy object. + ShaperChanged(Option), + + /// Fires when export policy has changed. + ExportPolicyChanged(ImportExportPolicy), + + // The checker for the router has changed. Event contains previous checker. + // Current checker is available in the router policy object. + CheckerChanged(Option), + + // Indicates the peer session should be reset. + Reset, + /// Local system administrator manually starts the peer connection. ManualStart, @@ -252,6 +268,12 @@ pub enum FsmEvent { /// Fires when an invalid update message is received. UpdateMsgErr, + + /// Fires when we need to ask the peer for a route refresh. + RouteRefreshNeeded, + + /// Fires when path attributes have changed. + PathAttributesChanged, } impl fmt::Debug for FsmEvent { @@ -260,6 +282,10 @@ impl fmt::Debug for FsmEvent { Self::Message(message) => write!(f, "message {message:?}"), Self::Connected(_) => write!(f, "connected"), Self::Announce(update) => write!(f, "update {update:?}"), + Self::ShaperChanged(_) => write!(f, "shaper changed"), + Self::CheckerChanged(_) => write!(f, "checker changed"), + Self::ExportPolicyChanged(_) => write!(f, "export policy changed"), + Self::Reset => write!(f, "reset"), Self::ManualStart => write!(f, "manual start"), Self::ManualStop => write!(f, "manual stop"), Self::AutomaticStart => write!(f, "automatic start"), @@ -296,66 +322,65 @@ impl fmt::Debug for FsmEvent { Self::KeepAliveMsg => write!(f, "keepalive message"), Self::UpdateMsg => write!(f, "update message"), Self::UpdateMsgErr => write!(f, "update message error"), + Self::RouteRefreshNeeded => write!(f, "route refresh needed"), + Self::PathAttributesChanged => write!(f, "path attributes changed"), } } } // TODO break up into config/state objects. /// Information about a session. +#[derive(Default, Debug, Deserialize, Serialize, JsonSchema, Clone)] pub struct SessionInfo { /// Track how many times a connection has been attempted. pub connect_retry_counter: u64, - /// Start the peer session automatically. - pub allow_automatic_start: bool, + /// Passively wait for the remote BGP peer to establish a TCP connection. + pub passive_tcp_establishment: bool, - /// Stop the peer automatically under certain conditions. - /// TODO: list conditions - pub allow_automatic_stop: bool, + /// The ASN of the remote peer. + pub remote_asn: Option, - /// Increase/decrease the idle_hold_timer in response to peer connectivity - /// flapping. - pub damp_peer_oscillations: bool, + /// The ASN of the remote peer. + pub remote_id: Option, - /// Allow connections from peers that are not explicitly configured. - pub accept_connections_unconfigured_peers: bool, + /// Minimum acceptable TTL value for incomming BGP packets. + pub min_ttl: Option, - /// Detect open message collisions when in the established state. - pub collision_detect_established_state: bool, + /// Md5 peer authentication key + pub md5_auth_key: Option, - /// Delay sending out the initial open message. - pub delay_open: bool, + /// Multi-exit discriminator. This an optional attribute that is intended to + /// be used on external eBGP sessions to discriminate among multiple exit or + /// entry points to the same neighboring AS. The value of this attribute is + /// a four-octet unsigned number, called a metric. All other factors being + /// equal, the exit point with the lower metric should be preferred. + pub multi_exit_discriminator: Option, - /// Passively wait for the remote BGP peer to establish a TCP connection. - pub passive_tcp_establishment: bool, + /// Communities to be attached to updates sent over this session. + pub communities: BTreeSet, - /// Allow sending notification messages without first sending an open - /// message. - pub send_notification_without_open: bool, + /// Local preference attribute added to updates if this is an iBGP session + pub local_pref: Option, - /// Enable fine-grained tracking and logging of TCP connection state. - pub track_tcp_state: bool, + /// Capabilities received from the peer. + pub capabilities_received: BTreeSet, - /// The ASN of the remote peer. - pub remote_asn: Option, -} + /// Capabilities sent to the peer. + pub capabilities_sent: BTreeSet, -impl Default for SessionInfo { - fn default() -> Self { - SessionInfo { - connect_retry_counter: 0, - allow_automatic_start: false, - allow_automatic_stop: false, - damp_peer_oscillations: false, - accept_connections_unconfigured_peers: false, - collision_detect_established_state: false, - delay_open: true, - passive_tcp_establishment: false, - send_notification_without_open: false, - track_tcp_state: false, - remote_asn: None, - } - } + /// Ensure that routes received from eBGP peers have the peer's ASN as the + /// first element in the AS path. + pub enforce_first_as: bool, + + /// Policy governing imported routes. + pub allow_import: ImportExportPolicy, + + /// Policy governing exported routes. + pub allow_export: ImportExportPolicy, + + /// Vlan tag to assign to data plane routes created by this session. + pub vlan_id: Option, } impl SessionInfo { @@ -367,7 +392,7 @@ impl SessionInfo { /// Information about a neighbor (peer). #[derive(Debug, Clone)] pub struct NeighborInfo { - pub name: String, + pub name: Arc>, pub host: SocketAddr, } @@ -413,6 +438,8 @@ impl MessageHistory { pub struct SessionCounters { pub keepalives_sent: AtomicU64, pub keepalives_received: AtomicU64, + pub route_refresh_sent: AtomicU64, + pub route_refresh_received: AtomicU64, pub opens_sent: AtomicU64, pub opens_received: AtomicU64, pub notifications_sent: AtomicU64, @@ -441,9 +468,15 @@ pub struct SessionCounters { pub notification_send_failure: AtomicU64, pub open_send_failure: AtomicU64, pub keepalive_send_failure: AtomicU64, + pub route_refresh_send_failure: AtomicU64, pub update_send_failure: AtomicU64, } +pub enum ShaperApplication { + Current, + Difference(Option), +} + /// This is the top level object that tracks a BGP session with a peer. pub struct SessionRunner { /// A sender that can be used to send FSM events to this session. When a @@ -462,19 +495,22 @@ pub struct SessionRunner { /// Counters for message types sent and received, state transitions, etc. pub counters: Arc, - session: Arc>, + /// Clock that drives the state machine for this session. + pub clock: Clock, + + pub session: Arc>, event_rx: Receiver>, state: Arc>, last_state_change: Mutex, asn: Asn, id: u32, - clock: Clock, bind_addr: Option, shutdown: AtomicBool, running: AtomicBool, db: Db, fanout: Arc>>, router: Arc>, + log: Logger, } @@ -554,6 +590,8 @@ impl SessionRunner { return; }; + self.initialize_capabilities(); + // Run the BGP peer state machine. dbg!(self; "starting peer state machine"); let mut current = FsmState::::Idle; @@ -626,9 +664,36 @@ impl SessionRunner { } } + fn initialize_capabilities(&self) { + let mut session = lock!(self.session); + session.capabilities_sent = BTreeSet::from([ + //Capability::RouteRefresh{}, + //Capability::EnhancedRouteRefresh{}, + Capability::MultiprotocolExtensions { + afi: 1, //IP + safi: 1, //NLRI for unicast + }, + //Capability::GracefulRestart{}, + Capability::AddPath { + elements: BTreeSet::from([AddPathElement { + afi: 1, //IP + safi: 1, //NLRI for unicast + send_receive: 1, //receive + }]), + }, + Capability::RouteRefresh {}, + ]); + } + /// Initial state. Refuse all incomming BGP connections. No resources /// allocated to peer. fn idle(&self) -> FsmState { + if lock!(self.clock.timers.idle_hold_timer).interval.is_zero() { + return FsmState::Connect; + } else { + lock!(self.clock.timers.idle_hold_timer).enable(); + } + let event = match self.event_rx.recv() { Ok(event) => event, Err(e) => { @@ -639,15 +704,16 @@ impl SessionRunner { match event { FsmEvent::ManualStart => { - self.clock.timers.idle_hold_timer.enable(); if lock!(self.session).passive_tcp_establishment { let conn = Cnx::new( self.bind_addr, self.neighbor.host, self.log.clone(), ); + lock!(self.clock.timers.idle_hold_timer).disable(); FsmState::Active(conn) } else { + lock!(self.clock.timers.idle_hold_timer).disable(); FsmState::Connect } } @@ -656,6 +722,7 @@ impl SessionRunner { self.counters .idle_hold_timer_expirations .fetch_add(1, Ordering::Relaxed); + lock!(self.clock.timers.idle_hold_timer).disable(); FsmState::Connect } FsmEvent::Message(Message::KeepAlive) => { @@ -689,14 +756,20 @@ impl SessionRunner { /// Waiting for the TCP connection to be completed. fn on_connect(&self) -> FsmState { lock!(self.session).connect_retry_counter = 0; - self.clock.timers.connect_retry_timer.enable(); + lock!(self.clock.timers.connect_retry_timer).enable(); + + let min_ttl = lock!(self.session).min_ttl; + let md5_auth_key = lock!(self.session).md5_auth_key.clone(); // Start with an initial connection attempt let conn = Cnx::new(self.bind_addr, self.neighbor.host, self.log.clone()); - if let Err(e) = - conn.connect(self.event_tx.clone(), self.clock.resolution) - { + if let Err(e) = conn.connect( + self.event_tx.clone(), + self.clock.resolution, + min_ttl, + md5_auth_key.clone(), + ) { wrn!(self; "initial connect attempt failed: {e}"); } loop { @@ -712,32 +785,75 @@ impl SessionRunner { } }; match event { + FsmEvent::Reset => { + inf!(self; "exit connect due to reset"); + lock!(self.session).connect_retry_counter = 0; + lock!(self.clock.timers.connect_retry_timer).disable(); + return FsmState::Idle; + } // If the connect retry timer fires, try to connect again. FsmEvent::ConnectRetryTimerExpires => { self.counters .connection_retries .fetch_add(1, Ordering::Relaxed); - if let Err(e) = conn - .connect(self.event_tx.clone(), self.clock.resolution) - { + if let Err(e) = conn.connect( + self.event_tx.clone(), + self.clock.resolution, + min_ttl, + md5_auth_key.clone(), + ) { wrn!(self; "connect attempt failed: {e}"); } lock!(self.session).connect_retry_counter += 1; } + FsmEvent::Message(Message::Open(om)) => { + self.message_history + .lock() + .unwrap() + .receive(om.clone().into()); + self.counters + .opens_received + .fetch_add(1, Ordering::Relaxed); + if let Err(e) = self.handle_open(&conn, &om) { + wrn!(self; "failed to handle open message: {e}"); + //TODO send a notification to the peer letting them know we are + // rejecting the open message? + return FsmState::Active(conn); + } + + // ACK the open with a reciprocal open and a keepalive and transition + // to open confirm. + if let Err(e) = self.send_open(&conn) { + err!(self; "send open failed {e}"); + return FsmState::Idle; + } + self.send_keepalive(&conn); + return FsmState::OpenConfirm(PeerConnection { + conn, + id: om.id, + }); + } + // The underlying connection has accepted a TCP connection // initiated by the peer. FsmEvent::Connected(accepted) => { + lock!(self.session).connect_retry_counter = 0; + lock!(self.clock.timers.connect_retry_timer).disable(); + if let Err(e) = self.ensure_connection_policy(&accepted) { + err!(self; "{e}"); + return FsmState::Idle; + } inf!(self; "accepted connection from {}", accepted.peer()); - self.clock.timers.connect_retry_timer.disable(); if let Err(e) = self.send_open(&accepted) { err!(self; "send open failed {e}"); return FsmState::Idle; } - self.clock.timers.hold_timer.reset(); - self.clock.timers.hold_timer.enable(); - lock!(self.session).connect_retry_counter = 0; - self.clock.timers.connect_retry_timer.disable(); + { + let ht = self.clock.timers.hold_timer.lock().unwrap(); + ht.reset(); + ht.enable(); + } self.counters .passive_connections_accepted .fetch_add(1, Ordering::Relaxed); @@ -747,16 +863,22 @@ impl SessionRunner { // The the peer has accepted the TCP connection we have // initiated. FsmEvent::TcpConnectionConfirmed => { + lock!(self.session).connect_retry_counter = 0; + lock!(self.clock.timers.connect_retry_timer).disable(); + if let Err(e) = self.ensure_connection_policy(&conn) { + err!(self; "{e}"); + return FsmState::Idle; + } inf!(self; "connected to {}", conn.peer()); - self.clock.timers.connect_retry_timer.disable(); if let Err(e) = self.send_open(&conn) { err!(self; "send open failed {e}"); return FsmState::Idle; } - self.clock.timers.hold_timer.reset(); - self.clock.timers.hold_timer.enable(); - lock!(self.session).connect_retry_counter = 0; - self.clock.timers.connect_retry_timer.disable(); + { + let ht = self.clock.timers.hold_timer.lock().unwrap(); + ht.reset(); + ht.enable(); + } self.counters .active_connections_accepted .fetch_add(1, Ordering::Relaxed); @@ -768,12 +890,6 @@ impl SessionRunner { .fetch_add(1, Ordering::Relaxed); wrn!(self; "unexpected keep alive message in connect"); } - FsmEvent::Message(Message::Open(_)) => { - self.counters - .unexpected_open_message - .fetch_add(1, Ordering::Relaxed); - wrn!(self; "unexpected open message in connect"); - } FsmEvent::Message(Message::Update(_)) => { self.counters .unexpected_update_message @@ -803,6 +919,10 @@ impl SessionRunner { // The only thing we really care about in the active state is receiving // an open message from the peer. let om = match event { + FsmEvent::Reset => { + inf!(self; "exit active due to reset"); + return FsmState::Idle; + } FsmEvent::Message(Message::Open(om)) => { self.message_history .lock() @@ -821,16 +941,23 @@ impl SessionRunner { // The underlying connection has accepted a TCP connection // initiated by the peer. FsmEvent::Connected(accepted) => { + if let Err(e) = self.ensure_connection_policy(&accepted) { + err!(self; "{e}"); + return FsmState::Idle; + } inf!(self; "active: accepted connection from {}", accepted.peer()); if let Err(e) = self.send_open(&accepted) { err!(self; "active: send open failed {e}"); return FsmState::Idle; } - self.clock.timers.connect_retry_timer.disable(); - self.clock.timers.hold_timer.reset(); - self.clock.timers.hold_timer.enable(); + lock!(self.clock.timers.connect_retry_timer).disable(); + { + let ht = self.clock.timers.hold_timer.lock().unwrap(); + ht.reset(); + ht.enable(); + } lock!(self.session).connect_retry_counter = 0; - self.clock.timers.connect_retry_timer.disable(); + lock!(self.clock.timers.connect_retry_timer).disable(); self.counters .passive_connections_accepted .fetch_add(1, Ordering::Relaxed); @@ -863,7 +990,7 @@ impl SessionRunner { return FsmState::Active(conn); } }; - if let Err(e) = self.handle_open(&om) { + if let Err(e) = self.handle_open(&conn, &om) { wrn!(self; "failed to handle open message: {e}"); //TODO send a notification to the peer letting them know we are // rejecting the open message? @@ -893,6 +1020,10 @@ impl SessionRunner { // The only thing we really care about in the open sent state is // receiving a reciprocal open message from the peer. let om = match event { + FsmEvent::Reset => { + inf!(self; "exit open sent due to reset"); + return FsmState::Idle; + } FsmEvent::Message(Message::Open(om)) => { self.message_history .lock() @@ -923,24 +1054,58 @@ impl SessionRunner { wrn!(self; "unexpected update message in open sent"); return FsmState::Active(conn); } + FsmEvent::Connected(accepted) => { + // collision detection RFC 4271 6.8 + if lock!(self.session).remote_id.unwrap_or(0) > self.id { + lock!(self.session).connect_retry_counter = 0; + lock!(self.clock.timers.connect_retry_timer).disable(); + if let Err(e) = self.ensure_connection_policy(&accepted) { + err!(self; "{e}"); + return FsmState::Idle; + } + inf!(self; "accepted connection from {}", accepted.peer()); + if let Err(e) = self.send_open(&accepted) { + err!(self; "send open failed {e}"); + return FsmState::Idle; + } + { + let ht = self.clock.timers.hold_timer.lock().unwrap(); + ht.reset(); + ht.enable(); + } + self.counters + .passive_connections_accepted + .fetch_add(1, Ordering::Relaxed); + return FsmState::OpenSent(accepted); + } else { + return FsmState::Active(conn); + } + } other => { wrn!( self; "open sent: expected open, received {:#?}, ignoring", other ); - self.clock.timers.connect_retry_timer.enable(); + lock!(self.clock.timers.connect_retry_timer).enable(); return FsmState::Active(conn); } }; - if let Err(e) = self.handle_open(&om) { - wrn!(self; "failed to handle open message: {e}"); - //TODO send a notification to the peer letting them know we are - // rejecting the open message? - self.clock.timers.connect_retry_timer.enable(); - self.counters - .open_handle_failures - .fetch_add(1, Ordering::Relaxed); - return FsmState::Active(conn); + if let Err(e) = self.handle_open(&conn, &om) { + match e { + Error::PolicyCheckFailed => { + inf!(self; "{}", e) + } + e => { + wrn!(self; "failed to handle open message: {e}"); + //TODO send a notification to the peer letting them know we are + // rejecting the open message? + lock!(self.clock.timers.connect_retry_timer).enable(); + self.counters + .open_handle_failures + .fetch_add(1, Ordering::Relaxed); + return FsmState::Active(conn); + } + } } // ACK the open with a keepalive and transition to open confirm. @@ -958,13 +1123,23 @@ impl SessionRunner { } }; match event { + FsmEvent::Reset => { + inf!(self; "exit open confirm due to reset"); + FsmState::Idle + } // The peer has ACK'd our open message with a keepalive. Start the // session timers and enter session setup. FsmEvent::Message(Message::KeepAlive) => { - self.clock.timers.hold_timer.reset(); - self.clock.timers.hold_timer.enable(); - self.clock.timers.keepalive_timer.reset(); - self.clock.timers.keepalive_timer.enable(); + { + let ht = self.clock.timers.hold_timer.lock().unwrap(); + ht.reset(); + ht.enable(); + } + { + let kt = self.clock.timers.keepalive_timer.lock().unwrap(); + kt.reset(); + kt.enable(); + } self.counters .keepalives_received .fetch_add(1, Ordering::Relaxed); @@ -980,8 +1155,8 @@ impl SessionRunner { .receive(m.clone().into()); wrn!(self; "notification received: {:#?}", m); lock!(self.session).connect_retry_counter += 1; - self.clock.timers.hold_timer.disable(); - self.clock.timers.keepalive_timer.disable(); + self.clock.timers.hold_timer.lock().unwrap().disable(); + self.clock.timers.keepalive_timer.lock().unwrap().disable(); self.counters .notifications_received .fetch_add(1, Ordering::Relaxed); @@ -989,7 +1164,7 @@ impl SessionRunner { } FsmEvent::HoldTimerExpires => { wrn!(self; "open sent: hold timer expired"); - self.clock.timers.hold_timer.disable(); + self.clock.timers.hold_timer.lock().unwrap().disable(); self.send_hold_timer_expired_notification(&pc.conn); self.counters .hold_timer_expirations @@ -1010,6 +1185,33 @@ impl SessionRunner { wrn!(self; "unexpected update message in open confirm"); FsmState::Idle } + FsmEvent::Connected(accepted) => { + // collision detection RFC 4271 6.8 + if lock!(self.session).remote_id.unwrap_or(0) > self.id { + lock!(self.session).connect_retry_counter = 0; + lock!(self.clock.timers.connect_retry_timer).disable(); + if let Err(e) = self.ensure_connection_policy(&accepted) { + err!(self; "{e}"); + return FsmState::Idle; + } + inf!(self; "accepted connection from {}", accepted.peer()); + if let Err(e) = self.send_open(&accepted) { + err!(self; "send open failed {e}"); + return FsmState::Idle; + } + { + let ht = self.clock.timers.hold_timer.lock().unwrap(); + ht.reset(); + ht.enable(); + } + self.counters + .passive_connections_accepted + .fetch_add(1, Ordering::Relaxed); + FsmState::OpenSent(accepted) + } else { + FsmState::OpenConfirm(pc) + } + } // An event we are not expecting, log it and re-enter this state. other => { wrn!( @@ -1024,7 +1226,7 @@ impl SessionRunner { /// Sync up with peers. fn session_setup(&self, pc: PeerConnection) -> FsmState { // Collect the prefixes this router is originating. - let originated = match self.db.get_originated4() { + let originated = match self.db.get_origin4() { Ok(value) => value, Err(e) => { //TODO possible death loop. Should we just panic here? @@ -1057,7 +1259,9 @@ impl SessionRunner { update.nlri.push(p.into()); } read_lock!(self.fanout).send_all(&update); - if let Err(e) = self.send_update(update, &pc.conn) { + if let Err(e) = + self.send_update(update, &pc.conn, ShaperApplication::Current) + { err!(self; "sending update to peer failed {e}"); return self.exit_established(pc); } @@ -1067,6 +1271,31 @@ impl SessionRunner { FsmState::Established(pc) } + fn originate_update( + &self, + pc: &PeerConnection, + sa: ShaperApplication, + ) -> anyhow::Result<()> { + let originated = match self.db.get_origin4() { + Ok(value) => value, + Err(e) => { + //TODO possible death loop. Should we just panic here? + anyhow::bail!("failed to get originated from db: {e}"); + } + }; + let mut update = UpdateMessage { + path_attributes: self.router.base_attributes(), + ..Default::default() + }; + for p in originated { + update.nlri.push(p.into()); + } + if let Err(e) = self.send_update(update, &pc.conn, sa) { + anyhow::bail!("shaper changed: sending update to peer failed {e}"); + } + Ok(()) + } + /// Able to exchange update, notification and keepliave messages with peers. fn on_established(&self, pc: PeerConnection) -> FsmState { let event = match self.event_rx.recv() { @@ -1080,6 +1309,10 @@ impl SessionRunner { } }; match event { + FsmEvent::Reset => { + inf!(self; "exit established due to reset"); + self.exit_established(pc) + } // When the keepliave timer fires, send a keepliave to the peer. FsmEvent::KeepaliveTimerExpires => { self.send_keepalive(&pc.conn); @@ -1101,9 +1334,10 @@ impl SessionRunner { // We've received an update message from the peer. Reset the hold // timer and apply the update to the RIB. FsmEvent::Message(Message::Update(m)) => { - self.clock.timers.hold_timer.reset(); + self.clock.timers.hold_timer.lock().unwrap().reset(); inf!(self; "update received: {m:#?}"); - self.apply_update(m.clone(), pc.id); + let peer_as = lock!(self.session).remote_asn.unwrap_or(0); + self.apply_update(m.clone(), pc.id, peer_as); self.message_history.lock().unwrap().receive(m.into()); self.counters .updates_received @@ -1111,6 +1345,24 @@ impl SessionRunner { FsmState::Established(pc) } + FsmEvent::Message(Message::RouteRefresh(m)) => { + self.clock.timers.hold_timer.lock().unwrap().reset(); + inf!(self; "route refresh received: {m:#?}"); + self.message_history + .lock() + .unwrap() + .receive(m.clone().into()); + self.counters + .route_refresh_received + .fetch_add(1, Ordering::Relaxed); + if let Err(e) = self.handle_refresh(m, &pc) { + err!(self; "handle refresh: {e:}"); + self.exit_established(pc) + } else { + FsmState::Established(pc) + } + } + // We've received a notification from the peer. They are displeased // with us. Exit established and restart from the connect state. FsmEvent::Message(Message::Notification(m)) => { @@ -1129,7 +1381,7 @@ impl SessionRunner { self.counters .keepalives_received .fetch_add(1, Ordering::Relaxed); - self.clock.timers.hold_timer.reset(); + self.clock.timers.hold_timer.lock().unwrap().reset(); FsmState::Established(pc) } @@ -1137,7 +1389,11 @@ impl SessionRunner { // another peer session (redistribution). Send the update to our // peer. FsmEvent::Announce(update) => { - if let Err(e) = self.send_update(update, &pc.conn) { + if let Err(e) = self.send_update( + update, + &pc.conn, + ShaperApplication::Current, + ) { err!(self; "sending update to peer failed {e}"); return self.exit_established(pc); } @@ -1156,6 +1412,108 @@ impl SessionRunner { FsmState::Established(pc) } + FsmEvent::ShaperChanged(previous) => { + match self.originate_update( + &pc, + ShaperApplication::Difference(previous), + ) { + Err(e) => { + err!(self; "originate failed: {e}"); + self.exit_established(pc) + } + Ok(()) => FsmState::Established(pc), + } + } + + FsmEvent::ExportPolicyChanged(previous) => { + let originated = match self.db.get_origin4() { + Ok(value) => value, + Err(e) => { + //TODO possible death loop. Should we just panic here? + err!(self; "failed to get originated from db: {e}"); + return FsmState::SessionSetup(pc); + } + }; + let originated_before: BTreeSet = match previous { + ImportExportPolicy::NoFiltering => { + originated.iter().cloned().collect() + } + ImportExportPolicy::Allow(list) => originated + .clone() + .into_iter() + .filter(|x| list.contains(&Prefix::from(*x))) + .collect(), + }; + let session = lock!(self.session); + let current = &session.allow_export; + let originated_after: BTreeSet = match current { + ImportExportPolicy::NoFiltering => { + originated.iter().cloned().collect() + } + ImportExportPolicy::Allow(list) => originated + .clone() + .into_iter() + .filter(|x| list.contains(&Prefix::from(*x))) + .collect(), + }; + + let to_withdraw: BTreeSet<&Prefix4> = + originated_before.difference(&originated_after).collect(); + + let to_announce: BTreeSet<&Prefix4> = + originated_after.difference(&originated_before).collect(); + + if to_withdraw.is_empty() && to_announce.is_empty() { + return FsmState::Established(pc); + } + + let update = UpdateMessage { + path_attributes: self.router.base_attributes(), + withdrawn: to_withdraw + .into_iter() + .map(|x| crate::messages::Prefix::from(*x)) + .collect(), + nlri: to_announce + .into_iter() + .map(|x| crate::messages::Prefix::from(*x)) + .collect(), + }; + + if let Err(e) = self.send_update( + update, + &pc.conn, + ShaperApplication::Current, + ) { + err!(self; "sending update to peer failed {e}"); + return self.exit_established(pc); + } + + FsmState::Established(pc) + } + + FsmEvent::PathAttributesChanged => { + match self.originate_update(&pc, ShaperApplication::Current) { + Err(e) => { + err!(self; "originate failed: {e}"); + self.exit_established(pc) + } + Ok(()) => FsmState::Established(pc), + } + } + + FsmEvent::RouteRefreshNeeded => { + if let Some(remote_id) = lock!(self.session).remote_id { + self.db.mark_bgp_id_stale(remote_id); + self.send_route_refresh(&pc.conn); + } + FsmState::Established(pc) + } + + FsmEvent::CheckerChanged(_previous) => { + //TODO + FsmState::Established(pc) + } + // Some unexpeted event, log and re-enter established. e => { wrn!(self; "unhandled event: {e:?}"); @@ -1186,8 +1544,9 @@ impl SessionRunner { } /// Handle an open message - fn handle_open(&self, om: &OpenMessage) -> Result<(), Error> { + fn handle_open(&self, conn: &Cnx, om: &OpenMessage) -> Result<(), Error> { let mut remote_asn = om.asn as u32; + let remote_id = om.id; for p in &om.parameters { if let OptionalParameter::Capabilities(caps) = p { for c in caps { @@ -1197,7 +1556,79 @@ impl SessionRunner { } } } - lock!(self.session).remote_asn = Some(remote_asn); + if let Some(expected_remote_asn) = lock!(self.session).remote_asn { + if remote_asn != expected_remote_asn { + self.send_notification( + conn, + ErrorCode::Open, + ErrorSubcode::Open( + crate::messages::OpenErrorSubcode::BadPeerAS, + ), + ); + return Err(Error::UnexpectedAsn(ExpectationMismatch { + expected: expected_remote_asn, + got: remote_asn, + })); + } + } + if let Some(checker) = + self.router.policy.checker.read().unwrap().as_ref() + { + match crate::policy::check_incoming_open( + om.clone(), + checker, + remote_asn, + self.neighbor.host.ip(), + self.log.clone(), + ) { + Ok(result) => match result { + CheckerResult::Accept => {} + CheckerResult::Drop => { + return Err(Error::PolicyCheckFailed) + } + }, + Err(e) => { + err!(self; "open checker exec: {e}"); + } + } + } + { + let mut session = lock!(self.session); + session.remote_asn = Some(remote_asn); + session.remote_id = Some(remote_id); + session.capabilities_received = om.get_capabilities(); + } + + { + let mut ht = self.clock.timers.hold_timer.lock().unwrap(); + let mut kt = self.clock.timers.keepalive_timer.lock().unwrap(); + let mut theirs = false; + let requested = u64::from(om.hold_time); + if requested > 0 { + if requested < 3 { + self.send_notification(conn, ErrorCode::Open, ErrorSubcode::Open( + crate::messages::OpenErrorSubcode::UnacceptableHoldTime, + )); + return Err(Error::HoldTimeTooSmall); + } + if requested < ht.interval.as_secs() { + theirs = true; + ht.interval = Duration::from_secs(requested); + ht.reset(); + // per BGP RFC section 10 + kt.interval = Duration::from_secs(requested / 3); + kt.reset(); + } + } + if !theirs { + ht.interval = + *lock!(self.clock.timers.hold_configured_interval); + ht.reset(); + kt.interval = + *lock!(self.clock.timers.keepalive_configured_interval); + kt.reset(); + } + } Ok(()) } @@ -1216,6 +1647,23 @@ impl SessionRunner { } } + fn send_route_refresh(&self, conn: &Cnx) { + trc!(self; "sending route refresh"); + if let Err(e) = conn.send(Message::RouteRefresh(RouteRefreshMessage { + afi: Afi::Ipv4 as u16, + safi: Safi::NlriUnicast as u8, + })) { + err!(self; "failed to send route refresh {e}"); + self.counters + .keepalive_send_failure + .fetch_add(1, Ordering::Relaxed); + } else { + self.counters + .keepalives_sent + .fetch_add(1, Ordering::Relaxed); + } + } + fn send_hold_timer_expired_notification(&self, conn: &Cnx) { self.send_notification( conn, @@ -1251,42 +1699,60 @@ impl SessionRunner { /// Send an open message to the session peer. fn send_open(&self, conn: &Cnx) -> Result<(), Error> { + let capabilities = lock!(self.session).capabilities_sent.clone(); let mut msg = match self.asn { Asn::FourOctet(asn) => OpenMessage::new4( asn, - self.clock.timers.hold_timer.interval.as_secs() as u16, + self.clock + .timers + .hold_configured_interval + .lock() + .unwrap() + .as_secs() as u16, self.id, ), Asn::TwoOctet(asn) => OpenMessage::new2( asn, - self.clock.timers.hold_timer.interval.as_secs() as u16, + self.clock + .timers + .hold_configured_interval + .lock() + .unwrap() + .as_secs() as u16, self.id, ), }; - // TODO negotiate capabilities - msg.add_capabilities(&[ - //Capability::RouteRefresh{}, - //Capability::EnhancedRouteRefresh{}, - Capability::MultiprotocolExtensions { - afi: 1, //IP - safi: 1, //NLRI for unicast - }, - //Capability::GracefulRestart{}, - Capability::AddPath { - elements: vec![AddPathElement { - afi: 1, //IP - safi: 1, //NLRI for unicast - send_receive: 1, //receive - }], - }, - ]); - self.message_history - .lock() - .unwrap() - .send(msg.clone().into()); + msg.add_capabilities(&capabilities); + + let mut out = Message::from(msg.clone()); + if let Some(shaper) = self.router.policy.shaper.read().unwrap().as_ref() + { + let peer_as = lock!(self.session).remote_asn.unwrap_or(0); + match crate::policy::shape_outgoing_open( + msg.clone(), + shaper, + peer_as, + self.neighbor.host.ip(), + self.log.clone(), + ) { + Ok(result) => match result { + ShaperResult::Emit(msg) => { + out = msg; + } + ShaperResult::Drop => { + return Ok(()); + } + }, + Err(e) => { + err!(self; "open shaper exec: {e}"); + } + } + } + drop(msg); + self.message_history.lock().unwrap().send(out.clone()); self.counters.opens_sent.fetch_add(1, Ordering::Relaxed); - if let Err(e) = conn.send(msg.into()) { + if let Err(e) = conn.send(out) { err!(self; "failed to send open {e}"); self.counters .open_send_failure @@ -1297,11 +1763,80 @@ impl SessionRunner { } } + fn is_ebgp(&self) -> bool { + if let Some(remote) = self.session.lock().unwrap().remote_asn { + if remote != self.asn.as_u32() { + return true; + } + } + false + } + + fn is_ibgp(&self) -> bool { + !self.is_ebgp() + } + + fn shape_update( + &self, + update: UpdateMessage, + shaper_application: ShaperApplication, + ) -> Result { + match shaper_application { + ShaperApplication::Current => self.shape_update_basic(update), + ShaperApplication::Difference(previous) => { + self.shape_update_differential(update, previous) + } + } + } + + fn shape_update_basic( + &self, + update: UpdateMessage, + ) -> Result { + if let Some(shaper) = self.router.policy.shaper.read().unwrap().as_ref() + { + let peer_as = lock!(self.session).remote_asn.unwrap_or(0); + Ok(crate::policy::shape_outgoing_update( + update.clone(), + shaper, + peer_as, + self.neighbor.host.ip(), + self.log.clone(), + )?) + } else { + Ok(ShaperResult::Emit(update.into())) + } + } + + fn shape_update_differential( + &self, + update: UpdateMessage, + previous: Option, + ) -> Result { + let peer_as = lock!(self.session).remote_asn.unwrap_or(0); + + let former = match previous { + Some(shaper) => crate::policy::shape_outgoing_update( + update.clone(), + &shaper, + peer_as, + self.neighbor.host.ip(), + self.log.clone(), + )?, + None => ShaperResult::Emit(update.clone().into()), + }; + + let current = self.shape_update_basic(update)?; + + Ok(former.difference(¤t)) + } + /// Send an update message to the session peer. fn send_update( &self, mut update: UpdateMessage, conn: &Cnx, + shaper_application: ShaperApplication, ) -> Result<(), Error> { let nexthop = to_canonical(match conn.local() { Some(sockaddr) => sockaddr.ip(), @@ -1315,14 +1850,64 @@ impl SessionRunner { .path_attributes .push(PathAttributeValue::NextHop(nexthop).into()); - self.message_history + if let Some(med) = self.session.lock().unwrap().multi_exit_discriminator + { + update + .path_attributes + .push(PathAttributeValue::MultiExitDisc(med).into()); + } + + if self.is_ibgp() { + update.path_attributes.push( + PathAttributeValue::LocalPref( + self.session.lock().unwrap().local_pref.unwrap_or(0), + ) + .into(), + ); + } + + let cs: Vec = self + .session .lock() .unwrap() - .send(update.clone().into()); + .communities + .clone() + .into_iter() + .map(Community::from) + .collect(); + + if !cs.is_empty() { + update + .path_attributes + .push(PathAttributeValue::Communities(cs).into()) + } + + if let ImportExportPolicy::Allow(ref policy) = + lock!(self.session).allow_export + { + let message_policy = policy + .iter() + .filter_map(|x| match x { + rdb::Prefix::V4(x) => Some(x), + _ => None, + }) + .map(|x| crate::messages::Prefix::from(*x)) + .collect::>(); + + update.nlri.retain(|x| message_policy.contains(x)); + update.withdrawn.retain(|x| message_policy.contains(x)); + }; + + let out = match self.shape_update(update, shaper_application)? { + ShaperResult::Emit(msg) => msg, + ShaperResult::Drop => return Ok(()), + }; + + self.message_history.lock().unwrap().send(out.clone()); self.counters.updates_sent.fetch_add(1, Ordering::Relaxed); - if let Err(e) = conn.send(update.into()) { + if let Err(e) = conn.send(out) { err!(self; "failed to send update {e}"); self.counters .update_send_failure @@ -1338,49 +1923,19 @@ impl SessionRunner { /// to the connect state. fn exit_established(&self, pc: PeerConnection) -> FsmState { lock!(self.session).connect_retry_counter += 1; - self.clock.timers.hold_timer.disable(); - self.clock.timers.keepalive_timer.disable(); + self.clock.timers.hold_timer.lock().unwrap().disable(); + self.clock.timers.keepalive_timer.lock().unwrap().disable(); write_lock!(self.fanout).remove_egress(self.neighbor.host.ip()); // remove peer prefixes from db - let withdraw = self.db.remove_peer_prefixes4(pc.id); - - // propagate a withdraw message through fanout - let mut m = BTreeMap::>::new(); - for o in withdraw { - match m.get_mut(&o.nexthop) { - Some(ref mut prefixes) => { - prefixes.push(o.prefix); - } - None => { - m.insert(o.nexthop, vec![o.prefix]); - } - } - } - - for (nexthop, prefixes) in m { - let mut update = UpdateMessage { - path_attributes: vec![PathAttributeValue::NextHop( - nexthop.into(), - ) - .into()], - ..Default::default() - }; - for p in prefixes { - update.withdrawn.push(p.into()); - if let Err(e) = self.db.remove_origin4(p) { - err!(self; "failed to remove origin {p} from db {e}"); - } - } - read_lock!(self.fanout).send_all(&update); - } + self.db.remove_peer_prefixes(pc.id); FsmState::Idle } /// Apply an update by adding it to our RIB. - fn apply_update(&self, update: UpdateMessage, id: u32) { + fn apply_update(&self, mut update: UpdateMessage, id: u32, peer_as: u32) { if let Err(e) = self.check_update(&update) { wrn!( self; @@ -1389,7 +1944,47 @@ impl SessionRunner { ); return; } - self.update_rib(&update, id); + self.apply_static_update_policy(&mut update); + + if let Some(checker) = + self.router.policy.checker.read().unwrap().as_ref() + { + match crate::policy::check_incoming_update( + update.clone(), + checker, + peer_as, + self.neighbor.host.ip(), + self.log.clone(), + ) { + Ok(result) => match result { + CheckerResult::Accept => {} + CheckerResult::Drop => { + return; + } + }, + Err(e) => { + err!(self; "update checker exec: {e}"); + } + } + } + + if let ImportExportPolicy::Allow(ref policy) = + lock!(self.session).allow_import + { + let message_policy = policy + .iter() + .filter_map(|x| match x { + rdb::Prefix::V4(x) => Some(x), + _ => None, + }) + .map(|x| crate::messages::Prefix::from(*x)) + .collect::>(); + + update.nlri.retain(|x| message_policy.contains(x)); + update.withdrawn.retain(|x| message_policy.contains(x)); + }; + + self.update_rib(&update, id, peer_as); // NOTE: for now we are only acting as an edge router. This means we // do not redistribute announcements. If this changes, uncomment @@ -1398,19 +1993,43 @@ impl SessionRunner { // self.fanout_update(&update); } - /// Update this router's RIB based on an update message from a peer. - fn update_rib(&self, update: &UpdateMessage, id: u32) { - let priority = if update.graceful_shutdown() { - 0 - } else { - DEFAULT_ROUTE_PRIORITY + fn handle_refresh( + &self, + msg: RouteRefreshMessage, + pc: &PeerConnection, + ) -> Result<(), Error> { + if msg.afi != Afi::Ipv4 as u16 { + return Ok(()); + } + let originated = match self.db.get_origin4() { + Ok(value) => value, + Err(e) => { + err!(self; "failed to get originated from db: {e}"); + // This is not a protocol level issue + return Ok(()); + } }; + if !originated.is_empty() { + let mut update = UpdateMessage { + path_attributes: self.router.base_attributes(), + ..Default::default() + }; + for p in originated { + update.nlri.push(p.into()); + } + read_lock!(self.fanout).send_all(&update); + self.send_update(update, &pc.conn, ShaperApplication::Current)?; + } + Ok(()) + } + /// Update this router's RIB based on an update message from a peer. + fn update_rib(&self, update: &UpdateMessage, id: u32, peer_as: u32) { for w in &update.withdrawn { - self.db.remove_peer_prefix4(id, w.into()); + self.db.remove_peer_prefix(id, w.as_prefix4().into()); } - let originated = match self.db.get_originated4() { + let originated = match self.db.get_origin4() { Ok(value) => value, Err(e) => { err!(self; "failed to get originated from db: {e}"); @@ -1434,19 +2053,37 @@ impl SessionRunner { }; for n in &update.nlri { - let prefix = n.into(); + let prefix = n.as_prefix4(); // ignore prefixes we originate if originated.contains(&prefix) { continue; } - let k = rdb::Route4ImportKey { - prefix, - nexthop, - id, - priority, + + let mut as_path = Vec::new(); + if let Some(segments_list) = update.as_path() { + for segments in &segments_list { + as_path.extend(segments.value.iter()); + } + } + + let path = rdb::Path { + nexthop: nexthop.into(), + shutdown: update.graceful_shutdown(), + local_pref: update.local_pref(), + bgp: Some(BgpPathProperties { + origin_as: peer_as, + med: update.multi_exit_discriminator(), + stale: None, + id, + as_path, + }), + vlan_id: lock!(self.session).vlan_id, }; - if let Err(e) = self.db.set_nexthop4(k, false) { - err!(self; "failed to set nexthop {k:#?}: {e}"); + + if let Err(e) = + self.db.add_prefix_path(prefix.into(), path.clone(), false) + { + err!(self; "failed to add path {:?} -> {:?}: {e}", prefix, path); } } } @@ -1456,7 +2093,25 @@ impl SessionRunner { /// Perform a set of checks on an update to see if we can accept it. fn check_update(&self, update: &UpdateMessage) -> Result<(), Error> { - self.check_for_self_in_path(update) + self.check_for_self_in_path(update)?; + self.check_v4_prefixes(update)?; + self.check_nexthop_self(update)?; + let info = lock!(self.session); + if info.enforce_first_as { + if let Some(peer_as) = info.remote_asn { + self.enforce_first_as(update, peer_as)?; + } + } + Ok(()) + } + + fn apply_static_update_policy(&self, update: &mut UpdateMessage) { + if self.is_ebgp() { + update.clear_local_pref() + } + if let Some(pref) = lock!(self.session).local_pref { + update.set_local_pref(pref); + } } /// Do not accept routes that have our ASN in the AS_PATH e.g., do @@ -1477,6 +2132,7 @@ impl SessionRunner { }; for segment in path { if segment.value.contains(&asn) { + wrn!(self; "self in AS path: {:?}", update); return Err(Error::SelfLoopDetected); } } @@ -1484,6 +2140,61 @@ impl SessionRunner { Ok(()) } + //TODO similar check needed for v6 once we get full v6 support + fn check_v4_prefixes(&self, update: &UpdateMessage) -> Result<(), Error> { + for prefix in &update.nlri { + if prefix.length == 0 { + continue; + } + let first = prefix.value[0]; + // check 127.0.0.0/8, 224.0.0.0/4 + if (first == 127) || (first & 0xf0 == 224) { + return Err(Error::InvalidNlriPrefix(prefix.as_prefix4())); + } + } + Ok(()) + } + + fn check_nexthop_self(&self, update: &UpdateMessage) -> Result<(), Error> { + // nothing to check when no prefixes presnt, and nexthop not required + // for pure withdraw + if update.nlri.is_empty() { + return Ok(()); + } + let nexthop = match update.nexthop4() { + Some(nh) => nh, + None => return Err(Error::MissingNexthop), + }; + for prefix in &update.nlri { + let prefix = prefix.as_prefix4(); + if prefix.length == 32 && prefix.value == nexthop { + return Err(Error::NexthopSelf(prefix.value.into())); + } + } + Ok(()) + } + + fn enforce_first_as( + &self, + update: &UpdateMessage, + peer_as: u32, + ) -> Result<(), Error> { + let path = match update.as_path() { + Some(path) => path, + None => return Err(Error::MissingAsPath), + }; + let path: Vec = path.into_iter().flat_map(|x| x.value).collect(); + if path.is_empty() { + return Err(Error::EmptyAsPath); + } + + if path[0] != peer_as { + return Err(Error::EnforceAsFirst(peer_as, path)); + } + + Ok(()) + } + // NOTE: for now we are only acting as an edge router. This means we // do not redistribute announcements. So for now this function // is unused. However, this may change in the future. @@ -1508,4 +2219,160 @@ impl SessionRunner { pub fn current_state_duration(&self) -> Duration { lock!(self.last_state_change).elapsed() } + + pub fn ensure_connection_policy(&self, conn: &Cnx) -> anyhow::Result<()> { + if let Some(md5_key) = lock!(self.session).md5_auth_key.clone() { + let mut key = [0u8; MAX_MD5SIG_KEYLEN]; + let len = md5_key.len(); + if len > MAX_MD5SIG_KEYLEN { + return Err(anyhow::anyhow!( + "md5 key too long, max size is {}", + MAX_MD5SIG_KEYLEN + )); + } + key[..len].copy_from_slice(md5_key.as_bytes()); + if let Err(e) = conn.set_md5_sig(len as u16, key) { + return Err(anyhow::anyhow!("failed to set md5 key: {e}")); + } + } + if let Some(ttl) = lock!(self.session).min_ttl { + if let Err(e) = conn.set_min_ttl(ttl) { + return Err(anyhow::anyhow!("failed to set min ttl: {e}")); + } + } + + Ok(()) + } + + pub fn update_session_parameters( + &self, + cfg: PeerConfig, + info: SessionInfo, + ) -> Result<(), Error> { + let mut reset_needed = self.update_session_config(cfg)?; + reset_needed |= self.update_session_info(info)?; + + if reset_needed { + self.event_tx + .send(FsmEvent::Reset) + .map_err(|e| Error::EventSend(e.to_string()))?; + } + + Ok(()) + } + + pub fn update_session_config( + &self, + cfg: PeerConfig, + ) -> Result { + *lock!(self.neighbor.name) = cfg.name; + let mut reset_needed = false; + + if self.neighbor.host != cfg.host { + return Err(Error::PeerAddressUpdate); + } + + if cfg.keepalive >= cfg.hold_time { + return Err(Error::KeepaliveLargerThanHoldTime); + } + + let mut hold_time = lock!(self.clock.timers.hold_configured_interval); + if hold_time.as_secs() != cfg.hold_time { + *hold_time = Duration::from_secs(cfg.hold_time); + reset_needed = true; + } + + let mut keepalive = + lock!(self.clock.timers.keepalive_configured_interval); + if keepalive.as_secs() != cfg.keepalive { + *keepalive = Duration::from_secs(cfg.keepalive); + reset_needed = true; + } + + lock!(self.clock.timers.idle_hold_timer).interval = + Duration::from_secs(cfg.idle_hold_time); + + lock!(self.clock.timers.delay_open_timer).interval = + Duration::from_secs(cfg.delay_open); + + lock!(self.clock.timers.connect_retry_timer).interval = + Duration::from_secs(cfg.connect_retry); + + Ok(reset_needed) + } + + pub fn update_session_info( + &self, + info: SessionInfo, + ) -> Result { + let mut reset_needed = false; + let mut path_attributes_changed = false; + let mut refresh_needed = false; + let mut current = lock!(self.session); + + current.connect_retry_counter = info.connect_retry_counter; + current.passive_tcp_establishment = info.passive_tcp_establishment; + + if current.remote_asn != info.remote_asn { + current.remote_asn = info.remote_asn; + reset_needed = true; + } + + if current.min_ttl != info.min_ttl { + current.min_ttl = info.min_ttl; + reset_needed = true; + } + + if current.md5_auth_key != info.md5_auth_key { + current.md5_auth_key = info.md5_auth_key; + reset_needed = true; + } + + if current.multi_exit_discriminator != info.multi_exit_discriminator { + current.multi_exit_discriminator = info.multi_exit_discriminator; + path_attributes_changed = true; + } + + if current.communities != info.communities { + current.communities = info.communities.clone(); + path_attributes_changed = true; + } + + if current.local_pref != info.local_pref { + current.local_pref = info.local_pref; + refresh_needed = true; + } + + if current.enforce_first_as != info.enforce_first_as { + current.enforce_first_as = info.enforce_first_as; + reset_needed = true; + } + + if current.allow_import != info.allow_import { + current.allow_import = info.allow_import; + refresh_needed = true; + } + + if current.allow_export != info.allow_export { + let previous = current.allow_export.clone(); + current.allow_export = info.allow_export; + self.event_tx + .send(FsmEvent::ExportPolicyChanged(previous)) + .map_err(|e| Error::EventSend(e.to_string()))?; + } + + if path_attributes_changed { + self.event_tx + .send(FsmEvent::PathAttributesChanged) + .map_err(|e| Error::EventSend(e.to_string()))?; + } + + if refresh_needed { + self.event_tx + .send(FsmEvent::RouteRefreshNeeded) + .map_err(|e| Error::EventSend(e.to_string()))?; + } + + Ok(reset_needed) + } } diff --git a/bgp/src/test.rs b/bgp/src/test.rs index 966a50d0..3426b38b 100644 --- a/bgp/src/test.rs +++ b/bgp/src/test.rs @@ -5,7 +5,7 @@ use crate::config::{PeerConfig, RouterConfig}; use crate::connection_channel::{BgpConnectionChannel, BgpListenerChannel}; use crate::session::{FsmStateKind, SessionInfo}; -use rdb::{Asn, Prefix4}; +use rdb::{Asn, Prefix}; use std::collections::BTreeMap; use std::sync::mpsc::channel; use std::sync::{Arc, Mutex}; @@ -102,7 +102,8 @@ fn test_basic_update() { let (r1, d1, r2, _d2) = two_router_test_setup("basic_update", None, None); // originate a prefix - r1.originate4(vec![ip!("1.2.3.0/24")]).expect("originate"); + r1.create_origin4(vec![ip!("1.2.3.0/24")]) + .expect("originate"); // once we reach established the originated routes should have propagated let r1_session = r1.get_session(ip!("2.0.0.1")).expect("get session one"); @@ -110,9 +111,9 @@ fn test_basic_update() { wait_for_eq!(r1_session.state(), FsmStateKind::Established); wait_for_eq!(r2_session.state(), FsmStateKind::Established); - let prefix: Prefix4 = cidr!("1.2.3.0/24"); + let prefix = Prefix::V4(cidr!("1.2.3.0/24")); - wait_for_eq!(r2.db.get_nexthop4(&prefix).is_empty(), false); + wait_for_eq!(r2.db.get_prefix_paths(&prefix).is_empty(), false); // shut down r1 and ensure that the prefixes are withdrawn from r2 on // session timeout. @@ -120,7 +121,7 @@ fn test_basic_update() { d1.shutdown(); wait_for_eq!(r2_session.state(), FsmStateKind::Connect); wait_for_eq!(r1_session.state(), FsmStateKind::Idle); - wait_for_eq!(r2.db.get_nexthop4(&prefix).is_empty(), true); + wait_for_eq!(r2.db.get_prefix_paths(&prefix).is_empty(), true); } fn two_router_test_setup( diff --git a/clab/.gitignore b/clab/.gitignore new file mode 100644 index 00000000..f78c61a0 --- /dev/null +++ b/clab/.gitignore @@ -0,0 +1,2 @@ +clab-pop +*.bak diff --git a/clab/cdn-set-config.sh b/clab/cdn-set-config.sh new file mode 100755 index 00000000..fcec2de2 --- /dev/null +++ b/clab/cdn-set-config.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +curl -s 'http://admin:NokiaSrl1!@clab-pop-cdn/jsonrpc' -d @cdn.json +echo; diff --git a/clab/cdn.json b/clab/cdn.json new file mode 100644 index 00000000..83a4b77e --- /dev/null +++ b/clab/cdn.json @@ -0,0 +1,152 @@ +{ + "jsonrpc": "2.0", + "id": 0, + "method": "set", + "params": { + "commands": [ + { + "action": "update", + "path": "/", + "value": + { + "interface": [ + { + "name": "ethernet-1/1", + "subinterface": [ + { + "index": 0, + "admin-state": "enable", + "ipv4": { + "admin-state": "enable", + "address": [ + { + "ip-prefix": "169.254.20.1/30", + "primary": [null] + } + ] + } + } + ] + }, + { + "name": "ethernet-1/2", + "subinterface": [ + { + "index": 0, + "type": "routed", + "admin-state": "enable", + "ipv4": { + "admin-state": "enable", + "address": [ + { + "ip-prefix": "2.3.4.5/24" + } + ] + } + } + ] + } + ], + "network-instance": [ + { + "name": "default", + "interface": [ + { + "name": "ethernet-1/1.0" + }, + { + "name": "ethernet-1/2.0" + } + ], + "protocols": { + "bgp": { + "admin-state": "enable", + "autonomous-system": 64501, + "export-policy": "all", + "router-id": "2.3.4.5", + "afi-safi": [ + { + "afi-safi-name": "ipv4-unicast", + "admin-state": "enable" + } + ], + "group": [ + { + "group-name": "oxpop", + "admin-state": "enable", + "afi-safi": [ + { + "afi-safi-name": "ipv4-unicast", + "admin-state": "enable" + } + ], + "trace-options": { + "flag": [ + { + "name": "events" + } + ] + } + } + ], + "neighbor": [ + { + "peer-address": "169.254.20.2", + "description": "oxide point of presence", + "peer-as": 65547, + "peer-group": "oxpop", + "authentication": { + "password": "$aes1$ATTuNB0NU2L7AW8=$HpBACI63gldrmF9SBkiuPQ==" + }, + "multihop": { + "admin-state": "enable", + "maximum-hops": 255 + }, + "local-as": { + "as-number": 64501 + } + } + ] + } + }, + "static-routes": { + "admin-state": "enable", + "route": [ + { + "prefix": "0.0.0.0/0", + "admin-state": "enable", + "next-hop-group": "upstream" + } + ] + }, + "next-hop-groups": { + "group": [ + { + "name": "upstream", + "admin-state": "enable", + "nexthop": [ + { + "index": 1, + "ip-address": "2.3.4.1" + } + ] + } + ] + } + } + ], + "routing-policy": { + "policy": [ + { + "name": "all", + "default-action": { + "policy-result": "accept" + } + } + ] + } + } + } + ] + } +} diff --git a/clab/diagram.svg b/clab/diagram.svg new file mode 100644 index 00000000..8390b8b7 --- /dev/null +++ b/clab/diagram.svg @@ -0,0 +1,3 @@ + + +
0.0.0.0/0
198.51.100.0/24
0.0.0.0/0
Transit
AS64500
CDN
AS64501
Internet
169.254.10.1/30
169.254.10.2/30
169.254.20.1/30
10.128.0.0/24
10.128.1.0/24
169.254.20.2/30
169.254.30.2/30
OxPop
65547
169.254.30.1/30
192.168.12.0/24
198.51.100.0/24
169.254.40.2/30
169.254.40.1/30
Public
Cloud West
64502
Public
Cloud East
64502
\ No newline at end of file diff --git a/clab/get-bgp.sh b/clab/get-bgp.sh new file mode 100755 index 00000000..428588d2 --- /dev/null +++ b/clab/get-bgp.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +curl -s 'http://admin:NokiaSrl1!@clab-pop-transit/jsonrpc' -d @- < anyhow::Result> { let addr = local_underlay_address()?; let sa = SocketAddr::new(addr, port); - let dropshot = ConfigDropshot { - bind_address: sa, - request_body_max_bytes: 1024 * 1024 * 1024, - default_handler_task_mode: HandlerTaskMode::Detached, - }; let log_config = LogConfig::Config(ConfigLogging::StderrTerminal { level: ConfigLoggingLevel::Debug, }); @@ -315,7 +308,8 @@ pub fn start_server( id: registry.producer_id(), kind: ProducerKind::Service, address: sa, - base_route: "/collect".to_string(), + // NOTE: This is now unused and will be removed in the future. + base_route: String::new(), interval: Duration::from_secs(1), }; @@ -323,9 +317,9 @@ pub fn start_server( let nexus_addr = resolve_nexus(log.clone(), &dns_servers).await; let config = oximeter_producer::Config { server_info: producer_info, - registration_address: nexus_addr, + registration_address: Some(nexus_addr), log: log_config, - dropshot, + request_body_max_bytes: 1024 * 1024 * 1024, }; run_oximeter(registry.clone(), config.clone(), log.clone()).await })) diff --git a/ddm/src/sys.rs b/ddm/src/sys.rs index 1f932557..da4a4fc2 100644 --- a/ddm/src/sys.rs +++ b/ddm/src/sys.rs @@ -12,15 +12,19 @@ use dpd_client::types; use dpd_client::Client; use dpd_client::ClientState; use libnet::{IpPrefix, Ipv4Prefix, Ipv6Prefix}; -use opte_ioctl::OpteHdl; -use oxide_vpc::api::TunnelEndpoint; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use slog::Logger; -use std::collections::{HashMap, HashSet}; +use std::collections::HashSet; use std::net::IpAddr; use std::sync::Arc; +#[cfg(target_os = "illumos")] +use ::{ + opte_ioctl::OpteHdl, oxide_vpc::api::TunnelEndpoint, + std::collections::HashMap, +}; + const DDM_DPD_TAG: &str = "ddmd"; #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)] @@ -216,6 +220,7 @@ pub fn add_routes_dendrite( port_id, link_id, tgt_ip: gw, + vlan_id: None, }; let route_set = types::RouteSet { cidr: cidr.into(), @@ -233,6 +238,7 @@ pub fn add_routes_dendrite( } } +#[cfg(target_os = "illumos")] fn tunnel_route_update_map( routes: &HashSet, ) -> HashMap> { @@ -256,16 +262,26 @@ fn tunnel_route_update_map( m } +#[cfg(not(target_os = "illumos"))] +pub fn add_tunnel_routes( + _log: &Logger, + _ifname: &str, + _routes: &HashSet, +) -> Result<(), String> { + todo!(); +} + +#[cfg(target_os = "illumos")] pub fn add_tunnel_routes( log: &Logger, ifname: &str, routes: &HashSet, -) -> Result<(), opte_ioctl::Error> { +) -> Result<(), String> { use oxide_vpc::api::{ IpCidr, Ipv4Cidr, Ipv4PrefixLen, Ipv6Cidr, Ipv6PrefixLen, SetVirt2BoundaryReq, }; - let hdl = OpteHdl::open(OpteHdl::XDE_CTL)?; + let hdl = OpteHdl::open(OpteHdl::XDE_CTL).map_err(|e| e.to_string())?; for (pfx, tep) in tunnel_route_update_map(routes) { for t in &tep { @@ -297,16 +313,26 @@ pub fn add_tunnel_routes( Ok(()) } +#[cfg(not(target_os = "illumos"))] +pub fn remove_tunnel_routes( + _log: &Logger, + _ifname: &str, + _routes: &HashSet, +) -> Result<(), String> { + todo!() +} + +#[cfg(target_os = "illumos")] pub fn remove_tunnel_routes( log: &Logger, ifname: &str, routes: &HashSet, -) -> Result<(), opte_ioctl::Error> { +) -> Result<(), String> { use oxide_vpc::api::{ ClearVirt2BoundaryReq, IpCidr, Ipv4Cidr, Ipv4PrefixLen, Ipv6Cidr, Ipv6PrefixLen, }; - let hdl = OpteHdl::open(OpteHdl::XDE_CTL)?; + let hdl = OpteHdl::open(OpteHdl::XDE_CTL).map_err(|e| e.to_string())?; for (pfx, tep) in tunnel_route_update_map(routes) { for t in &tep { inf!( diff --git a/interop-lab/add-images.sh b/interop-lab/add-images.sh new file mode 100755 index 00000000..4e5b17c4 --- /dev/null +++ b/interop-lab/add-images.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +docker import cEOS64-lab-4.28.3M.tar ceos:4.28.3M +docker load -i junos-routing-crpd-docker-amd64-23.2R1.13.tar diff --git a/interop-lab/create-containers.sh b/interop-lab/create-containers.sh new file mode 100755 index 00000000..c2ea637d --- /dev/null +++ b/interop-lab/create-containers.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +# create public cloud container +docker create \ + --privileged \ + --name=pubcloud \ + -h pubcloud \ + -e INTFTYPE=eth \ + -e ETBA=1 \ + -e SKIP_ZEROTOUCH_BARRIER_IN_SYSDBINIT=1 \ + -e CEOS=1 \ + -e EOS_PLATFORM=ceoslab \ + -e container=docker \ + -i \ + -t ceos:4.28.3M \ + /sbin/init \ + systemd.setenv=INTFTYPE=eth \ + systemd.setenv=ETBA=1 \ + systemd.setenv=SKIP_ZEROTOUCH_BARRIER_IN_SYSDBINIT=1 \ + systemd.setenv=CEOS=1 \ + systemd.setenv=EOS_PLATFORM=ceoslab \ + systemd.setenv=container=docker + +# create cdn container +docker create \ + --name cdn \ + -h cdn \ + -t crpd:23.2R1.13 + +# create oxpop container +docker create \ + --name oxpop \ + -h oxpop \ + --entrypoint=/bin/bash \ + -v /opt/oxide:/opt/oxide \ + -t ubuntu:22.04 diff --git a/interop-lab/create-network.sh b/interop-lab/create-network.sh new file mode 100755 index 00000000..7dd19c9a --- /dev/null +++ b/interop-lab/create-network.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +#docker network create --driver bridge oxpop_isp +docker network create --driver bridge oxpop_cdn +docker network create --driver bridge oxpop_pubcloud + +docker network connect oxpop_cdn oxpop +docker network connect oxpop_cdn cdn + +docker network connect oxpop_pubcloud oxpop +docker network connect oxpop_pubcloud pubcloud diff --git a/interop-lab/destroy-network.sh b/interop-lab/destroy-network.sh new file mode 100755 index 00000000..0d9c4f1d --- /dev/null +++ b/interop-lab/destroy-network.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +#docker network rm oxpop_isp +docker network rm oxpop_cdn +docker network rm oxpop_pubcloud diff --git a/interop-lab/install-docker.sh b/interop-lab/install-docker.sh new file mode 100755 index 00000000..3f9b598c --- /dev/null +++ b/interop-lab/install-docker.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +# Add Docker's official GPG key: +sudo apt-get update +sudo apt-get install ca-certificates curl +sudo install -m 0755 -d /etc/apt/keyrings +sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc +sudo chmod a+r /etc/apt/keyrings/docker.asc + +# Add the repository to Apt sources: +echo \ + "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \ + $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \ + sudo tee /etc/apt/sources.list.d/docker.list > /dev/null +sudo apt-get update diff --git a/interop-lab/remove.sh b/interop-lab/remove.sh new file mode 100755 index 00000000..02edce4f --- /dev/null +++ b/interop-lab/remove.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +#docker rm isp +docker rm cdn +docker rm pubcloud +docker rm oxpop diff --git a/interop-lab/run-containers.sh b/interop-lab/run-containers.sh new file mode 100755 index 00000000..b190c056 --- /dev/null +++ b/interop-lab/run-containers.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +#docker start isp +docker start pubcloud +docker start cdn +docker start oxpop diff --git a/interop-lab/stop.sh b/interop-lab/stop.sh new file mode 100755 index 00000000..fd9d40b8 --- /dev/null +++ b/interop-lab/stop.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +#docker stop isp -s kill +docker stop pubcloud -s kill +docker stop cdn -s kill +docker stop oxpop -s kill diff --git a/mg-admin-client/build.rs b/mg-admin-client/build.rs new file mode 100644 index 00000000..df355f75 --- /dev/null +++ b/mg-admin-client/build.rs @@ -0,0 +1,3 @@ +fn main() { + println!("cargo:rerun-if-changed=../openapi/mg-admin.json"); +} diff --git a/mg-admin-client/src/lib.rs b/mg-admin-client/src/lib.rs index 82594816..2be86fba 100644 --- a/mg-admin-client/src/lib.rs +++ b/mg-admin-client/src/lib.rs @@ -2,12 +2,6 @@ // 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/. -/* -pub use bgp::messages::Message; -pub use bgp::session::{MessageHistory, MessageHistoryEntry}; -pub use rdb::{PolicyAction, Prefix4}; -*/ - progenitor::generate_api!( spec = "../openapi/mg-admin.json", inner_type = slog::Logger, @@ -40,3 +34,21 @@ impl std::cmp::PartialEq for types::Prefix4 { impl std::cmp::Eq for types::Prefix4 {} impl Copy for types::Prefix4 {} + +impl std::str::FromStr for types::Prefix4 { + type Err = String; + + fn from_str(s: &str) -> Result { + let (value, length) = + s.split_once('/').ok_or("malformed route key".to_string())?; + + Ok(Self { + value: value + .parse() + .map_err(|_| "malformed ip addr".to_string())?, + length: length + .parse() + .map_err(|_| "malformed length".to_string())?, + }) + } +} diff --git a/mg-common/src/nexus.rs b/mg-common/src/nexus.rs index ac0e0317..e1f4950e 100644 --- a/mg-common/src/nexus.rs +++ b/mg-common/src/nexus.rs @@ -58,26 +58,10 @@ pub async fn run_oximeter( log: Logger, ) { let op = || async { - match oximeter_producer::Server::with_registry( - registry.clone(), - &config, - ) - .await - { - Ok(s) => Ok(s), - Err(e) => { - if let oximeter_producer::Error::RegistrationError { - retryable, - msg: _, - } = &e - { - if !retryable { - return Err(backoff::Error::Permanent(e)); - } - } - Err(e.into()) - } - } + oximeter_producer::Server::with_registry(registry.clone(), &config) + .map_err(|e| { + omicron_common::backoff::BackoffError::transient(e.to_string()) + }) }; let log_failure = |e, delay| { diff --git a/mg-lower/src/ddm.rs b/mg-lower/src/ddm.rs index 17e34b67..54c87b02 100644 --- a/mg-lower/src/ddm.rs +++ b/mg-lower/src/ddm.rs @@ -4,16 +4,20 @@ use ddm_admin_client::types::{Ipv6Prefix, TunnelOrigin}; use ddm_admin_client::Client; -use rdb::Route4ImportKey; +use dpd_client::Cidr; +use rdb::db::Rib; +use rdb::{Prefix, Prefix4, Prefix6, DEFAULT_ROUTE_PRIORITY}; use slog::{error, info, Logger}; use std::{collections::HashSet, net::Ipv6Addr, sync::Arc}; +use crate::dendrite::RouteHash; + const BOUNDARY_SERVICES_VNI: u32 = 99; pub(crate) fn update_tunnel_endpoints( tep: Ipv6Addr, // tunnel endpoint address client: &Client, - routes: &[Route4ImportKey], + routes: &Rib, rt: Arc, log: &Logger, ) { @@ -30,8 +34,10 @@ pub(crate) fn update_tunnel_endpoints( .into_iter() .collect(); - let target: HashSet = - routes.iter().map(|x| route_to_tunnel(tep, x)).collect(); + let target: HashSet = routes + .iter() + .map(|(prefix, _path)| route_to_tunnel(tep, prefix)) + .collect(); let to_add = target.difference(¤t); let to_remove = current.difference(&target); @@ -72,29 +78,62 @@ fn ensure_tep_underlay_origin( }; } -fn route_to_tunnel(tep: Ipv6Addr, x: &Route4ImportKey) -> TunnelOrigin { - TunnelOrigin { - overlay_prefix: ddm_admin_client::types::IpPrefix::V4( - ddm_admin_client::types::Ipv4Prefix { - addr: x.prefix.value, - len: x.prefix.length, - }, - ), - boundary_addr: tep, - vni: BOUNDARY_SERVICES_VNI, //TODO? - metric: x.priority, +fn route_to_tunnel(tep: Ipv6Addr, prefix: &Prefix) -> TunnelOrigin { + match prefix { + Prefix::V4(p) => { + TunnelOrigin { + overlay_prefix: ddm_admin_client::types::IpPrefix::V4( + ddm_admin_client::types::Ipv4Prefix { + addr: p.value, + len: p.length, + }, + ), + boundary_addr: tep, + vni: BOUNDARY_SERVICES_VNI, //TODO? + metric: DEFAULT_ROUTE_PRIORITY, //TODO + } + } + Prefix::V6(p) => { + TunnelOrigin { + overlay_prefix: ddm_admin_client::types::IpPrefix::V6( + ddm_admin_client::types::Ipv6Prefix { + addr: p.value, + len: p.length, + }, + ), + boundary_addr: tep, + vni: BOUNDARY_SERVICES_VNI, //TODO? + metric: DEFAULT_ROUTE_PRIORITY, //TODO + } + } } } pub(crate) fn add_tunnel_routes( tep: Ipv6Addr, // tunnel endpoint address client: &Client, - routes: &[Route4ImportKey], + routes: &HashSet, rt: Arc, log: &Logger, ) { - let teps: Vec = - routes.iter().map(|x| route_to_tunnel(tep, x)).collect(); + let teps: Vec = routes + .iter() + .map(|rt| { + let pfx = match rt.cidr { + Cidr::V4(p) => Prefix4 { + value: p.prefix, + length: p.prefix_len, + } + .into(), + Cidr::V6(p) => Prefix6 { + value: p.prefix, + length: p.prefix_len, + } + .into(), + }; + route_to_tunnel(tep, &pfx) + }) + .collect(); add_tunnel_endpoints(tep, client, teps.iter(), &rt, log) } @@ -120,12 +159,28 @@ pub(crate) fn add_tunnel_endpoints<'a, I: Iterator>( pub(crate) fn remove_tunnel_routes( tep: Ipv6Addr, // tunnel endpoint address client: &Client, - routes: &[Route4ImportKey], + routes: &HashSet, rt: Arc, log: &Logger, ) { - let teps: Vec = - routes.iter().map(|x| route_to_tunnel(tep, x)).collect(); + let teps: Vec = routes + .iter() + .map(|rt| { + let pfx = match rt.cidr { + Cidr::V4(p) => Prefix4 { + value: p.prefix, + length: p.prefix_len, + } + .into(), + Cidr::V6(p) => Prefix6 { + value: p.prefix, + length: p.prefix_len, + } + .into(), + }; + route_to_tunnel(tep, &pfx) + }) + .collect(); remove_tunnel_endpoints(client, teps.iter(), &rt, log) } diff --git a/mg-lower/src/dendrite.rs b/mg-lower/src/dendrite.rs index 90b41f7e..48a62a6e 100644 --- a/mg-lower/src/dendrite.rs +++ b/mg-lower/src/dendrite.rs @@ -3,34 +3,38 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. use crate::Error; -use crate::Stats; use crate::MG_LOWER_TAG; use dendrite_common::network::Cidr; use dendrite_common::ports::PortId; use dendrite_common::ports::QsfpPort; use dpd_client::types; +use dpd_client::types::LinkId; use dpd_client::types::LinkState; use dpd_client::Client as DpdClient; +use dpd_client::Ipv4Cidr; +use dpd_client::Ipv6Cidr; use http::StatusCode; +use libnet::Ipv6Prefix; use libnet::{get_route, IpPrefix, Ipv4Prefix}; -use rdb::Route4ImportKey; +use rdb::Path; +use rdb::Prefix; use slog::{error, warn, Logger}; +use std::collections::BTreeSet; use std::collections::HashSet; use std::hash::Hash; -use std::net::IpAddr; -use std::net::Ipv6Addr; -use std::sync::atomic::Ordering; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; use std::sync::Arc; use std::time::Duration; const TFPORT_QSFP_DEVICE_PREFIX: &str = "tfportqsfp"; -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub(crate) struct RouteHash { - cidr: Cidr, - port_id: PortId, - link_id: types::LinkId, - nexthop: IpAddr, + pub(crate) cidr: Cidr, + pub(crate) port_id: PortId, + pub(crate) link_id: types::LinkId, + pub(crate) nexthop: IpAddr, + pub(crate) vlan_id: Option, } impl RouteHash { @@ -39,6 +43,7 @@ impl RouteHash { port_id: PortId, link_id: types::LinkId, nexthop: IpAddr, + vlan_id: Option, ) -> Result { match (cidr, nexthop) { (Cidr::V4(_), IpAddr::V4(_)) | (Cidr::V6(_), IpAddr::V6(_)) => { @@ -47,11 +52,39 @@ impl RouteHash { port_id, link_id, nexthop, + vlan_id, }) } _ => Err("mismatched subnet and target"), } } + + pub fn for_prefix_path( + prefix: Prefix, + path: Path, + ) -> Result { + let (port_id, link_id) = get_port_and_link(path.nexthop)?; + let rh = RouteHash { + cidr: match prefix { + Prefix::V4(p) => Ipv4Cidr { + prefix: p.value, + prefix_len: p.length, + } + .into(), + Prefix::V6(p) => Ipv6Cidr { + prefix: p.value, + prefix_len: p.length, + } + .into(), + }, + port_id, + link_id, + nexthop: path.nexthop, + vlan_id: path.vlan_id, + }; + + Ok(rh) + } } pub(crate) fn ensure_tep_addr( @@ -73,6 +106,52 @@ pub(crate) fn ensure_tep_addr( } } +pub(crate) fn link_is_up( + dpd: &DpdClient, + port_id: PortId, + link_id: LinkId, + rt: &Arc, +) -> Result { + let link_info = + rt.block_on(async { dpd.link_get(&port_id, &link_id).await })?; + + Ok(link_info.link_state == LinkState::Up) +} + +fn get_local_addrs( + dpd: &DpdClient, + rt: &Arc, +) -> Result<(BTreeSet, BTreeSet), Error> { + let links = rt + .block_on(async { dpd.link_list_all().await })? + .into_inner(); + + let mut v4 = BTreeSet::new(); + let mut v6 = BTreeSet::new(); + + for link in links { + let addrs = rt + .block_on(async { + dpd.link_ipv4_list(&link.port_id, &link.link_id, None, None) + .await + })? + .into_inner() + .items; + v4.extend(addrs.into_iter().map(|x| x.addr)); + + let addrs = rt + .block_on(async { + dpd.link_ipv6_list(&link.port_id, &link.link_id, None, None) + .await + })? + .into_inner() + .items; + v6.extend(addrs.into_iter().map(|x| x.addr)); + } + + Ok((v4, v6)) +} + /// Perform a set of route additions and deletions via the Dendrite API. pub(crate) fn update_dendrite<'a, I>( to_add: I, @@ -84,27 +163,70 @@ pub(crate) fn update_dendrite<'a, I>( where I: Iterator, { + let (local_v4_addrs, local_v6_addrs) = get_local_addrs(dpd, &rt)?; + for r in to_add { let cidr = r.cidr; let tag = dpd.inner().tag.clone(); let port_id = r.port_id; let link_id = r.link_id; + let vlan_id = r.vlan_id; let target = match (r.cidr, r.nexthop) { - (Cidr::V4(_), IpAddr::V4(tgt_ip)) => types::Ipv4Route { - tag, - port_id, - link_id, - tgt_ip, + (Cidr::V4(c), IpAddr::V4(tgt_ip)) => { + if c.prefix_len == 32 && local_v4_addrs.contains(&c.prefix) { + warn!( + log, + "martian detected: prefix={c:?}, \ + skipping data plane installation" + ); + continue; + } + if local_v4_addrs.contains(&tgt_ip) { + warn!( + log, + "martian detected: nexthop={tgt_ip:?}, \ + skipping data plane installation" + ); + continue; + } + + types::Ipv4Route { + tag, + port_id, + link_id, + tgt_ip, + vlan_id, + } + .into() } - .into(), - (Cidr::V6(_), IpAddr::V6(tgt_ip)) => types::Ipv6Route { - tag, - port_id, - link_id, - tgt_ip, + (Cidr::V6(c), IpAddr::V6(tgt_ip)) => { + if c.prefix_len == 128 && local_v6_addrs.contains(&c.prefix) { + warn!( + log, + "martian detected: prefix={c:?}, \ + skipping data plane installation" + ); + continue; + } + if local_v6_addrs.contains(&tgt_ip) { + warn!( + log, + "martian detected: nexthop={tgt_ip:?}, \ + skipping data plane installation" + ); + continue; + } + + types::Ipv6Route { + tag, + port_id, + link_id, + tgt_ip, + vlan_id, + } + .into() } - .into(), _ => { error!( log, @@ -144,28 +266,21 @@ where } fn get_port_and_link( - r: &Route4ImportKey, -) -> Result<(PortId, types::LinkId), String> { - let sys_route = match get_route( - IpPrefix::V4(Ipv4Prefix { - addr: r.nexthop, - mask: 32, - }), - Some(Duration::from_secs(1)), - ) { - Ok(r) => r, - Err(e) => { - return Err(format!("Unable to get route for {r:?}: {e:?}")); - } + nexthop: IpAddr, +) -> Result<(PortId, types::LinkId), Error> { + let prefix = match nexthop { + IpAddr::V4(addr) => IpPrefix::V4(Ipv4Prefix { addr, mask: 32 }), + IpAddr::V6(addr) => IpPrefix::V6(Ipv6Prefix { addr, mask: 128 }), }; + let sys_route = get_route(prefix, Some(Duration::from_secs(1)))?; let ifname = match sys_route.ifx { Some(name) => name, None => { - return Err(format!( + return Err(Error::NoNexthopRoute(format!( "No interface associated with route for {:?}: {:?}", - r, sys_route, - )); + prefix, sys_route, + ))); } }; @@ -177,15 +292,15 @@ fn get_port_and_link( .map(|x| x.trim()) .and_then(|x| x.parse::().ok()) else { - return Err(format!( + return Err(Error::Tfport(format!( "expected {}$M_0, got {}", TFPORT_QSFP_DEVICE_PREFIX, ifname - )); + ))); }; let port_id = match QsfpPort::try_from(egress_port_num) { Ok(qsfp) => PortId::Qsfp(qsfp), - Err(e) => return Err(format!("bad port name: {e}")), + Err(e) => return Err(Error::Tfport(format!("bad port name: {e}"))), }; // TODO breakout considerations @@ -193,73 +308,117 @@ fn get_port_and_link( Ok((port_id, link_id)) } -/// Translate a vector of RIB route data structures to a HashSet of RouteHashes -pub(crate) fn db_route_to_dendrite_route( - rs: Vec, - log: &Logger, +pub(crate) fn get_routes_for_prefix( dpd: &DpdClient, - stats: Option<&Stats>, - require_link_up: bool, + prefix: &Prefix, rt: Arc, -) -> HashSet { - let mut result = HashSet::new(); - - let mut link_down_count = 0; - - for r in &rs { - let (port_id, link_id) = match get_port_and_link(r) { - Ok((p, l)) => (p, l), - Err(e) => { - error!(log, "failed to get port for {r:?}: {e:?}"); - continue; - } - }; + log: Logger, +) -> Result, Error> { + let result = match prefix { + Prefix::V4(p) => { + let cidr = Ipv4Cidr { + prefix: p.value, + prefix_len: p.length, + }; + let dpd_routes = + match rt.block_on(async { dpd.route_ipv4_get(&cidr).await }) { + Ok(routes) => routes, + Err(e) => { + if e.status() == Some(StatusCode::NOT_FOUND) { + return Ok(HashSet::new()); + } + return Err(e.into()); + } + } + .into_inner(); - if require_link_up { - let link_info = match rt - .block_on(async { dpd.link_get(&port_id, &link_id).await }) - { - Ok(info) => info.into_inner(), - Err(e) => { - error!( - log, - "failed to get link info for {port_id:?}/{link_id:?}: {e}" - ); - link_down_count += 1; + let mut result: Vec = Vec::new(); + for r in dpd_routes.iter() { + if r.tag != MG_LOWER_TAG { continue; } - }; - - if link_info.link_state != LinkState::Up { - warn!( - log, - "{port_id:?}/{link_id:?} is not up, not installing into RIB" - ); - link_down_count += 1; - continue; + match link_is_up(dpd, r.port_id, r.link_id, &rt) { + Err(e) => { + error!( + log, + "nexthop: {} failed to get link state for {:?}/{:?}: {e}", + r.tgt_ip, + r.port_id, + r.link_id + ); + continue; + } + Ok(false) => { + warn!( + log, + "nexthop: {} link {:?}/{:?} is not up, \ + not installing route for {:?}", + r.tgt_ip, + r.port_id, + r.link_id, + prefix, + ); + continue; + } + Ok(true) => {} + } + match RouteHash::new( + cidr.into(), + r.port_id, + r.link_id, + r.tgt_ip.into(), + r.vlan_id, + ) { + Ok(rh) => result.push(rh), + Err(e) => { + error!( + log, + "route hash creation failed for {:?}: {e}", prefix + ); + continue; + } + }; } - } - let cidr = dpd_client::Ipv4Cidr { - prefix: r.prefix.value, - prefix_len: r.prefix.length, - }; - - match RouteHash::new(cidr.into(), port_id, link_id, r.nexthop.into()) { - Ok(route) => { - let _ = result.insert(route); - } - Err(e) => error!(log, "bad route: {e}"), - }; - } - - if let Some(stats) = stats { - stats - .routes_blocked_by_link_state - .store(link_down_count, Ordering::Relaxed); - } + dpd_routes + .into_iter() + .map(|r| { + RouteHash::new( + cidr.into(), + r.port_id, + r.link_id, + r.tgt_ip.into(), + r.vlan_id, + ) + .unwrap() + }) + .collect() + } + Prefix::V6(p) => { + let cidr = Ipv6Cidr { + prefix: p.value, + prefix_len: p.length, + }; + let dpd_routes = rt + .block_on(async { dpd.route_ipv6_get(&cidr).await })? + .into_inner(); - result + dpd_routes + .into_iter() + .map(|r| { + RouteHash::new( + cidr.into(), + r.port_id, + r.link_id, + r.tgt_ip.into(), + r.vlan_id, + ) + .unwrap() + }) + .collect() + } + }; + Ok(result) } /// Create a new Dendrite/dpd client. The lower half always runs on the same diff --git a/mg-lower/src/error.rs b/mg-lower/src/error.rs index 2b6c105c..12668390 100644 --- a/mg-lower/src/error.rs +++ b/mg-lower/src/error.rs @@ -6,4 +6,13 @@ pub enum Error { #[error("dpd error {0}")] Dpd(#[from] dpd_client::Error), + + #[error("tfport error {0}")] + Tfport(String), + + #[error("no nexthop route {0}")] + NoNexthopRoute(String), + + #[error("libnet error route {0}")] + LibnetRoute(#[from] libnet::route::Error), } diff --git a/mg-lower/src/lib.rs b/mg-lower/src/lib.rs index e4ee8704..6cb408ef 100644 --- a/mg-lower/src/lib.rs +++ b/mg-lower/src/lib.rs @@ -7,7 +7,7 @@ //! routing platform. The only platform currently supported is Dendrite. use crate::dendrite::{ - db_route_to_dendrite_route, new_dpd_client, update_dendrite, RouteHash, + get_routes_for_prefix, new_dpd_client, update_dendrite, RouteHash, }; use crate::error::Error; use ddm::{ @@ -18,12 +18,13 @@ use ddm_admin_client::Client as DdmClient; use dendrite::ensure_tep_addr; use dpd_client::Client as DpdClient; use mg_common::stats::MgLowerStats as Stats; -use rdb::{ChangeSet, Db}; +use rdb::db::Rib; +use rdb::{Db, Prefix, PrefixChangeNotification}; use slog::{error, info, Logger}; use std::collections::HashSet; use std::net::Ipv6Addr; use std::sync::mpsc::{channel, RecvTimeoutError}; -use std::sync::Arc; +use std::sync::{Arc, Mutex}; use std::thread::sleep; use std::time::Duration; @@ -58,44 +59,37 @@ pub fn run( // initialize the underlying router with the current state let dpd = new_dpd_client(&log); let ddm = new_ddm_client(&log); - let mut generation = - match full_sync(tep, &db, &log, &dpd, &ddm, &stats, rt.clone()) { - Ok(gen) => gen, - Err(e) => { - error!(log, "initializing failed: {e}"); - info!(log, "restarting sync loop in one second"); - sleep(Duration::from_secs(1)); - continue; - } - }; + if let Err(e) = + full_sync(tep, &db, &log, &dpd, &ddm, &stats, rt.clone()) + { + error!(log, "initializing failed: {e}"); + info!(log, "restarting sync loop in one second"); + sleep(Duration::from_secs(1)); + continue; + }; // handle any changes that occur loop { match rx.recv_timeout(Duration::from_secs(1)) { Ok(change) => { - generation = match handle_change( + if let Err(e) = handle_change( tep, &db, change, &log, &dpd, &ddm, - generation, - &stats, rt.clone(), ) { - Ok(gen) => gen, - Err(e) => { - error!(log, "handling change failed: {e}"); - info!(log, "restarting sync loop"); - continue; - } + error!(log, "handling change failed: {e}"); + info!(log, "restarting sync loop"); + continue; } } // if we've not received updates in the timeout interval, do a // full sync in case something has changed out from under us. Err(RecvTimeoutError::Timeout) => { - generation = match full_sync( + if let Err(e) = full_sync( tep, &db, &log, @@ -104,13 +98,10 @@ pub fn run( &stats, rt.clone(), ) { - Ok(gen) => gen, - Err(e) => { - error!(log, "initializing failed: {e}"); - info!(log, "restarting sync loop in one second"); - sleep(Duration::from_secs(1)); - continue; - } + error!(log, "initializing failed: {e}"); + info!(log, "restarting sync loop in one second"); + sleep(Duration::from_secs(1)); + continue; } } Err(RecvTimeoutError::Disconnected) => { @@ -130,103 +121,75 @@ fn full_sync( log: &Logger, dpd: &DpdClient, ddm: &DdmClient, - stats: &Arc, + _stats: &Arc, //TODO(ry) rt: Arc, -) -> Result { - let generation = db.generation(); - - let db_imported: Vec = db.effective_route_set(); +) -> Result<(), Error> { + let rib = db.full_rib(); + // Make sure our tunnel endpoint address is on the switch ASIC ensure_tep_addr(tep, dpd, rt.clone(), log); - // announce tunnel endpoints via ddm - update_tunnel_endpoints(tep, ddm, &db_imported, rt.clone(), log); - - // get all imported routes from db - let imported: HashSet = db_route_to_dendrite_route( - db_imported, - log, - dpd, - Some(stats), - true, - rt.clone(), - ); - - // get all routes created by mg-lower from dendrite - let routes = - rt.block_on(async { dpd.route_ipv4_list(None, None).await })?; - - let mut active: HashSet = HashSet::new(); - for route in &routes.items { - for target in &route.targets { - if let dpd_client::types::RouteTarget::V4(t) = target { - if t.tag == MG_LOWER_TAG { - if let Ok(rh) = RouteHash::new( - route.cidr, - t.port_id, - t.link_id, - t.tgt_ip.into(), - ) { - active.insert(rh); - } - } - } - } - } - - // determine what routes need to be added and deleted - let to_add = imported.difference(&active); - let to_del = active.difference(&imported); + // Announce tunnel endpoints via ddm + update_tunnel_endpoints(tep, ddm, &rib, rt.clone(), log); - update_dendrite(to_add, to_del, dpd, rt, log)?; + // Compute the bestpath for each prefix and synchronize the ASIC routing + // tables with the chosen paths. + for (prefix, _paths) in rib.iter() { + sync_prefix(tep, db.loc_rib(), prefix, dpd, ddm, log, &rt)?; + } - Ok(generation) + Ok(()) } /// Synchronize a change set from the RIB to the underlying platform. -#[allow(clippy::too_many_arguments)] fn handle_change( tep: Ipv6Addr, // tunnel endpoint address db: &Db, - change: ChangeSet, + notification: PrefixChangeNotification, log: &Logger, dpd: &DpdClient, ddm: &DdmClient, - generation: u64, - stats: &Arc, rt: Arc, -) -> Result { - info!( - log, - "mg-lower: handling rib change generation {} -> {}: {:#?}", - generation, - change.generation, - change, - ); - - if change.generation > generation + 1 { - return full_sync(tep, db, log, dpd, ddm, stats, rt.clone()); +) -> Result<(), Error> { + for prefix in notification.changed.iter() { + sync_prefix(tep, db.loc_rib(), prefix, dpd, ddm, log, &rt)?; + } + + Ok(()) +} + +fn sync_prefix( + tep: Ipv6Addr, + rib_loc: Arc>, + prefix: &Prefix, + dpd: &DpdClient, + ddm: &DdmClient, + log: &Logger, + rt: &Arc, +) -> Result<(), Error> { + // The current routes that are on the ASIC. + let current = get_routes_for_prefix(dpd, prefix, rt.clone(), log.clone())?; + + // The best routes in the RIB + let mut best: HashSet = HashSet::new(); + if let Some(paths) = rib_loc.lock().unwrap().get(prefix) { + for path in paths { + best.insert(RouteHash::for_prefix_path(*prefix, path.clone())?); + } } - let to_add: Vec = - change.import.added.clone().into_iter().collect(); - - add_tunnel_routes(tep, ddm, &to_add, rt.clone(), log); - let to_add = db_route_to_dendrite_route( - to_add, - log, - dpd, - Some(stats), - true, - rt.clone(), - ); - - let to_del: Vec = - change.import.removed.clone().into_iter().collect(); - remove_tunnel_routes(tep, ddm, &to_del, rt.clone(), log); - let to_del = - db_route_to_dendrite_route(to_del, log, dpd, None, false, rt.clone()); - - update_dendrite(to_add.iter(), to_del.iter(), dpd, rt.clone(), log)?; - - Ok(change.generation) + + // Routes that are in the best set but not on the asic should be added. + let add: HashSet = best.difference(¤t).copied().collect(); + + // Routes that are on the asic but not in the best set should be removed. + let del: HashSet = current.difference(&best).copied().collect(); + + // Update DDM tunnel routing + add_tunnel_routes(tep, ddm, &add, rt.clone(), log); + remove_tunnel_routes(tep, ddm, &del, rt.clone(), log); + + // Update the ASIC routing tables + update_dendrite(add.iter(), del.iter(), dpd, rt.clone(), log)?; + + Ok(()) } diff --git a/mgadm/Cargo.toml b/mgadm/Cargo.toml index 6f943505..6738418e 100644 --- a/mgadm/Cargo.toml +++ b/mgadm/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] +bgp = { path = "../bgp" } rdb = { path = "../rdb" } mg-common = { path = "../mg-common" } mg-admin-client = { path = "../mg-admin-client" } diff --git a/mgadm/src/bgp.rs b/mgadm/src/bgp.rs index 7125b020..87724f96 100644 --- a/mgadm/src/bgp.rs +++ b/mgadm/src/bgp.rs @@ -5,78 +5,297 @@ use anyhow::Result; use clap::{Args, Subcommand}; use colored::*; -use mg_admin_client::types; +use mg_admin_client::types::{self, Path}; +use mg_admin_client::types::{ImportExportPolicy, Rib}; use mg_admin_client::Client; use rdb::types::{PolicyAction, Prefix4}; +use std::collections::BTreeMap; use std::fs::read_to_string; use std::io::{stdout, Write}; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::time::Duration; use tabwriter::TabWriter; -fn to_prefix4(p: &types::Prefix4) -> Prefix4 { - Prefix4 { - value: p.value, - length: p.length, - } +#[derive(Subcommand, Debug)] +pub enum Commands { + /// Manage router configuration. + Config(ConfigSubcommand), + + /// View dynamic router state. + Status(StatusSubcommand), + + /// Omicron control plane commands. + Omicron(OmicronSubcommand), +} + +#[derive(Debug, Args)] +pub struct ConfigSubcommand { + #[command(subcommand)] + command: ConfigCmd, } #[derive(Subcommand, Debug)] -pub enum Commands { +pub enum ConfigCmd { + /// Router management commands. + Router(RouterSubcommand), + + /// Neighbor mangement commands. + Neighbor(NeighborSubcommand), + + /// Origin management commands. + Origin(OriginSubcommand), + + /// Policy management commands. + Policy(PolicySubcommand), +} + +#[derive(Debug, Args)] +pub struct StatusSubcommand { + #[command(subcommand)] + command: StatusCmd, +} + +#[derive(Subcommand, Debug)] +pub enum StatusCmd { + /// Get the status of a router's neighbors. + Neighbors { + #[clap(env)] + asn: u32, + }, + + /// Get the prefixes imported by a BGP router. + Imported { + #[clap(env)] + asn: u32, + }, + + /// Get the selected paths chosen from imported paths. + Selected { + #[clap(env)] + asn: u32, + }, +} + +#[derive(Debug, Args)] +pub struct OmicronSubcommand { + #[command(subcommand)] + command: OmicronCmd, +} + +#[derive(Subcommand, Debug)] +pub enum OmicronCmd { + /// Apply an Omicron BGP configuration. + Apply { filename: String }, +} + +#[derive(Debug, Args)] +pub struct RouterSubcommand { + #[command(subcommand)] + command: RouterCmd, +} + +#[derive(Subcommand, Debug)] +pub enum RouterCmd { /// Get the running set of BGP routers. - GetRouters, + List, + + /// Create a router configuration. + Create(RouterConfig), - /// Add a BGP router. - AddRouter(RouterConfig), + /// Read a router's configuration. + Read { + #[clap(env)] + asn: u32, + }, + + /// Update a router's configuration. + Update(RouterConfig), /// Delete a BGP router. - DeleteRouter { asn: u32 }, + Delete { + #[clap(env)] + asn: u32, + }, +} + +#[derive(Args, Debug)] +pub struct NeighborSubcommand { + #[command(subcommand)] + command: NeighborCmd, +} + +#[derive(Subcommand, Debug)] +pub enum NeighborCmd { + /// List the neighbors of a given router. + List { + #[clap(env)] + asn: u32, + }, + + /// Create a neighbor configuration. + Create(Neighbor), + + /// Read a neighbor configuration. + Read { + addr: IpAddr, + #[clap(env)] + asn: u32, + }, + + /// Update a neighbor's configuration. + Update(Neighbor), + + /// Delete a neighbor configuration + Delete { + addr: IpAddr, + #[clap(env)] + asn: u32, + }, +} + +#[derive(Args, Debug)] +pub struct OriginSubcommand { + #[command(subcommand)] + command: OriginCmd, +} - /// Add a neighbor to a BGP router. - AddNeighbor(Neighbor), +#[derive(Subcommand, Debug)] +pub enum OriginCmd { + Ipv4(Origin4Subcommand), + //Ipv6, TODO +} - /// Remove a neighbor from a BGP router. - DeleteNeighbor { asn: u32, addr: IpAddr }, +#[derive(Args, Debug)] +pub struct Origin4Subcommand { + #[command(subcommand)] + command: Origin4Cmd, +} +#[derive(Subcommand, Debug)] +pub enum Origin4Cmd { /// Originate a set of prefixes from a BGP router. - Originate4(Originate4), + Create(Originate4), + + /// Read originated prefexes for a BGP router. + Read { + #[clap(env)] + asn: u32, + }, + + /// Update a routers originated prefixes. + Update(Originate4), + + /// Delete a router's originated prefixes. + Delete { + #[clap(env)] + asn: u32, + }, +} + +#[derive(Args, Debug)] +pub struct PolicySubcommand { + #[command(subcommand)] + command: PolicyCmd, +} - /// Withdraw a set of prefixes from a BGP router. - Withdraw4(Withdraw4), +#[derive(Subcommand, Debug)] +pub enum PolicyCmd { + /// Manage the policy checker for a router. + Checker(CheckerSubcommand), - /// Get the prefixes imported by a BGP router. - GetImported { asn: u32 }, + /// Manage the policy shaper for a router. + Shaper(ShaperSubcommand), +} - /// Get the prefixes originated by a BGP router. - GetOriginated { asn: u32 }, +#[derive(Args, Debug)] +pub struct CheckerSubcommand { + #[command(subcommand)] + command: CheckerCmd, +} - /// Apply a BGP peer group configuration. - Apply { filename: String }, +#[derive(Subcommand, Debug)] +pub enum CheckerCmd { + /// Create a BGP policy checker for the specified router. + Create { + file: String, + #[clap(env)] + asn: u32, + }, + + /// Read a routers policy checker. + Read { + #[clap(env)] + asn: u32, + }, + + /// Update the BGP policy checker for the specified router. + Update { + file: String, + #[clap(env)] + asn: u32, + }, + + /// Delete a routers policy checker. + Delete { + #[clap(env)] + asn: u32, + }, +} - /// Initiate a graceful shutdown of a BGP router. - EnableGshut { asn: u32 }, +#[derive(Args, Debug)] +pub struct ShaperSubcommand { + #[command(subcommand)] + command: ShaperCmd, +} - /// Disable graceful shutdown of a BGP router. - DisableGshut { asn: u32 }, +#[derive(Subcommand, Debug)] +pub enum ShaperCmd { + /// Create a BGP policy checker for the specified router. + Create { + file: String, + #[clap(env)] + asn: u32, + }, + + /// Read a routers policy checker. + Read { + #[clap(env)] + asn: u32, + }, + + /// Update the BGP policy checker for the specified router. + Update { + file: String, + #[clap(env)] + asn: u32, + }, + + /// Delete a routers policy checker. + Delete { + #[clap(env)] + asn: u32, + }, } #[derive(Args, Debug)] pub struct RouterConfig { - /// Autonomous system number for this router - pub asn: u32, - /// Id for this router pub id: u32, /// Listening address `:` pub listen: String, + + /// Gracefully shut this router down according to RFC 8326 + #[clap(long)] + pub graceful_shutdown: bool, + + /// Autonomous system number for this router + #[clap(env)] + pub asn: u32, } #[derive(Args, Debug)] pub struct ExportPolicy { - /// Autonomous system number for the router to add the export policy to. - pub asn: u32, - /// Address of the peer to apply this policy to. pub addr: IpAddr, @@ -88,31 +307,34 @@ pub struct ExportPolicy { /// The policy action to apply. pub action: PolicyAction, + + /// Autonomous system number for the router to add the export policy to. + #[clap(env)] + pub asn: u32, } #[derive(Args, Debug)] pub struct Originate4 { - /// Autonomous system number for the router to originated the prefixes from. - pub asn: u32, - /// Set of prefixes to originate. pub prefixes: Vec, -} -#[derive(Args, Debug)] -pub struct Withdraw4 { /// Autonomous system number for the router to originated the prefixes from. + #[clap(env)] pub asn: u32, +} +#[derive(Args, Debug)] +pub struct Withdraw4 { /// Set of prefixes to originate. pub prefixes: Vec, + + /// Autonomous system number for the router to originated the prefixes from. + #[clap(env)] + pub asn: u32, } #[derive(Args, Debug)] pub struct Neighbor { - /// Autonomous system number for the router to add the neighbor to. - pub asn: u32, - /// Name for this neighbor name: String, @@ -131,7 +353,7 @@ pub struct Neighbor { hold_time: u64, /// How long a peer is kept in idle before automatic restart (s). - #[arg(long, default_value_t = 6)] + #[arg(long, default_value_t = 0)] idle_hold_time: u64, /// How long to wait between connection retries (s). @@ -153,12 +375,56 @@ pub struct Neighbor { /// Do not initiate connections, only accept them. #[arg(long, default_value_t = false)] passive_connection: bool, + + /// Autonomous system number for the remote peer. + #[arg(long)] + pub remote_asn: Option, + + /// Minimum acceptable TTL for neighbor. + #[arg(long)] + pub min_ttl: Option, + + /// Authentication key used for TCP-MD5 with remote peer. + #[arg(long)] + pub md5_auth_key: Option, + + /// Multi-exit discriminator to send to eBGP peers. + #[arg(long)] + pub med: Option, + + // Communities to attach to update messages. + #[arg(long)] + pub communities: Vec, + + /// Local preference to send to iBGP peers. + #[arg(long)] + pub local_pref: Option, + + /// Ensure that routes received from eBGP peers have the peer's ASN as the + /// first element in the AS path. + #[arg(long)] + pub enforce_first_as: bool, + + #[arg(long)] + pub vlan_id: Option, + + #[arg(long)] + pub allow_export: Option>, + + #[arg(long)] + pub allow_import: Option>, + + /// Autonomous system number for the router to add the neighbor to. + #[clap(env)] + pub asn: u32, } -impl From for types::AddNeighborRequest { - fn from(n: Neighbor) -> types::AddNeighborRequest { - types::AddNeighborRequest { +impl From for types::Neighbor { + fn from(n: Neighbor) -> types::Neighbor { + types::Neighbor { asn: n.asn, + remote_asn: n.remote_asn, + min_ttl: n.min_ttl, name: n.name, host: SocketAddr::new(n.addr, n.port).to_string(), hold_time: n.hold_time, @@ -169,151 +435,241 @@ impl From for types::AddNeighborRequest { resolution: n.resolution, group: n.group, passive: n.passive_connection, + md5_auth_key: n.md5_auth_key.clone(), + multi_exit_discriminator: n.med, + communities: n.communities, + local_pref: n.local_pref, + enforce_first_as: n.enforce_first_as, + allow_export: match n.allow_export { + Some(prefixes) => ImportExportPolicy::Allow( + prefixes + .clone() + .into_iter() + .map(|x| { + types::Prefix::V4(types::Prefix4 { + length: x.length, + value: x.value, + }) + }) + .collect(), + ), + None => ImportExportPolicy::NoFiltering, + }, + allow_import: match n.allow_import { + Some(prefixes) => ImportExportPolicy::Allow( + prefixes + .clone() + .into_iter() + .map(|x| { + types::Prefix::V4(types::Prefix4 { + length: x.length, + value: x.value, + }) + }) + .collect(), + ), + None => ImportExportPolicy::NoFiltering, + }, + vlan_id: n.vlan_id, } } } -pub async fn commands(command: Commands, client: Client) -> Result<()> { +pub async fn commands(command: Commands, c: Client) -> Result<()> { match command { - Commands::GetRouters => get_routers(client).await, - Commands::AddRouter(cfg) => add_router(cfg, client).await, - Commands::DeleteRouter { asn } => delete_router(asn, client).await, - Commands::AddNeighbor(nbr) => add_neighbor(nbr, client).await, - Commands::DeleteNeighbor { asn, addr } => { - delete_neighbor(asn, addr, client).await - } - Commands::Originate4(originate) => originate4(originate, client).await, - Commands::Withdraw4(withdraw) => withdraw4(withdraw, client).await, - Commands::GetImported { asn } => get_imported(client, asn).await, - Commands::GetOriginated { asn } => get_originated(client, asn).await, - Commands::Apply { filename } => apply(filename, client).await, - Commands::EnableGshut { asn } => { - graceful_shutdown(asn, true, client).await - } - Commands::DisableGshut { asn } => { - graceful_shutdown(asn, false, client).await - } + Commands::Config(cmd) => match cmd.command { + ConfigCmd::Router(cmd) => match cmd.command { + RouterCmd::List => read_routers(c).await, + RouterCmd::Create(cfg) => create_router(cfg, c).await, + RouterCmd::Read { asn } => read_router(asn, c).await, + RouterCmd::Update(cfg) => update_router(cfg, c).await, + RouterCmd::Delete { asn } => delete_router(asn, c).await, + }, + + ConfigCmd::Neighbor(cmd) => match cmd.command { + NeighborCmd::List { asn } => list_nbr(asn, c).await, + NeighborCmd::Create(nbr) => create_nbr(nbr, c).await, + NeighborCmd::Read { asn, addr } => read_nbr(asn, addr, c).await, + NeighborCmd::Update(nbr) => update_nbr(nbr, c).await, + NeighborCmd::Delete { asn, addr } => { + delete_nbr(asn, addr, c).await + } + }, + + ConfigCmd::Origin(cmd) => match cmd.command { + OriginCmd::Ipv4(cmd) => match cmd.command { + Origin4Cmd::Create(origin) => { + create_origin4(origin, c).await + } + Origin4Cmd::Read { asn } => read_origin4(asn, c).await, + Origin4Cmd::Update(origin) => { + update_origin4(origin, c).await + } + Origin4Cmd::Delete { asn } => delete_origin4(asn, c).await, + }, + }, + + ConfigCmd::Policy(cmd) => match cmd.command { + PolicyCmd::Checker(cmd) => match cmd.command { + CheckerCmd::Create { file, asn } => { + create_chk(file, asn, c).await + } + CheckerCmd::Read { asn } => read_chk(asn, c).await, + CheckerCmd::Update { file, asn } => { + update_chk(file, asn, c).await + } + CheckerCmd::Delete { asn } => delete_chk(asn, c).await, + }, + PolicyCmd::Shaper(cmd) => match cmd.command { + ShaperCmd::Create { file, asn } => { + create_shp(file, asn, c).await + } + ShaperCmd::Read { asn } => read_shp(asn, c).await, + ShaperCmd::Update { file, asn } => { + update_shp(file, asn, c).await + } + ShaperCmd::Delete { asn } => delete_shp(asn, c).await, + }, + }, + }, + + Commands::Status(cmd) => match cmd.command { + StatusCmd::Neighbors { asn } => get_neighbors(c, asn).await, + StatusCmd::Imported { asn } => get_imported(c, asn).await, + StatusCmd::Selected { asn } => get_selected(c, asn).await, + }, + + Commands::Omicron(cmd) => match cmd.command { + OmicronCmd::Apply { filename } => apply(filename, c).await, + }, } Ok(()) } -async fn get_routers(c: Client) { - let routers = c.get_routers().await.unwrap().into_inner(); - for r in &routers { - let gshut = if r.graceful_shutdown { - " graceful shutdown".yellow() - } else { - "".normal() - }; - println!("{}: {}{gshut}", "ASN".dimmed(), r.asn); - let mut tw = TabWriter::new(stdout()); - writeln!( - &mut tw, - "{}\t{}\t{}\t{}", - "Peer Address".dimmed(), - "Peer ASN".dimmed(), - "State".dimmed(), - "State Duration".dimmed(), - ) - .unwrap(); +async fn read_routers(c: Client) { + let routers = c.read_routers().await.unwrap().into_inner(); + println!("{routers:#?}"); +} - for (addr, info) in &r.peers { - writeln!( - &mut tw, - "{}\t{:?}\t{:?}\t{:}", - addr, - info.asn, - info.state, - humantime::Duration::from(Duration::from_millis( - info.duration_millis - ),), - ) - .unwrap(); - } - tw.flush().unwrap(); - println!(); - } +async fn create_router(cfg: RouterConfig, c: Client) { + c.create_router(&types::Router { + asn: cfg.asn, + id: cfg.id, + listen: cfg.listen, + graceful_shutdown: cfg.graceful_shutdown, + }) + .await + .unwrap(); } -async fn add_router(cfg: RouterConfig, c: Client) { - c.new_router(&types::NewRouterRequest { +async fn update_router(cfg: RouterConfig, c: Client) { + c.update_router(&types::Router { asn: cfg.asn, id: cfg.id, listen: cfg.listen, + graceful_shutdown: cfg.graceful_shutdown, }) .await .unwrap(); } -async fn delete_router(asn: u32, c: Client) { - c.delete_router(&types::DeleteRouterRequest { asn }) - .await - .unwrap(); +async fn read_router(asn: u32, c: Client) { + let response = c.read_router(asn).await.unwrap(); + println!("{response:#?}"); } -async fn get_imported(c: Client, asn: u32) { - let imported = c - .get_imported4(&types::GetImported4Request { asn }) - .await - .unwrap() - .into_inner(); +async fn delete_router(asn: u32, c: Client) { + c.delete_router(asn).await.unwrap(); +} +async fn get_neighbors(c: Client, asn: u32) { + let result = c.get_neighbors(asn).await.unwrap(); + //println!("{result:#?}"); let mut tw = TabWriter::new(stdout()); writeln!( &mut tw, - "{}\t{}\t{}\t{}", - "Prefix".dimmed(), - "Nexthop".dimmed(), - "Peer Id".dimmed(), - "Priority".dimmed(), + "{}\t{}\t{}\t{}\t{}\t{}", + "Peer Address".dimmed(), + "Peer ASN".dimmed(), + "State".dimmed(), + "State Duration".dimmed(), + "Hold".dimmed(), + "Keepalive".dimmed(), ) .unwrap(); - for route in &imported { - let id = Ipv4Addr::from(route.id); + for (addr, info) in result.iter() { writeln!( &mut tw, - "{}\t{}\t{}\t{}", - to_prefix4(&route.prefix), - route.nexthop, - id, - route.priority, + "{}\t{:?}\t{:?}\t{:}\t{}/{}\t{}/{}", + addr, + info.asn, + info.state, + humantime::Duration::from(Duration::from_millis( + info.duration_millis + ),), + humantime::Duration::from(Duration::from_secs( + info.timers.hold.configured.secs + )), + humantime::Duration::from(Duration::from_secs( + info.timers.hold.negotiated.secs + )), + humantime::Duration::from(Duration::from_secs( + info.timers.keepalive.configured.secs, + )), + humantime::Duration::from(Duration::from_secs( + info.timers.keepalive.negotiated.secs, + )), ) .unwrap(); } - tw.flush().unwrap(); } -async fn get_originated(c: Client, asn: u32) { - let originated = c - .get_originated4(&types::GetOriginated4Request { asn }) +async fn get_imported(c: Client, asn: u32) { + let imported = c + .get_imported(&types::AsnSelector { asn }) .await .unwrap() .into_inner(); - let mut tw = TabWriter::new(stdout()); - writeln!(&mut tw, "{}", "Prefix".dimmed()).unwrap(); + print_rib(imported); +} - for prefix in &originated { - writeln!(&mut tw, "{}", to_prefix4(prefix)).unwrap(); - } +async fn get_selected(c: Client, asn: u32) { + let selected = c + .get_selected(&types::AsnSelector { asn }) + .await + .unwrap() + .into_inner(); - tw.flush().unwrap(); + print_rib(selected); } -async fn add_neighbor(nbr: Neighbor, c: Client) { - c.add_neighbor_handler(&nbr.into()).await.unwrap(); +async fn list_nbr(asn: u32, c: Client) { + let nbrs = c.read_neighbors(asn).await.unwrap(); + println!("{nbrs:#?}"); } -async fn delete_neighbor(asn: u32, addr: IpAddr, c: Client) { - c.delete_neighbor(&types::DeleteNeighborRequest { asn, addr }) - .await - .unwrap(); +async fn create_nbr(nbr: Neighbor, c: Client) { + c.create_neighbor(&nbr.into()).await.unwrap(); +} + +async fn read_nbr(asn: u32, addr: IpAddr, c: Client) { + let nbr = c.read_neighbor(&addr, asn).await.unwrap().into_inner(); + println!("{nbr:#?}"); +} + +async fn update_nbr(nbr: Neighbor, c: Client) { + c.update_neighbor(&nbr.into()).await.unwrap(); +} + +async fn delete_nbr(asn: u32, addr: IpAddr, c: Client) { + c.delete_neighbor(&addr, asn).await.unwrap(); } -async fn originate4(originate: Originate4, c: Client) { - c.originate4(&types::Originate4Request { +async fn create_origin4(originate: Originate4, c: Client) { + c.create_origin4(&types::Origin4 { asn: originate.asn, prefixes: originate .prefixes @@ -329,10 +685,10 @@ async fn originate4(originate: Originate4, c: Client) { .unwrap(); } -async fn withdraw4(withdraw: Withdraw4, c: Client) { - c.withdraw4(&types::Withdraw4Request { - asn: withdraw.asn, - prefixes: withdraw +async fn update_origin4(originate: Originate4, c: Client) { + c.update_origin4(&types::Origin4 { + asn: originate.asn, + prefixes: originate .prefixes .clone() .into_iter() @@ -346,6 +702,15 @@ async fn withdraw4(withdraw: Withdraw4, c: Client) { .unwrap(); } +async fn delete_origin4(asn: u32, c: Client) { + c.delete_origin4(asn).await.unwrap(); +} + +async fn read_origin4(asn: u32, c: Client) { + let o4 = c.read_origin4(asn).await.unwrap(); + println!("{o4:#?}"); +} + async fn apply(filename: String, c: Client) { let contents = read_to_string(filename).expect("read file"); let request: types::ApplyRequest = @@ -353,8 +718,141 @@ async fn apply(filename: String, c: Client) { c.bgp_apply(&request).await.expect("bgp apply"); } -async fn graceful_shutdown(asn: u32, enabled: bool, c: Client) { - c.graceful_shutdown(&types::GracefulShutdownRequest { asn, enabled }) +fn print_rib(rib: Rib) { + type CliRib = BTreeMap>; + + let mut static_routes = CliRib::new(); + let mut bgp_routes = CliRib::new(); + for (prefix, paths) in rib.0.into_iter() { + let (br, sr) = paths.into_iter().partition(|p| p.bgp.is_some()); + static_routes.insert(prefix.clone(), sr); + bgp_routes.insert(prefix, br); + } + + if static_routes.values().map(|x| x.len()).sum::() > 0 { + let mut tw = TabWriter::new(stdout()); + writeln!( + &mut tw, + "{}\t{}\t{}", + "Prefix".dimmed(), + "Nexthop".dimmed(), + "Local Pref".dimmed(), + ) + .unwrap(); + + for (prefix, paths) in static_routes.into_iter() { + for path in paths.into_iter() { + writeln!( + &mut tw, + "{}\t{}\t{:?}", + prefix, path.nexthop, path.local_pref, + ) + .unwrap(); + } + } + println!("{}", "Static Routes".dimmed()); + println!("{}", "=============".dimmed()); + tw.flush().unwrap(); + } + + if bgp_routes.values().map(|x| x.len()).sum::() > 0 { + let mut tw = TabWriter::new(stdout()); + writeln!( + &mut tw, + "{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}", + "Prefix".dimmed(), + "Nexthop".dimmed(), + "Local Pref".dimmed(), + "Origin AS".dimmed(), + "Peer ID".dimmed(), + "MED".dimmed(), + "AS Path".dimmed(), + "Stale".dimmed(), + ) + .unwrap(); + + for (prefix, paths) in bgp_routes.into_iter() { + for path in paths.into_iter() { + let bgp = path.bgp.as_ref().unwrap(); + writeln!( + &mut tw, + "{}\t{}\t{:?}\t{}\t{}\t{:?}\t{:?}\t{:?}", + prefix, + path.nexthop, + path.local_pref, + bgp.origin_as, + Ipv4Addr::from(bgp.id), + bgp.med, + bgp.as_path, + bgp.stale, + ) + .unwrap(); + } + } + println!("{}", "BGP Routes".dimmed()); + println!("{}", "=============".dimmed()); + tw.flush().unwrap(); + } +} + +async fn create_chk(filename: String, asn: u32, c: Client) { + let code = std::fs::read_to_string(filename).unwrap(); + + // check that the program is loadable first + bgp::policy::load_checker(&code).unwrap(); + + c.create_checker(&types::CheckerSource { asn, code }) .await .unwrap(); } + +async fn read_chk(asn: u32, c: Client) { + let result = c.read_checker(asn).await.unwrap(); + print!("{result:#?}"); +} + +async fn update_chk(filename: String, asn: u32, c: Client) { + let code = std::fs::read_to_string(filename).unwrap(); + + // check that the program is loadable first + bgp::policy::load_checker(&code).unwrap(); + + c.update_checker(&types::CheckerSource { asn, code }) + .await + .unwrap(); +} + +async fn delete_chk(asn: u32, c: Client) { + c.delete_checker(asn).await.unwrap(); +} + +async fn create_shp(filename: String, asn: u32, c: Client) { + let code = std::fs::read_to_string(filename).unwrap(); + + // check that the program is loadable first + bgp::policy::load_shaper(&code).unwrap(); + + c.create_shaper(&types::ShaperSource { asn, code }) + .await + .unwrap(); +} + +async fn read_shp(asn: u32, c: Client) { + let result = c.read_shaper(asn).await.unwrap(); + print!("{result:#?}"); +} + +async fn update_shp(filename: String, asn: u32, c: Client) { + let code = std::fs::read_to_string(filename).unwrap(); + + // check that the program is loadable first + bgp::policy::load_shaper(&code).unwrap(); + + c.update_shaper(&types::ShaperSource { asn, code }) + .await + .unwrap(); +} + +async fn delete_shp(asn: u32, c: Client) { + c.delete_shaper(asn).await.unwrap(); +} diff --git a/mgadm/src/main.rs b/mgadm/src/main.rs index 073ca10e..1f88fc53 100644 --- a/mgadm/src/main.rs +++ b/mgadm/src/main.rs @@ -2,6 +2,8 @@ // 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/. +#![allow(clippy::large_enum_variant)] + use anyhow::Result; use clap::{Parser, Subcommand}; use mg_admin_client::Client; @@ -15,13 +17,19 @@ mod bgp; mod static_routing; #[derive(Parser, Debug)] -#[command(version, about, long_about = None, styles = oxide_cli_style())] +#[command( + version, + about, + long_about = None, + styles = oxide_cli_style(), + infer_subcommands = true +)] struct Cli { #[command(subcommand)] command: Commands, /// Address of admin interface - #[arg(short, long, default_value = "::1")] + #[arg(short, env, long, default_value = "::1")] address: IpAddr, /// TCP port for admin interface diff --git a/mgadm/src/static_routing.rs b/mgadm/src/static_routing.rs index f5733c62..5d182be7 100644 --- a/mgadm/src/static_routing.rs +++ b/mgadm/src/static_routing.rs @@ -33,6 +33,7 @@ pub enum Ipv4PrefixParseError { pub struct StaticRoute4 { pub destination: Ipv4Prefix, pub nexthop: Ipv4Addr, + pub vlan_id: Option, } #[derive(Debug, Clone, Copy)] @@ -72,6 +73,7 @@ pub async fn commands(command: Commands, client: Client) -> Result<()> { length: route.destination.len, }, nexthop: route.nexthop, + vlan_id: route.vlan_id, }], }, }; @@ -86,6 +88,7 @@ pub async fn commands(command: Commands, client: Client) -> Result<()> { length: route.destination.len, }, nexthop: route.nexthop, + vlan_id: route.vlan_id, }], }, }; diff --git a/mgd/src/admin.rs b/mgd/src/admin.rs index eea6a86e..b0b3a82a 100644 --- a/mgd/src/admin.rs +++ b/mgd/src/admin.rs @@ -57,6 +57,7 @@ pub fn start_server( })) } +#[macro_export] macro_rules! register { ($api:expr, $endpoint:expr) => { $api.register($endpoint).expect(stringify!($endpoint)) @@ -66,31 +67,9 @@ macro_rules! register { pub fn api_description() -> ApiDescription> { let mut api = ApiDescription::new(); - // bgp - register!(api, bgp_admin::get_routers); - register!(api, bgp_admin::new_router); - register!(api, bgp_admin::ensure_router_handler); - register!(api, bgp_admin::delete_router); - register!(api, bgp_admin::add_neighbor_handler); - register!(api, bgp_admin::ensure_neighbor_handler); - register!(api, bgp_admin::delete_neighbor); - register!(api, bgp_admin::originate4); - register!(api, bgp_admin::withdraw4); - register!(api, bgp_admin::get_originated4); - register!(api, bgp_admin::get_imported4); - register!(api, bgp_admin::bgp_apply); - register!(api, bgp_admin::graceful_shutdown); - register!(api, bgp_admin::message_history); - - // static - register!(api, static_admin::static_add_v4_route); - register!(api, static_admin::static_remove_v4_route); - register!(api, static_admin::static_list_v4_routes); - - // bfd - register!(api, bfd_admin::get_bfd_peers); - register!(api, bfd_admin::add_bfd_peer); - register!(api, bfd_admin::remove_bfd_peer); + bgp_admin::api_description(&mut api); + static_admin::api_description(&mut api); + bfd_admin::api_description(&mut api); api } diff --git a/mgd/src/bfd_admin.rs b/mgd/src/bfd_admin.rs index 50a79d3d..32750d4e 100644 --- a/mgd/src/bfd_admin.rs +++ b/mgd/src/bfd_admin.rs @@ -2,10 +2,11 @@ // 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::admin::HandlerContext; +use crate::{admin::HandlerContext, register}; use anyhow::Result; use bfd::{bidi, packet, BfdPeerState, Daemon}; use dropshot::endpoint; +use dropshot::ApiDescription; use dropshot::HttpError; use dropshot::HttpResponseOk; use dropshot::HttpResponseUpdatedNoContent; @@ -53,6 +54,12 @@ pub struct BfdPeerInfo { state: BfdPeerState, } +pub(crate) fn api_description(api: &mut ApiDescription>) { + register!(api, get_bfd_peers); + register!(api, add_bfd_peer); + register!(api, remove_bfd_peer); +} + /// Get all the peers and their associated BFD state. Peers are identified by IP /// address. #[endpoint { method = GET, path = "/bfd/peers" }] diff --git a/mgd/src/bgp_admin.rs b/mgd/src/bgp_admin.rs index b8951292..5e3b8dd5 100644 --- a/mgd/src/bgp_admin.rs +++ b/mgd/src/bgp_admin.rs @@ -2,32 +2,31 @@ // 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::admin::HandlerContext; -use crate::error::Error; +use crate::bgp_param as resource; +use crate::{admin::HandlerContext, bgp_param::*, error::Error, register}; +use bgp::router::LoadPolicyError; use bgp::{ - config::{PeerConfig, RouterConfig}, + config::RouterConfig, connection::BgpConnection, connection_tcp::BgpConnectionTcp, - messages::Prefix, router::Router, - session::{FsmEvent, FsmStateKind, MessageHistory, SessionInfo}, + session::{FsmEvent, SessionInfo}, BGP_PORT, }; use dropshot::{ - endpoint, HttpError, HttpResponseDeleted, HttpResponseOk, - HttpResponseUpdatedNoContent, RequestContext, TypedBody, + endpoint, ApiDescription, HttpError, HttpResponseDeleted, HttpResponseOk, + HttpResponseUpdatedNoContent, Query, RequestContext, TypedBody, }; use http::status::StatusCode; -use rdb::{Asn, BgpRouterInfo, PolicyAction, Prefix4, Route4ImportKey}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use slog::{info, Logger}; +use rdb::{Asn, BgpRouterInfo}; +use slog::info; use std::collections::{BTreeMap, HashMap, HashSet}; use std::hash::{Hash, Hasher}; use std::net::{IpAddr, Ipv6Addr, SocketAddr, SocketAddrV6}; -use std::sync::mpsc::channel; -use std::sync::mpsc::Sender; -use std::sync::{Arc, Mutex}; +use std::sync::{ + mpsc::{channel, Sender}, + Arc, Mutex, +}; const DEFAULT_BGP_LISTEN: SocketAddr = SocketAddr::V6(SocketAddrV6::new(Ipv6Addr::UNSPECIFIED, BGP_PORT, 0, 0)); @@ -52,226 +51,97 @@ impl BgpContext { } } -#[derive(Debug, Deserialize, Serialize, JsonSchema)] -pub struct NewRouterRequest { - /// Autonomous system number for this router - pub asn: u32, - - /// Id for this router - pub id: u32, - - /// Listening address : - pub listen: String, -} - -#[derive(Debug, Deserialize, Serialize, JsonSchema)] -pub struct DeleteRouterRequest { - /// Autonomous system number for the router to remove - pub asn: u32, -} - -#[derive(Debug, Deserialize, Serialize, JsonSchema, Clone)] -pub struct AddNeighborRequest { - 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, -} - -impl From for PeerConfig { - fn from(rq: AddNeighborRequest) -> Self { - Self { - 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, - } - } -} - -impl AddNeighborRequest { - fn from_bgp_peer_config( - asn: u32, - group: String, - rq: BgpPeerConfig, - ) -> Self { - Self { - asn, - 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(), - } - } -} - -#[derive(Debug, Deserialize, Serialize, JsonSchema)] -pub struct DeleteNeighborRequest { - pub asn: u32, - pub addr: IpAddr, -} - -#[derive(Debug, Deserialize, Serialize, JsonSchema)] -pub struct AddExportPolicyRequest { - /// ASN of the router to apply the export policy to. - pub asn: u32, - - /// Address of the peer to apply this policy to. - pub addr: IpAddr, - - /// Prefix this policy applies to. - pub prefix: Prefix4, - - /// Priority of the policy, higher value is higher priority. - pub priority: u16, - - /// The policy action to apply. - pub action: PolicyAction, -} - -#[derive(Debug, Deserialize, Serialize, JsonSchema)] -pub struct Originate4Request { - /// ASN of the router to originate from. - pub asn: u32, - - /// Set of prefixes to originate. - pub prefixes: Vec, -} - -#[derive(Debug, Deserialize, Serialize, JsonSchema)] -pub struct Withdraw4Request { - /// ASN of the router to originate from. - pub asn: u32, - - /// Set of prefixes to originate. - pub prefixes: Vec, -} - -#[derive(Debug, Deserialize, Serialize, JsonSchema)] -pub struct GetImported4Request { - /// ASN of the router to get imported prefixes from. - pub asn: u32, -} - -#[derive(Debug, Deserialize, Serialize, JsonSchema)] -pub struct GracefulShutdownRequest { - /// ASN of the router to gracefully shut down. - pub asn: u32, - /// Set whether or not graceful shutdown is initiated from this router. - pub enabled: bool, -} - -#[derive(Debug, Deserialize, Serialize, JsonSchema)] -pub struct GetOriginated4Request { - /// ASN of the router to get originated prefixes from. - pub asn: u32, -} - -#[derive(Debug, Deserialize, Serialize, JsonSchema)] -pub struct GetRoutersRequest {} - -#[derive(Debug, Deserialize, Serialize, JsonSchema)] -pub struct GetRouersResponse { - router: Vec, -} - -#[derive(Debug, Deserialize, Serialize, JsonSchema)] -pub struct RouterInfo { - pub asn: u32, - pub peers: BTreeMap, - pub graceful_shutdown: bool, -} - -#[derive(Debug, Deserialize, Serialize, JsonSchema)] -pub struct PeerInfo { - pub state: FsmStateKind, - pub asn: Option, - pub duration_millis: u64, -} - macro_rules! lock { ($mtx:expr) => { $mtx.lock().expect("lock mutex") }; } -#[endpoint { method = GET, path = "/bgp/routers" }] -pub async fn get_routers( - ctx: RequestContext>, -) -> Result>, HttpError> { - let rs = lock!(ctx.context().bgp.router); - let mut result = Vec::new(); - - for r in rs.values() { - let mut peers = BTreeMap::new(); - for s in lock!(r.sessions).values() { - let dur = s.current_state_duration().as_millis() % u64::MAX as u128; - peers.insert( - s.neighbor.host.ip(), - PeerInfo { - state: s.state(), - asn: s.remote_asn(), - duration_millis: dur as u64, - }, - ); - } - result.push(RouterInfo { - asn: match r.config.asn { - Asn::TwoOctet(asn) => asn.into(), - Asn::FourOctet(asn) => asn, - }, - peers, - graceful_shutdown: r.in_graceful_shutdown(), - }); - } - - Ok(HttpResponseOk(result)) +macro_rules! get_router { + ($ctx:expr, $asn:expr) => { + lock!($ctx.bgp.router) + .get(&$asn) + .ok_or(Error::NotFound("no bgp router configured".into())) + }; } -#[endpoint { method = PUT, path = "/bgp/router" }] -pub async fn ensure_router_handler( +pub(crate) fn api_description(api: &mut ApiDescription>) { + // + // Config API + // + + // Router configuration + register!(api, read_routers); + register!(api, create_router); + register!(api, read_router); + register!(api, update_router); + register!(api, delete_router); + + // Neighbor configuration + register!(api, read_neighbors); + register!(api, create_neighbor); + register!(api, read_neighbor); + register!(api, update_neighbor); + register!(api, delete_neighbor); + + // Origin configuration + register!(api, create_origin4); + register!(api, read_origin4); + register!(api, update_origin4); + register!(api, delete_origin4); + + // Policy checker configuration + register!(api, create_checker); + register!(api, read_checker); + register!(api, update_checker); + register!(api, delete_checker); + + // Policy shaper configuration + register!(api, create_shaper); + register!(api, read_shaper); + register!(api, update_shaper); + register!(api, delete_shaper); + + // Omicron API XXX? use normal API now that it's somewhat civilized? + register!(api, bgp_apply); + + // + // Status API + // + + register!(api, get_neighbors); + register!(api, get_imported); + register!(api, get_selected); + register!(api, message_history); +} + +#[endpoint { method = GET, path = "/bgp/config/routers" }] +pub async fn read_routers( ctx: RequestContext>, - request: TypedBody, -) -> Result { +) -> Result>, HttpError> { let ctx = ctx.context(); - let rq = request.into_inner(); - Ok(ensure_router(ctx.clone(), rq).await?) -} + let routers = ctx + .db + .get_bgp_routers() + .map_err(|e| HttpError::for_internal_error(format!("{e}")))?; + let mut result = Vec::new(); -async fn ensure_router( - ctx: Arc, - rq: NewRouterRequest, -) -> Result { - let mut guard = lock!(ctx.bgp.router); - if guard.get(&rq.asn).is_some() { - return Ok(HttpResponseUpdatedNoContent()); + for (asn, info) in routers.iter() { + result.push(resource::Router { + asn: *asn, + id: info.id, + listen: info.listen.clone(), + graceful_shutdown: info.graceful_shutdown, + }); } - add_router(ctx.clone(), rq, &mut guard) + Ok(HttpResponseOk(result)) } -#[endpoint { method = POST, path = "/bgp/router" }] -pub async fn new_router( +#[endpoint { method = PUT, path = "/bgp/config/router" }] +pub async fn create_router( ctx: RequestContext>, - request: TypedBody, + request: TypedBody, ) -> Result { let ctx = ctx.context(); let rq = request.into_inner(); @@ -284,46 +154,49 @@ pub async fn new_router( )); } - Ok(add_router(ctx.clone(), rq, &mut guard)?) + Ok(helpers::add_router(ctx.clone(), rq, &mut guard)?) } -pub(crate) fn add_router( - ctx: Arc, - rq: NewRouterRequest, - routers: &mut BTreeMap>>, -) -> Result { - let cfg = RouterConfig { - asn: Asn::FourOctet(rq.asn), - id: rq.id, - }; - - let db = ctx.db.clone(); +#[endpoint { method = GET, path = "/bgp/config/router" }] +pub async fn read_router( + ctx: RequestContext>, + request: Query, +) -> Result, HttpError> { + let ctx = ctx.context(); + let rq = request.into_inner(); - let router = Arc::new(Router::::new( - cfg, - ctx.log.clone(), - db.clone(), - ctx.bgp.addr_to_session.clone(), - )); + let routers = ctx + .db + .get_bgp_routers() + .map_err(|e| HttpError::for_internal_error(format!("{e}")))?; - router.run(); + let info = routers.get(&rq.asn).ok_or(HttpError::for_not_found( + None, + format!("asn: {} not found in db", rq.asn), + ))?; - routers.insert(rq.asn, router); - db.add_bgp_router( - rq.asn, - BgpRouterInfo { - id: rq.id, - listen: rq.listen.clone(), - }, - )?; + Ok(HttpResponseOk(resource::Router { + asn: rq.asn, + id: info.id, + listen: info.listen.clone(), + graceful_shutdown: info.graceful_shutdown, + })) +} - Ok(HttpResponseUpdatedNoContent()) +#[endpoint { method = POST, path = "/bgp/config/router" }] +pub async fn update_router( + ctx: RequestContext>, + request: TypedBody, +) -> Result { + let ctx = ctx.context(); + let rq = request.into_inner(); + Ok(helpers::ensure_router(ctx.clone(), rq).await?) } -#[endpoint { method = DELETE, path = "/bgp/router" }] +#[endpoint { method = DELETE, path = "/bgp/config/router" }] pub async fn delete_router( ctx: RequestContext>, - request: TypedBody, + request: Query, ) -> Result { let rq = request.into_inner(); let mut routers = lock!(ctx.context().bgp.router); @@ -334,238 +207,235 @@ pub async fn delete_router( Ok(HttpResponseUpdatedNoContent()) } -macro_rules! get_router { - ($ctx:expr, $asn:expr) => { - lock!($ctx.bgp.router) - .get(&$asn) - .ok_or(Error::NotFound("no bgp router configured".into())) - }; +#[endpoint { method = GET, path = "/bgp/config/neighbors" }] +pub async fn read_neighbors( + 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| resource::Neighbor::from_rdb_neighbor_info(rq.asn, &x)) + .collect(); + + Ok(HttpResponseOk(result)) } -#[endpoint { method = POST, path = "/bgp/neighbor" }] -pub async fn add_neighbor_handler( +#[endpoint { method = PUT, path = "/bgp/config/neighbor" }] +pub async fn create_neighbor( ctx: RequestContext>, - request: TypedBody, + request: TypedBody, ) -> Result { - let log = ctx.context().log.clone(); let rq = request.into_inner(); let ctx = ctx.context(); - add_neighbor(ctx.clone(), rq, log).await?; + helpers::add_neighbor(ctx.clone(), rq, false)?; Ok(HttpResponseUpdatedNoContent()) } -async fn add_neighbor( - ctx: Arc, - rq: AddNeighborRequest, - log: Logger, -) -> Result<(), Error> { - info!(log, "add neighbor: {:#?}", rq); - - let (event_tx, event_rx) = channel(); - - let info = SessionInfo { - passive_tcp_establishment: rq.passive, - ..Default::default() - }; - - get_router!(&ctx, rq.asn)?.new_session( - rq.clone().into(), - DEFAULT_BGP_LISTEN, - event_tx.clone(), - event_rx, - info, - )?; - - ctx.db.add_bgp_neighbor(rdb::BgpNeighborInfo { - asn: rq.asn, - 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, +#[endpoint { method = GET, path = "/bgp/config/neighbor" }] +pub async fn read_neighbor( + ctx: RequestContext>, + request: Query, +) -> Result, HttpError> { + let rq = request.into_inner(); + let db_neighbors = ctx.context().db.get_bgp_neighbors().map_err(|e| { + HttpError::for_internal_error(format!("get neighbors kv tree: {e}")) })?; - - start_bgp_session(&event_tx)?; - - Ok(()) + let neighbor_info = db_neighbors + .iter() + .find(|n| n.host.ip() == rq.addr) + .ok_or(HttpError::for_not_found( + None, + format!("neighbor {} not found in db", rq.addr), + ))?; + + let result = + resource::Neighbor::from_rdb_neighbor_info(rq.asn, neighbor_info); + Ok(HttpResponseOk(result)) } -async fn remove_neighbor( - ctx: Arc, - asn: u32, - addr: IpAddr, -) -> Result { - info!(ctx.log, "remove neighbor: {}", addr); - - ctx.db.remove_bgp_neighbor(addr)?; - get_router!(&ctx, asn)?.delete_session(addr); - - Ok(HttpResponseDeleted()) +#[endpoint { method = POST, path = "/bgp/config/neighbor" }] +pub async fn update_neighbor( + ctx: RequestContext>, + request: TypedBody, +) -> Result { + let rq = request.into_inner(); + let ctx = ctx.context(); + helpers::add_neighbor(ctx.clone(), rq, true)?; + Ok(HttpResponseUpdatedNoContent()) } -#[endpoint { method = DELETE, path = "/bgp/neighbor" }] +#[endpoint { method = DELETE, path = "/bgp/config/neighbor" }] pub async fn delete_neighbor( ctx: RequestContext>, - request: TypedBody, + request: Query, ) -> Result { let rq = request.into_inner(); let ctx = ctx.context(); - Ok(remove_neighbor(ctx.clone(), rq.asn, rq.addr).await?) + Ok(helpers::remove_neighbor(ctx.clone(), rq.asn, rq.addr).await?) } -#[endpoint { method = PUT, path = "/bgp/neighbor" }] -pub async fn ensure_neighbor_handler( +#[endpoint { method = PUT, path = "/bgp/config/origin4" }] +pub async fn create_origin4( ctx: RequestContext>, - request: TypedBody, + request: TypedBody, ) -> Result { let rq = request.into_inner(); + let prefixes = rq.prefixes.into_iter().map(Into::into).collect(); let ctx = ctx.context(); - ensure_neighbor(ctx.clone(), rq) -} -pub(crate) fn ensure_neighbor( - ctx: Arc, - rq: AddNeighborRequest, -) -> Result { - info!(ctx.log, "add neighbor: {:#?}", rq); + get_router!(ctx, rq.asn)? + .create_origin4(prefixes) + .map_err(|e| HttpError::for_internal_error(e.to_string()))?; - let (event_tx, event_rx) = channel(); + Ok(HttpResponseUpdatedNoContent()) +} - let info = SessionInfo { - passive_tcp_establishment: rq.passive, - ..Default::default() - }; +#[endpoint { method = GET, path = "/bgp/config/origin4" }] +pub async fn read_origin4( + ctx: RequestContext>, + request: Query, +) -> Result, HttpError> { + let rq = request.into_inner(); + let ctx = ctx.context(); + let mut originated = get_router!(ctx, rq.asn)? + .db + .get_origin4() + .map_err(|e| HttpError::for_internal_error(e.to_string()))?; - match get_router!(&ctx, rq.asn)?.new_session( - rq.into(), - DEFAULT_BGP_LISTEN, - event_tx.clone(), - event_rx, - info, - ) { - Ok(_) => {} - Err(bgp::error::Error::PeerExists) => { - return Ok(HttpResponseUpdatedNoContent()); - } - Err(e) => { - return Err(HttpError::for_internal_error(format!("{:?}", e))); - } - } - start_bgp_session(&event_tx)?; + // stable output order for clients + originated.sort(); - Ok(HttpResponseUpdatedNoContent()) + Ok(HttpResponseOk(resource::Origin4 { + asn: rq.asn, + prefixes: originated, + })) } -#[endpoint { method = POST, path = "/bgp/originate4" }] -pub async fn originate4( +#[endpoint { method = POST, path = "/bgp/config/origin4" }] +pub async fn update_origin4( ctx: RequestContext>, - request: TypedBody, + request: TypedBody, ) -> Result { let rq = request.into_inner(); let prefixes = rq.prefixes.into_iter().map(Into::into).collect(); let ctx = ctx.context(); get_router!(ctx, rq.asn)? - .originate4(prefixes) + .set_origin4(prefixes) .map_err(|e| HttpError::for_internal_error(e.to_string()))?; Ok(HttpResponseUpdatedNoContent()) } -#[endpoint { method = POST, path = "/bgp/withdraw4" }] -pub async fn withdraw4( +#[endpoint { method = DELETE, path = "/bgp/config/origin4" }] +pub async fn delete_origin4( ctx: RequestContext>, - request: TypedBody, -) -> Result { + request: Query, +) -> Result { let rq = request.into_inner(); - let prefixes = rq.prefixes.into_iter().map(Into::into).collect(); let ctx = ctx.context(); get_router!(ctx, rq.asn)? - .withdraw4(prefixes) + .clear_origin4() .map_err(|e| HttpError::for_internal_error(e.to_string()))?; - Ok(HttpResponseUpdatedNoContent()) + Ok(HttpResponseDeleted()) } -#[endpoint { method = GET, path = "/bgp/originate4" }] -pub async fn get_originated4( +#[endpoint { method = GET, path = "/bgp/status/imported" }] +pub async fn get_imported( ctx: RequestContext>, - request: TypedBody, -) -> Result>, HttpError> { + request: TypedBody, +) -> Result, HttpError> { let rq = request.into_inner(); let ctx = ctx.context(); - let originated = get_router!(ctx, rq.asn)? - .db - .get_originated4() - .map_err(|e| HttpError::for_internal_error(e.to_string()))?; - Ok(HttpResponseOk(originated)) + let imported = get_router!(ctx, rq.asn)?.db.full_rib(); + Ok(HttpResponseOk(imported.into())) } -#[endpoint { method = GET, path = "/bgp/imported4" }] -pub async fn get_imported4( +#[endpoint { method = GET, path = "/bgp/status/selected" }] +pub async fn get_selected( ctx: RequestContext>, - request: TypedBody, -) -> Result>, HttpError> { + request: TypedBody, +) -> Result, HttpError> { let rq = request.into_inner(); let ctx = ctx.context(); - let imported = get_router!(ctx, rq.asn)?.db.get_imported4(); - Ok(HttpResponseOk(imported)) + let rib = get_router!(ctx, rq.asn)?.db.loc_rib(); + let selected = rib.lock().unwrap().clone(); + Ok(HttpResponseOk(selected.into())) } -#[endpoint { method = POST, path = "/bgp/graceful_shutdown" }] -pub async fn graceful_shutdown( +#[endpoint { method = GET, path = "/bgp/status/neighbors" }] +pub async fn get_neighbors( ctx: RequestContext>, - request: TypedBody, -) -> Result { + request: Query, +) -> Result>, HttpError> { let rq = request.into_inner(); let ctx = ctx.context(); - get_router!(ctx, rq.asn)? - .graceful_shutdown(rq.enabled) - .map_err(|e| HttpError::for_internal_error(e.to_string()))?; - Ok(HttpResponseUpdatedNoContent()) + + let mut peers = HashMap::new(); + let routers = lock!(ctx.bgp.router); + let r = routers + .get(&rq.asn) + .ok_or(HttpError::for_not_found(None, "ASN not found".to_string()))?; + + for s in lock!(r.sessions).values() { + let dur = s.current_state_duration().as_millis() % u64::MAX as u128; + peers.insert( + s.neighbor.host.ip(), + PeerInfo { + state: s.state(), + asn: s.remote_asn(), + duration_millis: dur as u64, + timers: PeerTimers { + hold: DynamicTimerInfo { + configured: *s + .clock + .timers + .hold_configured_interval + .lock() + .unwrap(), + negotiated: s + .clock + .timers + .hold_timer + .lock() + .unwrap() + .interval, + }, + keepalive: DynamicTimerInfo { + configured: *s + .clock + .timers + .keepalive_configured_interval + .lock() + .unwrap(), + negotiated: s + .clock + .timers + .keepalive_timer + .lock() + .unwrap() + .interval, + }, + }, + }, + ); + } + + Ok(HttpResponseOk(peers)) } -/// Apply changes to an ASN. -#[derive(Debug, Deserialize, Serialize, JsonSchema)] -pub struct ApplyRequest { - /// ASN to apply changes to. - pub asn: u32, - /// Complete set of prefixes to originate. Any active prefixes not in this - /// list will be removed. All prefixes in this list are ensured to be in - /// the originating set. - pub originate: Vec, - /// Lists of peers indexed by peer group. Set's within a peer group key are - /// a total set. For example, the value - /// - /// ```text - /// {"foo": [a, b, d]} - /// ``` - /// Means that the peer group "foo" only contains the peers `a`, `b` and - /// `d`. If there is a peer `c` currently in the peer group "foo", it will - /// be removed. - pub peers: HashMap>, -} - -#[derive(Debug, Deserialize, Serialize, JsonSchema, Clone)] -pub struct BgpPeerConfig { - pub host: SocketAddr, - pub name: String, - pub hold_time: u64, - pub idle_hold_time: u64, - pub delay_open: u64, - pub connect_retry: u64, - pub keepalive: u64, - pub resolution: u64, - pub passive: bool, -} - -#[endpoint { method = POST, path = "/bgp/apply" }] +#[endpoint { method = POST, path = "/bgp/omicron/apply" }] pub async fn bgp_apply( ctx: RequestContext>, request: TypedBody, @@ -638,31 +508,32 @@ pub async fn bgp_apply( // TODO all the db modification that happens below needs to happen in a // transaction. - ensure_router( + helpers::ensure_router( ctx.context().clone(), - NewRouterRequest { + resource::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 { - add_neighbor( + helpers::add_neighbor( ctx.context().clone(), - AddNeighborRequest::from_bgp_peer_config( + resource::Neighbor::from_bgp_peer_config( nbr.asn, group.clone(), cfg.clone(), ), - log.clone(), - ) - .await?; + false, + )?; } for nbr in to_delete { - remove_neighbor(ctx.context().clone(), nbr.asn, nbr.addr).await?; + helpers::remove_neighbor(ctx.context().clone(), nbr.asn, nbr.addr) + .await?; let mut routers = lock!(ctx.context().bgp.router); let mut remove = false; @@ -677,62 +548,13 @@ pub async fn bgp_apply( } } - let current_originate: HashSet = ctx - .context() - .db - .get_originated4() - .map_err(Error::Db)? - .into_iter() - .collect(); - - let specified_originate: HashSet = - rq.originate.iter().cloned().collect(); - - let to_delete = current_originate - .difference(&specified_originate) - .map(|x| (*x).into()) - .collect(); - - let to_add: Vec = specified_originate - .difference(¤t_originate) - .map(|x| (*x).into()) - .collect(); - - info!(log, "origin: current {current_originate:#?}"); - info!(log, "origin: adding {to_add:#?}"); - info!(log, "origin: removing {to_delete:#?}"); - - get_router!(ctx.context(), rq.asn)? - .originate4(to_add) - .map_err(|e| HttpError::for_internal_error(e.to_string()))?; - get_router!(ctx.context(), rq.asn)? - .withdraw4(to_delete) + .set_origin4(rq.originate.clone().into_iter().map(Into::into).collect()) .map_err(|e| HttpError::for_internal_error(e.to_string()))?; Ok(HttpResponseUpdatedNoContent()) } -fn start_bgp_session( - event_tx: &Sender>, -) -> Result<(), Error> { - event_tx.send(FsmEvent::ManualStart).map_err(|e| { - Error::InternalCommunication( - format!("failed to start bgp session {e}",), - ) - }) -} - -#[derive(Debug, Deserialize, JsonSchema, Clone)] -pub struct MessageHistoryRequest { - asn: u32, -} - -#[derive(Debug, Serialize, JsonSchema, Clone)] -pub struct MessageHistoryResponse { - by_peer: HashMap, -} - #[endpoint { method = GET, path = "/bgp/message-history" }] pub async fn message_history( ctx: RequestContext>, @@ -751,3 +573,370 @@ pub async fn message_history( Ok(HttpResponseOk(MessageHistoryResponse { by_peer: result })) } + +#[endpoint { method = PUT, path = "/bgp/config/checker" }] +pub async fn create_checker( + ctx: RequestContext>, + request: TypedBody, +) -> Result { + let ctx = ctx.context(); + let rq = request.into_inner(); + helpers::load_policy(ctx, rq.asn, PolicySource::Checker(rq.code), false) + .await +} + +#[endpoint { method = GET, path = "/bgp/config/checker" }] +pub async fn read_checker( + ctx: RequestContext>, + request: Query, +) -> Result, HttpError> { + let ctx = ctx.context(); + let rq = request.into_inner(); + match ctx.bgp.router.lock().unwrap().get(&rq.asn) { + None => Err(HttpError::for_not_found( + None, + String::from("ASN not found"), + )), + Some(rtr) => match rtr.policy.checker_source() { + Some(source) => Ok(HttpResponseOk(CheckerSource { + code: source, + asn: rq.asn, + })), + None => Err(HttpError::for_not_found( + None, + String::from("checker source not found"), + )), + }, + } +} + +#[endpoint { method = POST, path = "/bgp/config/checker" }] +pub async fn update_checker( + ctx: RequestContext>, + request: TypedBody, +) -> Result { + let ctx = ctx.context(); + let rq = request.into_inner(); + helpers::load_policy(ctx, rq.asn, PolicySource::Checker(rq.code), true) + .await +} + +#[endpoint { method = DELETE, path = "/bgp/config/checker" }] +pub async fn delete_checker( + ctx: RequestContext>, + request: Query, +) -> Result { + let ctx = ctx.context(); + let rq = request.into_inner(); + helpers::unload_policy(ctx, rq.asn, PolicyKind::Checker).await +} + +#[endpoint { method = PUT, path = "/bgp/config/shaper" }] +pub async fn create_shaper( + ctx: RequestContext>, + request: TypedBody, +) -> Result { + let ctx = ctx.context(); + let rq = request.into_inner(); + helpers::load_policy(ctx, rq.asn, PolicySource::Shaper(rq.code), false) + .await +} + +#[endpoint { method = GET, path = "/bgp/config/shaper" }] +pub async fn read_shaper( + ctx: RequestContext>, + request: Query, +) -> Result, HttpError> { + let ctx = ctx.context(); + let rq = request.into_inner(); + match ctx.bgp.router.lock().unwrap().get(&rq.asn) { + None => Err(HttpError::for_not_found( + None, + String::from("ASN not found"), + )), + Some(rtr) => match rtr.policy.shaper_source() { + Some(source) => Ok(HttpResponseOk(ShaperSource { + code: source, + asn: rq.asn, + })), + None => Err(HttpError::for_not_found( + None, + String::from("shaper source not found"), + )), + }, + } +} + +#[endpoint { method = POST, path = "/bgp/config/shaper" }] +pub async fn update_shaper( + ctx: RequestContext>, + request: TypedBody, +) -> Result { + let ctx = ctx.context(); + let rq = request.into_inner(); + helpers::load_policy(ctx, rq.asn, PolicySource::Shaper(rq.code), true).await +} + +#[endpoint { method = DELETE, path = "/bgp/config/shaper" }] +pub async fn delete_shaper( + ctx: RequestContext>, + request: Query, +) -> Result { + let ctx = ctx.context(); + let rq = request.into_inner(); + helpers::unload_policy(ctx, rq.asn, PolicyKind::Shaper).await +} + +pub(crate) mod helpers { + use bgp::router::{EnsureSessionResult, UnloadPolicyError}; + + use super::*; + + pub(crate) async fn ensure_router( + ctx: Arc, + rq: resource::Router, + ) -> Result { + let mut guard = lock!(ctx.bgp.router); + if let Some(current) = guard.get(&rq.asn) { + current.graceful_shutdown(rq.graceful_shutdown)?; + return Ok(HttpResponseUpdatedNoContent()); + } + + add_router(ctx.clone(), rq, &mut guard) + } + + pub(crate) async fn remove_neighbor( + ctx: Arc, + asn: u32, + addr: IpAddr, + ) -> Result { + info!(ctx.log, "remove neighbor: {}", addr); + + ctx.db.remove_bgp_neighbor(addr)?; + get_router!(&ctx, asn)?.delete_session(addr); + + Ok(HttpResponseDeleted()) + } + + pub(crate) fn add_neighbor( + ctx: Arc, + rq: resource::Neighbor, + ensure: bool, + ) -> Result<(), Error> { + let log = &ctx.log; + info!(log, "add neighbor: {:#?}", rq); + + let (event_tx, event_rx) = channel(); + + 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, + allow_import: rq.allow_import.clone(), + allow_export: rq.allow_export.clone(), + vlan_id: rq.vlan_id, + ..Default::default() + }; + + let start_session = if ensure { + match get_router!(&ctx, rq.asn)?.ensure_session( + rq.clone().into(), + DEFAULT_BGP_LISTEN, + event_tx.clone(), + event_rx, + info, + )? { + EnsureSessionResult::New(_) => true, + EnsureSessionResult::Updated(_) => false, + } + } else { + get_router!(&ctx, rq.asn)?.new_session( + rq.clone().into(), + DEFAULT_BGP_LISTEN, + event_tx.clone(), + event_rx, + info, + )?; + true + }; + + 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_import: rq.allow_import.clone(), + allow_export: rq.allow_export.clone(), + vlan_id: rq.vlan_id, + })?; + + if start_session { + start_bgp_session(&event_tx)?; + } + + Ok(()) + } + + pub(crate) fn add_router( + ctx: Arc, + rq: resource::Router, + routers: &mut BTreeMap>>, + ) -> Result { + let cfg = RouterConfig { + asn: Asn::FourOctet(rq.asn), + id: rq.id, + }; + + let db = ctx.db.clone(); + + let router = Arc::new(Router::::new( + cfg, + ctx.log.clone(), + db.clone(), + ctx.bgp.addr_to_session.clone(), + )); + + router.run(); + + routers.insert(rq.asn, router); + db.add_bgp_router( + rq.asn, + BgpRouterInfo { + id: rq.id, + listen: rq.listen.clone(), + graceful_shutdown: rq.graceful_shutdown, + }, + )?; + + Ok(HttpResponseUpdatedNoContent()) + } + + fn start_bgp_session( + event_tx: &Sender>, + ) -> Result<(), Error> { + event_tx.send(FsmEvent::ManualStart).map_err(|e| { + Error::InternalCommunication(format!( + "failed to start bgp session {e}", + )) + }) + } + + pub async fn load_policy( + ctx: &Arc, + asn: u32, + policy: PolicySource, + overwrite: bool, + ) -> Result { + match ctx.bgp.router.lock().unwrap().get(&asn) { + None => { + return Err(HttpError::for_not_found( + None, + String::from("ASN not found"), + )); + } + Some(rtr) => { + let load_result = match &policy { + PolicySource::Checker(code) => { + rtr.policy.load_checker(code, overwrite) + } + PolicySource::Shaper(code) => { + rtr.policy.load_shaper(code, overwrite) + } + }; + match load_result { + Err(LoadPolicyError::Compilation(e)) => { + // The program failed to compile, return a bad request error + // with the error string from the compiler. + return Err(HttpError::for_bad_request( + None, + e.to_string(), + )); + } + Err(LoadPolicyError::Confilct) => { + return Err(HttpError::for_client_error( + None, + StatusCode::CONFLICT, + "policy already loaded".to_string(), + )); + } + Ok(previous) => match &policy { + PolicySource::Checker(_) => { + rtr.send_event(FsmEvent::CheckerChanged(previous)) + .map_err(|e| { + HttpError::for_internal_error(format!( + "send event: {e}" + )) + })?; + } + PolicySource::Shaper(_) => { + rtr.send_event(FsmEvent::ShaperChanged(previous)) + .map_err(|e| { + HttpError::for_internal_error(format!( + "send event: {e}" + )) + })?; + } + }, + } + } + } + Ok(HttpResponseUpdatedNoContent()) + } + + pub async fn unload_policy( + ctx: &Arc, + asn: u32, + policy: PolicyKind, + ) -> Result { + match ctx.bgp.router.lock().unwrap().get(&asn) { + None => { + return Err(HttpError::for_not_found( + None, + String::from("ASN not found"), + )); + } + Some(rtr) => { + let unload_result = match policy { + PolicyKind::Checker => rtr.policy.unload_checker(), + PolicyKind::Shaper => rtr.policy.unload_shaper(), + }; + match unload_result { + Err(UnloadPolicyError::NotFound) => { + return Err(HttpError::for_not_found( + None, + "no policy loaded".to_string(), + )); + } + Ok(previous) => { + rtr.send_event(FsmEvent::ShaperChanged(Some(previous))) + .map_err(|e| { + HttpError::for_internal_error(format!( + "send event: {e}" + )) + })?; + } + } + } + } + Ok(HttpResponseDeleted()) + } +} diff --git a/mgd/src/bgp_param.rs b/mgd/src/bgp_param.rs new file mode 100644 index 00000000..36cc6a6a --- /dev/null +++ b/mgd/src/bgp_param.rs @@ -0,0 +1,329 @@ +// 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::config::PeerConfig; +use bgp::session::{FsmStateKind, MessageHistory}; +use rdb::{ImportExportPolicy, Path, PolicyAction, Prefix4}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use std::collections::{BTreeSet, HashMap}; +use std::time::Duration; +use std::{ + collections::BTreeMap, + net::{IpAddr, SocketAddr}, +}; + +#[derive(Debug, Deserialize, Serialize, JsonSchema)] +pub struct Router { + /// Autonomous system number for this router + pub asn: u32, + + /// Id for this router + pub id: u32, + + /// Listening address : + pub listen: String, + + /// Gracefully shut this router down. + pub graceful_shutdown: bool, +} + +#[derive(Debug, Deserialize, Serialize, JsonSchema)] +pub struct DeleteRouterRequest { + /// Autonomous system number for the router to remove + pub asn: u32, +} + +#[derive(Debug, Deserialize, Serialize, JsonSchema, Clone)] +pub struct NeighborSelector { + pub asn: u32, + pub addr: IpAddr, +} + +#[derive(Debug, Deserialize, Serialize, JsonSchema, Clone)] +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, + pub allow_import: ImportExportPolicy, + pub allow_export: ImportExportPolicy, + pub vlan_id: Option, +} + +impl From for PeerConfig { + fn from(rq: Neighbor) -> Self { + Self { + 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, + } + } +} + +impl Neighbor { + pub fn from_bgp_peer_config( + asn: u32, + group: String, + rq: BgpPeerConfig, + ) -> 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, + } + } + + pub fn from_rdb_neighbor_info(asn: u32, rq: &rdb::BgpNeighborInfo) -> 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: 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, + allow_import: rq.allow_import.clone(), + allow_export: rq.allow_export.clone(), + vlan_id: rq.vlan_id, + } + } +} + +#[derive(Debug, Deserialize, Serialize, JsonSchema)] +pub struct DeleteNeighborRequest { + pub asn: u32, + pub addr: IpAddr, +} + +#[derive(Debug, Deserialize, Serialize, JsonSchema)] +pub struct AddExportPolicyRequest { + /// ASN of the router to apply the export policy to. + pub asn: u32, + + /// Address of the peer to apply this policy to. + pub addr: IpAddr, + + /// Prefix this policy applies to. + pub prefix: Prefix4, + + /// Priority of the policy, higher value is higher priority. + pub priority: u16, + + /// The policy action to apply. + pub action: PolicyAction, +} + +#[derive(Debug, Deserialize, Serialize, JsonSchema)] +pub struct Origin4 { + /// ASN of the router to originate from. + pub asn: u32, + + /// Set of prefixes to originate. + pub prefixes: Vec, +} + +#[derive(Debug, Deserialize, Serialize, JsonSchema)] +pub struct Withdraw4Request { + /// ASN of the router to originate from. + pub asn: u32, + + /// Set of prefixes to originate. + pub prefixes: Vec, +} + +#[derive(Debug, Deserialize, Serialize, JsonSchema)] +pub struct AsnSelector { + /// ASN of the router to get imported prefixes from. + pub asn: u32, +} + +#[derive(Debug, Deserialize, Serialize, JsonSchema)] +pub struct GracefulShutdownRequest { + /// ASN of the router to gracefully shut down. + pub asn: u32, + /// Set whether or not graceful shutdown is initiated from this router. + pub enabled: bool, +} + +#[derive(Debug, Deserialize, Serialize, JsonSchema)] +pub struct GetOriginated4Request { + /// ASN of the router to get originated prefixes from. + pub asn: u32, +} + +#[derive(Debug, Deserialize, Serialize, JsonSchema)] +pub struct GetRoutersRequest {} + +#[derive(Debug, Deserialize, Serialize, JsonSchema)] +pub struct GetRouersResponse { + pub router: Vec, +} + +#[derive(Debug, Deserialize, Serialize, JsonSchema)] +pub struct RouterInfo { + pub asn: u32, + pub peers: BTreeMap, + pub graceful_shutdown: bool, +} + +#[derive(Debug, Deserialize, Serialize, JsonSchema)] +pub struct DynamicTimerInfo { + pub configured: Duration, + pub negotiated: Duration, +} + +#[derive(Debug, Deserialize, Serialize, JsonSchema)] +pub struct PeerTimers { + pub hold: DynamicTimerInfo, + pub keepalive: DynamicTimerInfo, +} + +#[derive(Debug, Deserialize, Serialize, JsonSchema)] +pub struct PeerInfo { + pub state: FsmStateKind, + pub asn: Option, + pub duration_millis: u64, + pub timers: PeerTimers, +} + +/// Apply changes to an ASN. +#[derive(Debug, Deserialize, Serialize, JsonSchema)] +pub struct ApplyRequest { + /// ASN to apply changes to. + pub asn: u32, + /// Complete set of prefixes to originate. Any active prefixes not in this + /// list will be removed. All prefixes in this list are ensured to be in + /// the originating set. + pub originate: Vec, + + /// Checker rhai code to apply to ingress open and update messages. + pub checker: Option, + + /// Checker rhai code to apply to egress open and update messages. + pub shaper: Option, + + /// Lists of peers indexed by peer group. Set's within a peer group key are + /// a total set. For example, the value + /// + /// ```text + /// {"foo": [a, b, d]} + /// ``` + /// Means that the peer group "foo" only contains the peers `a`, `b` and + /// `d`. If there is a peer `c` currently in the peer group "foo", it will + /// be removed. + pub peers: HashMap>, +} + +#[derive(Debug, Deserialize, Serialize, JsonSchema, Clone)] +pub struct BgpPeerConfig { + pub host: SocketAddr, + pub name: String, + pub hold_time: u64, + pub idle_hold_time: u64, + pub delay_open: u64, + pub connect_retry: u64, + pub keepalive: u64, + pub resolution: u64, //Create then read only + 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: ImportExportPolicy, + pub allow_export: ImportExportPolicy, + pub vlan_id: Option, +} + +#[derive(Debug, Deserialize, JsonSchema, Clone)] +pub struct MessageHistoryRequest { + pub asn: u32, +} + +#[derive(Debug, Serialize, JsonSchema, Clone)] +pub struct MessageHistoryResponse { + pub by_peer: HashMap, +} + +#[derive(Debug, Deserialize, Serialize, JsonSchema, Clone)] +pub struct Rib(BTreeMap>); + +#[derive(Debug, Deserialize, Serialize, JsonSchema, Clone)] +pub struct CheckerSource { + pub asn: u32, + pub code: String, +} + +#[derive(Debug, Deserialize, Serialize, JsonSchema, Clone)] +pub struct ShaperSource { + pub asn: u32, + pub code: String, +} + +pub enum PolicySource { + Checker(String), + Shaper(String), +} + +pub enum PolicyKind { + Checker, + Shaper, +} + +impl From for Rib { + fn from(value: rdb::db::Rib) -> Self { + Rib(value.into_iter().map(|(k, v)| (k.to_string(), v)).collect()) + } +} diff --git a/mgd/src/main.rs b/mgd/src/main.rs index f587e98c..184fcefa 100644 --- a/mgd/src/main.rs +++ b/mgd/src/main.rs @@ -11,10 +11,10 @@ use clap::{Parser, Subcommand}; use mg_common::cli::oxide_cli_style; use mg_common::stats::MgLowerStats; use rand::Fill; -use rdb::{BfdPeerConfig, BgpNeighborInfo, BgpRouterInfo}; +use rdb::{BfdPeerConfig, BgpNeighborInfo, BgpRouterInfo, Path}; use signal::handle_signals; use slog::{error, Logger}; -use std::collections::{BTreeMap, HashMap}; +use std::collections::BTreeMap; use std::net::{IpAddr, Ipv6Addr, SocketAddr}; use std::sync::{Arc, Mutex}; use std::thread::spawn; @@ -23,6 +23,7 @@ use uuid::Uuid; mod admin; mod bfd_admin; mod bgp_admin; +mod bgp_param; mod error; mod oxstats; mod signal; @@ -211,18 +212,19 @@ fn init_bgp(args: &RunArgs, log: &Logger) -> BgpContext { fn start_bgp_routers( context: Arc, - routers: HashMap, + routers: BTreeMap, neighbors: Vec, ) { slog::info!(context.log, "bgp routers: {:#?}", routers); let mut guard = context.bgp.router.lock().expect("lock bgp routers"); for (asn, info) in routers { - bgp_admin::add_router( + bgp_admin::helpers::add_router( context.clone(), - bgp_admin::NewRouterRequest { + bgp_param::Router { asn, id: info.id, listen: info.listen.clone(), + graceful_shutdown: info.graceful_shutdown, }, &mut guard, ) @@ -231,10 +233,12 @@ fn start_bgp_routers( drop(guard); for nbr in neighbors { - bgp_admin::ensure_neighbor( + bgp_admin::helpers::add_neighbor( context.clone(), - bgp_admin::AddNeighborRequest { + bgp_param::Neighbor { asn: nbr.asn, + remote_asn: nbr.remote_asn, + min_ttl: nbr.min_ttl, name: nbr.name.clone(), host: nbr.host, hold_time: nbr.hold_time, @@ -245,7 +249,16 @@ fn start_bgp_routers( 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, + allow_import: nbr.allow_import.clone(), + allow_export: nbr.allow_export.clone(), + vlan_id: nbr.vlan_id, }, + true, ) .unwrap_or_else(|_| panic!("add BGP neighbor {nbr:#?}")); } @@ -267,9 +280,11 @@ fn initialize_static_routes(db: &rdb::Db) { .get_static4() .expect("failed to get static routes from db"); for route in &routes { - db.set_nexthop4(*route, false).unwrap_or_else(|e| { - panic!("failed to initialize static route {route:#?}: {e}") - }); + let path = Path::for_static(route.nexthop, route.vlan_id); + db.add_prefix_path(route.prefix, path, true) + .unwrap_or_else(|e| { + panic!("failed to initialize static route {route:#?}: {e}") + }) } } diff --git a/mgd/src/oxstats.rs b/mgd/src/oxstats.rs index 7f168af3..eb935251 100644 --- a/mgd/src/oxstats.rs +++ b/mgd/src/oxstats.rs @@ -6,9 +6,7 @@ use crate::admin::HandlerContext; use crate::bfd_admin::BfdContext; use crate::bgp_admin::BgpContext; use chrono::{DateTime, Utc}; -use dropshot::{ - ConfigDropshot, ConfigLogging, ConfigLoggingLevel, HandlerTaskMode, -}; +use dropshot::{ConfigLogging, ConfigLoggingLevel}; use mg_common::nexus::{local_underlay_address, resolve_nexus, run_oximeter}; use mg_common::stats::MgLowerStats; use mg_common::{counter, quantity}; @@ -744,14 +742,17 @@ impl Stats { fn rib_stats(&mut self) -> Result, MetricsError> { let mut samples = Vec::new(); - let count = self.db.effective_route_set().len() as u64; + let mut count = 0usize; + for (_prefix, paths) in self.db.full_rib().iter() { + count += paths.len(); + } samples.push(rib_quantity!( self.hostname.clone(), self.rack_id, self.sled_id, self.start_time, ActiveRoutes, - count + count as u64 )); Ok(samples) @@ -780,11 +781,6 @@ pub(crate) fn start_server( ) -> anyhow::Result> { let addr = local_underlay_address()?; let sa = SocketAddr::new(addr, context.oximeter_port); - let dropshot = ConfigDropshot { - bind_address: sa, - request_body_max_bytes: 1024 * 1024 * 1024, - default_handler_task_mode: HandlerTaskMode::Detached, - }; let log_config = LogConfig::Config(ConfigLogging::StderrTerminal { level: ConfigLoggingLevel::Debug, }); @@ -805,7 +801,8 @@ pub(crate) fn start_server( id: registry.producer_id(), kind: ProducerKind::Service, address: sa, - base_route: "/collect".to_string(), + // NOTE: This is now unused and will be removed in the future. + base_route: String::new(), interval: Duration::from_secs(1), }; @@ -813,9 +810,9 @@ pub(crate) fn start_server( let nexus_addr = resolve_nexus(log.clone(), &dns_servers).await; let config = oximeter_producer::Config { server_info: producer_info, - registration_address: nexus_addr, + registration_address: Some(nexus_addr), log: log_config, - dropshot, + request_body_max_bytes: 1024 * 1024 * 1024, }; run_oximeter(registry.clone(), config.clone(), log.clone()).await })) diff --git a/mgd/src/static_admin.rs b/mgd/src/static_admin.rs index 0b6912aa..b07860cf 100644 --- a/mgd/src/static_admin.rs +++ b/mgd/src/static_admin.rs @@ -2,13 +2,12 @@ // 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::admin::HandlerContext; -use bgp::session::DEFAULT_ROUTE_PRIORITY; +use crate::{admin::HandlerContext, register}; use dropshot::{ - endpoint, HttpError, HttpResponseDeleted, HttpResponseOk, + endpoint, ApiDescription, HttpError, HttpResponseDeleted, HttpResponseOk, HttpResponseUpdatedNoContent, RequestContext, TypedBody, }; -use rdb::{Prefix4, Route4ImportKey}; +use rdb::{db::Rib, Path, Prefix4, StaticRouteKey}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::{net::Ipv4Addr, sync::Arc}; @@ -32,30 +31,23 @@ pub struct StaticRoute4List { pub struct StaticRoute4 { pub prefix: Prefix4, pub nexthop: Ipv4Addr, + pub vlan_id: Option, } -impl From for Route4ImportKey { +impl From for StaticRouteKey { fn from(val: StaticRoute4) -> Self { - Route4ImportKey { - prefix: val.prefix, - nexthop: val.nexthop, - // Having an ID of zero indicates this entry did not come from BGP. - // TODO: this could likely be done in a more rust-y way, or just - // have a cleaner data structure organization. - id: 0, - // - priority: DEFAULT_ROUTE_PRIORITY, + StaticRouteKey { + prefix: val.prefix.into(), + nexthop: val.nexthop.into(), + vlan_id: val.vlan_id, } } } -impl From for StaticRoute4 { - fn from(value: Route4ImportKey) -> Self { - Self { - prefix: value.prefix, - nexthop: value.nexthop, - } - } +pub(crate) fn api_description(api: &mut ApiDescription>) { + register!(api, static_add_v4_route); + register!(api, static_remove_v4_route); + register!(api, static_list_v4_routes); } #[endpoint { method = PUT, path = "/static/route4" }] @@ -63,7 +55,7 @@ pub async fn static_add_v4_route( ctx: RequestContext>, request: TypedBody, ) -> Result { - let routes: Vec = request + let routes: Vec = request .into_inner() .routes .list @@ -71,9 +63,10 @@ pub async fn static_add_v4_route( .map(Into::into) .collect(); for r in routes { + let path = Path::for_static(r.nexthop, r.vlan_id); ctx.context() .db - .set_nexthop4(r, true) + .add_prefix_path(r.prefix, path, true) .map_err(|e| HttpError::for_internal_error(e.to_string()))?; } Ok(HttpResponseUpdatedNoContent()) @@ -84,7 +77,7 @@ pub async fn static_remove_v4_route( ctx: RequestContext>, request: TypedBody, ) -> Result { - let routes: Vec = request + let routes: Vec = request .into_inner() .routes .list @@ -92,9 +85,10 @@ pub async fn static_remove_v4_route( .map(Into::into) .collect(); for r in routes { + let path = Path::for_static(r.nexthop, r.vlan_id); ctx.context() .db - .remove_nexthop4(r, true) + .remove_prefix_path(r.prefix, path, true) .map_err(|e| HttpError::for_internal_error(e.to_string()))?; } Ok(HttpResponseDeleted()) @@ -103,14 +97,7 @@ pub async fn static_remove_v4_route( #[endpoint { method = GET, path = "/static/route4" }] pub async fn static_list_v4_routes( ctx: RequestContext>, -) -> Result, HttpError> { - let list = ctx - .context() - .db - .get_imported4() - .into_iter() - .filter(|x| x.id == 0) // indicates not from bgp - .map(Into::into) - .collect(); - Ok(HttpResponseOk(StaticRoute4List { list })) +) -> Result, HttpError> { + let static_rib = ctx.context().db.static_rib(); + Ok(HttpResponseOk(static_rib)) } diff --git a/openapi/mg-admin.json b/openapi/mg-admin.json index eba8632b..9f907ad7 100644 --- a/openapi/mg-admin.json +++ b/openapi/mg-admin.json @@ -90,14 +90,48 @@ } } }, - "/bgp/apply": { - "post": { - "operationId": "bgp_apply", + "/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/ApplyRequest" + "$ref": "#/components/schemas/CheckerSource" } } }, @@ -114,16 +148,14 @@ "$ref": "#/components/responses/Error" } } - } - }, - "/bgp/graceful_shutdown": { + }, "post": { - "operationId": "graceful_shutdown", + "operationId": "update_checker", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GracefulShutdownRequest" + "$ref": "#/components/schemas/CheckerSource" } } }, @@ -140,31 +172,188 @@ "$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/imported4": { + "/bgp/config/neighbor": { "get": { - "operationId": "get_imported4", + "operationId": "read_neighbor", + "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", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GetImported4Request" + "$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", + "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", + "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", + "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_Route4ImportKey", + "title": "Array_of_Neighbor", "type": "array", "items": { - "$ref": "#/components/schemas/Route4ImportKey" + "$ref": "#/components/schemas/Neighbor" } } } @@ -179,26 +368,29 @@ } } }, - "/bgp/message-history": { + "/bgp/config/origin4": { "get": { - "operationId": "message_history", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MessageHistoryRequest" - } + "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 } - }, - "required": true - }, + } + ], "responses": { "200": { "description": "successful operation", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/MessageHistoryResponse" + "$ref": "#/components/schemas/Origin4" } } } @@ -210,16 +402,14 @@ "$ref": "#/components/responses/Error" } } - } - }, - "/bgp/neighbor": { + }, "put": { - "operationId": "ensure_neighbor_handler", + "operationId": "create_origin4", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AddNeighborRequest" + "$ref": "#/components/schemas/Origin4" } } }, @@ -238,12 +428,12 @@ } }, "post": { - "operationId": "add_neighbor_handler", + "operationId": "update_origin4", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AddNeighborRequest" + "$ref": "#/components/schemas/Origin4" } } }, @@ -262,12 +452,75 @@ } }, "delete": { - "operationId": "delete_neighbor", + "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/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/DeleteNeighborRequest" + "$ref": "#/components/schemas/Router" } } }, @@ -275,7 +528,7 @@ }, "responses": { "204": { - "description": "successful deletion" + "description": "resource updated" }, "4XX": { "$ref": "#/components/responses/Error" @@ -284,31 +537,72 @@ "$ref": "#/components/responses/Error" } } - } - }, - "/bgp/originate4": { - "get": { - "operationId": "get_originated4", + }, + "post": { + "operationId": "update_router", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GetOriginated4Request" + "$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_Prefix4", + "title": "Array_of_Router", "type": "array", "items": { - "$ref": "#/components/schemas/Prefix4" + "$ref": "#/components/schemas/Router" } } } @@ -321,14 +615,50 @@ "$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" + } + } }, - "post": { - "operationId": "originate4", + "put": { + "operationId": "create_shaper", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Originate4Request" + "$ref": "#/components/schemas/ShaperSource" } } }, @@ -345,16 +675,14 @@ "$ref": "#/components/responses/Error" } } - } - }, - "/bgp/router": { - "put": { - "operationId": "ensure_router_handler", + }, + "post": { + "operationId": "update_shaper", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/NewRouterRequest" + "$ref": "#/components/schemas/ShaperSource" } } }, @@ -372,13 +700,75 @@ } } }, + "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/message-history": { + "get": { + "operationId": "message_history", + "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": "new_router", + "operationId": "bgp_apply", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/NewRouterRequest" + "$ref": "#/components/schemas/ApplyRequest" } } }, @@ -395,22 +785,31 @@ "$ref": "#/components/responses/Error" } } - }, - "delete": { - "operationId": "delete_router", + } + }, + "/bgp/status/imported": { + "get": { + "operationId": "get_imported", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DeleteRouterRequest" + "$ref": "#/components/schemas/AsnSelector" } } }, "required": true }, "responses": { - "204": { - "description": "resource updated" + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Rib" + } + } + } }, "4XX": { "$ref": "#/components/responses/Error" @@ -421,19 +820,32 @@ } } }, - "/bgp/routers": { + "/bgp/status/neighbors": { "get": { - "operationId": "get_routers", + "operationId": "get_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_RouterInfo", - "type": "array", - "items": { - "$ref": "#/components/schemas/RouterInfo" + "title": "Map_of_PeerInfo", + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/PeerInfo" } } } @@ -448,22 +860,29 @@ } } }, - "/bgp/withdraw4": { - "post": { - "operationId": "withdraw4", + "/bgp/status/selected": { + "get": { + "operationId": "get_selected", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Withdraw4Request" + "$ref": "#/components/schemas/AsnSelector" } } }, "required": true }, "responses": { - "204": { - "description": "resource updated" + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Rib" + } + } + } }, "4XX": { "$ref": "#/components/responses/Error" @@ -483,7 +902,15 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/StaticRoute4List" + "title": "Map_of_Set_of_Path", + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Path" + }, + "uniqueItems": true + } } } } @@ -534,85 +961,20 @@ }, "responses": { "204": { - "description": "successful deletion" - }, - "4XX": { - "$ref": "#/components/responses/Error" - }, - "5XX": { - "$ref": "#/components/responses/Error" - } - } - } - } - }, - "components": { - "schemas": { - "AddNeighborRequest": { - "type": "object", - "properties": { - "asn": { - "type": "integer", - "format": "uint32", - "minimum": 0 - }, - "connect_retry": { - "type": "integer", - "format": "uint64", - "minimum": 0 - }, - "delay_open": { - "type": "integer", - "format": "uint64", - "minimum": 0 - }, - "group": { - "type": "string" - }, - "hold_time": { - "type": "integer", - "format": "uint64", - "minimum": 0 - }, - "host": { - "type": "string" - }, - "idle_hold_time": { - "type": "integer", - "format": "uint64", - "minimum": 0 - }, - "keepalive": { - "type": "integer", - "format": "uint64", - "minimum": 0 - }, - "name": { - "type": "string" + "description": "successful deletion" }, - "passive": { - "type": "boolean" + "4XX": { + "$ref": "#/components/responses/Error" }, - "resolution": { - "type": "integer", - "format": "uint64", - "minimum": 0 + "5XX": { + "$ref": "#/components/responses/Error" } - }, - "required": [ - "asn", - "connect_retry", - "delay_open", - "group", - "hold_time", - "host", - "idle_hold_time", - "keepalive", - "name", - "passive", - "resolution" - ] - }, + } + } + } + }, + "components": { + "schemas": { "AddPathElement": { "description": "The add path element comes as a BGP capability extension as described in RFC 7911.", "type": "object", @@ -663,6 +1025,15 @@ "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. Any active prefixes not in this list will be removed. All prefixes in this list are ensured to be in the originating set.", "type": "array", @@ -679,6 +1050,15 @@ "$ref": "#/components/schemas/BgpPeerConfig" } } + }, + "shaper": { + "nullable": true, + "description": "Checker rhai code to apply to egress open and update messages.", + "allOf": [ + { + "$ref": "#/components/schemas/ShaperSource" + } + ] } }, "required": [ @@ -726,6 +1106,20 @@ } ] }, + "AsnSelector": { + "type": "object", + "properties": { + "asn": { + "description": "ASN of the router to get imported prefixes from.", + "type": "integer", + "format": "uint32", + "minimum": 0 + } + }, + "required": [ + "asn" + ] + }, "BfdPeerConfig": { "type": "object", "properties": { @@ -816,9 +1210,62 @@ } ] }, + "BgpPathProperties": { + "type": "object", + "properties": { + "as_path": { + "type": "array", + "items": { + "type": "integer", + "format": "uint32", + "minimum": 0 + } + }, + "id": { + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "med": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "origin_as": { + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "stale": { + "nullable": true, + "type": "string", + "format": "date-time" + } + }, + "required": [ + "as_path", + "id", + "origin_as" + ] + }, "BgpPeerConfig": { "type": "object", "properties": { + "allow_export": { + "$ref": "#/components/schemas/ImportExportPolicy" + }, + "allow_import": { + "$ref": "#/components/schemas/ImportExportPolicy" + }, + "communities": { + "type": "array", + "items": { + "type": "integer", + "format": "uint32", + "minimum": 0 + } + }, "connect_retry": { "type": "integer", "format": "uint64", @@ -829,6 +1276,9 @@ "format": "uint64", "minimum": 0 }, + "enforce_first_as": { + "type": "boolean" + }, "hold_time": { "type": "integer", "format": "uint64", @@ -847,21 +1297,59 @@ "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": [ + "allow_export", + "allow_import", + "communities", "connect_retry", "delay_open", + "enforce_first_as", "hold_time", "host", "idle_hold_time", @@ -904,7 +1392,7 @@ "additionalProperties": false }, { - "description": "Route refresh capability as defined in RFC 2918. Note this capability is not yet implemented.", + "description": "Route refresh capability as defined in RFC 2918.", "type": "object", "properties": { "route_refresh": { @@ -1080,7 +1568,8 @@ "type": "array", "items": { "$ref": "#/components/schemas/AddPathElement" - } + }, + "uniqueItems": true } }, "required": [ @@ -1292,8 +1781,40 @@ } ] }, + "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 communities recognized by this BGP implementation.", + "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)", @@ -1322,50 +1843,67 @@ "enum": [ "graceful_shutdown" ] + }, + { + "description": "A user defined community", + "type": "object", + "properties": { + "user_defined": { + "type": "integer", + "format": "uint32", + "minimum": 0 + } + }, + "required": [ + "user_defined" + ], + "additionalProperties": false } ] }, - "DeleteNeighborRequest": { + "DeleteStaticRoute4Request": { "type": "object", "properties": { - "addr": { - "type": "string", - "format": "ip" - }, - "asn": { - "type": "integer", - "format": "uint32", - "minimum": 0 + "routes": { + "$ref": "#/components/schemas/StaticRoute4List" } }, "required": [ - "addr", - "asn" + "routes" ] }, - "DeleteRouterRequest": { + "Duration": { "type": "object", "properties": { - "asn": { - "description": "Autonomous system number for the router to remove", + "nanos": { "type": "integer", "format": "uint32", "minimum": 0 + }, + "secs": { + "type": "integer", + "format": "uint64", + "minimum": 0 } }, "required": [ - "asn" + "nanos", + "secs" ] }, - "DeleteStaticRoute4Request": { + "DynamicTimerInfo": { "type": "object", "properties": { - "routes": { - "$ref": "#/components/schemas/StaticRoute4List" + "configured": { + "$ref": "#/components/schemas/Duration" + }, + "negotiated": { + "$ref": "#/components/schemas/Duration" } }, "required": [ - "routes" + "configured", + "negotiated" ] }, "Error": { @@ -1470,9 +2008,7 @@ "type": "object", "properties": { "cease": { - "type": "integer", - "format": "uint8", - "minimum": 0 + "$ref": "#/components/schemas/CeaseErrorSubcode" } }, "required": [ @@ -1536,53 +2072,6 @@ } ] }, - "GetImported4Request": { - "type": "object", - "properties": { - "asn": { - "description": "ASN of the router to get imported prefixes from.", - "type": "integer", - "format": "uint32", - "minimum": 0 - } - }, - "required": [ - "asn" - ] - }, - "GetOriginated4Request": { - "type": "object", - "properties": { - "asn": { - "description": "ASN of the router to get originated prefixes from.", - "type": "integer", - "format": "uint32", - "minimum": 0 - } - }, - "required": [ - "asn" - ] - }, - "GracefulShutdownRequest": { - "type": "object", - "properties": { - "asn": { - "description": "ASN of the router to gracefully shut down.", - "type": "integer", - "format": "uint32", - "minimum": 0 - }, - "enabled": { - "description": "Set whether or not graceful shutdown is initiated from this router.", - "type": "boolean" - } - }, - "required": [ - "asn", - "enabled" - ] - }, "HeaderErrorSubcode": { "description": "Header error subcode types", "type": "string", @@ -1593,6 +2082,32 @@ "bad_message_type" ] }, + "ImportExportPolicy": { + "oneOf": [ + { + "type": "string", + "enum": [ + "NoFiltering" + ] + }, + { + "type": "object", + "properties": { + "Allow": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Prefix" + }, + "uniqueItems": true + } + }, + "required": [ + "Allow" + ], + "additionalProperties": false + } + ] + }, "Message": { "description": "Holds a BGP message. May be an Open, Update, Notification or Keep Alive message.", "oneOf": [ @@ -1656,12 +2171,30 @@ "type": { "type": "string", "enum": [ - "keep_alive" + "keep_alive" + ] + } + }, + "required": [ + "type" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "route_refresh" ] + }, + "value": { + "$ref": "#/components/schemas/RouteRefreshMessage" } }, "required": [ - "type" + "type", + "value" ] } ] @@ -1732,30 +2265,124 @@ "by_peer" ] }, - "NewRouterRequest": { + "Neighbor": { "type": "object", "properties": { + "allow_export": { + "$ref": "#/components/schemas/ImportExportPolicy" + }, + "allow_import": { + "$ref": "#/components/schemas/ImportExportPolicy" + }, "asn": { - "description": "Autonomous system number for this router", "type": "integer", "format": "uint32", "minimum": 0 }, - "id": { - "description": "Id for this router", + "communities": { + "type": "array", + "items": { + "type": "integer", + "format": "uint32", + "minimum": 0 + } + }, + "connect_retry": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "delay_open": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "enforce_first_as": { + "type": "boolean" + }, + "group": { + "type": "string" + }, + "hold_time": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "host": { + "type": "string" + }, + "idle_hold_time": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "keepalive": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "local_pref": { + "nullable": true, "type": "integer", "format": "uint32", "minimum": 0 }, - "listen": { - "description": "Listening address :", + "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": [ + "allow_export", + "allow_import", "asn", - "id", - "listen" + "communities", + "connect_retry", + "delay_open", + "enforce_first_as", + "group", + "hold_time", + "host", + "idle_hold_time", + "keepalive", + "name", + "passive", + "resolution" ] }, "NotificationMessage": { @@ -1898,7 +2525,8 @@ "type": "array", "items": { "$ref": "#/components/schemas/Capability" - } + }, + "uniqueItems": true } }, "required": [ @@ -1938,7 +2566,7 @@ } ] }, - "Originate4Request": { + "Origin4": { "type": "object", "properties": { "asn": { @@ -1960,6 +2588,42 @@ "prefixes" ] }, + "Path": { + "type": "object", + "properties": { + "bgp": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/BgpPathProperties" + } + ] + }, + "local_pref": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "nexthop": { + "type": "string", + "format": "ip" + }, + "shutdown": { + "type": "boolean" + }, + "vlan_id": { + "nullable": true, + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + "required": [ + "nexthop", + "shutdown" + ] + }, "PathAttribute": { "description": "A self-describing BGP path attribute", "type": "object", @@ -2234,14 +2898,61 @@ }, "state": { "$ref": "#/components/schemas/FsmStateKind" + }, + "timers": { + "$ref": "#/components/schemas/PeerTimers" } }, "required": [ "duration_millis", - "state" + "state", + "timers" + ] + }, + "PeerTimers": { + "type": "object", + "properties": { + "hold": { + "$ref": "#/components/schemas/DynamicTimerInfo" + }, + "keepalive": { + "$ref": "#/components/schemas/DynamicTimerInfo" + } + }, + "required": [ + "hold", + "keepalive" ] }, "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 + } + ] + }, + "Prefix2": { "description": "This data structure captures a network prefix as it's laid out in a BGP message. There is a prefix length followed by a variable number of bytes. Just enough bytes to express the prefix.", "type": "object", "properties": { @@ -2282,64 +2993,84 @@ "value" ] }, - "Route4ImportKey": { + "Prefix6": { "type": "object", "properties": { - "id": { - "description": "A BGP route identifier.", + "length": { "type": "integer", - "format": "uint32", + "format": "uint8", "minimum": 0 }, - "nexthop": { - "description": "The nexthop/gateway for the route.", + "value": { "type": "string", - "format": "ipv4" + "format": "ipv6" + } + }, + "required": [ + "length", + "value" + ] + }, + "Rib": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Path" }, - "prefix": { - "description": "The destination prefix of the route.", - "allOf": [ - { - "$ref": "#/components/schemas/Prefix4" - } - ] + "uniqueItems": true + } + }, + "RouteRefreshMessage": { + "type": "object", + "properties": { + "afi": { + "description": "Address family identifier.", + "type": "integer", + "format": "uint16", + "minimum": 0 }, - "priority": { - "description": "Local priority/preference for the route.", + "safi": { + "description": "Subsequent address family identifier.", "type": "integer", - "format": "uint64", + "format": "uint8", "minimum": 0 } }, "required": [ - "id", - "nexthop", - "prefix", - "priority" + "afi", + "safi" ] }, - "RouterInfo": { + "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" }, - "peers": { - "type": "object", - "additionalProperties": { - "$ref": "#/components/schemas/PeerInfo" - } + "id": { + "description": "Id for this router", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "listen": { + "description": "Listening address :", + "type": "string" } }, "required": [ "asn", "graceful_shutdown", - "peers" + "id", + "listen" ] }, "SessionMode": { @@ -2349,6 +3080,23 @@ "MultiHop" ] }, + "ShaperSource": { + "type": "object", + "properties": { + "asn": { + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "code": { + "type": "string" + } + }, + "required": [ + "asn", + "code" + ] + }, "StaticRoute4": { "type": "object", "properties": { @@ -2358,6 +3106,12 @@ }, "prefix": { "$ref": "#/components/schemas/Prefix4" + }, + "vlan_id": { + "nullable": true, + "type": "integer", + "format": "uint16", + "minimum": 0 } }, "required": [ @@ -2404,7 +3158,7 @@ "nlri": { "type": "array", "items": { - "$ref": "#/components/schemas/Prefix" + "$ref": "#/components/schemas/Prefix2" } }, "path_attributes": { @@ -2416,7 +3170,7 @@ "withdrawn": { "type": "array", "items": { - "$ref": "#/components/schemas/Prefix" + "$ref": "#/components/schemas/Prefix2" } } }, @@ -2425,28 +3179,6 @@ "path_attributes", "withdrawn" ] - }, - "Withdraw4Request": { - "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" - ] } }, "responses": { diff --git a/rdb/Cargo.toml b/rdb/Cargo.toml index 1f936189..a41c12ae 100644 --- a/rdb/Cargo.toml +++ b/rdb/Cargo.toml @@ -13,3 +13,5 @@ serde_json.workspace = true thiserror.workspace = true slog.workspace = true mg-common.workspace = true +itertools.workspace = true +chrono.workspace = true diff --git a/rdb/src/bestpath.rs b/rdb/src/bestpath.rs new file mode 100644 index 00000000..ef1d494d --- /dev/null +++ b/rdb/src/bestpath.rs @@ -0,0 +1,193 @@ +// 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 std::collections::BTreeSet; + +use crate::{db::Rib, types::Path, Prefix}; +use itertools::Itertools; + +/// The bestpath algorithms chooses the best set of up to `max` paths for a +/// particular prefix from the RIB. The set of paths chosen will all have +/// equal MED, local_pref, AS path length and shutdown status. The bestpath +/// algorithm performs path filtering in the following ordered sequece of +/// operations. +/// +/// - partition candidate paths into active and shutdown groups. +/// - if only shutdown routes exist, select from that group, otherwise +/// select from the active group. +/// - filter the selection group to the set of paths with the smallest +/// local preference +/// - filter the selection group to the set of paths with the smallest +/// AS path length +/// - filter the selection group to the set of paths with the smallest +/// multi-exit discriminator (MED) on a per-AS basis. +/// +/// Upon completion of these filtering operations, if the selection group +/// is larger than `max`, return the first `max` entries. This is a set, +/// so "first" has no semantic meaning, consider it to be random. If the +/// selection group is smaller than `max`, the entire group is returned. +pub fn bestpaths( + prefix: Prefix, + rib: &Rib, + max: usize, +) -> Option> { + let candidates = match rib.get(&prefix) { + Some(cs) => cs, + None => return None, + }; + + // Partition the choice space on whether routes are shutdown or not. If we + // only have shutdown routes then use those. Otherwise use active routes + let (active, shutdown): (BTreeSet<&Path>, BTreeSet<&Path>) = + candidates.iter().partition(|x| x.shutdown); + let candidates = if active.is_empty() { shutdown } else { active }; + + // Filter down to paths that are not stale. The `min_set_by_key` method + // allows us to assign "not stale" paths to the `0` set, and "stale" paths + // to the `1` set. The method will then return the `0` set. + let candidates = candidates.into_iter().min_set_by_key(|x| match x.bgp { + Some(ref bgp) => match bgp.stale { + Some(_) => 1, + None => 0, + }, + None => 0, + }); + + // Filter down to paths with the highest local preference + let candidates = candidates + .into_iter() + .max_set_by_key(|x| x.local_pref.unwrap_or(0)); + + // Filter down to paths with the shortest length + let candidates = candidates.into_iter().min_set_by_key(|x| match x.bgp { + Some(ref bgp) => bgp.as_path.len(), + None => 0, + }); + + // Group candidates by AS for MED selection + let as_groups = candidates.into_iter().group_by(|path| match path.bgp { + Some(ref bgp) => bgp.origin_as, + None => 0, + }); + // Filter AS groups to paths with lowest MED + let candidates = as_groups.into_iter().flat_map(|(_asn, paths)| { + paths.min_set_by_key(|x| match x.bgp { + Some(ref bgp) => bgp.med.unwrap_or(0), + None => 0, + }) + }); + + // Return up to max elements + Some(candidates.take(max).cloned().collect()) +} + +#[cfg(test)] +mod test { + use std::collections::BTreeSet; + + use super::bestpaths; + use crate::{db::Rib, BgpPathProperties, Path, Prefix, Prefix4}; + + #[test] + fn test_bestpath() { + let mut rib = Rib::default(); + let target: Prefix4 = "198.51.100.0/24".parse().unwrap(); + + // The best path for an empty RIB should be empty + const MAX_ECMP_FANOUT: usize = 2; + let result = bestpaths(target.into(), &rib, MAX_ECMP_FANOUT); + assert!(result.is_none()); + + // Add one path and make sure we get it back + let path1 = Path { + nexthop: "203.0.113.1".parse().unwrap(), + local_pref: Some(100), + shutdown: false, + bgp: Some(BgpPathProperties { + origin_as: 470, + id: 47, + med: Some(75), + stale: None, + as_path: vec![64500, 64501, 64502], + }), + vlan_id: None, + }; + rib.insert(target.into(), BTreeSet::from([path1.clone()])); + + let result = bestpaths(target.into(), &rib, MAX_ECMP_FANOUT).unwrap(); + assert_eq!(result.len(), 1); + assert_eq!(result, BTreeSet::from([path1.clone()])); + + // Add another path to the same prefix and make sure bestpath returns both + let mut path2 = Path { + nexthop: "203.0.113.2".parse().unwrap(), + local_pref: Some(100), + shutdown: false, + bgp: Some(BgpPathProperties { + origin_as: 480, + id: 48, + med: Some(75), + stale: None, + as_path: vec![64500, 64501, 64502], + }), + vlan_id: None, + }; + rib.get_mut(&Prefix::V4(target)) + .unwrap() + .insert(path2.clone()); + let result = bestpaths(target.into(), &rib, MAX_ECMP_FANOUT).unwrap(); + assert_eq!(result.len(), 2); + assert_eq!(result, BTreeSet::from([path1.clone(), path2.clone()])); + + // Add a thrid path and make sure that + // - results are limited to 2 paths when max is 2 + // - we get all three paths back wihen max is 3 + let mut path3 = Path { + nexthop: "203.0.113.3".parse().unwrap(), + local_pref: Some(100), + shutdown: false, + bgp: Some(BgpPathProperties { + origin_as: 490, + id: 49, + med: Some(100), + stale: None, + as_path: vec![64500, 64501, 64502], + }), + vlan_id: None, + }; + rib.get_mut(&Prefix::V4(target)) + .unwrap() + .insert(path3.clone()); + let result = bestpaths(target.into(), &rib, MAX_ECMP_FANOUT).unwrap(); + assert_eq!(result.len(), 2); + // paths 1 and 2 should always be selected since they have the lowest MED + assert_eq!(result, BTreeSet::from([path1.clone(), path2.clone()])); + + // set the med to 75 to get an ecmp group of size 3 + rib.get_mut(&Prefix::V4(target)).unwrap().remove(&path3); + path3.bgp.as_mut().unwrap().med = Some(75); + rib.get_mut(&Prefix::V4(target)) + .unwrap() + .insert(path3.clone()); + let result = + bestpaths(target.into(), &rib, MAX_ECMP_FANOUT + 1).unwrap(); + assert_eq!(result.len(), 3); + assert_eq!( + result, + BTreeSet::from([path1.clone(), path2.clone(), path3.clone()]) + ); + + // bump the local_pref on route 2, this should make it the singular + // best path + rib.get_mut(&Prefix::V4(target)).unwrap().remove(&path2); + path2.local_pref = Some(125); + rib.get_mut(&Prefix::V4(target)) + .unwrap() + .insert(path2.clone()); + let result = + bestpaths(target.into(), &rib, MAX_ECMP_FANOUT + 1).unwrap(); + assert_eq!(result.len(), 1); + assert_eq!(result, BTreeSet::from([path2.clone()])); + } +} diff --git a/rdb/src/db.rs b/rdb/src/db.rs index 1e6eb590..14cead37 100644 --- a/rdb/src/db.rs +++ b/rdb/src/db.rs @@ -9,15 +9,18 @@ //! in a sled key-value store that is persisted to disk via flush operations. //! Volatile information is stored in in-memory data structures such as hash //! sets. +use crate::bestpath::bestpaths; use crate::error::Error; -use crate::{types::*, DEFAULT_ROUTE_PRIORITY}; +use crate::types::*; +use chrono::Utc; use mg_common::{lock, read_lock, write_lock}; -use slog::{error, info, Logger}; -use std::collections::{HashMap, HashSet}; -use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; +use slog::{error, Logger}; +use std::collections::{BTreeMap, BTreeSet}; +use std::net::{IpAddr, Ipv6Addr}; use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::mpsc::Sender; -use std::sync::{Arc, Mutex, MutexGuard, RwLock}; +use std::sync::{Arc, Mutex, RwLock}; +use std::thread::{sleep, spawn}; /// The handle used to open a persistent key-value tree for BGP origin /// information. @@ -45,6 +48,11 @@ const TEP_KEY: &str = "tep"; /// information. const BFD_NEIGHBOR: &str = "bfd_neighbor"; +//TODO as parameter +const BESTPATH_FANOUT: usize = 1; + +pub type Rib = BTreeMap>; + /// The central routing information base. Both persistent an volatile route /// information is managed through this structure. #[derive(Clone)] @@ -52,8 +60,13 @@ pub struct Db { /// A sled database handle where persistent routing information is stored. persistent: sled::Db, - /// Routes imported via dynamic routing protocols. These are volatile. - imported: Arc>>, + /// Routes learned from BGP update messages or administratively added + /// static routes. These are volatile. + rib_in: Arc>, + + /// Routes selected from rib_in according to local policy and added to the + /// lower half forwarding plane. + rib_loc: Arc>, /// A generation number for the overall data store. generation: Arc, @@ -61,6 +74,9 @@ 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 + reaper: Arc, + log: Logger, } unsafe impl Sync for Db {} @@ -69,51 +85,41 @@ unsafe impl Send for Db {} #[derive(Clone)] struct Watcher { tag: String, - sender: Sender, -} - -/// Describes a set of routes as either active or inactive. -#[derive(Debug, Clone)] -pub enum EffectiveRouteSet { - /// The routes in the contained set are active with priority greater than - /// zero. - Active(HashSet), - - /// The routes in the contained set are inactive with a priority equal to - /// zero. - Inactive(HashSet), -} - -impl EffectiveRouteSet { - fn values(&self) -> &HashSet { - match self { - EffectiveRouteSet::Active(s) => s, - EffectiveRouteSet::Inactive(s) => s, - } - } + sender: Sender, } //TODO we need bulk operations with atomic semantics here. impl Db { /// Create a new routing database that stores persistent data at `path`. pub fn new(path: &str, log: Logger) -> Result { + let rib_loc = Arc::new(Mutex::new(Rib::new())); Ok(Self { persistent: sled::open(path)?, - imported: Arc::new(Mutex::new(HashSet::new())), + rib_in: Arc::new(Mutex::new(Rib::new())), + rib_loc: rib_loc.clone(), generation: Arc::new(AtomicU64::new(0)), watchers: Arc::new(RwLock::new(Vec::new())), + reaper: Reaper::new(rib_loc), log, }) } + pub fn set_reaper_interval(&self, interval: std::time::Duration) { + *self.reaper.interval.lock().unwrap() = interval; + } + + pub fn set_reaper_stale_max(&self, stale_max: chrono::Duration) { + *self.reaper.stale_max.lock().unwrap() = stale_max; + } + /// Register a routing databse watcher. - pub fn watch(&self, tag: String, sender: Sender) { + pub fn watch(&self, tag: String, sender: Sender) { write_lock!(self.watchers).push(Watcher { tag, sender }); } - fn notify(&self, c: ChangeSet) { + fn notify(&self, n: PrefixChangeNotification) { for Watcher { tag, sender } in read_lock!(self.watchers).iter() { - if let Err(e) = sender.send(c.clone()) { + if let Err(e) = sender.send(n.clone()) { error!( self.log, "failed to send notification to watcher '{tag}': {e}" @@ -122,14 +128,28 @@ impl Db { } } - // TODO return previous value if this is an update. - pub fn add_origin4(&self, p: Prefix4) -> Result<(), Error> { - let tree = self.persistent.open_tree(BGP_ORIGIN)?; - tree.insert(p.db_key(), "")?; - tree.flush()?; - let g = self.generation.fetch_add(1, Ordering::SeqCst); - self.notify(ChangeSet::from_origin(OriginChangeSet::added([p]), g + 1)); - Ok(()) + pub fn loc_rib(&self) -> Arc> { + self.rib_loc.clone() + } + + pub fn full_rib(&self) -> Rib { + lock!(self.rib_in).clone() + } + + pub fn static_rib(&self) -> Rib { + let mut rib = lock!(self.rib_in).clone(); + for (_prefix, paths) in rib.iter_mut() { + paths.retain(|x| x.bgp.is_none()) + } + rib + } + + pub fn bgp_rib(&self) -> Rib { + let mut rib = lock!(self.rib_in).clone(); + for (_prefix, paths) in rib.iter_mut() { + paths.retain(|x| x.bgp.is_some()) + } + rib } pub fn add_bgp_router( @@ -155,7 +175,7 @@ impl Db { pub fn get_bgp_routers( &self, - ) -> Result, Error> { + ) -> Result, Error> { let tree = self.persistent.open_tree(BGP_ROUTER)?; let result = tree .scan_prefix(vec![]) @@ -293,18 +313,33 @@ impl Db { Ok(result) } - pub fn remove_origin4(&self, p: Prefix4) -> Result<(), Error> { + pub fn create_origin4(&self, ps: &[Prefix4]) -> Result<(), Error> { + let current = self.get_origin4()?; + if !current.is_empty() { + return Err(Error::Conflict("origin already exists".to_string())); + } + + self.set_origin4(ps) + } + + pub fn set_origin4(&self, ps: &[Prefix4]) -> Result<(), Error> { + let tree = self.persistent.open_tree(BGP_ORIGIN)?; + tree.clear()?; + for p in ps.iter() { + tree.insert(p.db_key(), "")?; + } + tree.flush()?; + Ok(()) + } + + pub fn clear_origin4(&self) -> Result<(), Error> { let tree = self.persistent.open_tree(BGP_ORIGIN)?; - tree.remove(p.db_key())?; - let g = self.generation.fetch_add(1, Ordering::SeqCst); - self.notify(ChangeSet::from_origin( - OriginChangeSet::removed([p]), - g + 1, - )); + tree.clear()?; + tree.flush()?; Ok(()) } - pub fn get_originated4(&self) -> Result, Error> { + pub fn get_origin4(&self) -> Result, Error> { let tree = self.persistent.open_tree(BGP_ORIGIN)?; let result = tree .scan_prefix(vec![]) @@ -334,53 +369,65 @@ impl Db { Ok(result) } - pub fn get_nexthop4(&self, prefix: &Prefix4) -> Vec { - lock!(self.imported) - .iter() - .filter(|x| prefix == &x.prefix) - .cloned() - .collect() + pub fn get_prefix_paths(&self, prefix: &Prefix) -> Vec { + let rib = lock!(self.rib_in); + let paths = rib.get(prefix); + match paths { + None => Vec::new(), + Some(p) => p.iter().cloned().collect(), + } } - pub fn get_imported4(&self) -> Vec { - lock!(self.imported).clone().into_iter().collect() + pub fn get_imported(&self) -> Rib { + lock!(self.rib_in).clone() } - pub fn set_nexthop4( + pub fn update_loc_rib(rib_in: &Rib, rib_loc: &mut Rib, prefix: Prefix) { + let bp = bestpaths(prefix, rib_in, BESTPATH_FANOUT); + match bp { + Some(bp) => { + rib_loc.insert(prefix, bp.clone()); + } + None => { + rib_loc.remove(&prefix); + } + } + } + + pub fn add_prefix_path( &self, - r: Route4ImportKey, + prefix: Prefix, + path: Path, is_static: bool, ) -> Result<(), Error> { + let mut rib = lock!(self.rib_in); + match rib.get_mut(&prefix) { + Some(paths) => { + paths.replace(path.clone()); + } + None => { + rib.insert(prefix, BTreeSet::from([path.clone()])); + } + } + Self::update_loc_rib(&rib, &mut lock!(self.rib_loc), prefix); + if is_static { let tree = self.persistent.open_tree(STATIC4_ROUTES)?; - let key = serde_json::to_string(&r)?; + let srk = StaticRouteKey { + prefix, + nexthop: path.nexthop, + vlan_id: path.vlan_id, + }; + let key = serde_json::to_string(&srk)?; tree.insert(key.as_str(), "")?; tree.flush()?; } - let mut imported = lock!(self.imported); - let before = Self::effective_set_for_prefix4(&imported, r.prefix); - imported.replace(r); - let after = Self::effective_set_for_prefix4(&imported, r.prefix); - - if let Some(change_set) = self.import_route_change_set(&before, &after) - { - info!( - self.log, - "sending notification for change set {:#?}", change_set, - ); - self.notify(change_set); - } else { - info!( - self.log, - "no effective change for {:#?} -> {:#?}", before, after - ); - } - + self.notify(prefix.into()); Ok(()) } - pub fn get_static4(&self) -> Result, Error> { + pub fn get_static4(&self) -> Result, Error> { let tree = self.persistent.open_tree(STATIC4_ROUTES)?; Ok(tree .scan_prefix(vec![]) @@ -397,7 +444,7 @@ impl Db { }; let key = String::from_utf8_lossy(&key); - let rkey: Route4ImportKey = match serde_json::from_str(&key) { + let rkey: StaticRouteKey = match serde_json::from_str(&key) { Ok(item) => item, Err(e) => { error!( @@ -419,241 +466,124 @@ impl Db { pub fn get_static_nexthop4_count(&self) -> Result { let entries = self.get_static4()?; - let mut nexthops = HashSet::new(); + let mut nexthops = BTreeSet::new(); for e in entries { nexthops.insert(e.nexthop); } Ok(nexthops.len()) } - pub fn disable_nexthop4(&self, addr: Ipv4Addr) { - let mut imported = lock!(self.imported); - let changed: Vec = imported - .iter() - .cloned() - .filter(|x| x.nexthop == addr && x.priority != 0) - .map(|x| x.with_priority(0)) - .collect(); - - for x in changed { - let before = Self::effective_set_for_prefix4(&imported, x.prefix); - imported.replace(x); - let after = Self::effective_set_for_prefix4(&imported, x.prefix); - if let Some(change_set) = - self.import_route_change_set(&before, &after) - { - self.notify(change_set); + pub fn disable_nexthop(&self, nexthop: IpAddr) { + let mut rib = lock!(self.rib_in); + let mut pcn = PrefixChangeNotification::default(); + for (prefix, paths) in rib.iter_mut() { + for p in paths.clone().into_iter() { + if p.nexthop == nexthop && !p.shutdown { + let mut replacement = p.clone(); + replacement.shutdown = true; + paths.insert(replacement); + pcn.changed.insert(*prefix); + } } } - } - pub fn enable_nexthop4(&self, addr: Ipv4Addr) { - let mut imported = lock!(self.imported); - let changed: Vec = imported - .iter() - .cloned() - .filter(|x| { - x.nexthop == addr && x.priority != DEFAULT_ROUTE_PRIORITY - }) - .map(|x| x.with_priority(DEFAULT_ROUTE_PRIORITY)) - .collect(); + for prefix in pcn.changed.iter() { + Self::update_loc_rib(&rib, &mut lock!(self.rib_loc), *prefix); + } + + self.notify(pcn); + } - for x in changed { - let before = Self::effective_set_for_prefix4(&imported, x.prefix); - imported.replace(x); - let after = Self::effective_set_for_prefix4(&imported, x.prefix); - if let Some(change_set) = - self.import_route_change_set(&before, &after) - { - self.notify(change_set); + pub fn enable_nexthop(&self, nexthop: IpAddr) { + let mut rib = lock!(self.rib_in); + let mut pcn = PrefixChangeNotification::default(); + for (prefix, paths) in rib.iter_mut() { + for p in paths.clone().into_iter() { + if p.nexthop == nexthop && p.shutdown { + let mut replacement = p.clone(); + replacement.shutdown = false; + paths.insert(replacement); + pcn.changed.insert(*prefix); + } } } + + //TODO loc_rib updater as a pcn listener? + for prefix in pcn.changed.iter() { + Self::update_loc_rib(&rib, &mut lock!(self.rib_loc), *prefix); + } + self.notify(pcn); } - pub fn remove_nexthop4( + pub fn remove_prefix_path( &self, - r: Route4ImportKey, - is_static: bool, + prefix: Prefix, + path: Path, + is_static: bool, //TODO ) -> Result<(), Error> { + let mut rib = lock!(self.rib_in); + if let Some(paths) = rib.get_mut(&prefix) { + paths.retain(|x| x.nexthop != path.nexthop) + } + if is_static { let tree = self.persistent.open_tree(STATIC4_ROUTES)?; - let key = serde_json::to_string(&r)?; + let srk = StaticRouteKey { + prefix, + nexthop: path.nexthop, + vlan_id: path.vlan_id, + }; + let key = serde_json::to_string(&srk)?; tree.remove(key.as_str())?; tree.flush()?; } - let mut imported = lock!(self.imported); - let before = Self::effective_set_for_prefix4(&imported, r.prefix); - imported.remove(&r); - let after = Self::effective_set_for_prefix4(&imported, r.prefix); - - if let Some(change_set) = self.import_route_change_set(&before, &after) - { - self.notify(change_set); - } + Self::update_loc_rib(&rib, &mut lock!(self.rib_loc), prefix); + self.notify(prefix.into()); Ok(()) } - pub fn remove_peer_prefix4(&self, id: u32, prefix: Prefix4) { - let mut imported = lock!(self.imported); - imported.retain(|x| !(x.id == id && x.prefix == prefix)); - } - - pub fn remove_peer_prefixes4(&self, id: u32) -> Vec { - let mut imported = lock!(self.imported); - //TODO do in one pass instead of two - let result = imported.iter().filter(|x| x.id == id).copied().collect(); - imported.retain(|x| x.id != id); - result - } - - pub fn generation(&self) -> u64 { - self.generation.load(Ordering::SeqCst) - } - - /// Given a target prefix, compute the effective route set for that prefix. - /// This is needed to support graceful shutdown. Routes being shutdown are - /// always a last resort - so there are three cases. - /// - /// 1. Only shutdown routes exist, in which case the effective set is all - /// the shutdown routes. - /// 2. Only active routes (routes not being shut down) exist, in which - /// case the effective set is all the active routes. - /// 3. A mixture of shutdown routes and active routes exist, in which - /// case the effective set is only the active routes. - /// - fn effective_set_for_prefix4( - imported: &MutexGuard>, - prefix: Prefix4, - ) -> EffectiveRouteSet { - let full: HashSet = imported - .iter() - .filter(|x| x.prefix == prefix) - .copied() - .collect(); - - let shutdown: HashSet = - full.iter().filter(|x| x.priority == 0).copied().collect(); - - let active: HashSet = - full.iter().filter(|x| x.priority > 0).copied().collect(); + pub fn remove_peer_prefix(&self, id: u32, prefix: Prefix) { + let mut rib = lock!(self.rib_in); + let paths = match rib.get_mut(&prefix) { + None => return, + Some(ps) => ps, + }; + paths.retain(|x| match x.bgp { + Some(ref bgp) => bgp.id != id, + None => true, + }); - match (active.len(), shutdown.len()) { - (0, _) => EffectiveRouteSet::Inactive(shutdown), - (_, 0) => EffectiveRouteSet::Active(active), - _ => EffectiveRouteSet::Active(active), - } + Self::update_loc_rib(&rib, &mut lock!(self.rib_loc), prefix); + self.notify(prefix.into()); } - pub fn effective_route_set(&self) -> Vec { - let full = lock!(self.imported).clone(); - let mut sets = HashMap::::new(); - for x in full.iter() { - match sets.get_mut(&x.prefix) { - Some(set) => { - if x.priority > 0 { - match set { - EffectiveRouteSet::Active(s) => { - s.insert(*x); - } - EffectiveRouteSet::Inactive(_) => { - let mut value = HashSet::new(); - value.insert(*x); - sets.insert( - x.prefix, - EffectiveRouteSet::Active(value), - ); - } - } - } else { - match set { - EffectiveRouteSet::Active(_) => { - //Nothing to do here, the active set takes priority - } - EffectiveRouteSet::Inactive(s) => { - s.insert(*x); - } - } - } - } - None => { - let mut value = HashSet::new(); - value.insert(*x); - if x.priority > 0 { - sets.insert(x.prefix, EffectiveRouteSet::Active(value)); - } else { - sets.insert( - x.prefix, - EffectiveRouteSet::Inactive(value), - ); - } - } - }; + pub fn remove_peer_prefixes( + &self, + id: u32, + ) -> BTreeMap> { + let mut rib = lock!(self.rib_in); + + let mut pcn = PrefixChangeNotification::default(); + let mut result = BTreeMap::new(); + for (prefix, paths) in rib.iter_mut() { + paths.retain(|x| match x.bgp { + Some(ref bgp) => bgp.id != id, + None => true, + }); + result.insert(*prefix, paths.clone()); + pcn.changed.insert(*prefix); } - let mut result = Vec::new(); - for xs in sets.values() { - for x in xs.values() { - result.push(*x); - } + for prefix in pcn.changed.iter() { + Self::update_loc_rib(&rib, &mut lock!(self.rib_loc), *prefix); } + self.notify(pcn); result } - /// Compute a a change set for a before/after set of routes including - /// bumping the RIB generation number if there are changes. - fn import_route_change_set( - &self, - before: &EffectiveRouteSet, - after: &EffectiveRouteSet, - ) -> Option { - let gen = self.generation.fetch_add(1, Ordering::SeqCst); - match (before, after) { - ( - EffectiveRouteSet::Active(before), - EffectiveRouteSet::Active(after), - ) => { - let added: HashSet = - after.difference(before).copied().collect(); - - let removed: HashSet = - before.difference(after).copied().collect(); - - if added.is_empty() && removed.is_empty() { - return None; - } - - Some(ChangeSet::from_import( - ImportChangeSet { added, removed }, - gen, - )) - } - ( - EffectiveRouteSet::Active(before), - EffectiveRouteSet::Inactive(_after), - ) => Some(ChangeSet::from_import( - ImportChangeSet { - removed: before.clone(), - ..Default::default() - }, - gen, - )), - ( - EffectiveRouteSet::Inactive(_before), - EffectiveRouteSet::Active(after), - ) => Some(ChangeSet::from_import( - ImportChangeSet { - added: after.clone(), - ..Default::default() - }, - gen, - )), - - ( - EffectiveRouteSet::Inactive(_before), - EffectiveRouteSet::Inactive(_after), - ) => None, - } + pub fn generation(&self) -> u64 { + self.generation.load(Ordering::SeqCst) } pub fn get_tep_addr(&self) -> Result, Error> { @@ -680,4 +610,74 @@ impl Db { tree.flush()?; Ok(()) } + + pub fn mark_bgp_id_stale(&self, id: u32) { + let mut rib = self.rib_loc.lock().unwrap(); + rib.iter_mut().for_each(|(_prefix, path)| { + let targets: Vec = path + .iter() + .filter_map(|p| { + if let Some(bgp) = p.bgp.as_ref() { + if bgp.id == id { + let mut marked = p.clone(); + marked.bgp = Some(bgp.as_stale()); + return Some(marked); + } + } + None + }) + .collect(); + for t in targets.into_iter() { + path.replace(t); + } + }); + } +} + +struct Reaper { + interval: Mutex, + stale_max: Mutex, + rib: Arc>, +} + +impl Reaper { + fn new(rib: Arc>) -> Arc { + let reaper = Arc::new(Self { + interval: Mutex::new(std::time::Duration::from_millis(100)), + stale_max: Mutex::new(chrono::Duration::new(1, 0).unwrap()), + rib, + }); + reaper.run(); + reaper + } + + fn run(self: &Arc) { + let s = self.clone(); + spawn(move || loop { + s.reap(); + sleep(*s.interval.lock().unwrap()); + }); + } + + fn reap(self: &Arc) { + self.rib + .lock() + .unwrap() + .iter_mut() + .for_each(|(_prefix, paths)| { + paths.retain(|p| { + p.bgp + .as_ref() + .map(|b| { + b.stale + .map(|s| { + Utc::now().signed_duration_since(s) + < *self.stale_max.lock().unwrap() + }) + .unwrap_or(true) + }) + .unwrap_or(true) + }) + }); + } } diff --git a/rdb/src/error.rs b/rdb/src/error.rs index 4e28aba0..666811a5 100644 --- a/rdb/src/error.rs +++ b/rdb/src/error.rs @@ -15,4 +15,7 @@ pub enum Error { #[error("db value error {0}")] DbValue(String), + + #[error("Conflict {0}")] + Conflict(String), } diff --git a/rdb/src/lib.rs b/rdb/src/lib.rs index e51a4d14..4462bf40 100644 --- a/rdb/src/lib.rs +++ b/rdb/src/lib.rs @@ -7,7 +7,8 @@ pub mod types; pub use db::Db; pub use types::*; +pub mod bestpath; pub mod error; /// The priority routes default to. -pub const DEFAULT_ROUTE_PRIORITY: u64 = 100; +pub const DEFAULT_ROUTE_PRIORITY: u64 = u64::MAX; diff --git a/rdb/src/types.rs b/rdb/src/types.rs index 12d887a7..6842b361 100644 --- a/rdb/src/types.rs +++ b/rdb/src/types.rs @@ -2,56 +2,109 @@ // 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::error::Error; use anyhow::Result; +use chrono::{DateTime, Utc}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use std::collections::HashSet; -use std::fmt; -use std::hash::{Hash, Hasher}; +use std::cmp::Ordering; +use std::collections::{BTreeSet, HashSet}; +use std::fmt::{self, Formatter}; +use std::hash::Hash; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; use std::str::FromStr; -use crate::error::Error; +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Eq, PartialEq)] +pub struct Path { + pub nexthop: IpAddr, + pub shutdown: bool, + pub local_pref: Option, + pub bgp: Option, + pub vlan_id: Option, +} -#[derive(Copy, Clone, Eq, Serialize, Deserialize, JsonSchema, Debug)] -pub struct Route4ImportKey { - /// The destination prefix of the route. - pub prefix: Prefix4, +// Define a basic ordering on paths so bestpath selection is deterministic +impl PartialOrd for Path { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} +impl Ord for Path { + fn cmp(&self, other: &Self) -> Ordering { + if self.nexthop != other.nexthop { + return self.nexthop.cmp(&other.nexthop); + } + if self.shutdown != other.shutdown { + return self.shutdown.cmp(&other.shutdown); + } + if self.local_pref != other.local_pref { + return self.local_pref.cmp(&other.local_pref); + } + self.bgp.cmp(&other.bgp) + } +} - /// The nexthop/gateway for the route. - pub nexthop: Ipv4Addr, +impl Path { + pub fn for_static(nexthop: IpAddr, vlan_id: Option) -> Self { + Self { + nexthop, + vlan_id, + shutdown: false, + local_pref: None, + bgp: None, + } + } +} - /// A BGP route identifier. +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Eq, PartialEq)] +pub struct BgpPathProperties { + pub origin_as: u32, pub id: u32, - - /// Local priority/preference for the route. - pub priority: u64, + pub med: Option, + pub as_path: Vec, + pub stale: Option>, } -impl Route4ImportKey { - pub fn with_priority(&self, priority: u64) -> Self { - let mut x = *self; - x.priority = priority; - x +impl BgpPathProperties { + pub fn as_stale(&self) -> Self { + let mut s = self.clone(); + s.stale = Some(Utc::now()); + s } } -impl Hash for Route4ImportKey { - fn hash(&self, state: &mut H) { - self.prefix.hash(state); - self.nexthop.hash(state); +impl PartialOrd for BgpPathProperties { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) } } - -impl PartialEq for Route4ImportKey { - fn eq(&self, other: &Self) -> bool { - self.prefix == other.prefix && self.nexthop == other.nexthop +impl Ord for BgpPathProperties { + fn cmp(&self, other: &Self) -> Ordering { + if self.origin_as != other.origin_as { + return self.origin_as.cmp(&other.origin_as); + } + if self.id != other.id { + return self.id.cmp(&other.id); + } + // MED should *not* be used as a basis for comparison. Paths with + // distinct MED values are not distinct paths. + if self.as_path != other.as_path { + return self.as_path.cmp(&other.as_path); + } + self.stale.cmp(&other.stale) } } #[derive( - Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, JsonSchema, + Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Debug, )] +pub struct StaticRouteKey { + pub prefix: Prefix, + pub nexthop: IpAddr, + pub vlan_id: Option, +} + +#[derive(Copy, Clone, Eq, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct Route4Key { pub prefix: Prefix4, pub nexthop: Ipv4Addr, @@ -121,13 +174,27 @@ impl Policy4Key { } #[derive( - Debug, Copy, Clone, Serialize, Deserialize, Hash, Eq, PartialEq, JsonSchema, + Debug, Copy, Clone, Serialize, Deserialize, Eq, PartialEq, JsonSchema, )] pub struct Prefix4 { pub value: Ipv4Addr, pub length: u8, } +impl PartialOrd for Prefix4 { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} +impl Ord for Prefix4 { + fn cmp(&self, other: &Self) -> Ordering { + if self.value != other.value { + return self.value.cmp(&other.value); + } + self.length.cmp(&other.length) + } +} + impl Prefix4 { pub fn db_key(&self) -> Vec { let mut buf: Vec = self.value.octets().into(); @@ -174,18 +241,72 @@ impl FromStr for Prefix4 { } } -#[derive(Serialize, Deserialize)] +#[derive( + Debug, Copy, Clone, Serialize, Deserialize, Hash, Eq, PartialEq, JsonSchema, +)] pub struct Prefix6 { pub value: Ipv6Addr, pub length: u8, } +impl PartialOrd for Prefix6 { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} +impl Ord for Prefix6 { + fn cmp(&self, other: &Self) -> Ordering { + if self.value != other.value { + return self.value.cmp(&other.value); + } + self.length.cmp(&other.length) + } +} + impl fmt::Display for Prefix6 { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}/{}", self.value, self.length) } } +#[derive( + Debug, + Copy, + Clone, + Serialize, + Deserialize, + Eq, + PartialEq, + JsonSchema, + PartialOrd, + Ord, +)] +pub enum Prefix { + V4(Prefix4), + V6(Prefix6), +} + +impl std::fmt::Display for Prefix { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { + match self { + Prefix::V4(p) => p.fmt(f), + Prefix::V6(p) => p.fmt(f), + } + } +} + +impl From for Prefix { + fn from(value: Prefix4) -> Self { + Self::V4(value) + } +} + +impl From for Prefix { + fn from(value: Prefix6) -> Self { + Self::V6(value) + } +} + #[derive(Serialize, Deserialize)] pub struct BgpAttributes4 { pub origin: Ipv4Addr, @@ -225,6 +346,15 @@ impl From for Asn { } } +impl Asn { + pub fn as_u32(&self) -> u32 { + match self { + Self::TwoOctet(value) => u32::from(*value), + Self::FourOctet(value) => *value, + } + } +} + #[derive(Serialize, Deserialize)] pub enum Status { Up, @@ -261,27 +391,6 @@ pub struct Policy { pub priority: u16, } -#[derive(Clone, Default, Debug)] -pub struct ImportChangeSet { - pub added: HashSet, - pub removed: HashSet, -} - -impl ImportChangeSet { - pub fn added>>(v: V) -> Self { - Self { - added: v.into(), - ..Default::default() - } - } - pub fn removed>>(v: V) -> Self { - Self { - removed: v.into(), - ..Default::default() - } - } -} - #[derive(Clone, Default, Debug)] pub struct OriginChangeSet { pub added: HashSet, @@ -303,35 +412,20 @@ impl OriginChangeSet { } } -#[derive(Clone, Default, Debug)] -pub struct ChangeSet { - pub generation: u64, - pub import: ImportChangeSet, - pub origin: OriginChangeSet, -} - -impl ChangeSet { - pub fn from_origin(origin: OriginChangeSet, generation: u64) -> Self { - Self { - generation, - origin, - ..Default::default() - } - } - - pub fn from_import(import: ImportChangeSet, generation: u64) -> Self { - Self { - generation, - import, - ..Default::default() - } - } -} - #[derive(Debug, Serialize, Deserialize, Clone, JsonSchema)] pub struct BgpRouterInfo { pub id: u32, pub listen: String, + pub graceful_shutdown: bool, +} + +#[derive( + Default, Debug, Serialize, Deserialize, Clone, JsonSchema, Eq, PartialEq, +)] +pub enum ImportExportPolicy { + #[default] + NoFiltering, + Allow(BTreeSet), } #[derive(Debug, Serialize, Deserialize, Clone, JsonSchema)] @@ -347,6 +441,16 @@ pub struct BgpNeighborInfo { 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, + pub allow_import: ImportExportPolicy, + pub allow_export: ImportExportPolicy, + pub vlan_id: Option, } #[derive(Debug, Copy, Clone, Deserialize, Serialize, JsonSchema)] @@ -368,3 +472,32 @@ pub enum SessionMode { SingleHop, MultiHop, } + +#[derive(Clone, Default, Debug)] +pub struct PrefixChangeNotification { + pub changed: BTreeSet, +} + +impl From for PrefixChangeNotification { + fn from(value: Prefix) -> Self { + Self { + changed: BTreeSet::from([value]), + } + } +} + +impl From for PrefixChangeNotification { + fn from(value: Prefix4) -> Self { + Self { + changed: BTreeSet::from([value.into()]), + } + } +} + +impl From for PrefixChangeNotification { + fn from(value: Prefix6) -> Self { + Self { + changed: BTreeSet::from([value.into()]), + } + } +}