Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions doc/design/1.5-compatibility-and-versioning-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,14 @@ activation ranges. Layer-role presence and device geometry stay fixed: a
platform template accepts a substitute layer only with the same recorded
logical length and block sizes.

A template's compatibility key also records its network, as OpenVMM's
arguments to which its class lowers it: the guest's address and the policy of
its NIC, which every restore of the template passes unchanged, so a template
serves the clones of its own network policy alone. The key's encoding, version
1, includes that network and has not been released, so a template store that
an earlier build created is not supported: delete it and create a new one.
There is no migration.

## 1.5.3 Persisted State

The snapshot manifest is authoritative for a restored process. Native file
Expand Down
74 changes: 40 additions & 34 deletions nvx-rs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -589,17 +589,23 @@ other `Invariants`.

`TemplateKey::new` checks that the capture made the machine that its class
requested, and derives the template's compatibility key, from metadata alone:
the runtime and contract versions through which its clones are restored, and the
the runtime and contract versions through which its clones are restored, the
backend, CPU profile and effective CPUID, topology, rates, resources, kernel
command line, disks, and devices of its machine. A capture's own metadata, such
as its time anchor and host, stays out of the key.
command line, disks, and devices of its machine, and its network, as its class
lowered it to OpenVMM's arguments, the guest's address and the policy of its
NIC, which every restore of the template passes unchanged, so that a template
serves the clones of its network policy alone. A capture's own metadata, such as
its time anchor and host, stays out of the key.

A key and a class each have one canonical, versioned encoding, whose
domain-separated SHA-256 digest is its identity, a `KeyIdentity`, which converts
to and from the `nvx::activation::Identity` by which a clone's activation record
names its template's key, or a `ClassIdentity`; the
[conformance vectors](tests/vectors/README.md) of version 1,
`tests/vectors/template-key-v1.txt`, pin the key's encoding and identity.
`tests/vectors/template-key-v1.txt`, pin both encodings and identities. The
key's encoding, version 1, includes the template's network and has not been
released, so a template store that an earlier build created is not supported:
delete it and create a new one. There is no migration.

`Store` keeps the generations of templates that a host published, apart from
every sandbox's store, at a location that the adapter chooses: a private
Expand Down Expand Up @@ -648,32 +654,31 @@ and no launch.
`ClonePlan::new` plans a clone under the lock that acquired its lease, before
the launch that the lease reserves, from metadata alone: the leased generation,
its key, which must speak the current contracts, the boot and control contracts
of the runtime that captured it included, and its snapshot; a network exactly
when the template has a NIC, on the guest network of that NIC, whose address
OpenVMM restores from the snapshot, and under the policy of that NIC, which
OpenVMM checks as it restores it, since the key names the policy only by
OpenVMM's identity of it; a memory target in whole blocks of `MEMORY_BLOCK_SIZE`
from the template's RAM to its capacity; the clone's image, which the check of
its `DistroBinding` opens; its upper, either the caller's scratch image, which
the check of its `ScratchBinding` claims, and which nothing formats, copies, or
reads, or for an upper in RAM a fresh sparse file, `clone-scratch.raw` in the
sandbox's directory, which the release removes; its export, if it has one, an
`ExportPlan` that `filesystem::plan` made for the container root, which must
export exactly one directory, since the template's dormant filesystem slot binds
one directory and no file, and which must neither overlap the template store, by
the paths that the plan and the store's directory resolve to, nor let the guest
write in a directory through which the store's path leads, links included, since
the host reaches the store, and OpenVMM its snapshots, by that path, nor be or
hold the directory of the clone's sandbox, in which the host writes the clone's
records, checks of paths that leave two rules to whoever names the export, as
they leave the directories above the store: the export must not reach the store
through an alias of it that an administrator configures, such as a bind mount or
a mount point of the store, or of a directory above or inside it, a loopback
share, or a hard link of a payload, nor lie inside a directory that a writable
export of another sandbox or clone holds, since OpenVMM resolves an export's
path again at each launch and restore, so that the directories above the
export's directory must be ones that no guest may change, as those above a
clone's block devices must; and a fresh capability.
of the runtime that captured it included, and its snapshot; the template's
network exactly, as its key records it, or none for a template without a NIC, so
that a template serves the clones of its network policy, at its guest's address,
which OpenVMM restores from the snapshot, alone; a memory target in whole blocks
of `MEMORY_BLOCK_SIZE` from the template's RAM to its capacity; the clone's
image, which the check of its `DistroBinding` opens; its upper, either the
caller's scratch image, which the check of its `ScratchBinding` claims, and
which nothing formats, copies, or reads, or for an upper in RAM a fresh sparse
file, `clone-scratch.raw` in the sandbox's directory, which the release removes;
its export, if it has one, an `ExportPlan` that `filesystem::plan` made for the
container root, which must export exactly one directory, since the template's
dormant filesystem slot binds one directory and no file, and which must neither
overlap the template store, by the paths that the plan and the store's directory
resolve to, nor let the guest write in a directory through which the store's
path leads, links included, since the host reaches the store, and OpenVMM its
snapshots, by that path, nor be or hold the directory of the clone's sandbox, in
which the host writes the clone's records, checks of paths that leave two rules
to whoever names the export, as they leave the directories above the store: the
export must not reach the store through an alias of it that an administrator
configures, such as a bind mount or a mount point of the store, or of a
directory above or inside it, a loopback share, or a hard link of a payload, nor
lie inside a directory that a writable export of another sandbox or clone holds,
since OpenVMM resolves an export's path again at each launch and restore, so
that the directories above the export's directory must be ones that no guest may
change, as those above a clone's block devices must; and a fresh capability.

It builds the clone's activation record, `ClonePlan::activation`, which the
clone's guest applies in place of the configuration that the template captured,
Expand Down Expand Up @@ -710,10 +715,11 @@ writes the clone's activation record in its canonical encoding,
`clone-activation.bin`, from which a caller resends it unchanged; it listens for
the VMM's restore-ready event, at the socket `restore.sock` on Linux, and at a
named pipe of a random name on Windows; and it runs `supervisor::launch` with
OpenVMM's restore of the clone's devices, its export bound to the dormant slot,
`--mount` with the directory, and its hidden, re-exposed, and writable paths and
its owner, `--restore-snapshot` with the generation's snapshot in place, which
no clone copies or changes, so that clones of one generation restore it at once,
OpenVMM's restore of the clone's devices and of the template's network, as its
key records it, unchanged, its export bound to the dormant slot, `--mount` with
the directory, and its hidden, re-exposed, and writable paths and its owner,
`--restore-snapshot` with the generation's snapshot in place, which no clone
copies or changes, so that clones of one generation restore it at once,
`--restore-memory` with the memory target, and `--restore-ready-path`, under the
template's backend, CPU profile, and processors, with a boot console that
discards its output, as cold boots have, and the control console, but no kernel,
Expand Down
27 changes: 26 additions & 1 deletion nvx-rs/src/host/network/arguments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,30 @@ pub fn openvmm_arguments(
let Some(policy) = policy else {
return Ok(Vec::new());
};
lower_policy(policy, guest_network, false)
}

/// Returns the OpenVMM options of `policy` for a guest at `guest_network`, as
/// [`openvmm_arguments`] renders them, but those of an attached network device for a policy that
/// lets nothing through too, for which [`openvmm_arguments`] attaches none: the options of the
/// same policy with one more allow rule that lowers to no OpenVMM rule, such as one that excepts
/// its whole destination, and which [`openvmm_arguments`] renders with a device.
///
/// Fails as [`openvmm_arguments`] does.
pub(crate) fn attached_arguments(
policy: &NetworkPolicy,
guest_network: &str,
) -> Result<Vec<String>> {
lower_policy(policy, guest_network, true)
}

/// Returns the OpenVMM options of `policy` for a guest at `guest_network`, as
/// [`openvmm_arguments`] renders them, and, if `attached`, as [`attached_arguments`] does.
fn lower_policy(
policy: &NetworkPolicy,
guest_network: &str,
attached: bool,
) -> Result<Vec<String>> {
if let Some(proxy) = &policy.proxy {
return proxy_arguments(policy, proxy, guest_network);
}
Expand All @@ -62,7 +86,8 @@ pub fn openvmm_arguments(
shorter, which leaves an address at which OpenVMM presents host peers to the guest"
)));
}
if policy.egress == Access::Deny
if !attached
&& policy.egress == Access::Deny
&& policy.allow.rules.is_empty()
&& policy.ingress == Access::Deny
&& policy.host_loopback == HostLoopback::Deny
Expand Down
2 changes: 0 additions & 2 deletions nvx-rs/src/host/network/loopback.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ use std::collections::HashSet;
use super::address::Family;
use super::arguments::guest_subnet;
use super::lowering::Rule;
#[cfg(feature = "cli")]
use super::rule::parse_port;
use super::{Access, MAX_LOOPBACK_FORWARDS, NetworkPolicy};
use crate::error::{Error, Result};
Expand Down Expand Up @@ -86,7 +85,6 @@ impl LoopbackForward {
///
/// Fails with [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported) and a message that
/// starts with `value`.
#[cfg(feature = "cli")]
pub(crate) fn parse(value: &str) -> Result<Self> {
let refused = |reason: &str| Err(Error::unsupported(format!("{value}: {reason}")));
let fields: Vec<&str> = value.split(':').collect();
Expand Down
1 change: 1 addition & 0 deletions nvx-rs/src/host/network/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ pub mod policy;
mod proxy;
mod rule;

pub(crate) use self::arguments::attached_arguments;
pub use self::arguments::openvmm_arguments;
pub use self::loopback::{ForwardProtocol, HostLoopback, LoopbackForward};
pub use self::proxy::{NetworkProxy, ProxyScheme};
Expand Down
1 change: 0 additions & 1 deletion nvx-rs/src/host/network/proxy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,6 @@ impl NetworkProxy {
///
/// Fails with [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported) and a message that
/// starts with `endpoint`.
#[cfg(feature = "cli")]
pub(crate) fn parse_endpoint(endpoint: &str, gateway: Ipv4Addr) -> Result<Self> {
let refused = |reason: String| Err(Error::unsupported(format!("{endpoint}: {reason}")));
let Some((address, port)) = endpoint
Expand Down
7 changes: 1 addition & 6 deletions nvx-rs/src/host/network/rule.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,7 @@
//! the parsing of the rules of an action into the rules that the lowering takes.

use super::lowering::{self, PortValue, Rule, RuleProtocol};
#[cfg(feature = "cli")]
use crate::error::Error;
use crate::error::Result;
#[cfg(feature = "cli")]
use crate::error::{Error, Result};
use crate::net::cidr;

/// Allow or deny decision of a network posture.
Expand Down Expand Up @@ -141,7 +138,6 @@ impl EgressRule {
///
/// Fails with [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported) and a message that
/// starts with `value`.
#[cfg(feature = "cli")]
pub(crate) fn parse_native(value: &str) -> Result<Self> {
let refused = |reason: &str| Err(Error::unsupported(format!("{value}: {reason}")));
let (destination, selector) = split_native_rule(value);
Expand Down Expand Up @@ -203,7 +199,6 @@ impl EgressRule {
/// splits it: an IPv4 address holds no `:`, so its first `:` starts the selector; a network with a
/// prefix ends at the first `:` after its `/`; and an IPv6 address without a prefix ends at the
/// last `:` that leaves an IPv6 address before it, since protocol names are not hexadecimal.
#[cfg(feature = "cli")]
fn split_native_rule(value: &str) -> (&str, Option<&str>) {
let split_at = |index: usize| (&value[..index], Some(&value[index + 1..]));
if let Some(slash) = value.find('/') {
Expand Down
37 changes: 37 additions & 0 deletions nvx-rs/src/host/network/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,43 @@ fn rules(arguments: &[String], option: &str) -> Vec<String> {
.collect()
}

#[test]
fn an_attached_lowering_attaches_the_device_of_a_policy_that_lets_nothing_through() {
// An allow rule that excepts its whole destination lowers to no OpenVMM rule, but a policy
// with one attaches its device, closed, as the attached lowering of the same policy
// without that rule renders it.
let excluded = EgressRule {
to: vec![Destination {
cidr: "192.0.2.0/24".to_owned(),
except: vec!["192.0.2.0/24".to_owned()],
}],
ports: Vec::new(),
};
let denied = NetworkPolicy::new(Access::Deny).with_deny(EgressRule::to("1.2.3.4"));
for closed in [NetworkPolicy::new(Access::Deny), denied] {
assert!(
openvmm_arguments(Some(&closed), "10.0.0.2/24")
.unwrap()
.is_empty()
);
let excepted = closed.clone().with_allow(excluded.clone());
let attached = attached_arguments(&closed, "10.0.0.2/24").unwrap();
assert_eq!(
attached,
openvmm_arguments(Some(&excepted), "10.0.0.2/24").unwrap()
);
assert_eq!(attached[..2], ["--net", "10.0.0.2/24"]);
}
// The attached lowering of any other policy is its lowering.
let proxied = NetworkPolicy::proxy_only(NetworkProxy::new(3128));
for policy in [NetworkPolicy::new(Access::Allow), proxied] {
assert_eq!(
attached_arguments(&policy, "10.0.0.2/24").unwrap(),
openvmm_arguments(Some(&policy), "10.0.0.2/24").unwrap()
);
}
}

#[test]
fn default_postures_map_to_portable_profile_options() {
assert!(openvmm_arguments(None, "10.0.0.2/24").unwrap().is_empty());
Expand Down
Loading
Loading