Skip to content
Merged
8 changes: 7 additions & 1 deletion docs/sandbox-policy/0.8.0/networking/networking.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ Egress peer and port fields (used in `egress.allow[]` / `egress.deny[]`; not sho
| Field | Type | Notes |
|---|---|---|
| `to[].cidr` | IPv4 / IPv6 CIDR, or 0.0.0.0/0 / ::/0 for any | Single CIDR string (CNI/Kubernetes style), replacing separate address + prefix length. |
| `to[].except` | list of CIDRs, optional | Exclusions within the peer's CIDR (Kubernetes `ipBlock.except` style). Expressible on Windows process containers (WFP) and the Linux backends (iptables) as additional deny rules; not supported on Seatbelt (no destination filtering). |
| `to[].except` | list of CIDRs, optional | Exclusions within the peer's CIDR (Kubernetes `ipBlock.except` style). An exclusion narrows the rule that carries it and nothing else: it never states a verdict of its own, so an address it removes is decided by the remaining rules and the direction default. Windows process containers pass the exclusion to the platform (WFP); the Linux backends subtract it from the peer and program the covering blocks that remain, because an `iptables` rule cannot carry an exclusion and a separate rule would leak into later ones. The Linux backends bound the resulting expansion; the GA Scope by Backend section states the limits. Not supported on Seatbelt (no destination filtering). |
| `ports[].protocol` | tcp / udp / icmp / any | `any` matches at minimum TCP, UDP, and ICMPv4/6; a backend may match more. Enforced on Windows process containers (WFP) and the Linux backends (iptables); not supported on Seatbelt. |
| `ports[].port` | uint16, optional | Destination port. Omit `ports` to match all ports/protocols. |
| `ports[].endPort` | uint16, optional | End of a port range (Kubernetes `endPort` style); requires numeric port. Supported on Windows process containers (WFP) and the Linux backends (iptables); not supported on Seatbelt. |
Expand Down Expand Up @@ -474,6 +474,12 @@ LXC and Bubblewrap use iptables/nftables on the container network path. Their IN
and the host-to-container half of `ingress.hostLoopback`; routing and output policy enforce its container-to-host half.
Model 2 permits only the proxy endpoint.

Both backends subtract `to[].except` from the peer that carries it and program the covering blocks that remain, so one
peer can expand into many rules. Two ceilings bound that expansion: a single peer may expand into at most **256 address
blocks** once its exclusions are removed, and one `egress` policy may lower into at most **65,536 rules** in total,
counting every destination block in every port each rule names. A policy exceeding either ceiling is rejected with the
limit it hit; neither is silently truncated, and no partial policy is installed.

> **Implementation status (Bubblewrap).** Egress is enforced from schema 0.8+,
> but not on the path described above. Unprivileged Bubblewrap has no host-side
> veth, so no chain can be hooked into `FORWARD`. Instead the sandbox gets its
Expand Down
58 changes: 58 additions & 0 deletions src/backends/lxc/common/src/lxc_runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,15 @@ impl LxcScriptRunner {
}
}

// The policy lowers to the same rules with or without a container, so
// one that cannot be programmed is refused before a container exists.
if let Err(msg) = NetworkIptablesManager::validate_egress_lowering(
Comment on lines +301 to +303
&request.policy,
uses_directional_keys(&request.policy),
) {
return ScriptResponse::error(&msg);
}

if self.destroy_on_exit {
signal_cleanup::set_active(&container_name);
}
Expand Down Expand Up @@ -1423,6 +1432,55 @@ mod tests {
"a reused container must be destroyed on readiness timeout when destroyOnExit=true"
);
}

/// A policy whose `except` entry is malformed, so lowering refuses it.
fn request_with_unlowerable_egress() -> ExecutionRequest {
use wxc_common::models::{NetworkAction, NetworkCidr, NetworkPeer, NetworkRule};

let mut request = ExecutionRequest::default();
request.policy.network_egress = Some(NetworkEgressPolicy {
default: NetworkAction::Deny,
allow: vec![NetworkRule {
to: vec![NetworkPeer {
cidr: NetworkCidr {
address: "10.0.0.0".parse().expect("literal"),
prefix_length: 8,
},
except: vec![NetworkCidr {
address: "10.10.0.0".parse().expect("literal"),
prefix_length: 40,
}],
}],
ports: Vec::new(),
}],
deny: Vec::new(),
});
request
}

#[test]
fn an_egress_policy_that_cannot_be_lowered_is_refused_before_a_container_exists() {
let mut logger = Logger::new(Mode::Buffer);

let response =
runner_for_guard_tests().run_internal(&request_with_unlowerable_egress(), &mut logger);

assert_ne!(
response.exit_code, 0,
"input=allow.to=[{{cidr:10.0.0.0/8, except:[10.10.0.0/40]}}]; expected a refusal; output={response:?}"
);
assert!(
response
.error_message
.contains("wider than its address family"),
"expected the lowering's refusal rather than a container failure, got: {response:?}"
);
assert!(
!logger.get_buffer().contains("Creating LXC container"),
"the refusal must land before the container is created; log={}",
logger.get_buffer()
);
}
}

#[cfg(all(test, unix))]
Expand Down
Loading
Loading