docs/networking/dns.mdx
DNS queries from the sandbox are intercepted on the host. Rather than letting the guest talk to a resolver directly, microsandbox proxies each query, which lets you:
The knobs below cover the day-to-day controls. For the rebinding and TOCTOU protections that also ride on the interceptor, see the security model.
A DNS query is itself an egress action that the policy evaluates before the forwarder will answer. The walk runs once per query, against the transport the guest used to send it (UDP/53, TCP/53, or DoT TCP/853), and returns the action of the first matching rule (or default_egress if nothing matches):
Domain, DomainSuffix) match the query name directly and ignore their protocol/port filter, so a Domain allow rule grants the lookup regardless of which port the rule was scoped to.Any rules match the DNS transport's actual protocol and port. An Any udp/53 allow is the standard way to grant plain DNS broadly.Group::Host matches because it names the gateway forwarder the query is delivered to. A Group::Host udp/53 + tcp/53 allow rule is the narrow way to open DNS while still denying the guest from aiming queries at arbitrary resolver IPs.Under a deny-by-default policy whose rules are all IP-based (Cidr / non-Host Group), nothing matches at DNS-decision time and every query is refused. The default public_only and non_local presets ship with an explicit Group::Host udp/53 + tcp/53 allow rule (Rule::allow_dns() in Rust, Rule.allowDns() in TypeScript, Rule.allow_dns() in Python) so DNS works under those policies; custom deny-by-default policies need an equivalent allow.
DoT (TCP/853) is a separate port; allow_dns() does not include it. Pair an Any tcp/853 allow (or Group::Host tcp/853 allow) with TLS interception if you need DoT.
Domain blocking lives in the network policy as deny Domain(...) / deny DomainSuffix(...) rules. The interceptor enforces them at three layers:
REFUSED response; the upstream resolver never sees them.The bulk-deny shortcuts in each SDK are convenience for deny Domain / deny DomainSuffix rules; they prepend onto whatever policy is set so the deny takes precedence over later allow rules.
let sb = Sandbox::builder("safe-agent") .image("python") .network(|n| n.policy(policy)) .create() .await?;
```typescript TypeScript
import { Sandbox } from "microsandbox";
await using sb = await Sandbox.builder("safe-agent")
.image("python")
.network({
denyDomains: ["malware.example.com"],
denyDomainSuffixes: [".tracking.com"],
})
.create();
from microsandbox import Network, Sandbox
sb = await Sandbox.create(
"safe-agent",
image="python",
network=Network(
deny_domains=("malware.example.com",),
deny_domain_suffixes=(".tracking.com",),
),
)
msb create python --name safe-agent \
--deny-domain malware.example.com \
--deny-domain-suffix .tracking.com
By default the sandbox picks up the host's resolver list automatically:
State:/Network/Global/DNS from the system configuration store, with /etc/resolv.conf as a fallback when the store isn't available./etc/resolv.conf directly.You can override this by setting nameservers to pin the exact resolvers you want (e.g. 1.1.1.1, dns.google), which is useful when auto-discovery picks the wrong ones.
Values can be plain IPs, IP:PORT, hostnames, or HOST:PORT. Hostnames are resolved once at sandbox startup using the host's OS resolver.
let sb = Sandbox::builder("safe-agent") .image("python") .network(|n| n .dns(|d| d .nameservers([ "1.1.1.1".parse::<Nameserver>()?, "1.0.0.1".parse::<Nameserver>()?, ]) .query_timeout_ms(3000) ) ) .create() .await?;
```typescript TypeScript
await using sb = await Sandbox.builder("safe-agent")
.image("python")
.network((n) => n.dns((d) =>
d.nameservers(["1.1.1.1", "1.0.0.1"])
.queryTimeoutMs(3000),
))
.create();
sb = await Sandbox.create(
"safe-agent",
image="python",
network=Network(
dns=DnsConfig(
nameservers=("1.1.1.1", "1.0.0.1"),
query_timeout_ms=3000,
),
),
)
msb create python --name safe-agent \
--dns-nameserver 1.1.1.1 \
--dns-nameserver 1.0.0.1 \
--dns-query-timeout-ms 3000
If the guest aims a query at a specific resolver (dig @1.1.1.1, an /etc/resolv.conf entry inside the guest, or a library call that takes a resolver IP), the interceptor forwards it to that resolver directly. The pinned defaults aren't used in this case. The network policy still applies: the query only goes through if the destination IP is allowed.
The block list and rebind protection only apply to queries the gateway can see. A guest that routes DNS through an encrypted or non-DNS protocol bypasses both unless the gateway intercepts or refuses it.
Intercepted (block list and rebind protection apply):
Refused (guest's stub falls back to plain DNS):
Not distinguishable from regular traffic (you must filter via network policy):
For strict DNS integrity, combine DoT interception with a network policy denying egress UDP/53, TCP/53, and TCP/853 to anything except the gateway.
Network policy rules can match a destination by exact domain or by suffix. The interceptor watches DNS responses as they flow back to the guest and keeps track of which IP addresses belong to which domain.
A connection to 93.184.216.34 is only allowed by a rule targeting example.com if that IP actually came back as an answer to a lookup for example.com from this sandbox.
Because of that, domain rules only take effect on connections that are preceded by a DNS lookup from inside the sandbox. An application that connects to a hard-coded IP it didn't resolve through the interceptor won't match any domain rule.