microsoft / microsoft/mxc

[WSLC] One-shot vs state-aware network-policy parity: fail-open redundant host lists and dropped portMappings

Open
#824 2 comments 0 reactions 1 assignee Assigned to @SohamDas2021 View on GitHub
Area-Executor-Schema Area-SDK-Api Area-SDK-Configuration Container-WSLC Issue-Bug OS-Windows Priority2
Dominant language
Rust
Stars
1.3k
Forks
79
Avg merge
2d 7h
Merged PRs (30d)
117

Description

### Relevant area(s)

Windows (WSLc backend)

### Brief description of your issue

The one-shot WSLc flow (`WSLContainerRunner::start_container`) and the state-aware WSLc flow (`WslcStateAwareRunner` + daemon) diverge on two networking behaviors that should be identical. A full one-shot-vs-state-aware policy comparison found filesystem handling is identical and the cooperative proxy is enforced identically (same `apply_cooperative_proxy_env` on the workload; only the declaration site differs, which is structural and acceptable). Two real gaps remain:

**Gap 1 — Redundant host lists are handled inconsistently (fail-open in one-shot).**
Both flows already reject *meaningful* per-host filtering at parse time (`config_parser.rs:1064`, `needs_host_filtering()`, added in #681). But a *redundant* host list — `defaultPolicy: "block"` + `blockedHosts`, or `defaultPolicy: "allow"` + `allowedHosts` — passes that gate (`needs_host_filtering()` → false):
- One-shot `validate_runner` (`wsl_container_runner.rs:636`) also uses `needs_host_filtering()`, so it **accepts and silently ignores** the list.
- State-aware `reject_host_filtering` (`policy.rs:115`) rejects on **any** non-empty list.

This is a fail-open footgun: a user who writes `defaultPolicy: "allow"` + `allowedHosts: ["github.com"]` (a classic "I meant an allowlist" typo) gets **all** egress permitted in one-shot, with no error — the opposite of intent.

**Gap 2 — State-aware silently drops `experimental.wslc.portMappings`.**
One-shot forwards ports (`wsl_container_runner.rs:1491` → `WslcSetContainerSettingsPortMappings`). The state-aware provision config `WslcProvisionPhase` (`wire.rs:550`) deliberately omits `portMappings`, and because the `experimental` block is permissive (no `deny_unknown_fields`), a state-aware config carrying `portMappings` is **silently ignored** — no inbound forwarding, no error (`container_steps.rs:803` create_daemon_container passes `&[]`).

### Steps to reproduce

Gap 1:
1. Run a one-shot WSLc config with `network: { "defaultPolicy": "allow", "allowedHosts": ["github.com"] }`.
2. Observe the container starts and all egress is permitted (the `allowedHosts` list is ignored).
3. Run the same policy through the state-aware provision phase — it is rejected with `policy_validation`.

Gap 2:
1. Run a one-shot WSLc config with `experimental.wslc.portMappings` — the host→container port forward is configured.
2. Provide the same `portMappings` to a state-aware provision config.
3. Observe no port forwarding is set up and no error is raised.

### Expected behavior

- Gap 1: Both flows handle host lists identically, failing closed — any non-empty `allowedHosts`/`blockedHosts` on WSLc is rejected (WSLc cannot enforce per-host filtering).
- Gap 2: Both flows support `experimental.wslc.portMappings`, or state-aware explicitly rejects it rather than silently ignoring it.

### Actual behavior

- Gap 1: One-shot accepts and silently ignores redundant host lists (fail-open); state-aware rejects any non-empty list. Behavior differs.
- Gap 2: One-shot forwards ports; state-aware silently drops `portMappings`.

-----

### Proposed actions

**Gap 1 — make one-shot reject (align both flows to fail-closed):**
- Change the shared parser predicate `config_parser.rs:1064` from `policy.needs_host_filtering()` to reject any non-empty `allowed_hosts`/`blocked_hosts` for WSLc; reword the error message (drop the "allowedHosts with defaultPolicy='block'…" framing).
- Mirror the same predicate in one-shot `validate_runner` (`wsl_container_runner.rs:636`) for the parser-bypass path.
- Flip the redundant-list tests (`config_parser.rs` WSLc host-filtering tests, `wsl_container_runner.rs` `validate_runner_*` tests) to expect rejection; keep the bare-defaults-accepted tests.
- State-aware `reject_host_filtering` (`policy.rs:115`) already matches — no change.

**Gap 2 — make state-aware support `portMappings` (align to one-shot):**
- Add `port_mappings` to `WslcProvisionPhase` (`wire.rs:550`) and regenerate the dev schema + SDK wire types (`mxc_schema_gen`).
- Add `port_mappings: Vec` to the daemon `ProvisionConfig` (`daemon_protocol.rs:75`).
- Thread it: `state_aware.rs::provision` → `ProvisionConfig` → `session_manager.rs` → `create_daemon_container` (`container_steps.rs:803`), passing the slice into `ContainerSettings::build` (already supports `port_mappings`, `container_steps.rs:499`) instead of `&[]`.
- Add the SDK type (`sdk/node/src/state-aware-types.ts` `WslcProvisionConfig`) and an E2E state-aware port-mapping config.

Both are self-contained: Gap 1 is tiny (one predicate + tests); Gap 2 is a field threaded through the daemon wire protocol into container creation.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.