12 KiB
Custom libkrunfw VM Runtime
Status: Experimental and work in progress (WIP). The VM compute driver is under active development and may change.
Overview
The OpenShell gateway uses libkrun via the
openshell-driver-vm compute driver to boot a lightweight microVM per sandbox.
Each VM runs on Apple Hypervisor.framework (macOS) or KVM (Linux), with the guest
kernel embedded inside libkrunfw.
The stock libkrunfw from Homebrew ships a minimal kernel without bridge,
netfilter, or conntrack support. That is insufficient for the sandbox supervisor's
per-sandbox network namespace primitives (veth pair + iptables, see
crates/openshell-sandbox/src/sandbox/linux/netns.rs). The custom libkrunfw
runtime adds bridge, iptables/nftables, and conntrack support to the guest
kernel.
The driver is spawned by openshell-gateway as a subprocess, talks to it over a
Unix domain socket (compute-driver.sock) with the
openshell.compute.v1.ComputeDriver gRPC surface, and manages per-sandbox
microVMs. The runtime (libkrun + libkrunfw + gvproxy) and the sandbox rootfs are
embedded directly in the driver binary — no sibling files required at runtime.
Architecture
graph TD
subgraph Host["Host (macOS / Linux)"]
GATEWAY["openshell-gateway<br/>(compute::vm::spawn)"]
DRIVER["openshell-driver-vm<br/>(compute-driver.sock)"]
EMB["Embedded runtime (zstd)<br/>libkrun · libkrunfw · gvproxy<br/>+ sandbox rootfs.tar.zst"]
GVP["gvproxy (per sandbox)<br/>virtio-net · DHCP · DNS"]
GATEWAY <-->|gRPC over UDS| DRIVER
DRIVER --> EMB
DRIVER -->|spawns one per sandbox| GVP
end
subgraph Guest["Per-sandbox microVM"]
SBXINIT["/srv/openshell-vm-sandbox-init.sh"]
SBX["/opt/openshell/bin/openshell-sandbox<br/>(PID 1, supervisor)"]
SBXINIT --> SBX
end
DRIVER -- "fork + krun_start_enter" --> SBXINIT
GVP -- "virtio-net eth0" --> Guest
SBX -.->|"outbound ConnectSupervisor<br/>gRPC stream"| GATEWAY
CLIENT["openshell-cli"] -->|SSH over supervisor relay| GATEWAY
The driver spawns one microVM per sandbox. Each VM boots directly into
openshell-sandbox as PID 1. All gateway ingress — SSH, exec, connect — rides
the supervisor-initiated ConnectSupervisor gRPC stream opened from inside the
guest back out to the gateway, so gvproxy is configured with -ssh-port -1 and
never binds a host-side TCP listener.
Embedded Runtime
openshell-driver-vm embeds the VM runtime libraries and the sandbox rootfs as
zstd-compressed byte arrays, extracting on demand:
~/.local/share/openshell/vm-runtime/<version>/ # libkrun / libkrunfw / gvproxy
├── libkrun.{dylib,so}
├── libkrunfw.{5.dylib,so.5}
└── gvproxy
<state-dir>/sandboxes/<sandbox-id>/rootfs/ # per-sandbox rootfs
Old runtime cache versions are cleaned up when a new version is extracted.
Sandbox rootfs preparation
The rootfs tarball the driver embeds starts from the same minimal Ubuntu base used across the project, and is rewritten into a supervisor-only sandbox guest during extraction:
- k3s state and Kubernetes manifests are stripped out
/srv/openshell-vm-sandbox-init.shis installed as the guest entrypoint- the guest boots directly into
openshell-sandbox— no k3s, no kube-proxy, no CNI plugins
See crates/openshell-driver-vm/src/rootfs.rs for the rewrite logic and
crates/openshell-driver-vm/scripts/openshell-vm-sandbox-init.sh for the init
script that gets installed.
--internal-run-vm helper
The driver binary has two modes: the default mode is the gRPC server; when
launched with --internal-run-vm it becomes a per-sandbox launcher. The driver
spawns one launcher per sandbox as a subprocess, which in turn starts gvproxy
and calls krun_start_enter to boot the guest. Keeping the launcher in the
same binary means the driver ships a single artifact for both roles.
Network Plane
The driver launches a dedicated gvproxy instance per sandbox to provide the
guest's networking plane:
- virtio-net backend over a Unix SOCK_STREAM (Linux) or SOCK_DGRAM (macOS vfkit)
socket, which surfaces as
eth0inside the guest - DHCP server + default router (192.168.127.1 / 192.168.127.2) for the guest's udhcpc client
- DNS for host aliases: the guest init script seeds
/etc/hostswithhost.openshell.internal→ 192.168.127.1, while leaving gvproxy's legacyhost.containers.internal/host.docker.internalresolution intact
The -listen API socket and the -ssh-port forwarder are both intentionally
omitted. After the supervisor-initiated relay migration the driver does not
enqueue any host-side port forwards, and the guest's SSH listener lives on a
Unix socket at /run/openshell/ssh.sock inside the VM that is reached over the
outbound ConnectSupervisor gRPC stream. Binding a host listener would race
concurrent sandboxes for port 2222 and surface a misleading "sshd is reachable"
endpoint.
The sandbox supervisor's per-sandbox netns (veth pair + iptables) branches off of this plane. libkrun's built-in TSI socket impersonation would not satisfy those kernel-level primitives, which is why we need the custom libkrunfw.
Process Lifecycle Cleanup
openshell-driver-vm installs a cross-platform "die when my parent dies"
primitive (procguard) in every link of the spawn chain so that killing
openshell-gateway (SIGTERM, SIGKILL, or crash) reaps the driver, per-sandbox
launcher, gvproxy, and the libkrun worker:
- Linux:
nix::sys::prctl::set_pdeathsig(SIGKILL) - macOS / BSDs:
smol-rs/pollingwithProcessOps::Exiton a helper thread - gvproxy (the one non-Rust child) gets
PR_SET_PDEATHSIGviapre_execon Linux, and is SIGTERM'd from the launcher's procguard cleanup callback on macOS
See crates/openshell-driver-vm/src/procguard.rs for the implementation and
tasks/scripts/vm/smoke-orphan-cleanup.sh (exposed as
mise run vm:smoke:orphan-cleanup) for the regression test that covers both
SIGTERM and SIGKILL paths.
Runtime Provenance
At driver startup the loaded runtime bundle is logged with:
- Library paths and SHA-256 hashes
- Whether the runtime is custom-built or stock
- For custom runtimes: libkrunfw commit, kernel version, build timestamp
This information is sourced from provenance.json (generated by the build
script) and makes it straightforward to correlate sandbox VM behavior with a
specific runtime artifact.
Build Pipeline
graph LR
subgraph Source["crates/openshell-vm/runtime/"]
KCONF["kernel/openshell.kconfig<br/>Kernel config fragment"]
end
subgraph Linux["Linux CI (build-libkrun.sh)"]
BUILD_L["Build kernel + libkrunfw.so + libkrun.so"]
end
subgraph macOS["macOS CI (build-libkrun-macos.sh)"]
BUILD_M["Build libkrunfw.dylib + libkrun.dylib"]
end
subgraph Output["vm-runtime-<platform>.tar.zst"]
LIB_SO["libkrunfw.so + libkrun.so + gvproxy<br/>(Linux)"]
LIB_DY["libkrunfw.dylib + libkrun.dylib + gvproxy<br/>(macOS)"]
end
KCONF --> BUILD_L --> LIB_SO
KCONF --> BUILD_M --> LIB_DY
The vm-runtime-<platform>.tar.zst artifact is consumed by
openshell-driver-vm's build.rs, which embeds the library set into the
binary via include_bytes!(). Setting OPENSHELL_VM_RUNTIME_COMPRESSED_DIR
at build time (wired up by crates/openshell-driver-vm/start.sh) points the
build at the staged artifacts.
Kernel Config Fragment
The openshell.kconfig fragment enables these kernel features on top of the
stock libkrunfw kernel:
| Feature | Key Configs | Purpose |
|---|---|---|
| Network namespaces | CONFIG_NET_NS, CONFIG_NAMESPACES |
Sandbox netns isolation |
| veth | CONFIG_VETH |
Sandbox network namespace pairs |
| Bridge device | CONFIG_BRIDGE, CONFIG_BRIDGE_NETFILTER |
Bridge support + iptables visibility into bridge traffic |
| Netfilter framework | CONFIG_NETFILTER, CONFIG_NETFILTER_ADVANCED, CONFIG_NETFILTER_XTABLES |
iptables/nftables framework |
| xtables match modules | CONFIG_NETFILTER_XT_MATCH_CONNTRACK, _COMMENT, _MULTIPORT, _MARK, _STATISTIC, _ADDRTYPE, _RECENT, _LIMIT |
Sandbox supervisor iptables rules |
| Connection tracking | CONFIG_NF_CONNTRACK, CONFIG_NF_CT_NETLINK |
NAT state tracking |
| NAT | CONFIG_NF_NAT |
Sandbox egress DNAT/SNAT |
| iptables | CONFIG_IP_NF_IPTABLES, CONFIG_IP_NF_FILTER, CONFIG_IP_NF_NAT, CONFIG_IP_NF_MANGLE |
Masquerade and compat |
| nftables | CONFIG_NF_TABLES, CONFIG_NFT_CT, CONFIG_NFT_NAT, CONFIG_NFT_MASQ, CONFIG_NFT_NUMGEN, CONFIG_NFT_FIB_IPV4 |
nftables path |
| IP forwarding | CONFIG_IP_ADVANCED_ROUTER, CONFIG_IP_MULTIPLE_TABLES |
Sandbox-to-host routing |
| Traffic control | CONFIG_NET_SCH_HTB, CONFIG_NET_CLS_CGROUP |
QoS |
| Cgroups | CONFIG_CGROUPS, CONFIG_CGROUP_DEVICE, CONFIG_MEMCG, CONFIG_CGROUP_PIDS |
Sandbox resource limits |
| TUN/TAP | CONFIG_TUN |
CNI plugin compatibility; inherited from the shared kconfig, not exercised by the driver. |
| Dummy interface | CONFIG_DUMMY |
Fallback networking |
| Landlock | CONFIG_SECURITY_LANDLOCK |
Sandbox supervisor filesystem sandboxing |
| Seccomp filter | CONFIG_SECCOMP_FILTER |
Sandbox supervisor syscall filtering |
See crates/openshell-vm/runtime/kernel/openshell.kconfig for the full
fragment with inline comments explaining why each option is needed.
Verification
- Capability checker (
check-vm-capabilities.sh): runs inside a sandbox VM to verify kernel capabilities. Produces pass/fail results for each required feature. - Orphan-cleanup smoke test:
mise run vm:smoke:orphan-cleanupasserts that killing the gateway leaves zero driver, launcher, gvproxy, or libkrun survivors.
Build Commands
# One-time setup: download pre-built runtime (~30s)
mise run vm:setup
# Start openshell-gateway with the VM compute driver
mise run gateway:vm
# With custom kernel (optional, adds ~20 min)
FROM_SOURCE=1 mise run vm:setup
# Wipe everything and start over
mise run vm:clean
See crates/openshell-driver-vm/README.md for the full driver workflow,
including multi-gateway development, CLI registration, and sandbox creation
examples.
CI/CD
Two GitHub Actions workflows back the driver's release artifacts, both
publishing to a rolling vm-dev GitHub Release:
Kernel Runtime (release-vm-kernel.yml)
Builds the custom libkrunfw (kernel firmware), libkrun (VMM), and gvproxy for all supported platforms. Runs on-demand or when the kernel config / pinned versions change.
| Platform | Runner | Build Method |
|---|---|---|
| Linux ARM64 | build-arm64 (self-hosted) |
Native build-libkrun.sh |
| Linux x86_64 | build-amd64 (self-hosted) |
Native build-libkrun.sh |
| macOS ARM64 | macos-latest-xlarge (GitHub-hosted) |
build-libkrun-macos.sh |
Artifacts: vm-runtime-{platform}.tar.zst containing libkrun, libkrunfw,
gvproxy, and provenance metadata. Each platform builds its own libkrunfw and
libkrun natively; the kernel inside libkrunfw is always Linux regardless of
host platform.
Driver Binary (release-vm-dev.yml)
Builds the self-contained openshell-driver-vm binary for every platform,
with the kernel runtime + sandbox rootfs embedded. Runs on every push to
main that touches VM-related crates.
The download-kernel-runtime job pulls the current vm-runtime-<platform>.tar.zst
from the vm-dev release; the build-openshell-driver-vm jobs set
OPENSHELL_VM_RUNTIME_COMPRESSED_DIR=$PWD/target/vm-runtime-compressed and
run cargo build --release -p openshell-driver-vm. The macOS driver is
cross-compiled via osxcross (no macOS runner needed for the binary build —
only for the kernel build).
macOS driver binaries produced via osxcross are not codesigned. Development
builds are signed automatically by crates/openshell-driver-vm/start.sh; a
packaged release needs signing in CI.
Rollout Strategy
- Custom runtime is embedded by default when building
openshell-driver-vmwithOPENSHELL_VM_RUNTIME_COMPRESSED_DIRset (wired up bycrates/openshell-driver-vm/start.sh). - The sandbox init script validates kernel capabilities at boot and fails fast if missing.
- For development, override with
OPENSHELL_VM_RUNTIME_DIRto use a local directory instead of the extracted cache. - In CI, the kernel runtime is pre-built and cached in the
vm-devrelease. The driver build downloads it viadownload-kernel-runtime.sh.