Skip to content

Commit 45262b6

Browse files
daniel-nolandclaude
andcommitted
[squash] match-action vocabulary + ACL backends + lookup/fixed-size facades
Pre-review squash of the acl-pat-design branch (51 commits' worth of accumulated work + iteration above PR 7's threading-rewrite tip `d69246dbf`). To be re-split into a reviewable stack before pushing. Contents: - Two new facade crates: `dataplane-fixed-size` (FixedSize trait + primitive impls) and `dataplane-lookup` (Lookup + Projection + classify/classify_opt + Option<K> identity + std map impls). Both dependency-free. - `match-action` crate: backend-agnostic key vocabulary -- the `MatchKey` trait (+ derive supporting generics, default #[exact]), `FieldSpec`/`FieldKind`, `Backend`/`IntoBackendField`, four `*Spec<T>` rule wrappers, type-erased `FieldPredicate` enum with per-kind structs (Exact/Prefix/Mask/Range) and always-on assertions. `match-action-derive` proc-macro. - `net`: implements `FixedSize` for `TcpPort`, `UdpPort`, `UnicastIpv4Addr`, `Vni`. Wires its wire-newtypes into the match-action key vocabulary with no orphan-rule dance. - `cascade`: re-exports `Lookup` / `Projection` from the lookup crate (no behavior change for consumers). - `dpdk`: `#[with_eal]` proc macro for EAL-bound integration tests. - `acl` crate: the match-action ACL stack. * Software reference backend (linear scan, RIB-shaped, non-lossy invariant) + a differential bolero harness vs DPDK with rule-derived boundary probes. * DPDK `rte_acl` backend: layout planner, rule splice, install pipeline, single-shot + SIMD batch classify, `dpdk_table_alias!` macro, IPv6 via 4x u32 wide-field splitting, blanket `AclWord for all FixedSize` (so net wire newtypes work in DPDK keys with zero acl-side code). * `Lookup::classify_opt` for fallible projection (`Option<K>` identity for one-off call sites; the `PacketSource` + Projection impl pattern for reusable sources). Metadata projection demo: header fields + VRF + VNI through both backends. * Criterion benches: reference + DPDK lookup (single + batch) and table-build, IPv4 and IPv6, 1..16384 log2 rule-count sweep. - `nix`: `benches` target precompiling criterion binaries against the optimized release DPDK sysroot. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent d69246d commit 45262b6

67 files changed

Lines changed: 12098 additions & 141 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Cargo.lock

Lines changed: 233 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
[workspace]
22

33
members = [
4+
"acl",
45
"args",
6+
"cascade",
57
"cli",
68
"common",
79
"concurrency",
@@ -11,7 +13,9 @@ members = [
1113
"dpdk",
1214
"dpdk-sys",
1315
"dpdk-sysroot-helper",
16+
"dpdk-test-macros",
1417
"errno",
18+
"fixed-size",
1519
"flow-entry",
1620
"flow-filter",
1721
"hardware",
@@ -22,7 +26,10 @@ members = [
2226
"k8s-less",
2327
"left-right-tlcache",
2428
"lifecycle",
29+
"lookup",
2530
"lpm",
31+
"match-action",
32+
"match-action-derive",
2633
"mgmt",
2734
"nat",
2835
"net",
@@ -59,7 +66,9 @@ repository = "https://github.com/githedgehog/dataplane/"
5966
# justifying why it is workspace-wide.
6067

6168
# Internal
69+
acl = { path = "./acl", package = "dataplane-acl", features = [] }
6270
args = { path = "./args", package = "dataplane-args", features = [] }
71+
cascade = { path = "./cascade", package = "dataplane-cascade", features = [] }
6372
cli = { path = "./cli", package = "dataplane-cli", features = [] }
6473
common = { path = "./common", package = "dataplane-common", features = [] }
6574
concurrency = { path = "./concurrency", package = "dataplane-concurrency", features = [] }
@@ -68,8 +77,10 @@ config = { path = "./config", package = "dataplane-config", features = [] }
6877
dpdk = { path = "./dpdk", package = "dataplane-dpdk", features = [] }
6978
dpdk-sys = { path = "./dpdk-sys", package = "dataplane-dpdk-sys", features = [] }
7079
dpdk-sysroot-helper = { path = "./dpdk-sysroot-helper", package = "dataplane-dpdk-sysroot-helper", features = [] }
80+
dpdk-test-macros = { path = "./dpdk-test-macros", package = "dataplane-dpdk-test-macros", features = [] }
7181
dplane-rpc = { git = "https://github.com/githedgehog/dplane-rpc.git", rev = "e8fc33db10e1d00785f2a2b90cbadcad7900f200", features = [] }
7282
errno = { path = "./errno", package = "dataplane-errno", features = [] }
83+
fixed-size = { path = "./fixed-size", package = "dataplane-fixed-size", features = [] }
7384
flow-entry = { path = "./flow-entry", package = "dataplane-flow-entry", features = [] }
7485
flow-filter = { path = "./flow-filter", package = "dataplane-flow-filter", features = [] }
7586
hardware = { path = "./hardware", package = "dataplane-hardware", features = [] }
@@ -80,7 +91,10 @@ k8s-intf = { path = "./k8s-intf", package = "dataplane-k8s-intf", default-featur
8091
k8s-less = { path = "./k8s-less", package = "dataplane-k8s-less", features = [] }
8192
left-right-tlcache = { path = "./left-right-tlcache", package = "dataplane-left-right-tlcache", features = [] }
8293
lifecycle = { path = "./lifecycle", package = "dataplane-lifecycle", features = [] }
94+
lookup = { path = "./lookup", package = "dataplane-lookup", features = [] }
8395
lpm = { path = "./lpm", package = "dataplane-lpm", features = [] }
96+
match-action = { path = "./match-action", package = "dataplane-match-action", features = [] }
97+
match-action-derive = { path = "./match-action-derive", package = "dataplane-match-action-derive", features = [] }
8498
mgmt = { path = "./mgmt", package = "dataplane-mgmt", features = [] }
8599
nat = { path = "./nat", package = "dataplane-nat", features = [] }
86100
net = { path = "./net", package = "dataplane-net", features = [] }
@@ -113,6 +127,7 @@ bytes = { version = "1.11.1", default-features = false, features = [] }
113127
caps = { version = "0.5.6", default-features = false, features = [] }
114128
chrono = { version = "0.4.44", default-features = false, features = [] }
115129
clap = { version = "4.6.1", default-features = true, features = [] }
130+
criterion = { version = "0.5.1", default-features = false, features = ["cargo_bench_support"] }
116131
color-eyre = { version = "0.6.5", default-features = false, features = [] }
117132
colored = { version = "3.1.1", default-features = false, features = [] }
118133
crossbeam-utils = { version = "0.8.21", default-features = false, features = [] }

acl/Cargo.toml

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
[package]
2+
name = "dataplane-acl"
3+
edition.workspace = true
4+
license.workspace = true
5+
publish.workspace = true
6+
version.workspace = true
7+
8+
[features]
9+
default = ["dpdk"]
10+
# Enables the DPDK `rte_acl` backend for `cascade::Lookup`. Pulls in
11+
# the `dpdk` crate (and transitively `dpdk-sys`), so off by default;
12+
# only the production binary / DPDK-bound consumers turn it on.
13+
dpdk = ["dep:dpdk", "dep:thiserror"]
14+
15+
[dependencies]
16+
arrayvec = { workspace = true, default-features = true }
17+
concurrency = { workspace = true, features = [] }
18+
dpdk = { workspace = true, optional = true }
19+
lookup = { workspace = true, features = [] }
20+
match-action = { workspace = true, features = ["derive"] }
21+
net = { workspace = true, features = [] }
22+
thiserror = { workspace = true, optional = true }
23+
24+
[dev-dependencies]
25+
# Differential oracle harness: random rulesets + packets compared
26+
# between the reference and DPDK backends.
27+
bolero = { workspace = true, features = ["std"] }
28+
criterion = { workspace = true }
29+
# Override the regular `dpdk` dep to enable its `test` feature
30+
# (exposes `dpdk::test_support::start_eal` and `dpdk::with_eal`).
31+
dpdk = { workspace = true, features = ["test"] }
32+
# Override match-action to also enable `bolero` for property-test
33+
# generators (`FieldHit` / `FieldMiss`).
34+
match-action = { workspace = true, features = ["derive", "bolero"] }
35+
net = { workspace = true, features = ["test_buffer", "builder"] }
36+
37+
[[bench]]
38+
name = "reference_five_tuple"
39+
harness = false
40+
41+
[[bench]]
42+
name = "dpdk_five_tuple"
43+
harness = false
44+
45+
[[bench]]
46+
name = "table_build"
47+
harness = false

acl/benches/dpdk_five_tuple.rs

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// Copyright Open Network Fabric Authors
3+
4+
//! Criterion benchmark for DPDK `rte_acl` 5-tuple lookup at IPv4 and
5+
//! IPv6 widths. Companion to `reference_five_tuple`; same rule-count
6+
//! sweep, miss vs hit, single-shot vs SIMD batch. `rte_acl` compiles
7+
//! rules into a trie walked by the input bytes, so lookup cost is
8+
//! roughly flat in rule count -- contrast with the reference's `O(n)`
9+
//! linear scan. The v6 path exercises the wide-field split (one
10+
//! 16-byte address -> four 4-byte sub-fields). Requires a live EAL:
11+
//! `cargo bench -p dataplane-acl --bench dpdk_five_tuple`.
12+
13+
#![allow(clippy::unwrap_used, clippy::expect_used)]
14+
15+
#[cfg(feature = "dpdk")]
16+
mod bench {
17+
use core::net::{Ipv4Addr, Ipv6Addr};
18+
use core::num::NonZero;
19+
20+
use criterion::measurement::WallTime;
21+
use criterion::{BenchmarkGroup, BenchmarkId, Criterion, Throughput, black_box};
22+
23+
use dataplane_acl::dpdk::install::install_table;
24+
use dataplane_acl::dpdk::lookup::DpdkAclLookup;
25+
use dataplane_acl::dpdk::rule::{Dpdk, RuleSpec};
26+
use dataplane_acl::dpdk_table_alias;
27+
use dpdk::acl::{CategoryMask, Priority};
28+
use lookup::Lookup;
29+
use match_action::{ExactSpec, MatchKey, PrefixSpec, RangeSpec};
30+
31+
#[derive(MatchKey)]
32+
struct FiveTuple {
33+
#[exact]
34+
proto: u8,
35+
#[prefix]
36+
src: Ipv4Addr,
37+
#[prefix]
38+
dst: Ipv4Addr,
39+
#[range]
40+
sport: u16,
41+
#[range]
42+
dport: u16,
43+
}
44+
45+
#[derive(MatchKey)]
46+
struct FiveTuple6 {
47+
#[exact]
48+
proto: u8,
49+
#[prefix]
50+
src: Ipv6Addr,
51+
#[prefix]
52+
dst: Ipv6Addr,
53+
#[range]
54+
sport: u16,
55+
#[range]
56+
dport: u16,
57+
}
58+
59+
dpdk_table_alias!(type FiveTupleTable<A> = FiveTuple);
60+
dpdk_table_alias!(type FiveTuple6Table<A> = FiveTuple6);
61+
62+
/// Rule-count sweep: powers of two from 1 to 16384.
63+
const RULE_COUNTS: [usize; 15] = [
64+
1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384,
65+
];
66+
67+
/// Batch size for the SIMD path; matches `MAX_BATCH` in the backend.
68+
const BATCH: usize = 32;
69+
70+
/// `n` IPv4 rules sharing every field except a distinct `dport`,
71+
/// mirroring the reference bench for direct comparison. Action is
72+
/// the rule index.
73+
fn build_table_v4(n: usize) -> FiveTupleTable<u32> {
74+
let specs: Vec<RuleSpec<FiveTuple, u32>> = (0..n)
75+
.map(|i| {
76+
let prio = i32::try_from(i + 1).unwrap_or(i32::MAX);
77+
let dport = u16::try_from(i).unwrap_or(u16::MAX);
78+
let action = u32::try_from(i).unwrap_or(u32::MAX);
79+
RuleSpec::new(
80+
Priority::new(prio).expect("nonzero priority"),
81+
CategoryMask::new(1).expect("nonzero mask"),
82+
FiveTupleRule {
83+
proto: ExactSpec::new(6),
84+
src: PrefixSpec::new(Ipv4Addr::new(10, 0, 0, 0), 8),
85+
dst: PrefixSpec::new(Ipv4Addr::UNSPECIFIED, 0),
86+
sport: RangeSpec::new(0, u16::MAX),
87+
dport: RangeSpec::exact(dport),
88+
}
89+
.into_backend_fields::<Dpdk>(),
90+
action,
91+
)
92+
.expect("valid RuleSpec")
93+
})
94+
.collect();
95+
let max_rules = NonZero::new(u32::try_from(n).unwrap_or(u32::MAX)).expect("n >= 1");
96+
install_table(&format!("bench_dpdk_v4_{n}"), max_rules, 1, specs).expect("install_table")
97+
}
98+
99+
/// IPv6 analog of [`build_table_v4`]; the 16-byte source prefix
100+
/// exercises the wide-field split.
101+
fn build_table_v6(n: usize) -> FiveTuple6Table<u32> {
102+
let src: Ipv6Addr = "2001:db8::".parse().expect("v6 literal");
103+
let specs: Vec<RuleSpec<FiveTuple6, u32>> = (0..n)
104+
.map(|i| {
105+
let prio = i32::try_from(i + 1).unwrap_or(i32::MAX);
106+
let dport = u16::try_from(i).unwrap_or(u16::MAX);
107+
let action = u32::try_from(i).unwrap_or(u32::MAX);
108+
RuleSpec::new(
109+
Priority::new(prio).expect("nonzero priority"),
110+
CategoryMask::new(1).expect("nonzero mask"),
111+
FiveTuple6Rule {
112+
proto: ExactSpec::new(6),
113+
src: PrefixSpec::new(src, 32),
114+
dst: PrefixSpec::new(Ipv6Addr::UNSPECIFIED, 0),
115+
sport: RangeSpec::new(0, u16::MAX),
116+
dport: RangeSpec::exact(dport),
117+
}
118+
.into_backend_fields::<Dpdk>(),
119+
action,
120+
)
121+
.expect("valid RuleSpec")
122+
})
123+
.collect();
124+
let max_rules = NonZero::new(u32::try_from(n).unwrap_or(u32::MAX)).expect("n >= 1");
125+
install_table(&format!("bench_dpdk_v6_{n}"), max_rules, 1, specs).expect("install_table")
126+
}
127+
128+
/// Single-shot (miss + hit) and SIMD-batch lookup benches for one
129+
/// width. Generic over field/stride extents and key type so v4
130+
/// and v6 share one body.
131+
fn run_lookups<K, const N: usize, const STRIDE: usize>(
132+
group: &mut BenchmarkGroup<'_, WallTime>,
133+
n: usize,
134+
table: &DpdkAclLookup<N, STRIDE, u32>,
135+
miss: &K,
136+
hit: &K,
137+
batch: &[K],
138+
) where
139+
K: MatchKey,
140+
{
141+
// Single-shot: one rte_acl_classify per packet, forfeits SIMD.
142+
group.throughput(Throughput::Elements(1));
143+
group.bench_function(BenchmarkId::new("single_miss", n), |b| {
144+
b.iter(|| black_box(table.lookup(black_box(miss))));
145+
});
146+
group.bench_function(BenchmarkId::new("single_hit", n), |b| {
147+
b.iter(|| black_box(table.lookup(black_box(hit))));
148+
});
149+
150+
// Batch: one rte_acl_classify across BATCH packets; per-element
151+
// throughput reports the amortized per-packet cost.
152+
group.throughput(Throughput::Elements(
153+
u64::try_from(batch.len()).unwrap_or(0),
154+
));
155+
group.bench_function(BenchmarkId::new("batch", n), |b| {
156+
let mut out: [Option<&u32>; BATCH] = [None; BATCH];
157+
b.iter(|| {
158+
table
159+
.lookup_batch(black_box(batch), &mut out)
160+
.expect("batch");
161+
black_box(&out);
162+
});
163+
});
164+
}
165+
166+
fn bench_v4(c: &mut Criterion) {
167+
let batch: Vec<FiveTuple> = (0..BATCH)
168+
.map(|j| FiveTuple {
169+
proto: 6,
170+
src: Ipv4Addr::new(10, 0, 0, 1),
171+
dst: Ipv4Addr::new(192, 0, 2, 1),
172+
sport: 1234,
173+
dport: u16::try_from(j).unwrap_or(0),
174+
})
175+
.collect();
176+
177+
let mut group = c.benchmark_group("dpdk_five_tuple_v4");
178+
for n in RULE_COUNTS {
179+
let table = build_table_v4(n);
180+
// `dport = u16::MAX` is carried by no rule -> miss; `0` hits
181+
// the first rule.
182+
let miss = FiveTuple {
183+
proto: 6,
184+
src: Ipv4Addr::new(10, 0, 0, 1),
185+
dst: Ipv4Addr::new(192, 0, 2, 1),
186+
sport: 1234,
187+
dport: u16::MAX,
188+
};
189+
let hit = FiveTuple { dport: 0, ..miss };
190+
run_lookups(&mut group, n, &table, &miss, &hit, &batch);
191+
}
192+
group.finish();
193+
}
194+
195+
fn bench_v6(c: &mut Criterion) {
196+
let in_prefix: Ipv6Addr = "2001:db8::1".parse().expect("v6 literal");
197+
let dst: Ipv6Addr = "::1".parse().expect("v6 literal");
198+
let batch: Vec<FiveTuple6> = (0..BATCH)
199+
.map(|j| FiveTuple6 {
200+
proto: 6,
201+
src: in_prefix,
202+
dst,
203+
sport: 1234,
204+
dport: u16::try_from(j).unwrap_or(0),
205+
})
206+
.collect();
207+
208+
let mut group = c.benchmark_group("dpdk_five_tuple_v6");
209+
for n in RULE_COUNTS {
210+
let table = build_table_v6(n);
211+
let miss = FiveTuple6 {
212+
proto: 6,
213+
src: in_prefix,
214+
dst,
215+
sport: 1234,
216+
dport: u16::MAX,
217+
};
218+
let hit = FiveTuple6 { dport: 0, ..miss };
219+
run_lookups(&mut group, n, &table, &miss, &hit, &batch);
220+
}
221+
group.finish();
222+
}
223+
224+
pub fn benches(c: &mut Criterion) {
225+
// OnceLock-backed: one EAL per process.
226+
let _eal = dpdk::test_support::start_eal();
227+
bench_v4(c);
228+
bench_v6(c);
229+
}
230+
}
231+
232+
#[cfg(feature = "dpdk")]
233+
criterion::criterion_group!(benchmarks, bench::benches);
234+
#[cfg(feature = "dpdk")]
235+
criterion::criterion_main!(benchmarks);
236+
237+
// No EAL backend without the `dpdk` feature; a no-op `main` keeps the
238+
// `harness = false` target linkable.
239+
#[cfg(not(feature = "dpdk"))]
240+
fn main() {}

0 commit comments

Comments
 (0)