Skip to content

flow filter printer - #1679

Merged
daniel-noland merged 6 commits into
mainfrom
pr/daniel-noland/quick-fix-fuzz-and-print
Aug 4, 2026
Merged

flow filter printer#1679
daniel-noland merged 6 commits into
mainfrom
pr/daniel-noland/quick-fix-fuzz-and-print

Conversation

@daniel-noland

@daniel-noland daniel-noland commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Make the production CLI show the rules installed in the flow-filter and ACL-filter tables, rather than only reporting their counts.

DPDK’s rte_acl contexts are opaque after construction, so each table now retains the typed rules from which its classifier was built. Rendering from those typed rules lets domain values format themselves—for example, VNIs appear as VNIs and protocols as TCP—without decoding erased predicate bytes by width or position.

Rules are displayed in match order as a fixed-width grid containing:

  • the rule’s rank;
  • its typed match fields;
  • a separator between matches and actions;
  • the resulting action fields.

The displayed rank is the rule’s match position, not its internal priority encoding.

The classifier layout and matching semantics are intended to remain unchanged. The principal runtime tradeoff is that tables retain a typed copy of their rules and actions for operator inspection.

The final commit contains the user-visible behavior. The preceding commits establish the typed representation and formatting infrastructure it depends on.

Copilot AI review requested due to automatic review settings July 31, 2026 18:25
@daniel-noland daniel-noland added the dont-merge Do not merge this Pull Request label Jul 31, 2026
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The change adds typed protocol and VNI values, retains typed rules beside backend predicates, introduces shared rule-display utilities, and renders ACL and flow tables consistently across reference and DPDK backends.

Changes

Typed match-action contracts

Layer / File(s) Summary
Typed rule contracts and formatting
fixed-size/src/lib.rs, match-action/..., match-action-derive/..., net/...
MatchKey now exposes a typed Rule. Rule fields, masks, prefixes, ranges, and generated CLI column names support display formatting.
Protocol conversion
config/src/external/overlay/acl.rs
Protocol matches now produce MaskSpec<NextHeader> values with exact or wildcard constructors.

ACL and flow table integration

Layer / File(s) Summary
ACL typed lowering and storage
acl-filter/src/context.rs
ACL keys use NextHeader and Vni. AnyTable retains typed rule rows while building backend classifiers from predicates.
Flow table construction and lookup
flow-filter/src/context/tables.rs
Remote and local tables retain typed rules, sort them once, and use typed protocol and VNI values for single and batch lookup.
Backend-independent rendering
acl-filter/src/display.rs, flow-filter/src/context/display.rs
ACL and flow tables render ranked grids with typed match fields and action columns. Empty tables retain explicit empty output.

Validation

Layer / File(s) Summary
Cross-backend display tests
acl-filter/src/tests.rs, flow-filter/src/context/tests.rs
Tests compare reference and DPDK output and validate headings, rule ordering, typed fields, table sections, and action values.
Compatibility updates
acl/src/dpdk/rule.rs, acl/tests/...
Test helpers implement the new associated rule type and Display requirements.

Possibly related PRs

Suggested reviewers: mvachhar, fredi-raspall

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title identifies the flow-filter printer change, which is a real part of the pull request but does not mention the ACL-filter output.
Description check ✅ Passed The description clearly explains the CLI rule-printing changes for both flow-filter and ACL-filter tables.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR extends the MatchKey ecosystem to retain and render typed rules (rather than decoding erased predicate bytes), enabling consistent human-readable CLI output across both the reference and DPDK (opaque rte_acl) backends.

Changes:

  • Add MatchKey::Rule and enhance the derive macro to generate a companion *Rule type that implements Display, enabling typed rule retention and rendering.
  • Update flow-filter and acl-filter table builders to retain typed rules and render them consistently across backends; add cross-backend display equivalence tests.
  • Add/extend Display and FixedSize impls for protocol/port types used in match keys.

Reviewed changes

Copilot reviewed 18 out of 18 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
net/src/udp/port.rs Implement Display for UdpPort so it can participate in typed rule rendering.
net/src/tcp/port.rs Implement Display for TcpPort so it can participate in typed rule rendering.
net/src/fixed_size.rs Implement FixedSize for NextHeader and add tests for 1-byte encoding.
match-action/tests/derive_roundtrip.rs Add Display for test key field types to satisfy new derive bounds.
match-action/src/predicate.rs Expose be_bytes to the crate for typed-spec formatting helpers.
match-action/src/lib.rs Add MatchKey::Rule associated type and export the new display module.
match-action/src/display.rs New: Display impls for ExactSpec/PrefixSpec/RangeSpec/MaskSpec to format typed rules.
match-action-derive/src/lib.rs Emit a companion *Rule struct and Display impl; wire it into MatchKey::Rule.
flow-filter/src/context/tests.rs Add test asserting identical display across reference and DPDK backends.
flow-filter/src/context/tables.rs Switch key fields to typed NextHeader/Vni; retain typed rules alongside classifiers.
flow-filter/src/context/display.rs Render full rule dumps from retained typed rules in all builds/backends.
config/src/external/overlay/acl.rs Lower ACL proto matches into MaskSpec<NextHeader> (typed protocol field).
acl/tests/eal_install_classify.rs Add Display for test protocol type to satisfy new derive/display requirements.
acl/tests/eal_classify_via_projection.rs Add Display for test protocol type to satisfy new derive/display requirements.
acl/src/dpdk/rule.rs Update manual MatchKey impl in test to include type Rule.
acl-filter/src/tests.rs Add test asserting identical display across reference and DPDK backends.
acl-filter/src/display.rs Render full rule dumps from retained typed rules (no positional decoding).
acl-filter/src/context.rs Switch key fields to typed NextHeader/Vni; retain typed rules alongside classifiers.

Comment thread acl-filter/src/context.rs
let kind = match self {
AnyTable::Empty => "empty",
AnyTable::Dpdk(_) => "dpdk",
let kind = match self.classifier {
@daniel-noland daniel-noland changed the title quick fix fuzz and print quick fix: flow filter printer Jul 31, 2026

@mvachhar mvachhar left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please fix up some of these comments, plus could you post in the comments some sample output from the Display formatter so we can see what these look like. Also printing in priority order would be very helpful.

Copilot AI review requested due to automatic review settings July 31, 2026 21:41

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 23 out of 23 changed files in this pull request and generated no new comments.

Suppressed comments (3)

match-action/src/lib.rs:59

confidence: 8
tags: [other]

`MatchKey` is a public trait in a published crate (`publish.workspace = true`), and adding the required associated type `Rule` is a semver-breaking change for any downstream manual `impl MatchKey`. If this crate is consumed outside the workspace, this should be paired with an appropriate major version bump / release note (or an explicit statement that the API is not yet stable).

pub trait MatchKey: Sized {
/// The rule (predicate) form of this key: one spec per match field, still carrying each
/// field's type. A table can therefore retain the rules it was built from in a form that
/// renders itself, without having to decode erased bytes back into domain types.
type Rule;

const N: usize;
const KEY_SIZE: usize;
fn field_specs() -> &'static [FieldSpec];
fn as_key_into(&self, out: &mut [u8]);

}

**flow-filter/src/context/display.rs:32**
* ```yaml
confidence: 7
tags: [style]

This private render module isn’t referenced anywhere, and because it only contains impls (no referenced items), it’s likely to trip the dead_code/“module is never used” lint in warning-as-error builds. Consider inlining the contents at the file scope, or explicitly allowing the lint on the module.

mod render {

flow-filter/src/context/display.rs:39

confidence: 9
tags: [style]

`Write` is imported here but never used (the `write!`/`writeln!` macros don’t require the trait in scope), which will trigger an `unused_imports` warning.
use std::fmt::{self, Display, Formatter, Write};
</details>

Copilot AI review requested due to automatic review settings July 31, 2026 21:53

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 23 out of 23 changed files in this pull request and generated no new comments.

Suppressed comments (2)

acl-filter/src/tests.rs:1184

confidence: 9
tags: [style]

`printer_sample` is documented as a manual "printer" (not a real assertion test) but it is still annotated with `#[test]`, so it will run in normal CI by default. Mark it `#[ignore]` (or feature-gate it) so it only runs when explicitly requested.

#[test]
fn printer_sample() {

**flow-filter/src/context/tests.rs:754**
* ```yaml
confidence: 9
tags: [style]

printer_sample is labeled as THROWAWAY: not a test, but it is still annotated with #[test], so it will run in the default test suite (and spam output / add runtime) unless explicitly filtered. Mark it ignored (or gate behind a feature) so it only runs when intentionally requested.

#[test]
fn printer_sample() {

Copilot AI review requested due to automatic review settings July 31, 2026 22:29

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 33 out of 34 changed files in this pull request and generated 1 comment.

Suppressed comments (2)

flow-filter/src/context/tests.rs:754

confidence: 9
tags: [style]

`printer_sample` is described as “not a test, a printer” but it’s still a normal `#[test]`, so it will run in CI/`cargo nextest run` by default and produce noise/time cost. Mark it `#[ignore]` (or gate it behind a feature) so it only runs when explicitly requested.

#[test]
fn printer_sample() {

**acl-filter/src/tests.rs:1184**
* ```yaml
confidence: 9
tags: [style]

printer_sample is explicitly “not a test, a printer” but it will still execute as part of the default unit test suite. Mark it #[ignore] so it doesn’t run in CI unless explicitly selected.

#[test]
fn printer_sample() {

Comment thread match-action/src/lib.rs
Comment on lines 49 to +54
pub trait MatchKey: Sized {
/// The rule (predicate) form of this key: one spec per match field, still carrying each
/// field's type. A table can therefore retain the rules it was built from in a form that
/// renders itself, without having to decode erased bytes back into domain types.
type Rule;

Copilot AI review requested due to automatic review settings August 1, 2026 02:51

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 39 out of 40 changed files in this pull request and generated no new comments.

Suppressed comments (2)

flow-filter/src/context/tests.rs:783

confidence: 8
tags: [style]

`printer_sample` is described as “THROWAWAY: not a test, a printer”, but it is currently a normal `#[test]` and will run in the default CI suite. Mark it `#[ignore]` (and run it explicitly when needed) to avoid adding runtime to routine test runs.

#[test]
fn printer_sample() {

**acl-filter/src/tests.rs:1184**
* ```yaml
confidence: 8
tags: [style]

printer_sample is documented as a “THROWAWAY” printer, but it is currently a normal #[test] and will run in the default CI suite. Mark it #[ignore] (and run it explicitly when needed) to keep routine test runs focused.

#[test]
fn printer_sample() {

@Fredi-raspall Fredi-raspall left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@daniel-noland thanks for writing this.
However, it goes beyond the goal of being able to expose the contents of the flow filter tables. Can you please clarify the need for the changes related to masquerading?

const TCP: Self = IpProto(6);
}

impl core::fmt::Display for IpProto {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is IpProto re defined also in eal_classify_via_projection.rs, as well as its Display and FixedSize implementations?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Three reasons:

  1. I was avoiding test dependency on net (can create cycles)
  2. because these tests are intended for illustration as much as testing
  3. DPDK requires you to have at least one single byte field (which may be an ignored wildcard but still needs to exist)

The idea was to show how to use the library here and IpProto is a really simple example.

Comment thread config/src/external/overlay/vpc.rs Outdated
Comment thread flow-filter/src/context/tables.rs Outdated
/// An input to the lookup, not an output: it is the candidate stage 3 verifies when stage 1
/// reports a masquerade destination. Carried here rather than consulted in the NF so the
/// verification stays on the batched path with the other two stages.
pub(crate) flow_dst_vpcd: Option<VpcDiscriminant>,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the purpose of this?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you were reviewing scratch code from before I split and refactored all of that. This isn't part of this PR.

Comment thread flow-filter/src/lib.rs Outdated
// the address alone (two peers may masquerade behind one range), so they verified the
// candidate the flow supplied; `None` means no candidate, or one the configuration
// does not agree with. Either way nothing vouches for the packet.
LookupResult::MasqueradeDestination(verified) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand the need for MasqueradeDestination and the problem it addresses.
If a packet has a flow and the flow is active, it will bypass the filter and never get here.
If the flow is not anymore active, the packet should be just dropped; the flow entry won't live much (if it does at all). If the flow was outdated (a config was changed), the masquerading module will take care of checking if it has to survive or be cancelled. Am I missing something here?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1, I'm also struggling to understand these changes

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you were reviewing scratch code from before I split and refactored all of that. This isn't part of this PR

@@ -9,6 +9,12 @@ use dataplane_match_action::{
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
struct IpProto(u8);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is the 3rd definition I see of IpProto ? (and the impl of FixedSize and Display)

@Fredi-raspall

Copy link
Copy Markdown
Contributor

The current version shows as:

dataplane(✔)# show flow-filter table
 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Routing context (flow filter) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
remote v4 (destination -> dst VPC + dst NAT):
  [0] proto=*, src_vni=3000, dst_ip=1.2.3.4/32, dst_port=* -> VNI(4000), NAT: masquerade
  [1] proto=*, src_vni=2000, dst_ip=20.30.90.30/32, dst_port=* -> VNI(4000), NAT: masquerade
  [2] proto=*, src_vni=3000, dst_ip=192.168.128.0/27, dst_port=* -> VNI(2000), NAT: -
  [3] proto=*, src_vni=4000, dst_ip=192.168.128.0/27, dst_port=* -> VNI(2000), NAT: -
  [4] proto=TCP, src_vni=3000, dst_ip=20.10.90.0/24, dst_port=2222 -> VNI(4000), NAT: port-forwarding
  [5] proto=UDP, src_vni=3000, dst_ip=20.10.90.0/24, dst_port=2053 -> VNI(4000), NAT: port-forwarding
  [6] proto=*, src_vni=3000, dst_ip=192.168.80.0/24, dst_port=* -> VNI(4000), NAT: -
  [7] proto=*, src_vni=3000, dst_ip=192.168.100.0/24, dst_port=* -> VNI(2000), NAT: -
  [8] proto=*, src_vni=4000, dst_ip=192.168.50.0/24, dst_port=* -> VNI(3000), NAT: -
  [9] proto=*, src_vni=4000, dst_ip=192.168.60.0/24, dst_port=* -> VNI(3000), NAT: -
  [10] proto=*, src_vni=2000, dst_ip=192.168.50.0/24, dst_port=* -> VNI(3000), NAT: -
local v4 (source -> src NAT):
  [0] proto=*, src_vni=2000, dst_vni=3000, src_ip=192.168.128.0/27, src_port=* -> NAT: -
  [1] proto=*, src_vni=2000, dst_vni=4000, src_ip=192.168.128.0/27, src_port=* -> NAT: -
  [2] proto=*, src_vni=3000, dst_vni=4000, src_ip=192.168.50.0/24, src_port=* -> NAT: -
  [3] proto=*, src_vni=3000, dst_vni=4000, src_ip=192.168.60.0/24, src_port=* -> NAT: -
  [4] proto=*, src_vni=3000, dst_vni=2000, src_ip=192.168.50.0/24, src_port=* -> NAT: -
  [5] proto=*, src_vni=4000, dst_vni=3000, src_ip=192.168.90.0/24, src_port=* -> NAT: masquerade
  [6] proto=*, src_vni=4000, dst_vni=3000, src_ip=192.168.80.0/24, src_port=* -> NAT: -
  [7] proto=*, src_vni=4000, dst_vni=2000, src_ip=192.168.90.0/24, src_port=* -> NAT: masquerade
  [8] proto=*, src_vni=2000, dst_vni=3000, src_ip=192.168.100.0/24, src_port=* -> NAT: -
remote v6 (destination -> dst VPC + dst NAT):
  (no rules)
local v6 (source -> src NAT):
  (no rules)

I would opt for something more compact and human-friendly like

Remotes:
  rank  srcVpc   destination     proto   dst-port      to         action
  [0]   3000     1.2.3.4/32        *        *       |  VNI(4000)  masquerade
  [1]   2000     20.30.90.30/32    *        *       |  VNI(4000)  masquerade
  [2]   3000     192.168.128.0/27  *        *       |  VNI(2000)  -
  [3]   4000     192.168.128.0/27, *        *       |  VNI(2000)  -
  [4]   3000     20.10.90.0/24     TCP     2222     |  VNI(4000)  port-forwarding
  [5]   3000     20.10.90.0/24     UDP     2053     |  VNI(4000)  port-forwarding

Local:
  rank  srcVpc   dstVpc  source                        action
  [0]   2000     3000    192.168.128.0/27           |  -
  [1]   2000     4000    192.168.128.0/27           |  -     
  • Side note: is the proto match field needed in the local table?
  • Also, in the current view, we have src_vni=2000 (local) but then show it as VNI(2000) in the remote table. We should clarify / unify the vpcdiscriminant / vni at some point.

@qmonnet qmonnet linked an issue Aug 3, 2026 that may be closed by this pull request
@qmonnet

qmonnet commented Aug 3, 2026

Copy link
Copy Markdown
Member

I haven't really reviewed the PR, but I saw this commit:

fix(acl-filter): file each ACL rule by its own IP version, not the peering's

A manifest may hold exposes of both IP versions. `ValidatedManifest::is_v4`
is `any`, not `all`, so such a manifest reports `is_v4()` and `is_v6()` at
once, and `validate_ip_version` -- which compares only `is_v4()` -- accepts
the peering. [...]

A ValidatedManifest only has expose blocks of one IP version (unless it is only made of a single “default” expose - see validate_ip_version() in config, and manifest validation). So there's no need to look at individual rules' IP version, we know that it's always the same for the peering. Based on that, I'm not sure this commit is relevant at all.

@qmonnet

qmonnet commented Aug 3, 2026

Copy link
Copy Markdown
Member

The follow-up commit relies on a similar assumption:

fix(flow-filter): a catch-all expose covers every version its peering carries

A default expose names no address at all, so the lowering had to pick the
root prefix's IP version from somewhere -- and it picked
`ValidatedPeering::is_v4`. That is `any`, not `all`: a manifest holding one
expose of each version reports `is_v4()` and `is_v6()` at once, and
`validate_ip_version` accepts the peering.

But unless we have a bug, we ensure that validated manifests only contain one IP version so the difference between any and all of the validated expose blocks is not significant, and we should be able to rely on the IP version for the peering to determine the version to use.

daniel-noland added a commit that referenced this pull request Aug 4, 2026
Review feedback (Fredi-raspall on #1679): the per-rule `name=value, ...`
lines are hard to scan, and a compact columnar layout with a heading row
would read better.

Both dumps now render as a grid: a rank column, one column per key field, a
`|`, then the action columns. Before and after, same table:

    [0] proto=TCP, src_vni=100, dst_ip=80.0.0.5/32, dst_port=2222 -> VNI(200), NAT: port-forwarding
    [1] proto=*, src_vni=100, dst_ip=90.0.0.0/24, dst_port=* -> VNI(200), NAT: -

    rank  proto  srcVpc  destination  dst-port  |  to        NAT
    [0]   TCP    100     80.0.0.5/32  2222      |  VNI(200)  port-forwarding
    [1]   *      100     90.0.0.0/24  *         |  VNI(200)  -

Column headings are looked up by the field name the derive reports, not by
position, so reordering a key's fields cannot silently mislabel a column --
the same property that rendering from the typed form was introduced to give.
An unrecognised field falls back to its own name, so a newly added field
appears with a slightly raw heading rather than being dropped or, worse,
taking its neighbour's label.

The `|` separates what is matched on from what results, which the old
`-> ` did positionally and less visibly. The rank column keeps its previous
meaning: position in match order, not the internal priority value.

`ActionDisplay` becomes `ActionColumns`, since an action now contributes
cells rather than a rendered tail. It stays a bespoke trait rather than
`Display` for the same reason as before -- one action type is the alias
`Option<NatRequirement>`, which this crate cannot implement `Display` for.

The two `display_is_identical_across_backends` tests still assert that every
field renders through its own type (a VNI as a VNI, a protocol by keyword),
but now check the cells of a row and the heading row rather than an exact
substring. Column widths shift whenever a wider value appears anywhere in
the same table, so asserting on spacing would make these tests fail on
unrelated fixture edits.

Not addressed here, from the same review comment: whether the local table
needs its `proto` field at all (it is always `*` -- the local table is built
only from exposes that pass `can_init_connection()`, and only port
forwarding can carry a protocol -- but it is also the 1-byte first field
rte_acl requires), and unifying how a VNI and a `VpcDiscriminant` render
(`100` vs `VNI(200)`).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 4, 2026 00:21
@daniel-noland
daniel-noland force-pushed the pr/daniel-noland/quick-fix-fuzz-and-print branch from 054501d to 2911063 Compare August 4, 2026 00:21
@daniel-noland daniel-noland changed the title quick fix: flow filter printer flow filter printer Aug 4, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 23 out of 23 changed files in this pull request and generated no new comments.

Suppressed comments (2)

acl-filter/src/context.rs:378

confidence: 10
tags: [logic]

`fmt::Debug` currently matches on `self.classifier` by value, which attempts to move the `classifier` field out of `&self` and will fail to compile. Match on a reference instead.
    let kind = match self.classifier {
**flow-filter/src/context/tables.rs:274**
* ```yaml
confidence: 10
tags: [logic]

fmt::Debug currently matches on self.classifier by value, which attempts to move the classifier field out of &self and will fail to compile. Match on a reference instead.

        let kind = match self.classifier {

Copilot AI review requested due to automatic review settings August 4, 2026 05:37

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 23 out of 23 changed files in this pull request and generated no new comments.

daniel-noland and others added 2 commits August 3, 2026 23:47
…renderer

Add RuleFields and Field so consumers can render and align typed rule fields individually. Add
write_grid for fixed-width columnar output without trailing whitespace.

An out-of-range field index and a row whose cell count disagrees with the headings both surface as
formatting errors rather than panicking or silently dropping a column.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Daniel Noland <daniel@githedgehog.com>
Retain each table's typed rules and render them as a grid: rank, key columns from the rule's own
fields, a `|`, then the action columns. Rendering from the typed form means a VNI prints as a VNI
and a protocol as `TCP`, with no decoding of erased bytes by width or position.

The rank column is the rule's match position, not its internal priority, which is a computed
encoding with no meaning outside the table builder and no stability across releases.

MatchKey fields may carry `#[cli(column_name = "...")]` so a heading is declared next to the field
it heads while the identifier still drives classifier layout.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Daniel Noland <daniel@githedgehog.com>
@daniel-noland
daniel-noland force-pushed the pr/daniel-noland/quick-fix-fuzz-and-print branch from 16b1d80 to 098fa41 Compare August 4, 2026 05:47
@daniel-noland
daniel-noland marked this pull request as ready for review August 4, 2026 05:49
@daniel-noland
daniel-noland requested a review from a team as a code owner August 4, 2026 05:49
Copilot AI review requested due to automatic review settings August 4, 2026 05:49
@daniel-noland
daniel-noland enabled auto-merge August 4, 2026 05:49
const TCP: Self = IpProto(6);
}

impl core::fmt::Display for IpProto {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Three reasons:

  1. I was avoiding test dependency on net (can create cycles)
  2. because these tests are intended for illustration as much as testing
  3. DPDK requires you to have at least one single byte field (which may be an ignored wildcard but still needs to exist)

The idea was to show how to use the library here and IpProto is a really simple example.

Comment thread net/src/fixed_size.rs Outdated
Comment thread flow-filter/src/context/tests.rs Outdated
Comment thread acl-filter/src/context.rs
Comment thread flow-filter/src/context/tables.rs Outdated
/// An input to the lookup, not an output: it is the candidate stage 3 verifies when stage 1
/// reports a masquerade destination. Carried here rather than consulted in the NF so the
/// verification stays on the batched path with the other two stages.
pub(crate) flow_dst_vpcd: Option<VpcDiscriminant>,

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you were reviewing scratch code from before I split and refactored all of that. This isn't part of this PR.

Comment thread flow-filter/src/lib.rs Outdated
// the address alone (two peers may masquerade behind one range), so they verified the
// candidate the flow supplied; `None` means no candidate, or one the configuration
// does not agree with. Either way nothing vouches for the packet.
LookupResult::MasqueradeDestination(verified) => {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you were reviewing scratch code from before I split and refactored all of that. This isn't part of this PR

@daniel-noland

Copy link
Copy Markdown
Collaborator Author

Example flow filter CLI print

dataplane# show flow-filter table
 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Routing context (flow filter) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Remote v4 (destination -> dst VPC + dst NAT):
  rank  proto  src-vni  destination  dst-port  |  to        NAT
  [0]   TCP    100      80.0.0.5/32  2222      |  VNI(200)  port-forwarding
  [1]   *      100      90.0.0.0/24  *         |  VNI(200)  -
  [2]   *      100      70.0.0.0/24  *         |  VNI(200)  masquerade
  [3]   *      200      10.0.0.0/24  *         |  VNI(100)  -
Local v4 (source -> src NAT):
  rank  proto  src-vni  dst-vni  source           src-port  |  NAT
  [0]   *      100      200      10.0.0.0/24      *         |  -
  [1]   *      200      100      90.0.0.0/24      *         |  -
  [2]   *      200      100      192.168.70.0/24  *         |  masquerade
Remote v6 (destination -> dst VPC + dst NAT):
  (no rules)
Local v6 (source -> src NAT):
  (no rules)

Note

I didn't use the exact column names suggested by @Fredi-raspall because I think conflating vpc with vni is quite confusing

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 23 out of 23 changed files in this pull request and generated no new comments.

Suppressed comments (2)

flow-filter/src/context/tables.rs:276

confidence: 9
tags: [logic]

`AnyTable::fmt` matches on `self.classifier` by value, which attempts to move the `classifier` field out of `&self`. This won’t compile when the `Reference` variant is present (tests), because `ReferenceTable` is not `Copy` (`acl/src/reference/table.rs:37-41`). Match on a reference instead.

impl<K: MatchKey, A> fmt::Debug for AnyTable<K, A> {
fn fmt(&self, f: &mut fmt::Formatter<'>) -> fmt::Result {
let kind = match self.classifier {
Classifier::Empty => "empty",
Classifier::Dpdk(
) => "dpdk",

**acl-filter/src/context.rs:383**
* ```yaml
confidence: 9
tags: [logic]

AnyTable::fmt matches on self.classifier by value, which attempts to move the classifier field out of &self. This won’t compile under cfg(test) because the Reference variant holds ReferenceTable, which is not Copy (acl/src/reference/table.rs:37-41). Match on a reference instead.

impl<K: MatchKey, A> fmt::Debug for AnyTable<K, A> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let kind = match self.classifier {
            Classifier::Empty => "empty",
            Classifier::Dpdk(_) => "dpdk",
            #[cfg(test)]
            Classifier::Reference(_) => "reference",
        };

@daniel-noland
daniel-noland requested a review from qmonnet August 4, 2026 05:55

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
acl-filter/src/display.rs (1)

61-92: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider sharing the grid assembly with the flow-filter renderer.

fmt_table duplicates the row and heading assembly in flow-filter/src/context/display.rs (Lines 71-96): rank column, key-field loop over Field::of, | separator, then action columns. Only the action columns differ. A shared helper in match-action that takes the heading list and an action-column callback would keep both dumps aligned as fields are added.

This is optional. The two copies are small today.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@acl-filter/src/display.rs` around lines 61 - 92, Optionally reduce
duplication by extracting the shared heading and row assembly from fmt_table and
the flow-filter renderer into a match-action helper. The helper should accept
the key headings and an action-column callback, preserve the rank, Field::of,
and separator columns, and let each renderer supply its differing action
columns; update both renderers to use it while preserving their current output.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@acl-filter/src/display.rs`:
- Around line 61-92: Optionally reduce duplication by extracting the shared
heading and row assembly from fmt_table and the flow-filter renderer into a
match-action helper. The helper should accept the key headings and an
action-column callback, preserve the rank, Field::of, and separator columns, and
let each renderer supply its differing action columns; update both renderers to
use it while preserving their current output.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d508e02d-8f47-43d5-9da1-a738046283a5

📥 Commits

Reviewing files that changed from the base of the PR and between aa9501c and 098fa41.

📒 Files selected for processing (23)
  • acl-filter/src/context.rs
  • acl-filter/src/display.rs
  • acl-filter/src/tests.rs
  • acl/src/dpdk/rule.rs
  • acl/tests/eal_classify_via_projection.rs
  • acl/tests/eal_install_classify.rs
  • config/src/external/overlay/acl.rs
  • fixed-size/src/lib.rs
  • flow-filter/src/context/display.rs
  • flow-filter/src/context/tables.rs
  • flow-filter/src/context/tests.rs
  • match-action-derive/src/lib.rs
  • match-action/Cargo.toml
  • match-action/src/display.rs
  • match-action/src/field.rs
  • match-action/src/lib.rs
  • match-action/src/predicate.rs
  • match-action/src/rule.rs
  • match-action/tests/derive_roundtrip.rs
  • net/src/fixed_size.rs
  • net/src/ip/mod.rs
  • net/src/tcp/port.rs
  • net/src/udp/port.rs

@qmonnet qmonnet left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The last commit is large and would be easier to review as smaller chunks (although it's probably not worth splitting, now).

Looks good to me, thank you! It will be super helpful to be able to dump these tables.

@daniel-noland
daniel-noland dismissed mvachhar’s stale review August 4, 2026 18:27

Manish specifically requested that this be approved out of band

@daniel-noland
daniel-noland added this pull request to the merge queue Aug 4, 2026
Merged via the queue into main with commit 5576f1f Aug 4, 2026
64 of 66 checks passed
@daniel-noland
daniel-noland deleted the pr/daniel-noland/quick-fix-fuzz-and-print branch August 4, 2026 19:14
daniel-noland added a commit that referenced this pull request Aug 4, 2026
`VpcExpose::validate` enforces a single IP version *within* an expose, via
`have_consistent_ip_version` over that expose's own prefix sets. Nothing
enforced it *across* the exposes of a manifest, so a manifest could hold
`10.0.0.0/24` and `2001:db8::/32` at once and validate cleanly.

That is not a supported configuration, and both filters already rely on it
not being one: each reads a single IP version off the peering and files its
rules by it. The flow-filter picks the root prefix for a catch-all expose
from `ValidatedPeering::is_v4`; the ACL filter picks which per-version table
every rule of a peering goes into.

The gap was invisible from those call sites because `ValidatedManifest::is_v4`
is `any`, not `all`. A mixed manifest answers `true` to both `is_v4` and
`is_v6`, so `ValidatedPeering::validate_ip_version` -- which compares only
`is_v4` -- saw two mixed manifests as agreeing on version and accepted the
peering.

The consequences were real. A v6 ACL rule on such a peering was filed into
the v4 table, where narrowing the key discarded it and the peering's default
action then admitted the traffic the rule denied. A catch-all expose covered
only IPv4, leaving the peering's IPv6 traffic with nothing to match.

Enforcing the invariant here makes it true in one place, rather than
defended against in each lowering that assumes it -- which was the first
approach tried, and abandoned on review (qmonnet, #1679) because one check
in config beats defensive handling in two crates. A default expose
names no address, so it belongs to no version and is skipped -- consistent
with the carve-out `validate_ip_version` already makes for default-only
manifests.

Tests cover both orderings (so the check cannot depend on which version is
seen first), a default expose alongside either version, and single-version
manifests in both versions. Nothing asserted this invariant before, which is
how it came to drift.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
daniel-noland added a commit that referenced this pull request Aug 4, 2026
`lower_rules` used `filter_map` to drop a rule whose prefixes did not narrow
to the table's IP version, and then built the table without it. With the
preceding commit that is unreachable -- a manifest's exposes all share one
version, so every rule of a peering matches the table it is filed under --
but silently dropping a rule is the wrong shape whether or not the invariant
holds, and it was the shape that turned the missing invariant into a
fail-open rather than a loud failure.

A dropped rule is indistinguishable from a rule that never matched. An
operator's `Deny` simply stops being enforced, and under a permissive
peering default that is a fail-open: the traffic the rule names is admitted.
Nothing logs, nothing fails, and the CLI dump shows a table that is missing
a rule the configuration asked for.

Returning an error instead refuses the whole configuration, which leaves the
previously applied (and correct) tables in place and makes any future drift
loud rather than silent. This is defence in depth behind the config check,
not a substitute for it: enforcing the invariant once in config is what
review settled on (qmonnet, #1679), and this commit assumes it holds.

The `expect` messages on the test-only reference builder are updated to say
what is actually being asserted: the reference backend cannot fail, but rule
lowering now can, and it runs before the backend is chosen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
daniel-noland added a commit that referenced this pull request Aug 5, 2026
`VpcExpose::validate` enforces a single IP version *within* an expose, via
`have_consistent_ip_version` over that expose's own prefix sets. Nothing
enforced it *across* the exposes of a manifest, so a manifest could hold
`10.0.0.0/24` and `2001:db8::/32` at once and validate cleanly.

That is not a supported configuration, and both filters already rely on it
not being one: each reads a single IP version off the peering and files its
rules by it. The flow-filter picks the root prefix for a catch-all expose
from `ValidatedPeering::is_v4`; the ACL filter picks which per-version table
every rule of a peering goes into.

The gap was invisible from those call sites because `ValidatedManifest::is_v4`
is `any`, not `all`. A mixed manifest answers `true` to both `is_v4` and
`is_v6`, so `ValidatedPeering::validate_ip_version` -- which compares only
`is_v4` -- saw two mixed manifests as agreeing on version and accepted the
peering.

The consequences were real. A v6 ACL rule on such a peering was filed into
the v4 table, where narrowing the key discarded it and the peering's default
action then admitted the traffic the rule denied. A catch-all expose covered
only IPv4, leaving the peering's IPv6 traffic with nothing to match.

Enforcing the invariant here makes it true in one place, rather than
defended against in each lowering that assumes it -- which was the first
approach tried, and abandoned on review (qmonnet, #1679) because one check
in config beats defensive handling in two crates. A default expose
names no address, so it belongs to no version and is skipped -- consistent
with the carve-out `validate_ip_version` already makes for default-only
manifests.

Tests cover both orderings (so the check cannot depend on which version is
seen first), a default expose alongside either version, and single-version
manifests in both versions. Nothing asserted this invariant before, which is
how it came to drift.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
daniel-noland added a commit that referenced this pull request Aug 5, 2026
`lower_rules` used `filter_map` to drop a rule whose prefixes did not narrow
to the table's IP version, and then built the table without it. With the
preceding commit that is unreachable -- a manifest's exposes all share one
version, so every rule of a peering matches the table it is filed under --
but silently dropping a rule is the wrong shape whether or not the invariant
holds, and it was the shape that turned the missing invariant into a
fail-open rather than a loud failure.

A dropped rule is indistinguishable from a rule that never matched. An
operator's `Deny` simply stops being enforced, and under a permissive
peering default that is a fail-open: the traffic the rule names is admitted.
Nothing logs, nothing fails, and the CLI dump shows a table that is missing
a rule the configuration asked for.

Returning an error instead refuses the whole configuration, which leaves the
previously applied (and correct) tables in place and makes any future drift
loud rather than silent. This is defence in depth behind the config check,
not a substitute for it: enforcing the invariant once in config is what
review settled on (qmonnet, #1679), and this commit assumes it holds.

The `expect` messages on the test-only reference builder are updated to say
what is actually being asserted: the reference backend cannot fail, but rule
lowering now can, and it runs before the backend is chosen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci:+cross/full ci:+release Enable VLAB release tests ci:+vlab Enable VLAB tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fuzzer for User ACLs

5 participants