// 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//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 "" }