Repository navigation
network: select egress traffic by destination-port range - #451
Merged
Pedro Henrique Penna (ppenna) merged 4 commits intoOct 9, 2026
Merged
Pedro Henrique Penna (ppenna) merged 4 commits into
Pedro Henrique Penna (ppenna) merged 4 commits into
Conversation
Contributor
There was a problem hiding this comment.
🔵 Needs a closer look
Network-policy enforcement spans Python, Rust, and an OpenVMM pin whose prerequisite upstream pull requests remain open.
0 open findings
What changed in this PR
Adds inclusive TCP/UDP destination-port ranges across NVX policy compilation, OpenVMM integration, sandbox APIs, documentation, and validation.
Changes:
- Lowers policy ranges into compact native OpenVMM range rules.
- Adds range validation and
NetworkRule::on_port_range. - Adds unit and hypervisor-backed coverage for boundaries, protocols, and deny precedence.
| File | Description |
|---|---|
openvmm |
Pins native destination-port range support. |
scripts/nvx.py |
Documents range syntax in CLI help. |
scripts/nvx_tools/egress_policy.py |
Validates, merges, and lowers ranges. |
scripts/nvx_tools/microvm_tests.py |
Adds the range acceptance scenario. |
scripts/nvx_tools/microvm_test_scripts/l3-l4-egress-port-ranges.sh.in |
Probes range boundaries and protocols. |
scripts/test_egress_policy.py |
Tests range compilation and limits. |
scripts/test_microvm_tests.py |
Tests scenario execution and failures. |
scripts/test_nvx_tools.py |
Updates lowered-rule expectations. |
aci_edge_sandboxes/model/src/model.rs |
Adds the range builder API. |
aci_edge_sandboxes/src/openvmm/network.rs |
Emits native OpenVMM range rules. |
aci_edge_sandboxes/tests/validation.rs |
Tests validation and rule budgets. |
aci_edge_sandboxes/tests/openvmm_fake.rs |
Verifies generated OpenVMM arguments. |
aci_edge_sandboxes/tests/openvmm_e2e.rs |
Tests range enforcement on a hypervisor. |
aci_edge_sandboxes/README.md |
Documents sandbox range behavior. |
doc/usage.md |
Updates CLI syntax reference. |
doc/run.md |
Explains range semantics and lowering. |
doc/design/validation.md |
Records range validation coverage. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Pedro Henrique Penna (ppenna)
force-pushed
the
agents/fix-issue-278-add-tests-validate-implementation
branch
from
October 9, 2026 18:10
a683a75 to
4277d7b
Compare
f4566bab0 (nanvix/openvmm#125) lets microVM egress rules select an inclusive range of TCP or UDP destination ports: --network-egress-allow and --network-egress-deny accept IPv4[/PREFIX]:tcp:FIRST-LAST and IPv4[/PREFIX]:udp:FIRST-LAST beside the address-only, protocol-wide, and single-port forms. A range matches its first and last ports and every port between them, and only its protocol. Ports stay within 1 through 65535, and OpenVMM rejects a range that ends below its first port, or that lacks its first or last port, before boot. Deny rules still take precedence over allow rules, so a deny rule for one port inside an allowed range keeps that port blocked, and a range, like a single port, makes a policy reject IPv4 fragments. Existing rules match the same packets and keep their snapshot policy digests. run and sandbox pass these rules to OpenVMM unchanged, so the help of their --network-egress-allow and --network-egress-deny now describes the CIDR[:PROTOCOL[:PORT[-PORT]]] form, and the usage reference and the run guide document it. nvx.py verify passes with this pin. Part of #278. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
An egress policy file accepts an inclusive destination-port range, port through endPort, as MXC's network.egress port selectors do, but NVX lowered it to one native rule per port. Each action accepts at most 256 native rules, so a range of more than 256 ports, such as 1024 through 65535, was rejected, and a narrower range spent one rule per port of the budget that every other rule shares (#278). The pinned OpenVMM accepts CIDR:tcp:FIRST-LAST and CIDR:udp:FIRST-LAST, so a range now lowers to one native range rule for each destination network, and protocol any with a port or a port range to one TCP and one UDP rule, in MXC-shaped and flat rules alike. Lowering stays exact, canonical, and bounded: the sweep over port events emits, for each run of ports with the same addresses, one range for each network, and a network that the next run also matches extends its range, so adjacent and overlapping ranges of one network merge into one rule and equivalent policies lower alike. A range of one port still lowers to the single-port form, so policies without ranges or adjacent ports lower as before. Every network of one run is a distinct rule, so the 256-rule limit still bounds the work before any rule is built, and exclusions and protocol-wide coverage are unchanged. A flat rule's endPort without port is now reported as such even when the rule names no protocol, where it was reported as a port without a protocol, and a reversed range names both of its ports in either rule shape. Unit tests cover the lowering of TCP, UDP, any, full, and one-port ranges in MXC-shaped and flat allow and deny rules, a denied port inside an allowed range, the merging of adjacent and overlapping ranges, the shared rule budget and its 256-rule boundaries with ranges, exclusions multiplied by separate port runs, and the messages for reversed and incomplete ranges in both shapes. The run and sandbox provision tests, and the l3-l4-egress-policy scenario's policy test, expect range rules. The run guide documents the lowering. Part of #278. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The OpenVMM backend expanded a network.egress port range, port through endPort, into one OpenVMM rule per port, and rejected with policy_validation any range of more than 256 ports, because OpenVMM accepts at most 256 rules in each list. MXC allows any range within 1 through 65535 (#278). The pinned OpenVMM accepts CIDR:tcp:FIRST-LAST and CIDR:udp:FIRST-LAST, so the backend now expands a range exactly into one range rule for each destination network, and any with a port or a port range into one TCP and one UDP rule. A range of one port keeps the single-port form, so other rules expand as before. A full range such as 1 through 65535 is accepted, and only expansions beyond 256 OpenVMM rules, IPv6, and ICMP with a port are still rejected. Structural validation already rejects a reversed range and an endPort without a port as malformed requests, but the backend's own validate_provision and provision accept requests directly: a reversed range made the old expansion subtract past zero, and an endPort without a port selected every port of its protocol. The backend now rejects both shapes with policy_validation. NetworkRule::on_port_range builds a range selector. Unit tests cover TCP, UDP, any, full, and one-port ranges beside a denied port, the rejected shapes, and the 256-rule limit with separate ranges, and the model's JSON for a range. The validation tests accept full ranges and reject policies that need more than 256 rules before probing, and the fake OpenVMM test checks that a range reaches OpenVMM's command line. The new end-to-end test network_port_ranges_are_enforced probes the gateway's TCP DNS service, on port 53, under ranges that start, end, or surround port 53, ranges from the next port up or through the next port down, a UDP range of every port, and a range around a denied port 53, as allow rules under a deny default and as deny rules that override an allow rule. The README and the backend's documentation follow. Part of #278. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The l3-l4-egress-policy scenario sends TCP and UDP to the boundaries of one allowed range beside a denied port, but no scenario sent traffic through a UDP-only or any range, through a range as a deny rule, or checked that a range of one protocol leaves the other protocol alone on the same ports (#278). The l3-l4-egress-port-ranges scenario, in the default suite, reserves five consecutive ports free for both TCP and UDP and selects the middle three as a range. It boots one guest per case through the public nvx.py run, with an MXC-shaped egress policy file whose ports selectors carry port and endPort, egress denied by default, and ingress denied. A TCP, a UDP, and an any range for the gateway each run twice: alone as an allow rule, and as a deny rule beside an address-only allow rule that admits every probe, so that only deny precedence can block one. A seventh case allows the TCP range and denies the port inside it. The guest probes TCP and UDP below the range, at its first port, inside it, at its last port, and above it, and the harness requires exactly the case's traffic to pass: the guest reports each TCP response, and the host observes each TCP connection and UDP datagram. Each case's policy must lower its range to one native range rule, which the results record, so the traffic exercises OpenVMM's range form. The scenario then requires OpenVMM to reject a reversed native range and a range without its first port before boot, and nvx.py run to reject a policy file's reversed range and its endPort without port before it starts OpenVMM, and records each case in l3-l4-egress-port-ranges-results.json. The protocol scenario's case type and checker now serve both scenarios, and the probe server's messages name egress probes rather than protocol probes. Unit tests cover the cases, their expectations, and their native rules; each case's public command and policy file; leaked and missed traffic on either side; each pre-launch rejection that a regression would skip; the guest probe script under a POSIX shell; and the scenario's dispatch. The validation design describes the scenario. Fixes #278. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pedro Henrique Penna (ppenna)
force-pushed
the
agents/fix-issue-278-add-tests-validate-implementation
branch
from
October 9, 2026 18:46
4277d7b to
c5f28ad
Compare
Pedro Henrique Penna (ppenna)
deleted the
agents/fix-issue-278-add-tests-validate-implementation
branch
October 9, 2026 19:53
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Fixes #278 with nanvix/openvmm#125: a microVM egress rule can now select an inclusive range of TCP or UDP destination ports as one rule, instead of one rule for every port.
runandsandboxpass the new nativeCIDR:tcp:FIRST-LASTandCIDR:udp:FIRST-LASTrules through, egress policy files lower MXC'sports[].portthroughports[].endPortto them,aci_edge_sandboxesexpresses the same selectors with them, and a newtest-microvmscenario sends traffic through ranges in allow and deny rules.Why
MXC's
network.egressport selectors take an optional inclusiveendPort, such as{ "protocol": "tcp", "port": 8000, "endPort": 8010 }. OpenVMM's rules could select only one port, so NVX lowered a range to one native rule per port. Each action accepts at most 256 native rules, so egress policy files and theaci_edge_sandboxesOpenVMM backend rejected every range of more than 256 ports, such as 1024 through 65535, and a narrower range spent one rule per port of the budget that every other rule shares.Commits
be82ec267openvmm: pin egress rules that select a destination-port rangef4566bab0CIDR:tcp:FIRST-LASTandCIDR:udp:FIRST-LASTrules of nanvix/openvmm#125; therunandsandboxhelp, the usage reference, and the run guide's native form94458c2b8network: lower egress port ranges to native range rules34023baceaci_edge_sandboxes: express egress port ranges as OpenVMM range rulesNetworkRule::on_port_range; the unit, validation, fake OpenVMM, and end-to-end tests; the READMEc5f28ad01test-microvm: verify egress port ranges in allow and deny rulesl3-l4-egress-port-rangesscenario in the default suite, its unit tests, and the validation designThe branch is based on
57b4a4282, which includes #447's MXC-shaped policy files. Each commit passesnvx.py verify, the NVX unit suites, and ruff on its own.Behavior
portsselector{ "protocol": "tcp", "port": 8000, "endPort": 8010 }CIDR:tcp:8000-8010{ "protocol": "udp", "port": 5000, "endPort": 5010 }CIDR:udp:5000-5010any, 8000 through 8010{ "protocol": "any", "port": 8000, "endPort": 8010 }CIDR:tcp:8000-8010andCIDR:udp:8000-8010CIDR:tcp:8000-8010and a denyCIDR:tcp:8005{ "protocol": "tcp", "port": 443, "endPort": 443 }CIDR:tcp:443Ports stay within 1 through 65535. Each layer rejects a range that ends below its first port, and an end port without a first port, before launch:
egress port range 8010-8000 ends below its first portinvalid egress port range '-8010'; expected <FIRST>-<LAST>allow[0].ports[0].endPort 8000 cannot be below port 8010allow[0].ports[0].endPort requires portaci_edge_sandboxesmalformed_request:….ports[0].endPort must not be below portmalformed_request:….ports[0].endPort requires portA range restricts ports, so, like a single port, it makes a policy reject IPv4 fragments. Address-only, protocol-wide, and single-port rules behave as before and keep their snapshot policy digests, and a range has a digest record of its own. A policy file with a range, or with adjacent ports, now lowers to fewer native rules, so
run --restore-snapshotwith such a file does not restore a snapshot that an earlier NVX captured from it; managed sandboxes keep the rules they were provisioned with, andaci_edge_sandboxesdoes not restore snapshots.The
l3-l4-egress-port-rangesscenario reserves five consecutive ports free for both TCP and UDP and selects the middle three as a range. It boots seven guests throughnvx.py run, each with an MXC-shaped policy file: a TCP, a UDP, and ananyrange for the gateway, each as an allow rule and as a deny rule beside an address-only allow rule that admits every probe, and an allowed TCP range whose inside port a deny rule blocks. Every case's range reached OpenVMM as one native rule per protocol. Each guest probes TCP and UDP below the range, at its first port, inside it, at its last port, and above it, and the observations were identical on bare-metal KVM, MSHV, and WHP hosts and on a local WHP host:tcptcpudpudpanyanytcp, deny insideThe host observed each TCP connection and UDP datagram in the ✓ cells and none in the ✗ cells, and the guest's TCP results agreed. The scenario then requires OpenVMM to reject a reversed native range and a range without its first port before boot, and
nvx.py runto reject a policy file's reversed range and itsendPortwithoutportbefore it starts OpenVMM.Review
Copilot's reviews of this pull request reported no findings; they flagged only that the pin depends on the open nanvix/openvmm#124 and nanvix/openvmm#125. Copilot's review of nanvix/openvmm#125 found an imprecise sentence about ICMP in OpenVMM's CLI reference and a shadowed closure parameter in its port parser; both are fixed in the pinned
f4566bab0, which changes only that sentence and that name from the first pin,c85b04af5. Its suggestion to spellRangeInclusive::containsout as two comparisons is declined, becausecontainsinlines to the same comparisons and the manual form trips clippy'smanual_range_containslint.Validation
f4566bab0:cargo nextestfornet_backend_resources(23 passed) andopenvmm_entry(187 passed),clippy --all-targets, rustdoc with-D warnings, andxtask fmt, on Windows with Rust 1.95.0; onprometheus30, the 243 tests of both packages on Linux.c5f28ad01):nvx.py verify, the validate-nvx unit suites (836 tests, withHOMEpointing to an empty directory so that a test that commits in a temporary repository does not inherit this machine's SSH commit signing), ruff, and pyright for Linux and Windows; shellcheck and shfmt on the rendered guest script. Foraci_edge_sandboxes:fmt --all, clippy with-D warningsfor both feature sets, the tests (201 with all features, 138 with the defaults), rustdoc with-D warnings, and the Rust 1.89 MSRV check.f4566bab0and guest artifacts fromv0.1.0-dev.7d03d777fc9b:test-microvmwithl3-l4-egress-port-ranges,l3-l4-egress-policy,l3-l4-egress-protocols,directional-network-policy, andhost-loopback-policy, and the ninenvx.py test-aci-edge-sandboxeshypervisor tests, including the newnetwork_port_ranges_are_enforced.f4566bab0and guest artifacts fromv0.1.0-dev.7d03d777fc9b: onprometheus32(KVM),prometheus30(MSHV), andprometheus28(WHP),test-microvmwithl3-l4-egress-port-ranges,l3-l4-egress-policy,l3-l4-egress-protocols,directional-network-policy,host-loopback-policy,network-snapshot,endpoint-policy-snapshot, andvirtio-net; the ninenvx.py test-aci-edge-sandboxeshypervisor tests; and the NVX unit suites passed. The same checks passed on the first head,a683a7573, before the rebase onto network: support MXC-shaped rule-local CIDR exclusions #447, and on4277d7b96, which pinnedc85b04af5.4277d7b96, whose tree differs from this head only in thec85b04af5pin and indev's later performance baselines and Ubuntu guest document, passed, includingl3-l4-egress-port-rangesin the KVM, MSHV, and WHP microVM jobs and OpenVMM's unit tests and VMM tests on Linux and Windows. On this head,c5f28ad01, run 37975567309 passed on attempt 2. Attempt 1 passed every job except the OpenVMM Windows unit lane, wheresparse_mmap's fragmented-flush test timed out onazure-windows-4, the known ci: OpenVMM Windows unit lane times out in sparse_mmap fragmented-flush test on azure-windows-4 #312 flake, which this change does not touch; attempt 2 reran the failed jobs.Dependencies
f4566bab0, open), the pinned revision, stacked on net_backend_resources: select egress rules by protocol without a port nanvix/openvmm#124 (3796991d5, open), whichdevpins. Merge net_backend_resources: select egress rules by protocol without a port nanvix/openvmm#124 and then net_backend_resources: select egress rules by destination-port range nanvix/openvmm#125 by fast-forwarding nanvix/openvmmmain, so that the pin stays onmain.