mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-07 00:37:36 +00:00
269 lines
6.1 KiB
Go
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
|
|
}
|