Back to Microsandbox

DNS

docs/networking/dns.mdx

0.4.67.4 KB
Original Source

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:

  • block lookups by domain or suffix,
  • pin which upstream resolvers get used,
  • tune the query timeout,
  • power domain-based policy rules (by mapping responses back to IPs).

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.

DNS as egress

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):

  • Name-based rules (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.
  • IP-based rules mostly can't match — a query has no resolved IP yet — with one exception: 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.

Blocking domains

Domain blocking lives in the network policy as deny Domain(...) / deny DomainSuffix(...) rules. The interceptor enforces them at three layers:

  1. DNS resolution — denied names get a local REFUSED response; the upstream resolver never sees them.
  2. TLS first-flight — when a guest hardcodes an IP and opens a TLS connection, the SNI is checked against the same rules.
  3. TCP egress (cache fallback) — for non-TLS traffic, the resolved-hostname cache disambiguates by IP.

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.

<CodeGroup> ```rust Rust let policy = NetworkPolicy::builder() .default_allow() .egress(|e| e .deny_domains(["malware.example.com"]) .deny_domain_suffixes([".tracking.com"])) .build()?;

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();
python
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",),
    ),
)
bash
msb create python --name safe-agent \
  --deny-domain malware.example.com \
  --deny-domain-suffix .tracking.com
</CodeGroup>

Pinning nameservers

By default the sandbox picks up the host's resolver list automatically:

  • macOS: reads State:/Network/Global/DNS from the system configuration store, with /etc/resolv.conf as a fallback when the store isn't available.
  • Linux: reads /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.

<CodeGroup> ```rust Rust use microsandbox_network::dns::Nameserver;

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();
python
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,
        ),
    ),
)
bash
msb create python --name safe-agent \
  --dns-nameserver 1.1.1.1 \
  --dns-nameserver 1.0.0.1 \
  --dns-query-timeout-ms 3000
</CodeGroup>

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.

DNS over alternative transports

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):

  • DNS over UDP (UDP/53)
  • DNS over TCP (TCP/53)
  • DNS over TLS (DoT, TCP/853): requires TLS interception

Refused (guest's stub falls back to plain DNS):

  • DNS over QUIC (DoQ, UDP/853)
  • mDNS (UDP/5353), LLMNR (UDP/5355), NetBIOS-NS (UDP/137)

Not distinguishable from regular traffic (you must filter via network policy):

  • DNS over HTTPS (DoH, TCP/443)

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.

Domain-based policy rules

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.

See also

  • Security model: rebinding protection and DNS-to-IP binding (TOCTOU defense)
  • TLS interception: domains that get intercepted also go through TLS inspection