SmolVM automatically configures host networking to give each VM internet access while maintaining isolation. The networking stack varies by backend but provides a consistent interface.
A TAP (network tap) device is a virtual network interface that operates at Layer 2 (Ethernet):
Virtual NIC: Appears as a network interface on the host (like eth0 or wlan0)
Userspace I/O: Firecracker reads/writes raw Ethernet frames to the TAP device
Kernel routing: Host kernel routes packets between TAP and physical interfaces
Think of TAP devices like virtual network cables. Firecracker plugs one end into the guest VM, and the host kernel plugs the other end into its routing table.
# List all network interfacesip link show | grep tap-smol# Show TAP device detailsip addr show tap-smol-abc123# Example output:# tap-smol-abc123: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500# link/ether fe:fc:00:00:00:01# inet 172.16.0.1/24 scope global tap-smol-abc123
QEMU user networking uses a fixed IP scheme (from src/smolvm/vm.py:54):
Guest IP: 10.0.2.15 (always the same)
Gateway: 10.0.2.2 (QEMU built-in gateway)
DNS: 10.0.2.3 (QEMU built-in DNS forwarder)
This means you cannot run multiple QEMU-based VMs simultaneously that expect unique IPs.
QEMU Limitation: Multiple concurrent VMs will have the same guest IP (10.0.2.15). This is generally fine for isolated workflows but prevents direct VM-to-VM communication.
By default, VMs cannot communicate with each other (see src/smolvm/network.py:468):
# Drop all traffic between TAP devicesnft add rule inet smolvm_filter forward \ iifname "tap*" oifname "tap*" counter drop \ comment "smolvm:global:forward:tap-isolation"
This prevents:
Lateral movement between agent sessions
Data exfiltration via another VM
Accidental network conflicts
Security: Even if a VM is compromised, it cannot attack other VMs on the same host. This is a key isolation guarantee.
from smolvm import SmolVMwith SmolVM() as vm: # Start a web server in the guest vm.run("python3 -m http.server 8080 &") # Expose guest:8080 to host:18080 host_port = vm.expose_local(guest_port=8080, host_port=18080) # Access from host browser print(f"http://localhost:{host_port}") # http://localhost:18080
This creates similar DNAT rules but for application ports (see src/smolvm/network.py:564).
nftables (Preferred)
SSH Tunnel (Fallback)
SmolVM tries nftables forwarding first (fastest, kernel-level):
nft add rule ip smolvm_nat output \ ip daddr 127.0.0.1/32 tcp dport 18080 counter dnat to 172.16.0.2:8080
Pros: Low latency, no extra processes
Cons: Requires CAP_NET_ADMIN
If nftables fails, SmolVM falls back to SSH tunneling: