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
2 changes: 2 additions & 0 deletions docs/api-reference/dotnet/v1/types.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ public enum BackendCapability
FilesystemDeniedPaths = 3,
IngressHostLoopbackAllow = 4,
FilesystemEnumeratePaths = 5,
IdentitylessLoopbackProxy = 6,
}
```

Expand Down Expand Up @@ -954,6 +955,7 @@ public sealed class ProbeFacts
public bool BaseContainerSupportsDenyPaths { get; init; }
public bool BaseContainerSupportsEnumeratePaths { get; init; }
public bool BaseContainerSupportsIngressHostLoopbackAllow { get; init; }
public bool BaseContainerSupportsIdentitylessLoopbackProxy { get; init; }
public bool IsolationSessionAvailable { get; init; }
public bool HyperlightAvailable { get; init; }
public required UiCapabilitySupport UiCapabilities { get; init; }
Expand Down
2 changes: 2 additions & 0 deletions docs/api-reference/node/v1/types.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export type BackendCapability =
| 'filesystemEnumeratePaths'
| 'ingressHostLoopbackAllow'
| 'proxyEnforcement'
| 'identitylessLoopbackProxy'
| 'unknown';
```

Expand Down Expand Up @@ -578,6 +579,7 @@ export interface ProbeFacts {
baseContainerSupportsDenyPaths: boolean;
baseContainerSupportsEnumeratePaths: boolean;
baseContainerSupportsIngressHostLoopbackAllow: boolean;
baseContainerSupportsIdentitylessLoopbackProxy: boolean;
isolationSessionAvailable: boolean;
hyperlightAvailable: boolean;
uiCapabilities: UiCapabilitySupport;
Expand Down
2 changes: 2 additions & 0 deletions docs/api-reference/rust/v1/types.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ pub enum BackendCapability {
FilesystemEnumeratePaths,
IngressHostLoopbackAllow,
ProxyEnforcement,
IdentitylessLoopbackProxy,
}
```

Expand Down Expand Up @@ -535,6 +536,7 @@ pub struct ProbeFacts {
pub base_container_supports_deny_paths: bool,
pub base_container_supports_enumerate_paths: bool,
pub base_container_supports_ingress_host_loopback_allow: bool,
pub base_container_supports_identityless_loopback_proxy: bool,
pub isolation_session_available: bool,
pub hyperlight_available: bool,
pub ui_capabilities: UiCapabilitySupport,
Expand Down
31 changes: 28 additions & 3 deletions docs/backends/process-container/networking.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,11 +116,36 @@ PSEC rejects it alongside a proxy. Their ingress default still selects `privateN
host-loopback setting selects both the `networkLoopback` capability and the `MXC-Loopback` peer rather than an
identity-scoped proxy peer. The peer alone is insufficient: without the capability, the environment can be created
but its client cannot reach the host proxy.
The public ingress policy remains required, and host-loopback allow retains the PSEC 1.1 host requirement.
Requests that do not allow host loopback use the
PSEC 1.0 capability mapping; `hostLoopback: "allow"` is rejected when the PSEC 1.1 ingress contract is unavailable.
The public ingress policy remains required. Direct-egress host-loopback allow
requires PSEC 1.1 and the ingress support flag. For an identity-less
`runtimeConfig.networkProxy` with explicit `hostLoopback: "allow"`, MXC also
supports a compatibility workaround on hosts that support PSEC 1.0 but not 1.1.
Hosts that support PSEC 1.1 or newer must still advertise the ingress support
flag; accepting a 1.0 payload does not enable this workaround.
Comment thread
caarlos0 marked this conversation as resolved.
Requests that do not allow host loopback use the PSEC 1.0 capability mapping.
Requests the PSEC contract cannot preserve continue to an AppContainer tier.

#### Discovering identity-less loopback proxy support

Backend discovery reports `identitylessLoopbackProxy` on the ProcessContainer
`base-container` tier when the host supports an identity-less proxy on loopback.
The Windows request probe also reports
`probes.baseContainerSupportsIdentitylessLoopbackProxy`, including when called
without a request. Support is available on PSEC 1.0-only hosts and on hosts with
PSEC 1.1 or newer that advertise ingress support. A host that reports
`baseContainerSupportsIngressHostLoopbackAllow` also supports an identity-less
loopback proxy. PSEC 1.1 or newer without ingress support does not qualify,
even if it also accepts PSEC 1.0 payloads. Version-query failures omit the
capability, set the probe fact to `false`, and add a warning.

This does not grant general `ingressHostLoopbackAllow` support or report that the
current request uses an identity-less proxy. The request must set
`runtimeConfig.networkProxy`, omit `processContainer.network.allowedProxyPeer`,
and explicitly allow host loopback, while retaining the Model 2 network policy.
Other requested features still require their own host support.
Use the request-aware probe for the complete policy. Neither probe launches the
workload or checks proxy reachability.

**Unresolved ingress limitation:** on Windows build 26691.1002, this proxy mapping permits unprivileged connections
to the configured proxy and blocks direct loopback bypasses, but host-to-container listener connections still time
out. The same listener succeeds under a non-proxy PSEC ingress policy. The proxy mapping is therefore not yet a
Expand Down
8 changes: 5 additions & 3 deletions docs/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,9 +135,11 @@ the development/testing configuration is:

This identity-less host-loopback deployment restricts client egress to the
configured proxy address and port, but does not verify which process owns that
endpoint. It requires native PSEC 1.1 ingress/host-loopback support; unsupported
hosts reject the request. For production ProcessContainer deployments, identify
a packaged proxy through `processContainer.network.allowedProxyPeer` instead; see
endpoint. It supports PSEC 1.0-only hosts and hosts with PSEC 1.1 or newer that
advertise ingress support. PSEC 1.1 or newer without that flag rejects the
request, even if the host also accepts PSEC 1.0 payloads. For production
ProcessContainer deployments, identify a packaged proxy through
`processContainer.network.allowedProxyPeer` instead; see
[proxy deployment choices](backends/process-container/networking.md#proxy-deployment-choices).
Bubblewrap and Seatbelt also support a caller-managed loopback proxy, but
their supported ingress policies differ. See their backend guides.
Expand Down
11 changes: 10 additions & 1 deletion sdk/dotnet/Microsoft.Mxc.Sdk.AotSmokeTest/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,8 @@ static void CheckRoundTrip<T>(T value, Func<T, bool> check, string name)
"bfsCompiledIn": false,
"baseContainerSupportsDenyPaths": true,
"baseContainerSupportsEnumeratePaths": false,
"baseContainerSupportsIngressHostLoopbackAllow": true,
"baseContainerSupportsIngressHostLoopbackAllow": false,
"baseContainerSupportsIdentitylessLoopbackProxy": true,
"isolationSessionAvailable": true,
"hyperlightAvailable": false,
"uiCapabilities": {
Expand All @@ -359,6 +360,14 @@ static void CheckRoundTrip<T>(T value, Func<T, bool> check, string name)
Check(probe.Tier == IsolationTier.AppContainerDacl, "probe tier parsed");
Check(probe.NeedsDaclAugmentation == true, "probe dacl augmentation parsed");
Check(probe.Probes.BaseContainerApiPresent, "probe facts parsed");
Check(probe.Probes.BaseContainerSupportsIdentitylessLoopbackProxy, "identity-less loopback proxy parsed");
Check(probe.Probes.UiCapabilities.CanBlockClipboardRead, "probe ui capabilities parsed");
var nativeProbe = MxcJson.Deserialize<NativeProbeOutput>(probeJson, MxcJson.ProbeOptions)!;
CheckRoundTrip(nativeProbe.Probes!,
x => x.BaseContainerSupportsIdentitylessLoopbackProxy, "identity-less loopback proxy facts");
var discovery = MxcPlatform.ParseAvailableBackends(
"""[{"backend":"processcontainer","tier":"base-container","capabilities":["identitylessLoopbackProxy"]}]""");
Check(discovery.Single().Capabilities.SequenceEqual([BackendCapability.IdentitylessLoopbackProxy]),
"identity-less loopback proxy discovery");

Console.WriteLine("AOT smoke test passed: all JSON paths are reflection-free.");
38 changes: 38 additions & 0 deletions sdk/dotnet/Microsoft.Mxc.Sdk.Tests/V1/MxcContainerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public class MxcContainerTests
"baseContainerSupportsDenyPaths": true,
"baseContainerSupportsEnumeratePaths": false,
"baseContainerSupportsIngressHostLoopbackAllow": true,
"baseContainerSupportsIdentitylessLoopbackProxy": true,
"isolationSessionAvailable": true,
"hyperlightAvailable": false,
"uiCapabilities": {
Expand Down Expand Up @@ -272,6 +273,41 @@ public void ProbeParser_RejectsMissingRequiredProbeBoolean()
json["probes"]!.AsObject().Remove("bfscfgPresent"));
}

[Theory]
[InlineData(false, false)]
[InlineData(true, false)]
[InlineData(true, true)]
public void ProbeParser_PreservesIdentitylessLoopbackProxy(bool supported, bool ingressSupported)
{
var json = CreateCompleteProbeJson();
json["tier"] = "base-container";
json["needsDaclAugmentation"] = false;
json["probes"]!["baseContainerSupportsIngressHostLoopbackAllow"] = ingressSupported;
json["probes"]!["baseContainerSupportsIdentitylessLoopbackProxy"] = supported;

var output = MxcContainer.ParseProbeOutput(json.ToJsonString());

Assert.Equal(supported, output.Probes.BaseContainerSupportsIdentitylessLoopbackProxy);
Assert.Equal(ingressSupported, output.Probes.BaseContainerSupportsIngressHostLoopbackAllow);
}

[Fact]
public void ProbeParser_RejectsMissingIdentitylessLoopbackProxy()
{
AssertProbeJsonRejected(json =>
json["probes"]!.AsObject().Remove("baseContainerSupportsIdentitylessLoopbackProxy"));
}

[Theory]
[InlineData("null")]
[InlineData("\"true\"")]
[InlineData("1")]
public void ProbeParser_RejectsInvalidIdentitylessLoopbackProxy(string value)
{
AssertProbeJsonRejected(json =>
json["probes"]!["baseContainerSupportsIdentitylessLoopbackProxy"] = JsonNode.Parse(value));
}

[Fact]
public void ProbeParser_RejectsMissingRequiredUiBoolean()
{
Expand Down Expand Up @@ -456,6 +492,7 @@ public void Discovery_MapsEveryNativeIsolationTier(
[InlineData("filesystemEnumeratePaths", BackendCapability.FilesystemEnumeratePaths)]
[InlineData("ingressHostLoopbackAllow", BackendCapability.IngressHostLoopbackAllow)]
[InlineData("proxyEnforcement", BackendCapability.ProxyEnforcement)]
[InlineData("identitylessLoopbackProxy", BackendCapability.IdentitylessLoopbackProxy)]
public void Discovery_MapsEveryNativeCapability(
string wireName,
BackendCapability expected)
Expand All @@ -472,6 +509,7 @@ public void BackendCapability_PreservesReleasedNumericValues()
Assert.Equal(3, (int)BackendCapability.FilesystemDeniedPaths);
Assert.Equal(4, (int)BackendCapability.IngressHostLoopbackAllow);
Assert.Equal(5, (int)BackendCapability.FilesystemEnumeratePaths);
Assert.Equal(6, (int)BackendCapability.IdentitylessLoopbackProxy);
}

[Theory]
Expand Down
1 change: 1 addition & 0 deletions sdk/dotnet/Microsoft.Mxc.Sdk/MxcJsonSerialization.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ public CamelCaseJsonStringEnumConverter()
[JsonSerializable(typeof(NativeAvailableBackend[]))]
[JsonSerializable(typeof(NativePlatformSupport))]
[JsonSerializable(typeof(NativeProbeOutput))]
[JsonSerializable(typeof(NativeProbeFacts))]
[JsonSerializable(typeof(ExecutionMetadata))]
[JsonSerializable(typeof(CaptureDenialsResult))]
[JsonSerializable(typeof(CaptureDenialsError))]
Expand Down
2 changes: 2 additions & 0 deletions sdk/dotnet/Microsoft.Mxc.Sdk/V1/MxcContainer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,8 @@ internal static ProbeOutput ParseProbeOutput(string json)
probes.BaseContainerSupportsEnumeratePaths,
BaseContainerSupportsIngressHostLoopbackAllow =
probes.BaseContainerSupportsIngressHostLoopbackAllow,
BaseContainerSupportsIdentitylessLoopbackProxy =
probes.BaseContainerSupportsIdentitylessLoopbackProxy,
IsolationSessionAvailable = probes.IsolationSessionAvailable,
HyperlightAvailable = probes.HyperlightAvailable,
UiCapabilities = new UiCapabilitySupport
Expand Down
1 change: 1 addition & 0 deletions sdk/dotnet/Microsoft.Mxc.Sdk/V1/MxcPlatform.cs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ internal static BackendCapability ParseBackendCapability(string value) =>
"filesystemEnumeratePaths" => BackendCapability.FilesystemEnumeratePaths,
"ingressHostLoopbackAllow" => BackendCapability.IngressHostLoopbackAllow,
"proxyEnforcement" => BackendCapability.ProxyEnforcement,
"identitylessLoopbackProxy" => BackendCapability.IdentitylessLoopbackProxy,
_ => BackendCapability.Unknown,
};

Expand Down
12 changes: 12 additions & 0 deletions sdk/dotnet/Microsoft.Mxc.Sdk/V1/PlatformDiscovery.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,12 @@ public enum BackendCapability
/// Native filesystem enumeration-only access at the reported tier.
/// </summary>
FilesystemEnumeratePaths = 5,

/// <summary>
/// Identity-less proxy support on loopback.
/// Requires explicit host-loopback allow; not general ingress support.
/// </summary>
IdentitylessLoopbackProxy = 6,
}

/// <summary>One host-available backend and its probed capabilities.</summary>
Expand Down Expand Up @@ -146,6 +152,8 @@ public sealed class ProbeFacts
public bool BaseContainerSupportsDenyPaths { get; init; }
public bool BaseContainerSupportsEnumeratePaths { get; init; }
public bool BaseContainerSupportsIngressHostLoopbackAllow { get; init; }
/// <summary>Identity-less proxy support on loopback; requires explicit host-loopback allow.</summary>
public bool BaseContainerSupportsIdentitylessLoopbackProxy { get; init; }
public bool IsolationSessionAvailable { get; init; }
public bool HyperlightAvailable { get; init; }
public required UiCapabilitySupport UiCapabilities { get; init; }
Expand Down Expand Up @@ -279,6 +287,10 @@ internal sealed class NativeProbeFacts
[JsonPropertyName("baseContainerSupportsIngressHostLoopbackAllow")]
public bool BaseContainerSupportsIngressHostLoopbackAllow { get; init; }

[JsonRequired]
[JsonPropertyName("baseContainerSupportsIdentitylessLoopbackProxy")]
public bool BaseContainerSupportsIdentitylessLoopbackProxy { get; init; }

[JsonRequired]
[JsonPropertyName("isolationSessionAvailable")]
public bool IsolationSessionAvailable { get; init; }
Expand Down
1 change: 1 addition & 0 deletions sdk/node/src/v1/platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const discoveryBackends: readonly ContainmentBackend[] = [
const discoveryCapabilities: readonly BackendCapability[] = [
'captureDenials', 'filesystemDeniedPaths', 'filesystemEnumeratePaths',
'ingressHostLoopbackAllow', 'proxyEnforcement',
'identitylessLoopbackProxy',
];
const discoveryTiers: readonly IsolationTier[] = [
'base-container', 'appcontainer-bfs', 'appcontainer-dacl',
Expand Down
1 change: 1 addition & 0 deletions sdk/node/src/v1/probe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const probeFactBooleanFields: readonly (keyof ProbeFacts)[] = [
'baseContainerSupportsDenyPaths',
'baseContainerSupportsEnumeratePaths',
'baseContainerSupportsIngressHostLoopbackAllow',
'baseContainerSupportsIdentitylessLoopbackProxy',
'isolationSessionAvailable',
'hyperlightAvailable',
];
Expand Down
3 changes: 3 additions & 0 deletions sdk/node/src/v1/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -711,6 +711,7 @@ export type BackendCapability =
| 'filesystemEnumeratePaths'
| 'ingressHostLoopbackAllow'
| 'proxyEnforcement'
| 'identitylessLoopbackProxy'
| 'unknown';

/** One host-available backend and its native capabilities. */
Expand Down Expand Up @@ -784,6 +785,8 @@ export interface ProbeFacts {
baseContainerSupportsDenyPaths: boolean;
baseContainerSupportsEnumeratePaths: boolean;
baseContainerSupportsIngressHostLoopbackAllow: boolean;
/** Identity-less proxy support on loopback; requires explicit host-loopback allow. */
baseContainerSupportsIdentitylessLoopbackProxy: boolean;
Comment thread
caarlos0 marked this conversation as resolved.
/** True when this executor includes IsolationSession and the host can activate it. */
isolationSessionAvailable: boolean;
/** True when this executor includes Hyperlight and its host runtime is available. */
Expand Down
13 changes: 13 additions & 0 deletions sdk/node/tests/unit/platform.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,19 @@ describe('native backend discovery', () => {
}]);
});

it('reports identity-less loopback proxy support without general ingress support', () => {
assert.deepStrictEqual(parseAvailableBackends(JSON.stringify([{
backend: 'processcontainer',
tier: 'base-container',
capabilities: ['identitylessLoopbackProxy'],
}])), [{
backend: 'processcontainer',
tier: 'base-container',
capabilities: ['identitylessLoopbackProxy'],
warnings: [],
}]);
});

it('rejects malformed discovery results instead of returning no backends', () => {
for (const payload of [
null, {}, [null], [[]], [{}], [{ backend: 1 }],
Expand Down
30 changes: 30 additions & 0 deletions sdk/node/tests/unit/probe.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const completeProbe = {
baseContainerSupportsDenyPaths: false,
baseContainerSupportsEnumeratePaths: false,
baseContainerSupportsIngressHostLoopbackAllow: false,
baseContainerSupportsIdentitylessLoopbackProxy: false,
isolationSessionAvailable: false,
hyperlightAvailable: false,
uiCapabilities: {
Expand Down Expand Up @@ -192,6 +193,35 @@ describe('probe', () => {
assert.equal(output.needsDaclAugmentation, true);
});

it('preserves identity-less proxy support with or without general ingress', () => {
for (const [supported, ingressSupported] of [[false, false], [true, false], [true, true]]) {
const output = runProbeOutput({
...completeProbe,
tier: 'base-container',
needsDaclAugmentation: false,
probes: {
...completeProbe.probes,
baseContainerSupportsIdentitylessLoopbackProxy: supported,
baseContainerSupportsIngressHostLoopbackAllow: ingressSupported,
},
});
assert.equal(output.probes.baseContainerSupportsIdentitylessLoopbackProxy, supported);
assert.equal(output.probes.baseContainerSupportsIngressHostLoopbackAllow, ingressSupported);
}
});

it('rejects missing or invalid identity-less loopback proxy facts', () => {
for (const value of [undefined, null, 'true', 1]) {
assertProbeOutputRejected({
...completeProbe,
probes: {
...completeProbe.probes,
baseContainerSupportsIdentitylessLoopbackProxy: value,
},
});
}
});

it('rejects an unknown envelope field', () => {
const payload = cloneCompleteProbe();
payload.unknownEnvelopeField = true;
Expand Down
1 change: 1 addition & 0 deletions src/ffi/mxc_ffi/tests/ffi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ fn extern_discovery_returns_owned_json() {
| "filesystemEnumeratePaths"
| "ingressHostLoopbackAllow"
| "proxyEnforcement"
| "identitylessLoopbackProxy"
)
));
}
Expand Down
Loading
Loading