Kademlia: Use information requests from updated DHT library. More testing & fixes upcoming.

This commit is contained in:
Kleissner
2021-04-09 16:36:35 +02:00
parent c44e57a0a0
commit e69589c5c8
10 changed files with 185 additions and 64 deletions

View File

@@ -6,7 +6,7 @@ Author: Peter Kleissner
Strategy for sending our IPv6 Multicast and IPv4 Broadcast messages:
* During bootstrap: Immediately at the beginning, then every 10 seconds until there is at least 1 peer.
* Every 10 minutes during regular operation.
* Each time a network adapter / IP change is detected. [FUTURE]
* Each time a network adapter / IP change is detected.
*/
@@ -180,7 +180,7 @@ func autoMulticastBroadcast() {
// Send out multicast/broadcast immediately.
sendMulticastBroadcast()
// Phase 1: Resend every 10 seconds until at least 1 peer in the peer list
// Phase 1: Resend every 10 seconds until at least 1 peer in the peer list.
for {
time.Sleep(time.Second * 10)

View File

@@ -7,7 +7,6 @@ Author: Peter Kleissner
package core
import (
"bytes"
"fmt"
"time"
@@ -47,22 +46,36 @@ func (peer *PeerInfo) cmdAnouncement(msg *MessageAnnouncement) {
// FIND_SELF: Requesting peers close to the sender?
if msg.Actions&(1<<ActionFindSelf) > 0 {
selfD := Hash2Peer{ID: KeyHash{peer.NodeID}}
// do not respond the caller's own peer (add to ignore list)
for _, node := range nodesDHT.GetClosestContacts(respondClosesContactsCount, peer.NodeID, filterFunc(msg.connection.IsLocal(), allowIPv4, allowIPv6), peer.NodeID) {
if info := node.Info.(*PeerInfo).peer2Info(msg.connection.IsLocal(), allowIPv4, allowIPv6); info != nil {
hash2Peers = append(hash2Peers, Hash2Peer{ID: KeyHash{node.ID}, Closest: []InfoPeer{*info}})
if info := node.Info.(*PeerInfo).peer2Record(msg.connection.IsLocal(), allowIPv4, allowIPv6); info != nil {
selfD.Closest = append(selfD.Closest, *info)
}
}
hash2Peers = append(hash2Peers, selfD)
}
// FIND_PEER: Find a different peer? Note that in this case no IPv4/IPv6 connectivity check is performed.
if msg.Actions&(1<<ActionFindPeer) > 0 {
// TODO
// FIND_PEER: Find a different peer?
if msg.Actions&(1<<ActionFindPeer) > 0 && len(msg.FindPeerKeys) > 0 {
for _, findPeer := range msg.FindPeerKeys {
details := Hash2Peer{ID: findPeer}
for _, node := range nodesDHT.GetClosestContacts(respondClosesContactsCount, findPeer.Hash, filterFunc(msg.connection.IsLocal(), allowIPv4, allowIPv6)) {
if info := node.Info.(*PeerInfo).peer2Record(msg.connection.IsLocal(), allowIPv4, allowIPv6); info != nil {
details.Closest = append(details.Closest, *info)
}
}
hash2Peers = append(hash2Peers, details)
}
}
// Find a value?
if msg.Actions&(1<<ActionFindValue) > 0 {
// TODO
// TODO query store
}
// Information about files stored by the sender?
@@ -74,9 +87,9 @@ func (peer *PeerInfo) cmdAnouncement(msg *MessageAnnouncement) {
peer.sendResponse(added, hash2Peers, nil, nil)
}
func (peer *PeerInfo) peer2Info(allowLocal, allowIPv4, allowIPv6 bool) (result *InfoPeer) {
func (peer *PeerInfo) peer2Record(allowLocal, allowIPv4, allowIPv6 bool) (result *PeerRecord) {
if connection := peer.GetConnection2Share(allowLocal, allowIPv4, allowIPv6); connection != nil {
return &InfoPeer{
return &PeerRecord{
PublicKey: peer.PublicKey,
NodeID: peer.NodeID,
IP: connection.Address.IP,
@@ -89,22 +102,55 @@ func (peer *PeerInfo) peer2Info(allowLocal, allowIPv4, allowIPv6 bool) (result *
// cmdResponse handles the response to the announcement
func (peer *PeerInfo) cmdResponse(msg *MessageResponse) {
// Future: We should only accept responses from peers that we contacted first for security reasons. This can be easily identified by Peer ID.
if peer == nil {
peer, _ = PeerlistAdd(msg.SenderPublicKey, msg.connection)
fmt.Printf("Incoming initial response from %s\n", msg.connection.Address.String())
}
// Each result is checked against the list of information requests. Only 1 response by peer is permitted per query currently.
// Response packets could be duplicated, which could happen due to auto broadcast when the remote peer responds and has multiple connections to self but none was active.
for _, hash := range msg.HashesNotFound {
info := nodesDHT.IRLookup(peer.NodeID, hash)
if info == nil {
continue
}
info.ActiveNodesSub(1)
}
for _, hash2Peer := range msg.Hash2Peers {
info := nodesDHT.IRLookup(peer.NodeID, hash2Peer.ID.Hash)
if info == nil {
continue
}
info.ResultChan <- &dht.NodeMessage{SenderID: peer.NodeID, Closest: records2Nodes(hash2Peer.Closest, msg.connection.Network), Storing: records2Nodes(hash2Peer.Storing, msg.connection.Network)}
// Future: a terminate field inform wether the remote peer is done sending. For now terminate after 1 packet.
info.ActiveNodesSub(1)
}
for _, file := range msg.FilesEmbed {
info := nodesDHT.IRLookup(peer.NodeID, file.ID.Hash)
if info == nil {
continue
}
info.ResultChan <- &dht.NodeMessage{SenderID: peer.NodeID, Data: file.Data}
info.ActiveNodesSub(1)
info.Terminate() // file was found, terminate the request.
}
// check if incoming response to FIND_SELF
for _, hash2peer := range msg.Hash2Peers {
/*for _, hash2peer := range msg.Hash2Peers {
if !bytes.Equal(hash2peer.ID.Hash, nodeID) {
for _, closePeer := range hash2peer.Closest {
// Initiate contact. Once a response comes back, the peer is actually added to the list.
contactArbitraryPeer(closePeer.PublicKey, closePeer.IP, closePeer.Port)
}
}
}
}*/
//fmt.Printf("Incoming response from %s on %s\n", msg.connection.Address.String(), msg.connection.Address.String())
}

View File

@@ -6,26 +6,65 @@ Author: Peter Kleissner
package core
import "github.com/PeernetOfficial/core/dht"
import (
"bytes"
"github.com/PeernetOfficial/core/dht"
)
var nodesDHT *dht.DHT
func initKademlia() {
nodesDHT = dht.NewDHT(&dht.Node{ID: nodeID}, 256, 20)
nodesDHT = dht.NewDHT(&dht.Node{ID: nodeID}, 256, 20, 5)
// ShouldEvict determines whether the given node shall be evicted
// TODO For now always return true.
nodesDHT.ShouldEvict = func(node *dht.Node) bool {
// TODO: logic
return true
}
// SendStore sends a store message to the remote node. I.e. asking it to store the given key-value
nodesDHT.SendStore = func(node *dht.Node, key []byte, value []byte) {
// TODO
// SendRequestStore sends a store message to the remote node. I.e. asking it to store the given key-value
nodesDHT.SendRequestStore = func(node *dht.Node, key []byte, value []byte) {
node.Info.(*PeerInfo).sendAnnouncementStore(key, value)
}
// SendRequest sends an information request to the remote node. I.e. requesting information.
nodesDHT.SendRequest = func(request *dht.InformationRequest, nodes []*dht.Node) {
// TODO
// SendRequestFindNode sends an information request to find a particular node. nodes are the nodes to send the request to.
nodesDHT.SendRequestFindNode = func(request *dht.InformationRequest) {
for _, node := range request.Nodes {
node.Info.(*PeerInfo).sendAnnouncementFindNode(request)
}
}
// SendRequestFindValue sends an information request to find data. nodes are the nodes to send the request to.
nodesDHT.SendRequestFindValue = func(request *dht.InformationRequest) {
for _, node := range request.Nodes {
node.Info.(*PeerInfo).sendAnnouncementFindValue(request)
}
}
}
// Future sendAnnouncementX: If it detects that announcements are sent out to the same peer within 50ms it should activate a wait-and-group scheme.
func (peer *PeerInfo) sendAnnouncementFindNode(request *dht.InformationRequest) {
// If the key is self, send it as FIND_SELF
if bytes.Equal(request.Key, nodeID) {
peer.sendAnnouncement(false, true, nil, nil, nil)
} else {
peer.sendAnnouncement(false, false, []KeyHash{{Hash: request.Key}}, nil, nil)
}
}
func (peer *PeerInfo) sendAnnouncementFindValue(request *dht.InformationRequest) {
findSelf := false
var findPeer []KeyHash
var findValue []KeyHash
findValue = append(findValue, KeyHash{Hash: request.Key})
peer.sendAnnouncement(false, findSelf, findPeer, findValue, nil)
}
func (peer *PeerInfo) sendAnnouncementStore(key []byte, value []byte) {
peer.sendAnnouncement(false, false, nil, nil, []InfoStore{{ID: KeyHash{Hash: key}, Size: uint64(len(value)), Type: 0}})
}

View File

@@ -92,8 +92,8 @@ type InfoStore struct {
Type uint8 // Type of the file: 0 = File, 1 = Header file containing list of parts
}
// InfoPeer informs about a peer
type InfoPeer struct {
// PeerRecord informs about a peer
type PeerRecord struct {
PublicKey *btcec.PublicKey // Public Key
NodeID []byte // Kademlia Node ID
IP net.IP // IP
@@ -103,9 +103,9 @@ type InfoPeer struct {
// Hash2Peer links a hash to peers who are known to store the data and to peers who are considered close to the hash
type Hash2Peer struct {
ID KeyHash // Hash that was queried
Closest []InfoPeer // Closest peers
Storing []InfoPeer // Peers known to store the data identified by the hash
ID KeyHash // Hash that was queried
Closest []PeerRecord // Closest peers
Storing []PeerRecord // Peers known to store the data identified by the hash
}
// EmbeddedFileData contains embedded data sent within a response
@@ -289,11 +289,15 @@ func msgDecodeResponse(msg *MessageRaw) (result *MessageResponse, err error) {
countHashesNotFound := binary.LittleEndian.Uint16(msg.Payload[read+4 : read+4+2])
read += 6
if countPeerResponses == 0 && countEmbeddedFiles == 0 && countHashesNotFound == 0 {
return nil, errors.New("response: empty")
}
data := msg.Payload[read:]
// Peer response data
if countPeerResponses > 0 {
hash2Peers, read, valid := decodeInfoPeer(data, int(countPeerResponses))
hash2Peers, read, valid := decodePeerRecord(data, int(countPeerResponses))
if !valid {
return nil, errors.New("response: peer info invalid data")
}
@@ -330,8 +334,8 @@ func msgDecodeResponse(msg *MessageRaw) (result *MessageResponse, err error) {
return
}
// decodeInfoPeer decodes the response data for FIND_SELF, FIND_PEER and FIND_VALUE messages
func decodeInfoPeer(data []byte, count int) (hash2Peers []Hash2Peer, read int, valid bool) {
// decodePeerRecord decodes the response data for FIND_SELF, FIND_PEER and FIND_VALUE messages
func decodePeerRecord(data []byte, count int) (hash2Peers []Hash2Peer, read int, valid bool) {
index := 0
for n := 0; n < count; n++ {
@@ -352,7 +356,7 @@ func decodeInfoPeer(data []byte, count int) (hash2Peers []Hash2Peer, read int, v
return nil, 0, false
}
peer := InfoPeer{}
peer := PeerRecord{}
peerIDcompressed := make([]byte, 33)
copy(peerIDcompressed[:], data[index:index+33])
@@ -773,3 +777,8 @@ func (peer *PeerInfo) sendResponse(sendUA bool, hash2Peers []Hash2Peer, filesEmb
return err
}
// lastContact2Time translates a last contact time in seconds to a timestamp
func lastContact2Time(LastContact uint32) time.Time {
return time.Now().Add(-time.Second * time.Duration(LastContact))
}

View File

@@ -140,7 +140,7 @@ func networkStart(iface net.Interface, addresses []net.Addr) {
func networkPrepareListen(ipA string, port int) (network *Network, err error) {
ip := net.ParseIP(ipA)
if ip == nil {
return nil, errors.New("Invalid input IP")
return nil, errors.New("invalid input IP")
}
network = new(Network)
@@ -150,7 +150,7 @@ func networkPrepareListen(ipA string, port int) (network *Network, err error) {
if ipA != "0.0.0.0" && ipA != "::" {
network.iface, network.ipnet = FindInterfaceByIP(ip)
if network.iface == nil {
return nil, errors.New("Error finding the network interface belonging to IP")
return nil, errors.New("error finding the network interface belonging to IP")
}
}

View File

@@ -147,3 +147,6 @@ func hashData(data []byte) (hash []byte) {
hash32 := blake3.Sum256(data)
return hash32[:]
}
// HashSize is blake3 hash digest size = 256 bits
const HashSize = 32

View File

@@ -9,6 +9,7 @@ package core
import (
"encoding/hex"
"log"
"net"
"os"
"sync"
@@ -17,6 +18,7 @@ import (
)
// peerID is the current peers ID. It is a ECDSA (secp256k1) 257-bit public key.
// The node ID is the blake3 hash of the public key compressed form.
var peerPrivateKey *btcec.PrivateKey
var peerPublicKey *btcec.PublicKey
var nodeID []byte
@@ -158,3 +160,26 @@ func publicKey2Compressed(publicKey *btcec.PublicKey) [btcec.PubKeyBytesLenCompr
func publicKey2NodeID(publicKey *btcec.PublicKey) (nodeID []byte) {
return hashData(publicKey.SerializeCompressed())
}
// record2Peer translate a peer record (from a message) into an actual usable PeerInfo structure
// It requires the network parameter which must be the same as caller/supplier. This ensures that peer details do not "jump" between physical network adapters.
func record2Peer(record PeerRecord, network *Network) (peerN *PeerInfo) {
if peerN = PeerlistLookup(record.PublicKey); peerN != nil {
return peerN
}
// Create temporary peer which is not added to the global list and not added to Kademlia.
connection := &Connection{Network: network, Address: &net.UDPAddr{IP: record.IP, Port: int(record.Port)}, Status: ConnectionActive}
return &PeerInfo{PublicKey: record.PublicKey, connectionActive: []*Connection{connection}, connectionLatest: connection, NodeID: publicKey2NodeID(record.PublicKey)}
}
// records2Nodes translates infoPeer structures to nodes
// LastContact is passed on in the Node.LastSeen field.
func records2Nodes(records []PeerRecord, network *Network) (nodes []*dht.Node) {
for _, record := range records {
peer := record2Peer(record, network)
nodes = append(nodes, &dht.Node{ID: peer.NodeID, LastSeen: lastContact2Time(record.LastContact), Info: peer})
}
return
}

View File

@@ -1,8 +1,10 @@
# Peernet Core
The core library which is needed for any Peernet application. It provides connectivity to the network and all basic functions.
The core library which is needed for any Peernet application. It provides connectivity to the network and all basic functions. For details about Peernet see https://peernet.org/.
Current version: 0.1 (pre-alpha)
Current version: 0.2 (early alpha)
Current development status: Initial connectivity works. DHT functionality is in development.
## Use
@@ -42,11 +44,11 @@ func main() {
## Dependencies
Go 1.16 or higher is required. Before compiling, make sure to download and update all 3rd party packages:
Go 1.16 or higher is required. These are the major dependencies:
```
go get -u github.com/btcsuite/btcd/btcec
go get -u lukechampine.com/blake3
github.com/btcsuite/btcd/btcec
lukechampine.com/blake3
```
## Configuration
@@ -95,14 +97,10 @@ Above limits are constants and can be adjusted in the code via `pingTime`, `conn
### Kademlia
TBD
### Ongoing Peerlist Exchange
TBD
The routing table has a bucket size of 20 and the size of keys 256 bits (blake3 hash). Nodes within buckets are sorted by least recently seen. The number of nodes to contact concurrently in DHT lookups (also known as alpha number) is set to 5.
## Contributing
Please note that by contributing code, documentation, ideas, snippets, or any other intellectual property you agree that you have all the necessary rights and you agree that we, the Peernet organization, may use it for any purpose.
&copy; 2021 Peernet
&copy; 2021 Peernet s.r.o.

10
go.mod
View File

@@ -3,11 +3,11 @@ module github.com/PeernetOfficial/core
go 1.16
require (
github.com/PeernetOfficial/core/dht v0.0.0-20210405124533-d83123bf46c9
github.com/PeernetOfficial/core/reuseport v0.0.0-20210316211915-8b329f7258d1
github.com/btcsuite/btcd v0.21.0-beta
golang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4
github.com/PeernetOfficial/core/dht v0.0.0-20210409140720-c44e57a0a02b
github.com/PeernetOfficial/core/reuseport v0.0.0-20210409140720-c44e57a0a02b
github.com/btcsuite/btcd v0.21.0-beta.0.20210401013323-36a96f6a0025
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
lukechampine.com/blake3 v1.1.5
)

27
go.sum
View File

@@ -1,14 +1,14 @@
github.com/PeernetOfficial/core/dht v0.0.0-20210405124533-d83123bf46c9 h1:J3SCdaAy8XnuwgwdqYimNzyt4HFQv+EKCPYbItoky8c=
github.com/PeernetOfficial/core/dht v0.0.0-20210405124533-d83123bf46c9/go.mod h1:sb2H21VIVTohCXVrDFo/Skl2tN8Yzba+MXSS6NgCYXw=
github.com/PeernetOfficial/core/reuseport v0.0.0-20210316211915-8b329f7258d1 h1:m1dEg5E8ZwT3l/F6xiDrU2Q/ZJ9BeVFAZt/OUBxtr4M=
github.com/PeernetOfficial/core/reuseport v0.0.0-20210316211915-8b329f7258d1/go.mod h1:j5lvi5jDpdDyuQrsUkCv4kkibVhPWj87sMol41sXIxc=
github.com/PeernetOfficial/core/dht v0.0.0-20210409140720-c44e57a0a02b h1:4V7y4Kul7R57EAWxr85fVHG3/Gf6U6RBL3ZQFqjjSpU=
github.com/PeernetOfficial/core/dht v0.0.0-20210409140720-c44e57a0a02b/go.mod h1:sb2H21VIVTohCXVrDFo/Skl2tN8Yzba+MXSS6NgCYXw=
github.com/PeernetOfficial/core/reuseport v0.0.0-20210409140720-c44e57a0a02b h1:9pYGclxvKWSDhe1qlVg8bzeIS78kYehdbNSYautAT+4=
github.com/PeernetOfficial/core/reuseport v0.0.0-20210409140720-c44e57a0a02b/go.mod h1:j5lvi5jDpdDyuQrsUkCv4kkibVhPWj87sMol41sXIxc=
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
github.com/btcsuite/btcd v0.21.0-beta h1:At9hIZdJW0s9E/fAz28nrz6AmcNlSVucCH796ZteX1M=
github.com/btcsuite/btcd v0.21.0-beta/go.mod h1:ZSWyehm27aAuS9bvkATT+Xte3hjHZ+MRgMY/8NJ7K94=
github.com/btcsuite/btcd v0.21.0-beta.0.20210401013323-36a96f6a0025 h1:aoVqvZk4mLyF3WZbqEVPq+vXnwL2wekZg4P4mjYJNLs=
github.com/btcsuite/btcd v0.21.0-beta.0.20210401013323-36a96f6a0025/go.mod h1:9n5ntfhhHQBIhUvlhDvD3Qg6fRUj4jkN0VB8L8svzOA=
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
github.com/btcsuite/btcutil v1.0.2/go.mod h1:j9HUFwoQRsZL3V4n+qG+CUnEGHOarIxfC3Le2Yhbcts=
github.com/btcsuite/btcutil v1.0.3-0.20201208143702-a53e38424cce/go.mod h1:0DVlHczLPewLcPGEIeUEzfOJhqGPQ0mJJRDBtD307+o=
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg=
github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY=
github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I=
@@ -17,6 +17,7 @@ github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218=
@@ -39,22 +40,22 @@ golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnf
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b h1:wSOdpTq0/eI46Ez/LkDwIsAKA71YP2SRKBODiRWM0as=
golang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w=
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4 h1:b0LrWgu8+q7z4J+0Y3Umo5q1dL7NXBkKBWkaVkAq17E=
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 h1:4nGaVu0QrbjT/AK2PRLuQfQuh6DJve+pELhqTdAj3x0=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210316164454-77fc1eacc6aa h1:ZYxPR6aca/uhfRJyaOAtflSHjJYiktO7QnJC5ut7iY4=
golang.org/x/sys v0.0.0-20210316164454-77fc1eacc6aa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44 h1:Bli41pIlzTzf3KEY06n+xnzK/BESIg2ze4Pgfh/aI8c=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=