dnsseed: connect ServeDNS to the Zcash seeder

This commit is contained in:
George Tankersley 2020-05-20 22:16:41 -04:00
parent f1e7d75e28
commit 77728106cd
5 changed files with 109 additions and 14 deletions

View File

@ -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
}

View File

@ -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.

View File

@ -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)
}

View File

@ -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
}

View File

@ -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)
}