Repository navigation
[Bubblewrap] Network policy follow-ups: incomplete parser/validate() pairs, unusable IPv6 rules, and misleading diagnostics #955
Description
Relevant area(s)
Linux (Bubblewrap backend)
Brief description of your issue
Five follow-ups on the Bubblewrap network policy path. Items 1 and 2 are
incomplete parser/validate() pairs; items 3-5 are correctness and diagnostic
gaps found in review of the firewall-enforcement change.
None of these is a fail-open, and none weakens the enforced posture. Items
1, 2, 4 and 5 are error-reporting or robustness defects; item 3 fails closed
(traffic that cannot flow) plus an inaccurate doc claim.
Bubblewrap's network gates are deliberately duplicated across two entry points —
the JSON config parser (config_parser.rs) and the runner's validate()
(bwrap_runner.rs) — because a Rust caller can hand mxc_engine a hand-built
ExecutionRequest and never touch the parser. bwrap_runner.rs states this
outright:
The parser rejects both of these too, but only sees JSON configs; a Rust
caller can build anExecutionRequestand reach the runner directly, so the
checks are repeated here rather than assumed.
1. The D3 hostname rule-address check exists only in the runner.
validate() rejects non-IP/CIDR rule addresses via EgressPlan::for_policy
(src/backends/bubblewrap/common/src/bwrap_runner.rs). There is no
config_parser.rs twin, unlike the neighbouring unenforced-host-rules and
local-network gates, which exist in both.
Both callers are still rejected, so this is purely about which error the user
sees and when: backend_error at validate rather than config_parse at parse
time.
2. The proxy + firewall rejection exists only in the parser.
config_parser.rs rejects network.proxy combined with
network.enforcementMode of firewall or both on Bubblewrap. validate()
has no twin, and cannot reach one as written:
ResolvedNetworkMode::from_request tests proxy_active first
(src/backends/bubblewrap/common/src/bwrap_command.rs) and returns
ProxyOnly, so enforcementMode is never consulted on that path.
A programmatic caller therefore has an explicitly-set enforcementMode
silently discarded. The resulting posture is not weaker — ProxyOnly is a
private netns with egress closed to everything but the proxy endpoint — but the
caller is never told their field was ignored.
3. IPv6 rule addresses are accepted, but the namespace has no IPv6.
RuleAddress::parse accepts IPv6 literals and programs ip6tables rules, but
the supervisor launches slirp4netns without --enable-ipv6
(src/backends/bubblewrap/common/src/proxy_network.rs), and slirp4netns
disables IPv6 by default. An IPv6 entry in allowedHosts therefore validates
and installs successfully but can never carry traffic.
This fails closed, so it is not a bypass. The problem is the silent
impossibility plus an inaccurate doc claim: docs/bwrap-support/bubblewrap-backend.md
says "Both families are covered", which is untrue for the allow direction and
contradicts the same file's statements elsewhere that IPv6 egress is denied
outright.
Fix by either enabling/probing IPv6 in the slirp launch, or rejecting IPv6 rule
addresses and correcting the doc.
4. The shared dependency probe emits proxy-specific advice.
probe_dependencies() reports failures as "network.proxy requires
'slirp4netns'" and advises the caller to "omit network.proxy"
(src/backends/bubblewrap/common/src/proxy_network.rs). Firewall-only requests
now enter the same branch, so a caller who never set network.proxy is told to
remove it.
Generalize the wording to private-network enforcement, or pass the resolved
mode so firewall callers get actionable text.
5. rule_install_timeout has no startup headroom.
The budget is XTABLES_LOCK_WAIT * command_count, and its doc comment says it
covers the phase that "begins once slirp is up". In practice attach() starts
the timer before slirp signals readiness, so slirp startup is charged against
a budget sized only for the aggregate -w lock waits. If every rule waited near
its limit, an otherwise successful installation could exceed the deadline.
Only reachable under pathological lock contention (the floor is 5s x 8 = 40s),
but the fix is to add explicit startup headroom rather than using the exact
lock-wait sum, and to correct the comment.
Two further dimensions, found by a second review pass:
- The budget has no upper bound.
command_count()grows with the caller's
allowedHosts/blockedHosts, which are unbounded, so a large policy scales
the deadline arbitrarily. - It sits outside the caller's timeout.
attach()runs before the
BwrapChildtimeout derived fromrequest.script_timeoutis constructed, so
a large policy plus a stalled xtables lock can block sandbox creation for far
longer than the caller asked for.
So the fix should also cap the setup budget (or the host-list size) and bound
network setup by the request's overall timeout.
6. The live inbound test asserts only the IPv4 ingress chain.
render_filter_payloads emits the MXC_INGRESS declaration and the
-A INPUT -j MXC_INGRESS hook for both families, and the supervisor
restores the v6 payloads with ip6tables-restore
(src/backends/bubblewrap/common/src/proxy_network.rs).
tests/scripts/run_bwrap_inbound_deny_test.sh inspects only iptables, so no
live test pins the v6 chain's content.
The rendering-to-kernel hop itself is already covered: that ip6tables-restore
loop runs before slirp.ready is written and exits 1 on failure, so a v6
payload the kernel rejects aborts startup and fails every Bubblewrap network
test. What is missing is an explicit assertion that the v6 chain exists with the
expected rules and is hooked to INPUT, guarding against a future change that
silently stops rendering the family.
This should land with item 3: enabling IPv6 requires the RFC 4890 ICMPv6 ingress
exemptions, and the same test would need to cover them.
Steps to reproduce
Item 1 - with a schema 0.8.0-alpha config:
{
"containment": "bubblewrap",
"network": {
"defaultPolicy": "block",
"enforcementMode": "firewall",
"allowedHosts": ["example.com"]
}
}Run it through lxc-exec --experimental.
Item 2 - build an ExecutionRequest programmatically (via mxc-sdk or
mxc_engine directly, bypassing the parser) with both network.proxy enabled
and network_enforcement_mode = Firewall, then call the Bubblewrap runner.
Item 3 - run a schema 0.8.0-alpha firewall config whose allowedHosts
contains an IPv6 literal, and observe that the destination is unreachable
despite the rule installing successfully.
Item 4 - on a host without slirp4netns, run a firewall-mode config that
sets no network.proxy, and read the error.
Item 5 - inspect rule_install_timeout against the attach() call site.
Item 6 - run sudo tests/scripts/run_bwrap_inbound_deny_test.sh and grep the
script for ip6tables.
Expected behavior
Item 1 - rejected by the parser as config_parse, consistent with the other
Bubblewrap network gates.
Item 2 - rejected by validate() with the parser's message verbatim, so the
JSON and programmatic paths agree.
Item 3 - either the IPv6 destination is reachable, or the rule address is
rejected and the limitation documented. Docs must not claim both families work
if they do not.
Item 4 - the diagnostic names the mechanism the caller actually requested.
Item 5 - the budget covers slirp startup plus rule installation, matching
its documented intent.
Item 6 - the live test asserts ip6tables -S MXC_INGRESS and the v6 INPUT
hook alongside the IPv4 ones, without requiring IPv6 connectivity.
Actual behavior
Item 1 - rejected later by validate() as backend_error.
Item 2 - accepted. enforcementMode is silently dropped and the request
resolves to ProxyOnly.
Item 3 - accepted and installed, but no IPv6 traffic can flow; docs claim
otherwise.
Item 4 - the error advises removing a field the caller never set.
Item 5 - the timer starts before slirp readiness with no headroom for it.
Item 6 - only iptables is inspected, so the v6 chain's content is never
verified against a live kernel.
Additional context
Suggested severity: low for items 1, 4, 5 and 6; low-to-medium for item 2
(an explicit, security-relevant field silently ignored); medium for item 3's
documentation claim, low for its runtime effect.
Each fix is small and should reuse the existing shared message constants so the
parser and runner paths cannot drift.
Items 3-5 were raised in review of the Bubblewrap firewall-enforcement PR and
deliberately deferred: none affects the enforced posture, and the PR was already
validated end-to-end. The two gaps that did affect behaviour in that review -
an external network.proxy combined with host lists being accepted on the
programmatic path, and the missing allowLocalNetwork parser twin - were fixed
in the PR itself rather than deferred here.
Item 6 was raised as a non-blocking test suggestion in review of the Bubblewrap
inbound default-deny PR (#964). It is a defense-in-depth assertion over a hop
that already fails closed, so it is deferred here rather than blocking that PR.
Activity
github-actions commentedon Aug 19, 2026
Triage complete.
Labels applied:
Area-Executor-Schema,Area-Executor-LXC,Area-Build-Rust(in addition to the existingOS-LinuxandIssue-Bug).Owner assigned: Soham Das (@SohamDas2021) (already assigned — Linux / LXC / Bubblewrap area).
Maintainers: Comment
/investigateto check whether this issue or bug is valid against the most current code. Copilot will also produce a report with the changes that will be needed. For a small, unambiguous documentation or test fix, it may also create one draft PR.[-][Bubblewrap] Network policy gates: two incomplete parser/validate() pairs report the wrong error or none at all[/-][+][Bubblewrap] Network policy follow-ups: incomplete parser/validate() pairs, unusable IPv6 rules, and misleading diagnostics[/+]github-actions commentedon Aug 19, 2026
Triage complete. This issue was already correctly labeled and assigned; no changes were needed.
OS-Linux,Area-Executor-LXC,Area-Executor-SchemaThe five items (incomplete parser/validate() pairs, unusable IPv6 rules, misleading dependency probe message, and timer headroom) are all scoped to the Bubblewrap network policy path on Linux.
github-actions commentedon Aug 19, 2026
Triage summary
This issue was already partially triaged. No new labels were added; the existing labels (
OS-Linux,Area-Executor-LXC,Area-Executor-Schema) correctly cover the Bubblewrap network policy follow-ups described.SohamDas2021is already assigned as the Bubblewrap/Linux owner.Needs-Triagewas not present on this issue, so no removal was needed.Maintainers: Comment
/investigateto check whether this issue or bug is valid against the most current code. Copilot will also produce a report with the changes that will be needed. For a small, unambiguous documentation or test fix, it may also create one draft PR.github-actions commentedon Aug 21, 2026
Triage summary
Labels applied:
Area-Test-Executor(added for item 6's missingip6tableschain assertion).The issue already carried
OS-Linux,Area-Executor-LXC, andArea-Executor-Schema, which correctly cover the Bubblewrap runner, config parser, and slirp/iptables enforcement path.SohamDas2021was already assigned as the Linux/Bubblewrap owner.Needs-Triagewas not present on the issue.Maintainers: Comment
/investigateto check whether this issue or bug is valid against the most current code. Copilot will also produce a report with the changes that will be needed. For a small, unambiguous documentation or test fix, it may also create one draft PR.SohamDas2021 commentedon Aug 26, 2026
Item 1 premise does not hold today.
It assumes the neighboring unenforced-host-rules and local-network gates "exist in both" layers; they do not —
config_parser.rshas only four Bubblewrap network gates, all proxy-related, and both bwrap_command.rs and bwrap_runner.rs state that the runner is "the only layer that checks what Bubblewrap can enforce," with external-proxy grandfathered as the sole exception. Adding the twin would invert that convention and forceRuleAddressout of the Linux-gated, crate-privatenetwork_rules(or a hand-duplicate inwxc_common) to buy only a different error code on a request that is already rejected.Item 3: the documentation half is already fixed on
main— four places now state the namespace has no IPv6, andthe_supervisor_leaves_ipv6_disabled_in_the_namespacepins that slirp gets no--enable-ipv6— leaving only the low-severity, fail-closed runtime effect. The linked PR instead removes the silence that was the real complaint —spawnnow warns, naming the IPv6 destinations a plan allows — so proposing item 3 be closed on that basis.