183 lines
7.0 KiB
Go
Executable File
183 lines
7.0 KiB
Go
Executable File
// Cgroup-based quarantine with multi-runtime support.
|
|
//
|
|
// When the Warding detects a high-severity threat it freezes the offending
|
|
// container or VM. This uses the most appropriate mechanism for each runtime:
|
|
//
|
|
// - LXC: cgroup v2 freezer (preferred), then lxc-freeze, then systemctl
|
|
// - Podman: cgroup v2 freezer (preferred), then podman pause
|
|
// - Firecracker: VMM process SIGSTOP (no cgroup involvement)
|
|
// - BareMetal: cgroup v2 freezer if applicable, otherwise SIGSTOP
|
|
//
|
|
// On cgroup v2 systems (kernel >= 4.15, systemd >= 244) the freezer is at
|
|
// /sys/fs/cgroup/<container-path>/cgroup.freeze. Writing "1" freezes every
|
|
// process in the cgroup atomically.
|
|
package warding
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
// RuntimeAdapter is a minimal interface for runtime-specific freeze/thaw
|
|
// operations. This avoids importing the full runtime package from warding.
|
|
type RuntimeAdapter interface {
|
|
// Freeze suspends all processes in the named container/sanctum.
|
|
Freeze(nodeID string) error
|
|
// Thaw resumes a frozen container/sanctum.
|
|
Thaw(nodeID string) error
|
|
// CgroupPath returns the cgroup v2 path for the container.
|
|
CgroupPath(nodeID string) string
|
|
}
|
|
|
|
// validateNodeID rejects nodeID values that contain path traversal
|
|
// characters, preventing abuse of cgroup path construction.
|
|
func validateNodeID(nodeID string) error {
|
|
if nodeID == "" {
|
|
return fmt.Errorf("warding: nodeID must not be empty")
|
|
}
|
|
if strings.ContainsAny(nodeID, "/\\..") {
|
|
return fmt.Errorf("warding: nodeID contains invalid characters: %q", nodeID)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// freezeContainer freezes a container or VM by name using the configured
|
|
// runtime adapter. If no adapter is set, falls back to generic cgroup v2
|
|
// detection (for backward compatibility).
|
|
func freezeContainer(nodeID string) error {
|
|
if err := validateNodeID(nodeID); err != nil {
|
|
return err
|
|
}
|
|
// If a runtime adapter is configured, use it.
|
|
runtimeAdapterMu.RLock()
|
|
adapter := globalRuntimeAdapter
|
|
runtimeAdapterMu.RUnlock()
|
|
if adapter != nil {
|
|
return adapter.Freeze(nodeID)
|
|
}
|
|
return genericFreeze(nodeID)
|
|
}
|
|
|
|
// thawContainer thaws a frozen container or VM by name.
|
|
func thawContainer(nodeID string) error {
|
|
if err := validateNodeID(nodeID); err != nil {
|
|
return err
|
|
}
|
|
runtimeAdapterMu.RLock()
|
|
adapter := globalRuntimeAdapter
|
|
runtimeAdapterMu.RUnlock()
|
|
if adapter != nil {
|
|
return adapter.Thaw(nodeID)
|
|
}
|
|
return genericThaw(nodeID)
|
|
}
|
|
|
|
var (
|
|
// globalRuntimeAdapter is the runtime-specific freeze/thaw implementation.
|
|
// Set via SetRuntimeAdapter(). Protected by runtimeAdapterMu.
|
|
globalRuntimeAdapter RuntimeAdapter
|
|
runtimeAdapterMu sync.RWMutex
|
|
)
|
|
|
|
// SetRuntimeAdapter configures the runtime-specific freeze/thaw handler.
|
|
// Called by the CLI layer when the runtime is initialized.
|
|
func SetRuntimeAdapter(adapter RuntimeAdapter) {
|
|
runtimeAdapterMu.Lock()
|
|
defer runtimeAdapterMu.Unlock()
|
|
globalRuntimeAdapter = adapter
|
|
}
|
|
|
|
// genericFreeze is the fallback freeze mechanism that tries multiple
|
|
// approaches in order of preference.
|
|
func genericFreeze(nodeID string) error {
|
|
// 1. Try cgroup v2 freezer (preferred — works for LXC, Podman, any cgroup-managed process).
|
|
if v2Path := cgroupV2Path(nodeID); v2Path != "" {
|
|
if err := os.WriteFile(filepath.Join(v2Path, "cgroup.freeze"),
|
|
[]byte("1"), 0644); err == nil {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// 2. Try runtime-specific CLIs (ordered by detection).
|
|
freezeCommands := []struct {
|
|
binary string
|
|
args []string
|
|
}{
|
|
{"lxc-freeze", []string{"-n", nodeID}},
|
|
{"podman", []string{"pause", nodeID}},
|
|
{"systemctl", []string{"freeze", nodeID}},
|
|
}
|
|
|
|
for _, fc := range freezeCommands {
|
|
if _, err := exec.LookPath(fc.binary); err == nil {
|
|
if out, err := exec.Command(fc.binary, fc.args...).CombinedOutput(); err == nil {
|
|
return nil
|
|
} else {
|
|
return fmt.Errorf("%s: %w (%s)", fc.binary, err, string(out))
|
|
}
|
|
}
|
|
}
|
|
|
|
return fmt.Errorf("no freezer available (tried cgroup v2, lxc-freeze, podman pause, systemctl)")
|
|
}
|
|
|
|
// genericThaw is the fallback thaw mechanism.
|
|
func genericThaw(nodeID string) error {
|
|
// 1. Try cgroup v2.
|
|
if v2Path := cgroupV2Path(nodeID); v2Path != "" {
|
|
if err := os.WriteFile(filepath.Join(v2Path, "cgroup.freeze"),
|
|
[]byte("0"), 0644); err == nil {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// 2. Try runtime-specific CLIs.
|
|
thawCommands := []struct {
|
|
binary string
|
|
args []string
|
|
}{
|
|
{"lxc-unfreeze", []string{"-n", nodeID}},
|
|
{"podman", []string{"unpause", nodeID}},
|
|
{"systemctl", []string{"thaw", nodeID}},
|
|
}
|
|
|
|
for _, tc := range thawCommands {
|
|
if _, err := exec.LookPath(tc.binary); err == nil {
|
|
if out, err := exec.Command(tc.binary, tc.args...).CombinedOutput(); err == nil {
|
|
return nil
|
|
} else {
|
|
return fmt.Errorf("%s: %w (%s)", tc.binary, err, string(out))
|
|
}
|
|
}
|
|
}
|
|
|
|
return fmt.Errorf("no thaw mechanism available")
|
|
}
|
|
|
|
// cgroupV2Path tries to locate the cgroup v2 path for a container.
|
|
// It checks common locations for LXC, Podman, and systemd-managed containers.
|
|
func cgroupV2Path(nodeID string) string {
|
|
candidates := []string{
|
|
// LXC paths.
|
|
filepath.Join("/sys/fs/cgroup/lxc", nodeID),
|
|
filepath.Join("/sys/fs/cgroup/lxc.payload", nodeID),
|
|
filepath.Join("/sys/fs/cgroup/system.slice", "lxc-"+nodeID+".service"),
|
|
filepath.Join("/sys/fs/cgroup/machine.slice", "lxc-"+nodeID+".service"),
|
|
// Podman paths.
|
|
filepath.Join("/sys/fs/cgroup/machine.slice", "libpod-"+nodeID+".scope"),
|
|
filepath.Join("/sys/fs/cgroup/machine.slice", "podman-"+nodeID+".scope"),
|
|
// Generic systemd scope.
|
|
filepath.Join("/sys/fs/cgroup/system.slice", nodeID+".scope"),
|
|
filepath.Join("/sys/fs/cgroup/machine.slice", nodeID+".scope"),
|
|
}
|
|
for _, p := range candidates {
|
|
if _, err := os.Stat(filepath.Join(p, "cgroup.freeze")); err == nil {
|
|
return p
|
|
}
|
|
}
|
|
return ""
|
|
} |