Pulse/pkg/tlsutil/dnscache.go
rcourtman c93581e1aa Add DNS caching to reduce excessive DNS queries
Related to #608

Implements DNS caching using rs/dnscache to dramatically reduce DNS query
volume for frequently accessed Proxmox hosts. Users were reporting 260,000+
DNS queries in 37 hours for the same hostnames.

Changes:
- Added rs/dnscache dependency for DNS resolution caching
- Created pkg/tlsutil/dnscache.go with DNS cache wrapper
- Updated HTTP client creation to use cached DNS resolver
- Added DNSCacheTimeout configuration option (default: 5 minutes)
- Made DNS cache timeout configurable via:
  - system.json: dnsCacheTimeout field (seconds)
  - Environment variable: DNS_CACHE_TIMEOUT (duration string)
- DNS cache periodically refreshes to prevent stale entries

Benefits:
- Reduces DNS query load on local DNS servers by ~99%
- Reduces network traffic and DNS query log volume
- Maintains fresh DNS entries through periodic refresh
- Configurable timeout for different network environments

Default behavior: 5-minute cache timeout with automatic refresh
2025-11-05 18:25:38 +00:00

100 lines
2.2 KiB
Go

package tlsutil
import (
"context"
"net"
"sync"
"time"
"github.com/rs/dnscache"
"github.com/rs/zerolog/log"
)
var (
// Global DNS resolver with caching
globalResolver *dnscache.Resolver
globalResolverOnce sync.Once
resolverMutex sync.RWMutex
resolverRefreshTTL time.Duration = 5 * time.Minute // Default TTL
)
// GetDNSResolver returns the global DNS resolver instance with caching
func GetDNSResolver() *dnscache.Resolver {
globalResolverOnce.Do(func() {
initDNSResolver(resolverRefreshTTL)
})
return globalResolver
}
// initDNSResolver initializes the DNS resolver with the specified TTL
func initDNSResolver(ttl time.Duration) {
log.Info().
Dur("ttl", ttl).
Msg("Initializing DNS resolver cache to reduce DNS query load")
globalResolver = &dnscache.Resolver{}
// Start a goroutine to periodically refresh the DNS cache
// This prevents stale DNS entries while still providing caching benefits
go func() {
ticker := time.NewTicker(ttl)
defer ticker.Stop()
for range ticker.C {
globalResolver.Refresh(true)
log.Debug().
Dur("ttl", ttl).
Msg("DNS cache refreshed")
}
}()
}
// SetDNSCacheTTL updates the DNS cache TTL
// This function should be called before any HTTP clients are created
func SetDNSCacheTTL(ttl time.Duration) {
resolverMutex.Lock()
defer resolverMutex.Unlock()
if ttl <= 0 {
ttl = 5 * time.Minute // Default
}
resolverRefreshTTL = ttl
log.Info().
Dur("ttl", ttl).
Msg("DNS cache TTL configured")
}
// DialContextWithCache is a DialContext function that uses the DNS cache
func DialContextWithCache(ctx context.Context, network, address string) (net.Conn, error) {
resolver := GetDNSResolver()
host, port, err := net.SplitHostPort(address)
if err != nil {
return nil, err
}
// Look up the IP address using the cached resolver
ips, err := resolver.LookupHost(ctx, host)
if err != nil {
return nil, err
}
// Use the first IP address
if len(ips) == 0 {
return nil, &net.DNSError{
Err: "no IP addresses found",
Name: host,
}
}
// Create a dialer with the resolved IP
dialer := &net.Dialer{
Timeout: 10 * time.Second,
KeepAlive: 30 * time.Second,
}
// Dial with the resolved IP address
return dialer.DialContext(ctx, network, net.JoinHostPort(ips[0], port))
}