fix(felhomsshd): reachable=listener-present (belt blocks a dial); operator/key configured from persistent state

reachable now checks a bound listener via ss (the belt correctly drops even
localhost→felhom-sshd, so a dial always failed). operator_peer_configured derives
from the persistent wg-felhom.conf (2nd AllowedIPs /32) + operator_key from the
installed authorized_keys file — accurate immediately after an agent restart,
before the next desired-state fetch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
This commit is contained in:
2026-07-05 22:57:35 +02:00
parent 2bf9354c0e
commit 72d6132907
+34 -11
View File
@@ -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
}