Skip to content
Merged
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
4 changes: 2 additions & 2 deletions docs/development/build-and-test/schema-codegen.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,8 @@ contract-valid, and no relaxed schema twin is generated.
history and published registry identities.
- `validate-configs.js` validates the repository config corpus against exact
registered schemas discovered from the same registry command.
- `wire-conformance.test.ts` checks the Node one-shot public-to-raw v1.1
mapping against the exact generated v1.1 oracle.
- `wire-conformance.test.ts` checks the Node one-shot public-to-raw v1.0
mapping against the exact generated published v1.0 oracle.
- `wire-conformance-state-aware.test.ts` checks backend-specific state-aware
public types against the exact v0.9 and v1.1 oracles.
- `check-schema-versions.js` and `check-version-sync.js` enforce schema and
Expand Down
4 changes: 2 additions & 2 deletions sdk/node/src/bindings/one-shot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { MxcError } from '../v1/errors.js';
import { SDK_CONTRACT_VERSION } from '../v1/contract-version.js';
import type {
ContainerConfig,
NetworkConfig,
DirectionalNetworkConfig,
PortMapping,
ProcessContainerConfig,
WslcConfig,
Expand Down Expand Up @@ -114,7 +114,7 @@ function assertIntegerRange(
}
}

function validateNetworkPorts(network: NetworkConfig | undefined): void {
function validateNetworkPorts(network: DirectionalNetworkConfig | undefined): void {
const egress = network?.egress;
const rules = [...(egress?.allow ?? []), ...(egress?.deny ?? [])];
for (const [ruleIndex, rule] of rules.entries()) {
Expand Down
61 changes: 0 additions & 61 deletions sdk/node/src/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,67 +102,6 @@ export function makeLogFilePath(dir: string): string {
return path.join(dir, `mxc-diag-${ts}-${suffix}.log`);
}

/**
* Apply Linux network-policy defaults to a `ContainerConfig`.
*
* Linux enforces per-host filtering in one of two ways:
* 1. **iptables firewall** (`enforcementMode: 'firewall'`) — LXC's
* privileged enforcement path. Requires root / CAP_NET_ADMIN.
* 2. **Cooperative HTTP proxy** (`network.proxy` set) — Bubblewrap's
* unprivileged enforcement path. The proxy applies the host policy
* for cooperating HTTP clients; raw-socket clients bypass it.
*
* Legacy LXC network blocks with no explicit enforcement mode use `'firewall'`
* because capabilities are Windows-only. Bubblewrap uses firewall only for
* host lists without a proxy.
*
* If the caller explicitly passes `enforcementMode: 'capabilities'` we
* warn: `'capabilities'` is a Windows/AppContainer concept (a token
* capability mask) and has no Linux equivalent. The explicit value is left
* unchanged so native backend validation can report unsupported combinations.
*
* Shared between the explicit `'bubblewrap'` / `'lxc'` builders and the
* abstract `'process'` branch on Linux (which resolves to Bubblewrap
* server-side).
*
* NOTE: when `network.proxy` is configured on Bubblewrap, host filtering
* is enforced at the proxy layer (unprivileged, no CAP_NET_ADMIN). The
* Rust config parser explicitly rejects `bubblewrap + proxy + firewall`
* since the iptables path requires privilege the bwrap backend
* deliberately avoids. Callers in that mode must leave enforcementMode
* at its default.
*/
export function applyLinuxNetworkPolicy(config: ContainerConfig): void {
if (!config.network) {
return;
}
if (config.network.enforcementMode === 'capabilities') {
console.warn(
"mxc-sdk: network.enforcementMode='capabilities' has no effect on Linux " +
"(it is a Windows AppContainer / ProcessContainer concept). The Linux " +
"runner will not enforce host filtering via capabilities. Use the " +
"default mode (auto-promotes to 'firewall' for LXC, or use network.proxy " +
"for unprivileged Bubblewrap enforcement)."
);
}
// LXC cannot enforce Windows AppContainer capabilities, so omitted legacy modes use firewall.
const isLegacyNetwork =
config.network.egress === undefined && config.network.ingress === undefined;
if (
config.containment === 'lxc' &&
isLegacyNetwork &&
config.network.enforcementMode === undefined
) {
config.network.enforcementMode = 'firewall';
}
const hasProxy = !!config.network.proxy;
const hasHostRules =
!!(config.network.allowedHosts?.length || config.network.blockedHosts?.length);
if (hasHostRules && !hasProxy) {
config.network.enforcementMode = 'firewall';
}
}

/**
* Resolves the executor binary and builds the common CLI arguments for any
* MXC request envelope (one-shot or state-aware). Performs platform support
Expand Down
7 changes: 2 additions & 5 deletions sdk/node/src/v1/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import {
ExecutionResult,
UnsupportedV1NetworkFields,
} from './types.js';
import { applyLinuxNetworkPolicy } from '../helper.js';
import { diagLog } from '../diagnostic.js';
import { MxcError } from './errors.js';
import { prepareOneShotRequest } from '../bindings/one-shot.js';
Expand Down Expand Up @@ -129,14 +128,13 @@ function buildWslcContainerConfig(
/**
* Builds the Bubblewrap (bwrap) portion of a ContainerConfig.
* Bubblewrap is Linux-only and uses shared cross-backend fields only —
* no backend-specific config block. Network enforcement via iptables
* reuses the same approach as LXC.
* no backend-specific config block. Network policy uses shared directional
* fields enforced by the native backend.
*/
function buildBubblewrapConfig(
config: ContainerConfig,
): ContainerConfig {
config.containment = 'bubblewrap';
applyLinuxNetworkPolicy(config);
return config;
}

Expand All @@ -150,7 +148,6 @@ function buildLinuxProcessConfig(
distribution: 'alpine',
release: '3.23',
};
applyLinuxNetworkPolicy(config);
return config;
}

Expand Down
30 changes: 1 addition & 29 deletions sdk/node/src/v1/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,34 +251,6 @@ export interface FilesystemConfig {
clearPolicyOnExit?: boolean;
}

/**
* Network access configuration. Legacy fields remain for TypeScript source
* compatibility only; every registered exact contract rejects them. Use
* directional fields and runtimeConfig.networkProxy instead.
*/
export interface NetworkConfig extends DirectionalNetworkConfig {
/**
* Retired enforcement selector; registered contracts reject this field.
* @deprecated Use directional network policy instead.
*/
enforcementMode?: 'capabilities' | 'firewall' | 'both';
/** @deprecated Use egress.default instead; registered contracts reject this field. */
defaultPolicy?: 'allow' | 'block';
/**
* @deprecated Use ingress.default instead; registered contracts reject this field.
*/
allowLocalNetwork?: boolean;
/** @deprecated Use numeric CIDR egress rules or proxy-side hostname filtering. */
allowedHosts?: string[];
/** @deprecated Use numeric CIDR egress rules or proxy-side hostname filtering. */
blockedHosts?: string[];
/** @deprecated Registered contracts reject network.proxy. Use
* runtimeConfig.networkProxy with the backend's supported policy posture. */
proxy?: { builtinTestServer: true } | { localhost: number } | { url: string };
/** @deprecated Use lifecycle.preservePolicy; registered contracts reject this field. */
removeRulesOnExit?: boolean;
}

/** Directional network policy used by provisioning. */
export interface DirectionalNetworkConfig {
/** Outbound network policy. */
Expand Down Expand Up @@ -450,7 +422,7 @@ export interface ContainerConfig {
/** Filesystem access configuration */
filesystem?: FilesystemConfig;
/** Network access configuration */
network?: NetworkConfig;
network?: DirectionalNetworkConfig;
Comment thread
MGudgin marked this conversation as resolved.
/** Runtime values supplied separately from sandbox policy. */
runtimeConfig?: NetworkRuntimeConfig;
/** Telemetry configuration for TraceLogging ETW support */
Expand Down
72 changes: 29 additions & 43 deletions sdk/node/tests/unit/wire-conformance.test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

// Exact v1.1 one-shot type conformance oracle.
// Exact v1.0 one-shot type conformance oracle.
//
// The generated module `../../src/generated/v1_1_0_alpha/wire.ts` is emitted
// from the exact v1.1 contract. It is the source of truth for the raw one-shot
// shape produced by the current high-level SDK builder.
// The generated module `../../src/generated/v1_0_0/wire.ts` is emitted
// from the published v1.0 contract. It is the source of truth for the raw
// one-shot shape produced by the high-level V1 SDK builder.
//
// This file asserts — at COMPILE TIME — that the hand-written public SDK types
// in `../../src/types.ts` still conform to that generated shape. If the Rust
// in `../../src/v1/types.ts` still conform to that generated shape. If the Rust
// exact contract changes (a field renamed/removed, an enum value added/dropped,
// a type narrowed) the regenerated oracle shifts and these assertions stop
// compiling, so `npm run build:test-unit` fails. The runtime body is a no-op;
Expand All @@ -17,12 +17,11 @@
// Mapping rationale:
// * The exact contract rejects null and requires one-shot `process`. The
// high-level builder supplies the exact version, process, and LXC defaults;
// `PublicV1OneShotConfig` models that emitted raw shape.
// * `NetworkConfig` intentionally spans historical public versions. Its
// public-only key assertion documents the legacy fields omitted from v1.1
// raw JSON while keeping directional field conformance exact.
// `PublicV1OneShotConfig` models that emitted raw shape after containment
// validation excludes development-only choices.
// * Network comparisons use the directional fields emitted by the V1 builder.
// * Stable WSLC is additionally checked against the published v0.9 one-shot
// contract because this stack exposes WSLC in both v0.9 and v1.1.
// contract because this stack exposes WSLC in both v0.9 and v1.0.
// * `OnlyInPublic` additionally catches a public field whose wire counterpart
// was renamed or removed (width subtyping alone would not), and is asserted
// to equal a documented, explicit set of SDK-only fields — so a NEW
Expand Down Expand Up @@ -61,6 +60,7 @@ import type {
import type {
Containment as InternalContainment,
} from '../../src/v1/types.js';
import type { SDK_CONTRACT_VERSION } from '../../src/v1/contract-version.js';
import type {
ContainerId as InternalContainerId,
} from '../../src/v1/lifecycle-types.js';
Expand All @@ -85,7 +85,6 @@ import type {
ProcessConfig,
LifecycleConfig,
FilesystemConfig,
NetworkConfig,
DirectionalNetworkConfig,
NetworkEgressConfig,
NetworkIngressConfig,
Expand All @@ -103,8 +102,6 @@ import type {
TelemetryConfig,
ContainerConfig,
ClipboardPolicy as PublicClipboardPolicy,
ContainmentType,
ContainmentBackend,
} from '../../src/v1/types.js';

import type {
Expand Down Expand Up @@ -134,7 +131,7 @@ import type {
ProcessContainerUiIsolation as WireUiIsolation,
TransportProtocol as WireTransportProtocol,
Version as WireVersion,
} from '../../src/generated/v1_1_0_alpha/wire.js';
} from '../../src/generated/v1_0_0/wire.js';

import type {
OneShotWslc as WireV09Wslc,
Expand All @@ -151,6 +148,7 @@ import type {

// --- enum / union conformance ---------------------------------------------

type _SdkContractVersion = AssertTrue<Equivalent<typeof SDK_CONTRACT_VERSION, WireVersion>>;
type _V1BackendConfigExport = AssertTrue<
Equivalent<V1Containment, InternalContainment>
>;
Expand Down Expand Up @@ -203,10 +201,9 @@ type _ExecutionResultExitCode = AssertTrue<Assignable<ExecutionResult['exitCode'
// Clipboard policy must be value-for-value identical to the wire enum.
type _Clipboard = AssertTrue<Equivalent<PublicClipboardPolicy, WireClipboardPolicy>>;

// The SDK splits containment into abstract intents + concrete backends; their
// union must cover exactly the wire `Containment` enum.
// V1 creation supports exactly the containment choices in its published wire contract.
type _Containment = AssertTrue<
Equivalent<ContainmentType | ContainmentBackend, WireContainment>
Equivalent<V1Containment['type'], WireContainment>
>;

// Enum-backed object fields are checked bidirectionally so an exact-contract
Expand All @@ -226,6 +223,9 @@ type _NetworkPortProtocol = AssertTrue<
type _BaseProcessUiIsolation = AssertTrue<
Equivalent<NonNullable<BaseProcessUiConfig['isolation']>, WireUiIsolation>
>;
type _BaseProcessUiSystemSettings = AssertTrue<
Equivalent<BaseProcessUiConfig['systemSettings'], NonNullable<WireBaseProcessUi['systemSettings']>>
>;
type _PortProtocol = AssertTrue<
Equivalent<NonNullable<PublicPortMapping['protocol']>, WireTransportProtocol>
>;
Expand All @@ -237,7 +237,7 @@ type _PortProtocol = AssertTrue<
type _ProcessVals = AssertTrue<Assignable<ProcessConfig, WireProcess>>;
type _LifecycleVals = AssertTrue<Assignable<LifecycleConfig, WireLifecycle>>;
type _FilesystemVals = AssertTrue<Assignable<FilesystemConfig, WireFilesystem>>;
type _NetworkVals = AssertTrue<Assignable<NetworkConfig, WireNetwork>>;
type _NetworkVals = AssertTrue<Assignable<DirectionalNetworkConfig, WireNetwork>>;
type _NetworkEgressVals = AssertTrue<Assignable<NetworkEgressConfig, WireNetworkEgress>>;
type _NetworkIngressVals = AssertTrue<Assignable<NetworkIngressConfig, WireNetworkIngress>>;
type _NetworkPeerVals = AssertTrue<Assignable<NetworkPeerConfig, WireNetworkPeer>>;
Expand Down Expand Up @@ -280,21 +280,7 @@ type _TelemetryKeys = AssertTrue<Equivalent<OnlyInPublic<TelemetryConfig, WireTe
// into `lifecycle.preservePolicy`; it is not a wire `filesystem` field.
type _FilesystemKeys = AssertTrue<Equivalent<OnlyInPublic<FilesystemConfig, WireFilesystem>, 'clearPolicyOnExit'>>;

// The public network type spans historical contracts. Exact v1.1 emits only
// directional fields; the listed legacy and SDK-only fields are deliberately
// absent from its raw JSON mapping.
type _NetworkKeys = AssertTrue<
Equivalent<
OnlyInPublic<NetworkConfig, WireNetwork>,
| 'enforcementMode'
| 'defaultPolicy'
| 'allowLocalNetwork'
| 'allowedHosts'
| 'blockedHosts'
| 'proxy'
| 'removeRulesOnExit'
>
>;
type _NetworkKeys = AssertTrue<Equivalent<OnlyInPublic<DirectionalNetworkConfig, WireNetwork>, never>>;

// `ProcessContainerConfig.name` is the deprecated AppContainer profile name
// (superseded by top-level `containerId`); not a wire `processContainer` field.
Expand All @@ -307,18 +293,20 @@ type _LxcKeys = AssertTrue<Equivalent<OnlyInPublic<RawV1LxcConfig, WireLxc>, nev
// --- ROOT conformance (review finding F1) ---------------------------------
// Without these, a top-level wire field rename/removal regenerates wire.ts but
// no assertion notices, so the leaf-only checks above are not enough.
// `PublicV1OneShotConfig` models the current high-level builder output:
// * its value shape must be assignable to exact v1.1 `OneShotRequest`; and
// `PublicV1OneShotConfig` models the current high-level builder output,
// excluding the development-only Hyperlight section of the public raw config:
// * its value shape must be assignable to exact v1.0 `OneShotRequest`; and
// * it must have no public-only root keys.
// A new root divergence fails the build.
type PublicV1OneShotConfig = Omit<
ContainerConfig,
'version' | 'process' | 'network' | 'lxc' | 'appContainer'
'version' | 'process' | 'network' | 'lxc' | 'appContainer' | 'containment' | 'hyperlight'
> & {
version: WireVersion;
process: ProcessConfig;
network?: DirectionalNetworkConfig;
lxc?: RawV1LxcConfig;
containment?: V1Containment['type'];
};
type _RootVals = AssertTrue<Assignable<PublicV1OneShotConfig, WireMxcConfig>>;
type _RootKeys = AssertTrue<
Expand All @@ -334,7 +322,7 @@ type _RootKeys = AssertTrue<
type _ProcessWireKeys = AssertTrue<Equivalent<OnlyInWire<ProcessConfig, WireProcess>, never>>;
type _LifecycleWireKeys = AssertTrue<Equivalent<OnlyInWire<LifecycleConfig, WireLifecycle>, never>>;
type _FilesystemWireKeys = AssertTrue<Equivalent<OnlyInWire<FilesystemConfig, WireFilesystem>, never>>;
type _NetworkWireKeys = AssertTrue<Equivalent<OnlyInWire<NetworkConfig, WireNetwork>, never>>;
type _NetworkWireKeys = AssertTrue<Equivalent<OnlyInWire<DirectionalNetworkConfig, WireNetwork>, never>>;
type _NetworkEgressWireKeys = AssertTrue<Equivalent<OnlyInWire<NetworkEgressConfig, WireNetworkEgress>, never>>;
type _NetworkIngressWireKeys = AssertTrue<Equivalent<OnlyInWire<NetworkIngressConfig, WireNetworkIngress>, never>>;
type _NetworkPeerWireKeys = AssertTrue<Equivalent<OnlyInWire<NetworkPeerConfig, WireNetworkPeer>, never>>;
Expand All @@ -358,26 +346,24 @@ type _SeatbeltWireKeys = AssertTrue<
>;
type _TelemetryWireKeys = AssertTrue<Equivalent<OnlyInWire<TelemetryConfig, WireTelemetry>, never>>;

// Root: the high-level builder intentionally omits schema metadata, fallback,
// development-only test and Windows Sandbox sections.
// Root: the high-level builder intentionally omits schema metadata and fallback.
type _RootWireKeys = AssertTrue<
Equivalent<
OnlyInWire<PublicV1OneShotConfig, WireMxcConfig>,
| '$schema'
| '_comment'
| 'fallback'
| 'test'
| 'windowsSandbox'
>
>;

// Reference the assertion aliases so they read as intentionally load-bearing.
export type WireConformanceAssertions = [
_V1BackendConfigExport, _V1IdExport,
_SdkContractVersion, _V1BackendConfigExport, _V1IdExport,
_ContainerRequestFilesystem, _ContainerRequestNetwork, _ContainerRequestContainment,
_Clipboard, _Containment,
_NetworkEgressDefault, _NetworkIngressDefault, _NetworkIngressHostLoopback,
_NetworkPortProtocol, _BaseProcessUiIsolation, _PortProtocol,
_NetworkPortProtocol, _BaseProcessUiIsolation, _BaseProcessUiSystemSettings,
_PortProtocol,
_ProcessVals, _LifecycleVals, _FilesystemVals, _NetworkVals, _UiVals,
_NetworkEgressVals, _NetworkIngressVals, _NetworkPeerVals, _NetworkPortVals,
_NetworkRuleVals, _RuntimeConfigVals,
Expand Down
Loading