Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions acl-filter/src/fuzz.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,16 @@ fn resolved_action(rule: Option<OracleVerdict>, default: Option<AclAction>) -> A
rule.map_or_else(|| default.unwrap_or(AclAction::Allow), |v| v.action)
}

pub(crate) fn oracle_resolved_action(
overlay: &ValidatedOverlay,
packet: &PacketSummary,
) -> AclAction {
resolved_action(
oracle_lookup(overlay, packet),
oracle_default_action(overlay, packet.src_vni, packet.dst_vni),
)
}

// -------------------------------------------------------------------------------------------------
// Properties.

Expand Down
2 changes: 2 additions & 0 deletions acl-filter/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ mod fuzz;
#[cfg(test)]
mod fuzz_gen;
#[cfg(test)]
mod nf_fuzz;
#[cfg(test)]
mod tests;

pub use access::{
Expand Down
319 changes: 319 additions & 0 deletions acl-filter/src/nf_fuzz.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,319 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Open Network Fabric Authors

#![cfg(test)]

use crate::fuzz::oracle_resolved_action;
use crate::fuzz_gen::{OverlaySpec, ProbeSpec};
use crate::{AclFilter, AclFilterContext, AclFilterContextWriter, PacketSummary};
use concurrency::sync::atomic::{AtomicUsize, Ordering};
use config::external::overlay::acl::AclAction;
use net::buffer::TestBuffer;
use net::ip::{NextHeader, UnicastIpAddr};
use net::packet::test_utils::{
build_test_ipv4_packet_with_transport, build_test_ipv6_packet_with_transport,
};
use net::packet::{DoneReason, Packet, VpcDiscriminant};
use net::tcp::port::TcpPort;
use net::udp::UdpPort;
use pipeline::NetworkFunction;
use std::net::IpAddr;

const PROBES: usize = 8;

fn packet_for(summary: &PacketSummary) -> Option<Packet<TestBuffer>> {
let (sport, dport) = summary.ports?;
let tcp = match summary.proto {
NextHeader::TCP => true,
NextHeader::UDP => false,
_ => return None,
};

let mut packet = match (summary.src_ip, summary.dst_ip) {
(IpAddr::V4(_), IpAddr::V4(_)) => {
build_test_ipv4_packet_with_transport(64, Some(summary.proto)).ok()?
}
(IpAddr::V6(_), IpAddr::V6(_)) => {
build_test_ipv6_packet_with_transport(64, Some(summary.proto)).ok()?
}
_ => return None,
};

packet
.set_ip_source(UnicastIpAddr::try_from(summary.src_ip).ok()?)
.ok()?;
packet.set_ip_destination(summary.dst_ip).ok()?;
if tcp {
packet
.set_tcp_source_port(TcpPort::new_checked(sport.max(1)).ok()?)
.ok()?;
packet
.set_tcp_destination_port(TcpPort::new_checked(dport.max(1)).ok()?)
.ok()?;
} else {
packet
.set_udp_source_port(UdpPort::new_checked(sport.max(1)).ok()?)
.ok()?;
packet
.set_udp_destination_port(UdpPort::new_checked(dport.max(1)).ok()?)
.ok()?;
}

let meta = packet.meta_mut();
meta.src_vpcd = Some(VpcDiscriminant::from_vni(summary.src_vni));
meta.dst_vpcd = Some(VpcDiscriminant::from_vni(summary.dst_vni));
meta.set_overlay(true);
meta.set_keep(true);
Some(packet)
}

fn expected_summary(summary: &PacketSummary) -> PacketSummary {
let mut expected = summary.clone();
expected.ports = summary.ports.map(|(s, d)| (s.max(1), d.max(1)));
expected
}

fn filter(built: &crate::fuzz_gen::BuiltOverlay) -> AclFilter {
let writer = AclFilterContextWriter::new();
writer.store(AclFilterContext::for_test(&built.overlay));
AclFilter::new("nf-fuzz-acl-filter", writer.get_reader())
}

/// Whether this run saw enough to judge the ratios below, or only to print them.
///
/// Three kinds of run are too small, and they are not interchangeable:
///
/// - **instrumentation.** A coverage run of these properties got through a
/// single configuration. The ratios then measure the build, not the property.
/// - **emulation.** miri and qemu-user see roughly two orders of magnitude
/// fewer cases; the same stand-down appears in `clock` and in the config
/// algebra's completeness table.
/// - **a corpus replay.** bolero runs exactly the inputs it is handed, so
/// `BOLERO_RANDOM_ITERATIONS=0` over one saved entry is *one case*. An
/// aggregate rate over one case reports how that case happened to fall --
/// and that is precisely the run a developer makes to confirm a fix, so
/// failing it there is worse than not checking at all.
///
/// The floor separates the last of those from a real campaign. It has to sit
/// below what the smallest honest run reaches, which is why it is measured
/// rather than picked.
///
/// Measured natively 2026-09-06: 27480 to 31616 probes drawn per property, at
/// `PROBES` per case, of which 24% were denied. One replayed corpus entry draws
/// `PROBES` and can legitimately deny none of them -- which is the failure this
/// floor exists to prevent.
fn judged(drawn: usize) -> bool {
/// Sixteen cases' worth: far under any real run, far over a single replay.
const ENOUGH_PROBES: usize = PROBES * 16;
!cfg!(instrumented) && !cfg!(emulated) && drawn >= ENOUGH_PROBES
}

#[derive(Default)]
struct Tally {
drawn: AtomicUsize,
reached: AtomicUsize,
denied: AtomicUsize,
}

impl Tally {
fn report_arrivals_only(&self, what: &str) {
let (drawn, reached) = (
self.drawn.load(Ordering::Relaxed),
self.reached.load(Ordering::Relaxed),
);
if drawn == 0 {
return;
}
println!("{what}: {reached}/{drawn} probes became packets");
if !judged(drawn) {
println!(" {what}: not judged -- {drawn} probes is too small a sample");
return;
}
assert!(
reached > 0 && reached * 4 >= drawn,
"only {reached} of {drawn} probes became packets, so the {what} assertion is barely \
running"
);
}

fn report(&self, what: &str) {
let (drawn, reached, denied) = (
self.drawn.load(Ordering::Relaxed),
self.reached.load(Ordering::Relaxed),
self.denied.load(Ordering::Relaxed),
);
if drawn == 0 {
return;
}
println!("{what}: {reached}/{drawn} probes became packets, {denied} of them denied");
if !judged(drawn) {
println!(" {what}: not judged -- {drawn} probes is too small a sample");
return;
}
assert!(
reached > 0 && reached * 4 >= drawn,
"only {reached} of {drawn} probes became packets, so the {what} assertion is barely \
running"
);
assert!(
denied * 20 >= reached,
"only {denied} of {reached} probes were denied, so the drop path is barely exercised \
and this property is mostly checking that nothing happens"
);
}
}

#[test]
fn the_stage_agrees_with_the_configuration() {
let tally = Tally::default();

bolero::check!()
.with_type::<(OverlaySpec, [ProbeSpec; PROBES])>()
.for_each(|(overlay_spec, probe_specs)| {
let built = overlay_spec.build();
let mut acl = filter(&built);

for probe_spec in probe_specs {
tally.drawn.fetch_add(1, Ordering::Relaxed);
let summary = probe_spec.resolve(&built);
let Some(packet) = packet_for(&summary) else {
continue;
};

let judged = expected_summary(&summary);
let want = oracle_resolved_action(&built.overlay, &judged);
let out: Vec<_> = acl.process(std::iter::once(packet)).collect();
let got = out[0].get_done();

match want {
AclAction::Deny => {
assert_eq!(
got,
Some(DoneReason::AclDropped),
"the configuration denies {summary:?} and the stage let it through \
with {got:?}\nspec: {overlay_spec:?}"
);
tally.denied.fetch_add(1, Ordering::Relaxed);
}
AclAction::Allow => {
assert_eq!(
got, None,
"the configuration allows {summary:?} and the stage dropped it for \
{got:?}\nspec: {overlay_spec:?}"
);
}
}
tally.reached.fetch_add(1, Ordering::Relaxed);
}
});

tally.report("stage verdict");
}

#[test]
fn the_summary_survives_the_round_trip_through_a_packet() {
let tally = Tally::default();

bolero::check!()
.with_type::<(OverlaySpec, [ProbeSpec; PROBES])>()
.for_each(|(overlay_spec, probe_specs)| {
let built = overlay_spec.build();

for probe_spec in probe_specs {
tally.drawn.fetch_add(1, Ordering::Relaxed);
let summary = probe_spec.resolve(&built);
let Some(packet) = packet_for(&summary) else {
continue;
};

let read = PacketSummary::try_from(&packet)
.unwrap_or_else(|e| panic!("a built packet did not yield a summary: {e:?}"));
let expected = expected_summary(&summary);

assert_eq!(
(read.src_vni, read.dst_vni),
(expected.src_vni, expected.dst_vni),
"discriminants came back swapped or wrong\nspec: {overlay_spec:?}"
);
assert_eq!(
(read.src_ip, read.dst_ip),
(expected.src_ip, expected.dst_ip),
"addresses came back swapped or wrong\nspec: {overlay_spec:?}"
);
assert_eq!(
read.proto, expected.proto,
"protocol came back wrong\nspec: {overlay_spec:?}"
);
assert_eq!(
read.ports, expected.ports,
"ports came back swapped or wrong\nspec: {overlay_spec:?}"
);
tally.reached.fetch_add(1, Ordering::Relaxed);
}
});

tally.report_arrivals_only("summary round trip");
}

#[test]
fn a_packet_with_no_discriminants_is_dropped() {
let tally = Tally::default();

bolero::check!()
.with_type::<(OverlaySpec, [ProbeSpec; PROBES])>()
.for_each(|(overlay_spec, probe_specs)| {
let built = overlay_spec.build();
let mut acl = filter(&built);

for probe_spec in probe_specs {
tally.drawn.fetch_add(1, Ordering::Relaxed);
let summary = probe_spec.resolve(&built);
let Some(mut packet) = packet_for(&summary) else {
continue;
};
packet.meta_mut().dst_vpcd = None;

let out: Vec<_> = acl.process(std::iter::once(packet)).collect();
assert_eq!(
out[0].get_done(),
Some(DoneReason::Unroutable),
"a packet with no destination vpc was not refused\nspec: {overlay_spec:?}"
);
tally.reached.fetch_add(1, Ordering::Relaxed);
}
});

tally.report_arrivals_only("missing discriminant");
}

#[test]
fn underlay_traffic_is_not_judged() {
let tally = Tally::default();

bolero::check!()
.with_type::<(OverlaySpec, [ProbeSpec; PROBES])>()
.for_each(|(overlay_spec, probe_specs)| {
let built = overlay_spec.build();
let mut acl = filter(&built);

for probe_spec in probe_specs {
tally.drawn.fetch_add(1, Ordering::Relaxed);
let summary = probe_spec.resolve(&built);
let Some(mut packet) = packet_for(&summary) else {
continue;
};
packet.meta_mut().set_overlay(false);

let out: Vec<_> = acl.process(std::iter::once(packet)).collect();
assert_eq!(
out[0].get_done(),
None,
"a packet that is not overlay traffic was judged by an overlay acl\nspec: \
{overlay_spec:?}"
);
tally.reached.fetch_add(1, Ordering::Relaxed);
}
});

tally.report_arrivals_only("underlay gate");
}
Loading
Loading