Skip to content

network: select egress traffic by destination-port range - #451

Merged
Pedro Henrique Penna (ppenna) merged 4 commits into
devfrom
agents/fix-issue-278-add-tests-validate-implementation
Oct 9, 2026
Merged

Pedro Henrique Penna (ppenna) merged 4 commits into
devfrom
agents/fix-issue-278-add-tests-validate-implementation

Conversation

@ppenna

@ppenna Pedro Henrique Penna (ppenna) commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

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. run and sandbox pass the new native CIDR:tcp:FIRST-LAST and CIDR:udp:FIRST-LAST rules through, egress policy files lower MXC's ports[].port through ports[].endPort to them, aci_edge_sandboxes expresses the same selectors with them, and a new test-microvm scenario sends traffic through ranges in allow and deny rules.

Why

MXC's network.egress port selectors take an optional inclusive endPort, 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 the aci_edge_sandboxes OpenVMM 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

Commit OpenVMM pin Change
be82ec267 openvmm: pin egress rules that select a destination-port range f4566bab0 The native CIDR:tcp:FIRST-LAST and CIDR:udp:FIRST-LAST rules of nanvix/openvmm#125; the run and sandbox help, the usage reference, and the run guide's native form
94458c2b8 network: lower egress port ranges to native range rules Policy files, in the MXC shape and the flat shape, lower a range to one native range rule for each network, merge the adjacent and overlapping ranges of a network, and report reversed and incomplete ranges clearly; the run guide
34023bace aci_edge_sandboxes: express egress port ranges as OpenVMM range rules The OpenVMM backend expands a range into one range rule instead of one rule per port, accepts ranges of any width, and rejects malformed ranges itself; NetworkRule::on_port_range; the unit, validation, fake OpenVMM, and end-to-end tests; the README
c5f28ad01 test-microvm: verify egress port ranges in allow and deny rules The l3-l4-egress-port-ranges scenario in the default suite, its unit tests, and the validation design

The branch is based on 57b4a4282, which includes #447's MXC-shaped policy files. Each commit passes nvx.py verify, the NVX unit suites, and ruff on its own.

Behavior

Rule for a destination Matches Policy file ports selector Native rule
TCP, 8000 through 8010 TCP on 8000, on 8010, and on every port between; not 7999, 8011, or UDP { "protocol": "tcp", "port": 8000, "endPort": 8010 } CIDR:tcp:8000-8010
UDP, 5000 through 5010 UDP on 5000 through 5010; not TCP { "protocol": "udp", "port": 5000, "endPort": 5010 } CIDR:udp:5000-5010
any, 8000 through 8010 TCP and UDP on 8000 through 8010; not ICMP { "protocol": "any", "port": 8000, "endPort": 8010 } CIDR:tcp:8000-8010 and CIDR:udp:8000-8010
Allow 8000 through 8010, deny 8005 8000 through 8004 and 8006 through 8010 an allow range and a deny port an allow CIDR:tcp:8000-8010 and a deny CIDR:tcp:8005
One port written as a range, 443 through 443 TCP on 443, as before { "protocol": "tcp", "port": 443, "endPort": 443 } CIDR:tcp:443

Ports 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:

Layer Reversed range End port without a first port
OpenVMM rule egress port range 8010-8000 ends below its first port invalid egress port range '-8010'; expected <FIRST>-<LAST>
Policy file allow[0].ports[0].endPort 8000 cannot be below port 8010 allow[0].ports[0].endPort requires port
aci_edge_sandboxes malformed_request: ….ports[0].endPort must not be below port malformed_request: ….ports[0].endPort requires port

A 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-snapshot with such a file does not restore a snapshot that an earlier NVX captured from it; managed sandboxes keep the rules they were provisioned with, and aci_edge_sandboxes does not restore snapshots.

The l3-l4-egress-port-ranges scenario reserves five consecutive ports free for both TCP and UDP and selects the middle three as a range. It boots seven guests through nvx.py run, each with an MXC-shaped policy file: a TCP, a UDP, and an any range 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:

Case TCP below TCP first TCP inside TCP last TCP above UDP below UDP first UDP inside UDP last UDP above
allow tcp ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ ✗
deny tcp ✓ ✗ ✗ ✗ ✓ ✓ ✓ ✓ ✓ ✓
allow udp ✗ ✗ ✗ ✗ ✗ ✗ ✓ ✓ ✓ ✗
deny udp ✓ ✓ ✓ ✓ ✓ ✓ ✗ ✗ ✗ ✓
allow any ✗ ✓ ✓ ✓ ✗ ✗ ✓ ✓ ✓ ✗
deny any ✓ ✗ ✗ ✗ ✓ ✓ ✗ ✗ ✗ ✓
allow tcp, deny inside ✗ ✓ ✗ ✓ ✗ ✗ ✗ ✗ ✗ ✗

The 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 run to reject a policy file's reversed range and its endPort without port before 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 spell RangeInclusive::contains out as two comparisons is declined, because contains inlines to the same comparisons and the manual form trips clippy's manual_range_contains lint.

Validation

  • OpenVMM, on f4566bab0: cargo nextest for net_backend_resources (23 passed) and openvmm_entry (187 passed), clippy --all-targets, rustdoc with -D warnings, and xtask fmt, on Windows with Rust 1.95.0; on prometheus30, the 243 tests of both packages on Linux.
  • NVX, on this head (c5f28ad01): nvx.py verify, the validate-nvx unit suites (836 tests, with HOME pointing 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. For aci_edge_sandboxes: fmt --all, clippy with -D warnings for both feature sets, the tests (201 with all features, 138 with the defaults), rustdoc with -D warnings, and the Rust 1.89 MSRV check.
  • Local WHP, on this head with OpenVMM built from f4566bab0 and guest artifacts from v0.1.0-dev.7d03d777fc9b: test-microvm with l3-l4-egress-port-ranges, l3-l4-egress-policy, l3-l4-egress-protocols, directional-network-policy, and host-loopback-policy, and the nine nvx.py test-aci-edge-sandboxes hypervisor tests, including the new network_port_ranges_are_enforced.
  • Bare metal, on this head in fresh worktrees with OpenVMM built from f4566bab0 and guest artifacts from v0.1.0-dev.7d03d777fc9b: on prometheus32 (KVM), prometheus30 (MSHV), and prometheus28 (WHP), test-microvm with l3-l4-egress-port-ranges, l3-l4-egress-policy, l3-l4-egress-protocols, directional-network-policy, host-loopback-policy, network-snapshot, endpoint-policy-snapshot, and virtio-net; the nine nvx.py test-aci-edge-sandboxes hypervisor 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 on 4277d7b96, which pinned c85b04af5.
  • CI: run 37971385632 on 4277d7b96, whose tree differs from this head only in the c85b04af5 pin and in dev's later performance baselines and Ubuntu guest document, passed, including l3-l4-egress-port-ranges in 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, where sparse_mmap's fragmented-flush test timed out on azure-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

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.

🔵 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.

@ppenna
Pedro Henrique Penna (ppenna) force-pushed the agents/fix-issue-278-add-tests-validate-implementation branch from a683a75 to 4277d7b Compare October 9, 2026 18:10
Copilot AI balanced review requested due to automatic review settings October 9, 2026 18:10

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.

🔵 Needs a closer look

The exact-head CI is still running and the pinned OpenVMM change depends on two upstream pull requests that remain open.

0 open findings

🧠 Review effort: Balanced

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>
@ppenna
Pedro Henrique Penna (ppenna) force-pushed the agents/fix-issue-278-add-tests-validate-implementation branch from 4277d7b to c5f28ad Compare October 9, 2026 18:46
Copilot AI balanced review requested due to automatic review settings October 9, 2026 18:46

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.

🟡 Changes recommended

The PR description still attributes its pin and validation to a superseded OpenVMM revision.

1 open finding

🧠 Review effort: Balanced

Comment thread scripts/nvx.py
@ppenna
Pedro Henrique Penna (ppenna) merged commit 75df184 into dev Oct 9, 2026
59 of 61 checks passed
@ppenna
Pedro Henrique Penna (ppenna) deleted the agents/fix-issue-278-add-tests-validate-implementation branch October 9, 2026 19:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support destination-port ranges in egress rules

2 participants