[Bubblewrap/LXC] Address common network policy gaps - model 1#631
[Bubblewrap/LXC] Address common network policy gaps - model 1#631dhoehna wants to merge 4 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 |
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