From ec809029d850a6f73fbc8cf24cc48c02895142c6 Mon Sep 17 00:00:00 2001 From: Kleissner Date: Wed, 21 Apr 2021 03:27:03 +0200 Subject: [PATCH] Sequences add pointer to high-level data. Proper mapping of sequences to information requests. --- Bootstrap.go | 20 ++++++++++--- Commands.go | 70 ++++++++++++++++++++++++++------------------- Connection.go | 6 ++-- Kademlia.go | 18 ++++++------ Message Encoding.go | 19 ++++++------ Message Sequence.go | 50 ++++++++++++++++++-------------- go.mod | 8 +++--- go.sum | 18 ++++++------ 8 files changed, 120 insertions(+), 89 deletions(-) diff --git a/Bootstrap.go b/Bootstrap.go index 8d62c3b..d5e3327 100644 --- a/Bootstrap.go +++ b/Bootstrap.go @@ -204,13 +204,25 @@ func contactArbitraryPeer(publicKey *btcec.PublicKey, addresses []*net.UDPAddr) } for _, address := range addresses { - sendAllNetworks(publicKey, &PacketRaw{Command: CommandAnnouncement, Payload: packets[0].raw}, address) + sendAllNetworks(publicKey, &PacketRaw{Command: CommandAnnouncement, Payload: packets[0].raw}, address, &bootstrapFindSelf{}) } } -// cmdResponseFindSelf processes FIND_SELF responses -func (peer *PeerInfo) cmdResponseFindSelf(msg *MessageResponse, Closest []PeerRecord) { - for _, closePeer := range Closest { +// bootstrapFindSelf is a dummy structure assigned to sequences when sending the Announcement message. +// When receiving the Response message, it will know that it was a legitimate bootstrap request. +type bootstrapFindSelf struct { +} + +// bootstrapAcceptContacts is the maximum count of contacts considered. It limits the impact of fake peers. +const bootstrapAcceptContacts = 5 + +// cmdResponseBootstrapFindSelf processes FIND_SELF responses +func (peer *PeerInfo) cmdResponseBootstrapFindSelf(msg *MessageResponse, closest []PeerRecord) { + if len(closest) > bootstrapAcceptContacts { + closest = closest[:bootstrapAcceptContacts] + } + + for _, closePeer := range closest { // Initiate contact. Once a response comes back, the peer is actually added to the list. contactArbitraryPeer(closePeer.PublicKey, []*net.UDPAddr{{IP: closePeer.IP, Port: int(closePeer.Port)}}) } diff --git a/Commands.go b/Commands.go index 50eaf87..3c74cc3 100644 --- a/Commands.go +++ b/Commands.go @@ -123,52 +123,64 @@ func (peer *PeerInfo) cmdResponse(msg *MessageResponse) { 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 + // The sequence data is used to correlate this response with the announcement. + if msg.sequence == nil || msg.sequence.data == nil { + // If there is no sequence data but there were results returned, it means we received unsolicited response data. It will be rejected. + if len(msg.HashesNotFound) > 0 || len(msg.Hash2Peers) > 0 || len(msg.FilesEmbed) > 0 { + log.Printf("cmdResponse unsolicited response data received from %s\n", msg.connection.Address.String()) } - info.ActiveNodesSub(1) + + return } - for _, hash2Peer := range msg.Hash2Peers { - info := nodesDHT.IRLookup(peer.NodeID, hash2Peer.ID.Hash) - if info == nil { - // Response to FIND_SELF? - if bytes.Equal(hash2Peer.ID.Hash, nodeID) && len(hash2Peer.Closest) > 0 { - peer.cmdResponseFindSelf(msg, hash2Peer.Closest) + // bootstrap FIND_SELF? + if _, ok := msg.sequence.data.(*bootstrapFindSelf); ok { + for _, hash2Peer := range msg.Hash2Peers { + // Make sure no garbage is returned. The key must be self and only Closest is expected. + if !bytes.Equal(hash2Peer.ID.Hash, nodeID) || len(hash2Peer.Closest) == 0 { + log.Printf("Incoming response to bootstrap FIND_SELF contains invalid data from %s\n", msg.connection.Address.String()) + return } - continue + + peer.cmdResponseBootstrapFindSelf(msg, hash2Peer.Closest) } - 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) + return } - for _, file := range msg.FilesEmbed { - info := nodesDHT.IRLookup(peer.NodeID, file.ID.Hash) - if info == nil { - continue + // Response to an information request? + if _, ok := msg.sequence.data.(*dht.InformationRequest); ok { + // Future: Once multiple information requests are pooled (multiplexed) into one or multiple Announcement sequences (messages), the responses need to be de-pooled. + // A simple multiplex structure linked via the sequence containing a map (hash 2 IR) could simplify this. + info := msg.sequence.data.(*dht.InformationRequest) + + if len(msg.HashesNotFound) > 0 { + info.Done() } - info.ResultChan <- &dht.NodeMessage{SenderID: peer.NodeID, Data: file.Data} + for _, hash2Peer := range msg.Hash2Peers { + info.ResultChan <- &dht.NodeMessage{SenderID: peer.NodeID, Closest: records2Nodes(hash2Peer.Closest, msg.connection.Network), Storing: records2Nodes(hash2Peer.Storing, msg.connection.Network)} - info.ActiveNodesSub(1) - info.Terminate() // file was found, terminate the request. + if hash2Peer.IsLast { + info.Done() + } + } + + for _, file := range msg.FilesEmbed { + info.ResultChan <- &dht.NodeMessage{SenderID: peer.NodeID, Data: file.Data} + + info.Done() + info.Terminate() // file was found, terminate the request. + } } } // cmdPing handles an incoming ping message func (peer *PeerInfo) cmdPing(msg *MessageRaw) { if peer == nil { - // Unexpected incoming ping, reply with announce message + // Unexpected incoming ping, reply with announcement message. For security reasons the remote peer is not asked for FIND_SELF. peer, _ = PeerlistAdd(msg.SenderPublicKey, msg.connection) - peer.sendAnnouncement(true, true, nil, nil, nil) + peer.sendAnnouncement(true, false, nil, nil, nil, nil) } peer.send(&PacketRaw{Command: CommandPong, Sequence: msg.Sequence}) //fmt.Printf("Incoming ping from %s on %s\n", msg.connection.Address.String(), msg.connection.Address.String()) @@ -201,7 +213,7 @@ func (peer *PeerInfo) cmdLocalDiscovery(msg *MessageAnnouncement) { // fmt.Printf("Incoming secondary local discovery from %s\n", msg.connection.Address.String()) } - peer.sendAnnouncement(true, true, nil, nil, nil) + peer.sendAnnouncement(true, true, nil, nil, nil, &bootstrapFindSelf{}) } // pingTime is the time in seconds to send out ping messages diff --git a/Connection.go b/Connection.go index 92a1e45..851cbe5 100644 --- a/Connection.go +++ b/Connection.go @@ -303,7 +303,7 @@ func (peer *PeerInfo) sendConnection(packet *PacketRaw, connection *Connection) } // sendAllNetworks sends a raw packet via all networks. It assigns a new sequence for each sent packet. -func sendAllNetworks(receiverPublicKey *btcec.PublicKey, packet *PacketRaw, remote *net.UDPAddr) (err error) { +func sendAllNetworks(receiverPublicKey *btcec.PublicKey, packet *PacketRaw, remote *net.UDPAddr, sequenceData interface{}) (err error) { var raw []byte packet.Protocol = ProtocolVersion @@ -319,7 +319,7 @@ func sendAllNetworks(receiverPublicKey *btcec.PublicKey, packet *PacketRaw, remo continue } - packet.Sequence = msgArbitrarySequence(receiverPublicKey) + packet.Sequence = msgArbitrarySequence(receiverPublicKey, sequenceData).sequence if raw, err = PacketEncrypt(peerPrivateKey, receiverPublicKey, packet); err != nil { return err } @@ -336,7 +336,7 @@ func sendAllNetworks(receiverPublicKey *btcec.PublicKey, packet *PacketRaw, remo continue } - packet.Sequence = msgArbitrarySequence(receiverPublicKey) + packet.Sequence = msgArbitrarySequence(receiverPublicKey, sequenceData).sequence if raw, err = PacketEncrypt(peerPrivateKey, receiverPublicKey, packet); err != nil { return err } diff --git a/Kademlia.go b/Kademlia.go index b3b3d6a..1fa7e0b 100644 --- a/Kademlia.go +++ b/Kademlia.go @@ -49,9 +49,9 @@ func initKademlia() { 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) + peer.sendAnnouncement(false, true, nil, nil, nil, request) } else { - peer.sendAnnouncement(false, false, []KeyHash{{Hash: request.Key}}, nil, nil) + peer.sendAnnouncement(false, false, []KeyHash{{Hash: request.Key}}, nil, nil, request) } } @@ -63,11 +63,11 @@ func (peer *PeerInfo) sendAnnouncementFindValue(request *dht.InformationRequest) findValue = append(findValue, KeyHash{Hash: request.Key}) - peer.sendAnnouncement(false, findSelf, findPeer, findValue, nil) + peer.sendAnnouncement(false, findSelf, findPeer, findValue, nil, request) } func (peer *PeerInfo) sendAnnouncementStore(fileHash []byte, fileSize uint64) { - peer.sendAnnouncement(false, false, nil, nil, []InfoStore{{ID: KeyHash{Hash: fileHash}, Size: fileSize, Type: 0}}) + peer.sendAnnouncement(false, false, nil, nil, []InfoStore{{ID: KeyHash{Hash: fileHash}, Size: fileSize, Type: 0}}, nil) } // ---- CORE DATA FUNCTIONS ---- @@ -78,9 +78,9 @@ func Data2Hash(data []byte) (hash []byte) { } // GetData returns the requested data. It checks first the local store and then tries via DHT. -func GetData(hash []byte) (data []byte, found bool) { +func GetData(hash []byte) (data []byte, senderNodeID []byte, found bool) { if data, found = GetDataLocal(hash); found { - return data, found + return data, nodeID, found } return GetDataDHT(hash) @@ -92,9 +92,9 @@ func GetDataLocal(hash []byte) (data []byte, found bool) { } // GetDataDHT requests data via DHT -func GetDataDHT(hash []byte) (data []byte, found bool) { - data, found, _ = nodesDHT.Get(hash) - return data, found +func GetDataDHT(hash []byte) (data []byte, senderNodeID []byte, found bool) { + data, senderNodeID, found, _ = nodesDHT.Get(hash) + return data, senderNodeID, found } // StoreDataLocal stores data into the local warehouse. diff --git a/Message Encoding.go b/Message Encoding.go index 7c424f5..5d57837 100644 --- a/Message Encoding.go +++ b/Message Encoding.go @@ -67,6 +67,7 @@ type MessageRaw struct { PacketRaw SenderPublicKey *btcec.PublicKey // Sender Public Key, ECDSA (secp256k1) 257-bit connection *Connection // Connection that received the packet + sequence *sequenceExpiry // Sequence } // MessageAnnouncement is the decoded announcement message. @@ -455,10 +456,10 @@ func selfFeatureSupport() (feature byte) { // announcementPacket contains information about a single announcement message type announcementPacket struct { - raw []byte // The raw packet - hashes [][]byte // List of hashes that are being searched for - sequence uint32 // Sequence number once sent - err error // Sending error, if any + raw []byte // The raw packet + hashes [][]byte // List of hashes that are being searched for + sequence *sequenceExpiry // Sequence + err error // Sending error, if any } // msgEncodeAnnouncement encodes an announcement message. It may return multiple messages if the input does not fit into one. @@ -761,7 +762,7 @@ createPacketLoop: // pingConnection sends a ping to the target peer via the specified connection func (peer *PeerInfo) pingConnection(connection *Connection) { - err := peer.sendConnection(&PacketRaw{Command: CommandPing, Sequence: peer.msgNewSequence()}, connection) + err := peer.sendConnection(&PacketRaw{Command: CommandPing, Sequence: peer.msgNewSequence(nil).sequence}, connection) connection.LastPingOut = time.Now() if (connection.Status == ConnectionActive || connection.Status == ConnectionRedundant) && IsNetworkErrorFatal(err) { @@ -774,13 +775,13 @@ func (peer *PeerInfo) Chat(text string) { peer.send(&PacketRaw{Command: CommandChat, Payload: []byte(text)}) } -// sendAnnouncement sends the announcement message -func (peer *PeerInfo) sendAnnouncement(sendUA, findSelf bool, findPeer []KeyHash, findValue []KeyHash, files []InfoStore) (packets []*announcementPacket) { +// sendAnnouncement sends the announcement message. It acquires a new sequence for each message. +func (peer *PeerInfo) sendAnnouncement(sendUA, findSelf bool, findPeer []KeyHash, findValue []KeyHash, files []InfoStore, sequenceData interface{}) (packets []*announcementPacket) { packets = msgEncodeAnnouncement(sendUA, findSelf, findPeer, findValue, files) for _, packet := range packets { - packet.sequence = peer.msgNewSequence() - packet.err = peer.send(&PacketRaw{Command: CommandAnnouncement, Payload: packet.raw, Sequence: packet.sequence}) + packet.sequence = peer.msgNewSequence(sequenceData) + packet.err = peer.send(&PacketRaw{Command: CommandAnnouncement, Payload: packet.raw, Sequence: packet.sequence.sequence}) } return diff --git a/Message Sequence.go b/Message Sequence.go index 91a8a22..d48c7f1 100644 --- a/Message Sequence.go +++ b/Message Sequence.go @@ -30,9 +30,11 @@ var sequences map[string]*sequenceExpiry var sequencesMutex sync.Mutex type sequenceExpiry struct { - created time.Time // When the sequence was created. - expires time.Time // When the sequence expires. This can be extended on the fly! - counter int // How many replies used the sequence. Multiple Response messages may be returned for a single Announcement one. + sequence uint32 // Sequence number + created time.Time // When the sequence was created. + expires time.Time // When the sequence expires. This can be extended on the fly! + counter int // How many replies used the sequence. Multiple Response messages may be returned for a single Announcement one. + data interface{} // Optional high-level data associated with the sequence } func initMessageSequence() { @@ -57,40 +59,42 @@ func initMessageSequence() { // msgNewSequence returns a new sequence and registers is // Use only for Announcement and Ping messages. -func (peer *PeerInfo) msgNewSequence() (sequence uint32) { - sequence = atomic.AddUint32(&peer.messageSequence, 1) - - key := string(peer.PublicKey.SerializeCompressed()) + strconv.FormatUint(uint64(sequence), 10) +func (peer *PeerInfo) msgNewSequence(data interface{}) (info *sequenceExpiry) { + info = &sequenceExpiry{ + sequence: atomic.AddUint32(&peer.messageSequence, 1), + created: time.Now(), + expires: time.Now().Add(time.Duration(ReplyTimeout) * time.Second), + data: data, + } // Add the sequence to the list. Sequences are unique enough that collisions are unlikely and negligible. + key := string(peer.PublicKey.SerializeCompressed()) + strconv.FormatUint(uint64(info.sequence), 10) sequencesMutex.Lock() - sequences[key] = &sequenceExpiry{ - created: time.Now(), - expires: time.Now().Add(time.Duration(ReplyTimeout) * time.Second), - } + sequences[key] = info sequencesMutex.Unlock() - return sequence + return } // msgArbitrarySequence returns an arbitrary sequence to be used for uncontacted peers -func msgArbitrarySequence(publicKey *btcec.PublicKey) (sequence uint32) { - sequence = rand.Uint32() - - key := string(publicKey.SerializeCompressed()) + strconv.FormatUint(uint64(sequence), 10) +func msgArbitrarySequence(publicKey *btcec.PublicKey, data interface{}) (info *sequenceExpiry) { + info = &sequenceExpiry{ + sequence: rand.Uint32(), + created: time.Now(), + expires: time.Now().Add(time.Duration(ReplyTimeout) * time.Second), + data: data, + } // Add the sequence to the list. Sequences are unique enough that collisions are unlikely and negligible. + key := string(publicKey.SerializeCompressed()) + strconv.FormatUint(uint64(info.sequence), 10) sequencesMutex.Lock() - sequences[key] = &sequenceExpiry{ - created: time.Now(), - expires: time.Now().Add(time.Duration(ReplyTimeout) * time.Second), - } + sequences[key] = info sequencesMutex.Unlock() - return sequence + return } -// msgValidateSequence validates the sequence number of an incoming message +// msgValidateSequence validates the sequence number of an incoming message. It will set raw.sequence if valid. func msgValidateSequence(raw *MessageRaw, invalidate bool) (valid bool, rtt time.Duration) { // Only Response and Pong if raw.Command != CommandResponse && raw.Command != CommandPong { @@ -124,6 +128,8 @@ func msgValidateSequence(raw *MessageRaw, invalidate bool) (valid bool, rtt time sequence.expires = time.Now().Add(time.Duration(ReplyTimeout) * time.Second / 2) } + raw.sequence = sequence + return sequence.expires.After(time.Now()), rtt } diff --git a/go.mod b/go.mod index 018a858..d675808 100644 --- a/go.mod +++ b/go.mod @@ -3,12 +3,12 @@ module github.com/PeernetOfficial/core go 1.16 require ( - github.com/PeernetOfficial/core/dht v0.0.0-20210410140655-6a9d16717779 + github.com/PeernetOfficial/core/dht v0.0.0-20210421010630-e432696ad116 github.com/btcsuite/btcd v0.21.0-beta.0.20210401013323-36a96f6a0025 github.com/pkg/errors v0.9.1 - golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 - golang.org/x/net v0.0.0-20210414194228-064579744ee0 - golang.org/x/sys v0.0.0-20210415045647-66c3f260301c + golang.org/x/crypto v0.0.0-20210415154028-4f45737414dc + golang.org/x/net v0.0.0-20210420210106-798c2154c571 + golang.org/x/sys v0.0.0-20210420205809-ac73e9fd8988 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b lukechampine.com/blake3 v1.1.5 ) diff --git a/go.sum b/go.sum index e1f9376..6f06fd1 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -github.com/PeernetOfficial/core/dht v0.0.0-20210410140655-6a9d16717779 h1:P21VQkgh7qrNuXG/tiXC56BNv2cjGI6z/CYpfrCAVCk= -github.com/PeernetOfficial/core/dht v0.0.0-20210410140655-6a9d16717779/go.mod h1:sb2H21VIVTohCXVrDFo/Skl2tN8Yzba+MXSS6NgCYXw= +github.com/PeernetOfficial/core/dht v0.0.0-20210421010630-e432696ad116 h1:1dqRhS20S6dRPdLL5dqp4TjTWm3vNvSm/EtRpPfxU6U= +github.com/PeernetOfficial/core/dht v0.0.0-20210421010630-e432696ad116/go.mod h1:sb2H21VIVTohCXVrDFo/Skl2tN8Yzba+MXSS6NgCYXw= 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.0.20210401013323-36a96f6a0025 h1:aoVqvZk4mLyF3WZbqEVPq+vXnwL2wekZg4P4mjYJNLs= @@ -38,22 +38,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-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w= -golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210415154028-4f45737414dc h1:+q90ECDSAQirdykUN6sPEiBXBsp8Csjcca8Oy7bgLTA= +golang.org/x/crypto v0.0.0-20210415154028-4f45737414dc/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-20210414194228-064579744ee0 h1:iqW3Mjl/6IP9cHJC/wdiIu3lyBDMUfDElRMyFlqbtiQ= -golang.org/x/net v0.0.0-20210414194228-064579744ee0/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= +golang.org/x/net v0.0.0-20210420210106-798c2154c571 h1:Q6Bg8xzKzpFPU4Oi1sBnBTHBwlMsLeEXpu4hYBY8rAg= +golang.org/x/net v0.0.0-20210420210106-798c2154c571/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= 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-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210415045647-66c3f260301c h1:6L+uOeS3OQt/f4eFHXZcTxeZrGCuz+CLElgEBjbcTA4= -golang.org/x/sys v0.0.0-20210415045647-66c3f260301c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210420205809-ac73e9fd8988 h1:EjgCl+fVlIaPJSori0ikSz3uV0DOHKWOJFpv1sAAhBM= +golang.org/x/sys v0.0.0-20210420205809-ac73e9fd8988/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=