Skip to content

(11) config - #1763

Open
daniel-noland wants to merge 44 commits into
pr/daniel-noland/model-checkingfrom
pr/daniel-noland/config-algebra
Open

daniel-noland wants to merge 44 commits into
pr/daniel-noland/model-checkingfrom
pr/daniel-noland/config-algebra

Conversation

@daniel-noland

@daniel-noland daniel-noland commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

todo:

The last commit needs cleanup. And a few lines of comments need trimmed.

I will push trivial fixes once (08)-(10) land. Till then this is ready for review

@coderabbitai

coderabbitai Bot commented Aug 26, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Essentials

Run ID: 4120e75d-9986-4a40-9f4a-9c07f99b5084

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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

@codecov

codecov Bot commented Aug 26, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.31897% with 62 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
config/src/external/overlay/algebra.rs 94.01% 16 Missing and 11 partials ⚠️
dataplane/src/packet_processor/fuzz.rs 93.36% 15 Missing and 12 partials ⚠️
hardware/src/nic/mod.rs 0.00% 7 Missing ⚠️
config/src/external/overlay/completeness.rs 97.87% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@daniel-noland
daniel-noland force-pushed the pr/daniel-noland/model-checking branch from 8e14aad to 68de4de Compare August 26, 2026 17:30
@daniel-noland
daniel-noland force-pushed the pr/daniel-noland/config-algebra branch from 47b6176 to 9ef4a02 Compare August 26, 2026 17:30
@daniel-noland
daniel-noland force-pushed the pr/daniel-noland/model-checking branch from 68de4de to bcbd57a Compare August 26, 2026 19:36
@daniel-noland
daniel-noland force-pushed the pr/daniel-noland/config-algebra branch 2 times, most recently from 120d507 to f65f958 Compare August 26, 2026 20:41
@daniel-noland
daniel-noland force-pushed the pr/daniel-noland/model-checking branch 2 times, most recently from 8cd913c to be74286 Compare August 26, 2026 21:02
@daniel-noland
daniel-noland force-pushed the pr/daniel-noland/config-algebra branch 2 times, most recently from 25deec3 to a75a2b7 Compare August 26, 2026 21:13
@daniel-noland
daniel-noland force-pushed the pr/daniel-noland/model-checking branch from be74286 to 95dd658 Compare August 26, 2026 21:13
@daniel-noland
daniel-noland force-pushed the pr/daniel-noland/config-algebra branch from a75a2b7 to 70fd2e6 Compare August 26, 2026 21:25
@daniel-noland
daniel-noland force-pushed the pr/daniel-noland/model-checking branch 2 times, most recently from 6446c90 to e58b3d3 Compare August 27, 2026 01:29
@daniel-noland
daniel-noland force-pushed the pr/daniel-noland/config-algebra branch from 70fd2e6 to 2c7edca Compare August 27, 2026 01:29
@daniel-noland
daniel-noland force-pushed the pr/daniel-noland/model-checking branch from e58b3d3 to bc6dcbb Compare August 27, 2026 01:41
@daniel-noland
daniel-noland force-pushed the pr/daniel-noland/config-algebra branch from 2c7edca to 806e9de Compare August 27, 2026 01:41
@daniel-noland
daniel-noland force-pushed the pr/daniel-noland/model-checking branch from bc6dcbb to 829e3f8 Compare August 27, 2026 02:25
@daniel-noland
daniel-noland force-pushed the pr/daniel-noland/config-algebra branch from 806e9de to ee7a16a Compare August 27, 2026 02:25
@daniel-noland
daniel-noland force-pushed the pr/daniel-noland/model-checking branch from 829e3f8 to 40659a3 Compare August 27, 2026 04:38
@daniel-noland
daniel-noland force-pushed the pr/daniel-noland/config-algebra branch from ee7a16a to bf31000 Compare August 27, 2026 04:38
@daniel-noland
daniel-noland force-pushed the pr/daniel-noland/model-checking branch from 40659a3 to 3a81a5b Compare August 27, 2026 05:12
@daniel-noland
daniel-noland force-pushed the pr/daniel-noland/config-algebra branch 2 times, most recently from 9049415 to 9e27aed Compare August 27, 2026 06:30
@daniel-noland
daniel-noland force-pushed the pr/daniel-noland/model-checking branch 2 times, most recently from 93cb81d to 7295f75 Compare August 27, 2026 18:00
@daniel-noland
daniel-noland force-pushed the pr/daniel-noland/config-algebra branch from 9e27aed to b4be140 Compare August 27, 2026 18:00
@daniel-noland
daniel-noland force-pushed the pr/daniel-noland/model-checking branch from 7295f75 to bdaa96a Compare August 27, 2026 18:29
@daniel-noland
daniel-noland force-pushed the pr/daniel-noland/config-algebra branch from b4be140 to 64c7005 Compare August 27, 2026 18:29
daniel-noland and others added 30 commits September 20, 2026 23:56
Comparing generated overlays with arbitrary real configurations would report
zero reachability because the algebra derives addresses from its handles.
Survey schema degrees of freedom instead, using exhaustive destructuring to
catch new unclassified fields and counters for actual draws. Report results
only after the full sample count; shorter emulated runs state their size and
make no coverage claim.

Signed-off-by: Daniel Noland <daniel@githedgehog.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Codex <codex@openai.com>
Config-apply tests need a frame condition: traffic outside the changed
operation's footprint must continue unaffected. Split a generated sequence
at its last operation, derive untouched loads from its read/write footprint,
and assert stable re-enactment after the initial race. Also propagate worker
panics through barriers and advance genids so the harness cannot hang or
manufacture stale-config behavior.

Signed-off-by: Daniel Noland <daniel@githedgehog.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Codex <codex@openai.com>
Fleet always told the NAT allocator it was on generation one and never
published a generation to pipeline stages. That made flow revalidation
unreachable and caused repeated enactments to model the wrong configuration
lifecycle. Publish allocator and stage generations in management order,
enact one component at a time, and separate worker tuples with disjoint port
ranges.

Signed-off-by: Daniel Noland <daniel@githedgehog.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Codex <codex@openai.com>
unsafe-allow-abi-mismatch disabled rustc's protection against linking crates
built with different sanitizer flags, although build-std already instruments
the full sysroot consistently. A stale sysroot could therefore produce a
green but meaningless fuzz run. Restore the compiler check, stamp each
sysroot with its sanitizer, and make just fuzz reject disagreement.

Signed-off-by: Daniel Noland <daniel@githedgehog.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Codex <codex@openai.com>
A short burst ended before a full configuration apply reached the masquerade
allocator because rebuilding rte_acl took about a millisecond. The instrument
then measured timing rather than impact and made the full apply look far
safer than the allocator step. Keep traffic running across the entire update
window and include the router-configuration gap used by management.

Signed-off-by: Daniel Noland <daniel@githedgehog.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Codex <codex@openai.com>
The enactment instrument found an allocator swap assigning one public tuple
to two live flows, allowing a reply to reach the wrong tenant conversation.
The harness's deterministic allocation was a possible artifact. Exercise
both deterministic and randomized allocators, priming the randomized case
before the swap, and confirm the transient belongs to the dataplane update.

Signed-off-by: Daniel Noland <daniel@githedgehog.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Codex <codex@openai.com>
Some model-checked properties synchronized workers with std::sync::Barrier,
making those ordering points invisible to Loom and Shuttle. Route modeled
barriers and related primitives through the concurrency facade. Keep direct
standard synchronization only for process-global registries and diagnostic
instruments, where modeled primitives are invalid or would perturb the
schedule, with explicit lint suppressions.

Signed-off-by: Daniel Noland <daniel@githedgehog.com>
Co-authored-by: Codex <codex@openai.com>
main grew a `no_multipath` field on `VpcPeering`, and the census destructures
the type exhaustively so that exactly this cannot pass unnoticed: the build
failed with E0027 until someone classified the new degree of freedom. That is
the census working, not a rebase casualty to paper over.

It is `Fixed(false)`. Every `VpcPeering` constructor sets it false and no
operation flips it; the only writer is the k8s converter, from
`GatewayAgentPeerings::no_multi_path`. So no generated configuration disables
multipath, and any property about stateless peering is currently unreachable
from the algebra.

Signed-off-by: Daniel Noland <daniel@githedgehog.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Codex <codex@openai.com>
`LazyLock` and `AtomicU64` were imported at file scope but referenced only
inside `concurrency::with_std!`; the `loom` and `shuttle` arms build their
statics from `std::sync::atomic` directly. So the imports were dead in exactly
the two builds the block exists to serve, and warned there.

Verified: no unused-import warnings under `--features shuttle`.

Signed-off-by: Daniel Noland <daniel@githedgehog.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Codex <codex@openai.com>
The rationale for `OnceLock` over a `static` initializer stopped at "would fail
to typecheck on those", followed by two empty doc lines -- losing the half that
says what `OnceLock` does about it.

Signed-off-by: Daniel Noland <daniel@githedgehog.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Codex <codex@openai.com>
…therwise

The sanitizer guard only ran when `DATAPLANE_SYSROOT` was set *and* its
`.sanitize` stamp was readable. With no stamp there is nothing to compare, so
the check simply did not run and a request for instrumentation quietly got a
sysroot that may carry none -- the exact failure the guard exists to prevent.

Refuse in that case, but only when a sanitizer was actually asked for: the
default `sanitize` is the empty string and `NONE` is an explicit opt-out, and
neither has anything to be wrong about. Verified all three paths: default does
not refuse, `sanitize=address` without a stamp refuses, `sanitize=NONE` does
not.

Signed-off-by: Daniel Noland <daniel@githedgehog.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Codex <codex@openai.com>
Bring the applicable concurrency and race changes from #1805 into #1762:
the Shuttle-aware left-right fork and feature wiring, process-global
bookkeeping, the flow-table count, generation staging, port-forwarded
flow migration and arbitration, and masquerade reverse-tuple protection.
Carry their regression tests and sanitizer coverage guards with them.

Adapt the changes to the current harness and retain native test coverage.
Keep six pipeline tests excluded under Shuttle because DashMap's native
shard locks can block its scheduler. The pipeline smoke test reproduces
the same futex wait and needs that exclusion on this version of the tree.
Use the process-global ACL registry mutex with an explicit lint exception
under the model backends.

Later clock, profile-rename, and VM-specific changes remain in #1805.

Validation: 27 workspace Shuttle tests; 354 ordinary tests across the
affected dataplane, NAT, flow-entry, pipeline, and ACL-filter packages;
Clippy on both backends with -D warnings; workspace all-targets Loom
check; formatting; feature resolution; Nix sanitizer flag evaluation.

Signed-off-by: Daniel Noland <daniel@githedgehog.com>
A missing driver symlink means a NIC is unbound, which is valid when its
kernel module was never loaded or it was already detached. Treating that as
an error made BindToVfioPci refuse the device before its override-and-bind
path. Continue binding unclaimed NICs, recognize e1000 and e1000e, and link
the matching DPDK PMD for QEMU and bare-metal hosts.

Signed-off-by: Daniel Noland <daniel@githedgehog.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Conversation replied from the address a sender dialed instead of the address
that arrived after NAT. Those differ when an expose both translates and
accepts connections, making the far side send from an address it does not
own and causing a misleading flow-filter drop. Build replies from the
delivered packet so the existing oracle genuinely checks reverse
translation.

Signed-off-by: Daniel Noland <daniel@githedgehog.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Static NAT rewrites a destination before masquerade, so the packet's current
key differs from the initial key where FlowLookup filed its forward flow.
The intra-burst fallback searched only the rewritten key and allocated a new
public tuple for every packet. Keep the current-key lookup first for replies,
then retry the original key on a miss and cover the combined translation.

Signed-off-by: Daniel Noland <daniel@githedgehog.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The operation algebra could not produce static NAT, leaving two schema
degrees unreachable and making the config-apply StaticNat step store only
empty tables. Add the missing expose flavour using the algebra's disjoint
private and public address blocks, and update the completeness census. Mixed
static NAT and masquerade configurations now reach their shared pipeline
path.

Signed-off-by: Daniel Noland <daniel@githedgehog.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Inbound loads used a peer's public translation range as the sender address
and answered to the dialed address rather than the request's delivered
source. Both values happened to agree before translated inbound exposes were
generated; afterward the flow filter correctly rejected them as unowned.
Separate dial destinations from peer source addresses and reply to the
observed sender.

Signed-off-by: Daniel Noland <daniel@githedgehog.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Port forwarding was the last missing translated expose flavour and the only
source of generated port ranges, leaving several configuration fields
untested. Add
equal-width but distinct public and private ranges, forbid conflicting
directional translations on the opposite side, and require evidence that
inbound loads exercise the new flavour. Leave protocol and timeout variation
to separate changes.

Signed-off-by: Daniel Noland <daniel@githedgehog.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every generated peering permitted all traffic, so pipeline properties never
exercised a configuration intended to deny a packet. Add coarse ACL shapes
whose verdicts remain predictable without copying the production evaluator.
Update traffic derivation to omit denied loads and retain guards proving ACL
decisions are actually reached.

Signed-off-by: Daniel Noland <daniel@githedgehog.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The completeness survey stopped at whether a peering had an ACL, hiding
fixed rule, pattern, and scope fields behind a single present value. Extend
the census through the entire ACL schema. This reports eight pre-existing
fixed degrees of freedom without claiming the algebra gained new reach.

Signed-off-by: Daniel Noland <daniel@githedgehog.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Packet-level ACL rules never exercise reverse-flow permission, where the
verdict depends on NAT state. Generate permit-by-flow peerings and verify
their replies through AclFilter's reverse lookup. The first run exposes and
pins a separate defect: port-forwarded flows retain generation one and lose
permission after any configuration change.

Signed-off-by: Daniel Noland <daniel@githedgehog.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
With at most one rule per direction, generated ACLs could not make precedence
affect a verdict. Add two predictable overlapping rules that partition a
peering around one masquerade expose; reversing them now breaks the denial
property. Track the named expose position and assign traffic variation before
filtering so independent derivations remain comparable.

Signed-off-by: Daniel Noland <daniel@githedgehog.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Gateway-group names, ACL logging, and idle timeouts remained fixed solely
because the algebra never selected them. Vary all three and update the
census. Record the current limits: disjoint exposes do not exercise gateway
grouping semantics, and generated timeouts outlast these properties, so the
fields lower successfully without testing expiration.

Signed-off-by: Daniel Noland <daniel@githedgehog.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Generated ACLs still left protocol, port, and destination matching fixed, so
dropping a field or lowering it as a wildcard could pass. Add predictable
narrow rules whose target prefixes appear in exactly one traffic role,
avoiding a duplicate ACL evaluator. Include a protocol guard that must not
fire so both omission and wildcarding become observable.

Signed-off-by: Daniel Noland <daniel@githedgehog.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every generated port-forwarding expose accepted any protocol, leaving its
protocol key and collision behavior untested. Cycle protocols across expose
slots so one manifest can contain several. Because derived traffic is UDP,
exclude TCP-only exposes from offered loads and make Draft::carries account
for both expose protocol and peering ACL.

Signed-off-by: Daniel Noland <daniel@githedgehog.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Generated exposes had no exclusions, so matchers, LPM tables, and
RangeBuilder saw only contiguous prefixes. Remove a middle slice rather than
one half, forcing the effective set into two ranges. This change measures
reach and shape only; traffic derivation still selects from the effective set
and therefore does not yet test the excluded hole.

Signed-off-by: Daniel Noland <daniel@githedgehog.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The algebra could not express default-route exposes. Default routes
overlap every neighboring route, so their validity depends on more than the
operation's own arguments. Restrict them to otherwise empty peerings and let
Op::reads inspect the draft, keeping applicability local and restoring the
independence property. Skip loads whose expose protocol cannot carry their
traffic.

Signed-off-by: Daniel Noland <daniel@githedgehog.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Existing exclusion coverage drew traffic only from the effective address
set, so a matcher that ignored every exclusion still passed. Aim packets
directly at excluded addresses and require them to be unreachable. Exempt a
manifest with a default expose because its default route legitimately covers
the hole, and mark the census row as behaviorally enforced.

Signed-off-by: Daniel Noland <daniel@githedgehog.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…t names

The `-except` denial inherited `src_any_ports`/`dst_any_ports` = `[1..65535]`
from the `rule` closure and only narrowed `pattern.src`/`dst`. Those ranges mean
"any address within the peering, restricted to these ports", and
`AclRule::validate_patterns_coverage` materializes them back into `src`/`dst`,
so the narrowing was undone: the rule denied *all TCP across the peering* rather
than the one prefix it was built to except.

`PermitExcept` carries empty vectors already, so only the protocol-narrowed
guard was affected -- and it was invisible because every derived load is UDP, so
no property ever sent traffic the broken denial could match. It also contradicted
the `AclPattern.src`/`dst` rows in the completeness census, which record the
exception as address-scoped.

Clear the matching `*_any_ports` when narrowing, and assert it over generated
configurations: a `-except` denial that names a prefix on a side must not also
carry any-ports on that side. Break-tested by restoring the old two lines --
`every_sequence_builds_a_valid_configuration` fails with the prefix and the
ranges that undo it.

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

Labels

ci:+merge-ready Run all checks which will be run in the merge queue regardless of label status

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants