diff --git a/docs/bwrap-support/bubblewrap-backend.md b/docs/bwrap-support/bubblewrap-backend.md index 866d1012d..6e9a81fd1 100644 --- a/docs/bwrap-support/bubblewrap-backend.md +++ b/docs/bwrap-support/bubblewrap-backend.md @@ -758,7 +758,7 @@ an exit is detected; see [Limitations](#limitations). "commandLine": "curl -fsSL https://api.github.com/zen && echo OK" }, "network": { - "defaultPolicy": "allow", + "defaultPolicy": "block", "proxy": { "builtinTestServer": true }, "allowedHosts": ["api.github.com"] } diff --git a/docs/nanvix-microvm/nanvix.md b/docs/nanvix-microvm/nanvix.md index 708cae788..28c843b54 100644 --- a/docs/nanvix-microvm/nanvix.md +++ b/docs/nanvix-microvm/nanvix.md @@ -189,18 +189,16 @@ The following describes the retained legacy runtime filter, not accepted v0.10 JSON vocabulary. The exact cutover does not silently translate directional rules into this weaker contract. -`allowedHosts` and `blockedHosts` are supported and forwarded to the guest's -host-side socket proxy, which enforces egress at `connect()`. The guest filter -is **allow-XOR-block**, so the two lists are mutually exclusive (specifying both -is rejected at preflight). Presence of either list implies host networking, so -`defaultPolicy` is ignored when a list is set: - -| `allowedHosts` | `blockedHosts` | Effect | -| -------------- | -------------- | ------ | -| _(empty)_ | _(empty)_ | follows `defaultPolicy` (`block` = no egress, `allow` = unrestricted) | -| `[A, ...]` | _(empty)_ | **allowlist** — only the listed destinations are reachable | -| _(empty)_ | `[B, ...]` | **blocklist** — everything except the listed destinations is reachable | -| `[A, ...]` | `[B, ...]` | rejected at preflight (mutually exclusive) | +Legacy `defaultPolicy` and host-list interactions follow the +[backend-agnostic network policy semantics](../schema.md#legacy-network-host-list-semantics). +Invalid legacy combinations are rejected by shared policy validation: +`blockedHosts` requires an `allowedHosts` exception set under a block default, +and `allowedHosts` cannot be used under an allow default. + +NanVix forwards the validated host list to the guest's host-side socket proxy, +which enforces egress at `connect()`. The guest filter is **allow-XOR-block**, +so NanVix rejects requests that supply both lists instead of dropping either +one. Entries may be IPv4 literals (`93.184.216.34`), IPv4 CIDR blocks (`10.0.0.0/8`), or hostnames. Hostnames are resolved to their IPv4 (A-record) diff --git a/docs/sandbox-policy/0.7.0/policy.md b/docs/sandbox-policy/0.7.0/policy.md index a17ee17d7..cc41fc9c9 100644 --- a/docs/sandbox-policy/0.7.0/policy.md +++ b/docs/sandbox-policy/0.7.0/policy.md @@ -215,10 +215,10 @@ All flags default to `false` (no network access). | Field | Description | |--------------------|-------------| -| `allowOutbound` | Allow outbound connections to the internet (HTTP, DNS, etc.). | +| `allowOutbound` | Allow outbound connections by default when no `allowedHosts` list narrows the policy. | | `allowLocalNetwork`| Allow connections to local networks. | -| `allowedHosts` | When set, only these outbound hosts are reachable on host-filtering backends. Seatbelt accepts the field for compatibility but degrades to allow-all outbound because it cannot filter DNS names. | -| `blockedHosts` | Hosts to block even when outbound is allowed. Seatbelt rejects this field because hostname blocks cannot be enforced. | +| `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. diff --git a/docs/schema.md b/docs/schema.md index 51992753a..200e0cc9b 100644 --- a/docs/schema.md +++ b/docs/schema.md @@ -80,6 +80,27 @@ schema 0.6 and 0.7. During the additive schema 0.8 transition, requests may continue to use those legacy fields or use the directional fields above, but cannot mix both formats in one request. +#### Legacy network host-list semantics + +Legacy host lists refine `defaultPolicy`; they do not replace it. Shared +validation rejects a list that cannot refine the selected default before the +backend executes. + +| `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 diff --git a/docs/seatbelt/seatbelt-backend.md b/docs/seatbelt/seatbelt-backend.md index fb2e5584b..47c04df05 100644 --- a/docs/seatbelt/seatbelt-backend.md +++ b/docs/seatbelt/seatbelt-backend.md @@ -343,7 +343,7 @@ rejected.** | `defaultPolicy: "allow"` | `egress.default: "allow"` | Identical profile output | | `allowLocalNetwork: true` | `ingress.default: "allow"` | Identical profile output | | `network.proxy.localhost` / loopback `network.proxy.url` | `runtimeConfig.networkProxy` | | -| `allowedHosts` | *(no equivalent)* | Under `"allow"`: accepted but **ignored** — outbound is already unrestricted, so the list narrows nothing. **Rejected** under `"block"` unless `builtinTestServer` | +| `allowedHosts` | *(no equivalent)* | With `"allow"`: rejected by shared legacy-network validation because an allowlist cannot narrow an allow default. With `"block"`: shared-valid, but Seatbelt rejects it unless `builtinTestServer` supplies the enforcement path | | `blockedHosts` | *(no equivalent)* | **Rejected** always | | *(no equivalent)* | `ingress.hostLoopback` | New in 0.8 — legacy configs never emit a host-loopback rule | diff --git a/sdk/dotnet/Microsoft.Mxc.Sdk/SandboxPolicy.cs b/sdk/dotnet/Microsoft.Mxc.Sdk/SandboxPolicy.cs index b091642b4..bbdb7613e 100644 --- a/sdk/dotnet/Microsoft.Mxc.Sdk/SandboxPolicy.cs +++ b/sdk/dotnet/Microsoft.Mxc.Sdk/SandboxPolicy.cs @@ -199,7 +199,7 @@ private void MarkLegacyField(string field) _legacyFields.Add(field); } - /// Allow outbound network access. + /// Allow outbound by default when no allowlist narrows the policy. [JsonPropertyName("allowOutbound")] public bool AllowOutbound { @@ -215,7 +215,7 @@ public bool AllowLocalNetwork set => AuthoredAllowLocalNetwork = value; } - /// Hosts explicitly allowed. + /// Destinations allowed under a default-block policy. [JsonPropertyName("allowedHosts")] public List AllowedHosts { @@ -228,7 +228,7 @@ public List AllowedHosts } } - /// Hosts explicitly blocked. + /// Destinations denied after allow rules are applied. [JsonPropertyName("blockedHosts")] public List BlockedHosts { diff --git a/sdk/dotnet/README.md b/sdk/dotnet/README.md index 58f44428d..a65d044f3 100644 --- a/sdk/dotnet/README.md +++ b/sdk/dotnet/README.md @@ -57,6 +57,12 @@ catch (MxcException ex) } ``` +For legacy `NetworkPolicy`, a non-empty `AllowedHosts` list selects a block +default even when `AllowOutbound` is true, so the allowlist narrows outbound +access. With no allowlist, `AllowOutbound = true` selects an allow default and +`BlockedHosts` expresses allow-all-except-these. A blocklist without either an +allowlist or `AllowOutbound` is rejected by the native SDK. + `MxcSandbox.RunAsync(policy, command)` offloads the blocking native call to the thread pool. `MxcSandbox.NativeVersion` returns the loaded `mxc_ffi` version. Optional feature outputs are returned through `RunResult.OutputMetadata`; for diff --git a/sdk/node/README.md b/sdk/node/README.md index d74540623..f841c2dc3 100644 --- a/sdk/node/README.md +++ b/sdk/node/README.md @@ -94,6 +94,13 @@ as directional default-deny for egress, ingress, and host loopback. See the [Sandbox Policy 0.8.0 specification](https://github.com/microsoft/mxc/blob/main/docs/sandbox-policy/0.8.0/policy.md) for the complete cross-platform authoring shape. +For legacy `SandboxPolicy` authoring, a non-empty `allowedHosts` list selects a +block default even when `allowOutbound` is true, so the list narrows outbound +access rather than forming the invalid `allow` + allowlist wire combination. +With no allowlist, `allowOutbound: true` selects an allow default and +`blockedHosts` expresses allow-all-except-these. A blocklist without either an +allowlist or `allowOutbound` is rejected. + Model 1 permits direct connections selected by IP/CIDR, protocol, and port rules; it does not configure an application-layer proxy. Model 2 denies direct internet access and supplies a loopback HTTP/S proxy endpoint. Backend-specific diff --git a/sdk/node/src/sandbox.ts b/sdk/node/src/sandbox.ts index 8b4de2e20..4f274e541 100644 --- a/sdk/node/src/sandbox.ts +++ b/sdk/node/src/sandbox.ts @@ -539,11 +539,18 @@ export function createConfigFromPolicy( ); } } - // Unix backends accept host lists without allowOutbound. Bubblewrap and - // LXC enforce them; WSLC does not (per-host filtering is non-functional — - // no in-kernel iptables + no CAP_NET_ADMIN — and is rejected at parse - // time); Seatbelt accepts them for SDK compatibility and leaves its - // limitations to native validation. + if ( + policy.network.blockedHosts?.length && + !policy.network.allowedHosts?.length && + !policy.network.allowOutbound + ) { + throw new Error( + "blockedHosts requires allowedHosts when network.defaultPolicy='block'", + ); + } + // Unix backends accept allowlists without allowOutbound. Bubblewrap and + // LXC enforce them under a block default; WSLC and Seatbelt leave their + // backend-specific limitations to native validation. const acceptsHostRulesWithoutOutbound = containment === 'wslc' || containment === 'seatbelt' || @@ -558,7 +565,10 @@ export function createConfigFromPolicy( } config.network = { - defaultPolicy: policy.network.allowOutbound ? 'allow' : 'block', + defaultPolicy: + policy.network.allowOutbound && !policy.network.allowedHosts?.length + ? 'allow' + : 'block', allowLocalNetwork: policy.network.allowLocalNetwork, allowedHosts: policy.network.allowedHosts, blockedHosts: policy.network.blockedHosts, diff --git a/sdk/node/src/types.ts b/sdk/node/src/types.ts index c1e49f071..3778fbf6b 100644 --- a/sdk/node/src/types.ts +++ b/sdk/node/src/types.ts @@ -478,13 +478,13 @@ export type SandboxPolicy = { }; /** Network access restrictions. All flags default to false (no network access). */ network?: { - /** Whether to allow outbound connections to the Internet. (default: false) Legacy network field. */ + /** Allow outbound by default when no allowlist narrows the policy. (default: false) Legacy network field. */ allowOutbound?: boolean; /** Whether to allow connections to local networks. (default: false) Legacy network field. */ allowLocalNetwork?: boolean; - /** When set, ONLY these outbound hosts are reachable. Requires allowOutbound. Legacy network field. */ + /** Destinations allowed under a default-block policy. Windows ProcessContainer also requires allowOutbound. Legacy network field. */ allowedHosts?: string[]; - /** Hosts to block even when outbound is allowed. Requires allowOutbound. Legacy network field. */ + /** Destinations denied after allow rules. Without allowedHosts, set allowOutbound so the default is allow. Legacy network field. */ blockedHosts?: string[]; /** * Proxy configuration. Routes cooperating HTTP traffic through this proxy. diff --git a/sdk/node/tests/integration/linux-bubblewrap.test.ts b/sdk/node/tests/integration/linux-bubblewrap.test.ts index e1a0505f8..74caf8028 100644 --- a/sdk/node/tests/integration/linux-bubblewrap.test.ts +++ b/sdk/node/tests/integration/linux-bubblewrap.test.ts @@ -165,7 +165,7 @@ describe('Linux Bubblewrap network proxy (schema 0.6.0-alpha)', { 'fi'; config.network = { ...(config.network ?? {}), - defaultPolicy: 'allow', + defaultPolicy: 'block', proxy: { builtinTestServer: true }, allowedHosts: ['pkgs.dev.azure.com'], }; diff --git a/sdk/node/tests/unit/sandbox.test.ts b/sdk/node/tests/unit/sandbox.test.ts index 4960fbca7..141946b35 100644 --- a/sdk/node/tests/unit/sandbox.test.ts +++ b/sdk/node/tests/unit/sandbox.test.ts @@ -921,6 +921,7 @@ describe('createConfigFromPolicy', () => { }); assert.ok(config.processContainer!.capabilities!.includes('internetClient')); assert.ok(config.processContainer!.capabilities!.includes('privateNetworkClientServer')); + assert.strictEqual(config.network!.defaultPolicy, 'block'); assert.deepStrictEqual(config.network!.allowedHosts, ['example.com']); assert.deepStrictEqual(config.network!.blockedHosts, ['evil.com']); } finally { @@ -1130,6 +1131,7 @@ describe('createConfigFromPolicy', () => { // Abstract 'process' on Linux must apply the same iptables firewall // enforcement as explicit 'bubblewrap', because the native binary // resolves the abstract intent to Bubblewrap server-side. + assert.strictEqual(config.network!.defaultPolicy, 'block'); assert.strictEqual(config.network!.enforcementMode, 'firewall'); } finally { restore(); @@ -1202,6 +1204,7 @@ describe('createConfigFromPolicy', () => { 'bubblewrap', ); assert.strictEqual(config.containment, 'bubblewrap'); + assert.strictEqual(config.network!.defaultPolicy, 'block'); assert.strictEqual(config.network!.enforcementMode, undefined); assert.deepStrictEqual(config.network!.proxy, { builtinTestServer: true }); assert.deepStrictEqual(config.network!.allowedHosts, ['example.com']); @@ -1499,7 +1502,7 @@ describe('createConfigFromPolicy', () => { } }; - it('should pass allowedHosts through for Seatbelt compatibility', () => { + it('should preserve a shared-valid allowlist for native Seatbelt validation', () => { mockDarwin(); try { const config = createConfigFromPolicy({ @@ -1516,16 +1519,16 @@ describe('createConfigFromPolicy', () => { } }); - it('should pass blockedHosts through for native Seatbelt validation', () => { + it('should pass blocklist-only outbound policy through for native Seatbelt validation', () => { mockDarwin(); try { const config = createConfigFromPolicy({ version: '0.7.0-alpha', - network: { blockedHosts: ['evil.com'] }, + network: { allowOutbound: true, blockedHosts: ['evil.com'] }, }); assert.strictEqual(config.containment, 'seatbelt'); assert.deepStrictEqual(config.network!.blockedHosts, ['evil.com']); - assert.strictEqual(config.network!.defaultPolicy, 'block'); + assert.strictEqual(config.network!.defaultPolicy, 'allow'); } finally { restore(); } @@ -1838,7 +1841,10 @@ describe('createConfigFromPolicy', () => { version: '0.6.0-alpha', network: { blockedHosts: ['evil.com'] }, }), - { message: /allowedHosts\/blockedHosts require allowOutbound/ }, + { + message: + "blockedHosts requires allowedHosts when network.defaultPolicy='block'", + }, ); } finally { restore(); @@ -1973,6 +1979,7 @@ describe('createConfigFromPolicy', () => { assert.deepStrictEqual(config.filesystem!.readwritePaths, ['/workspace']); assert.deepStrictEqual(config.filesystem!.readonlyPaths, ['/data']); assert.deepStrictEqual(config.filesystem!.deniedPaths, ['/secrets']); + assert.strictEqual(config.network!.defaultPolicy, 'block'); // Per applyLinuxNetworkPolicy, host filtering forces firewall mode. assert.strictEqual(config.network!.enforcementMode, 'firewall'); }); @@ -2018,6 +2025,7 @@ describe('createConfigFromPolicy', () => { network: { allowOutbound: true, allowedHosts: ['example.com'] }, }, 'lxc'); assert.strictEqual(config.containment, 'lxc'); + assert.strictEqual(config.network!.defaultPolicy, 'block'); assert.strictEqual(config.network!.enforcementMode, 'firewall'); }); @@ -2031,6 +2039,19 @@ describe('createConfigFromPolicy', () => { assert.strictEqual(config.network!.defaultPolicy, 'block'); assert.strictEqual(config.network!.enforcementMode, 'firewall'); }); + + it('should reject blockedHosts without an allowlist or outbound default', () => { + assert.throws( + () => createConfigFromPolicy({ + version: '0.6.0-alpha', + network: { blockedHosts: ['evil.com'] }, + }, 'lxc'), + { + message: + "blockedHosts requires allowedHosts when network.defaultPolicy='block'", + }, + ); + }); }); }); diff --git a/src/backends/bubblewrap/common/src/bwrap_runner.rs b/src/backends/bubblewrap/common/src/bwrap_runner.rs index 2eb6a37d6..c7b6575c8 100644 --- a/src/backends/bubblewrap/common/src/bwrap_runner.rs +++ b/src/backends/bubblewrap/common/src/bwrap_runner.rs @@ -2074,23 +2074,45 @@ mod tests { #[test] fn validate_accepts_host_rules_when_a_proxy_enforces_them_at_0_8() { - // The proxy is the mechanism, so the same lists are fine with one. + // The proxy is the mechanism, so a valid allow-default blocklist + // reaches the environmental probe instead of failing policy validation. let mut req = base_request(); req.network_enforcement_compatibility = NetworkEnforcementCompatibility::Strict; + req.policy.default_network_policy = wxc_common::models::NetworkPolicy::Allow; + req.policy.blocked_hosts = vec!["evil.example.com".into()]; + req.policy.network_proxy = ProxyConfig { + address: None, + builtin_test_server: true, + }; + + let unavailable = bwrap_version::BwrapUnavailable::NotFound; + let expected = unavailable.to_string(); + let error = BubblewrapScriptRunner::new() + .validate_prepared_with_probe(&req, || Err(unavailable)) + .unwrap_err(); + assert_eq!(error.error_message, expected); + } + + #[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 = wxc_common::models::NetworkPolicy::Block; req.policy.blocked_hosts = vec!["evil.example.com".into()]; req.policy.network_proxy = ProxyConfig { address: Some(ProxyAddress::new("127.0.0.1".into(), 3128)), builtin_test_server: false, }; - if let Err(err) = BubblewrapScriptRunner::new().validate(&req) { - assert!( - !err.error_message - .contains("require an enforcement mechanism"), - "a proxy enforces the lists: {}", - err.error_message - ); - } + let error = BubblewrapScriptRunner::new() + .validate_prepared_with_probe(&req, || { + panic!("environment probe must not run for an invalid legacy host list") + }) + .unwrap_err(); + assert_eq!( + error.error_message, + "blockedHosts requires allowedHosts when network.defaultPolicy='block'" + ); } #[test] @@ -2101,14 +2123,12 @@ mod tests { req.policy.default_network_policy = wxc_common::models::NetworkPolicy::Block; req.policy.allowed_hosts = vec!["api.github.com".into()]; - if let Err(err) = BubblewrapScriptRunner::new().validate(&req) { - assert!( - !err.error_message - .contains("require an enforcement mechanism"), - "0.7 must not be rejected: {}", - err.error_message - ); - } + let unavailable = bwrap_version::BwrapUnavailable::NotFound; + let expected = unavailable.to_string(); + let error = BubblewrapScriptRunner::new() + .validate_prepared_with_probe(&req, || Err(unavailable)) + .unwrap_err(); + assert_eq!(error.error_message, expected); } #[test] diff --git a/src/backends/hyperlight/common/src/lib.rs b/src/backends/hyperlight/common/src/lib.rs index 714d5025b..a3e97d1b2 100644 --- a/src/backends/hyperlight/common/src/lib.rs +++ b/src/backends/hyperlight/common/src/lib.rs @@ -480,7 +480,10 @@ impl HyperlightScriptRunner { if !request.working_directory.is_empty() { return Err(RunnerError::Preflight(ERR_WORKDIR.to_string())); } - if !request.policy.allowed_hosts.is_empty() && !request.policy.blocked_hosts.is_empty() { + if request.policy.default_network_policy == NetworkPolicy::Block + && !request.policy.allowed_hosts.is_empty() + && !request.policy.blocked_hosts.is_empty() + { return Err(RunnerError::Preflight( "allowedHosts and blockedHosts are mutually exclusive".to_string(), )); @@ -1698,6 +1701,7 @@ mod tests { fn network_policy_blocklist_from_blocked_hosts() { let request = ExecutionRequest { policy: ContainerPolicy { + default_network_policy: NetworkPolicy::Allow, blocked_hosts: vec!["127.0.0.1".to_string()], ..Default::default() }, @@ -1712,6 +1716,41 @@ mod tests { )); } + #[test] + fn policy_rejects_blocklist_without_allowlist_under_block_default() { + let request = ExecutionRequest { + policy: ContainerPolicy { + blocked_hosts: vec!["127.0.0.1".to_string()], + ..Default::default() + }, + ..Default::default() + }; + + let error = runner().validate_runner(&request).unwrap_err(); + assert_eq!( + error.error_message, + "blockedHosts requires allowedHosts when network.defaultPolicy='block'" + ); + } + + #[test] + fn policy_rejects_allowlist_under_allow_default() { + let request = ExecutionRequest { + policy: ContainerPolicy { + default_network_policy: NetworkPolicy::Allow, + allowed_hosts: vec!["127.0.0.1".to_string()], + ..Default::default() + }, + ..Default::default() + }; + + let error = runner().validate_runner(&request).unwrap_err(); + assert_eq!( + error.error_message, + "allowedHosts requires network.defaultPolicy='block'" + ); + } + #[test] fn policy_rejects_allowed_and_blocked_hosts() { let mut r = runner(); diff --git a/src/backends/nanvix/runner/src/lib.rs b/src/backends/nanvix/runner/src/lib.rs index 1491893bf..b35e7f302 100644 --- a/src/backends/nanvix/runner/src/lib.rs +++ b/src/backends/nanvix/runner/src/lib.rs @@ -525,23 +525,11 @@ impl NanVixScriptRunner { } } - /// Compatibility lowering for legacy requests without directional policy. - /// - /// Host networking is enabled when `network.defaultPolicy = "allow"` OR when - /// a per-host allow/block list is present (a list always implies networking, - /// regardless of `defaultPolicy`). When enabled, the runner passes - /// `-allow-host-networking` to nanvixd; per-host lists are additionally - /// forwarded as `-allow-host`/`-block-host` (see [`Self::spawn_nanvixd`]). - fn legacy_host_networking_enabled(request: &ExecutionRequest) -> bool { - request.policy.default_network_policy == NetworkPolicy::Allow - || !request.policy.allowed_hosts.is_empty() - || !request.policy.blocked_hosts.is_empty() - } - fn resolve_networking_mode(request: &ExecutionRequest) -> Result { let policy = &request.policy; if policy.network_egress.is_none() && policy.network_ingress.is_none() { - return Ok(Self::legacy_host_networking_enabled(request)); + return Ok(policy.default_network_policy == NetworkPolicy::Allow + || !policy.allowed_hosts.is_empty()); } if !policy.allowed_hosts.is_empty() || !policy.blocked_hosts.is_empty() @@ -630,10 +618,10 @@ impl NanVixScriptRunner { /// Resolves the request's allow/block host lists, failing closed. /// - /// Returns the resolved allow/block IPv4 lists plus human-readable - /// warnings for any allowlist entries that were dropped. At most one list - /// is non-empty (the mutual-exclusion check in [`Self::validate_policies`] - /// runs first). + /// Returns the resolved allow/block IPv4 lists plus human-readable warnings + /// for any allowlist entries that were dropped. Shared validation rejects + /// invalid default/list combinations before execution, and NanVix rejects + /// simultaneous lists because its guest filter is allow-XOR-block. /// /// Fail-closed semantics differ by list direction: /// - **Allowlist** (deny-by-default): a fully unresolvable allowlist is an @@ -681,11 +669,12 @@ impl NanVixScriptRunner { if !request.policy.denied_paths.is_empty() { return Err(NanVixError::Preflight(ERR_DENIED_PATHS.to_string())); } - // Per-host filtering is supported (forwarded to nanvixd as - // -allow-host/-block-host). The guest egress filter is allow-XOR-block, - // so the two lists are mutually exclusive; defaultPolicy is ignored when - // either list is present. - if !request.policy.allowed_hosts.is_empty() && !request.policy.blocked_hosts.is_empty() { + // NanVix's guest egress filter is allow-XOR-block and cannot represent + // simultaneous allow and block lists, even though the shared policy model can. + if request.policy.default_network_policy == NetworkPolicy::Block + && !request.policy.allowed_hosts.is_empty() + && !request.policy.blocked_hosts.is_empty() + { return Err(NanVixError::Preflight(ERR_NETWORK_HOSTS.to_string())); } if request.policy.network_proxy.is_enabled() { @@ -729,12 +718,11 @@ impl NanVixScriptRunner { cmd.arg("-allow-host-networking"); } - // Per-host egress filtering. The two lists are mutually exclusive - // (validated upstream), so at most one of these loops emits flags. - // nanvixd requires `-allow-host-networking` for these to take effect, - // which is guaranteed because a non-empty list forces host_networking - // on (see `legacy_host_networking_enabled`). The guest daemon auto-exempts the - // DNS port in allowlist mode, so no resolver IPs are added here. + // Per-host egress filtering. The supplied lists have already been + // reduced to the one that refines the shared legacy default. NanVix + // rejects simultaneous lists during validation, so at most one loop + // emits flags. The guest daemon auto-exempts the DNS port in allowlist + // mode, so no resolver IPs are added here. for host in allow_hosts { cmd.arg("-allow-host").arg(host); } @@ -1246,7 +1234,11 @@ mod tests { let error = NanVixScriptRunner::new() .validate_runner(&request) .unwrap_err(); - assert!(error.error_message.contains(ERR_DIRECTIONAL_FILTERS)); + assert!( + error.error_message.contains(ERR_DIRECTIONAL_FILTERS), + "unexpected validation error: {}", + error.error_message + ); assert!(command_arguments(&request, &[], &[]).is_err()); } } @@ -1265,9 +1257,6 @@ mod tests { }; NanVixScriptRunner::new().validate_runner(&request).unwrap(); assert!(NanVixScriptRunner::resolve_networking_mode(&request).unwrap()); - assert!(!NanVixScriptRunner::legacy_host_networking_enabled( - &request - )); let arguments = command_arguments(&request, &[], &[]).unwrap(); assert_eq!( arguments.first().map(String::as_str), @@ -1300,7 +1289,7 @@ mod tests { vec!["-allow-host-networking", "-allow-host", "192.0.2.1"], ), ( - NetworkPolicy::Block, + NetworkPolicy::Allow, vec![], vec!["192.0.2.0/24"], vec!["-allow-host-networking", "-block-host", "192.0.2.0/24"], @@ -1316,12 +1305,9 @@ mod tests { }, ..Default::default() }; - let arguments = command_arguments( - &request, - &request.policy.allowed_hosts, - &request.policy.blocked_hosts, - ) - .unwrap(); + let resolved = NanVixScriptRunner::resolve_host_lists(&request).unwrap(); + let arguments = + command_arguments(&request, &resolved.allow, &resolved.block).unwrap(); let expected: Vec = expected_prefix.into_iter().map(str::to_owned).collect(); assert!( @@ -1400,13 +1386,10 @@ mod tests { NanVixScriptRunner::validate_policies(&request).is_ok(), "a bare allowlist should pass validation" ); - // A list implies host networking regardless of defaultPolicy (Block). - assert!(NanVixScriptRunner::legacy_host_networking_enabled(&request)); } #[test] - fn policy_accepts_blocklist_only() { - // A bare blocklist is now supported (forwarded as -block-host). + fn policy_rejects_blocklist_without_allowlist_under_block_default() { let request = ExecutionRequest { script_code: "echo test".to_string(), policy: ContainerPolicy { @@ -1415,11 +1398,13 @@ mod tests { }, ..Default::default() }; - assert!( - NanVixScriptRunner::validate_policies(&request).is_ok(), - "a bare blocklist should pass validation" + let error = NanVixScriptRunner::new() + .validate_runner(&request) + .unwrap_err(); + assert_eq!( + error.error_message, + "blockedHosts requires allowedHosts when network.defaultPolicy='block'" ); - assert!(NanVixScriptRunner::legacy_host_networking_enabled(&request)); } #[test] @@ -1556,6 +1541,7 @@ mod tests { // be resolved -- silently dropping it would let blocked traffic flow. let request = ExecutionRequest { policy: ContainerPolicy { + default_network_policy: NetworkPolicy::Allow, blocked_hosts: vec!["10.0.0.1".to_string(), "::1".to_string()], ..Default::default() }, @@ -1578,6 +1564,7 @@ mod tests { fn resolve_host_lists_accepts_fully_resolved_blocklist() { let request = ExecutionRequest { policy: ContainerPolicy { + default_network_policy: NetworkPolicy::Allow, blocked_hosts: vec!["10.0.0.1".to_string(), "192.168.0.0/16".to_string()], ..Default::default() }, @@ -1593,9 +1580,7 @@ mod tests { fn default_block_no_lists_disables_host_networking() { // The default posture (block, no lists) keeps networking off. let request = ExecutionRequest::default(); - assert!(!NanVixScriptRunner::legacy_host_networking_enabled( - &request - )); + assert!(!NanVixScriptRunner::resolve_networking_mode(&request).unwrap()); let resolved = NanVixScriptRunner::resolve_host_lists(&request).unwrap(); assert!(resolved.allow.is_empty() && resolved.block.is_empty()); assert!(resolved.warnings.is_empty()); @@ -1614,7 +1599,7 @@ mod tests { }, ..Default::default() }; - assert!(NanVixScriptRunner::legacy_host_networking_enabled(&request)); + assert!(NanVixScriptRunner::resolve_networking_mode(&request).unwrap()); assert!( NanVixScriptRunner::validate_policies(&request).is_ok(), "allow posture without per-host filtering should pass validation" @@ -1655,9 +1640,7 @@ mod tests { script_code: "echo test".to_string(), ..Default::default() }; - assert!(!NanVixScriptRunner::legacy_host_networking_enabled( - &request - )); + assert!(!NanVixScriptRunner::resolve_networking_mode(&request).unwrap()); let mut logger = Logger::new(Mode::Buffer); let resp = runner.run(&request, &mut logger); assert_eq!(resp.exit_code, ERROR_EXIT_CODE); diff --git a/src/backends/seatbelt/common/src/seatbelt_policy.rs b/src/backends/seatbelt/common/src/seatbelt_policy.rs index f83b6b1d0..1031af0e5 100644 --- a/src/backends/seatbelt/common/src/seatbelt_policy.rs +++ b/src/backends/seatbelt/common/src/seatbelt_policy.rs @@ -144,8 +144,8 @@ pub fn validate_seatbelt_network_policy(policy: &ContainerPolicy) -> Result<(), would degrade to allow-all outbound -- the inverse of the \ requested policy. Use 'network.proxy.builtinTestServer: true' \ (testing only) for MXC-enforced host filtering, remove \ - allowedHosts to keep the deny, or use defaultPolicy='allow' if \ - unrestricted egress is intended." + allowedHosts to keep the deny, or remove allowedHosts and use \ + defaultPolicy='allow' if unrestricted egress is intended." .to_string()); } @@ -154,8 +154,8 @@ pub fn validate_seatbelt_network_policy(policy: &ContainerPolicy) -> Result<(), if !policy.blocked_hosts.is_empty() { return Err( "macOS Seatbelt does not support per-host network filtering. \ - 'blockedHosts' cannot be enforced; remove it or use \ - defaultPolicy: \"block\" to deny all network." + 'blockedHosts' cannot be enforced; remove it. To deny all \ + network, use defaultPolicy: \"block\" without host lists." .to_string(), ); } @@ -361,7 +361,9 @@ mod tests { } #[test] - fn accepts_allowed_hosts_with_default_allow() { + fn seatbelt_specific_validation_defers_allow_default_allowlist_to_shared_validation() { + // Shared validation rejects this combination before Seatbelt validation. + // This helper owns only the remaining backend-representability checks. let mut p = policy(); p.default_network_policy = NetworkPolicy::Allow; p.allowed_hosts = vec!["api.github.com".to_string()]; diff --git a/src/backends/seatbelt/common/src/seatbelt_runner.rs b/src/backends/seatbelt/common/src/seatbelt_runner.rs index 52189860b..7f90b4698 100644 --- a/src/backends/seatbelt/common/src/seatbelt_runner.rs +++ b/src/backends/seatbelt/common/src/seatbelt_runner.rs @@ -1096,14 +1096,19 @@ mod tests { } #[test] - fn rejects_blocked_hosts() { + fn rejects_shared_valid_blocked_hosts() { let mut request = base_request(); + request.policy.default_network_policy = NetworkPolicy::Allow; request.policy.blocked_hosts = vec!["evil.example.com".into()]; let runner = SeatbeltScriptRunner::new(); let response = runner.validate(&request).unwrap_err(); assert_eq!(response.exit_code, -1); - assert!(response.error_message.contains("blockedHosts")); - assert!(response.error_message.contains("cannot be enforced")); + assert_eq!( + response.error_message, + "macOS Seatbelt does not support per-host network filtering. \ + 'blockedHosts' cannot be enforced; remove it. To deny all \ + network, use defaultPolicy: \"block\" without host lists." + ); } /// The parser is not a door at all for these rules: `validate` is the only diff --git a/src/core/mxc-sdk/README.md b/src/core/mxc-sdk/README.md index 6e6f01594..17b166fd8 100644 --- a/src/core/mxc-sdk/README.md +++ b/src/core/mxc-sdk/README.md @@ -58,6 +58,12 @@ BaseProcessContainer UI isolation, proxy peer identity, and denial capture. Schema 0.8 directional networking is available through `NetworkSection::{egress, ingress, runtime_config}`. +For legacy networking, a non-empty `NetworkSection::allowed_hosts` list selects +a block default even when `allow_outbound` is true, so the allowlist narrows +outbound access. With no allowlist, `allow_outbound = true` selects an allow +default and `blocked_hosts` expresses allow-all-except-these. A blocklist +without either an allowlist or `allow_outbound` is rejected. + The new ProcessContainer and directional-network configuration types are non-exhaustive so fields can be added compatibly. Construct types whose fields are all optional with `Default`, then assign the settings the request needs. diff --git a/src/core/mxc-sdk/tests/sdk_helpers.rs b/src/core/mxc-sdk/tests/sdk_helpers.rs index 627c852b3..c338f2695 100644 --- a/src/core/mxc-sdk/tests/sdk_helpers.rs +++ b/src/core/mxc-sdk/tests/sdk_helpers.rs @@ -163,6 +163,27 @@ fn build_request_host_rules_require_outbound() { } } +#[test] +fn build_request_blocklist_only_requires_outbound() { + let mut network = mxc_sdk::policy::NetworkSection::default(); + network.blocked_hosts = vec!["198.51.100.10".to_string()]; + + let policy = SandboxPolicy { + version: "0.7.0-alpha".to_string(), + filesystem: None, + network: Some(network), + ui: None, + timeout_ms: None, + }; + + let error = build_request(&policy, "echo hello", None) + .expect_err("blocklist-only requires an allow default"); + assert_eq!( + error.message, + "blockedHosts requires allowedHosts when network.defaultPolicy='block'" + ); +} + #[test] fn rust_sdk_builds_legacy_networking() { use mxc_sdk::policy::NetworkSection; diff --git a/src/core/mxc_engine/src/policy/exact/mod.rs b/src/core/mxc_engine/src/policy/exact/mod.rs index f948a103f..2f51998f5 100644 --- a/src/core/mxc_engine/src/policy/exact/mod.rs +++ b/src/core/mxc_engine/src/policy/exact/mod.rs @@ -10,7 +10,7 @@ use wxc_common::mxc_error::MxcError; use crate::configs::{Lxc, ProcessContainer, Seatbelt}; -use super::network::{select_network_format, NetworkFormat}; +use super::network::{legacy_default_allows, select_network_format, NetworkFormat}; use super::{Containment, NetworkAction, ProxySpec, SandboxPolicy, SandboxRequest}; macro_rules! optional { @@ -77,6 +77,15 @@ fn validate_common( )); } + if !network.blocked_hosts.is_empty() + && network.allowed_hosts.is_empty() + && !network.allow_outbound + { + return Err(error( + "blockedHosts requires allowedHosts when network.defaultPolicy='block'", + )); + } + let accepts_host_rules_without_outbound = match containment { Containment::Process => cfg!(any(target_os = "linux", target_os = "macos")), Containment::ProcessContainer(_) => false, diff --git a/src/core/mxc_engine/src/policy/exact/v0_6.rs b/src/core/mxc_engine/src/policy/exact/v0_6.rs index af0bd3596..5a410f5ea 100644 --- a/src/core/mxc_engine/src/policy/exact/v0_6.rs +++ b/src/core/mxc_engine/src/policy/exact/v0_6.rs @@ -186,7 +186,7 @@ pub(super) fn build(input: &PreparedInput<'_>) -> Result) -> Result) -> Result serde::Deserialize<'de> for ProxySpec { #[derive(Debug, Clone, Default)] #[non_exhaustive] pub struct NetworkSection { + /// Allow outbound by default when no allowlist narrows the policy. pub allow_outbound: bool, pub allow_local_network: bool, + /// Destinations allowed under a default-block legacy policy. pub allowed_hosts: Vec, + /// Destinations denied after legacy allow rules are applied. pub blocked_hosts: Vec, pub proxy: Option, /// Schema 0.8 outbound network policy. @@ -76,6 +79,10 @@ pub struct NetworkSection { pub(crate) legacy_fields_specified: bool, } +pub(super) fn legacy_default_allows(network: &NetworkSection) -> bool { + network.allow_outbound && network.allowed_hosts.is_empty() +} + impl<'de> serde::Deserialize<'de> for NetworkSection { fn deserialize(deserializer: D) -> Result where @@ -392,9 +399,9 @@ pub(super) fn proxy_to_wire(proxy: &ProxySpec) -> serde_json::Value { #[cfg(test)] mod tests { use super::{ - proxy_to_wire, select_network_format, ContractVersion, NetworkAction, NetworkEgressSection, - NetworkFormat, NetworkPeerSection, NetworkPortSection, NetworkRuleSection, NetworkSection, - ProxySpec, RuntimeConfigSection, + legacy_default_allows, proxy_to_wire, select_network_format, ContractVersion, + NetworkAction, NetworkEgressSection, NetworkFormat, NetworkPeerSection, NetworkPortSection, + NetworkRuleSection, NetworkSection, ProxySpec, RuntimeConfigSection, }; #[test] @@ -402,6 +409,30 @@ mod tests { assert_eq!(NetworkAction::default(), NetworkAction::Deny); } + #[test] + fn legacy_allowlist_selects_a_block_default() { + for allow_outbound in [false, true] { + let network = NetworkSection { + allow_outbound, + allowed_hosts: vec!["192.0.2.10".to_string()], + ..Default::default() + }; + + assert!(!legacy_default_allows(&network)); + } + } + + #[test] + fn legacy_blocklist_only_preserves_the_outbound_default() { + let network = NetworkSection { + allow_outbound: true, + blocked_hosts: vec!["198.51.100.10".to_string()], + ..Default::default() + }; + + assert!(legacy_default_allows(&network)); + } + #[test] fn development_rejects_every_legacy_authoring_field_including_neutral_values() { for field in [ diff --git a/src/core/wxc_common/src/validator.rs b/src/core/wxc_common/src/validator.rs index a04b9e4d0..ac838be49 100644 --- a/src/core/wxc_common/src/validator.rs +++ b/src/core/wxc_common/src/validator.rs @@ -58,6 +58,27 @@ impl std::ops::BitOr for NetworkPolicySupport { } } +fn validate_legacy_host_lists(request: &ExecutionRequest) -> Result<(), ScriptResponse> { + let policy = &request.policy; + + if policy.default_network_policy == NetworkPolicy::Block + && policy.allowed_hosts.is_empty() + && !policy.blocked_hosts.is_empty() + { + return Err(ScriptResponse::error( + "blockedHosts requires allowedHosts when network.defaultPolicy='block'", + )); + } + + if policy.default_network_policy == NetworkPolicy::Allow && !policy.allowed_hosts.is_empty() { + return Err(ScriptResponse::error( + "allowedHosts requires network.defaultPolicy='block'", + )); + } + + Ok(()) +} + /// Reject network policy features that the selected backend cannot enforce. pub fn validate_network_policy_support( request: &ExecutionRequest, @@ -156,6 +177,8 @@ pub fn validate_network_policy_support( )); } + validate_legacy_host_lists(request)?; + if !support.contains(NetworkPolicySupport::PROXY_PEER_IDENTITY) && request.policy.allowed_proxy_peer.is_some() { @@ -231,8 +254,8 @@ pub fn validate_exec_common(request: &ExecutionRequest) -> Result<(), MxcError> mod tests { use super::*; use crate::models::{ - ExecutionRequest, NetworkAction, NetworkEgressPolicy, NetworkIngressPolicy, NetworkRule, - ProxyAddress, ProxyConfig, + ContainerPolicy, ExecutionRequest, NetworkAction, NetworkEgressPolicy, + NetworkIngressPolicy, NetworkRule, ProxyAddress, ProxyConfig, }; use crate::mxc_error::MxcErrorCode; @@ -350,6 +373,103 @@ mod tests { assert!(validate_common(&req).is_ok()); } + #[test] + fn network_support_accepts_valid_legacy_host_list_combinations() { + for (default_network_policy, allowed_hosts, blocked_hosts) in [ + (NetworkPolicy::Block, vec![], vec![]), + ( + NetworkPolicy::Block, + vec!["203.0.113.7".to_string()], + vec![], + ), + ( + NetworkPolicy::Block, + vec!["203.0.113.0/24".to_string()], + vec!["203.0.113.7".to_string()], + ), + (NetworkPolicy::Allow, vec![], vec![]), + ( + NetworkPolicy::Allow, + vec![], + vec!["203.0.113.7".to_string()], + ), + ] { + let request = ExecutionRequest { + policy: ContainerPolicy { + default_network_policy, + allowed_hosts, + blocked_hosts, + ..Default::default() + }, + ..Default::default() + }; + + for support in [NetworkPolicySupport::LEGACY, NetworkPolicySupport::ALL] { + assert!( + validate_network_policy_support(&request, support).is_ok(), + "valid legacy host-list policy was rejected for support {support:?}" + ); + } + } + } + + #[test] + fn network_support_rejects_legacy_lists_that_do_not_refine_the_default() { + let cases = [ + ( + NetworkPolicy::Block, + vec![], + vec!["203.0.113.7".to_string()], + "blockedHosts requires allowedHosts when network.defaultPolicy='block'", + ), + ( + NetworkPolicy::Allow, + vec!["203.0.113.7".to_string()], + vec![], + "allowedHosts requires network.defaultPolicy='block'", + ), + ( + NetworkPolicy::Allow, + vec!["203.0.113.7".to_string()], + vec!["203.0.113.8".to_string()], + "allowedHosts requires network.defaultPolicy='block'", + ), + ]; + + for (default_network_policy, allowed_hosts, blocked_hosts, expected) in cases { + let request = ExecutionRequest { + policy: ContainerPolicy { + default_network_policy, + allowed_hosts, + blocked_hosts, + ..Default::default() + }, + ..Default::default() + }; + + for support in [NetworkPolicySupport::LEGACY, NetworkPolicySupport::ALL] { + let error = validate_network_policy_support(&request, support).unwrap_err(); + assert_eq!(error.error_message, expected); + } + } + } + + #[test] + fn network_support_reports_directional_error_before_legacy_host_list_error() { + let mut request = ExecutionRequest::default(); + request.policy.network_mode_specified = true; + request.policy.network_egress = Some(NetworkEgressPolicy::default()); + request.policy.default_network_policy = NetworkPolicy::Allow; + request.policy.allowed_hosts = vec!["203.0.113.7".to_string()]; + + let error = + validate_network_policy_support(&request, NetworkPolicySupport::LEGACY).unwrap_err(); + assert_eq!( + error.error_message, + "network.egress.default is not supported by the selected backend" + ); + } + #[test] fn network_support_rejects_unimplemented_features() { let mut request = ExecutionRequest::default(); diff --git a/tests/configs/bubblewrap_network_firewall_denylist.json b/tests/configs/bubblewrap_network_firewall_denylist.json index 3fc4af860..1d1a35b30 100644 --- a/tests/configs/bubblewrap_network_firewall_denylist.json +++ b/tests/configs/bubblewrap_network_firewall_denylist.json @@ -6,9 +6,9 @@ "commandLine": "bash -c 'set -u; if ! timeout 6 bash -c \"exec 3<>/dev/tcp/10.0.2.2/{{ALLOWED_PORT}}\" >/dev/null 2>&1; then echo OPEN_TERMINAL_UNREACHABLE; exit 1; fi; echo OPEN_TERMINAL_OK; timeout 6 bash -c \"exec 3<>/dev/tcp/1.1.1.1/443\" >/dev/null 2>&1; if [ $? = 0 ]; then echo DENY_OVERRIDDEN_BY_ALLOW; exit 1; fi; echo DENY_WINS_OK'" }, "network": { - "defaultPolicy": "allow", + "defaultPolicy": "block", "enforcementMode": "firewall", - "allowedHosts": ["1.1.1.0/24"], + "allowedHosts": ["10.0.2.2/32", "1.1.1.0/24"], "blockedHosts": ["1.1.1.1/32"] } } diff --git a/tests/configs/bubblewrap_network_proxy_allowlist.json b/tests/configs/bubblewrap_network_proxy_allowlist.json index d780e6f46..b641e9d20 100644 --- a/tests/configs/bubblewrap_network_proxy_allowlist.json +++ b/tests/configs/bubblewrap_network_proxy_allowlist.json @@ -6,7 +6,7 @@ "commandLine": "set -e; curl -fsSL --max-time 15 http://mxc-test.invalid/ | grep -q MXC_TEST_ORIGIN_OK && echo SENTINEL_OK; if curl -fsS --max-time 5 http://mxc-blocked.invalid/ > /dev/null 2>&1; then echo SENTINEL_BAD_LEAK; exit 1; else echo BLOCKED_OK; fi" }, "network": { - "defaultPolicy": "allow", + "defaultPolicy": "block", "proxy": { "builtinTestServer": true }, "allowedHosts": ["mxc-test.invalid"] } diff --git a/tests/configs/lxc_network_cidr_boundary.json b/tests/configs/lxc_network_cidr_boundary.json index babbfc05f..e4fe03d45 100644 --- a/tests/configs/lxc_network_cidr_boundary.json +++ b/tests/configs/lxc_network_cidr_boundary.json @@ -13,7 +13,7 @@ "release": "3.23" }, "network": { - "defaultPolicy": "allow", + "defaultPolicy": "block", "enforcementMode": "firewall", "allowedHosts": [ "0.0.0.0/0", diff --git a/tests/configs/lxc_network_v07_capabilities.json b/tests/configs/lxc_network_v07_capabilities.json index 937cb057f..b192fecf2 100644 --- a/tests/configs/lxc_network_v07_capabilities.json +++ b/tests/configs/lxc_network_v07_capabilities.json @@ -13,7 +13,6 @@ "release": "3.23" }, "network": { - "defaultPolicy": "block", - "blockedHosts": ["example.com"] + "defaultPolicy": "block" } } diff --git a/tests/scripts/run_lxc_network_cidr_boundary_test.sh b/tests/scripts/run_lxc_network_cidr_boundary_test.sh index 5b71200ed..b351944c2 100644 --- a/tests/scripts/run_lxc_network_cidr_boundary_test.sh +++ b/tests/scripts/run_lxc_network_cidr_boundary_test.sh @@ -196,11 +196,11 @@ assert_programmed_rule iptables "198.51.100.42/32" DROP assert_programmed_rule ip6tables "2001:db8::5" DROP assert_programmed_rule ip6tables "2001:db8::5/128" DROP -if ! echo "$OUTPUT" | grep -q "Default network policy: ACCEPT"; then - fail "default-allow policy was not applied." +if ! echo "$OUTPUT" | grep -q "Default network policy: DROP"; then + fail "default-deny policy was not applied." fi -if echo "$OUTPUT" | grep -q "Default network policy: DROP"; then - fail "default-deny policy was applied unexpectedly." +if echo "$OUTPUT" | grep -q "Default network policy: ACCEPT"; then + fail "default-allow policy was applied unexpectedly." fi # The v6 half is required by roadmap item 19 / AB#62830559; skipping it would be a dual-stack bypass.