fix: unblock private DNS in macOS sandbox (#17370)

## Summary
- keep hostname targets proxied by default by removing hostname suffixes
from the managed `NO_PROXY` value while preserving private/link-local
CIDRs
- make the macOS `allow_local_binding` sandbox rules match the local
socket shape used by DNS tools by allowing wildcard local binds
- allow raw DNS egress to remote port 53 only when `allow_local_binding`
is enabled, without opening blanket outbound network access

## Root cause
Raw DNS tools do not honor `HTTP_PROXY` or `ALL_PROXY`, so the
proxy-only Seatbelt policy blocked their resolver traffic before it
could reach host DNS. In the affected managed config,
`allow_local_binding = true`, but the existing rule only allowed
`localhost:*` binds; `dig`/BIND can bind sockets in a way that needs
wildcard local binding. Separately, hostname suffixes in `NO_PROXY`
could force internal hostnames to resolve locally instead of through the
proxy path.

---------

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
viyatb-oai
2026-04-10 20:34:04 -07:00
committed by GitHub
co-authored by Codex
parent 66e13efd9c
commit 8a474a6561
3 changed files with 66 additions and 8 deletions
+6 -2
View File
@@ -249,11 +249,15 @@ fn dynamic_network_policy_for_network(
if should_use_restricted_network_policy {
let mut policy = String::new();
if proxy.allow_local_binding {
policy.push_str("; allow loopback local binding and loopback traffic\n");
policy.push_str("(allow network-bind (local ip \"localhost:*\"))\n");
policy.push_str("; allow local binding and loopback traffic\n");
policy.push_str("(allow network-bind (local ip \"*:*\"))\n");
policy.push_str("(allow network-inbound (local ip \"localhost:*\"))\n");
policy.push_str("(allow network-outbound (remote ip \"localhost:*\"))\n");
}
if proxy.allow_local_binding && !proxy.ports.is_empty() {
policy.push_str("; allow DNS lookups while application traffic remains proxy-routed\n");
policy.push_str("(allow network-outbound (remote ip \"*:53\"))\n");
}
for port in &proxy.ports {
policy.push_str(&format!(
"(allow network-outbound (remote ip \"localhost:{port}\"))\n"
+48 -3
View File
@@ -99,13 +99,17 @@ fn create_seatbelt_args_routes_network_through_proxy_ports() {
"policy should not include blanket outbound allowance when proxy ports are present:\n{policy}"
);
assert!(
!policy.contains("(allow network-bind (local ip \"localhost:*\"))"),
"policy should not allow loopback binding unless explicitly enabled:\n{policy}"
!policy.contains("(allow network-bind (local ip \"*:*\"))"),
"policy should not allow local binding unless explicitly enabled:\n{policy}"
);
assert!(
!policy.contains("(allow network-inbound (local ip \"localhost:*\"))"),
"policy should not allow loopback inbound unless explicitly enabled:\n{policy}"
);
assert!(
!policy.contains("(allow network-outbound (remote ip \"*:53\"))"),
"policy should not allow raw DNS unless local binding is explicitly enabled:\n{policy}"
);
}
#[test]
@@ -290,7 +294,7 @@ fn create_seatbelt_args_allows_local_binding_when_explicitly_enabled() {
);
assert!(
policy.contains("(allow network-bind (local ip \"localhost:*\"))"),
policy.contains("(allow network-bind (local ip \"*:*\"))"),
"policy should allow loopback local binding when explicitly enabled:\n{policy}"
);
assert!(
@@ -301,6 +305,10 @@ fn create_seatbelt_args_allows_local_binding_when_explicitly_enabled() {
policy.contains("(allow network-outbound (remote ip \"localhost:*\"))"),
"policy should allow loopback outbound when explicitly enabled:\n{policy}"
);
assert!(
policy.contains("(allow network-outbound (remote ip \"*:53\"))"),
"policy should allow DNS egress when local binding is explicitly enabled:\n{policy}"
);
assert!(
!policy.contains("\n(allow network-outbound)\n"),
"policy should keep proxy-routed behavior without blanket outbound allowance:\n{policy}"
@@ -338,6 +346,39 @@ fn dynamic_network_policy_preserves_restricted_policy_when_proxy_config_without_
!policy.contains("(allow network-outbound (remote ip \"localhost:"),
"policy should not include proxy port allowance when proxy config is present without ports:\n{policy}"
);
assert!(
!policy.contains("(allow network-outbound (remote ip \"*:53\"))"),
"policy should stay fail-closed for DNS when no proxy ports are available:\n{policy}"
);
}
#[test]
fn dynamic_network_policy_blocks_dns_when_local_binding_has_no_proxy_ports() {
let policy = dynamic_network_policy(
&SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
read_only_access: Default::default(),
network_access: true,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
},
/*enforce_managed_network*/ false,
&ProxyPolicyInputs {
ports: vec![],
has_proxy_config: true,
allow_local_binding: true,
..ProxyPolicyInputs::default()
},
);
assert!(
policy.contains("(allow network-bind (local ip \"*:*\"))"),
"policy should still allow explicitly configured local binding:\n{policy}"
);
assert!(
!policy.contains("(allow network-outbound (remote ip \"*:53\"))"),
"policy should not allow DNS egress when no proxy ports are available:\n{policy}"
);
}
#[test]
@@ -367,6 +408,10 @@ fn dynamic_network_policy_preserves_restricted_policy_for_managed_network_withou
!policy.contains("\n(allow network-outbound)\n"),
"policy should not include blanket outbound allowance when managed network is active without proxy endpoints:\n{policy}"
);
assert!(
!policy.contains("(allow network-outbound (remote ip \"*:53\"))"),
"policy should stay fail-closed for DNS when no proxy endpoints are available:\n{policy}"
);
}
#[test]