Skip to main content
SmolVM provides automatic networking setup with TAP devices, NAT, and port forwarding. This page explains how VM networking works and how to configure it.

Networking Architecture

Each SmolVM instance gets its own isolated network configuration:
┌─────────────────────────────────────────┐
│ Host System                              │
│                                          │
│  ┌──────────┐         ┌──────────┐     │
│  │   VM 1   │         │   VM 2   │     │
│  │172.16.0.2│         │172.16.0.3│     │
│  └────┬─────┘         └────┬─────┘     │
│       │                    │            │
│  ┌────┴─────┐         ┌────┴─────┐     │
│  │  tap0    │         │  tap1    │     │
│  │172.16.0.1│         │172.16.0.1│     │
│  └────┬─────┘         └────┬─────┘     │
│       │                    │            │
│       └────────┬───────────┘            │
│                │                        │
│           ┌────┴─────┐                  │
│           │   NAT    │                  │
│           │(nftables)│                  │
│           └────┬─────┘                  │
│                │                        │
│           ┌────┴──────┐                 │
│           │  eth0     │                 │
│           │(Internet) │                 │
│           └───────────┘                 │
└─────────────────────────────────────────┘

Network Configuration

The NetworkConfig model defines VM network settings:
# From types.py:108-128
class NetworkConfig(BaseModel):
    """Network configuration for a VM.

    Attributes:
        guest_ip: IP address assigned to the guest.
        gateway_ip: Gateway IP (host side of TAP).
        netmask: Network mask.
        tap_device: Name of the TAP device.
        guest_mac: MAC address for the guest interface.
        ssh_host_port: Optional host TCP port forwarded to guest SSH (22).
    """

    guest_ip: str
    gateway_ip: str = "172.16.0.1"
    netmask: str = "255.255.255.0"
    tap_device: str
    guest_mac: str
    ssh_host_port: int | None = None

Default Network Range

# From network.py:33-35
DEFAULT_HOST_IP = "172.16.0.1"
DEFAULT_NETMASK = "24"
Each VM receives:
  • Guest IP: 172.16.0.2 through 172.16.0.255
  • Gateway: 172.16.0.1 (host side of TAP)
  • Netmask: 255.255.255.0 (or /24)

TAP Devices

What is a TAP Device?

A TAP (Network Tap) device is a virtual network interface that:
  • Operates at Layer 2 (Ethernet)
  • Allows user-space programs to read/write raw Ethernet frames
  • Acts as a bridge between the VM and host networking

TAP Device Lifecycle

1. Creation

# From network.py:87-103
def create_tap(self, tap_name: str, user: str | None = None) -> None:
    """Create TAP device if missing."""
    if user is None:
        user = os.environ.get("USER", "root")

    logger.info("Creating TAP device: %s (user: %s)", tap_name, user)

    try:
        run_command(["ip", "tuntap", "add", tap_name, "mode", "tap", "user", user])
    except SmolVMError as e:
        if "File exists" in str(e) or "EEXIST" in str(e):
            logger.debug("TAP device %s already exists", tap_name)
        else:
            raise
Creates a TAP device with iproute2:
sudo ip tuntap add tap0 mode tap user $USER

2. Configuration

# From network.py:105-134
def configure_tap(
    self,
    tap_name: str,
    host_ip: str | None = None,
    netmask: str = DEFAULT_NETMASK,
) -> None:
    """Assign host IP and bring TAP link up."""
    logger.info("Configuring TAP %s with IP %s/%s", tap_name, host_ip, netmask)

    batch = [
        f"addr flush dev {tap_name}",
        f"addr add {host_ip}/{netmask} dev {tap_name}",
        f"link set {tap_name} up",
    ]

    self._run_ip_batch(batch)

    # Allow localhost DNAT to guest addresses.
    self._write_sysctl(f"net/ipv4/conf/{tap_name}/route_localnet", "1")
Configures the TAP device:
# Assign IP and bring up
sudo ip addr add 172.16.0.1/24 dev tap0
sudo ip link set tap0 up

# Enable route_localnet for port forwarding
sudo sysctl -w net.ipv4.conf.tap0.route_localnet=1

3. Routing

# From network.py:136-148
def add_route(self, ip_address: str, device: str) -> None:
    """Add host route for one guest IP through a TAP device."""
    logger.info("Adding route: %s via %s", ip_address, device)
    try:
        run_command(["ip", "route", "add", f"{ip_address}/32", "dev", device])
    except SmolVMError as e:
        if "File exists" not in str(e):
            raise
Adds a host route to the guest:
sudo ip route add 172.16.0.2/32 dev tap0

4. Cleanup

# From network.py:150-160
def cleanup_tap(self, tap_name: str) -> None:
    """Delete TAP device (best effort)."""
    logger.info("Cleaning up TAP device: %s", tap_name)
    try:
        run_command(["ip", "link", "delete", tap_name])
    except SmolVMError as e:
        if "Cannot find device" not in str(e):
            logger.warning("Failed to delete TAP %s: %s", tap_name, e)
Removes the TAP device:
sudo ip link delete tap0

MAC Address Generation

# From network.py:672-676
def generate_mac(self, vm_number: int) -> str:
    """Generate deterministic VM MAC address for vm_number in [0,255]."""
    if vm_number < 0 or vm_number > 255:
        raise ValueError("vm_number must be between 0 and 255")
    return f"AA:FC:00:00:00:{vm_number:02X}"
Generates deterministic MAC addresses:
  • VM 0: AA:FC:00:00:00:00
  • VM 1: AA:FC:00:00:00:01
  • VM 255: AA:FC:00:00:00:FF

NAT and Firewalling

SmolVM uses nftables for NAT and firewall rules.

nftables Tables and Chains

# From network.py:37-41
_NFT_NAT_FAMILY = "ip"
_NFT_NAT_TABLE = "smolvm_nat"
_NFT_FILTER_FAMILY = "inet"
_NFT_FILTER_TABLE = "smolvm_filter"
Creates two tables:
  • ip smolvm_nat: NAT rules (DNAT, SNAT, masquerade)
  • inet smolvm_filter: Firewall rules (forward, drop)

NAT Setup

# From network.py:425-474
def setup_nat(self, tap_name: str) -> None:
    """Configure outbound NAT and forwarding for a TAP device."""
    logger.info("Setting up NAT for TAP: %s", tap_name)

    self.enable_ip_forwarding()
    self._ensure_nftables_base()

    iface = self.outbound_interface

    self._add_nft_rules_if_missing(
        [
            # Masquerade outbound traffic
            (
                _NFT_NAT_FAMILY,
                _NFT_NAT_TABLE,
                "postrouting",
                f"oifname {self._quote(iface)} counter masquerade",
                f"smolvm:global:nat:masquerade:{iface}",
            ),
            # Allow established connections back
            (
                _NFT_FILTER_FAMILY,
                _NFT_FILTER_TABLE,
                "forward",
                "ct state related,established counter accept",
                "smolvm:global:forward:established",
            ),
            # Forward TAP to internet
            (
                _NFT_FILTER_FAMILY,
                _NFT_FILTER_TABLE,
                "forward",
                f"iifname {self._quote(tap_name)} oifname {self._quote(iface)} counter accept",
                f"smolvm:nat:tap:{tap_name}:to:{iface}",
            ),
            # Isolate VMs from each other
            (
                _NFT_FILTER_FAMILY,
                _NFT_FILTER_TABLE,
                "forward",
                f"iifname {self._quote('tap*')} oifname {self._quote('tap*')} counter drop",
                "smolvm:global:forward:tap-isolation",
            ),
        ]
    )
This creates nftables rules equivalent to:
# Masquerade outbound traffic
nft add rule ip smolvm_nat postrouting oifname eth0 counter masquerade

# Allow established connections
nft add rule inet smolvm_filter forward ct state related,established counter accept

# Forward TAP to internet
nft add rule inet smolvm_filter forward iifname tap0 oifname eth0 counter accept

# Isolate VMs from each other
nft add rule inet smolvm_filter forward iifname tap* oifname tap* counter drop

IP Forwarding

# From network.py:166-172
def enable_ip_forwarding(self) -> None:
    """Enable IPv4 forwarding once per manager instance."""
    if self._ip_forwarding_enabled:
        return

    if self._write_sysctl("net/ipv4/ip_forward", "1"):
        self._ip_forwarding_enabled = True
Enables kernel packet forwarding:
sudo sysctl -w net.ipv4.ip_forward=1

VM Isolation

VMs are isolated from each other by default:
# From network.py:467-472 (in setup_nat)
(
    _NFT_FILTER_FAMILY,
    _NFT_FILTER_TABLE,
    "forward",
    f"iifname {self._quote('tap*')} oifname {self._quote('tap*')} counter drop",
    "smolvm:global:forward:tap-isolation",
),
This nftables rule drops all VM-to-VM traffic:
nft add rule inet smolvm_filter forward iifname tap* oifname tap* counter drop
VMs can access the internet via NAT, but cannot directly communicate with each other.

Port Forwarding

SmolVM provides two types of port forwarding:

1. SSH Port Forwarding

Exposes guest SSH (port 22) to a host port:
# From network.py:476-540
def setup_ssh_port_forward(
    self,
    vm_id: str,
    guest_ip: str,
    host_port: int,
    guest_port: int = 22,
) -> None:
    """Expose host TCP port to guest SSH port via nftables."""
    self.enable_ip_forwarding()
    self._ensure_nftables_base()

    iface = self.outbound_interface
    target = f"{guest_ip}:{guest_port}"
    comment = f"smolvm:{vm_id}:ssh"

    self._add_nft_rules_if_missing(
        [
            # DNAT from external interface
            (
                _NFT_NAT_FAMILY,
                _NFT_NAT_TABLE,
                "prerouting",
                f"iifname {self._quote(iface)} tcp dport {host_port} counter dnat to {target}",
                comment,
            ),
            # DNAT from localhost
            (
                _NFT_NAT_FAMILY,
                _NFT_NAT_TABLE,
                "output",
                f"ip daddr 127.0.0.1/32 tcp dport {host_port} counter dnat to {target}",
                comment,
            ),
            # SNAT localhost traffic
            (
                _NFT_NAT_FAMILY,
                _NFT_NAT_TABLE,
                "postrouting",
                f"ip saddr 127.0.0.0/8 ip daddr {guest_ip}/32 tcp dport {guest_port} counter snat to {self.host_ip}",
                comment,
            ),
            # Allow forwarding
            (
                _NFT_FILTER_FAMILY,
                _NFT_FILTER_TABLE,
                "forward",
                f"ip daddr {guest_ip}/32 tcp dport {guest_port} ct state new,related,established counter accept",
                comment,
            ),
        ]
    )
Usage:
from smolvm import SmolVM

vm = SmolVM()
vm.start()

# SSH forwarded automatically during start
# Connect via: ssh -p <host_port> root@localhost

2. Local Port Forwarding

Exposes guest application ports to localhost:
# From network.py:564-617
def setup_local_port_forward(
    self,
    vm_id: str,
    guest_ip: str,
    host_port: int,
    guest_port: int,
) -> None:
    """Expose localhost:host_port to guest_ip:guest_port."""
    self.enable_ip_forwarding()
    self._ensure_nftables_base()

    comment = f"smolvm:{vm_id}:local:{host_port}:{guest_port}"
    target = f"{guest_ip}:{guest_port}"

    self._add_nft_rules_if_missing(
        [
            # DNAT from localhost
            (
                _NFT_NAT_FAMILY,
                _NFT_NAT_TABLE,
                "output",
                f"ip daddr 127.0.0.1/32 tcp dport {host_port} counter dnat to {target}",
                comment,
            ),
            # SNAT localhost traffic
            (
                _NFT_NAT_FAMILY,
                _NFT_NAT_TABLE,
                "postrouting",
                f"ip saddr 127.0.0.0/8 ip daddr {guest_ip}/32 tcp dport {guest_port} counter snat to {self.host_ip}",
                comment,
            ),
            # Allow forwarding
            (
                _NFT_FILTER_FAMILY,
                _NFT_FILTER_TABLE,
                "forward",
                f"ip daddr {guest_ip}/32 tcp dport {guest_port} ct state new,related,established counter accept",
                comment,
            ),
        ]
    )
Usage:
from smolvm import SmolVM

with SmolVM() as vm:
    # App in VM listening on port 8080
    vm.run("python -m http.server 8080")
    
    # Expose to host port 18080
    host_port = vm.expose_local(guest_port=8080, host_port=18080)
    
    # Access from host
    print(f"App available at http://localhost:{host_port}")

Port Forwarding Flow

┌──────────────────────────────────────────────────┐
│ Host                                              │
│                                                   │
│  Browser                                          │
│  http://localhost:18080                          │
│         │                                         │
│         ▼                                         │
│  ┌──────────────┐                                │
│  │   nftables   │                                │
│  │     DNAT     │                                │
│  │ 127.0.0.1:18080                               │
│  │      ↓                                        │
│  │ 172.16.0.2:8080                               │
│  └──────┬───────┘                                │
│         │                                         │
│         ▼                                         │
│  ┌──────────────┐                                │
│  │     TAP      │                                │
│  │  172.16.0.1  │                                │
│  └──────┬───────┘                                │
│         │                                         │
│         ▼                                         │
│  ┌──────────────┐                                │
│  │      VM      │                                │
│  │  172.16.0.2  │                                │
│  │  :8080       │                                │
│  └──────────────┘                                │
└──────────────────────────────────────────────────┘

Network Prerequisites

Check required networking tools:
# From network.py:679-701
def check_network_prerequisites() -> list[str]:
    """Validate required host networking binaries and sudo access."""
    errors: list[str] = []

    for binary in ["ip", "nft"]:
        try:
            run_command(["which", binary], use_sudo=False)
        except SmolVMError:
            errors.append(f"'{binary}' command not found")

    if os.geteuid() != 0:
        checks = [
            (["ip", "link", "show"], "sudo ip"),
            (["nft", "list", "tables"], "sudo nft"),
            (["sysctl", "net.ipv4.ip_forward"], "sudo sysctl"),
        ]
        for cmd, label in checks:
            try:
                run_command(cmd, use_sudo=True)
            except SmolVMError:
                errors.append(f"{label} missing (run setup script)")

    return errors
Required tools:
  • ip (iproute2) - TAP device management
  • nft (nftables) - NAT and firewall rules
  • sudo access for networking commands

Networking Examples

Basic VM with Internet Access

from smolvm import SmolVM

with SmolVM() as vm:
    # VM has automatic internet access via NAT
    result = vm.run("curl -s https://httpbin.org/ip")
    print(f"VM public IP: {result.output}")

Expose Web Server

from smolvm import SmolVM

with SmolVM() as vm:
    # Start web server in VM
    vm.run("python3 -m http.server 8000 &")
    
    # Expose to host
    host_port = vm.expose_local(guest_port=8000, host_port=18000)
    print(f"Access server at http://localhost:{host_port}")

Multiple Port Forwards

from smolvm import SmolVM

with SmolVM() as vm:
    # Web server
    vm.run("python3 -m http.server 8000 &")
    web_port = vm.expose_local(guest_port=8000, host_port=18000)
    
    # API server
    vm.run("python3 api_server.py &")  # Runs on :8080
    api_port = vm.expose_local(guest_port=8080, host_port=18080)
    
    print(f"Web: http://localhost:{web_port}")
    print(f"API: http://localhost:{api_port}")

Custom Network Configuration

from smolvm import VMConfig, NetworkConfig

config = VMConfig(
    vcpu_count=2,
    mem_size_mib=1024,
    # ... other config ...
)

network = NetworkConfig(
    guest_ip="172.16.0.10",
    gateway_ip="172.16.0.1",
    netmask="255.255.255.0",
    tap_device="tap5",
    guest_mac="AA:FC:00:00:00:0A"
)

# Use with SmolVM
# (Note: NetworkConfig is typically managed internally)

Troubleshooting

”ip: command not found”

Problem: iproute2 not installed Solution:
# Ubuntu/Debian
sudo apt install iproute2

# Fedora/RHEL
sudo dnf install iproute

“nft: command not found”

Problem: nftables not installed Solution:
# Ubuntu/Debian
sudo apt install nftables

# Fedora/RHEL
sudo dnf install nftables

“Permission denied” on TAP creation

Problem: Missing sudo permissions Solution:
# Run system setup to configure sudo
sudo ./scripts/system-setup.sh --configure-runtime

VM has no internet access

Problem: IP forwarding disabled or NAT not configured Solution:
# Check IP forwarding
cat /proc/sys/net/ipv4/ip_forward  # Should be 1

# Enable manually
sudo sysctl -w net.ipv4.ip_forward=1

# Check nftables rules
sudo nft list table ip smolvm_nat
sudo nft list table inet smolvm_filter

Port forwarding not working

Problem: Firewall rules or route_localnet not enabled Solution:
# Check route_localnet
cat /proc/sys/net/ipv4/conf/tap0/route_localnet  # Should be 1

# Enable manually
sudo sysctl -w net.ipv4.conf.tap0.route_localnet=1

# Check forwarding rules
sudo nft list chain inet smolvm_filter forward

TAP device persists after VM deletion

Problem: Cleanup failed or manual cleanup needed Solution:
# List TAP devices
ip link show | grep tap

# Delete manually
sudo ip link delete tap0

# Or use SmolVM cleanup
smolvm cleanup --all

Security Considerations

Understand these networking security implications:

Outbound Access

By default, VMs have unrestricted internet access via NAT:
# VM can access any internet service
result = vm.run("curl https://example.com")
Mitigation: Use firewall rules to restrict outbound access.

VM Isolation

VMs cannot communicate with each other:
# This traffic is DROPPED by nftables
vm1 = SmolVM()  # Gets 172.16.0.2
vm2 = SmolVM()  # Gets 172.16.0.3

# vm1 cannot ping vm2
vm1.run("ping -c 1 172.16.0.3")  # Fails

Port Exposure

expose_local() creates localhost-only forwarding:
# Only accessible from host
vm.expose_local(guest_port=8080, host_port=18080)
# Access: http://localhost:18080 ✅
# Access: http://<host-ip>:18080 ❌
See Security Model for more details on network isolation and trust boundaries.

Next Steps

Last modified on March 3, 2026