From 258d2be18a97a77049637f010251c233365b1727 Mon Sep 17 00:00:00 2001 From: Zeeshan Lakhani Date: Fri, 6 Mar 2026 00:36:08 +0000 Subject: [PATCH 1/5] remove redundant multicast-test CI job; multicast is now a default feature --- .github/buildomat/jobs/multicast-test.sh | 33 ------------------------ 1 file changed, 33 deletions(-) delete mode 100755 .github/buildomat/jobs/multicast-test.sh diff --git a/.github/buildomat/jobs/multicast-test.sh b/.github/buildomat/jobs/multicast-test.sh deleted file mode 100755 index ba0abcf1..00000000 --- a/.github/buildomat/jobs/multicast-test.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/bin/bash -#: -#: name = "multicast-test" -#: variety = "basic" -#: target = "ubuntu-22.04" -#: rust_toolchain = true -#: -#: output_rules = [ -#: "/work/simulator.log", -#: "/work/dpd.log", -#: ] -#: - -#### >>>>>>>>>>>>>>>>>>>>>>>>>>>> Local Usage >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> -#### -#### The following environment variables are useful. -#### -#### - JUST_TEST=1 Just runs the tests, skipping system prep. -#### - TESTNAME='$name' Will just run the specified test. -#### - STARTUP_TIMEOUT=n Seconds to wait for tofino-model/dpd to start. -#### Defaults to 15. -#### - NOBUILD=1 Don't build sidecar.p4 (in case you've already -#### built it) -#### -#### <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< - -export RUST_BACKTRACE=1 - -set -o errexit -set -o pipefail -set -o xtrace - -source .github/buildomat/packet-test-common.sh From e10e4f5a993fe950ab1b478abb5dcbfa7aa92091 Mon Sep 17 00:00:00 2001 From: Zeeshan Lakhani Date: Tue, 28 Apr 2026 11:46:49 +0900 Subject: [PATCH 2/5] [multicast] wire up softnpu backend to multicast table ops + (#242) This wires up the softnpu ASIC backend to support multicast end-to-end by translating DPD's sidecar.p4 table operations into sidecar-lite.p4's simplified P4 pipeline. ## AsicMulticastOps We replace the stubbed AsicMulticastOps implementation (which returned "OperationUnsupported" for group creation and port addition with in-memory group tracking via McGroupData, following the tofino_stub pattern. Group membership is used by the table translation layer to build port bitmaps for sidecar-lite's Replicate extern. Ports >= 128 are rejected at add time to match sidecar-lite's 128-bit bitmap width. ## Table translation (asic/src/softnpu/table.rs) We map sidecar.p4 table names to sidecar-lite equivalents and translate action parameters where the designs differ for emulation. ## Fix update semantics We fix an issue with stale in-memory sources after update with absent source list(s). `modify_group_external` wrote back `new.sources.or(existing)` to the in-memory record while `update_external_tables` in the same call canonicalized `new.sources` directly to rewrite the P4 source-filter entries. A `None` update therefore cleared the P4 table but left a stale `Some([...])` in memory. All multicast action arms are gated with #[cfg(feature = "multicast")]. ## References and Deps - [softnpu #183](https://github.com/oxidecomputer/softnpu/pull/183) - [propolis #1093](https://github.com/oxidecomputer/propolis/pull/1093) - [p4rs #240](https://github.com/oxidecomputer/p4/pull/240) - [sidecar-lite #152](https://github.com/oxidecomputer/sidecar-lite/pull/152) - tokio: 1.50 (due to softnpu) - oxide-tokio-rt: 0.1.3 (following-up from tokio's move to 1.50) - bump oxide-tokio-rt to 0.1.4, tokio to 1.52.1 (matching Omicron) --- Cargo.lock | 1510 ++++++++++++++++--- Cargo.toml | 10 +- asic/src/softnpu/mcast.rs | 302 ++++ asic/src/softnpu/mod.rs | 67 +- asic/src/softnpu/table.rs | 565 +++++-- common/src/illumos.rs | 2 +- dpd-client/tests/integration_tests/mcast.rs | 94 ++ dpd/src/mcast/mod.rs | 12 +- packet/src/eth.rs | 2 +- swadm/src/main.rs | 2 +- 10 files changed, 2203 insertions(+), 363 deletions(-) create mode 100644 asic/src/softnpu/mcast.rs diff --git a/Cargo.lock b/Cargo.lock index 196f7b15..2909bec6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -133,6 +133,17 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "api_identity" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?rev=becbbb616f5f18b59cc42e511c148734c2ba3831#becbbb616f5f18b59cc42e511c148734c2ba3831" +dependencies = [ + "omicron-workspace-hack", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "argon2" version = "0.5.3" @@ -141,7 +152,7 @@ checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" dependencies = [ "base64ct", "blake2", - "cpufeatures", + "cpufeatures 0.2.17", "password-hash", ] @@ -187,7 +198,7 @@ dependencies = [ "slog", "slog-async", "slog-term", - "softnpu 0.2.0 (git+https://github.com/oxidecomputer/softnpu?branch=main)", + "softnpu", "strum 0.27.2", "thiserror 1.0.69", "tofino 0.1.0 (git+https://github.com/oxidecomputer/tofino?branch=main)", @@ -257,6 +268,25 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "attest-data" +version = "0.5.0" +source = "git+https://github.com/oxidecomputer/dice-util?rev=1d3084b514389847e8e0f5d966d2be4f18d02d32#1d3084b514389847e8e0f5d966d2be4f18d02d32" +dependencies = [ + "const-oid", + "der", + "getrandom 0.3.4", + "hex", + "hubpack", + "rats-corim", + "salty", + "serde", + "serde_with", + "sha3", + "static_assertions", + "thiserror 2.0.18", +] + [[package]] name = "autocfg" version = "1.5.0" @@ -381,6 +411,16 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "bhyve_api" +version = "0.0.0" +source = "git+https://github.com/oxidecomputer/propolis?branch=zl%2Fmulticast#784de5e4b123b48e574bbffc1188c0d5c30b2e62" +dependencies = [ + "bhyve_api_sys 0.0.0 (git+https://github.com/oxidecomputer/propolis?branch=zl%2Fmulticast)", + "libc", + "strum 0.26.3", +] + [[package]] name = "bhyve_api" version = "0.0.0" @@ -394,9 +434,18 @@ dependencies = [ [[package]] name = "bhyve_api" version = "0.0.0" -source = "git+https://github.com/oxidecomputer/propolis#827e6615bfebfd94d41504dcd1517a0f22e3166a" +source = "git+https://github.com/oxidecomputer/propolis?rev=8ccddb47a4c93b7e3480919495dae851afc83782#8ccddb47a4c93b7e3480919495dae851afc83782" +dependencies = [ + "bhyve_api_sys 0.0.0 (git+https://github.com/oxidecomputer/propolis?rev=8ccddb47a4c93b7e3480919495dae851afc83782)", + "libc", + "strum 0.26.3", +] + +[[package]] +name = "bhyve_api_sys" +version = "0.0.0" +source = "git+https://github.com/oxidecomputer/propolis?branch=zl%2Fmulticast#784de5e4b123b48e574bbffc1188c0d5c30b2e62" dependencies = [ - "bhyve_api_sys 0.0.0 (git+https://github.com/oxidecomputer/propolis)", "libc", "strum 0.26.3", ] @@ -413,7 +462,7 @@ dependencies = [ [[package]] name = "bhyve_api_sys" version = "0.0.0" -source = "git+https://github.com/oxidecomputer/propolis#827e6615bfebfd94d41504dcd1517a0f22e3166a" +source = "git+https://github.com/oxidecomputer/propolis?rev=8ccddb47a4c93b7e3480919495dae851afc83782#8ccddb47a4c93b7e3480919495dae851afc83782" dependencies = [ "libc", "strum 0.26.3", @@ -454,6 +503,12 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" +[[package]] +name = "bit_field" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" + [[package]] name = "bitflags" version = "1.3.2" @@ -546,15 +601,44 @@ dependencies = [ "derive_more", "hex", "hkdf", - "omicron-common", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-workspace-hack", + "rand 0.8.5", + "secrecy", + "serde", + "serde_with", + "sha3", + "sled-hardware-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "slog", + "thiserror 2.0.18", + "tokio", + "uuid", + "vsss-rs", + "zeroize", +] + +[[package]] +name = "bootstore" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?rev=becbbb616f5f18b59cc42e511c148734c2ba3831#becbbb616f5f18b59cc42e511c148734c2ba3831" +dependencies = [ + "bytes", + "camino", + "chacha20poly1305", + "ciborium", + "derive_more", + "hex", + "hkdf", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?rev=becbbb616f5f18b59cc42e511c148734c2ba3831)", "omicron-workspace-hack", "rand 0.8.5", "secrecy", "serde", "serde_with", "sha3", - "sled-hardware-types", + "sled-hardware-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?rev=becbbb616f5f18b59cc42e511c148734c2ba3831)", "slog", + "slog-error-chain", "thiserror 2.0.18", "tokio", "uuid", @@ -695,7 +779,18 @@ checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" dependencies = [ "cfg-if", "cipher", - "cpufeatures", + "cpufeatures 0.2.17", +] + +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", ] [[package]] @@ -705,7 +800,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" dependencies = [ "aead", - "chacha20", + "chacha20 0.9.1", "cipher", "poly1305", "zeroize", @@ -849,7 +944,7 @@ dependencies = [ "derive_more", "expectorate", "itertools 0.14.0", - "omicron-common", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", "omicron-workspace-hack", "schemars 0.8.22", "serde", @@ -908,7 +1003,7 @@ source = "git+https://github.com/oxidecomputer/omicron?branch=main#8e30605f52172 dependencies = [ "chrono", "csv", - "omicron-uuid-kinds", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", "omicron-workspace-hack", "schemars 0.8.22", "serde", @@ -1024,6 +1119,12 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "const_format" version = "0.2.34" @@ -1091,14 +1192,23 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "cpuid_utils" version = "0.0.0" -source = "git+https://github.com/oxidecomputer/propolis#827e6615bfebfd94d41504dcd1517a0f22e3166a" +source = "git+https://github.com/oxidecomputer/propolis?branch=zl%2Fmulticast#784de5e4b123b48e574bbffc1188c0d5c30b2e62" dependencies = [ - "bhyve_api 0.0.0 (git+https://github.com/oxidecomputer/propolis)", + "bhyve_api 0.0.0 (git+https://github.com/oxidecomputer/propolis?branch=zl%2Fmulticast)", "bitflags 2.9.4", - "propolis_types 0.0.0 (git+https://github.com/oxidecomputer/propolis)", + "propolis_types 0.0.0 (git+https://github.com/oxidecomputer/propolis?branch=zl%2Fmulticast)", "thiserror 1.0.69", ] @@ -1259,7 +1369,7 @@ dependencies = [ "openssl-probe", "openssl-sys", "schannel", - "socket2 0.6.0", + "socket2 0.6.3", "windows-sys 0.59.0", ] @@ -1285,7 +1395,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "curve25519-dalek-derive", "digest", "fiat-crypto", @@ -1336,8 +1446,18 @@ version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.21.3", + "darling_macro 0.21.3", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core 0.23.0", + "darling_macro 0.23.0", ] [[package]] @@ -1354,13 +1474,37 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.117", +] + [[package]] name = "darling_macro" version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ - "darling_core", + "darling_core 0.21.3", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core 0.23.0", "quote", "syn 2.0.117", ] @@ -1431,6 +1575,30 @@ dependencies = [ "semver 1.0.27", ] +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "der_derive", + "flagset", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "der_derive" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "deranged" version = "0.5.3" @@ -1477,6 +1645,31 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "dice-verifier" +version = "0.3.0-pre0" +source = "git+https://github.com/oxidecomputer/dice-util?rev=1d3084b514389847e8e0f5d966d2be4f18d02d32#1d3084b514389847e8e0f5d966d2be4f18d02d32" +dependencies = [ + "async-trait", + "attest-data", + "const-oid", + "ed25519-dalek", + "env_logger", + "hex", + "hubpack", + "log", + "p384", + "rats-corim", + "sha3", + "sled-agent-client", + "sled-agent-types-versions 0.1.0 (git+https://github.com/oxidecomputer/omicron?rev=becbbb616f5f18b59cc42e511c148734c2ba3831)", + "slog", + "tempfile", + "thiserror 2.0.18", + "tokio", + "x509-cert", +] + [[package]] name = "diff" version = "0.1.13" @@ -1490,6 +1683,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", + "const-oid", "crypto-common", "subtle", ] @@ -1514,7 +1708,7 @@ dependencies = [ [[package]] name = "dladm" version = "0.0.0" -source = "git+https://github.com/oxidecomputer/propolis#827e6615bfebfd94d41504dcd1517a0f22e3166a" +source = "git+https://github.com/oxidecomputer/propolis?branch=zl%2Fmulticast#784de5e4b123b48e574bbffc1188c0d5c30b2e62" dependencies = [ "libc", "strum 0.26.3", @@ -1523,11 +1717,11 @@ dependencies = [ [[package]] name = "dlpi" version = "0.2.0" -source = "git+https://github.com/oxidecomputer/dlpi-sys?branch=main#555fa6e1315a64f40c72716e4d168697c03795c6" +source = "git+https://github.com/oxidecomputer/dlpi-sys?branch=main#7cce2d3ab9dcac909642e1d1060f27bb2549cfdc" dependencies = [ "libc", "libdlpi-sys 0.1.0 (git+https://github.com/oxidecomputer/dlpi-sys?branch=main)", - "num_enum 0.7.5", + "num_enum 0.7.6", "pretty-hex", "thiserror 2.0.18", "tokio", @@ -1536,11 +1730,11 @@ dependencies = [ [[package]] name = "dlpi" version = "0.2.0" -source = "git+https://github.com/oxidecomputer/dlpi-sys#42b2bfeefdfb8c7b96fc6cfa9ec45ef4554c2714" +source = "git+https://github.com/oxidecomputer/dlpi-sys#7cce2d3ab9dcac909642e1d1060f27bb2549cfdc" dependencies = [ "libc", "libdlpi-sys 0.1.0 (git+https://github.com/oxidecomputer/dlpi-sys)", - "num_enum 0.7.5", + "num_enum 0.7.6", "pretty-hex", "thiserror 2.0.18", ] @@ -1607,7 +1801,7 @@ dependencies = [ "libc", "mockall", "nexus-client", - "omicron-common", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", "openssl", "oxide-tokio-rt", "oximeter", @@ -1719,7 +1913,7 @@ dependencies = [ "aal", "chrono", "common 0.1.0", - "omicron-common", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", "oxnet", "schemars 0.8.22", "serde", @@ -1888,6 +2082,44 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2", + "subtle", + "zeroize", +] + [[package]] name = "either" version = "1.15.0" @@ -1907,7 +2139,10 @@ dependencies = [ "generic-array", "group", "hkdf", + "pem-rfc7468", + "pkcs8", "rand_core 0.6.4", + "sec1", "subtle", "zeroize", ] @@ -1951,6 +2186,25 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "env_filter" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef" +dependencies = [ + "log", +] + +[[package]] +name = "env_logger" +version = "0.11.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a" +dependencies = [ + "env_filter", + "log", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -1983,7 +2237,7 @@ version = "0.1.0" source = "git+https://github.com/oxidecomputer/omicron?branch=main#8e30605f52172a26ea531d1cfc870117a7370418" dependencies = [ "dropshot", - "omicron-uuid-kinds", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", "omicron-workspace-hack", "schemars 0.8.22", "serde", @@ -2065,6 +2319,12 @@ version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" +[[package]] +name = "flagset" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7ac824320a75a52197e8f2d787f6a38b6718bb6897a35142d749af3c0e8f4fe" + [[package]] name = "flate2" version = "1.1.2" @@ -2276,7 +2536,7 @@ dependencies = [ "ereport-types", "gateway-messages", "gateway-types", - "omicron-uuid-kinds", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", "omicron-workspace-hack", "progenitor 0.10.0", "rand 0.9.2", @@ -2325,7 +2585,7 @@ dependencies = [ "dropshot", "gateway-messages", "hex", - "omicron-uuid-kinds", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", "omicron-workspace-hack", "schemars 0.8.22", "serde", @@ -2364,24 +2624,38 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi 0.11.1+wasi-snapshot-preview1", + "wasi", "wasm-bindgen", ] [[package]] name = "getrandom" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi", - "wasi 0.14.7+wasi-0.2.4", + "r-efi 5.3.0", + "wasip2", "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", + "wasip2", + "wasip3", +] + [[package]] name = "gfss" version = "0.1.0" @@ -2398,6 +2672,22 @@ dependencies = [ "zeroize", ] +[[package]] +name = "gfss" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?rev=becbbb616f5f18b59cc42e511c148734c2ba3831#becbbb616f5f18b59cc42e511c148734c2ba3831" +dependencies = [ + "digest", + "omicron-workspace-hack", + "rand 0.9.2", + "schemars 0.8.22", + "secrecy", + "serde", + "subtle", + "thiserror 2.0.18", + "zeroize", +] + [[package]] name = "git-stub" version = "1.0.0" @@ -3022,6 +3312,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "iddqd" version = "0.3.17" @@ -3085,6 +3381,14 @@ dependencies = [ "bitflags 2.9.4", ] +[[package]] +name = "illumos-sys-hdrs" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/opte?rev=e547d07b08c3f3d6c821c9eb7a958adcffce6e56#e547d07b08c3f3d6c821c9eb7a958adcffce6e56" +dependencies = [ + "bitflags 2.9.4", +] + [[package]] name = "illumos-utils" version = "0.1.0" @@ -3107,13 +3411,13 @@ dependencies = [ "itertools 0.14.0", "libc", "macaddr", - "nix", - "omicron-common", - "omicron-uuid-kinds", + "nix 0.30.1", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", "omicron-workspace-hack", - "opte-ioctl", - "oxide-vpc", - "oxlog", + "opte-ioctl 0.1.0 (git+https://github.com/oxidecomputer/opte?rev=4bd8a40c0f5c05de7bb29b5f592f2dc99b4fd1d7)", + "oxide-vpc 0.1.0 (git+https://github.com/oxidecomputer/opte?rev=4bd8a40c0f5c05de7bb29b5f592f2dc99b4fd1d7)", + "oxlog 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", "oxnet", "schemars 0.8.22", "serde", @@ -3131,23 +3435,70 @@ dependencies = [ ] [[package]] -name = "indent_write" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cfe9645a18782869361d9c8732246be7b410ad4e919d3609ebabdac00ba12c3" - -[[package]] -name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +name = "illumos-utils" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?rev=becbbb616f5f18b59cc42e511c148734c2ba3831#becbbb616f5f18b59cc42e511c148734c2ba3831" dependencies = [ - "autocfg", - "hashbrown 0.12.3", + "anyhow", + "async-trait", + "bhyve_api 0.0.0 (git+https://github.com/oxidecomputer/propolis?rev=8ccddb47a4c93b7e3480919495dae851afc83782)", + "byteorder", + "camino", + "camino-tempfile", + "cfg-if", + "chrono", + "crucible-smf", + "debug-ignore", + "dropshot", + "futures", + "http", + "iddqd", + "ipnetwork", + "itertools 0.14.0", + "libc", + "macaddr", + "nix 0.30.1", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?rev=becbbb616f5f18b59cc42e511c148734c2ba3831)", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?rev=becbbb616f5f18b59cc42e511c148734c2ba3831)", + "omicron-workspace-hack", + "opte-ioctl 0.1.0 (git+https://github.com/oxidecomputer/opte?rev=e547d07b08c3f3d6c821c9eb7a958adcffce6e56)", + "oxide-vpc 0.1.0 (git+https://github.com/oxidecomputer/opte?rev=e547d07b08c3f3d6c821c9eb7a958adcffce6e56)", + "oxlog 0.1.0 (git+https://github.com/oxidecomputer/omicron?rev=becbbb616f5f18b59cc42e511c148734c2ba3831)", + "oxnet", + "rustix 1.1.2", + "schemars 0.8.22", "serde", -] - -[[package]] + "slog", + "slog-async", + "slog-error-chain", + "slog-term", + "smf 0.2.3", + "thiserror 2.0.18", + "tofino 0.1.0 (git+https://github.com/oxidecomputer/tofino)", + "tokio", + "uuid", + "whoami", + "zone", +] + +[[package]] +name = "indent_write" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cfe9645a18782869361d9c8732246be7b410ad4e919d3609ebabdac00ba12c3" + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] name = "indexmap" version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3179,7 +3530,7 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4c94e8b5b5e08b71943d585acdb6062daaeb6b19de82ebb377c7c9cbeff44bb" dependencies = [ - "darling", + "darling 0.21.3", "itertools 0.14.0", "proc-macro2", "quote", @@ -3225,8 +3576,8 @@ dependencies = [ "hickory-proto 0.25.2", "hickory-resolver 0.25.2", "internal-dns-types", - "omicron-common", - "omicron-uuid-kinds", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", "omicron-workspace-hack", "qorb", "reqwest 0.12.23", @@ -3242,8 +3593,8 @@ dependencies = [ "anyhow", "chrono", "internal-dns-types-versions", - "omicron-common", - "omicron-uuid-kinds", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", "omicron-workspace-hack", "schemars 0.8.22", "serde", @@ -3257,7 +3608,7 @@ source = "git+https://github.com/oxidecomputer/omicron?branch=main#8e30605f52172 dependencies = [ "anyhow", "chrono", - "omicron-common", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", "omicron-workspace-hack", "schemars 0.8.22", "serde", @@ -3409,7 +3760,7 @@ version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", "libc", ] @@ -3429,7 +3780,7 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" dependencies = [ - "cpufeatures", + "cpufeatures 0.2.17", ] [[package]] @@ -3441,6 +3792,15 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "kstat-macro" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/opte?rev=e547d07b08c3f3d6c821c9eb7a958adcffce6e56#e547d07b08c3f3d6c821c9eb7a958adcffce6e56" +dependencies = [ + "quote", + "syn 2.0.117", +] + [[package]] name = "kstat-rs" version = "0.2.4" @@ -3457,6 +3817,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libc" version = "0.2.183" @@ -3466,12 +3832,12 @@ checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" [[package]] name = "libdlpi-sys" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/dlpi-sys?branch=main#555fa6e1315a64f40c72716e4d168697c03795c6" +source = "git+https://github.com/oxidecomputer/dlpi-sys?branch=main#7cce2d3ab9dcac909642e1d1060f27bb2549cfdc" [[package]] name = "libdlpi-sys" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/dlpi-sys#42b2bfeefdfb8c7b96fc6cfa9ec45ef4554c2714" +source = "git+https://github.com/oxidecomputer/dlpi-sys#7cce2d3ab9dcac909642e1d1060f27bb2549cfdc" [[package]] name = "libgit2-sys" @@ -3514,23 +3880,23 @@ checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] name = "libnet" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/netadm-sys?branch=main#24167d269038223b9d5d50c333ecaa34001d8f94" +source = "git+https://github.com/oxidecomputer/netadm-sys?branch=main#e9bf1e519ce304bf9cc6e87b50b9c64a77c8b0c1" dependencies = [ "anyhow", "cfg-if", "colored", "dlpi 0.2.0 (git+https://github.com/oxidecomputer/dlpi-sys)", "libc", - "num_enum 0.7.5", + "num_enum 0.7.6", "nvpair 0.5.0", "nvpair-sys", "oxnet", - "rand 0.9.2", + "rand 0.10.1", "rusty-doors", - "socket2 0.6.0", + "socket2 0.6.3", "thiserror 2.0.18", "tracing", - "winnow 0.7.14", + "winnow 1.0.1", ] [[package]] @@ -3543,13 +3909,13 @@ dependencies = [ "colored", "dlpi 0.2.0 (git+https://github.com/oxidecomputer/dlpi-sys)", "libc", - "num_enum 0.7.5", + "num_enum 0.7.6", "nvpair 0.5.0", "nvpair-sys", "oxnet", "rand 0.9.2", "rusty-doors", - "socket2 0.6.0", + "socket2 0.6.3", "thiserror 2.0.18", "tracing", "winnow 0.7.14", @@ -3781,7 +4147,25 @@ dependencies = [ "chrono", "colored", "progenitor 0.11.1", - "rdb-types", + "rdb-types 0.1.0 (git+https://github.com/oxidecomputer/maghemite?rev=396bb3c570be65f4e8c73ea3243f4af6561a823a)", + "reqwest 0.12.23", + "schemars 0.8.22", + "serde", + "serde_json", + "slog", + "tabwriter", + "uuid", +] + +[[package]] +name = "mg-admin-client" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/maghemite?rev=3abfb8eb7f6d4ca4658981b4a7a76759a0a3f8ec#3abfb8eb7f6d4ca4658981b4a7a76759a0a3f8ec" +dependencies = [ + "chrono", + "colored", + "progenitor 0.11.1", + "rdb-types 0.1.0 (git+https://github.com/oxidecomputer/maghemite?rev=3abfb8eb7f6d4ca4658981b4a7a76759a0a3f8ec)", "reqwest 0.12.23", "schemars 0.8.22", "serde", @@ -3814,13 +4198,13 @@ dependencies = [ [[package]] name = "mio" -version = "1.0.4" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ "libc", - "wasi 0.11.1+wasi-snapshot-preview1", - "windows-sys 0.59.0", + "wasi", + "windows-sys 0.61.0", ] [[package]] @@ -3954,8 +4338,8 @@ dependencies = [ "futures", "iddqd", "nexus-types", - "omicron-common", - "omicron-uuid-kinds", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", "omicron-workspace-hack", "oxnet", "progenitor 0.10.0", @@ -3974,7 +4358,7 @@ version = "0.1.0" source = "git+https://github.com/oxidecomputer/omicron?branch=main#8e30605f52172a26ea531d1cfc870117a7370418" dependencies = [ "anyhow", - "api_identity", + "api_identity 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", "async-trait", "base64 0.22.1", "chrono", @@ -3993,7 +4377,7 @@ dependencies = [ "http", "humantime", "iddqd", - "illumos-utils", + "illumos-utils 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", "indent_write", "internal-dns-types", "ipnet", @@ -4001,9 +4385,9 @@ dependencies = [ "itertools 0.14.0", "newtype-uuid", "newtype_derive", - "omicron-common", - "omicron-passwords", - "omicron-uuid-kinds", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-passwords 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", "omicron-workspace-hack", "openssl", "oximeter-db", @@ -4016,9 +4400,9 @@ dependencies = [ "serde", "serde_json", "serde_with", - "sled-agent-types", - "sled-agent-types-versions", - "sled-hardware-types", + "sled-agent-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "sled-agent-types-versions 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "sled-hardware-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", "slog", "slog-error-chain", "steno", @@ -4031,7 +4415,7 @@ dependencies = [ "tokio", "tough", "trust-quorum-protocol", - "trust-quorum-types", + "trust-quorum-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", "tufaceous-artifact", "unicode-width 0.1.14", "update-engine", @@ -4052,6 +4436,18 @@ dependencies = [ "memoffset", ] +[[package]] +name = "nix" +version = "0.31.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3" +dependencies = [ + "bitflags 2.9.4", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "nom" version = "7.1.3" @@ -4178,11 +4574,11 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" dependencies = [ - "num_enum_derive 0.7.5", + "num_enum_derive 0.7.6", "rustversion", ] @@ -4200,9 +4596,9 @@ dependencies = [ [[package]] name = "num_enum_derive" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -4222,7 +4618,7 @@ dependencies = [ [[package]] name = "nvpair" version = "0.0.0" -source = "git+https://github.com/oxidecomputer/propolis#827e6615bfebfd94d41504dcd1517a0f22e3166a" +source = "git+https://github.com/oxidecomputer/propolis?branch=zl%2Fmulticast#784de5e4b123b48e574bbffc1188c0d5c30b2e62" dependencies = [ "libc", "nvpair_sys", @@ -4246,7 +4642,7 @@ source = "git+https://github.com/jmesmon/rust-libzfs?branch=master#ecd5a922247a6 [[package]] name = "nvpair_sys" version = "0.0.0" -source = "git+https://github.com/oxidecomputer/propolis#827e6615bfebfd94d41504dcd1517a0f22e3166a" +source = "git+https://github.com/oxidecomputer/propolis?branch=zl%2Fmulticast#784de5e4b123b48e574bbffc1188c0d5c30b2e62" dependencies = [ "libc", ] @@ -4268,7 +4664,7 @@ version = "0.1.0" source = "git+https://github.com/oxidecomputer/omicron?branch=main#8e30605f52172a26ea531d1cfc870117a7370418" dependencies = [ "anyhow", - "api_identity", + "api_identity 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", "async-trait", "backoff", "camino", @@ -4282,8 +4678,53 @@ dependencies = [ "ipnetwork", "itertools 0.14.0", "macaddr", - "mg-admin-client", - "omicron-uuid-kinds", + "mg-admin-client 0.1.0 (git+https://github.com/oxidecomputer/maghemite?rev=396bb3c570be65f4e8c73ea3243f4af6561a823a)", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-workspace-hack", + "oxnet", + "parse-display", + "progenitor-client 0.10.0", + "protocol", + "rand 0.9.2", + "regress", + "reqwest 0.12.23", + "schemars 0.8.22", + "semver 1.0.27", + "serde", + "serde_human_bytes", + "serde_json", + "serde_with", + "slog", + "slog-error-chain", + "strum 0.27.2", + "thiserror 2.0.18", + "tokio", + "tufaceous-artifact", + "uuid", +] + +[[package]] +name = "omicron-common" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?rev=becbbb616f5f18b59cc42e511c148734c2ba3831#becbbb616f5f18b59cc42e511c148734c2ba3831" +dependencies = [ + "anyhow", + "api_identity 0.1.0 (git+https://github.com/oxidecomputer/omicron?rev=becbbb616f5f18b59cc42e511c148734c2ba3831)", + "async-trait", + "backoff", + "camino", + "chrono", + "daft", + "dropshot", + "futures", + "hex", + "http", + "iddqd", + "ipnetwork", + "itertools 0.14.0", + "macaddr", + "mg-admin-client 0.1.0 (git+https://github.com/oxidecomputer/maghemite?rev=3abfb8eb7f6d4ca4658981b4a7a76759a0a3f8ec)", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?rev=becbbb616f5f18b59cc42e511c148734c2ba3831)", "omicron-workspace-hack", "oxnet", "parse-display", @@ -4322,6 +4763,21 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "omicron-passwords" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?rev=becbbb616f5f18b59cc42e511c148734c2ba3831#becbbb616f5f18b59cc42e511c148734c2ba3831" +dependencies = [ + "argon2", + "omicron-workspace-hack", + "rand 0.9.2", + "schemars 0.8.22", + "secrecy", + "serde", + "serde_with", + "thiserror 2.0.18", +] + [[package]] name = "omicron-uuid-kinds" version = "0.1.0" @@ -4334,6 +4790,18 @@ dependencies = [ "schemars 0.8.22", ] +[[package]] +name = "omicron-uuid-kinds" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?rev=becbbb616f5f18b59cc42e511c148734c2ba3831#becbbb616f5f18b59cc42e511c148734c2ba3831" +dependencies = [ + "daft", + "newtype-uuid", + "newtype-uuid-macros", + "paste", + "schemars 0.8.22", +] + [[package]] name = "omicron-workspace-hack" version = "0.1.0" @@ -4456,10 +4924,29 @@ source = "git+https://github.com/oxidecomputer/opte?rev=4bd8a40c0f5c05de7bb29b5f dependencies = [ "bitflags 2.9.4", "dyn-clone", - "illumos-sys-hdrs", + "illumos-sys-hdrs 0.1.0 (git+https://github.com/oxidecomputer/opte?rev=4bd8a40c0f5c05de7bb29b5f592f2dc99b4fd1d7)", + "ingot", + "kstat-macro 0.1.0 (git+https://github.com/oxidecomputer/opte?rev=4bd8a40c0f5c05de7bb29b5f592f2dc99b4fd1d7)", + "opte-api 0.1.0 (git+https://github.com/oxidecomputer/opte?rev=4bd8a40c0f5c05de7bb29b5f592f2dc99b4fd1d7)", + "postcard", + "ref-cast", + "serde", + "tabwriter", + "version_check", + "zerocopy 0.8.27", +] + +[[package]] +name = "opte" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/opte?rev=e547d07b08c3f3d6c821c9eb7a958adcffce6e56#e547d07b08c3f3d6c821c9eb7a958adcffce6e56" +dependencies = [ + "bitflags 2.9.4", + "dyn-clone", + "illumos-sys-hdrs 0.1.0 (git+https://github.com/oxidecomputer/opte?rev=e547d07b08c3f3d6c821c9eb7a958adcffce6e56)", "ingot", - "kstat-macro", - "opte-api", + "kstat-macro 0.1.0 (git+https://github.com/oxidecomputer/opte?rev=e547d07b08c3f3d6c821c9eb7a958adcffce6e56)", + "opte-api 0.1.0 (git+https://github.com/oxidecomputer/opte?rev=e547d07b08c3f3d6c821c9eb7a958adcffce6e56)", "postcard", "ref-cast", "serde", @@ -4473,7 +4960,20 @@ name = "opte-api" version = "0.1.0" source = "git+https://github.com/oxidecomputer/opte?rev=4bd8a40c0f5c05de7bb29b5f592f2dc99b4fd1d7#4bd8a40c0f5c05de7bb29b5f592f2dc99b4fd1d7" dependencies = [ - "illumos-sys-hdrs", + "illumos-sys-hdrs 0.1.0 (git+https://github.com/oxidecomputer/opte?rev=4bd8a40c0f5c05de7bb29b5f592f2dc99b4fd1d7)", + "ingot", + "ipnetwork", + "postcard", + "serde", + "smoltcp", +] + +[[package]] +name = "opte-api" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/opte?rev=e547d07b08c3f3d6c821c9eb7a958adcffce6e56#e547d07b08c3f3d6c821c9eb7a958adcffce6e56" +dependencies = [ + "illumos-sys-hdrs 0.1.0 (git+https://github.com/oxidecomputer/opte?rev=e547d07b08c3f3d6c821c9eb7a958adcffce6e56)", "ingot", "ipnetwork", "postcard", @@ -4488,8 +4988,22 @@ source = "git+https://github.com/oxidecomputer/opte?rev=4bd8a40c0f5c05de7bb29b5f dependencies = [ "libc", "libnet 0.1.0 (git+https://github.com/oxidecomputer/netadm-sys?branch=main)", - "opte", - "oxide-vpc", + "opte 0.1.0 (git+https://github.com/oxidecomputer/opte?rev=4bd8a40c0f5c05de7bb29b5f592f2dc99b4fd1d7)", + "oxide-vpc 0.1.0 (git+https://github.com/oxidecomputer/opte?rev=4bd8a40c0f5c05de7bb29b5f592f2dc99b4fd1d7)", + "postcard", + "serde", + "thiserror 2.0.18", +] + +[[package]] +name = "opte-ioctl" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/opte?rev=e547d07b08c3f3d6c821c9eb7a958adcffce6e56#e547d07b08c3f3d6c821c9eb7a958adcffce6e56" +dependencies = [ + "libc", + "libnet 0.1.0 (git+https://github.com/oxidecomputer/netadm-sys?branch=main)", + "opte 0.1.0 (git+https://github.com/oxidecomputer/opte?rev=e547d07b08c3f3d6c821c9eb7a958adcffce6e56)", + "oxide-vpc 0.1.0 (git+https://github.com/oxidecomputer/opte?rev=e547d07b08c3f3d6c821c9eb7a958adcffce6e56)", "postcard", "serde", "thiserror 2.0.18", @@ -4503,11 +5017,12 @@ checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d" [[package]] name = "oxide-tokio-rt" -version = "0.1.2" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84bd87abf37c68d414e4df90a545857542140e07206f75039b8f63b244da87b8" +checksum = "f7d3e6073f692ff812f2d99b61e0ea7f503e54fc9ba44481f619c50a16f0565d" dependencies = [ "anyhow", + "nix 0.31.2", "tokio", "tokio-dtrace", ] @@ -4518,8 +5033,22 @@ version = "0.1.0" source = "git+https://github.com/oxidecomputer/opte?rev=4bd8a40c0f5c05de7bb29b5f592f2dc99b4fd1d7#4bd8a40c0f5c05de7bb29b5f592f2dc99b4fd1d7" dependencies = [ "cfg-if", - "illumos-sys-hdrs", - "opte", + "illumos-sys-hdrs 0.1.0 (git+https://github.com/oxidecomputer/opte?rev=4bd8a40c0f5c05de7bb29b5f592f2dc99b4fd1d7)", + "opte 0.1.0 (git+https://github.com/oxidecomputer/opte?rev=4bd8a40c0f5c05de7bb29b5f592f2dc99b4fd1d7)", + "serde", + "tabwriter", + "uuid", + "zerocopy 0.8.27", +] + +[[package]] +name = "oxide-vpc" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/opte?rev=e547d07b08c3f3d6c821c9eb7a958adcffce6e56#e547d07b08c3f3d6c821c9eb7a958adcffce6e56" +dependencies = [ + "cfg-if", + "illumos-sys-hdrs 0.1.0 (git+https://github.com/oxidecomputer/opte?rev=e547d07b08c3f3d6c821c9eb7a958adcffce6e56)", + "opte 0.1.0 (git+https://github.com/oxidecomputer/opte?rev=e547d07b08c3f3d6c821c9eb7a958adcffce6e56)", "serde", "tabwriter", "uuid", @@ -4571,7 +5100,7 @@ dependencies = [ "libc", "nom", "num", - "omicron-common", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", "omicron-workspace-hack", "oxide-tokio-rt", "oximeter", @@ -4637,7 +5166,7 @@ dependencies = [ "internal-dns-resolver", "internal-dns-types", "nexus-client", - "omicron-common", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", "omicron-workspace-hack", "oximeter", "schemars 0.8.22", @@ -4692,7 +5221,7 @@ dependencies = [ "chrono", "float-ord", "num", - "omicron-common", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", "omicron-workspace-hack", "oximeter-types-versions", "parse-display", @@ -4710,7 +5239,7 @@ version = "0.1.0" source = "git+https://github.com/oxidecomputer/omicron?branch=main#8e30605f52172a26ea531d1cfc870117a7370418" dependencies = [ "chrono", - "omicron-common", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", "omicron-workspace-hack", "schemars 0.8.22", "serde", @@ -4734,6 +5263,23 @@ dependencies = [ "uuid", ] +[[package]] +name = "oxlog" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?rev=becbbb616f5f18b59cc42e511c148734c2ba3831#becbbb616f5f18b59cc42e511c148734c2ba3831" +dependencies = [ + "anyhow", + "camino", + "chrono", + "clap", + "glob", + "jiff", + "omicron-workspace-hack", + "rayon", + "sigpipe", + "uuid", +] + [[package]] name = "oxnet" version = "0.1.4" @@ -4763,10 +5309,22 @@ dependencies = [ "uuid", ] +[[package]] +name = "p384" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + [[package]] name = "p4rs" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/p4?branch=main#c13435444832c28e0fb19bc65eaa8b431583a1cf" +source = "git+https://github.com/oxidecomputer/p4?branch=zl%2Fmulticast#0e8a28a2edce0a96dd0ac3a3df95af3d58cee839" dependencies = [ "bitvec", "num", @@ -4777,7 +5335,7 @@ dependencies = [ [[package]] name = "p9ds" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/p9fs#113f170aff6aa1d5add00c19b3a2f3241e16a763" +source = "git+https://github.com/oxidecomputer/p9fs#04fb133ac730f6f69612dcb20d1cb2195e4e89a6" dependencies = [ "ispf", "num_enum 0.5.11", @@ -4905,6 +5463,15 @@ dependencies = [ "serde", ] +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -5029,6 +5596,16 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.32" @@ -5047,7 +5624,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" dependencies = [ - "cpufeatures", + "cpufeatures 0.2.17", "opaque-debug", "universal-hash", ] @@ -5155,6 +5732,15 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + [[package]] name = "proc-macro-crate" version = "1.3.1" @@ -5421,43 +6007,75 @@ dependencies = [ [[package]] name = "propolis" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/propolis#827e6615bfebfd94d41504dcd1517a0f22e3166a" +source = "git+https://github.com/oxidecomputer/propolis?branch=zl%2Fmulticast#784de5e4b123b48e574bbffc1188c0d5c30b2e62" dependencies = [ "anyhow", "async-trait", - "bhyve_api 0.0.0 (git+https://github.com/oxidecomputer/propolis)", + "bhyve_api 0.0.0 (git+https://github.com/oxidecomputer/propolis?branch=zl%2Fmulticast)", + "bit_field", "bitflags 2.9.4", "bitstruct", "byteorder", "cpuid_utils", + "dice-verifier", "dladm", "dlpi 0.2.0 (git+https://github.com/oxidecomputer/dlpi-sys?branch=main)", "erased-serde 0.4.8", "futures", + "iddqd", "ispf", "lazy_static", "libc", "libloading 0.7.4", + "nix 0.31.2", "p9ds", + "paste", "pin-project-lite", - "propolis_types 0.0.0 (git+https://github.com/oxidecomputer/propolis)", + "propolis_types 0.0.0 (git+https://github.com/oxidecomputer/propolis?branch=zl%2Fmulticast)", "rand 0.9.2", "rfb", "rgb_frame", "serde", "serde_arrays", "serde_json", + "sha2", "slog", - "softnpu 0.2.0 (git+https://github.com/oxidecomputer/softnpu)", + "softnpu", + "static_assertions", "strum 0.26.3", "thiserror 1.0.69", "tokio", - "usdt 0.5.0", + "usdt 0.6.0", "uuid", "viona_api", + "vm-attest", "zerocopy 0.8.27", ] +[[package]] +name = "propolis-client" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/propolis?rev=8ccddb47a4c93b7e3480919495dae851afc83782#8ccddb47a4c93b7e3480919495dae851afc83782" +dependencies = [ + "async-trait", + "base64 0.21.7", + "crucible-client-types", + "futures", + "progenitor 0.10.0", + "progenitor-client 0.10.0", + "propolis_api_types 0.0.0 (git+https://github.com/oxidecomputer/propolis?rev=8ccddb47a4c93b7e3480919495dae851afc83782)", + "rand 0.9.2", + "reqwest 0.12.23", + "schemars 0.8.22", + "serde", + "serde_json", + "slog", + "thiserror 1.0.69", + "tokio", + "tokio-tungstenite", + "uuid", +] + [[package]] name = "propolis_api_types" version = "0.0.0" @@ -5471,6 +6089,28 @@ dependencies = [ "uuid", ] +[[package]] +name = "propolis_api_types" +version = "0.0.0" +source = "git+https://github.com/oxidecomputer/propolis?rev=8ccddb47a4c93b7e3480919495dae851afc83782#8ccddb47a4c93b7e3480919495dae851afc83782" +dependencies = [ + "crucible-client-types", + "propolis_types 0.0.0 (git+https://github.com/oxidecomputer/propolis?rev=8ccddb47a4c93b7e3480919495dae851afc83782)", + "schemars 0.8.22", + "serde", + "thiserror 1.0.69", + "uuid", +] + +[[package]] +name = "propolis_types" +version = "0.0.0" +source = "git+https://github.com/oxidecomputer/propolis?branch=zl%2Fmulticast#784de5e4b123b48e574bbffc1188c0d5c30b2e62" +dependencies = [ + "schemars 0.8.22", + "serde", +] + [[package]] name = "propolis_types" version = "0.0.0" @@ -5483,7 +6123,7 @@ dependencies = [ [[package]] name = "propolis_types" version = "0.0.0" -source = "git+https://github.com/oxidecomputer/propolis#827e6615bfebfd94d41504dcd1517a0f22e3166a" +source = "git+https://github.com/oxidecomputer/propolis?rev=8ccddb47a4c93b7e3480919495dae851afc83782#8ccddb47a4c93b7e3480919495dae851afc83782" dependencies = [ "schemars 0.8.22", "serde", @@ -5544,7 +6184,7 @@ dependencies = [ [[package]] name = "protocol" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/lldp#4c9df170cdfdeead0896e35f20a560f2102ed907" +source = "git+https://github.com/oxidecomputer/lldp#ed02e1a3a96f98107cbbe90f90c7ed1c16edf97d" dependencies = [ "anyhow", "schemars 0.8.22", @@ -5606,7 +6246,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" dependencies = [ "bytes", - "getrandom 0.3.3", + "getrandom 0.3.4", "lru-slab", "rand 0.9.2", "ring", @@ -5649,6 +6289,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "radium" version = "0.7.0" @@ -5676,6 +6322,17 @@ dependencies = [ "rand_core 0.9.3", ] +[[package]] +name = "rand" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +dependencies = [ + "chacha20 0.10.0", + "getrandom 0.4.2", + "rand_core 0.10.1", +] + [[package]] name = "rand_chacha" version = "0.3.1" @@ -5711,9 +6368,15 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", ] +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + [[package]] name = "rand_xorshift" version = "0.4.0" @@ -5723,6 +6386,21 @@ dependencies = [ "rand_core 0.9.3", ] +[[package]] +name = "rats-corim" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/rats-corim#f0d5d5168d3d31487a56df32c676b0c6240bcc6b" +dependencies = [ + "ciborium", + "ciborium-io", + "clap", + "hex", + "serde", + "serde_with", + "strum 0.26.3", + "thiserror 2.0.18", +] + [[package]] name = "rayon" version = "1.11.0" @@ -5753,6 +6431,16 @@ dependencies = [ "serde", ] +[[package]] +name = "rdb-types" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/maghemite?rev=3abfb8eb7f6d4ca4658981b4a7a76759a0a3f8ec#3abfb8eb7f6d4ca4658981b4a7a76759a0a3f8ec" +dependencies = [ + "oxnet", + "schemars 0.8.22", + "serde", +] + [[package]] name = "redox_syscall" version = "0.5.17" @@ -5912,7 +6600,7 @@ checksum = "6b3789b30bd25ba102de4beabd95d21ac45b69b1be7d14522bab988c526d6799" [[package]] name = "rfb" version = "0.0.0" -source = "git+https://github.com/oxidecomputer/propolis#827e6615bfebfd94d41504dcd1517a0f22e3166a" +source = "git+https://github.com/oxidecomputer/propolis?branch=zl%2Fmulticast#784de5e4b123b48e574bbffc1188c0d5c30b2e62" dependencies = [ "ascii", "bitflags 2.9.4", @@ -5925,10 +6613,20 @@ dependencies = [ "zerocopy 0.8.27", ] +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + [[package]] name = "rgb_frame" version = "0.0.0" -source = "git+https://github.com/oxidecomputer/propolis#827e6615bfebfd94d41504dcd1517a0f22e3166a" +source = "git+https://github.com/oxidecomputer/propolis?branch=zl%2Fmulticast#784de5e4b123b48e574bbffc1188c0d5c30b2e62" dependencies = [ "strum 0.26.3", ] @@ -6111,6 +6809,16 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +[[package]] +name = "salty" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b947325a585e90733e0e9ec097228f40b637cc346f9bd68f84d5c6297d0fcfef" +dependencies = [ + "subtle", + "zeroize", +] + [[package]] name = "same-file" version = "1.0.6" @@ -6234,6 +6942,20 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5145b2263bec888224d054d1c820ceffa7d4a23723a2a822f970fcf1c5b64770" +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + [[package]] name = "secrecy" version = "0.10.3" @@ -6439,9 +7161,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.14.1" +version = "3.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c522100790450cf78eeac1507263d0a350d4d5b30df0c8e1fe051a10c22b376e" +checksum = "dd5414fad8e6907dbdd5bc441a50ae8d6e26151a03b1de04d89a5576de61d01f" dependencies = [ "base64 0.22.1", "chrono", @@ -6451,8 +7173,7 @@ dependencies = [ "schemars 0.8.22", "schemars 0.9.0", "schemars 1.2.0", - "serde", - "serde_derive", + "serde_core", "serde_json", "serde_with_macros", "time", @@ -6460,11 +7181,11 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.14.1" +version = "3.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327ada00f7d64abaac1e55a6911e90cf665aa051b9a561c7006c157f4633135e" +checksum = "d3db8978e608f1fe7357e211969fd9abdcae80bac1ba7a3369bb7eb6b404eb65" dependencies = [ - "darling", + "darling 0.23.0", "proc-macro2", "quote", "syn 2.0.117", @@ -6490,7 +7211,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest", ] @@ -6501,7 +7222,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest", ] @@ -6561,6 +7282,16 @@ dependencies = [ "tokio", ] +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + [[package]] name = "sigpipe" version = "0.1.3" @@ -6579,40 +7310,99 @@ dependencies = [ "bstr", ] -[[package]] -name = "siphasher" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" - -[[package]] -name = "slab" -version = "0.4.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" - +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "sled-agent-client" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?rev=becbbb616f5f18b59cc42e511c148734c2ba3831#becbbb616f5f18b59cc42e511c148734c2ba3831" +dependencies = [ + "anyhow", + "async-trait", + "chrono", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?rev=becbbb616f5f18b59cc42e511c148734c2ba3831)", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?rev=becbbb616f5f18b59cc42e511c148734c2ba3831)", + "omicron-workspace-hack", + "oxnet", + "progenitor 0.10.0", + "propolis-client", + "regress", + "reqwest 0.12.23", + "schemars 0.8.22", + "serde", + "serde_json", + "sled-agent-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?rev=becbbb616f5f18b59cc42e511c148734c2ba3831)", + "sled-agent-types-versions 0.1.0 (git+https://github.com/oxidecomputer/omicron?rev=becbbb616f5f18b59cc42e511c148734c2ba3831)", + "sled-hardware-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?rev=becbbb616f5f18b59cc42e511c148734c2ba3831)", + "slog", + "trust-quorum-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?rev=becbbb616f5f18b59cc42e511c148734c2ba3831)", + "uuid", +] + +[[package]] +name = "sled-agent-types" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#8e30605f52172a26ea531d1cfc870117a7370418" +dependencies = [ + "anyhow", + "async-trait", + "bootstore 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "camino", + "chrono", + "daft", + "iddqd", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-workspace-hack", + "oxnet", + "schemars 0.8.22", + "serde", + "serde_human_bytes", + "serde_json", + "sled-agent-types-versions 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "sled-hardware-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "slog", + "slog-error-chain", + "strum 0.27.2", + "swrite", + "thiserror 2.0.18", + "toml 0.8.23", + "tufaceous-artifact", + "uuid", +] + [[package]] name = "sled-agent-types" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#8e30605f52172a26ea531d1cfc870117a7370418" +source = "git+https://github.com/oxidecomputer/omicron?rev=becbbb616f5f18b59cc42e511c148734c2ba3831#becbbb616f5f18b59cc42e511c148734c2ba3831" dependencies = [ "anyhow", "async-trait", - "bootstore", + "bootstore 0.1.0 (git+https://github.com/oxidecomputer/omicron?rev=becbbb616f5f18b59cc42e511c148734c2ba3831)", "camino", "chrono", "daft", "iddqd", - "omicron-common", - "omicron-uuid-kinds", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?rev=becbbb616f5f18b59cc42e511c148734c2ba3831)", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?rev=becbbb616f5f18b59cc42e511c148734c2ba3831)", "omicron-workspace-hack", "oxnet", "schemars 0.8.22", "serde", "serde_human_bytes", "serde_json", - "sled-agent-types-versions", - "sled-hardware-types", + "sled-agent-types-versions 0.1.0 (git+https://github.com/oxidecomputer/omicron?rev=becbbb616f5f18b59cc42e511c148734c2ba3831)", + "sled-hardware-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?rev=becbbb616f5f18b59cc42e511c148734c2ba3831)", "slog", "slog-error-chain", "strum 0.27.2", @@ -6629,30 +7419,64 @@ version = "0.1.0" source = "git+https://github.com/oxidecomputer/omicron?branch=main#8e30605f52172a26ea531d1cfc870117a7370418" dependencies = [ "async-trait", - "bootstore", + "bootstore 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "camino", + "chrono", + "daft", + "iddqd", + "illumos-utils 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "indent_write", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-passwords 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-workspace-hack", + "oxnet", + "propolis_api_types 0.0.0 (git+https://github.com/oxidecomputer/propolis?rev=2aa7f9d0ee84a1c45e821d6444b1d2f0e69b743e)", + "schemars 0.8.22", + "serde", + "serde_human_bytes", + "serde_json", + "serde_with", + "sha3", + "sled-hardware-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "slog", + "strum 0.27.2", + "thiserror 2.0.18", + "trust-quorum-types-versions 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "tufaceous-artifact", + "uuid", +] + +[[package]] +name = "sled-agent-types-versions" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?rev=becbbb616f5f18b59cc42e511c148734c2ba3831#becbbb616f5f18b59cc42e511c148734c2ba3831" +dependencies = [ + "async-trait", + "bootstore 0.1.0 (git+https://github.com/oxidecomputer/omicron?rev=becbbb616f5f18b59cc42e511c148734c2ba3831)", "camino", "chrono", "daft", "iddqd", - "illumos-utils", + "illumos-utils 0.1.0 (git+https://github.com/oxidecomputer/omicron?rev=becbbb616f5f18b59cc42e511c148734c2ba3831)", "indent_write", - "omicron-common", - "omicron-passwords", - "omicron-uuid-kinds", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?rev=becbbb616f5f18b59cc42e511c148734c2ba3831)", + "omicron-passwords 0.1.0 (git+https://github.com/oxidecomputer/omicron?rev=becbbb616f5f18b59cc42e511c148734c2ba3831)", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?rev=becbbb616f5f18b59cc42e511c148734c2ba3831)", "omicron-workspace-hack", "oxnet", - "propolis_api_types", + "propolis_api_types 0.0.0 (git+https://github.com/oxidecomputer/propolis?rev=8ccddb47a4c93b7e3480919495dae851afc83782)", "schemars 0.8.22", "serde", "serde_human_bytes", "serde_json", "serde_with", "sha3", - "sled-hardware-types", + "sled-hardware-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?rev=becbbb616f5f18b59cc42e511c148734c2ba3831)", "slog", "strum 0.27.2", "thiserror 2.0.18", - "trust-quorum-types-versions", + "trust-quorum-types-versions 0.1.0 (git+https://github.com/oxidecomputer/omicron?rev=becbbb616f5f18b59cc42e511c148734c2ba3831)", "tufaceous-artifact", "uuid", ] @@ -6663,8 +7487,23 @@ version = "0.1.0" source = "git+https://github.com/oxidecomputer/omicron?branch=main#8e30605f52172a26ea531d1cfc870117a7370418" dependencies = [ "daft", - "illumos-utils", - "omicron-common", + "illumos-utils 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-workspace-hack", + "schemars 0.8.22", + "serde", + "slog", + "thiserror 2.0.18", +] + +[[package]] +name = "sled-hardware-types" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?rev=becbbb616f5f18b59cc42e511c148734c2ba3831#becbbb616f5f18b59cc42e511c148734c2ba3831" +dependencies = [ + "daft", + "illumos-utils 0.1.0 (git+https://github.com/oxidecomputer/omicron?rev=becbbb616f5f18b59cc42e511c148734c2ba3831)", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?rev=becbbb616f5f18b59cc42e511c148734c2ba3831)", "omicron-workspace-hack", "schemars 0.8.22", "serde", @@ -6848,29 +7687,18 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.0" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.59.0", -] - -[[package]] -name = "softnpu" -version = "0.2.0" -source = "git+https://github.com/oxidecomputer/softnpu?branch=main#7e015d167a914777bc21434d1c61f205f22993b1" -dependencies = [ - "p4rs", - "serde", - "serde_json", - "tokio", + "windows-sys 0.61.0", ] [[package]] name = "softnpu" version = "0.2.0" -source = "git+https://github.com/oxidecomputer/softnpu#7e015d167a914777bc21434d1c61f205f22993b1" +source = "git+https://github.com/oxidecomputer/softnpu?branch=zl%2Fmulticast#284c6830722548714128e63ea04bcca78ee27154" dependencies = [ "p4rs", "serde", @@ -6884,6 +7712,16 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -7188,7 +8026,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" dependencies = [ "fastrand", - "getrandom 0.3.3", + "getrandom 0.3.4", "once_cell", "rustix 1.1.2", "windows-sys 0.61.0", @@ -7272,7 +8110,7 @@ dependencies = [ "libc", "libnet 0.1.0 (git+https://github.com/oxidecomputer/netadm-sys)", "lldpd-client", - "omicron-common", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", "oxide-tokio-rt", "oximeter", "oximeter-instruments", @@ -7286,7 +8124,7 @@ dependencies = [ "signal-hook", "slog", "smf 0.10.0", - "socket2 0.6.0", + "socket2 0.6.3", "thiserror 1.0.69", "tokio", "uuid", @@ -7439,6 +8277,27 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "tls_codec" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de2e01245e2bb89d6f05801c564fa27624dbd7b1846859876c7dad82e90bf6b" +dependencies = [ + "tls_codec_derive", + "zeroize", +] + +[[package]] +name = "tls_codec_derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d2e76690929402faae40aebdda620a2c0e25dd6d3b9afe48867dfd95991f4bd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "tofino" version = "0.1.0" @@ -7452,7 +8311,7 @@ dependencies = [ [[package]] name = "tofino" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/tofino#7e56ab6e9a64ebae27cd97cd6e10ebf2cfdc3a33" +source = "git+https://github.com/oxidecomputer/tofino#e25e52991785039e967fd8fe7d86554d976e6d4b" dependencies = [ "anyhow", "cc", @@ -7461,9 +8320,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.49.0" +version = "1.52.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" dependencies = [ "bytes", "libc", @@ -7471,7 +8330,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.0", + "socket2 0.6.3", "tokio-macros", "tracing", "windows-sys 0.61.0", @@ -7490,9 +8349,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", @@ -7542,6 +8401,18 @@ dependencies = [ "tokio-util", ] +[[package]] +name = "tokio-tungstenite" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite", +] + [[package]] name = "tokio-util" version = "0.7.16" @@ -7853,7 +8724,7 @@ dependencies = [ "clap", "hubpack", "itertools 0.14.0", - "nix", + "nix 0.30.1", "schemars 0.8.22", "serde", "slog", @@ -7898,32 +8769,32 @@ name = "trust-quorum-protocol" version = "0.1.0" source = "git+https://github.com/oxidecomputer/omicron?branch=main#8e30605f52172a26ea531d1cfc870117a7370418" dependencies = [ - "bootstore", + "bootstore 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", "bytes", "camino", "chacha20poly1305", "ciborium", "daft", "derive_more", - "gfss", + "gfss 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", "hex", "hkdf", "iddqd", - "omicron-uuid-kinds", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", "omicron-workspace-hack", "rand 0.9.2", "secrecy", "serde", "serde_with", "sha3", - "sled-agent-types", - "sled-hardware-types", + "sled-agent-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "sled-hardware-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", "slog", "slog-error-chain", "static_assertions", "subtle", "thiserror 2.0.18", - "trust-quorum-types", + "trust-quorum-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", "uuid", "zeroize", ] @@ -7934,7 +8805,16 @@ version = "0.1.0" source = "git+https://github.com/oxidecomputer/omicron?branch=main#8e30605f52172a26ea531d1cfc870117a7370418" dependencies = [ "omicron-workspace-hack", - "trust-quorum-types-versions", + "trust-quorum-types-versions 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", +] + +[[package]] +name = "trust-quorum-types" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?rev=becbbb616f5f18b59cc42e511c148734c2ba3831#becbbb616f5f18b59cc42e511c148734c2ba3831" +dependencies = [ + "omicron-workspace-hack", + "trust-quorum-types-versions 0.1.0 (git+https://github.com/oxidecomputer/omicron?rev=becbbb616f5f18b59cc42e511c148734c2ba3831)", ] [[package]] @@ -7944,16 +8824,38 @@ source = "git+https://github.com/oxidecomputer/omicron?branch=main#8e30605f52172 dependencies = [ "daft", "derive_more", - "gfss", + "gfss 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "iddqd", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-workspace-hack", + "rand 0.9.2", + "schemars 0.8.22", + "serde", + "serde_human_bytes", + "serde_with", + "sled-hardware-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "slog", + "slog-error-chain", + "thiserror 2.0.18", +] + +[[package]] +name = "trust-quorum-types-versions" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?rev=becbbb616f5f18b59cc42e511c148734c2ba3831#becbbb616f5f18b59cc42e511c148734c2ba3831" +dependencies = [ + "daft", + "derive_more", + "gfss 0.1.0 (git+https://github.com/oxidecomputer/omicron?rev=becbbb616f5f18b59cc42e511c148734c2ba3831)", "iddqd", - "omicron-uuid-kinds", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?rev=becbbb616f5f18b59cc42e511c148734c2ba3831)", "omicron-workspace-hack", "rand 0.9.2", "schemars 0.8.22", "serde", "serde_human_bytes", "serde_with", - "sled-hardware-types", + "sled-hardware-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?rev=becbbb616f5f18b59cc42e511c148734c2ba3831)", "slog", "slog-error-chain", "thiserror 2.0.18", @@ -7982,6 +8884,25 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "tungstenite" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand 0.8.5", + "sha1", + "thiserror 1.0.69", + "url", + "utf-8", +] + [[package]] name = "typed-path" version = "0.9.3" @@ -8363,6 +9284,12 @@ dependencies = [ "usdt-impl 0.6.0", ] +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -8381,7 +9308,7 @@ version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", "js-sys", "serde_core", "wasm-bindgen", @@ -8424,19 +9351,35 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "viona_api" version = "0.0.0" -source = "git+https://github.com/oxidecomputer/propolis#827e6615bfebfd94d41504dcd1517a0f22e3166a" +source = "git+https://github.com/oxidecomputer/propolis?branch=zl%2Fmulticast#784de5e4b123b48e574bbffc1188c0d5c30b2e62" dependencies = [ "libc", "nvpair 0.0.0", - "viona_api_sys", ] [[package]] -name = "viona_api_sys" -version = "0.0.0" -source = "git+https://github.com/oxidecomputer/propolis#827e6615bfebfd94d41504dcd1517a0f22e3166a" +name = "vm-attest" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/vm-attest?rev=2cdd17580a4fc6c871d24797016af8dbaac9421d#2cdd17580a4fc6c871d24797016af8dbaac9421d" dependencies = [ + "anyhow", + "attest-data", + "const-oid", + "dice-verifier", + "ed25519-dalek", + "getrandom 0.3.4", + "hex", + "hubpack", "libc", + "log", + "rats-corim", + "serde", + "serde_json", + "serde_with", + "sha2", + "thiserror 2.0.18", + "uuid", + "x509-cert", ] [[package]] @@ -8501,21 +9444,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] -name = "wasi" -version = "0.14.7+wasi-0.2.4" +name = "wasip2" +version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ - "wasip2", + "wit-bindgen 0.46.0", ] [[package]] -name = "wasip2" -version = "1.0.1+wasi-0.2.4" +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.51.0", ] [[package]] @@ -8583,6 +9526,28 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap 2.13.0", + "wasm-encoder", + "wasmparser", +] + [[package]] name = "wasm-streams" version = "0.4.2" @@ -8609,6 +9574,18 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.9.4", + "hashbrown 0.15.5", + "indexmap 2.13.0", + "semver 1.0.27", +] + [[package]] name = "web-sys" version = "0.3.85" @@ -9029,6 +10006,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "winnow" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5" +dependencies = [ + "memchr", +] + [[package]] name = "winreg" version = "0.50.0" @@ -9045,6 +10031,94 @@ version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck 0.5.0", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck 0.5.0", + "indexmap 2.13.0", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.9.4", + "indexmap 2.13.0", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.13.0", + "log", + "semver 1.0.27", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + [[package]] name = "writeable" version = "0.6.1" @@ -9060,6 +10134,18 @@ dependencies = [ "tap", ] +[[package]] +name = "x509-cert" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1301e935010a701ae5f8655edc0ad17c44bad3ac5ce8c39185f75453b720ae94" +dependencies = [ + "const-oid", + "der", + "spki", + "tls_codec", +] + [[package]] name = "xattr" version = "1.6.1" diff --git a/Cargo.toml b/Cargo.toml index efbf670a..310bf380 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,9 +52,9 @@ oximeter = { git = "https://github.com/oxidecomputer/omicron", branch = "main" } oximeter-producer = { git = "https://github.com/oxidecomputer/omicron", branch = "main" } oximeter-instruments = { git = "https://github.com/oxidecomputer/omicron", branch = "main", default-features = false, features = ["kstat"] } oxnet = { version = "0.1.4", default-features = false, features = ["schemars", "serde"] } -propolis = { git = "https://github.com/oxidecomputer/propolis" } +propolis = { git = "https://github.com/oxidecomputer/propolis", branch = "zl/multicast" } smf = { git = "https://github.com/illumos/smf-rs" } -softnpu-lib = { git = "https://github.com/oxidecomputer/softnpu" , package = "softnpu" , branch = "main"} +softnpu-lib = { git = "https://github.com/oxidecomputer/softnpu" , package = "softnpu" , branch = "zl/multicast"} tofino = { git = "https://github.com/oxidecomputer/tofino", branch = "main" } transceiver-controller = { git = "https://github.com/oxidecomputer/transceiver-control", branch = "main" } @@ -82,7 +82,7 @@ libc = "0.2" mockall = "0.13.1" omicron-zone-package = "0.12" openssl = "0.10" -oxide-tokio-rt = "0.1.2" +oxide-tokio-rt = "0.1.4" parking_lot = "0.12" pretty_assertions = "1.4" proc-macro2 = "1.0" @@ -107,7 +107,8 @@ strum = { version = "0.27", features = [ "derive" ] } syn = { version = "2.0", features = ["extra-traits"]} tabwriter = { version = "1", features = ["ansi_formatting"] } thiserror = "1.0" -tokio = "1.37" +# pinned to match omicron main (tokio-rs/tokio#8056) +tokio = "1.52.1" toml = "0.9" usdt = "0.6" uuid = { version = "1.10", features = [ "v4", "serde" ] } @@ -117,3 +118,4 @@ internet-checksum = "0.2" # It's common during development to use a local copy of various complex # dependencies. If you want to use those, uncomment one of these blocks. # + diff --git a/asic/src/softnpu/mcast.rs b/asic/src/softnpu/mcast.rs new file mode 100644 index 00000000..76dd0f78 --- /dev/null +++ b/asic/src/softnpu/mcast.rs @@ -0,0 +1,302 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/ +// +// Copyright 2026 Oxide Computer Company + +//! In-memory multicast group tracking for the [softnpu] backend. +//! +//! Sidecar-lite handles packet replication via port bitmaps in the P4 +//! pipeline, so this module only needs to track group membership for +//! the `AsicMulticastOps` contract. +//! +//! [softnpu]: https://github.com/oxidecomputer/softnpu + +use std::collections::{HashMap, HashSet}; + +use aal::{AsicError, AsicResult}; + +pub struct McGroupData { + groups: HashMap>, +} + +fn no_group(group_id: u16) -> AsicError { + AsicError::InvalidArg(format!("no such multicast group: {group_id}")) +} + +impl McGroupData { + /// Get the list of multicast domains. + pub fn domains(&self) -> Vec { + self.groups.keys().copied().collect() + } + + /// Build a 128-bit port bitmap for a group. Bit N is set if port N + /// is a member. Returns zero for unknown groups. + pub fn port_bitmap(&self, group_id: u16) -> u128 { + match self.groups.get(&group_id) { + Some(ports) => { + let mut bitmap: u128 = 0; + for &port in ports { + bitmap |= 1u128 << port; + } + bitmap + } + None => 0, + } + } + + /// Get the number of ports in a multicast domain. + pub fn domain_port_count(&self, group_id: u16) -> AsicResult { + match self.groups.get(&group_id) { + Some(g) => Ok(g.len()), + None => Err(no_group(group_id)), + } + } + + /// Add a port to a multicast domain. Port must be < 128 to fit + /// in sidecar-lite's 128-bit replication bitmap. + pub fn domain_port_add( + &mut self, + group_id: u16, + port: u16, + _rid: u16, + _level1_excl_id: u16, + ) -> AsicResult<()> { + if port >= 128 { + return Err(AsicError::InvalidArg(format!( + "port {port} exceeds softnpu 128-port bitmap limit" + ))); + } + let group = match self.groups.get_mut(&group_id) { + Some(g) => Ok(g), + None => Err(no_group(group_id)), + }?; + + match group.insert(port) { + true => Ok(()), + false => Err(AsicError::InvalidArg(format!( + "multicast group {group_id} already contains port {port}" + ))), + } + } + + /// Remove a port from a multicast domain. + pub fn domain_port_remove( + &mut self, + group_id: u16, + port: u16, + ) -> AsicResult<()> { + let group = match self.groups.get_mut(&group_id) { + Some(g) => Ok(g), + None => Err(no_group(group_id)), + }?; + + match group.remove(&port) { + true => Ok(()), + false => Err(AsicError::InvalidArg(format!( + "multicast group {group_id} doesn't contain port {port}" + ))), + } + } + + /// Create a multicast domain. + #[allow(clippy::map_entry)] + pub fn domain_create(&mut self, group_id: u16) -> AsicResult<()> { + if self.groups.contains_key(&group_id) { + Err(AsicError::InvalidArg(format!( + "multicast group {group_id} already exists" + ))) + } else { + self.groups.insert(group_id, HashSet::new()); + Ok(()) + } + } + + /// Destroy a multicast domain. + pub fn domain_destroy(&mut self, group_id: u16) -> AsicResult<()> { + match self.groups.remove(&group_id) { + Some(_) => Ok(()), + None => Err(no_group(group_id)), + } + } + + /// Get the total number of multicast domains. + pub fn domains_count(&self) -> usize { + self.groups.len() + } + + /// Validate that the current group count does not exceed the limit. + pub fn set_max_nodes( + &mut self, + max_nodes: u32, + _max_link_aggregated_nodes: u32, + ) -> AsicResult<()> { + let total = self.domains_count(); + if total as u32 > max_nodes { + return Err(AsicError::InvalidArg(format!( + "number of multicast groups {total} exceeds max nodes {max_nodes}" + ))); + } + + Ok(()) + } +} + +pub fn init() -> McGroupData { + McGroupData { groups: HashMap::new() } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn group_lifecycle() { + let mut mc = init(); + + // Create group, add ports. + mc.domain_create(100).unwrap(); + mc.domain_port_add(100, 1, 0, 0).unwrap(); + mc.domain_port_add(100, 5, 0, 0).unwrap(); + + assert_eq!(mc.domain_port_count(100).unwrap(), 2); + assert_eq!(mc.domains_count(), 1); + + // Remove a port. + mc.domain_port_remove(100, 1).unwrap(); + assert_eq!(mc.domain_port_count(100).unwrap(), 1); + + // Destroy group. + mc.domain_destroy(100).unwrap(); + assert_eq!(mc.domains_count(), 0); + } + + #[test] + fn duplicate_group_rejected() { + let mut mc = init(); + mc.domain_create(1).unwrap(); + assert!(mc.domain_create(1).is_err()); + } + + #[test] + fn duplicate_port_rejected() { + let mut mc = init(); + mc.domain_create(1).unwrap(); + mc.domain_port_add(1, 5, 0, 0).unwrap(); + assert!(mc.domain_port_add(1, 5, 0, 0).is_err()); + } + + #[test] + fn remove_nonexistent_port_rejected() { + let mut mc = init(); + mc.domain_create(1).unwrap(); + assert!(mc.domain_port_remove(1, 99).is_err()); + } + + #[test] + fn operations_on_missing_group_rejected() { + let mut mc = init(); + assert!(mc.domain_port_add(42, 1, 0, 0).is_err()); + assert!(mc.domain_port_remove(42, 1).is_err()); + assert!(mc.domain_port_count(42).is_err()); + assert!(mc.domain_destroy(42).is_err()); + } + + #[test] + fn port_bitmap_empty_group() { + let mut mc = init(); + mc.domain_create(1).unwrap(); + assert_eq!(mc.port_bitmap(1), 0); + } + + #[test] + fn port_bitmap_populated() { + let mut mc = init(); + mc.domain_create(1).unwrap(); + mc.domain_port_add(1, 0, 0, 0).unwrap(); + mc.domain_port_add(1, 3, 0, 0).unwrap(); + mc.domain_port_add(1, 7, 0, 0).unwrap(); + + let bm = mc.port_bitmap(1); + assert_eq!(bm & (1 << 0), 1 << 0); + assert_eq!(bm & (1 << 3), 1 << 3); + assert_eq!(bm & (1 << 7), 1 << 7); + assert_eq!(bm & (1 << 1), 0); + } + + #[test] + fn port_bitmap_unknown_group_returns_zero() { + let mc = init(); + assert_eq!(mc.port_bitmap(999), 0); + } + + #[test] + fn port_bitmap_high_port() { + let mut mc = init(); + mc.domain_create(1).unwrap(); + mc.domain_port_add(1, 127, 0, 0).unwrap(); + + let bm = mc.port_bitmap(1); + assert_eq!(bm & (1u128 << 127), 1u128 << 127); + } + + #[test] + fn port_bitmap_ignores_ports_above_127() { + let mut mc = init(); + mc.domain_create(1).unwrap(); + // Port 128 is out of range for a 128-bit bitmap. + assert!(mc.domain_port_add(1, 128, 0, 0).is_err()); + } + + #[test] + fn set_max_nodes_validates() { + let mut mc = init(); + mc.domain_create(1).unwrap(); + mc.domain_create(2).unwrap(); + + assert!(mc.set_max_nodes(1, 0).is_err()); + assert!(mc.set_max_nodes(2, 0).is_ok()); + assert!(mc.set_max_nodes(100, 0).is_ok()); + } + + #[test] + fn domains_returns_created_group_ids() { + let mut mc = init(); + mc.domain_create(10).unwrap(); + mc.domain_create(20).unwrap(); + mc.domain_create(30).unwrap(); + + let mut ids = mc.domains(); + ids.sort(); + assert_eq!(ids, vec![10, 20, 30]); + } + + #[test] + fn port_bitmap_reflects_removal() { + let mut mc = init(); + mc.domain_create(1).unwrap(); + mc.domain_port_add(1, 0, 0, 0).unwrap(); + mc.domain_port_add(1, 3, 0, 0).unwrap(); + + mc.domain_port_remove(1, 0).unwrap(); + + let bm = mc.port_bitmap(1); + assert_eq!(bm & (1 << 0), 0); + assert_eq!(bm & (1 << 3), 1 << 3); + } + + #[test] + fn groups_are_independent() { + let mut mc = init(); + mc.domain_create(1).unwrap(); + mc.domain_create(2).unwrap(); + mc.domain_port_add(1, 5, 0, 0).unwrap(); + mc.domain_port_add(2, 5, 0, 0).unwrap(); + + mc.domain_port_remove(1, 5).unwrap(); + + assert_eq!(mc.domain_port_count(1).unwrap(), 0); + assert_eq!(mc.domain_port_count(2).unwrap(), 1); + assert_eq!(mc.port_bitmap(2) & (1 << 5), 1 << 5); + } +} diff --git a/asic/src/softnpu/mod.rs b/asic/src/softnpu/mod.rs index cfb497ad..ac7172cd 100644 --- a/asic/src/softnpu/mod.rs +++ b/asic/src/softnpu/mod.rs @@ -28,6 +28,8 @@ use common::ports::{ use softnpu_lib::ManagementRequest; +#[cfg(feature = "multicast")] +pub mod mcast; pub mod mgmt; pub mod table; @@ -105,6 +107,9 @@ pub struct Handle { pub mgmt_config: mgmt::ManagementConfig, update_tx: Mutex>>, + + #[cfg(feature = "multicast")] + mc_data: Mutex, } impl Handle { @@ -130,6 +135,8 @@ impl Handle { ports: Mutex::new(HashMap::new()), mgmt_config, update_tx: Mutex::new(None), + #[cfg(feature = "multicast")] + mc_data: Mutex::new(mcast::init()), }) } @@ -148,46 +155,68 @@ impl Handle { #[cfg(feature = "multicast")] impl AsicMulticastOps for Handle { fn mc_domains(&self) -> Vec { - let len = self.ports.lock().unwrap().len() as u16; - (0..len).collect() + let mc_data = self.mc_data.lock().unwrap(); + mc_data.domains() } - fn mc_port_count(&self, _group_id: u16) -> AsicResult { - Ok(self.ports.lock().unwrap().len()) + fn mc_port_count(&self, group_id: u16) -> AsicResult { + let mc_data = self.mc_data.lock().unwrap(); + mc_data.domain_port_count(group_id) } fn mc_port_add( &self, - _group_id: u16, - _port: u16, - _rid: u16, - _level1_excl_id: u16, + group_id: u16, + port: u16, + rid: u16, + level1_excl_id: u16, ) -> AsicResult<()> { - Err(AsicError::OperationUnsupported) + slog::info!( + self.log, + "adding port {port} to multicast group {group_id}" + ); + let mut mc_data = self.mc_data.lock().unwrap(); + mc_data.domain_port_add(group_id, port, rid, level1_excl_id) } - fn mc_port_remove(&self, _group_id: u16, _port: u16) -> AsicResult<()> { - Ok(()) + fn mc_port_remove(&self, group_id: u16, port: u16) -> AsicResult<()> { + slog::info!( + self.log, + "removing port {port} from multicast group {group_id}" + ); + let mut mc_data = self.mc_data.lock().unwrap(); + mc_data.domain_port_remove(group_id, port) } - fn mc_group_create(&self, _group_id: u16) -> AsicResult<()> { - Err(AsicError::OperationUnsupported) + fn mc_group_create(&self, group_id: u16) -> AsicResult<()> { + slog::info!(self.log, "creating multicast group {group_id}"); + let mut mc_data = self.mc_data.lock().unwrap(); + mc_data.domain_create(group_id) } - fn mc_group_destroy(&self, _group_id: u16) -> AsicResult<()> { - Ok(()) + fn mc_group_destroy(&self, group_id: u16) -> AsicResult<()> { + slog::info!(self.log, "destroying multicast group {group_id}"); + let mut mc_data = self.mc_data.lock().unwrap(); + mc_data.domain_destroy(group_id) } fn mc_groups_count(&self) -> AsicResult { - Ok(self.ports.lock().unwrap().len()) + let mc_data = self.mc_data.lock().unwrap(); + Ok(mc_data.domains_count()) } fn mc_set_max_nodes( &self, - _max_nodes: u32, - _max_link_aggregated_nodes: u32, + max_nodes: u32, + max_link_aggregated_nodes: u32, ) -> AsicResult<()> { - Ok(()) + slog::info!( + self.log, + "setting max nodes to {max_nodes}, \ + max link aggregated nodes to {max_link_aggregated_nodes}" + ); + let mut mc_data = self.mc_data.lock().unwrap(); + mc_data.set_max_nodes(max_nodes, max_link_aggregated_nodes) } } diff --git a/asic/src/softnpu/table.rs b/asic/src/softnpu/table.rs index 56d9c1fe..7e045e30 100644 --- a/asic/src/softnpu/table.rs +++ b/asic/src/softnpu/table.rs @@ -4,6 +4,8 @@ // // Copyright 2026 Oxide Computer Company +use std::hash::Hash; + use slog::{error, trace}; use softnpu_lib::{ManagementRequest, TableAdd, TableRemove}; @@ -14,16 +16,41 @@ use aal::{ }; use common::table::TableType; -/// Represents a handle to a SoftNPU ASIC table. The `id` member corresponds to -/// the table path in the P4 program. Well known sidecar-lite.p4 paths follow -/// below. +// Match field names used by the VLAN validity dispatch. +#[cfg(feature = "multicast")] +const VALID_FIELD: &str = "$valid"; +#[cfg(feature = "multicast")] +const VLAN_ID_FIELD: &str = "vlan_id"; + +// Sidecar-lite untagged table names for VLAN dispatch. +#[cfg(feature = "multicast")] +const MCAST_NAT_V4_UNTAGGED: &str = "ingress.nat.nat_v4_mcast_untagged"; +#[cfg(feature = "multicast")] +const MCAST_NAT_V6_UNTAGGED: &str = "ingress.nat.nat_v6_mcast_untagged"; + +/// Represents a handle to a SoftNPU ASIC table. The `type_` member identifies +/// the table and `softnpu_table_name()` maps it to the corresponding +/// sidecar-lite.p4 path. pub struct Table { type_: TableType, implemented: bool, + /// Alternate sidecar-lite table for untagged entries + /// ([`VALID_FIELD`] is false). x4c does not support `$valid` as + /// a table key, so any table that matches on VLAN validity splits + /// into tagged/untagged variants with per-entry dispatch. + /// [`VLAN_ID_FIELD`] is stripped from the keyset for the untagged + /// table. + /// + /// Currently used by multicast NAT (`nat_v4_mcast` / + /// `nat_v4_mcast_untagged`, `nat_v6_mcast` / + /// `nat_v6_mcast_untagged`). + untagged_id: Option<&'static str>, size: usize, } impl Table { + /// Return the sidecar-lite table name for this table, or `None` if the + /// table is not implemented in the softnpu backend. pub fn softnpu_table_name(&self) -> Option<&'static str> { if self.implemented { match self.type_ { @@ -44,6 +71,32 @@ impl Table { TableType::ArpIpv4 => Some("ingress.resolver.resolver_v4"), TableType::NeighborIpv6 => Some("ingress.resolver.resolver_v6"), TableType::PortMacAddress => Some("ingress.mac.mac_rewrite"), + #[cfg(feature = "multicast")] + TableType::McastIpv6 => { + Some("ingress.mcast.mcast_replication_v6") + } + #[cfg(feature = "multicast")] + TableType::McastIpv4SrcFilter => { + Some("ingress.mcast.mcast_source_filter_v4") + } + #[cfg(feature = "multicast")] + TableType::McastIpv6SrcFilter => { + Some("ingress.mcast.mcast_source_filter_v6") + } + #[cfg(feature = "multicast")] + TableType::NatIngressIpv4Mcast => { + Some("ingress.nat.nat_v4_mcast") + } + #[cfg(feature = "multicast")] + TableType::NatIngressIpv6Mcast => { + Some("ingress.nat.nat_v6_mcast") + } + #[cfg(feature = "multicast")] + TableType::McastEgressDecapPorts => { + Some("egress.tbl_decap_ports") + } + #[cfg(feature = "multicast")] + TableType::PortMacAddressMcast => Some("egress.mcast_src_mac"), _ => panic!( "implemented table {} has no softnpu table", self.type_ @@ -53,16 +106,68 @@ impl Table { None } } + + /// Inspect the VLAN `$valid` match field to route entries to the + /// tagged or untagged sidecar-lite table. Returns the primary table + /// name for tables without a VLAN split. + #[cfg(feature = "multicast")] + fn resolve_vlan_table_id( + &self, + fields: &[MatchEntryField], + ) -> Option<&str> { + if let Some(untagged) = self.untagged_id { + let is_untagged = fields.iter().any(|f| { + f.name == VALID_FIELD + && matches!( + &f.value, + MatchEntryValue::Value(ValueTypes::U64(0)) + ) + }); + if is_untagged { + return Some(untagged); + } + } + self.softnpu_table_name() + } + + /// Filter match fields for sidecar-lite serialization. Strips + /// the `$valid` field (consumed by `resolve_vlan_table_id`) and + /// strips `vlan_id` when targeting the untagged table. + #[cfg(feature = "multicast")] + fn filter_vlan_match_fields( + &self, + fields: Vec, + target_table: &str, + ) -> Vec { + if self.untagged_id.is_none() { + return fields; + } + let is_untagged = self.untagged_id == Some(target_table); + fields + .into_iter() + .filter(|f| { + // $valid is consumed by dispatch, not serialized. + if f.name == VALID_FIELD { + return false; + } + // Untagged table has no vlan_id key. + if is_untagged && f.name == VLAN_ID_FIELD { + return false; + } + true + }) + .collect() + } } -// All tables are defined to be 1024 entries deep +// All tables are defined to be 4096 entries deep. const TABLE_SIZE: usize = 4096; impl TableOps for Table { fn new(hdl: &Handle, type_: TableType) -> AsicResult { - // TODO just mapping sidecar.p4 things onto simplified sidecar-lite.p4 - // things to get started. - let implemented = match type_ { + // Mapping sidecar.p4 table types onto simplified sidecar-lite.p4 + // equivalents. + let (implemented, untagged_id) = match type_ { TableType::RouteIdxIpv4 | TableType::RouteFwdIpv4 | TableType::RouteIdxIpv6 @@ -75,14 +180,28 @@ impl TableOps for Table { | TableType::NatIngressIpv4 | TableType::NatIngressIpv6 | TableType::AttachedSubnetIpv4 - | TableType::AttachedSubnetIpv6 => true, + | TableType::AttachedSubnetIpv6 => (true, None), + #[cfg(feature = "multicast")] + TableType::McastIpv6 + | TableType::McastIpv4SrcFilter + | TableType::McastIpv6SrcFilter + | TableType::McastEgressDecapPorts + | TableType::PortMacAddressMcast => (true, None), + #[cfg(feature = "multicast")] + TableType::NatIngressIpv4Mcast => { + (true, Some(MCAST_NAT_V4_UNTAGGED)) + } + #[cfg(feature = "multicast")] + TableType::NatIngressIpv6Mcast => { + (true, Some(MCAST_NAT_V6_UNTAGGED)) + } x => { - error!(hdl.log, "TABLE NOT HANDLED {x}"); - false + error!(hdl.log, "table not handled: {x}"); + (false, None) } }; - Ok(Table { type_, implemented, size: TABLE_SIZE }) + Ok(Table { type_, implemented, untagged_id, size: TABLE_SIZE }) } fn size(&self) -> usize { @@ -90,7 +209,7 @@ impl TableOps for Table { } fn clear(&self, _hdl: &Handle) -> AsicResult<()> { - //TODO implement in softnpu + // TODO: implement in softnpu Ok(()) } @@ -100,19 +219,33 @@ impl TableOps for Table { key: &M, data: &A, ) -> AsicResult<()> { - let Some(table) = self.softnpu_table_name() else { + let Some(default_table) = self.softnpu_table_name() else { return Ok(()); }; let name = self.type_.to_string(); let match_data = key.key_to_ir().unwrap(); let action_data = data.action_to_ir().unwrap(); + // For tables with VLAN dispatch, resolve which sidecar-lite table + // to target and filter out synthetic match fields. + #[cfg(feature = "multicast")] + let (table, fields) = { + let resolved = self + .resolve_vlan_table_id(&match_data.fields) + .unwrap_or(default_table); + let filtered = + self.filter_vlan_match_fields(match_data.fields, resolved); + (resolved.to_string(), filtered) + }; + #[cfg(not(feature = "multicast"))] + let (table, fields) = (default_table.to_string(), match_data.fields); + trace!(hdl.log, "entry_add called"); trace!(hdl.log, "table: {name}"); - trace!(hdl.log, "match_data:\n{:#?}", match_data); + trace!(hdl.log, "match_data (filtered): {fields:#?}"); trace!(hdl.log, "action_data:\n{:#?}", action_data); - let keyset_data = keyset_data(match_data.fields, self.type_); + let keyset_data = keyset_data(fields, self.type_, &table); let (action, parameter_data) = match ( self.type_, @@ -432,99 +565,107 @@ impl TableOps for Table { | (TableType::NatIngressIpv6, "forward_ipv6_to") | (TableType::AttachedSubnetIpv4, "forward_to_v4") | (TableType::AttachedSubnetIpv6, "forward_to_v6") => { - let mut target = Vec::new(); - let mut vni = Vec::new(); - let mut mac = Vec::new(); - for arg in action_data.args { - match arg.name.as_str() { - "target" => { - // "target" is 128 bits - let mut data: Vec = Vec::new(); - match &arg.value { - ValueTypes::U64(_) => { - // Currently the ValueType is always Ptr - error!( - hdl.log, - "expected ValueType::Ptr, \ - received ValueType::U64" - ); - return Ok(()); - } - ValueTypes::Ptr(v) => { - data.extend_from_slice(v.as_slice()); - } - } - let len = data.len(); - let buf = &mut data[len - 16..]; - buf.reverse(); - target.extend_from_slice(buf); - } - "vni" => { - // "vni" is 24 bits - let mut data: Vec = Vec::new(); - match &arg.value { - ValueTypes::U64(v) => { - data.extend_from_slice(&v.to_le_bytes()); - } - ValueTypes::Ptr(_) => { - // Currently the ValueType is always U64 - error!( - hdl.log, - "expected ValueType::U64, \ - received ValueType::Ptr" - ); - return Ok(()); - } - } - vni.extend_from_slice(&data[0..3]); + forward_to_sled_params(hdl, &name, action_data)? + } + #[cfg(feature = "multicast")] + (TableType::NatIngressIpv4Mcast, "mcast_forward_ipv4_to") + | (TableType::NatIngressIpv6Mcast, "mcast_forward_ipv6_to") => { + forward_to_sled_params(hdl, &name, action_data)? + } + // Multicast source filters: no action parameters. + #[cfg(feature = "multicast")] + (TableType::McastIpv4SrcFilter, "allow_source_mcastv4") + | (TableType::McastIpv6SrcFilter, "allow_source_mcastv6") => { + ("allow_source", Vec::new()) + } + // Multicast replication: translate group IDs to port bitmaps. + // + // DPD sends configure_mcastv6 with (mcast_grp_a, mcast_grp_b, + // rid, level1_excl_id, level2_excl_id). Sidecar-lite expects + // set_port_bitmap with (external, underlay, rid). We look up + // the group membership in McGroupData and build the bitmaps. + #[cfg(feature = "multicast")] + (TableType::McastIpv6, "configure_mcastv6") => { + let mut external_grp: u16 = 0; + let mut underlay_grp: u16 = 0; + let mut rid: u16 = 0; + for arg in action_data.args.iter() { + if let ValueTypes::U64(v) = &arg.value { + match arg.name.as_str() { + "mcast_grp_a" => external_grp = *v as u16, + "mcast_grp_b" => underlay_grp = *v as u16, + "rid" => rid = *v as u16, + _ => {} } - "inner_mac" => { - // "mac" is 48 bits - let mut data: Vec = Vec::new(); - match &arg.value { - ValueTypes::U64(v) => { - data.extend_from_slice(&v.to_le_bytes()); - } - ValueTypes::Ptr(_) => { - // Currently the ValueType is always U64 - error!( - hdl.log, - "expected ValueType::U64, \ - received ValueType::Ptr" - ); - return Ok(()); - } - } - mac.extend_from_slice(&data[0..6]) + } + } + + let mc_data = hdl.mc_data.lock().unwrap(); + let external_bitmap = mc_data.port_bitmap(external_grp); + let underlay_bitmap = mc_data.port_bitmap(underlay_grp); + drop(mc_data); + + let mut params = Vec::new(); + params.extend_from_slice(&external_bitmap.to_le_bytes()); + params.extend_from_slice(&underlay_bitmap.to_le_bytes()); + params.extend_from_slice(&rid.to_le_bytes()); + ("set_port_bitmap", params) + } + // Multicast egress decap: pack 8x32-bit bitmap into 128 bits. + // + // DPD sends set_decap_ports with 8x32-bit bitmap fields + // keyed on RID. Sidecar-lite expects a single 128-bit + // bitmap. We pack the low 4 chunks (ports 0-127) into a + // u128 for sidecar-lite's bit<128> decap_bitmap field. + #[cfg(feature = "multicast")] + (TableType::McastEgressDecapPorts, "set_decap_ports") => { + ("set_decap_ports", pack_decap_bitmap(&action_data)) + } + #[cfg(feature = "multicast")] + (TableType::McastEgressDecapPorts, "set_decap_ports_and_vlan") => { + let mut params = pack_decap_bitmap(&action_data); + let mut vlan_id: u16 = 0; + for arg in action_data.args.iter() { + if let ValueTypes::U64(v) = &arg.value + && arg.name.as_str() == "vlan_id" + { + vlan_id = *v as u16; + } + } + params.extend_from_slice(&vlan_id.to_le_bytes()); + ("set_decap_ports_and_vlan", params) + } + // Multicast egress MAC rewrite. + #[cfg(feature = "multicast")] + (TableType::PortMacAddressMcast, "rewrite") => { + let mut params = Vec::new(); + for arg in action_data.args { + match arg.value { + ValueTypes::U64(v) => { + let mac = v.to_le_bytes(); + params.extend_from_slice(&mac[0..6]); } - _ => { - error!(hdl.log, "unknown argument: {}", arg.name); - return Ok(()); + ValueTypes::Ptr(v) => { + params.extend_from_slice(v.as_slice()); } } } - let mut params = Vec::new(); - // arguments currently don't arrive in the correct order, - // so we'll order them manually - params.extend_from_slice(target.as_slice()); - params.extend_from_slice(vni.as_slice()); - params.extend_from_slice(mac.as_slice()); - ("forward_to_sled", params) + ("rewrite_src_mac", params) } (_, x) => { - error!(hdl.log, "ACTION NOT HANDLED {name} {x}"); + error!(hdl.log, "action not handled: {name} {x}"); return Ok(()); } }; let action = action.to_string(); trace!(hdl.log, "sending request to softnpu"); - trace!(hdl.log, "table: {name}"); + trace!(hdl.log, "table: {table}"); trace!(hdl.log, "action: {:#?}", action); trace!(hdl.log, "keyset_data:\n{:#?}", keyset_data); trace!(hdl.log, "parameter_data:\n{:#?}", parameter_data); let msg = ManagementRequest::TableAdd(TableAdd { - table: table.to_string(), + table, action, keyset_data, parameter_data, @@ -535,27 +676,19 @@ impl TableOps for Table { Ok(()) } - fn entry_update( + fn entry_update( &self, hdl: &Handle, key: &M, data: &A, ) -> AsicResult<()> { - let Some(_table) = self.softnpu_table_name() else { - return Ok(()); - }; - let name = self.type_.to_string(); - - let match_data = key.key_to_ir().unwrap(); - let action_data = data.action_to_ir().unwrap(); - - trace!(hdl.log, "entry_update called"); - trace!(hdl.log, "table: {name}"); - trace!(hdl.log, "match_data:\n{:#?}", match_data); - trace!(hdl.log, "action_data:\n{:#?}", action_data); - - //TODO implement in softnpu - Ok(()) + // Softnpu does not currently support in-place updates. + // Delete the old entry and re-add with the new action data. + // Both operations are currently fire-and-forget over the + // management channel, so neither can fail from DPD's + // perspective. + self.entry_del(hdl, key)?; + self.entry_add(hdl, key, data) } fn entry_del( @@ -563,26 +696,36 @@ impl TableOps for Table { hdl: &Handle, key: &M, ) -> AsicResult<()> { - let Some(table) = self.softnpu_table_name() else { + let Some(default_table) = self.softnpu_table_name() else { return Ok(()); }; let name = self.type_.to_string(); let match_data = key.key_to_ir().unwrap(); + #[cfg(feature = "multicast")] + let (table, fields) = { + let resolved = self + .resolve_vlan_table_id(&match_data.fields) + .unwrap_or(default_table); + let filtered = + self.filter_vlan_match_fields(match_data.fields, resolved); + (resolved.to_string(), filtered) + }; + #[cfg(not(feature = "multicast"))] + let (table, fields) = (default_table.to_string(), match_data.fields); + trace!(hdl.log, "entry_del called"); trace!(hdl.log, "table: {name}"); - trace!(hdl.log, "match_data:\n{:#?}", match_data); + trace!(hdl.log, "match_data (filtered): {fields:#?}"); - let keyset_data = keyset_data(match_data.fields, self.type_); + let keyset_data = keyset_data(fields, self.type_, &table); trace!(hdl.log, "sending request to softnpu"); - trace!(hdl.log, "table: {name}"); + trace!(hdl.log, "table: {table}"); trace!(hdl.log, "keyset_data:\n{:#?}", keyset_data); - let msg = ManagementRequest::TableRemove(TableRemove { - keyset_data, - table: table.to_string(), - }); + let msg = + ManagementRequest::TableRemove(TableRemove { keyset_data, table }); crate::softnpu::mgmt::write(msg, &hdl.mgmt_config); @@ -606,9 +749,127 @@ impl TableOps for Table { } } +/// Extract the forward_to_sled action parameters shared by unicast and +/// multicast NAT/attached-subnet actions. Returns the sidecar-lite action +/// name and serialized parameter bytes. +fn forward_to_sled_params( + hdl: &Handle, + name: &str, + action_data: aal::ActionData, +) -> AsicResult<(&'static str, Vec)> { + let mut target = Vec::new(); + let mut vni = Vec::new(); + let mut mac = Vec::new(); + for arg in action_data.args { + match arg.name.as_str() { + "target" => { + // "target" is 128 bits + let mut data: Vec = Vec::new(); + match &arg.value { + ValueTypes::U64(_) => { + // Currently the ValueType is always Ptr + error!( + hdl.log, + "expected ValueType::Ptr, \ + received ValueType::U64" + ); + return Ok(("forward_to_sled", Vec::new())); + } + ValueTypes::Ptr(v) => { + data.extend_from_slice(v.as_slice()); + } + } + let len = data.len(); + let buf = &mut data[len - 16..]; + buf.reverse(); + target.extend_from_slice(buf); + } + "vni" => { + // "vni" is 24 bits + let mut data: Vec = Vec::new(); + match &arg.value { + ValueTypes::U64(v) => { + data.extend_from_slice(&v.to_le_bytes()); + } + ValueTypes::Ptr(_) => { + // Currently the ValueType is always U64 + error!( + hdl.log, + "expected ValueType::U64, \ + received ValueType::Ptr" + ); + return Ok(("forward_to_sled", Vec::new())); + } + } + vni.extend_from_slice(&data[0..3]); + } + "inner_mac" => { + // "mac" is 48 bits + let mut data: Vec = Vec::new(); + match &arg.value { + ValueTypes::U64(v) => { + data.extend_from_slice(&v.to_le_bytes()); + } + ValueTypes::Ptr(_) => { + // Currently the ValueType is always U64 + error!( + hdl.log, + "expected ValueType::U64, \ + received ValueType::Ptr" + ); + return Ok(("forward_to_sled", Vec::new())); + } + } + mac.extend_from_slice(&data[0..6]) + } + _ => { + error!(hdl.log, "unknown argument: {} in {name}", arg.name); + return Ok(("forward_to_sled", Vec::new())); + } + } + } + let mut params = Vec::new(); + // Arguments currently don't arrive in the correct order, + // so we order them manually. + params.extend_from_slice(target.as_slice()); + params.extend_from_slice(vni.as_slice()); + params.extend_from_slice(mac.as_slice()); + Ok(("forward_to_sled", params)) +} + +/// Pack DPD's 8x32-bit decap port bitmap into a byte vector for +/// sidecar-lite's decap_bitmap field. All 8 chunks are serialized +/// little-endian and the P4 field width determines how many are consumed. +#[cfg(feature = "multicast")] +fn pack_decap_bitmap(args: &aal::ActionData) -> Vec { + let mut chunks = [0u32; 8]; + for arg in &args.args { + if let ValueTypes::U64(v) = &arg.value { + match arg.name.as_str() { + "ports_0" => chunks[0] = *v as u32, + "ports_1" => chunks[1] = *v as u32, + "ports_2" => chunks[2] = *v as u32, + "ports_3" => chunks[3] = *v as u32, + "ports_4" => chunks[4] = *v as u32, + "ports_5" => chunks[5] = *v as u32, + "ports_6" => chunks[6] = *v as u32, + "ports_7" => chunks[7] = *v as u32, + _ => {} + } + } + } + chunks.iter().flat_map(|c| c.to_le_bytes()).collect() +} + /// Extract keys from `match_data` and ensure that they are -/// in a data structure with the correct length -fn keyset_data(match_data: Vec, table: TableType) -> Vec { +/// in a data structure with the correct length. The `table_name` +/// parameter is the resolved sidecar-lite table name, which may +/// differ from the primary name for VLAN-dispatched tables. +fn keyset_data( + match_data: Vec, + table: TableType, + table_name: &str, +) -> Vec { let mut keyset_data: Vec = Vec::new(); for m in match_data { match m.value { @@ -622,7 +883,7 @@ fn keyset_data(match_data: Vec, table: TableType) -> Vec { keyset_data.extend_from_slice(&data[..4]); } TableType::NeighborIpv6 => { - // "nexthop_ipv4" => bit<128> + // "nexthop_ipv6" => bit<128> let mut buf = Vec::new(); serialize_value_type(&x, &mut buf); buf.reverse(); @@ -655,6 +916,53 @@ fn keyset_data(match_data: Vec, table: TableType) -> Vec { buf.reverse(); keyset_data.extend_from_slice(&buf); } + // Multicast replication: hdr.ipv6.dst => bit<128> + #[cfg(feature = "multicast")] + TableType::McastIpv6 => { + let mut buf = Vec::new(); + serialize_value_type(&x, &mut buf); + buf.reverse(); + keyset_data.extend_from_slice(&buf); + } + // Multicast source filter exact keys: inner dst => + // bit<32> or bit<128>. The LPM src key is handled + // in the Lpm arm. + #[cfg(feature = "multicast")] + TableType::McastIpv4SrcFilter => { + serialize_value_type(&x, &mut data); + keyset_data.extend_from_slice(&data[..4]); + } + #[cfg(feature = "multicast")] + TableType::McastIpv6SrcFilter => { + let mut buf = Vec::new(); + serialize_value_type(&x, &mut buf); + buf.reverse(); + keyset_data.extend_from_slice(&buf); + } + // Multicast NAT: dst (bit<32> or bit<128>) and + // optionally vlan_id (bit<12>) after VLAN field + // filtering. The resolved table_name determines + // whether the tagged or untagged variant is used; + // the keyset width is the same for both. + #[cfg(feature = "multicast")] + TableType::NatIngressIpv4Mcast => { + serialize_value_type(&x, &mut data); + keyset_data.extend_from_slice(&data[..4]); + } + #[cfg(feature = "multicast")] + TableType::NatIngressIpv6Mcast => { + let mut buf = Vec::new(); + serialize_value_type(&x, &mut buf); + buf.reverse(); + keyset_data.extend_from_slice(&buf); + } + // Multicast egress tables: port => bit<16> + #[cfg(feature = "multicast")] + TableType::McastEgressDecapPorts + | TableType::PortMacAddressMcast => { + serialize_value_type(&x, &mut data); + keyset_data.extend_from_slice(&data[..2]); + } _ => { serialize_value_type(&x, &mut keyset_data); } @@ -663,6 +971,8 @@ fn keyset_data(match_data: Vec, table: TableType) -> Vec { // Longest prefix MatchEntryValue::Lpm(x) => { let mut data: Vec = Vec::new(); + #[allow(unused_mut)] + let mut handled = false; match table { TableType::RouteIdxIpv4 | TableType::AttachedSubnetIpv4 => { // prefix for longest prefix match operation @@ -670,19 +980,29 @@ fn keyset_data(match_data: Vec, table: TableType) -> Vec { serialize_value_type_be(&x.prefix, &mut data); keyset_data.extend_from_slice(&data[data.len() - 4..]); // prefix length for longest prefix match operation - keyset_data.push(x.len as u8) + keyset_data.push(x.len as u8); + handled = true; } - _ => { - serialize_value_type_be(&x.prefix, &mut keyset_data); + #[cfg(feature = "multicast")] + TableType::McastIpv4SrcFilter => { + // "src_addr" => bit<32> lpm + serialize_value_type_be(&x.prefix, &mut data); + keyset_data.extend_from_slice(&data[data.len() - 4..]); keyset_data.push(x.len as u8); + handled = true; } + _ => {} + } + if !handled { + serialize_value_type_be(&x.prefix, &mut keyset_data); + keyset_data.push(x.len as u8); } } // Ranges (i.e. port ranges) MatchEntryValue::Range(x) => { match table { TableType::NatIngressIpv4 | TableType::NatIngressIpv6 => { - // "l4_dst_port" => ingress.nat_id: range => bit<16> + // "l4_dst_port" => ingress.nat_id: range => bit<16> let low = &x.low.to_le_bytes(); let high = &x.high.to_le_bytes(); keyset_data.extend_from_slice(&low[..2]); @@ -701,6 +1021,7 @@ fn keyset_data(match_data: Vec, table: TableType) -> Vec { } } } + let _ = table_name; keyset_data } diff --git a/common/src/illumos.rs b/common/src/illumos.rs index 5eca9c2b..095663bf 100644 --- a/common/src/illumos.rs +++ b/common/src/illumos.rs @@ -2,7 +2,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/ // -// Copyright 2025 Oxide Computer Company +// Copyright 2026 Oxide Computer Company //! Illumos-specific common modules and operations. diff --git a/dpd-client/tests/integration_tests/mcast.rs b/dpd-client/tests/integration_tests/mcast.rs index 1a97c6e2..c022d933 100644 --- a/dpd-client/tests/integration_tests/mcast.rs +++ b/dpd-client/tests/integration_tests/mcast.rs @@ -6687,6 +6687,100 @@ async fn test_source_filter_update_to_any() -> TestResult { "After update with Any, should have only 1 entry (the /0)" ); + // Re-narrow to specific sources for a (S,G) -> (*,G) transition, which is + // exercised below with `sources: None`. + let narrow_entry = types::MulticastGroupUpdateExternalEntry { + internal_forwarding: types::InternalForwarding { + nat_target: Some(nat_target.clone()), + }, + external_forwarding: types::ExternalForwarding { vlan_id: Some(10) }, + sources: Some(vec![ + types::IpSrc::Exact("192.168.1.1".parse().unwrap()), + types::IpSrc::Exact("192.168.1.2".parse().unwrap()), + ]), + }; + + let narrowed = switch + .client + .multicast_group_update_external( + &external_group_ip, + &make_tag(TEST_TAG), + &narrow_entry, + ) + .await + .expect("Should re-narrow external group sources") + .into_inner(); + + assert_eq!( + narrowed.sources.as_deref().map(<[_]>::len), + Some(2), + "Re-narrow should restore 2 specific sources" + ); + + let after_narrow_table = switch + .client + .table_dump(SOURCE_FILTER_IPV4_TABLE, false) + .await + .expect("Should dump table after re-narrow"); + + assert_eq!( + after_narrow_table.entries.len(), + baseline_count + 2, + "Re-narrow should restore 2 specific entries" + ); + + // Widen via `sources: None`. This is the path Omicron's RPW takes for ASM + // groups when an "any source" member joins. + let widen_entry = types::MulticastGroupUpdateExternalEntry { + internal_forwarding: types::InternalForwarding { + nat_target: Some(nat_target.clone()), + }, + external_forwarding: types::ExternalForwarding { vlan_id: Some(10) }, + sources: None, + }; + + let widened = switch + .client + .multicast_group_update_external( + &external_group_ip, + &make_tag(TEST_TAG), + &widen_entry, + ) + .await + .expect("Should widen external group sources to None") + .into_inner(); + + assert_eq!( + widened.sources, None, + "sources: None must clear the in-memory record" + ); + + // Round-trip via GET to confirm the stored representation matches. + let fetched = switch + .client + .multicast_group_get(&external_group_ip) + .await + .expect("Should get external group") + .into_inner(); + + assert_eq!( + get_sources(&fetched), + None, + "GET after widen-via-None must agree with the update response" + ); + + let after_widen_table = switch + .client + .table_dump(SOURCE_FILTER_IPV4_TABLE, false) + .await + .expect("Should dump table after widen-via-None"); + + assert_eq!( + after_widen_table.entries.len(), + baseline_count + 1, + "After widen with None, should have only the /0 entry" + ); + // Cleanup in correct order: external first, then internal cleanup_test_group(switch, external_group_ip, TEST_TAG).await.unwrap(); cleanup_test_group(switch, internal_group_ip, TEST_TAG).await diff --git a/dpd/src/mcast/mod.rs b/dpd/src/mcast/mod.rs index 3ac32330..6a2b171c 100644 --- a/dpd/src/mcast/mod.rs +++ b/dpd/src/mcast/mod.rs @@ -723,9 +723,15 @@ pub(crate) fn modify_group_external( // VLAN is assigned directly -> `Some(x)` sets VLAN, `None` removes VLAN updated_group.ext_fwding.vlan_id = new_group_info.external_forwarding.vlan_id; - updated_group.sources = canonicalize_sources( - new_group_info.sources.clone().or(updated_group.sources), - ); + + // Mirror the canonicalization that `update_external_tables` above used to + // rewrite the P4 source-filter entries. Earlier revisions wrote back + // `new.sources.or(existing)` here, so a `None` update cleared the P4 + // table but left the in-memory `Some([...])` stale; the GET response + // then disagreed with the hardware/softnpu state and callers driving + // `(S,G) -> (*,G)` transitions saw perpetual drift. + updated_group.sources = + canonicalize_sources(new_group_info.sources.clone()); // Update bitmap tables with new VLAN if VLAN changed // Also, handles possible membership skew between update internal + external calls. diff --git a/packet/src/eth.rs b/packet/src/eth.rs index 0cff359a..4cbdca8d 100644 --- a/packet/src/eth.rs +++ b/packet/src/eth.rs @@ -2,7 +2,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/ // -// Copyright 2025 Oxide Computer Company +// Copyright 2026 Oxide Computer Company use std::fmt; diff --git a/swadm/src/main.rs b/swadm/src/main.rs index b5fd1221..69274944 100644 --- a/swadm/src/main.rs +++ b/swadm/src/main.rs @@ -212,7 +212,7 @@ async fn build_info(client: &Client) -> anyhow::Result<()> { fn main() -> anyhow::Result<()> { oxide_tokio_rt::run_builder( - &mut oxide_tokio_rt::Builder::new_current_thread(), + oxide_tokio_rt::Builder::new_current_thread(), main_impl(), ) } From f27f612dde317f2f05716c761a14500e46581ff7 Mon Sep 17 00:00:00 2001 From: Zeeshan Lakhani Date: Tue, 2 Jun 2026 02:02:58 +0000 Subject: [PATCH 3/5] [swadm,multicast] add multicast inspection subcommand This adds a read-only `swadm multicast` subcommand (aliased as `mcast` as well) for inspecting the multicast groups programmed on the switch: - `multicast list [-t ]` renders all groups as a table. - `multicast get ` shows one group's full config. Formatting follows existing swadm conventions. To test varying display points of interest, this includes an `#[ignore]`d integration test, run by the `packet-test` CI job, that seeds groups via `dpd-client` and asserts each display branch. --- .github/buildomat/packet-test-common.sh | 4 +- swadm/Cargo.toml | 3 + swadm/src/main.rs | 9 + swadm/src/multicast.rs | 259 +++++++++++++++++++++ swadm/tests/counters.rs | 4 +- swadm/tests/multicast.rs | 293 ++++++++++++++++++++++++ 6 files changed, 568 insertions(+), 4 deletions(-) create mode 100644 swadm/src/multicast.rs create mode 100644 swadm/tests/multicast.rs diff --git a/.github/buildomat/packet-test-common.sh b/.github/buildomat/packet-test-common.sh index 06bd5eea..f2214a36 100755 --- a/.github/buildomat/packet-test-common.sh +++ b/.github/buildomat/packet-test-common.sh @@ -11,7 +11,6 @@ STARTUP_TIMEOUT=${STARTUP_TIMEOUT:=120} BUILD_FEATURES=tofino_asic CODEGEN_FEATURES=--multicast -SWADM_FEATURES="--features=multicast" function cleanup { set +o errexit @@ -100,9 +99,10 @@ DENDRITE_TEST_HOST='[::1]' \ DENDRITE_TEST_VERBOSITY=3 \ cargo test \ --no-fail-fast \ - $SWADM_FEATURES \ --test \ counters \ + --test \ + multicast \ -- \ --ignored diff --git a/swadm/Cargo.toml b/swadm/Cargo.toml index 064df30c..bd2a9db1 100644 --- a/swadm/Cargo.toml +++ b/swadm/Cargo.toml @@ -25,3 +25,6 @@ reqwest.workspace = true slog.workspace = true tabwriter.workspace = true tokio.workspace = true + +[dev-dependencies] +tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } diff --git a/swadm/src/main.rs b/swadm/src/main.rs index 69274944..3bae8a0d 100644 --- a/swadm/src/main.rs +++ b/swadm/src/main.rs @@ -23,6 +23,7 @@ mod attached; mod compliance; mod counters; mod link; +mod multicast; mod nat; mod route; mod snapshot; @@ -66,6 +67,11 @@ enum Commands { #[command(subcommand)] cmd: nat::Nat, }, + #[clap(visible_alias = "mcast")] + Multicast { + #[command(subcommand)] + cmd: multicast::Multicast, + }, #[clap(visible_alias = "attsub")] AttachedSubnet { #[command(subcommand)] @@ -243,6 +249,9 @@ async fn main_impl() -> anyhow::Result<()> { attached::attsub_cmd(&client, p).await } Commands::Nat { cmd: p } => nat::nat_cmd(&client, p).await, + Commands::Multicast { cmd } => { + multicast::multicast_cmd(&client, cmd).await + } Commands::Counters { cmd: c } => counters::ctrs_cmd(&client, c).await, Commands::SwitchPort { cmd: p } => { switchport::switch_cmd(&client, p).await diff --git a/swadm/src/multicast.rs b/swadm/src/multicast.rs new file mode 100644 index 00000000..b837ff32 --- /dev/null +++ b/swadm/src/multicast.rs @@ -0,0 +1,259 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/ +// +// Copyright 2026 Oxide Computer Company + +use std::io::{Write, stdout}; +use std::net::IpAddr; + +use anyhow::Context; +use clap::Subcommand; +use colored::Colorize; +use futures::stream::TryStreamExt; +use tabwriter::TabWriter; + +use dpd_client::{Client, types}; + +#[derive(Debug, Subcommand)] +/// Inspect the multicast groups programmed on the switch. +pub enum Multicast { + /// List multicast groups, optionally filtered by tag. + #[clap(visible_alias = "ls")] + List { + /// Limit the listing to groups carrying the given tag. + #[clap(short = 't')] + tag: Option, + }, + /// Show the full configuration of a single multicast group. + Get { + /// Group IP address (IPv4, external IPv6, or underlay IPv6). + group_ip: IpAddr, + }, +} + +/// Lowercase label for a member's replication direction, matching the +/// lowercase `KIND` column. +fn direction_label(direction: &types::Direction) -> &'static str { + match direction { + types::Direction::Underlay => "underlay", + types::Direction::External => "external", + } +} + +/// Render the members of an underlay group as a compact `port/link(dir)` list. +fn members_summary(members: &[types::MulticastGroupMember]) -> String { + if members.is_empty() { + return "-".to_string(); + } + members + .iter() + .map(|member| { + format!( + "{}/{}({})", + member.port_id, + *member.link_id, + direction_label(&member.direction) + ) + }) + .collect::>() + .join(" ") +} + +/// Render the NAT target, VLAN, and source filter of an external group. +fn external_summary( + internal_forwarding: &types::InternalForwarding, + external_forwarding: &types::ExternalForwarding, + sources: Option<&Vec>, +) -> String { + let nat = match &internal_forwarding.nat_target { + Some(t) => format!("nat={}", t.internal_ip), + None => "nat=-".to_string(), + }; + let vlan = match external_forwarding.vlan_id { + Some(v) => format!("vlan={v}"), + None => "vlan=-".to_string(), + }; + let srcs = match sources { + Some(s) if !s.is_empty() => { + let list = s + .iter() + .map(|src| match src { + types::IpSrc::Exact(ip) => ip.to_string(), + types::IpSrc::Any => "any".to_string(), + }) + .collect::>() + .join(","); + format!("src={list}") + } + _ => "src=any".to_string(), + }; + format!("{nat} {vlan} {srcs}") +} + +async fn multicast_list( + client: &Client, + tag: Option, +) -> anyhow::Result<()> { + let groups: Vec = match &tag { + Some(tag) => { + let tag = tag + .parse::() + .map_err(|e| anyhow::anyhow!("invalid multicast tag: {e}"))?; + client + .multicast_groups_list_by_tag_stream(&tag, None) + .try_collect() + .await + .context("failed to list multicast groups by tag")? + } + None => client + .multicast_groups_list_stream(None) + .try_collect() + .await + .context("failed to list multicast groups")?, + }; + + let mut tw = TabWriter::new(stdout()); + writeln!( + &mut tw, + "{}\t{}\t{}\t{}\t{}\t{}", + "GROUP IP".underline(), + "KIND".underline(), + "EXT GROUP ID".underline(), + "UL GROUP ID".underline(), + "TAG".underline(), + "DETAIL".underline(), + )?; + + for group in &groups { + match group { + types::MulticastGroupResponse::Underlay { + group_ip, + external_group_id, + underlay_group_id, + tag, + members, + } => writeln!( + &mut tw, + "{}\tunderlay\t{}\t{}\t{}\t{}", + group_ip, + external_group_id, + underlay_group_id, + tag, + members_summary(members), + )?, + types::MulticastGroupResponse::External { + group_ip, + external_group_id, + tag, + internal_forwarding, + external_forwarding, + sources, + } => writeln!( + &mut tw, + "{}\texternal\t{}\t-\t{}\t{}", + group_ip, + external_group_id, + tag, + external_summary( + internal_forwarding, + external_forwarding, + sources.as_ref(), + ), + )?, + } + } + + tw.flush()?; + Ok(()) +} + +async fn multicast_get( + client: &Client, + group_ip: IpAddr, +) -> anyhow::Result<()> { + let group = client + .multicast_group_get(&group_ip) + .await + .with_context(|| format!("failed to get multicast group {group_ip}"))? + .into_inner(); + + match group { + types::MulticastGroupResponse::Underlay { + group_ip, + external_group_id, + underlay_group_id, + tag, + members, + } => { + println!("Group IP: {group_ip}"); + println!("Kind: underlay"); + println!("External group ID: {external_group_id}"); + println!("Underlay group ID: {underlay_group_id}"); + println!("Tag: {tag}"); + println!("Members:"); + if members.is_empty() { + println!(" (none)"); + } + for member in &members { + println!( + " {}/{} ({})", + member.port_id, + *member.link_id, + direction_label(&member.direction) + ); + } + } + types::MulticastGroupResponse::External { + group_ip, + external_group_id, + tag, + internal_forwarding, + external_forwarding, + sources, + } => { + println!("Group IP: {group_ip}"); + println!("Kind: external"); + println!("External group ID: {external_group_id}"); + println!("Tag: {tag}"); + match &internal_forwarding.nat_target { + Some(t) => println!( + "NAT target: {} (mac {}, vni {})", + t.internal_ip, t.inner_mac, *t.vni, + ), + None => println!("NAT target: (none)"), + } + match external_forwarding.vlan_id { + Some(v) => println!("VLAN: {v}"), + None => println!("VLAN: (none)"), + } + print!("Sources: "); + match &sources { + Some(s) if !s.is_empty() => { + let list = s + .iter() + .map(|src| match src { + types::IpSrc::Exact(ip) => ip.to_string(), + types::IpSrc::Any => "any".to_string(), + }) + .collect::>() + .join(", "); + println!("{list}"); + } + _ => println!("any"), + } + } + } + + Ok(()) +} + +pub async fn multicast_cmd( + client: &Client, + cmd: Multicast, +) -> anyhow::Result<()> { + match cmd { + Multicast::List { tag } => multicast_list(client, tag).await, + Multicast::Get { group_ip } => multicast_get(client, group_ip).await, + } +} diff --git a/swadm/tests/counters.rs b/swadm/tests/counters.rs index f01f7e15..7a4ab85e 100644 --- a/swadm/tests/counters.rs +++ b/swadm/tests/counters.rs @@ -48,10 +48,10 @@ fn test_p4_counter_list() { ]; #[cfg(not(feature = "multicast"))] - let multicast_counters = Vec::new(); + let multicast_counters: [&str; 0] = []; #[cfg(feature = "multicast")] - let multicast_counters = vec![ + let multicast_counters = [ "Egress_Drop_Port", "Egress_Drop_Reason", "Unicast", diff --git a/swadm/tests/multicast.rs b/swadm/tests/multicast.rs new file mode 100644 index 00000000..a6250ab9 --- /dev/null +++ b/swadm/tests/multicast.rs @@ -0,0 +1,293 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/ +// +// Copyright 2026 Oxide Computer Company + +//! Integration test for the `swadm multicast` inspection subcommand. +//! +//! Seeds groups via `dpd-client`, then asserts `multicast list` / `multicast +//! get` render them correctly. Needs a running dpd, so it is `#[ignore]`d. + +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; +use std::process::Command; + +use common::network::MacAddr; +use dpd_client::{Client, ClientState, default_port, types}; + +// Path to the `swadm` executable. +const SWADM: &str = env!("CARGO_BIN_EXE_swadm"); +const HOST: &str = "[::1]"; +const TEST_TAG: &str = "swadm_multicast_test"; + +// External IPv4 group. +// +// External groups carry no members, only a NAT target. +// An SSM address (232.0.0.0/8) is used so the source filter is well-formed. +const EXT_IPV4: Ipv4Addr = Ipv4Addr::new(232, 123, 45, 99); +const EXT_VLAN: u16 = 10; +const EXT_SOURCE: Ipv4Addr = Ipv4Addr::new(10, 0, 0, 1); +// NAT target VNI (matches omicron's `DEFAULT_MULTICAST_VNI`). +const EXT_VNI: u32 = 77; +// A second external group on an ASM address (224.0.0.0/4) with no VLAN or +// source filter. Exercises the absent-VLAN and any-source display branches. +const EXT_IPV4_ASM: Ipv4Addr = Ipv4Addr::new(224, 0, 1, 50); + +// Admin-local underlay group (ff04::/64) and the NAT target the external group +// forwards to. +const UNDERLAY_IPV6: Ipv6Addr = Ipv6Addr::new(0xff04, 0, 0, 0, 0, 0, 0, 1); +const NAT_IP: Ipv6Addr = UNDERLAY_IPV6; +// A second underlay group with no members just to exercise the empty-member +// display branches ("-" in the list, "(none)" in get). +const UNDERLAY_IPV6_EMPTY: Ipv6Addr = + Ipv6Addr::new(0xff04, 0, 0, 0, 0, 0, 0, 2); + +fn swadm() -> Command { + let mut cmd = Command::new(SWADM); + cmd.arg("--host").arg(HOST); + cmd +} + +fn client() -> Client { + let log = slog::Logger::root(slog::Discard, slog::o!()); + let state = ClientState { tag: String::from("swadm-mcast-test"), log }; + Client::new(&format!("http://{HOST}:{}", default_port()), state) +} + +/// Run `swadm` with the given args, asserting success and returning stdout. +fn run_ok(args: &[&str]) -> String { + let output = swadm() + .args(args) + .output() + .unwrap_or_else(|e| panic!("failed to execute swadm {args:?}: {e}")); + assert!( + output.status.success(), + "swadm {args:?} failed: {}", + String::from_utf8_lossy(&output.stderr), + ); + String::from_utf8_lossy(&output.stdout).into_owned() +} + +#[tokio::test] +#[ignore] +async fn test_multicast_list_and_get() { + let client = client(); + + client.multicast_reset().await.expect("failed to reset multicast groups"); + + // Seed a bifurcated underlay group with one underlay-replication member + // and one external-replication member, both on the first available link, + // so both direction labels are exercised in the rendered output. + let links = client.link_list_all(None).await.expect("failed to list links"); + let link = links + .into_inner() + .into_iter() + .next() + .expect("no links available to seed an underlay member"); + let member_path = format!("{}/{}", link.port_id, *link.link_id); + let underlay_member = types::MulticastGroupMember { + port_id: link.port_id.clone(), + link_id: link.link_id, + direction: types::Direction::Underlay, + }; + let external_member = types::MulticastGroupMember { + port_id: link.port_id.clone(), + link_id: link.link_id, + direction: types::Direction::External, + }; + + client + .multicast_group_create_underlay( + &types::MulticastGroupCreateUnderlayEntry { + group_ip: types::UnderlayMulticastIpv6(UNDERLAY_IPV6), + tag: Some(TEST_TAG.to_string()), + members: vec![underlay_member, external_member], + }, + ) + .await + .expect("failed to create underlay group"); + + // Seed a second underlay group with no members. + client + .multicast_group_create_underlay( + &types::MulticastGroupCreateUnderlayEntry { + group_ip: types::UnderlayMulticastIpv6(UNDERLAY_IPV6_EMPTY), + tag: Some(TEST_TAG.to_string()), + members: vec![], + }, + ) + .await + .expect("failed to create empty underlay group"); + + // Seed an external IPv4 group that NATs to the underlay group: VLAN-tagged + // on egress and filtered on a single source. + client + .multicast_group_create_external( + &types::MulticastGroupCreateExternalEntry { + group_ip: IpAddr::V4(EXT_IPV4), + tag: Some(TEST_TAG.to_string()), + internal_forwarding: types::InternalForwarding { + nat_target: Some(types::NatTarget { + internal_ip: NAT_IP, + inner_mac: MacAddr::new( + 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, + ) + .into(), + vni: EXT_VNI.into(), + }), + }, + external_forwarding: types::ExternalForwarding { + vlan_id: Some(EXT_VLAN), + }, + sources: Some(vec![types::IpSrc::Exact(IpAddr::V4( + EXT_SOURCE, + ))]), + }, + ) + .await + .expect("failed to create external group"); + + // Seed a second external group on an ASM address with no VLAN or source + // filter. External groups always require a NAT target, so this one keeps + // the NAT target but drops the VLAN and sources. + client + .multicast_group_create_external( + &types::MulticastGroupCreateExternalEntry { + group_ip: IpAddr::V4(EXT_IPV4_ASM), + tag: Some(TEST_TAG.to_string()), + internal_forwarding: types::InternalForwarding { + nat_target: Some(types::NatTarget { + internal_ip: NAT_IP, + inner_mac: MacAddr::new( + 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, + ) + .into(), + vni: EXT_VNI.into(), + }), + }, + external_forwarding: types::ExternalForwarding { + vlan_id: None, + }, + sources: None, + }, + ) + .await + .expect("failed to create ASM external group"); + + // `multicast list` should render both groups. + let list = run_ok(&["multicast", "list"]); + // The header carries the "spelled-out" column labels (this also guards + // against a silent rename of the "GROUP ID" columns). + for header in + ["GROUP IP", "KIND", "EXT GROUP ID", "UL GROUP ID", "TAG", "DETAIL"] + { + assert!(list.contains(header), "list missing {header} header:\n{list}"); + } + assert!( + list.contains(&UNDERLAY_IPV6.to_string()) && list.contains("underlay"), + "list missing underlay group:\n{list}" + ); + // Both member directions render as `port/link(dir)` in the DETAIL column. + assert!( + list.contains(&format!("{member_path}(underlay)")) + && list.contains(&format!("{member_path}(external)")), + "list missing bifurcated members {member_path}:\n{list}" + ); + // The member-less underlay group renders "-" for its (empty) DETAIL. + let empty_row = list + .lines() + .find(|line| line.contains(&UNDERLAY_IPV6_EMPTY.to_string())) + .unwrap_or_else(|| { + panic!("list missing empty underlay group:\n{list}") + }); + assert!( + empty_row.contains("underlay") && empty_row.trim_end().ends_with('-'), + "empty underlay row missing \"-\" detail: {empty_row:?}" + ); + assert!( + list.contains(&EXT_IPV4.to_string()) && list.contains("external"), + "list missing external group:\n{list}" + ); + // The fully-populated external group renders its NAT target, VLAN, and + // source filter. + assert!( + list.contains(&format!("nat={NAT_IP}")) + && list.contains("vlan=10") + && list.contains(&format!("src={EXT_SOURCE}")), + "list missing external forwarding detail:\n{list}" + ); + // The ASM group exercises the absent-VLAN and any-source branches. + assert!( + list.contains(&EXT_IPV4_ASM.to_string()) + && list.contains("vlan=-") + && list.contains("src=any"), + "list missing ASM external empty-detail branches:\n{list}" + ); + + // Tag filtering should include our groups... + let by_tag = run_ok(&["multicast", "list", "-t", TEST_TAG]); + assert!( + by_tag.contains(&EXT_IPV4.to_string()), + "tag-filtered list missing external group:\n{by_tag}" + ); + // ...and a non-matching tag should return only the header. + let other = run_ok(&["multicast", "list", "-t", "no_such_tag"]); + assert!( + !other.contains(&EXT_IPV4.to_string()) + && !other.contains(&UNDERLAY_IPV6.to_string()), + "non-matching tag returned groups:\n{other}" + ); + + // `multicast get` on the external group should show its detail, including + // the aligned key/value labels and the fully-populated NAT/VLAN/source + // values. + let ext = run_ok(&["multicast", "get", &EXT_IPV4.to_string()]); + for label in ["Group IP:", "Kind:", "NAT target:", "VLAN:", "Sources:"] { + assert!(ext.contains(label), "get external missing {label}:\n{ext}"); + } + assert!(ext.contains("external"), "get external missing kind:\n{ext}"); + assert!( + ext.contains(&NAT_IP.to_string()) && ext.contains("vni 77"), + "get external missing NAT target detail:\n{ext}" + ); + assert!(ext.contains("VLAN: 10"), "get external vlan:\n{ext}"); + assert!( + ext.contains(&EXT_SOURCE.to_string()), + "get external missing source:\n{ext}" + ); + + // `multicast get` on the ASM group should show the "(none)"/"any" branches + // for an absent VLAN and source filter. + let asm = run_ok(&["multicast", "get", &EXT_IPV4_ASM.to_string()]); + assert!( + asm.contains("VLAN: (none)"), + "get ASM missing absent VLAN:\n{asm}" + ); + assert!( + asm.contains("Sources: any"), + "get ASM missing any-source branch:\n{asm}" + ); + + // `multicast get` on the underlay group should list both members, one per + // replication direction, as `port/link (dir)`. + let underlay = run_ok(&["multicast", "get", &UNDERLAY_IPV6.to_string()]); + assert!( + underlay.contains("underlay"), + "get underlay missing kind:\n{underlay}" + ); + assert!( + underlay.contains(&format!("{member_path} (underlay)")) + && underlay.contains(&format!("{member_path} (external)")), + "get underlay missing bifurcated members {member_path}:\n{underlay}" + ); + + // `multicast get` on the member-less underlay group shows the "(none)" + // members branch. + let empty = run_ok(&["multicast", "get", &UNDERLAY_IPV6_EMPTY.to_string()]); + assert!( + empty.contains("Members:") && empty.contains("(none)"), + "get empty underlay missing \"(none)\" members:\n{empty}" + ); + + client.multicast_reset().await.expect("failed to reset multicast groups"); +} From 945b55ab8ca6ca28fb61cfaa3968aa5c1a0c277b Mon Sep 17 00:00:00 2001 From: Zeeshan Lakhani Date: Sat, 20 Jun 2026 13:44:02 +0000 Subject: [PATCH 4/5] [softnpu,multicast] refresh replication bitmap on membership change SoftNPU bakes multicast port bitmaps into replication table entries at write time, unlike Tofino, where membership lives in the hardware PRE and can change independently of match-action state. A membership change, therefore, requires rewriting the affected replication entries. Includes: *asic/SoftNPU*: - We now track live replication entries per group and rewrite them when members are added or removed. The replication tracking lock is held across the snapshot and table writes so concurrent refreshes cannot interleave. - We implement a TODO -> `Table::clear`. The management protocol has no clear message, so we emulate one by dumping table state and removing entries individually, covering the tagged and untagged VLAN variants. - A fix for management reads: accumulate UART responses until the newline terminator rather than assuming a single 1024-byte read, and size the UDS receive buffer past any expected dump, since datagram recv truncates silently. *dpd/p4*: - Fix egress multicast counter attribution. The external arm previously preceded the underlay tag check and matched on every encapsulated replica, leaving `underlay_mcast_ctr` unreachable. Underlay attribution now keys on the UNDERLAY and UNDERLAY_EXTERNAL tags, and the external arm is the residual case because external deliveries are decapsulated before the counters run and never carry geneve in that path. *dpd/mcast*: - During external-group rollback, we now delete bitmap and replication entries only for internal groups; those entries are only extant for internal groups. External groups share the internal group's IDs, so deleting unconditionally would destroy the live internal group's entries. --- asic/src/softnpu/mcast.rs | 333 +++++++++++++++----- asic/src/softnpu/mgmt.rs | 17 +- asic/src/softnpu/mod.rs | 71 ++++- asic/src/softnpu/table.rs | 270 +++++++++++++--- dpd-client/tests/integration_tests/mcast.rs | 285 +++++++++++++++++ dpd/p4/sidecar.p4 | 12 +- dpd/src/mcast/rollback.rs | 44 +-- 7 files changed, 891 insertions(+), 141 deletions(-) diff --git a/asic/src/softnpu/mcast.rs b/asic/src/softnpu/mcast.rs index 76dd0f78..4eebcb79 100644 --- a/asic/src/softnpu/mcast.rs +++ b/asic/src/softnpu/mcast.rs @@ -4,20 +4,59 @@ // // Copyright 2026 Oxide Computer Company -//! In-memory multicast group tracking for the [softnpu] backend. +//! In-memory multicast group tracking for the [SoftNPU] backend. //! //! Sidecar-lite handles packet replication via port bitmaps in the P4 //! pipeline, so this module only needs to track group membership for //! the `AsicMulticastOps` contract. //! -//! [softnpu]: https://github.com/oxidecomputer/softnpu +//! [SoftNPU]: https://github.com/oxidecomputer/softnpu use std::collections::{HashMap, HashSet}; use aal::{AsicError, AsicResult}; +/// Sidecar-lite replication table that holds the baked underlay/external +/// port bitmaps for an underlay multicast group. +pub const MCAST_REPLICATION_V6_TABLE: &str = + "ingress.mcast.mcast_replication_v6"; + +/// Sidecar-lite action that writes the external and underlay port bitmaps +/// (and replication id) for a [`MCAST_REPLICATION_V6_TABLE`] entry. +pub const SET_PORT_BITMAP_ACTION: &str = "set_port_bitmap"; + +/// A replication table entry's group references, recorded so that a later +/// membership change can recompute port bitmaps. +/// +/// On the production sidecar, the replication table holds a group identifier, +/// and the replication engine tracks membership live, so a port addition needs +/// no table rewrite. The SoftNPU pipeline, instead, bakes a generic port bitmap +/// data structure into the table entry at write time, so each membership change +/// must rewrite every entry that references the affected group. +#[derive(Clone, Copy, Debug)] +struct ReplEntryRef { + external_grp: u16, + underlay_grp: u16, + rid: u16, +} + +/// A replication entry refreshed against current membership, ready to be +/// rewritten into the SoftNPU pipeline. +#[derive(Clone, Debug)] +pub struct RefreshedReplEntry { + /// Match keyset bytes identifying the table entry (the underlay group's + /// destination address). + pub keyset: Vec, + pub external_bitmap: u128, + pub underlay_bitmap: u128, + pub rid: u16, +} + pub struct McGroupData { groups: HashMap>, + /// Replication table entries keyed on their matched keyset, recorded at + /// table-write time so membership changes can refresh bitmaps. + repl_entries: HashMap, ReplEntryRef>, } fn no_group(group_id: u16) -> AsicError { @@ -125,6 +164,59 @@ impl McGroupData { self.groups.len() } + /// Record a replication table entry's group references at table-write + /// time so a later membership change can refresh its bitmaps. + pub fn record_repl_entry( + &mut self, + keyset: Vec, + external_grp: u16, + underlay_grp: u16, + rid: u16, + ) { + self.repl_entries + .insert(keyset, ReplEntryRef { external_grp, underlay_grp, rid }); + } + + /// Forget a replication table entry when it is removed from the pipeline. + pub fn forget_repl_entry(&mut self, keyset: &[u8]) { + self.repl_entries.remove(keyset); + } + + /// Forget every tracked replication entry when the replication table is + /// cleared, so a later membership change for a reused group identifier + /// cannot resurrect a stale bitmap from a forgotten keyset. + pub fn clear_repl_entries(&mut self) { + self.repl_entries.clear(); + } + + /// Recompute the bitmaps for every replication entry that references + /// `group_id` in either its external or underlay slot. + /// + /// This closes the impedance mismatch between the sidecar's production + /// replication engine and SoftNPU's snapshot-at-write-time bitmaps. + /// + /// # Returns + /// + /// The refreshed [`RefreshedReplEntry`] values for the caller to rewrite + /// into the SoftNPU pipeline. + pub fn refresh_params_for_group( + &self, + group_id: u16, + ) -> Vec { + self.repl_entries + .iter() + .filter(|(_, e)| { + e.external_grp == group_id || e.underlay_grp == group_id + }) + .map(|(keyset, e)| RefreshedReplEntry { + keyset: keyset.clone(), + external_bitmap: self.port_bitmap(e.external_grp), + underlay_bitmap: self.port_bitmap(e.underlay_grp), + rid: e.rid, + }) + .collect() + } + /// Validate that the current group count does not exceed the limit. pub fn set_max_nodes( &mut self, @@ -143,7 +235,7 @@ impl McGroupData { } pub fn init() -> McGroupData { - McGroupData { groups: HashMap::new() } + McGroupData { groups: HashMap::new(), repl_entries: HashMap::new() } } #[cfg(test)] @@ -152,72 +244,72 @@ mod tests { #[test] fn group_lifecycle() { - let mut mc = init(); + let mut group_data = init(); // Create group, add ports. - mc.domain_create(100).unwrap(); - mc.domain_port_add(100, 1, 0, 0).unwrap(); - mc.domain_port_add(100, 5, 0, 0).unwrap(); + group_data.domain_create(100).unwrap(); + group_data.domain_port_add(100, 1, 0, 0).unwrap(); + group_data.domain_port_add(100, 5, 0, 0).unwrap(); - assert_eq!(mc.domain_port_count(100).unwrap(), 2); - assert_eq!(mc.domains_count(), 1); + assert_eq!(group_data.domain_port_count(100).unwrap(), 2); + assert_eq!(group_data.domains_count(), 1); // Remove a port. - mc.domain_port_remove(100, 1).unwrap(); - assert_eq!(mc.domain_port_count(100).unwrap(), 1); + group_data.domain_port_remove(100, 1).unwrap(); + assert_eq!(group_data.domain_port_count(100).unwrap(), 1); // Destroy group. - mc.domain_destroy(100).unwrap(); - assert_eq!(mc.domains_count(), 0); + group_data.domain_destroy(100).unwrap(); + assert_eq!(group_data.domains_count(), 0); } #[test] fn duplicate_group_rejected() { - let mut mc = init(); - mc.domain_create(1).unwrap(); - assert!(mc.domain_create(1).is_err()); + let mut group_data = init(); + group_data.domain_create(1).unwrap(); + assert!(group_data.domain_create(1).is_err()); } #[test] fn duplicate_port_rejected() { - let mut mc = init(); - mc.domain_create(1).unwrap(); - mc.domain_port_add(1, 5, 0, 0).unwrap(); - assert!(mc.domain_port_add(1, 5, 0, 0).is_err()); + let mut group_data = init(); + group_data.domain_create(1).unwrap(); + group_data.domain_port_add(1, 5, 0, 0).unwrap(); + assert!(group_data.domain_port_add(1, 5, 0, 0).is_err()); } #[test] fn remove_nonexistent_port_rejected() { - let mut mc = init(); - mc.domain_create(1).unwrap(); - assert!(mc.domain_port_remove(1, 99).is_err()); + let mut group_data = init(); + group_data.domain_create(1).unwrap(); + assert!(group_data.domain_port_remove(1, 99).is_err()); } #[test] fn operations_on_missing_group_rejected() { - let mut mc = init(); - assert!(mc.domain_port_add(42, 1, 0, 0).is_err()); - assert!(mc.domain_port_remove(42, 1).is_err()); - assert!(mc.domain_port_count(42).is_err()); - assert!(mc.domain_destroy(42).is_err()); + let mut group_data = init(); + assert!(group_data.domain_port_add(42, 1, 0, 0).is_err()); + assert!(group_data.domain_port_remove(42, 1).is_err()); + assert!(group_data.domain_port_count(42).is_err()); + assert!(group_data.domain_destroy(42).is_err()); } #[test] fn port_bitmap_empty_group() { - let mut mc = init(); - mc.domain_create(1).unwrap(); - assert_eq!(mc.port_bitmap(1), 0); + let mut group_data = init(); + group_data.domain_create(1).unwrap(); + assert_eq!(group_data.port_bitmap(1), 0); } #[test] fn port_bitmap_populated() { - let mut mc = init(); - mc.domain_create(1).unwrap(); - mc.domain_port_add(1, 0, 0, 0).unwrap(); - mc.domain_port_add(1, 3, 0, 0).unwrap(); - mc.domain_port_add(1, 7, 0, 0).unwrap(); + let mut group_data = init(); + group_data.domain_create(1).unwrap(); + group_data.domain_port_add(1, 0, 0, 0).unwrap(); + group_data.domain_port_add(1, 3, 0, 0).unwrap(); + group_data.domain_port_add(1, 7, 0, 0).unwrap(); - let bm = mc.port_bitmap(1); + let bm = group_data.port_bitmap(1); assert_eq!(bm & (1 << 0), 1 << 0); assert_eq!(bm & (1 << 3), 1 << 3); assert_eq!(bm & (1 << 7), 1 << 7); @@ -226,77 +318,172 @@ mod tests { #[test] fn port_bitmap_unknown_group_returns_zero() { - let mc = init(); - assert_eq!(mc.port_bitmap(999), 0); + let group_data = init(); + assert_eq!(group_data.port_bitmap(999), 0); } #[test] fn port_bitmap_high_port() { - let mut mc = init(); - mc.domain_create(1).unwrap(); - mc.domain_port_add(1, 127, 0, 0).unwrap(); + let mut group_data = init(); + group_data.domain_create(1).unwrap(); + group_data.domain_port_add(1, 127, 0, 0).unwrap(); - let bm = mc.port_bitmap(1); + let bm = group_data.port_bitmap(1); assert_eq!(bm & (1u128 << 127), 1u128 << 127); } #[test] - fn port_bitmap_ignores_ports_above_127() { - let mut mc = init(); - mc.domain_create(1).unwrap(); + fn port_add_rejects_ports_above_127() { + let mut group_data = init(); + group_data.domain_create(1).unwrap(); // Port 128 is out of range for a 128-bit bitmap. - assert!(mc.domain_port_add(1, 128, 0, 0).is_err()); + assert!(group_data.domain_port_add(1, 128, 0, 0).is_err()); } #[test] fn set_max_nodes_validates() { - let mut mc = init(); - mc.domain_create(1).unwrap(); - mc.domain_create(2).unwrap(); + let mut group_data = init(); + group_data.domain_create(1).unwrap(); + group_data.domain_create(2).unwrap(); - assert!(mc.set_max_nodes(1, 0).is_err()); - assert!(mc.set_max_nodes(2, 0).is_ok()); - assert!(mc.set_max_nodes(100, 0).is_ok()); + assert!(group_data.set_max_nodes(1, 0).is_err()); + assert!(group_data.set_max_nodes(2, 0).is_ok()); + assert!(group_data.set_max_nodes(100, 0).is_ok()); } #[test] fn domains_returns_created_group_ids() { - let mut mc = init(); - mc.domain_create(10).unwrap(); - mc.domain_create(20).unwrap(); - mc.domain_create(30).unwrap(); + let mut group_data = init(); + group_data.domain_create(10).unwrap(); + group_data.domain_create(20).unwrap(); + group_data.domain_create(30).unwrap(); - let mut ids = mc.domains(); + let mut ids = group_data.domains(); ids.sort(); assert_eq!(ids, vec![10, 20, 30]); } #[test] fn port_bitmap_reflects_removal() { - let mut mc = init(); - mc.domain_create(1).unwrap(); - mc.domain_port_add(1, 0, 0, 0).unwrap(); - mc.domain_port_add(1, 3, 0, 0).unwrap(); + let mut group_data = init(); + group_data.domain_create(1).unwrap(); + group_data.domain_port_add(1, 0, 0, 0).unwrap(); + group_data.domain_port_add(1, 3, 0, 0).unwrap(); - mc.domain_port_remove(1, 0).unwrap(); + group_data.domain_port_remove(1, 0).unwrap(); - let bm = mc.port_bitmap(1); + let bm = group_data.port_bitmap(1); assert_eq!(bm & (1 << 0), 0); assert_eq!(bm & (1 << 3), 1 << 3); } + #[test] + fn refresh_recomputes_bitmap_after_membership_change() { + let mut group_data = init(); + // Underlay group 65533 with one member, recorded as a replication + // entry keyed by the underlay destination address bytes. + group_data.domain_create(65533).unwrap(); + group_data.domain_port_add(65533, 1, 0, 0).unwrap(); + // ff04::1, an admin-scoped IPv6 multicast destination, matching the + // 128-bit key of the sidecar-lite replication table. + let keyset = std::net::Ipv6Addr::new(0xff04, 0, 0, 0, 0, 0, 0, 1) + .octets() + .to_vec(); + group_data.record_repl_entry(keyset.clone(), 0, 65533, 65533); + + // Refreshed against current membership @ only port 1. + let initial = group_data.refresh_params_for_group(65533); + assert_eq!(initial.len(), 1); + assert_eq!(initial[0].underlay_bitmap, 1u128 << 1); + + // A later member add must surface in a fresh refresh. + group_data.domain_port_add(65533, 2, 0, 0).unwrap(); + group_data.domain_port_add(65533, 3, 0, 0).unwrap(); + let refreshed = group_data.refresh_params_for_group(65533); + assert_eq!(refreshed.len(), 1); + assert_eq!(refreshed[0].keyset, keyset); + assert_eq!(refreshed[0].rid, 65533); + assert_eq!( + refreshed[0].underlay_bitmap, + (1u128 << 1) | (1u128 << 2) | (1u128 << 3) + ); + } + + #[test] + fn refresh_skips_unrelated_groups_and_forgotten_entries() { + let mut group_data = init(); + group_data.domain_create(100).unwrap(); + group_data.domain_create(200).unwrap(); + group_data.domain_port_add(100, 4, 0, 0).unwrap(); + + // These are opaque single-byte keysets standing in for distinct + // replication entries (not any real address). + let key_a = vec![0xaa]; + let key_b = vec![0xbb]; + + group_data.record_repl_entry(key_a.clone(), 0, 100, 7); + group_data.record_repl_entry(key_b, 0, 200, 8); + + // Only the entry referencing group 100 is refreshed. + let refreshed = group_data.refresh_params_for_group(100); + assert_eq!(refreshed.len(), 1); + assert_eq!(refreshed[0].keyset, key_a); + + // After forgetting it, the group has no entries to refresh. + group_data.forget_repl_entry(&key_a); + assert!(group_data.refresh_params_for_group(100).is_empty()); + } + + #[test] + fn refresh_matches_external_slot() { + let mut group_data = init(); + group_data.domain_create(50).unwrap(); + group_data.domain_port_add(50, 9, 0, 0).unwrap(); + // Group 50 sits in the external slot here, not the underlay slot. + group_data.record_repl_entry(vec![0xcc], 50, 0, 1); + + let refreshed = group_data.refresh_params_for_group(50); + assert_eq!(refreshed.len(), 1); + assert_eq!(refreshed[0].external_bitmap, 1u128 << 9); + assert_eq!(refreshed[0].underlay_bitmap, 0); + } + + #[test] + fn clear_repl_entries_drops_all_tracked_entries() { + let mut group_data = init(); + group_data.domain_create(100).unwrap(); + group_data.domain_create(200).unwrap(); + group_data.domain_port_add(100, 4, 0, 0).unwrap(); + group_data.domain_port_add(200, 5, 0, 0).unwrap(); + group_data.record_repl_entry(vec![0xaa], 0, 100, 7); + group_data.record_repl_entry(vec![0xbb], 0, 200, 8); + + // Both entries are tracked before the clear. + assert_eq!(group_data.refresh_params_for_group(100).len(), 1); + assert_eq!(group_data.refresh_params_for_group(200).len(), 1); + + group_data.clear_repl_entries(); + + // After clearing, no keyset survives to be resurrected, even though the + // group memberships themselves are untouched. + assert!(group_data.refresh_params_for_group(100).is_empty()); + assert!(group_data.refresh_params_for_group(200).is_empty()); + assert_eq!(group_data.domain_port_count(100).unwrap(), 1); + assert_eq!(group_data.domain_port_count(200).unwrap(), 1); + } + #[test] fn groups_are_independent() { - let mut mc = init(); - mc.domain_create(1).unwrap(); - mc.domain_create(2).unwrap(); - mc.domain_port_add(1, 5, 0, 0).unwrap(); - mc.domain_port_add(2, 5, 0, 0).unwrap(); + let mut group_data = init(); + group_data.domain_create(1).unwrap(); + group_data.domain_create(2).unwrap(); + group_data.domain_port_add(1, 5, 0, 0).unwrap(); + group_data.domain_port_add(2, 5, 0, 0).unwrap(); - mc.domain_port_remove(1, 5).unwrap(); + group_data.domain_port_remove(1, 5).unwrap(); - assert_eq!(mc.domain_port_count(1).unwrap(), 0); - assert_eq!(mc.domain_port_count(2).unwrap(), 1); - assert_eq!(mc.port_bitmap(2) & (1 << 5), 1 << 5); + assert_eq!(group_data.domain_port_count(1).unwrap(), 0); + assert_eq!(group_data.domain_port_count(2).unwrap(), 1); + assert_eq!(group_data.port_bitmap(2) & (1 << 5), 1 << 5); } } diff --git a/asic/src/softnpu/mgmt.rs b/asic/src/softnpu/mgmt.rs index 65f24338..616343f5 100644 --- a/asic/src/softnpu/mgmt.rs +++ b/asic/src/softnpu/mgmt.rs @@ -101,9 +101,18 @@ fn read_uart(msg: ManagementRequest) -> String { f.write_all(&buf).unwrap(); f.sync_all().unwrap(); + // Responses are newline-terminated and a table dump can span multiple + // reads, so accumulate until the terminator arrives. + let mut response = Vec::new(); let mut buf = [0u8; 1024]; - let n = f.read(&mut buf).unwrap(); - String::from_utf8_lossy(&buf[..n]).to_string() + loop { + let n = f.read(&mut buf).unwrap(); + response.extend_from_slice(&buf[..n]); + if n == 0 || response.last() == Some(&b'\n') { + break; + } + } + String::from_utf8_lossy(&response).to_string() } fn read_uds(msg: ManagementRequest, socket_path: &str) -> String { @@ -111,7 +120,9 @@ fn read_uds(msg: ManagementRequest, socket_path: &str) -> String { write_uds(msg, socket_path); - let mut buf = vec![0u8; 10240]; + // A table dump arrives as a single datagram, which recv truncates if + // the buffer is too small, so size it well past any expected response. + let mut buf = vec![0u8; 1 << 20]; let n = client_uds.recv(&mut buf).unwrap(); String::from_utf8_lossy(&buf[..n]).to_string() } diff --git a/asic/src/softnpu/mod.rs b/asic/src/softnpu/mod.rs index ac7172cd..852ad773 100644 --- a/asic/src/softnpu/mod.rs +++ b/asic/src/softnpu/mod.rs @@ -7,7 +7,7 @@ use std::collections::HashMap; use std::sync::Mutex; -use slog::{Logger, o}; +use slog::{Logger, debug, o}; use tokio::sync::mpsc; use uuid::Uuid; @@ -150,6 +150,57 @@ impl Handle { } pub fn fini(&self) {} + + /// Rewrite the replication entries refreshed after a membership change. + /// + /// SoftNPU operates on a generic port bitmap data structure for each + /// replication entry at write time, so a membership change must re-emit + /// every affected entry. + /// + /// SoftNPU has no in-place table update, so each entry is removed and + /// re-added with the recomputed bitmaps. + #[cfg(feature = "multicast")] + fn rewrite_replication_entries( + &self, + entries: Vec, + ) { + use softnpu_lib::{TableAdd, TableRemove}; + + for entry in entries { + // Logged at debug so the recomputed membership is visible in a + // release build, where trace is compiled out. The bitmaps are the + // post-refresh output ports, so an empty bitmap here is the signal + // that a group has no members to replicate to. + debug!( + self.log, + "refreshing mcast replication entry: \ + external_bitmap={:#x} underlay_bitmap={:#x} rid={}", + entry.external_bitmap, + entry.underlay_bitmap, + entry.rid + ); + let remove = ManagementRequest::TableRemove(TableRemove { + keyset_data: entry.keyset.clone(), + table: mcast::MCAST_REPLICATION_V6_TABLE.to_string(), + }); + mgmt::write(remove, &self.mgmt_config); + + let parameter_data = [ + entry.external_bitmap.to_le_bytes().as_slice(), + entry.underlay_bitmap.to_le_bytes().as_slice(), + entry.rid.to_le_bytes().as_slice(), + ] + .concat(); + + let add = ManagementRequest::TableAdd(TableAdd { + table: mcast::MCAST_REPLICATION_V6_TABLE.to_string(), + action: mcast::SET_PORT_BITMAP_ACTION.to_string(), + keyset_data: entry.keyset, + parameter_data, + }); + mgmt::write(add, &self.mgmt_config); + } + } } #[cfg(feature = "multicast")] @@ -175,8 +226,15 @@ impl AsicMulticastOps for Handle { self.log, "adding port {port} to multicast group {group_id}" ); + // Hold the lock across the refresh and rewrite so the recomputed + // bitmap snapshot and its table writes are atomic with respect to a + // concurrent membership change. The rewrite touches only + // `mgmt_config`, not `mc_data`, so there's no re-entrant lock. let mut mc_data = self.mc_data.lock().unwrap(); - mc_data.domain_port_add(group_id, port, rid, level1_excl_id) + mc_data.domain_port_add(group_id, port, rid, level1_excl_id)?; + let refreshed = mc_data.refresh_params_for_group(group_id); + self.rewrite_replication_entries(refreshed); + Ok(()) } fn mc_port_remove(&self, group_id: u16, port: u16) -> AsicResult<()> { @@ -185,7 +243,10 @@ impl AsicMulticastOps for Handle { "removing port {port} from multicast group {group_id}" ); let mut mc_data = self.mc_data.lock().unwrap(); - mc_data.domain_port_remove(group_id, port) + mc_data.domain_port_remove(group_id, port)?; + let refreshed = mc_data.refresh_params_for_group(group_id); + self.rewrite_replication_entries(refreshed); + Ok(()) } fn mc_group_create(&self, group_id: u16) -> AsicResult<()> { @@ -272,7 +333,7 @@ impl AsicOps for Handle { let asic_port_id = self.port_to_asic_id(port_hdl)?; let tx = self.update_tx.lock().unwrap(); if let Some(tx) = tx.as_ref() { - // When a port is enabled in softnpu, it automatically comes online. + // When a port is enabled in SoftNPU, it automatically comes online. // When switching between enabled and disabled, we send the main body // of dpd the PortUpdate events we would expect to see on real hardware. let present = @@ -298,7 +359,7 @@ impl AsicOps for Handle { } } } else { - slog::debug!(self.log, "no PortUpdate handler registered"); + debug!(self.log, "no PortUpdate handler registered"); } Ok(()) } diff --git a/asic/src/softnpu/table.rs b/asic/src/softnpu/table.rs index 7e045e30..f120bf03 100644 --- a/asic/src/softnpu/table.rs +++ b/asic/src/softnpu/table.rs @@ -4,10 +4,14 @@ // // Copyright 2026 Oxide Computer Company +use std::collections::BTreeMap; use std::hash::Hash; use slog::{error, trace}; -use softnpu_lib::{ManagementRequest, TableAdd, TableRemove}; +use softnpu_lib::p4rs::TableEntry; +use softnpu_lib::{ + ManagementRequest, ManagementResponse, TableAdd, TableRemove, +}; use crate::softnpu::Handle; use aal::{ @@ -50,7 +54,7 @@ pub struct Table { impl Table { /// Return the sidecar-lite table name for this table, or `None` if the - /// table is not implemented in the softnpu backend. + /// table is not implemented in the SoftNPU backend. pub fn softnpu_table_name(&self) -> Option<&'static str> { if self.implemented { match self.type_ { @@ -73,7 +77,7 @@ impl Table { TableType::PortMacAddress => Some("ingress.mac.mac_rewrite"), #[cfg(feature = "multicast")] TableType::McastIpv6 => { - Some("ingress.mcast.mcast_replication_v6") + Some(crate::softnpu::mcast::MCAST_REPLICATION_V6_TABLE) } #[cfg(feature = "multicast")] TableType::McastIpv4SrcFilter => { @@ -158,6 +162,83 @@ impl Table { }) .collect() } + + /// Translate a `configure_mcastv6` request into a sidecar-lite + /// `set_port_bitmap` addition, holding the replication tracking lock across + /// the entire op. + /// + /// DPD sends `(mcast_grp_a, mcast_grp_b, rid, level1_excl_id, + /// level2_excl_id)`, while sidecar-lite expects `(external, underlay, rid)`, + /// from `McGroupData` membership. On SoftNPU the bitmap is a write-time + /// snapshot, unlike the live replication engine on the sidecar, so the + /// recorded group references let a later membership change recompute + /// the entry. + /// + /// The lock is held from the bitmap snapshot through to the table write so + /// a concurrent membership refresh, which takes the same lock across its + /// own rewrite (see @ `mc_port_add`), cannot interleave a newer bitmap that + /// this older snapshot would then overwrite. + #[cfg(feature = "multicast")] + fn mcast_replication_add( + &self, + hdl: &Handle, + table: String, + keyset_data: Vec, + action_data: &aal::ActionData, + ) -> AsicResult<()> { + let mut external_grp: u16 = 0; + let mut underlay_grp: u16 = 0; + let mut rid: u16 = 0; + for arg in action_data.args.iter() { + if let ValueTypes::U64(v) = &arg.value { + match arg.name.as_str() { + "mcast_grp_a" => external_grp = *v as u16, + "mcast_grp_b" => underlay_grp = *v as u16, + "rid" => rid = *v as u16, + _ => {} + } + } + } + + let mut mc_data = hdl.mc_data.lock().unwrap(); + let external_bitmap = mc_data.port_bitmap(external_grp); + let underlay_bitmap = mc_data.port_bitmap(underlay_grp); + mc_data.record_repl_entry( + keyset_data.clone(), + external_grp, + underlay_grp, + rid, + ); + + let parameter_data = [ + external_bitmap.to_le_bytes().as_slice(), + underlay_bitmap.to_le_bytes().as_slice(), + rid.to_le_bytes().as_slice(), + ] + .concat(); + + // Logged at debug so the initial bitmap snapshot is visible in a + // release build. + slog::debug!( + hdl.log, + "mcast replication add: external_grp={external_grp} \ + underlay_grp={underlay_grp} external_bitmap={external_bitmap:#x} \ + underlay_bitmap={underlay_bitmap:#x} rid={rid}" + ); + trace!(hdl.log, "table: {table}"); + trace!(hdl.log, "keyset_data:\n{keyset_data:#?}"); + trace!(hdl.log, "parameter_data:\n{parameter_data:#?}"); + + let msg = ManagementRequest::TableAdd(TableAdd { + table, + action: crate::softnpu::mcast::SET_PORT_BITMAP_ACTION.to_string(), + keyset_data, + parameter_data, + }); + crate::softnpu::mgmt::write(msg, &hdl.mgmt_config); + + Ok(()) + } } // All tables are defined to be 4096 entries deep. @@ -208,8 +289,49 @@ impl TableOps for Table { self.size } - fn clear(&self, _hdl: &Handle) -> AsicResult<()> { - // TODO: implement in softnpu + fn clear(&self, hdl: &Handle) -> AsicResult<()> { + let Some(table) = self.softnpu_table_name() else { + return Ok(()); + }; + + // Hold the replication tracking lock across the dump and removals + // when clearing the replication table, matching the locking + // discipline in `mcast_replication_add`, so a concurrent membership + // refresh cannot re-add entries mid-clear. + #[cfg(feature = "multicast")] + let mut mc_data = (self.type_ == TableType::McastIpv6) + .then(|| hdl.mc_data.lock().unwrap()); + + // The management protocol has no clear message, so emulate one by + // dumping table state and removing each entry. + let response = crate::softnpu::mgmt::read( + ManagementRequest::DumpRequest, + &hdl.mgmt_config, + ); + let tables = parse_dump(&response)?; + + // Include the untagged variant when the table splits on VLAN validity. + for name in std::iter::once(table).chain(self.untagged_id) { + let Some(entries) = tables.get(name) else { + continue; + }; + for entry in entries { + let msg = ManagementRequest::TableRemove(TableRemove { + table: name.to_string(), + keyset_data: entry.keyset_data.clone(), + }); + crate::softnpu::mgmt::write(msg, &hdl.mgmt_config); + } + } + + // Drop all tracked replication entries so that a later membership + // change for a reused group identifier cannot resurrect a stale bitmap + // from a forgotten keyset. + #[cfg(feature = "multicast")] + if let Some(mc_data) = mc_data.as_mut() { + mc_data.clear_repl_entries(); + } + Ok(()) } @@ -247,6 +369,21 @@ impl TableOps for Table { let keyset_data = keyset_data(fields, self.type_, &table); + // Multicast replication is handled ahead of the generic match so the + // replication tracking lock can be held from the bitmap snapshot through + // the table write. + #[cfg(feature = "multicast")] + if let (TableType::McastIpv6, "configure_mcastv6") = + (self.type_, action_data.action.as_str()) + { + return self.mcast_replication_add( + hdl, + table, + keyset_data, + &action_data, + ); + } + let (action, parameter_data) = match ( self.type_, action_data.action.as_str(), @@ -578,39 +715,6 @@ impl TableOps for Table { | (TableType::McastIpv6SrcFilter, "allow_source_mcastv6") => { ("allow_source", Vec::new()) } - // Multicast replication: translate group IDs to port bitmaps. - // - // DPD sends configure_mcastv6 with (mcast_grp_a, mcast_grp_b, - // rid, level1_excl_id, level2_excl_id). Sidecar-lite expects - // set_port_bitmap with (external, underlay, rid). We look up - // the group membership in McGroupData and build the bitmaps. - #[cfg(feature = "multicast")] - (TableType::McastIpv6, "configure_mcastv6") => { - let mut external_grp: u16 = 0; - let mut underlay_grp: u16 = 0; - let mut rid: u16 = 0; - for arg in action_data.args.iter() { - if let ValueTypes::U64(v) = &arg.value { - match arg.name.as_str() { - "mcast_grp_a" => external_grp = *v as u16, - "mcast_grp_b" => underlay_grp = *v as u16, - "rid" => rid = *v as u16, - _ => {} - } - } - } - - let mc_data = hdl.mc_data.lock().unwrap(); - let external_bitmap = mc_data.port_bitmap(external_grp); - let underlay_bitmap = mc_data.port_bitmap(underlay_grp); - drop(mc_data); - - let mut params = Vec::new(); - params.extend_from_slice(&external_bitmap.to_le_bytes()); - params.extend_from_slice(&underlay_bitmap.to_le_bytes()); - params.extend_from_slice(&rid.to_le_bytes()); - ("set_port_bitmap", params) - } // Multicast egress decap: pack 8x32-bit bitmap into 128 bits. // // DPD sends set_decap_ports with 8x32-bit bitmap fields @@ -682,7 +786,7 @@ impl TableOps for Table { key: &M, data: &A, ) -> AsicResult<()> { - // Softnpu does not currently support in-place updates. + // SoftNPU does not currently support in-place updates. // Delete the old entry and re-add with the new action data. // Both operations are currently fire-and-forget over the // management channel, so neither can fail from DPD's @@ -720,6 +824,13 @@ impl TableOps for Table { let keyset_data = keyset_data(fields, self.type_, &table); + // Stop tracking a replication entry once it leaves the pipeline so a + // later membership change does not resurrect a stale bitmap. + #[cfg(feature = "multicast")] + if self.type_ == TableType::McastIpv6 { + hdl.mc_data.lock().unwrap().forget_repl_entry(&keyset_data); + } + trace!(hdl.log, "sending request to softnpu"); trace!(hdl.log, "table: {table}"); trace!(hdl.log, "keyset_data:\n{:#?}", keyset_data); @@ -861,6 +972,27 @@ fn pack_decap_bitmap(args: &aal::ActionData) -> Vec { chunks.iter().flat_map(|c| c.to_le_bytes()).collect() } +/// Parse a `DumpRequest` response into per-table entries. +/// +/// The standalone responder, managed over a Unix domain socket (UDS), wraps +/// the dump in `ManagementResponse::DumpResponse`, while the propolis +/// responder, managed over the emulated serial device (UART), writes the +/// bare table map. +/// +/// Note: there is no key collision between the two forms: table +/// names are dotted P4 paths, never `DumpResponse`. +fn parse_dump(response: &str) -> AsicResult>> { + let response = response.trim(); + if let Ok(ManagementResponse::DumpResponse(tables)) = + serde_json::from_str(response) + { + return Ok(tables); + } + serde_json::from_str(response).map_err(|e| { + AsicError::Internal(format!("failed to parse SoftNPU table dump: {e}")) + }) +} + /// Extract keys from `match_data` and ensure that they are /// in a data structure with the correct length. The `table_name` /// parameter is the resolved sidecar-lite table name, which may @@ -1046,3 +1178,63 @@ fn serialize_value_type_be(x: &ValueTypes, data: &mut Vec) { } } } + +#[cfg(test)] +mod tests { + use super::*; + #[cfg(feature = "multicast")] + use crate::softnpu::mcast::MCAST_REPLICATION_V6_TABLE as REPL_TABLE; + // `parse_dump` is name-agnostic; the alias just keeps the fixtures + // realistic when the mcast module is compiled out. + #[cfg(not(feature = "multicast"))] + const REPL_TABLE: &str = "ingress.mcast.mcast_replication_v6"; + + #[test] + fn parse_dump_standalone_uds_wrapped_response() { + // Round-trip through the responder's own types so the fixture tracks + // softnpu_lib's wire format rather than a hand-written copy of it. + let mut tables = BTreeMap::new(); + tables.insert( + REPL_TABLE.to_string(), + vec![TableEntry { + action_id: "set_port_bitmap".to_string(), + keyset_data: vec![255, 4, 0, 1], + parameter_data: vec![2, 0], + }], + ); + let response = + serde_json::to_string(&ManagementResponse::DumpResponse(tables)) + .unwrap(); + let tables = parse_dump(&response).unwrap(); + let entries = tables.get(REPL_TABLE).unwrap(); + assert_eq!(entries.len(), 1); + assert_eq!(entries[0].keyset_data, vec![255, 4, 0, 1]); + } + + #[test] + fn parse_dump_propolis_uart_bare_map() { + // The propolis UART responder writes the bare table map with a + // trailing newline. This fixture stays hand-written: the producer + // lives in the propolis repo. + let response = format!( + "{{\"{REPL_TABLE}\":[{{\"action_id\":\"set_port_bitmap\",\"keyset_data\":[1],\"parameter_data\":[]}}],\"ingress.nat.nat_v4\":[]}}\n" + ); + let tables = parse_dump(&response).unwrap(); + assert_eq!(tables.len(), 2); + assert_eq!(tables.get(REPL_TABLE).unwrap()[0].keyset_data, vec![1]); + assert!(tables.get("ingress.nat.nat_v4").unwrap().is_empty()); + + // An empty map still parses via the bare-map fallback. + assert!(parse_dump("{}\n").unwrap().is_empty()); + } + + #[test] + fn parse_dump_malformed_rejected() { + assert!(parse_dump("not json").is_err()); + // Models `read_uart` returning on n == 0 or an empty UDS datagram. + assert!(parse_dump("").is_err()); + // A wrapped response that is not a dump must fall through to the + // bare-map parse and fail there, not yield garbage. + assert!(parse_dump(r#"{"RadixResponse":8}"#).is_err()); + } +} diff --git a/dpd-client/tests/integration_tests/mcast.rs b/dpd-client/tests/integration_tests/mcast.rs index c022d933..a0167221 100644 --- a/dpd-client/tests/integration_tests/mcast.rs +++ b/dpd-client/tests/integration_tests/mcast.rs @@ -2147,9 +2147,35 @@ async fn test_encapped_multicast_geneve_mcast_tag_to_external_members() ]; let port_label_ingress = switch.port_label(ingress).unwrap(); + let port_label_egress1 = switch.port_label(egress1).unwrap(); + let port_label_egress2 = switch.port_label(egress2).unwrap(); let ctr_baseline_ingress = switch.get_counter(&port_label_ingress, Some("ingress")).await.unwrap(); + let ctr_baseline_mcast_egress1 = switch + .get_counter(&port_label_egress1, Some("multicast")) + .await + .unwrap(); + let ctr_baseline_mcast_egress2 = switch + .get_counter(&port_label_egress2, Some("multicast")) + .await + .unwrap(); + let ctr_baseline_external_egress1 = switch + .get_counter(&port_label_egress1, Some("multicast_external")) + .await + .unwrap(); + let ctr_baseline_external_egress2 = switch + .get_counter(&port_label_egress2, Some("multicast_external")) + .await + .unwrap(); + let ctr_baseline_underlay_egress1 = switch + .get_counter(&port_label_egress1, Some("multicast_underlay")) + .await + .unwrap(); + let ctr_baseline_underlay_egress2 = switch + .get_counter(&port_label_egress2, Some("multicast_underlay")) + .await + .unwrap(); switch.packet_test(vec![test_pkt], expected_pkts).unwrap(); @@ -2163,6 +2189,64 @@ async fn test_encapped_multicast_geneve_mcast_tag_to_external_members() .await .unwrap(); + // One decapped replica should be counted per external member port. + check_counter_incremented( + switch, + &port_label_egress1, + ctr_baseline_mcast_egress1, + 1, + Some("multicast"), + ) + .await + .unwrap(); + check_counter_incremented( + switch, + &port_label_egress2, + ctr_baseline_mcast_egress2, + 1, + Some("multicast"), + ) + .await + .unwrap(); + + // Decapped replicas to external members must be attributed to the + // external counter, not the underlay one. + check_counter_incremented( + switch, + &port_label_egress1, + ctr_baseline_external_egress1, + 1, + Some("multicast_external"), + ) + .await + .unwrap(); + check_counter_incremented( + switch, + &port_label_egress2, + ctr_baseline_external_egress2, + 1, + Some("multicast_external"), + ) + .await + .unwrap(); + + let ctr_underlay_egress1 = switch + .get_counter(&port_label_egress1, Some("multicast_underlay")) + .await + .unwrap(); + let ctr_underlay_egress2 = switch + .get_counter(&port_label_egress2, Some("multicast_underlay")) + .await + .unwrap(); + assert_eq!( + ctr_underlay_egress1, ctr_baseline_underlay_egress1, + "decapped replicas must not hit the underlay counter" + ); + assert_eq!( + ctr_underlay_egress2, ctr_baseline_underlay_egress2, + "decapped replicas must not hit the underlay counter" + ); + cleanup_test_group(switch, get_group_ip(&created_group), TEST_TAG) .await .unwrap(); @@ -2277,9 +2361,27 @@ async fn test_encapped_multicast_geneve_mcast_tag_to_underlay_members() ]; let port_label_ingress = switch.port_label(ingress).unwrap(); + let port_label_egress3 = switch.port_label(egress3).unwrap(); + let port_label_egress4 = switch.port_label(egress4).unwrap(); let ctr_baseline_ingress = switch.get_counter(&port_label_ingress, Some("ingress")).await.unwrap(); + let ctr_baseline_underlay_egress3 = switch + .get_counter(&port_label_egress3, Some("multicast_underlay")) + .await + .unwrap(); + let ctr_baseline_underlay_egress4 = switch + .get_counter(&port_label_egress4, Some("multicast_underlay")) + .await + .unwrap(); + let ctr_baseline_external_egress3 = switch + .get_counter(&port_label_egress3, Some("multicast_external")) + .await + .unwrap(); + let ctr_baseline_external_egress4 = switch + .get_counter(&port_label_egress4, Some("multicast_external")) + .await + .unwrap(); switch.packet_test(vec![test_pkt], expected_pkts).unwrap(); @@ -2293,6 +2395,44 @@ async fn test_encapped_multicast_geneve_mcast_tag_to_underlay_members() .await .unwrap(); + // Encapped replicas to underlay members must be attributed to the + // underlay counter, not the external one. + check_counter_incremented( + switch, + &port_label_egress3, + ctr_baseline_underlay_egress3, + 1, + Some("multicast_underlay"), + ) + .await + .unwrap(); + check_counter_incremented( + switch, + &port_label_egress4, + ctr_baseline_underlay_egress4, + 1, + Some("multicast_underlay"), + ) + .await + .unwrap(); + + let ctr_external_egress3 = switch + .get_counter(&port_label_egress3, Some("multicast_external")) + .await + .unwrap(); + let ctr_external_egress4 = switch + .get_counter(&port_label_egress4, Some("multicast_external")) + .await + .unwrap(); + assert_eq!( + ctr_external_egress3, ctr_baseline_external_egress3, + "underlay-tagged replicas must not hit the external counter" + ); + assert_eq!( + ctr_external_egress4, ctr_baseline_external_egress4, + "underlay-tagged replicas must not hit the external counter" + ); + cleanup_test_group(switch, get_group_ip(&created_group), TEST_TAG) .await .unwrap(); @@ -2420,9 +2560,61 @@ async fn test_encapped_multicast_geneve_mcast_tag_to_underlay_and_external_membe ]; let port_label_ingress = switch.port_label(ingress).unwrap(); + let port_label_egress1 = switch.port_label(egress1).unwrap(); + let port_label_egress2 = switch.port_label(egress2).unwrap(); + let port_label_egress3 = switch.port_label(egress3).unwrap(); + let port_label_egress4 = switch.port_label(egress4).unwrap(); let ctr_baseline_ingress = switch.get_counter(&port_label_ingress, Some("ingress")).await.unwrap(); + let ctr_baseline_mcast_egress1 = switch + .get_counter(&port_label_egress1, Some("multicast")) + .await + .unwrap(); + let ctr_baseline_mcast_egress2 = switch + .get_counter(&port_label_egress2, Some("multicast")) + .await + .unwrap(); + let ctr_baseline_mcast_egress3 = switch + .get_counter(&port_label_egress3, Some("multicast")) + .await + .unwrap(); + let ctr_baseline_mcast_egress4 = switch + .get_counter(&port_label_egress4, Some("multicast")) + .await + .unwrap(); + let ctr_baseline_underlay_egress1 = switch + .get_counter(&port_label_egress1, Some("multicast_underlay")) + .await + .unwrap(); + let ctr_baseline_underlay_egress2 = switch + .get_counter(&port_label_egress2, Some("multicast_underlay")) + .await + .unwrap(); + let ctr_baseline_underlay_egress3 = switch + .get_counter(&port_label_egress3, Some("multicast_underlay")) + .await + .unwrap(); + let ctr_baseline_underlay_egress4 = switch + .get_counter(&port_label_egress4, Some("multicast_underlay")) + .await + .unwrap(); + let ctr_baseline_external_egress1 = switch + .get_counter(&port_label_egress1, Some("multicast_external")) + .await + .unwrap(); + let ctr_baseline_external_egress2 = switch + .get_counter(&port_label_egress2, Some("multicast_external")) + .await + .unwrap(); + let ctr_baseline_external_egress3 = switch + .get_counter(&port_label_egress3, Some("multicast_external")) + .await + .unwrap(); + let ctr_baseline_external_egress4 = switch + .get_counter(&port_label_egress4, Some("multicast_external")) + .await + .unwrap(); switch.packet_test(vec![test_pkt], expected_pkts).unwrap(); @@ -2436,6 +2628,99 @@ async fn test_encapped_multicast_geneve_mcast_tag_to_underlay_and_external_membe .await .unwrap(); + // One replica should be counted per member port, external & underlay alike. + for (label, baseline) in [ + (&port_label_egress1, ctr_baseline_mcast_egress1), + (&port_label_egress2, ctr_baseline_mcast_egress2), + (&port_label_egress3, ctr_baseline_mcast_egress3), + (&port_label_egress4, ctr_baseline_mcast_egress4), + ] { + check_counter_incremented( + switch, + label, + baseline, + 1, + Some("multicast"), + ) + .await + .unwrap(); + } + + // Still-encapsulated replicas to underlay members count as underlay, + // not external, for UNDERLAY_EXTERNAL tagged groups. + check_counter_incremented( + switch, + &port_label_egress3, + ctr_baseline_underlay_egress3, + 1, + Some("multicast_underlay"), + ) + .await + .unwrap(); + check_counter_incremented( + switch, + &port_label_egress4, + ctr_baseline_underlay_egress4, + 1, + Some("multicast_underlay"), + ) + .await + .unwrap(); + + // Decapped replicas to external members count as external. + check_counter_incremented( + switch, + &port_label_egress1, + ctr_baseline_external_egress1, + 1, + Some("multicast_external"), + ) + .await + .unwrap(); + check_counter_incremented( + switch, + &port_label_egress2, + ctr_baseline_external_egress2, + 1, + Some("multicast_external"), + ) + .await + .unwrap(); + + let ctr_external_egress3 = switch + .get_counter(&port_label_egress3, Some("multicast_external")) + .await + .unwrap(); + let ctr_external_egress4 = switch + .get_counter(&port_label_egress4, Some("multicast_external")) + .await + .unwrap(); + assert_eq!( + ctr_external_egress3, ctr_baseline_external_egress3, + "underlay replicas must not hit the external counter" + ); + assert_eq!( + ctr_external_egress4, ctr_baseline_external_egress4, + "underlay replicas must not hit the external counter" + ); + + let ctr_underlay_egress1 = switch + .get_counter(&port_label_egress1, Some("multicast_underlay")) + .await + .unwrap(); + let ctr_underlay_egress2 = switch + .get_counter(&port_label_egress2, Some("multicast_underlay")) + .await + .unwrap(); + assert_eq!( + ctr_underlay_egress1, ctr_baseline_underlay_egress1, + "decapped replicas must not hit the underlay counter" + ); + assert_eq!( + ctr_underlay_egress2, ctr_baseline_underlay_egress2, + "decapped replicas must not hit the underlay counter" + ); + cleanup_test_group(switch, get_group_ip(&created_group), TEST_TAG) .await .unwrap(); diff --git a/dpd/p4/sidecar.p4 b/dpd/p4/sidecar.p4 index 07a694ae..f69dda1a 100644 --- a/dpd/p4/sidecar.p4 +++ b/dpd/p4/sidecar.p4 @@ -2375,12 +2375,18 @@ control Egress( if (is_link_local_ipv6_mcast) { link_local_mcast_ctr.count(eg_intr_md.egress_port); - } else if (hdr.geneve.isValid()) { - external_mcast_ctr.count(eg_intr_md.egress_port); } else if (hdr.geneve.isValid() && hdr.geneve_opts.oxg_mcast.isValid() && - hdr.geneve_opts.oxg_mcast.mcast_tag == MULTICAST_TAG_UNDERLAY) { + (hdr.geneve_opts.oxg_mcast.mcast_tag == MULTICAST_TAG_UNDERLAY || + hdr.geneve_opts.oxg_mcast.mcast_tag == MULTICAST_TAG_UNDERLAY_EXTERNAL)) { underlay_mcast_ctr.count(eg_intr_md.egress_port); + } else { + // External deliveries were decapsulated before this + // point: at ingress for external-only groups, or by + // mcast_egress above for bifurcated-group copies + // bound for front panel ports. Keying on a valid + // geneve header would therefore never count them. + external_mcast_ctr.count(eg_intr_md.egress_port); } } else { // non-multicast packets should bypass the egress diff --git a/dpd/src/mcast/rollback.rs b/dpd/src/mcast/rollback.rs index fb73cd78..88e7ce9f 100644 --- a/dpd/src/mcast/rollback.rs +++ b/dpd/src/mcast/rollback.rs @@ -443,25 +443,33 @@ impl<'a> GroupCreateRollbackContext<'a> { ); } IpAddr::V6(ipv6) => { - // Clean up external bitmap entry only if both external and underlay groups exist - // (bitmap entries are only created for internal groups with both group types) - self.log_rollback_error( - "delete IPv6 egress bitmap entry", - &format!("for external group {}", self.external_id), - table::mcast::mcast_egress::del_bitmap_entry( - self.switch, - self.external_id, - ), - ); + // The bitmap and replication entries belong to internal groups + // only. External groups share the internal group's IDs, so + // deleting them now during an external-group rollback would + // destroy the live internal group's entries. + // + // Instead, we guard on `nat_target.is_none()` (internal group) + // to match the symmetric guards used by `remove_groups` and the + // source filter and NAT deletes below. + if self.nat_target.is_none() { + self.log_rollback_error( + "delete IPv6 egress bitmap entry", + &format!("for external group {}", self.external_id), + table::mcast::mcast_egress::del_bitmap_entry( + self.switch, + self.external_id, + ), + ); - self.log_rollback_error( - "delete IPv6 replication entry", - &format!("for group {ipv6}"), - table::mcast::mcast_replication::del_ipv6_entry( - self.switch, - ipv6, - ), - ); + self.log_rollback_error( + "delete IPv6 replication entry", + &format!("for group {ipv6}"), + table::mcast::mcast_replication::del_ipv6_entry( + self.switch, + ipv6, + ), + ); + } // Source filters only exist for external groups (which have // NAT targets). Internal groups don't have source filtering. From ab6c1a4326abbcb9f98a459ae74da4995e6b41af Mon Sep 17 00:00:00 2001 From: Zeeshan Lakhani Date: Sat, 4 Jul 2026 00:40:08 +0000 Subject: [PATCH 5/5] [p4, sidecar] attribute egress mcast counters by geneve validity A replica still carrying geneve at the egress counter block is bound for an underlay port while external replicas are decapped earlier (at ingress for external-only groups or in mcast_egress for bifurcated ones). We count underlay vs external as independent conditions on geneve validity so that each counter compiles to a gateway with payload counting, matching the form of the working multicast counter. --- Cargo.toml | 1 - dpd-client/tests/integration_tests/mcast.rs | 205 +++++++++++++++++++- dpd/p4/sidecar.p4 | 21 +- 3 files changed, 207 insertions(+), 20 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a9f050ce..59d65abe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -122,4 +122,3 @@ internet-checksum = "0.2" # It's common during development to use a local copy of various complex # dependencies. If you want to use those, uncomment one of these blocks. # - diff --git a/dpd-client/tests/integration_tests/mcast.rs b/dpd-client/tests/integration_tests/mcast.rs index 29ca0868..a0167221 100644 --- a/dpd-client/tests/integration_tests/mcast.rs +++ b/dpd-client/tests/integration_tests/mcast.rs @@ -2160,6 +2160,22 @@ async fn test_encapped_multicast_geneve_mcast_tag_to_external_members() .get_counter(&port_label_egress2, Some("multicast")) .await .unwrap(); + let ctr_baseline_external_egress1 = switch + .get_counter(&port_label_egress1, Some("multicast_external")) + .await + .unwrap(); + let ctr_baseline_external_egress2 = switch + .get_counter(&port_label_egress2, Some("multicast_external")) + .await + .unwrap(); + let ctr_baseline_underlay_egress1 = switch + .get_counter(&port_label_egress1, Some("multicast_underlay")) + .await + .unwrap(); + let ctr_baseline_underlay_egress2 = switch + .get_counter(&port_label_egress2, Some("multicast_underlay")) + .await + .unwrap(); switch.packet_test(vec![test_pkt], expected_pkts).unwrap(); @@ -2193,6 +2209,44 @@ async fn test_encapped_multicast_geneve_mcast_tag_to_external_members() .await .unwrap(); + // Decapped replicas to external members must be attributed to the + // external counter, not the underlay one. + check_counter_incremented( + switch, + &port_label_egress1, + ctr_baseline_external_egress1, + 1, + Some("multicast_external"), + ) + .await + .unwrap(); + check_counter_incremented( + switch, + &port_label_egress2, + ctr_baseline_external_egress2, + 1, + Some("multicast_external"), + ) + .await + .unwrap(); + + let ctr_underlay_egress1 = switch + .get_counter(&port_label_egress1, Some("multicast_underlay")) + .await + .unwrap(); + let ctr_underlay_egress2 = switch + .get_counter(&port_label_egress2, Some("multicast_underlay")) + .await + .unwrap(); + assert_eq!( + ctr_underlay_egress1, ctr_baseline_underlay_egress1, + "decapped replicas must not hit the underlay counter" + ); + assert_eq!( + ctr_underlay_egress2, ctr_baseline_underlay_egress2, + "decapped replicas must not hit the underlay counter" + ); + cleanup_test_group(switch, get_group_ip(&created_group), TEST_TAG) .await .unwrap(); @@ -2312,12 +2366,20 @@ async fn test_encapped_multicast_geneve_mcast_tag_to_underlay_members() let ctr_baseline_ingress = switch.get_counter(&port_label_ingress, Some("ingress")).await.unwrap(); - let ctr_baseline_mcast_egress3 = switch - .get_counter(&port_label_egress3, Some("multicast")) + let ctr_baseline_underlay_egress3 = switch + .get_counter(&port_label_egress3, Some("multicast_underlay")) .await .unwrap(); - let ctr_baseline_mcast_egress4 = switch - .get_counter(&port_label_egress4, Some("multicast")) + let ctr_baseline_underlay_egress4 = switch + .get_counter(&port_label_egress4, Some("multicast_underlay")) + .await + .unwrap(); + let ctr_baseline_external_egress3 = switch + .get_counter(&port_label_egress3, Some("multicast_external")) + .await + .unwrap(); + let ctr_baseline_external_egress4 = switch + .get_counter(&port_label_egress4, Some("multicast_external")) .await .unwrap(); @@ -2333,26 +2395,44 @@ async fn test_encapped_multicast_geneve_mcast_tag_to_underlay_members() .await .unwrap(); - // One still-encapsulated replica should be counted per underlay member port. + // Encapped replicas to underlay members must be attributed to the + // underlay counter, not the external one. check_counter_incremented( switch, &port_label_egress3, - ctr_baseline_mcast_egress3, + ctr_baseline_underlay_egress3, 1, - Some("multicast"), + Some("multicast_underlay"), ) .await .unwrap(); check_counter_incremented( switch, &port_label_egress4, - ctr_baseline_mcast_egress4, + ctr_baseline_underlay_egress4, 1, - Some("multicast"), + Some("multicast_underlay"), ) .await .unwrap(); + let ctr_external_egress3 = switch + .get_counter(&port_label_egress3, Some("multicast_external")) + .await + .unwrap(); + let ctr_external_egress4 = switch + .get_counter(&port_label_egress4, Some("multicast_external")) + .await + .unwrap(); + assert_eq!( + ctr_external_egress3, ctr_baseline_external_egress3, + "underlay-tagged replicas must not hit the external counter" + ); + assert_eq!( + ctr_external_egress4, ctr_baseline_external_egress4, + "underlay-tagged replicas must not hit the external counter" + ); + cleanup_test_group(switch, get_group_ip(&created_group), TEST_TAG) .await .unwrap(); @@ -2503,6 +2583,38 @@ async fn test_encapped_multicast_geneve_mcast_tag_to_underlay_and_external_membe .get_counter(&port_label_egress4, Some("multicast")) .await .unwrap(); + let ctr_baseline_underlay_egress1 = switch + .get_counter(&port_label_egress1, Some("multicast_underlay")) + .await + .unwrap(); + let ctr_baseline_underlay_egress2 = switch + .get_counter(&port_label_egress2, Some("multicast_underlay")) + .await + .unwrap(); + let ctr_baseline_underlay_egress3 = switch + .get_counter(&port_label_egress3, Some("multicast_underlay")) + .await + .unwrap(); + let ctr_baseline_underlay_egress4 = switch + .get_counter(&port_label_egress4, Some("multicast_underlay")) + .await + .unwrap(); + let ctr_baseline_external_egress1 = switch + .get_counter(&port_label_egress1, Some("multicast_external")) + .await + .unwrap(); + let ctr_baseline_external_egress2 = switch + .get_counter(&port_label_egress2, Some("multicast_external")) + .await + .unwrap(); + let ctr_baseline_external_egress3 = switch + .get_counter(&port_label_egress3, Some("multicast_external")) + .await + .unwrap(); + let ctr_baseline_external_egress4 = switch + .get_counter(&port_label_egress4, Some("multicast_external")) + .await + .unwrap(); switch.packet_test(vec![test_pkt], expected_pkts).unwrap(); @@ -2534,6 +2646,81 @@ async fn test_encapped_multicast_geneve_mcast_tag_to_underlay_and_external_membe .unwrap(); } + // Still-encapsulated replicas to underlay members count as underlay, + // not external, for UNDERLAY_EXTERNAL tagged groups. + check_counter_incremented( + switch, + &port_label_egress3, + ctr_baseline_underlay_egress3, + 1, + Some("multicast_underlay"), + ) + .await + .unwrap(); + check_counter_incremented( + switch, + &port_label_egress4, + ctr_baseline_underlay_egress4, + 1, + Some("multicast_underlay"), + ) + .await + .unwrap(); + + // Decapped replicas to external members count as external. + check_counter_incremented( + switch, + &port_label_egress1, + ctr_baseline_external_egress1, + 1, + Some("multicast_external"), + ) + .await + .unwrap(); + check_counter_incremented( + switch, + &port_label_egress2, + ctr_baseline_external_egress2, + 1, + Some("multicast_external"), + ) + .await + .unwrap(); + + let ctr_external_egress3 = switch + .get_counter(&port_label_egress3, Some("multicast_external")) + .await + .unwrap(); + let ctr_external_egress4 = switch + .get_counter(&port_label_egress4, Some("multicast_external")) + .await + .unwrap(); + assert_eq!( + ctr_external_egress3, ctr_baseline_external_egress3, + "underlay replicas must not hit the external counter" + ); + assert_eq!( + ctr_external_egress4, ctr_baseline_external_egress4, + "underlay replicas must not hit the external counter" + ); + + let ctr_underlay_egress1 = switch + .get_counter(&port_label_egress1, Some("multicast_underlay")) + .await + .unwrap(); + let ctr_underlay_egress2 = switch + .get_counter(&port_label_egress2, Some("multicast_underlay")) + .await + .unwrap(); + assert_eq!( + ctr_underlay_egress1, ctr_baseline_underlay_egress1, + "decapped replicas must not hit the underlay counter" + ); + assert_eq!( + ctr_underlay_egress2, ctr_baseline_underlay_egress2, + "decapped replicas must not hit the underlay counter" + ); + cleanup_test_group(switch, get_group_ip(&created_group), TEST_TAG) .await .unwrap(); diff --git a/dpd/p4/sidecar.p4 b/dpd/p4/sidecar.p4 index f69dda1a..c838e8f8 100644 --- a/dpd/p4/sidecar.p4 +++ b/dpd/p4/sidecar.p4 @@ -2373,19 +2373,20 @@ control Egress( } else if (is_mcast == true) { mcast_ctr.count(eg_intr_md.egress_port); + // A replica still carrying geneve at this point is bound + // for an underlay port. External replicas were + // decapsulated at ingress (external-only groups) or by + // mcast_egress above (bifurcated groups). + // + // This is written as independent conditions so each + // counter compiles to its own simple gateway. if (is_link_local_ipv6_mcast) { link_local_mcast_ctr.count(eg_intr_md.egress_port); - } else if (hdr.geneve.isValid() && - hdr.geneve_opts.oxg_mcast.isValid() && - (hdr.geneve_opts.oxg_mcast.mcast_tag == MULTICAST_TAG_UNDERLAY || - hdr.geneve_opts.oxg_mcast.mcast_tag == MULTICAST_TAG_UNDERLAY_EXTERNAL)) { + } + if (!is_link_local_ipv6_mcast && hdr.geneve.isValid()) { underlay_mcast_ctr.count(eg_intr_md.egress_port); - } else { - // External deliveries were decapsulated before this - // point: at ingress for external-only groups, or by - // mcast_egress above for bifurcated-group copies - // bound for front panel ports. Keying on a valid - // geneve header would therefore never count them. + } + if (!is_link_local_ipv6_mcast && !hdr.geneve.isValid()) { external_mcast_ctr.count(eg_intr_md.egress_port); } } else {