Back to Microsandbox

Overview

docs/networking/overview.mdx

0.6.911.5 KB
Original Source

All sandbox traffic flows through a host-controlled networking stack. From inside the VM it looks like a normal network interface; from the host side, every packet is checked against policy before it leaves.

Defaults

By default, sandboxes can reach the public internet but cannot reach private networks, loopback, link-local addresses, or cloud metadata endpoints. Only published ports accept inbound traffic.

To block network access:

<CodeGroup> ```rust Rust let sb = Sandbox::builder("isolated") .image("python") .network(|n| n.enabled(false)) .create() .await?; ```
typescript
await using sb = await Sandbox.builder("isolated")
    .image("python")
    .disableNetwork()
    .create();
python
from microsandbox import Network, Sandbox

sb = await Sandbox.create("isolated", image="python", network=Network.none())
go
sb, err := m.CreateSandbox(ctx, "isolated",
    m.WithImage("python"),
    m.WithNetwork(m.NetworkPolicy.None()),
)
bash
msb create python --name isolated --no-net
</CodeGroup>

The Rust and TypeScript examples disable the network device. Python Network.none(), Go NetworkPolicy.None(), and CLI --no-net retain the device but deny traffic in both directions through policy.

Deployment profiles

single-tenant is the default and preserves the network configuration requested by the sandbox. multi-tenant adds a host-runtime isolation floor: traffic must pass both the platform public-network policy and the sandbox's policy, DNS rebinding protection is forced on, custom DNS servers and interface overrides are removed, host CA import and published ports are disabled, and connection limits are capped. Sandbox policy can further restrict this floor but cannot broaden it.

<CodeGroup> ```rust Rust use microsandbox::sandbox::DeploymentProfile;

let sb = Sandbox::builder("shared-worker") .image("python") .deployment_profile(DeploymentProfile::MultiTenant) .create() .await?;


```typescript TypeScript
await using sb = await Sandbox.builder("shared-worker")
    .image("python")
    .deploymentProfile("multi-tenant")
    .create();
python
from microsandbox import DeploymentProfile, Sandbox

sb = await Sandbox.create(
    "shared-worker",
    image="python",
    deployment_profile=DeploymentProfile.MULTI_TENANT,
)
go
sb, err := m.CreateSandbox(ctx, "shared-worker",
    m.WithImage("python"),
    m.WithDeploymentProfile(m.DeploymentProfileMultiTenant),
)
bash
msb create python --name shared-worker --deployment-profile multi-tenant
</CodeGroup>

An operator can set an authoritative profile with the top-level deployment_profile field in ~/.microsandbox/config.json or programmatically on LocalBackend. That operator choice overrides the sandbox request on create and restart. Managed cloud create requests intentionally do not carry a deployment profile—the hosting driver selects it.

High-level profiles

For common access shapes, compose high-level profiles. Every non-empty profile set automatically adds narrow DNS access through the sandbox gateway.

<CodeGroup> ```rust Rust let policy = NetworkPolicy::from_profiles([ NetworkProfile::Public, NetworkProfile::Private, ]); ```
typescript
const policy = NetworkPolicy.fromProfiles(["public", "private"]);
python
network = Network.from_profiles(NetworkProfile.PUBLIC, NetworkProfile.PRIVATE)
go
network := m.NetworkPolicy.FromProfiles(
    m.NetworkProfilePublic,
    m.NetworkProfilePrivate,
)
bash
msb run alpine --net "public,private"
</CodeGroup>

The composable profiles are public, private, and host. none and all remain terminal whole-policy choices rather than profiles. Duplicate profiles are ignored, generated rules use a stable order, and explicit low-level rules can be placed before generated profile rules to override them.

Low-level custom policies

A policy has two defaults and an ordered list of rules. The first matching rule wins.

text
default_egress  : allow | deny
default_ingress : allow | deny
rules           : first match wins

For example, this creates a deny-by-default sandbox that can make HTTPS requests to the public internet and DNS requests through the host gateway:

<CodeGroup> ```rust Rust let policy = NetworkPolicy::builder() .default_deny() .egress(|e| e.tcp().port(443).allow_public()) .egress(|e| e.udp().tcp().port(53).allow_host()) .build()?;

let sb = Sandbox::builder("restricted-worker") .image("alpine") .network(|n| n.policy(policy)) .create() .await?;


```typescript TypeScript
import { NetworkPolicy, Sandbox } from "microsandbox";

await using sb = await Sandbox.builder("restricted-worker")
    .image("alpine")
    .network((n) => n.policy(
        NetworkPolicy.builder()
            .defaultDeny()
            .egress((e) => e.tcp().port(443).allowPublic())
            .egress((e) => e.udp().tcp().port(53).allowHost())
            .build(),
    ))
    .create();
python
from microsandbox import Action, DestGroup, Destination, Network, NetworkPolicy, Protocol, Rule, Sandbox

sb = await Sandbox.create(
    "restricted-worker",
    image="alpine",
    network=Network(policy=NetworkPolicy(
        default_egress=Action.DENY,
        rules=(
            Rule.allow(destination=Destination.group(DestGroup.PUBLIC), protocol=Protocol.TCP, port=443),
            *Rule.allow_dns(),
        ),
    )),
)
go
sb, err := m.CreateSandbox(ctx, "restricted-worker",
    m.WithImage("alpine"),
    m.WithNetwork(&m.NetworkConfig{
        DefaultEgress: m.PolicyActionDeny,
        Rules: []m.PolicyRule{
            {Action: m.PolicyActionAllow, Direction: m.PolicyDirectionEgress, Destination: "public", Protocol: m.PolicyProtocolTCP, Port: "443"},
            m.Rule.AllowDNS(),
        },
    }),
)
bash
msb create alpine --name restricted-worker \
  --net-default-egress deny \
  --net-rule "allow@public:tcp:443,allow@dns"
</CodeGroup>

Rules can target groups like public, private, and host, or specific IPs, CIDRs, domains, and port ranges. See the CLI reference or your language's SDK networking reference for exact syntax.

Port mapping

<Tooltip tip="Publishing host ports is not available on microsandbox cloud; there is no local host to publish to."><span className="msb-badge-local">Local-only <Icon icon="circle-info" size={11} /></span></Tooltip>

Publish a guest port when a service inside the sandbox should be reachable from the host. Published ports bind to 127.0.0.1 by default.

<CodeGroup> ```rust Rust let sb = Sandbox::builder("api") .image("python") .port(8080, 80) .create() .await?; ```
typescript
await using sb = await Sandbox.builder("api")
    .image("python")
    .port(8080, 80)
    .create();
python
from microsandbox import PortBinding, Sandbox

sb = await Sandbox.create("api", image="python", ports=[PortBinding.tcp(8080, 80)])
go
sb, err := m.CreateSandbox(ctx, "api",
    m.WithImage("python"),
    m.WithPorts(map[uint16]uint16{8080: 80}),
)
bash
msb create python --name api -p 8080:80
</CodeGroup>

Use an explicit bind address, such as 0.0.0.0, only when you intentionally want to listen beyond localhost. -p 8080:80 and SDK helpers like .port(8080, 80) bind to 127.0.0.1; -p 127.0.0.1:8080:80 is the same local-only shape. -p 0.0.0.0:8080:80 or a specific LAN interface address makes the host listener reachable outside the machine, subject to your OS firewall and network policy.

On Windows, the first published port may trigger a Windows Defender Firewall prompt for msb.exe because the runtime opens a host listening socket. For local development, keep the bind address on 127.0.0.1. Only allow private/public network access in the firewall prompt when you intentionally bind a published port beyond loopback.

Rate limits

Sandboxes are unlimited by default. Optional per-sandbox rate limiters cap outbound (egress) and inbound (ingress) traffic independently. Each limiter has two token buckets: a bandwidth bucket measured in bytes, and an ops bucket measured in packets. Buckets start full, refill continuously, and can carry a one-time startup burst that never refills. Limits are set at creation and take effect on the next sandbox start.

<CodeGroup> ```rust Rust use std::time::Duration; use microsandbox::size::SizeExt;

let sb = Sandbox::builder("throttled") .image("python") .network(|n| n.rate_limiter(|r| r.egress(|r| r .bandwidth(1.mib(), Duration::from_secs(1)) .bandwidth_burst(512.kib()) .ops(1_000, Duration::from_secs(1))))) .create() .await?;


```typescript TypeScript
await using sb = await Sandbox.builder("throttled")
    .image("python")
    .network((n) => n.rateLimiter((r) => r.egress((r) => r
        .bandwidth(1_048_576, 1_000)
        .bandwidthBurst(524_288)
        .ops(1_000, 1_000))))
    .create();
python
from microsandbox import Network, NetworkRateLimiter, RateLimiter, Sandbox, TokenBucket

sb = await Sandbox.create(
    "throttled",
    image="python",
    network=Network(
        rate_limiter=NetworkRateLimiter(
            egress=RateLimiter(
                bandwidth=TokenBucket(size=1_048_576, refill_time_ms=1_000, one_time_burst=524_288),
                ops=TokenBucket(size=1_000, refill_time_ms=1_000),
            ),
        ),
    ),
)
go
sb, err := m.CreateSandbox(ctx, "throttled",
    m.WithImage("python"),
    m.WithNetwork(&m.NetworkConfig{
        RateLimiter: &m.NetworkRateLimiterConfig{
            Egress: &m.RateLimiterConfig{
                Bandwidth: &m.TokenBucketConfig{Size: 1 << 20, RefillTime: time.Second, OneTimeBurst: 512 << 10},
                Ops:       &m.TokenBucketConfig{Size: 1000, RefillTime: time.Second},
            },
        },
    }),
)
bash
msb create python --name throttled \
  --net-egress-bandwidth 1M/1s \
  --net-egress-bandwidth-burst 512K \
  --net-egress-ops 1000/1s
</CodeGroup>

The --net-ingress-* flags mirror the egress flags for inbound traffic (--net-ingress-bandwidth, --net-ingress-bandwidth-burst, --net-ingress-ops, --net-ingress-ops-burst). Sizes accept raw bytes plus K, M, and G suffixes; the interval defaults to one second when omitted. A frame larger than the bandwidth bucket is delivered once and the limiter then pauses long enough to pay it off, so oversized packets are throttled instead of stuck.

Reaching the host

<Tooltip tip="The host group is backend-relative; on microsandbox cloud it does not refer to the machine running the SDK or CLI, so expose local services through a reachable network endpoint."><span className="msb-badge-limited">Limited on cloud <Icon icon="circle-info" size={11} /></span></Tooltip>

From inside the sandbox, host.microsandbox.internal resolves to the host machine. The default policy denies host access, so allow the host group when a sandbox needs to call a dev server, database, or other local service.

bash
msb create python --name devbox --net "public,host"

loopback means the sandbox's own 127.0.0.1, not your laptop's localhost. Use host for host.microsandbox.internal.

Next