Reject superseded Softnet policy revisions

This commit is contained in:
Fedor Korotkov 2026-07-21 13:59:44 -04:00
parent 8ac47c46ff
commit 20fd755d0d
2 changed files with 53 additions and 1 deletions

View File

@ -55,6 +55,6 @@ The supported methods are `softnet.policy.get` and `softnet.policy.set`. A compl
{"jsonrpc":"2.0","id":"42","result":{"allow":["10.0.0.0/8","@host"],"block":["0.0.0.0/0"],"desiredRevision":"vm-uid:42","ruleCount":3,"bridgeIsolation":true}}
```
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 a revision with the same policy is idempotent; reusing a revision with a different policy 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.
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.

View File

@ -12,6 +12,7 @@ use prefix_trie::{Prefix, PrefixMap};
use serde::Deserialize;
use serde_json::{Value, json};
use smoltcp::wire::Ipv4Address;
use std::collections::HashSet;
use std::io::{self, ErrorKind, Read, Write};
use std::mem::{size_of, zeroed};
use std::os::fd::{AsRawFd, FromRawFd, RawFd};
@ -31,6 +32,7 @@ pub(super) struct Policy {
allow: Vec<Target>,
block: Vec<Target>,
desired_revision: Option<String>,
applied_revisions: HashSet<String>,
bridge_isolation: bool,
gateway_ip: Ipv4Address,
}
@ -47,6 +49,7 @@ impl Policy {
allow,
block,
desired_revision: None,
applied_revisions: HashSet::new(),
bridge_isolation,
gateway_ip,
}
@ -88,6 +91,13 @@ impl Policy {
));
}
if self.applied_revisions.contains(&desired_revision) {
return Err(rpc_error(
REVISION_CONFLICT,
"desiredRevision was already superseded by a newer policy",
));
}
if bridge_isolation != self.bridge_isolation {
return Err(rpc_error(
BRIDGE_ISOLATION_CONFLICT,
@ -100,6 +110,7 @@ impl Policy {
self.rules = rules;
self.allow = allow;
self.block = block;
self.applied_revisions.insert(desired_revision.clone());
self.desired_revision = Some(desired_revision);
Ok(())
@ -631,6 +642,47 @@ mod tests {
assert_eq!(policy.result(), before);
}
#[test]
fn superseded_revision_cannot_roll_back_the_active_policy() {
let mut policy = policy(&[], &[]);
let first = request(
&mut policy,
json!({
"jsonrpc": "2.0",
"id": 1,
"method": "softnet.policy.set",
"params": {"allow": ["10.0.0.0/8"], "block": [], "desiredRevision": "vm-uid:41"}
}),
);
assert_eq!(first["result"]["desiredRevision"], "vm-uid:41");
let second = request(
&mut policy,
json!({
"jsonrpc": "2.0",
"id": 2,
"method": "softnet.policy.set",
"params": {"allow": [], "block": ["0.0.0.0/0"], "desiredRevision": "vm-uid:42"}
}),
);
assert_eq!(second["result"]["desiredRevision"], "vm-uid:42");
let before = policy.result();
let stale = request(
&mut policy,
json!({
"jsonrpc": "2.0",
"id": 3,
"method": "softnet.policy.set",
"params": {"allow": ["10.0.0.0/8"], "block": [], "desiredRevision": "vm-uid:41"}
}),
);
assert_eq!(stale["error"]["code"], REVISION_CONFLICT);
assert_eq!(policy.result(), before);
}
#[test]
fn invalid_targets_limits_and_bridge_isolation_changes_leave_policy_unchanged() {
let mut policy = policy(&["@host"], &["0.0.0.0/0"]);