From d01cac227ce3994e008e69d4a305dbea7c87f457 Mon Sep 17 00:00:00 2001 From: Gudge Date: Tue, 6 Oct 2026 14:15:06 -0700 Subject: [PATCH] Retire pre-v0.9 compatibility scaffolding This PR removes obsolete contract and runtime compatibility markers now that executable requests start at 0.9.0-alpha. Published schema artifacts stay immutable while unsupported policy guides are removed. Details * Remove constant default-environment and strict-network provenance. * Prune unused wire fields and obsolete policy documentation. * Preserve exact-contract validation and supported request semantics. Tests * cargo fmt --all -- --check and cargo test --workspace --quiet: passed. * cargo check --workspace --all-targets --all-features --quiet: passed. * cargo clippy --workspace --all-targets --all-features --quiet -- -D warnings: passed. * Linux Bubblewrap runner: 30 passed; schema history: 9 passed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7570a662-a429-46d2-aa24-47fae6655cc4 Generated-with: gpt-6-sol --- README.md | 1 - docs/api-reference/dotnet/v1/types.md | 8 +- .../examples/0.8.0-schema.md | 8 +- docs/backends/process-container/networking.md | 7 +- .../process-container/os-version-support.md | 13 +- docs/backends/seatbelt/seatbelt-backend.md | 4 +- .../containment-configuration/0.7.0/policy.md | 579 ----------------- .../0.8.0/networking/networking.md | 583 ------------------ .../0.8.0/networking/schema-updates.md | 151 ----- .../containment-configuration/0.8.0/policy.md | 150 ----- docs/development/architecture/telemetry.md | 6 +- docs/development/architecture/versioning.md | 33 +- .../ci-validation-infrastructure.md | 4 +- .../guides/authoring-a-new-feature.md | 7 +- .../process-container-adding-os-features.md | 10 +- docs/examples.md | 16 +- sdk/dotnet/README.md | 2 +- sdk/node/README.md | 2 +- .../bubblewrap/common/bwrap_command.rs | 212 +------ .../bubblewrap/common/bwrap_runner.rs | 132 +--- .../bubblewrap/common/proxy_network.rs | 2 - .../src/backends/hyperlight/common/mod.rs | 2 +- .../isolation_session/common/policy.rs | 32 +- .../src/backends/lxc/common/lxc_runner.rs | 38 +- .../backends/seatbelt/common/default_env.rs | 23 +- .../seatbelt/common/seatbelt_runner.rs | 26 +- .../windows_sandbox/lifecycle/one_shot.rs | 2 +- .../windows_sandbox/lifecycle/state_aware.rs | 2 +- .../src/backends/wslc/common/process_env.rs | 2 - .../src/backends/wslc/common/state_aware.rs | 12 +- src/mxc-sdk/src/configs/process_container.rs | 5 - .../src/core/mxc_common/common_request_ir.rs | 3 - .../config_contract_adapters/dev/common.rs | 6 - .../config_contract_adapters/dev/one_shot.rs | 3 - .../dev/one_shot_tests/stable_candidate.rs | 21 +- .../dev/state_aware.rs | 9 - .../dev/state_aware_tests/exec.rs | 3 - .../dev/state_aware_tests/provision.rs | 8 +- .../config_contract_adapters/v0_9/common.rs | 6 - .../config_contract_adapters/v0_9/one_shot.rs | 3 - .../v0_9/one_shot_tests/stable_candidate.rs | 21 +- .../v0_9/state_aware.rs | 9 - .../v0_9/state_aware_tests/exec.rs | 3 - .../config_contract_adapters/v1_0/common.rs | 6 - .../config_contract_adapters/v1_0/one_shot.rs | 3 - .../v1_0/one_shot_tests/stable_candidate.rs | 21 +- .../v1_0/state_aware.rs | 9 - .../v1_0/state_aware_tests/exec.rs | 3 - .../src/core/mxc_common/config_parser.rs | 16 - .../src/core/mxc_common/default_env.rs | 24 +- src/mxc-sdk/src/core/mxc_common/models.rs | 78 +-- .../src/core/mxc_common/network_parser.rs | 6 - .../network_parser_ingress_default_tests.rs | 11 +- .../src/core/mxc_common/policy_identity.rs | 19 +- .../src/core/mxc_common/sandbox_process.rs | 2 +- .../src/core/mxc_common/script_runner.rs | 2 +- src/mxc-sdk/src/core/mxc_common/sdk_input.rs | 10 - src/mxc-sdk/src/core/mxc_common/validator.rs | 19 +- src/mxc-sdk/src/core/mxc_common/wire.rs | 37 +- src/mxc-sdk/src/core/mxc_engine/run.rs | 10 +- src/mxc-sdk/src/policy.rs | 4 - .../lib/WinProcessContainer.Common.ps1 | 2 +- .../run_lxc_network_no_network_test.sh | 7 +- 63 files changed, 195 insertions(+), 2263 deletions(-) delete mode 100644 docs/containment-configuration/0.7.0/policy.md delete mode 100644 docs/containment-configuration/0.8.0/networking/networking.md delete mode 100644 docs/containment-configuration/0.8.0/networking/schema-updates.md delete mode 100644 docs/containment-configuration/0.8.0/policy.md diff --git a/README.md b/README.md index 1b3b69921..fb2d549cc 100644 --- a/README.md +++ b/README.md @@ -334,7 +334,6 @@ Repository contributors can browse the [development documentation](docs/developm | [tests/scripts/README.md](tests/scripts/README.md) | Local and CI backend test suites | | [Host preparation](docs/backends/process-container/host-prep.md) | Windows host preparation (`wxc-host-prep.exe`) | | [Diagnostics](docs/development/guides/diagnostics.md) | Diagnostic logging and ETW | -| [Containment configuration 0.7.0](docs/containment-configuration/0.7.0/policy.md) | Containment configuration 0.7.0 specification | | [Adding ProcessContainer OS features](docs/development/guides/process-container-adding-os-features.md) | Windows AppContainer / BaseContainer developer guide | | [docs/backends/lxc/lxc-backend.md](docs/backends/lxc/lxc-backend.md) | LXC backend (Linux) | | [docs/backends/bwrap/bubblewrap-backend.md](docs/backends/bwrap/bubblewrap-backend.md) | Bubblewrap backend (Linux) | diff --git a/docs/api-reference/dotnet/v1/types.md b/docs/api-reference/dotnet/v1/types.md index 1c059743b..8bcd2cd18 100644 --- a/docs/api-reference/dotnet/v1/types.md +++ b/docs/api-reference/dotnet/v1/types.md @@ -785,7 +785,7 @@ public enum NetworkAction ## `Microsoft.Mxc.Sdk.V1.NetworkEgressPolicy` -Schema-0.8 outbound network policy. +Supported directional outbound network policy. ```csharp public sealed class NetworkEgressPolicy @@ -801,7 +801,7 @@ public sealed class NetworkEgressPolicy ## `Microsoft.Mxc.Sdk.V1.NetworkIngressPolicy` -Schema-0.8 inbound and host-loopback network policy. +Supported directional inbound and host-loopback network policy. ```csharp public sealed class NetworkIngressPolicy @@ -893,7 +893,7 @@ public sealed class NetworkRulePolicy ## `Microsoft.Mxc.Sdk.V1.NetworkRuntimeConfig` -Schema-0.8 runtime network values. +Supported runtime network values. ```csharp public sealed class NetworkRuntimeConfig @@ -1209,7 +1209,7 @@ Schema versions supported by this SDK release. ```csharp public static class SchemaVersions { - public const string Minimum = "0.6.0-alpha"; + public const string Minimum = "0.9.0-alpha"; public const string MaximumSupported = "1.1.0-alpha"; public const string LatestStable = "1.0.0"; } diff --git a/docs/backends/process-container/examples/0.8.0-schema.md b/docs/backends/process-container/examples/0.8.0-schema.md index 5fd66df0f..62a0b416a 100644 --- a/docs/backends/process-container/examples/0.8.0-schema.md +++ b/docs/backends/process-container/examples/0.8.0-schema.md @@ -2,12 +2,12 @@ > **Audience:** MXC consumers and developers -> Historical design reference: schema 0.8 is retired. For supported requests, +> Schema 0.8 is retired. For supported requests, > use [current ProcessContainer networking guidance](../networking.md). -See the shared [0.7-to-0.8 network schema comparison](../../../containment-configuration/0.8.0/networking/schema-updates.md) -for egress, ingress, and proxy endpoint changes. This page covers the -ProcessContainer-specific proxy peer. +For the supported egress, ingress, and proxy endpoint fields, see +[directional networking](../../../schema.md#directional-networking-supported-contracts). +The example below covers the ProcessContainer-specific proxy peer. ## Contained proxy diff --git a/docs/backends/process-container/networking.md b/docs/backends/process-container/networking.md index 2bbc2a153..134f6efc2 100644 --- a/docs/backends/process-container/networking.md +++ b/docs/backends/process-container/networking.md @@ -6,10 +6,9 @@ Supported exact contracts from `0.9.0-alpha` use the shared `network.egress` and `network.ingress` policy plus `runtimeConfig.networkProxy` and `processContainer.network.allowedProxyPeer` configuration. -Implementation companion to the parent -[MXC Network Configuration, GA design](../../containment-configuration/0.8.0/networking/networking.md) -doc. The parent owns the shared policy schema, connectivity models, and GA goal. -This doc covers only how the Windows ProcessContainer backend enforces them. +For the shared policy shape and defaults, see +[directional networking](../../schema.md#directional-networking-supported-contracts). +This guide covers how the Windows ProcessContainer backend enforces them. ## 1. What this backend targets at GA diff --git a/docs/backends/process-container/os-version-support.md b/docs/backends/process-container/os-version-support.md index b2a06dde2..4619d17fc 100644 --- a/docs/backends/process-container/os-version-support.md +++ b/docs/backends/process-container/os-version-support.md @@ -9,7 +9,7 @@ document are Windows 11**, and the minimum considered here is Windows 11 23H2. For the enforcement mechanisms themselves see the [UI policy schema](./UIPolicy_Schema.md) and the -[containment configuration spec](../../containment-configuration/0.7.0/policy.md). +[supported configuration schema](../../schema.md). ## Windows 11 releases @@ -125,7 +125,9 @@ Notes: ## Network policy -The release matrix describes the legacy schema 0.6/0.7 implementation. +The following release matrix documents the retired schema 0.6/0.7 +implementation, not support for executable requests. For current directional +policy and tier behavior, see [ProcessContainer networking](networking.md). | Aspect | 23H2 | 24H2 | 25H2 | 25H2+ | |--------|:--:|:--:|:--:|:--:| @@ -143,9 +145,10 @@ Notes: the forward-looking proxy architecture; support for the model-2 BaseContainer contract should replace this fallback in a separate change. -Schema 0.8 support is selected by runtime contract rather than Windows release: +Supported directional network policy (schema 0.9+) depends on the available +backend enforcement tier rather than the declared version: -| Schema 0.8 capability | PSEC | AppContainer fallback | +| Directional capability | PSEC | AppContainer fallback | |---|:---:|:---:| | Directional defaults represented by capabilities | ✅ | ✅ when the capability mapping preserves the request | | Explicit egress IP/CIDR/port/protocol rules | ✅ WFP | ❌ | @@ -155,7 +158,7 @@ Schema 0.8 support is selected by runtime contract rather than Windows release: PSEC owns the WFP policy lifetime through workload completion. The AppContainer fallback rejects `egress.default: "deny"` with `ingress.default: "allow"` because -`privateNetworkClientServer` is bidirectional and no schema 0.8 WFP filter is +`privateNetworkClientServer` is bidirectional and no per-container WFP filter is available there to block private-network egress. ## UI restrictions diff --git a/docs/backends/seatbelt/seatbelt-backend.md b/docs/backends/seatbelt/seatbelt-backend.md index b09cc3318..5efd7d445 100644 --- a/docs/backends/seatbelt/seatbelt-backend.md +++ b/docs/backends/seatbelt/seatbelt-backend.md @@ -183,8 +183,8 @@ Anything it hasn't declared is rejected up front. ### Fields (supported schema 0.9+) This is the cross-backend directional shape accepted by the registered exact -contracts. The original [0.8 networking design](../../containment-configuration/0.8.0/networking/networking.md) -is historical; schema 0.8 is no longer accepted. +contracts. Schema 0.8 is no longer accepted; see the +[supported network fields](../../schema.md#directional-networking-supported-contracts). > **Omitting `network` entirely denies all IP networking.** Every field below > defaults to `deny`, so a config with no `network` block behaves exactly like diff --git a/docs/containment-configuration/0.7.0/policy.md b/docs/containment-configuration/0.7.0/policy.md deleted file mode 100644 index ff5d33e64..000000000 --- a/docs/containment-configuration/0.7.0/policy.md +++ /dev/null @@ -1,579 +0,0 @@ -# MXC Sandbox Policy Spec v0.7.0 - -> **Audience:** MXC consumers - -> Historical design reference: pre-v0.9 contracts are no longer accepted. -> These examples describe the original policy design, not current authoring. -> See [the supported schema and migration guide](../../schema.md). - ---- - -## Table of Contents - -1. [Problem Statement](#1-problem-statement) -2. [Non-Goals](#2-non-goals) -3. [Design Principles](#3-design-principles) -4. [Architecture](#4-architecture) -5. [SandboxPolicy](#5-sandboxpolicy) -6. [ContainerConfig](#6-containerconfig) -7. [Containment Types](#7-containment-types) -8. [Versioning](#8-versioning) -9. [Development Guide](#9-development-guide) -10. [Worked Example: UI Policy](#10-worked-example-ui-policy) -11. [FAQ](#11-faq) - ---- - -## 1. Problem Statement - -- The SDK has no way to express isolation intent. Callers cannot request stronger containment - (microVM) when they need it. -- UI containment (clipboard, windows, input injection) has no cross-platform abstraction. - Existing drafts are Windows-specific. -- The boundary between user-facing Policy and backend-specific Config is undefined. - Contributors do not know where new features belong. -- New backends (Nanvix, WSLC, macOS) are planned. The current approach does not scale to - N backends x M policy surfaces. - ---- - -## 2. Non-Goals - -- **Enterprise policy injection:** IT admin restrictions (Group Policy, MDM) and merge strategy. -- **Runtime permission brokering:** Flatpak-style portal dialogs. MXC policies are upfront. -- **Multi-container orchestration:** Composing sandboxes or sandbox-to-sandbox communication. -- **Audit logging and telemetry:** Compliance logging is a separate spec. - ---- - -## 3. Design Principles - -### Principle 1: Intent, Not Mechanism - -Policy describes *what* the caller wants. ContainerConfig describes *how* the platform enforces it. - -A caller writes `network: { allowOutbound: true }`. They never write firewall rules, iptables -chains, or capability lists. Mechanisms belong in ContainerConfig and backend executors. - -### Principle 2: Default-Deny - -Omitted policy fields = most restrictive permissions. Adding a field opts *in* to a capability. - -```typescript -// Fully locked down: -spawnSandbox("script.sh", { version: "0.6.0-alpha" }); - -// Allow outbound network: -spawnSandbox("script.sh", { - version: "0.6.0-alpha", - network: { allowOutbound: true }, -}); -``` - -New fields added in future versions default to "denied" for existing policies that do not set -them. This is a security guarantee. - -### Principle 3: Cross-Platform Where Possible - -Policy fields work on two or more platforms. The enforcement mechanism differs per platform, but -the intent is the same. Fields that only apply to one backend live in ContainerConfig, not Policy. - -### Principle 4: Version Is a Contract - -> See [versioning design](../../development/architecture/versioning.md) for the full versioning design. - -The Policy version and ContainerConfig schema version are locked in step. A version number -guarantees behavior. - ---- - -## 4. Architecture - -MXC has two user-facing concepts: - -1. **SandboxPolicy** (input): what the caller wants restricted. - Cross-platform security intent: filesystem, - network, UI, timeout. No OS-specific content. -2. **ContainerConfig** (output): the full configuration for one - specific backend. Returned by `createConfigFromPolicy()`. - Modifiable before spawning. Contains backend-specific sections. - -A **backend** is the containment technology that the executor -uses to create the sandbox (e.g., BaseProcessContainer on -Windows, LXC on Linux). - -### Flow - -``` -SandboxPolicy --> createConfigFromPolicy(policy, containment) --> ContainerConfig --> spawnSandboxFromConfig() --> executor --> OS -``` - -### Two API paths - -```typescript -// Simple: policy in, sandbox out. Always uses process containment. -spawnSandbox(script, policy); - -// Advanced: choose containment, get config, modify, then spawn. -const config = createConfigFromPolicy(policy, "process"); -config.processContainer!.ui!.isolation = "atoms"; // backend-specific tweak -spawnSandboxFromConfig(config); -``` - -`spawnSandbox` accepts a SandboxPolicy. For pre-built configs, use `spawnSandboxFromConfig`. - -### Layer diagram - -``` -┌───────────────────────────────────────────────────────────────┐ -│ LAYER 1: SDK + SandboxPolicy │ -│ │ -│ Users: GitHub CLI, Copilot, third-party agents │ -│ │ -│ SandboxPolicy: filesystem, network, ui, timeout │ -│ Simple: spawnSandbox(script, policy) │ -│ Advanced: createConfigFromPolicy(policy, "process") │ -│ → modify config │ -│ → spawnSandboxFromConfig(config) │ -└─────────────────────────────┬─────────────────────────────────┘ - │ ContainerConfig (JSON) - ▼ -┌───────────────────────────────────────────────────────────────┐ -│ LAYER 2: Executors (wxc-exec, lxc-exec) │ -│ │ -│ Parse ContainerConfig JSON, select backend runner │ -│ Backends: BaseProcessContainer, LXC, │ -│ microVM (Nanvix), WSLC │ -│ │ -│ Rust. Schema-validated. │ -└─────────────────────────────┬─────────────────────────────────┘ - │ OS API calls - ▼ -┌───────────────────────────────────────────────────────────────┐ -│ LAYER 3: OS Primitives │ -│ │ -│ Windows: BaseProcessContainer, BFS, Firewall, Job Objects │ -│ Linux: LXC cgroups, bind mounts, iptables, seccomp │ -│ │ -│ Kernel-level enforcement. │ -│ Never referenced by name in Layer 1. │ -└───────────────────────────────────────────────────────────────┘ -``` - -### Config granularity - -ContainerConfig translates policy into something the executors can act on. It is not a -general-purpose deployment manifest. - -Rules: - -- **Every Config field must be reachable** from policy or SDK defaults. If a - field has no path from user-facing input, it should not exist. -- **Keep Config minimal.** Only add fields that executors actually need. -- **SDK defaults to deny** for unfilled policy fields. - ---- - -## 5. SandboxPolicy - -SandboxPolicy is the user-facing input. It expresses security intent: what to allow, what to -deny. Cross-platform. No OS-specific content. - -```typescript -type SandboxPolicy = { - version: string; - filesystem?: { - readwritePaths?: string[]; - readonlyPaths?: string[]; - deniedPaths?: string[]; - tempDir?: "shared" | "isolated"; - }; - network?: { - allowOutbound?: boolean; - allowLocalNetwork?: boolean; - allowedHosts?: string[]; - blockedHosts?: string[]; - proxy?: { builtinTestServer: true } | { localhost: number } | { url: string }; - }; - ui?: { - allowWindows?: boolean; - clipboard?: "none" | "read" | "write" | "readwrite"; - allowInputInjection?: boolean; - }; - timeoutMs?: number; -}; -``` - -### `filesystem` - -| Field | Description | -|------------------|------------------------------------------------------------------------------| -| `readwritePaths` | Paths the sandbox can read and write. | -| `readonlyPaths` | Paths the sandbox can read but not write. | -| `deniedPaths` | Paths the sandbox cannot access at all. | -| `tempDir` | `"shared"`: host temp dir. `"isolated"`: private temp dir (`TEMP`/`TMPDIR`). | - -Omitted = no filesystem access beyond the default sandbox root. - -### `network` - -All flags default to `false` (no network access). - -| Field | Description | -|--------------------|-------------| -| `allowOutbound` | Allow outbound connections by default when no `allowedHosts` list narrows the policy. | -| `allowLocalNetwork`| Allow connections to local networks. | -| `allowedHosts` | Hosts or CIDRs allowed under a default-block policy. SDK builders select the block default whenever this list is non-empty; Windows ProcessContainer callers must also set `allowOutbound`. Seatbelt rejects the shared-valid combination unless `builtinTestServer` supplies the enforcement path. | -| `blockedHosts` | Hosts or CIDRs denied after allow rules are applied. Without `allowedHosts`, set `allowOutbound` to express allow-all-except-these. Seatbelt rejects this field because hostname blocks cannot be enforced. | -| `proxy` | `{ builtinTestServer: true }`, `{ localhost: }`, or `{ url: "..." }`. Windows enforces the proxy through WinHTTP; Bubblewrap and Seatbelt inject cooperative proxy environment variables that raw-socket clients can bypass. Combination rules are backend-specific: Bubblewrap and Seatbelt both reject proxy plus `enforcementMode: "firewall"` / `"both"` (neither has a privileged packet-filter layer), and Bubblewrap external proxies cannot be combined with host lists or `defaultPolicy: "block"`. `builtinTestServer` is testing-only and requires the `--allow-testing-features` flag (set `allowTestingFeatures: true` in the SDK spawn options). | - -Omitted = no network access. - -### `ui` - -Cross-platform intent fields only. Backend-specific UI mechanisms live in ContainerConfig. - -| Field | Description | -|-----------------------|---------------------------------------------------------------| -| `allowWindows` | Whether the sandbox may create visible windows. Default: no. | -| `clipboard` | `"none"` (default), `"read"`, `"write"`, or `"readwrite"`. | -| `allowInputInjection` | Whether the sandbox may inject keyboard/mouse input. Default: no. | - -### `timeoutMs` - -Execution timeout in milliseconds. Omitted = SDK default (no timeout). - -### Default-deny - -An empty policy is fully locked down: - -```typescript -spawnSandbox("script.sh", { version: "0.6.0-alpha" }); -// No filesystem, no network, no UI, no input injection. -``` - ---- - -## 6. ContainerConfig - -`createConfigFromPolicy()` returns a ContainerConfig: the complete configuration for one -specific backend. Users receive it, may modify backend-specific fields, then pass it to -`spawnSandboxFromConfig()`. -Key rules: - -- **One backend per Config.** A Windows process config has an `processcontainer` section; no `lxc` - section. A Linux process config has an `lxc` section; no `processcontainer`. -- **User-modifiable.** Advanced users can set any Config field before spawning. -- **Schema-defined.** Schemas live in `schemas/`. The SDK TypeScript types mirror them. - -All configs share common sections (filesystem, network, ui). -Each backend adds its own sections on top: - -```typescript -type ContainerConfig = - | ProcessContainerConfig - | LxcContainerConfig - | MicroVmConfig; -``` - -### Example: ProcessContainerConfig - -```json -{ - "version": "0.6.0-alpha", - "containment": "process", - "process": { - "commandLine": "node agent.js", - "cwd": "C:\\workspace", - "env": ["NODE_ENV=production"], - "timeout": 30000 - }, - "lifecycle": { - "destroyOnExit": true, - "preservePolicy": false - }, - "filesystem": { - "readwritePaths": ["C:\\workspace"], - "readonlyPaths": ["C:\\tools"], - "deniedPaths": [] - }, - "network": { - "defaultPolicy": "outbound", - "enforcementMode": "firewall", - "allowedHosts": [], - "blockedHosts": [], - "proxy": null - }, - "processcontainer": { - "leastPrivilege": true, - "capabilities": [], - "ui": { - "isolation": "full", - "desktopSystemControl": false, - "systemSettings": "none", - "ime": false - } - }, - "ui": { - "disable": true, - "clipboard": "none", - "injection": false - } -} -``` - -Backend-specific UI fields (`isolation`, `desktopSystemControl`, `systemSettings`, `ime`) -live inside `processcontainer.ui`, not at the top level. Top-level `ui` contains only the -cross-platform fields mapped from Policy. - -### Example: LxcContainerConfig - -```json -{ - "version": "0.6.0-alpha", - "containment": "lxc", - "process": { - "commandLine": "bash run.sh", - "cwd": "/workspace", - "env": [], - "timeout": 30000 - }, - "lifecycle": { - "destroyOnExit": true - }, - "filesystem": { - "readwritePaths": ["/workspace"], - "readonlyPaths": [], - "deniedPaths": [] - }, - "network": { - "defaultPolicy": "outbound", - "allowedHosts": [], - "blockedHosts": [] - }, - "lxc": { - "distribution": "alpine", - "release": "3.23" - } -} -``` - -No `processcontainer` section. No `ui` section. Backend-specific fields are scoped to the backend -that uses them. - ---- - -## 7. Containment Types - -The second parameter to `createConfigFromPolicy()` selects -the containment backend: - -```typescript -const config = createConfigFromPolicy(policy, "process"); -``` - -| Value | Windows backend | Linux backend | Status | -|--------------------|---------------------------|-----------------|-------------| -| `"process"` | BaseProcessContainer | LXC | Implemented | -| `"microvm"` | Nanvix | microVM | Future | -| `"agentSession"` | Agent session container | N/A | Future | - -Only `"process"` is end-to-end implemented today. - -`spawnSandbox(script, policy)` always defaults to `"process"`. - ---- - -## 8. Versioning - -> See [versioning design](../../development/architecture/versioning.md) for full details. - -- Policy version = ContainerConfig schema version. Always bumped together. -- SDK version is independent. SDK v5.0 can use policy/schema v2.1. -- During alpha, expect breaking changes. - ---- - -## 9. Development Guide - -> See [authoring a new feature](../../development/guides/authoring-a-new-feature.md) for the full workflow and -> decision tree. -> -> See [adding ProcessContainer OS features](../../development/guides/process-container-adding-os-features.md) for OS-level implementation -> details. - ---- - -## 10. Worked Example: UI Policy - -### 10.1 SDK user perspective - -**Simple path:** policy only, defaults to process containment: - -```typescript -const policy: SandboxPolicy = { - version: "0.6.0-alpha", - filesystem: { readwritePaths: ["C:\\workspace"] }, - network: {}, - ui: { - allowWindows: true, - clipboard: "read", - allowInputInjection: false, - }, - timeoutMs: 60000, -}; - -spawnSandbox("myapp.exe --flag1 arg", policy); -``` - -**Advanced path:** choose containment, get config, tweak: - -```typescript -const config = createConfigFromPolicy(policy, "process"); -config.processContainer!.ui!.isolation = "atoms"; -spawnSandboxFromConfig(config); -``` - -### 10.2 MXC developer perspective - -An MXC developer adding UI containment support would: - -**1. Add policy fields (if applicable)** (`sdk/node/src/types.ts`): - -If the feature is cross-platform security intent, add it to -`SandboxPolicy`. If the feature introduces a new containment -backend, add a new value to the `createConfigFromPolicy()` -containment parameter. UI is not a new backend, so we only -add policy fields here: - -```typescript -ui?: { - allowWindows?: boolean; - clipboard?: "none" | "read" | "write" | "readwrite"; - allowInputInjection?: boolean; -}; -``` - -**2. Add Config schema fields** (`schemas/dev/`): - -Top-level `ui` (maps from policy, all backends): - -```json -"ui": { - "disable": true, - "clipboard": "none", - "injection": false -} -``` - -Process container-specific UI fields (in this case Windows): - -```json -"processcontainer": { - "ui": { - "isolation": "full", - "desktopSystemControl": false, - "systemSettings": "none", - "ime": false - } -} -``` - -**3. Add Config TypeScript types** (`sdk/node/src/types.ts`): - -Top-level UI (all backends): - -```typescript -// Add to BaseContainerConfig: -ui: { - disable: boolean; - clipboard: string; - injection: boolean; -}; -``` - -Process container-specific UI (inside `processcontainer`): - -```typescript -// Add to ProcessContainerConfig.processcontainer: -ui: { - isolation: "desktop" | "handles" | "atoms" | "full"; - desktopSystemControl: boolean; - systemSettings: string; - ime: boolean; -}; -``` - -**4. Map policy to Config in SDK** (`sdk/node/src/sandbox.ts`): - -In `createConfigFromPolicy()`, map policy `ui` fields to -Config `ui` fields and fill backend-specific defaults: - -```typescript -// Top-level ui (from policy, all backends): -config.ui = { - disable: !policy.ui?.allowWindows, - clipboard: mapClipboard(policy.ui?.clipboard ?? 'none'), - injection: policy.ui?.allowInputInjection ?? false, -}; - -// Process container-specific ui (Windows only): -if (platform === 'win32' && config.containment === 'process') { - config.processcontainer.ui = { - isolation: 'full', - desktopSystemControl: false, - systemSettings: 'none', - ime: false, - }; -} -``` - -**5. Implement in executor** (`base_container_runner.rs`): - -Build the `ui_restrictions` bitmask from the `ui` section of -the ContainerConfig schema and translate it to OS-level Job -Object UI restriction flags. - -> See -> [Authoring a new feature](../../development/guides/authoring-a-new-feature.md) -> for the full workflow. -> See -> [Adding ProcessContainer OS features](../../development/guides/process-container-adding-os-features.md) for -> the executor and OS implementation guide. - ---- - -## 11. FAQ - -### "Where does my new feature go: Policy or ContainerConfig?" - -Use the decision tree in -[Authoring a new feature](../../development/guides/authoring-a-new-feature.md). In short: if it is a -cross-platform security intent that works on two or more platforms, it belongs in Policy. -Backend-specific mechanisms belong in ContainerConfig. - -### "What if my feature is Windows-only?" - -Put it in ContainerConfig under the appropriate backend section (e.g., `processcontainer` or `ui` -inside ProcessContainerConfig). Policy stays cross-platform. - -### "Can a developer bypass the SDK defaults?" - -Yes, via the advanced path. `createConfigFromPolicy()` returns secure defaults; the user -modifies any Config field before calling `spawnSandboxFromConfig()`. No bypass needed: Config is the -thing they modify. - -### "What happens if I omit all policy fields?" - -```typescript -spawnSandbox("script.sh", { version: "0.6.0-alpha" }); -``` - -Produces a fully locked-down config: no filesystem access, no network, UI disabled, no input -injection. Default-deny. - -### "Can I pass ContainerConfig JSON directly to an executor?" - -Yes. `wxc-exec config.json` accepts raw ContainerConfig. Useful for testing and internal -development. `createConfigFromPolicy()` is the recommended SDK path for production if needing control over -the configuration itself. diff --git a/docs/containment-configuration/0.8.0/networking/networking.md b/docs/containment-configuration/0.8.0/networking/networking.md deleted file mode 100644 index f1d75d1d7..000000000 --- a/docs/containment-configuration/0.8.0/networking/networking.md +++ /dev/null @@ -1,583 +0,0 @@ -# MXC Network Configuration, GA - -> **Audience:** MXC consumers - -Schema 0.8.0 accepts the `network.egress` and `network.ingress` sections -described in this document. Schema 0.7 and earlier retain their legacy network -configuration shape. During the additive transition, schema 0.8 requests may -use either shape, but cannot mix them. - -This change defines and parses the schema 0.8 contract. A backend must also -declare validation support and implement enforcement before accepting a -directional policy; unsupported backends reject it in the stacked validation -work. - -## Overview - -The MXC network configuration describes what network access a sandboxed workload has. The schema is shared across all container types (process containers, WSLc, LXC, Bubblewrap, Seatbelt). Enforcement varies by backend and platform. - -This document covers the GA scope for the General Availability release. - -**GA Goal:** Reduce the network surface an AI agent can use to escape its sandbox or exfiltrate data. By default, all outbound traffic is dropped. The recommended path for GA is: localhost HTTP/S proxy for application traffic (API calls, package downloads). Direct outbound connections (raw sockets, SSH, custom TCP/UDP) are blocked by default and only allowed when explicitly permitted by IP/CIDR rules. This is a hard problem to solve across multiple platforms. GitHub Copilot expects sandboxes to behave consistently cross-platform, but each platform has different enforcement primitives. This document describes what MXC can enforce on each backend, where platform limitations exist. - -### GA Commitments: What Traffic Goes Where - -This document specifies the shared MXC networking schema and GA behavior for backends that support network configuration. Backend-specific support, enforcement differences, and unsupported modes are described in the GA Scope by Backend section. - -#### Connectivity models - -MXC defines three outbound connectivity models, listed in increasing order of -network restriction. Model 1 applies the configured L3/L4 filtering -(IP/CIDR/port/protocol allow/block rules). Direct egress allow and deny rules -do not apply when runtime proxy configuration selects model 2. - -- **Direct internet + L3/L4 filtering, no proxy (least restrictive).** The sandbox reaches the internet directly over HTTP(S) (and other protocols), subject only to IP/CIDR/port/protocol allow/block rules. No proxy is configured, so there is no application-layer (domain/URL/content) inspection. -- **No direct internet + loopback HTTP(S) proxy (more restrictive).** The container has no direct internet path; the - proxy is its only internet egress. Cooperating clients route HTTP(S) to the proxy, where the consumer can inspect - and filter it. A client that ignores the proxy and tries to reach the internet directly is dropped. Backend-specific - private-network behavior is described below. The single endpoint named by `runtimeConfig.networkProxy` is a defined, - narrowly scoped exception to the container-to-host closure. It does not grant general host-loopback access. -- **No direct internet + no inbound (most restrictive):** This is the most - restrictive model. External network traffic is dropped; backend-local - intra-sandbox IPC is described separately below. - -There was a fourth model that was looked at Direct internet + L3/L4 filtering + loopback HTTP(S) proxy. However, unlike model 2 which only allows traffic through a specific loopback port, direct internet access greatly decreases the ways to control egress and increases the opportunities for agent bypass. It is not a model we will have for GA. - -**GA goal:** model 2 (recommended to MXC consumers) on every backend. Every backend blocks direct internet access and -configures cooperating HTTP(S) clients to use the proxy. Backend-specific private-network behavior is described below. - -This loopback-only-plus-proxy-routing pattern is a well-established way to -confine sandboxed agent egress on macOS and Linux. The GA target enforces a -strict localhost-only egress restriction. See GA Scope by Backend for -per-backend details and compatibility limitations. - -Throughout this document, the deny-all-except-proxy posture (the GA goal) refers to model 2. - -#### Outbound Traffic Routing - -**Default stance:** outbound is blocked by default. The paths below describe how allowed traffic flows. - -**Proxy path (recommended for application traffic):** - -- **Protocol:** HTTP and HTTPS only -- **Destination:** Localhost proxy only (e.g., 127.0.0.1, ::1) -- **Ports:** Any port within the 1 – 65535 range. -- **Routing mechanism:** This applies to HTTP(S) only, never to other protocols. The sandbox's outbound is restricted so - the only reachable destination is the loopback proxy port. Cooperating clients are pointed at the proxy through - `HTTP_PROXY`/`HTTPS_PROXY` on Linux and macOS. Windows uses both per-AppContainer WinHTTP configuration and proxy - environment variables for non-WinHTTP clients. A client that ignores the proxy cannot reach the internet directly - on an enforcing model-2 path because the egress restriction drops everything except the localhost proxy port. In - model 1, such a client may instead egress directly, subject to the IP/CIDR/port/protocol rules. In model 2, access to - the configured proxy endpoint is the sanctioned egress path and is allowed independently of - `ingress.hostLoopback`; no other host-loopback endpoint is opened by the proxy configuration. -- **What is NOT routed:** Non-HTTP traffic (raw TCP/UDP sockets, SSH, custom protocols, QUIC, WebRTC, etc.) is never - redirected to the proxy. In model 2, direct internet traffic is blocked. On ProcessContainer, private-network - traffic follows `ingress.default` because AppContainer exposes one bidirectional private-network capability. In - model 1, non-HTTP traffic is subject to the IP/CIDR/port/protocol rules. Transparently routing this traffic through - the proxy is a gap that requires further design and is out of scope for GA. - -**Direct outbound path (model 1 only):** - -- **When allowed:** Only when explicitly allowed by IP/CIDR + port + protocol rules in `egress.allow`. In model 2 there is no direct egress path, so these connections are not possible regardless of any allow rules. -- **Use case:** e.g. SSH to a specific dev server, a direct TCP connection to a database, UDP to a specific endpoint, ICMP for diagnostics. -- **Caveat (coarse filtering):** Rules match IP/CIDR + port + protocol, not the application protocol. A port number does not identify a service (DNS need not use 53; a database may listen on any port), so allowing or denying a port is a blunt control rather than service-level filtering. -- **Enforcement:** WFP filters (Windows process containers), network namespace + iptables (WSLc/LXC/Bubblewrap). Model 1 for macOS is not supported for GA. Seatbelt cannot filter arbitrary destinations and macOS packet filtering is not fine-grained enough for per-sandbox scenarios out of the box. On Bubblewrap, model 1 egress is enforced from schema 0.8+ inside the sandbox's own network namespace — see "LXC and Bubblewrap: GA enforcement" below for what is and is not covered there. - -**Example:** - -| Consumer scenario | Which model they should use | -|---|---| -| HTTP/HTTPS API calls → localhost proxy (e.g., port 8080) | model 2 | -| SSH to 192.0.2.10:22 → explicit allow rule required | model 1 | -| DNS to 8.8.8.8:53 → explicit allow rule required (or blocked if resolver IP not allowed) | model 1 | -| Raw TCP to 140.82.112.0/20:443 → explicit allow rule required | model 1 | - -#### Host Loopback and Inbound Policy - -**Default stance:** Host-loopback and LAN/private-network inbound traffic are -blocked by default on backends that enforce the GA network policy. - -On backends with private loopback, this does not affect intra-container -loopback. Processes inside the same sandbox may communicate over localhost / -127.0.0.1 / ::1. Seatbelt has the exception described below. - -Ingress has two allow/deny controls and no rule arrays: - -- `ingress.default` controls LAN/private-network inbound traffic where the - backend supports it. -- `ingress.hostLoopback` controls host-loopback connectivity in both directions: container-to-host and - host-to-container, except for the single outbound proxy endpoint named by `runtimeConfig.networkProxy`. - -Both controls default to `deny` when omitted. `hostLoopback` resolves -independently of `ingress.default` and never inherits it: omitting -`hostLoopback` denies host-loopback connectivity even under -`ingress.default: allow`. Host-loopback access must therefore be requested -explicitly. - -An explicit `hostLoopback` value is independent of `default` for the -host-loopback path. For example, `default: deny` with `hostLoopback: allow` -permits bidirectional host-loopback connectivity while denying other inbound -traffic. A backend that cannot enforce both directions must reject -`hostLoopback: allow` rather than accept it with partial enforcement. - -When `runtimeConfig.networkProxy` is present, `hostLoopback: deny` remains valid -and blocks every host-loopback path other than outbound connections to that -exact proxy endpoint. This exception is part of model 2, not a general grant of -host-loopback access. An enforcing model-2 implementation must preserve that -meaning or reject the configuration. A temporary compatibility path may require -broader host-loopback access only when its backend section explicitly documents -the weaker posture; such a path does not provide the strict model-2 guarantee. - -**Scope:** - -- Intra-container loopback: allowed on backends with private loopback; - Seatbelt cannot separate it from host loopback -- Container-originated private-network traffic: controlled by `egress` on backends that cleanly separate - private-network ingress from egress; backend-specific limitations are documented below -- Container-to-host-loopback and host-loopback-to-container traffic: controlled by `ingress.hostLoopback`, except for - outbound access to the exact model-2 proxy endpoint -- LAN/private-network inbound: controlled by `ingress.default`, where supported -- WAN inbound: not enabled by the GA policy - -**Use cases for `ingress.hostLoopback: allow`:** - -- Caller-provided services other than the configured network proxy, listening on host loopback and accessed from the - container -- MCP servers in SSE/WebSocket mode (server listens on a port for client connections from host) -- Language server daemons (e.g., TypeScript language server) accessed from host IDE -- Local dev servers (e.g., npm run dev on port 3000) accessed from host browser - -#### Inter-Container Networking Policy - -**GA stance:** Only Windows process containers support this today. - -- **Windows process containers:** Two AppContainers can communicate over host loopback only if AppContainer loopback-exemption rules are installed for the pair. These rules are directional, so both directions must be granted. This allows pointed container to container communication over the loopback. MXC will allow this for GA. -- **WSLc / LXC / Bubblewrap:** Each sandbox has its own network namespace, so 127.0.0.1 is private to the sandbox and separate sandboxes cannot reach each other over loopback. Inter-container communication requires explicit virtual networking (veth/bridge/routing, a shared namespace, or a brokered host proxy/IPC path). LXC has mature primitives for this; Bubblewrap would require MXC to build the networking around it. This is out of scope for GA for all 3. -- **macOS (Seatbelt):** Seatbelt does not create a network namespace, so two Seatbelt-sandboxed processes share the host loopback and can communicate over 127.0.0.1 (or Unix sockets/XPC) if both profiles allow it. This is host-level IPC, not isolated container-to-container networking, and is not a GA commitment. - -#### Intra-Container Networking Policy - -Backends with private loopback allow loopback traffic within the same sandbox. -This enables multi-process applications, local IPC over localhost sockets, and -sandbox-local helper processes. Seatbelt shares the host network stack, so -`ingress.hostLoopback: deny` also prevents intra-sandbox TCP loopback; use -another permitted IPC mechanism when that posture is required. - -On backends with private loopback, traffic between processes in the same -sandbox is not governed by outbound IP/CIDR rules or host-to-container inbound -policy. - -## Schema - -### Connectivity model 1 and 3 - -Direct internet, no proxy (least restrictive). OR no egress at all: default deny with no allow rules (most restrictive) - -```json -{ - "network": { // Network policy - "egress": { - "default": "deny", - "allow": [ - { - "to": [ { "cidr": "140.82.112.0/20" } ], - "ports": [ { "protocol": "tcp", "port": 443 } ] - } - ], - "deny": [] - }, - "ingress": { - "default": "deny", - "hostLoopback": "deny" - } - } -} -``` - -### Connectivity model 2 - -No direct internet, loopback proxy only (more restrictive). - -```json -{ - "network": { - "egress": { "default": "deny" }, - "ingress": { "default": "deny", "hostLoopback": "deny" } - }, - "runtimeConfig": { // runtime data passed to MXC (not policy) - // http(s)://localhost:, http(s)://127.0.0.1:, or http(s)://[::1]: - "networkProxy": "http://127.0.0.1:8080" - } -} -``` - -This backend-neutral example expresses the portable model-2 policy intent: -general host-loopback and private-network access remain denied, while -`runtimeConfig.networkProxy` designates the exact outbound proxy endpoint for -the model-2 exception. Cooperating HTTP(S) clients are configured to use the -proxy; clients that ignore the proxy settings are blocked from direct egress. -Without runtime proxy configuration, the deny defaults form model 3. Backends -may use different mechanisms to make the proxy reachable. An enforcing path -must preserve this meaning; a backend that cannot do so rejects the -configuration unless its section below explicitly identifies a temporary, -weaker compatibility posture. The example expresses the shared contract; the -GA Scope by Backend section states whether each backend accepts this exact JSON -or requires a documented backend-specific configuration. - -This schema follows container-ecosystem conventions (CIDR peers, egress/ingress, to/ports), modeled loosely on -Kubernetes NetworkPolicy (the CNCF standard layered on CNI/OCI) rather than on platform firewall primitives. MXC keeps -an explicit deny list and a per-direction default, which are a deliberate extension over pure Kubernetes NetworkPolicy -(allow-only with `ipBlock.except`) to give an auditable default and block-precedence. - -Egress peer and port fields (used in `egress.allow[]` / `egress.deny[]`; not shown in the minimal example above): - -| Field | Type | Notes | -|---|---|---| -| `to[].cidr` | IPv4 / IPv6 CIDR, or 0.0.0.0/0 / ::/0 for any | Single CIDR string (CNI/Kubernetes style), replacing separate address + prefix length. | -| `to[].except` | list of CIDRs, optional | Exclusions within the peer's CIDR (Kubernetes `ipBlock.except` style). An exclusion narrows the rule that carries it and nothing else: it never states a verdict of its own, so an address it removes is decided by the remaining rules and the direction default. Windows process containers pass the exclusion to the platform (WFP); the Linux backends subtract it from the peer and program the covering blocks that remain, because an `iptables` rule cannot carry an exclusion and a separate rule would leak into later ones. The Linux backends bound the resulting expansion; the GA Scope by Backend section states the limits. Not supported on Seatbelt (no destination filtering). | -| `ports[].protocol` | tcp / udp / icmp / any | `any` matches at minimum TCP, UDP, and ICMPv4/6; a backend may match more. Enforced on Windows process containers (WFP) and the Linux backends (iptables); not supported on Seatbelt. | -| `ports[].port` | uint16, optional | Destination port. Omit `ports` to match all ports/protocols. | -| `ports[].endPort` | uint16, optional | End of a port range (Kubernetes `endPort` style); requires numeric port. Supported on Windows process containers (WFP) and the Linux backends (iptables); not supported on Seatbelt. | - -Omit `to` or `ports` to select their wildcard behavior. When either field is -present, its array must contain at least one selector; an explicit empty array -is rejected rather than broadened into a wildcard. - -`icmp` expands by destination address family. A rule containing IPv4 and IPv6 peers produces both ICMPv4 and ICMPv6 -filters; a rule without `to` also produces both. - -`any` is a floor, not an exhaustive protocol list. Every backend that enforces -port rules matches at minimum TCP, UDP, and ICMPv4/6; some match considerably -more — Windows process containers cover a much wider set of IP protocols. The -exact set is a backend property, so consult the backend's implementation doc -when a policy depends on a protocol outside that floor. Pairing `any` with a -`port` narrows the match to the protocols that carry port numbers, since a -port-scoped filter cannot name a portless protocol such as ICMP. - -Ingress has no CIDR peers or port rules. `ingress.default` and -`ingress.hostLoopback` are the complete GA ingress surface. - -## Design decisions - -### D1: Default-deny outbound - -**Decision:** Unlisted destinations are unreachable. A configuration that mentions nothing grants nothing. - -**Why:** This ensures the configuration explicitly describes the sandbox's network permissions (auditable on enforcing backends). Forgotten rules fail closed (safer). The same configuration means the same thing on different hosts and container types (portable intent, though enforcement fidelity varies by platform). - -**Limitation:** Enforcement requires an egress restriction at the OS level: -WFP on Windows process containers, a network namespace plus iptables on the -Linux backends, and a Seatbelt profile confining network-outbound to the -loopback proxy port on macOS. Rich IP/CIDR/port allow-lists are expressible on -Windows and the Linux backends but not on macOS, where Seatbelt restricts -egress to the proxy port rather than filtering arbitrary destinations. -Backend-specific docs identify temporary compatibility behaviors whose -enforcement is intentionally coarser. Outside those documented exceptions, a -backend rejects configurations it cannot enforce. - -### D2: Inbound and host-loopback connectivity are blocked by default - -**Decision:** GA defines outbound configuration and inbound control. -`ingress.default: deny` blocks LAN/private-network inbound traffic, and -`ingress.hostLoopback: deny` separately blocks host-loopback connectivity in both directions, except for the exact -outbound proxy endpoint when `runtimeConfig.networkProxy` selects model 2. The host-loopback value overrides `default` -for all other host-loopback traffic. -Intra-container loopback is allowed on backends with private loopback. -Seatbelt has the caveat described below. - -**Why inbound and host-loopback connectivity are blocked by default:** - -- **Attack surface:** Host-loopback access can expose host services to contained code and container listeners to the - host. For agentic workloads, either direction can create command-and-control, exfiltration, or lateral-movement paths. -- **Opt-in model:** Customers must explicitly set `ingress.hostLoopback: allow` when either direction is required beyond - the configured model-2 proxy endpoint. -- **GA enforcement:** Windows process containers use loopback exemption rules - scoped to the AppContainer SID. WSLc/LXC/Bubblewrap require paired routing - and filtering for both directions across their private network namespaces. - Seatbelt maps `ingress.default` to its existing - `(allow network-inbound (local ip))` behavior and enforces `hostLoopback` on - the container-to-host direction with a `localhost`-scoped `network-outbound` - rule. The inbound half is not expressible in a Seatbelt profile, so Seatbelt - refuses `hostLoopback: "allow"` under `ingress.default: "deny"` — an inbound - promise no rule can carry. It accepts `hostLoopback: "deny"` under - `ingress.default: "allow"`, enforcing the container-to-host half while the - blanket inbound grant over-permits the host-to-container half; that pair is - the only way Seatbelt can offer a listener without surrendering the - container-to-host direction, which is what a model-2 proxy depends on. That rule is scoped to the - host, not to loopback: SBPL's `localhost` means *this machine*, so it covers - every address bound to the host (loopback, LAN, and any public address) in - both the `deny` and `allow` cases. Other machines are unaffected — they - remain governed by `egress.default` — and Seatbelt offers no narrower - primitive, since a literal `127.0.0.1` is a profile syntax error. - -**Seatbelt caveat:** On Seatbelt there is no private loopback, so `ingress.hostLoopback: deny` also blocks -intra-sandbox TCP loopback, breaking loopback servers used by processes in the same sandbox. For intra-sandbox IPC on -macOS, Unix-domain sockets in a sandbox-private path rather than TCP loopback could be used. That said, Unix-domain -sockets come with their own security questions and should be outlined in a separate macOS doc if necessary. - -**Elevation caveat:** Installing these filters (WFP on Windows, iptables on the Linux backends) generally requires elevation. Elevating on every sandbox launch is out of the question, so MXC applies them through a privileged broker/service rather than from the unelevated launch path. A per-platform, per-technology elevation story must be defined in a separate MXC elevation design doc and is a prerequisite for this enforcement. - -> **Bubblewrap does not need this.** The caveat above holds only for filters installed on the *host*. Bubblewrap gives the sandbox its own network namespace and programs iptables inside it from a supervisor holding `CAP_NET_ADMIN` in an unprivileged user namespace — namespace-scoped capabilities are not host privilege, so the rules install with no elevation and no broker. The sandbox still cannot alter them, because it drops `CAP_NET_ADMIN` before the workload runs. Shipped at schema 0.8; see `docs/backends/bwrap/bubblewrap-backend.md`. The elevation design remains a prerequisite for the host-filter backends only. - -### D3: IP literals and CIDRs only (no DNS names) - -**Decision:** Rule addresses must be IPv4/IPv6 literals or CIDRs. DNS names are rejected at validation time. The backend does not resolve names on behalf of callers. - -**Why:** Names are bypassable and non-deterministic. A container that resolves DNS itself can map a blocked name to an IP and connect directly. These results can change between config-time and runtime (TOCTOU) and vary by resolver/TTL/cache. IP/CIDR literals are deterministic and auditable. - -**GA DNS behavior:** GA does not implement domain-based DNS policy. DNS is not a first-class policy surface. If firewall rules block the DNS resolver IP, DNS queries fail. If rules allow the resolver IP, the sandbox can resolve any domain. - -**Secure domain allow-listing:** For HTTP(S) routed to the proxy, the proxy can inspect the domain on each CONNECT request in the HTTP header, and then choose to allow or deny by domain. The proxy resolves the hostname on the sandbox's behalf and enforces the domain allow-list before connecting. DNS resolution security at this point is on the proxy and not MXC. - -### D4: Explicit deny takes precedence over explicit allow - -**Decision:** When a connection matches both an `egress.allow` rule and an `egress.deny` rule, the deny wins. With `egress.default: "deny"`, no matching allow means no outbound access. With `egress.default: "allow"`, no matching deny means unrestricted outbound. - -**Why:** Fail-closed security posture. Deny rules act as overrides for broader allow rules (e.g., allow 0.0.0.0/0 but deny specific malicious IPs). - -### D5: Proxy is HTTP/S via platform-native APIs; localhost only for GA - -**Decision:** For GA, proxy routing covers HTTP and HTTPS traffic routed through the platform's native proxy surface. -The proxy must be on localhost (same-machine loopback) for GA. Remote proxies are out of scope for GA. When configured, -the exact endpoint in `runtimeConfig.networkProxy` is the sole sanctioned exception to container-to-host closure; -`ingress.hostLoopback: deny` blocks all other host-loopback access. - -**Platform-specific enforcement:** - -| Platform | Enforcement Mechanism | -|---|---| -| Windows (Process Containers) | PSEC BaseContainer path: per-AppContainer WinHTTP proxy configuration and scoped proxy-only access. AppContainer fallback rejects schema 0.8 runtime proxy. | -| WSLc | VM-level network policy permits only the translated proxy endpoint. Proxy variables are routing hints. | -| Linux (LXC, Bubblewrap) | iptables permits only the proxy endpoint; proxy variables are routing hints. | -| macOS (Seatbelt) | Seatbelt profile confines network-outbound to the loopback proxy port. MXC-set `HTTP_PROXY`/`HTTPS_PROXY` env variables are an advisory routing hint; a client that ignores the variables is denied by the profile (only the proxy port is reachable), so it is dropped, not bypassed. | - -**Backend implementation rule:** A backend implementing model 2: - -1. validates that `runtimeConfig.networkProxy` names an HTTP/S loopback endpoint with an explicit port; -2. makes only that outbound endpoint reachable, or only its backend-specific translation when the sandbox has a - private network namespace; -3. applies `ingress.hostLoopback` to every other container-to-host and host-to-container loopback path; and -4. rejects the configuration if it cannot preserve those semantics. - -A backend-specific compatibility path may instead require a broader ingress -setting only when its section documents both the required configuration and the -resulting loss of isolation. Capability gates such as ProcessContainer's -`ingress.default: "allow"` do not turn the proxy exception into general -host-loopback permission. - -**Why localhost only:** Remote proxies introduce trust boundary issues (proxy on different machine = different security context). Localhost proxy simplifies GA implementation and ensures proxy is under the same administrative control as the sandbox. - -**Proxy environment-variable hygiene (all backends):** The sandbox starts with all HTTP(S)-related proxy environment variables cleared to empty (`HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`, `FTP_PROXY`, `NO_PROXY`, and their lowercase variants). The sandbox never inherits host or stale proxy settings. MXC sets these variables explicitly, and only to the configured loopback proxy, when a proxy is in use (model 2); in model 1 they remain empty. - -**Consumer-provided proxy (all backends):** MXC does not provide, launch, or manage the proxy, and knows nothing about it beyond the host:port in the configuration. The consumer must start their proxy listening before launching the workload. MXC restricts the sandbox's egress to that endpoint and points the proxy variables at it. Making the proxy reachable from inside the sandbox is MXC's responsibility and is backend-specific. - -What is and is not routed through the proxy is described under Outbound Traffic Routing. - -### D6: Per-sandbox scoping - -**Decision:** Every configuration is scoped to one sandbox instance. Two concurrent sandboxes from different clients (e.g., gh-copilot and vscode) have independent configurations. One sandbox's configuration cannot affect another's network access. - -**Why:** Isolation between sandboxes prevents cross-sandbox interference and ensures configuration changes in one sandbox do not weaken another. - -**Implementation:** The sandbox identity used for scoping is backend-specific (AppContainer SID on Windows process containers, network namespace on WSLc/LXC, Seatbelt profile). - -### D7: Schema is container-type-agnostic; enforcement is backend-specific - -**Decision:** The network schema block is shared across all container types. The same JSON configuration means the same thing whether the backend is a Windows process container, a WSLc container, or Seatbelt on macOS. - -**Why:** Portable intent across platforms. Customers write one configuration that expresses their security policy; MXC maps it to backend-specific enforcement. - -**Reality:** Enforcement fidelity varies. A capability available on one backend (e.g., per-AppContainer WFP filters) may not have an equivalent on another. Cooperation-dependent routing (e.g., honoring proxy env vars) is allowed only as an optimization above an enforcing layer that already blocks non-cooperative traffic; it is never the enforcement mechanism itself. - -Backends that cleanly separate private-network ingress from egress apply both directions independently. Windows -AppContainer requires the bidirectional `privateNetworkClientServer` capability before private-network traffic can -flow in either direction. ProcessContainer therefore requires `ingress.default: "allow"` for outbound private-network -access, after which `egress` rules apply to both public and private outbound destinations. - -This capability coupling is a backend mapping, not a change to the shared -meaning of `ingress.default`. The ProcessContainer section identifies which -proxy deployments retain strict endpoint scoping and which compatibility -deployment additionally requires general host-loopback access. - -### D8: Delegation from the invoking user - -**Decision:** Like the filesystem configuration, the network configuration is a delegation: the contained code receives no more network access than the invoking user could exercise themselves. - -**Why:** The sandbox can only be more restricted, never less. A sandboxed process cannot reach a network destination that the invoking user's own process could not reach. Host-level firewalls, VPN configuration, and similar environmental controls still apply on top of this. - -## GA Scope by Backend - -GA includes all backends for their respective isolation capabilities. Network -configuration enforcement varies by backend. This section describes the GA -target for each backend; it does not claim that unfinished roadmap work is -already implemented. - -### Process containers (Windows): GA target and compatibility behavior - -The strict host-loopback guarantee applies to the identity-scoped -ProcessContainer path with OS-scoped proxy enforcement. The identity-less host -proxy path requires broader host-loopback access and is an explicitly documented -PSEC compatibility behavior rather than strict model-2 enforcement. The -AppContainer fallback rejects schema 0.8 runtime proxy requests because -they cannot preserve either posture. - -**Connectivity models:** - -- **Model 2 (recommended):** Grants no `internetClient`, so direct internet traffic is blocked. Any packaged proxy, - with or without AppContainer isolation, uses its Package Family Name in a non-blank `allowedProxyPeer`; an unpackaged - AppContainer proxy uses its profile name. Windows requires `ingress.default: "allow"` to grant the bidirectional - `privateNetworkClientServer` capability. With `allowedProxyPeer`, proxy reachability remains scoped to that peer and - endpoint and `ingress.hostLoopback` stays `"deny"`. An identity-less host proxy cannot use peer scoping and is the - documented development/testing compatibility path that requires `ingress.hostLoopback: "allow"`; it does not - provide the strict host-loopback-closure guarantee. -- **Model 1:** Grants `internetClient`, allowing direct internet egress under WFP IP/CIDR/port/protocol rules. - Private-network outbound also requires `ingress.default: "allow"` and remains subject to the same `egress` rules. -- **Model 3:** Grants no `internetClient`, private-network capability, or loopback exemptions. - -**Enforcement:** - -| Configuration concept | Enforcement mechanism | Notes | -|---|---|---| -| IP/CIDR allow/block | PSEC IPv4/IPv6 WFP filters scoped to AppContainer SID | Public and private destinations; unsupported on AppContainer fallback | -| Port filtering | Port filtering via WFP | Port ranges supported. | -| Protocol filtering | Protocol filtering via WFP | Schema values are `tcp`, `udp`, `icmp`, and `any`; WFP maps ICMP by address family. | -| Default-deny | PSEC WFP block-all baseline filter at lower precedence than explicit allows. | A non-empty allow list grants `internetClient` only as the capability prerequisite; WFP still limits egress to explicit allows. With no allows, `internetClient` is absent. | -| Proxy (HTTP/S only) | PSEC per-AppContainer WinHTTP configuration, endpoint filtering, and optional scoped peer access | Identity-scoped proxies keep `hostLoopback: "deny"`; the identity-less development/testing compatibility path requires `"allow"`; schema 0.8 proxy requests do not fall back to AppContainer | -| Per-sandbox scoping | AppContainer SID, unique per sandbox instance | | -| Private network | `privateNetworkClientServer` via `ingress.default` | Capability gate; `egress` filters outbound | -| Inbound | Capabilities and loopback rules | Private network uses `ingress.default`; loopback is separate | -| DNS | DNS queries follow same IP/CIDR allow/block rules as other traffic. No domain-based filtering. | If DNS resolver IP is blocked, DNS fails. If allowed, sandbox can resolve any domain. **For HTTP(S) via the proxy, DNS resolution happens in the proxy.** | -| Bypass resistance | High. Kernel-enforced WFP filters. Bypass requires kernel compromise or AppContainer escape (elevation). | | - -**Implementation doc:** [Process Container Networking Configuration, GA](../../../backends/process-container/networking.md) - -### WSLc: GA enforcement - -**Default stance:** Deny all outbound through the VM-level network policy API. - -**Connectivity model:** The VM-level network policy API enforces all three models. For model 2, MXC translates the -caller's loopback proxy to a VM-reachable endpoint and permits only that endpoint. The original loopback address is not -reachable directly from the WSL2 VM. - -**Recommended path:** Localhost proxy (HTTP/S via env vars) with no direct -egress allow-list. - -**Enforcement:** - -| Configuration concept | Enforcement mechanism | Notes | -|---|---|---| -| IP/CIDR allow/block | VM-level network policy API | IPv4 + IPv6 at GA | -| Port filtering | VM-level network policy API | Port ranges supported | -| Protocol filtering | VM-level network policy API | tcp, udp, icmp | -| Default-deny | VM-level network policy API | | -| Proxy (HTTP/S only) | Proxy variables plus VM-level allow for the translated endpoint | Direct bypass is blocked | -| Per-container scoping | VM and container identity | | -| Inbound and host loopback | VM-level policy applies `ingress.default` and bidirectional `ingress.hostLoopback` | | -| DNS | DNS queries follow same IP/CIDR allow/block rules as other traffic. No domain-based filtering. | If DNS resolver IP is blocked, DNS fails. If allowed, sandbox can resolve any domain. **For HTTP(S) via the proxy, DNS resolution happens in the proxy.** | -| Bypass resistance | Medium. Depends on VM-level enforcement and correct per-container scoping. | | - -### LXC and Bubblewrap: GA enforcement - -LXC and Bubblewrap use iptables/nftables on the container network path. Their INPUT policy applies `ingress.default` -and the host-to-container half of `ingress.hostLoopback`; routing and output policy enforce its container-to-host half. -Model 2 permits only the proxy endpoint. - -Both backends subtract `to[].except` from the peer that carries it and program the covering blocks that remain, so one -peer can expand into many rules. Two ceilings bound that expansion: a single peer may expand into at most **256 address -blocks** once its exclusions are removed, and one `egress` policy may lower into at most **65,536 rules** in total, -counting every destination block in every port each rule names. A policy exceeding either ceiling is rejected with the -limit it hit; neither is silently truncated, and no partial policy is installed. - -> **Implementation status (Bubblewrap).** Egress is enforced from schema 0.8+, -> but not on the path described above. Unprivileged Bubblewrap has no host-side -> veth, so no chain can be hooked into `FORWARD`. Instead the sandbox gets its -> own network namespace and a supervisor holding namespace-scoped -> `CAP_NET_ADMIN` filters on `OUTPUT` inside it (see the D2 note above). Rule -> addresses must be IP literals or CIDRs per D3; a hostname is rejected. -> Model 2 is enforced by the same chain, which opens only the proxy endpoint. -> An `INPUT` chain applies the inbound deny posture in the same transaction, -> accepting loopback and reply traffic and dropping new inbound connections. It -> requires `nf_conntrack`; the sandbox fails to launch without it rather than -> running unenforced. -> -> Not yet covered: `ingress.default: "allow"` and `ingress.hostLoopback: -> "allow"` are both rejected, because honoring either needs slirp port -> forwarding and a port contract neither schema expresses. The legacy -> `network.allowLocalNetwork: true` is rejected on a private-namespace mode for -> the same reason. Deny is therefore the only reachable inbound posture today. -> -> Both deny postures are enforced outside proxy mode. `ingress.default` -> installs the `INPUT` chain described above. `ingress.hostLoopback` is -> bidirectional per this contract, so its deny also closes the container-to-host -> direction: under slirp that path is the gateway `10.0.2.2`, which maps onto -> the host's own loopback, and a drop for it is lowered *ahead* of every caller -> rule so that a broad allow — including a bare `0.0.0.0/0` — cannot reopen it -> on the first-match chain. An omitted `ingress` section enforces the same deny, -> since this contract resolves both ingress controls to `deny` when they are -> absent rather than treating them as an absence of policy. IPv4 -> only — slirp gives the sandbox no IPv6 route to the host. -> -> **Proxy mode is the defined exception.** A runtime proxy is reached at the -> gateway `10.0.2.2:`, which *is* host loopback, so the chain opens that -> one TCP endpoint and drops the rest of the gateway. That is exactly the -> exception the 0.8 contract sanctions: the endpoint named by -> `runtimeConfig.networkProxy` is allowed independently of -> `ingress.hostLoopback`, and no other host-loopback path is opened. A proxy -> config that states — or defaults to — `hostLoopback: "deny"` therefore gets -> the posture it writes; the deny remains in force for every host-loopback path -> other than that one endpoint. `ingress.hostLoopback` is not consulted in this -> mode, because the chain comes from a proxy-specific builder rather than the -> directional one that lowers the drop, but the result the caller observes -> matches the contract either way. -> -> Inbound denial does not currently depend on the `INPUT` chain in practice: -> no port forwarding is configured, so nothing outside the sandbox can reach in -> regardless. Schema 0.6/0.7 keeps the previous warn-and-continue behavior and -> gains no host-loopback rule. -> LXC is unaffected: it has a veth and runs privileged, and enforces as -> described. - -### macOS (Seatbelt): GA enforcement - -**Default stance:** Egress is confined to the loopback proxy port by the Seatbelt profile. - -**Connectivity model:** Model 2 and 3 only. Model 1 (direct egress under IP/CIDR rules) is not supported because Seatbelt cannot enforce arbitrary destination policy. It can deny network entirely or allow only the localhost proxy path, but not general IP/CIDR, hostname, port, or protocol allow-lists. - -**Recommended path:** Loopback proxy with the Seatbelt localhost egress restriction and proxy variables. - -**Enforcement:** - -| Configuration concept | Enforcement mechanism | Notes | -|---|---|---| -| Egress restriction (model 2) | Seatbelt profile: (deny default) then allow network-outbound only to `localhost:`. All other outbound (direct internet, raw sockets, direct DNS) is denied by the profile. | Confines egress to the proxy port; does not filter arbitrary destinations. | -| Proxy routing (HTTP/S) | `HTTP_PROXY`/`HTTPS_PROXY` set to the loopback proxy; cooperating clients route there. | A minority of clients ignore the variables; their traffic is dropped by the egress restriction, not bypassed. | -| IP/CIDR / port / protocol allow-lists | Not supported. | | -| Per-sandbox scoping | Seatbelt profile per sandbox-exec invocation | | -| Inbound | Seatbelt `network-inbound (local ip)` rule | Preserves current `allowLocalNetwork` behavior through `ingress.default`. `hostLoopback: "allow"` under `default: "deny"` is rejected with `policy_validation`; `hostLoopback: "deny"` under `default: "allow"` is accepted, enforcing only the container-to-host half. | -| DNS | Direct outbound DNS to an external resolver is blocked (egress confined to the proxy port); cooperating clients pass hostnames to the proxy, which resolves them. All others would be blocked. | | -| Bypass resistance | Medium. Egress is profile-restricted to the proxy port, so raw-socket and direct-DNS attempts are denied. Weaker than a separate network namespace (Seatbelt shares the host network stack) and depends on a correct profile. | | - -### Other backends - -- **Windows Sandbox:** Guest-side firewall only, with hardcoded rules. In GA for development/testing scenarios where network isolation is not critical. -- **Isolation Session:** No network filtering or denial is possible — outbound is open and a process inside can listen - on a localhost-reachable port. Schema 0.7 requires the unrestricted-network acknowledgment - (`network.defaultPolicy=allow` + `network.allowLocalNetwork=true`). The directional schema 0.8 acknowledgment is - reserved for the backend migration work; until that lands, callers must continue using the legacy unrestricted - acknowledgment. Other network/proxy policy is rejected. - In GA for process isolation only (identity, lifecycle). -- **Hyperlight, Nanvix:** Not in this GA scope doc. Additional follow up is needed to confirm their capabilities and whether they align with this doc. - -## Gaps and limitations - -**What GA cannot do:** - -- **DNS domain filtering:** The IP/CIDR schema cannot distinguish hostnames that resolve to the same IP. Domain allow/deny requires either deeper platform support or the proxy inspecting the CONNECT/request host plus blocking direct DNS egress (see D3). -- **Inter-container networking:** Containers cannot communicate with each other (except Windows process containers). -- **macOS direct-egress models:** Seatbelt cannot filter arbitrary remote destinations, so model 1 (direct egress under IP/CIDR/port/protocol rules) is not available on macOS; macOS supports model 2 (proxy-only). -- **Proxy arbitrary network traffic:** GA MXC configures proxies for HTTP/S traffic only. On Windows, only clients that - use the WinHTTP stack or correctly query the platform proxy configuration are proxied. Many libraries on all three - platforms use proxy environment variables as their configuration mechanism. On Linux and macOS these are the - standard way to apply proxy configurations; however, it is advisory only and not a full RFC standard. Libraries and - applications that honor them use the proxy; those that ignore them do not have their traffic directed to the proxy - and instead have their egress blocked. diff --git a/docs/containment-configuration/0.8.0/networking/schema-updates.md b/docs/containment-configuration/0.8.0/networking/schema-updates.md deleted file mode 100644 index 6eed3f815..000000000 --- a/docs/containment-configuration/0.8.0/networking/schema-updates.md +++ /dev/null @@ -1,151 +0,0 @@ -# MXC network schema updates: 0.7 to 0.8 - -> **Audience:** MXC consumers and developers - -Schema 0.8 adds explicit egress and ingress policy and moves proxy runtime data -outside the shared policy. During the additive transition, a 0.8 request may -use either the legacy fields or the new fields, but cannot mix both formats. -Schemas before 0.8 cannot use the new fields. - -## Field mapping - -| Schema 0.7 | Schema 0.8 | Change | -|---|---|---| -| `defaultPolicy: "allow"` | `egress.default: "allow"` | Same outbound posture | -| `defaultPolicy: "block"` | `egress.default: "deny"` | `block` is renamed `deny` | -| IP/CIDR entries in `allowedHosts` | `egress.allow[].to[].cidr` | Can scope by port/protocol | -| IP/CIDR entries in `blockedHosts` | `egress.deny[].to[].cidr` | Deny overrides allow | -| DNS names in `allowedHosts` or `blockedHosts` | No GA equivalent | Schema 0.8 accepts only IP/CIDR rules | -| `enforcementMode` | Removed | The backend enforces the policy or rejects it | -| `allowLocalNetwork` | `ingress.default` | Capability gate; outbound follows `egress` | -| No equivalent | `ingress.hostLoopback` | New bidirectional host-loopback connectivity control | -| `proxy.localhost` | `runtimeConfig.networkProxy` | Loopback proxy endpoint becomes runtime data | -| `proxy.url` with an HTTP/S loopback URL | `runtimeConfig.networkProxy` | Loopback URL remains supported | -| `proxy.url` with a remote or non-loopback URL | No GA equivalent | Schema 0.8 accepts only loopback proxy URLs | - -`proxy.builtinTestServer` has no schema 0.8 GA equivalent. - -On backends that cleanly separate private-network ingress from egress, `egress` governs all outbound traffic and -`ingress` governs traffic entering the container. ProcessContainer also applies `egress` rules to all outbound -destinations, but private-network traffic additionally requires `ingress.default: "allow"` to grant Windows' -bidirectional `privateNetworkClientServer` capability. - -This table maps the immutable 0.7 wire fields. In schema 0.7, `allowLocalNetwork` expresses inbound bind/listen -permission and is honored by Seatbelt; ProcessContainer capabilities are separate. It maps only to -`ingress.default` and must not change `egress.default`. Under schema 0.8, ProcessContainer uses that ingress value as -the capability gate for private-network traffic, while outbound private destinations remain subject to `egress`. - -## Direct egress - -Schema 0.7: - -```jsonc -{ - "network": { - "defaultPolicy": "block", - "enforcementMode": "both", - "allowedHosts": [ "140.82.112.0/20" ], - "allowLocalNetwork": false - } -} -``` - -Schema 0.8: - -```jsonc -{ - "network": { - "egress": { - "default": "deny", - "allow": [ - { - "to": [ { "cidr": "140.82.112.0/20" } ], - "ports": [ { "protocol": "tcp", "port": 443 } ] - } - ] - }, - "ingress": { - "default": "deny", - "hostLoopback": "deny" - } - } -} -``` - -This schema 0.8 example intentionally narrows the schema 0.7 all-port, -all-protocol allow to TCP port 443. Omit `ports` to preserve the schema 0.7 -rule exactly. - -## Proxy - -Schema 0.7 uses cooperative proxy variables: - -```jsonc -{ - "network": { - "proxy": { "localhost": 8080 } - } -} -``` - -Schema 0.8 moves the endpoint to runtime metadata: - -```jsonc -{ - "runtimeConfig": { - "networkProxy": "http://127.0.0.1:8080" - } -} -``` - -The omitted 0.8 `network` block uses deny defaults. When `runtimeConfig.networkProxy` is present, cooperating HTTP(S) -clients are configured to use the proxy; clients that ignore the proxy settings are blocked from direct egress. - -For example, this legacy policy: - -```jsonc -{ - "network": { - "defaultPolicy": "block", - "allowLocalNetwork": true - } -} -``` - -migrates to deny-default egress with allowed private/LAN inbound: - -```jsonc -{ - "network": { - "egress": { "default": "deny" }, - "ingress": { - "default": "allow", - "hostLoopback": "deny" - } - } -} -``` - -On backends that cleanly separate private-network ingress from egress, this does not grant outbound private-network or -internet access. On ProcessContainer, `ingress.default: "allow"` grants the bidirectional private-network capability, -but deny-default `egress` still blocks outbound public and private destinations unless an allow rule or the configured -proxy path applies. - -Backend-specific migration can require an additional acknowledgment without changing the shared field mapping: - -- Seatbelt cannot separate host loopback from other local inbound traffic, so `ingress.hostLoopback: "allow"` under - `ingress.default: "deny"` is rejected: no rule can carry the promised host-to-container grant. The reverse pair, - `ingress.hostLoopback: "deny"` under `ingress.default: "allow"`, is accepted and enforces the container-to-host - half only; the blanket inbound grant over-permits the host-to-container half, which is documented in the - [Seatbelt backend guide](../../../backends/seatbelt/seatbelt-backend.md). -- Isolation Session cannot enforce any network restriction. Its directional - acknowledgment is reserved for the backend migration work; until that lands, - callers must continue using the legacy unrestricted acknowledgment - (`defaultPolicy: "allow"` plus `allowLocalNetwork: true`). - -## Backend-specific schema 0.8 configuration - -| Backend | Configuration | -|---|---| -| ProcessContainer | [Schema 0.8 proxy configuration](../../../backends/process-container/examples/0.8.0-schema.md) | -| Seatbelt (macOS) | [Schema 0.8 configuration](../../../backends/seatbelt/seatbelt-backend.md#schema-08-network-shape-egress--ingress--runtimeconfignetworkproxy) diff --git a/docs/containment-configuration/0.8.0/policy.md b/docs/containment-configuration/0.8.0/policy.md deleted file mode 100644 index 8fc5aa0d7..000000000 --- a/docs/containment-configuration/0.8.0/policy.md +++ /dev/null @@ -1,150 +0,0 @@ -# MXC Sandbox Policy Spec v0.8.0 - -> **Audience:** MXC consumers - -> Historical design reference: schema 0.8 is retired. Use the -> [current schema and migration guide](../../schema.md) for supported requests. - -## SandboxPolicy - -`SandboxPolicy` is MXC's cross-platform JSON authoring contract. It expresses -portable filesystem, network, UI, and execution restrictions without selecting -backend-specific mechanisms. This page defines the language-neutral JSON shape; -SDK API coverage is version-dependent, and an SDK's native types or builders -may not yet expose every field. When supported, those APIs produce the -`ContainerConfig` consumed by MXC. - -```json -{ - "version": "0.8.0-alpha", - "network": { - "egress": { - "default": "deny", - "allow": [ - { - "to": [{ "cidr": "192.0.2.0/24" }], - "ports": [{ "protocol": "tcp", "port": 443 }] - } - ] - }, - "ingress": { - "default": "deny", - "hostLoopback": "deny" - } - }, - "ui": { - "allowWindows": false, - "clipboard": "none", - "allowInputInjection": false - }, - "timeoutMs": 30000 -} -``` - -### `filesystem` - -| Field | Description | -|---|---| -| `readwritePaths` | Paths the sandbox can read and write. | -| `readonlyPaths` | Paths the sandbox can read but not write. | -| `deniedPaths` | Paths the sandbox cannot access. | -| `clearPolicyOnExit` | Clear retained policy after execution. Maps to the inverse of `lifecycle.preservePolicy` and can cover multiple policy types, including filesystem and network policy. Defaults to `true`. | - -Omitted filesystem permissions remain denied. - -### `network` - -Schema 0.8 accepts either the legacy network fields or the directional fields, -but not both in one policy. - -#### Legacy network fields - -These fields preserve schema 0.6 and 0.7 authoring compatibility. - -| Field | Description | -|---|---| -| `allowOutbound` | Allow outbound network access. Defaults to `false`. | -| `allowLocalNetwork` | Allow inbound connections from local/private networks. Defaults to `false`. | -| `allowedHosts` | Hosts or CIDRs allowed by backends that support host filtering. | -| `blockedHosts` | Hosts or CIDRs denied by backends that support host filtering. | -| `proxy` | Legacy proxy configuration: `builtinTestServer`, `localhost`, or `url`. | - -#### `network.egress` - -| Field | Description | -|---|---| -| `default` | Action when no rule matches: `"allow"` or `"deny"`. Defaults to `"deny"`. | -| `allow` | Direct-egress rules to allow. | -| `deny` | Direct-egress rules to deny. Deny rules take precedence. | - -Each allow or deny rule has this shape: - -```typescript -type NetworkRuleConfig = { - to?: Array<{ - cidr: string; - except?: string[]; - }>; - ports?: Array<{ - protocol?: "tcp" | "udp" | "icmp" | "any"; - port?: number; - endPort?: number; - }>; -}; -``` - -| Rule field | Description | -|---|---| -| `to` | Destination CIDRs. Omission matches both IP families. | -| `to[].cidr` | IPv4 or IPv6 CIDR. | -| `to[].except` | CIDRs excluded from the containing peer. Each exclusion must use the same address family and be contained within `to[].cidr`. | -| `ports` | Protocol and destination-port selectors. Omission matches all. | -| `ports[].protocol` | `"tcp"`, `"udp"`, `"icmp"`, or `"any"`. Defaults to `"any"`. | -| `ports[].port` | Destination port from 1 through 65535. Omission matches every port. Must be omitted for `"icmp"`. | -| `ports[].endPort` | Inclusive range end from 1 through 65535. Requires `port`, must be greater than or equal to it, and must be omitted for `"icmp"`. | - -Explicit `to` and `ports` arrays must not be empty. - -#### `network.ingress` - -| Field | Description | -|---|---| -| `default` | LAN/private-network inbound action: `"allow"` or `"deny"`. Defaults to `"deny"`. | -| `hostLoopback` | Bidirectional host-loopback action: `"allow"` or `"deny"`. Defaults to `"deny"`. | - -Backends that cannot enforce a requested ingress combination reject it rather -than weakening the policy. - -### `runtimeConfig` - -Schema 0.8 introduces `runtimeConfig` for cross-platform runtime metadata -needed to execute a policy but which is not itself security-policy intent. It -is configuration rather than policy, and it is not tied to a specific backend. -The only runtime metadata currently defined is the network proxy endpoint. - -| Field | Description | -|---|---| -| `networkProxy` | HTTP/S loopback proxy URL with an explicit port. Requires `network.egress.default` to be `"deny"` with no direct allow or deny rules. | - -### `ui` - -| Field | Description | -|---|---| -| `allowWindows` | Allow visible windows. Defaults to `false`. | -| `clipboard` | `"none"`, `"read"`, `"write"`, or `"all"`. Defaults to `"none"`. | -| `allowInputInjection` | Allow keyboard or mouse input injection. Defaults to `false`. | - -### `timeoutMs` - -Execution timeout in milliseconds. Omission means no timeout. - -### Default-deny - -Omitted permissions remain denied. A schema 0.8 policy with no network fields -selects directional deny defaults. - -## Examples and detailed behavior - -- [Current directional config example (supported v1.0)](../../../tests/examples/30_network_0_8_directional.json) -- [Schema updates from 0.7 to 0.8](networking/schema-updates.md) -- [Network modes and rule semantics](networking/networking.md) diff --git a/docs/development/architecture/telemetry.md b/docs/development/architecture/telemetry.md index ff937026d..3e8f62616 100644 --- a/docs/development/architecture/telemetry.md +++ b/docs/development/architecture/telemetry.md @@ -639,8 +639,10 @@ Excluded, and why: | `dry_run`, `testing_features_enabled` | Invocation modes, not policy. | | `experimental_enabled` | Authorizes selecting an experimental backend, not enforcement; changing it leaves policy identity unchanged. | -`network_enforcement_compatibility` is included because it changes how the -normalized network policy is interpreted and enforced. +The retired network compatibility marker is no longer part of the projection. +Hashes from builds that included it can differ even when the supported +effective policy is unchanged; compare policy hashes across builds only with +that projection change in mind. Enforcement-relevant backend configuration is hashed from `ExecutionRequest.windows_sandbox` and `ExecutionRequest.wslc`. The canonical diff --git a/docs/development/architecture/versioning.md b/docs/development/architecture/versioning.md index c81bb8f86..a8b523cb0 100644 --- a/docs/development/architecture/versioning.md +++ b/docs/development/architecture/versioning.md @@ -67,7 +67,7 @@ take a caller-supplied schema version. Each v1 SDK targets the published exact `1.0.0` contract internally. Raw configuration APIs require the caller to declare a registered `version` -to select an immutable historical or mutable development contract. +to select an immutable published or mutable development contract. ```typescript // sdk/node/src/types.ts @@ -167,9 +167,9 @@ mxc/schemas/ └── mxc-config.schema.1.1.0-alpha.json (exact closed development contract) ``` -Retired stable schema files are **kept as immutable historical artifacts** — the -parser simply stops accepting those versions (the supported floor is -`0.9.0-alpha`). Released schemas are never edited or deleted. +Published stable schemas remain immutable and cannot be removed, including +those below the supported floor (`0.9.0-alpha`). The parser rejects retired +versions even though their schema artifacts remain in the repository. The development artifact is generated from the exact `mxc_contract::dev` model. It describes all eight closed one-shot and @@ -231,11 +231,11 @@ attribution after exact validation because they did not originate as external configuration JSON. Runtime behavior is selected from explicit normalized semantics rather than by -comparing contract-version strings. In particular, -`NetworkEnforcementCompatibility::Strict` is the only mode for registered -exact contracts and direct typed SDK requests. Retired pre-v0.9 contracts -cannot select legacy enforcement behavior. Direct typed SDK construction -clears only source-contract attribution; it does not weaken validation. +comparing contract-version strings. Registered exact contracts and direct typed +SDK requests use the same strict network enforcement. Retired pre-v0.9 +contracts cannot select legacy enforcement behavior. Direct typed SDK +construction clears only source-contract attribution; it does not weaken +validation. ### IsolationSession directional networking @@ -715,9 +715,9 @@ all experimental features in the config are active. There is no per-feature enable/disable mechanism — simplicity over granularity. **Migration after promotion:** Promotion changes publication and runtime -authorization, not the feature's JSON path. Old contracts retain their exact -historical shapes; new requests must use the shape defined by their declared -exact version. +authorization, not the feature's JSON path. Each exact contract retains its +version-specific shape; requests must use the shape defined by their declared +version. ## Deprecation Aliases @@ -730,9 +730,8 @@ schema, generated raw type, and raw SDK validator must agree on the spellings accepted by the declared `config.version`; aliases are not version-independent parser exceptions. -The historical v0 contracts retain their original compatibility aliases. -`appcontainer` and `appContainer` are accepted by v0.6-v0.9, while -`macos_sandbox` is accepted by v0.7-v0.9. The v1 line removes all three +The supported v0.9 contract accepts `appcontainer`, `appContainer`, and +`macos_sandbox` as compatibility aliases. The v1 line removes all three spellings at its major-version boundary, so raw v1.0 and v1.1 requests must use `processcontainer`, `processContainer`, and `seatbelt`. @@ -746,8 +745,8 @@ native parser; the TypeScript SDK validator may still surface a deprecation hint via `diagLog` while inspecting raw config. **Removal.** Removing an accepted alias is a breaking wire change and therefore -belongs at a major-version boundary. Older immutable contracts continue to -accept their historical spellings; the new major rejects them with the +belongs at a major-version boundary. The supported v0.9 contract continues to +accept its aliases; the new major rejects them with the standard exact-contract diagnostic. ## Open Questions diff --git a/docs/development/build-and-test/ci-validation-infrastructure.md b/docs/development/build-and-test/ci-validation-infrastructure.md index 669e6192c..9834ea6d6 100644 --- a/docs/development/build-and-test/ci-validation-infrastructure.md +++ b/docs/development/build-and-test/ci-validation-infrastructure.md @@ -226,8 +226,8 @@ get fixed or wired. | Backend | Status | Notes | |---------|--------|-------| -| Process T1 | ✅ Good | Windows 24H2+ only. Runs the primitives suite, tier-gated to `base-container`. Includes the schema 0.8 directional networking phases (capability matrix, model-3 equivalence, explicit egress rules, host loopback, runtime proxy, reject surface) and the legacy 0.7 network lane. Remaining failures are genuine MXC bugs or harness limitations. | -| Process T3 | ✅ Good | Windows 23H2 only. Runs the primitives suite tier-gated to `appcontainer-dacl`, plus `T3-Workloads.ps1` (real programs — pwsh, git, node, python, cmd — on top of the T3 primitives). The 0.8 networking phases assert the documented *rejection* behavior here, since AppContainer cannot carry egress rules, proxy peer identity, or host-loopback configuration. | +| Process T1 | ✅ Good | Windows 24H2+ only. Runs the primitives suite, tier-gated to `base-container`. Includes supported directional networking phases (capability matrix, model-3 equivalence, explicit egress rules, host loopback, runtime proxy, reject surface). Remaining failures are genuine MXC bugs or harness limitations. | +| Process T3 | ✅ Good | Windows 23H2 only. Runs the primitives suite tier-gated to `appcontainer-dacl`, plus `T3-Workloads.ps1` (real programs — pwsh, git, node, python, cmd) on top of the T3 primitives. The directional networking phases assert the documented *rejection* behavior here, since AppContainer cannot carry egress rules, proxy peer identity, or host-loopback configuration. | | Bubblewrap | ✅ Good | | | LXC | ✅ Good | Some networking tests fail on distros other than Ubuntu 24.04; seems to be an issue with MXC. The in-process streaming handle is covered at PR time by `lxc-e2e.yml` instead, because this matrix runs prebuilt binaries. | | WSLC | ✅ Good | Might have to retry hung jobs - this is an issue with overzealous agent reclaiming. | diff --git a/docs/development/guides/authoring-a-new-feature.md b/docs/development/guides/authoring-a-new-feature.md index 7767c0271..df7e91975 100644 --- a/docs/development/guides/authoring-a-new-feature.md +++ b/docs/development/guides/authoring-a-new-feature.md @@ -18,8 +18,8 @@ Read these in order: -1. [Containment configuration spec](../../containment-configuration/0.7.0/policy.md): what -Policy and ContainerConfig are, design principles. +1. [Configuration schema](../../schema.md): supported policy fields and their + default behavior. 2. [Versioning Design](../architecture/versioning.md): how policy/schema/SDK versions relate and when to bump. @@ -101,8 +101,7 @@ If your feature touches SandboxPolicy, update If your feature adds policy or config fields, you will need to plumb them through `createConfigFromPolicy()` in `sdk/node/src/sandbox.ts`. See the -[worked example in the containment configuration spec](../../containment-configuration/0.7.0/policy.md#10-worked-example-ui-policy) -for a walkthrough. +[UI policy section](../../schema.md#ui-policy) for the supported request shape. --- diff --git a/docs/development/guides/process-container-adding-os-features.md b/docs/development/guides/process-container-adding-os-features.md index c391d2a6a..9f7e3aa5a 100644 --- a/docs/development/guides/process-container-adding-os-features.md +++ b/docs/development/guides/process-container-adding-os-features.md @@ -5,9 +5,9 @@ This guide covers adding new OS-level features that flow through MXC's Windows process container pipeline. -Exact v0.9 networking uses directional egress/ingress, -`runtimeConfig.networkProxy`, and ProcessContainer proxy-peer identity. Legacy -networking fields remain available under their published contracts. Backend +Supported networking uses directional egress/ingress, +`runtimeConfig.networkProxy`, and ProcessContainer proxy-peer identity. Retired +networking fields are rejected by every registered contract. Backend selection depends on host capability and requested policy, not schema version. For which policy aspects this backend can enforce on each Windows 11 release, @@ -15,8 +15,8 @@ see [Windows OS-version policy support](../../backends/process-container/os-vers ## Prerequisites -1. Read the [containment configuration spec](../../containment-configuration/0.7.0/policy.md) to - understand how `SandboxPolicy` maps to `ContainerConfig`. +1. Read the [supported configuration schema](../../schema.md) for policy and + request fields. 2. Read [authoring-a-new-feature.md](authoring-a-new-feature.md), especially Step 1 (feature spec) and Step 2 (OS changes). 3. Submit a feature spec so reviewers understand the end-to-end flow. diff --git a/docs/examples.md b/docs/examples.md index 00c1aee31..23a06ddc0 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -100,10 +100,9 @@ separate ingress defaults: See [`tests/examples/30_network_0_8_directional.json`](../tests/examples/30_network_0_8_directional.json) -for the complete config and -[`containment-configuration/0.8.0/networking/networking.md`](containment-configuration/0.8.0/networking/networking.md) -for the historical GA design; use the -[supported schema guide](schema.md) for current backend authoring. +for the complete config and the +[supported schema guide](schema.md#directional-networking-supported-contracts) +for current backend authoring. ### Network Proxy @@ -146,10 +145,8 @@ their supported ingress policies differ. See their backend guides. Every supported contract (`0.9.0-alpha` or later) accepts the directional shape and rejects the retired `defaultPolicy`, host-list, and `network.proxy` -fields. This is the cross-backend schema (see -[`docs/containment-configuration/0.8.0/networking/networking.md`](containment-configuration/0.8.0/networking/networking.md) -for the full design and per-backend enforcement matrix), not a -backend-specific format: it's parsed the same way regardless of +fields. This is the [cross-backend schema](schema.md#directional-networking-supported-contracts), +not a backend-specific format: it's parsed the same way regardless of `containment`. Each backend independently declares which parts of it — if any — it actually enforces; a backend that hasn't declared support for a given field rejects a config that sets it. @@ -158,8 +155,7 @@ Note that `EGRESS_RULES` is what carries per-CIDR/port rules; a backend without it accepts only `egress.default`. On Seatbelt, `runtimeConfig.networkProxy` covers only loopback endpoints; there is no supported equivalent for a remote proxy URL or `builtinTestServer`. See -[`docs/containment-configuration/0.8.0/networking/schema-updates.md`](containment-configuration/0.8.0/networking/schema-updates.md) -for the full field mapping and [`tests/examples/31_mac_network_0_8.json`](../tests/examples/31_mac_network_0_8.json) +[`tests/examples/31_mac_network_0_8.json`](../tests/examples/31_mac_network_0_8.json) for a complete example: ```json diff --git a/sdk/dotnet/README.md b/sdk/dotnet/README.md index bdedb26e7..b78535c95 100644 --- a/sdk/dotnet/README.md +++ b/sdk/dotnet/README.md @@ -216,7 +216,7 @@ Backend/platform discovery, errors, telemetry, and helpers are also in | Terminal process outcome | `WaitResult` | All types above are in `Microsoft.Mxc.Sdk.V1`. See the -[networking guide](https://github.com/microsoft/mxc/blob/main/docs/containment-configuration/0.8.0/networking/networking.md) +[networking guide](https://github.com/microsoft/mxc/blob/main/docs/schema.md#directional-networking-supported-contracts); for policy behavior and the [SDK API reference](https://github.com/microsoft/mxc/blob/main/docs/api-reference/README.md) for complete signatures and types. diff --git a/sdk/node/README.md b/sdk/node/README.md index 9f0f120cc..b757b6591 100644 --- a/sdk/node/README.md +++ b/sdk/node/README.md @@ -214,7 +214,7 @@ remain on the selected containment configuration. | Host consent presenter | `TelemetryConsentPresenter` | Network policy details are in the -[networking guide](https://github.com/microsoft/mxc/blob/main/docs/containment-configuration/0.8.0/networking/networking.md); +[networking guide](https://github.com/microsoft/mxc/blob/main/docs/schema.md#directional-networking-supported-contracts) host-specific behavior and supported capabilities are documented in the backend guides under [`docs/`](https://github.com/microsoft/mxc/tree/main/docs). See the [SDK API reference](https://github.com/microsoft/mxc/blob/main/docs/api-reference/README.md) diff --git a/src/mxc-sdk/src/backends/bubblewrap/common/bwrap_command.rs b/src/mxc-sdk/src/backends/bubblewrap/common/bwrap_command.rs index 5e6420e79..62613f5c7 100644 --- a/src/mxc-sdk/src/backends/bubblewrap/common/bwrap_command.rs +++ b/src/mxc-sdk/src/backends/bubblewrap/common/bwrap_command.rs @@ -11,8 +11,8 @@ use std::collections::HashSet; use crate::mxc_common::filesystem_resolve::FsIntent; use crate::mxc_common::models::{ - ContainerPolicy, ExecutionRequest, NetworkAction, NetworkEnforcementCompatibility, - NetworkEnforcementMode, NetworkPolicy, ProxyAddress, + ContainerPolicy, ExecutionRequest, NetworkAction, NetworkEnforcementMode, NetworkPolicy, + ProxyAddress, }; use crate::mxc_common::proxy_env::{is_managed_proxy_key, PROXY_SET_KEYS}; @@ -104,14 +104,6 @@ pub(crate) enum ResolvedNetworkMode { Isolated, /// The host network namespace without MXC firewall filtering. Shared, - /// Pre-0.8 cooperative proxy routing in the host network namespace. - LegacyProxy, - /// The host network namespace with per-destination iptables filtering. - /// - /// Pre-0.8 only. The rules land on the *host* chain, which the sandbox - /// never traverses, so this filters nothing; it is retained unchanged - /// because pre-0.8 callers already run under it. - FirewallFiltered, /// Per-destination iptables filtering inside a slirp-backed private /// namespace, with no proxy. 0.8+ only. FirewallEnforced, @@ -120,11 +112,6 @@ pub(crate) enum ResolvedNetworkMode { ProxyOnly, } -/// Whether this request uses strict network-contract behavior. -fn uses_strict_network_contract(request: &ExecutionRequest) -> bool { - request.network_enforcement_compatibility == NetworkEnforcementCompatibility::Strict -} - /// Whether the policy carries the 0.8 directional shape. /// /// Either section is enough: the parser fills both together, but checking only @@ -143,11 +130,7 @@ impl ResolvedNetworkMode { /// Classify the internal request using the proxy's resolved runtime state. pub(crate) fn from_request(request: &ExecutionRequest, proxy_active: bool) -> Self { if proxy_active { - return if uses_strict_network_contract(request) { - Self::ProxyOnly - } else { - Self::LegacyProxy - }; + return Self::ProxyOnly; } // The directional 0.8 posture is authoritative when present: the parser @@ -167,12 +150,6 @@ impl ResolvedNetworkMode { // both directions, so an open egress posture becomes an accept-all // chain rather than an unfiltered namespace. if is_directional(&request.policy) { - // Off the 0.8 contract there is no private namespace to program. - // Report the unfiltered truth and let `validate` refuse it rather - // than pretending the posture was applied. - if !uses_strict_network_contract(request) { - return Self::Shared; - } let egress = request.policy.network_egress.clone().unwrap_or_default(); // A bare deny is `--unshare-net`: cheaper than slirp plus a // deny-all chain, identical in effect, and inbound is denied by @@ -193,11 +170,7 @@ impl ResolvedNetworkMode { !request.policy.allowed_hosts.is_empty() || !request.policy.blocked_hosts.is_empty(); if uses_firewall && has_host_rules { - if uses_strict_network_contract(request) { - Self::FirewallEnforced - } else { - Self::FirewallFiltered - } + Self::FirewallEnforced } else if request.policy.default_network_policy == NetworkPolicy::Block && !has_host_rules { Self::Isolated } else { @@ -218,17 +191,6 @@ impl ResolvedNetworkMode { matches!(self, Self::ProxyOnly | Self::FirewallEnforced) } - /// Whether the mode uses the host-side firewall manager. - /// - /// False for `ProxyOnly` and `FirewallEnforced`, which are *not* - /// iptables-free: they program rules directly into the sandbox's own - /// network namespace from the supervisor (see `proxy_network`) rather than - /// going through the host manager. - #[cfg(any(target_os = "linux", test))] - pub(crate) fn requires_host_firewall_manager(self) -> bool { - matches!(self, Self::FirewallFiltered) - } - /// Whether this mode actually applies the policy's host lists. /// /// `FirewallEnforced` filters by address in the sandbox's own namespace. @@ -293,10 +255,6 @@ pub fn directional_network_rejection(request: &ExecutionRequest) -> Option<&'sta return None; } - if !uses_strict_network_contract(request) { - return Some(BWRAP_DIRECTIONAL_PRE_0_8); - } - if let Some(ingress) = ingress { if ingress.default == NetworkAction::Allow { return Some(BWRAP_INGRESS_DEFAULT_ALLOW); @@ -309,14 +267,6 @@ pub fn directional_network_rejection(request: &ExecutionRequest) -> Option<&'sta None } -/// Rejection text for a directional section on a pre-0.8 schema. -pub const BWRAP_DIRECTIONAL_PRE_0_8: &str = - "Bubblewrap: network.egress/network.ingress require schema 0.9.0-alpha or later. \ - Earlier schemas run the sandbox in the host network namespace, where the \ - directional posture would be accepted and then never programmed. Raise the \ - config's version field, or express the policy with defaultPolicy, \ - allowedHosts and blockedHosts."; - /// Rejection text for an inbound-accepting directional posture. pub const BWRAP_INGRESS_DEFAULT_ALLOW: &str = "Bubblewrap: network.ingress.default='allow' is not supported. The sandbox runs in a \ @@ -341,9 +291,6 @@ pub const BWRAP_HOST_LOOPBACK_ALLOW: &str = /// `ExecutionRequest` — reaches a runner, while only JSON configs reach the /// parser. pub fn unenforced_host_rules_rejection(request: &ExecutionRequest) -> Option<&'static str> { - if !uses_strict_network_contract(request) { - return None; - } let has_host_rules = !request.policy.allowed_hosts.is_empty() || !request.policy.blocked_hosts.is_empty(); let mode = @@ -475,9 +422,6 @@ pub fn proxy_with_firewall_rejection(request: &ExecutionRequest) -> Option<&'sta /// the exposure with `allowLocalNetwork=true`. The warning path keeps the /// looser check: it only logs. pub fn local_network_rejection(request: &ExecutionRequest) -> Option<&'static str> { - if !uses_strict_network_contract(request) { - return None; - } let mode = ResolvedNetworkMode::from_request(request, request.policy.network_proxy.is_enabled()); local_network_diagnostic_for_mode(request, mode) @@ -547,8 +491,7 @@ const DEFAULT_TERM: &str = "xterm-256color"; /// directories — a relative `--chdir` would otherwise resolve against whatever /// cwd bwrap carried into the namespace, leaving `HOME` naming a different /// directory than the one the child landed in. Normalizing against the sandbox -/// root is what makes them agree, so it is gated on the schema that introduced -/// `HOME`; below 0.9 the caller's spelling reaches `--chdir` untouched. +/// root is what makes them agree for every supported contract. /// /// A policy grant is deliberately *not* consulted: bwrap enters one only when /// `process.cwd` names it, so treating it as the start directory would put @@ -556,13 +499,7 @@ const DEFAULT_TERM: &str = "xterm-256color"; fn start_directory(request: &ExecutionRequest) -> Option { Some(request.working_directory.as_str()) .filter(|dir| !dir.is_empty()) - .map(|dir| { - if request.supplies_default_env() { - crate::mxc_common::models::sandbox_absolute_path(dir) - } else { - dir.to_string() - } - }) + .map(crate::mxc_common::models::sandbox_absolute_path) } /// The default environment: `PATH`, `TERM`, and — when one resolves — `HOME`. @@ -785,13 +722,8 @@ mod tests { /// `process.env` resolution, which schema 0.9 gave a default block. mod env { use super::*; - use crate::mxc_common::models::DefaultEnvCompatibility; - - fn request(compatibility: DefaultEnvCompatibility) -> ExecutionRequest { - ExecutionRequest { - default_env_compatibility: compatibility, - ..Default::default() - } + fn request() -> ExecutionRequest { + ExecutionRequest::default() } fn value<'a>(entries: &'a [String], key: &str) -> Option<&'a str> { @@ -810,7 +742,7 @@ mod tests { #[test] fn an_omitted_env_gets_the_default_block() { - let mut r = request(DefaultEnvCompatibility::DefaultBlock); + let mut r = request(); r.env = None; r.working_directory = "/workspace".into(); let entries = resolved_env(&r); @@ -832,21 +764,21 @@ mod tests { #[test] fn an_explicitly_empty_env_stays_empty() { - let mut r = request(DefaultEnvCompatibility::DefaultBlock); + let mut r = request(); r.env = Some(vec![]); assert!(resolved_env(&r).is_empty()); } #[test] fn a_supplied_env_is_used_verbatim() { - let mut r = request(DefaultEnvCompatibility::DefaultBlock); + let mut r = request(); r.env = Some(vec!["FOO=bar".into()]); assert_eq!(resolved_env(&r), vec!["FOO=bar".to_string()]); } #[test] fn inherit_default_env_layers_over_the_default_block() { - let mut r = request(DefaultEnvCompatibility::DefaultBlock); + let mut r = request(); r.env = Some(vec!["FOO=bar".into(), "PATH=/only/mine".into()]); r.inherit_default_env = true; let entries = resolved_env(&r); @@ -864,7 +796,7 @@ mod tests { #[test] fn home_follows_the_directory_the_child_starts_in() { - let mut r = request(DefaultEnvCompatibility::DefaultBlock); + let mut r = request(); r.env = None; r.working_directory = "/workspace".into(); assert_eq!(value(&resolved_env(&r), "HOME"), Some("/workspace")); @@ -875,7 +807,6 @@ mod tests { fn a_caller_entry_without_a_value_reaches_no_setenv() { for inherit in [false, true] { let mut r = base_request(); - r.default_env_compatibility = DefaultEnvCompatibility::DefaultBlock; r.env = Some(vec!["FEATURE_FLAG".into(), "FOO=bar".into()]); r.inherit_default_env = inherit; let args = build_args(&r, None); @@ -897,7 +828,7 @@ mod tests { /// enters must not become its `HOME`. #[test] fn a_policy_grant_alone_does_not_become_home() { - let mut r = request(DefaultEnvCompatibility::DefaultBlock); + let mut r = request(); r.env = None; r.working_directory = String::new(); // A real directory, so the shared resolver's `is_dir` probe would @@ -917,7 +848,7 @@ mod tests { #[test] fn home_and_chdir_agree() { for cwd in ["", "/workspace", "work", "./work", "a/../b", "/x/../y/./z"] { - let mut r = request(DefaultEnvCompatibility::DefaultBlock); + let mut r = request(); r.env = None; r.working_directory = cwd.into(); let args = build_args(&r, None); @@ -942,7 +873,7 @@ mod tests { ("a/../b", "/b"), ("/x/../y/./z", "/y/z"), ] { - let mut r = request(DefaultEnvCompatibility::DefaultBlock); + let mut r = request(); r.env = None; r.working_directory = cwd.into(); assert_eq!(value(&resolved_env(&r), "HOME"), Some(expected)); @@ -952,7 +883,6 @@ mod tests { #[test] fn the_default_block_reaches_the_argument_list() { let mut r = base_request(); - r.default_env_compatibility = DefaultEnvCompatibility::DefaultBlock; r.env = None; let args = build_args(&r, None); @@ -973,10 +903,7 @@ mod tests { // Without an active proxy the version is irrelevant — a 0.8 request // still classifies on policy alone (default policy is block, so this // lands on the plain isolated namespace, not the proxy one). - let request = ExecutionRequest { - network_enforcement_compatibility: NetworkEnforcementCompatibility::Strict, - ..base_request() - }; + let request = ExecutionRequest { ..base_request() }; assert_eq!( ResolvedNetworkMode::from_request(&request, false), ResolvedNetworkMode::Isolated @@ -1003,7 +930,6 @@ mod tests { struct NetworkPlanCase { name: &'static str, - compatibility: NetworkEnforcementCompatibility, default_policy: NetworkPolicy, enforcement_mode: NetworkEnforcementMode, allowed_hosts: &'static [&'static str], @@ -1017,7 +943,6 @@ mod tests { let cases = [ NetworkPlanCase { name: "default block is isolated", - compatibility: NetworkEnforcementCompatibility::Strict, default_policy: NetworkPolicy::Block, enforcement_mode: NetworkEnforcementMode::Capabilities, allowed_hosts: &[], @@ -1027,7 +952,6 @@ mod tests { }, NetworkPlanCase { name: "default allow is shared", - compatibility: NetworkEnforcementCompatibility::Strict, default_policy: NetworkPolicy::Allow, enforcement_mode: NetworkEnforcementMode::Capabilities, allowed_hosts: &[], @@ -1037,7 +961,6 @@ mod tests { }, NetworkPlanCase { name: "firewall allow rules are enforced in-namespace at 0.8", - compatibility: NetworkEnforcementCompatibility::Strict, default_policy: NetworkPolicy::Block, enforcement_mode: NetworkEnforcementMode::Firewall, allowed_hosts: &["203.0.113.7"], @@ -1047,7 +970,6 @@ mod tests { }, NetworkPlanCase { name: "firewall allow rules are enforced for host rules", - compatibility: NetworkEnforcementCompatibility::Strict, default_policy: NetworkPolicy::Block, enforcement_mode: NetworkEnforcementMode::Firewall, allowed_hosts: &["example.com"], @@ -1057,7 +979,6 @@ mod tests { }, NetworkPlanCase { name: "combined enforcement block rules are enforced at 0.8", - compatibility: NetworkEnforcementCompatibility::Strict, default_policy: NetworkPolicy::Allow, enforcement_mode: NetworkEnforcementMode::Both, allowed_hosts: &[], @@ -1067,7 +988,6 @@ mod tests { }, NetworkPlanCase { name: "combined enforcement block rules are enforced for host rules", - compatibility: NetworkEnforcementCompatibility::Strict, default_policy: NetworkPolicy::Allow, enforcement_mode: NetworkEnforcementMode::Both, allowed_hosts: &[], @@ -1077,7 +997,6 @@ mod tests { }, NetworkPlanCase { name: "capabilities mode with host rules stays shared", - compatibility: NetworkEnforcementCompatibility::Strict, default_policy: NetworkPolicy::Block, enforcement_mode: NetworkEnforcementMode::Capabilities, allowed_hosts: &["example.com"], @@ -1087,7 +1006,6 @@ mod tests { }, NetworkPlanCase { name: "proxy takes precedence over isolation", - compatibility: NetworkEnforcementCompatibility::Strict, default_policy: NetworkPolicy::Block, enforcement_mode: NetworkEnforcementMode::Capabilities, allowed_hosts: &[], @@ -1097,7 +1015,6 @@ mod tests { }, NetworkPlanCase { name: "proxy takes precedence over firewall filtering", - compatibility: NetworkEnforcementCompatibility::Strict, default_policy: NetworkPolicy::Allow, enforcement_mode: NetworkEnforcementMode::Firewall, allowed_hosts: &[], @@ -1108,10 +1025,7 @@ mod tests { ]; for case in cases { - let mut request = ExecutionRequest { - network_enforcement_compatibility: case.compatibility, - ..Default::default() - }; + let mut request = ExecutionRequest::default(); request.policy.default_network_policy = case.default_policy; request.policy.network_enforcement_mode = case.enforcement_mode; request.policy.allowed_hosts = case @@ -1136,17 +1050,10 @@ mod tests { assert!(ResolvedNetworkMode::ProxyOnly.uses_private_netns()); assert!(ResolvedNetworkMode::ProxyOnly.uses_external_userns()); assert!(!ResolvedNetworkMode::Isolated.uses_external_userns()); - assert!(!ResolvedNetworkMode::LegacyProxy.uses_private_netns()); - assert!(ResolvedNetworkMode::FirewallFiltered.requires_host_firewall_manager()); - assert!(!ResolvedNetworkMode::Shared.requires_host_firewall_manager()); - // ProxyOnly enforces via in-netns rules, not the host manager. - assert!(!ResolvedNetworkMode::ProxyOnly.requires_host_firewall_manager()); - // FirewallEnforced is the same shape as ProxyOnly minus the proxy: its - // rules are programmed from inside the sandbox's own namespace, which - // is the whole reason it filters where FirewallFiltered does not. + // ProxyOnly and FirewallEnforced program rules inside their private + // namespaces, never on the host. assert!(ResolvedNetworkMode::FirewallEnforced.uses_private_netns()); assert!(ResolvedNetworkMode::FirewallEnforced.uses_external_userns()); - assert!(!ResolvedNetworkMode::FirewallEnforced.requires_host_firewall_manager()); } #[test] @@ -1205,9 +1112,8 @@ mod tests { } #[test] - fn local_network_denied_with_0_8_proxy_is_not_warned() { - let mut r = base_request(); - r.network_enforcement_compatibility = NetworkEnforcementCompatibility::Strict; + fn local_network_denied_with_runtime_proxy_is_not_warned() { + let r = base_request(); let addr = ProxyAddress::new("127.0.0.1".into(), 8080); assert!(local_network_diagnostic(&r, Some(&addr)).is_none()); } @@ -1233,7 +1139,6 @@ mod tests { #[test] fn local_network_mismatch_is_rejected_at_0_8() { let mut r = base_request(); - r.network_enforcement_compatibility = NetworkEnforcementCompatibility::Strict; r.policy.default_network_policy = NetworkPolicy::Allow; let msg = local_network_rejection(&r).expect("shared netns cannot honor the deny"); assert!(msg.contains("allowLocalNetwork=false")); @@ -1246,7 +1151,6 @@ mod tests { // `false` is the schema default *and* a deny, so silence still asks for // inbound denial. let mut r = base_request(); - r.network_enforcement_compatibility = NetworkEnforcementCompatibility::Strict; r.policy.default_network_policy = NetworkPolicy::Allow; assert!(!r.policy.allow_local_network, "default is the deny posture"); let msg = local_network_rejection(&r).expect("an omitted deny is still a deny"); @@ -1259,18 +1163,16 @@ mod tests { fn acknowledged_local_network_exposure_is_accepted_at_0_8() { // The escape hatch from the rejection above. let mut r = base_request(); - r.network_enforcement_compatibility = NetworkEnforcementCompatibility::Strict; r.policy.default_network_policy = NetworkPolicy::Allow; r.policy.allow_local_network = true; assert!(local_network_rejection(&r).is_none()); } #[test] - fn honored_local_network_is_not_rejected_at_0_8() { + fn honored_local_network_is_not_rejected_with_ruleless_deny() { // block + no host rules + no proxy is a private netns, which satisfies // allowLocalNetwork=false; nothing to reject. - let mut r = base_request(); - r.network_enforcement_compatibility = NetworkEnforcementCompatibility::Strict; + let r = base_request(); assert!(local_network_rejection(&r).is_none()); } @@ -1281,7 +1183,6 @@ mod tests { // defaultPolicy='allow' would be a shared namespace on its own, so the // rejection fires only if the proxy is recognized without an address. let mut r = base_request(); - r.network_enforcement_compatibility = NetworkEnforcementCompatibility::Strict; r.policy.default_network_policy = NetworkPolicy::Allow; r.policy.allow_local_network = true; assert!(local_network_rejection(&r).is_none()); @@ -1296,7 +1197,6 @@ mod tests { #[test] fn external_proxy_with_host_rules_is_rejected() { let mut r = base_request(); - r.network_enforcement_compatibility = NetworkEnforcementCompatibility::Strict; r.policy.default_network_policy = NetworkPolicy::Allow; r.policy.network_proxy.address = Some(ProxyAddress::new("proxy.example.com".into(), 3128)); assert!(external_proxy_host_rules_rejection(&r).is_none()); @@ -1318,7 +1218,6 @@ mod tests { #[test] fn builtin_test_server_with_host_rules_is_not_rejected() { let mut r = base_request(); - r.network_enforcement_compatibility = NetworkEnforcementCompatibility::Strict; r.policy.network_proxy.builtin_test_server = true; r.policy.allowed_hosts = vec!["10.0.0.1".into()]; assert!(external_proxy_host_rules_rejection(&r).is_none()); @@ -1377,7 +1276,6 @@ mod tests { use crate::mxc_common::models::{NetworkEgressPolicy, NetworkRule}; let mut r = base_request(); - r.network_enforcement_compatibility = NetworkEnforcementCompatibility::Strict; r.policy.network_proxy.address = Some(ProxyAddress::new("127.0.0.1".into(), 3128)); // Legacy shape, defaulted `Block`: still refused, as before. @@ -1491,11 +1389,7 @@ mod tests { /// the gate that decides whether it is ever called. #[test] fn a_directional_rule_selects_the_namespace_whose_chain_is_programmed() { - let request = directional_egress_request( - NetworkEnforcementCompatibility::Strict, - NetworkAction::Deny, - true, - ); + let request = directional_egress_request(NetworkAction::Deny, true); assert_eq!( ResolvedNetworkMode::from_request(&request, false), ResolvedNetworkMode::FirewallEnforced @@ -1509,21 +1403,13 @@ mod tests { /// because the host namespace could not deny inbound at all. #[test] fn a_ruleless_directional_policy_still_honors_the_inbound_posture() { - let denied = directional_egress_request( - NetworkEnforcementCompatibility::Strict, - NetworkAction::Deny, - false, - ); + let denied = directional_egress_request(NetworkAction::Deny, false); assert_eq!( ResolvedNetworkMode::from_request(&denied, false), ResolvedNetworkMode::Isolated ); - let allowed = directional_egress_request( - NetworkEnforcementCompatibility::Strict, - NetworkAction::Allow, - false, - ); + let allowed = directional_egress_request(NetworkAction::Allow, false); let mode = ResolvedNetworkMode::from_request(&allowed, false); assert_eq!(mode, ResolvedNetworkMode::FirewallEnforced); assert!(mode.uses_private_netns()); @@ -1537,11 +1423,7 @@ mod tests { fn no_directional_posture_shares_the_host_namespace() { for default in [NetworkAction::Allow, NetworkAction::Deny] { for with_rule in [false, true] { - let request = directional_egress_request( - NetworkEnforcementCompatibility::Strict, - default, - with_rule, - ); + let request = directional_egress_request(default, with_rule); let mode = ResolvedNetworkMode::from_request(&request, false); assert!( mode.uses_private_netns(), @@ -1556,11 +1438,7 @@ mod tests { /// mode derives its chain from the resolved proxy endpoint. #[test] fn a_directional_section_does_not_divert_a_proxy_run() { - let mut request = directional_egress_request( - NetworkEnforcementCompatibility::Strict, - NetworkAction::Deny, - true, - ); + let mut request = directional_egress_request(NetworkAction::Deny, true); request.policy.network_proxy.builtin_test_server = true; assert_eq!( ResolvedNetworkMode::from_request(&request, true), @@ -1579,7 +1457,6 @@ mod tests { use crate::mxc_common::models::{NetworkIngressPolicy, NetworkPolicy}; let mut request = base_request(); - request.network_enforcement_compatibility = NetworkEnforcementCompatibility::Strict; request.policy.network_ingress = Some(NetworkIngressPolicy::default()); request.policy.default_network_policy = NetworkPolicy::Allow; @@ -1627,15 +1504,10 @@ mod tests { /// Build a directional request: no legacy network field is touched, which /// is exactly what the parser produces on that path. - fn directional_egress_request( - compatibility: NetworkEnforcementCompatibility, - default: NetworkAction, - with_rule: bool, - ) -> ExecutionRequest { + fn directional_egress_request(default: NetworkAction, with_rule: bool) -> ExecutionRequest { use crate::mxc_common::models::{NetworkEgressPolicy, NetworkRule}; let mut request = base_request(); - request.network_enforcement_compatibility = compatibility; request.policy.network_egress = Some(NetworkEgressPolicy { default, allow: if with_rule { @@ -1650,14 +1522,12 @@ mod tests { /// Build a directional request carrying both sections. fn directional_request( - compatibility: NetworkEnforcementCompatibility, ingress_default: NetworkAction, host_loopback: NetworkAction, ) -> ExecutionRequest { use crate::mxc_common::models::{NetworkEgressPolicy, NetworkIngressPolicy}; let mut request = base_request(); - request.network_enforcement_compatibility = compatibility; request.policy.network_egress = Some(NetworkEgressPolicy::default()); request.policy.network_ingress = Some(NetworkIngressPolicy { default: ingress_default, @@ -1670,11 +1540,7 @@ mod tests { /// list to forward one, so an inbound-accepting posture cannot be honored. #[test] fn an_inbound_accepting_directional_posture_is_refused() { - let request = directional_request( - NetworkEnforcementCompatibility::Strict, - NetworkAction::Allow, - NetworkAction::Deny, - ); + let request = directional_request(NetworkAction::Allow, NetworkAction::Deny); assert_eq!( directional_network_rejection(&request), Some(BWRAP_INGRESS_DEFAULT_ALLOW) @@ -1684,11 +1550,7 @@ mod tests { /// The sandbox's loopback is its own namespace's, not the host's. #[test] fn a_host_loopback_accepting_posture_is_refused() { - let request = directional_request( - NetworkEnforcementCompatibility::Strict, - NetworkAction::Deny, - NetworkAction::Allow, - ); + let request = directional_request(NetworkAction::Deny, NetworkAction::Allow); assert_eq!( directional_network_rejection(&request), Some(BWRAP_HOST_LOOPBACK_ALLOW) @@ -1699,11 +1561,7 @@ mod tests { /// just refusing every directional config. #[test] fn a_fully_denied_directional_posture_is_accepted() { - let request = directional_request( - NetworkEnforcementCompatibility::Strict, - NetworkAction::Deny, - NetworkAction::Deny, - ); + let request = directional_request(NetworkAction::Deny, NetworkAction::Deny); assert_eq!(directional_network_rejection(&request), None); } @@ -1712,7 +1570,6 @@ mod tests { #[test] fn a_programmatic_request_without_directional_fields_bypasses_the_directional_gate() { let mut request = base_request(); - request.network_enforcement_compatibility = NetworkEnforcementCompatibility::Strict; request.policy.default_network_policy = NetworkPolicy::Allow; request.policy.allowed_hosts = vec!["10.0.0.1".into()]; assert_eq!(directional_network_rejection(&request), None); @@ -1732,8 +1589,6 @@ mod tests { for policy in [NetworkPolicy::Allow, NetworkPolicy::Block] { for hosts in [false, true] { let mut r = base_request(); - r.network_enforcement_compatibility = - NetworkEnforcementCompatibility::Strict; r.policy.network_proxy.builtin_test_server = proxy; r.policy.network_enforcement_mode = mode.clone(); r.policy.default_network_policy = policy.clone(); @@ -2029,7 +1884,6 @@ mod tests { #[test] fn proxy_active_uses_private_network_and_external_user_namespace() { let mut r = base_request(); - r.network_enforcement_compatibility = NetworkEnforcementCompatibility::Strict; r.policy.default_network_policy = NetworkPolicy::Block; let addr = ProxyAddress::new("127.0.0.1".into(), 12345); let args = build_args(&r, Some(&addr)); diff --git a/src/mxc-sdk/src/backends/bubblewrap/common/bwrap_runner.rs b/src/mxc-sdk/src/backends/bubblewrap/common/bwrap_runner.rs index 788a117bc..4015a50aa 100644 --- a/src/mxc-sdk/src/backends/bubblewrap/common/bwrap_runner.rs +++ b/src/mxc-sdk/src/backends/bubblewrap/common/bwrap_runner.rs @@ -9,21 +9,9 @@ //! arguments via [`crate::bwrap_common::bwrap_command::build_args`], then spawns `bwrap` //! with stdout/stderr capture and optional timeout enforcement. //! -//! For per-host network filtering (`allowedHosts`/`blockedHosts`) the runner -//! supports two paths: -//! - **Cooperative env-var proxy** (default, no privilege required): when -//! `network.proxy` is configured the runner launches an unprivileged HTTP -//! proxy via [`crate::mxc_common::unix_proxy_coordinator::UnixProxyCoordinator`] -//! and the command builder injects `HTTP_PROXY` / `HTTPS_PROXY` / -//! `NO_PROXY` env vars into the sandbox. -//! - **iptables firewall** (requires `CAP_NET_ADMIN` / root): when -//! `network.enforcementMode` is `firewall` or `both`, the runner reuses -//! [`crate::lxc_common::network_iptables::NetworkIptablesManager`] from the LXC -//! backend. -//! -//! When only `defaultPolicy: "block"` is set (no host lists and no proxy), -//! the runner uses `--unshare-net` for zero-overhead full isolation -//! without root. +//! Directional network rules are programmed inside a private namespace; +//! ruleless deny uses `--unshare-net` without additional network tooling. +//! A runtime proxy uses the private namespace and a caller-managed endpoint. use std::collections::HashSet; use std::fmt::Write as FmtWrite; @@ -34,7 +22,6 @@ use std::process::{Child, ChildStdin, Command, ExitStatus, Stdio}; use std::sync::{Arc, Mutex, MutexGuard, PoisonError}; use std::time::{Duration, Instant}; -use crate::lxc_common::network_iptables::{EgressHookPoint, NetworkIptablesManager}; use crate::mxc_common::interruptible_reader::{wrap_pipe, InterruptibleReader, ReadCanceller}; use crate::mxc_common::logger::Logger; use crate::mxc_common::models::{ExecutionRequest, ScriptResponse}; @@ -551,44 +538,7 @@ impl BubblewrapScriptRunner { args.len() ); - // 3. Determine whether the host-side firewall manager is needed. Proxy - // mode does not use it: it programs its own rules inside the - // sandbox's network namespace instead (see proxy_network). - let needs_iptables = network_mode.requires_host_firewall_manager(); - let container_name = if request.container_id.is_empty() { - format!("bwrap-{:08x}", std::process::id()) - } else { - request.container_id.clone() - }; - - let fw_manager = if needs_iptables { - let _ = writeln!( - logger, - "Bubblewrap: applying iptables rules for host-level network filtering" - ); - let mut mgr = build_firewall_manager(&container_name); - match mgr.apply_firewall_rules(&request.policy, logger) { - Ok(true) => {} - Ok(false) => { - proxy.stop(logger); - return Err(ScriptResponse::error( - "Bubblewrap: failed to apply iptables firewall rules.", - )); - } - Err(e) => { - proxy.stop(logger); - return Err(ScriptResponse::error(&format!( - "Bubblewrap: network policy error: {}", - e - ))); - } - } - Some(mgr) - } else { - None - }; - - // 4. Spawn `bwrap`. + // 3. Spawn `bwrap`. let mut command = Command::new("bwrap"); command.args(&args); let pty = match stdio { @@ -620,8 +570,6 @@ impl BubblewrapScriptRunner { ) { Ok(pty) => Some(pty), Err(error) => { - let mut fw_manager = fw_manager; - cleanup_iptables(&mut fw_manager, logger); stop_proxy_network(&mut proxy_network, logger); proxy.stop(logger); return Err(ScriptResponse::error(&format!( @@ -648,8 +596,6 @@ impl BubblewrapScriptRunner { let mut child = match command.spawn() { Ok(process) => process, Err(error) => { - let mut fw_manager = fw_manager; - cleanup_iptables(&mut fw_manager, logger); stop_proxy_network(&mut proxy_network, logger); proxy.stop(logger); return Err(ScriptResponse::error(&format!( @@ -677,8 +623,6 @@ impl BubblewrapScriptRunner { if let Err(error) = startup_result { let _ = child.kill(); let _ = child.wait(); - let mut fw_manager = fw_manager; - cleanup_iptables(&mut fw_manager, logger); stop_proxy_network(&mut proxy_network, logger); proxy.stop(logger); return Err(ScriptResponse::error(&error)); @@ -702,8 +646,6 @@ impl BubblewrapScriptRunner { (out_result, err_result) => { let _ = child.kill(); let _ = child.wait(); - let mut fw_manager = fw_manager; - cleanup_iptables(&mut fw_manager, logger); stop_proxy_network(&mut proxy_network, logger); proxy.stop(logger); let error = out_result.err().or(err_result.err()); @@ -739,7 +681,6 @@ impl BubblewrapScriptRunner { group, proxy, proxy_network, - fw_manager, monitor, timeout, started, @@ -750,7 +691,7 @@ impl BubblewrapScriptRunner { } /// A spawned `bwrap` sandbox: the child process, its parent-side pipe ends, -/// and the per-run network proxy / iptables state torn down once it exits. +/// and the per-run private network state torn down once it exits. struct BwrapChild { /// The lock is what keeps [`ProviderMonitor`]'s thread from signalling a /// pid this side has already reaped. @@ -769,7 +710,6 @@ struct BwrapChild { group: bool, proxy: UnixProxyCoordinator, proxy_network: Option, - fw_manager: Option, monitor: Option, timeout: Option, started: Instant, @@ -782,14 +722,12 @@ impl BwrapChild { self.child.lock().unwrap_or_else(PoisonError::into_inner) } - /// Tear down per-run network state (iptables rules + proxy). Idempotent at - /// the manager level. + /// Tear down per-run proxy and private network state. fn cleanup(&mut self, logger: &mut Logger) { // Stopping the supervisor closes the descriptor the monitor watches, so // disarming first is what keeps a normal teardown from reading as a // provider that died. self.monitor.take(); - cleanup_iptables(&mut self.fw_manager, logger); if let Some(mut network) = self.proxy_network.take() { network.stop(logger); } @@ -1175,26 +1113,6 @@ impl Drop for BubblewrapSandboxProcess { } } -/// Build the iptables manager for a Bubblewrap sandbox. -/// -/// Unprivileged bwrap has no container network namespace MXC can enforce in: -/// the sandbox either shares the host's, where a chain would filter the host -/// itself, or holds a private one bwrap created and MXC does not manage (see -/// `local_network_diagnostic` in `bwrap_command`). The chain is built and -/// never hooked, which leaves this backend's egress policy unenforced. -fn build_firewall_manager(container_name: &str) -> NetworkIptablesManager { - NetworkIptablesManager::new(container_name, EgressHookPoint::Unhooked) -} - -/// Best-effort iptables cleanup. Called on both success and error paths. -fn cleanup_iptables(manager: &mut Option, logger: &mut Logger) { - if let Some(ref mut mgr) = manager { - if mgr.rules_applied() { - let _ = mgr.remove_firewall_rules(logger); - } - } -} - /// Tear down the proxy network namespace against the caller's logger. /// /// `Drop` would also stop it, but only through a throwaway in-memory logger, so @@ -1336,9 +1254,7 @@ fn resolve_through_symlinks(path: &Path) -> Option { #[cfg(test)] mod tests { use super::*; - use crate::mxc_common::models::{ - NetworkEnforcementCompatibility, NetworkEnforcementMode, ProxyAddress, ProxyConfig, - }; + use crate::mxc_common::models::{NetworkEnforcementMode, ProxyAddress, ProxyConfig}; fn base_request() -> ExecutionRequest { ExecutionRequest { @@ -1389,7 +1305,6 @@ mod tests { (NetworkAction::Deny, NetworkAction::Allow), ] { let mut request = base_request(); - request.network_enforcement_compatibility = NetworkEnforcementCompatibility::Strict; request.policy.network_egress = Some(NetworkEgressPolicy::default()); request.policy.network_ingress = Some(NetworkIngressPolicy { default, @@ -1451,7 +1366,6 @@ mod tests { // produces (`apply_directional_network` fills them in together). fn directional() -> ExecutionRequest { let mut request = base_request(); - request.network_enforcement_compatibility = NetworkEnforcementCompatibility::Strict; request.policy.network_mode_specified = true; request.policy.network_egress = Some(NetworkEgressPolicy::default()); request.policy.network_ingress = Some(NetworkIngressPolicy::default()); @@ -1654,7 +1568,7 @@ mod tests { // A bit added to `ALL` but not categorized above fails here. let categorized = decisions .iter() - .fold(NetworkPolicySupport::LEGACY, |acc, (bit, ..)| acc | *bit); + .fold(NetworkPolicySupport::default(), |acc, (bit, ..)| acc | *bit); assert!( categorized.contains(NetworkPolicySupport::ALL) && NetworkPolicySupport::ALL.contains(categorized), @@ -1707,21 +1621,6 @@ mod tests { } } - #[test] - fn the_firewall_manager_this_backend_builds_has_nowhere_to_enforce() { - // Unprivileged bwrap leaves MXC no container namespace to hook, so the - // manager must say so at construction. A manager that claimed a - // namespace would hook an OUTPUT chain in whatever namespace the - // process happens to be in -- the host's -- and filter the host itself. - let mgr = build_firewall_manager("bwrap-cov"); - - assert!( - !mgr.is_hooked(), - "Bubblewrap has no namespace to enforce in, so the manager it builds \ - must not claim one" - ); - } - #[test] fn an_ipv6_allow_warns_that_it_cannot_carry_traffic() { let mut req = base_request(); @@ -1798,7 +1697,6 @@ mod tests { // longer reject it locally. Reaching the injected environment probe // proves this without depending on private-network tools on the host. let mut req = base_request(); - req.network_enforcement_compatibility = NetworkEnforcementCompatibility::Strict; req.policy.network_egress = Some(NetworkEgressPolicy::default()); req.policy.network_proxy = ProxyConfig { address: None, @@ -1822,7 +1720,6 @@ mod tests { use crate::mxc_common::models::NetworkEgressPolicy; let mut req = base_request(); - req.network_enforcement_compatibility = NetworkEnforcementCompatibility::Strict; req.policy.network_egress = Some(NetworkEgressPolicy::default()); req.policy.network_proxy = ProxyConfig { address: Some(ProxyAddress::new("[::1]".into(), 3128)), @@ -1871,7 +1768,6 @@ mod tests { for egress in unhonorable { for builtin in [false, true] { let mut req = base_request(); - req.network_enforcement_compatibility = NetworkEnforcementCompatibility::Strict; req.policy.network_egress = Some(egress.clone()); req.policy.network_proxy = ProxyConfig { address: (!builtin).then(|| ProxyAddress::new("127.0.0.1".into(), 3128)), @@ -1896,7 +1792,6 @@ mod tests { use crate::mxc_common::models::{NetworkAction, NetworkEgressPolicy}; let mut req = base_request(); - req.network_enforcement_compatibility = NetworkEnforcementCompatibility::Strict; req.policy.network_egress = Some(NetworkEgressPolicy { default: NetworkAction::Deny, ..Default::default() @@ -1921,7 +1816,6 @@ mod tests { // The egress rules are IPv4-only, so this endpoint could never be // opened -- `run` would discover that only after starting slirp. let mut req = base_request(); - req.network_enforcement_compatibility = NetworkEnforcementCompatibility::Strict; req.policy.network_egress = Some(NetworkEgressPolicy::default()); req.policy.network_proxy = ProxyConfig { address: Some(ProxyAddress::new("2001:db8::1".into(), 3128)), @@ -1949,7 +1843,6 @@ mod tests { #[test] fn validate_does_not_apply_the_endpoint_check_to_the_builtin_test_server() { let mut req = base_request(); - req.network_enforcement_compatibility = NetworkEnforcementCompatibility::Strict; req.policy.network_proxy = ProxyConfig { address: Some(ProxyAddress::new("127.0.0.1".into(), 0)), builtin_test_server: true, @@ -1977,7 +1870,6 @@ mod tests { use crate::mxc_common::models::NetworkEgressPolicy; let mut req = base_request(); - req.network_enforcement_compatibility = NetworkEnforcementCompatibility::Strict; req.policy.network_egress = Some(NetworkEgressPolicy::default()); req.policy.denied_paths = vec!["/etc/hosts".into()]; req.policy.network_proxy = ProxyConfig { @@ -2007,7 +1899,6 @@ mod tests { #[test] fn a_dotdot_spelling_of_a_denied_hosts_file_still_refuses_the_pin() { let mut req = base_request(); - req.network_enforcement_compatibility = NetworkEnforcementCompatibility::Strict; req.policy.network_proxy = ProxyConfig { address: Some(ProxyAddress::new("proxy.example.com".into(), 3128)), builtin_test_server: false, @@ -2036,7 +1927,6 @@ mod tests { use crate::mxc_common::models::NetworkEgressPolicy; let mut req = base_request(); - req.network_enforcement_compatibility = NetworkEnforcementCompatibility::Strict; req.policy.network_egress = Some(NetworkEgressPolicy::default()); req.policy.denied_paths = vec!["/etc/hosts".into()]; req.policy.network_proxy = ProxyConfig { @@ -2059,7 +1949,6 @@ mod tests { #[test] fn validate_rejects_a_programmatic_hostname_firewall_rule_under_strict_policy() { let mut req = base_request(); - req.network_enforcement_compatibility = NetworkEnforcementCompatibility::Strict; req.policy.network_enforcement_mode = crate::mxc_common::models::NetworkEnforcementMode::Firewall; req.policy.allowed_hosts = vec!["api.github.com".into()]; @@ -2080,7 +1969,6 @@ mod tests { #[test] fn validate_accepts_programmatic_literal_and_cidr_firewall_rules() { let mut req = base_request(); - req.network_enforcement_compatibility = NetworkEnforcementCompatibility::Strict; req.policy.network_enforcement_mode = crate::mxc_common::models::NetworkEnforcementMode::Firewall; req.policy.allowed_hosts = vec!["203.0.113.7".into(), "10.0.0.0/8".into()]; @@ -2102,7 +1990,6 @@ mod tests { // vehicle for this: at 0.8 they resolve to a private namespace under // either enforcement mechanism. let mut req = base_request(); - req.network_enforcement_compatibility = NetworkEnforcementCompatibility::Strict; req.policy.default_network_policy = crate::mxc_common::models::NetworkPolicy::Allow; let err = BubblewrapScriptRunner::new().validate(&req).unwrap_err(); @@ -2121,7 +2008,6 @@ mod tests { // refuses the combination. Without this test, relaxing the rejection // would silently open inbound rather than fail a build. let mut req = base_request(); - req.network_enforcement_compatibility = NetworkEnforcementCompatibility::Strict; req.policy.network_enforcement_mode = NetworkEnforcementMode::Firewall; req.policy.allowed_hosts = vec!["10.0.2.2/32".into()]; req.policy.allow_local_network = true; @@ -2140,7 +2026,6 @@ mod tests { // 'capabilities' with no proxy nothing applies them, so a default-deny // policy ran with fully open egress on the host's namespace. let mut req = base_request(); - req.network_enforcement_compatibility = NetworkEnforcementCompatibility::Strict; req.policy.default_network_policy = crate::mxc_common::models::NetworkPolicy::Block; req.policy.allowed_hosts = vec!["api.github.com".into()]; req.policy.allow_local_network = true; @@ -2159,7 +2044,6 @@ mod tests { #[test] fn validate_rejects_block_default_blocklist_without_allowlist() { let mut req = base_request(); - req.network_enforcement_compatibility = NetworkEnforcementCompatibility::Strict; req.policy.default_network_policy = crate::mxc_common::models::NetworkPolicy::Block; req.policy.blocked_hosts = vec!["evil.example.com".into()]; req.policy.network_proxy = ProxyConfig { diff --git a/src/mxc-sdk/src/backends/bubblewrap/common/proxy_network.rs b/src/mxc-sdk/src/backends/bubblewrap/common/proxy_network.rs index 97b48e04f..a372b1905 100644 --- a/src/mxc-sdk/src/backends/bubblewrap/common/proxy_network.rs +++ b/src/mxc-sdk/src/backends/bubblewrap/common/proxy_network.rs @@ -2066,7 +2066,6 @@ fn terminate_child(child: &mut Child) { #[cfg(test)] mod tests { use super::*; - use crate::mxc_common::models::NetworkEnforcementCompatibility; /// The reported concern: a hostname proxy resolves through the host's /// resolver during setup, before the script timeout applies, so an @@ -3006,7 +3005,6 @@ mod tests { /// public constructor so the count matches what the supervisor installs. fn plan_with_rule_count(count: usize) -> EgressPlan { let mut request = crate::mxc_common::models::ExecutionRequest { - network_enforcement_compatibility: NetworkEnforcementCompatibility::Strict, ..Default::default() }; request.policy.allowed_hosts = (0..count) diff --git a/src/mxc-sdk/src/backends/hyperlight/common/mod.rs b/src/mxc-sdk/src/backends/hyperlight/common/mod.rs index 73b9559f3..512beaff4 100644 --- a/src/mxc-sdk/src/backends/hyperlight/common/mod.rs +++ b/src/mxc-sdk/src/backends/hyperlight/common/mod.rs @@ -755,7 +755,7 @@ impl HyperlightScriptRunner { impl ScriptRunner for HyperlightScriptRunner { fn validate_runner(&self, request: &ExecutionRequest) -> Result<(), ScriptResponse> { Self::validate_policies(request).map_err(|e| e.to_response())?; - validate_network_policy_support(request, NetworkPolicySupport::LEGACY)?; + validate_network_policy_support(request, NetworkPolicySupport::default())?; Ok(()) } diff --git a/src/mxc-sdk/src/backends/isolation_session/common/policy.rs b/src/mxc-sdk/src/backends/isolation_session/common/policy.rs index acfa8e52f..23911f243 100644 --- a/src/mxc-sdk/src/backends/isolation_session/common/policy.rs +++ b/src/mxc-sdk/src/backends/isolation_session/common/policy.rs @@ -168,8 +168,8 @@ fn validate_provision_network_policy( mod tests { use super::*; use crate::mxc_common::models::{ - ContainerPolicy, DefaultEnvCompatibility, NetworkEgressPolicy, NetworkIngressPolicy, - ProxyAddress, ProxyConfig, UiPolicy, + ContainerPolicy, NetworkEgressPolicy, NetworkIngressPolicy, ProxyAddress, ProxyConfig, + UiPolicy, }; use crate::mxc_common::mxc_error::MxcErrorCode; @@ -746,13 +746,8 @@ mod tests { // ====== process.env (refused when it would replace the default) ====== - fn request_with_env( - compatibility: DefaultEnvCompatibility, - env: Option>, - inherit_default_env: bool, - ) -> ExecutionRequest { + fn request_with_env(env: Option>, inherit_default_env: bool) -> ExecutionRequest { ExecutionRequest { - default_env_compatibility: compatibility, env: env.map(|e| e.into_iter().map(String::from).collect()), inherit_default_env, ..Default::default() @@ -761,21 +756,18 @@ mod tests { #[test] fn only_an_environment_that_would_replace_the_default_is_refused() { - use DefaultEnvCompatibility::DefaultBlock; - let cases = [ - (None, false, DefaultBlock, true), - (None, true, DefaultBlock, true), - (Some(vec![]), false, DefaultBlock, false), - (Some(vec![]), true, DefaultBlock, true), - (Some(vec!["FOO=bar"]), false, DefaultBlock, false), - (Some(vec!["FOO=bar"]), true, DefaultBlock, true), + (None, false, true), + (None, true, true), + (Some(vec![]), false, false), + (Some(vec![]), true, true), + (Some(vec!["FOO=bar"]), false, false), + (Some(vec!["FOO=bar"]), true, true), ]; - for (env, inherit_default_env, compatibility, accepted) in cases { - let state = - format!("{env:?}, inherit_default_env={inherit_default_env}, {compatibility:?}"); - let request = request_with_env(compatibility, env, inherit_default_env); + for (env, inherit_default_env, accepted) in cases { + let state = format!("{env:?}, inherit_default_env={inherit_default_env}"); + let request = request_with_env(env, inherit_default_env); match (reject_unhonorable_environment(&request), accepted) { (Ok(()), true) => {} (Err(err), false) => assert_policy_err_contains(err, ERR_ENVIRONMENT_POLICY), diff --git a/src/mxc-sdk/src/backends/lxc/common/lxc_runner.rs b/src/mxc-sdk/src/backends/lxc/common/lxc_runner.rs index 68e59f186..556404352 100644 --- a/src/mxc-sdk/src/backends/lxc/common/lxc_runner.rs +++ b/src/mxc-sdk/src/backends/lxc/common/lxc_runner.rs @@ -59,8 +59,7 @@ const DEFAULT_TERM: &str = "xterm-256color"; /// different directories — `lxc-attach` starts at the container root, so a /// relative `process.cwd` would otherwise leave `HOME` naming a different /// directory than the one the child landed in. Normalizing against the -/// container root is what makes them agree, so it is gated on the schema that -/// introduced `HOME`; below 0.9 the caller's spelling reaches `cd` untouched. +/// container root is what makes them agree for every supported contract. /// /// A policy grant is deliberately *not* consulted: with `process.cwd` omitted /// the child starts at the container root, so treating a grant as the start @@ -68,13 +67,7 @@ const DEFAULT_TERM: &str = "xterm-256color"; fn start_directory(request: &ExecutionRequest) -> Option { Some(request.working_directory.as_str()) .filter(|dir| !dir.is_empty()) - .map(|dir| { - if request.supplies_default_env() { - crate::mxc_common::models::sandbox_absolute_path(dir) - } else { - dir.to_string() - } - }) + .map(crate::mxc_common::models::sandbox_absolute_path) } /// The default environment, from schema 0.9: `PATH`, `TERM`, and -- when one @@ -2330,13 +2323,8 @@ mod tests { /// `process.env` resolution, which schema 0.9 gave a default block. mod env { use super::*; - use crate::mxc_common::models::DefaultEnvCompatibility; - - fn request(compatibility: DefaultEnvCompatibility) -> ExecutionRequest { - ExecutionRequest { - default_env_compatibility: compatibility, - ..Default::default() - } + fn request() -> ExecutionRequest { + ExecutionRequest::default() } fn value<'a>(entries: &'a [String], key: &str) -> Option<&'a str> { @@ -2355,7 +2343,7 @@ mod tests { #[test] fn an_omitted_env_gets_the_default_block() { - let mut r = request(DefaultEnvCompatibility::DefaultBlock); + let mut r = request(); r.env = None; r.working_directory = "/workspace".into(); let entries = resolved_env(&r); @@ -2376,21 +2364,21 @@ mod tests { #[test] fn an_explicitly_empty_env_stays_empty() { - let mut r = request(DefaultEnvCompatibility::DefaultBlock); + let mut r = request(); r.env = Some(vec![]); assert!(resolved_env(&r).is_empty()); } #[test] fn a_supplied_env_is_used_verbatim() { - let mut r = request(DefaultEnvCompatibility::DefaultBlock); + let mut r = request(); r.env = Some(vec!["FOO=bar".into()]); assert_eq!(resolved_env(&r), vec!["FOO=bar".to_string()]); } #[test] fn inherit_default_env_layers_over_the_default_block() { - let mut r = request(DefaultEnvCompatibility::DefaultBlock); + let mut r = request(); r.env = Some(vec!["FOO=bar".into(), "PATH=/only/mine".into()]); r.inherit_default_env = true; let entries = resolved_env(&r); @@ -2408,7 +2396,7 @@ mod tests { #[test] fn home_follows_the_directory_the_child_starts_in() { - let mut r = request(DefaultEnvCompatibility::DefaultBlock); + let mut r = request(); r.env = None; r.working_directory = "/workspace".into(); assert_eq!(value(&resolved_env(&r), "HOME"), Some("/workspace")); @@ -2422,7 +2410,7 @@ mod tests { ("a/../b", "/b"), ("/x/../y/./z", "/y/z"), ] { - let mut r = request(DefaultEnvCompatibility::DefaultBlock); + let mut r = request(); r.env = None; r.working_directory = cwd.into(); assert_eq!(value(&resolved_env(&r), "HOME"), Some(expected)); @@ -2436,7 +2424,7 @@ mod tests { /// `HOME`. #[test] fn a_policy_grant_alone_does_not_become_home() { - let mut r = request(DefaultEnvCompatibility::DefaultBlock); + let mut r = request(); r.env = None; r.working_directory = String::new(); // A real directory, so the shared resolver's `is_dir` probe would @@ -2452,7 +2440,7 @@ mod tests { #[test] fn home_and_the_attach_directory_agree() { for cwd in ["", "/workspace", "work", "./work", "a/../b", "/x/../y/./z"] { - let mut r = request(DefaultEnvCompatibility::DefaultBlock); + let mut r = request(); r.env = None; r.working_directory = cwd.into(); assert_eq!( @@ -2465,7 +2453,7 @@ mod tests { #[test] fn a_caller_entry_without_a_value_is_dropped_by_the_merge() { - let mut r = request(DefaultEnvCompatibility::DefaultBlock); + let mut r = request(); r.env = Some(vec!["FEATURE_FLAG".into(), "FOO=bar".into()]); // Verbatim: carried through, dropped when `lxc-attach` args build. diff --git a/src/mxc-sdk/src/backends/seatbelt/common/default_env.rs b/src/mxc-sdk/src/backends/seatbelt/common/default_env.rs index 495362fe5..0fbdfbf1e 100644 --- a/src/mxc-sdk/src/backends/seatbelt/common/default_env.rs +++ b/src/mxc-sdk/src/backends/seatbelt/common/default_env.rs @@ -50,13 +50,8 @@ pub use crate::mxc_common::default_env::env_pairs; #[cfg(test)] mod tests { use super::*; - use crate::mxc_common::models::DefaultEnvCompatibility; - - fn request(compatibility: DefaultEnvCompatibility) -> ExecutionRequest { - ExecutionRequest { - default_env_compatibility: compatibility, - ..Default::default() - } + fn request() -> ExecutionRequest { + ExecutionRequest::default() } fn value<'a>(entries: &'a [String], key: &str) -> Option<&'a str> { @@ -78,7 +73,7 @@ mod tests { #[test] fn an_omitted_env_gets_the_default_block() { - let mut r = request(DefaultEnvCompatibility::DefaultBlock); + let mut r = request(); r.env = None; let entries = resolved_env(&r, Some("/workspace")); assert_eq!(value(&entries, "PATH"), Some(DEFAULT_SANDBOX_PATH)); @@ -88,7 +83,7 @@ mod tests { #[test] fn an_explicitly_empty_env_stays_empty() { - let mut r = request(DefaultEnvCompatibility::DefaultBlock); + let mut r = request(); r.env = Some(vec![]); assert!(resolved_env(&r, None).is_empty()); } @@ -96,14 +91,14 @@ mod tests { #[test] fn a_supplied_env_is_used_verbatim() { // The 0.9 behavior change: no implicit PATH under a supplied env. - let mut r = request(DefaultEnvCompatibility::DefaultBlock); + let mut r = request(); r.env = Some(vec!["FOO=bar".into()]); assert_eq!(resolved_env(&r, None), vec!["FOO=bar".to_string()]); } #[test] fn inherit_default_env_layers_over_the_default_block() { - let mut r = request(DefaultEnvCompatibility::DefaultBlock); + let mut r = request(); r.env = Some(vec!["FOO=bar".into(), "PATH=/only/mine".into()]); r.inherit_default_env = true; let entries = resolved_env(&r, None); @@ -122,7 +117,7 @@ mod tests { /// deliberately does not re-derive it from the request. #[test] fn home_follows_the_directory_the_child_starts_in() { - let mut r = request(DefaultEnvCompatibility::DefaultBlock); + let mut r = request(); r.env = None; assert_eq!( value(&resolved_env(&r, Some("/workspace")), "HOME"), @@ -132,7 +127,7 @@ mod tests { #[test] fn an_unresolved_working_directory_leaves_home_unset() { - let mut r = request(DefaultEnvCompatibility::DefaultBlock); + let mut r = request(); r.env = None; // Set on the request but never resolved by the runner: only what the // runner passes in counts. @@ -147,7 +142,7 @@ mod tests { #[test] fn a_caller_entry_without_a_value_is_dropped_by_both_modes() { - let mut r = request(DefaultEnvCompatibility::DefaultBlock); + let mut r = request(); r.env = Some(vec!["FEATURE_FLAG".into(), "FOO=bar".into()]); let verbatim = resolved_env(&r, None); diff --git a/src/mxc-sdk/src/backends/seatbelt/common/seatbelt_runner.rs b/src/mxc-sdk/src/backends/seatbelt/common/seatbelt_runner.rs index 5c47a3ff0..909e25d35 100644 --- a/src/mxc-sdk/src/backends/seatbelt/common/seatbelt_runner.rs +++ b/src/mxc-sdk/src/backends/seatbelt/common/seatbelt_runner.rs @@ -43,7 +43,9 @@ use crate::mxc_common::validator::{ }; use crate::mxc_pty::{LivePty, PtySize as UnixPtySize}; -use crate::seatbelt_common::default_env::{env_pairs, resolved_env, DEFAULT_SANDBOX_PATH}; +#[cfg(test)] +use crate::seatbelt_common::default_env::DEFAULT_SANDBOX_PATH; +use crate::seatbelt_common::default_env::{env_pairs, resolved_env}; use crate::seatbelt_common::profile_builder::build_profile_with_proxy; #[link(name = "proc")] @@ -1241,11 +1243,6 @@ fn apply_clean_environment( working_directory: Option<&str>, ) { command.env_clear(); - // From 0.9 `resolved_env` carries `PATH`, and an explicitly empty - // `process.env` must stay empty rather than keep a floor under it. - if !request.supplies_default_env() { - command.env("PATH", DEFAULT_SANDBOX_PATH); - } for (key, value) in resolve_environment(request, proxy_address, working_directory) { command.env(key, value); } @@ -1341,8 +1338,8 @@ fn cleanup_files(paths: &[&str]) { mod tests { use super::*; use crate::mxc_common::models::{ - DefaultEnvCompatibility, ExecutionRequest, NetworkAction, NetworkEgressPolicy, - NetworkPolicy, ProxyAddress, SeatbeltConfig, + ExecutionRequest, NetworkAction, NetworkEgressPolicy, NetworkPolicy, ProxyAddress, + SeatbeltConfig, }; #[allow(clippy::field_reassign_with_default)] @@ -1389,7 +1386,6 @@ mod tests { // The 0.9 default block must not bypass the proxy stripping in // `resolve_environment`. let mut request = base_request(); - request.default_env_compatibility = DefaultEnvCompatibility::DefaultBlock; request.env = Some(vec!["HTTP_PROXY=http://attacker.example:9999".into()]); request.inherit_default_env = true; let addr = ProxyAddress::new("127.0.0.1".into(), 8888); @@ -1909,16 +1905,7 @@ mod tests { assert_eq!( env_value(&pairs, "HOME"), None, - "below 0.9 supplies no HOME" - ); - - let mut modern = base_request(); - modern.default_env_compatibility = DefaultEnvCompatibility::DefaultBlock; - let pairs = resolve_environment(&modern, None, None); - assert_eq!( - env_value(&pairs, "HOME"), - None, - "0.9 leaves HOME unset when no directory resolves" + "HOME stays unset when no directory resolves" ); } @@ -1928,7 +1915,6 @@ mod tests { #[test] fn home_follows_the_directory_the_child_starts_in() { let mut request = base_request(); - request.default_env_compatibility = DefaultEnvCompatibility::DefaultBlock; let pairs = resolve_environment(&request, None, Some("/Users/someone/work")); assert_eq!(env_value(&pairs, "HOME"), Some("/Users/someone/work")); } diff --git a/src/mxc-sdk/src/backends/windows_sandbox/lifecycle/one_shot.rs b/src/mxc-sdk/src/backends/windows_sandbox/lifecycle/one_shot.rs index 4077ce685..91f6fc6f8 100644 --- a/src/mxc-sdk/src/backends/windows_sandbox/lifecycle/one_shot.rs +++ b/src/mxc-sdk/src/backends/windows_sandbox/lifecycle/one_shot.rs @@ -129,7 +129,7 @@ impl WindowsSandboxRunner { impl ScriptRunner for WindowsSandboxRunner { fn validate_runner(&self, request: &ExecutionRequest) -> Result<(), ScriptResponse> { - validate_network_policy_support(request, NetworkPolicySupport::LEGACY)?; + validate_network_policy_support(request, NetworkPolicySupport::default())?; Ok(()) } diff --git a/src/mxc-sdk/src/backends/windows_sandbox/lifecycle/state_aware.rs b/src/mxc-sdk/src/backends/windows_sandbox/lifecycle/state_aware.rs index 63f04948a..739a8f603 100644 --- a/src/mxc-sdk/src/backends/windows_sandbox/lifecycle/state_aware.rs +++ b/src/mxc-sdk/src/backends/windows_sandbox/lifecycle/state_aware.rs @@ -1013,7 +1013,7 @@ impl StatefulSandboxBackend for WindowsSandboxRunner { request: &ExecutionRequest, _config: Option<&()>, ) -> Result<(), MxcError> { - validate_state_aware_network_policy_support(request, NetworkPolicySupport::LEGACY)?; + validate_state_aware_network_policy_support(request, NetworkPolicySupport::default())?; policy::plan_policy(request) .map(|_| ()) .map_err(map_policy_error) diff --git a/src/mxc-sdk/src/backends/wslc/common/process_env.rs b/src/mxc-sdk/src/backends/wslc/common/process_env.rs index ed995f9bb..5f8c1b86b 100644 --- a/src/mxc-sdk/src/backends/wslc/common/process_env.rs +++ b/src/mxc-sdk/src/backends/wslc/common/process_env.rs @@ -88,11 +88,9 @@ pub fn sdk_entries(scope: EnvScope, entries: &[String]) -> Vec { #[cfg(test)] mod tests { use super::*; - use crate::mxc_common::models::DefaultEnvCompatibility; fn request(env: Option>, inherit_default_env: bool) -> ExecutionRequest { ExecutionRequest { - default_env_compatibility: DefaultEnvCompatibility::DefaultBlock, env: env.map(|e| e.into_iter().map(String::from).collect()), inherit_default_env, ..Default::default() diff --git a/src/mxc-sdk/src/backends/wslc/common/state_aware.rs b/src/mxc-sdk/src/backends/wslc/common/state_aware.rs index 4edc796cc..761982b69 100644 --- a/src/mxc-sdk/src/backends/wslc/common/state_aware.rs +++ b/src/mxc-sdk/src/backends/wslc/common/state_aware.rs @@ -626,8 +626,8 @@ fn split_env(env: &[String]) -> Vec<(String, String)> { mod tests { use super::*; use crate::mxc_common::models::{ - ContainerPolicy, DefaultEnvCompatibility, NetworkAction, NetworkEgressPolicy, - NetworkIngressPolicy, ProxyAddress, ProxyConfig, + ContainerPolicy, NetworkAction, NetworkEgressPolicy, NetworkIngressPolicy, ProxyAddress, + ProxyConfig, }; /// The exit code is unrecoverable once dropped, so failing the call must not @@ -1115,7 +1115,6 @@ mod tests { fn the_exec_config_carries_the_scope_each_state_of_process_env_selects() { struct Case { label: &'static str, - compatibility: DefaultEnvCompatibility, env: Option>, inherit_default_env: bool, scope: EnvScope, @@ -1125,7 +1124,6 @@ mod tests { let cases = [ Case { label: "omitted takes the image environment", - compatibility: DefaultEnvCompatibility::DefaultBlock, env: None, inherit_default_env: false, scope: EnvScope::Merge, @@ -1133,7 +1131,6 @@ mod tests { }, Case { label: "explicitly empty leaves the child nothing", - compatibility: DefaultEnvCompatibility::DefaultBlock, env: Some(vec![]), inherit_default_env: false, scope: EnvScope::Replace, @@ -1141,7 +1138,6 @@ mod tests { }, Case { label: "explicitly empty plus inheritDefaultEnv takes the image environment", - compatibility: DefaultEnvCompatibility::DefaultBlock, env: Some(vec![]), inherit_default_env: true, scope: EnvScope::Merge, @@ -1149,7 +1145,6 @@ mod tests { }, Case { label: "supplied is used verbatim", - compatibility: DefaultEnvCompatibility::DefaultBlock, env: Some(vec!["FOO=bar"]), inherit_default_env: false, scope: EnvScope::Replace, @@ -1157,7 +1152,6 @@ mod tests { }, Case { label: "supplied plus inheritDefaultEnv layers over the image environment", - compatibility: DefaultEnvCompatibility::DefaultBlock, env: Some(vec!["FOO=bar"]), inherit_default_env: true, scope: EnvScope::Merge, @@ -1167,7 +1161,6 @@ mod tests { for case in cases { let request = ExecutionRequest { - default_env_compatibility: case.compatibility, env: case.env.map(|e| e.into_iter().map(String::from).collect()), inherit_default_env: case.inherit_default_env, script_code: "echo hi".to_string(), @@ -1196,7 +1189,6 @@ mod tests { #[test] fn the_exec_config_keeps_the_cooperative_proxy_out_of_the_callers_reach() { let request = ExecutionRequest { - default_env_compatibility: DefaultEnvCompatibility::DefaultBlock, env: Some(vec![ "FOO=bar".to_string(), "HTTP_PROXY=http://attacker.invalid:1".to_string(), diff --git a/src/mxc-sdk/src/configs/process_container.rs b/src/mxc-sdk/src/configs/process_container.rs index 07140cd33..99ec6b2a3 100644 --- a/src/mxc-sdk/src/configs/process_container.rs +++ b/src/mxc-sdk/src/configs/process_container.rs @@ -139,7 +139,6 @@ mod tests { use super::*; use crate::mxc_common::models::{ CaptureDenialsMode as RuntimeCaptureDenialsMode, ContainmentBackend, - NetworkEnforcementCompatibility, }; use crate::policy::{ build_request_with_containment, ContainerPolicy, Containment, NetworkAction, @@ -191,10 +190,6 @@ mod tests { let inner = &request.inner; assert_eq!(inner.source_contract, None); - assert_eq!( - inner.network_enforcement_compatibility, - NetworkEnforcementCompatibility::Strict - ); assert_eq!(inner.containment, ContainmentBackend::ProcessContainer); assert!(!inner.policy.least_privilege_mode); assert!(inner diff --git a/src/mxc-sdk/src/core/mxc_common/common_request_ir.rs b/src/mxc-sdk/src/core/mxc_common/common_request_ir.rs index 7c27dfd43..74d9a8f6b 100644 --- a/src/mxc-sdk/src/core/mxc_common/common_request_ir.rs +++ b/src/mxc-sdk/src/core/mxc_common/common_request_ir.rs @@ -3,7 +3,6 @@ //! Common request intermediate representation assembled by exact contract adapters. -use crate::mxc_common::models::{DefaultEnvCompatibility, NetworkEnforcementCompatibility}; use crate::mxc_common::wire; use crate::mxc_contract::ContractVersion; @@ -12,8 +11,6 @@ pub(crate) struct CommonRequestIR { pub(crate) schema: Option, pub(crate) comment: Option, pub(crate) source_contract: ContractVersion, - pub(crate) network_enforcement_compatibility: NetworkEnforcementCompatibility, - pub(crate) default_env_compatibility: DefaultEnvCompatibility, pub(crate) phase: Option, pub(crate) sandbox_id: Option, pub(crate) container_id: Option, diff --git a/src/mxc-sdk/src/core/mxc_common/config_contract_adapters/dev/common.rs b/src/mxc-sdk/src/core/mxc_common/config_contract_adapters/dev/common.rs index d2e837167..01135fa0d 100644 --- a/src/mxc-sdk/src/core/mxc_common/config_contract_adapters/dev/common.rs +++ b/src/mxc-sdk/src/core/mxc_common/config_contract_adapters/dev/common.rs @@ -123,12 +123,6 @@ fn convert_network_ingress(value: contract::NetworkIngress) -> wire::NetworkIngr pub(super) fn convert_network(value: contract::Network) -> wire::Network { let contract::Network { egress, ingress } = value; wire::Network { - default_policy: None, - enforcement_mode: None, - allow_local_network: None, - allowed_hosts: None, - blocked_hosts: None, - proxy: None, egress: egress.into_option().map(convert_network_egress), ingress: ingress.into_option().map(convert_network_ingress), } diff --git a/src/mxc-sdk/src/core/mxc_common/config_contract_adapters/dev/one_shot.rs b/src/mxc-sdk/src/core/mxc_common/config_contract_adapters/dev/one_shot.rs index 56da7a56b..31471ea81 100644 --- a/src/mxc-sdk/src/core/mxc_common/config_contract_adapters/dev/one_shot.rs +++ b/src/mxc-sdk/src/core/mxc_common/config_contract_adapters/dev/one_shot.rs @@ -286,9 +286,6 @@ pub(super) fn into_common_request_ir( schema: schema.into_option(), comment: comment.into_option(), source_contract: crate::mxc_contract::ContractVersion::V1_1_0Alpha, - default_env_compatibility: crate::mxc_common::models::DefaultEnvCompatibility::DefaultBlock, - network_enforcement_compatibility: - crate::mxc_common::models::NetworkEnforcementCompatibility::Strict, phase: None, sandbox_id: None, container_id: container_id.into_option(), diff --git a/src/mxc-sdk/src/core/mxc_common/config_contract_adapters/dev/one_shot_tests/stable_candidate.rs b/src/mxc-sdk/src/core/mxc_common/config_contract_adapters/dev/one_shot_tests/stable_candidate.rs index fbe06247f..98fd657cd 100644 --- a/src/mxc-sdk/src/core/mxc_common/config_contract_adapters/dev/one_shot_tests/stable_candidate.rs +++ b/src/mxc-sdk/src/core/mxc_common/config_contract_adapters/dev/one_shot_tests/stable_candidate.rs @@ -399,8 +399,6 @@ fn process_container_request_maps_expected_wire_fields() { assert_eq!(fallback.allow_dacl_mutation, Some(true)); let network = wire.network.expect("network should be populated"); - assert!(network.default_policy.is_none()); - assert!(network.proxy.is_none()); assert!(matches!( network.egress.unwrap().default, Some(super::wire::NetworkAction::Deny) @@ -498,8 +496,6 @@ fn lxc_request_maps_expected_wire_fields() { ); let network = wire.network.expect("network should be populated"); - assert!(network.default_policy.is_none()); - assert!(network.enforcement_mode.is_none()); assert!(matches!( network.egress.unwrap().default, Some(super::wire::NetworkAction::Allow) @@ -508,7 +504,6 @@ fn lxc_request_maps_expected_wire_fields() { network.ingress.unwrap().default, Some(super::wire::NetworkAction::Allow) )); - assert!(network.proxy.is_none()); let ui = wire.ui.expect("ui should be populated"); assert_eq!(ui.disable, Some(true)); @@ -567,8 +562,6 @@ fn seatbelt_request_maps_expected_wire_fields() { ); let network = wire.network.expect("network should be populated"); - assert!(network.default_policy.is_none()); - assert!(network.enforcement_mode.is_none()); assert!(matches!( network.egress.unwrap().default, Some(super::wire::NetworkAction::Allow) @@ -577,7 +570,6 @@ fn seatbelt_request_maps_expected_wire_fields() { network.ingress.unwrap().default, Some(super::wire::NetworkAction::Allow) )); - assert!(network.proxy.is_none()); let seatbelt = wire.seatbelt.expect("seatbelt should be populated"); assert_eq!( @@ -611,12 +603,8 @@ fn empty_optional_sections_map_to_present_empty_wire_sections() { assert!(fallback.allow_dacl_mutation.is_none()); let network = wire.network.expect("network should be populated"); - assert!(network.default_policy.is_none()); - assert!(network.enforcement_mode.is_none()); - assert!(network.allow_local_network.is_none()); - assert!(network.allowed_hosts.is_none()); - assert!(network.blocked_hosts.is_none()); - assert!(network.proxy.is_none()); + assert!(network.egress.is_none()); + assert!(network.ingress.is_none()); let ui = wire.ui.expect("ui should be populated"); assert!(ui.disable.is_none()); @@ -1023,11 +1011,6 @@ fn directional_network_request_maps_expected_wire_fields() { ingress.host_loopback, Some(super::wire::NetworkAction::Allow) )); - - // The legacy fields stay absent when only directional policy is supplied. - assert!(network.default_policy.is_none()); - assert!(network.enforcement_mode.is_none()); - assert!(network.proxy.is_none()); } #[test] fn every_network_action_maps_to_the_expected_wire_value() { diff --git a/src/mxc-sdk/src/core/mxc_common/config_contract_adapters/dev/state_aware.rs b/src/mxc-sdk/src/core/mxc_common/config_contract_adapters/dev/state_aware.rs index bf11364e5..f22be133d 100644 --- a/src/mxc-sdk/src/core/mxc_common/config_contract_adapters/dev/state_aware.rs +++ b/src/mxc-sdk/src/core/mxc_common/config_contract_adapters/dev/state_aware.rs @@ -39,12 +39,6 @@ fn convert_isolation_session_network(value: contract::IsolationSessionNetwork) - host_loopback: contract::IsolationSessionNetworkAllow, } = ingress; wire::Network { - allow_local_network: None, - default_policy: None, - allowed_hosts: None, - enforcement_mode: None, - blocked_hosts: None, - proxy: None, egress: Some(wire::NetworkEgress { default: Some(wire::NetworkAction::Allow), allow: None, @@ -83,9 +77,6 @@ fn state_aware_common( schema: schema.into_option(), comment: comment.into_option(), source_contract: crate::mxc_contract::ContractVersion::V1_1_0Alpha, - default_env_compatibility: crate::mxc_common::models::DefaultEnvCompatibility::DefaultBlock, - network_enforcement_compatibility: - crate::mxc_common::models::NetworkEnforcementCompatibility::Strict, phase: None, test_feature: None, windows_sandbox: None, diff --git a/src/mxc-sdk/src/core/mxc_common/config_contract_adapters/dev/state_aware_tests/exec.rs b/src/mxc-sdk/src/core/mxc_common/config_contract_adapters/dev/state_aware_tests/exec.rs index 21cf93915..6f5dcb641 100644 --- a/src/mxc-sdk/src/core/mxc_common/config_contract_adapters/dev/state_aware_tests/exec.rs +++ b/src/mxc-sdk/src/core/mxc_common/config_contract_adapters/dev/state_aware_tests/exec.rs @@ -47,9 +47,6 @@ fn exec_preserves_process_network_telemetry_and_empty_values() { network.ingress.unwrap().default, Some(wire::NetworkAction::Deny) )); - assert!(network.allow_local_network.is_none()); - assert!(network.allowed_hosts.is_none()); - assert!(network.blocked_hosts.is_none()); assert_eq!( common.runtime_config.unwrap().network_proxy.as_deref(), Some("http://127.0.0.1:8080") diff --git a/src/mxc-sdk/src/core/mxc_common/config_contract_adapters/dev/state_aware_tests/provision.rs b/src/mxc-sdk/src/core/mxc_common/config_contract_adapters/dev/state_aware_tests/provision.rs index 61a342ee7..2fe831fce 100644 --- a/src/mxc-sdk/src/core/mxc_common/config_contract_adapters/dev/state_aware_tests/provision.rs +++ b/src/mxc-sdk/src/core/mxc_common/config_contract_adapters/dev/state_aware_tests/provision.rs @@ -38,8 +38,6 @@ fn isolation_session_configuration_presence_matches_explicit_values() { assert!(common.filesystem.is_none()); assert!(common.process.is_none()); let network = common.network.unwrap(); - assert!(network.allow_local_network.is_none()); - assert!(network.default_policy.is_none()); assert!(matches!( network.egress.as_ref().unwrap().default, Some(wire::NetworkAction::Allow) @@ -220,10 +218,8 @@ fn provision_common_fields_are_independent_of_backend_payload() { } let (common, _) = adapt(&source("wslc", r#","network":{}"#)); let network = common.network.unwrap(); - assert!(network.default_policy.is_none()); - assert!(network.allowed_hosts.is_none()); - assert!(network.blocked_hosts.is_none()); - assert!(network.proxy.is_none()); + assert!(network.egress.is_none()); + assert!(network.ingress.is_none()); } #[test] diff --git a/src/mxc-sdk/src/core/mxc_common/config_contract_adapters/v0_9/common.rs b/src/mxc-sdk/src/core/mxc_common/config_contract_adapters/v0_9/common.rs index 1d64f1722..3bdcb3a06 100644 --- a/src/mxc-sdk/src/core/mxc_common/config_contract_adapters/v0_9/common.rs +++ b/src/mxc-sdk/src/core/mxc_common/config_contract_adapters/v0_9/common.rs @@ -123,12 +123,6 @@ fn convert_network_ingress(value: contract::NetworkIngress) -> wire::NetworkIngr pub(super) fn convert_network(value: contract::Network) -> wire::Network { let contract::Network { egress, ingress } = value; wire::Network { - default_policy: None, - enforcement_mode: None, - allow_local_network: None, - allowed_hosts: None, - blocked_hosts: None, - proxy: None, egress: egress.into_option().map(convert_network_egress), ingress: ingress.into_option().map(convert_network_ingress), } diff --git a/src/mxc-sdk/src/core/mxc_common/config_contract_adapters/v0_9/one_shot.rs b/src/mxc-sdk/src/core/mxc_common/config_contract_adapters/v0_9/one_shot.rs index fc75bf168..a80a0cce4 100644 --- a/src/mxc-sdk/src/core/mxc_common/config_contract_adapters/v0_9/one_shot.rs +++ b/src/mxc-sdk/src/core/mxc_common/config_contract_adapters/v0_9/one_shot.rs @@ -258,9 +258,6 @@ pub(super) fn into_common_request_ir( schema: schema.into_option(), comment: comment.into_option(), source_contract: crate::mxc_contract::ContractVersion::V0_9_0Alpha, - default_env_compatibility: crate::mxc_common::models::DefaultEnvCompatibility::DefaultBlock, - network_enforcement_compatibility: - crate::mxc_common::models::NetworkEnforcementCompatibility::Strict, phase: None, sandbox_id: None, container_id: container_id.into_option(), diff --git a/src/mxc-sdk/src/core/mxc_common/config_contract_adapters/v0_9/one_shot_tests/stable_candidate.rs b/src/mxc-sdk/src/core/mxc_common/config_contract_adapters/v0_9/one_shot_tests/stable_candidate.rs index f63a78703..be2d540eb 100644 --- a/src/mxc-sdk/src/core/mxc_common/config_contract_adapters/v0_9/one_shot_tests/stable_candidate.rs +++ b/src/mxc-sdk/src/core/mxc_common/config_contract_adapters/v0_9/one_shot_tests/stable_candidate.rs @@ -426,8 +426,6 @@ fn process_container_request_maps_expected_wire_fields() { assert_eq!(fallback.allow_dacl_mutation, Some(true)); let network = wire.network.expect("network should be populated"); - assert!(network.default_policy.is_none()); - assert!(network.proxy.is_none()); assert!(matches!( network.egress.unwrap().default, Some(super::wire::NetworkAction::Deny) @@ -525,8 +523,6 @@ fn lxc_request_maps_expected_wire_fields() { ); let network = wire.network.expect("network should be populated"); - assert!(network.default_policy.is_none()); - assert!(network.enforcement_mode.is_none()); assert!(matches!( network.egress.unwrap().default, Some(super::wire::NetworkAction::Allow) @@ -535,7 +531,6 @@ fn lxc_request_maps_expected_wire_fields() { network.ingress.unwrap().default, Some(super::wire::NetworkAction::Allow) )); - assert!(network.proxy.is_none()); let ui = wire.ui.expect("ui should be populated"); assert_eq!(ui.disable, Some(true)); @@ -594,8 +589,6 @@ fn seatbelt_request_maps_expected_wire_fields() { ); let network = wire.network.expect("network should be populated"); - assert!(network.default_policy.is_none()); - assert!(network.enforcement_mode.is_none()); assert!(matches!( network.egress.unwrap().default, Some(super::wire::NetworkAction::Allow) @@ -604,7 +597,6 @@ fn seatbelt_request_maps_expected_wire_fields() { network.ingress.unwrap().default, Some(super::wire::NetworkAction::Allow) )); - assert!(network.proxy.is_none()); let seatbelt = wire.seatbelt.expect("seatbelt should be populated"); assert_eq!( @@ -638,12 +630,8 @@ fn empty_optional_sections_map_to_present_empty_wire_sections() { assert!(fallback.allow_dacl_mutation.is_none()); let network = wire.network.expect("network should be populated"); - assert!(network.default_policy.is_none()); - assert!(network.enforcement_mode.is_none()); - assert!(network.allow_local_network.is_none()); - assert!(network.allowed_hosts.is_none()); - assert!(network.blocked_hosts.is_none()); - assert!(network.proxy.is_none()); + assert!(network.egress.is_none()); + assert!(network.ingress.is_none()); let ui = wire.ui.expect("ui should be populated"); assert!(ui.disable.is_none()); @@ -1086,11 +1074,6 @@ fn directional_network_request_maps_expected_wire_fields() { ingress.host_loopback, Some(super::wire::NetworkAction::Allow) )); - - // The legacy fields stay absent when only directional policy is supplied. - assert!(network.default_policy.is_none()); - assert!(network.enforcement_mode.is_none()); - assert!(network.proxy.is_none()); } #[test] fn every_network_action_maps_to_the_expected_wire_value() { diff --git a/src/mxc-sdk/src/core/mxc_common/config_contract_adapters/v0_9/state_aware.rs b/src/mxc-sdk/src/core/mxc_common/config_contract_adapters/v0_9/state_aware.rs index 1a394023e..b480a6447 100644 --- a/src/mxc-sdk/src/core/mxc_common/config_contract_adapters/v0_9/state_aware.rs +++ b/src/mxc-sdk/src/core/mxc_common/config_contract_adapters/v0_9/state_aware.rs @@ -39,12 +39,6 @@ fn convert_isolation_session_network(value: contract::IsolationSessionNetwork) - host_loopback: contract::IsolationSessionNetworkAllow, } = ingress; wire::Network { - allow_local_network: None, - default_policy: None, - allowed_hosts: None, - enforcement_mode: None, - blocked_hosts: None, - proxy: None, egress: Some(wire::NetworkEgress { default: Some(wire::NetworkAction::Allow), allow: None, @@ -83,9 +77,6 @@ fn state_aware_common( schema: schema.into_option(), comment: comment.into_option(), source_contract: crate::mxc_contract::ContractVersion::V0_9_0Alpha, - default_env_compatibility: crate::mxc_common::models::DefaultEnvCompatibility::DefaultBlock, - network_enforcement_compatibility: - crate::mxc_common::models::NetworkEnforcementCompatibility::Strict, phase: None, test_feature: None, windows_sandbox: None, diff --git a/src/mxc-sdk/src/core/mxc_common/config_contract_adapters/v0_9/state_aware_tests/exec.rs b/src/mxc-sdk/src/core/mxc_common/config_contract_adapters/v0_9/state_aware_tests/exec.rs index 251e02fda..126530dad 100644 --- a/src/mxc-sdk/src/core/mxc_common/config_contract_adapters/v0_9/state_aware_tests/exec.rs +++ b/src/mxc-sdk/src/core/mxc_common/config_contract_adapters/v0_9/state_aware_tests/exec.rs @@ -47,9 +47,6 @@ fn exec_preserves_process_network_telemetry_and_empty_values() { network.ingress.unwrap().default, Some(wire::NetworkAction::Deny) )); - assert!(network.allow_local_network.is_none()); - assert!(network.allowed_hosts.is_none()); - assert!(network.blocked_hosts.is_none()); assert_eq!( common.runtime_config.unwrap().network_proxy.as_deref(), Some("http://127.0.0.1:8080") diff --git a/src/mxc-sdk/src/core/mxc_common/config_contract_adapters/v1_0/common.rs b/src/mxc-sdk/src/core/mxc_common/config_contract_adapters/v1_0/common.rs index 13470a6e6..617e795fc 100644 --- a/src/mxc-sdk/src/core/mxc_common/config_contract_adapters/v1_0/common.rs +++ b/src/mxc-sdk/src/core/mxc_common/config_contract_adapters/v1_0/common.rs @@ -123,12 +123,6 @@ fn convert_network_ingress(value: contract::NetworkIngress) -> wire::NetworkIngr pub(super) fn convert_network(value: contract::Network) -> wire::Network { let contract::Network { egress, ingress } = value; wire::Network { - default_policy: None, - enforcement_mode: None, - allow_local_network: None, - allowed_hosts: None, - blocked_hosts: None, - proxy: None, egress: egress.into_option().map(convert_network_egress), ingress: ingress.into_option().map(convert_network_ingress), } diff --git a/src/mxc-sdk/src/core/mxc_common/config_contract_adapters/v1_0/one_shot.rs b/src/mxc-sdk/src/core/mxc_common/config_contract_adapters/v1_0/one_shot.rs index 592405b12..ef87fe4da 100644 --- a/src/mxc-sdk/src/core/mxc_common/config_contract_adapters/v1_0/one_shot.rs +++ b/src/mxc-sdk/src/core/mxc_common/config_contract_adapters/v1_0/one_shot.rs @@ -258,9 +258,6 @@ pub(super) fn into_common_request_ir( schema: schema.into_option(), comment: comment.into_option(), source_contract: crate::mxc_contract::ContractVersion::V1_0_0, - default_env_compatibility: crate::mxc_common::models::DefaultEnvCompatibility::DefaultBlock, - network_enforcement_compatibility: - crate::mxc_common::models::NetworkEnforcementCompatibility::Strict, phase: None, sandbox_id: None, container_id: container_id.into_option(), diff --git a/src/mxc-sdk/src/core/mxc_common/config_contract_adapters/v1_0/one_shot_tests/stable_candidate.rs b/src/mxc-sdk/src/core/mxc_common/config_contract_adapters/v1_0/one_shot_tests/stable_candidate.rs index 9bf90b82a..6222f0741 100644 --- a/src/mxc-sdk/src/core/mxc_common/config_contract_adapters/v1_0/one_shot_tests/stable_candidate.rs +++ b/src/mxc-sdk/src/core/mxc_common/config_contract_adapters/v1_0/one_shot_tests/stable_candidate.rs @@ -391,8 +391,6 @@ fn process_container_request_maps_expected_wire_fields() { assert_eq!(fallback.allow_dacl_mutation, Some(true)); let network = wire.network.expect("network should be populated"); - assert!(network.default_policy.is_none()); - assert!(network.proxy.is_none()); assert!(matches!( network.egress.unwrap().default, Some(super::wire::NetworkAction::Deny) @@ -490,8 +488,6 @@ fn lxc_request_maps_expected_wire_fields() { ); let network = wire.network.expect("network should be populated"); - assert!(network.default_policy.is_none()); - assert!(network.enforcement_mode.is_none()); assert!(matches!( network.egress.unwrap().default, Some(super::wire::NetworkAction::Allow) @@ -500,7 +496,6 @@ fn lxc_request_maps_expected_wire_fields() { network.ingress.unwrap().default, Some(super::wire::NetworkAction::Allow) )); - assert!(network.proxy.is_none()); let ui = wire.ui.expect("ui should be populated"); assert_eq!(ui.disable, Some(true)); @@ -559,8 +554,6 @@ fn seatbelt_request_maps_expected_wire_fields() { ); let network = wire.network.expect("network should be populated"); - assert!(network.default_policy.is_none()); - assert!(network.enforcement_mode.is_none()); assert!(matches!( network.egress.unwrap().default, Some(super::wire::NetworkAction::Allow) @@ -569,7 +562,6 @@ fn seatbelt_request_maps_expected_wire_fields() { network.ingress.unwrap().default, Some(super::wire::NetworkAction::Allow) )); - assert!(network.proxy.is_none()); let seatbelt = wire.seatbelt.expect("seatbelt should be populated"); assert_eq!( @@ -603,12 +595,8 @@ fn empty_optional_sections_map_to_present_empty_wire_sections() { assert!(fallback.allow_dacl_mutation.is_none()); let network = wire.network.expect("network should be populated"); - assert!(network.default_policy.is_none()); - assert!(network.enforcement_mode.is_none()); - assert!(network.allow_local_network.is_none()); - assert!(network.allowed_hosts.is_none()); - assert!(network.blocked_hosts.is_none()); - assert!(network.proxy.is_none()); + assert!(network.egress.is_none()); + assert!(network.ingress.is_none()); let ui = wire.ui.expect("ui should be populated"); assert!(ui.disable.is_none()); @@ -1015,11 +1003,6 @@ fn directional_network_request_maps_expected_wire_fields() { ingress.host_loopback, Some(super::wire::NetworkAction::Allow) )); - - // The legacy fields stay absent when only directional policy is supplied. - assert!(network.default_policy.is_none()); - assert!(network.enforcement_mode.is_none()); - assert!(network.proxy.is_none()); } #[test] fn every_network_action_maps_to_the_expected_wire_value() { diff --git a/src/mxc-sdk/src/core/mxc_common/config_contract_adapters/v1_0/state_aware.rs b/src/mxc-sdk/src/core/mxc_common/config_contract_adapters/v1_0/state_aware.rs index be62a111b..e9dc3763c 100644 --- a/src/mxc-sdk/src/core/mxc_common/config_contract_adapters/v1_0/state_aware.rs +++ b/src/mxc-sdk/src/core/mxc_common/config_contract_adapters/v1_0/state_aware.rs @@ -39,12 +39,6 @@ fn convert_isolation_session_network(value: contract::IsolationSessionNetwork) - host_loopback: contract::IsolationSessionNetworkAllow, } = ingress; wire::Network { - allow_local_network: None, - default_policy: None, - allowed_hosts: None, - enforcement_mode: None, - blocked_hosts: None, - proxy: None, egress: Some(wire::NetworkEgress { default: Some(wire::NetworkAction::Allow), allow: None, @@ -83,9 +77,6 @@ fn state_aware_common( schema: schema.into_option(), comment: comment.into_option(), source_contract: crate::mxc_contract::ContractVersion::V1_0_0, - default_env_compatibility: crate::mxc_common::models::DefaultEnvCompatibility::DefaultBlock, - network_enforcement_compatibility: - crate::mxc_common::models::NetworkEnforcementCompatibility::Strict, phase: None, test_feature: None, windows_sandbox: None, diff --git a/src/mxc-sdk/src/core/mxc_common/config_contract_adapters/v1_0/state_aware_tests/exec.rs b/src/mxc-sdk/src/core/mxc_common/config_contract_adapters/v1_0/state_aware_tests/exec.rs index fbf874b78..38c1de80e 100644 --- a/src/mxc-sdk/src/core/mxc_common/config_contract_adapters/v1_0/state_aware_tests/exec.rs +++ b/src/mxc-sdk/src/core/mxc_common/config_contract_adapters/v1_0/state_aware_tests/exec.rs @@ -47,9 +47,6 @@ fn exec_preserves_process_network_telemetry_and_empty_values() { network.ingress.unwrap().default, Some(wire::NetworkAction::Deny) )); - assert!(network.allow_local_network.is_none()); - assert!(network.allowed_hosts.is_none()); - assert!(network.blocked_hosts.is_none()); assert_eq!( common.runtime_config.unwrap().network_proxy.as_deref(), Some("http://127.0.0.1:8080") diff --git a/src/mxc-sdk/src/core/mxc_common/config_parser.rs b/src/mxc-sdk/src/core/mxc_common/config_parser.rs index f55adf662..fce3da10c 100644 --- a/src/mxc-sdk/src/core/mxc_common/config_parser.rs +++ b/src/mxc-sdk/src/core/mxc_common/config_parser.rs @@ -1293,8 +1293,6 @@ fn normalize_common_request_ir( let present_backend_sections = present_backend_sections(&cfg); let source_contract = cfg.source_contract; - let network_enforcement_compatibility = cfg.network_enforcement_compatibility; - let default_env_compatibility = cfg.default_env_compatibility; let container_id = cfg.container_id.unwrap_or_default(); // Process section: required for one-shot and state-aware exec; optional for @@ -1626,8 +1624,6 @@ fn normalize_common_request_ir( Ok(ExecutionRequest { source_contract: Some(source_contract), - network_enforcement_compatibility, - default_env_compatibility, container_id, env, inherit_default_env, @@ -1747,16 +1743,11 @@ mod tests { fn assert_exact_contract_bridge( request: ExactOneShotContract, expected_contract: ContractVersion, - expected_compatibility: crate::mxc_common::models::NetworkEnforcementCompatibility, ) { let mut logger = test_logger(); let execution = load_one_shot_request_from_contract(request, &mut logger).unwrap(); assert_eq!(execution.source_contract, Some(expected_contract)); - assert_eq!( - execution.network_enforcement_compatibility, - expected_compatibility - ); assert_eq!( execution.source_contract_version(), expected_contract.as_str() @@ -2146,10 +2137,6 @@ mod tests { match parse_exact_for_test(json).unwrap() { MxcRequest::OneShot(request) => { assert_eq!(request.source_contract, Some(ContractVersion::V1_1_0Alpha)); - assert_eq!( - request.network_enforcement_compatibility, - crate::mxc_common::models::NetworkEnforcementCompatibility::Strict - ); assert_eq!(request.script_code, "echo dev"); } MxcRequest::StateAware(_) => panic!("expected one-shot request"), @@ -2776,7 +2763,6 @@ mod tests { assert_exact_contract_bridge( ExactOneShotContract::V0_9(Box::new(v0_9)), ContractVersion::V0_9_0Alpha, - crate::mxc_common::models::NetworkEnforcementCompatibility::Strict, ); let v1_0 = serde_json::from_str::( @@ -2789,7 +2775,6 @@ mod tests { assert_exact_contract_bridge( ExactOneShotContract::V1_0(Box::new(v1_0)), ContractVersion::V1_0_0, - crate::mxc_common::models::NetworkEnforcementCompatibility::Strict, ); let dev = serde_json::from_str::( @@ -2802,7 +2787,6 @@ mod tests { assert_exact_contract_bridge( ExactOneShotContract::Dev(Box::new(dev)), ContractVersion::V1_1_0Alpha, - crate::mxc_common::models::NetworkEnforcementCompatibility::Strict, ); } diff --git a/src/mxc-sdk/src/core/mxc_common/default_env.rs b/src/mxc-sdk/src/core/mxc_common/default_env.rs index 7594abae6..fd7a576b2 100644 --- a/src/mxc-sdk/src/core/mxc_common/default_env.rs +++ b/src/mxc-sdk/src/core/mxc_common/default_env.rs @@ -94,7 +94,6 @@ pub fn env_pairs(entries: &[String]) -> Vec<(&str, &str)> { #[cfg(test)] mod tests { use super::*; - use crate::mxc_common::models::DefaultEnvCompatibility; fn defaults() -> Vec<(String, String)> { vec![ @@ -103,11 +102,8 @@ mod tests { ] } - fn request(compatibility: DefaultEnvCompatibility) -> ExecutionRequest { - ExecutionRequest { - default_env_compatibility: compatibility, - ..Default::default() - } + fn request() -> ExecutionRequest { + ExecutionRequest::default() } fn resolved(request: &ExecutionRequest) -> Vec { @@ -116,27 +112,27 @@ mod tests { #[test] fn an_omitted_env_takes_the_default_block() { - let r = request(DefaultEnvCompatibility::DefaultBlock); + let r = request(); assert_eq!(resolved(&r), ["PATH=/default/bin", "TERM=xterm-256color"]); } #[test] fn an_explicitly_empty_env_stays_empty() { - let mut r = request(DefaultEnvCompatibility::DefaultBlock); + let mut r = request(); r.env = Some(Vec::new()); assert!(resolved(&r).is_empty()); } #[test] fn a_supplied_env_is_used_verbatim() { - let mut r = request(DefaultEnvCompatibility::DefaultBlock); + let mut r = request(); r.env = Some(vec!["FOO=bar".to_string()]); assert_eq!(resolved(&r), ["FOO=bar"]); } #[test] fn an_overlay_replaces_in_place_and_appends_in_order() { - let mut r = request(DefaultEnvCompatibility::DefaultBlock); + let mut r = request(); r.env = Some(vec![ "TERM=vt100".to_string(), "FOO=bar".to_string(), @@ -152,7 +148,7 @@ mod tests { #[test] fn a_repeated_caller_key_collapses_to_the_last_value() { - let mut r = request(DefaultEnvCompatibility::DefaultBlock); + let mut r = request(); r.env = Some(vec![ "FOO=first".to_string(), "FOO=second".to_string(), @@ -169,7 +165,7 @@ mod tests { #[test] fn an_empty_caller_value_still_replaces_the_default() { - let mut r = request(DefaultEnvCompatibility::DefaultBlock); + let mut r = request(); r.env = Some(vec!["PATH=".to_string()]); r.inherit_default_env = true; @@ -178,7 +174,7 @@ mod tests { #[test] fn a_caller_entry_without_a_value_is_dropped_by_the_overlay() { - let mut r = request(DefaultEnvCompatibility::DefaultBlock); + let mut r = request(); r.env = Some(vec!["FEATURE_FLAG".to_string(), "FOO=bar".to_string()]); r.inherit_default_env = true; @@ -189,7 +185,7 @@ mod tests { #[test] fn each_state_of_process_env_resolves_to_its_own_outcome() { - let mut r = request(DefaultEnvCompatibility::DefaultBlock); + let mut r = request(); r.env = None; assert_eq!(EnvResolution::of(&r), EnvResolution::Default); diff --git a/src/mxc-sdk/src/core/mxc_common/models.rs b/src/mxc-sdk/src/core/mxc_common/models.rs index ad035a993..67f066e2f 100644 --- a/src/mxc-sdk/src/core/mxc_common/models.rs +++ b/src/mxc-sdk/src/core/mxc_common/models.rs @@ -318,15 +318,6 @@ impl NetworkPolicy { } } -impl From for NetworkPolicy { - fn from(p: crate::mxc_common::wire::NetworkPolicy) -> Self { - match p { - crate::mxc_common::wire::NetworkPolicy::Allow => Self::Allow, - crate::mxc_common::wire::NetworkPolicy::Block => Self::Block, - } - } -} - #[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "lowercase")] pub enum NetworkEnforcementMode { @@ -428,16 +419,6 @@ pub struct NetworkIngressPolicy { pub host_loopback: NetworkAction, } -impl From for NetworkEnforcementMode { - fn from(m: crate::mxc_common::wire::NetworkEnforcement) -> Self { - match m { - crate::mxc_common::wire::NetworkEnforcement::Capabilities => Self::Capabilities, - crate::mxc_common::wire::NetworkEnforcement::Firewall => Self::Firewall, - crate::mxc_common::wire::NetworkEnforcement::Both => Self::Both, - } - } -} - /// A hostname-to-IP mapping that makes a sandbox resolve the proxy to exactly /// the address the firewall authorized. /// @@ -1042,11 +1023,7 @@ pub struct ExecutionRequest { /// Direct typed SDK construction has no external contract attribution. #[serde(serialize_with = "serialize_source_contract")] pub source_contract: Option, - /// Whether backends preserve pre-v0.8 network compatibility behavior or - /// enforce the current strict posture. - pub network_enforcement_compatibility: NetworkEnforcementCompatibility, /// Whether backends supply the default `process.env` block. - pub default_env_compatibility: DefaultEnvCompatibility, /// Externally assigned container identifier. pub container_id: String, /// Environment variables as "KEY=VALUE" strings (from `process.env`). @@ -1070,18 +1047,16 @@ pub struct ExecutionRequest { /// process's variables must merge them in themselves, or set /// [`ExecutionRequest::inherit_default_env`]. /// - /// The Windows process container honors the distinction at every schema - /// version; LXC, Bubblewrap, Seatbelt, and WSLc honor it from 0.9, and - /// below 0.9 treat `None` and `Some(vec![])` alike. IsolationSession starts - /// every process from the agent user's default environment, so it rejects - /// `Some` without [`ExecutionRequest::inherit_default_env`]. + /// IsolationSession starts every process from the agent user's default + /// environment, so it rejects `Some` without + /// [`ExecutionRequest::inherit_default_env`]. pub env: Option>, /// Layer [`ExecutionRequest::env`] on top of the backend's default /// environment instead of replacing it (from `process.inheritDefaultEnv`). /// /// Only meaningful when `env` is `Some`: with `None` the child already gets - /// the default. Rejected below schema 0.9 by the config parser. + /// the default. pub inherit_default_env: bool, pub script_code: String, pub working_directory: String, @@ -1119,25 +1094,6 @@ pub struct ExecutionRequest { pub dry_run: bool, } -/// Backend network behavior after exact contract normalization. -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize)] -#[serde(rename_all = "kebab-case")] -pub enum NetworkEnforcementCompatibility { - /// Enforce the current network posture. - #[default] - Strict, -} - -/// Backend `process.env` behavior for registered contracts and typed requests. -/// Every supported request uses the default block. -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize)] -#[serde(rename_all = "kebab-case")] -pub enum DefaultEnvCompatibility { - /// Supply the default block and keep the four states of `process.env` distinct. - #[default] - DefaultBlock, -} - fn serialize_source_contract( value: &Option, serializer: S, @@ -1150,15 +1106,6 @@ where .serialize(serializer) } -impl NetworkEnforcementCompatibility { - /// Stable diagnostic spelling for policy identity and tests. - pub const fn as_str(self) -> &'static str { - match self { - Self::Strict => "strict", - } - } -} - /// Where a [`ResolvedWorkingDirectory`] came from. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum WorkingDirectorySource { @@ -1211,21 +1158,14 @@ impl ExecutionRequest { .unwrap_or_default() } - /// Whether this request's contract supplies the backend default - /// environment block, introduced by `0.9.0-alpha`. - pub fn supplies_default_env(&self) -> bool { - self.default_env_compatibility == DefaultEnvCompatibility::DefaultBlock - } - /// The caller's environment entries, with "not supplied" and "supplied but /// empty" flattened to the same empty slice. /// - /// Only for backends that have no default environment to distinguish them - /// against — every backend below schema 0.9. A backend with a default block - /// must match on [`ExecutionRequest::env`] directly, since `None` means - /// "give the child the default" and `Some(vec![])` means "give the child - /// nothing". A backend whose default MXC cannot enumerate takes the state - /// from `env` and the entries from here: WSLc, whose default is the + /// Backends with an enumerable default must match on + /// [`ExecutionRequest::env`] directly, since `None` means "give the child + /// the default" and `Some(vec![])` means "give the child nothing". A + /// backend whose default MXC cannot enumerate takes the state from `env` + /// and the entries from here: WSLc, whose default is the /// container image's `ENV`, and IsolationSession, which starts every /// process from the agent user's default environment. pub fn env_entries(&self) -> &[String] { diff --git a/src/mxc-sdk/src/core/mxc_common/network_parser.rs b/src/mxc-sdk/src/core/mxc_common/network_parser.rs index dfab3ec08..117dacb84 100644 --- a/src/mxc-sdk/src/core/mxc_common/network_parser.rs +++ b/src/mxc-sdk/src/core/mxc_common/network_parser.rs @@ -463,12 +463,6 @@ mod proxy_policy_tests { fn directional_sections(proxy: &str) -> NetworkSections { NetworkSections { network: Some(wire::Network { - default_policy: None, - enforcement_mode: None, - allow_local_network: None, - allowed_hosts: None, - blocked_hosts: None, - proxy: None, egress: Some(wire::NetworkEgress { default: Some(wire::NetworkAction::Allow), allow: None, diff --git a/src/mxc-sdk/src/core/mxc_common/network_parser_ingress_default_tests.rs b/src/mxc-sdk/src/core/mxc_common/network_parser_ingress_default_tests.rs index 12bf6dcf4..daddcccce 100644 --- a/src/mxc-sdk/src/core/mxc_common/network_parser_ingress_default_tests.rs +++ b/src/mxc-sdk/src/core/mxc_common/network_parser_ingress_default_tests.rs @@ -3,9 +3,8 @@ //! Tests the resolved value of an omitted `network.ingress.hostLoopback`. //! -//! Contract source: `docs/containment-configuration/0.8.0/networking/networking.md` -//! ("Host Loopback and Inbound Policy") and `docs/containment-configuration/0.8.0/policy.md`: -//! both ingress controls default to `deny`, and `hostLoopback` resolves +//! Contract source: `docs/schema.md` ("Directional networking"): both +//! ingress controls default to `deny`, and `hostLoopback` resolves //! independently of `ingress.default` rather than inheriting it. //! //! The inherit reading and the deny reading agree everywhere except under @@ -17,12 +16,6 @@ use super::*; fn sections(ingress: Option, network_present: bool) -> NetworkSections { NetworkSections { network: network_present.then_some(wire::Network { - default_policy: None, - enforcement_mode: None, - allow_local_network: None, - allowed_hosts: None, - blocked_hosts: None, - proxy: None, egress: Some(wire::NetworkEgress { default: Some(wire::NetworkAction::Allow), allow: None, diff --git a/src/mxc-sdk/src/core/mxc_common/policy_identity.rs b/src/mxc-sdk/src/core/mxc_common/policy_identity.rs index e11c8678a..819b77a37 100644 --- a/src/mxc-sdk/src/core/mxc_common/policy_identity.rs +++ b/src/mxc-sdk/src/core/mxc_common/policy_identity.rs @@ -42,12 +42,10 @@ //! | `capture_denials.output_path` | Only decides where the diagnostic JSON deliverable is written; not enforcement. `capture_denials.mode` remains hashed. | //! | `dry_run`, `testing_features_enabled` | Invocation modes, not policy. | //! | `experimental_enabled` | Authorizes selecting an experimental backend; it does not change the selected backend's enforcement. | -//! | `source_contract` | External JSON provenance used only for diagnostics and telemetry. Normalized network compatibility is hashed separately. | -//! | `default_env_compatibility` | Decides whether a default environment block is supplied, which is process launch behavior rather than enforcement. | +//! | `source_contract` | External JSON provenance used only for diagnostics and telemetry. | //! -//! `network_enforcement_compatibility` is hashed because it changes how the -//! normalized network policy is enforced. WSLC and Windows Sandbox -//! configuration is hashed at the `wslc` and `windowsSandbox` root keys. +//! WSLC and Windows Sandbox configuration is hashed at the `wslc` and +//! `windowsSandbox` root keys. //! //! `ContainerPolicy::network_proxy` is `#[serde(skip)]`, so the proxy's //! credential-bearing URL cannot reach the hash through the blanket policy @@ -191,10 +189,8 @@ fn hash_canonical_json(canonical: &str) -> String { fn policy_projection(request: &ExecutionRequest) -> Value { let ExecutionRequest { // External contract provenance is diagnostics/telemetry attribution, - // not enforcement. The normalized compatibility value below is. + // not enforcement. source_contract: _excluded_source_contract, - network_enforcement_compatibility, - default_env_compatibility: _excluded_default_env_compatibility, container_id, working_directory, script_timeout, @@ -228,10 +224,6 @@ fn policy_projection(request: &ExecutionRequest) -> Value { let mut root = Map::new(); - root.insert( - "networkEnforcementCompatibility".into(), - Value::String(network_enforcement_compatibility.as_str().to_string()), - ); root.insert( "containment".into(), Value::String(containment.wire_name().to_string()), @@ -452,8 +444,6 @@ mod tests { fn request() -> ExecutionRequest { let mut r = ExecutionRequest { source_contract: Some(crate::mxc_contract::ContractVersion::V0_9_0Alpha), - network_enforcement_compatibility: - crate::mxc_common::models::NetworkEnforcementCompatibility::Strict, container_id: "test".to_string(), script_code: "echo hello".to_string(), working_directory: "C:\\work".to_string(), @@ -531,7 +521,6 @@ mod tests { "containment", "lifecycle", "lxc", - "networkEnforcementCompatibility", "policy", "proxy", "scriptTimeout", diff --git a/src/mxc-sdk/src/core/mxc_common/sandbox_process.rs b/src/mxc-sdk/src/core/mxc_common/sandbox_process.rs index bf78a07c1..bb0d530b5 100644 --- a/src/mxc-sdk/src/core/mxc_common/sandbox_process.rs +++ b/src/mxc-sdk/src/core/mxc_common/sandbox_process.rs @@ -568,7 +568,7 @@ pub trait SandboxBackend { /// Return a named feature constant or compose constants with `|`. Use /// [`NetworkPolicySupport::ALL`] only when every feature is enforced. fn network_policy_support(&self) -> NetworkPolicySupport { - NetworkPolicySupport::LEGACY + NetworkPolicySupport::default() } /// Validates shared network support and backend-specific constraints. diff --git a/src/mxc-sdk/src/core/mxc_common/script_runner.rs b/src/mxc-sdk/src/core/mxc_common/script_runner.rs index 11df2e0cb..7e94f4365 100644 --- a/src/mxc-sdk/src/core/mxc_common/script_runner.rs +++ b/src/mxc-sdk/src/core/mxc_common/script_runner.rs @@ -19,7 +19,7 @@ use crate::mxc_common::validator::{ pub trait ScriptRunner { /// Validate shared network support and runner-specific constraints. fn validate_runner(&self, request: &ExecutionRequest) -> Result<(), ScriptResponse> { - validate_network_policy_support(request, NetworkPolicySupport::LEGACY)?; + validate_network_policy_support(request, NetworkPolicySupport::default())?; Ok(()) } diff --git a/src/mxc-sdk/src/core/mxc_common/sdk_input.rs b/src/mxc-sdk/src/core/mxc_common/sdk_input.rs index 2626e6ba9..3f460c45c 100644 --- a/src/mxc-sdk/src/core/mxc_common/sdk_input.rs +++ b/src/mxc-sdk/src/core/mxc_common/sdk_input.rs @@ -10,7 +10,6 @@ use crate::mxc_contract::ContractVersion; use crate::mxc_common::common_request_ir::CommonRequestIR; use crate::mxc_common::error::WxcError; -use crate::mxc_common::models::NetworkEnforcementCompatibility; use crate::mxc_common::state_aware_input::StateAwareInput; use crate::mxc_common::state_aware_operation::StateAwareOperation; use crate::mxc_common::wire; @@ -148,9 +147,6 @@ impl SdkStateAwareInput { schema: None, comment: None, source_contract: self.version, - network_enforcement_compatibility: NetworkEnforcementCompatibility::Strict, - default_env_compatibility: - crate::mxc_common::models::DefaultEnvCompatibility::DefaultBlock, phase: None, sandbox_id: None, container_id: None, @@ -191,12 +187,6 @@ impl SdkStateAwareInput { fn map_network(network: SdkNetworkInput) -> wire::Network { wire::Network { - default_policy: None, - enforcement_mode: None, - allow_local_network: None, - allowed_hosts: None, - blocked_hosts: None, - proxy: None, egress: network.egress.map(|egress| wire::NetworkEgress { default: egress.default.map(map_action), allow: egress.allow.map(map_rules), diff --git a/src/mxc-sdk/src/core/mxc_common/validator.rs b/src/mxc-sdk/src/core/mxc_common/validator.rs index 5ac9345a7..07ff2b219 100644 --- a/src/mxc-sdk/src/core/mxc_common/validator.rs +++ b/src/mxc-sdk/src/core/mxc_common/validator.rs @@ -13,9 +13,6 @@ use crate::mxc_common::mxc_error::MxcError; pub struct NetworkPolicySupport(u8); impl NetworkPolicySupport { - /// Support for the legacy network policy without additive 0.8 features. - pub const LEGACY: Self = Self(0); - /// Support for the outbound network default policy. pub const EGRESS_DEFAULT: Self = Self(1 << 4); @@ -404,7 +401,7 @@ mod tests { ..Default::default() }; - for support in [NetworkPolicySupport::LEGACY, NetworkPolicySupport::ALL] { + for support in [NetworkPolicySupport::default(), NetworkPolicySupport::ALL] { assert!( validate_network_policy_support(&request, support).is_ok(), "valid legacy host-list policy was rejected for support {support:?}" @@ -447,7 +444,7 @@ mod tests { ..Default::default() }; - for support in [NetworkPolicySupport::LEGACY, NetworkPolicySupport::ALL] { + for support in [NetworkPolicySupport::default(), NetworkPolicySupport::ALL] { let error = validate_network_policy_support(&request, support).unwrap_err(); assert_eq!(error.error_message, expected); } @@ -463,7 +460,7 @@ mod tests { request.policy.allowed_hosts = vec!["203.0.113.7".to_string()]; let error = - validate_network_policy_support(&request, NetworkPolicySupport::LEGACY).unwrap_err(); + validate_network_policy_support(&request, NetworkPolicySupport::default()).unwrap_err(); assert_eq!( error.error_message, "network.egress.default is not supported by the selected backend" @@ -478,7 +475,7 @@ mod tests { ..Default::default() }); let error = - validate_network_policy_support(&request, NetworkPolicySupport::LEGACY).unwrap_err(); + validate_network_policy_support(&request, NetworkPolicySupport::default()).unwrap_err(); assert!(error.error_message.contains("network.egress.default")); let mut request = ExecutionRequest::default(); @@ -590,7 +587,7 @@ mod tests { request.policy.network_egress = Some(NetworkEgressPolicy::default()); request.policy.default_network_policy = NetworkPolicy::Allow; let error = - validate_network_policy_support(&request, NetworkPolicySupport::LEGACY).unwrap_err(); + validate_network_policy_support(&request, NetworkPolicySupport::default()).unwrap_err(); assert!(error.error_message.contains("legacy outbound policy")); let mut request = ExecutionRequest::default(); @@ -612,12 +609,12 @@ mod tests { } #[test] - fn network_support_accepts_implicit_directional_defaults_for_legacy_backends() { + fn network_support_accepts_implicit_directional_defaults_without_declared_features() { let mut request = ExecutionRequest::default(); request.policy.network_egress = Some(NetworkEgressPolicy::default()); request.policy.network_ingress = Some(NetworkIngressPolicy::default()); - assert!(validate_network_policy_support(&request, NetworkPolicySupport::LEGACY).is_ok()); + assert!(validate_network_policy_support(&request, NetworkPolicySupport::default()).is_ok()); } #[test] @@ -641,7 +638,7 @@ mod tests { }); let error = - validate_state_aware_network_policy_support(&request, NetworkPolicySupport::LEGACY) + validate_state_aware_network_policy_support(&request, NetworkPolicySupport::default()) .unwrap_err(); assert_eq!(error.code, MxcErrorCode::PolicyValidation); diff --git a/src/mxc-sdk/src/core/mxc_common/wire.rs b/src/mxc-sdk/src/core/mxc_common/wire.rs index 8491ecc56..1aa80be8d 100644 --- a/src/mxc-sdk/src/core/mxc_common/wire.rs +++ b/src/mxc-sdk/src/core/mxc_common/wire.rs @@ -134,9 +134,8 @@ pub struct ProcessContainer { /// process's access attempts to a learning-mode ETL trace for later /// inspection. MXC prefers native PSEC plus V2 Learning Mode when that API /// set can fully honor the request. Otherwise it retains the highest - /// compatible legacy containment tier and uses guarded WPR capture, so - /// `leastPrivilege`, `network.proxy`, and deny-path policies can remain - /// enforced without weakening the request. + /// compatible containment tier and uses guarded WPR capture, so + /// `leastPrivilege` and deny-path policies remain enforced. pub capture_denials: Option, /// BaseProcessContainer UI settings (Windows). pub ui: Option, @@ -283,18 +282,6 @@ pub struct Fallback { #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct Network { - /// Default outbound policy when no host rule matches. - pub default_policy: Option, - /// How the policy is enforced. - pub enforcement_mode: Option, - /// Allow binding/listening on local IPs and accepting inbound connections. - pub allow_local_network: Option, - /// Hosts explicitly allowed. - pub allowed_hosts: Option>, - /// Hosts explicitly blocked. - pub blocked_hosts: Option>, - /// Proxy configuration (one of localhost / builtinTestServer / url). - pub proxy: Option, /// Outbound network policy. pub egress: Option, /// Inbound and host-loopback network policy. @@ -381,26 +368,6 @@ pub struct RuntimeConfig { pub network_proxy: Option, } -/// Default network policy. -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "lowercase")] -pub enum NetworkPolicy { - Allow, - Block, -} - -/// Network enforcement mechanism. -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "lowercase")] -pub enum NetworkEnforcement { - /// Per-process capability-based filtering. - Capabilities, - /// Host firewall rules. - Firewall, - /// Both capability and firewall enforcement. - Both, -} - /// Proxy configuration. Exactly one variant applies. #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] diff --git a/src/mxc-sdk/src/core/mxc_engine/run.rs b/src/mxc-sdk/src/core/mxc_engine/run.rs index c2fd51462..f10e51128 100644 --- a/src/mxc-sdk/src/core/mxc_engine/run.rs +++ b/src/mxc-sdk/src/core/mxc_engine/run.rs @@ -143,7 +143,7 @@ fn policy_hash_identity(container_id: &str) -> String { mod attribution_tests { use super::config_schema_version; use crate::mxc_common::logger::{Logger, Mode}; - use crate::mxc_common::models::{ExecutionRequest, NetworkEnforcementCompatibility}; + use crate::mxc_common::models::ExecutionRequest; use crate::mxc_common::state_aware_request::MxcRequest; #[test] @@ -161,10 +161,6 @@ mod attribution_tests { panic!("expected one-shot request"); }; assert_eq!(config_schema_version(&exact), "0.9.0-alpha"); - assert_eq!( - exact.network_enforcement_compatibility, - NetworkEnforcementCompatibility::Strict - ); let direct = ExecutionRequest { script_code: "echo direct".to_string(), @@ -172,10 +168,6 @@ mod attribution_tests { }; assert_eq!(config_schema_version(&direct), ""); assert_eq!(direct.source_contract, None); - assert_eq!( - direct.network_enforcement_compatibility, - NetworkEnforcementCompatibility::Strict - ); } } diff --git a/src/mxc-sdk/src/policy.rs b/src/mxc-sdk/src/policy.rs index a9655edc3..b0c9ca397 100644 --- a/src/mxc-sdk/src/policy.rs +++ b/src/mxc-sdk/src/policy.rs @@ -1065,10 +1065,6 @@ mod tests { execution.source_contract, Some(crate::mxc_contract::ContractVersion::V0_9_0Alpha) ); - assert_eq!( - execution.network_enforcement_compatibility, - crate::mxc_common::models::NetworkEnforcementCompatibility::Strict - ); assert_eq!(execution.script_code, "echo hello"); } diff --git a/tests/scripts/lib/WinProcessContainer.Common.ps1 b/tests/scripts/lib/WinProcessContainer.Common.ps1 index 151a426b7..c461082d0 100644 --- a/tests/scripts/lib/WinProcessContainer.Common.ps1 +++ b/tests/scripts/lib/WinProcessContainer.Common.ps1 @@ -1069,7 +1069,7 @@ function Get-LoopbackFetchCommand { # Phase 8 — directional network policy. # # Asserts the documented contract (docs/backends/process-container/networking.md and -# docs/containment-configuration/0.8.0/networking/networking.md), not the current code, so +# docs/schema.md), not the current code, so # an assertion that outruns the backend fails by design. Every positive is # paired with a negative control on an otherwise identical config: from one run # on a host with no connectivity, "reached it" and "blocked by policy" look the diff --git a/tests/scripts/run_lxc_network_no_network_test.sh b/tests/scripts/run_lxc_network_no_network_test.sh index 1a1dd6f09..586523bb9 100644 --- a/tests/scripts/run_lxc_network_no_network_test.sh +++ b/tests/scripts/run_lxc_network_no_network_test.sh @@ -2,9 +2,8 @@ # LXC omitted-network-section test # # Proves that a request with the `network` section omitted entirely receives -# the directional deny defaults stated in the contract: the workload cannot reach -# the network. The contract is docs/containment-configuration/0.8.0/policy.md: "A schema -# 0.8 policy with no network fields selects directional deny defaults." +# the directional deny defaults in docs/schema.md: the workload cannot reach +# the network. # # The first run is the positive control. It sends the same workload and probe # with an explicit egress allow rule. If that run cannot reach the destination, @@ -112,7 +111,7 @@ run_config "positive control: request with explicit egress allow to $PROBE_ADDRE assert_allowed "an explicitly allowed destination was unreachable on the positive control. The second run's blocked result would prove nothing, so this test fails rather than proceeding." run_config "case under test: request with network section omitted entirely" "$OMIT_CONFIG" -assert_blocked "the workload reached $PROBE_ADDRESS under a request with no network section. The contract (docs/containment-configuration/0.8.0/policy.md) states that omitted permissions remain denied and a 0.8 policy with no network fields selects directional deny defaults." +assert_blocked "the workload reached $PROBE_ADDRESS under a request with no network section. The supported contract (docs/schema.md) states that omitted network policy selects directional deny defaults." echo "PASS: a request with the network section omitted cannot reach the network." echo "LXC omitted-network-section test complete."