github.com/julesgoullee/go-ethereum@v1.9.7/p2p/dnsdisc/client.go (about)

     1  // Copyright 2018 The go-ethereum Authors
     2  // This file is part of the go-ethereum library.
     3  //
     4  // The go-ethereum library is free software: you can redistribute it and/or modify
     5  // it under the terms of the GNU Lesser General Public License as published by
     6  // the Free Software Foundation, either version 3 of the License, or
     7  // (at your option) any later version.
     8  //
     9  // The go-ethereum library is distributed in the hope that it will be useful,
    10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    12  // GNU Lesser General Public License for more details.
    13  //
    14  // You should have received a copy of the GNU Lesser General Public License
    15  // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
    16  
    17  package dnsdisc
    18  
    19  import (
    20  	"bytes"
    21  	"context"
    22  	"fmt"
    23  	"math/rand"
    24  	"net"
    25  	"strings"
    26  	"time"
    27  
    28  	"github.com/ethereum/go-ethereum/common/mclock"
    29  	"github.com/ethereum/go-ethereum/crypto"
    30  	"github.com/ethereum/go-ethereum/log"
    31  	"github.com/ethereum/go-ethereum/p2p/enode"
    32  	"github.com/ethereum/go-ethereum/p2p/enr"
    33  	lru "github.com/hashicorp/golang-lru"
    34  )
    35  
    36  // Client discovers nodes by querying DNS servers.
    37  type Client struct {
    38  	cfg       Config
    39  	clock     mclock.Clock
    40  	linkCache linkCache
    41  	trees     map[string]*clientTree
    42  
    43  	entries *lru.Cache
    44  }
    45  
    46  // Config holds configuration options for the client.
    47  type Config struct {
    48  	Timeout         time.Duration      // timeout used for DNS lookups (default 5s)
    49  	RecheckInterval time.Duration      // time between tree root update checks (default 30min)
    50  	CacheLimit      int                // maximum number of cached records (default 1000)
    51  	ValidSchemes    enr.IdentityScheme // acceptable ENR identity schemes (default enode.ValidSchemes)
    52  	Resolver        Resolver           // the DNS resolver to use (defaults to system DNS)
    53  	Logger          log.Logger         // destination of client log messages (defaults to root logger)
    54  }
    55  
    56  // Resolver is a DNS resolver that can query TXT records.
    57  type Resolver interface {
    58  	LookupTXT(ctx context.Context, domain string) ([]string, error)
    59  }
    60  
    61  func (cfg Config) withDefaults() Config {
    62  	const (
    63  		defaultTimeout = 5 * time.Second
    64  		defaultRecheck = 30 * time.Minute
    65  		defaultCache   = 1000
    66  	)
    67  	if cfg.Timeout == 0 {
    68  		cfg.Timeout = defaultTimeout
    69  	}
    70  	if cfg.RecheckInterval == 0 {
    71  		cfg.RecheckInterval = defaultRecheck
    72  	}
    73  	if cfg.CacheLimit == 0 {
    74  		cfg.CacheLimit = defaultCache
    75  	}
    76  	if cfg.ValidSchemes == nil {
    77  		cfg.ValidSchemes = enode.ValidSchemes
    78  	}
    79  	if cfg.Resolver == nil {
    80  		cfg.Resolver = new(net.Resolver)
    81  	}
    82  	if cfg.Logger == nil {
    83  		cfg.Logger = log.Root()
    84  	}
    85  	return cfg
    86  }
    87  
    88  // NewClient creates a client.
    89  func NewClient(cfg Config, urls ...string) (*Client, error) {
    90  	c := &Client{
    91  		cfg:   cfg.withDefaults(),
    92  		clock: mclock.System{},
    93  		trees: make(map[string]*clientTree),
    94  	}
    95  	var err error
    96  	if c.entries, err = lru.New(c.cfg.CacheLimit); err != nil {
    97  		return nil, err
    98  	}
    99  	for _, url := range urls {
   100  		if err := c.AddTree(url); err != nil {
   101  			return nil, err
   102  		}
   103  	}
   104  	return c, nil
   105  }
   106  
   107  // SyncTree downloads the entire node tree at the given URL. This doesn't add the tree for
   108  // later use, but any previously-synced entries are reused.
   109  func (c *Client) SyncTree(url string) (*Tree, error) {
   110  	le, err := parseLink(url)
   111  	if err != nil {
   112  		return nil, fmt.Errorf("invalid enrtree URL: %v", err)
   113  	}
   114  	ct := newClientTree(c, le)
   115  	t := &Tree{entries: make(map[string]entry)}
   116  	if err := ct.syncAll(t.entries); err != nil {
   117  		return nil, err
   118  	}
   119  	t.root = ct.root
   120  	return t, nil
   121  }
   122  
   123  // AddTree adds a enrtree:// URL to crawl.
   124  func (c *Client) AddTree(url string) error {
   125  	le, err := parseLink(url)
   126  	if err != nil {
   127  		return fmt.Errorf("invalid enrtree URL: %v", err)
   128  	}
   129  	ct, err := c.ensureTree(le)
   130  	if err != nil {
   131  		return err
   132  	}
   133  	c.linkCache.add(ct)
   134  	return nil
   135  }
   136  
   137  func (c *Client) ensureTree(le *linkEntry) (*clientTree, error) {
   138  	if tree, ok := c.trees[le.domain]; ok {
   139  		if !tree.matchPubkey(le.pubkey) {
   140  			return nil, fmt.Errorf("conflicting public keys for domain %q", le.domain)
   141  		}
   142  		return tree, nil
   143  	}
   144  	ct := newClientTree(c, le)
   145  	c.trees[le.domain] = ct
   146  	return ct, nil
   147  }
   148  
   149  // RandomNode retrieves the next random node.
   150  func (c *Client) RandomNode(ctx context.Context) *enode.Node {
   151  	for {
   152  		ct := c.randomTree()
   153  		if ct == nil {
   154  			return nil
   155  		}
   156  		n, err := ct.syncRandom(ctx)
   157  		if err != nil {
   158  			if err == ctx.Err() {
   159  				return nil // context canceled.
   160  			}
   161  			c.cfg.Logger.Debug("Error in DNS random node sync", "tree", ct.loc.domain, "err", err)
   162  			continue
   163  		}
   164  		if n != nil {
   165  			return n
   166  		}
   167  	}
   168  }
   169  
   170  // randomTree returns a random tree.
   171  func (c *Client) randomTree() *clientTree {
   172  	if !c.linkCache.valid() {
   173  		c.gcTrees()
   174  	}
   175  	limit := rand.Intn(len(c.trees))
   176  	for _, ct := range c.trees {
   177  		if limit == 0 {
   178  			return ct
   179  		}
   180  		limit--
   181  	}
   182  	return nil
   183  }
   184  
   185  // gcTrees rebuilds the 'trees' map.
   186  func (c *Client) gcTrees() {
   187  	trees := make(map[string]*clientTree)
   188  	for t := range c.linkCache.all() {
   189  		trees[t.loc.domain] = t
   190  	}
   191  	c.trees = trees
   192  }
   193  
   194  // resolveRoot retrieves a root entry via DNS.
   195  func (c *Client) resolveRoot(ctx context.Context, loc *linkEntry) (rootEntry, error) {
   196  	txts, err := c.cfg.Resolver.LookupTXT(ctx, loc.domain)
   197  	c.cfg.Logger.Trace("Updating DNS discovery root", "tree", loc.domain, "err", err)
   198  	if err != nil {
   199  		return rootEntry{}, err
   200  	}
   201  	for _, txt := range txts {
   202  		if strings.HasPrefix(txt, rootPrefix) {
   203  			return parseAndVerifyRoot(txt, loc)
   204  		}
   205  	}
   206  	return rootEntry{}, nameError{loc.domain, errNoRoot}
   207  }
   208  
   209  func parseAndVerifyRoot(txt string, loc *linkEntry) (rootEntry, error) {
   210  	e, err := parseRoot(txt)
   211  	if err != nil {
   212  		return e, err
   213  	}
   214  	if !e.verifySignature(loc.pubkey) {
   215  		return e, entryError{typ: "root", err: errInvalidSig}
   216  	}
   217  	return e, nil
   218  }
   219  
   220  // resolveEntry retrieves an entry from the cache or fetches it from the network
   221  // if it isn't cached.
   222  func (c *Client) resolveEntry(ctx context.Context, domain, hash string) (entry, error) {
   223  	cacheKey := truncateHash(hash)
   224  	if e, ok := c.entries.Get(cacheKey); ok {
   225  		return e.(entry), nil
   226  	}
   227  	e, err := c.doResolveEntry(ctx, domain, hash)
   228  	if err != nil {
   229  		return nil, err
   230  	}
   231  	c.entries.Add(cacheKey, e)
   232  	return e, nil
   233  }
   234  
   235  // doResolveEntry fetches an entry via DNS.
   236  func (c *Client) doResolveEntry(ctx context.Context, domain, hash string) (entry, error) {
   237  	wantHash, err := b32format.DecodeString(hash)
   238  	if err != nil {
   239  		return nil, fmt.Errorf("invalid base32 hash")
   240  	}
   241  	name := hash + "." + domain
   242  	txts, err := c.cfg.Resolver.LookupTXT(ctx, hash+"."+domain)
   243  	c.cfg.Logger.Trace("DNS discovery lookup", "name", name, "err", err)
   244  	if err != nil {
   245  		return nil, err
   246  	}
   247  	for _, txt := range txts {
   248  		e, err := parseEntry(txt, c.cfg.ValidSchemes)
   249  		if err == errUnknownEntry {
   250  			continue
   251  		}
   252  		if !bytes.HasPrefix(crypto.Keccak256([]byte(txt)), wantHash) {
   253  			err = nameError{name, errHashMismatch}
   254  		} else if err != nil {
   255  			err = nameError{name, err}
   256  		}
   257  		return e, err
   258  	}
   259  	return nil, nameError{name, errNoEntry}
   260  }