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 Fisher–Yates 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) }