fix(sandbox/linux): allow open_port=0 to disable Landlock TCP in Prox…#1053
fix(sandbox/linux): allow open_port=0 to disable Landlock TCP in Prox…#1053jbndbc wants to merge 3 commits into
Conversation
…yOnly mode Testcontainers maps Docker containers to random ephemeral ports on localhost. Maven Surefire binds to 127.0.0.1:0 for inter-process coordination. Neither can be expressed as explicit port rules in nono's ProxyOnly configuration. Landlock network rules match on exact port number with no wildcard and no IP filter. Port 0 is not a catch-all: ConnectTcp/0 matches connects to destination port 0 only; BindTcp/0 matches bind(port=0) calls only. There is no Landlock primitive for 'allow 127.0.0.1:* only'. When open_port = [0] is set in ProxyOnly mode, skip handle_access(AccessNet) on the Landlock ruleset entirely. Without AccessNet handling the kernel imposes no TCP restrictions; the proxy becomes the sole domain enforcer via HTTPS_PROXY / HTTP_PROXY / ALL_PROXY. The same applies to the seccomp fallback path on kernels without Landlock v4+. Disclosed tradeoff: this removes kernel-level TCP enforcement entirely, not just for localhost. Non-HTTP TCP clients (psql, redis-cli, nc, raw sockets) can reach remote hosts directly. ProxyOnly mode protects HTTP/HTTPS traffic via standard library proxy support; raw TCP bypass requires hardcoded IPs. Proper localhost-only restriction would require network namespaces or cgroup BPF. open_port = [0] in Blocked mode (no proxy) is still rejected with an error. Closes always-further#611 Signed-off-by: Jan Bauer Nielsen <jbn@dbc.dk>
PR Review SummarySize
Affected crates
Blast radius — ContainedThis PR touches: source code Updated automatically on each push to this PR. |
There was a problem hiding this comment.
Code Review
This pull request updates the Linux sandbox implementation to support port 0 (localhost TCP wildcard) in ProxyOnly mode by skipping Landlock network enforcement and relying solely on the proxy. In Blocked mode, port 0 remains rejected. Feedback points out a critical issue in the new unit test: calling apply_with_abi directly without forking will sandbox the test runner process itself, which is irreversible and will cause subsequent tests to fail. A suggestion is provided to run the test in a forked child process.
`test_accept_localhost_port_wildcard_zero_in_proxy_only_mode` called `apply_with_abi()` directly, which invokes `restrict_self()` and permanently sandboxes the test-runner process. Tests running later in the same process could fail spuriously due to the unexpected Landlock restrictions. Fix: wrap the call in a `fork()`/`waitpid()` child, matching the pattern used in `probe_abstract_socket_with_landlock` (line 2630). Signed-off-by: Jan Bauer Nielsen <jbn@dbc.dk> Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Collapse single-argument assert! calls to one line as rustfmt requires. Signed-off-by: Jan Bauer Nielsen <jbn@dbc.dk> Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Sorry for the late engagement @jbndbc - this is something we are aware of an intend to resolve, but we are thinking of leveraging seccomp-notify and in fact making the fallback the main approach, this saves having the brittle Landlock ABI selection gate and more importantly it can express the policy Landlock fundamentally can't and is limiting the use pattern you have with maven etc The tradeoff as you outlined (raw TCP to any host bypassing the proxy) is the part we want avoid. taking a seccomp-notify / supervisor path avoids that tradeoff and the bonus is the supervisor infrastructure mostly exists already: the ProxyOnly fallback routes destination is loopback → allow any port (Testcontainers, Surefire); otherwise → proxy port only. the kernel-level enforcement stays on, so no raw-TCP window. We already plan to set that path the default instead of an ABI-gated fallback - as it gives us one enforcement path to test across all kernels. @SequeI is covering this work as part of the network refactor, and we have an issue: #1101 - we will put heads together and make sure this is covered. if you need a make shift for now, maybe iptables could cover, but either apologies for the brittle current implementation and we will have a solution for you in the not to distance future. |
Allow
open_port = [0]in ProxyOnly mode on LinuxCloses #611
What this fixes
When domain filtering is active (
--allow-domain/--proxy), nono enters ProxyOnly mode.Landlock restricts outbound TCP to the proxy port only. This breaks Testcontainers and Maven
Surefire, both of which bind to dynamically assigned ports on localhost that cannot be known in
advance:
127.0.0.1127.0.0.1:0(OS-assigned ephemeral port) for inter-processcoordination
Before this change, users had to disable domain filtering entirely to run their test suite,
losing the protection they wanted.
Approach
On macOS,
open_port = [0]already works: Seatbelt generates(allow network-outbound (remote tcp "localhost:*")), a genuine IP-scoped wildcard that permitsany localhost port while external hosts remain restricted. Linux cannot do the same.
Why Linux requires a different mechanism
Landlock network rules match on exact port number via a red-black tree lookup in
security/landlock/net.c. There is no wildcard rule type and no IP-address filter. Port 0 hasaccess-specific semantics but is not a general wildcard:
BindTcp+ port 0: meaningful — matchesbind(port=0)calls, triggering OS ephemeralport assignment from
/proc/sys/net/ipv4/ip_local_port_range. Covers Maven Surefire's bindside only.
ConnectTcp+ port 0: matchesconnect()to destination port 0 only. A Testcontainersconnect(localhost:32768)has no matching rule and is deniedEACCES.There is no way to express "allow
127.0.0.1:*but deny*.*.*.*:*" in Landlock — rules carryno IP-address component.
The correct mechanism: when
open_port = [0]is set in ProxyOnly mode, skiphandle_access(AccessNet)on the Landlock ruleset entirely. A ruleset with noAccessNethandling imposes no kernel-level TCP restrictions. The proxy — already running, already injecting
HTTPS_PROXY/HTTP_PROXY/ALL_PROXY— becomes the sole network enforcer.This also applies to the seccomp fallback path (kernels without Landlock v4+): the seccomp TCP
filter is similarly skipped.
Security scope of this change
Skipping
handle_access(AccessNet)removes kernel-level TCP enforcement entirely, not just forlocalhost. A process can make direct raw TCP connections to remote hosts on any port,
bypassing the proxy. This affects non-HTTP TCP clients such as
psql,redis-cli,nc, orcustom socket code with a hardcoded IP — tools that do not read
HTTPS_PROXY.This is a deliberate and disclosed tradeoff: ProxyOnly mode is designed to constrain HTTP/HTTPS
requests to unauthorized domains, which standard HTTP libraries route through the proxy
automatically. The remaining sandbox layers (Landlock filesystem restrictions, command blocklists,
process isolation) continue to apply.
A complete fix for raw TCP bypass would require either:
veth to the proxy, making external IPs unreachable by construction
BPF_PROG_TYPE_CGROUP_SOCK_ADDR): a BPF program that permits127.0.0.1:*and denies all other destinations
Both require capabilities outside nono's current architecture. This change is a pragmatic
improvement for the common case, with the limitation fully documented in code and in this PR.
open_port = [0]inBlockedmode (no proxy) continues to be rejected with an error directingusers to
--allow-domain.Files changed
crates/nono/src/sandbox/linux.rsBlockedmode only;localhost_wildcard_proxyflag added;needs_network_handlingskips when flag is set;network_handled_by_landlockgates the NetPort rule block (prevents adding port rules to a ruleset not configured forAccessNet);warn!()emitted at runtimecrates/nono/src/capability.rsallow_localhost_portdoc updated to explain port 0 semantics per platform and the Linux scope differenceTests
test_reject_localhost_port_wildcard_zero_in_blocked_mode— renamed and updated; verifiesport 0 in
block_networkmode is still rejected with a message directing to ProxyOnly modetest_accept_localhost_port_wildcard_zero_in_proxy_only_mode— new; verifiesapply_with_abisucceeds with
proxy_only(8080)+localhost_port(0)on Landlock v4+ kernels; skipsgracefully on older kernels
Contributor notes
This PR was prepared by Claude (Anthropic), an AI coding assistant, at the request of a
maintainer. The implementation went through two iterations: the first attempt incorrectly assumed
NetPort::new(0, ...)was a Landlock wildcard (it is not); the second iteration skips networkhandling entirely, which is the correct mechanism. The scope of the TCP relaxation — not just
localhost but all TCP — was identified and disclosed during implementation.
Files and sections consulted:
crates/nono/src/sandbox/linux.rs—apply_with_abi,SeccompNetFallback,seccomp_network_fallback_mode, existing port 0 rejection guardcrates/nono/src/sandbox/macos.rs—push_localhost_tcp_outbound_seatbelt_rules,port 0 →
localhost:*wildcard handlingcrates/nono/src/capability.rs—NetworkMode,CapabilitySet,allow_localhost_port,localhost_portsfield docscrates/nono-cli/src/capability_ext.rs— port flow from CLI/profile intoCapabilitySetsecurity/landlock/net.c— confirmed exact-port red-black tree matching;no wildcard rule exists; port 0 semantics per access type
Agent Compliance Check
unwrap/expectNonoErrorwhere requiredThe final item is unchecked: implementation preceded issue discussion because this was a
maintainer-directed change. The security tradeoff (raw TCP beyond localhost) was identified during
implementation and is disclosed here rather than in the issue thread.