Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/Build.Linux.Job.yml
Original file line number Diff line number Diff line change
Expand Up @@ -196,9 +196,9 @@ jobs:

# Exercises the real binary against the real dependencies: launches the
# slirp4netns supervisor, joins its user namespace, and asserts the
# sandbox lands in a private network namespace. Needs unix-test-proxy
# (builtinTestServer) alongside lxc-exec, which the two build steps
# above place in the same target directory.
# sandbox lands in a private network namespace. The test script starts
# unix-test-proxy independently alongside lxc-exec; the two build steps
# above place them in the same target directory.
- name: Test Bubblewrap proxy networking (end-to-end)
working-directory: ${{ github.workspace }}
env:
Expand Down
8 changes: 3 additions & 5 deletions build-mac.sh
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,8 @@ echo ""
echo "=== Building mxc-exec-mac ($BUILD_TYPE) ==="
cd "$SRC_DIR"

# mxc-exec-mac is the seatbelt executor. unix-test-proxy is the bundled,
# testing-only HTTP proxy that backs `network.proxy.builtinTestServer`; it is
# spawned as a sibling of mxc-exec-mac by the proxy coordinator.
# mxc-exec-mac is the Seatbelt executor. Keep unix-test-proxy as an
# externally managed test endpoint for integration probes.
CARGO_FLAGS=("-p" "mxc_darwin" "-p" "unix_test_proxy" "-p" "mxc_ffi")
if [ "$BUILD_TYPE" = "release" ]; then
CARGO_FLAGS+=("--release")
Expand Down Expand Up @@ -141,8 +140,7 @@ copy_binary_for_target() {
echo "Warning: $ffi_src not found, skipping copy"
fi

# unix-test-proxy backs network.proxy.builtinTestServer (testing only).
# It must sit next to mxc-exec-mac so the proxy coordinator can resolve it.
# Retain the test proxy binary for callers that start it independently.
local proxy_src="$SRC_DIR/target/$triple/$BUILD_TYPE/unix-test-proxy"
if [ -f "$proxy_src" ]; then
cp "$proxy_src" "$bin_dir/unix-test-proxy"
Expand Down
16 changes: 8 additions & 8 deletions docs/bwrap-support/bubblewrap-backend.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ requiring root privileges or a container runtime.
> Versions before `0.9.0-alpha` are rejected; changing only the version of an
> old config does not migrate its policy. Legacy `defaultPolicy`,
> `enforcementMode`, host lists, `allowLocalNetwork`, and `network.proxy`
> are not accepted. See [schema migration](../schema.md).
> Directly constructed runtime requests with non-default retired fields are
> also rejected before sandbox provisioning. Bubblewrap also rejects
> directional requests it cannot enforce, such as `ingress.default: "allow"`
> or direct egress rules combined with a runtime proxy. An omitted `network`
> section takes the directional deny defaults.
> are rejected by supported exact contracts. Typed Rust requests no longer
> contain these fields. See [schema migration](../schema.md).
> Bubblewrap still rejects enforceable-looking requests it cannot honor
> (such as `ingress.default: "allow"` or direct egress rules combined with a
> runtime proxy) before provisioning. An omitted `network` section takes the
> directional deny defaults.

## Prerequisites

Expand Down Expand Up @@ -382,8 +382,8 @@ Be honest about what this buys. It is **not** new protection: nothing outside
the sandbox can reach in already, because the runner configures no port
forwarding into the namespace, so there is no path for an inbound packet to
arrive on. The chain is defense in depth against a future change that adds
one, and the mechanism the GA networking spec expects a backend to apply
`ingress.default` through. The terminal `DROP` is deliberately independent of
one, and a defense-in-depth implementation of `ingress.default`. The terminal
`DROP` is deliberately independent of
`egress.default`, which governs outbound traffic only β€” an open outbound posture
must not open inbound as a side effect.

Expand Down
89 changes: 47 additions & 42 deletions docs/linux-wsl-roadmap-june-2026.md

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion docs/process-container/networking.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,9 @@ Do not infer otherwise from the schema:
- Per-source or per-port inbound rules. GA ingress is limited to the
`default` and `hostLoopback` allow/deny toggles.

See the parent doc on the last 4.
The egress schema selects numeric destinations, protocols, and ports, not
durable DNS names or application payloads. Its ingress schema has only
`default` and `hostLoopback` toggles; it cannot select inbound peers or ports.

## 2. Supported-contract selection and downlevel behavior

Expand Down
38 changes: 11 additions & 27 deletions docs/schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,20 @@ and put the loopback proxy endpoint in runtime configuration:
```

Direct egress rules and `runtimeConfig.networkProxy` select different
connectivity models and cannot be combined. Both ingress controls deny when
omitted, and `hostLoopback` resolves independently of `ingress.default` rather
than inheriting it, so host-loopback access must be requested explicitly.
connectivity models and cannot be combined. Direct mode applies numeric CIDR,
protocol, and port rules where the backend supports them. A runtime proxy
names a caller-managed HTTP/S endpoint; the proxy owns any destination
filtering. Whether MXC can restrict raw-socket traffic to that endpoint
depends on the backend; see its guide. Both ingress controls deny when
omitted, and `hostLoopback` resolves independently of
`ingress.default` rather than inheriting it, so host-loopback access must be
requested explicitly.
A ProcessContainer proxy requires `ingress.default: "allow"`. Identity-scoped
proxies set a non-blank `allowedProxyPeer` and keep `hostLoopback: "deny"`;
identity-less host proxies omit `allowedProxyPeer` and require
`hostLoopback: "allow"`. The identity-less route is a weaker development/testing
compatibility deployment because it opens both host-loopback directions; it is
not the strict proxy-endpoint exception defined by the shared model-2 policy.
deployment: it opens both host-loopback directions without restricting access
to a named proxy peer. It does not enforce a proxy-only host-loopback exception.

```json
{
Expand Down Expand Up @@ -84,26 +89,6 @@ No supported exact contract accepts them. Migrate existing policies to
directional fields and `runtimeConfig.networkProxy` rather than changing
the version string alone.

#### Historical legacy network host-list semantics (retired)

In the retired contracts, host lists refined `defaultPolicy`; they did not
replace it. This table describes historical behavior, not supported authoring:

| `defaultPolicy` | `allowedHosts` | `blockedHosts` | Result |
| --- | --- | --- | --- |
| `block` | empty | empty | Valid: no egress |
| `block` | non-empty | empty | Valid: allow only listed destinations |
| `block` | empty | non-empty | Invalid: a blocklist cannot refine a block default without an allowlist |
| `block` | non-empty | non-empty | Valid shared policy: explicit blocks override allowed destinations; backends may reject if they cannot represent both lists |
| `allow` | empty | empty | Valid: unrestricted egress |
| `allow` | empty | non-empty | Valid: allow all except listed destinations |
| `allow` | non-empty | empty | Invalid: an allowlist cannot refine an allow default |
| `allow` | non-empty | non-empty | Invalid: `allowedHosts` cannot be used with an allow default |

For the valid block-default combination containing both lists, explicit blocks
take precedence over allowed destinations. A backend that cannot represent both
lists must reject the combination rather than dropping either list.

### IsolationSession unrestricted networking (0.9)

IsolationSession cannot restrict networking. Exact v0.9 requests must describe
Expand All @@ -127,8 +112,7 @@ that actual posture through the standard directional network fields:
All three directional values must be explicitly `allow`; omission defaults to
deny. Legacy network fields, rules, mixed postures, and proxies are rejected.
An absent or empty `network` object is rejected. Exact v0.9 IsolationSession
does not require an experimental execution opt-in. Earlier published contracts
remain immutable history but are no longer accepted.
does not require an experimental execution opt-in.
Every complete request that carries a process requires a non-empty
`process.commandLine`. The Windows native CLI may accept a template without
that field when the command is supplied after `--`; `wxc-exec.exe` inserts or
Expand Down
22 changes: 15 additions & 7 deletions docs/telemetry/telemetry.md
Original file line number Diff line number Diff line change
Expand Up @@ -540,7 +540,7 @@ rejection records from the same invocation. A successful launch emits no
| `mxc.PolicyHash` | Every launch, after the effective request is resolved | `backend`, `policy_hash`, `config_schema_version` |
| `mxc.SandboxIdentity` | After a successful state-aware phase | `backend`, `identity`, `phase` |
| `mxc.EnforcementDegraded` | ProcessContainer dispatch resolved below the preferred tier | `backend`, `identity`, `tier`, `needs_dacl_augmentation`, `effective_enforcement_level`, `degradation_reasons`, `degradation_reason_count` |
| `mxc.NetworkPolicyApplied` | After network policy setup, on success **and** failure | `backend`, `identity`, `tier` (no `pid` yet), plus `enforcement_mode`, `default_policy`, `proxy_port`, `firewall_rules_created`, `firewall_applied`, `status` |
| `mxc.NetworkPolicyApplied` | AppContainer: after network setup, before process launch. BaseContainer: success after launch. Both tiers: failure when network setup fails | `backend`, `identity`, `tier` (no `pid` field), plus `enforcement_mode`, `default_policy`, `proxy_port`, `firewall_rules_created`, `firewall_applied`, `status` |
| `mxc.ProcessExited` | Sandboxed process exited on its own | `exit_code` |
| `mxc.ProcessTimedOut` | `scriptTimeout` breached | `timeout_ms` |
| `mxc.ProcessKillFailed` | A kill/terminate call failed (**failure only**) | `kill_method`, `error_code` |
Expand All @@ -550,7 +550,14 @@ rejection records from the same invocation. A successful launch emits no
For the ProcessContainer AppContainer fallback, the firewall fields remain in
these records for compatibility: no local firewall rules are created or
removed, `firewall_applied` is `false`, and `firewall_removal_ok` is `true`.
Proxy setup failures still set `mxc.NetworkPolicyApplied.status` to failure.
AppContainer proxy-shim startup failures set `mxc.NetworkPolicyApplied.status`
to `failure` and report `proxy_port: 0` after the coordinator cleans up.
AppContainer reports network success before spawning the process; a later
launch failure can therefore follow a successful network record.
BaseContainer reports `failure` if native PSEC setup (including its proxy
policy) fails, with the requested proxy port; it emits `success` only after
launch succeeds. Unrelated pre-setup and process-launch failures do not emit a
BaseContainer network record.

### Error semantics: `FallbackError` vs `ActivityError`

Expand Down Expand Up @@ -641,13 +648,14 @@ Excluded, and why:
| `source_contract` | External exact-contract provenance used for diagnostics and telemetry attribution, not enforcement. |
| `telemetry`, internal `test` feature | No enforcement effect. |
| proxy `original_url` | Can embed `user:password@`. The host and port *are* hashed. |
| `dry_run`, `testing_features_enabled` | Invocation modes, not policy. |
| `dry_run` | Invocation mode, not policy. |
| `experimental_enabled` | Authorizes selecting an experimental backend, not enforcement; changing it leaves policy identity unchanged. |

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.
The retired network compatibility marker, legacy network model fields, and
built-in test proxy discriminator are no longer part of the projection. Hashes
from builds that included them can differ even when the supported effective
policy is unchanged; compare policy hashes across builds only with those
projection changes in mind.

Enforcement-relevant backend configuration is hashed from
`ExecutionRequest.windows_sandbox` and `ExecutionRequest.wslc`. The canonical
Expand Down
6 changes: 3 additions & 3 deletions sdk/dotnet/Microsoft.Mxc.Sdk/V1/ContainerRequestSections.cs
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ public sealed class NetworkRulePolicy
public List<NetworkPortPolicy>? Ports { get; set; }
}

/// <summary>Schema-0.8 outbound network policy.</summary>
/// <summary>Directional outbound network policy.</summary>
public sealed class NetworkEgressPolicy
{
/// <summary>Action for traffic not matched by a rule.</summary>
Expand All @@ -194,7 +194,7 @@ public sealed class NetworkEgressPolicy
public List<NetworkRulePolicy>? Deny { get; set; }
}

/// <summary>Schema-0.8 inbound and host-loopback network policy.</summary>
/// <summary>Directional inbound and host-loopback network policy.</summary>
public sealed class NetworkIngressPolicy
{
/// <summary>Default inbound action.</summary>
Expand All @@ -206,7 +206,7 @@ public sealed class NetworkIngressPolicy
public NetworkAction? HostLoopback { get; set; }
}

/// <summary>Schema-0.8 runtime network values.</summary>
/// <summary>Runtime network values.</summary>
public sealed class NetworkRuntimeConfig
{
/// <summary>
Expand Down
52 changes: 2 additions & 50 deletions src/mxc-sdk/src/backends/bubblewrap/common/bwrap_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ use std::collections::HashSet;

use crate::mxc_common::filesystem_resolve::FsIntent;
use crate::mxc_common::models::{ExecutionRequest, NetworkAction, ProxyAddress};
#[cfg(any(test, target_os = "linux"))]
use crate::mxc_common::models::{NetworkEnforcementMode, NetworkPolicy};
use crate::mxc_common::proxy_env::{is_managed_proxy_key, PROXY_SET_KEYS};

/// The fixed prefix of the command bwrap is asked to run.
Expand Down Expand Up @@ -135,32 +133,6 @@ impl ResolvedNetworkMode {
}
}

/// Interim protection for directly constructed requests while the shared
/// runtime model still exposes fields retired from every supported contract.
#[cfg(any(test, target_os = "linux"))]
pub(crate) fn retired_network_fields_rejection(request: &ExecutionRequest) -> Option<&'static str> {
let policy = &request.policy;
(policy.default_network_policy != NetworkPolicy::Block
|| policy.network_enforcement_mode != NetworkEnforcementMode::Capabilities
|| policy.allow_local_network
|| !policy.allowed_hosts.is_empty()
|| !policy.blocked_hosts.is_empty())
.then_some(
"Bubblewrap: retired network.defaultPolicy, enforcementMode, allowLocalNetwork, \
allowedHosts and blockedHosts are not supported; use network.egress and \
network.ingress.",
)
}

/// The built-in test proxy has no supported exact contract spelling.
#[cfg(any(test, target_os = "linux"))]
pub(crate) fn builtin_proxy_rejection(request: &ExecutionRequest) -> Option<&'static str> {
request.policy.network_proxy.builtin_test_server.then_some(
"Bubblewrap: network.proxy.builtinTestServer is retired; configure an externally \
managed runtimeConfig.networkProxy.",
)
}

/// Refuse an inbound posture Bubblewrap cannot honor.
///
/// The backend declares `INGRESS_DEFAULT` and `HOST_LOOPBACK` in
Expand Down Expand Up @@ -205,7 +177,7 @@ pub const BWRAP_HOST_LOOPBACK_ALLOW: &str =
network.ingress.hostLoopback='deny'.";

/// Rejection text for a proxy combined with direct egress.
#[cfg(any(test, target_os = "linux"))]
#[cfg(any(target_os = "linux", test))]
pub(crate) const BWRAP_PROXY_DIRECTIONAL_EGRESS: &str =
"Bubblewrap: runtimeConfig.networkProxy cannot be combined with direct network.egress rules \
or default='allow'. \
Expand All @@ -215,7 +187,7 @@ pub(crate) const BWRAP_PROXY_DIRECTIONAL_EGRESS: &str =
remove runtimeConfig.networkProxy and express the policy with network.egress.";

/// Reject direct egress that proxy-only routing would otherwise discard.
#[cfg(any(test, target_os = "linux"))]
#[cfg(any(target_os = "linux", test))]
pub(crate) fn proxy_with_egress_rejection(request: &ExecutionRequest) -> Option<&'static str> {
let proxy = &request.policy.network_proxy;
if !proxy.is_enabled() {
Expand Down Expand Up @@ -811,24 +783,6 @@ mod tests {
assert_eq!(directional_network_rejection(&request), None);
}

#[test]
fn directly_constructed_retired_network_fields_are_refused() {
let mut request = base_request();
let mut cases: [fn(&mut ExecutionRequest); 5] = [
|r| r.policy.default_network_policy = NetworkPolicy::Allow,
|r| r.policy.network_enforcement_mode = NetworkEnforcementMode::Firewall,
|r| r.policy.allow_local_network = true,
|r| r.policy.allowed_hosts.push("192.0.2.1".into()),
|r| r.policy.blocked_hosts.push("192.0.2.2".into()),
];
for set_field in &mut cases {
set_field(&mut request);
assert!(retired_network_fields_rejection(&request).is_some());
request = base_request();
}
assert!(retired_network_fields_rejection(&request).is_none());
}

#[test]
fn only_external_runtime_proxies_are_accepted() {
let mut request = directional_egress_request(NetworkAction::Deny, false);
Expand All @@ -842,8 +796,6 @@ mod tests {
proxy_with_egress_rejection(&request),
Some(BWRAP_PROXY_DIRECTIONAL_EGRESS)
);
request.policy.network_proxy.builtin_test_server = true;
assert!(builtin_proxy_rejection(&request).is_some());
}

#[test]
Expand Down
Loading
Loading