knaller (/ˈknalɐ/) — a Go library and CLI for running Firecracker microVMs.
- start/stop/pause/resume/snapshot microvms
- set limits on CPU, memory, network bandwidth, disk bandwidth and disk IOPS
- start new microvm from an existing snapshot
- two networking modes:
- rootless (default): user-space networking via pasta — no privileges required
- direct: per-VM kernel network namespace — needed where pasta breaks KVM (e.g. Kubernetes pods that already run in a user namespace)
- adopt running VMs across supervisor restarts (
AdoptVM) — useful for daemons that outlive their VMs - raw-disk mode: hand a pre-attached block device to firecracker (NBD, LVM, etc.) and skip the rootfs copy
- raw snapshots: pause/snapshot/resume a VM with a
whilePausedhook for callers managing the disk lifecycle out of band
- Linux with KVM
- Firecracker binary
- For rootless mode: pasta
- For direct mode (not rootless):
iproute2,nftables,util-linux(nsenter) onPATH. The supervisor needsCAP_NET_ADMIN+CAP_NET_RAW+CAP_SYS_ADMINin the host network namespace — in practice that means running as root, or as a Kubernetes pod withhostNetwork: trueplus a privileged security context.e2fsprogsis also required if you useConfig.RootFSSize. If you setConfig.EscapeCgroupSlice, the supervisor must additionally be able to write under/sys/fs/cgroup. - Podman for building the guest rootfs image
CLI:
go install github.com/benben/knaller/cmd/knaller@v0.0.1
Library:
go get github.com/benben/knaller
sudo pacman -S passt
make create-guest
make download-kernel
knaller start --name my-vm
copy pasta the ssh command into another terminal window and connect with password root.
$ knaller help
Usage: knaller <command> [flags]
Commands:
start Start a microVM (connect via SSH)
stop Stop a running microVM
rm Remove a stopped microVM
pause Pause a running microVM
resume Resume a paused microVM
snapshot Create a VM snapshot
snapshot ls List all snapshots
snapshot delete Delete a snapshot
list List running microVMs
ls Alias for list
version Print version information
help Show this help
Use knaller <command> --help for more information about a command.$ knaller start --help
Usage of start:
-cpus float
vCPUs (e.g. 0.5 = 1 vCPU at 50% CPU quota) (default 1)
-detach
Run VM in the background
-disk-bandwidth int
Disk bandwidth limit in MB/s (0 = unlimited)
-disk-iops int
Disk I/O operations per second limit (0 = unlimited)
-firecracker string
Firecracker binary path (default "firecracker")
-from-snapshot string
Restore from snapshot ID
-kernel string
Kernel image path (default "~/.local/share/knaller/vmlinux")
-mem int
Memory in MiB (default 1024)
-name string
VM name (required)
-network-bandwidth float
Network bandwidth limit in Mbps per direction (0 = unlimited)
-pasta string
pasta binary path (default "pasta")
-port HOST:GUEST
Port forwarding HOST:GUEST (repeatable)
-rootfs string
Base rootfs path (default "~/.local/share/knaller/rootfs.ext4")
-verbose
Show serial console and process output
package main
import (
"context"
"fmt"
"github.com/benben/knaller"
)
func main() {
ctx := context.Background()
// Start a microVM — knaller handles starting Firecracker,
// configuring the VM, and booting it.
vm, err := knaller.Run(ctx, &knaller.Config{
Name: "myvm",
Kernel: "/path/to/vmlinux",
RootFS: "/path/to/rootfs.ext4",
CPUs: 2,
Memory: 2048,
})
if err != nil {
panic(err)
}
defer vm.Cleanup()
// Connect via SSH
fmt.Printf("VM started: ssh -p %d root@localhost\n", vm.Port)
// Block until VM exits
vm.Wait()
}knaller.RunDirect is a drop-in replacement for knaller.Run that puts the
VM inside a per-VM kernel network namespace instead of pasta's user+network
namespace. Use it where pasta isn't viable — most commonly when your supervisor
already runs in a user namespace (Kubernetes pods, rootless containers), since
KVM's KVM_CREATE_VM ioctl returns EPERM from inside a user namespace.
What direct mode sets up per VM:
- a kernel netns named
kn-<hash>containing the firecracker TAP device - a veth pair (
vh-<hash>host-side /vg-<hash>guest-side) with a /30 in172.20.0.0/16, plumbed on both sides - an in-netns
knaller_box_natnft table that DNATs port 22 (and anyConfig.Ports) to the guest IP, and SNATs new outbound flows to the veth-guest IP so siblings on the host see distinct sources - a host-side
knaller_hostnft table that DNATshost:<sshPort>(and forwarded ports) to the per-VM veth-guest IP, masquerades outbound flows so the upstream NIC sees the host's IP, and rejects guest→RFC1918 reachability by default (DNS to169.254.169.253is allowed; everything else in10/8,192.168/16,100.64/10,224/4,169.254/16and the knaller veth supernet itself is rejected) - the firecracker process is
nsenter'd into the netns, so/proc/<pid>/cmdlineshowsfirecracker(notnsenter) and discovery/adoption can match on the command line
Required capabilities and binaries:
- Direct mode is not rootless. The supervisor needs
CAP_NET_ADMIN(forip link,nft, sysctls) plusCAP_SYS_ADMIN(to create the kernel netns andnsenterinto it) plusCAP_NET_RAW, all in the host network namespace. In Kubernetes that'shostNetwork: trueplus a privileged security context (or the explicit capability set); on a bare host it means running as root or granting the equivalent file capabilities to the binary. ip(iproute2),nft(nftables),nsenter(util-linux) onPATH.- If you set
Config.EscapeCgroupSlice, the supervisor must also be able to write into/sys/fs/cgroup(i.e. the host cgroupv2 hierarchy must be mounted RW and visible to the process).
vm, err := knaller.RunDirect(ctx, &knaller.Config{
Name: "myvm",
Kernel: "/path/to/vmlinux",
RootFS: "/path/to/rootfs.ext4",
CPUs: 2,
Memory: 2048,
})Set Config.RawDiskPath to a block device or file the caller manages out of
band (e.g. an NBD device backed by a content-addressed cache, or an LVM
logical volume). knaller does not copy, truncate, or resize the device,
and Cleanup() leaves it alone — the caller owns the disk lifecycle. This
also disables the per-VM rootfs copy, so VM start time is bounded by the
firecracker handshake instead of by cp + resize2fs.
vm, err := knaller.RunDirect(ctx, &knaller.Config{
Name: "myvm",
Kernel: "/path/to/vmlinux",
RawDiskPath: "/dev/nbd0",
CPUs: 2,
Memory: 2048,
})A long-running supervisor that gets restarted (e.g. a Kubernetes DaemonSet)
can re-attach to VMs from its previous lifetime with AdoptVM. Persist
vm.Name, vm.SocketPath, and vm.PID somewhere durable; on restart, call:
vm, err := knaller.AdoptVM(name, socketPath, pid)AdoptVM verifies the firecracker is alive (kill -0 pid) and that its
API socket still answers (GetInfo with a 2 s timeout). The returned *VM
has cmd == nil; Wait switches to polling /proc/<pid> and Kill falls
back to syscall.Kill(pid, SIGKILL). Pair this with
Config.EscapeCgroupSlice to move the firecracker process into a host-level
cgroupv2 slice on launch so it survives the supervisor's container being
restarted.
CreateSnapshotRaw is like CreateSnapshot but skips the rootfs copy and
the drive-path patching, so it composes with RawDiskPath. It returns
timing for the paused-window so callers can attribute pause-tail latency,
and accepts a whilePaused callback that runs after the firecracker
state+memory dump is written but before the VM is resumed — useful for
flushing a dirty queue or copying an external manifest into snapDir.
res, err := knaller.CreateSnapshotRaw(ctx, "myvm", os.Stderr, func(snapDir string) error {
return copyManifestInto(snapDir)
})On restore (RunDirect with SnapshotID + RawDiskPath), LoadSnapshot
is followed by PatchDrive so the new RawDiskPath replaces whatever
device path was baked into the state file.