Files
volt/pkg/kernel/manager.go
Karl Clinger 81ad0b597c Volt CLI: source-available under AGPSL v5.0
Complete infrastructure platform CLI:
- Container runtime (systemd-nspawn)
- VoltVisor VMs (Neutron Stardust / QEMU)
- Stellarium CAS (content-addressed storage)
- ORAS Registry
- GitOps integration
- Landlock LSM security
- Compose orchestration
- Mesh networking

Copyright (c) Armored Gates LLC. All rights reserved.
Licensed under AGPSL v5.0
2026-03-21 00:31:12 -05:00

439 lines
13 KiB
Go

/*
Kernel Manager - Download, verify, and manage kernels for Volt hybrid runtime.
Provides kernel lifecycle operations:
- Download kernels to /var/lib/volt/kernels/
- Verify SHA-256 checksums
- List available (local) kernels
- Default kernel selection (host kernel fallback)
- Kernel config validation (namespaces, cgroups, Landlock)
Copyright (c) Armored Gates LLC. All rights reserved.
*/
package kernel
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"net/http"
"os"
"os/exec"
"path/filepath"
"sort"
"strings"
"time"
)
const (
// DefaultKernelDir is where kernels are stored on disk.
DefaultKernelDir = "/var/lib/volt/kernels"
// HostKernelPath is the default host kernel image location.
HostKernelPath = "/boot/vmlinuz"
// configGzPath is the compressed kernel config inside /proc.
configGzPath = "/proc/config.gz"
)
// KernelInfo describes a locally available kernel.
type KernelInfo struct {
Version string // e.g. "6.1.0-42-amd64"
Path string // absolute path to vmlinuz
Size int64 // bytes
SHA256 string // hex-encoded checksum
Source string // "host", "downloaded", "custom"
AddedAt time.Time // when the kernel was registered
IsDefault bool // whether this is the active default
}
// RequiredFeature is a kernel config option that must be present.
type RequiredFeature struct {
Config string // e.g. "CONFIG_NAMESPACES"
Description string // human-readable explanation
}
// RequiredFeatures lists kernel config options needed for Volt hybrid mode.
var RequiredFeatures = []RequiredFeature{
{Config: "CONFIG_NAMESPACES", Description: "Namespace support (PID, NET, MNT, UTS, IPC)"},
{Config: "CONFIG_PID_NS", Description: "PID namespace isolation"},
{Config: "CONFIG_NET_NS", Description: "Network namespace isolation"},
{Config: "CONFIG_USER_NS", Description: "User namespace isolation"},
{Config: "CONFIG_UTS_NS", Description: "UTS namespace isolation"},
{Config: "CONFIG_IPC_NS", Description: "IPC namespace isolation"},
{Config: "CONFIG_CGROUPS", Description: "Control groups support"},
{Config: "CONFIG_CGROUP_V2", Description: "Cgroups v2 unified hierarchy"},
{Config: "CONFIG_SECURITY_LANDLOCK", Description: "Landlock LSM filesystem sandboxing"},
{Config: "CONFIG_SECCOMP", Description: "Seccomp syscall filtering"},
{Config: "CONFIG_SECCOMP_FILTER", Description: "Seccomp BPF filter programs"},
}
// Manager handles kernel downloads, verification, and selection.
type Manager struct {
kernelDir string
}
// NewManager creates a new kernel manager rooted at the given directory.
// If kernelDir is empty, DefaultKernelDir is used.
func NewManager(kernelDir string) *Manager {
if kernelDir == "" {
kernelDir = DefaultKernelDir
}
return &Manager{kernelDir: kernelDir}
}
// Init ensures the kernel directory exists.
func (m *Manager) Init() error {
return os.MkdirAll(m.kernelDir, 0755)
}
// KernelDir returns the base directory for kernel storage.
func (m *Manager) KernelDir() string {
return m.kernelDir
}
// ── Download & Verify ────────────────────────────────────────────────────────
// Download fetches a kernel image from url into the kernel directory under the
// given version name. If expectedSHA256 is non-empty the download is verified
// against it; a mismatch causes the file to be removed and an error returned.
func (m *Manager) Download(version, url, expectedSHA256 string) (*KernelInfo, error) {
if err := m.Init(); err != nil {
return nil, fmt.Errorf("kernel dir init: %w", err)
}
destDir := filepath.Join(m.kernelDir, version)
if err := os.MkdirAll(destDir, 0755); err != nil {
return nil, fmt.Errorf("create version dir: %w", err)
}
destPath := filepath.Join(destDir, "vmlinuz")
// Download to temp file first, then rename.
tmpPath := destPath + ".tmp"
out, err := os.Create(tmpPath)
if err != nil {
return nil, fmt.Errorf("create temp file: %w", err)
}
defer func() {
out.Close()
os.Remove(tmpPath) // clean up on any failure path
}()
client := &http.Client{Timeout: 10 * time.Minute}
resp, err := client.Get(url)
if err != nil {
return nil, fmt.Errorf("download failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("download returned HTTP %d", resp.StatusCode)
}
hasher := sha256.New()
writer := io.MultiWriter(out, hasher)
if _, err := io.Copy(writer, resp.Body); err != nil {
return nil, fmt.Errorf("download interrupted: %w", err)
}
if err := out.Close(); err != nil {
return nil, fmt.Errorf("close temp file: %w", err)
}
checksum := hex.EncodeToString(hasher.Sum(nil))
if expectedSHA256 != "" && !strings.EqualFold(checksum, expectedSHA256) {
os.Remove(tmpPath)
return nil, fmt.Errorf("checksum mismatch: got %s, expected %s", checksum, expectedSHA256)
}
if err := os.Rename(tmpPath, destPath); err != nil {
return nil, fmt.Errorf("rename to final path: %w", err)
}
// Write checksum sidecar.
checksumPath := filepath.Join(destDir, "sha256")
os.WriteFile(checksumPath, []byte(checksum+"\n"), 0644)
fi, _ := os.Stat(destPath)
return &KernelInfo{
Version: version,
Path: destPath,
Size: fi.Size(),
SHA256: checksum,
Source: "downloaded",
AddedAt: time.Now(),
}, nil
}
// VerifyChecksum checks that the kernel at path matches the expected SHA-256
// hex digest. Returns nil on match.
func VerifyChecksum(path, expectedSHA256 string) error {
f, err := os.Open(path)
if err != nil {
return fmt.Errorf("open kernel: %w", err)
}
defer f.Close()
h := sha256.New()
if _, err := io.Copy(h, f); err != nil {
return fmt.Errorf("read kernel: %w", err)
}
got := hex.EncodeToString(h.Sum(nil))
if !strings.EqualFold(got, expectedSHA256) {
return fmt.Errorf("checksum mismatch: got %s, expected %s", got, expectedSHA256)
}
return nil
}
// Checksum computes and returns the SHA-256 hex digest of the file at path.
func Checksum(path string) (string, error) {
f, err := os.Open(path)
if err != nil {
return "", fmt.Errorf("open: %w", err)
}
defer f.Close()
h := sha256.New()
if _, err := io.Copy(h, f); err != nil {
return "", fmt.Errorf("read: %w", err)
}
return hex.EncodeToString(h.Sum(nil)), nil
}
// ── List ─────────────────────────────────────────────────────────────────────
// List returns all locally available kernels sorted by version name.
func (m *Manager) List() ([]KernelInfo, error) {
entries, err := os.ReadDir(m.kernelDir)
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, fmt.Errorf("read kernel dir: %w", err)
}
var kernels []KernelInfo
for _, entry := range entries {
if !entry.IsDir() {
continue
}
version := entry.Name()
vmlinuz := filepath.Join(m.kernelDir, version, "vmlinuz")
fi, err := os.Stat(vmlinuz)
if err != nil {
continue // not a valid kernel directory
}
ki := KernelInfo{
Version: version,
Path: vmlinuz,
Size: fi.Size(),
Source: "downloaded",
}
// Read checksum sidecar if present.
if data, err := os.ReadFile(filepath.Join(m.kernelDir, version, "sha256")); err == nil {
ki.SHA256 = strings.TrimSpace(string(data))
}
kernels = append(kernels, ki)
}
sort.Slice(kernels, func(i, j int) bool {
return kernels[i].Version < kernels[j].Version
})
return kernels, nil
}
// ── Default Kernel Selection ─────────────────────────────────────────────────
// DefaultKernel returns the best kernel to use:
// 1. The host kernel at /boot/vmlinuz-$(uname -r).
// 2. Generic /boot/vmlinuz fallback.
// 3. The latest locally downloaded kernel.
//
// Returns the absolute path to the kernel image.
func (m *Manager) DefaultKernel() (string, error) {
// Prefer the host kernel matching the running version.
uname := currentKernelVersion()
hostPath := "/boot/vmlinuz-" + uname
if fileExists(hostPath) {
return hostPath, nil
}
// Generic fallback.
if fileExists(HostKernelPath) {
return HostKernelPath, nil
}
// Check locally downloaded kernels — pick the latest.
kernels, err := m.List()
if err == nil && len(kernels) > 0 {
return kernels[len(kernels)-1].Path, nil
}
return "", fmt.Errorf("no kernel found (checked %s, %s, %s)", hostPath, HostKernelPath, m.kernelDir)
}
// ResolveKernel resolves a kernel reference to an absolute path.
// If kernelRef is an absolute path and exists, it is returned directly.
// Otherwise, it is treated as a version name under kernelDir.
// If empty, DefaultKernel() is used.
func (m *Manager) ResolveKernel(kernelRef string) (string, error) {
if kernelRef == "" {
return m.DefaultKernel()
}
// Absolute path — use directly.
if filepath.IsAbs(kernelRef) {
if !fileExists(kernelRef) {
return "", fmt.Errorf("kernel not found: %s", kernelRef)
}
return kernelRef, nil
}
// Treat as version name.
path := filepath.Join(m.kernelDir, kernelRef, "vmlinuz")
if fileExists(path) {
return path, nil
}
return "", fmt.Errorf("kernel version %q not found in %s", kernelRef, m.kernelDir)
}
// ── Kernel Config Validation ─────────────────────────────────────────────────
// ValidationResult holds the outcome of a kernel config check.
type ValidationResult struct {
Feature RequiredFeature
Present bool
Value string // "y", "m", or empty
}
// ValidateHostKernel checks the running host kernel's config for required
// features. It reads from /boot/config-$(uname -r) or /proc/config.gz.
func ValidateHostKernel() ([]ValidationResult, error) {
uname := currentKernelVersion()
configPath := "/boot/config-" + uname
configData, err := os.ReadFile(configPath)
if err != nil {
// Try /proc/config.gz via zcat
configData, err = readProcConfigGz()
if err != nil {
return nil, fmt.Errorf("cannot read kernel config (tried %s and %s): %w",
configPath, configGzPath, err)
}
}
return validateConfig(string(configData)), nil
}
// ValidateConfigFile checks a kernel config file at the given path for
// required features.
func ValidateConfigFile(path string) ([]ValidationResult, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read config file: %w", err)
}
return validateConfig(string(data)), nil
}
// validateConfig parses a kernel .config text and checks for required features.
func validateConfig(configText string) []ValidationResult {
configMap := make(map[string]string)
for _, line := range strings.Split(configText, "\n") {
line = strings.TrimSpace(line)
if line == "" {
continue
}
// Check for "# CONFIG_FOO is not set" pattern.
if strings.HasPrefix(line, "# ") && strings.HasSuffix(line, " is not set") {
key := strings.TrimPrefix(line, "# ")
key = strings.TrimSuffix(key, " is not set")
configMap[key] = "n"
continue
}
if strings.HasPrefix(line, "#") {
continue
}
parts := strings.SplitN(line, "=", 2)
if len(parts) == 2 {
configMap[parts[0]] = parts[1]
}
}
var results []ValidationResult
for _, feat := range RequiredFeatures {
val := configMap[feat.Config]
r := ValidationResult{Feature: feat}
if val == "y" || val == "m" {
r.Present = true
r.Value = val
}
results = append(results, r)
}
return results
}
// AllFeaturesPresent returns true if every validation result is present.
func AllFeaturesPresent(results []ValidationResult) bool {
for _, r := range results {
if !r.Present {
return false
}
}
return true
}
// MissingFeatures returns only the features that are not present.
func MissingFeatures(results []ValidationResult) []ValidationResult {
var missing []ValidationResult
for _, r := range results {
if !r.Present {
missing = append(missing, r)
}
}
return missing
}
// ── Helpers ──────────────────────────────────────────────────────────────────
// currentKernelVersion returns the running kernel version string (uname -r).
func currentKernelVersion() string {
data, err := os.ReadFile("/proc/sys/kernel/osrelease")
if err == nil {
return strings.TrimSpace(string(data))
}
// Fallback: shell out to uname.
out, err := exec.Command("uname", "-r").Output()
if err == nil {
return strings.TrimSpace(string(out))
}
return "unknown"
}
// readProcConfigGz reads kernel config from /proc/config.gz using zcat.
func readProcConfigGz() ([]byte, error) {
if !fileExists(configGzPath) {
return nil, fmt.Errorf("%s not found (try: modprobe configs)", configGzPath)
}
return exec.Command("zcat", configGzPath).Output()
}
// fileExists returns true if the path exists and is not a directory.
func fileExists(path string) bool {
fi, err := os.Stat(path)
if err != nil {
return false
}
return !fi.IsDir()
}