Skip to content

Commit db13ea7

Browse files
committed
test(acl): differential property + Headers/metadata projection demos
Pure test broadening; no src changes. Adds the single-rule v4/v6 differential against the reference oracle and three Headers / metadata projection demos that exercise classify / classify_opt against real net::HeadersView packets. tests/property_predicate.rs is the differential. For a random 5-tuple rule + random hit/miss byte seeds drawn via match-action's FieldHit / FieldMiss generators, both the reference oracle and the DPDK backend must accept every hits() draw and reject every misses() draw. Parameterised over the address width via a sealed IpAddress trait so a single body covers v4 (Ipv4Addr) and v6 (Ipv6Addr) -- the DPDK wide-field split (one 16-byte address -> four 4-byte sub-fields) is exercised end-to-end by the v6 invocation. Single rule only; multi-rule differential is deferred (positional precedence vs numeric Priority). tests/eal_classify_via_projection.rs is the end-to-end projection demo: a real packet -> HeadersView -> Projection<FiveTuple> -> DPDK Lookup<FiveTuple, _> -> action. Shows Lookup::classify runs the projection and the lookup as a single call -- the call site reads table.classify(\&headers) and doesn't see the intermediate key construction. tests/metadata_projection.rs is the partial-projection demo. Header fields live in Headers; VRF / VNI live in PacketMeta. A projection source bundles &HeadersView with &PacketMeta and projects to Option<K>: the header part is total (shape proves presence), the metadata part narrows from its Option with ?. Missing metadata projects to None and Lookup::classify_opt turns that into a table miss with no explicit branch in user code. tests/net_field_types.rs uses net wire newtypes (TcpPort, UdpPort, Vni, UnicastIpv4Addr) directly as MatchKey fields with no acl-side AclWord impl, leaning on net's FixedSize impls (PR 2a) and the DPDK backend's blanket AclWord-over-FixedSize impl. acl/Cargo.toml grows the net[test_buffer, builder] dev-dep these projection demos need. just fmt; cargo check --workspace --all-targets and cargo clippy -p dataplane-acl --features dpdk -- -D warnings pass. Signed-off-by: Daniel Noland <daniel@githedgehog.com>
1 parent eb19c50 commit db13ea7

5 files changed

Lines changed: 740 additions & 1 deletion

File tree

acl/Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,13 @@ net = { workspace = true, features = [] }
2525
thiserror = { workspace = true }
2626

2727
[dev-dependencies]
28-
# Property-test draws for the shape-fuzz oracle.
28+
# Differential oracle harness: random rulesets + packets compared
29+
# between the reference and DPDK backends.
2930
bolero = { workspace = true, features = ["std"] }
3031
# Override the regular `dpdk` dep to enable its `test` feature
3132
# (exposes `dpdk::test_support::start_eal` and `dpdk::with_eal`).
3233
dpdk = { workspace = true, features = ["test"] }
3334
# Override match-action to also enable `bolero` for property-test
3435
# generators (`FieldHit` / `FieldMiss`).
3536
match-action = { workspace = true, features = ["derive", "bolero"] }
37+
net = { workspace = true, features = ["test_buffer", "builder"] }
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// Copyright Open Network Fabric Authors
3+
4+
#![cfg(feature = "dpdk")]
5+
#![allow(clippy::expect_used, clippy::unwrap_used)]
6+
7+
use core::net::Ipv4Addr;
8+
use core::num::NonZero;
9+
10+
use dataplane_acl::dpdk::install::install_table;
11+
use dataplane_acl::dpdk::rule::{Dpdk, RuleSpec};
12+
use dataplane_acl::dpdk_table_alias;
13+
use dpdk::acl::{CategoryMask, Priority};
14+
use lookup::{Lookup, Projection};
15+
use match_action::{ExactSpec, FixedSize, MatchKey, PrefixSpec, RangeSpec};
16+
use net::eth::Eth;
17+
use net::headers::builder::HeaderStack;
18+
use net::headers::{HeadersView, Look};
19+
use net::ipv4::{Ipv4, UnicastIpv4Addr};
20+
use net::tcp::{Tcp, TcpPort};
21+
22+
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
23+
struct IpProto(u8);
24+
25+
impl FixedSize for IpProto {
26+
const SIZE: usize = 1;
27+
fn write_be(&self, out: &mut [u8]) {
28+
out[0] = self.0;
29+
}
30+
}
31+
32+
#[derive(MatchKey)]
33+
#[allow(dead_code)]
34+
struct FiveTuple {
35+
#[exact]
36+
proto: IpProto,
37+
#[prefix]
38+
src_ip: Ipv4Addr,
39+
#[prefix]
40+
dst_ip: Ipv4Addr,
41+
#[range]
42+
src_port: u16,
43+
#[range]
44+
dst_port: u16,
45+
}
46+
47+
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
48+
#[allow(dead_code)]
49+
enum Verdict {
50+
Allow,
51+
Drop,
52+
}
53+
54+
dpdk_table_alias!(type FiveTupleTable<A> = FiveTuple);
55+
struct V4TcpSource<'a>(&'a HeadersView<(&'a Eth, &'a Ipv4, &'a Tcp)>);
56+
57+
impl Projection<FiveTuple> for &V4TcpSource<'_> {
58+
fn project(self) -> FiveTuple {
59+
let (_eth, ipv4, tcp) = self.0.look();
60+
FiveTuple {
61+
proto: IpProto(6),
62+
src_ip: ipv4.source().inner(),
63+
dst_ip: ipv4.destination(),
64+
src_port: u16::from(tcp.source()),
65+
dst_port: u16::from(tcp.destination()),
66+
}
67+
}
68+
}
69+
70+
fn build_packet(src: Ipv4Addr, dst: Ipv4Addr, sport: u16, dport: u16) -> net::headers::Headers {
71+
HeaderStack::new()
72+
.eth(|_| {})
73+
.ipv4(|ip| {
74+
ip.set_source(UnicastIpv4Addr::new(src).unwrap());
75+
ip.set_destination(dst);
76+
})
77+
.tcp(|tcp| {
78+
tcp.set_source(TcpPort::try_from(sport).unwrap());
79+
tcp.set_destination(TcpPort::try_from(dport).unwrap());
80+
})
81+
.build_headers()
82+
.unwrap()
83+
}
84+
85+
#[dpdk::with_eal]
86+
#[test]
87+
fn classify_real_packet_via_projection() {
88+
let rule = FiveTupleRule {
89+
proto: ExactSpec::new(IpProto(6)),
90+
src_ip: PrefixSpec::new(Ipv4Addr::new(10, 0, 0, 0), 8),
91+
dst_ip: PrefixSpec::new(Ipv4Addr::UNSPECIFIED, 0),
92+
src_port: RangeSpec::new(0, u16::MAX),
93+
dst_port: RangeSpec::exact(22),
94+
};
95+
let rule_spec = RuleSpec::<FiveTuple, Verdict>::new(
96+
Priority::new(100).expect("priority"),
97+
CategoryMask::new(1).expect("category mask"),
98+
rule.into_backend_fields::<Dpdk>(),
99+
Verdict::Drop,
100+
)
101+
.expect("RuleSpec");
102+
103+
let table: FiveTupleTable<Verdict> = install_table(
104+
"eal_classify_via_projection",
105+
NonZero::new(16).expect("max rules"),
106+
vec![rule_spec],
107+
)
108+
.expect("install_table");
109+
let hit = build_packet(
110+
Ipv4Addr::new(10, 0, 1, 5),
111+
Ipv4Addr::new(192, 168, 1, 1),
112+
54321,
113+
22,
114+
);
115+
let view = hit.as_view::<(&Eth, &Ipv4, &Tcp)>().expect("v4 tcp shape");
116+
let src = V4TcpSource(view);
117+
assert_eq!(table.classify(&src), Some(&Verdict::Drop));
118+
let miss = build_packet(
119+
Ipv4Addr::new(192, 168, 1, 5),
120+
Ipv4Addr::new(192, 168, 1, 1),
121+
54321,
122+
22,
123+
);
124+
let miss_view = miss.as_view::<(&Eth, &Ipv4, &Tcp)>().unwrap();
125+
assert_eq!(table.classify(&V4TcpSource(miss_view)), None);
126+
}
127+
128+
#[test]
129+
fn non_tcp_packet_cannot_be_projected_to_five_tuple() {
130+
let udp = HeaderStack::new()
131+
.eth(|_| {})
132+
.ipv4(|_| {})
133+
.udp(|_| {})
134+
.build_headers()
135+
.unwrap();
136+
assert!(udp.as_view::<(&Eth, &Ipv4, &Tcp)>().is_none());
137+
}

acl/tests/metadata_projection.rs

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// Copyright Open Network Fabric Authors
3+
4+
#![cfg(feature = "dpdk")]
5+
#![allow(clippy::expect_used, clippy::unwrap_used)]
6+
7+
use core::net::Ipv4Addr;
8+
use core::num::NonZero;
9+
10+
use dataplane_acl::dpdk::install::install_table;
11+
use dataplane_acl::dpdk::rule::{Dpdk, RuleSpec};
12+
use dataplane_acl::dpdk_table_alias;
13+
use dataplane_acl::reference::{Erased, RefRule, ReferenceTable};
14+
use dpdk::acl::{CategoryMask, Priority};
15+
use lookup::{Lookup, Projection};
16+
use match_action::{ExactSpec, MatchKey, PrefixSpec, RangeSpec};
17+
use net::eth::Eth;
18+
use net::headers::builder::HeaderStack;
19+
use net::headers::{HeadersView, Look};
20+
use net::ipv4::{Ipv4, UnicastIpv4Addr};
21+
use net::packet::{PacketMeta, VpcDiscriminant, VrfId};
22+
use net::tcp::{Tcp, TcpPort};
23+
use net::vxlan::Vni;
24+
#[derive(MatchKey, Debug, Clone, Copy)]
25+
struct OverlayFlowKey {
26+
#[exact]
27+
proto: u8,
28+
#[prefix]
29+
src: Ipv4Addr,
30+
#[range]
31+
dport: u16,
32+
#[exact]
33+
vrf: VrfId,
34+
#[exact]
35+
vni: Vni,
36+
}
37+
38+
dpdk_table_alias!(type OverlayFlowTable<A> = OverlayFlowKey);
39+
40+
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
41+
enum Verdict {
42+
Drop,
43+
}
44+
struct PacketSource<'a> {
45+
view: &'a HeadersView<(&'a Eth, &'a Ipv4, &'a Tcp)>,
46+
meta: &'a PacketMeta,
47+
}
48+
49+
impl Projection<Option<OverlayFlowKey>> for &PacketSource<'_> {
50+
fn project(self) -> Option<OverlayFlowKey> {
51+
let (_eth, ipv4, tcp) = self.view.look();
52+
Some(OverlayFlowKey {
53+
proto: 6,
54+
src: ipv4.source().inner(),
55+
dport: tcp.destination().as_u16(),
56+
vrf: self.meta.vrf?,
57+
vni: Vni::try_from(self.meta.src_vpcd?).ok()?,
58+
})
59+
}
60+
}
61+
62+
fn rule() -> OverlayFlowKeyRule {
63+
OverlayFlowKeyRule {
64+
proto: ExactSpec::new(6),
65+
src: PrefixSpec::new(Ipv4Addr::new(10, 0, 0, 0), 8),
66+
dport: RangeSpec::exact(443),
67+
vrf: ExactSpec::new(42),
68+
vni: ExactSpec::new(Vni::new_checked(1000).unwrap()),
69+
}
70+
}
71+
72+
fn packet(src: Ipv4Addr, dport: u16) -> net::headers::Headers {
73+
HeaderStack::new()
74+
.eth(|_| {})
75+
.ipv4(|ip| {
76+
ip.set_source(UnicastIpv4Addr::new(src).unwrap());
77+
ip.set_destination(Ipv4Addr::new(192, 0, 2, 1));
78+
})
79+
.tcp(|tcp| {
80+
tcp.set_source(TcpPort::try_from(40000u16).unwrap());
81+
tcp.set_destination(TcpPort::try_from(dport).unwrap());
82+
})
83+
.build_headers()
84+
.unwrap()
85+
}
86+
#[allow(clippy::field_reassign_with_default)]
87+
fn meta_with(vrf: Option<VrfId>, vni: Option<Vni>) -> PacketMeta {
88+
let mut meta = PacketMeta::default();
89+
meta.vrf = vrf;
90+
meta.src_vpcd = vni.map(VpcDiscriminant::from);
91+
meta
92+
}
93+
94+
#[test]
95+
#[dpdk::with_eal]
96+
fn classify_on_headers_plus_metadata() {
97+
let dpdk: OverlayFlowTable<Verdict> = install_table(
98+
"metadata_projection",
99+
NonZero::new(8).unwrap(),
100+
vec![
101+
RuleSpec::<OverlayFlowKey, Verdict>::new(
102+
Priority::new(1).unwrap(),
103+
CategoryMask::new(1).unwrap(),
104+
rule().into_backend_fields::<Dpdk>(),
105+
Verdict::Drop,
106+
)
107+
.unwrap(),
108+
],
109+
)
110+
.expect("install_table");
111+
let reference = ReferenceTable::<OverlayFlowKey, Verdict>::new(vec![RefRule::new(
112+
rule().into_backend_fields::<Erased>(),
113+
Verdict::Drop,
114+
)]);
115+
116+
let headers = packet(Ipv4Addr::new(10, 9, 9, 9), 443);
117+
let view = headers
118+
.as_view::<(&Eth, &Ipv4, &Tcp)>()
119+
.expect("v4 tcp shape");
120+
let vni = Vni::new_checked(1000).unwrap();
121+
let full = meta_with(Some(42), Some(vni));
122+
let src = PacketSource { view, meta: &full };
123+
assert_eq!(dpdk.classify_opt(&src), Some(&Verdict::Drop));
124+
assert_eq!(
125+
reference.classify_opt(&PacketSource { view, meta: &full }),
126+
Some(&Verdict::Drop)
127+
);
128+
let no_vrf = meta_with(None, Some(vni));
129+
assert_eq!(
130+
dpdk.classify_opt(&PacketSource {
131+
view,
132+
meta: &no_vrf
133+
}),
134+
None
135+
);
136+
assert_eq!(
137+
reference.classify_opt(&PacketSource {
138+
view,
139+
meta: &no_vrf
140+
}),
141+
None
142+
);
143+
let wrong_vni = meta_with(Some(42), Some(Vni::new_checked(2000).unwrap()));
144+
assert_eq!(
145+
dpdk.classify_opt(&PacketSource {
146+
view,
147+
meta: &wrong_vni
148+
}),
149+
None
150+
);
151+
assert_eq!(
152+
reference.classify_opt(&PacketSource {
153+
view,
154+
meta: &wrong_vni
155+
}),
156+
None
157+
);
158+
}
159+
#[test]
160+
#[dpdk::with_eal]
161+
fn classify_opt_accepts_an_inline_key() {
162+
let dpdk: OverlayFlowTable<Verdict> = install_table(
163+
"metadata_projection_by",
164+
NonZero::new(8).unwrap(),
165+
vec![
166+
RuleSpec::<OverlayFlowKey, Verdict>::new(
167+
Priority::new(1).unwrap(),
168+
CategoryMask::new(1).unwrap(),
169+
rule().into_backend_fields::<Dpdk>(),
170+
Verdict::Drop,
171+
)
172+
.unwrap(),
173+
],
174+
)
175+
.expect("install_table");
176+
177+
let headers = packet(Ipv4Addr::new(10, 9, 9, 9), 443);
178+
let view = headers
179+
.as_view::<(&Eth, &Ipv4, &Tcp)>()
180+
.expect("v4 tcp shape");
181+
let key_for = |meta: &PacketMeta| -> Option<OverlayFlowKey> {
182+
let (_eth, ipv4, tcp) = view.look();
183+
Some(OverlayFlowKey {
184+
proto: 6,
185+
src: ipv4.source().inner(),
186+
dport: tcp.destination().as_u16(),
187+
vrf: meta.vrf?,
188+
vni: Vni::try_from(meta.src_vpcd?).ok()?,
189+
})
190+
};
191+
192+
let full = meta_with(Some(42), Some(Vni::new_checked(1000).unwrap()));
193+
assert_eq!(dpdk.classify_opt(key_for(&full)), Some(&Verdict::Drop));
194+
let no_vrf = meta_with(None, Some(Vni::new_checked(1000).unwrap()));
195+
assert_eq!(dpdk.classify_opt(key_for(&no_vrf)), None);
196+
}

0 commit comments

Comments
 (0)