github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/network/p2p/dns/resolver.go (about) 1 package dns 2 3 import ( 4 "context" 5 "net" 6 "time" 7 _ "unsafe" // for linking runtimeNano 8 9 madns "github.com/multiformats/go-multiaddr-dns" 10 "github.com/rs/zerolog" 11 12 "github.com/onflow/flow-go/module" 13 "github.com/onflow/flow-go/module/component" 14 "github.com/onflow/flow-go/module/irrecoverable" 15 "github.com/onflow/flow-go/module/mempool" 16 "github.com/onflow/flow-go/module/util" 17 ) 18 19 //go:linkname runtimeNano runtime.nanotime 20 func runtimeNano() int64 21 22 // Resolver is a cache-based dns resolver for libp2p. 23 // DNS cache implementation notes: 24 // 1. Generic / possibly expected functionality NOT implemented: 25 // - Caches domains for TTL seconds as given by upstream DNS resolver, e.g. [1]. 26 // - Possibly pre-expire cached domains so no connection time resolve delay. 27 // 2. Actual / pragmatic functionality implemented below: 28 // - Caches domains for global (not individual domain record TTL) TTL seconds. 29 // - Cached IP is returned even if cached entry expired; so no connection time resolve delay. 30 // - Detecting expired cached domain triggers async DNS lookup to refresh cached entry. 31 // 32 // [1] https://en.wikipedia.org/wiki/Name_server#Caching_name_server 33 type Resolver struct { 34 c *cache 35 res madns.BasicResolver // underlying resolver 36 collector module.ResolverMetrics 37 ipRequests chan *lookupIPRequest 38 txtRequests chan *lookupTXTRequest 39 logger zerolog.Logger 40 component.Component 41 cm *component.ComponentManager 42 } 43 44 type lookupIPRequest struct { 45 domain string 46 } 47 48 type lookupTXTRequest struct { 49 txt string 50 } 51 52 // optFunc is the option function for Resolver. 53 type optFunc func(resolver *Resolver) 54 55 // WithBasicResolver is an option function for setting the basic resolver of this Resolver. 56 func WithBasicResolver(basic madns.BasicResolver) optFunc { 57 return func(resolver *Resolver) { 58 resolver.res = basic 59 } 60 } 61 62 // WithTTL is an option function for setting the time to live for cache entries. 63 func WithTTL(ttl time.Duration) optFunc { 64 return func(resolver *Resolver) { 65 resolver.c.ttl = ttl 66 } 67 } 68 69 const ( 70 numIPAddrLookupWorkers = 16 71 numTxtLookupWorkers = 16 72 ipAddrLookupQueueSize = 64 73 txtLookupQueueSize = 64 74 ) 75 76 // NewResolver is the factory function for creating an instance of this resolver. 77 func NewResolver(logger zerolog.Logger, collector module.ResolverMetrics, dnsCache mempool.DNSCache, opts ...optFunc) *Resolver { 78 resolver := &Resolver{ 79 logger: logger.With().Str("component", "dns-resolver").Logger(), 80 res: madns.DefaultResolver, 81 c: newCache(logger, dnsCache), 82 collector: collector, 83 ipRequests: make(chan *lookupIPRequest, ipAddrLookupQueueSize), 84 txtRequests: make(chan *lookupTXTRequest, txtLookupQueueSize), 85 } 86 87 cm := component.NewComponentManagerBuilder() 88 89 for i := 0; i < numIPAddrLookupWorkers; i++ { 90 cm.AddWorker(resolver.processIPAddrLookups) 91 } 92 93 for i := 0; i < numTxtLookupWorkers; i++ { 94 cm.AddWorker(resolver.processTxtLookups) 95 } 96 97 resolver.cm = cm.Build() 98 resolver.Component = resolver.cm 99 100 for _, opt := range opts { 101 opt(resolver) 102 } 103 104 return resolver 105 } 106 107 func (r *Resolver) processIPAddrLookups(ctx irrecoverable.SignalerContext, ready component.ReadyFunc) { 108 ready() 109 110 r.logger.Trace().Msg("processing ip worker started") 111 112 for { 113 select { 114 case req := <-r.ipRequests: 115 lg := r.logger.With().Str("domain", req.domain).Logger() 116 lg.Trace().Msg("ip domain request picked for resolving") 117 _, err := r.lookupResolverForIPAddr(ctx, req.domain) 118 if err != nil { 119 // invalidates cached entry when hits error on resolving. 120 invalidated := r.c.invalidateIPCacheEntry(req.domain) 121 if invalidated { 122 r.collector.OnDNSCacheInvalidated() 123 } 124 lg.Error().Err(err).Msg("resolving ip address faced an error") 125 } 126 lg.Trace().Msg("ip domain resolved successfully") 127 case <-ctx.Done(): 128 r.logger.Trace().Msg("processing ip worker terminated") 129 return 130 } 131 } 132 } 133 134 func (r *Resolver) processTxtLookups(ctx irrecoverable.SignalerContext, ready component.ReadyFunc) { 135 ready() 136 r.logger.Trace().Msg("processing txt worker started") 137 138 for { 139 select { 140 case req := <-r.txtRequests: 141 lg := r.logger.With().Str("domain", req.txt).Logger() 142 lg.Trace().Msg("txt domain picked for resolving") 143 _, err := r.lookupResolverForTXTRecord(ctx, req.txt) 144 if err != nil { 145 // invalidates cached entry when hits error on resolving. 146 invalidated := r.c.invalidateTXTCacheEntry(req.txt) 147 if invalidated { 148 r.collector.OnDNSCacheInvalidated() 149 } 150 lg.Error().Err(err).Msg("resolving txt domain faced an error") 151 } 152 lg.Trace().Msg("txt domain resolved successfully") 153 case <-ctx.Done(): 154 r.logger.Trace().Msg("processing txt worker terminated") 155 return 156 } 157 } 158 } 159 160 // LookupIPAddr implements BasicResolver interface for libp2p for looking up ip addresses through resolver. 161 func (r *Resolver) LookupIPAddr(ctx context.Context, domain string) ([]net.IPAddr, error) { 162 started := runtimeNano() 163 164 addr, err := r.lookupIPAddr(ctx, domain) 165 166 r.collector.DNSLookupDuration( 167 time.Duration(runtimeNano() - started)) 168 return addr, err 169 } 170 171 // lookupIPAddr encapsulates the logic of resolving an ip address through cache. 172 // If domain exists on cache it is resolved through the cache. 173 // An expired domain on cache is still addressed through the cache, however, a request is fired up asynchronously 174 // through the underlying basic resolver to resolve it from the network. 175 func (r *Resolver) lookupIPAddr(ctx context.Context, domain string) ([]net.IPAddr, error) { 176 result := r.c.resolveIPCache(domain) 177 178 lg := r.logger.With(). 179 Str("domain", domain). 180 Bool("cache_hit", result.exists). 181 Bool("cache_fresh", result.fresh). 182 Bool("locked_for_resolving", result.locked).Logger() 183 184 lg.Trace().Msg("ip lookup request arrived") 185 186 if !result.exists { 187 r.collector.OnDNSCacheMiss() 188 return r.lookupResolverForIPAddr(ctx, domain) 189 } 190 191 if !result.fresh && result.locked { 192 lg.Trace().Msg("ip expired, but a resolving is in progress, returning expired one for now") 193 return result.addresses, nil 194 } 195 196 if !result.fresh && r.c.shouldResolveIP(domain) && !util.CheckClosed(r.cm.ShutdownSignal()) { 197 select { 198 case r.ipRequests <- &lookupIPRequest{domain}: 199 lg.Trace().Msg("ip lookup request queued for resolving") 200 default: 201 lg.Warn().Msg("ip lookup request queue is full, dropping request") 202 r.collector.OnDNSLookupRequestDropped() 203 } 204 } 205 206 r.collector.OnDNSCacheHit() 207 return result.addresses, nil 208 } 209 210 // lookupResolverForIPAddr queries the underlying resolver for the domain and updates the cache if query is successful. 211 func (r *Resolver) lookupResolverForIPAddr(ctx context.Context, domain string) ([]net.IPAddr, error) { 212 addr, err := r.res.LookupIPAddr(ctx, domain) 213 if err != nil { 214 return nil, err 215 } 216 217 r.c.updateIPCache(domain, addr) // updates cache 218 r.logger.Info().Str("ip_domain", domain).Msg("domain updated in cache") 219 return addr, nil 220 } 221 222 // LookupTXT implements BasicResolver interface for libp2p. 223 // If txt exists on cache it is resolved through the cache. 224 // An expired txt on cache is still addressed through the cache, however, a request is fired up asynchronously 225 // through the underlying basic resolver to resolve it from the network. 226 func (r *Resolver) LookupTXT(ctx context.Context, txt string) ([]string, error) { 227 228 started := runtimeNano() 229 230 addr, err := r.lookupTXT(ctx, txt) 231 232 r.collector.DNSLookupDuration( 233 time.Duration(runtimeNano() - started)) 234 return addr, err 235 } 236 237 // lookupIPAddr encapsulates the logic of resolving a txt through cache. 238 func (r *Resolver) lookupTXT(ctx context.Context, txt string) ([]string, error) { 239 result := r.c.resolveTXTCache(txt) 240 241 lg := r.logger.With(). 242 Str("txt", txt). 243 Bool("cache_hit", result.exists). 244 Bool("cache_fresh", result.fresh). 245 Bool("locked_for_resolving", result.locked).Logger() 246 247 lg.Trace().Msg("txt lookup request arrived") 248 249 if !result.exists { 250 r.collector.OnDNSCacheMiss() 251 return r.lookupResolverForTXTRecord(ctx, txt) 252 } 253 254 if !result.fresh && result.locked { 255 lg.Trace().Msg("txt expired, but a resolving is in progress, returning expired one for now") 256 return result.records, nil 257 } 258 259 if !result.fresh && r.c.shouldResolveTXT(txt) && !util.CheckClosed(r.cm.ShutdownSignal()) { 260 select { 261 case r.txtRequests <- &lookupTXTRequest{txt}: 262 lg.Trace().Msg("ip lookup request queued for resolving") 263 default: 264 lg.Warn().Msg("txt lookup request queue is full, dropping request") 265 r.collector.OnDNSLookupRequestDropped() 266 } 267 } 268 269 r.collector.OnDNSCacheHit() 270 return result.records, nil 271 } 272 273 // lookupResolverForTXTRecord queries the underlying resolver for the domain and updates the cache if query is successful. 274 func (r *Resolver) lookupResolverForTXTRecord(ctx context.Context, txt string) ([]string, error) { 275 addr, err := r.res.LookupTXT(ctx, txt) 276 if err != nil { 277 return nil, err 278 } 279 280 r.c.updateTXTCache(txt, addr) // updates cache 281 r.logger.Info().Str("txt_domain", txt).Msg("domain updated in cache") 282 return addr, nil 283 }