Skip to content

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

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:1491WslcSetContainerSettingsPortMappings). 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<PortMapping> to the daemon ProvisionConfig (daemon_protocol.rs:75).
  • Thread it: state_aware.rs::provisionProvisionConfigsession_manager.rscreate_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.

Activity

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

Metadata

Metadata

Labels

Area-Executor-SchemaExecutor schema definitionsArea-SDK-ApiSDK API surfaceArea-SDK-ConfigurationSDK configuration layerContainer-WSLCIssues relating to WSL ContainersIssue-BugSomething isn't workingOS-WindowsWindowsPriority2High-impact issue affecting key functionality; workaround exists, fix in next release.

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions