Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions guest/firecracker-supervisor/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/bin/sh
set -eu

ROOT=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
GO_VERSION=go1.25.0
VERSION=${VERSION:-dev}
OUTPUT=${OUTPUT:-"$ROOT/firecracker-supervisor"}

actual=$(go env GOVERSION)
if [ "$actual" != "$GO_VERSION" ]; then
echo "required Go toolchain: $GO_VERSION (found $actual)" >&2
exit 1
fi

cd "$ROOT"
CGO_ENABLED=0 GOOS=linux GOARCH="${GOARCH:-amd64}" \
go build -trimpath -buildvcs=false -ldflags="-s -w -X main.version=$VERSION" -o "$OUTPUT" .
if command -v sha256sum >/dev/null 2>&1; then
sha256sum "$OUTPUT" > "$OUTPUT.sha256"
else
shasum -a 256 "$OUTPUT" > "$OUTPUT.sha256"
fi
83 changes: 83 additions & 0 deletions guest/firecracker-supervisor/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package main

import (
"fmt"
"net"
"path/filepath"
"strconv"
"strings"
)

type bootConfig struct {
WorkspaceDevice string
WorkspaceMount string
VsockPort uint32
GuestIP net.IP
GuestPrefix int
Gateway net.IP
Interface string
}

func parseBootConfig(cmdline string) (bootConfig, error) {
values := make(map[string]string)
for _, token := range strings.Fields(cmdline) {
key, value, ok := strings.Cut(token, "=")
if !ok || !strings.HasPrefix(key, "awf.") {
continue
}
if _, duplicate := values[key]; duplicate {
return bootConfig{}, fmt.Errorf("duplicate boot argument %q", key)
}
values[key] = value
}
required := []string{
"awf.workspace-device", "awf.workspace-mount", "awf.vsock-port",
"awf.guest-ip", "awf.guest-prefix", "awf.guest-gateway", "awf.guest-interface",
}
for _, key := range required {
if values[key] == "" {
return bootConfig{}, fmt.Errorf("missing required boot argument %q", key)
}
}
port, err := strconv.ParseUint(values["awf.vsock-port"], 10, 32)
if err != nil || port == 0 {
return bootConfig{}, fmt.Errorf("invalid awf.vsock-port")
}
prefix, err := strconv.Atoi(values["awf.guest-prefix"])
if err != nil || prefix < 0 || prefix > 32 {
return bootConfig{}, fmt.Errorf("invalid awf.guest-prefix")
}
ip := net.ParseIP(values["awf.guest-ip"]).To4()
gateway := net.ParseIP(values["awf.guest-gateway"]).To4()
if ip == nil || gateway == nil {
return bootConfig{}, fmt.Errorf("guest IP and gateway must be IPv4 addresses")
}
device := values["awf.workspace-device"]
if !strings.HasPrefix(device, "/dev/") || filepath.Clean(device) != device || strings.Contains(device, "..") {
return bootConfig{}, fmt.Errorf("invalid awf.workspace-device")
}
mount := values["awf.workspace-mount"]
if !filepath.IsAbs(mount) || filepath.Clean(mount) != mount || mount == "/" {
return bootConfig{}, fmt.Errorf("invalid awf.workspace-mount")
}
iface := values["awf.guest-interface"]
if !validInterface(iface) {
return bootConfig{}, fmt.Errorf("invalid awf.guest-interface")
}
return bootConfig{
WorkspaceDevice: device, WorkspaceMount: mount, VsockPort: uint32(port),
GuestIP: ip, GuestPrefix: prefix, Gateway: gateway, Interface: iface,
}, nil
}

func validInterface(name string) bool {
if name == "" || len(name) > 15 {
return false
}
for i, r := range name {
if !(r == '-' || r == '_' || r == '.' || r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || (i > 0 && r >= '0' && r <= '9')) {
return false
}
}
return true
}
35 changes: 35 additions & 0 deletions guest/firecracker-supervisor/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package main

import "testing"

const validCmdline = "console=ttyS0 awf.workspace-device=/dev/vdb awf.workspace-mount=/workspace awf.vsock-port=1024 awf.guest-ip=192.0.2.2 awf.guest-prefix=24 awf.guest-gateway=192.0.2.1 awf.guest-interface=eth0"

func TestParseBootConfig(t *testing.T) {
config, err := parseBootConfig(validCmdline)
if err != nil {
t.Fatalf("parseBootConfig: %v", err)
}
if config.VsockPort != 1024 || config.Interface != "eth0" || config.GuestIP.String() != "192.0.2.2" {
t.Fatalf("unexpected config: %#v", config)
}
}

func TestParseBootConfigRejectsUnsafeValues(t *testing.T) {
cases := []string{
"awf.workspace-device=/dev/vdb awf.workspace-mount=/workspace awf.vsock-port=0 awf.guest-ip=192.0.2.2 awf.guest-prefix=24 awf.guest-gateway=192.0.2.1 awf.guest-interface=eth0",
"awf.workspace-device=/dev/../etc/passwd awf.workspace-mount=/workspace awf.vsock-port=1 awf.guest-ip=192.0.2.2 awf.guest-prefix=24 awf.guest-gateway=192.0.2.1 awf.guest-interface=eth0",
"awf.workspace-device=/dev/vdb awf.workspace-mount=/ awf.vsock-port=1 awf.guest-ip=192.0.2.2 awf.guest-prefix=24 awf.guest-gateway=192.0.2.1 awf.guest-interface=eth0",
"awf.workspace-device=/dev/vdb awf.workspace-mount=/workspace awf.vsock-port=1 awf.guest-ip=bad awf.guest-prefix=24 awf.guest-gateway=192.0.2.1 awf.guest-interface=eth0",
}
for _, cmdline := range cases {
if _, err := parseBootConfig(cmdline); err == nil {
t.Errorf("unsafe command line accepted: %q", cmdline)
}
}
}

func TestParseBootConfigRejectsDuplicateArguments(t *testing.T) {
if _, err := parseBootConfig(validCmdline + " awf.vsock-port=1025"); err == nil {
t.Fatal("duplicate argument accepted")
}
}
5 changes: 5 additions & 0 deletions guest/firecracker-supervisor/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module github.com/github/gh-aw-firewall/firecracker-supervisor

go 1.24.0

toolchain go1.25.0
23 changes: 23 additions & 0 deletions guest/firecracker-supervisor/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// firecracker-supervisor is the minimal guest-side command supervisor.
package main

import (
"flag"
"fmt"
"os"
)

var version = "dev"

func main() {
showVersion := flag.Bool("version", false, "print version")
flag.Parse()
if *showVersion {
fmt.Println(version)
return
}
if err := runSupervisor(); err != nil {
fmt.Fprintln(os.Stderr, "firecracker-supervisor:", err)
os.Exit(1)
}
}
Loading
Loading