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 }