github.com/koko1123/flow-go-1@v0.29.6/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/koko1123/flow-go-1/module"
    13  	"github.com/koko1123/flow-go-1/module/component"
    14  	"github.com/koko1123/flow-go-1/module/irrecoverable"
    15  	"github.com/koko1123/flow-go-1/module/mempool"
    16  	"github.com/koko1123/flow-go-1/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  }