Simplify Softnet policy RPC methods

This commit is contained in:
Fedor Korotkov 2026-07-21 13:33:22 -04:00
parent e091241651
commit 0c5454b83a
No known key found for this signature in database
2 changed files with 26 additions and 50 deletions

View File

@ -48,13 +48,13 @@ Softnet is started and managed automatically by Tart if `--net-softnet` flag is
Softnet can update the running VM's IPv4 egress policy without restarting the VM. Pass a connected Unix stream socket as `--control-fd` to enable a newline-delimited [JSON-RPC 2.0](https://www.jsonrpc.org/specification) control channel. The socket is duplex and must be separate from `--vm-fd`, which carries VM packets.
The supported methods are `softnet.capabilities`, `softnet.policy.get`, and `softnet.policy.replace`. A complete replacement looks like this (each request and response occupies one line):
The supported methods are `softnet.policy.get` and `softnet.policy.set`. A complete policy update looks like this (each request and response occupies one line):
```json
{"jsonrpc":"2.0","id":"42","method":"softnet.policy.replace","params":{"allow":["@host","10.0.0.0/8"],"block":["0.0.0.0/0"],"desiredRevision":"vm-uid:42"}}
{"jsonrpc":"2.0","id":"42","method":"softnet.policy.set","params":{"allow":["@host","10.0.0.0/8"],"block":["0.0.0.0/0"],"desiredRevision":"vm-uid:42"}}
{"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 or integer `id`; notifications are rejected so policy changes always have an acknowledgment. Replacements 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. `softnet.capabilities` advertises these limits.
Every request must include a non-null string or 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.
`--allow=0.0.0.0/0` additionally disables vmnet bridge isolation during interface creation. Bridge isolation cannot be changed for a running VM, so replacements 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.
`--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

@ -49,7 +49,7 @@ impl Policy {
}
}
fn replace(
fn set(
&mut self,
allow: Vec<String>,
block: Vec<String>,
@ -92,7 +92,7 @@ impl Policy {
));
}
// Build and validate everything above before replacing any active state. The packet
// Build and validate everything above before updating any active state. The packet
// filter observes either the old PrefixMap or the complete new one.
self.rules = rules;
self.allow = allow;
@ -341,7 +341,7 @@ struct Request {
#[derive(Deserialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
struct ReplaceParams {
struct SetParams {
allow: Vec<String>,
block: Vec<String>,
desired_revision: String,
@ -377,21 +377,6 @@ fn handle_request(policy: &mut Policy, line: &[u8]) -> Value {
let id = request.id.unwrap_or(Value::Null);
let result = match request.method.as_str() {
"softnet.capabilities" => {
if !empty_params(&request.params) {
Err(RpcError::new(
INVALID_PARAMS,
"softnet.capabilities does not accept parameters",
))
} else {
Ok(json!({
"policyReplace": true,
"bridgeIsolationMutable": false,
"maxRequestBytes": MAX_REQUEST_BYTES,
"maxTargets": MAX_TARGETS,
}))
}
}
"softnet.policy.get" => {
if !empty_params(&request.params) {
Err(RpcError::new(
@ -402,16 +387,16 @@ fn handle_request(policy: &mut Policy, line: &[u8]) -> Value {
Ok(policy.result())
}
}
"softnet.policy.replace" => {
let params = serde_json::from_value::<ReplaceParams>(request.params).map_err(|_| {
"softnet.policy.set" => {
let params = serde_json::from_value::<SetParams>(request.params).map_err(|_| {
RpcError::new(
INVALID_PARAMS,
"softnet.policy.replace requires allow, block, and desiredRevision",
"softnet.policy.set requires allow, block, and desiredRevision",
)
});
params.and_then(|params| {
policy.replace(params.allow, params.block, params.desired_revision)?;
policy.set(params.allow, params.block, params.desired_revision)?;
Ok(policy.result())
})
}
@ -573,18 +558,9 @@ mod tests {
}
#[test]
fn capabilities_and_get_report_limits_and_initial_policy() {
fn get_reports_initial_policy() {
let mut policy = policy(&["@host"], &["0.0.0.0/0"]);
let capabilities = request(
&mut policy,
json!({"jsonrpc": "2.0", "id": "capabilities", "method": "softnet.capabilities"}),
);
assert_eq!(capabilities["result"]["policyReplace"], true);
assert_eq!(capabilities["result"]["bridgeIsolationMutable"], false);
assert_eq!(capabilities["result"]["maxRequestBytes"], MAX_REQUEST_BYTES);
assert_eq!(capabilities["result"]["maxTargets"], MAX_TARGETS);
let response = request(
&mut policy,
json!({"jsonrpc": "2.0", "id": 1, "method": "softnet.policy.get", "params": {}}),
@ -597,15 +573,15 @@ mod tests {
}
#[test]
fn replace_applies_complete_policy_and_preserves_block_precedence() {
fn set_applies_complete_policy_and_preserves_block_precedence() {
let mut policy = policy(&[], &[]);
let response = request(
&mut policy,
json!({
"jsonrpc": "2.0",
"id": "replace",
"method": "softnet.policy.replace",
"id": "set",
"method": "softnet.policy.set",
"params": {
"allow": ["@host", "10.0.0.0/8", "10.0.0.0/8"],
"block": ["192.168.64.1/32", "10.0.0.0/8"],
@ -643,7 +619,7 @@ mod tests {
json!({
"jsonrpc": "2.0",
"id": 1,
"method": "softnet.policy.replace",
"method": "softnet.policy.set",
"params": {"allow": ["@host", "10.1.2.3/8"], "block": [], "desiredRevision": "7"}
}),
);
@ -652,7 +628,7 @@ mod tests {
json!({
"jsonrpc": "2.0",
"id": 2,
"method": "softnet.policy.replace",
"method": "softnet.policy.set",
"params": {"allow": ["10.0.0.0/8", "@host", "@host"], "block": [], "desiredRevision": "7"}
}),
);
@ -665,7 +641,7 @@ mod tests {
json!({
"jsonrpc": "2.0",
"id": 3,
"method": "softnet.policy.replace",
"method": "softnet.policy.set",
"params": {"allow": ["192.168.0.0/16"], "block": [], "desiredRevision": "7"}
}),
);
@ -683,7 +659,7 @@ mod tests {
json!({
"jsonrpc": "2.0",
"id": 1,
"method": "softnet.policy.replace",
"method": "softnet.policy.set",
"params": {"allow": ["2001:db8::/32"], "block": [], "desiredRevision": "8"}
}),
);
@ -696,7 +672,7 @@ mod tests {
json!({
"jsonrpc": "2.0",
"id": 2,
"method": "softnet.policy.replace",
"method": "softnet.policy.set",
"params": {"allow": targets, "block": [], "desiredRevision": "9"}
}),
);
@ -708,7 +684,7 @@ mod tests {
json!({
"jsonrpc": "2.0",
"id": 3,
"method": "softnet.policy.replace",
"method": "softnet.policy.set",
"params": {"allow": ["0.0.0.0/0"], "block": ["0.0.0.0/0"], "desiredRevision": "10"}
}),
);
@ -749,7 +725,7 @@ mod tests {
json!({
"jsonrpc": "2.0",
"id": 3,
"method": "softnet.policy.replace",
"method": "softnet.policy.set",
"params": {"allow": [], "block": []}
}),
);
@ -760,7 +736,7 @@ mod tests {
&mut policy,
json!({
"jsonrpc": "2.0",
"method": "softnet.policy.replace",
"method": "softnet.policy.set",
"params": {"allow": ["@host"], "block": [], "desiredRevision": "12"}
}),
);
@ -773,7 +749,7 @@ mod tests {
json!({
"jsonrpc": "2.0",
"id": null,
"method": "softnet.policy.replace",
"method": "softnet.policy.set",
"params": {"allow": ["@host"], "block": [], "desiredRevision": "13"}
}),
);
@ -795,7 +771,7 @@ mod tests {
.write_all(
concat!(
"{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"softnet.policy.get\"}\n",
"{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"softnet.policy.replace\",\"params\":{\"allow\":[\"@host\"],\"block\":[\"0.0.0.0/0\"],\"desiredRevision\":\"11\"}}\n"
"{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"softnet.policy.set\",\"params\":{\"allow\":[\"@host\"],\"block\":[\"0.0.0.0/0\"],\"desiredRevision\":\"11\"}}\n"
)
.as_bytes(),
)
@ -859,7 +835,7 @@ mod tests {
client
.write_all(
b"{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"softnet.policy.replace\",\"params\":{\"allow\":[\"@host\"],",
b"{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"softnet.policy.set\",\"params\":{\"allow\":[\"@host\"],",
)
.unwrap();
assert!(control.service(&mut policy).unwrap());