diff --git a/Cargo.lock b/Cargo.lock index 1dc43a0af..a3d36e1a5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1286,6 +1286,10 @@ dependencies = [ "thiserror", ] +[[package]] +name = "dataplane-fixed-size" +version = "0.21.0" + [[package]] name = "dataplane-flow-entry" version = "0.21.0" @@ -1457,6 +1461,10 @@ dependencies = [ "tracing", ] +[[package]] +name = "dataplane-lookup" +version = "0.21.0" + [[package]] name = "dataplane-lpm" version = "0.21.0" @@ -1563,6 +1571,7 @@ dependencies = [ "bytecheck", "dataplane-common", "dataplane-concurrency", + "dataplane-fixed-size", "dataplane-id", "derive_builder", "downcast-rs", diff --git a/Cargo.toml b/Cargo.toml index a23d2b3f1..beb80d162 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ members = [ "dpdk-sysroot-helper", "dpdk-test-macros", "errno", + "fixed-size", "flow-entry", "flow-filter", "hardware", @@ -23,6 +24,7 @@ members = [ "k8s-less", "left-right-tlcache", "lifecycle", + "lookup", "lpm", "mgmt", "nat", @@ -72,6 +74,7 @@ dpdk-sysroot-helper = { path = "./dpdk-sysroot-helper", package = "dataplane-dpd dpdk-test-macros = { path = "./dpdk-test-macros", package = "dataplane-dpdk-test-macros", features = [] } dplane-rpc = { git = "https://github.com/githedgehog/dplane-rpc.git", branch = "pr/daniel-noland/bumps", features = [] } errno = { path = "./errno", package = "dataplane-errno", features = [] } +fixed-size = { path = "./fixed-size", package = "dataplane-fixed-size", features = [] } flow-entry = { path = "./flow-entry", package = "dataplane-flow-entry", features = [] } flow-filter = { path = "./flow-filter", package = "dataplane-flow-filter", features = [] } hardware = { path = "./hardware", package = "dataplane-hardware", features = [] } @@ -82,6 +85,7 @@ k8s-intf = { path = "./k8s-intf", package = "dataplane-k8s-intf", default-featur k8s-less = { path = "./k8s-less", package = "dataplane-k8s-less", features = [] } left-right-tlcache = { path = "./left-right-tlcache", package = "dataplane-left-right-tlcache", features = [] } lifecycle = { path = "./lifecycle", package = "dataplane-lifecycle", features = [] } +lookup = { path = "./lookup", package = "dataplane-lookup", features = [] } lpm = { path = "./lpm", package = "dataplane-lpm", features = [] } mgmt = { path = "./mgmt", package = "dataplane-mgmt", features = [] } nat = { path = "./nat", package = "dataplane-nat", features = [] } @@ -283,6 +287,11 @@ package = "dataplane-dpdk-sys" miri = false # hopeless + pointless wasm = false # hopeless + pointless +[workspace.metadata.package.fixed-size] +package = "dataplane-fixed-size" +miri = true +wasm = true + [workspace.metadata.package.flow-entry] package = "dataplane-flow-entry" miri = true @@ -318,6 +327,11 @@ package = "dataplane-k8s-less" miri = true wasm = false # split +[workspace.metadata.package.lookup] +package = "dataplane-lookup" +miri = true +wasm = false # split (std collections) + [workspace.metadata.package.mgmt] package = "dataplane-mgmt" miri = false diff --git a/fixed-size/Cargo.toml b/fixed-size/Cargo.toml new file mode 100644 index 000000000..3e0596e85 --- /dev/null +++ b/fixed-size/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "dataplane-fixed-size" +edition.workspace = true +license.workspace = true +publish.workspace = true +version.workspace = true + +[dependencies] diff --git a/fixed-size/src/lib.rs b/fixed-size/src/lib.rs new file mode 100644 index 000000000..1a0617612 --- /dev/null +++ b/fixed-size/src/lib.rs @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Open Network Fabric Authors + +#![no_std] +#![deny( + unsafe_code, + clippy::all, + clippy::pedantic, + clippy::unwrap_used, + clippy::expect_used, + clippy::panic +)] + +use core::net::{Ipv4Addr, Ipv6Addr}; +pub trait FixedSize: Copy { + const SIZE: usize; + fn write_be(&self, out: &mut [u8]); +} + +impl FixedSize for u8 { + const SIZE: usize = 1; + fn write_be(&self, out: &mut [u8]) { + out[0] = *self; + } +} + +impl FixedSize for u16 { + const SIZE: usize = 2; + fn write_be(&self, out: &mut [u8]) { + out[..Self::SIZE].copy_from_slice(&self.to_be_bytes()); + } +} + +impl FixedSize for u32 { + const SIZE: usize = 4; + fn write_be(&self, out: &mut [u8]) { + out[..Self::SIZE].copy_from_slice(&self.to_be_bytes()); + } +} + +impl FixedSize for u64 { + const SIZE: usize = 8; + fn write_be(&self, out: &mut [u8]) { + out[..Self::SIZE].copy_from_slice(&self.to_be_bytes()); + } +} + +impl FixedSize for u128 { + const SIZE: usize = 16; + fn write_be(&self, out: &mut [u8]) { + out[..Self::SIZE].copy_from_slice(&self.to_be_bytes()); + } +} + +impl FixedSize for Ipv4Addr { + const SIZE: usize = 4; + fn write_be(&self, out: &mut [u8]) { + out[..Self::SIZE].copy_from_slice(&self.octets()); + } +} + +impl FixedSize for Ipv6Addr { + const SIZE: usize = 16; + fn write_be(&self, out: &mut [u8]) { + out[..Self::SIZE].copy_from_slice(&self.octets()); + } +} diff --git a/lookup/Cargo.toml b/lookup/Cargo.toml new file mode 100644 index 000000000..0d5a4f266 --- /dev/null +++ b/lookup/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "dataplane-lookup" +edition.workspace = true +license.workspace = true +publish.workspace = true +version.workspace = true + +[dependencies] diff --git a/lookup/src/lib.rs b/lookup/src/lib.rs new file mode 100644 index 000000000..4bf4358a4 --- /dev/null +++ b/lookup/src/lib.rs @@ -0,0 +1,190 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Open Network Fabric Authors + +#![deny( + unsafe_code, + clippy::all, + clippy::pedantic, + clippy::unwrap_used, + clippy::expect_used, + clippy::panic +)] +#![allow(missing_docs)] + +use std::collections::{BTreeMap, HashMap}; +use std::hash::Hash; +pub trait Projection { + fn project(self) -> T; +} +impl Projection> for Option { + fn project(self) -> Option { + self + } +} +pub trait Lookup { + fn lookup(&self, key: &K) -> Option<&A>; + fn classify(&self, source: S) -> Option<&A> + where + S: Projection, + { + self.lookup(&source.project()) + } + fn classify_opt(&self, source: S) -> Option<&A> + where + S: Projection>, + { + source.project().and_then(|key| self.lookup(&key)) + } +} + +impl Lookup for BTreeMap { + fn lookup(&self, key: &K) -> Option<&V> { + BTreeMap::get(self, key) + } +} + +impl Lookup for HashMap { + fn lookup(&self, key: &K) -> Option<&V> { + HashMap::get(self, key) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + struct Pkt { + src: u32, + dst: u32, + sport: u16, + dport: u16, + } + + impl Projection<(u32, u32)> for &Pkt { + fn project(self) -> (u32, u32) { + (self.src, self.dst) + } + } + + impl Projection<(u32, u32, u16, u16)> for &Pkt { + fn project(self) -> (u32, u32, u16, u16) { + (self.src, self.dst, self.sport, self.dport) + } + } + + impl<'a> Projection<(&'a u32, &'a u32)> for &'a Pkt { + fn project(self) -> (&'a u32, &'a u32) { + (&self.src, &self.dst) + } + } + impl Projection> for &Pkt { + fn project(self) -> Option<(u32, u32)> { + (self.src != 0).then_some((self.src, self.dst)) + } + } + + #[derive(Debug, PartialEq, Eq)] + enum Action { + Allow, + Drop, + } + + #[test] + fn classify_picks_the_two_tuple_projection_from_the_table_type() { + let mut table: BTreeMap<(u32, u32), Action> = BTreeMap::new(); + table.insert((10, 20), Action::Drop); + let pkt = Pkt { + src: 10, + dst: 20, + sport: 22, + dport: 80, + }; + assert_eq!(table.classify(&pkt), Some(&Action::Drop)); + } + + #[test] + fn classify_picks_the_four_tuple_projection_from_the_table_type() { + let mut table: BTreeMap<(u32, u32, u16, u16), Action> = BTreeMap::new(); + table.insert((10, 20, 22, 80), Action::Allow); + let pkt = Pkt { + src: 10, + dst: 20, + sport: 22, + dport: 80, + }; + assert_eq!(table.classify(&pkt), Some(&Action::Allow)); + } + + #[test] + fn borrowed_tuple_projection_threads_lifetime() { + let pkt = Pkt { + src: 10, + dst: 20, + sport: 0, + dport: 0, + }; + let (src, dst): (&u32, &u32) = (&pkt).project(); + assert_eq!(*src, 10); + assert_eq!(*dst, 20); + } + + #[test] + fn miss_returns_none() { + let table: BTreeMap<(u32, u32), Action> = BTreeMap::new(); + let pkt = Pkt { + src: 1, + dst: 2, + sport: 3, + dport: 4, + }; + assert_eq!(table.classify(&pkt), None); + } + + #[test] + fn classify_opt_looks_up_when_projection_yields_some() { + let mut table: BTreeMap<(u32, u32), Action> = BTreeMap::new(); + table.insert((10, 20), Action::Drop); + let pkt = Pkt { + src: 10, + dst: 20, + sport: 0, + dport: 0, + }; + assert_eq!(table.classify_opt(&pkt), Some(&Action::Drop)); + } + + #[test] + fn classify_opt_short_circuits_when_projection_yields_none() { + let mut table: BTreeMap<(u32, u32), Action> = BTreeMap::new(); + table.insert((0, 20), Action::Drop); + let pkt = Pkt { + src: 0, + dst: 20, + sport: 0, + dport: 0, + }; + assert_eq!(table.classify_opt(&pkt), None); + } + + #[test] + fn classify_opt_accepts_a_computed_option_via_identity() { + let mut table: BTreeMap<(u32, u32), Action> = BTreeMap::new(); + table.insert((10, 20), Action::Drop); + let built: Option<(u32, u32)> = Some((10, 20)); + assert_eq!(table.classify_opt(built), Some(&Action::Drop)); + assert_eq!(table.classify_opt(None::<(u32, u32)>), None); + } + + #[test] + fn hashmap_backend_works_the_same_way() { + let mut table: HashMap<(u32, u32), Action> = HashMap::new(); + table.insert((10, 20), Action::Drop); + let pkt = Pkt { + src: 10, + dst: 20, + sport: 0, + dport: 0, + }; + assert_eq!(table.classify(&pkt), Some(&Action::Drop)); + } +} diff --git a/net/Cargo.toml b/net/Cargo.toml index fac84a966..bbda3162a 100644 --- a/net/Cargo.toml +++ b/net/Cargo.toml @@ -27,6 +27,7 @@ concurrency = { workspace = true } derive_builder = { workspace = true, features = ["alloc"] } downcast-rs = { workspace = true, features = ["sync"] } etherparse = { workspace = true, features = ["std"] } +fixed-size = { workspace = true, features = [] } id = { workspace = true } multi_index_map = { workspace = true, default-features = false, features = ["serde"] } rapidhash = { workspace = true } diff --git a/net/src/fixed_size.rs b/net/src/fixed_size.rs new file mode 100644 index 000000000..f14effa17 --- /dev/null +++ b/net/src/fixed_size.rs @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Open Network Fabric Authors + +use fixed_size::FixedSize; + +use crate::ipv4::UnicastIpv4Addr; +use crate::tcp::TcpPort; +use crate::udp::UdpPort; +use crate::vxlan::Vni; + +impl FixedSize for TcpPort { + const SIZE: usize = 2; + fn write_be(&self, out: &mut [u8]) { + self.as_u16().write_be(out); + } +} + +impl FixedSize for UdpPort { + const SIZE: usize = 2; + fn write_be(&self, out: &mut [u8]) { + self.as_u16().write_be(out); + } +} + +impl FixedSize for UnicastIpv4Addr { + const SIZE: usize = 4; + fn write_be(&self, out: &mut [u8]) { + self.inner().write_be(out); + } +} + +impl FixedSize for Vni { + const SIZE: usize = 4; + fn write_be(&self, out: &mut [u8]) { + self.as_u32().write_be(out); + } +} + +#[cfg(test)] +mod tests { + use core::net::Ipv4Addr; + + use super::*; + + #[test] + fn ports_write_two_big_endian_bytes() { + assert_eq!(::SIZE, 2); + assert_eq!(::SIZE, 2); + let mut buf = [0u8; 2]; + TcpPort::new_checked(443).unwrap().write_be(&mut buf); + assert_eq!(buf, 443u16.to_be_bytes()); + UdpPort::new_checked(4789).unwrap().write_be(&mut buf); + assert_eq!(buf, 4789u16.to_be_bytes()); + } + + #[test] + fn unicast_v4_writes_four_octets() { + assert_eq!(::SIZE, 4); + let mut buf = [0u8; 4]; + UnicastIpv4Addr::new(Ipv4Addr::new(10, 0, 1, 2)) + .unwrap() + .write_be(&mut buf); + assert_eq!(buf, [10, 0, 1, 2]); + } + + #[test] + fn vni_writes_four_bytes_with_zero_high_byte() { + assert_eq!(::SIZE, 4); + let mut buf = [0u8; 4]; + Vni::new_checked(0x00AB_CDEF).unwrap().write_be(&mut buf); + assert_eq!(buf, [0x00, 0xAB, 0xCD, 0xEF]); + } +} diff --git a/net/src/lib.rs b/net/src/lib.rs index e1683d429..e76f84ebe 100644 --- a/net/src/lib.rs +++ b/net/src/lib.rs @@ -19,6 +19,8 @@ pub mod addr_parse_error; pub mod buffer; pub mod checksum; pub mod eth; +/// `FixedSize` impls bridging `net` field types into match-action keys. +mod fixed_size; #[cfg(unix)] pub mod flows; pub mod headers;