Move the supported systemd path to two services: an owner-user bangerd for orchestration and a narrow root helper for bridge/tap, NAT/resolver, dm/loop, and Firecracker ownership. This removes repeated sudo from daily vm and image flows without leaving the general daemon running as root. Add install metadata, system install/status/restart/uninstall commands, and a system-owned runtime layout. Keep user SSH/config material in the owner home, lock file_sync to the owner home, and move daemon known_hosts handling out of the old root-owned control path. Route privileged lifecycle steps through typed privilegedOps calls, harden the two systemd units, and rewrite smoke plus docs around the supported service model. Verified with make build, make test, make lint, and make smoke on the supported systemd host path.
840 lines
26 KiB
Go
840 lines
26 KiB
Go
package roothelper
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"golang.org/x/sys/unix"
|
|
|
|
"banger/internal/daemon/dmsnap"
|
|
"banger/internal/daemon/fcproc"
|
|
"banger/internal/firecracker"
|
|
"banger/internal/hostnat"
|
|
"banger/internal/installmeta"
|
|
"banger/internal/paths"
|
|
"banger/internal/rpc"
|
|
"banger/internal/system"
|
|
)
|
|
|
|
const (
|
|
methodEnsureBridge = "priv.ensure_bridge"
|
|
methodCreateTap = "priv.create_tap"
|
|
methodDeleteTap = "priv.delete_tap"
|
|
methodSyncResolverRouting = "priv.sync_resolver_routing"
|
|
methodClearResolverRouting = "priv.clear_resolver_routing"
|
|
methodEnsureNAT = "priv.ensure_nat"
|
|
methodCreateDMSnapshot = "priv.create_dm_snapshot"
|
|
methodCleanupDMSnapshot = "priv.cleanup_dm_snapshot"
|
|
methodRemoveDMSnapshot = "priv.remove_dm_snapshot"
|
|
methodFsckSnapshot = "priv.fsck_snapshot"
|
|
methodReadExt4File = "priv.read_ext4_file"
|
|
methodWriteExt4Files = "priv.write_ext4_files"
|
|
methodResolveFirecrackerBin = "priv.resolve_firecracker_binary"
|
|
methodLaunchFirecracker = "priv.launch_firecracker"
|
|
methodEnsureSocketAccess = "priv.ensure_socket_access"
|
|
methodFindFirecrackerPID = "priv.find_firecracker_pid"
|
|
methodKillProcess = "priv.kill_process"
|
|
methodSignalProcess = "priv.signal_process"
|
|
methodProcessRunning = "priv.process_running"
|
|
rootfsDMNamePrefix = "fc-rootfs-"
|
|
vmTapPrefix = "tap-fc-"
|
|
tapPoolPrefix = "tap-pool-"
|
|
vmResolverRouteDomain = "~vm"
|
|
defaultFirecrackerBinaryName = "firecracker"
|
|
)
|
|
|
|
type NetworkConfig struct {
|
|
BridgeName string `json:"bridge_name"`
|
|
BridgeIP string `json:"bridge_ip"`
|
|
CIDR string `json:"cidr"`
|
|
}
|
|
|
|
type Ext4Write struct {
|
|
GuestPath string `json:"guest_path"`
|
|
Data []byte `json:"data"`
|
|
Mode uint32 `json:"mode"`
|
|
}
|
|
|
|
type FirecrackerLaunchRequest struct {
|
|
BinaryPath string `json:"binary_path"`
|
|
VMID string `json:"vm_id"`
|
|
SocketPath string `json:"socket_path"`
|
|
LogPath string `json:"log_path"`
|
|
MetricsPath string `json:"metrics_path"`
|
|
KernelImagePath string `json:"kernel_image_path"`
|
|
InitrdPath string `json:"initrd_path,omitempty"`
|
|
KernelArgs string `json:"kernel_args"`
|
|
Drives []firecracker.DriveConfig `json:"drives"`
|
|
TapDevice string `json:"tap_device"`
|
|
VSockPath string `json:"vsock_path"`
|
|
VSockCID uint32 `json:"vsock_cid"`
|
|
VCPUCount int `json:"vcpu_count"`
|
|
MemoryMiB int `json:"memory_mib"`
|
|
Network NetworkConfig `json:"network"`
|
|
}
|
|
|
|
type findPIDResult struct {
|
|
PID int `json:"pid"`
|
|
}
|
|
|
|
type processRunningResult struct {
|
|
Running bool `json:"running"`
|
|
}
|
|
|
|
type readExt4FileResult struct {
|
|
Data []byte `json:"data"`
|
|
}
|
|
|
|
type resolveFirecrackerResult struct {
|
|
Path string `json:"path"`
|
|
}
|
|
|
|
type launchFirecrackerResult struct {
|
|
PID int `json:"pid"`
|
|
}
|
|
|
|
type Client struct {
|
|
socketPath string
|
|
}
|
|
|
|
func NewClient(socketPath string) *Client {
|
|
return &Client{socketPath: strings.TrimSpace(socketPath)}
|
|
}
|
|
|
|
func (c *Client) EnsureBridge(ctx context.Context, cfg NetworkConfig) error {
|
|
_, err := rpc.Call[struct{}](ctx, c.socketPath, methodEnsureBridge, cfg)
|
|
return err
|
|
}
|
|
|
|
func (c *Client) CreateTap(ctx context.Context, cfg NetworkConfig, tapName string) error {
|
|
_, err := rpc.Call[struct{}](ctx, c.socketPath, methodCreateTap, struct {
|
|
NetworkConfig
|
|
TapName string `json:"tap_name"`
|
|
}{NetworkConfig: cfg, TapName: tapName})
|
|
return err
|
|
}
|
|
|
|
func (c *Client) DeleteTap(ctx context.Context, tapName string) error {
|
|
_, err := rpc.Call[struct{}](ctx, c.socketPath, methodDeleteTap, struct {
|
|
TapName string `json:"tap_name"`
|
|
}{TapName: tapName})
|
|
return err
|
|
}
|
|
|
|
func (c *Client) SyncResolverRouting(ctx context.Context, bridgeName, serverAddr string) error {
|
|
_, err := rpc.Call[struct{}](ctx, c.socketPath, methodSyncResolverRouting, struct {
|
|
BridgeName string `json:"bridge_name"`
|
|
ServerAddr string `json:"server_addr"`
|
|
}{BridgeName: bridgeName, ServerAddr: serverAddr})
|
|
return err
|
|
}
|
|
|
|
func (c *Client) ClearResolverRouting(ctx context.Context, bridgeName string) error {
|
|
_, err := rpc.Call[struct{}](ctx, c.socketPath, methodClearResolverRouting, struct {
|
|
BridgeName string `json:"bridge_name"`
|
|
}{BridgeName: bridgeName})
|
|
return err
|
|
}
|
|
|
|
func (c *Client) EnsureNAT(ctx context.Context, guestIP, tap string, enable bool) error {
|
|
_, err := rpc.Call[struct{}](ctx, c.socketPath, methodEnsureNAT, struct {
|
|
GuestIP string `json:"guest_ip"`
|
|
Tap string `json:"tap"`
|
|
Enable bool `json:"enable"`
|
|
}{GuestIP: guestIP, Tap: tap, Enable: enable})
|
|
return err
|
|
}
|
|
|
|
func (c *Client) CreateDMSnapshot(ctx context.Context, rootfsPath, cowPath, dmName string) (dmsnap.Handles, error) {
|
|
return rpc.Call[dmsnap.Handles](ctx, c.socketPath, methodCreateDMSnapshot, struct {
|
|
RootfsPath string `json:"rootfs_path"`
|
|
COWPath string `json:"cow_path"`
|
|
DMName string `json:"dm_name"`
|
|
}{RootfsPath: rootfsPath, COWPath: cowPath, DMName: dmName})
|
|
}
|
|
|
|
func (c *Client) CleanupDMSnapshot(ctx context.Context, handles dmsnap.Handles) error {
|
|
_, err := rpc.Call[struct{}](ctx, c.socketPath, methodCleanupDMSnapshot, handles)
|
|
return err
|
|
}
|
|
|
|
func (c *Client) RemoveDMSnapshot(ctx context.Context, target string) error {
|
|
_, err := rpc.Call[struct{}](ctx, c.socketPath, methodRemoveDMSnapshot, struct {
|
|
Target string `json:"target"`
|
|
}{Target: target})
|
|
return err
|
|
}
|
|
|
|
func (c *Client) FsckSnapshot(ctx context.Context, dmDev string) error {
|
|
_, err := rpc.Call[struct{}](ctx, c.socketPath, methodFsckSnapshot, struct {
|
|
DMDev string `json:"dm_dev"`
|
|
}{DMDev: dmDev})
|
|
return err
|
|
}
|
|
|
|
func (c *Client) ReadExt4File(ctx context.Context, imagePath, guestPath string) ([]byte, error) {
|
|
result, err := rpc.Call[readExt4FileResult](ctx, c.socketPath, methodReadExt4File, struct {
|
|
ImagePath string `json:"image_path"`
|
|
GuestPath string `json:"guest_path"`
|
|
}{ImagePath: imagePath, GuestPath: guestPath})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return result.Data, nil
|
|
}
|
|
|
|
func (c *Client) WriteExt4Files(ctx context.Context, imagePath string, files []Ext4Write) error {
|
|
_, err := rpc.Call[struct{}](ctx, c.socketPath, methodWriteExt4Files, struct {
|
|
ImagePath string `json:"image_path"`
|
|
Files []Ext4Write `json:"files"`
|
|
}{ImagePath: imagePath, Files: files})
|
|
return err
|
|
}
|
|
|
|
func (c *Client) ResolveFirecrackerBinary(ctx context.Context, requested string) (string, error) {
|
|
result, err := rpc.Call[resolveFirecrackerResult](ctx, c.socketPath, methodResolveFirecrackerBin, struct {
|
|
Requested string `json:"requested"`
|
|
}{Requested: requested})
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return result.Path, nil
|
|
}
|
|
|
|
func (c *Client) LaunchFirecracker(ctx context.Context, req FirecrackerLaunchRequest) (int, error) {
|
|
result, err := rpc.Call[launchFirecrackerResult](ctx, c.socketPath, methodLaunchFirecracker, req)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return result.PID, nil
|
|
}
|
|
|
|
func (c *Client) EnsureSocketAccess(ctx context.Context, socketPath, label string) error {
|
|
_, err := rpc.Call[struct{}](ctx, c.socketPath, methodEnsureSocketAccess, struct {
|
|
SocketPath string `json:"socket_path"`
|
|
Label string `json:"label"`
|
|
}{SocketPath: socketPath, Label: label})
|
|
return err
|
|
}
|
|
|
|
func (c *Client) FindFirecrackerPID(ctx context.Context, apiSock string) (int, error) {
|
|
result, err := rpc.Call[findPIDResult](ctx, c.socketPath, methodFindFirecrackerPID, struct {
|
|
APISock string `json:"api_sock"`
|
|
}{APISock: apiSock})
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return result.PID, nil
|
|
}
|
|
|
|
func (c *Client) KillProcess(ctx context.Context, pid int) error {
|
|
_, err := rpc.Call[struct{}](ctx, c.socketPath, methodKillProcess, struct {
|
|
PID int `json:"pid"`
|
|
}{PID: pid})
|
|
return err
|
|
}
|
|
|
|
func (c *Client) SignalProcess(ctx context.Context, pid int, signal string) error {
|
|
_, err := rpc.Call[struct{}](ctx, c.socketPath, methodSignalProcess, struct {
|
|
PID int `json:"pid"`
|
|
Signal string `json:"signal"`
|
|
}{PID: pid, Signal: signal})
|
|
return err
|
|
}
|
|
|
|
func (c *Client) ProcessRunning(ctx context.Context, pid int, apiSock string) (bool, error) {
|
|
result, err := rpc.Call[processRunningResult](ctx, c.socketPath, methodProcessRunning, struct {
|
|
PID int `json:"pid"`
|
|
APISock string `json:"api_sock"`
|
|
}{PID: pid, APISock: apiSock})
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return result.Running, nil
|
|
}
|
|
|
|
type Server struct {
|
|
meta installmeta.Metadata
|
|
runner system.CommandRunner
|
|
logger *slog.Logger
|
|
listener net.Listener
|
|
}
|
|
|
|
func Open() (*Server, error) {
|
|
meta, err := installmeta.Load(installmeta.DefaultPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := os.MkdirAll(installmeta.DefaultRootHelperRuntimeDir, 0o711); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := os.Chmod(installmeta.DefaultRootHelperRuntimeDir, 0o711); err != nil {
|
|
return nil, err
|
|
}
|
|
return &Server{
|
|
meta: meta,
|
|
runner: system.NewRunner(),
|
|
logger: slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelInfo})),
|
|
}, nil
|
|
}
|
|
|
|
func (s *Server) Close() error {
|
|
if s == nil || s.listener == nil {
|
|
return nil
|
|
}
|
|
return s.listener.Close()
|
|
}
|
|
|
|
func (s *Server) Serve(ctx context.Context) error {
|
|
_ = os.Remove(installmeta.DefaultRootHelperSocketPath)
|
|
listener, err := net.Listen("unix", installmeta.DefaultRootHelperSocketPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
s.listener = listener
|
|
defer listener.Close()
|
|
defer os.Remove(installmeta.DefaultRootHelperSocketPath)
|
|
if err := os.Chmod(installmeta.DefaultRootHelperSocketPath, 0o600); err != nil {
|
|
return err
|
|
}
|
|
if err := os.Chown(installmeta.DefaultRootHelperSocketPath, s.meta.OwnerUID, s.meta.OwnerGID); err != nil {
|
|
return err
|
|
}
|
|
|
|
done := make(chan struct{})
|
|
defer close(done)
|
|
go func() {
|
|
select {
|
|
case <-ctx.Done():
|
|
_ = listener.Close()
|
|
case <-done:
|
|
}
|
|
}()
|
|
|
|
for {
|
|
conn, err := listener.Accept()
|
|
if err != nil {
|
|
select {
|
|
case <-ctx.Done():
|
|
return nil
|
|
default:
|
|
}
|
|
var netErr net.Error
|
|
if errors.As(err, &netErr) && netErr.Temporary() {
|
|
time.Sleep(100 * time.Millisecond)
|
|
continue
|
|
}
|
|
return err
|
|
}
|
|
go s.handleConn(conn)
|
|
}
|
|
}
|
|
|
|
func (s *Server) handleConn(conn net.Conn) {
|
|
defer conn.Close()
|
|
if err := s.authorizeConn(conn); err != nil {
|
|
_ = json.NewEncoder(conn).Encode(rpc.NewError("unauthorized", err.Error()))
|
|
return
|
|
}
|
|
var req rpc.Request
|
|
if err := json.NewDecoder(bufio.NewReader(conn)).Decode(&req); err != nil {
|
|
_ = json.NewEncoder(conn).Encode(rpc.NewError("bad_request", err.Error()))
|
|
return
|
|
}
|
|
resp := s.dispatch(context.Background(), req)
|
|
_ = json.NewEncoder(conn).Encode(resp)
|
|
}
|
|
|
|
func (s *Server) authorizeConn(conn net.Conn) error {
|
|
unixConn, ok := conn.(*net.UnixConn)
|
|
if !ok {
|
|
return errors.New("root helper requires unix connections")
|
|
}
|
|
rawConn, err := unixConn.SyscallConn()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var cred *unix.Ucred
|
|
var controlErr error
|
|
if err := rawConn.Control(func(fd uintptr) {
|
|
cred, controlErr = unix.GetsockoptUcred(int(fd), unix.SOL_SOCKET, unix.SO_PEERCRED)
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
if controlErr != nil {
|
|
return controlErr
|
|
}
|
|
if cred == nil {
|
|
return errors.New("missing peer credentials")
|
|
}
|
|
if int(cred.Uid) == 0 || int(cred.Uid) == s.meta.OwnerUID {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("uid %d is not allowed to use the root helper", cred.Uid)
|
|
}
|
|
|
|
func (s *Server) dispatch(ctx context.Context, req rpc.Request) rpc.Response {
|
|
switch req.Method {
|
|
case methodEnsureBridge:
|
|
params, err := rpc.DecodeParams[NetworkConfig](req)
|
|
if err != nil {
|
|
return rpc.NewError("bad_params", err.Error())
|
|
}
|
|
return marshalResultOrError(struct{}{}, s.ensureBridge(ctx, params))
|
|
case methodCreateTap:
|
|
params, err := rpc.DecodeParams[struct {
|
|
NetworkConfig
|
|
TapName string `json:"tap_name"`
|
|
}](req)
|
|
if err != nil {
|
|
return rpc.NewError("bad_params", err.Error())
|
|
}
|
|
return marshalResultOrError(struct{}{}, s.createTap(ctx, params.NetworkConfig, params.TapName))
|
|
case methodDeleteTap:
|
|
params, err := rpc.DecodeParams[struct {
|
|
TapName string `json:"tap_name"`
|
|
}](req)
|
|
if err != nil {
|
|
return rpc.NewError("bad_params", err.Error())
|
|
}
|
|
return marshalResultOrError(struct{}{}, s.deleteTap(ctx, params.TapName))
|
|
case methodSyncResolverRouting:
|
|
params, err := rpc.DecodeParams[struct {
|
|
BridgeName string `json:"bridge_name"`
|
|
ServerAddr string `json:"server_addr"`
|
|
}](req)
|
|
if err != nil {
|
|
return rpc.NewError("bad_params", err.Error())
|
|
}
|
|
return marshalResultOrError(struct{}{}, s.syncResolverRouting(ctx, params.BridgeName, params.ServerAddr))
|
|
case methodClearResolverRouting:
|
|
params, err := rpc.DecodeParams[struct {
|
|
BridgeName string `json:"bridge_name"`
|
|
}](req)
|
|
if err != nil {
|
|
return rpc.NewError("bad_params", err.Error())
|
|
}
|
|
return marshalResultOrError(struct{}{}, s.clearResolverRouting(ctx, params.BridgeName))
|
|
case methodEnsureNAT:
|
|
params, err := rpc.DecodeParams[struct {
|
|
GuestIP string `json:"guest_ip"`
|
|
Tap string `json:"tap"`
|
|
Enable bool `json:"enable"`
|
|
}](req)
|
|
if err != nil {
|
|
return rpc.NewError("bad_params", err.Error())
|
|
}
|
|
return marshalResultOrError(struct{}{}, hostnat.Ensure(ctx, s.runner, params.GuestIP, params.Tap, params.Enable))
|
|
case methodCreateDMSnapshot:
|
|
params, err := rpc.DecodeParams[struct {
|
|
RootfsPath string `json:"rootfs_path"`
|
|
COWPath string `json:"cow_path"`
|
|
DMName string `json:"dm_name"`
|
|
}](req)
|
|
if err != nil {
|
|
return rpc.NewError("bad_params", err.Error())
|
|
}
|
|
if err := s.validateManagedPath(params.RootfsPath, paths.ResolveSystem().StateDir); err != nil {
|
|
return rpc.NewError("bad_params", err.Error())
|
|
}
|
|
if err := s.validateManagedPath(params.COWPath, paths.ResolveSystem().StateDir); err != nil {
|
|
return rpc.NewError("bad_params", err.Error())
|
|
}
|
|
if err := validateDMName(params.DMName); err != nil {
|
|
return rpc.NewError("bad_params", err.Error())
|
|
}
|
|
result, err := dmsnap.Create(ctx, s.runner, params.RootfsPath, params.COWPath, params.DMName)
|
|
return marshalResultOrError(result, err)
|
|
case methodCleanupDMSnapshot:
|
|
params, err := rpc.DecodeParams[dmsnap.Handles](req)
|
|
if err != nil {
|
|
return rpc.NewError("bad_params", err.Error())
|
|
}
|
|
return marshalResultOrError(struct{}{}, dmsnap.Cleanup(ctx, s.runner, params))
|
|
case methodRemoveDMSnapshot:
|
|
params, err := rpc.DecodeParams[struct {
|
|
Target string `json:"target"`
|
|
}](req)
|
|
if err != nil {
|
|
return rpc.NewError("bad_params", err.Error())
|
|
}
|
|
return marshalResultOrError(struct{}{}, dmsnap.Remove(ctx, s.runner, params.Target))
|
|
case methodFsckSnapshot:
|
|
params, err := rpc.DecodeParams[struct {
|
|
DMDev string `json:"dm_dev"`
|
|
}](req)
|
|
if err != nil {
|
|
return rpc.NewError("bad_params", err.Error())
|
|
}
|
|
return marshalResultOrError(struct{}{}, s.fsckSnapshot(ctx, params.DMDev))
|
|
case methodReadExt4File:
|
|
params, err := rpc.DecodeParams[struct {
|
|
ImagePath string `json:"image_path"`
|
|
GuestPath string `json:"guest_path"`
|
|
}](req)
|
|
if err != nil {
|
|
return rpc.NewError("bad_params", err.Error())
|
|
}
|
|
data, readErr := system.ReadExt4File(ctx, s.runner, params.ImagePath, params.GuestPath)
|
|
return marshalResultOrError(readExt4FileResult{Data: data}, readErr)
|
|
case methodWriteExt4Files:
|
|
params, err := rpc.DecodeParams[struct {
|
|
ImagePath string `json:"image_path"`
|
|
Files []Ext4Write `json:"files"`
|
|
}](req)
|
|
if err != nil {
|
|
return rpc.NewError("bad_params", err.Error())
|
|
}
|
|
return marshalResultOrError(struct{}{}, s.writeExt4Files(ctx, params.ImagePath, params.Files))
|
|
case methodResolveFirecrackerBin:
|
|
params, err := rpc.DecodeParams[struct {
|
|
Requested string `json:"requested"`
|
|
}](req)
|
|
if err != nil {
|
|
return rpc.NewError("bad_params", err.Error())
|
|
}
|
|
path, resolveErr := s.resolveFirecrackerBinary(params.Requested)
|
|
return marshalResultOrError(resolveFirecrackerResult{Path: path}, resolveErr)
|
|
case methodLaunchFirecracker:
|
|
params, err := rpc.DecodeParams[FirecrackerLaunchRequest](req)
|
|
if err != nil {
|
|
return rpc.NewError("bad_params", err.Error())
|
|
}
|
|
pid, launchErr := s.launchFirecracker(ctx, params)
|
|
return marshalResultOrError(launchFirecrackerResult{PID: pid}, launchErr)
|
|
case methodEnsureSocketAccess:
|
|
params, err := rpc.DecodeParams[struct {
|
|
SocketPath string `json:"socket_path"`
|
|
Label string `json:"label"`
|
|
}](req)
|
|
if err != nil {
|
|
return rpc.NewError("bad_params", err.Error())
|
|
}
|
|
return marshalResultOrError(struct{}{}, s.ensureSocketAccess(ctx, params.SocketPath, params.Label))
|
|
case methodFindFirecrackerPID:
|
|
params, err := rpc.DecodeParams[struct {
|
|
APISock string `json:"api_sock"`
|
|
}](req)
|
|
if err != nil {
|
|
return rpc.NewError("bad_params", err.Error())
|
|
}
|
|
pid, findErr := fcproc.New(s.runner, fcproc.Config{}, s.logger).FindPID(ctx, params.APISock)
|
|
return marshalResultOrError(findPIDResult{PID: pid}, findErr)
|
|
case methodKillProcess:
|
|
params, err := rpc.DecodeParams[struct {
|
|
PID int `json:"pid"`
|
|
}](req)
|
|
if err != nil {
|
|
return rpc.NewError("bad_params", err.Error())
|
|
}
|
|
_, killErr := s.runner.Run(ctx, "kill", "-KILL", strconv.Itoa(params.PID))
|
|
return marshalResultOrError(struct{}{}, killErr)
|
|
case methodSignalProcess:
|
|
params, err := rpc.DecodeParams[struct {
|
|
PID int `json:"pid"`
|
|
Signal string `json:"signal"`
|
|
}](req)
|
|
if err != nil {
|
|
return rpc.NewError("bad_params", err.Error())
|
|
}
|
|
signal := strings.TrimSpace(params.Signal)
|
|
if signal == "" {
|
|
signal = "TERM"
|
|
}
|
|
_, signalErr := s.runner.Run(ctx, "kill", "-"+signal, strconv.Itoa(params.PID))
|
|
return marshalResultOrError(struct{}{}, signalErr)
|
|
case methodProcessRunning:
|
|
params, err := rpc.DecodeParams[struct {
|
|
PID int `json:"pid"`
|
|
APISock string `json:"api_sock"`
|
|
}](req)
|
|
if err != nil {
|
|
return rpc.NewError("bad_params", err.Error())
|
|
}
|
|
return marshalResultOrError(processRunningResult{Running: system.ProcessRunning(params.PID, params.APISock)}, nil)
|
|
default:
|
|
return rpc.NewError("unknown_method", req.Method)
|
|
}
|
|
}
|
|
|
|
func (s *Server) ensureBridge(ctx context.Context, cfg NetworkConfig) error {
|
|
return fcproc.New(s.runner, fcproc.Config{
|
|
BridgeName: cfg.BridgeName,
|
|
BridgeIP: cfg.BridgeIP,
|
|
CIDR: cfg.CIDR,
|
|
}, s.logger).EnsureBridge(ctx)
|
|
}
|
|
|
|
func (s *Server) createTap(ctx context.Context, cfg NetworkConfig, tapName string) error {
|
|
if err := validateTapName(tapName); err != nil {
|
|
return err
|
|
}
|
|
return fcproc.New(s.runner, fcproc.Config{
|
|
BridgeName: cfg.BridgeName,
|
|
BridgeIP: cfg.BridgeIP,
|
|
CIDR: cfg.CIDR,
|
|
}, s.logger).CreateTapOwned(ctx, tapName, s.meta.OwnerUID, s.meta.OwnerGID)
|
|
}
|
|
|
|
func (s *Server) deleteTap(ctx context.Context, tapName string) error {
|
|
if err := validateTapName(tapName); err != nil {
|
|
return err
|
|
}
|
|
_, err := s.runner.Run(ctx, "ip", "link", "del", tapName)
|
|
return err
|
|
}
|
|
|
|
func (s *Server) syncResolverRouting(ctx context.Context, bridgeName, serverAddr string) error {
|
|
if strings.TrimSpace(bridgeName) == "" || strings.TrimSpace(serverAddr) == "" {
|
|
return nil
|
|
}
|
|
if _, err := system.LookupExecutable("resolvectl"); err != nil {
|
|
return nil
|
|
}
|
|
if _, err := s.runner.Run(ctx, "resolvectl", "dns", bridgeName, serverAddr); err != nil {
|
|
return err
|
|
}
|
|
if _, err := s.runner.Run(ctx, "resolvectl", "domain", bridgeName, vmResolverRouteDomain); err != nil {
|
|
return err
|
|
}
|
|
_, err := s.runner.Run(ctx, "resolvectl", "default-route", bridgeName, "no")
|
|
return err
|
|
}
|
|
|
|
func (s *Server) clearResolverRouting(ctx context.Context, bridgeName string) error {
|
|
if strings.TrimSpace(bridgeName) == "" {
|
|
return nil
|
|
}
|
|
if _, err := system.LookupExecutable("resolvectl"); err != nil {
|
|
return nil
|
|
}
|
|
_, err := s.runner.Run(ctx, "resolvectl", "revert", bridgeName)
|
|
return err
|
|
}
|
|
|
|
func (s *Server) fsckSnapshot(ctx context.Context, dmDev string) error {
|
|
if strings.TrimSpace(dmDev) == "" {
|
|
return errors.New("dm device is required")
|
|
}
|
|
if _, err := s.runner.Run(ctx, "e2fsck", "-fy", dmDev); err != nil {
|
|
if code := system.ExitCode(err); code < 0 || code > 1 {
|
|
return fmt.Errorf("fsck snapshot: %w", err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) writeExt4Files(ctx context.Context, imagePath string, files []Ext4Write) error {
|
|
for _, file := range files {
|
|
mode := os.FileMode(file.Mode)
|
|
if mode == 0 {
|
|
mode = 0o644
|
|
}
|
|
if err := system.WriteExt4FileOwned(ctx, s.runner, imagePath, file.GuestPath, mode, 0, 0, file.Data); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) resolveFirecrackerBinary(requested string) (string, error) {
|
|
requested = strings.TrimSpace(requested)
|
|
if requested == "" {
|
|
requested = defaultFirecrackerBinaryName
|
|
}
|
|
cfg := fcproc.Config{FirecrackerBin: requested}
|
|
resolved, err := fcproc.New(s.runner, cfg, s.logger).ResolveBinary()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if err := validateRootExecutable(resolved); err != nil {
|
|
return "", err
|
|
}
|
|
return resolved, nil
|
|
}
|
|
|
|
func (s *Server) launchFirecracker(ctx context.Context, req FirecrackerLaunchRequest) (int, error) {
|
|
systemLayout := paths.ResolveSystem()
|
|
for _, path := range []string{req.SocketPath, req.VSockPath} {
|
|
if err := s.validateManagedPath(path, systemLayout.RuntimeDir); err != nil {
|
|
return 0, err
|
|
}
|
|
}
|
|
for _, path := range []string{req.LogPath, req.MetricsPath, req.KernelImagePath} {
|
|
if err := s.validateManagedPath(path, systemLayout.StateDir); err != nil {
|
|
return 0, err
|
|
}
|
|
}
|
|
if strings.TrimSpace(req.InitrdPath) != "" {
|
|
if err := s.validateManagedPath(req.InitrdPath, systemLayout.StateDir); err != nil {
|
|
return 0, err
|
|
}
|
|
}
|
|
if err := validateTapName(req.TapDevice); err != nil {
|
|
return 0, err
|
|
}
|
|
if err := validateRootExecutable(req.BinaryPath); err != nil {
|
|
return 0, err
|
|
}
|
|
for _, drive := range req.Drives {
|
|
if err := s.validateLaunchDrivePath(drive, systemLayout.StateDir); err != nil {
|
|
return 0, err
|
|
}
|
|
}
|
|
machine, err := firecracker.NewMachine(ctx, firecracker.MachineConfig{
|
|
BinaryPath: req.BinaryPath,
|
|
VMID: req.VMID,
|
|
SocketPath: req.SocketPath,
|
|
LogPath: req.LogPath,
|
|
MetricsPath: req.MetricsPath,
|
|
KernelImagePath: req.KernelImagePath,
|
|
InitrdPath: req.InitrdPath,
|
|
KernelArgs: req.KernelArgs,
|
|
Drives: req.Drives,
|
|
TapDevice: req.TapDevice,
|
|
VSockPath: req.VSockPath,
|
|
VSockCID: req.VSockCID,
|
|
VCPUCount: req.VCPUCount,
|
|
MemoryMiB: req.MemoryMiB,
|
|
Logger: s.logger,
|
|
})
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if err := machine.Start(ctx); err != nil {
|
|
manager := fcproc.New(s.runner, fcproc.Config{BridgeName: req.Network.BridgeName, BridgeIP: req.Network.BridgeIP, CIDR: req.Network.CIDR}, s.logger)
|
|
if pid := manager.ResolvePID(context.Background(), machine, req.SocketPath); pid > 0 {
|
|
_, _ = s.runner.Run(context.Background(), "kill", "-KILL", strconv.Itoa(pid))
|
|
}
|
|
return 0, err
|
|
}
|
|
manager := fcproc.New(s.runner, fcproc.Config{BridgeName: req.Network.BridgeName, BridgeIP: req.Network.BridgeIP, CIDR: req.Network.CIDR}, s.logger)
|
|
if err := manager.EnsureSocketAccessFor(ctx, req.SocketPath, "firecracker api socket", s.meta.OwnerUID, s.meta.OwnerGID); err != nil {
|
|
return 0, err
|
|
}
|
|
if strings.TrimSpace(req.VSockPath) != "" {
|
|
if err := manager.EnsureSocketAccessFor(ctx, req.VSockPath, "firecracker vsock socket", s.meta.OwnerUID, s.meta.OwnerGID); err != nil {
|
|
return 0, err
|
|
}
|
|
}
|
|
pid := manager.ResolvePID(context.Background(), machine, req.SocketPath)
|
|
if pid <= 0 {
|
|
return 0, errors.New("firecracker started but pid could not be resolved")
|
|
}
|
|
return pid, nil
|
|
}
|
|
|
|
func (s *Server) validateLaunchDrivePath(drive firecracker.DriveConfig, stateDir string) error {
|
|
if err := s.validateManagedPath(drive.Path, stateDir); err == nil {
|
|
return nil
|
|
}
|
|
if drive.IsRoot {
|
|
if err := validateDMDevicePath(drive.Path); err == nil {
|
|
return nil
|
|
}
|
|
}
|
|
return fmt.Errorf("path %q is outside banger-managed directories", drive.Path)
|
|
}
|
|
|
|
func (s *Server) ensureSocketAccess(ctx context.Context, socketPath, label string) error {
|
|
return fcproc.New(s.runner, fcproc.Config{}, s.logger).EnsureSocketAccessFor(ctx, socketPath, label, s.meta.OwnerUID, s.meta.OwnerGID)
|
|
}
|
|
|
|
func (s *Server) validateManagedPath(path string, roots ...string) error {
|
|
path = strings.TrimSpace(path)
|
|
if path == "" {
|
|
return errors.New("path is required")
|
|
}
|
|
if !filepath.IsAbs(path) {
|
|
return fmt.Errorf("path %q must be absolute", path)
|
|
}
|
|
cleaned := filepath.Clean(path)
|
|
for _, root := range roots {
|
|
root = strings.TrimSpace(root)
|
|
if root == "" {
|
|
continue
|
|
}
|
|
root = filepath.Clean(root)
|
|
if cleaned == root || strings.HasPrefix(cleaned, root+string(os.PathSeparator)) {
|
|
return nil
|
|
}
|
|
}
|
|
return fmt.Errorf("path %q is outside banger-managed directories", path)
|
|
}
|
|
|
|
func validateTapName(tapName string) error {
|
|
tapName = strings.TrimSpace(tapName)
|
|
if strings.HasPrefix(tapName, vmTapPrefix) || strings.HasPrefix(tapName, tapPoolPrefix) {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("tap %q is outside banger-managed naming", tapName)
|
|
}
|
|
|
|
func validateDMName(dmName string) error {
|
|
dmName = strings.TrimSpace(dmName)
|
|
if strings.HasPrefix(dmName, rootfsDMNamePrefix) {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("dm target %q is outside banger-managed naming", dmName)
|
|
}
|
|
|
|
func validateDMDevicePath(path string) error {
|
|
path = strings.TrimSpace(path)
|
|
if path == "" {
|
|
return errors.New("dm device path is required")
|
|
}
|
|
if !filepath.IsAbs(path) {
|
|
return fmt.Errorf("dm device path %q must be absolute", path)
|
|
}
|
|
cleaned := filepath.Clean(path)
|
|
if filepath.Dir(cleaned) != "/dev/mapper" {
|
|
return fmt.Errorf("dm device path %q is outside /dev/mapper", path)
|
|
}
|
|
return validateDMName(filepath.Base(cleaned))
|
|
}
|
|
|
|
func validateRootExecutable(path string) error {
|
|
info, err := os.Stat(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !info.Mode().IsRegular() {
|
|
return fmt.Errorf("firecracker binary %q is not a regular file", path)
|
|
}
|
|
if info.Mode().Perm()&0o111 == 0 {
|
|
return fmt.Errorf("firecracker binary %q is not executable", path)
|
|
}
|
|
if info.Mode().Perm()&0o022 != 0 {
|
|
return fmt.Errorf("firecracker binary %q must not be group/world writable", path)
|
|
}
|
|
stat, ok := info.Sys().(*syscall.Stat_t)
|
|
if !ok {
|
|
return fmt.Errorf("inspect owner for %q: unsupported file metadata", path)
|
|
}
|
|
if stat.Uid != 0 {
|
|
return fmt.Errorf("firecracker binary %q must be root-owned in system mode", path)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func marshalResultOrError(v any, err error) rpc.Response {
|
|
if err != nil {
|
|
return rpc.NewError("operation_failed", err.Error())
|
|
}
|
|
resp, marshalErr := rpc.NewResult(v)
|
|
if marshalErr != nil {
|
|
return rpc.NewError("marshal_failed", marshalErr.Error())
|
|
}
|
|
return resp
|
|
}
|