Repository navigation
Port nvx-ingress to Rust - #533
Merged
Merged
Conversation
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
The central live networking loop lacks automated coverage.
1 open finding
What changed in this PR
Ports nvx-ingress to the Rust guest tooling while preserving the existing C protocol and behavior.
Changes:
- Adds bounded socket, address, netlink, and UDP-settling logic.
- Implements snapshot mode and the live announcement sender.
- Adds musl build validation and extensive parser/collector tests.
| File | Description |
|---|---|
.github/actions/check-nvx-rs/action.yml |
Builds and validates nvx-ingress. |
nvx-rs/Cargo.toml |
Registers the guest binary. |
nvx-rs/README.md |
Documents the binary and build. |
nvx-rs/src/bin/nvx-ingress.rs |
Adds the executable entry point. |
nvx-rs/src/guest/ingress/address.rs |
Classifies listener reachability. |
nvx-rs/src/guest/ingress/collect.rs |
Collects and settles listeners. |
nvx-rs/src/guest/ingress/diag.rs |
Parses socket diagnostics. |
nvx-rs/src/guest/ingress/encode.rs |
Encodes NVXL announcements. |
nvx-rs/src/guest/ingress/mod.rs |
Defines ingress behavior and constants. |
nvx-rs/src/guest/ingress/sender.rs |
Implements live announcement sending. |
nvx-rs/src/guest/ingress/table.rs |
Parses procfs socket tables. |
nvx-rs/src/guest/ingress/test_mode.rs |
Implements snapshot test mode. |
nvx-rs/src/guest/mod.rs |
Exposes the ingress module. |
nvx-rs/tests/guest_tools.rs |
Exercises the compiled binary. |
🧠 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-358-ingress
branch
from
October 13, 2026 21:44
557906a to
28b9f52
Compare
This was referenced Oct 13, 2026
Pedro Henrique Penna (ppenna)
force-pushed
the
agents/fix-issue-358-ingress
branch
from
October 14, 2026 21:27
28b9f52 to
c70cb9c
Compare
Pedro Henrique Penna (ppenna)
force-pushed
the
agents/fix-issue-358-ingress
branch
from
October 14, 2026 21:39
c70cb9c to
7c98bf5
Compare
Pedro Henrique Penna (ppenna)
force-pushed
the
agents/fix-issue-358-ingress
branch
from
October 15, 2026 01:58
7c98bf5 to
a2c9378
Compare
Pedro Henrique Penna (ppenna)
force-pushed
the
agents/fix-issue-358-ingress
branch
from
October 15, 2026 05:08
a2c9378 to
f3a7fbb
Compare
This was referenced Oct 15, 2026
Pedro Henrique Penna (ppenna)
marked this pull request as draft
October 16, 2026 11:34
Pedro Henrique Penna (ppenna)
force-pushed
the
agents/fix-issue-358-ingress
branch
from
October 16, 2026 12:12
f3a7fbb to
f78eaab
Compare
Pedro Henrique Penna (ppenna)
marked this pull request as ready for review
October 16, 2026 12:15
Pedro Henrique Penna (ppenna)
force-pushed
the
agents/fix-issue-358-ingress
branch
from
October 16, 2026 13:56
f78eaab to
4577631
Compare
Port the listener announcer as a static Rust guest binary that keeps the C program's command line, exit statuses, output, timing, and wire format. Split socket-table parsing, address classification, socket diagnostics, UDP settling, announcement encoding, the live sender, and snapshot test mode by concern. Name every protocol, kernel, and buffer constant, and document why it has its value: OpenVMM's announcement port, privileged source ports, and 64-listener limit, the 100 ms scans, 2 s refreshes, and 1 s UDP settling that doc/run.md documents, Linux's socket states, and the C program's buffer sizes. Follow the guest programs' conventions. The program fails with the shared CommandError and exits with the shared statuses 0, 1, and 2, and its diagnostics describe errno values with nix's text. The safe nvx::guest::sys wrappers make its system calls through nix: an owned UDP socket that binds and sends, and the ignored SIGINT and SIGHUP dispositions. A new sys::sock_diag module owns the NETLINK_SOCK_DIAG messages, which nix does not model: it queries the kernel's IPv6 TCP listeners with a receive timeout and decodes the dump's netlink framing, inet_diag_msg, and INET_DIAG_SKV6ONLY attribute from explicit byte layouts into typed messages, so that the program keeps only its policy for wildcard listeners. The monotonic clock now serves every architecture, in milliseconds as well; the standard library sleeps, parses the gateway as inet_pton did, and classifies loopback, link-local, and IPv4-mapped addresses; and no resource is dropped by hand. Keep every descriptor with the library that opened it. The program opens the socket tables, the sysctl, and snapshot files through sys with O_CLOEXEC, so that every descriptor that it opens comes from nix, and FileDescriptor implements std::io::Read, so that BufReader and Read::take buffer and bound a descriptor without becoming a second owner. The guest module documents the rule and denies the standard library's print macros, whose buffered standard output would reorder with sys::Stdout's direct writes. No library parses the kernel's socket tables, but the standard library's strict parsers read their numbers once the program splits the columns. The program reads each table in lines of at most 511 bytes, as the C program's fgets did, splits each row at ASCII white space, and parses the kernel's decimal and hexadecimal columns, bindv6only, and snapshot times with parse_decimal and a new parse_hexadecimal. It thus reads all that the kernel writes as the C program did, but rejects the signed or 0x-prefixed numbers and address words, overflowing values, numbers followed by other bytes, and vertical tabs that C's strtoul and scanf also accepted, which only malformed snapshots contain. It also rejects a row with a NUL byte, which ended C's string, or with an address column longer than the 63 bytes that C's %63s read, which C split. The guest module's documentation records this exception to the compatibility contract. Port the eight C-driven ingress cases, pin the golden wire vector, and cover the kernel's row format and the C forms that the program rejects, the strict parsers, malformed diagnostics, the netlink layouts and a live dump, bounded encoding, sender failures, refreshes, the UDP socket, descriptor reads through std::io, and the real binary through NVX_GUEST_BINARIES. Part of #358. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add nvx-ingress to the Rust guest-binary inventory. The existing nvx.py build-guest-binaries and test-guest-binaries commands now build its static musl executable in the same Cargo build as the other guest programs, check its ELF form, include its digest in the manifest, and run its prebuilt integration tests. A build adds nvx-ingress, as it adds nvx-managed-agent, to an earlier output whose manifest does not record it. Test that the immediate predecessor's output, which lacks only nvx-ingress, upgrades in place. Document the command line, and count seven binaries in the command documentation. The guest-image build remains unchanged until the final switch. Part of #358. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pedro Henrique Penna (ppenna)
force-pushed
the
agents/fix-issue-358-ingress
branch
from
October 16, 2026 14:44
4577631 to
26c73ae
Compare
Pedro Henrique Penna (ppenna)
deleted the
agents/fix-issue-358-ingress
branch
October 16, 2026 15:12
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.


Summary
nvx-ingressto a static Rust guest binary that keeps the C program'scommand line, exit statuses, output, timing, and NVXL wire format, split by
concern: socket-table parsing, address classification, socket-diagnostics
policy, UDP settlement, announcement encoding, the live sender, and snapshot
test mode
only through
nixbehind safenvx::guest::syswrappers,nix's errnotext, the shared
CommandErrorandExitStatus, owned descriptors, and nodirect libc
bindv6only, and snapshot times strictlywith the standard library, in the form in which the kernel writes them,
rather than emulate C's
strtoul,sscanf, andfscanfits value
the manifest-driven
nvx.pyguest-binary workflowPart of #358 (PR 3 of 5). Rebased onto
devafter #528 and #531 merged, sothe PR now holds only its own two commits.
Conventions and system boundary
The ingress module denies unsafe code and calls no libc function, and it adds
none to
guest::sys. Its newsysoperations make their calls through nix:UdpSocket: an owned close-on-exec IPv4 UDP socket that binds a port onevery local address and sends datagrams; the sender retries only
EADDRINUSEfrom port 1023 down through 512sock_diag: theNETLINK_SOCK_DIAGmessages, which nix does not model.dump_ipv6_tcp_socketssends a byte-builtinet_diag_req_v2request froman owned close-on-exec socket and receives the dump in parts of at most
32 KiB, each within a 100 ms timeout, and
decode_inet_diag_dumpdecodesits netlink framing,
inet_diag_msg, andINET_DIAG_SKV6ONLYattributefrom explicit byte layouts into typed
InetDiagMessages, so that theprogram keeps only its policy for wildcard listeners
ignore_interrupt_and_hangup: the ignored SIGINT and SIGHUP dispositionsmonotonic_ms: the existing monotonic clock, now built for everyarchitecture, in whole milliseconds
std::io::ReadforFileDescriptor: onereadcall per call, as forstd::fs::FileEvery descriptor stays with the library that opened it. The program opens the
socket tables,
bindv6only, and snapshot files throughsys::openwithO_CLOEXEC, so every descriptor that it opens comes from nix and has oneowner, and
BufReaderandRead::takebuffer and bound them throughReadwithout a second owner. Because
sys::Stdoutwrites standard output directly,the guest module now denies
print!andeprint!throughclippy::print_stdoutandclippy::print_stderr, and its documentationstates the rule.
The standard library sleeps, resuming an interrupted sleep as the C
nanosleeploop did, parses the gateway, whereIpv4Addraccepts exactlywhat
inet_pton(AF_INET, ...)accepts, and classifies loopback, link-local,and IPv4-mapped addresses with
Ipv4AddrandIpv6Addr. Raw descriptors andnative socket addresses do not leave
sys, and netlink messages are built andparsed as explicit byte layouts without pointer casts. Guests continue to ship
guest/common/nvx-ingress.cuntil PR 5 changes the image build.The integration commit adds
nvx-ingresstoGuestRustBuildConstants.BINARIESand
ADDED_BINARIES, so a build upgrades an earlier build's output in place,and tests the upgrade of the immediate predecessor, which lacks only
nvx-ingress.Behavioral parity
No library parses the kernel's socket tables, but once a row is split into
columns, the standard library's strict parsers read its numbers. The program
reads each table in lines of at most 511 bytes, as the C program's
fgetsdid, splits each row at ASCII white space, and parses the decimal and
hexadecimal columns,
bindv6only, and snapshot times withparse_decimalanda new
parse_hexadecimalinnvx::guest::parse. It reads everything that thekernel writes,
%4d:row numbers,%08Xaddress words,%04Xports,%02Xstates, and
%luinodes, as the C program did.Unlike the C program, it rejects the lenient forms that C's
strtoulandscanfalso read, which only synthetic snapshots contain: signs and0xprefixes, including a signed address word such as
-0000001, which anearlier review made wrap to
0xffffffff; bytes after a number; vertical tabs;NUL bytes, which ended C's line; tokens longer than 63 bytes, which C split;
and numbers too large for their type, which C saturated or truncated. It
skips such a row, keeps the dual-stack default for such a
bindv6only, andends test mode with status 1 for such a time. A
bindv6onlyfrom 2^32 to2^64 − 1, which C truncated to an
int, counts as nonzero. Thenvx::guestdocumentation records this exception to the compatibility contract.
The port also preserves strict IPv4 syntax, timing, socket behavior,
native-endian netlink framing, malformed-message termination, per-port
IPv6-only diagnostics, per-
(port, inode)UDP settlement and retention, thereachability rules, the 1,024-entry tracking limit, and the sorted 64-entry
NVXL payload with its omitted flag. The golden vector remains
4e56584c0100000206011f90060220fb. Each protocol, kernel, and buffer constantdocuments why it has its value: OpenVMM's announcement port, privileged
source ports, and 64-listener limit, the 100 ms scans, 2 s refreshes, and 1 s
UDP settling that
doc/run.mddocuments, Linux's socket states, and the Cprogram's buffer sizes.
Diagnostics describe errno values with
nix's text, as the other guestprograms' do; a few differ from musl's
strerrortext, such asEMFILE's.Validation
This head, on WSL (Ubuntu)
check-nvx-rscargo command: format, both Clippy modes, all-featureand default tests (238 library, 6 fake-OpenVMM, 23 guest-tool, and doc
tests), rustdoc, the Rust 1.89 MSRV, and the aarch64-apple-darwin check
check-aci-edge-sandboxescommand with CI's environment, includingtest-agent-bundlingnvx.py check-guest-binaries: musl Clippy and musl tests (167 library, 23guest-tool, and doc tests)
nvx.py build-guest-binariesinto a fresh directory:nvx-ingressis a422,368-byte stripped static PIE (
dfdb9887...c9ff)nvx.py test-guest-binaries: all 23 prebuilt integration tests passtest_guest_binaries.pyandtest_guest_ingress.py: 23 tests pass withoutskips
Previous head (
f78eaab47)These cover files that this head leaves unchanged:
errors
seven, and a fresh build writes an identical manifest
Differential and live
image builds it (
musl-gcc -static -Os -s -Wall -Wextra -Werror), and theRust program matched status, stdout, and stderr byte for byte for 14,000
generated and mutated snapshot sequences and 75 command-line cases.
kernel-format snapshot sequences, 5,000 of them with tab, form-feed, and
carriage-return separators, and on 12 command-line cases. The corpus covers
loopback, link-local, IPv4-mapped, and unspecified addresses, more than 64
listeners, UDP settling and identity across snapshots, wrapping times,
bindv6only0, 1, or absent, and well-formed and malformed diagnosticdumps. On the mutated corpus, which writes the lenient forms above into most
sequences, 8,891 of 10,000 sequences differ.
unshare -rn), this head and the previoushead send the same announcements from port 1023 at the same times: a TCP
IPv4 listener, an IPv6-only and a dual-stack wildcard listener, which the
netlink diagnostics distinguish, no loopback listener, the UDP socket once
it settles after 1 s, and a refresh after 2 s. Both ignore SIGINT and SIGHUP
and stop on SIGTERM.
CI
CI run 38911772485
is in progress at this head; the previous head's run passed.