Back to Microsandbox

Networking

docs/sdk/rust/networking.mdx

0.4.416.0 KB
Original Source

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

NetworkPolicy

A network access policy consisting of two per-direction defaults and an ordered list of rules evaluated first-match-wins per direction.

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

The default NetworkPolicy::default() is default_egress = Deny + implicit allow@public, default_ingress = Allow + no rules. This preserves today's public_only egress behavior plus today's unfiltered published-port behavior. See the defaults rationale for why the defaults are asymmetric.

Presets

Static methods that return pre-configured policies. Most callers should use the builder; presets exist as compat shims for the legacy API.

rust
NetworkPolicy::allow_all()    // unrestricted
NetworkPolicy::none()         // deny both directions
NetworkPolicy::public_only()  // egress=Deny + allow Public; ingress=Allow (today's default)
NetworkPolicy::non_local()    // egress=Deny + allow Public+Private; ingress=Allow

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; the chain stays clean. The first parse / validation failure surfaces as BuildError.

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()?;

Top-level methods

MethodEffect
.default_allow()Set both default_egress and default_ingress to Allow
.default_deny()Set both to Deny
.default_egress(Action)Per-direction override
.default_ingress(Action)Per-direction override
.rule(|r| ...)Multi-rule batch; direction set inside via .egress() / .ingress() / .any()
.egress(|e| ...)Sugar for .rule(|r| { r.egress(); ... })
.ingress(|i| ...)Sugar with direction pre-set to Ingress
.any(|a| ...)Sugar with direction pre-set to Any (rule applies in either direction)
.build()Result<NetworkPolicy, BuildError>

The closure signature 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.

Inside the closure: RuleBuilder

Inside .rule(|r| ...) (or any of the direction sub-builders), state setters and rule-adders interleave freely. State accumulates eagerly across the closure; each rule-adder commits a rule using the state at that point.

State setters:

MethodEffect
.egress() / .ingress() / .any()Set direction. Last-write-wins. .any() makes rules apply in both directions
.tcp() / .udp() / .icmpv4() / .icmpv6()Add to the protocols set (set semantics; duplicates dedupe). ICMP is egress-only
.port(u16)Add a single port to the ports set
.port_range(u16, u16)Add an inclusive range. lo > hi records BuildError::InvalidPortRange

Per-category rule-adders (each commits one rule using the current state):

MethodGroup
.allow_public() / .deny_public()Group::Public (complement of named categories)
.allow_private() / .deny_private()Group::Private (RFC1918 + ULA + CGN)
.allow_loopback() / .deny_loopback()Group::Loopback (127.0.0.0/8, ::1) — see the loopback-vs-host watch-out
.allow_link_local() / .deny_link_local()Group::LinkLocal (169.254.0.0/16, fe80::/10, excluding metadata)
.allow_meta() / .deny_meta()Group::Metadata (169.254.169.254). Dangerous on cloud hosts
.allow_multicast() / .deny_multicast()Group::Multicast
.allow_host() / .deny_host()Group::Host (per-sandbox gateway IPs that back host.microsandbox.internal)

Bulk-domain shortcuts (one rule per name, inheriting current direction/protocol/port state):

MethodEffect
<a id="deny_domains"></a>.allow_domains([..]) / .deny_domains([..])One Destination::Domain rule per name. Lazy-parse: invalid names surface as BuildError::InvalidDomain from .build()
<a id="deny_domain_suffixes"></a>.allow_domain_suffixes([..]) / .deny_domain_suffixes([..])One Destination::DomainSuffix rule per suffix. Same lazy-parse contract
rust
NetworkPolicy::builder()
    .default_allow()
    .egress(|e| e
        .deny_domains(["evil.com", "tracker.example"])
        .deny_domain_suffixes([".ads.example", ".doubleclick.net"]))
    .build()?

Composite sugar:

MethodEffect
.allow_local() / .deny_local()Adds three rules atomically: Loopback + LinkLocal + Host. Metadata is explicitly not included (cloud-IMDS opt-in only via .allow_meta())

Explicit-rule sub-builder (.allow() / .deny() open it):

rust
.rule(|r| r.egress().tcp().port(443).allow().domain("api.example.com"))
.rule(|r| r.any().deny().cidr("198.51.100.0/24"))

Returns an ExplicitRuleBuilder requiring a destination call (.ip / .cidr / .domain / .domain_suffix / .group / .any) to commit. Dropping without a destination call adds no rule (the type is #[must_use]).

State accumulation

State is not reset between rule-adders within a closure. Callers wanting different state per rule use separate .rule() calls or interleave state setters 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
                                       //   (UDP added; TCP and 443 still in state)

Shadow detection

At .build() time, the builder walks the rules list and emits a tracing::warn! for any rule whose match set is fully contained in an earlier rule's match set in a compatible direction. Coverage is Ip / Cidr / Group destinations; domain shadowing is intentionally out of scope (depends on runtime DNS state).


NetworkBuilder

Builder for configuring the sandbox's network stack. Used in SandboxBuilder::network(|n| n...).

policy()

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

Set the network access policy. Override the default with a builder-constructed value:

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

dns()

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

Configure DNS interception. Errors accumulated by DnsBuilder cascade up; the outermost SandboxBuilder::build() surfaces them as MicrosandboxError::NetworkBuilder(BuildError).

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.

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.) whose gateway CA is installed on the host but unknown to the guest's stock Mozilla bundle.

tls()

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

Configure TLS interception. See TlsBuilder.

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.


DnsBuilder

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

rebind_protection()

rust
fn rebind_protection(self, enabled: bool) -> Self

When enabled, DNS responses that resolve to private IP addresses are blocked. This prevents DNS rebinding attacks. Default: true.

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.


TlsBuilder

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

MethodEffect
.bypass(pattern)Skip TLS interception for this domain. Supports *.suffix wildcards
.intercepted_ports(ports)TCP ports where interception is active. Default: [443]
.intercept_ca_cert(path) / .intercept_ca_key(path)Provide a stable CA across sandbox restarts
.upstream_ca_cert(path)Trust an additional CA when verifying upstream servers
.verify_upstream(bool)Whether the proxy verifies upstream server certs. Default: true
.block_quic(bool)Block QUIC/HTTP3 on intercepted ports. Default: true

Types

Action

ValueDescription
AllowPermit the traffic
DenyDrop the traffic silently

Wire format: "allow" / "deny".

Direction

ValueDescription
EgressTraffic leaving the sandbox
IngressTraffic entering the sandbox (via published ports)
AnyRule applies in either direction

Wire format: "egress" / "ingress" / "any".

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

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.

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