Files
josh a0c0fb114f
CI / Lint + build + test (push) Has been cancelled
Add host-mode heartbeat: vetting-agent host + last-seen badge
vetting-agent gains a `host` subcommand that runs as a systemd service
installed by the quick-register one-liner, POSTing every 30s to
/api/v1/hosts/{mac}/heartbeat so the dashboard tile shows "online" or
"Nm ago" without waiting on WoL. Ships dormant client code for the
Phase 2 reboot_for_vetting command so the server can flip it on later
without a binary redeploy.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-17 23:34:15 -04:00

48 lines
1.3 KiB
Go

package hostmode
import (
"bufio"
"errors"
"fmt"
"os"
"os/exec"
"strings"
)
// primaryMAC resolves the MAC of the iface that carries the default
// IPv4 route. Mirrors quick.sh.tmpl's primary_iface so the agent
// reports the same MAC that was registered (important on Proxmox
// where vmbr0 inherits its physical NIC's MAC).
func primaryMAC() (string, error) {
iface, err := defaultRouteIface()
if err != nil {
return "", err
}
raw, err := os.ReadFile(fmt.Sprintf("/sys/class/net/%s/address", iface))
if err != nil {
return "", fmt.Errorf("read mac for %s: %w", iface, err)
}
return strings.ToLower(strings.TrimSpace(string(raw))), nil
}
// defaultRouteIface shells out to `ip` because reading /proc/net/route
// requires hex-swap logic and still misses the IPv4-only "dev"
// qualification. The service runs as root on a Linux box; `ip` is
// always present.
func defaultRouteIface() (string, error) {
out, err := exec.Command("ip", "-o", "-4", "route", "show", "default").Output()
if err != nil {
return "", fmt.Errorf("ip route: %w", err)
}
scan := bufio.NewScanner(strings.NewReader(string(out)))
for scan.Scan() {
fields := strings.Fields(scan.Text())
for i, f := range fields {
if f == "dev" && i+1 < len(fields) {
return fields[i+1], nil
}
}
}
return "", errors.New("no default IPv4 route")
}