network: allow combining net allow and deny policies - #228
bferanmi806-sketch wants to merge 2 commits into
Conversation
|
@bferanmi806-sketch Thanks for the PR! Please fix the failing tests and rebase to solve the conflicts. I will look into it tomorrow. |
e8b8e4a to
7eb6fa8
Compare
|
Thanks! I’ve rebased this onto the latest The learn-mode bind regression is covered by preserving the upstream wildcard bind allowance, and I added regression coverage around the combined bind policy and the Local build/core validation is passing as far as this host supports. The new Actions run is currently waiting for approval to start on the fork, so there aren’t any CI jobs/results yet. Should be ready for another look when you get a chance. |
|
Thanks for the rebase. Before a line-level pass I want to settle two design points. 1. The This flag exists only because Please fix this at the root instead: keep HTTP-derived rules out of 2. The compatibility machinery Sandlock does not keep backward compatibility before 1.0, so please drop all of it:
With point 1 done, none of this is needed anyway. I will also approve the CI run once these are addressed, and then review the rest. |
|
Thanks for the detailed feedback! I've addressed both design points. HTTP-derived reachability is now generated separately at resolution time, keeping I bumped The Rust library tests pass locally (802 passed, 0 failed). The Linux integration tests still need CI validation, as my WSL host only supports Landlock ABI v3. The changes are pushed, and I've updated the PR description accordingly. Could you approve the pending CI run when you get a chance? Also, if there are any other issues or areas of Sandlock where you could use an extra pair of hands, I'd be happy to help. I'm keen to keep contributing to the project! Thanks again! |
congwang-mk
left a comment
There was a problem hiding this comment.
Thanks, both design points are addressed and the separation looks right. Remaining items are inline. One that has no line to anchor to:
Commit history. Please squash into 2 or 3 logical commits. Right now there are fixup commits, one with the bare subject docs:, early commits that add net_allow_explicit only for later ones to remove it, and the body of 884356f says "IMAGE_VERSION stays 2" which the next commit contradicts. Every commit also needs a Signed-off-by (git commit -s).
| /// could OOM/abort, taking down every sandbox. The child sends with a bogus 4 GiB | ||
| /// `addr_len` via raw `libc.sendto`. The supervisor must (a) survive — the whole | ||
| /// run completes — and (b) reject the oversized length with `EINVAL` (22), | ||
| /// `addr_len` via raw `libc.sendto`. The supervisor must (a) survive — the whole |
There was a problem hiding this comment.
Encoding damage: 24 comment lines in this file that the PR does not otherwise touch had their dashes turned into mojibake (looks like a Windows editor re-save). Please revert all of those hunks.
| let pid_overrides = Arc::new(RwLock::new(HashMap::new())); | ||
| let denied = Arc::new(crate::seccomp::state::DeniedSet::default()); | ||
| let mut ctx = PolicyContext::new(live.clone(), ceiling, pid_overrides, denied); | ||
| let mut ctx = PolicyContext::new( |
There was a problem hiding this comment.
Unrelated churn: these six hunks only re-wrap PolicyContext::new(...), left over from a removed parameter. Please revert, along with the same re-wrap of spawn_policy_fn in sandbox.rs, the let nr move in seccomp/notif.rs, and the docstring deletions in test_policy_fn.rs and python/tests/test_policy_fn.py.
| let sup = Arc::clone(&__sup); | ||
| let notif_fd = cx.notif_fd; | ||
| async move { | ||
| crate::port_remap::handle_bind(¬if, &sup, notif_fd).await |
There was a problem hiding this comment.
This block is now indented one level too deep. Only the &sup.network to &sup argument change is needed here.
| .effective_network_policy( | ||
| notif.pid, | ||
| crate::network::Protocol::Udp, | ||
| live_policy.as_ref(), |
There was a problem hiding this comment.
Behavior change unrelated to #135: main passes None here. With a non-empty policy_fn live allowlist the UDP layer becomes an IP-only allow, denies_everything() turns false, and UDP binds start succeeding under TCP-only rule sets. Please revert to None, or split it out with a test and a justification.
| let per_ip = ips.iter().map(|&ip| (ip, PortAllow::Any)).collect(); | ||
| let per_ip = ips | ||
| .iter() | ||
| .map(|ip| (ip.to_canonical(), PortAllow::Any)) |
There was a problem hiding this comment.
to_canonical() is also unrelated to this PR and untested. Please drop it or send it separately with a test.
| legacy priority (per-PID override > live policy > static allowlist) and | ||
| the static denylist is applied after the allow verdict and always wins, | ||
| so a dynamic override can never erase it. | ||
| 3. **Seccomp COW with `workdir`.** When `workdir` is set, the |
There was a problem hiding this comment.
The list now has two items numbered 3 (this one and "HTTP host auto-expansion"), and the continuation lines of the new item 2 are indented 4 spaces instead of 3.
| prefixes opt other protocols in (`"udp://1.1.1.1:53"`, `"udp://*"`, | ||
| `"icmp://host"`, `"icmp://*"`). `NetDeny` is the inverse (default-allow | ||
| denylist, IP/CIDR targets only, mutually exclusive with `NetAllow`). | ||
| denylist, IP/CIDR targets only); when both are set, denied destinations win. |
There was a problem hiding this comment.
Stray leading space here and on the two NetDenyBind lines below.
| for p in &base.fs_readable { b = b.fs_read(p); } | ||
| for p in &base.fs_writable { b = b.fs_write(p); } | ||
| for p in &base.fs_denied { b = b.fs_deny(p); } | ||
| // `Sandbox.net_allow` holds only explicit rules; HTTP reachability is |
There was a problem hiding this comment.
Nit: this "net_allow holds only explicit rules" note is repeated in main.rs, profile.rs, builder.rs, http.rs, image.rs and twice in sandbox.rs. The field doc on Sandbox.net_allow is enough; please remove the rest.
| /// always activate the layer; HTTP-derived reachability activates it only | ||
| /// for non-deny policies (HTTP-only stays a restrictive allowlist, while | ||
| /// deny-only+HTTP stays default-allow with the HTTP proxy on top). | ||
| pub fn net_allow_is_active(&self) -> bool { |
There was a problem hiding this comment.
Nit: this is pub only so the integration tests can assert on it. Prefer pub(crate) and let those tests assert through behavior (they already do), or mark it #[doc(hidden)].
| || ns.bind_deny_ports.contains(&port) | ||
| } | ||
| None => false, | ||
| // Non-IP families: bind verbatim — there is no TCP port policy to apply. |
There was a problem hiding this comment.
Nit: please avoid em dashes in new comments (here and at line 202); use a colon or a comma.
Allow outbound net_allow and net_deny together with allow-minus-deny semantics: a destination must match the allow layer and must not match the deny layer. Same precedence for net_allow_bind and net_deny_bind, and the static deny layer runs after the allow verdict so dynamic policy_fn overrides can never erase it. Keep HTTP-derived reachability out of Sandbox.net_allow; generate at resolution time from http_allow, http_deny and http_ports and merge only where rules are consumed, with mode derived from separated config. Address review: restore None for UDP live-policy query in port_remap, drop unrelated to_canonical, formatting churn, mojibake, dead code and duplicate test, fix dispatch indent, helper visibility, docs, numbering and whitespace, and remove em dashes from new comments and docs. Signed-off-by: Balogun Feranmi <bferanmi806@gmail.com>
v3 stores only explicit net_allow rules; v2 images may contain HTTP-derived reachability inside net_allow, which v3 would misread as explicit rules (for example deny-only plus HTTP loading as combined), so v2 is rejected outright with no compatibility fallback (pre-1.0 policy). Add round-trip coverage for combined, deny-only plus HTTP and HTTP-only policies. Signed-off-by: Balogun Feranmi <bferanmi806@gmail.com>
e88b61b to
5e57081
Compare
|
Thx i addedd the fix if there are any road map items or issues u need help with feel free to assign them to me |
Fixes #135.
Outbound policy is now allow-only, deny-only, or combined allow-minus-deny: with both lists present a destination must match an allow rule and must not match a deny rule. The same precedence applies to TCP bind allow/deny, where denied ports win.
Architecture (per congwang-mk review 2026-09-18): Sandbox.net_allow holds only explicit user rules. HTTP reachability is generated at resolution time via http_net_allow_rules() from http_allow/http_deny/http_ports and merged only at consumption sites (sandbox spawn resolution including virtual /etc/hosts, Landlock connect-port gates, resolved feature flags). The mode is derived from the separated configuration with no net_allow_explicit flag, no custom trailing deserializer, no checkpoint/meta.json persistence or backfill, and no profile/CLI special cases: explicit allow always activates the layer; HTTP-derived reachability activates it only when no deny is present (HTTP-only stays a restrictive allowlist, deny-only+HTTP stays default-allow).
Checkpoint: IMAGE_VERSION bumped 2 -> 3. Upstream v2 images may store HTTP-derived rules inside net_allow, which the separated representation would misread as explicit rules (deny-only+HTTP loading as combined), so v2 is rejected outright with no compatibility fallback (pre-1.0 policy). Regression test: image_rejects_v2_http_mixed_net_allow.
Static deny stays effective after dynamic allow resolution: the effective allow resolves per-PID override > live policy > static allowlist, then the immutable static deny is checked and wins, including under Unrestricted allow.
Combined finite TCP policies keep the Landlock allow-side port gate; deny-only and all-ports/wildcard allow drop CONNECT_TCP so the on-behalf path enforces.
Regression coverage: combined finite overlap/non-overlap, wildcard and Unrestricted allow with deny carve-outs, hostname resolving into a denied CIDR, deny-only with HTTP, HTTP-only, combined+HTTP, profile/CLI rebuild round-trips, checkpoint round-trips plus v2 rejection, and Landlock mode selection (deny-only, combined finite/wildcard, HTTP-only, deny-only+HTTP, combined+HTTP) including ephemeral bind(0).
Validation (WSL Ubuntu, Rust 1.98.1): cargo test -p sandlock-core --lib => 802 passed, 0 failed. Sandbox-execution CLI/integration tests cannot run in this WSL host (Landlock ABI v3, NetTcp requires v4); expected in CI. Go/Python sources untouched by this change.