dcrd/connmgr/seed.go
Dave Collins 3c7913dbb6
connmgr: Enforce max http seeder response size.
This imposes a limit on the response size from the http seeder such that
each returned entry is allowed to have max size of 256 bytes.  Since
there is also a limit of 16 entries per response, it amounts to a max
overall response size of 4KiB.
2020-08-19 17:58:53 -05:00

205 lines
6.3 KiB
Go

// Copyright (c) 2016 The btcsuite developers
// Copyright (c) 2019-2020 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package connmgr
import (
"context"
"encoding/json"
"fmt"
"io"
mrand "math/rand"
"net"
"net/http"
"strconv"
"time"
"github.com/decred/dcrd/wire"
)
const (
// These constants are used by the seed code to pick a random last
// seen time.
secondsIn3Days int32 = 24 * 60 * 60 * 3
secondsIn4Days int32 = 24 * 60 * 60 * 4
)
// DialFunc is the signature of the Dialer function.
type DialFunc func(context.Context, string, string) (net.Conn, error)
// HttpsSeederFilters houses filter parameters for use when making a request to
// an HTTPS seeder. It can be configured via the various exported functions
// that start with the prefix SeedFilter.
type HttpsSeederFilters struct {
ipVersion uint16
hasIPVersion bool
pver uint32
hasPver bool
services wire.ServiceFlag
hasServices bool
}
// SeedFilterIPVersion configures a request to an HTTPS seeder to filter all
// results that are not the provided ip version, which is expected to be either
// 4 or 6, to indicate IPv4 or IPv6 addresses, respectively. The HTTPS seeder
// may choose to ignore other IP versions.
func SeedFilterIPVersion(ipVersion uint16) func(f *HttpsSeederFilters) {
return func(f *HttpsSeederFilters) {
f.ipVersion = ipVersion
f.hasIPVersion = true
}
}
// SeedFilterProtocolVersion configures a request to an HTTPS seeder to filter
// all results that are not the provided peer-to-peer protocol version. This
// can be useful to discover peers that support specific peer versions.
func SeedFilterProtocolVersion(pver uint32) func(f *HttpsSeederFilters) {
return func(f *HttpsSeederFilters) {
f.pver = pver
f.hasPver = true
}
}
// SeedFilterServices configures a request to an HTTPS seeder to filter all
// results that do not support the provided service flags. This can be useful
// to discover peers that support specific services such as fully-validating
// nodes.
func SeedFilterServices(services wire.ServiceFlag) func(f *HttpsSeederFilters) {
return func(f *HttpsSeederFilters) {
f.services = services
f.hasServices = true
}
}
// node defines a single JSON object returned by the https seeders.
type node struct {
Host string `json:"host"`
Services uint64 `json:"services"`
ProtocolVersion uint32 `json:"pver"`
}
// SeedAddrs uses HTTPS seeding to return a list of addresses of p2p peers on
// the network.
//
// The seeder parameter specifies the domain name of the seed. The dial
// function specifies the dialer to use to contact the HTTPS seeder and allows
// the caller to use whatever configuration it deems fit such as using a proxy,
// like Tor.
//
// The variadic filters parameter allows additional query filters to be applied.
// The available filters can be set via the exported functions that start with
// the prefix SeedFilter. See the documentation for each function for more
// details.
func SeedAddrs(ctx context.Context, seeder string, dialFn DialFunc, filters ...func(f *HttpsSeederFilters)) ([]*wire.NetAddress, error) {
// Set any caller provided filters.
var seederFilters HttpsSeederFilters
for _, f := range filters {
f(&seederFilters)
}
// Setup the HTTPS request.
url := "https://" + seeder
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
}
req.URL.Path = "/api/addrs"
// Configure the query parameters based on the caller-provided filters.
queryParams := req.URL.Query()
if seederFilters.hasIPVersion {
ipVersionStr := strconv.FormatInt(int64(seederFilters.ipVersion), 10)
queryParams.Add("ipversion", ipVersionStr)
}
if seederFilters.hasPver {
pverStr := strconv.FormatInt(int64(seederFilters.pver), 10)
queryParams.Add("pver", pverStr)
}
if seederFilters.hasServices {
servicesStr := strconv.FormatUint(uint64(seederFilters.services), 10)
queryParams.Add("services", servicesStr)
}
req.URL.RawQuery = queryParams.Encode()
// Make the request.
client := &http.Client{
Transport: &http.Transport{
DialContext: dialFn,
},
}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("seeder %s returned invalid status code '%d': %v",
seeder, resp.StatusCode, http.StatusText(resp.StatusCode))
}
// Parse the JSON response.
const maxNodes = 16
const maxRespSize = maxNodes * 256
var nodes []node
dec := json.NewDecoder(io.LimitReader(resp.Body, maxRespSize))
for ctx.Err() == nil && dec.More() {
var node node
if err = dec.Decode(&node); err != nil {
return nil, fmt.Errorf("unable to parse response: %w", err)
}
nodes = append(nodes, node)
if len(nodes) >= maxNodes {
break
}
}
// Nothing more to do when no addresses are returned.
if len(nodes) == 0 {
log.Infof("0 addresses found from seeder %s", seeder)
return nil, nil
}
// Convert the response to net addresses.
randSource := mrand.New(mrand.NewSource(time.Now().UnixNano()))
addrs := make([]*wire.NetAddress, 0, len(nodes))
for _, node := range nodes {
host, portStr, err := net.SplitHostPort(node.Host)
if err != nil {
log.Warnf("seeder returned invalid host %q", node.Host)
continue
}
port, err := strconv.ParseUint(portStr, 10, 16)
if err != nil {
log.Warnf("seeder returned invalid port %q", node.Host)
continue
}
ip := net.ParseIP(host)
if ip == nil {
log.Warnf("seeder returned a hostname that is not an IP address %q",
host)
continue
}
// Set the timestamp to a value randomly selected between 3 and 7 days
// ago in order to improve the ranking of peers discovered from seeders
// since they are a more authoritative source than other random peers.
offsetSecs := secondsIn3Days + randSource.Int31n(secondsIn4Days)
ts := time.Now().Add(-1 * time.Second * time.Duration(offsetSecs))
na := wire.NewNetAddressTimestamp(ts, wire.ServiceFlag(node.Services),
ip, uint16(port))
addrs = append(addrs, na)
}
if len(addrs) < len(nodes) {
log.Infof("%d addresses found from seeder %s (excluded %d invalid)",
len(addrs), seeder, len(nodes)-len(addrs))
} else {
log.Infof("%d addresses found from seeder %s", len(addrs), seeder)
}
return addrs, nil
}