Normalize Softnet default-route isolation

This commit is contained in:
Fedor Korotkov 2026-07-21 14:04:36 -04:00
parent 6fe0db016d
commit 50628e08fc
3 changed files with 36 additions and 9 deletions

View File

@ -57,4 +57,4 @@ The supported methods are `softnet.policy.get` and `softnet.policy.set`. A compl
Every request must include a non-null string (at most 256 bytes) or non-negative integer `id`; notifications are rejected so policy changes always have an acknowledgment. Policy updates are atomic: all targets are parsed and a new prefix map is built before the active policy changes. Longest-prefix matching and block precedence for identical prefixes are preserved. Targets are normalized and deduplicated, so retrying the active revision with the same policy is idempotent; reusing it with a different policy or retrying a superseded revision returns a JSON-RPC conflict error. A policy may contain at most 4096 combined allow/block targets, and a request frame may not exceed 1 MiB.
`--allow=0.0.0.0/0` additionally disables vmnet bridge isolation during interface creation. Bridge isolation cannot be changed for a running VM, so policy updates that would toggle it are rejected without changing the active policy. Use `block=["0.0.0.0/0"]` with specific allow targets for a default-deny policy. Closing the control socket leaves the last accepted policy active.
An allow target that normalizes to `0.0.0.0/0` additionally disables vmnet bridge isolation during interface creation. Bridge isolation cannot be changed for a running VM, so policy updates that would toggle it are rejected without changing the active policy. Use `block=["0.0.0.0/0"]` with specific allow targets for a default-deny policy. Closing the control socket leaves the last accepted policy active.

View File

@ -8,7 +8,7 @@ use jsonrpsee_types::{
METHOD_NOT_FOUND_CODE as METHOD_NOT_FOUND, PARSE_ERROR_CODE as PARSE_ERROR,
},
};
use prefix_trie::{Prefix, PrefixMap};
use prefix_trie::PrefixMap;
use serde::Deserialize;
use serde_json::{Value, json};
use smoltcp::wire::Ipv4Address;
@ -47,9 +47,9 @@ struct PolicyUpdate {
impl Policy {
pub(super) fn new(gateway_ip: Ipv4Address, allow: Vec<Target>, block: Vec<Target>) -> Self {
let bridge_isolation = !allow.contains(&Target::Prefix(Ipv4Net::zero()));
let allow = normalize_targets(allow);
let block = normalize_targets(block);
let bridge_isolation = Self::bridge_isolation(&allow);
let rules = build_rules(gateway_ip, &allow, &block);
Policy {
@ -85,7 +85,7 @@ impl Policy {
let allow = parse_targets(allow)?;
let block = parse_targets(block)?;
let bridge_isolation = !allow.contains(&Target::Prefix(Ipv4Net::zero()));
let bridge_isolation = Self::bridge_isolation(&allow);
let rules = build_rules(self.gateway_ip, &allow, &block);
if self.desired_revision.as_deref() == Some(desired_revision.as_str()) {
@ -141,6 +141,12 @@ impl Policy {
self.bridge_isolation,
)
}
pub(super) fn bridge_isolation(allow: &[Target]) -> bool {
!allow
.iter()
.any(|target| matches!(target, Target::Prefix(prefix) if prefix.prefix_len() == 0))
}
}
impl PolicyUpdate {
@ -818,6 +824,31 @@ mod tests {
assert_eq!(policy.result(), before);
}
#[test]
fn noncanonical_default_route_disables_isolation_and_round_trips() {
let raw_default = Target::Prefix(Ipv4Net::from_str("10.1.2.3/0").unwrap());
assert!(!Policy::bridge_isolation(&[raw_default]));
let mut policy = policy(&["10.1.2.3/0"], &[]);
let initial = policy.result();
assert_eq!(initial["allow"], json!(["0.0.0.0/0"]));
assert_eq!(initial["bridgeIsolation"], false);
let response = request(
&mut policy,
json!({
"jsonrpc": "2.0",
"id": 1,
"method": "softnet.policy.set",
"params": {"allow": ["0.0.0.0/0"], "block": [], "desiredRevision": "vm-uid:42"}
}),
);
assert_eq!(response["result"]["allow"], json!(["0.0.0.0/0"]));
assert_eq!(response["result"]["bridgeIsolation"], false);
assert_eq!(response["result"]["desiredRevision"], "vm-uid:42");
}
#[test]
fn validates_json_rpc_envelope_method_and_parameters() {
let mut policy = policy(&[], &[]);

View File

@ -16,7 +16,6 @@ pub use exposed_port::ExposedPort;
use ipnet::Ipv4Net;
use mac_address::MacAddress;
use port_forwarder::PortForwarder;
use prefix_trie::Prefix;
use smoltcp::wire::EthernetFrame;
use std::io::ErrorKind;
use std::os::unix::io::{AsRawFd, RawFd};
@ -71,10 +70,7 @@ impl Proxy<'_> {
control_fd: Option<RawFd>,
) -> Result<Proxy<'proxy>> {
let vm = VM::new(vm_fd)?;
let host = Host::new(
vm_net_type,
!allow.contains(&Target::Prefix(Ipv4Net::zero())),
)?;
let host = Host::new(vm_net_type, Policy::bridge_isolation(&allow))?;
let poller_timeout = Duration::from_millis(100);
let policy = Policy::new(host.gateway_ip, allow, block);
let control = control_fd.map(Control::new).transpose()?;