Fix temperature monitoring on standalone Proxmox nodes (addresses #571)

Root cause: The systemd service hardening blocked AF_NETLINK sockets,
preventing IP address discovery on standalone nodes. The proxy could
only discover hostnames, causing node_not_cluster_member rejections
when users configured Pulse with IP addresses.

Changes:
1. Add AF_NETLINK to RestrictAddressFamilies in all systemd services
   - pulse-sensor-proxy.service
   - install-sensor-proxy.sh (both modes)
   - pulse-sensor-cleanup.service

2. Replace shell-based 'ip addr' with Go native net.Interfaces() API
   - More reliable and doesn't require external commands
   - Works even with strict systemd restrictions
   - Properly filters loopback, link-local, and down interfaces

3. Improve error logging and user guidance
   - Warn when no IP addresses can be discovered
   - Provide clear instructions about allowed_nodes workaround
   - Include address counts in logs for debugging

This fix ensures standalone Proxmox nodes can properly validate
temperature requests by IP address without requiring manual
allowed_nodes configuration.
This commit is contained in:
rcourtman 2025-11-13 13:02:15 +00:00
parent c8fc20dae0
commit ca8cc0844a
4 changed files with 62 additions and 31 deletions

View file

@ -7,6 +7,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"net"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
@ -680,42 +681,64 @@ func discoverLocalHostAddresses() ([]string, error) {
} }
} }
// Get all non-loopback IP addresses using ip command // Get all non-loopback IP addresses using Go's native net.Interfaces API
cmd := exec.Command("ip", "-o", "addr", "show") // This is more reliable than shelling out to 'ip addr' and works even with strict systemd restrictions
out, err := cmd.Output() ipCount := 0
interfaces, err := net.Interfaces()
if err != nil { if err != nil {
log.Warn().Err(err).Msg("Failed to get IP addresses via 'ip addr'") log.Warn().
Err(err).
Msg("Failed to enumerate network interfaces - temperature monitoring may require manual allowed_nodes configuration")
} else { } else {
// Parse output lines like: for _, iface := range interfaces {
// 2: eth0 inet 192.168.0.100/24 brd 192.168.0.255 scope global eth0 // Skip loopback interfaces
// 2: eth0 inet6 fe80::a00:27ff:fe4e:66a1/64 scope link if iface.Flags&net.FlagLoopback != 0 {
lines := strings.Split(string(out), "\n")
for _, line := range lines {
fields := strings.Fields(line)
if len(fields) < 4 {
continue continue
} }
// Field 2 is interface, field 3 is inet/inet6, field 4 is addr/prefix // Skip interfaces that are down
if fields[2] != "inet" && fields[2] != "inet6" { if iface.Flags&net.FlagUp == 0 {
continue continue
} }
// Split addr/prefix (e.g., "192.168.0.100/24") addrs, err := iface.Addrs()
addrWithPrefix := fields[3] if err != nil {
addr := strings.Split(addrWithPrefix, "/")[0] log.Debug().
Err(err).
// Skip loopback addresses Str("interface", iface.Name).
if strings.HasPrefix(addr, "127.") || addr == "::1" { Msg("Failed to get addresses for interface")
continue continue
} }
// Skip link-local IPv6 for _, addr := range addrs {
if strings.HasPrefix(addr, "fe80:") { var ip net.IP
continue switch v := addr.(type) {
} case *net.IPNet:
ip = v.IP
case *net.IPAddr:
ip = v.IP
default:
continue
}
addresses[addr] = struct{}{} // Skip loopback addresses
if ip.IsLoopback() {
continue
}
// Skip link-local IPv6 addresses
if ip.IsLinkLocalUnicast() {
continue
}
// Skip unspecified addresses
if ip.IsUnspecified() {
continue
}
addresses[ip.String()] = struct{}{}
ipCount++
}
} }
} }
@ -729,9 +752,17 @@ func discoverLocalHostAddresses() ([]string, error) {
return nil, fmt.Errorf("no local host addresses found") return nil, fmt.Errorf("no local host addresses found")
} }
log.Info(). // Log helpful info about discovered addresses
logger := log.Info().
Strs("addresses", result). Strs("addresses", result).
Msg("Discovered local host addresses for standalone node validation") Int("ip_count", ipCount).
Int("hostname_count", len(result)-ipCount)
if ipCount == 0 {
logger.Msg("WARNING: No IP addresses discovered for standalone node - only hostnames available. If temperature monitoring fails with 'node_not_cluster_member' errors, add the node's IP to allowed_nodes in /etc/pulse-sensor-proxy/config.yaml")
} else {
logger.Msg("Discovered local host addresses for standalone node validation")
}
return result, nil return result, nil
} }

View file

@ -726,7 +726,7 @@ ProcSubset=pid
LockPersonality=true LockPersonality=true
RemoveIPC=true RemoveIPC=true
RestrictSUIDSGID=true RestrictSUIDSGID=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK
RestrictNamespaces=true RestrictNamespaces=true
SystemCallFilter=@system-service SystemCallFilter=@system-service
SystemCallErrorNumber=EPERM SystemCallErrorNumber=EPERM
@ -786,7 +786,7 @@ ProcSubset=pid
LockPersonality=true LockPersonality=true
RemoveIPC=true RemoveIPC=true
RestrictSUIDSGID=true RestrictSUIDSGID=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK
RestrictNamespaces=true RestrictNamespaces=true
SystemCallFilter=@system-service SystemCallFilter=@system-service
SystemCallErrorNumber=EPERM SystemCallErrorNumber=EPERM
@ -1237,7 +1237,7 @@ ProtectKernelModules=true
ProtectControlGroups=true ProtectControlGroups=true
PrivateTmp=true PrivateTmp=true
RestrictSUIDSGID=true RestrictSUIDSGID=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK
LimitNOFILE=1024 LimitNOFILE=1024
[Install] [Install]

View file

@ -33,7 +33,7 @@ ProcSubset=pid
LockPersonality=true LockPersonality=true
RemoveIPC=true RemoveIPC=true
RestrictSUIDSGID=true RestrictSUIDSGID=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK
RestrictNamespaces=true RestrictNamespaces=true
SystemCallFilter=@system-service SystemCallFilter=@system-service
SystemCallErrorNumber=EPERM SystemCallErrorNumber=EPERM

View file

@ -24,7 +24,7 @@ ProtectKernelModules=true
ProtectControlGroups=true ProtectControlGroups=true
PrivateTmp=true PrivateTmp=true
RestrictSUIDSGID=true RestrictSUIDSGID=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK
LimitNOFILE=1024 LimitNOFILE=1024
[Install] [Install]