github.com/aigarnetwork/aigar@v0.0.0-20191115204914-d59a6eb70f8e/p2p/dnsdisc/client.go (about)

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