master
go 141 lines 3.33 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package snmptopology
4
5 import (
6 "context"
7 "net"
8 "net/netip"
9 "sort"
10 "strings"
11 "sync"
12 "time"
13 )
14
15 const (
16 topologyReverseDNSTimeout = 50 * time.Millisecond
17 topologyReverseDNSCacheTTL = 10 * time.Minute
18 topologyReverseDNSNegTTL = 30 * time.Second
19 )
20
21 type topologyReverseDNSCacheEntry struct {
22 name string
23 expiresAt time.Time
24 }
25
26 type topologyReverseDNSResolver struct {
27 mu sync.RWMutex
28 timeout time.Duration
29 ttl time.Duration
30 cache map[string]topologyReverseDNSCacheEntry
31 }
32
33 func newTopologyReverseDNSResolver(timeout, ttl time.Duration) *topologyReverseDNSResolver {
34 return &topologyReverseDNSResolver{
35 timeout: timeout,
36 ttl: ttl,
37 cache: make(map[string]topologyReverseDNSCacheEntry),
38 }
39 }
40
41 // lookupCached returns the cached result for ip without performing any network I/O.
42 // Returns "" when the IP has never been resolved or its cache entry has expired.
43 func (r *topologyReverseDNSResolver) lookupCached(ip string) string {
44 if r == nil {
45 return ""
46 }
47 addr, err := netip.ParseAddr(strings.TrimSpace(ip))
48 if err != nil || !addr.IsValid() {
49 return ""
50 }
51 ip = addr.Unmap().String()
52
53 r.mu.RLock()
54 entry, ok := r.cache[ip]
55 r.mu.RUnlock()
56 if ok && time.Now().Before(entry.expiresAt) {
57 return entry.name
58 }
59 return ""
60 }
61
62 func (r *topologyReverseDNSResolver) lookup(ip string) string {
63 if r == nil {
64 return ""
65 }
66 addr, err := netip.ParseAddr(strings.TrimSpace(ip))
67 if err != nil || !addr.IsValid() {
68 return ""
69 }
70 ip = addr.Unmap().String()
71 now := time.Now()
72
73 r.mu.RLock()
74 entry, ok := r.cache[ip]
75 r.mu.RUnlock()
76 if ok && now.Before(entry.expiresAt) {
77 return entry.name
78 }
79
80 ctx, cancel := context.WithTimeout(context.Background(), r.timeout)
81 defer cancel()
82 names, err := net.DefaultResolver.LookupAddr(ctx, ip)
83 resolved := ""
84 if err == nil {
85 resolved = topologyNormalizeReverseDNSName(names)
86 }
87 ttl := r.ttl
88 if resolved == "" && topologyReverseDNSNegTTL > 0 {
89 ttl = topologyReverseDNSNegTTL
90 }
91
92 r.mu.Lock()
93 r.cache[ip] = topologyReverseDNSCacheEntry{
94 name: resolved,
95 expiresAt: now.Add(ttl),
96 }
97 r.mu.Unlock()
98
99 return resolved
100 }
101
102 func topologyNormalizeReverseDNSName(names []string) string {
103 if len(names) == 0 {
104 return ""
105 }
106 seen := make(map[string]struct{}, len(names))
107 out := make([]string, 0, len(names))
108 for _, name := range names {
109 name = strings.TrimSpace(name)
110 name = strings.TrimSuffix(name, ".")
111 name = strings.ToLower(name)
112 if name == "" {
113 continue
114 }
115 if _, ok := seen[name]; ok {
116 continue
117 }
118 seen[name] = struct{}{}
119 out = append(out, name)
120 }
121 if len(out) == 0 {
122 return ""
123 }
124 sort.Strings(out)
125 return out[0]
126 }
127
128 var defaultTopologyReverseDNSResolver = newTopologyReverseDNSResolver(topologyReverseDNSTimeout, topologyReverseDNSCacheTTL)
129
130 // resolveTopologyReverseDNSName performs a live DNS lookup (with cache).
131 // Used during the collector's Collect() cycle to warm the cache.
132 func resolveTopologyReverseDNSName(ip string) string {
133 return defaultTopologyReverseDNSResolver.lookup(ip)
134 }
135
136 // resolveTopologyReverseDNSNameCached returns a cached DNS name if available,
137 // or an empty string if the IP has not been resolved yet. Never blocks on network I/O.
138 // Used during function responses to avoid external calls.
139 func resolveTopologyReverseDNSNameCached(ip string) string {
140 return defaultTopologyReverseDNSResolver.lookupCached(ip)
141 }