How Network Namespaces Isolate Each Firecracker MicroVM
A Firecracker guest connects to the outside world through a TAP device — a virtual network interface the VMM reads and writes packets from. If you stop there and just drop every VM's TAP into the host's main network stack, you've built something that works and is quietly terrible: every guest can see the same broadcast domain, ARP for its neighbors, and, if compromised, sniff or spoof traffic that isn't its own. The fix Linux hands you is network namespaces, and this post is about how they isolate each microVM's networking cleanly — and how PandaStack pre-allocates them so the isolation is fast as well as strong.
I'm Ajay; I built PandaStack, and per-VM network isolation is one of those things that's invisible when it works and a breach when it doesn't. Let me explain the primitive, then the pooling trick that keeps it off the latency critical path.
What a network namespace actually is
A Linux network namespace is a private, independent copy of the entire network stack. Not a filtered view — a separate copy: its own set of network interfaces, its own routing table, its own ARP cache, its own iptables/nftables rules, its own conntrack table, its own loopback. A process inside a netns literally cannot see interfaces or connections that live in another netns. There's no rule to bypass because the other namespace's stack isn't in its world at all. This is the same primitive containers use for their networking; here we're using it to wall off VMs from each other rather than processes.
The design that follows is: give each microVM its own network namespace, put that VM's TAP device inside it, and connect the namespace to the host with a veth pair (a virtual cable — two linked interfaces, one end in the netns, one in the host). Route and NAT the veth on the host side. Now the guest's entire network reality is the inside of its namespace: a single point-to-point link to the host and nothing else. It can't ARP for a neighbor because there are no neighbors in its stack. It can't sniff another VM's traffic because that traffic never traverses its namespace.
# The per-VM isolation, by hand. (A platform does this for you, faster.)
ID=abc123
# 1. A private network stack for this VM.
ip netns add ns-$ID
# 2. A virtual cable: host end (vh-) in root netns, guest end (vg-) in the ns.
ip link add vh-$ID type veth peer name vg-$ID
ip link set vg-$ID netns ns-$ID
# 3. Address the point-to-point /30 link (only 2 usable hosts -- guest + gw).
ip addr add 10.200.0.1/30 dev vh-$ID
ip netns exec ns-$ID ip addr add 10.200.0.2/30 dev vg-$ID
ip link set vh-$ID up
ip netns exec ns-$ID ip link set vg-$ID up
# 4. The Firecracker TAP lives INSIDE the namespace, bridged to the guest.
ip netns exec ns-$ID ip tuntap add tap0 mode tap
ip netns exec ns-$ID ip link set tap0 up
# The guest now sees ONLY its /30. No neighbors to sniff or spoof.
# Cleanup is one line and atomic:
ip netns del ns-$ID # takes tap0, vg-, routes, conntrack with itWhy atomic cleanup matters
The last line above is underrated. When you're churning through thousands of short-lived sandboxes, leaked network state is a slow-motion outage: orphaned TAP devices, stale iptables rules, conntrack entries that never expire, veth halves dangling in the root namespace. Do that per-interface cleanup imperatively and you will eventually miss one, and the misses accumulate until something stops working in a way that's miserable to debug.
Namespaces make cleanup a single, atomic operation. Delete the netns and everything that lived inside it — the TAP, the guest-side veth, the routes, the ARP cache, the conntrack — goes with it. There's no per-object accounting to get wrong. A destroyed sandbox is a deleted namespace, and a deleted namespace leaves nothing behind. For a platform running strangers' code at high churn, "cleanup can't be partially forgotten" is a security property, not just an operational nicety — a leftover rule is exactly the kind of thing a later tenant can trip over.
The latency problem, and how NATID solves it
There's a catch: building all of that cold is slow. `ip netns add`, `ip link add`, moving the veth, addressing it, and installing the iptables rules is on the order of ~100ms of syscalls and netlink chatter. If you do it on the create path, per-VM networking becomes your latency floor — and a hundred milliseconds is a lot when the whole point is sub-200ms creates.
PandaStack's answer is NATID: pre-allocate the namespaces. Rather than building a netns per create, the agent pre-creates a pool of 16,384 /30 subnets in `10.200.0.0/16`, each already a complete (netns, veth pair, TAP, iptables) unit sitting warm and ready. On create, allocation is just picking a free slot and patching the guest's MAC and routes to match the baked snapshot's identity — on the order of ~9ms instead of ~100ms. The expensive namespace construction happened ahead of time, off the critical path.
- ns-<id> — the dedicated network namespace: the VM's entire private network stack.
- vh-<id> — the host-side veth: the uplink end, living in the root namespace where routing and NAT happen.
- vg-<id> — the guest-side veth: the other end of the cable, inside the namespace.
- tap0 — the Firecracker TAP inside the namespace: what the VMM actually reads and writes guest packets through.
- The /16 ceiling — 16,384 /30 blocks is the hard per-agent sandbox ceiling from the subnet space; in practice memory and CPU bind first, not network slots.
Namespace-per-VM vs a shared bridge
- Neighbor visibility — Shared bridge: every guest is on the same L2 segment and can ARP, sniff, and spoof its neighbors. Namespace-per-VM: the guest's stack contains only its own /30 — there are no neighbors in its world to reach.
- Blast radius of a compromise — Shared bridge: a compromised guest can attack the whole segment. Namespace-per-VM: a compromised guest is confined to a point-to-point link to the host and whatever egress policy allows.
- Cleanup — Shared bridge: per-interface teardown you can partially forget, leaking TAPs and rules over time. Namespace-per-VM: delete the netns and everything inside it is gone atomically.
- Setup latency — Cold namespace-per-VM: ~100ms of netlink work per create. NATID pre-allocated pool: ~9ms to claim a warm slot and patch identity — the construction happened ahead of time.
The lesson generalizes past networking: strong isolation and low latency aren't in tension if you move the expensive setup off the create path. Namespaces give you the isolation — a private stack per VM with atomic cleanup — and pre-allocation gives you the speed. On PandaStack that combination is why a hardware-isolated, network-isolated sandbox comes up in p50 179ms without the network being the thing that slows it down.
For the layers around this: /blog/firecracker-networking-explained covers the TAP-and-virtio-net path end to end, /blog/controlling-network-egress-untrusted-code is the egress-policy deep dive, and /blog/firecracker-tap-vs-macvtap-networking compares the interface options.
Frequently asked questions
What is a network namespace and how does it isolate a microVM?
A Linux network namespace is a private, independent copy of the whole network stack — its own interfaces, routing table, ARP cache, iptables rules, and conntrack. Give each Firecracker microVM its own namespace, put the VM's TAP device inside it, and connect it to the host with a veth pair, and the guest's entire network reality is the inside of its namespace. A compromised guest can't see or reach other VMs because their network stacks simply aren't present in its namespace — there's no rule to bypass, the neighbors don't exist in its world.
Why give each VM a /30 subnet?
A /30 has exactly two usable addresses: the guest and its gateway. That means the guest's own subnet has no room for a neighbor to exist, which is the cleanest way to express "you have an uplink to the host, not a LAN." It's isolation by subnet math on top of namespace isolation — even within its own stack, the guest has nobody to ARP for or spoof. PandaStack pre-allocates 16,384 such /30 blocks per agent in 10.200.0.0/16.
Why is namespace cleanup a security property, not just tidiness?
Because deleting a network namespace atomically removes everything inside it — the TAP device, the guest-side veth, routes, ARP cache, and conntrack entries — with no per-object accounting to forget. Imperative per-interface cleanup eventually misses something, and leaked TAPs or stale iptables rules accumulate into an outage or, worse, into state a later tenant can trip over. At high sandbox churn, cleanup that can't be partially forgotten keeps one tenant's leftovers from becoming another tenant's problem.
How does PandaStack's NATID keep network isolation fast?
Building a network namespace cold — `ip netns add`, `ip link add`, moving the veth, addressing it, installing iptables rules — is roughly 100ms of syscall and netlink work, which would dominate a sub-200ms create. NATID pre-allocates a pool of 16,384 complete (netns, veth, TAP, iptables) units so they sit warm and ready. On create, the agent just claims a free slot and patches the guest's MAC and routes to match the baked snapshot — about 9ms instead of 100ms, with the expensive construction done ahead of time.
Do network namespaces control what a guest can reach on the internet?
No — namespaces isolate guests from each other, but a guest with an uplink can still reach the internet unless you add policy. Egress control is separate and deliberate: you install default-deny rules with an explicit allowlist in the host-side rules of each namespace, which matters most when the guest runs untrusted code that would otherwise phone home or exfiltrate data. Pre-allocating the plumbing gives you connectivity and isolation; the egress policy is something you configure on top.
49ms p50 cold start. Fork, snapshot, and scale to zero.