From 77728106cd244788dd323673bf4931cb9a1d5600 Mon Sep 17 00:00:00 2001 From: George Tankersley Date: Wed, 20 May 2020 22:16:41 -0400 Subject: [PATCH] dnsseed: connect ServeDNS to the Zcash seeder --- dnsseed/dnsseed.go | 48 ++++++++++++++++++++++++++++++++++++++++--- dnsseed/setup.go | 36 ++++++++++++++++++++++++++------ dnsseed/setup_test.go | 4 ++-- zcash/address_book.go | 30 ++++++++++++++++++++++++--- zcash/client.go | 5 +++++ 5 files changed, 109 insertions(+), 14 deletions(-) diff --git a/dnsseed/dnsseed.go b/dnsseed/dnsseed.go index b1d6fbe..e11344f 100644 --- a/dnsseed/dnsseed.go +++ b/dnsseed/dnsseed.go @@ -4,14 +4,15 @@ import ( "context" "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/request" "github.com/miekg/dns" "github.com/zcashfoundation/dnsseeder/zcash" ) // ZcashSeeder discovers IP addresses by asking Zcash peers for them. type ZcashSeeder struct { - Next plugin.Handler - + Next plugin.Handler + Zones []string seeder *zcash.Seeder } @@ -26,5 +27,46 @@ func (zs ZcashSeeder) Ready() bool { } func (zs ZcashSeeder) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { - return plugin.NextOrFailure(zs.Name(), zs.Next, ctx, w, r) + // Check if it's a question for us + state := request.Request{W: w, Req: r} + zone := plugin.Zones(zs.Zones).Matches(state.Name()) + if zone == "" { + return plugin.NextOrFailure(zs.Name(), zs.Next, ctx, w, r) + } + + peerIPs := zs.seeder.Addresses(25) + + a := new(dns.Msg) + a.SetReply(r) + a.Authoritative = true + + a.Extra = make([]dns.RR, 0, len(peerIPs)) + + for i := 0; i < len(peerIPs); i++ { + var rr dns.RR + + ip := peerIPs[i] + + switch state.QType() { + case dns.TypeA: + rr = new(dns.A) + rr.(*dns.A).Hdr = dns.RR_Header{Name: state.QName(), Rrtype: dns.TypeA, Class: state.QClass()} + rr.(*dns.A).A = ip.To4() + case dns.TypeAAAA: + if ip.To4() != nil { + rr = new(dns.AAAA) + rr.(*dns.AAAA).Hdr = dns.RR_Header{Name: state.QName(), Rrtype: dns.TypeAAAA, Class: state.QClass()} + rr.(*dns.AAAA).AAAA = ip + } + default: + return dns.RcodeNotImplemented, nil + } + + // TODO: why don't we offer SRV records? Zcash has a configurable port. + + a.Extra = append(a.Extra, rr) + } + + w.WriteMsg(a) + return dns.RcodeSuccess, nil } diff --git a/dnsseed/setup.go b/dnsseed/setup.go index c239276..7d6dd7d 100644 --- a/dnsseed/setup.go +++ b/dnsseed/setup.go @@ -2,6 +2,7 @@ package dnsseed import ( "net" + "time" "github.com/caddyserver/caddy" "github.com/coredns/coredns/core/dnsserver" @@ -12,18 +13,21 @@ import ( "github.com/zcashfoundation/dnsseeder/zcash/network" ) -var log = clog.NewWithPlugin("dnsseed") +var ( + log = clog.NewWithPlugin("dnsseed") + updateInterval = 15 * time.Minute +) func init() { plugin.Register("dnsseed", setup) } // setup is the function that gets called when the config parser see the token "dnsseed". Setup is responsible // for parsing any extra options the example plugin may have. The first token this function sees is "dnsseed". func setup(c *caddy.Controller) error { - var networkArg, hostArg string + var zoneArg, networkArg, hostArg string c.Next() // Ignore "dnsseed" and give us the next token. - if !c.Args(&networkArg, &hostArg) { + if !c.Args(&zoneArg, &networkArg, &hostArg) { return plugin.Error("dnsseed", c.ArgErr()) } @@ -49,17 +53,37 @@ func setup(c *caddy.Controller) error { return plugin.Error("dnsseed", err) } + // Connect to the bootstrap peer err = seeder.Connect(address, port) if err != nil { return plugin.Error("dnsseed", err) } - // TODO: make initial addr request - // TODO: begin update timer + // Send the initial request for more addresses; spawns goroutines to process the responses. + // Ready() will flip to true once we've received and confirmed at least 10 peers. + seeder.RequestAddresses() + + go func() { + for { + select { + case <-time.After(updateInterval): + seeder.RequestAddresses() + err := seeder.WaitForAddresses(10, 30*time.Second) + if err != nil { + log.Errorf("Failed to refresh addresses: %v", err) + } + // XXX: If we wanted to crawl independently, this would be the place. + } + } + }() // Add the Plugin to CoreDNS, so Servers can use it in their plugin chain. dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler { - return ZcashSeeder{Next: next, seeder: seeder} + return ZcashSeeder{ + Next: next, + Zones: []string{zoneArg}, + seeder: seeder, + } }) // All OK, return a nil error. diff --git a/dnsseed/setup_test.go b/dnsseed/setup_test.go index 70e6ea6..0e28e27 100644 --- a/dnsseed/setup_test.go +++ b/dnsseed/setup_test.go @@ -9,12 +9,12 @@ import ( // TestSetup tests the various things that should be parsed by setup. func TestSetup(t *testing.T) { - c := caddy.NewTestController("dns", "dnsseed mainnet 127.0.0.1:8233") + c := caddy.NewTestController("dns", "dnsseed mainnet.seeder.yolo.money mainnet 127.0.0.1:8233") if err := setup(c); err != nil { if strings.Contains(err.Error(), "connection refused") { // No local peer running // TODO: mock a local peer, which will be easier with an API rework in the zcash package - t.Skip() + t.SkipNow() } t.Fatalf("Expected no errors, but got: %v", err) } diff --git a/zcash/address_book.go b/zcash/address_book.go index 3440398..7380a5b 100644 --- a/zcash/address_book.go +++ b/zcash/address_book.go @@ -1,6 +1,7 @@ package zcash import ( + mrand "math/rand" "net" "strconv" "sync" @@ -170,7 +171,30 @@ func (bk *AddressBook) waitForAddresses(n int, done chan struct{}) { return } -// GetShuffledAddressList returns a slice of n valid addresses in random order. -// func (bk *AddressBook) GetShuffledAddressList(n int) []*Address { +// GetAddressList returns a slice of n valid addresses in random order. +// If there aren't enough known addresses, it returns as many as we have. +func (bk *AddressBook) shuffleAddressList(n int) []net.IP { + bk.addrState.RLock() + defer bk.addrState.RUnlock() -// } + resp := make([]net.IP, 0, len(bk.addrs)) + + for _, v := range bk.addrs { + if v.blacklisted { + continue + } + + resp = append(resp, v.netaddr.IP) + } + + mrand.Seed(time.Now().UnixNano()) + mrand.Shuffle(len(resp), func(i, j int) { + resp[i], resp[j] = resp[j], resp[i] + }) + + if len(resp) > n { + return resp[:n] + } + + return resp +} diff --git a/zcash/client.go b/zcash/client.go index c957cf7..eb665ae 100644 --- a/zcash/client.go +++ b/zcash/client.go @@ -358,3 +358,8 @@ func (s *Seeder) WaitForAddresses(n int, timeout time.Duration) error { func (s *Seeder) Ready() bool { return s.WaitForAddresses(minimumReadyAddresses, 1*time.Millisecond) == nil } + +// Addresses returns a slice of n addresses or as many as we have if it's less than that. +func (s *Seeder) Addresses(n int) []net.IP { + return s.addrBook.shuffleAddressList(n) +}