Virtual machines
#Mounting into a VM
Run the server on the host. Create the mount in the virtual machine (VM) guest.
Linux kernels include 9P and Network File System (NFS) clients. mountx serves both protocols over a normal Transmission Control Protocol (TCP) socket. Run one mount command in the guest to expose your driver as a directory. You do not need to install an agent, shared-directory daemon, or guest additions.
#Which transport?
| your VM | use | why |
|---|---|---|
| QEMU | 9P | its default networking reaches the host already |
| Firecracker | NFSv4.1 | its guest kernels have no 9P client |
| Anything else | either | whichever client the guest kernel has |
Serving does not need root. See Do I need root?. The mount command in the guest needs root.
#QEMU
QEMU default networking routes 10.0.2.2 to the host loopback interface. You do not need a tap device, bridge, host ip commands, or root on the host. Keep the server bound to 127.0.0.1 so other hosts on the network cannot reach it.
#1. Serve on the host
import { createP9Server } from "mountx/9p";
import { createMemoryDriver } from "mountx/drivers/memory";
const driver = createMemoryDriver();
const server = await createP9Server(driver, { port: 5640 }).listen();
console.log(`serving 9P on 127.0.0.1:${server.port}`);Keep this process running. The example uses the memory driver, but any driver works.
#2. Boot a guest
Any Linux with a 9P client, which is nearly all of them. Alpine's virt image is a good tiny one to start with:
qemu-system-x86_64 -enable-kvm -m 512 -nographic \
-cdrom alpine-virt-3.21.3-x86_64.iso \
-nic user-nic user is the default networking, spelled out. Log in as root with no password.
#3. Mount, inside the guest
mkdir -p /mnt/x
mount -t 9p -o trans=tcp,port=5640,version=9p2000.L 10.0.2.2 /mnt/xThe client setup is complete. ls /mnt/x now reads your driver.
The command uses these three settings:
trans=tcp,port=: 9P over a socket. The address is the mount source and the port is its own option, which is why there is no:5640after10.0.2.2.version=9p2000.L: the Linux dialect, the only one mountx speaks. Note the capitalP.- No
modprobe:mount -t 9ploads the modules itself. The cache and access defaults are already what you want.
Caution
The source must be a dotted-quad IPv4 address. The kernel accepts only this form. 10.0.2.2 works, but localhost and ::1 do not. More.
Note
If the guest has no network yet. Minimal images sometimes boot with eth0 down. One line gets you to the host:
ip addr add 10.0.2.15/24 dev eth0 && ip link set eth0 upTo give the guest general network access, also run ip route add default via 10.0.2.2. The mount does not need this route.
#Making it faster
msize is the main bulk I/O setting. It defaults to 128 KiB; 1 MiB is the maximum both mountx and the kernel accept:
mount -t 9p -o trans=tcp,port=5640,version=9p2000.L,msize=1048576 10.0.2.2 /mnt/xSee Tuning for the rest. Before you increase the cache mode, read cache=none. 9P cannot invalidate a guest cache. Any mode above none assumes that only the guest changes the driver.
#This is not -virtfs
QEMU also uses 9P for a different feature. Quick Emulator (QEMU) includes a 9P server behind -virtfs and -fsdev. It can export only a directory on the host. You cannot point it at mountx.
So mountx connects over TCP instead. You give up virtio's shared memory and get the thing you wanted: a filesystem that is your JavaScript rather than a host directory.
#Firecracker
Firecracker cannot use 9P. It has no virtio-9p device. Its published guest kernels do not include the 9P client. No Firecracker option adds it.
Those kernels include the NFS client and NFSv4.1, but not NFSv3. Use NFSv4.1 with Firecracker.
Kernels vary, so check yours if you are not using Firecracker's own:
zcat /proc/config.gz 2>/dev/null || cat "/boot/config-$(uname -r)"and grep for CONFIG_NET_9P, CONFIG_NFS_V4_1 and CONFIG_NFS_V3.
#1. Set up a tap device
Firecracker does not provide user-mode networking. Create a tap device for the guest network. QEMU does not need this step. This is the only step that needs host privileges:
ip tuntap add dev tap0 mode tap
ip addr add 172.20.0.1/24 dev tap0
ip link set tap0 upThe host is 172.20.0.1, the guest will be 172.20.0.2.
#2. Serve on the host
The guest arrives from the tap subnet rather than loopback, so the server has to be told to accept it:
import { createNfsServer } from "mountx/nfs";
import { createMemoryDriver } from "mountx/drivers/memory";
const server = await createNfsServer(createMemoryDriver(), {
port: 2049,
host: "172.20.0.1", // the tap address, not 0.0.0.0
allowRemote: true, // the guest is not on loopback
}).listen();Caution
allowRemote: true exposes your driver to all clients that can reach the socket. NFS does not authenticate them. Bind to the tap address rather than 0.0.0.0, and treat the network as your only boundary. Same posture as the NFS page.
#3. Boot the guest
Let the kernel configure the network with an ip= boot argument and the guest needs no network commands of its own:
{
"boot-source": {
"kernel_image_path": "vmlinux",
"boot_args": "console=ttyS0 reboot=k panic=1 pci=off ip=172.20.0.2::172.20.0.1:255.255.255.0::eth0:off"
},
"drives": [
{
"drive_id": "rootfs",
"path_on_host": "rootfs.ext4",
"is_root_device": true,
"is_read_only": false
}
],
"network-interfaces": [{ "iface_id": "eth0", "host_dev_name": "tap0" }],
"machine-config": { "vcpu_count": 2, "mem_size_mib": 1024 }
}firecracker --no-api --config-file vm.json#4. Mount, inside the guest
mount -t nfs4 -o vers=4.1,port=2049,proto=tcp,sec=sys,hard,addr=172.20.0.1,clientaddr=172.20.0.2 \
172.20.0.1:/ /mnt/xvers=4.1 is what mountx implements and what that kernel has. NFSv4.1 does not need rpcbind or a MOUNT service. Therefore, it is easier to serve to a VM than NFSv3.
Note
No nfs-common needed in the guest either. Firecracker's root image does not include mount.nfs, but the command above still works. The explicit addr= and clientaddr= values need no helper resolution. Therefore, mount calls the kernel directly. That is what lets a microVM image stay empty.
#Other hypervisors
For other hypervisors, serve over TCP and use a protocol client that the guest kernel provides.
- Cloud Hypervisor, or QEMU with a tap or bridge: the same 9P recipe, with your host's address on that network in place of
10.0.2.2. Still a dotted-quad literal. - WSL2, Multipass, Lima, UTM, VirtualBox, Proxmox: a Linux guest with a route to the host is all it takes. 9P if the kernel has it, NFSv4.1 if not.
- macOS guests: NFS only; no BSD kernel has a 9P client.
Apply these two rules on every hypervisor:
- Bind deliberately. Both servers bind
127.0.0.1and refuse non-loopback peers by default. Reaching them from a guest on a real network meansallowRemote: true, which exposes the driver to everything that can reach the socket. - The guest is a second client. Both servers accept several connections. 9P shares one byte-range lock table across all connections. Therefore, the host sees a lock taken in the guest. However, renames are serialized only within each connection. Before the host and guest mount one driver at the same time, read Serving more than one client.
#Next
- 9P:
trans=tcp,msize, and whycache=noneis the default. - NFS: v3 and v4.1, and what NFS gives up that 9P does not.
- Tuning: the settings to evaluate after the mount works.
- Troubleshooting: including why not to run a binary that lives on your own mount.