[Bubblewrap/LXC] Address common network policy gaps - model 1#631
[Bubblewrap/LXC] Address common network policy gaps - model 1#631dhoehna wants to merge 6 commits into
Conversation
…(AB#62830559) - resolve_host returns dual-stack; add ip6tables v6 chain mirroring the v4 chain. - CIDR (v4/v6) passthrough; per-rule --dport and -p tcp/udp/icmp via new EgressRule model field. - Pure rule-builder helpers with unit tests; update legacy IPv6-drop tests to dual-stack. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3b78bec0-e139-4cfd-9c10-092ef986d4f4
There was a problem hiding this comment.
Pull request overview
This PR extends the LXC firewall enforcement path to support a richer, dual-stack egress network policy model (IPv4 + IPv6), including CIDR destinations and per-rule protocol/port filtering, while preserving the legacy allowed/blocked host lists.
Changes:
- Added
ContainerPolicy.egress_ruleswith supportingEgressRule,Protocol, andRuleActiontypes inwxc_common. - Updated the LXC iptables enforcement implementation to build parallel
iptables+ip6tableschains and to preserve IPv6 literals/AAAA resolution results. - Refactored rule construction into helper functions returning argument vectors, with unit tests for the new rule-building behavior.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
| src/core/wxc_common/src/models.rs | Introduces the egress_rules policy model and supporting enums/structs on ContainerPolicy. |
| src/backends/lxc/common/src/network_iptables.rs | Implements dual-stack (iptables/ip6tables) rule application, CIDR/protocol/port handling, and adds unit tests for rule argument construction. |
Resolves the five Copilot reviewer comments: - resolve_host doc comment now accurately describes CIDR validation (address parses + prefix in range; host bits are not required to be zero since iptables/ip6tables apply the mask). - protocol_arg is address-family aware: IPv6 ICMP rules use `ipv6-icmp` (ip6tables rejects `icmp`). - ICMP rules never emit `--dport`; the port dimension is collapsed for portless protocols so no invalid or duplicate rules are generated. - FORWARD hook and its cleanup now match container egress by input interface (`-i <veth>`) instead of `-o <veth>`, and the delete matches the insert. Adds unit tests for IPv4/IPv6 ICMP protocol naming and ICMP dport suppression. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
PR description only calls out LXC- "Adds IPv6, CIDR, port, and protocol filtering to the LXC iptables enforcement path.", but this is for Bwrap as well, please update? |
| vec![ | ||
| "-A", chain_name, "-p", "udp", "--dport", "53", "-j", "ACCEPT", | ||
| ], | ||
| vec![ | ||
| "-A", chain_name, "-p", "tcp", "--dport", "53", "-j", "ACCEPT", |
There was a problem hiding this comment.
These ACCEPT udp/tcp port 53 to any destination, applied to both chains. Under default: Block, this is a standing DNS-tunneling exfiltration path- exactly the threat the GA doc’s default-deny targets and the PR now extends it to IPv6. Pre-existing for v4, but this is the natural place to tighten.
Suggest: scope the DNS ACCEPT to the container’s configured resolver(s), not 0.0.0.0/0 / ::/0.
There was a problem hiding this comment.
Agreed this is worth tightening, and thanks for flagging it. Two constraints on doing it inside this PR: (1) it's pre-existing v4 behavior, and (2) there is no resolver/nameserver field on ContainerPolicy today, so "the container's configured resolver(s)" isn't expressible yet — scoping the port-53 ACCEPT to specific resolvers needs a new config surface. The default-deny still drops all non-DNS egress and the ACCEPT is limited to udp/tcp dport 53, but I agree it remains a residual DNS-exfil channel.
I'd rather land resolver-scoping as a dedicated follow-up that (a) adds a dnsServers/resolver policy field and (b) scopes both the v4 and v6 port-53 ACCEPTs to it, instead of hard-coding a resolver here. Leaving this open to track it — I can file it as its own work item if you'd like.
| .map(|ip| ip.to_string()) | ||
| .collect(), | ||
| Err(_) => Vec::new(), | ||
| fn build_policy_rule_args(chain_name: &str, policy: &ContainerPolicy) -> FirewallRuleArgs { |
There was a problem hiding this comment.
allow-before-deny ordering has no deny-precedence. build_policy_rule_args appends allowed before blocked, then egress_rules in author order. With
iptables first-match-wins, an IP in both lists is ACCEPTed, and egress_rules carry no deny priority, contrary to GA D4 (deny-wins). This is captured in #62830341, but since this PR introduces the mixed allow/deny model, flag it so the ordering is reconciled rather than assumed correct in the interim.
There was a problem hiding this comment.
Flagged in 96af8f9: added a NOTE on build_policy_rule_args documenting that rules are emitted allow-list -> block-list -> egress_rules (author order) and applied first-match-wins, so this model-1 change does not implement deny-precedence — a destination present in both lists is ACCEPTed, and egress_rules carry no deny priority. Reconciling to the GA "deny-wins" ordering across the combined allow/deny model stays owned by net-model-2 (AB#62830341), as noted in the PR's Coupling section. Leaving this thread open so the interim behavior stays tracked until that reconciliation lands.
|
Lets add a test config jsons for bwrap and lxc that exercises ipv6 filtering and CIDR ranges. |
…-proto Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e5d2aa5b-7f04-4e4d-83d3-a02efe7020ab
… fail-closed egress default - network_iptables: probe ip6tables once and skip the parallel v6 chain (with a warning) when the binary is absent or IPv6 is disabled, so IPv4-only hosts no longer fail the whole firewall setup. - network_iptables: roll back partially-created chains/FORWARD hooks on any apply error so a retry does not hit "chain already exists" and leak state; share the teardown path with remove_firewall_rules. - models: EgressRule default action is now Deny (fail-closed) so an under-specified egress rule cannot silently widen access. - network_iptables: document the interim allow-before-deny ordering (deny-precedence reconciliation tracked by AB#62830341). - tests: add lxc + bubblewrap network configs exercising IPv6 and CIDR host filtering. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
@SohamDas2021 thanks for the review — addressed the two non-inline items in 96af8f9: Re: "PR description only calls out LXC ... but this is for Bwrap as well" Re: "add test config jsons for bwrap and lxc that exercises ipv6 filtering and CIDR ranges"
Both validate against the dev schema ( Also pushed the three inline fixes (ip6tables probe, partial-apply rollback, fail-closed |
🧪 Local test re-verification — 2026-07-17Re-ran the test suites locally at branch tip Windows host (
Linux (WSL2 Ubuntu-24.04,
Note: the |
The 'SDK Integration Tests (linux)' job failed on the pre-existing proxy test 'should enforce allowedHosts at the proxy layer' because the allowed-host sentinel 'curl https://api.github.com/zen' hit a GitHub API 403 rate-limit on the shared runner IP (two earlier tests in the same job fetched the same URL successfully). This is environmental and unrelated to this PR, which only touches the iptables/ip6tables enforcement path (models.rs is additive-only; the proxy allowlist path is untouched). Empty commit to re-run CI on a fresh runner. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔁 PR processing — 2026-07-20 (branch tip
|
|
✅ CI is green after the re-trigger — the |
…et-model1-ipv6-cidr-port-proto
Linked work item: AB#62830559 — [Bubblewrap/LXC] Address common network policy gaps - model 1
Summary
Adds IPv6, CIDR, port, and protocol filtering to the shared iptables/ip6tables enforcement path. This applies to both the LXC and Bubblewrap backends, since Bubblewrap reuses
lxc_common::network_iptables::NetworkIptablesManagerfor host-level network filtering.resolve_hostnow returns dual-stack (IPv4 + IPv6); IPv6 literals / AAAA results are retained instead of dropped.run_ip6tableshelper + a parallel v6 chain mirroring loopback / ESTABLISHED,RELATED / DNS / default-policy, with anip6tables -I FORWARDhook and matching cleanup.std::net), routed to the correct table by address family.--dport) and protocol (-p tcp/udp/icmp) enforcement via a new internalegress_rulesmodel field; the legacyallowed_hosts/blocked_hostspath is preserved.resolve_hostdoc comment were updated to the new dual-stack behavior.Model
ContainerPolicy.egress_rules: Vec<EgressRule>(+EgressRule,Protocol,RuleAction), default empty.Validation
cargo fmt --all -- --check;cargo clippy --target x86_64-unknown-linux-gnu -p lxc_common -D warnings— cleancargo test -p wxc_common— 395 passedcargo check --target x86_64-unknown-linux-gnu -p lxc_common --tests— passCoupling
EgressRule/Protocol/RuleActionare also introduced independently by the schema PR (AB#62830582) and net-model-2 (AB#62830341); merge-time reconciliation is expected. The JSON parser foregress_rulesis intentionally owned by the schema PR.Review follow-ups (round 2)
apply_firewall_rulesnow tears down any partially-created chains / FORWARD hooks (shared teardown withremove_firewall_rules), so a retry no longer trips over "chain already exists".EgressRulenow defaults toaction: denyso an under-specified rule cannot silently widen access.build_policy_rule_args; deny-precedence reconciliation stays owned by AB#62830341.tests/configs/lxc_network_ipv6_cidr.jsonandtests/configs/bubblewrap_network_ipv6_cidr.jsonexercising IPv6 + CIDR host filtering.Microsoft Reviewers: Open in CodeFlow