all posts

vsock Explained: How the Host Talks to a microVM

Ajay Kumar··9 min read

You have a microVM running someone else's code. You need to do things to it from the outside: run a command, read a file, write a file, and — before any of that — find out the instant the guest is actually ready to take work. The obvious instinct is to open a TCP port inside the guest and connect to it. Resist it. Opening a port means the guest has a network, and the moment it has a network it has an attack surface, a routing problem, and a firewall you now have to reason about. There is a cleaner channel built exactly for host-to-guest control traffic, and it needs no guest networking at all: virtio-vsock. This post explains what vsock is, the unusual addressing model it uses, why it's the right primitive for a control channel into a sandbox, and how Firecracker exposes it to the host as a plain Unix domain socket — which is precisely how PandaStack's pandastack-init guest agent handles exec, filesystem, and readiness over a single vsock link.

What is virtio-vsock?

vsock is a socket address family — AF_VSOCK — that Linux exposes for communication between a hypervisor host and the virtual machines running on it. You use it through the same sockets API you already know: socket(), bind(), listen(), accept(), connect(), read(), write(). The difference is the address family. Where a TCP socket is AF_INET and addressed by IP and port, a vsock socket is AF_VSOCK and addressed by a context ID and a port. There is no IP layer, no routing table, no ARP, no DNS — none of the machinery that makes a network a network. It is a direct, point-to-point pipe between the host and one specific guest, carried by the virtio transport rather than by any wire or virtual NIC.

The "virtio" in virtio-vsock is the same paravirtualization story as virtio-net or virtio-blk: instead of emulating real hardware, the hypervisor and guest cooperate over shared-memory ring buffers. The guest kernel has a vsock transport driver, the host VMM (Firecracker, QEMU, etc.) implements the other half, and bytes move between them without ever being framed as Ethernet or IP packets. From an application's perspective it is just a stream socket that happens to reach across the VM boundary. From a security perspective it is a channel that exists whether or not the guest has any network configured — which turns out to be the whole point.

The one-line mental model: vsock is a socket family (AF_VSOCK) for host-to-guest talk that uses (CID, port) instead of (IP, port) and rides the virtio transport instead of a network. No guest NIC required — it works even on a VM with networking entirely disabled.

The addressing model: CID and port

A vsock endpoint is identified by a context ID (CID) and a port number. The CID names the machine; the port names the service on it, just like a TCP port. A few CID values are reserved and worth memorizing. CID 0 is hypervisor-reserved. CID 1 is the local/loopback context. CID 2 is always the host — when a guest wants to reach the host, it connects to CID 2. Everything from CID 3 upward is available to name individual guests; each VM is assigned a unique guest CID by the hypervisor (in Firecracker, 3 is the conventional choice for the single guest).

So the addressing reads naturally once you have those constants. A process inside the guest that wants to call the host opens an AF_VSOCK socket and connects to (CID 2, some port). A process on the host that wants to call into the guest connects to (the guest's CID, some port). A guest agent that wants to accept connections binds and listens on (its own CID, a chosen port) and the host dials that port. There is no address resolution and no ambiguity: the CID space is tiny, the host is always 2, and there is exactly one guest on the other end of a given microVM's transport.

Because the CID is assigned by the hypervisor and the transport is point-to-point, there is no way for one guest to address another guest over vsock — there is no shared vsock segment the way there can be a shared Layer 2 bridge. A guest can talk to the host (CID 2) and to itself; that's it. For a multi-tenant sandbox platform that property is a gift: the control channel is structurally incapable of becoming a cross-tenant path.

Why vsock is ideal for a sandbox control channel

Consider what you actually need from a channel into a sandbox. You need to reach the guest reliably from the host. You need it to work the instant the guest's userspace is up, before — and independently of — any network configuration. You need it to not widen the guest's attack surface. And for untrusted code, you want a guest whose network you can lock down to default-deny egress without breaking your own ability to manage it. vsock satisfies all four because it is orthogonal to networking entirely.

A guest agent listens on a vsock port. The host connects to it. No TCP port is exposed on any interface the guest's code can see or bind to; no IP address has to be assigned to the guest before the channel works; no firewall rule has to carve out a management exception. You can run a sandbox with its network namespace fully clamped down — no egress, no NIC even — and the host can still exec into it, push and pull files, and watch it for readiness, because the control plane never touched the network in the first place. The networking and the control channel are completely decoupled, which is exactly the separation of concerns you want when the workload is hostile by assumption. (We cover the network-isolation half of that story in /blog/firecracker-networking-explained.)

  • Works with zero guest networking — vsock rides the virtio transport, so the channel is up as soon as guest userspace is, even on a VM with no NIC and no IP.
  • No exposed TCP port — nothing listens on a routable interface, so there's no port for the guest's own code (or an attacker) to discover, hijack, or collide with.
  • Decoupled from egress policy — you can run default-deny networking on the guest and still manage it, because control traffic never goes through the network stack.
  • Structurally point-to-point — a guest can reach only the host (CID 2), never a sibling guest, so the control channel can't become a cross-tenant path.
  • TCP-over-tap, by contrast — needs the guest to have an IP, a route, and a listening socket; couples management to networking; and adds a port you must firewall and a config step that can fail before the guest is reachable.
vsock is a transport, not an auth layer. Anything that can open an AF_VSOCK socket on the host can reach a guest's listening port, and any guest process can dial the host on CID 2. Treat the channel as plumbing and put real authentication on top of it — PandaStack's guest agent, for instance, validates injected ed25519 keys rather than trusting the link itself.

How Firecracker exposes vsock: a Unix socket on the host

Here's the elegant part of Firecracker's design. Inside the guest, vsock looks like vsock — AF_SOCK sockets on (CID, port). But on the host side, Firecracker does not ask you to speak AF_VSOCK at all. It bridges the guest's vsock onto a Unix domain socket (UDS) on the host filesystem. You configure the device with a guest CID and a path to a host-side UDS, and Firecracker handles the translation between the two.

{
  "vsock": {
    "guest_cid": 3,
    "uds_path": "/var/lib/pandastack/vms/<id>/vsock.sock"
  }
}

The bridging convention is worth knowing because it shapes how your host code connects. When a host process wants to reach a port inside the guest, it does not connect to a vsock address — it connects to the Unix socket at uds_path, and then, as the very first line it writes, sends CONNECT <port>\n to tell Firecracker which guest vsock port to wire it through to. Firecracker forwards from there, and the rest of the conversation is an ordinary byte stream. The reverse direction works too: when the guest connects out to the host (CID 2) on port N, Firecracker accepts it on a companion socket named uds_path_N on the host. In both directions, the host never touches AF_VSOCK — it speaks plain Unix sockets, which is delightfully easy to wire into any language and any tooling.

Connecting from the host

Because the host side is just a Unix socket plus a one-line CONNECT handshake, you can reach a guest vsock port with tools you already have. With socat, connect to the UDS and write the handshake before your payload:

# Reach guest vsock port 1024 via Firecracker's host-side Unix socket.
# socat connects to the UDS; we send the CONNECT handshake, then talk.
( printf 'CONNECT 1024\n'; cat ) | \
  socat - UNIX-CONNECT:/var/lib/pandastack/vms/<id>/vsock.sock
# Firecracker replies 'OK <assigned_host_port>' on success, then forwards
# the byte stream straight through to whatever is listening in the guest.

The same thing in Python is just stdlib sockets — no AF_VSOCK on the host at all, since Firecracker did the translation. You connect to the Unix socket, send the CONNECT line, read the OK acknowledgement, and then the socket is a transparent pipe to the guest:

import socket

UDS = "/var/lib/pandastack/vms/<id>/vsock.sock"
GUEST_PORT = 1024

s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
s.connect(UDS)

# Firecracker's handshake: ask it to wire us to a guest vsock port.
s.sendall(f"CONNECT {GUEST_PORT}\n".encode())
ack = s.recv(64)              # e.g. b"OK 12345\n"
assert ack.startswith(b"OK"), ack

# From here it's a plain byte stream to the guest agent.
s.sendall(b"ping\n")
print(s.recv(1024))
s.close()

Listening inside the guest

Inside the guest, you do speak real vsock. A guest agent binds an AF_VSOCK socket and listens; binding to VMADDR_CID_ANY accepts from the host. This is the loop a guest agent runs — accept a connection, read a request, do the work, write a reply:

import socket

PORT = 1024

# AF_VSOCK is real here — we're inside the guest.
srv = socket.socket(socket.AF_VSOCK, socket.SOCK_STREAM)
srv.bind((socket.VMADDR_CID_ANY, PORT))   # accept from the host (CID 2)
srv.listen()

while True:
    conn, (peer_cid, peer_port) = srv.accept()
    req = conn.recv(4096)
    # ... dispatch: exec a command, read/write a file, report readiness ...
    conn.sendall(b"pong\n")
    conn.close()

That's the entire shape of a guest agent's transport. Everything richer — framing a request protocol, multiplexing exec streams, authenticating the caller — is built on top of this accept loop. The transport itself stays this small.

How PandaStack's guest agent uses it

Every PandaStack microVM ships a tiny guest agent, pandastack-init, whose job is to be the inside half of the control channel. It listens on a vsock port and the host's per-host agent dials Firecracker's UDS to reach it. Over that one link the host drives the operations a sandbox needs: exec (run a command, stream stdout/stderr/exit code back), filesystem operations (read, write, stat, list a directory), and — critically — a readiness ping that tells the control plane the exact moment the guest is up and able to accept work.

Putting all of that on vsock rather than TCP-over-tap is what lets a sandbox's networking be locked down independently. The guest can run with default-deny egress, or be handed a network only for the workload's own outbound needs, while the management plane keeps working because it never depended on the network. And because vsock is point-to-point to the host, the control channel can't be reached by a sibling sandbox. The deep mechanics of exec and filesystem over this bridge live in PandaStack's guest-bridge internals, but the transport underneath all of it is exactly the accept loop above. PandaStack is open source under Apache-2.0, so you can read the pandastack-init source and the host-side dialer and watch the CONNECT handshake go by.

The readiness ping and the v1.16 UDS override

The readiness ping deserves its own note because it intersects with how PandaStack actually boots. There is no warm pool of idle VMs: every create restores a baked Firecracker snapshot, which is what gets a fresh sandbox to a usable state with a p50 around 179ms and a p99 around 203ms (a true first cold boot, before a snapshot exists, is roughly 3s). The control plane wants to return a sandbox to the caller the instant it's genuinely ready — and "ready" is best answered by the guest itself over vsock, not by guessing from the outside.

Firecracker v1.16 added the ability to override the vsock device's host-side UDS path at snapshot restore. That matters here because a snapshot is restored many times into many different sandboxes, and each restored instance needs its own host-side socket path rather than the one frozen into the snapshot at bake time. The override lets each restore point the vsock bridge at a fresh per-sandbox UDS, so the host can do a clean per-sandbox readiness ping on the restored guest over its own socket. It's a small VMM feature, but it's the piece that makes a snapshot-restore boot path and a vsock readiness check compose cleanly. (The snapshot-restore side of the story is in /blog/how-firecracker-boots-fast.)

The whole channel, end to end

Put it together and the control path is short and deliberately boring. Inside the guest, pandastack-init binds an AF_VSOCK socket and listens. Firecracker bridges that vsock to a Unix domain socket on the host. The per-host agent connects to that UDS, sends CONNECT <port>, and from there has a plain byte stream to the guest over which it runs exec, moves files, and pings for readiness. No TCP port is exposed, no guest network is required, and a sibling sandbox can never reach the channel. The guest's networking — locked down or wide open — is an entirely separate decision, which is exactly what you want when the code inside is untrusted. vsock isn't glamorous; it's the right amount of plumbing, and getting the plumbing right is most of what makes a sandbox a sandbox.

Frequently asked questions

What is virtio-vsock and how is it different from TCP?

virtio-vsock is a Linux socket address family (AF_VSOCK) for communication between a hypervisor host and its guest VMs. You use it through the normal sockets API, but instead of addressing endpoints by IP and port like TCP (AF_INET), you address them by a context ID (CID) and a port. There's no IP layer, routing, ARP, or DNS — it's a direct point-to-point pipe carried by the virtio transport. The practical upshot is that vsock works even when the guest has no network interface configured at all, which makes it ideal for a host-to-guest control channel.

What do the vsock CID values mean?

A vsock endpoint is named by a context ID (CID) plus a port. CID 0 is reserved for the hypervisor, CID 1 is local/loopback, and CID 2 always means the host — a guest reaches the host by connecting to (CID 2, port). CIDs 3 and up name individual guests; the hypervisor assigns each VM a unique guest CID (Firecracker conventionally uses 3 for its single guest). Because the CID space is tiny and the transport is point-to-point, a guest can only address the host or itself — never a sibling guest.

Why use vsock instead of TCP-over-tap for a sandbox control channel?

vsock is orthogonal to networking, so a control channel built on it doesn't depend on the guest having an IP, a route, or a listening TCP port. The host can exec into the guest, move files, and ping it for readiness even when the guest's network is locked down to default-deny egress or disabled entirely, because the management plane never touches the network stack. TCP-over-tap, by contrast, requires the guest to be networked and to expose a port, couples management to networking, adds an attack surface and a firewall exception, and can fail before the guest is reachable. vsock is also structurally point-to-point to the host, so it can't become a cross-tenant path.

How does Firecracker expose vsock to the host?

Inside the guest, vsock is real AF_VSOCK. On the host, Firecracker bridges it to a Unix domain socket — you configure the vsock device with a guest_cid and a uds_path. To reach a guest port, a host process connects to that Unix socket and sends 'CONNECT <port>\n' as its first line; Firecracker replies with OK and then forwards a transparent byte stream to whatever is listening in the guest. When the guest connects out to the host (CID 2) on port N, Firecracker accepts it on a companion socket named uds_path_N. So host-side code never has to speak AF_VSOCK — it uses plain Unix sockets.

How does PandaStack use vsock for its guest agent?

Every PandaStack microVM runs a small guest agent, pandastack-init, that binds a vsock port and listens. The per-host agent dials Firecracker's host-side Unix socket to reach it and drives exec, filesystem operations, and a readiness ping over that one link — with authentication via injected ed25519 keys layered on top, since vsock itself is just transport. Because PandaStack creates sandboxes by restoring a baked snapshot many times, it relies on Firecracker v1.16's ability to override the vsock UDS path at restore so each restored sandbox gets its own host-side socket for a clean per-sandbox readiness ping.

Run code in a microVM in one API call.

49ms p50 cold start. Fork, snapshot, and scale to zero.

Start free
Written by Ajay Kumar, Founder, PandaStack.