User/dahoehna/honoring inbound rule#627
Conversation
There was a problem hiding this comment.
Pull request overview
This PR aims to make the LXC backend honor the cross-platform allowLocalNetwork network policy knob when installing iptables rules, and updates documentation/examples to surface the field.
Changes:
- Updated LXC iptables rule emission to conditionally ACCEPT/DROP traffic based on
allowLocalNetwork. - Documented
allowLocalNetworkin the main config schema example and LXC backend docs. - Added
allowLocalNetworkto the sandbox policy v1 JSON example.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| src/backends/lxc/common/src/network_iptables.rs | Adds allowLocalNetwork-driven ACCEPT/DROP behavior in the iptables chain. |
| docs/schema.md | Documents allowLocalNetwork in the example config. |
| docs/sandbox-policy/v1/policy.md | Adds allowLocalNetwork to the policy example payload. |
| docs/lxc-support/lxc-backend.md | Documents how allowLocalNetwork affects LXC network policy enforcement. |
|
Are your changes addressing all the requirements in https://microsoft.visualstudio.com/OS/_workitems/edit/62864412/?view=edit? "When ingress.hostLoopback is "allow", accept connections from host loopback only" My understanding was that we would need the new config json schema changes to go in first and you will have a subsequent PR to address that? { |
My changes are against the current schema. I can make a second PR to read If I change the code to read |
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Adds a manual end-to-end test for the LXC backend's allowLocalNetwork
schema field:
- tests/helpers/netcheck/netcheck.rs: std-only TCP probe (serve/connect)
- tests/configs/lxc_local_network_{allow,deny}.json: Alpine 3.23 configs
- tests/scripts/run_lxc_local_network_test.sh: root-gated orchestrator that
builds netcheck (static musl), runs each config through lxc-exec, and
asserts the emitted iptables loopback verb (ACCEPT for allow, DROP for deny)
- docs/lxc-support/testing-allowlocalnetwork.md: runbook
Verified on WSL2 Ubuntu-24.04 + LXC 5.0.3: [RULE] pass=2 fail=0.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 3b78bec0-e139-4cfd-9c10-092ef986d4f4
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e5d2aa5b-7f04-4e4d-83d3-a02efe7020ab
| "proxy": { "localhost": 8080 }, // Loopback proxy port (processcontainer; bubblewrap) | ||
| // (use { "builtinTestServer": true } for the bundled | ||
| // testing-only proxy; requires --allow-testing-features) | ||
| "allowLocalNetwork": true // "true" or "false" |
There was a problem hiding this comment.
Fair, and I don't want to overclaim. This PR is scoped to making the LXC backend honor the legacy allowLocalNetwork per roadmap N2 (item 14); it does not unify semantics across backends.
The cross-backend unification is the GA ingress.hostLoopback design in docs/sandbox-policy/v2/networking.md:148, which defines one semantic for all backends ("host may reach sandbox-local listening sockets over loopback only ... Enforced on all backends: Windows via loopback rules, WSLc/LXC/Bubblewrap via iptables INPUT, Seatbelt via profile").
Worth flagging a real discrepancy that surfaced while doing this: that GA doc says LXC enforces via INPUT (loopback-only), whereas roadmap N2 says FORWARD on the veth - which is what this PR implements. Different chains, different paths (INPUT = host->sandbox-direct; FORWARD = forwarded/peer inbound). Someone owns reconciling N2 with the GA networking spec before ingress.hostLoopback lands, so I'm leaving this thread open as the tracking point for that cross-backend decision rather than resolving it.
🧪 Local test re-verification — 2026-07-17Re-ran the test suites locally at branch tip Linux (WSL2 Ubuntu-24.04,
The end-to-end harness ( |
…ingInboundRule # Conflicts: # docs/schema.md
…t-testable Gudge's review of PR microsoft#627 found the allowLocalNetwork/hostLoopback control was a no-op: the verb-flip was applied to `-i lo` in a chain hooked only at `-I FORWARD -o <veth>`, which forwarded-to-container packets never traverse. It also had no unit coverage and its docs disagreed. Enforcement (roadmap N2 / FORWARD design): - Restore loopback to an unconditional `-i lo -j ACCEPT` (the old conditional DROP was both inert and a latent localhost-break hazard). - Add `-m state --state NEW -j {ACCEPT|DROP}` after ESTABLISHED,RELATED so the field actually gates new inbound connections forwarded to the container, independent of defaultPolicy. Testability refactor (mirrors bubblewrap build_args / seatbelt build_profile): - Extract a pure build_firewall_rules(chain, policy, veth, allowed, blocked) -> Vec<Vec<String>> (no spawn, no DNS, no logging). - Move DNS resolution + host logging into resolve_hosts_logged; slim apply_firewall_rules to resolve -> build -> run. - 11 new unit tests pin the emitted argv (NEW verb, unconditional loopback, rule ordering, FORWARD hook presence, DNS, default policy, host rules). Harness (Gudge T6): - netcheck: read_exact the 4-byte PONG (a short read spuriously failed). - run_lxc_local_network_test.sh: [RULE] now asserts the --state NEW verb; [BEHAVIOR] connects from a PEER container so traffic is forwarded and actually governed by the rule, and asserts reachable/blocked (INCONCLUSIVE if the peer can't start). Registered in run_lxc_all_tests.sh. Docs (Gudge T4): align testing-allowlocalnetwork.md and policy.md with the emitted rules. lxc-backend.md already matched. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b6b3323b-7297-4b07-9e6e-ab4b220124e6
…pec)
Rework the LXC inbound (allowLocalNetwork / ingress.hostLoopback) control to
match the GA networking spec (docs/sandbox-policy/v2/networking.md), which
mandates enforcement in the container's iptables INPUT chain rather than the
host-side FORWARD hook the prior commit installed.
Why: a packet destined to a container socket traverses the CONTAINER's INPUT
chain inside its own netns, never the host's INPUT, and only the host's FORWARD
if routed. Host->container-direct traffic (host OUTPUT -> container INPUT) never
hit the old `-I FORWARD -o <veth>` hook, so it was ungoverned; the `-d <ip>`
allow/block rules on that inbound hook were also dead (inbound dest is the
container's own IP). This moves enforcement to where GA specifies.
network_iptables.rs (shared with Bubblewrap):
- build_firewall_rules now emits an ingress-only chain: -N; -i lo ACCEPT;
ESTABLISHED,RELATED ACCEPT; --state NEW {ACCEPT|DROP} per allowLocalNetwork;
terminal DROP (ingress default-deny, independent of egress defaultPolicy);
and -I INPUT -j chain gated on a `hook` flag.
- Execute via `nsenter -t <init-pid> -n iptables` (host iptables binary, no
dependency on iptables in the container image). netns_pid replaces the veth
field; no PID => chain built but unhooked, so the host INPUT is never touched
(Bubblewrap shared-net stays inert, unchanged).
- Drop the DNS and allowed/blocked -d rules and host resolution (egress-intent;
GA ingress has no CIDR peers). Rewrite unit tests for the INPUT design
(terminal always DROP; INPUT hook present only with a netns PID).
lxc_bindings.rs: add LxcContainer::init_pid() (parses `lxc-info -p`).
lxc_runner.rs: discover init PID -> set_netns_pid; register PID for cleanup.
signal_cleanup.rs: watchdog cleans up by netns PID instead of veth.
nanvix: fix a stale doc-comment reference to the removed resolve_host.
Docs: rewrite testing-allowlocalnetwork.md and the lxc-backend.md network
section for the container-INPUT/netns design (dump via nsenter; egress fields
are not enforced by this path). Behavioral harness dumps the chain inside the
netns (with a bounded retry to avoid racing rule application) and bumps the two
fixtures from the unsupported schema 0.4.0-alpha to 0.8.0-alpha.
Validated in WSL Ubuntu-24.04: cargo test -p lxc_common (46) and -p
bwrap_common (37) green; clippy clean; end-to-end harness [RULE] 2/2 and
[BEHAVIOR] 2/2 (allow reachable, deny blocked) against real unprivileged LXC
containers, confirming nsenter-into-netns iptables enforcement works.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: b6b3323b-7297-4b07-9e6e-ab4b220124e6
|
@SohamDas2021 @MGudgin — flagging a roadmap vs GA-doc discrepancy on the LXC inbound ( GA networking spec (
LXC roadmap ( They disagree on the chain (host-side FORWARD-on-veth vs container-netns INPUT). A host→container-direct packet is host This PR implements the GA doc: an ingress-only chain in the container netns (via Surfacing so we can correct the roadmap. Does container-netns INPUT look right to you both? |
|
@SohamDas2021 — separate, roadmap-internal inconsistency I hit while doing the above (not GA-doc related): In
So one shared manager would have to emit FORWARD for LXC but INPUT for Bwrap — and neither line matches the GA doc, which routes both through the container-netns INPUT chain. Should all the Linux backends (WSLc/LXC/Bubblewrap) standardize on the same chain (container-netns INPUT, per the GA doc) so the shared manager stays single-behavior? Happy to update the roadmap once you confirm the intended direction. |
PR out to address this- #675 |
PR out to update the roadmap doc- #675 |
📖 Description
LXC now ACCEPTs or DROPs new inbound connections to the container depending on
allowLocalNetwork, via a-m state --state NEWrule in the container's forwarded (-o <veth>) chain (roadmap N2, item 14). Loopback stays unconditionally accepted and ESTABLISHED/RELATED return traffic is accepted before the NEW rule, so container-initiated flows and in-container127.0.0.1are unaffected.🔗 References
62864412
🔍 Validation
Unit tests (run here, green). The emitted
iptablesargument vectors are now produced by a purebuild_firewall_rules(chain, policy, veth, allowed_ips, blocked_ips) -> Vec<Vec<String>>and pinned by unit tests inmod tests:These assert the NEW-inbound verb (
--state NEW -j ACCEPTwhenallowLocalNetwork: true,-j DROPby default), an unconditional-i lo -j ACCEPT, thatESTABLISHED,RELATEDprecedes the NEW rule and the NEW rule precedes the terminal default, that theFORWARDhook is present only with a veth, plus the DNS and allowed/blocked host rules. They need no root, noiptables, and no DNS, so they run in CI on any host.Rule + behavioral harness (requires a root LXC + WSL2 host - not run by CI).
tests/scripts/run_lxc_local_network_test.sh(now registered inrun_lxc_all_tests.sh) applies each config on a real Linux host and asserts two layers:[RULE]- dumpsiptables -S MXC-<containerId>and asserts-m state --state NEW -j ACCEPT|DROPplus the unconditional-i lo -j ACCEPT.[BEHAVIOR]- launches a peer container as the client so the connection is forwarded and therefore governed by the server chain's-o <veth>hook: reachable for allow, blocked for deny. A decisive mismatch fails the run;INCONCLUSIVE(never a silent pass) if the peer container cannot start.This harness is root-gated and needs an LXC + WSL2 bridge/NAT host, so it is not part of the
cargo testrun above and must be executed on such a host. Full runbook:docs/lxc-support/testing-allowlocalnetwork.md.Scope note. The
FORWARD -o vethdesign governs forwarded inbound (peer/routed). A host->container direct-IP connection traversesINPUT, notFORWARD, and is out of scope here - that is the GAingress.hostLoopbackdirection (docs/sandbox-policy/v2/networking.md), a separate milestone.✅ Checklist
Cargo.lock, thedependency-feed-checkcheck passes (see docs/pull-requests.md)📋 Issue Type
GitHub Actions runs the PR validation build automatically. The ADO pipeline
(
MXC-PR-Build) is the Azure version of the PR pipeline, kept in parity with the GitHubActions build; it runs on merge to
main, and Microsoft reviewers with write access can trigger iton a PR with
/azp run. See docs/pull-requests.md.If the
dependency-feed-checkcheck fails on a new dependency, the crate must be added tothe feed before the PR can pass. See docs/pull-requests.md
for the steps.
Microsoft Reviewers: Open in CodeFlow