From b586dfd421384ae60dad72c7771e9b4e83560148 Mon Sep 17 00:00:00 2001 From: Zeeshan Lakhani Date: Thu, 26 Mar 2026 08:44:23 +0000 Subject: [PATCH] [multicast] wire up softnpu backend to multicast table ops 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. All multicast action arms are gated with #[cfg(feature = "multicast")]. ## References - [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) --- Cargo.lock | 106 +++---- Cargo.toml | 9 +- asic/src/softnpu/mcast.rs | 302 ++++++++++++++++++++ asic/src/softnpu/mod.rs | 67 +++-- asic/src/softnpu/table.rs | 565 ++++++++++++++++++++++++++++++-------- common/src/illumos.rs | 2 +- packet/src/eth.rs | 2 +- swadm/src/main.rs | 2 +- 8 files changed, 856 insertions(+), 199 deletions(-) create mode 100644 asic/src/softnpu/mcast.rs diff --git a/Cargo.lock b/Cargo.lock index 196f7b15..fb6ed8f2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -187,7 +187,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)", @@ -384,9 +384,9 @@ dependencies = [ [[package]] name = "bhyve_api" version = "0.0.0" -source = "git+https://github.com/oxidecomputer/propolis?rev=2aa7f9d0ee84a1c45e821d6444b1d2f0e69b743e#2aa7f9d0ee84a1c45e821d6444b1d2f0e69b743e" +source = "git+https://github.com/oxidecomputer/propolis?branch=zl%2Fmulticast#4a98f905bcd56f74cfc77d1d317e77116d4c4827" dependencies = [ - "bhyve_api_sys 0.0.0 (git+https://github.com/oxidecomputer/propolis?rev=2aa7f9d0ee84a1c45e821d6444b1d2f0e69b743e)", + "bhyve_api_sys 0.0.0 (git+https://github.com/oxidecomputer/propolis?branch=zl%2Fmulticast)", "libc", "strum 0.26.3", ] @@ -394,9 +394,9 @@ 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=2aa7f9d0ee84a1c45e821d6444b1d2f0e69b743e#2aa7f9d0ee84a1c45e821d6444b1d2f0e69b743e" dependencies = [ - "bhyve_api_sys 0.0.0 (git+https://github.com/oxidecomputer/propolis)", + "bhyve_api_sys 0.0.0 (git+https://github.com/oxidecomputer/propolis?rev=2aa7f9d0ee84a1c45e821d6444b1d2f0e69b743e)", "libc", "strum 0.26.3", ] @@ -404,7 +404,7 @@ dependencies = [ [[package]] name = "bhyve_api_sys" version = "0.0.0" -source = "git+https://github.com/oxidecomputer/propolis?rev=2aa7f9d0ee84a1c45e821d6444b1d2f0e69b743e#2aa7f9d0ee84a1c45e821d6444b1d2f0e69b743e" +source = "git+https://github.com/oxidecomputer/propolis?branch=zl%2Fmulticast#4a98f905bcd56f74cfc77d1d317e77116d4c4827" dependencies = [ "libc", "strum 0.26.3", @@ -413,7 +413,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=2aa7f9d0ee84a1c45e821d6444b1d2f0e69b743e#2aa7f9d0ee84a1c45e821d6444b1d2f0e69b743e" dependencies = [ "libc", "strum 0.26.3", @@ -454,6 +454,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" @@ -1094,11 +1100,11 @@ dependencies = [ [[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#4a98f905bcd56f74cfc77d1d317e77116d4c4827" 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", ] @@ -1514,7 +1520,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#4a98f905bcd56f74cfc77d1d317e77116d4c4827" dependencies = [ "libc", "strum 0.26.3", @@ -3107,7 +3113,7 @@ dependencies = [ "itertools 0.14.0", "libc", "macaddr", - "nix", + "nix 0.30.1", "omicron-common", "omicron-uuid-kinds", "omicron-workspace-hack", @@ -4052,6 +4058,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" @@ -4222,7 +4240,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#4a98f905bcd56f74cfc77d1d317e77116d4c4827" dependencies = [ "libc", "nvpair_sys", @@ -4246,7 +4264,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#4a98f905bcd56f74cfc77d1d317e77116d4c4827" dependencies = [ "libc", ] @@ -4503,11 +4521,12 @@ checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d" [[package]] name = "oxide-tokio-rt" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84bd87abf37c68d414e4df90a545857542140e07206f75039b8f63b244da87b8" +checksum = "cb926ddb4c76e47e312fb4cf0491573760042037ef6b3b09756ebc1a06f68845" dependencies = [ "anyhow", + "nix 0.31.2", "tokio", "tokio-dtrace", ] @@ -4766,7 +4785,7 @@ dependencies = [ [[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", @@ -5421,11 +5440,12 @@ 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#4a98f905bcd56f74cfc77d1d317e77116d4c4827" 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", @@ -5434,13 +5454,16 @@ dependencies = [ "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", @@ -5448,11 +5471,12 @@ dependencies = [ "serde_arrays", "serde_json", "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", "zerocopy 0.8.27", @@ -5474,7 +5498,7 @@ dependencies = [ [[package]] name = "propolis_types" version = "0.0.0" -source = "git+https://github.com/oxidecomputer/propolis?rev=2aa7f9d0ee84a1c45e821d6444b1d2f0e69b743e#2aa7f9d0ee84a1c45e821d6444b1d2f0e69b743e" +source = "git+https://github.com/oxidecomputer/propolis?branch=zl%2Fmulticast#4a98f905bcd56f74cfc77d1d317e77116d4c4827" dependencies = [ "schemars 0.8.22", "serde", @@ -5483,7 +5507,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=2aa7f9d0ee84a1c45e821d6444b1d2f0e69b743e#2aa7f9d0ee84a1c45e821d6444b1d2f0e69b743e" dependencies = [ "schemars 0.8.22", "serde", @@ -5912,7 +5936,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#4a98f905bcd56f74cfc77d1d317e77116d4c4827" dependencies = [ "ascii", "bitflags 2.9.4", @@ -5928,7 +5952,7 @@ dependencies = [ [[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#4a98f905bcd56f74cfc77d1d317e77116d4c4827" dependencies = [ "strum 0.26.3", ] @@ -6859,18 +6883,7 @@ dependencies = [ [[package]] name = "softnpu" version = "0.2.0" -source = "git+https://github.com/oxidecomputer/softnpu?branch=main#7e015d167a914777bc21434d1c61f205f22993b1" -dependencies = [ - "p4rs", - "serde", - "serde_json", - "tokio", -] - -[[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", @@ -7461,9 +7474,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.49.0" +version = "1.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" dependencies = [ "bytes", "libc", @@ -7853,7 +7866,7 @@ dependencies = [ "clap", "hubpack", "itertools 0.14.0", - "nix", + "nix 0.30.1", "schemars 0.8.22", "serde", "slog", @@ -8424,19 +8437,10 @@ 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#4a98f905bcd56f74cfc77d1d317e77116d4c4827" 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" -dependencies = [ - "libc", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index efbf670a..9ad788f7 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.3" parking_lot = "0.12" pretty_assertions = "1.4" proc-macro2 = "1.0" @@ -107,7 +107,7 @@ 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" +tokio = "1.50" toml = "0.9" usdt = "0.6" uuid = { version = "1.10", features = [ "v4", "serde" ] } @@ -117,3 +117,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/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(), ) }