Repository navigation
nvx-rs: per-clone network policies on deny-all templates - #978
Conversation
There was a problem hiding this comment.
🔵 Needs a closer look
The changes cross the host/guest isolation boundary and add a privileged process, requiring human confirmation of fail-closed restoration and lifecycle behavior across KVM, MSHV, and WHP, whose suite remains pending.
No open issues 🎉
What changed in this PR
This PR enables NVX clones to share a deny-all network template while applying their own policies at restore, addressing #850 using the OpenVMM support from #931. It also adds guest listener announcement and supervision for policies that publish listeners.
Changes:
- Captures network templates under complete deny-all policies and validates clones by NIC address rather than policy.
- Applies each clone’s network policy, DNS, proxy, and listener-publication settings, with announcer lifecycle management.
- Expands tests and vectors for shared templates, inherited sockets, and forward-bind failure attribution.
| File | Description |
|---|---|
tests/runtime-adapter/tests/vm_template.rs |
Tests template sharing across network policies and deny-all capture. |
tests/runtime-adapter/src/worker/capture.rs |
Passes the template gateway into clone activation. |
tests/runtime-adapter/src/worker/activation.rs |
Applies record publication settings and covers network failures. |
tests/runtime-adapter/src/worker.rs |
Supervises the announcer and fails clones when it exits. |
tests/runtime-adapter/src/template.rs |
Parses and validates template gateway tokens. |
tests/runtime-adapter/src/clone.rs |
Documents per-clone policies at the template’s address. |
tests/runtime-adapter/src/capture.rs |
Documents policy-independent capture classes. |
tests/runtime-adapter/src/bin/runtime-adapter-host.rs |
Updates template and clone CLI documentation. |
tests/runtime-adapter/src/bin/runtime-adapter-guest.rs |
Forwards the parsed gateway to template capture. |
tests/runtime-adapter/scenarios.txt |
Registers shared-policy and busy-forward scenarios. |
nvx-rs/tests/vectors/template-key-v1.txt |
Updates the network class vector for deny-all capture. |
nvx-rs/tests/vectors/README.md |
Documents revised key vectors and the forward-error excerpt. |
nvx-rs/tests/vectors/openvmm/port_forward_socket_creation.rs |
Pins OpenVMM’s socket-creation error text. |
nvx-rs/tests/openvmm_vectors.rs |
Checks the new excerpt against OpenVMM. |
nvx-rs/tests/guest_tools.rs |
Tests inherited-socket validation and ingress usage errors. |
nvx-rs/src/protocol/status.rs |
Documents announcer failures under network status 57. |
nvx-rs/src/protocol/cmdline/tests.rs |
Tests strict gateway parsing. |
nvx-rs/src/protocol/cmdline.rs |
Adds gateway parsing through shared address-token logic. |
nvx-rs/src/host/template/store/tests.rs |
Updates unsupported key-version coverage. |
nvx-rs/src/host/template/store/capture.rs |
Compares capture networks by their template NIC arguments. |
nvx-rs/src/host/template/plan/tests.rs |
Tests deny-all captures across supported policies. |
nvx-rs/src/host/template/plan.rs |
Separates template NIC arguments and removes policy restrictions. |
nvx-rs/src/host/template/mod.rs |
Updates compatibility and encoding documentation. |
nvx-rs/src/host/template/key.rs |
Removes clone policy arguments from keys and exposes NIC addressing. |
nvx-rs/src/host/template/clone/tests/restore.rs |
Tests forward-bind failure attribution during restore. |
nvx-rs/src/host/template/clone/tests/launch.rs |
Tests replacement-policy arguments and simulates forward failures. |
nvx-rs/src/host/template/clone/tests.rs |
Tests address compatibility and clone-specific activation records. |
nvx-rs/src/host/template/clone/records.rs |
Retains forward-bind causes from OpenVMM’s fatal error chain. |
nvx-rs/src/host/template/clone/record.rs |
Builds network activation settings from the clone’s policy. |
nvx-rs/src/host/template/clone/launch.rs |
Emits --restore-network-policy for networked clones. |
nvx-rs/src/host/template/clone/failure.rs |
Attributes forward socket-creation failures to the host. |
nvx-rs/src/host/template/clone.rs |
Validates NIC compatibility and retains each clone’s network. |
nvx-rs/src/host/sandbox/plan.rs |
Adds template NIC lowering and policy-based DNS selection. |
nvx-rs/src/host/network/mod.rs |
Adds deny-all capture arguments, publication detection, and DNS selection. |
nvx-rs/src/guest/sys/socket.rs |
Validates inherited IPv4 datagram sockets and exposes bound ports. |
nvx-rs/src/guest/mod.rs |
Documents the guest announcer API. |
nvx-rs/src/guest/ingress/sender.rs |
Supports sending announcements through an inherited socket. |
nvx-rs/src/guest/ingress/mod.rs |
Adds inherit-mode dispatch and shared privileged-port binding. |
nvx-rs/src/guest/ingress/announcer.rs |
Implements announcer startup, supervision handles, and cleanup. |
nvx-rs/src/guest/clone.rs |
Documents announcer activation, ownership, and teardown. |
Review effort: Balanced
There was a problem hiding this comment.
🔵 Needs a closer look
Changes to host/guest network isolation and privileged listener publication require maintainer confirmation of restore-policy enforcement and cross-backend VM validation, which was unavailable in the source-only review.
No open issues 🎉
Review effort: Balanced
7d391f4 to
1954de6
Compare
There was a problem hiding this comment.
🔵 Needs a closer look
The changes cross host/VMM/guest isolation and privileged listener-publication boundaries, requiring maintainer confirmation that the pending KVM, MSHV, and WHP VM scenarios pass on the exact PR head.
No open issues 🎉
Review effort: Balanced
2dd7ce6 to
02b92e2
Compare
1954de6 to
341dce9
Compare
02b92e2 to
f87f1f1
Compare
… policy A template's NIC was captured under its network's own policy, and each restore passed that policy unchanged, so a template served the clones of one network policy alone, and refused ingress and host-loopback access both ways, whose published listeners no snapshot holds. A template's capture now attaches its NIC under OpenVMM's complete deny-all policy, whatever its network's own: egress denied without rules, ingress and host-loopback access denied, without a proxy or forwards. It is the one policy of a snapshot that OpenVMM lets a restore replace, so each clone's restore passes `--restore-network-policy` with the options of the clone's own policy, without `--net` and the guest's address, which OpenVMM takes from the snapshot. Ingress, host-loopback access both ways, and forwards become admissible, within OpenVMM's checks. The policy leaves the template's identity. The key no longer holds the network's lowering: it keeps what the capture made, whether there is a NIC, and the NIC's addresses and the identity of its deny-all policy. A capture class encodes its network as the template captures it, so the classes of networks at one guest address are one, and `ClonePlan` checks only that a clone's network attaches a NIC exactly when the template has one, at the template NIC's guest address. The encodings of a key and a class stay at version 1, which has not been released: the version-1 vectors change in place, and a template store that an earlier build created is deleted and created again. The clone's activation record names its own network: the identity of the network, the domain-separated SHA-256 digest of its serde form, computed once per launch; the DNS server that OpenVMM serves the guest under its policy, which NVX's copy of OpenVMM's rule finds with the IPv6 gateway of the template's NIC, since the template's guest learned none; listeners published under a policy that allows ingress or host-loopback access both ways; and the proxy's URL. Part of #850 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b459655f-e7a2-4f19-b0b1-9feda7428534
A clone's VMM binds the host-loopback port of each forward of the clone's own network as it restores the NIC, before its restore-ready event. When another socket holds the port, OpenVMM's VM worker fails to launch with a fatal error that carries no code, so the launch recorded a lost status, of unknown outcome. Once a clone's VMM exited before its restore-ready event, the launch now looks for the cause of OpenVMM's error chain that names a port forward whose socket it cannot create, after the line that leads the fatal error, appends that cause to the line that the failure's message reports, and attributes the failure to the host with the forward-bind evidence. A successful launch does nothing more: no bind, no system call, and no wait. The text is OpenVMM's alone, so a third excerpt of OpenVMM's sources, the SocketCreation error of net_consomme's resolver, pins it, which the template tests read and tests/openvmm_vectors.rs compares with the submodule. #972 asks OpenVMM for a stable code instead. Part of #850 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b459655f-e7a2-4f19-b0b1-9feda7428534
…steners A template captures its NIC under a policy that publishes nothing, so its init starts no announcer of the guest's listeners. A clone that restores the NIC under a policy of its own, which publishes the workload's listeners, then needs one, which nothing started. The clone's application now starts it, after the resolver file and before the runtime's binds, if the record's network publishes the workload's listeners: `nvx::guest::ingress::Announcer::start` binds an IPv4 UDP socket to the highest free privileged source port, from which alone OpenVMM's gateway takes announcements, and spawns `/sbin/nvx-ingress inherit GATEWAY_IPV4`, in a session of its own, with that socket as its standard input. It waits only until the program runs. The application takes the gateway from the template's kernel command line, through the new `nvx::protocol::cmdline::gateway`, as the caller's `RootTemplate::gateway`. A template without a gateway, a socket that cannot be bound, and a program that does not run fail the application with a network error, ACTIVATION_NETWORK, once it tore the root down. `Applied` holds the announcer, whose pidfd its caller polls, and stops it, with SIGKILL, before the rest of its teardown. `nvx-ingress inherit GATEWAY_IPV4` announces from the socket that it inherited, which must be an IPv4 datagram socket bound to a privileged source port from 512 to 1023, rather than from one of its own. Its other modes are unchanged. A clone whose network publishes nothing starts no process and pays nothing more. The test adapter gives the application no gateway yet: until the next commit, its clones whose records publish the workload's listeners fail their application with ACTIVATION_NETWORK. Part of #850 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b459655f-e7a2-4f19-b0b1-9feda7428534
…does A clone's executions entered the root with listeners published, the entry's default, whatever the clone's network, since every clone had the template's. A template now serves networks of every policy at its NIC's address, so the worker takes the publication of the record's network instead: published under a policy that publishes the workload's listeners, which refuses CAP_NET_BIND_SERVICE and CAP_NET_RAW, and unpublished otherwise, a clone without a network included. The worker reads the template's IPv4 gateway from its kernel command line, in `template::boot`, and hands it to the clone's application in `worker::capture::root_template`, so that the application starts the announcer of the listeners that the record's network publishes. A gateway that OpenVMM would not render ends the template's boot with the command line's status, 20. The worker then polls the announcer's pidfd with the session's stream, and once the announcer ended, before the clone was ready or after, ends the clone with ACTIVATION_NETWORK, 57, as one whose announcer could not start. The stop's teardown stops it first. The host program's and the libraries' documentation now describe a template's NIC under the complete deny-all policy, which each clone's restore replaces with the policy of the clone's own network. Part of #850 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b459655f-e7a2-4f19-b0b1-9feda7428534
…in VMs The VM scenarios of templates and clones tested the interim rule, under which each network policy had a template of its own, and templates refused ingress and host-loopback access both ways. vm_template now creates one template of a network that publishes the guest's listeners and forwards a port: its key's command line names the NIC's addresses, but neither a DNS server nor a publication, and the creation of each other network at the same guest's address serves it without a capture. vm_clone starts the clones of every network policy from one template: each restore installs the clone's own policy, and the clone's record names its DNS server, proxy, and publication. Through listeners of the host on host-loopback ports that the host chose, as in #958's traffic checks, a clone reaches what its policy allows alone. The host reaches a listener of the guest at its host-loopback port exactly with host-loopback access both ways, once the clone's announcer announced it, and through a forward, and the guest refuses CAP_NET_BIND_SERVICE and CAP_NET_RAW exactly while the listeners are published. A forward whose host port another socket holds ends the clone before its restore-ready event, as a failure of the host, by the evidence of the forward's bind. The resolver and proxy scenarios now use templates of other networks than their clones'. Part of #850 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b459655f-e7a2-4f19-b0b1-9feda7428534
341dce9 to
dba01f0
Compare
There was a problem hiding this comment.
🔵 Needs a closer look
Human review is needed to confirm that per-clone policy replacement preserves isolation across supported backends when OpenVMM’s pre-vCPU enforcement depends on privileged guest announcements.
No open issues 🎉
Review effort: Balanced
Chain: #951 → #954 → #957 → #959 → #960 → #955 → #958 → #962 → #963 → #925 → #926 → this PR, on
dev; #879, #881, #885, #886, #877, #932, #884, #887, #933, #880, #936, #937, #939, #940, #941, #888, #949, and #950 have merged.Stacked on #926.
Fixes #850.
What and why
Until now, a template's NIC carried the policy of its clones:
OpenVMM lets a platform restore install a new policy on a NIC captured under the complete deny-all policy (#536, in
dev's OpenVMM pin). With this PR:It is one PR because a clone that publishes its listeners is correct only together with the announcer that publishes them.
Host:
nvx::host::templateCapture:
SandboxNetwork::template_argumentslowers a network to the NIC that its template captures:--netat the guest's address, under the complete deny-all policy (--network-profile portable --network-egress deny --network-ingress deny --host-loopback deny), whatever the network's own policy.Class and key:
template-key-v1.txtchanges in place.Clone plan:
ClonePlan::newrequires the clone's network to be at the guest's address of the template's NIC, under a policy of its own. Before any VMM starts, it refuses, with the failurerequest, invalid-settings, plan:Restore: the restore's device arguments replace
--netand the guest's address with--restore-network-policy, followed by the options of the clone's policy. The clone's policy therefore replaces the template's deny-all policy on the NIC before any restored processor runs.Activation record: the record names the clone's own network:
activation::Identity);network::dns_server, OpenVMM's rule: the first gateway whose DNS service the egress policy lets the guest reach);Under the deny-all policy, OpenVMM writes neither
virtnet_dns=norvirtnet_ingress=on the template's command line. A clone's guest therefore learns its DNS server and publication from the record alone.Busy forwards:
failed to create socket for port forward (Tcp 127.0.0.1:PORT). The clone then fails as the host's: outcomehost, evidenceforward-bind, at the restore.nvx-rs/tests/vectors/openvmm/port_forward_socket_creation.rspins the text, andopenvmm_vectorschecks it against the pinned OpenVMM.Guest:
nvx::guest::cloneandnvx::guest::ingressclone::applystarts the announcer of the workload's listeners when the record publishes them (ingress::Announcer):/sbin/nvx-ingress inherit GATEWAYwith that socket as its standard input, and waits only until the launch report says that the program runs.virtnet_gw=token, whichprotocol::cmdline::gatewayreads as strictly asvirtnet_dns=.nvx-ingress inherit GATEWAYis a new mode. It takes its socket from its standard input and refuses anything but an IPv4 datagram socket bound to a privileged port. The cold-boot form,nvx-ingress GATEWAY, is unchanged.ApplyError::Network(NetworkError::GatewayorNetworkError::Announcer), status 57.Appliedkeeps the announcer and its pidfd. The teardown stops it first, as a drop does.Runtime adapter
nvx::guest::clone::apply.Entry::checktherefore refuses an execution whose identity holdsNET_BIND_SERVICEorNET_RAW.createtakes a network of any policy, and the creations of networks with a NIC at one guest's address share one template.starttakes a network at the template's guest's address, under any policy. The README describes templates and clones of every policy, the announcer, and statuses 20 and 57.Tests
nvx-rs unit tests:
network:templates_attach_their_nic_under_the_complete_deny_all_policy;the_dns_server_is_the_first_gateway_whose_dns_service_the_policy_reaches.template::plan: every capture now covers policies with ingress, both-way host loopback, and a forward, each attaching its NIC under the deny-all policy.template::key:a_key_that_holds_its_network_s_lowering_after_its_invariants_is_refused. It replaces the tests of a key that holds its network or names a DNS server.template::clone:templates_serve_the_networks_of_every_policy_at_their_nics_address_alone;the_activation_record_of_a_clone_of_a_template_with_a_nic_names_its_own_network;a_clone_of_a_template_with_a_nic_restores_its_own_policy_in_place_of_the_templates;openvmm_s_fatal_error_is_evidence_of_a_forward_bind_when_it_names_a_forward_it_cannot_bind;the_cause_that_names_a_forward_that_openvmm_cannot_bind_follows_the_fatal_line.guest::ingress:an_inherited_socket_is_taken_only_at_a_privileged_source_port;guest::sys::socket:an_inherited_descriptor_is_taken_only_as_an_ipv4_datagram_socket.guest::clone::activation::apply:a_network_that_publishes_the_workload_s_listeners_starts_their_announcer_to_its_gateway. The order, teardown, and failure tests now include the announcer: it starts after the resolver file and before the binds, and a start failure, or a missing gateway, fails the activation with the network's error.protocol::cmdline:the_gateway_is_one_ipv4_address_in_one_token_as_openvmm_renders_it.guest_tools:ingress_inherits_only_an_ipv4_datagram_socket_bound_to_a_privileged_source_port.openvmm_vectors: the forward's excerpt at the pinned OpenVMM.Runtime adapter, without VMs:
template:template_mode_refuses_a_gateway_that_openvmm_would_not_render.guest_session:a_clone_s_executions_enter_the_root_with_the_publication_of_its_record_s_network;a_clone_whose_announcer_ends_ends_with_the_network_s_status.Runtime adapter VM scenarios (
scenarios.txt, now 154):vm_templatethe_networks_of_every_policy_at_one_guest_s_address_share_one_template:an_egress_network_has_its_nic,an_allow_rule_has_its_nic,outbound_host_loopback_has_its_nic,a_proxy_only_network_has_its_nic,ingress_is_refused_before_any_vm_starts, andhost_loopback_both_ways_is_refused_before_any_vm_starts.vm_cloneone_template_serves_the_clones_of_every_network_policy_at_its_guest_s_addressreplacesa_template_serves_the_clones_of_its_own_network_policy_alone. One template serves clones of seven policies: egress, an allow rule, outbound host loopback, a proxy, ingress, both-way host loopback, and a forward. For each clone, it checks:--restore-network-policyarguments;NET_BIND_SERVICEorNET_RAWfails to launch exactly while the listeners are published.The records' policy identities all differ. Before any VMM starts, the plan refuses a network at another address, one that lets nothing through, and none.
vm_clonea_forward_whose_port_another_socket_holds_fails_the_clone_as_the_host_s:host, forward-bind, restore, spawned, none, exited. It quarantines nothing, and the lease is released.The DNS and proxy scenarios now restore clones of other policies than their templates':
Replaced tests
The interim rule of one template per network policy and the key's network part go, so these base tests and rows go, each replaced:
host::network:an_attached_lowering_attaches_the_device_of_a_policy_that_lets_nothing_through→templates_attach_their_nic_under_the_complete_deny_all_policy.template::key:keys_read_back_the_network_of_each_policyanda_key_holds_the_network_that_its_class_lowered, with the network checks ofother_encodings_of_a_key_are_refused→a_key_that_holds_its_network_s_lowering_after_its_invariants_is_refusedand the version-1 vectors.template::key:a_key_names_the_dns_server_that_its_command_line_names→network::the_dns_server_is_the_first_gateway_whose_dns_service_the_policy_reaches.template::clone:networks_that_the_template_does_not_have_are_refused→templates_serve_the_networks_of_every_policy_at_their_nics_address_alone;the_activation_record_of_a_clone_of_a_template_with_a_nic_names_its_policy_and_dns_server→the_activation_record_of_a_clone_of_a_template_with_a_nic_names_its_own_network;a_clone_of_a_template_with_a_nic_restores_its_policy_without_its_address→a_clone_of_a_template_with_a_nic_restores_its_own_policy_in_place_of_the_templates.vm_template:an_egress_network_has_its_nic,an_allow_rule_has_its_nic,outbound_host_loopback_has_its_nic,a_proxy_only_network_has_its_nic,ingress_is_refused_before_any_vm_starts, andhost_loopback_both_ways_is_refused_before_any_vm_starts→the_networks_of_every_policy_at_one_guest_s_address_share_one_template.vm_clone:a_template_serves_the_clones_of_its_own_network_policy_alone→one_template_serves_the_clones_of_every_network_policy_at_its_guest_s_address.Guide changes (#591's numbering)
At 950c1fe, the guide already describes deny-all templates and new policies at restore. These refinements are not made in #591; they are listed here for it:
--restore-network-policyand its options, in place of--netand the address, which the snapshot fixes.nvx-ingress inherit GATEWAY). A cold boot is unchanged.forward-bind).NET_BIND_SERVICEorNET_RAWis refused.Not in this PR
Validation
At this PR's head dba01f0, the five commits of the earlier head 341dce9, restacked onto #926's head f87f1f1, which sits on #963 rebased onto
devat f8469b8. One commit needed a resolution: #1015 rewrote the module docs ofnvx-rs/src/protocol/cmdline.rsfor the tokens that the guest's init reads, and this PR adds the IPv4 gateway andvirtnet_gw=to the same lines; the rebase keeps both. With that resolution, this PR and the one above it, merged intodevat 8f3a840, passed CI (run 40938392497). 341dce9 held the same five commits as the earlier head 1954de6; the rest of this section describes 1954de6, on #926's earlier head 2dd7ce6.At the head, 1954de6, on #926's 2dd7ce6. The rebase adapted this PR's announcer tests to their new base: they pass its activation deadline as
None, and sendStartonly afterReady, as its activation requires.Windows, at each commit:
openvmm_vectors, ignored tests included, against the pin, 832d5bb4.x86_64-unknown-linux-musl; the check of its scenario list.-D warnings; MSRV 1.89.validate-nvx, and the adversarial and host-connection tests.Linux, on an MSHV host, at each commit:
openvmm_vectors, ignored tests included;-D warnings;validate-nvx, and the adversarial, host-connection, and Specula tests.The adapter's VM suites at the head:
devor in this chain, changes the kernel's inputs or the image's.CI: one manual full run of
ci.ymlon this branch at 1954de6, run 40866679824.Guests and sandboxesjob ran all 154 adapter scenarios.Review: