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 }