lnd/discovery/bootstrapper.go

538 lines
17 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package discovery
import (
"bytes"
"crypto/rand"
"crypto/sha256"
"errors"
"fmt"
prand "math/rand"
"net"
"strconv"
"strings"
"time"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil/bech32"
"github.com/lightningnetwork/lnd/autopilot"
"github.com/lightningnetwork/lnd/lnutils"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/tor"
"github.com/miekg/dns"
)
func init() {
prand.Seed(time.Now().Unix())
}
// NetworkPeerBootstrapper is an interface that represents an initial peer
// bootstrap mechanism. This interface is to be used to bootstrap a new peer to
// the connection by providing it with the pubkey+address of a set of existing
// peers on the network. Several bootstrap mechanisms can be implemented such
// as DNS, in channel graph, DHT's, etc.
type NetworkPeerBootstrapper interface {
// SampleNodeAddrs uniformly samples a set of specified address from
// the network peer bootstrapper source. The num addrs field passed in
// denotes how many valid peer addresses to return. The passed set of
// node nodes allows the caller to ignore a set of nodes perhaps
// because they already have connections established.
SampleNodeAddrs(numAddrs uint32,
ignore map[autopilot.NodeID]struct{}) ([]*lnwire.NetAddress, error)
// Name returns a human readable string which names the concrete
// implementation of the NetworkPeerBootstrapper.
Name() string
}
// MultiSourceBootstrap attempts to utilize a set of NetworkPeerBootstrapper
// passed in to return the target (numAddrs) number of peer addresses that can
// be used to bootstrap a peer just joining the Lightning Network. Each
// bootstrapper will be queried successively until the target amount is met. If
// the ignore map is populated, then the bootstrappers will be instructed to
// skip those nodes.
func MultiSourceBootstrap(ignore map[autopilot.NodeID]struct{}, numAddrs uint32,
bootstrappers ...NetworkPeerBootstrapper) ([]*lnwire.NetAddress, error) {
// We'll randomly shuffle our bootstrappers before querying them in
// order to avoid from querying the same bootstrapper method over and
// over, as some of these might tend to provide better/worse results
// than others.
bootstrappers = shuffleBootstrappers(bootstrappers)
var addrs []*lnwire.NetAddress
for _, bootstrapper := range bootstrappers {
// If we already have enough addresses, then we can exit early
// w/o querying the additional bootstrappers.
if uint32(len(addrs)) >= numAddrs {
break
}
log.Infof("Attempting to bootstrap with: %v", bootstrapper.Name())
// If we still need additional addresses, then we'll compute
// the number of address remaining that we need to fetch.
numAddrsLeft := numAddrs - uint32(len(addrs))
log.Tracef("Querying for %v addresses", numAddrsLeft)
netAddrs, err := bootstrapper.SampleNodeAddrs(numAddrsLeft, ignore)
if err != nil {
// If we encounter an error with a bootstrapper, then
// we'll continue on to the next available
// bootstrapper.
log.Errorf("Unable to query bootstrapper %v: %v",
bootstrapper.Name(), err)
continue
}
addrs = append(addrs, netAddrs...)
}
if len(addrs) == 0 {
return nil, errors.New("no addresses found")
}
log.Infof("Obtained %v addrs to bootstrap network with", len(addrs))
return addrs, nil
}
// shuffleBootstrappers shuffles the set of bootstrappers in order to avoid
// querying the same bootstrapper over and over. To shuffle the set of
// candidates, we use a version of the FisherYates shuffle algorithm.
func shuffleBootstrappers(candidates []NetworkPeerBootstrapper) []NetworkPeerBootstrapper {
shuffled := make([]NetworkPeerBootstrapper, len(candidates))
perm := prand.Perm(len(candidates))
for i, v := range perm {
shuffled[v] = candidates[i]
}
return shuffled
}
// ChannelGraphBootstrapper is an implementation of the NetworkPeerBootstrapper
// which attempts to retrieve advertised peers directly from the active channel
// graph. This instance requires a backing autopilot.ChannelGraph instance in
// order to operate properly.
type ChannelGraphBootstrapper struct {
chanGraph autopilot.ChannelGraph
// hashAccumulator is a set of 32 random bytes that are read upon the
// creation of the channel graph bootstrapper. We use this value to
// randomly select nodes within the known graph to connect to. After
// each selection, we rotate the accumulator by hashing it with itself.
hashAccumulator [32]byte
tried map[autopilot.NodeID]struct{}
}
// A compile time assertion to ensure that ChannelGraphBootstrapper meets the
// NetworkPeerBootstrapper interface.
var _ NetworkPeerBootstrapper = (*ChannelGraphBootstrapper)(nil)
// NewGraphBootstrapper returns a new instance of a ChannelGraphBootstrapper
// backed by an active autopilot.ChannelGraph instance. This type of network
// peer bootstrapper will use the authenticated nodes within the known channel
// graph to bootstrap connections.
func NewGraphBootstrapper(cg autopilot.ChannelGraph) (NetworkPeerBootstrapper, error) {
c := &ChannelGraphBootstrapper{
chanGraph: cg,
tried: make(map[autopilot.NodeID]struct{}),
}
if _, err := rand.Read(c.hashAccumulator[:]); err != nil {
return nil, err
}
return c, nil
}
// SampleNodeAddrs uniformly samples a set of specified address from the
// network peer bootstrapper source. The num addrs field passed in denotes how
// many valid peer addresses to return.
//
// NOTE: Part of the NetworkPeerBootstrapper interface.
func (c *ChannelGraphBootstrapper) SampleNodeAddrs(numAddrs uint32,
ignore map[autopilot.NodeID]struct{}) ([]*lnwire.NetAddress, error) {
// We'll merge the ignore map with our currently selected map in order
// to ensure we don't return any duplicate nodes.
for n := range ignore {
log.Tracef("Ignored node %x for bootstrapping", n)
c.tried[n] = struct{}{}
}
// In order to bootstrap, we'll iterate all the nodes in the channel
// graph, accumulating nodes until either we go through all active
// nodes, or we reach our limit. We ensure that we meet the randomly
// sample constraint as we maintain an xor accumulator to ensure we
// randomly sample nodes independent of the iteration of the channel
// graph.
sampleAddrs := func() ([]*lnwire.NetAddress, error) {
var (
a []*lnwire.NetAddress
// We'll create a special error so we can return early
// and abort the transaction once we find a match.
errFound = fmt.Errorf("found node")
)
err := c.chanGraph.ForEachNode(func(node autopilot.Node) error {
nID := autopilot.NodeID(node.PubKey())
if _, ok := c.tried[nID]; ok {
return nil
}
// We'll select the first node we come across who's
// public key is less than our current accumulator
// value. When comparing, we skip the first byte as
// it's 50/50. If it isn't less, than then we'll
// continue forward.
nodePubKeyBytes := node.PubKey()
if bytes.Compare(c.hashAccumulator[:], nodePubKeyBytes[1:]) > 0 {
return nil
}
for _, nodeAddr := range node.Addrs() {
// If we haven't yet reached our limit, then
// we'll copy over the details of this node
// into the set of addresses to be returned.
switch nodeAddr.(type) {
case *net.TCPAddr, *tor.OnionAddr:
default:
// If this isn't a valid address
// supported by the protocol, then we'll
// skip this node.
return nil
}
nodePub, err := btcec.ParsePubKey(
nodePubKeyBytes[:],
)
if err != nil {
return err
}
// At this point, we've found an eligible node,
// so we'll return early with our shibboleth
// error.
a = append(a, &lnwire.NetAddress{
IdentityKey: nodePub,
Address: nodeAddr,
})
}
c.tried[nID] = struct{}{}
return errFound
})
if err != nil && err != errFound {
return nil, err
}
return a, nil
}
// We'll loop and sample new addresses from the graph source until
// we've reached our target number of outbound connections or we hit 50
// attempts, which ever comes first.
var (
addrs []*lnwire.NetAddress
tries uint32
)
for tries < 30 && uint32(len(addrs)) < numAddrs {
sampleAddrs, err := sampleAddrs()
if err != nil {
return nil, err
}
tries++
// We'll now rotate our hash accumulator one value forwards.
c.hashAccumulator = sha256.Sum256(c.hashAccumulator[:])
// If this attempt didn't yield any addresses, then we'll exit
// early.
if len(sampleAddrs) == 0 {
continue
}
addrs = append(addrs, sampleAddrs...)
}
log.Tracef("Ending hash accumulator state: %x", c.hashAccumulator)
return addrs, nil
}
// Name returns a human readable string which names the concrete implementation
// of the NetworkPeerBootstrapper.
//
// NOTE: Part of the NetworkPeerBootstrapper interface.
func (c *ChannelGraphBootstrapper) Name() string {
return "Authenticated Channel Graph"
}
// DNSSeedBootstrapper as an implementation of the NetworkPeerBootstrapper
// interface which implements peer bootstrapping via a special DNS seed as
// defined in BOLT-0010. For further details concerning Lightning's current DNS
// boot strapping protocol, see this link:
// - https://github.com/lightningnetwork/lightning-rfc/blob/master/10-dns-bootstrap.md
type DNSSeedBootstrapper struct {
// dnsSeeds is an array of two tuples we'll use for bootstrapping. The
// first item in the tuple is the primary host we'll use to attempt the
// SRV lookup we require. If we're unable to receive a response over
// UDP, then we'll fall back to manual TCP resolution. The second item
// in the tuple is a special A record that we'll query in order to
// receive the IP address of the current authoritative DNS server for
// the network seed.
dnsSeeds [][2]string
net tor.Net
// timeout is the maximum amount of time a dial will wait for a connect to
// complete.
timeout time.Duration
}
// A compile time assertion to ensure that DNSSeedBootstrapper meets the
// NetworkPeerjBootstrapper interface.
var _ NetworkPeerBootstrapper = (*ChannelGraphBootstrapper)(nil)
// NewDNSSeedBootstrapper returns a new instance of the DNSSeedBootstrapper.
// The set of passed seeds should point to DNS servers that properly implement
// Lightning's DNS peer bootstrapping protocol as defined in BOLT-0010. The set
// of passed DNS seeds should come in pairs, with the second host name to be
// used as a fallback for manual TCP resolution in the case of an error
// receiving the UDP response. The second host should return a single A record
// with the IP address of the authoritative name server.
func NewDNSSeedBootstrapper(
seeds [][2]string, net tor.Net,
timeout time.Duration) NetworkPeerBootstrapper {
return &DNSSeedBootstrapper{dnsSeeds: seeds, net: net, timeout: timeout}
}
// fallBackSRVLookup attempts to manually query for SRV records we need to
// properly bootstrap. We do this by querying the special record at the "soa."
// sub-domain of supporting DNS servers. The returned IP address will be the IP
// address of the authoritative DNS server. Once we have this IP address, we'll
// connect manually over TCP to request the SRV record. This is necessary as
// the records we return are currently too large for a class of resolvers,
// causing them to be filtered out. The targetEndPoint is the original end
// point that was meant to be hit.
func (d *DNSSeedBootstrapper) fallBackSRVLookup(soaShim string,
targetEndPoint string) ([]*net.SRV, error) {
log.Tracef("Attempting to query fallback DNS seed")
// First, we'll lookup the IP address of the server that will act as
// our shim.
addrs, err := d.net.LookupHost(soaShim)
if err != nil {
return nil, err
}
// Once we have the IP address, we'll establish a TCP connection using
// port 53.
dnsServer := net.JoinHostPort(addrs[0], "53")
conn, err := d.net.Dial("tcp", dnsServer, d.timeout)
if err != nil {
return nil, err
}
dnsHost := fmt.Sprintf("_nodes._tcp.%v.", targetEndPoint)
dnsConn := &dns.Conn{Conn: conn}
defer dnsConn.Close()
// With the connection established, we'll craft our SRV query, write
// toe request, then wait for the server to give our response.
msg := new(dns.Msg)
msg.SetQuestion(dnsHost, dns.TypeSRV)
if err := dnsConn.WriteMsg(msg); err != nil {
return nil, err
}
resp, err := dnsConn.ReadMsg()
if err != nil {
return nil, err
}
// If the message response code was not the success code, fail.
if resp.Rcode != dns.RcodeSuccess {
return nil, fmt.Errorf("unsuccessful SRV request, "+
"received: %v", resp.Rcode)
}
// Retrieve the RR(s) of the Answer section, and convert to the format
// that net.LookupSRV would normally return.
var rrs []*net.SRV
for _, rr := range resp.Answer {
srv := rr.(*dns.SRV)
rrs = append(rrs, &net.SRV{
Target: srv.Target,
Port: srv.Port,
Priority: srv.Priority,
Weight: srv.Weight,
})
}
return rrs, nil
}
// SampleNodeAddrs uniformly samples a set of specified address from the
// network peer bootstrapper source. The num addrs field passed in denotes how
// many valid peer addresses to return. The set of DNS seeds are used
// successively to retrieve eligible target nodes.
func (d *DNSSeedBootstrapper) SampleNodeAddrs(numAddrs uint32,
ignore map[autopilot.NodeID]struct{}) ([]*lnwire.NetAddress, error) {
var netAddrs []*lnwire.NetAddress
// We'll try all the registered DNS seeds, exiting early if one of them
// gives us all the peers we need.
//
// TODO(roasbeef): should combine results from both
search:
for _, dnsSeedTuple := range d.dnsSeeds {
// We'll first query the seed with an SRV record so we can
// obtain a random sample of the encoded public keys of nodes.
// We use the lndLookupSRV function for this task.
primarySeed := dnsSeedTuple[0]
_, addrs, err := d.net.LookupSRV(
"nodes", "tcp", primarySeed, d.timeout,
)
if err != nil {
log.Tracef("Unable to lookup SRV records via "+
"primary seed (%v): %v", primarySeed, err)
log.Trace("Falling back to secondary")
// If the host of the secondary seed is blank, then
// we'll bail here as we can't proceed.
if dnsSeedTuple[1] == "" {
log.Tracef("DNS seed %v has no secondary, "+
"skipping fallback", primarySeed)
continue
}
// If we get an error when trying to query via the
// primary seed, we'll fallback to the secondary seed
// before concluding failure.
soaShim := dnsSeedTuple[1]
addrs, err = d.fallBackSRVLookup(
soaShim, primarySeed,
)
if err != nil {
log.Tracef("Unable to query fall "+
"back dns seed (%v): %v", soaShim, err)
continue
}
log.Tracef("Successfully queried fallback DNS seed")
}
log.Tracef("Retrieved SRV records from dns seed: %v",
lnutils.SpewLogClosure(addrs))
// Next, we'll need to issue an A record request for each of
// the nodes, skipping it if nothing comes back.
for _, nodeSrv := range addrs {
if uint32(len(netAddrs)) >= numAddrs {
break search
}
// With the SRV target obtained, we'll now perform
// another query to obtain the IP address for the
// matching bech32 encoded node key. We use the
// lndLookup function for this task.
bechNodeHost := nodeSrv.Target
addrs, err := d.net.LookupHost(bechNodeHost)
if err != nil {
return nil, err
}
if len(addrs) == 0 {
log.Tracef("No addresses for %v, skipping",
bechNodeHost)
continue
}
log.Tracef("Attempting to convert: %v", bechNodeHost)
// If the host isn't correctly formatted, then we'll
// skip it.
if len(bechNodeHost) == 0 ||
!strings.Contains(bechNodeHost, ".") {
continue
}
// If we have a set of valid addresses, then we'll need
// to parse the public key from the original bech32
// encoded string.
bechNode := strings.Split(bechNodeHost, ".")
_, nodeBytes5Bits, err := bech32.Decode(bechNode[0])
if err != nil {
return nil, err
}
// Once we have the bech32 decoded pubkey, we'll need
// to convert the 5-bit word grouping into our regular
// 8-bit word grouping so we can convert it into a
// public key.
nodeBytes, err := bech32.ConvertBits(
nodeBytes5Bits, 5, 8, false,
)
if err != nil {
return nil, err
}
nodeKey, err := btcec.ParsePubKey(nodeBytes)
if err != nil {
return nil, err
}
// If we have an ignore list, and this node is in the
// ignore list, then we'll go to the next candidate.
if ignore != nil {
nID := autopilot.NewNodeID(nodeKey)
if _, ok := ignore[nID]; ok {
continue
}
}
// Finally we'll convert the host:port peer to a proper
// TCP address to use within the lnwire.NetAddress. We
// don't need to use the lndResolveTCP function here
// because we already have the host:port peer.
addr := net.JoinHostPort(
addrs[0],
strconv.FormatUint(uint64(nodeSrv.Port), 10),
)
tcpAddr, err := net.ResolveTCPAddr("tcp", addr)
if err != nil {
return nil, err
}
// Finally, with all the information parsed, we'll
// return this fully valid address as a connection
// attempt.
lnAddr := &lnwire.NetAddress{
IdentityKey: nodeKey,
Address: tcpAddr,
}
log.Tracef("Obtained %v as valid reachable "+
"node", lnAddr)
netAddrs = append(netAddrs, lnAddr)
}
}
return netAddrs, nil
}
// Name returns a human readable string which names the concrete
// implementation of the NetworkPeerBootstrapper.
func (d *DNSSeedBootstrapper) Name() string {
return fmt.Sprintf("BOLT-0010 DNS Seed: %v", d.dnsSeeds)
}