Skip to content

User/dahoehna/honoring inbound rule#627

Open
dhoehna wants to merge 8 commits into
microsoft:mainfrom
dhoehna:user/dahoehna/HonoringInboundRule
Open

User/dahoehna/honoring inbound rule#627
dhoehna wants to merge 8 commits into
microsoft:mainfrom
dhoehna:user/dahoehna/HonoringInboundRule

Conversation

@dhoehna

@dhoehna dhoehna commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

📖 Description

LXC now ACCEPTs or DROPs new inbound connections to the container depending on allowLocalNetwork, via a -m state --state NEW rule 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-container 127.0.0.1 are unaffected.

🔗 References

62864412

🔍 Validation

Unit tests (run here, green). The emitted iptables argument vectors are now produced by a pure build_firewall_rules(chain, policy, veth, allowed_ips, blocked_ips) -> Vec<Vec<String>> and pinned by unit tests in mod tests:

cd src && cargo test -p lxc_common network_iptables    # 17 passed (11 new + 6 existing)

These assert the NEW-inbound verb (--state NEW -j ACCEPT when allowLocalNetwork: true, -j DROP by default), an unconditional -i lo -j ACCEPT, that ESTABLISHED,RELATED precedes the NEW rule and the NEW rule precedes the terminal default, that the FORWARD hook is present only with a veth, plus the DNS and allowed/blocked host rules. They need no root, no iptables, 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 in run_lxc_all_tests.sh) applies each config on a real Linux host and asserts two layers:

  • [RULE] - dumps iptables -S MXC-<containerId> and asserts -m state --state NEW -j ACCEPT|DROP plus 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 test run above and must be executed on such a host. Full runbook: docs/lxc-support/testing-allowlocalnetwork.md.

Scope note. The FORWARD -o veth design governs forwarded inbound (peer/routed). A host->container direct-IP connection traverses INPUT, not FORWARD, and is out of scope here - that is the GA ingress.hostLoopback direction (docs/sandbox-policy/v2/networking.md), a separate milestone.

✅ Checklist

📋 Issue Type

  • Bug fix
  • Feature
  • Task

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 GitHub
Actions build; it runs on merge to main, and Microsoft reviewers with write access can trigger it
on a PR with /azp run. See docs/pull-requests.md.

If the dependency-feed-check check fails on a new dependency, the crate must be added to
the feed before the PR can pass. See docs/pull-requests.md
for the steps.

Microsoft Reviewers: Open in CodeFlow

@dhoehna
dhoehna requested a review from a team as a code owner July 9, 2026 21:21
Copilot AI review requested due to automatic review settings July 9, 2026 21:21

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 allowLocalNetwork in the main config schema example and LXC backend docs.
  • Added allowLocalNetwork to 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.

Comment thread src/backends/lxc/common/src/network_iptables.rs Outdated
Comment thread docs/lxc-support/lxc-backend.md Outdated
Comment thread docs/schema.md
@SohamDas2021

SohamDas2021 commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

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?

{
"network": {
"egress": {
"default": "deny",
"allow": [{ "to": [{ "cidr": "140.82.112.0/20" }], "ports": [{ "protocol": "tcp", "port": 443 }] }],
"deny": [{ "to": [{ "cidr": "10.0.0.0/8" }] }]
},
"ingress": { "hostLoopback": "deny" },
"proxy": { "http": "127.0.0.1:8080" }
}
}

@dhoehna

dhoehna commented Jul 10, 2026

Copy link
Copy Markdown
Contributor Author

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?

{ "network": { "egress": { "default": "deny", "allow": [{ "to": [{ "cidr": "140.82.112.0/20" }], "ports": [{ "protocol": "tcp", "port": 443 }] }], "deny": [{ "to": [{ "cidr": "10.0.0.0/8" }] }] }, "ingress": { "hostLoopback": "deny" }, "proxy": { "http": "127.0.0.1:8080" } } }

My changes are against the current schema. I can make a second PR to read ingress.hostloopback when the schema change is merged into main.

If I change the code to read ingress then I can't test this change until the schema goes in.

dhoehna and others added 3 commits July 10, 2026 12:26
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
Comment thread docs/lxc-support/lxc-backend.md Outdated
Comment thread src/backends/lxc/common/src/network_iptables.rs Outdated
Comment thread tests/scripts/run_lxc_local_network_test.sh
Comment thread docs/schema.md
"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"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

allowLocalNetwork

I'm concerned that this is very LXC specific. It doesn't seem like we have a consistent semantic here across the various platforms and backends.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@MGudgin MGudgin left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🕐

@microsoft-github-policy-service microsoft-github-policy-service Bot added the Needs-Author-Feedback Issue needs attention from issue or PR author label Jul 16, 2026
@dhoehna

dhoehna commented Jul 17, 2026

Copy link
Copy Markdown
Contributor Author

🧪 Local test re-verification — 2026-07-17

Re-ran the test suites locally at branch tip 2cc91e7 on a dev workstation. All green (0 failures).

Linux (WSL2 Ubuntu-24.04, x86_64-unknown-linux-gnu, cargo 1.97.0, isolated CARGO_TARGET_DIR):

  • cargo test -p lxc_common39 passed, 0 failed — the LXC iptables inbound/local-network enforcement ran on Linux (the original Validation ran the WSL E2E harness; here the crate unit tests ran).

The end-to-end harness (tests/scripts/run_lxc_local_network_test.sh) was not re-run here (it needs LXC + root + a bridge); only the lxc_common unit tests were executed.

@microsoft-github-policy-service microsoft-github-policy-service Bot added Needs-Attention Issue needs attention from Microsoft and removed Needs-Author-Feedback Issue needs attention from issue or PR author labels Jul 17, 2026
dhoehna and others added 3 commits July 20, 2026 15:00
…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
@dhoehna

dhoehna commented Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

@SohamDas2021 @MGudgin — flagging a roadmap vs GA-doc discrepancy on the LXC inbound (ingress.hostLoopback / allowLocalNetwork) enforcement chain, now that this PR implements it.

GA networking spec (docs/sandbox-policy/v2/networking.md) says every Linux backend enforces ingress via the iptables INPUT chain in the container's network namespace:

  • L148 — "Enforced on all backends: … WSLc/LXC/Bubblewrap via iptables INPUT, …"
  • L181 — "ingress.hostLoopback is enforced on all backends … WSLc/LXC/Bubblewrap via iptables INPUT"
  • L289 — "Inbound | Enforced via iptables INPUT chain. When hostLoopback: deny (default), all host/external inbound blocked."
  • L295 (LXC) — "Same model and enforcement as WSLc (… ingress.hostLoopback via INPUT)."

LXC roadmap (docs/linux-wsl-roadmap-june-2026.md L81, N2) says the opposite: "add iptables FORWARD rules on the container veth — DROP new inbound by default; ACCEPT from host loopback when ingress.hostLoopback: "allow"."

They disagree on the chain (host-side FORWARD-on-veth vs container-netns INPUT). A host→container-direct packet is host OUTPUT → container INPUT, and only hits host FORWARD if routed — so the container-netns INPUT chain is where the GA doc places it and where host-loopback inbound is actually caught. (The old FORWARD-on-veth -d <ip> rules were effectively dead for this case, since the inbound dest is the container's own IP.)

This PR implements the GA doc: an ingress-only chain in the container netns (via nsenter -t <init-pid> -n iptables) — -i lo ACCEPT, ESTABLISHED,RELATED ACCEPT, --state NEW {ACCEPT|DROP} keyed on allowLocalNetwork, terminal DROP, hooked with -I INPUT. Validated end-to-end against real unprivileged LXC containers in WSL (allow → reachable, deny → blocked).

Surfacing so we can correct the roadmap. Does container-netns INPUT look right to you both?

@dhoehna

dhoehna commented Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

@SohamDas2021 — separate, roadmap-internal inconsistency I hit while doing the above (not GA-doc related):

In docs/linux-wsl-roadmap-june-2026.md, the two backends that share the same NetworkIptablesManager are prescribed different chains for the same N2 inbound feature:

  • L81 (LXC, N2) — "add iptables FORWARD rules on the container veth …"
  • L164 (Bubblewrap, N2) — "add host-side iptables INPUT rules."
  • L484 (cross-cutting) — "Shared NetworkIptablesManager … serves LXC and Bwrap."

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.

@SohamDas2021

Copy link
Copy Markdown
Contributor

@SohamDas2021 — separate, roadmap-internal inconsistency I hit while doing the above (not GA-doc related):

In docs/linux-wsl-roadmap-june-2026.md, the two backends that share the same NetworkIptablesManager are prescribed different chains for the same N2 inbound feature:

  • L81 (LXC, N2) — "add iptables FORWARD rules on the container veth …"
  • L164 (Bubblewrap, N2) — "add host-side iptables INPUT rules."
  • L484 (cross-cutting) — "Shared NetworkIptablesManager … serves LXC and Bwrap."

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

@SohamDas2021

Copy link
Copy Markdown
Contributor

@SohamDas2021 @MGudgin — flagging a roadmap vs GA-doc discrepancy on the LXC inbound (ingress.hostLoopback / allowLocalNetwork) enforcement chain, now that this PR implements it.

GA networking spec (docs/sandbox-policy/v2/networking.md) says every Linux backend enforces ingress via the iptables INPUT chain in the container's network namespace:

  • L148 — "Enforced on all backends: … WSLc/LXC/Bubblewrap via iptables INPUT, …"
  • L181 — "ingress.hostLoopback is enforced on all backends … WSLc/LXC/Bubblewrap via iptables INPUT"
  • L289 — "Inbound | Enforced via iptables INPUT chain. When hostLoopback: deny (default), all host/external inbound blocked."
  • L295 (LXC) — "Same model and enforcement as WSLc (… ingress.hostLoopback via INPUT)."

LXC roadmap (docs/linux-wsl-roadmap-june-2026.md L81, N2) says the opposite: "add iptables FORWARD rules on the container veth — DROP new inbound by default; ACCEPT from host loopback when ingress.hostLoopback: "allow"."

They disagree on the chain (host-side FORWARD-on-veth vs container-netns INPUT). A host→container-direct packet is host OUTPUT → container INPUT, and only hits host FORWARD if routed — so the container-netns INPUT chain is where the GA doc places it and where host-loopback inbound is actually caught. (The old FORWARD-on-veth -d <ip> rules were effectively dead for this case, since the inbound dest is the container's own IP.)

This PR implements the GA doc: an ingress-only chain in the container netns (via nsenter -t <init-pid> -n iptables) — -i lo ACCEPT, ESTABLISHED,RELATED ACCEPT, --state NEW {ACCEPT|DROP} keyed on allowLocalNetwork, terminal DROP, hooked with -I INPUT. Validated end-to-end against real unprivileged LXC containers in WSL (allow → reachable, deny → blocked).

Surfacing so we can correct the roadmap. Does container-netns INPUT look right to you both?

PR out to update the roadmap doc- #675

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Needs-Attention Issue needs attention from Microsoft

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants