diff --git a/peer/peer.go b/peer/peer.go index 78e77de6..95f5b094 100644 --- a/peer/peer.go +++ b/peer/peer.go @@ -293,6 +293,7 @@ type Config struct { // UsingV2Conn is defined if and only if we accept and attempt to make // v2 connections. + // TODO: Modify this so that we can downgrade certain peers. UsingV2Conn bool } @@ -468,7 +469,8 @@ type Peer struct { witnessEnabled bool sendAddrV2 bool - V2Transport *v2transport.Peer + V2Transport *v2transport.Peer + shouldDowngradeToV1 atomic.Bool wireEncoding wire.MessageEncoding @@ -2345,13 +2347,19 @@ func (p *Peer) negotiateOutboundProtocol() error { // and reconnect using a v1 connection. This is the logic that // bitcoind uses. // TODO: random value for garbage len? - if err := p.V2Transport.InitiateV2Handshake(1); err != nil { + if err := p.V2Transport.InitiateV2Handshake(0); err != nil { return err } - if err := p.V2Transport.CompleteHandshake( + err := p.V2Transport.CompleteHandshake( true, nil, p.cfg.ChainParams.Net, - ); err != nil { + ) + if errors.Is(err, v2transport.ErrShouldDowngradeToV1) { + // If we should downgrade, mark Peer and then return an error to + // trigger a Disconnect call. + p.shouldDowngradeToV1.Store(true) + return err + } else if err != nil { return err } } @@ -2470,6 +2478,13 @@ func (p *Peer) WaitForDisconnect() { <-p.quit } +// ShouldDowngradeToV1 is called when we try to connect to a peer via v2 BIP324 +// transport and they hang up. In this case, we should reconnect with the +// legacy transport. +func (p *Peer) ShouldDowngradeToV1() bool { + return p.shouldDowngradeToV1.Load() +} + // newPeerBase returns a new base bitcoin peer based on the inbound flag. This // is used by the NewInboundPeer and NewOutboundPeer functions to perform base // setup needed by both types of peers. diff --git a/server.go b/server.go index 41b2ac51..a592a01c 100644 --- a/server.go +++ b/server.go @@ -2209,6 +2209,19 @@ func (s *server) outboundPeerConnected(c *connmgr.ConnReq, conn net.Conn) { // done along with other performing other desirable cleanup. func (s *server) peerDoneHandler(sp *serverPeer) { sp.WaitForDisconnect() + + // If this is an outbound peer and the shouldDowngradeToV1 bool is set on + // the underlying Peer, trigger a reconnect using the OG v1 connection + // scheme. + if !sp.Inbound() && sp.Peer.ShouldDowngradeToV1() { + // TODO: Determine _how_ to trigger a reconnect that does not use v2 + // transport. If it goes to donePeers, the Disconnect call for + // persistent peers will trigger a reconnect. For non-persistent + // peers, we will disconnect them and then find a new peer to + // connect to. + } + + // This is sent to a buffered channel, so it may not execute immediately. s.donePeers <- sp // Only tell sync manager we are gone if we ever told it we existed. diff --git a/v2transport/transport.go b/v2transport/transport.go index ad382c82..cb2684d9 100644 --- a/v2transport/transport.go +++ b/v2transport/transport.go @@ -88,6 +88,14 @@ var ( // errGarbageTooLarge is returned if a caller attempts to send garbage // larger than normal. errGarbageTooLarge = fmt.Errorf("garbage too large") + + // ErrShouldDowngradeToV1 is returned when we send the peer our + // ellswift key and they immediately hang up. This indicates that they + // don't understand v2 transport and interpreted the 64-byte key as a + // v1 message header + message. This will (always?) decode to an + // invalid command and checksum. The caller should try connecting to + // the peer with the OG v1 transport. + ErrShouldDowngradeToV1 = fmt.Errorf("should downgrade to v1") ) // Peer defines the components necessary for sending/receiving data over the v2 @@ -376,7 +384,7 @@ func (p *Peer) RespondV2Handshake(garbageLen int, net wire.BitcoinNet) error { len(p.receivedPrefix), len(v1Prefix)) var receiveBytes []byte - receiveBytes, err = p.Receive(1) + receiveBytes, _, err = p.Receive(1) if err != nil { log.Errorf("Failed to receive byte for v1 prefix "+ "check: %v", err) @@ -473,11 +481,11 @@ func createV1Prefix(net wire.BitcoinNet) []byte { // CompleteHandshake finishes the v2 protocol negotiation and optionally sends // decoy packets after sending the garbage terminator. func (p *Peer) CompleteHandshake(initiating bool, decoyContentLens []int, - net wire.BitcoinNet) error { + btcnet wire.BitcoinNet) error { log.Debugf("Completing v2 handshake (initiating=%v, "+ "num_decoys=%d, net=%v)", initiating, len(decoyContentLens), - net) + btcnet) var receivedPrefix []byte if initiating { @@ -498,8 +506,28 @@ func (p *Peer) CompleteHandshake(initiating bool, decoyContentLens []int, len(receivedPrefix), 64-len(receivedPrefix)) } - recvData, err := p.Receive(64 - len(receivedPrefix)) + recvData, numRead, err := p.Receive(64 - len(receivedPrefix)) if err != nil { + // If we receive an error when reading off the wire and we read + // zero bytes, then we will reconnect to the peer using v1. + // There are several different errors that Receive can return + // that indicate we should reconnect. Instead of special-casing + // them all, just perform these checks if any error was + // returned. + if numRead == 0 && initiating { + // The peer most likely attempted to parse our 64-byte + // elligator-swift key as a version message and failed + // when trying to parse the message header into + // something valid. In this case, return a special + // error that signals to the server that we can + // reconnect with the OG v1 scheme. + log.Debugf("Received transport error during " + + "v2 handshake, retying downgraded v1 " + + "connection.") + return ErrShouldDowngradeToV1 + } + + // If we are the recipient, we can fail. log.Errorf("Failed to receive peer's ellswift key data: %v", err) return err @@ -533,7 +561,7 @@ func (p *Peer) CompleteHandshake(initiating bool, decoyContentLens []int, // Calculate the v1 protocol's message prefix and see if the bytes read // read into ellswiftTheirs matches it. - v1Prefix := createV1Prefix(net) + v1Prefix := createV1Prefix(btcnet) // ellswiftTheirs should be at least 16 bytes if receive succeeded, but // just in case, check the size. @@ -546,7 +574,7 @@ func (p *Peer) CompleteHandshake(initiating bool, decoyContentLens []int, if !initiating && bytes.Equal(ellswiftTheirs[4:16], v1Prefix[4:16]) { log.Warnf("Peer sent v1 version message for wrong network "+ - "(expected %v)", net) + "(expected %v)", btcnet) return errWrongNetV1Peer } @@ -563,7 +591,7 @@ func (p *Peer) CompleteHandshake(initiating bool, decoyContentLens []int, log.Tracef("Calculated ECDH shared secret: %x", ecdhSecret) - err = p.createV2Ciphers(ecdhSecret[:], initiating, net) + err = p.createV2Ciphers(ecdhSecret[:], initiating, btcnet) if err != nil { // createV2Ciphers logs its own errors return err @@ -606,7 +634,7 @@ func (p *Peer) CompleteHandshake(initiating bool, decoyContentLens []int, p.recvGarbageTerm) // Skip garbage until encountering garbage terminator. - recvGarbage, err := p.Receive(16) + recvGarbage, _, err := p.Receive(16) if err != nil { log.Errorf("Failed to receive initial 16 bytes of "+ "garbage: %v", err) @@ -645,7 +673,7 @@ func (p *Peer) CompleteHandshake(initiating bool, decoyContentLens []int, log.Tracef("Garbage terminator not found, receiving 1 more "+ "byte (total_received=%d)", recvGarbageLen) - recvData, err := p.Receive(1) + recvData, _, err := p.Receive(1) if err != nil { log.Errorf("Failed to receive garbage "+ "byte %d: %v", recvGarbageLen+1, err) @@ -746,7 +774,7 @@ func (p *Peer) V2ReceivePacket(aad []byte) ([]byte, error) { lengthFieldLen) // Decrypt the length field so we know how many more bytes to receive. - encContentsLen, err := p.Receive(lengthFieldLen) + encContentsLen, _, err := p.Receive(lengthFieldLen) if err != nil { log.Errorf("Failed to receive encrypted length: %v", err) @@ -783,7 +811,7 @@ func (p *Peer) V2ReceivePacket(aad []byte) ([]byte, error) { log.Tracef("Receiving %d bytes for encrypted packet body", numBytes) - aeadCiphertext, err := p.Receive(numBytes) + aeadCiphertext, _, err := p.Receive(numBytes) if err != nil { log.Errorf("Failed to receive encrypted "+ "packet body: %v", err) @@ -844,7 +872,7 @@ func (p *Peer) Send(data []byte) (int, error) { } // Receive receives numBytes bytes from the underlying connection. -func (p *Peer) Receive(numBytes int) ([]byte, error) { +func (p *Peer) Receive(numBytes int) ([]byte, int, error) { b := make([]byte, numBytes) index := 0 total := 0 @@ -858,12 +886,12 @@ func (p *Peer) Receive(numBytes int) ([]byte, error) { // used implicitly by the loop structure. log.Criticalf("Receive logic error: total=%d > "+ "numBytes=%d", total, numBytes) - return nil, errFailedToRecv + return nil, total, errFailedToRecv } if total == numBytes { log.Tracef("Successfully received %d bytes", total) - return b, nil + return b, total, nil } log.Tracef("Calling Read (need %d bytes, have "+ @@ -873,7 +901,7 @@ func (p *Peer) Receive(numBytes int) ([]byte, error) { if err != nil { log.Errorf("Receive failed after reading %d bytes "+ "(target %d): %v", total+n, numBytes, err) - return nil, err + return nil, total, err } log.Tracef("Read returned %d bytes", n)