diff --git a/internal/felhomsshd/health.go b/internal/felhomsshd/health.go index 41d9e73..d4d24c0 100644 --- a/internal/felhomsshd/health.go +++ b/internal/felhomsshd/health.go @@ -2,8 +2,8 @@ package felhomsshd import ( "context" - "net" "os" + "os/exec" "strconv" "strings" "time" @@ -64,19 +64,31 @@ func (m *Manager) Status(ctx context.Context, block *hub.WireWireguard) *hub.OOB if _, _, err := m.runner.Run(ctx, "sshd", "-t", "-f", ConfPath); err != nil { st.ConfigInvalid = true } - // local reachability (a TCP dial to the OOB port) + // Reachability = a LISTENER is bound on the OOB port (catches "active but crashed post-fork"). We + // do NOT dial: the belt (correctly) drops even localhost→felhom-sshd (tunnel-only), so a local + // dial always fails and would misreport a healthy daemon as unreachable. if st.FelhomSshdPort > 0 { - st.Reachable = dialLocal(ctx, st.FelhomSshdPort) + st.Reachable = listenerPresent(st.FelhomSshdPort) } // wg-felhom handshake age (the OOB path rides the tunnel) if age, ok := m.wgHandshakeAge(ctx); ok { st.WGHandshakeAgeS = &age } - // desired-state config reflection + // Operator-peer configured: from the in-memory block when fetched, OR (robust across an agent + // restart, before the next desired-state fetch) from the PERSISTENT rendered wg-felhom.conf — a + // second /32 in AllowedIPs is the operator peer. This keeps the report (and the oob_degraded alert + // gate) accurate immediately after a restart, not only after the next 900s heartbeat fetch. + st.OperatorPeerConfigured = wgConfHasOperatorPeer() if block != nil { - st.OperatorPeerConfigured = block.OOBPeerIP != "" + if block.OOBPeerIP != "" { + st.OperatorPeerConfigured = true + } st.OperatorKeyConfigured = strings.TrimSpace(block.OOBOperatorSSHKey) != "" } + // Operator key: robust across restart via the installed authorized_keys file. + if fi, err := os.Stat(AuthKeysUserPath); err == nil && fi.Size() > 0 { + st.OperatorKeyConfigured = true + } // last auto-heal if raw, err := os.ReadFile(HealMarkerPath); err == nil { st.HealedAt = strings.TrimSpace(string(raw)) @@ -127,13 +139,24 @@ func (m *Manager) wgHandshakeAge(ctx context.Context) (int64, bool) { return 0, false } -// dialLocal reports whether a TCP connect to 127.0.0.1:port succeeds within a short timeout. -func dialLocal(ctx context.Context, port int) bool { - d := net.Dialer{Timeout: 2 * time.Second} - conn, err := d.DialContext(ctx, "tcp", net.JoinHostPort("127.0.0.1", strconv.Itoa(port))) +// listenerPresent reports whether something is LISTENing on the port (via ss — reads kernel state, +// so the belt never blocks it, unlike a real dial). +func listenerPresent(port int) bool { + out, err := exec.Command("ss", "-Htln", "sport = :"+strconv.Itoa(port)).Output() + return err == nil && strings.TrimSpace(string(out)) != "" +} + +// wgConfHasOperatorPeer reports whether the rendered wg-felhom.conf carries a SECOND AllowedIPs /32 +// (the operator OOB peer, alongside the PBS /32). A pure file read — always current, survives restart. +func wgConfHasOperatorPeer() bool { + raw, err := os.ReadFile("/etc/wireguard/wg-felhom.conf") if err != nil { return false } - _ = conn.Close() - return true + for _, line := range strings.Split(string(raw), "\n") { + if strings.HasPrefix(strings.TrimSpace(line), "AllowedIPs") && strings.Contains(line, ",") { + return true // two or more /32s = PBS + operator + } + } + return false }