docs/sdk/rust/networking.mdx
See Networking for conceptual overview and TLS Interception for TLS proxy details.
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.
| Field | Type | Description |
|---|---|---|
| default_egress | Action | Action when no egress-applicable rule matches |
| default_ingress | Action | Action when no ingress-applicable rule matches |
| rules | Vec<Rule> | Ordered list of rules; first match wins |
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.
.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 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.
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.
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.
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.
fn default_allow(self) -> Self
Set both default_egress and default_ingress to Allow.
fn default_deny(self) -> Self
Set both default_egress and default_ingress to Deny.
fn default_egress(self, action: Action) -> Self
Per-direction override for the egress default action.
Parameters
| Name | Type | Description |
|---|---|---|
| action | Action | Default action for egress |
fn default_ingress(self, action: Action) -> Self
Per-direction override for the ingress default action.
Parameters
| Name | Type | Description |
|---|---|---|
| action | Action | Default action for ingress |
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.
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.
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.
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).
Last-write-wins. ICMP rule-adders are egress-only at build time.
fn any(&mut self) -> &mut Self
Set direction to Any for subsequent rule-adders. Rules committed after this apply in both directions.
fn egress(&mut self) -> &mut Self
Set direction to Egress for subsequent rule-adders.
fn ingress(&mut self) -> &mut Self
Set direction to Ingress for subsequent rule-adders.
Protocols accumulate as a set; duplicates dedupe.
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.
fn icmpv6(&mut self) -> &mut Self
Add Icmpv6 to the protocols set. Egress-only; same rules as icmpv4().
fn tcp(&mut self) -> &mut Self
Add Tcp to the protocols set.
fn udp(&mut self) -> &mut Self
Add Udp to the protocols set.
Ports accumulate as a set; duplicates dedupe. Always guest-side (egress destination port / ingress listening port).
fn port(&mut self, port: u16) -> &mut Self
Add a single port to the ports set.
Parameters
| Name | Type | Description |
|---|---|---|
| port | u16 | Port number |
fn port_range(&mut self, lo: u16, hi: u16) -> &mut Self
Add an inclusive port range.
Parameters
| Name | Type | Description |
|---|---|---|
| lo | u16 | Lower bound (inclusive) |
| hi | u16 | Upper bound (inclusive). lo > hi records BuildError::InvalidPortRange |
fn ports<I: IntoIterator<Item = u16>>(&mut self, ports: I) -> &mut Self
Add multiple single ports. Equivalent to calling port() once per element.
Each adder commits one rule using the current state and the named destination group.
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.
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.
fn allow_meta(&mut self) -> &mut Self
Allow the Metadata group (169.254.169.254). Dangerous on cloud hosts (exposes IAM credentials).
fn allow_multicast(&mut self) -> &mut Self
Allow the Multicast group (224.0.0.0/4, ff00::/8).
fn allow_private(&mut self) -> &mut Self
Allow the Private group (RFC1918 + ULA + CGN).
fn allow_public(&mut self) -> &mut Self
Allow the Public group (complement of named categories: every IP not in any other group).
fn deny_host(&mut self) -> &mut Self
Deny the Host group.
fn deny_link_local(&mut self) -> &mut Self
Deny the LinkLocal group.
fn deny_loopback(&mut self) -> &mut Self
Deny the Loopback group.
fn deny_meta(&mut self) -> &mut Self
Deny the Metadata group.
fn deny_multicast(&mut self) -> &mut Self
Deny the Multicast group.
fn deny_private(&mut self) -> &mut Self
Deny the Private group.
fn deny_public(&mut self) -> &mut Self
Deny the Public group.
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()?
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.
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.
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.
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.
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.
fn deny_local(&mut self) -> &mut Self
Add three deny rules atomically: Loopback + LinkLocal + Host. Metadata is intentionally not included.
.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]).
fn allow(&mut self) -> ExplicitRuleBuilder<'_>
Begin an explicit-destination rule with action Allow.
fn deny(&mut self) -> ExplicitRuleBuilder<'_>
Begin an explicit-destination rule with action Deny.
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).
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)
)
)
fn max_connections(self, max: usize) -> Self
Limit the maximum number of concurrent network connections from the sandbox.
fn on_secret_violation(self, action: ViolationAction) -> Self
Set the action taken when a secret placeholder is detected in traffic destined for a host not in the secret's allow list. See ViolationAction.
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()?))
fn tls(self, f: impl FnOnce(TlsBuilder) -> TlsBuilder) -> Self
Configure TLS interception. See TlsBuilder.
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.
Builder for DNS interception settings. Used in NetworkBuilder::dns(|d| d...). Owns rebind protection, nameserver pinning, and the per-query timeout.
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>()?).
fn query_timeout_ms(self, ms: u64) -> Self
Set the per-DNS-query timeout in milliseconds. Default: 5000.
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.
Builder for TLS interception settings. Used in NetworkBuilder::tls(|t| t...).
fn block_quic(self, block: bool) -> Self
Block QUIC/HTTP3 on intercepted ports, forcing TCP/TLS fallback. Default: true.
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.
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.
fn intercept_ca_key(self, path: impl Into<PathBuf>) -> Self
PEM file used as the intercepting CA's private key.
fn intercepted_ports(self, ports: Vec<u16>) -> Self
TCP ports where TLS interception is active. Default: [443].
fn upstream_ca_cert(self, path: impl Into<PathBuf>) -> Self
PEM file with extra root CAs the proxy should trust when verifying upstream servers.
fn verify_upstream(self, verify: bool) -> Self
Whether the proxy verifies upstream server certificates. Default: true. Set to false only for self-signed servers.
| Value | Wire format | Description |
|---|---|---|
Allow | "allow" | Permit the traffic |
Deny | "deny" | Drop the traffic silently |
| Value | Wire format | Description |
|---|---|---|
Egress | "egress" | Traffic leaving the sandbox |
Ingress | "ingress" | Traffic entering the sandbox (via published ports) |
Any | "any" | Rule applies in either direction |
| Variant | Description |
|---|---|
Any | Match 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 |
| Value | Wire format | Matches |
|---|---|---|
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 |
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.
| Value | Wire 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.
struct PortRange {
start: u16, // inclusive
end: u16, // inclusive
}
| Method | Description |
|---|---|
PortRange::single(port) | Match a single port |
PortRange::range(start, end) | Match an inclusive range |
A single network policy rule.
| Field | Type | Description |
|---|---|---|
| direction | Direction | Which evaluator considers this rule |
| destination | Destination | Target filter (egress destination / ingress source) |
| protocols | Vec<Protocol> | Set semantics; empty = any protocol |
| ports | Vec<PortRange> | Set semantics; empty = any port. Always guest-side (egress destination port / ingress listening port) |
| action | Action | What to do on match |
Convenience constructors:
| Method | Description |
|---|---|
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 |
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.
| Method | Description |
|---|---|
.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 |
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).
| Variant | Cause |
|---|---|
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).
Action taken when a secret placeholder is sent to a disallowed host.
| Value | Description |
|---|---|
Block | Silently drop the request. The guest sees a connection reset. This is the default. |
BlockAndLog | Drop the request and emit a warning log on the host side. |
BlockAndTerminate | Drop the request, log an error, and shut down the entire sandbox. |