Files
mesh-host/internal/profile/detectors.go
T
jschoubben b1e9ccff6d The profile names the network manager that is running, and travels in every report (hq ADR 0161)
One capability per manager — uplink-networkmanager, uplink-systemd-networkd, uplink-dhcpcd — from
systemctl is-active, so the uplink seat's holder for a manager this machine does not run is refused
the way any missing capability is, naming it (issue 138). The apply that reports detects the profile
again and sends it, the same shape enrolment sends, so a machine that switched managers reaches the
mesh at its next push.
2026-10-01 15:58:31 +02:00

234 lines
8.8 KiB
Go

package profile
import (
"context"
"fmt"
"os"
"strings"
)
// Named capabilities. Constants rather than strings at the call site, because a capability
// nothing declares is a capability nothing can require, and a typo would produce exactly that.
const (
CapContainerRuntime = "container-runtime"
CapPackageManager = "package-manager"
CapServiceManager = "service-manager"
CapFirewall = "firewall"
CapOverlay = "overlay"
CapGraphicalSession = "graphical-session"
// CapSeat is hardware: somewhere a display server COULD run. CapGraphicalSession above is
// state: whether one IS running. Assignment needs the first.
CapSeat = "seat"
CapPrivileged = "privileged"
// The network manager this machine runs, one capability per dialect (novox/hq ADR 0161): the
// uplink seat's holder declares its own, so the holder for a manager the machine does not run
// is refused the way any missing capability is, naming it. Active, not installed — a machine
// may have two of these on disk and runs one.
CapUplinkNetworkManager = "uplink-networkmanager"
CapUplinkSystemdNetworkd = "uplink-systemd-networkd"
CapUplinkDhcpcd = "uplink-dhcpcd"
)
// commandCapability is the shape most detectors take: run something, and treat a working
// invocation as evidence.
//
// It runs a command that only succeeds if the thing is FUNCTIONING, never `--version` alone.
// A version string proves a binary is on disk, which is the assumption 04-ISSUES/007 records
// as false: the package was installed and the daemon was not running.
type commandCapability struct {
name string
command string
args []string
// why describes what a success actually proves, and is reported as the detector's `How`.
why string
// interpret decides the verdict from what the command said and how it exited.
//
// Exists because "exit zero" is not a universal answer. A degraded service manager reports
// its state on stdout and exits non-zero — it is running, and reading only the exit code
// declared no service manager on a machine whose init it was. That is 04-ISSUES/007 in the
// mirror: 007 is installed-but-broken reported present; this is working-but-imperfect
// reported absent. Both place work wrongly, and this one was only visible by running
// against a real machine.
//
// nil means the ordinary rule: success is exit zero.
interpret func(stdout string, err error) (present bool, detail string)
runner Runner
}
func (c commandCapability) Name() string { return c.name }
func (c commandCapability) Detect(ctx context.Context) Verdict {
out, err := c.runner(ctx, c.command, c.args...)
interpret := c.interpret
if interpret == nil {
interpret = exitZero
}
present, detail := interpret(out, err)
if strings.TrimSpace(detail) == "" {
// A verdict with no reason is the fault in a new place: something nobody can act on.
// Reached when a command fails silently, which systemctl does.
if present {
detail = "responded"
} else {
detail = fmt.Sprintf("%s gave no reason", c.command)
}
}
return Verdict{Name: c.name, Present: present, Detail: firstLine(detail), How: c.why}
}
// exitZero is the ordinary rule: the command worked, so the capability is there.
func exitZero(stdout string, err error) (bool, string) {
if err != nil {
return false, err.Error()
}
return true, stdout
}
// systemRunning reads what an init system says about itself rather than how it exited.
//
// `is-system-running` exits non-zero for every state except `running` — including `degraded`,
// which means units failed and the init is emphatically present. Treating that as absent made
// a machine running systemd report no service manager.
func systemRunning(stdout string, err error) (bool, string) {
state := strings.TrimSpace(firstLine(stdout))
switch state {
case "running", "degraded", "starting", "maintenance", "stopping":
return true, state
case "":
if err != nil {
return false, err.Error()
}
return false, "said nothing"
default:
// `offline` and `unknown` mean it is not managing this machine.
return false, state
}
}
func firstLine(s string) string {
s = strings.TrimSpace(s)
if i := strings.IndexByte(s, '\n'); i >= 0 {
s = s[:i]
}
if len(s) > 200 {
s = s[:200] + "…"
}
return s
}
// privileged reports whether the host can change this machine at all.
//
// Reported as a capability rather than checked at startup on purpose: a host that cannot act
// is still a host that can report, and novox/hq ADR 0004 says what varies between nodes lives
// here rather than in the definition of a node.
type privileged struct{}
func (privileged) Name() string { return CapPrivileged }
func (privileged) Detect(context.Context) Verdict {
uid := os.Geteuid()
if uid == 0 {
return Verdict{
Name: CapPrivileged, Present: true,
Detail: "effective uid 0",
How: "effective uid — the host changes a machine, which needs root",
}
}
return Verdict{
Name: CapPrivileged, Present: false,
Detail: fmt.Sprintf("effective uid %d, not 0", uid),
How: "effective uid — the host changes a machine, which needs root",
}
}
// graphicalSession reports whether anything could display a window here.
//
// Environment rather than a probe, because a display server is reachable through a socket a
// detector would have to guess at, and the variables are what an application would use anyway.
// Stated so the limit is visible: this detects that a session is ADVERTISED, which is weaker
// than the other detectors here.
type graphicalSession struct{}
func (graphicalSession) Name() string { return CapGraphicalSession }
func (graphicalSession) Detect(context.Context) Verdict {
const how = "DISPLAY / WAYLAND_DISPLAY — weaker than the other checks: advertised, not probed"
if d := os.Getenv("WAYLAND_DISPLAY"); d != "" {
return Verdict{Name: CapGraphicalSession, Present: true, Detail: "wayland: " + d, How: how}
}
if d := os.Getenv("DISPLAY"); d != "" {
return Verdict{Name: CapGraphicalSession, Present: true, Detail: "x11: " + d, How: how}
}
return Verdict{
Name: CapGraphicalSession, Present: false,
Detail: "neither DISPLAY nor WAYLAND_DISPLAY is set",
How: how,
}
}
// Default returns the detectors the host runs when nobody says otherwise.
//
// Each command is chosen to prove the thing WORKS rather than exists:
// - the container runtime is asked for server-side information, which fails when the daemon
// is down even though the client is installed — the exact shape of 04-ISSUES/007;
// - the service manager is asked whether it is the running init, not whether it is present;
// - the firewall is asked to list a ruleset, which needs both the tool and the permission.
func Default(runner Runner) []Detector {
if runner == nil {
runner = ExecRunner
}
return []Detector{
privileged{},
graphicalSession{},
seat{},
commandCapability{
name: CapContainerRuntime, command: "docker", args: []string{"info", "--format", "{{.ServerVersion}}"},
why: "asks the daemon for its version — a running daemon, not an installed client",
runner: runner,
},
commandCapability{
name: CapPackageManager, command: "pacman", args: []string{"-Q", "pacman"},
why: "queries the package database — a working database, not a binary on disk",
runner: runner,
},
commandCapability{
name: CapServiceManager, command: "systemctl", args: []string{"is-system-running"},
why: "reads the init's own account of its state — degraded is still running",
interpret: systemRunning,
runner: runner,
},
commandCapability{
name: CapFirewall, command: "nft", args: []string{"list", "ruleset"},
why: "lists the ruleset — needs the tool AND the privilege to use it",
runner: runner,
},
commandCapability{
name: CapOverlay, command: "wg", args: []string{"show", "interfaces"},
why: "asks the kernel for interfaces — needs the module, not just the tool",
runner: runner,
},
commandCapability{
name: CapUplinkNetworkManager, command: "systemctl", args: []string{"is-active", "NetworkManager.service"},
why: "asks the init whether NetworkManager is running — the dialect the uplink seat's holder must speak",
runner: runner,
},
commandCapability{
name: CapUplinkSystemdNetworkd, command: "systemctl", args: []string{"is-active", "systemd-networkd.service"},
why: "asks the init whether systemd-networkd is running — the dialect the uplink seat's holder must speak",
runner: runner,
},
commandCapability{
name: CapUplinkDhcpcd, command: "systemctl", args: []string{"is-active", "dhcpcd.service"},
why: "asks the init whether dhcpcd is running — the dialect the uplink seat's holder must speak",
runner: runner,
},
}
}
// isRoot is the same question `privileged` answers, exposed for tests that must check the
// detector against something other than itself.
func isRoot() bool { return os.Geteuid() == 0 }