Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.microsandbox.dev/llms.txt

Use this file to discover all available pages before exploring further.

See Networking for conceptual overview and TLS Interception for TLS proxy details.

NetworkPolicy

A list of rules plus two per-direction defaults, evaluated first-match-wins. Build one with NetworkPolicy::builder():
use microsandbox::NetworkPolicy;

let policy = NetworkPolicy::builder()
    .default_deny()
    .egress(|e| e.tcp().port(443).allow_public().allow_private())
    .rule(|r| r.any().deny().ip("198.51.100.5"))
    .build()?;
The default policy denies egress except for an implicit allow public, and allows ingress with no rules. See the defaults rationale for the asymmetry.
FieldTypeDescription
default_egressActionAction when no egress-applicable rule matches
default_ingressActionAction when no ingress-applicable rule matches
rulesVec<Rule>Ordered list of rules; first match wins

Rule order matters

The first matching rule wins, so a broad rule placed before a narrow one swallows it:
NetworkPolicy::builder()
    .default_deny()
    .egress(|e| e
        .allow().cidr("10.0.0.0/8")     // matches everything in 10.x
        .deny().ip("10.0.0.5"))          // never reached
    .build()?;
Put specific rules before general ones.

Shadow detection

.build() walks the rules and warns (via tracing::warn!) when a rule is fully covered by an earlier one in the same direction. Only Ip, Cidr, and Group destinations are checked; domain coverage depends on runtime DNS and is skipped. Builds still succeed:
WARN rule #1 (Egress Cidr(10.0.0.5/32) Deny) is shadowed by rule #0 (Egress Cidr(10.0.0.0/8) Allow); to narrow, place the more specific rule first

State accumulation

State setters (.tcp(), .port(), etc.) inside a closure carry into every rule-adder that follows. State is not reset between adders:
.rule(|r| r.egress()
    .tcp().port(443).allow_public()    // rule 1: egress, TCP, 443, allow Public
    .udp().allow_private())            // rule 2: egress, [TCP, UDP], 443, allow Private
Use separate closures for rules that need different state.

NetworkPolicy::builder()

The fluent builder is the primary construction path. String inputs (.ip(&str), .cidr(&str), .domain(&str), .domain_suffix(&str)) are stored raw and parsed at .build() time, so the chain stays clean. The first parse / validation failure surfaces as BuildError. The closure signature for .rule() / .egress() / .ingress() / .any() is FnOnce(&mut RuleBuilder) -> &mut RuleBuilder. A chain ending in any rule-adder (.allow_public(), .deny().ip(...), etc.) returns the builder reference and satisfies the bound. Multi-statement bodies end with an explicit r return.

any()

fn any<F>(self, f: F) -> Self
where
    F: for<'a> FnOnce(&'a mut RuleBuilder) -> &'a mut RuleBuilder
Sugar for rule() with direction pre-set to Any. Rules committed inside apply in both directions.

build()

fn build(self) -> Result<NetworkPolicy, BuildError>
Consume the builder and produce a NetworkPolicy. Lazy-parses every .ip() / .cidr() / .domain() / .domain_suffix() input, validates direction-set and ICMP-egress-only invariants, and emits a tracing::warn! for each shadowed rule pair detected.

default_allow()

fn default_allow(self) -> Self
Set both default_egress and default_ingress to Allow.

default_deny()

fn default_deny(self) -> Self
Set both default_egress and default_ingress to Deny.

default_egress()

fn default_egress(self, action: Action) -> Self
Per-direction override for the egress default action. Parameters
NameTypeDescription
actionActionDefault action for egress

default_ingress()

fn default_ingress(self, action: Action) -> Self
Per-direction override for the ingress default action. Parameters
NameTypeDescription
actionActionDefault action for ingress

egress()

fn egress<F>(self, f: F) -> Self
where
    F: for<'a> FnOnce(&'a mut RuleBuilder) -> &'a mut RuleBuilder
Sugar for rule() with direction pre-set to Egress.

ingress()

fn ingress<F>(self, f: F) -> Self
where
    F: for<'a> FnOnce(&'a mut RuleBuilder) -> &'a mut RuleBuilder
Sugar for rule() with direction pre-set to Ingress.

rule()

fn rule<F>(self, f: F) -> Self
where
    F: for<'a> FnOnce(&'a mut RuleBuilder) -> &'a mut RuleBuilder
Open a multi-rule batch closure. Direction must be set inside via .egress(), .ingress(), or .any() before any rule-adder.

RuleBuilder

The closure passed to .rule(...) (or any of the direction sugar methods) gives you a RuleBuilder. State setters and rule-adders interleave freely. State accumulates eagerly across the closure (see State accumulation).

Direction setters

Last-write-wins. ICMP rule-adders are egress-only at build time.

any()

fn any(&mut self) -> &mut Self
Set direction to Any for subsequent rule-adders. Rules committed after this apply in both directions.

egress()

fn egress(&mut self) -> &mut Self
Set direction to Egress for subsequent rule-adders.

ingress()

fn ingress(&mut self) -> &mut Self
Set direction to Ingress for subsequent rule-adders.

Protocol setters

Protocols accumulate as a set; duplicates dedupe.

icmpv4()

fn icmpv4(&mut self) -> &mut Self
Add Icmpv4 to the protocols set. Egress-only; an ICMP rule on an Ingress or Any direction fails build with BuildError::IngressDoesNotSupportIcmp.

icmpv6()

fn icmpv6(&mut self) -> &mut Self
Add Icmpv6 to the protocols set. Egress-only; same rules as icmpv4().

tcp()

fn tcp(&mut self) -> &mut Self
Add Tcp to the protocols set.

udp()

fn udp(&mut self) -> &mut Self
Add Udp to the protocols set.

Port setters

Ports accumulate as a set; duplicates dedupe. Always guest-side (egress destination port / ingress listening port).

port()

fn port(&mut self, port: u16) -> &mut Self
Add a single port to the ports set. Parameters
NameTypeDescription
portu16Port number

port_range()

fn port_range(&mut self, lo: u16, hi: u16) -> &mut Self
Add an inclusive port range. Parameters
NameTypeDescription
lou16Lower bound (inclusive)
hiu16Upper bound (inclusive). lo > hi records BuildError::InvalidPortRange

ports()

fn ports<I: IntoIterator<Item = u16>>(&mut self, ports: I) -> &mut Self
Add multiple single ports. Equivalent to calling port() once per element.

Group rule-adders

Each adder commits one rule using the current state and the named destination group.

allow_host()

fn allow_host(&mut self) -> &mut Self
Allow the Host group: per-sandbox gateway IPs that back host.microsandbox.internal. This is the right shortcut for “let the sandbox reach my host’s localhost”, not allow_loopback().
fn allow_link_local(&mut self) -> &mut Self
Allow the LinkLocal group (169.254.0.0/16, fe80::/10). Excludes the metadata IP 169.254.169.254.

allow_loopback()

fn allow_loopback(&mut self) -> &mut Self
Allow the Loopback group (127.0.0.0/8, ::1). The guest’s own loopback, not the host. To reach a service on the host’s localhost, use allow_host() instead. See the loopback-vs-host watch-out.

allow_meta()

fn allow_meta(&mut self) -> &mut Self
Allow the Metadata group (169.254.169.254). Dangerous on cloud hosts (exposes IAM credentials).

allow_multicast()

fn allow_multicast(&mut self) -> &mut Self
Allow the Multicast group (224.0.0.0/4, ff00::/8).

allow_private()

fn allow_private(&mut self) -> &mut Self
Allow the Private group (RFC1918 + ULA + CGN).

allow_public()

fn allow_public(&mut self) -> &mut Self
Allow the Public group (complement of named categories: every IP not in any other group).

deny_host()

fn deny_host(&mut self) -> &mut Self
Deny the Host group.
fn deny_link_local(&mut self) -> &mut Self
Deny the LinkLocal group.

deny_loopback()

fn deny_loopback(&mut self) -> &mut Self
Deny the Loopback group.

deny_meta()

fn deny_meta(&mut self) -> &mut Self
Deny the Metadata group.

deny_multicast()

fn deny_multicast(&mut self) -> &mut Self
Deny the Multicast group.

deny_private()

fn deny_private(&mut self) -> &mut Self
Deny the Private group.

deny_public()

fn deny_public(&mut self) -> &mut Self
Deny the Public group.

Bulk-domain rule-adders

Each call adds one rule per name, inheriting the current direction / protocol / port state. Lazy-parse: invalid names surface as BuildError::InvalidDomain from .build().
NetworkPolicy::builder()
    .default_allow()
    .egress(|e| e
        .deny_domains(["evil.com", "tracker.example"])
        .deny_domain_suffixes([".ads.example", ".doubleclick.net"]))
    .build()?

allow_domain_suffixes()

fn allow_domain_suffixes<I, S>(&mut self, suffixes: I) -> &mut Self
where I: IntoIterator<Item = S>, S: Into<String>
Add one Destination::DomainSuffix allow rule per suffix.

allow_domains()

fn allow_domains<I, S>(&mut self, names: I) -> &mut Self
where I: IntoIterator<Item = S>, S: Into<String>
Add one Destination::Domain allow rule per name.

deny_domain_suffixes()

fn deny_domain_suffixes<I, S>(&mut self, suffixes: I) -> &mut Self
where I: IntoIterator<Item = S>, S: Into<String>
Add one Destination::DomainSuffix deny rule per suffix.

deny_domains()

fn deny_domains<I, S>(&mut self, names: I) -> &mut Self
where I: IntoIterator<Item = S>, S: Into<String>
Add one Destination::Domain deny rule per name.

Composite rule-adders


allow_local()

fn allow_local(&mut self) -> &mut Self
Add three allow rules atomically: Loopback + LinkLocal + Host. Each uses the closure’s current state. Metadata is intentionally not included; opt in via allow_meta() separately.

deny_local()

fn deny_local(&mut self) -> &mut Self
Add three deny rules atomically: Loopback + LinkLocal + Host. Metadata is intentionally not included.

Explicit-destination rule-adders

.allow() / .deny() open an ExplicitRuleBuilder that requires a destination call to commit.
.rule(|r| r.egress().tcp().port(443).allow().domain("api.example.com"))
.rule(|r| r.any().deny().cidr("198.51.100.0/24"))
Dropping without a destination call adds no rule (the type is #[must_use]).

allow()

fn allow(&mut self) -> ExplicitRuleBuilder<'_>
Begin an explicit-destination rule with action Allow.

deny()

fn deny(&mut self) -> ExplicitRuleBuilder<'_>
Begin an explicit-destination rule with action Deny.

NetworkBuilder

Builder for configuring the sandbox’s network stack. Used in SandboxBuilder::network(|n| n...). Errors accumulated by nested builders cascade up; the outermost SandboxBuilder::build() surfaces them as MicrosandboxError::NetworkBuilder(BuildError).

dns()

fn dns(self, f: impl FnOnce(DnsBuilder) -> DnsBuilder) -> Self
Configure DNS interception. See DnsBuilder.
.network(|n| n
    .dns(|d| d
        .nameservers(["1.1.1.1".parse::<Nameserver>()?])
        .query_timeout_ms(3000)
    )
)

max_connections()

fn max_connections(self, max: usize) -> Self
Limit the maximum number of concurrent network connections from the sandbox.

ipv4_pool()

fn ipv4_pool(self, pool: Ipv4Network) -> Self
Set the IPv4 pool used to derive per-sandbox /30 guest subnets. Defaults to 172.16.0.0/12.

ipv6_pool()

fn ipv6_pool(self, pool: Ipv6Network) -> Self
Set the IPv6 pool used to derive per-sandbox /64 guest prefixes. Defaults to fd42:6d73:62::/48.

on_secret_violation()

fn on_secret_violation(
    self,
    f: impl FnOnce(ViolationActionBuilder) -> ViolationActionBuilder,
) -> Self
Set the action taken when a secret placeholder is detected in traffic destined for a host not in the secret’s allow list. The builder can configure a blocking action or passthrough hosts:
.network(|n| n.on_secret_violation(|v| {
    v.block_and_log()
     .passthrough_host("api.anthropic.com")
     .passthrough_host_pattern("*.example.com")
}))
Passthrough hosts receive the placeholder unchanged. They do not receive real secret values.

ViolationActionBuilder

Builder for secret violation behavior. Used by NetworkBuilder::on_secret_violation() and SecretBuilder::on_violation(...).
MethodDescription
block()Block the request silently
block_and_log()Block the request and emit a warning log
block_and_terminate()Block the request and terminate the sandbox
passthrough_host(host)Allow placeholders to pass through unchanged to an exact host
passthrough_host_pattern(pattern)Allow placeholders to pass through unchanged to matching wildcard hosts
passthrough_all_hosts(true)Allow placeholders to pass through unchanged to any host
Passthrough host calls accumulate. When passthrough hosts are configured, non-matching hosts use the default secret violation action.

policy()

fn policy(self, policy: NetworkPolicy) -> Self
Set the network access policy. Pass a builder-constructed NetworkPolicy:
.network(|n| n.policy(NetworkPolicy::builder().default_deny().build()?))

tls()

fn tls(self, f: impl FnOnce(TlsBuilder) -> TlsBuilder) -> Self
Configure TLS interception. See TlsBuilder.

trust_host_cas()

fn trust_host_cas(self, enabled: bool) -> Self
Whether to ship the host’s trusted root CAs into the guest at boot. Default: false. Opt in when egress HTTPS inside the sandbox needs to work behind corporate MITM proxies (Cloudflare Warp Zero Trust, Zscaler, Netskope, etc.). These proxies install a gateway CA on the host that’s unknown to the guest’s stock Mozilla bundle.

DnsBuilder

Builder for DNS interception settings. Used in NetworkBuilder::dns(|d| d...). Owns rebind protection, nameserver pinning, and the per-query timeout.

nameservers()

fn nameservers<I>(self, nameservers: I) -> Self
where
    I: IntoIterator,
    I::Item: Into<Nameserver>
Set the upstream nameservers to forward DNS queries to. Replaces any previously-set nameservers. Each element is convertible into Nameserver (SocketAddr, IpAddr, or a parsed string via "dns.google:53".parse::<Nameserver>()?).

query_timeout_ms()

fn query_timeout_ms(self, ms: u64) -> Self
Set the per-DNS-query timeout in milliseconds. Default: 5000.

rebind_protection()

fn rebind_protection(self, enabled: bool) -> Self
When enabled, DNS responses that resolve to private IP addresses are blocked. Prevents DNS rebinding attacks. Default: true.

TlsBuilder

Builder for TLS interception settings. Used in NetworkBuilder::tls(|t| t...).

block_quic()

fn block_quic(self, block: bool) -> Self
Block QUIC/HTTP3 on intercepted ports, forcing TCP/TLS fallback. Default: true.

bypass()

fn bypass(self, pattern: impl Into<String>) -> Self
Skip TLS interception for hosts matching this glob (e.g. "*.internal.corp"). Use for domains with certificate pinning.

intercept_ca_cert()

fn intercept_ca_cert(self, path: impl Into<PathBuf>) -> Self
PEM file used as the intercepting CA’s certificate. Pair with intercept_ca_key() to provide a stable CA across sandbox restarts.

intercept_ca_key()

fn intercept_ca_key(self, path: impl Into<PathBuf>) -> Self
PEM file used as the intercepting CA’s private key.

intercepted_ports()

fn intercepted_ports(self, ports: Vec<u16>) -> Self
TCP ports where TLS interception is active. Default: [443].

upstream_ca_cert()

fn upstream_ca_cert(self, path: impl Into<PathBuf>) -> Self
PEM file with extra root CAs the proxy should trust when verifying upstream servers.

verify_upstream()

fn verify_upstream(self, verify: bool) -> Self
Whether the proxy verifies upstream server certificates. Default: true. Set to false only for self-signed servers.

Types

Action

ValueWire formatDescription
Allow"allow"Permit the traffic
Deny"deny"Drop the traffic silently

Direction

ValueWire formatDescription
Egress"egress"Traffic leaving the sandbox
Ingress"ingress"Traffic entering the sandbox (via published ports)
Any"any"Rule applies in either direction

Destination

VariantDescription
AnyMatch any address
Cidr(IpNetwork)Match a CIDR range (e.g. 10.0.0.0/8); single IPs are stored as /32 or /128
Domain(DomainName)Match an exact domain (e.g. example.com)
DomainSuffix(DomainName)Match the apex domain and every subdomain (e.g. example.com and api.example.com)
Group(DestinationGroup)Match a predefined address group

DestinationGroup

ValueWire formatMatches
Public"public"Complement of the other categories: every address not in any other group
Private"private"10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 100.64.0.0/10, fc00::/7
Loopback"loopback"127.0.0.0/8, ::1 (the guest’s own loopback, not the host)
LinkLocal"link_local"169.254.0.0/16, fe80::/10 (excludes metadata)
Metadata"metadata"Cloud metadata endpoints (169.254.169.254)
Multicast"multicast"224.0.0.0/4, ff00::/8
Host"host"Per-sandbox gateway IPs that back host.microsandbox.internal

DomainName

A validated DNS name. Construction goes through str::parse (or TryFrom<String>), which delegates to hickory_proto::rr::Name and canonicalizes the input (lowercased ASCII, leading and trailing dots stripped) so rule matching is a byte-wise compare against the DNS cache. Invalid inputs return a DomainNameError.
use microsandbox_network::policy::{Destination, DomainName};

let exact: DomainName = "PyPI.Org.".parse()?;     // -> "pypi.org"
let suffix: DomainName = ".example.com".parse()?;  // -> "example.com"
Labels follow the permissive DNS grammar (RFC 2181 §11), so underscore-prefixed names like _service._tcp.example.com are accepted. The builder methods (.domain(&str), .domain_suffix(&str)) take strings and parse them lazily at .build(); callers don’t need to construct DomainName directly.

Protocol

ValueWire format
Tcp"tcp"
Udp"udp"
Icmpv4"icmpv4"
Icmpv6"icmpv6"
ICMP protocols are egress-only. A rule with direction Ingress or Any carrying an ICMP protocol fails build with BuildError::IngressDoesNotSupportIcmp.

PortRange

struct PortRange {
    start: u16,  // inclusive
    end: u16,    // inclusive
}
MethodDescription
PortRange::single(port)Match a single port
PortRange::range(start, end)Match an inclusive range

Rule

A single network policy rule.
FieldTypeDescription
directionDirectionWhich evaluator considers this rule
destinationDestinationTarget filter (egress destination / ingress source)
protocolsVec<Protocol>Set semantics; empty = any protocol
portsVec<PortRange>Set semantics; empty = any port. Always guest-side (egress destination port / ingress listening port)
actionActionWhat to do on match
Convenience constructors:
MethodDescription
Rule::allow_egress(destination)Allow rule, direction Egress
Rule::deny_egress(destination)Deny rule, direction Egress
Rule::allow_ingress(destination)Allow rule, direction Ingress
Rule::deny_ingress(destination)Deny rule, direction Ingress
Rule::allow_any(destination)Allow rule, direction Any
Rule::deny_any(destination)Deny rule, direction Any
Rule::allow_dns()Allow plain DNS (UDP/53 + TCP/53) to the gateway forwarder (Group::Host). The standard one-liner for opening DNS under a deny-by-default policy. See DNS as egress

ExplicitRuleBuilder

Returned by RuleBuilder::allow() / ::deny(). Requires exactly one destination method call to commit the rule. The type is #[must_use]; dropping without a call adds no rule.
MethodDescription
.ip(impl Into<String>)Commit with Destination::Cidr of the IP as /32 or /128
.cidr(impl Into<String>)Commit with Destination::Cidr
.domain(impl Into<String>)Commit with Destination::Domain
.domain_suffix(impl Into<String>)Commit with Destination::DomainSuffix
.group(DestinationGroup)Commit with Destination::Group
.any()Commit with Destination::Any

BuildError

Errors surfaced by the builders’ .build() methods. The same enum covers NetworkPolicy::builder(), DnsBuilder, and NetworkBuilder (the network and DNS builders accumulate errors lazily; the first failure surfaces from the outermost .build() in the chain).
VariantCause
DirectionNotSet { rule_index }A rule was committed without .egress() / .ingress() / .any()
MissingDestination { rule_index }.allow() or .deny() was called but no destination method followed
InvalidIp { rule_index, raw }.ip(&str) got an unparseable value
InvalidCidr { rule_index, raw }.cidr(&str) got an unparseable value
InvalidDomain { rule_index, raw, source }.domain or .domain_suffix got a value that failed DomainName parse
InvalidPortRange { rule_index, lo, hi }.port_range(lo, hi) had lo > hi
IngressDoesNotSupportIcmp { rule_index }ICMP protocol on a non-egress rule
Inside SandboxBuilder::build(), BuildError is wrapped as MicrosandboxError::NetworkBuilder(BuildError).

ViolationAction

Action taken when a secret placeholder is sent to a disallowed host.
ValueDescription
BlockSilently drop the request. The guest sees a connection reset. This is the default.
BlockAndLogDrop the request and emit a warning log on the host side.
BlockAndTerminateDrop the request, log an error, and shut down the entire sandbox.
Passthrough(Vec<HostPattern>)Forward matching hosts with the placeholder unchanged. Non-matching hosts use the default secret violation action.