Skip to content

Commit dea2780

Browse files
committed
fix(mxc): allow explicit sandbox-local TCP loopback
- Add pc_allow_loopback without enabling host-loopback or private-network ingress. - Check effective loopback permission when forwarding, while allowing sandbox creation. - Update the WebSocket demo configuration and listener readiness checks. - Add driver and native regression coverage and document the network grants. Signed-off-by: Akber Raza <akberr@nvidia.com>
1 parent d1ae20a commit dea2780

13 files changed

Lines changed: 589 additions & 184 deletions

File tree

‎.agents/skills/build-openshell-mxc-windows/SKILL.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,14 @@ architecture-appropriate task is therefore safe without real MXC hardware but
194194
must not be described as wholly skip-safe on supported hardware. Neither task
195195
is part of `windows:ci`'s ordered contract, so invoke it explicitly.
196196

197+
The native suite also verifies `pc_allow_loopback` with no sandbox network
198+
rules or host proxy: same-sandbox TCP succeeds while host-loopback and the
199+
host's private interface remain blocked. Keep this distinct from broader
200+
`pc_allow_local_network` or governed-egress grants. The WebSocket lifecycle
201+
example uses `pc_allow_loopback = true`; capabilities alone do not supply it.
202+
Sandbox creation without the grant is allowed; a dynamic forward without an
203+
effective loopback grant is rejected before opening a relay listener.
204+
197205
For GB300 Windows ARM64 qualification, do not use the skip-safe developer task
198206
as release evidence. `windows:test:mxc-gb300:arm64` runs the required
199207
ProcessContainer subset and fails on any required `SKIP`.

‎architecture/windows.md‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,9 +137,16 @@ prevent another sandbox from using the OpenShell proxy, but they do not isolate
137137
unrelated host-loopback services or authenticate individual processes within
138138
one sandbox.
139139

140+
ProcessContainer sandbox-local TCP uses a separate, explicit
141+
`pc_allow_loopback` grant. It permits `127.0.0.1/32` egress while leaving
142+
private-network ingress and host-loopback denied, independently of governed
143+
egress or sandbox network rules. Capability names alone do not override MXC's
144+
deny-default networking. All network compatibility settings default to false.
145+
140146
Two gateway-wide ProcessContainer compatibility settings can deliberately
141147
broaden this boundary. `pc_network_allow` permits unrestricted outbound TCP, and
142-
`pc_allow_local_network` enables MXC local-network access. These are operator
148+
`pc_allow_local_network` enables loopback egress, private-network ingress, and
149+
host-loopback access. These are operator
143150
configuration choices, not sandbox policy grants, and they must not be treated
144151
as policy-governed egress.
145152

@@ -178,6 +185,11 @@ multiplexes `forward_open`, `forward_read`, `forward_write`, and
178185
authenticates each host-side forward. This capability does not add interactive
179186
shell or general exec support.
180187

188+
The relay needs sandbox-local TCP access to dial its target. Sandbox creation
189+
does not require that grant. Dynamic forwarding checks the gateway's explicit
190+
network grants and the sandbox's active proxy state before opening a relay
191+
listener, rejecting a missing grant with a diagnostic naming `pc_allow_loopback`.
192+
181193
The implementation boundary spans
182194
`crates/openshell-driver-mxc/src/control_channel.rs`,
183195
`crates/openshell-driver-mxc/src/relay.rs`, and

‎crates/openshell-driver-mxc/README.md‎

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,11 @@ pc_minimal_env = false
6666
# A sandbox with egress_proxy enabled but no explicit network rules rejects
6767
# this fallback instead of silently changing governed egress to allow-all.
6868
pc_network_allow = false
69-
# processContainer only: include "allowLocalNetwork": true in the MXC
70-
# network section. This compatibility setting broadens network access and is
71-
# not required by the BaseContainer qualification profile.
69+
# processContainer only: sandbox-local 127.0.0.1 TCP, with host-loopback and
70+
# private-network ingress denied. Independent of egress_proxy/network rules.
71+
pc_allow_loopback = false
72+
# processContainer only: loopback egress plus private-network ingress and
73+
# host-loopback access. Prefer pc_allow_loopback for sandbox-local forwarding.
7274
pc_allow_local_network = false
7375
# Pattern C governed egress. Requires backend = "process_container".
7476
egress_proxy = false
@@ -95,6 +97,24 @@ fallback. If it is combined with `egress_proxy = true`, a sandbox policy
9597
without explicit network rules is rejected synchronously rather than falling
9698
through from governed egress to `defaultPolicy = "allow"`.
9799

100+
`pc_allow_loopback = true` permits TCP between processes in the same sandbox
101+
through an MXC directional `127.0.0.1/32` egress rule. It keeps
102+
`ingress.default = "deny"` and `ingress.hostLoopback = "deny"`; it does not
103+
grant private-network or host-loopback access. It requires a native MXC build
104+
supporting directional CIDR rules and covers IPv4 only. The default is `false`.
105+
Neither `egress_proxy` nor sandbox `network_policies` is required, and no proxy
106+
listener, proxy environment, or CA share is created for this setting.
107+
`pc_capabilities = ["privateNetworkClientServer"]` alone does not override
108+
deny-default networking. `pc_allow_local_network` is a broader opt-in that
109+
also enables private-network ingress and host-loopback access.
110+
111+
Sandbox creation does not require a loopback grant. When a dynamic forward is
112+
requested, the driver checks whether this grant, an active governed-egress
113+
proxy, or a compatibility network grant supplies loopback access. Otherwise,
114+
the forward is rejected before opening a relay listener, with a diagnostic
115+
naming `pc_allow_loopback = true` and instructing the operator to recreate the
116+
sandbox after changing the gateway configuration.
117+
98118
Supply workload settings for each sandbox. The public config is keyed by driver name; the gateway forwards only the inner `mxc` object to the driver:
99119

100120
```powershell
@@ -107,7 +127,7 @@ The `command` array is required and preserves Windows argument boundaries. `cwd`
107127

108128
UI capability (Win32k syscalls, clipboard, input injection) is a `SandboxPolicy` concern, not gateway TOML -- see the Capability Matrix above and `docs/reference/policy-schema.mdx`'s `ui` section. Defaults to disabled (Win32k syscall lockdown) when a policy has no explicit `ui:` section; set `allow_graphical_ui: true` for agents that touch user32/gdi32 at startup even without opening a real window (e.g. Node.js-based targets like OpenClaw's gateway -- see `examples/e2e-policies/openclaw-gateway.yaml`).
109129

110-
`egress_proxy_addr` must be a `127.0.0.1:PORT` address. The port acts only as a configuration seed: for a sandbox policy with explicit network rules, the driver reserves a unique ephemeral loopback port. MXC 0.8 denies direct Internet egress and permits `127.0.0.1/32`; the driver points proxy-aware clients at the per-sandbox listener using environment variables. A policy without network rules keeps MXC's default network posture and receives neither a host listener nor proxy environment variables. The current governed-egress policy permits all loopback ports, so governed sandboxes can also reach unrelated host services bound to loopback. Control-channel forwarding does not require the legacy reverse-WebSocket connections to fresh host ports; restricting the generated policy is separate hardening work. Do not treat this path as loopback-service isolation. Live policy replacement or merge updates remain unsupported; delete and recreate the sandbox to apply a different policy.
130+
`egress_proxy_addr` must be a `127.0.0.1:PORT` address. The port acts only as a configuration seed: for a sandbox policy with explicit network rules, the driver reserves a unique ephemeral loopback port. MXC 0.8 denies direct Internet egress and permits `127.0.0.1/32`; the driver points proxy-aware clients at the per-sandbox listener using environment variables. A policy without network rules receives neither a host listener nor proxy environment variables; its effective network posture follows the explicit ProcessContainer settings above. The current governed-egress policy permits all loopback ports, so governed sandboxes can also reach unrelated host services bound to loopback. Control-channel forwarding does not require the legacy reverse-WebSocket connections to fresh host ports; restricting the generated policy is separate hardening work. Do not treat this path as loopback-service isolation. Live policy replacement or merge updates remain unsupported; delete and recreate the sandbox to apply a different policy.
111131

112132
When `etw_audit` is enabled, each gateway process owns a distinct real-time ETW
113133
session named from the stable `OpenShell-MXC-ETW` prefix, its process ID, and a

‎crates/openshell-driver-mxc/examples/e2e-policies/ws-agent.yaml‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@
88
# - Execute access to mxc-ws-agent.exe / openshell-supervisor-relay.exe and
99
# their runtime DLLs (provided by the AppContainer inheriting access to
1010
# system paths and share_dir).
11-
# - TCP socket binding on port 22000 (governed by pc_capabilities in the
12-
# gateway TOML, not by filesystem policy here).
13-
# - Private/loopback client access for openshell-supervisor-relay to dial
14-
# the driver's on-demand relay — granted by pc_capabilities in the TOML.
11+
# - Sandbox-local TCP on port 22000 so openshell-supervisor-relay can dial
12+
# the wrapped target — granted by pc_allow_loopback in the gateway TOML.
13+
# Private-network ingress and host-loopback access remain denied. Forwarding
14+
# between the gateway and relay uses inherited stdin/stdout handles.
1515
# - No writes to the host filesystem.
1616
#
1717
# workload directory (passed by run-ws-agent-test.ps1, default C:\work\openshell-mxc-ws)

‎crates/openshell-driver-mxc/examples/mxc-openclaw-gateway.toml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ version = 2
2222
wxc_exec_path = "C:\\mxc-kit\\bin\\wxc-exec.exe"
2323

2424
backend = "process_container"
25+
# MXC derives effective network capabilities from its directional policy.
26+
# This capability alone does not permit sandbox-local loopback connections.
2527
pc_capabilities = ["privateNetworkClientServer"]
2628
pc_least_privilege = false
2729
# UI capability is now a SandboxPolicy concern, not gateway TOML -- see

‎crates/openshell-driver-mxc/examples/mxc-ws-gateway.toml‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
#
2424
# Connectivity: entirely on-demand via `openshell forward service
2525
# --target-port 22000` (ForwardSink::open_dynamic_forward / the control
26-
# channel's "forward" op) -- there is no static/always-on bridge and no
26+
# channel's "forward_open" op) -- there is no static/always-on bridge and no
2727
# port pre-declared in this config beyond pc_relay_target_port's own
2828
# startup liveness check.
2929
#
@@ -43,16 +43,16 @@ wxc_exec_path = "C:\\mxc\\wxc-exec.exe"
4343
# One-shot AppContainer backend: genuinely default-deny at the OS level.
4444
backend = "process_container"
4545

46-
# AppContainer capability required for the server to bind a TCP socket on
47-
# 0.0.0.0:22000. "privateNetworkClientServer" allows the sandbox to act as
48-
# both a client and a server on private (home/work/loopback) networks.
49-
pc_capabilities = ["privateNetworkClientServer"]
46+
# The workload needs sandbox-local TCP, not private/LAN or host-loopback access.
47+
# AppContainer capabilities alone do not override MXC directional network policy.
48+
pc_capabilities = []
49+
pc_allow_loopback = true
5050

5151
# process_container only: keep standard privilege level (not LPA).
5252
pc_least_privilege = false
5353

54-
# No governed Internet egress is needed. The relay reaches the driver's
55-
# on-demand private-interface listener through privateNetworkClientServer above.
54+
# No governed Internet egress is needed. The control channel carries forwarding
55+
# traffic through inherited handles; the relay dials the target on sandbox loopback.
5656
egress_proxy = false
5757
egress_proxy_addr = ""
5858

‎crates/openshell-driver-mxc/examples/run-ws-agent-test.ps1‎

Lines changed: 14 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,8 @@ function Register-Cli {
268268
function Wait-PortOpen([int]$port, [int]$seconds) {
269269
$deadline = (Get-Date).AddSeconds($seconds)
270270
while ((Get-Date) -lt $deadline) {
271-
if (Get-NetTCPConnection -State Listen -LocalPort $port -ErrorAction SilentlyContinue) {
271+
if (@([System.Net.NetworkInformation.IPGlobalProperties]::GetIPGlobalProperties().GetActiveTcpListeners() |
272+
Where-Object { $_.Port -eq $port }).Count -gt 0) {
272273
return $true
273274
}
274275
Start-Sleep -Milliseconds 500
@@ -279,7 +280,8 @@ function Wait-PortOpen([int]$port, [int]$seconds) {
279280
function Wait-PortClosed([int]$port, [int]$seconds) {
280281
$deadline = (Get-Date).AddSeconds($seconds)
281282
while ((Get-Date) -lt $deadline) {
282-
if (-not (Get-NetTCPConnection -State Listen -LocalPort $port -ErrorAction SilentlyContinue)) {
283+
if (@([System.Net.NetworkInformation.IPGlobalProperties]::GetIPGlobalProperties().GetActiveTcpListeners() |
284+
Where-Object { $_.Port -eq $port }).Count -eq 0) {
283285
return $true
284286
}
285287
Start-Sleep -Milliseconds 500
@@ -424,12 +426,10 @@ try {
424426
}
425427
Ok "wxc-exec: $WxcExecPath"
426428

427-
# A real run exercises process_container with egress_proxy disabled
428-
# (mxc-ws-gateway.toml). The sandbox connects directly to the driver's
429-
# route-selected private-interface relay listener through the
430-
# privateNetworkClientServer capability; the governed host CONNECT
431-
# proxy is not part of this qualification path. Elevation is not
432-
# required here; keep logging the elevation state for diagnostics only.
429+
# A real run exercises process_container with pc_allow_loopback enabled
430+
# and egress_proxy disabled. The sandbox relay dials its local target;
431+
# gateway traffic uses inherited handles without a host-network callback.
432+
# Keep logging elevation state for diagnostics only.
433433
$wid = [Security.Principal.WindowsIdentity]::GetCurrent()
434434
$wp = New-Object Security.Principal.WindowsPrincipal($wid)
435435
$admin = $wp.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
@@ -612,6 +612,8 @@ try {
612612
if (-not $Mock) {
613613

614614
Step "Wait for WebSocket server on port $WsPort"
615+
# Host-to-sandbox loopback is denied. Observe the listener without
616+
# connecting to it; the echo check below exercises the actual relay path.
615617
$serverUp = Wait-PortOpen -port $WsPort -seconds 30
616618
if ($serverUp) {
617619
Record "server-port-open" $true "port $WsPort is listening"
@@ -624,21 +626,12 @@ try {
624626
Record "server-port-open" $false $detail
625627
}
626628

627-
# Cross-check server-port-open against openshell-supervisor-relay's own
628-
# confirmation, from inside the sandbox: it detects target-port
629-
# readiness itself (wait_for_port_ready, gated by the "launch"
630-
# handshake) and logs it, forwarded into the gateway log the same way
631-
# as every other wxc-exec stdout/stderr line. No share_dir file
632-
# needed -- this is the same information the marker file used to
633-
# carry, just sourced from the spawner's own diagnostic instead.
629+
# Cross-check the listener against the relay's acknowledgement of the
630+
# driver's target_ready handshake. Readiness does not require the host
631+
# to dial the sandbox's listener directly.
634632
if ($serverUp) {
635633
Step "Verify spawner's own port-ready confirmation (gateway log)"
636-
# The spawner's own polling (wait_for_port_ready, 300ms interval)
637-
# runs independently of this script's Wait-PortOpen above -- its
638-
# log line can land a couple of seconds after the raw TCP connect
639-
# already succeeded (observed up to ~2.3s). Poll for it rather
640-
# than checking once immediately, or this races and fails spuriously.
641-
$readyPattern = "port $WsPort ready after"
634+
$readyPattern = "host confirmed target listener on port $WsPort"
642635
$readyDeadline = (Get-Date).AddSeconds(15)
643636
$readyOk = $false
644637
while ((Get-Date) -lt $readyDeadline -and -not $readyOk) {

0 commit comments

Comments
 (0)