diff --git a/cmd/pulse-sensor-proxy/ssh.go b/cmd/pulse-sensor-proxy/ssh.go index faf45cd..d999be4 100644 --- a/cmd/pulse-sensor-proxy/ssh.go +++ b/cmd/pulse-sensor-proxy/ssh.go @@ -690,6 +690,13 @@ func discoverLocalHostAddresses() ([]string, error) { ipCount := 0 interfaces, err := net.Interfaces() if err != nil { + // Check if this is an AF_NETLINK restriction error from systemd + if strings.Contains(err.Error(), "netlinkrib") || strings.Contains(err.Error(), "address family not supported") { + log.Warn(). + Err(err). + Msg("AF_NETLINK restricted by systemd - falling back to 'ip addr' command. Update systemd unit to add AF_NETLINK to RestrictAddressFamilies.") + return discoverLocalHostAddressesFallback() + } log.Warn(). Err(err). Msg("Failed to enumerate network interfaces - temperature monitoring may require manual allowed_nodes configuration") @@ -771,6 +778,87 @@ func discoverLocalHostAddresses() ([]string, error) { return result, nil } +// discoverLocalHostAddressesFallback uses 'ip addr' command when AF_NETLINK is restricted +func discoverLocalHostAddressesFallback() ([]string, error) { + addresses := make(map[string]struct{}) + + // Get hostname and FQDN (same as native version) + if hostname, err := os.Hostname(); err == nil && hostname != "" { + addresses[strings.ToLower(hostname)] = struct{}{} + cmd := exec.Command("hostname", "-f") + if out, err := cmd.Output(); err == nil { + fqdn := strings.TrimSpace(string(out)) + if fqdn != "" && fqdn != hostname { + addresses[strings.ToLower(fqdn)] = struct{}{} + } + } + } + + // Use 'ip addr' to get IP addresses + cmd := exec.Command("ip", "addr", "show") + out, err := cmd.Output() + if err != nil { + log.Warn().Err(err).Msg("Failed to run 'ip addr' command") + // Return at least the hostname + result := make([]string, 0, len(addresses)) + for addr := range addresses { + result = append(result, addr) + } + return result, nil + } + + // Parse 'ip addr' output for inet/inet6 lines + // Example: " inet 192.168.0.5/24 brd 192.168.0.255 scope global eno1" + ipCount := 0 + lines := strings.Split(string(out), "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + if !strings.HasPrefix(line, "inet ") && !strings.HasPrefix(line, "inet6 ") { + continue + } + + fields := strings.Fields(line) + if len(fields) < 2 { + continue + } + + // Second field is the IP/CIDR (e.g., "192.168.0.5/24") + ipCIDR := fields[1] + ipStr, _, _ := strings.Cut(ipCIDR, "/") + + ip := net.ParseIP(ipStr) + if ip == nil || ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsUnspecified() { + continue + } + + addresses[ip.String()] = struct{}{} + ipCount++ + } + + result := make([]string, 0, len(addresses)) + for addr := range addresses { + result = append(result, addr) + } + + if len(result) == 0 { + return nil, fmt.Errorf("no local host addresses found") + } + + // Log helpful info about discovered addresses + logger := log.Info(). + Strs("addresses", result). + Int("ip_count", ipCount). + Int("hostname_count", len(result)-ipCount) + + if ipCount == 0 { + logger.Msg("WARNING: No IP addresses discovered via 'ip addr' fallback - only hostnames available. Temperature monitoring may require manual allowed_nodes configuration.") + } else { + logger.Msg("Discovered local host addresses via 'ip addr' fallback (systemd unit needs AF_NETLINK update)") + } + + return result, nil +} + // isProxmoxHost checks if we're running on a Proxmox host func isProxmoxHost() bool { // Check for pvecm command diff --git a/cmd/pulse-sensor-proxy/validation.go b/cmd/pulse-sensor-proxy/validation.go index 2016dcc..5949eb6 100644 --- a/cmd/pulse-sensor-proxy/validation.go +++ b/cmd/pulse-sensor-proxy/validation.go @@ -324,7 +324,7 @@ func newNodeValidator(cfg *Config, metrics *ProxyMetrics) (*nodeValidator, error if v.strict { log.Warn().Msg("strict_node_validation enabled but no allowlist or cluster context is available") } else { - log.Info().Msg("Node validator running in permissive mode (no allowlist or cluster context)") + log.Warn().Msg("SECURITY: Node validator running in permissive mode (no allowlist or cluster context) - all nodes allowed. Configure allowed_nodes to restrict access.") } } @@ -360,10 +360,10 @@ func (v *nodeValidator) Validate(ctx context.Context, node string) error { // Cluster query failed (e.g., IPC permission denied, running in LXC) // Fall through to permissive mode rather than blocking all requests v.recordFailure(validationReasonClusterFailed) - log.Debug(). + log.Warn(). Err(err). Str("node", node). - Msg("Cluster validation unavailable, allowing request (consider configuring allowed_nodes for security)") + Msg("SECURITY: Cluster validation unavailable - allowing all nodes. Configure allowed_nodes in config to restrict access.") // Fall through to permissive mode below } else if !allowed { return v.deny(node, validationReasonNotClusterMember)