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:
| Mode | Description | Use Case |
|---|---|---|
None | No network access | Maximum isolation |
Nat | Platform-native NAT | Simple internet access (macOS only) |
UserNat | Userspace NAT with policies | Cross-platform, supports filtering |
Cluster | VM-to-VM networking | Multi-VM environments |
Network Modes
No Network
The default mode. The VM has no network connectivity:
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:
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.
UserNat (Recommended)
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
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:
| Setting | Default Value |
|---|---|
| Subnet | 10.0.2.0/24 |
| Gateway IP | 10.0.2.2 |
| DHCP Range | 10.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:
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:
// 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:
// 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:
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:
let vm = Capsa::vm(config)
.network(
NetworkMode::user_nat()
.forward_tcp(2222, 22)
.build()
)
.start()
.await?;
// Connect from host:
// ssh -p 2222 user@localhostNetwork 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:
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:
// 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:
let policy = NetworkPolicy::deny_all()
.allow_https() // Port 443
.allow_dns(); // UDP port 53IP-Based Rules
Allow or deny traffic to specific IP addresses:
use std::net::Ipv4Addr;
let policy = NetworkPolicy::deny_all()
.allow_ip(Ipv4Addr::new(8, 8, 8, 8)); // Google DNSIP Range Rules
Match traffic to IP ranges using CIDR notation:
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:
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
- The guest's DNS queries are intercepted by Capsa's DNS proxy
- Domain-to-IP mappings are cached when the guest resolves domains
- When the guest connects to an IP, the policy checker looks up the original domain
- 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:
let policy = NetworkPolicy::deny_all()
.allow_domain("api.anthropic.com");Wildcard Domain Matching
Allow traffic to all subdomains of a domain:
let policy = NetworkPolicy::deny_all()
.allow_domain("*.github.com");The wildcard *.github.com matches:
api.github.comraw.github.comdeep.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:
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:
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:
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:
// deny_all() -> default action is Deny
// allow_all() -> default action is AllowLog Action
The Log action logs traffic and continues to the next rule (non-terminal):
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:
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:
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:
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:
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:8080Restricted API Sandbox
Sandbox for running code that can only access specific APIs:
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:
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:
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.10Security Best Practices
When configuring networking for untrusted code:
Start with deny_all: Always begin with a deny-all policy and explicitly allow what is needed
Use domain filtering: Prefer domain-based rules over IP-based rules for external services
Restrict ports: Only allow the protocols your application actually needs (usually just HTTPS)
Avoid port forwarding for untrusted VMs: Port forwarding exposes guest services to the host network
Prefer Cluster over NAT for multi-VM setups: Cluster networking isolates VM-to-VM traffic from the internet
// 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:
| Protocol | Support | Notes |
|---|---|---|
| TCP | Full | Connection tracking with NAT |
| UDP | Full | Stateless NAT with timeout |
| ICMP | Outbound | Ping works to external hosts |
| DNS | Proxied | Intercepted for domain filtering |