firecracker

package
v0.0.0-...-13f862e Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 22, 2026 License: MIT Imports: 23 Imported by: 0

Documentation

Overview

Package firecracker — runtime egress enforcement entry points (gm-o9t8.3.6.2). The platform-specific applier lives in egress_linux.go / egress_other.go; this file holds the cross- platform translation IR and the helpers tests use to assert the shape of the generated nftables ruleset without needing the `github.com/google/nftables` library on every host.

The translation IR is the bridge between an []egress.Rule and the `nftables` Go-library calls the Linux applier makes. We model each statement we know how to emit (verdict, log) and each match clause we care about (host IPs, ports, l4-proto). The applier walks the IR in priority order and feeds each entry into nftables.AddRule; the test path renders the IR to a deterministic JSON form and asserts on that — guaranteeing the rule plan stays stable even on macOS where nftables itself can't be exercised.

Hostname resolution policy for v1: one-shot at apply time. We resolve each rule's HostPattern (or its concrete host literal) via the default resolver and stamp the resulting v4 addresses onto the IR. Wildcard patterns (`*.example.com`, `**.foo`) are unresolvable at apply time without an interception DNS path; for those rules we fall back to a host-set entry that records the literal pattern so the applier can emit a log-only stub (and the operator gets a warning). TTL-aware refresh is the explicit follow-up — tracked in the report below.

Package firecracker is the VM Supervisor abstraction for Gemba's remote workspace dispatch path (gm-o9t8.3.2.3 / gm-o9t8.3.2.8).

On Linux with KVM available, the build-tagged `firecracker_linux.go` hooks up `firecracker-go-sdk` to boot a real Firecracker microVM with a vmlinux kernel + ext4 rootfs, tap-per-VM networking on the `gemba0` bridge, and MMDS for boot-time secret injection.

On non-Linux hosts (macOS/Windows dev/CI), the build-tagged `firecracker_fallback.go` provides a subprocess shim that satisfies the same lifecycle interface using `bash -c` so the rest of the dispatch path compiles and runs without KVM.

The actual kernel + rootfs image artifacts (gm-o9t8.3.2.2), the VM-aware dispatch path (gm-o9t8.3.2.6), egress nftables rules (gm-o9t8.3.6.2), volume-mount semantics beyond the stub Spec field (gm-o9t8.3.2.4), and boot-time vault.Inject integration (gm-o9t8.3.2.5) all live in their own beads.

Image verification for Firecracker VM artifacts (gm-o9t8.3.2.2).

A "VM image" in Gemba is the triple (vmlinux, rootfs.ext4, manifest.json). The kernel + rootfs are produced by the Buildroot pipeline in `vm-image/` on a Linux build host; the manifest is a small JSON file describing the artifacts and their sha256 hashes.

VerifyImage is called by the Firecracker supervisor before booting any VM to refuse-to-start on a corrupted or tampered image. This is the runtime half of the supply-chain story; the build half (signing) lives in `vm-image/scripts/`.

This file is plain Go — no Linux-only build tags — so it compiles on every dev host (macOS/Windows/Linux). The fallback supervisor on non-Linux hosts simply never calls into it (no real image to verify).

Index

Constants

This section is empty.

Variables

View Source
var ErrEgressNotEnforced = errors.New("firecracker: egress enforcement unavailable on this platform")

ErrEgressNotEnforced is returned by the fallback applier so callers can distinguish "I asked for enforcement but the host has no nftables stack" from "I applied successfully." Production wiring treats it as a soft warning, not a spawn-abort.

View Source
var ErrNoImageInstalled = errors.New("firecracker: no VM image found in $GEMBA_VM_DIR, ~/.gemba/vm, or /usr/local/share/gemba/vm")

ErrNoImageInstalled is returned by helpers that need a default image triple but DefaultImagePaths found none. The supervisor maps this to a user-facing "install the gemba VM image" message.

Functions

func DefaultImagePaths

func DefaultImagePaths() (kernel, rootfs, manifest string, ok bool)

DefaultImagePaths looks for a (kernel, rootfs, manifest) triple in the well-known locations a gemba server install might place them, in priority order:

  1. $GEMBA_VM_DIR (operator override)
  2. ~/.gemba/vm/ (single-user / dev install)
  3. /usr/local/share/gemba/vm/ (system-wide install)

The first directory containing all three files wins. ok is false if none of the candidates contain a full set; in that case the supervisor is expected to surface a clear "no VM image installed" error before any dispatch attempt.

func KVMAvailable

func KVMAvailable() bool

KVMAvailable reports whether /dev/kvm exists and the current process can open it. Used by tests to skip the real Firecracker path on Linux boxes that lack hardware virtualization (most CI runners). Returns false on permission failures even if the device node exists.

func VerifyImage

func VerifyImage(kernelPath, rootfsPath, manifestPath string) error

VerifyImage recomputes the sha256 of kernelPath and rootfsPath and returns nil iff both match the digests recorded in manifestPath. Any I/O error or mismatch is surfaced as a non-nil error. Callers (the Firecracker supervisor at boot) treat a non-nil return as a hard refusal to start the VM.

Types

type EgressAware

type EgressAware interface {
	AttachEgress(p EgressProvider)
}

EgressAware is implemented by both the Linux and fallback supervisors. Callers obtain a *fc.Supervisor and type-assert to EgressAware before plugging in a provider — this keeps the core Supervisor interface unchanged and avoids forcing a second method onto callers (like fake supervisors in tests) that don't need it.

type EgressProvider

type EgressProvider interface {
	Effective(ctx context.Context, wsid string) ([]egress.Rule, error)
}

EgressProvider is the supervisor-side view of the egress policy store. We accept the narrow Effective() surface rather than the full egress.Store so callers can plug in any source — the in-mem store under test, the SQL-backed store in production, a stub that always returns Defaults() for cold-start.

The supervisor calls Effective once per VM Start and stamps the result onto the per-VM nftables table. TTL-aware refresh is a follow-up (see report).

type LifecycleAuditor

type LifecycleAuditor interface {
	VMEvent(ctx context.Context, event string, payload map[string]any)
}

LifecycleAuditor is the narrow audit hook the supervisor calls on Start success / Stop completion (gm-o9t8.3.2.7). Modeled as an interface to avoid the supervisor importing the audit package directly — the server wires a tiny adapter.

type Manifest

type Manifest struct {
	KernelSHA256    string `json:"kernel_sha256"`
	RootfsSHA256    string `json:"rootfs_sha256"`
	BuiltAt         string `json:"built_at"`
	Builder         string `json:"builder"`
	KernelVersion   string `json:"kernel_version"`
	RootfsSizeBytes int64  `json:"rootfs_size_bytes"`
	Signature       string `json:"signature,omitempty"`
}

Manifest is the on-disk schema for `manifest.json` shipped alongside the kernel + rootfs. KernelSHA256 / RootfsSHA256 are lower-case hex digests. BuiltAt is RFC 3339. KernelVersion is the upstream Linux version string (e.g. "6.1.123"). RootfsSizeBytes is the byte length of the rootfs file at build time and is informational only — the authoritative integrity check is the sha256.

Signature is optional; when present it is the base64-encoded ed25519 signature over the canonical JSON of the *unsigned* manifest (i.e. the manifest with Signature emptied). The signing pubkey distribution model lives in `docs/server/vm-image.md` and is out of scope for v1.

func LoadManifest

func LoadManifest(path string) (*Manifest, error)

LoadManifest reads and JSON-decodes the manifest at path. It performs no integrity checking against the kernel/rootfs — that's VerifyImage's job — but does sanity-check that the two hash fields look like hex.

type Options

type Options struct {
	// Logger may be nil — slog.Default() is used in that case.
	Logger any // *slog.Logger; "any" to avoid platform-specific imports here

	// Vault is the secrets provider for boot-time injection. When nil,
	// only explicit Spec.Secrets are passed to the guest.
	Vault VaultInjector

	// Auditor receives VM lifecycle events (Spawn/Destroy). Nil-safe.
	Auditor LifecycleAuditor
}

Options is the construction surface for the Supervisor that wants dependencies wired explicitly (vault for boot-time secret injection, audit for lifecycle events — see gm-o9t8.3.2.5 and gm-o9t8.3.2.7). Existing callers can keep using NewSupervisor(log) which routes through Options with nil dependencies.

type Ruleset

type Ruleset struct {
	TableName  string      `json:"table_name"`
	Family     string      `json:"family"`
	ChainName  string      `json:"chain_name"`
	Hook       string      `json:"hook"`
	Priority   int         `json:"priority"`
	Statements []Statement `json:"statements"`
}

Ruleset is the full translated plan for a single VM. It also carries the per-VM nftables table name so the applier and the cleanup path agree on what to add and remove.

func (Ruleset) JSON

func (rs Ruleset) JSON() string

JSON renders the ruleset to a stable JSON form. Used by tests as the canonical assertion surface — diffing JSON is far cheaper than reflecting over the nftables library's nested structs.

type Spec

type Spec struct {
	WorkspaceID     string
	KernelPath      string
	RootfsPath      string
	VolumeMounts    []VolumeMount
	Secrets         map[string]string
	MemMB           int
	CPUCount        int
	FallbackCommand string
}

Spec describes a single VM to start. WorkspaceID identifies the workspace this VM belongs to and feeds into the generated VM ID for observability. KernelPath / RootfsPath are the host paths to the vmlinux + ext4 image artifacts (gm-o9t8.3.2.2). Secrets are injected via MMDS on Linux and via subprocess env on the fallback — both paths land them in the guest before the dispatch command runs. MemMB and CPUCount default to 512MB / 1 vCPU when zero.

FallbackCommand is the bash -c command the non-Linux fallback uses as the VM stand-in. Production Linux callers do not set this. Empty on the fallback means "sleep until killed" — i.e. a passive VM that outlives a Start/Stop pair but does nothing useful.

type Statement

type Statement struct {
	// RuleID echoes egress.Rule.ID so operators can map a drop event
	// in the kernel log back to the policy row that produced it.
	RuleID string `json:"rule_id"`

	// Priority is copied from the source rule so the test fixture
	// can assert the IR sort matches Effective() order.
	Priority int `json:"priority"`

	// Proto is "tcp", "udp", or "" for any.
	Proto string `json:"proto,omitempty"`

	// DestIPs is the resolved-at-apply-time IPv4/IPv6 set for the
	// rule's host pattern. Empty when the pattern is a wildcard that
	// can't be eagerly resolved.
	DestIPs []string `json:"dest_ips,omitempty"`

	// HostPattern is preserved verbatim so the Linux applier can
	// stamp it onto the rule comment (and the log prefix when the
	// rule fires a drop event).
	HostPattern string `json:"host_pattern"`

	// PortStart / PortEnd are inclusive; both zero == any port.
	PortStart int `json:"port_start"`
	PortEnd   int `json:"port_end"`

	// Verdict is the terminal verdict the rule applies.
	Verdict Verdict `json:"verdict"`

	// Log toggles a `log prefix "gemba-egress-drop:<rule_id>"`
	// statement before the verdict — set on every deny so kernel
	// ringbuffer / nflog readers can attribute the drop.
	Log bool `json:"log,omitempty"`

	// Note is a free-form annotation used when a rule's pattern
	// can't be expressed as concrete IP literals; the applier emits
	// a degraded log-only rule and warns the operator.
	Note string `json:"note,omitempty"`
}

Statement is one translated rule plan entry — what the Linux applier will emit as a single nftables rule. Fields are exported so tests can introspect them and so the JSON form is stable across builds (no map iteration ordering, no unexported reflection).

type Supervisor

type Supervisor interface {
	// Start launches a new VM described by spec. It blocks until the
	// VM is reachable / dispatch-ready or ctx is cancelled. Each call
	// returns a distinct *VM; callers are expected to track their own
	// VM handles.
	Start(ctx context.Context, spec Spec) (*VM, error)

	// Stop terminates a previously-started VM gracefully, then force-
	// kills anything still alive after timeout elapses. Idempotent —
	// calling Stop twice on the same *VM returns nil the second time.
	Stop(ctx context.Context, vm *VM, timeout time.Duration) error

	// Wait returns a channel that emits a single error when the
	// underlying VM process terminates. A clean Stop results in a nil
	// send. Unexpected exits surface the wait error (e.g. exit-status
	// non-zero). The channel is closed after the single send.
	Wait(ctx context.Context, vm *VM) <-chan error
}

Supervisor is the lifecycle interface for a Firecracker-style VM host. It mirrors `internal/adapter/dolt/supervisor.Supervisor` in spirit — Start launches, Stop terminates, Wait surfaces termination — but is a *factory* for many independent VMs rather than a wrapper around a single long-lived child like dolt-sql-server.

func NewSupervisor

func NewSupervisor(log *slog.Logger) Supervisor

NewSupervisor returns a Linux-backed Supervisor. Sockets default to $TMPDIR/gemba-firecracker; bridge defaults to "gemba0". On systems without KVM (KVMAvailable() == false) callers may still construct the supervisor — Start will be the call that fails, which keeps the API surface uniform between platforms.

func NewSupervisorWithOptions

func NewSupervisorWithOptions(opts Options) Supervisor

NewSupervisorWithOptions returns a Linux Supervisor wired with the supplied dependencies (vault, audit). gm-o9t8.3.2.5 and 3.2.7.

type VM

type VM struct {
	ID        string
	IPAddr    string
	StartedAt time.Time
	// contains filtered or unexported fields
}

VM is the opaque handle returned by Start. Fields are read-only after Start returns; mutation is the Supervisor's job.

state is exported via getter methods so the fallback and Linux implementations share the bookkeeping without leaking platform- specific fields into the public surface.

type VaultInjector

type VaultInjector interface {
	Inject(ctx context.Context, wsid string) (map[string]string, error)
}

VaultInjector is the narrow surface the supervisor uses to pull boot-time secrets out of the workspace vault (gm-o9t8.3.2.5). It is satisfied by *vault.boltVault — keeping the dependency as an interface lets tests inject fakes and avoids an import cycle on internal/vault from this low-level adapter package.

type Verdict

type Verdict string

Verdict is the action a rule emits when it matches: accept, drop, or log+drop. Allow rules become accept; deny rules become drop (with an embedded log statement when the operator wants visibility, which v1 always does so we get the audit ingest path for free).

const (
	VerdictAccept Verdict = "accept"
	VerdictDrop   Verdict = "drop"
)

type VolumeMount

type VolumeMount struct {
	HostPath  string
	GuestPath string
	ReadOnly  bool
}

VolumeMount is a stub for virtio-fs mount semantics. The real host-path / guest-path / read-only / cache-mode shape lands in gm-o9t8.3.2.4; for now the field exists so callers can compile against the final Spec shape.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL