Skip to content

[WSLC] Fix network policy parity between one-shot and state-aware surfaces - #1042

Open
Soham Das (SohamDas2021) wants to merge 3 commits into
mainfrom
user/sodas/824-wslc-network-parity
Open

[WSLC] Fix network policy parity between one-shot and state-aware surfaces#1042
Soham Das (SohamDas2021) wants to merge 3 commits into
mainfrom
user/sodas/824-wslc-network-parity

Conversation

@SohamDas2021

@SohamDas2021Soham Das (SohamDas2021) commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

📖 Description


Summary

Three WSLc network-policy gaps where the one-shot and state-aware surfaces disagreed.

1. Redundant host lists failed open. The parser gated on needs_host_filtering(), which only inspects the list that tightens the default — so block + blockedHosts and allow + allowedHosts were neither rejected nor enforced. Replaced with has_host_lists(), which is presence-based and fail-closed.

2. portMappings was one-shot only. Added the state-aware twin on the provision phase, honoured as container port forwards and frozen post-provision (TCP only; ports must be unique and non-zero).

3. Two parser gates mislabelled state-aware errors. Both the host-list and network.proxy gates are written for one-shot, where a single config carries the whole policy. On state-aware the network mode is bound at provision, so later phases legitimately omit defaultPolicy — and a proxy-carrying exec/start/stop was rejected as malformed_request before dispatch. Both gates are now !state_aware; every state-aware phase already gates the proxy itself, so the errors surface as policy_validation and exec's cooperative proxy injection works. This last one was pre-existing on main (verified against a main build producing byte-identical output).

Validation

  • 1,896 Rust unit tests, 277 SDK tests, all 6 codegen/version gates green
  • cargo fmt + clippy -D warnings (incl. -p wxc --features wslc)
  • Live WSLc E2E on a WSL2 host: run_wslc_all_tests.ps1 (one-shot + state-aware), 0 failures — including a real port-forward round-trip on a dynamically-allocated host port

Closes#824

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

@SohamDas2021
Soham Das (SohamDas2021) requested review from a team and a balanced review from CopilotAugust 25, 2026 19:34
@SohamDas2021
Soham Das (SohamDas2021) requested a review from a team as a code ownerAugust 25, 2026 19:34
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

CopilotAI 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

Aligns WSLc one-shot and state-aware network-policy behavior.

Changes:

  • Rejects all unsupported WSLc host lists.
  • Adds state-aware provision-time TCP port mappings.
  • Improves state-aware proxy validation routing and test coverage.

Reviewed changes

Copilot reviewed 24 out of 26 changed files in this pull request and generated 1 comment.

Show a summary per file
FileDescription
tests/scripts/run_wslc_state_aware_tests.ps1Adds port-forwarding E2E coverage.
tests/configs/wslc_state_aware_provision_ports.jsonAdds a port-mapping fixture.
src/core/wxc_common/src/wire.rsExtends the provision wire model.
src/core/wxc_common/src/models.rsAdds host-list presence detection.
src/core/wxc_common/src/config_parser.rsUpdates WSLc policy validation.
src/core/wxc_common/src/config_contract_adapters/dev/state_aware.rsConverts state-aware port mappings.
src/core/wxc_common/src/config_contract_adapters/dev/one_shot.rsReuses shared mapping conversion.
src/core/wxc_common/src/config_contract_adapters/dev/common.rsCentralizes mapping conversion.
src/core/mxc_config_contract/src/dev/state_aware/provision/wslc.rsAdds port mappings to the contract.
src/core/mxc_config_contract/src/dev/state_aware/provision/mod.rsBoxes the larger WSLc request.
src/core/mxc_config_contract/src/dev/request.rsConstructs the boxed request.
src/backends/wslc/daemon/tests/daemon_ipc.rsUpdates daemon test configuration.
src/backends/wslc/daemon/src/session_manager.rsApplies mappings during provisioning.
src/backends/wslc/common/src/wsl_container_runner.rsRejects every host-list form.
src/backends/wslc/common/src/state_aware.rsValidates and forwards mappings.
src/backends/wslc/common/src/daemon_protocol.rsExtends and versions daemon IPC.
src/backends/wslc/common/src/container_steps.rsPasses mappings to the SDK.
sdk/node/tests/unit/wire-conformance-state-aware.test.tsChecks SDK wire parity.
sdk/node/src/state-aware-types.tsExposes provision port mappings.
sdk/node/src/generated/wire.tsRegenerates rolling wire types.
sdk/node/src/generated/v0_9_0_alpha/wire.tsRegenerates exact wire types.
sdk/node/README.mdDocuments SDK behavior.
schemas/dev/mxc-config.schema.0.9.0-dev.jsonUpdates the rolling schema.
schemas/dev/mxc-config.schema.0.9.0-alpha.jsonUpdates the exact schema.
docs/wsl/wslc-state-aware.mdDocuments state-aware forwarding.
docs/wsl/wsl-container-getting-started.mdDocuments fail-closed host lists.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment threadsrc/core/wxc_common/src/config_parser.rs
Comment threaddocs/wsl/wsl-container-getting-started.md Outdated
Comment threaddocs/wsl/wsl-container-getting-started.md Outdated
Comment threadsrc/backends/wslc/common/src/daemon_protocol.rs Outdated
Comment threadsrc/backends/wslc/common/src/state_aware.rs Outdated
Comment threadsrc/backends/wslc/common/src/state_aware.rs Outdated

@dhoehnaDarren Hoehna (dhoehna) 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.

Just some questions.

CopilotAI review requested due to automatic review settings August 25, 2026 23:27

CopilotAI 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

Copilot reviewed 24 out of 26 changed files in this pull request and generated 1 comment.

CopilotAI review requested due to automatic review settings August 25, 2026 23:31
@SohamDas2021
Soham Das (SohamDas2021)force-pushed the user/sodas/824-wslc-network-parity branch from 22b08f1 to 61424e8CompareAugust 25, 2026 23:31

CopilotAI 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

Copilot reviewed 24 out of 26 changed files in this pull request and generated 1 comment.

Suppressed comments (1)

src/core/wxc_common/src/config_contract_adapters/dev/state_aware.rs:107

  • The exact 0.9 state-aware adapter now maps port_mappings, but its WSLC_ALL_FIELDS_REQUEST_JSON adapter/convergence test still omits this field and never asserts the mapped value. The only E2E added here uses 0.8 (the rolling parser), while backend unit tests construct WslcProvisionPhase directly, so dropping this conversion would not be detected. Add portMappings to that all-fields fixture and assert provision.port_mappings to cover the exact-parser path.
 port_mappings: port_mappings.into_option().map(|mappings| {
mappings
.into_iter()
.map(convert_wslc_port_mapping)
.collect()

Comment threadtests/scripts/run_wslc_state_aware_tests.ps1
Comment threadsrc/backends/wslc/common/src/state_aware.rs Outdated
CopilotAI review requested due to automatic review settings August 26, 2026 16:59
@SohamDas2021
Soham Das (SohamDas2021)force-pushed the user/sodas/824-wslc-network-parity branch from 61424e8 to 5f71a99CompareAugust 26, 2026 16:59

CopilotAI 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

Copilot reviewed 25 out of 27 changed files in this pull request and generated no new comments.

@SohamDas2021Soham Das (SohamDas2021) changed the title [WSL] Fix network policy parity between one-shot and state-aware surfaces[WSLC] Fix network policy parity between one-shot and state-aware surfacesAug 27, 2026
Comment threaddocs/wsl/wsl-container-getting-started.md Outdated
## Port forwarding

`experimental.wslc.provision.portMappings` forwards host (Windows) ports to the container, mirroring
the one-shot `experimental.wslc.portMappings` surface. Each entry is `{ windowsPort, containerPort }`

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

question: (non-blocking): so WSLc has host to container networking but specifically only related to port redirection? If so I wonder if this should mean that wslc configs that want to use port mappings need to also do network.ingress.hostloopback : allow together. Up to you though if you want to keep portmappings a completely separate exception.

@SohamDas2021Soham Das (SohamDas2021)Sep 2, 2026

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Yes, that is correct.

However, coupling it to network.ingress.hostLoopback isn't expressible today: WSLc passes NetworkPolicySupport::LEGACY on every phase, so the whole 0.8 directional network.ingress section is rejected. Requiring it would mean first declaring INGRESS_DEFAULT | HOST_LOOPBACK, claiming a posture WSLC can't actually enforce. They're also different directions: hostLoopback governs whether the container can reach the host's loopback, whereas portMappings is inbound NAT into the container.

Comment threadsrc/backends/wslc/daemon/src/session_manager.rs
@@ -710,7 +710,7 @@
"type": "string"
},
"PortMapping": {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

question: not related to this file but do we need to add anything for the Rust SDK in terms of the types consumers will use to author a wlsc config?. OR does it look like it's already taken care of?

Here: https://github.com/microsoft/mxc/blob/main/src/core/mxc_engine/src/policy.rs#L504

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Types are already covered.

One-shot:WslcSection.port_mappings: Vec<(u16, u16)> is the authoring surface, and wire() emits protocol: "tcp".

State-aware: the Rust SDK is JSON-in/JSON-out by design here, run_state_aware_json takes the wire-format request, so there's no typed per-phase builder to extend. Callers write experimental.wslc.provision.portMappings directly, same as the other state-aware backends.

CopilotAI 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.

🟢 Approval recommended

The implementation consistently threads validated mappings through all layers and includes appropriate unit, contract, and live coverage.

Review details
  • Files reviewed: 26/28 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

Tighten the comments this branch introduced and correct three that were wrong.
The `block` + `blockedHosts` rejection was documented as a guard against
failing open. That reasoning is backwards: `map_network` maps `Block` to
`NetworkMode::None`, so ignoring a blocklist under that default leaves the
container with no networking at all -- strictly more restricted. Only `allow` +
`allowedHosts` can fail open, and the rewrite keeps that framing on the `allow`
case while giving both cases the reason that actually covers them: an
accepted-then-ignored list misstates what is enforced. The rejection itself was
already correct; only the stated rationale changed.
Two other comments claimed things the code does not do. The domain
`PortMapping.protocol` doc said `"tcp" or "udp"` with `Default: "tcp"`, but
`wire::TransportProtocol` declares no `udp` variant and the struct derives
`Default`, so the real default is `""` -- that `"tcp"` belonged to the wire
layer. Note the scope: UDP is supported in the wire model generally
(`NetworkProtocol` has `Tcp/Udp/Icmp/Any`, honored by LXC, Bubblewrap, and
PSEC); it is WSLc port forwarding specifically that is TCP-only. The
`WslcSection` doc in `mxc_engine::policy` said per-host rules "are accepted"
and "fail the run at spawn"; they are rejected when the request is built, which
this branch widens to redundant lists.
That last correction had no test behind it, so this adds
`wslc_rejects_redundant_host_lists`. Both cases it covers pass the old
`needs_host_filtering` predicate and were accepted before this branch, so the
test fails against the previous behavior rather than restating it.
Also expands MDE to Microsoft Defender for Endpoint on first use in the WSL
getting-started doc, per review feedback.
Deliberately left long: the `WslcProvisionPhase` wire doc, which records which
knobs are absent on purpose and is emitted into two schema artifacts and two
generated TypeScript oracles; the `One-shot only:` paragraphs justifying the
`!state_aware` guards, which would otherwise read as oversights; and the
`--dry-run` note explaining why a mapping function is called for its validating
side effect.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 6b41b4f0-4bc3-4516-9a83-ab63d70b3af5
@SohamDas2021
Soham Das (SohamDas2021)force-pushed the user/sodas/824-wslc-network-parity branch from 2d9c2be to 66f0202CompareSeptember 3, 2026 15:55
Comment on lines +389 to +390
validate_wslc_port_mappings(mappings, "experimental.wslc.provision.portMappings")
.map_err(MxcError::policy_validation)?;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

note: I guess this will conflict with your promotion PR

Comment on lines +16 to +17
/// A WSLC provision request. Boxed: its port-mapping list makes it much
/// larger than its siblings.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

note: probably don't need the second part of the comment.

Comment on lines +1644 to +1647
fn wslc_rejects_redundant_host_lists() {
// A list matching the default enforces nothing, so before #824 these
// two were accepted and dropped. `allow` + allowedHosts is the harmful
// one: it reads as a restriction while leaving all egress open.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

note: Jeff brought this up the other day actually. I don't think it's a WSLc issue. Its a greater config issue. I'm thinking the logic for this should go in network_parser.rs since it's a greater config violation. Thoughts?


use crate::config_contract_adapters::dev::common::{
convert_filesystem, convert_network, convert_process, convert_telemetry, convert_version,
convert_wslc_port_mapping,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

note: I think this addition can be deleted right?

Comment on lines -213 to -231
fn convert_protocol(value: contract::TransportProtocol) -> wire::TransportProtocol {
match value {
contract::TransportProtocol::Tcp => wire::TransportProtocol::Tcp,
}
}

fn convert_wslc_port_mapping(value: contract::PortMapping) -> wire::PortMapping {
let contract::PortMapping {
windows_port,
container_port,
protocol,
} = value;
wire::PortMapping {
windows_port: windows_port.get(),
container_port: container_port.get(),
protocol: protocol.into_option().map(convert_protocol),
}
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

question: since this PR doesn't remove wslc from experimental, does removing these make sense?

}

#[test]
fn wslc_rejects_redundant_block_with_blocked_hosts() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

note: see comments above, I think this isn't a wslc issue and a config issue. In general we shouldn't allow default block + block hosts, and default allow + allow hosts. So we should have the network_parser.rs handle that rather than it being backend specific.

/// Validate + convert WSLc wire port mappings. Shared by the one-shot parser
/// and the state-aware provision phase so the two cannot drift. `path` is the
/// caller's JSON location; the error is a bare message each caller wraps.
pub fn validate_wslc_port_mappings(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

thought: I'm not sure model.rs is the right place to put validation code tbh. When I think about models I think of just the raw objects themselves. Is there maybe another place we can put this?

Comment on lines +665 to +667
/// one-shot-only sizing knobs (`cpuCount` / `memoryMb` / `gpu` / `storagePath`)
/// are deliberately absent: the daemon shares a single session across sandboxes
/// and does not apply per-sandbox sizing. `portMappings`, by contrast, is

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

thought: Oh this is what "sizing knobs" mean. That said, thinking about it some more

The daemon shares a single session across sandboxes and does not apply per-sandbox sizing

Would developers expect this? This seems a bit unexpected that I can do things with the one shot that I can't do with the state aware. This backend might potentially be the only one doing that. Would it be a big lift to re-align the two?

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

4 participants

@SohamDas2021@dhoehna@bbonaby