Pulse/internal/monitoring/monitor_cluster_helpers.go
2026-04-01 11:51:19 +01:00

269 lines
6.1 KiB
Go

package monitoring
import (
"net"
"net/url"
"strings"
"github.com/rcourtman/pulse-go-rewrite/internal/config"
)
var lookupIPFunc = net.LookupIP
func lookupClusterEndpointLabel(instance *config.PVEInstance, nodeName string) string {
if instance == nil {
return ""
}
for _, endpoint := range instance.ClusterEndpoints {
if !strings.EqualFold(endpoint.NodeName, nodeName) {
continue
}
if host := strings.TrimSpace(endpoint.Host); host != "" {
if label := normalizeEndpointHost(host); label != "" && !isLikelyIPAddress(label) {
return label
}
}
if nodeNameLabel := strings.TrimSpace(endpoint.NodeName); nodeNameLabel != "" {
return nodeNameLabel
}
if ip := strings.TrimSpace(endpoint.IP); ip != "" {
return ip
}
}
return ""
}
func normalizeEndpointHost(raw string) string {
value := strings.TrimSpace(raw)
if value == "" {
return ""
}
if parsed, err := url.Parse(value); err == nil && parsed.Host != "" {
host := parsed.Hostname()
if host != "" {
return host
}
return parsed.Host
}
value = strings.TrimPrefix(value, "https://")
value = strings.TrimPrefix(value, "http://")
value = strings.TrimSpace(value)
if value == "" {
return ""
}
if idx := strings.Index(value, "/"); idx >= 0 {
value = strings.TrimSpace(value[:idx])
}
if idx := strings.Index(value, ":"); idx >= 0 {
value = strings.TrimSpace(value[:idx])
}
return value
}
func isLikelyIPAddress(value string) bool {
if value == "" {
return false
}
if ip := net.ParseIP(value); ip != nil {
return true
}
// Handle IPv6 with zone identifier (fe80::1%eth0)
if i := strings.Index(value, "%"); i > 0 {
if ip := net.ParseIP(value[:i]); ip != nil {
return true
}
}
return false
}
func ensureClusterEndpointURL(raw string) string {
value := strings.TrimSpace(raw)
if value == "" {
return ""
}
lower := strings.ToLower(value)
if strings.HasPrefix(lower, "http://") || strings.HasPrefix(lower, "https://") {
return value
}
if _, _, err := net.SplitHostPort(value); err == nil {
return "https://" + value
}
return "https://" + net.JoinHostPort(value, "8006")
}
func clusterEndpointEffectiveURL(endpoint config.ClusterEndpoint, verifySSL bool, hasFingerprint bool) string {
// When TLS hostname verification is required (VerifySSL=true and no fingerprint),
// prefer hostname over IP to ensure certificate CN/SAN validation works correctly.
// When TLS is not verified (VerifySSL=false) or a fingerprint is provided (which
// bypasses hostname checks), prefer IP to reduce DNS lookups (refs #620).
requiresHostnameForTLS := verifySSL && !hasFingerprint
// Use EffectiveIP() which prefers user-specified IPOverride over auto-discovered IP
effectiveIP := endpoint.EffectiveIP()
if requiresHostnameForTLS {
// Prefer hostname for proper TLS certificate validation
if endpoint.Host != "" {
return ensureClusterEndpointURL(endpoint.Host)
}
if effectiveIP != "" {
return ensureClusterEndpointURL(effectiveIP)
}
} else {
// Prefer IP address to avoid excessive DNS lookups
if effectiveIP != "" {
return ensureClusterEndpointURL(effectiveIP)
}
if endpoint.Host != "" {
return ensureClusterEndpointURL(endpoint.Host)
}
}
return ""
}
func discoveryPolicyCIDRs(cidrs []string) []*net.IPNet {
networks := make([]*net.IPNet, 0, len(cidrs))
for _, cidr := range cidrs {
cidr = strings.TrimSpace(cidr)
if cidr == "" {
continue
}
_, network, err := net.ParseCIDR(cidr)
if err != nil {
continue
}
networks = append(networks, network)
}
return networks
}
func discoveryPolicyBlockedIPs(ips []string) map[string]struct{} {
blocked := make(map[string]struct{}, len(ips))
for _, raw := range ips {
ip := net.ParseIP(strings.TrimSpace(raw))
if ip == nil {
continue
}
blocked[ip.String()] = struct{}{}
}
return blocked
}
func discoveryPolicyAllowsIP(ip net.IP, allowlist, blocklist []*net.IPNet, blockedIPs map[string]struct{}) bool {
if ip == nil {
return false
}
if _, blocked := blockedIPs[ip.String()]; blocked {
return false
}
for _, network := range blocklist {
if network.Contains(ip) {
return false
}
}
if len(allowlist) == 0 {
return true
}
for _, network := range allowlist {
if network.Contains(ip) {
return true
}
}
return false
}
func discoveryPolicyIPsForEndpointHost(candidateURL string) []net.IP {
if candidateURL == "" {
return nil
}
host := normalizeEndpointHost(candidateURL)
if host == "" {
return nil
}
if ip := net.ParseIP(host); ip != nil {
return []net.IP{ip}
}
ips, err := lookupIPFunc(host)
if err != nil {
return nil
}
filtered := make([]net.IP, 0, len(ips))
for _, ip := range ips {
if ip == nil {
continue
}
filtered = append(filtered, ip)
}
return filtered
}
func clusterEndpointAllowedByDiscoveryPolicy(endpoint config.ClusterEndpoint, candidateURL string, discoveryCfg config.DiscoveryConfig) bool {
if len(discoveryCfg.SubnetAllowlist) == 0 && len(discoveryCfg.SubnetBlocklist) == 0 && len(discoveryCfg.IPBlocklist) == 0 {
return true
}
allowlist := discoveryPolicyCIDRs(discoveryCfg.SubnetAllowlist)
blocklist := discoveryPolicyCIDRs(discoveryCfg.SubnetBlocklist)
blockedIPs := discoveryPolicyBlockedIPs(discoveryCfg.IPBlocklist)
resolvedIPs := discoveryPolicyIPsForEndpointHost(candidateURL)
if len(resolvedIPs) == 0 {
if ip := net.ParseIP(strings.TrimSpace(endpoint.EffectiveIP())); ip != nil {
resolvedIPs = []net.IP{ip}
}
}
if len(resolvedIPs) == 0 {
return true
}
for _, ip := range resolvedIPs {
if !discoveryPolicyAllowsIP(ip, allowlist, blocklist, blockedIPs) {
return false
}
}
return true
}
func clusterEndpointRuntimeURL(endpoint config.ClusterEndpoint, verifySSL bool, hasFingerprint bool, discoveryCfg config.DiscoveryConfig) string {
candidateURL := clusterEndpointEffectiveURL(endpoint, verifySSL, hasFingerprint)
if candidateURL == "" {
return ""
}
if !clusterEndpointAllowedByDiscoveryPolicy(endpoint, candidateURL, discoveryCfg) {
return ""
}
return candidateURL
}
func monitorDiscoveryConfig(m *Monitor) config.DiscoveryConfig {
if m == nil || m.config == nil {
return config.DiscoveryConfig{}
}
return m.config.Discovery
}