Back to Microsandbox

Networking

docs/sdk/rust/networking.mdx

0.4.522.2 KB
Original Source

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

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

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

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

rust
.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()

rust
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()

rust
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()

rust
fn default_allow(self) -> Self

Set both default_egress and default_ingress to Allow.


default_deny()

rust
fn default_deny(self) -> Self

Set both default_egress and default_ingress to Deny.


default_egress()

rust
fn default_egress(self, action: Action) -> Self

Per-direction override for the egress default action.

Parameters

NameTypeDescription
actionActionDefault action for egress

default_ingress()

rust
fn default_ingress(self, action: Action) -> Self

Per-direction override for the ingress default action.

Parameters

NameTypeDescription
actionActionDefault action for ingress

egress()

rust
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()

rust
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()

rust
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()

rust
fn any(&mut self) -> &mut Self

Set direction to Any for subsequent rule-adders. Rules committed after this apply in both directions.


egress()

rust
fn egress(&mut self) -> &mut Self

Set direction to Egress for subsequent rule-adders.


ingress()

rust
fn ingress(&mut self) -> &mut Self

Set direction to Ingress for subsequent rule-adders.


Protocol setters

Protocols accumulate as a set; duplicates dedupe.


icmpv4()

rust
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()

rust
fn icmpv6(&mut self) -> &mut Self

Add Icmpv6 to the protocols set. Egress-only; same rules as icmpv4().


tcp()

rust
fn tcp(&mut self) -> &mut Self

Add Tcp to the protocols set.


udp()

rust
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()

rust
fn port(&mut self, port: u16) -> &mut Self

Add a single port to the ports set.

Parameters

NameTypeDescription
portu16Port number

port_range()

rust
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()

rust
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()

rust
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().


allow_link_local()

rust
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()

rust
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()

rust
fn allow_meta(&mut self) -> &mut Self

Allow the Metadata group (169.254.169.254). Dangerous on cloud hosts (exposes IAM credentials).


allow_multicast()

rust
fn allow_multicast(&mut self) -> &mut Self

Allow the Multicast group (224.0.0.0/4, ff00::/8).


allow_private()

rust
fn allow_private(&mut self) -> &mut Self

Allow the Private group (RFC1918 + ULA + CGN).


allow_public()

rust
fn allow_public(&mut self) -> &mut Self

Allow the Public group (complement of named categories: every IP not in any other group).


deny_host()

rust
fn deny_host(&mut self) -> &mut Self

Deny the Host group.


deny_link_local()

rust
fn deny_link_local(&mut self) -> &mut Self

Deny the LinkLocal group.


deny_loopback()

rust
fn deny_loopback(&mut self) -> &mut Self

Deny the Loopback group.


deny_meta()

rust
fn deny_meta(&mut self) -> &mut Self

Deny the Metadata group.


deny_multicast()

rust
fn deny_multicast(&mut self) -> &mut Self

Deny the Multicast group.


deny_private()

rust
fn deny_private(&mut self) -> &mut Self

Deny the Private group.


deny_public()

rust
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().

rust
NetworkPolicy::builder()
    .default_allow()
    .egress(|e| e
        .deny_domains(["evil.com", "tracker.example"])
        .deny_domain_suffixes([".ads.example", ".doubleclick.net"]))
    .build()?

allow_domain_suffixes()

rust
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()

rust
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()

rust
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()

rust
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()

rust
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()

rust
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.

rust
.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()

rust
fn allow(&mut self) -> ExplicitRuleBuilder<'_>

Begin an explicit-destination rule with action Allow.


deny()

rust
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()

rust
fn dns(self, f: impl FnOnce(DnsBuilder) -> DnsBuilder) -> Self

Configure DNS interception. See DnsBuilder.

rust
.network(|n| n
    .dns(|d| d
        .nameservers(["1.1.1.1".parse::<Nameserver>()?])
        .query_timeout_ms(3000)
    )
)

max_connections()

rust
fn max_connections(self, max: usize) -> Self

Limit the maximum number of concurrent network connections from the sandbox.


on_secret_violation()

rust
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.


policy()

rust
fn policy(self, policy: NetworkPolicy) -> Self

Set the network access policy. Pass a builder-constructed NetworkPolicy:

rust
.network(|n| n.policy(NetworkPolicy::builder().default_deny().build()?))

tls()

rust
fn tls(self, f: impl FnOnce(TlsBuilder) -> TlsBuilder) -> Self

Configure TLS interception. See TlsBuilder.


trust_host_cas()

rust
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()

rust
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()

rust
fn query_timeout_ms(self, ms: u64) -> Self

Set the per-DNS-query timeout in milliseconds. Default: 5000.


rebind_protection()

rust
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()

rust
fn block_quic(self, block: bool) -> Self

Block QUIC/HTTP3 on intercepted ports, forcing TCP/TLS fallback. Default: true.


bypass()

rust
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()

rust
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()

rust
fn intercept_ca_key(self, path: impl Into<PathBuf>) -> Self

PEM file used as the intercepting CA's private key.


intercepted_ports()

rust
fn intercepted_ports(self, ports: Vec<u16>) -> Self

TCP ports where TLS interception is active. Default: [443].


upstream_ca_cert()

rust
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()

rust
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.

rust
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

rust
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

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.