Skip to content

Networking

Configure network access for your VMs with Capsa's flexible networking options, from complete isolation to full internet access with domain-based filtering.

Overview

Capsa provides multiple network modes to match different security and connectivity requirements:

ModeDescriptionUse Case
NoneNo network accessMaximum isolation
NatPlatform-native NATSimple internet access (macOS only)
UserNatUserspace NAT with policiesCross-platform, supports filtering
ClusterVM-to-VM networkingMulti-VM environments

Network Modes

No Network

The default mode. The VM has no network connectivity:

rust
use capsa::{Capsa, LinuxVmConfig};
use capsa_core::NetworkMode;

let vm = Capsa::vm(LinuxVmConfig::new(kernel, rootfs))
    .network(NetworkMode::None)
    .start()
    .await?;

Use None when:

  • Running untrusted code that should not access the network
  • Testing offline behavior
  • Maximum security isolation is required

Platform NAT (macOS only)

Uses the platform's built-in NAT networking:

rust
use capsa_core::NetworkMode;

let vm = Capsa::vm(config)
    .network(NetworkMode::Nat)
    .start()
    .await?;

This provides simple internet access without policy enforcement. Only available on macOS.

Userspace NAT networking is the recommended mode for most use cases. It provides:

  • Cross-platform support (Linux and macOS)
  • Port forwarding
  • Network policy enforcement
  • Domain-based filtering
rust
use capsa_core::NetworkMode;

let vm = Capsa::vm(config)
    .network(NetworkMode::user_nat().build())
    .start()
    .await?;

UserNat Configuration

Default Settings

When you create a UserNat network with default settings, the guest receives:

SettingDefault Value
Subnet10.0.2.0/24
Gateway IP10.0.2.2
DHCP Range10.0.2.15 - 10.0.2.254

The guest automatically obtains an IP address via DHCP on boot.

Custom Subnet

Override the default subnet for your network:

rust
let vm = Capsa::vm(config)
    .network(
        NetworkMode::user_nat()
            .subnet("192.168.100.0/24")
            .build()
    )
    .start()
    .await?;

When you set a custom subnet, the gateway and DHCP range are automatically adjusted to match.

Port Forwarding

Expose guest services to the host by forwarding ports.

TCP Port Forwarding

Forward a TCP port from the host to the guest:

rust
// Forward host:8080 to guest:80
let vm = Capsa::vm(config)
    .network(
        NetworkMode::user_nat()
            .forward_tcp(8080, 80)
            .build()
    )
    .start()
    .await?;

After the VM starts, connections to localhost:8080 on the host are forwarded to port 80 inside the guest.

UDP Port Forwarding

Forward UDP ports similarly:

rust
// Forward host:5353 to guest:53 for DNS
let vm = Capsa::vm(config)
    .network(
        NetworkMode::user_nat()
            .forward_udp(5353, 53)
            .build()
    )
    .start()
    .await?;

Multiple Port Forwards

Chain multiple port forwards:

rust
let vm = Capsa::vm(config)
    .network(
        NetworkMode::user_nat()
            .forward_tcp(8080, 80)
            .forward_tcp(8443, 443)
            .forward_tcp(2222, 22)
            .forward_udp(5353, 53)
            .build()
    )
    .start()
    .await?;

SSH Access Example

A common pattern is forwarding SSH for remote access to the guest:

rust
let vm = Capsa::vm(config)
    .network(
        NetworkMode::user_nat()
            .forward_tcp(2222, 22)
            .build()
    )
    .start()
    .await?;

// Connect from host:
// ssh -p 2222 user@localhost

Network Policies

Network policies control what traffic the guest can send. This is essential for sandboxing untrusted code.

Base Policies

Start with a base policy that determines the default behavior:

rust
use capsa_core::NetworkPolicy;

// Deny all traffic by default (allowlist mode)
let policy = NetworkPolicy::deny_all();

// Allow all traffic by default (denylist mode)
let policy = NetworkPolicy::allow_all();

TIP

For AI sandboxes and untrusted code, always start with deny_all() and explicitly allow only what is needed.

Port-Based Rules

Allow or deny traffic to specific ports:

rust
// Allow HTTPS only
let policy = NetworkPolicy::deny_all()
    .allow_port(443);

// Block HTTP, allow everything else
let policy = NetworkPolicy::allow_all()
    .deny_port(80);

Convenience methods for common protocols:

rust
let policy = NetworkPolicy::deny_all()
    .allow_https()  // Port 443
    .allow_dns();   // UDP port 53

IP-Based Rules

Allow or deny traffic to specific IP addresses:

rust
use std::net::Ipv4Addr;

let policy = NetworkPolicy::deny_all()
    .allow_ip(Ipv4Addr::new(8, 8, 8, 8));  // Google DNS

IP Range Rules

Match traffic to IP ranges using CIDR notation:

rust
use capsa_core::{NetworkPolicy, PolicyAction, RuleMatcher};
use std::net::Ipv4Addr;

let policy = NetworkPolicy::deny_all()
    .rule(
        PolicyAction::Allow,
        RuleMatcher::IpRange {
            network: Ipv4Addr::new(10, 0, 0, 0),
            prefix: 8,
        }
    );

Applying a Policy

Attach the policy to your UserNat configuration:

rust
let policy = NetworkPolicy::deny_all()
    .allow_https()
    .allow_dns();

let vm = Capsa::vm(config)
    .network(
        NetworkMode::user_nat()
            .policy(policy)
            .build()
    )
    .start()
    .await?;

Domain-Based Filtering

Filter traffic based on domain names rather than IP addresses. This is ideal for sandboxing AI agents that need to access specific APIs.

How It Works

  1. The guest's DNS queries are intercepted by Capsa's DNS proxy
  2. Domain-to-IP mappings are cached when the guest resolves domains
  3. When the guest connects to an IP, the policy checker looks up the original domain
  4. Traffic is allowed or denied based on domain matching rules

TIP

DNS queries to the gateway are handled internally and are always allowed. You do not need to add explicit allow_dns() rules for domain filtering to work.

Exact Domain Matching

Allow traffic to a specific domain:

rust
let policy = NetworkPolicy::deny_all()
    .allow_domain("api.anthropic.com");

Wildcard Domain Matching

Allow traffic to all subdomains of a domain:

rust
let policy = NetworkPolicy::deny_all()
    .allow_domain("*.github.com");

The wildcard *.github.com matches:

  • api.github.com
  • raw.github.com
  • deep.sub.github.com

It does not match github.com itself (the base domain).

Combining Domain and Port Rules

Restrict access to specific domains on specific ports:

rust
use capsa_core::{NetworkPolicy, PolicyAction, RuleMatcher, DomainPattern};

// Only allow HTTPS to anthropic
let policy = NetworkPolicy::deny_all()
    .rule(
        PolicyAction::Allow,
        RuleMatcher::All(vec![
            RuleMatcher::Domain(DomainPattern::parse("api.anthropic.com")),
            RuleMatcher::Port(443),
        ])
    );

AI Sandbox Example

A complete policy for an AI coding agent:

rust
use capsa_core::{NetworkMode, NetworkPolicy};

let policy = NetworkPolicy::deny_all()
    .allow_domain("api.anthropic.com")
    .allow_domain("api.openai.com")
    .allow_domain("*.github.com")
    .allow_domain("*.githubusercontent.com")
    .allow_domain("registry.npmjs.org")
    .allow_domain("*.crates.io");

let vm = Capsa::vm(config)
    .network(
        NetworkMode::user_nat()
            .policy(policy)
            .build()
    )
    .start()
    .await?;

Policy Evaluation

First Match Wins

Rules are evaluated in order. The first matching rule determines the action:

rust
let policy = NetworkPolicy::deny_all()
    .allow_domain("api.anthropic.com")   // Rule 1
    .deny_port(80);                       // Rule 2 (never reached for anthropic)

If traffic matches api.anthropic.com, Rule 1 applies and the traffic is allowed, regardless of the port.

Default Action

If no rule matches, the default action applies:

rust
// deny_all() -> default action is Deny
// allow_all() -> default action is Allow

Log Action

The Log action logs traffic and continues to the next rule (non-terminal):

rust
use capsa_core::{NetworkPolicy, PolicyAction, RuleMatcher};

let policy = NetworkPolicy::deny_all()
    .rule(PolicyAction::Log, RuleMatcher::Any)  // Log everything
    .allow_domain("api.anthropic.com");

This logs all traffic but still applies the domain filter.

Cluster Networking

For scenarios where multiple VMs need to communicate with each other, use Cluster mode.

Creating a Cluster

VMs on the same cluster can communicate directly via a virtual L2 switch:

rust
use capsa_core::NetworkMode;

// Create VMs on the same cluster
let vm1 = Capsa::vm(config1)
    .network(NetworkMode::cluster("my-cluster").build())
    .start()
    .await?;

let vm2 = Capsa::vm(config2)
    .network(NetworkMode::cluster("my-cluster").build())
    .start()
    .await?;

Static IP Assignment

Assign static IPs to cluster VMs:

rust
use std::net::Ipv4Addr;
use capsa_core::NetworkMode;

let vm = Capsa::vm(config)
    .network(
        NetworkMode::cluster("my-cluster")
            .with_ip(Ipv4Addr::new(10, 0, 3, 10))
            .build()
    )
    .start()
    .await?;

Cluster Features

  • MAC Learning: The virtual switch learns MAC addresses automatically
  • DHCP: VMs receive IPs via DHCP if no static IP is set
  • Direct L2 Communication: VMs communicate at layer 2 without NAT

Complete Examples

Basic Internet Access

Simple VM with unrestricted internet access:

rust
use capsa::{Capsa, LinuxVmConfig};
use capsa_core::NetworkMode;

let vm = Capsa::vm(LinuxVmConfig::new(kernel, rootfs))
    .network(NetworkMode::user_nat().build())
    .start()
    .await?;

Web Server with Port Forward

Expose a web server running in the guest:

rust
use capsa::{Capsa, LinuxVmConfig};
use capsa_core::NetworkMode;
use std::time::Duration;

let vm = Capsa::vm(LinuxVmConfig::new(kernel, rootfs))
    .network(
        NetworkMode::user_nat()
            .forward_tcp(8080, 80)
            .build()
    )
    .console()
    .start()
    .await?;

let console = vm.console().unwrap();
console.wait_for_boot(Duration::from_secs(30)).await?;
console.exec("python3 -m http.server 80", Duration::from_secs(5)).await?;

// Access at http://localhost:8080

Restricted API Sandbox

Sandbox for running code that can only access specific APIs:

rust
use capsa::{Capsa, LinuxVmConfig};
use capsa_core::{NetworkMode, NetworkPolicy};

let policy = NetworkPolicy::deny_all()
    .allow_domain("api.anthropic.com")
    .allow_domain("*.github.com");

let vm = Capsa::vm(LinuxVmConfig::new(kernel, rootfs))
    .network(
        NetworkMode::user_nat()
            .policy(policy)
            .build()
    )
    .start()
    .await?;

HTTPS-Only Sandbox

Allow only secure connections:

rust
use capsa_core::{NetworkMode, NetworkPolicy};

let policy = NetworkPolicy::deny_all()
    .allow_https()
    .allow_dns();

let vm = Capsa::vm(config)
    .network(
        NetworkMode::user_nat()
            .policy(policy)
            .build()
    )
    .start()
    .await?;

Multi-VM Cluster

Set up a cluster of VMs that can communicate with each other:

rust
use capsa::{Capsa, LinuxVmConfig};
use capsa_core::NetworkMode;
use std::net::Ipv4Addr;

// Database VM
let db = Capsa::vm(LinuxVmConfig::new(kernel, rootfs))
    .network(
        NetworkMode::cluster("app-cluster")
            .with_ip(Ipv4Addr::new(10, 0, 3, 10))
            .build()
    )
    .start()
    .await?;

// Web server VM
let web = Capsa::vm(LinuxVmConfig::new(kernel, rootfs))
    .network(
        NetworkMode::cluster("app-cluster")
            .with_ip(Ipv4Addr::new(10, 0, 3, 20))
            .build()
    )
    .start()
    .await?;

// web VM can reach db VM at 10.0.3.10

Security Best Practices

When configuring networking for untrusted code:

  1. Start with deny_all: Always begin with a deny-all policy and explicitly allow what is needed

  2. Use domain filtering: Prefer domain-based rules over IP-based rules for external services

  3. Restrict ports: Only allow the protocols your application actually needs (usually just HTTPS)

  4. Avoid port forwarding for untrusted VMs: Port forwarding exposes guest services to the host network

  5. Prefer Cluster over NAT for multi-VM setups: Cluster networking isolates VM-to-VM traffic from the internet

rust
// Secure sandbox configuration
let policy = NetworkPolicy::deny_all()
    .allow_domain("api.anthropic.com");  // Only what is needed

let vm = Capsa::vm(config)
    .network(
        NetworkMode::user_nat()
            .policy(policy)
            // No port forwards for untrusted code
            .build()
    )
    .start()
    .await?;

Protocol Support

UserNat networking supports the following protocols:

ProtocolSupportNotes
TCPFullConnection tracking with NAT
UDPFullStateless NAT with timeout
ICMPOutboundPing works to external hosts
DNSProxiedIntercepted for domain filtering

Released under the MIT License.