Skip to main content

Egress policy: fluxvm-network vs. the userspace gateway

Two enforcement points decide what a guest may reach:

WhereTypePlatform
fluxvm-networkhost kernel (nftables / eBPF TC)dataplane::VmNetworkPolicyLinux
fluxvm-usernetuserspace NAT gateway of apple.usernetEgressPolicyany (macOS first)

fluxvm-network pulls in eBPF, nftables and an HTTP proxy stack and fluxvm-usernet deliberately depends on none of it, so the two types are not unified into one Rust type. Instead fluxvm_apple::usernet_policy converts a VmNetworkPolicy document (it reads the JSON form through a serde mirror, NetworkPolicyView) into an EgressPolicy, reporting what could not be carried over. fluxvm-network's behaviour is unchanged.

let c = fluxvm_apple::usernet_policy::egress_from_network_policy(&view)?;
cfg.egress = c.egress;
cfg.icmp_proxy = c.icmp_proxy;
// c.notes: differences that do not weaken the policy; c.unsupported: features that would be weaker.

Models​

VmNetworkPolicy. Enforcing when allow_cidrs, allow_ports is non-empty or default_allow is false; otherwise everything is allowed (including private ranges). When enforcing, a packet must match the CIDR dimension (if set) and the L4 dimension (if set), deny_cidrs always wins, ICMP needs allow_icmp. Ports are proto/port pairs.

EgressPolicy. Order: non-unicast, deny_cidrs, deny_ports, allow_ports (a miss denies), then explicit allow (allow_cidrs, or an address learned from a DNS answer for allow_domains), then private ranges denied unless allow_private, then any configured allow list denies the rest. With no lists the default is "public internet only". Ports are protocol-agnostic. Domains also gate DNS itself (NXDOMAIN).

Conversion rules​

VmNetworkPolicyEgressPolicy / UserNetConfigNotes
allow_cidrs (v4) / (v6)allow_cidrs / allow_cidrs6/prefix required on input
deny_cidrsdeny_cidrs / deny_cidrs6
allow_ports tcp/N, udp/Nallow_ports (N, deduplicated)protocol is lost: tcp/443 + udp/53 allows 443 and 53 for both; reported in notes when the tcp and udp sets differ
allow_ports icmp/*, icmp6/*, allow_icmpicmp_proxy = trueicmp_proxy is also true when the source is not enforcing
allow_ports sctp/Ndroppedthe gateway never forwards SCTP; reported in notes
allow_fqdns name, *.suffixallow_domainsthe gateway matches the name and everything under it, and learns addresses at DNS time (the kernel path resolves once at apply time and skips wildcards)
allow_fqdns other patternsdroppednarrower, reported; if nothing usable remains and there are no CIDRs the result is deny-all (0.0.0.0/0, ::/0 in deny_cidrs)
default_allow = false, no listsdeny_cidrs = 0.0.0.0/0, ::/0deny-all
(always)allow_private = truefluxvm-network never filters private ranges on its own; an allow list denies them by omission on both sides. Tighten to false for a hardened gateway
entities, deny_udp, max_egress_mbps, max_egress_ppsnot representablelisted in unsupported: the converted policy is weaker
audit_modeenforcedgateway has no log-and-allow; stricter, noted
groups, labelsn/amerged by fluxvm-network before conversion

CIDR and port dimensions combine as AND in both models, so allow_cidrs x allow_ports (the nftables cross product) is exactly allow_cidrs AND allow_ports in the gateway.

Gateway-only features​

deny_ports, deny_domains, the default private-range deny, allow_host_loopback and the per-flow DNS learning have no counterpart in VmNetworkPolicy; they are set from apple.usernet directly.

Verification​

Unit tests in crates/fluxvm-apple/src/usernet_policy.rs evaluate the converted policy with EgressPolicy::check. Nothing compares the two enforcers against the same live traffic.