diff --git a/README.md b/README.md index c0c7c80..0dd48c5 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/lib/proxy/control.rs b/lib/proxy/control.rs index 8b10ddb..972907e 100644 --- a/lib/proxy/control.rs +++ b/lib/proxy/control.rs @@ -49,7 +49,7 @@ impl Policy { } } - fn replace( + fn set( &mut self, allow: Vec, block: Vec, @@ -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, block: Vec, 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::(request.params).map_err(|_| { + "softnet.policy.set" => { + let params = serde_json::from_value::(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());