Uh oh!
There was an error while loading. Please reload this page.
[WSLC] Fix network policy parity between one-shot and state-aware surfaces - #1042
[WSLC] Fix network policy parity between one-shot and state-aware surfaces#1042Soham Das (SohamDas2021) wants to merge 3 commits into
Conversation
|
Azure Pipelines: There may be pipelines that require an authorized user to comment /azp run to run. |
There was a problem hiding this comment.
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
| File | Description |
|---|---|
tests/scripts/run_wslc_state_aware_tests.ps1 | Adds port-forwarding E2E coverage. |
tests/configs/wslc_state_aware_provision_ports.json | Adds a port-mapping fixture. |
src/core/wxc_common/src/wire.rs | Extends the provision wire model. |
src/core/wxc_common/src/models.rs | Adds host-list presence detection. |
src/core/wxc_common/src/config_parser.rs | Updates WSLc policy validation. |
src/core/wxc_common/src/config_contract_adapters/dev/state_aware.rs | Converts state-aware port mappings. |
src/core/wxc_common/src/config_contract_adapters/dev/one_shot.rs | Reuses shared mapping conversion. |
src/core/wxc_common/src/config_contract_adapters/dev/common.rs | Centralizes mapping conversion. |
src/core/mxc_config_contract/src/dev/state_aware/provision/wslc.rs | Adds port mappings to the contract. |
src/core/mxc_config_contract/src/dev/state_aware/provision/mod.rs | Boxes the larger WSLc request. |
src/core/mxc_config_contract/src/dev/request.rs | Constructs the boxed request. |
src/backends/wslc/daemon/tests/daemon_ipc.rs | Updates daemon test configuration. |
src/backends/wslc/daemon/src/session_manager.rs | Applies mappings during provisioning. |
src/backends/wslc/common/src/wsl_container_runner.rs | Rejects every host-list form. |
src/backends/wslc/common/src/state_aware.rs | Validates and forwards mappings. |
src/backends/wslc/common/src/daemon_protocol.rs | Extends and versions daemon IPC. |
src/backends/wslc/common/src/container_steps.rs | Passes mappings to the SDK. |
sdk/node/tests/unit/wire-conformance-state-aware.test.ts | Checks SDK wire parity. |
sdk/node/src/state-aware-types.ts | Exposes provision port mappings. |
sdk/node/src/generated/wire.ts | Regenerates rolling wire types. |
sdk/node/src/generated/v0_9_0_alpha/wire.ts | Regenerates exact wire types. |
sdk/node/README.md | Documents SDK behavior. |
schemas/dev/mxc-config.schema.0.9.0-dev.json | Updates the rolling schema. |
schemas/dev/mxc-config.schema.0.9.0-alpha.json | Updates the exact schema. |
docs/wsl/wslc-state-aware.md | Documents state-aware forwarding. |
docs/wsl/wsl-container-getting-started.md | Documents fail-closed host lists. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Darren Hoehna (dhoehna)
left a comment
There was a problem hiding this comment.
Just some questions.
Uh oh!
There was an error while loading. Please reload this page.
22b08f1 to
61424e8CompareThere was a problem hiding this comment.
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 itsWSLC_ALL_FIELDS_REQUEST_JSONadapter/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 constructWslcProvisionPhasedirectly, so dropping this conversion would not be detected. AddportMappingsto that all-fields fixture and assertprovision.port_mappingsto cover the exact-parser path.
port_mappings: port_mappings.into_option().map(|mappings| {
mappings
.into_iter()
.map(convert_wslc_port_mapping)
.collect()
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
61424e8 to
5f71a99CompareUh oh!
There was an error while loading. Please reload this page.
| ## 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 }` |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
Uh oh!
There was an error while loading. Please reload this page.
| @@ -710,7 +710,7 @@ | |||
| "type": "string" | |||
| }, | |||
| "PortMapping": { | |||
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
5f71a99 to
2d9c2beCompareThere was a problem hiding this comment.
🟢 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
2d9c2be to
66f0202Compare| validate_wslc_port_mappings(mappings, "experimental.wslc.provision.portMappings") | ||
| .map_err(MxcError::policy_validation)?; |
There was a problem hiding this comment.
note: I guess this will conflict with your promotion PR
| /// A WSLC provision request. Boxed: its port-mapping list makes it much | ||
| /// larger than its siblings. |
There was a problem hiding this comment.
note: probably don't need the second part of the comment.
| 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. |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
note: I think this addition can be deleted right?
| 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), | ||
| } | ||
| } | ||
There was a problem hiding this comment.
question: since this PR doesn't remove wslc from experimental, does removing these make sense?
| } | ||
| #[test] | ||
| fn wslc_rejects_redundant_block_with_blocked_hosts() { |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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?
| /// 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 |
There was a problem hiding this comment.
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?
📖 Description
.github/copilot-instructions.md.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 — soblock+blockedHostsandallow+allowedHostswere neither rejected nor enforced. Replaced withhas_host_lists(), which is presence-based and fail-closed.2.
portMappingswas 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.proxygates 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 omitdefaultPolicy— and a proxy-carryingexec/start/stopwas rejected asmalformed_requestbefore dispatch. Both gates are now!state_aware; every state-aware phase already gates the proxy itself, so the errors surface aspolicy_validationand exec's cooperative proxy injection works. This last one was pre-existing onmain(verified against amainbuild producing byte-identical output).Validation
cargo fmt+clippy -D warnings(incl.-p wxc --features wslc)run_wslc_all_tests.ps1(one-shot + state-aware), 0 failures — including a real port-forward round-trip on a dynamically-allocated host portCloses#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 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