From c3fad1f29df093e8fd03d70eb43f25ee9d531bf7 Mon Sep 17 00:00:00 2001 From: Pieter Wuille <pieter@wuille.net> Date: Wed, 16 Aug 2023 13:21:35 -0400 Subject: net: add have_next_message argument to Transport::GetBytesToSend() Before this commit, there are only two possibly outcomes for the "more" prediction in Transport::GetBytesToSend(): * true: the transport itself has more to send, so the answer is certainly yes. * false: the transport has nothing further to send, but if vSendMsg has more message(s) left, that still will result in more wire bytes after the next SetMessageToSend(). For the BIP324 v2 transport, there will arguably be a third state: * definitely not: the transport has nothing further to send, but even if vSendMsg has more messages left, they can't be sent (right now). This happens before the handshake is complete. To implement this, we move the entire decision logic to the Transport, by adding a boolean to GetBytesToSend(), called have_next_message, which informs the transport whether more messages are available. The return values are still true and false, but they mean "definitely yes" and "definitely no", rather than "yes" and "maybe". --- src/net.h | 37 +++++++++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 8 deletions(-) (limited to 'src/net.h') diff --git a/src/net.h b/src/net.h index 60a15fea55..1507ff7384 100644 --- a/src/net.h +++ b/src/net.h @@ -308,19 +308,40 @@ public: const std::string& /*m_type*/ >; - /** Get bytes to send on the wire. + /** Get bytes to send on the wire, if any, along with other information about it. * * As a const function, it does not modify the transport's observable state, and is thus safe * to be called multiple times. * - * The bytes returned by this function act as a stream which can only be appended to. This - * means that with the exception of MarkBytesSent, operations on the transport can only append - * to what is being returned. + * @param[in] have_next_message If true, the "more" return value reports whether more will + * be sendable after a SetMessageToSend call. It is set by the caller when they know + * they have another message ready to send, and only care about what happens + * after that. The have_next_message argument only affects this "more" return value + * and nothing else. * - * Note that m_type and to_send refer to data that is internal to the transport, and calling - * any non-const function on this object may invalidate them. + * Effectively, there are three possible outcomes about whether there are more bytes + * to send: + * - Yes: the transport itself has more bytes to send later. For example, for + * V1Transport this happens during the sending of the header of a + * message, when there is a non-empty payload that follows. + * - No: the transport itself has no more bytes to send, but will have bytes to + * send if handed a message through SetMessageToSend. In V1Transport this + * happens when sending the payload of a message. + * - Blocked: the transport itself has no more bytes to send, and is also incapable + * of sending anything more at all now, if it were handed another + * message to send. + * + * The boolean 'more' is true for Yes, false for Blocked, and have_next_message + * controls what is returned for No. + * + * @return a BytesToSend object. The to_send member returned acts as a stream which is only + * ever appended to. This means that with the exception of MarkBytesSent (which pops + * bytes off the front of later to_sends), operations on the transport can only append + * to what is being returned. Also note that m_type and to_send refer to data that is + * internal to the transport, and calling any non-const function on this object may + * invalidate them. */ - virtual BytesToSend GetBytesToSend() const noexcept = 0; + virtual BytesToSend GetBytesToSend(bool have_next_message) const noexcept = 0; /** Report how many bytes returned by the last GetBytesToSend() have been sent. * @@ -416,7 +437,7 @@ public: CNetMessage GetReceivedMessage(std::chrono::microseconds time, bool& reject_message) override EXCLUSIVE_LOCKS_REQUIRED(!m_recv_mutex); bool SetMessageToSend(CSerializedNetMsg& msg) noexcept override EXCLUSIVE_LOCKS_REQUIRED(!m_send_mutex); - BytesToSend GetBytesToSend() const noexcept override EXCLUSIVE_LOCKS_REQUIRED(!m_send_mutex); + BytesToSend GetBytesToSend(bool have_next_message) const noexcept override EXCLUSIVE_LOCKS_REQUIRED(!m_send_mutex); void MarkBytesSent(size_t bytes_sent) noexcept override EXCLUSIVE_LOCKS_REQUIRED(!m_send_mutex); size_t GetSendMemoryUsage() const noexcept override EXCLUSIVE_LOCKS_REQUIRED(!m_send_mutex); }; -- cgit v1.2.3 From 5f4b2c6d79e81ee0445752ad558fcc17831f4b2f Mon Sep 17 00:00:00 2001 From: Pieter Wuille <pieter@wuille.net> Date: Wed, 30 Aug 2023 16:10:58 -0400 Subject: net: remove unused Transport::SetReceiveVersion --- src/net.h | 10 ---------- 1 file changed, 10 deletions(-) (limited to 'src/net.h') diff --git a/src/net.h b/src/net.h index 1507ff7384..6f989aa175 100644 --- a/src/net.h +++ b/src/net.h @@ -266,8 +266,6 @@ public: /** Returns true if the current message is complete (so GetReceivedMessage can be called). */ virtual bool ReceivedMessageComplete() const = 0; - /** Set the deserialization context version for objects returned by GetReceivedMessage. */ - virtual void SetReceiveVersion(int version) = 0; /** Feed wire bytes to the transport. * @@ -413,14 +411,6 @@ public: return WITH_LOCK(m_recv_mutex, return CompleteInternal()); } - void SetReceiveVersion(int nVersionIn) override EXCLUSIVE_LOCKS_REQUIRED(!m_recv_mutex) - { - AssertLockNotHeld(m_recv_mutex); - LOCK(m_recv_mutex); - hdrbuf.SetVersion(nVersionIn); - vRecv.SetVersion(nVersionIn); - } - bool ReceivedBytes(Span<const uint8_t>& msg_bytes) override EXCLUSIVE_LOCKS_REQUIRED(!m_recv_mutex) { AssertLockNotHeld(m_recv_mutex); -- cgit v1.2.3 From 13a7f01557272db652b3f333af3f06af6897253f Mon Sep 17 00:00:00 2001 From: Pieter Wuille <pieter@wuille.net> Date: Thu, 27 Jul 2023 15:10:34 -0400 Subject: net: add V2Transport class with subset of BIP324 functionality This introduces a V2Transport with a basic subset of BIP324 functionality: * no ability to send garbage (but receiving is supported) * no ability to send decoy packets (but receiving them is supported) * no support for short message id encoding (neither encoding or decoding) * no waiting until 12 non-V1 bytes have been received * (and thus) no detection of V1 connections on the responder side (on the sender side, detecting V1 is not supported either, but that needs to be dealt with at a higher layer, by reconnecting) --- src/net.h | 181 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 179 insertions(+), 2 deletions(-) (limited to 'src/net.h') diff --git a/src/net.h b/src/net.h index 6f989aa175..27d141bc6e 100644 --- a/src/net.h +++ b/src/net.h @@ -6,6 +6,7 @@ #ifndef BITCOIN_NET_H #define BITCOIN_NET_H +#include <bip324.h> #include <chainparams.h> #include <common/bloom.h> #include <compat/compat.h> @@ -298,7 +299,8 @@ public: * - Span<const uint8_t> to_send: span of bytes to be sent over the wire (possibly empty). * - bool more: whether there will be more bytes to be sent after the ones in to_send are * all sent (as signaled by MarkBytesSent()). - * - const std::string& m_type: message type on behalf of which this is being sent. + * - const std::string& m_type: message type on behalf of which this is being sent + * ("" for bytes that are not on behalf of any message). */ using BytesToSend = std::tuple< Span<const uint8_t> /*to_send*/, @@ -327,7 +329,9 @@ public: * happens when sending the payload of a message. * - Blocked: the transport itself has no more bytes to send, and is also incapable * of sending anything more at all now, if it were handed another - * message to send. + * message to send. This occurs in V2Transport before the handshake is + * complete, as the encryption ciphers are not set up for sending + * messages before that point. * * The boolean 'more' is true for Yes, false for Blocked, and have_next_message * controls what is returned for No. @@ -432,6 +436,179 @@ public: size_t GetSendMemoryUsage() const noexcept override EXCLUSIVE_LOCKS_REQUIRED(!m_send_mutex); }; +class V2Transport final : public Transport +{ +private: + /** Contents of the version packet to send. BIP324 stipulates that senders should leave this + * empty, and receivers should ignore it. Future extensions can change what is sent as long as + * an empty version packet contents is interpreted as no extensions supported. */ + static constexpr std::array<std::byte, 0> VERSION_CONTENTS = {}; + + // The sender side and receiver side of V2Transport are state machines that are transitioned + // through, based on what has been received. The receive state corresponds to the contents of, + // and bytes received to, the receive buffer. The send state controls what can be appended to + // the send buffer. + + /** State type that defines the current contents of the receive buffer and/or how the next + * received bytes added to it will be interpreted. + * + * Diagram: + * + * start /---------\ + * | | | + * v v | + * KEY -> GARB_GARBTERM -> GARBAUTH -> VERSION -> APP -> APP_READY + */ + enum class RecvState : uint8_t { + /** Public key. + * + * This is the initial state, during which the other side's public key is + * received. When that information arrives, the ciphers get initialized and the state + * becomes GARB_GARBTERM. */ + KEY, + + /** Garbage and garbage terminator. + * + * Whenever a byte is received, the last 16 bytes are compared with the expected garbage + * terminator. When that happens, the state becomes GARBAUTH. If no matching terminator is + * received in 4111 bytes (4095 for the maximum garbage length, and 16 bytes for the + * terminator), the connection aborts. */ + GARB_GARBTERM, + + /** Garbage authentication packet. + * + * A packet is received, and decrypted/verified with AAD set to the garbage received during + * the GARB_GARBTERM state. If that succeeds, the state becomes VERSION. If it fails the + * connection aborts. */ + GARBAUTH, + + /** Version packet. + * + * A packet is received, and decrypted/verified. If that succeeds, the state becomes APP, + * and the decrypted contents is interpreted as version negotiation (currently, that means + * ignoring it, but it can be used for negotiating future extensions). If it fails, the + * connection aborts. */ + VERSION, + + /** Application packet. + * + * A packet is received, and decrypted/verified. If that succeeds, the state becomes + * APP_READY and the decrypted contents is kept in m_recv_decode_buffer until it is + * retrieved as a message by GetMessage(). */ + APP, + + /** Nothing (an application packet is available for GetMessage()). + * + * Nothing can be received in this state. When the message is retrieved by GetMessage, + * the state becomes APP again. */ + APP_READY, + }; + + /** State type that controls the sender side. + * + * Diagram: + * + * start + * | + * v + * AWAITING_KEY -> READY + */ + enum class SendState : uint8_t { + /** Waiting for the other side's public key. + * + * This is the initial state. The public key is sent out. When the receiver receives the + * other side's public key and transitions to GARB_GARBTERM, the sender state becomes + * READY. */ + AWAITING_KEY, + + /** Normal sending state. + * + * In this state, the ciphers are initialized, so packets can be sent. When this state is + * entered, the garbage terminator, garbage authentication packet, and version packet are + * appended to the send buffer (in addition to the key which may still be there). In this + * state a message can be provided if the send buffer is empty. */ + READY, + }; + + /** Cipher state. */ + BIP324Cipher m_cipher; + /** Whether we are the initiator side. */ + const bool m_initiating; + /** NodeId (for debug logging). */ + const NodeId m_nodeid; + + /** Lock for receiver-side fields. */ + mutable Mutex m_recv_mutex ACQUIRED_BEFORE(m_send_mutex); + /** In {GARBAUTH, VERSION, APP}, the decrypted packet length, if m_recv_buffer.size() >= + * BIP324Cipher::LENGTH_LEN. Unspecified otherwise. */ + uint32_t m_recv_len GUARDED_BY(m_recv_mutex) {0}; + /** Receive buffer; meaning is determined by m_recv_state. */ + std::vector<uint8_t> m_recv_buffer GUARDED_BY(m_recv_mutex); + /** During GARBAUTH, the garbage received during GARB_GARBTERM. */ + std::vector<uint8_t> m_recv_garbage GUARDED_BY(m_recv_mutex); + /** Buffer to put decrypted contents in, for converting to CNetMessage. */ + std::vector<uint8_t> m_recv_decode_buffer GUARDED_BY(m_recv_mutex); + /** Deserialization type. */ + const int m_recv_type; + /** Deserialization version number. */ + const int m_recv_version; + /** Current receiver state. */ + RecvState m_recv_state GUARDED_BY(m_recv_mutex); + + /** Lock for sending-side fields. If both sending and receiving fields are accessed, + * m_recv_mutex must be acquired before m_send_mutex. */ + mutable Mutex m_send_mutex ACQUIRED_AFTER(m_recv_mutex); + /** The send buffer; meaning is determined by m_send_state. */ + std::vector<uint8_t> m_send_buffer GUARDED_BY(m_send_mutex); + /** How many bytes from the send buffer have been sent so far. */ + uint32_t m_send_pos GUARDED_BY(m_send_mutex) {0}; + /** Type of the message being sent. */ + std::string m_send_type GUARDED_BY(m_send_mutex); + /** Current sender state. */ + SendState m_send_state GUARDED_BY(m_send_mutex); + + /** Change the receive state. */ + void SetReceiveState(RecvState recv_state) noexcept EXCLUSIVE_LOCKS_REQUIRED(m_recv_mutex); + /** Change the send state. */ + void SetSendState(SendState send_state) noexcept EXCLUSIVE_LOCKS_REQUIRED(m_send_mutex); + /** Given a packet's contents, find the message type (if valid), and strip it from contents. */ + static std::optional<std::string> GetMessageType(Span<const uint8_t>& contents) noexcept; + /** Determine how many received bytes can be processed in one go (not allowed in V1 state). */ + size_t GetMaxBytesToProcess() noexcept EXCLUSIVE_LOCKS_REQUIRED(m_recv_mutex); + /** Process bytes in m_recv_buffer, while in KEY state. */ + void ProcessReceivedKeyBytes() noexcept EXCLUSIVE_LOCKS_REQUIRED(m_recv_mutex, !m_send_mutex); + /** Process bytes in m_recv_buffer, while in GARB_GARBTERM state. */ + bool ProcessReceivedGarbageBytes() noexcept EXCLUSIVE_LOCKS_REQUIRED(m_recv_mutex); + /** Process bytes in m_recv_buffer, while in GARBAUTH/VERSION/APP state. */ + bool ProcessReceivedPacketBytes() noexcept EXCLUSIVE_LOCKS_REQUIRED(m_recv_mutex); + +public: + static constexpr uint32_t MAX_GARBAGE_LEN = 4095; + + /** Construct a V2 transport with securely generated random keys. + * + * @param[in] nodeid the node's NodeId (only for debug log output). + * @param[in] initiating whether we are the initiator side. + * @param[in] type_in the serialization type of returned CNetMessages. + * @param[in] version_in the serialization version of returned CNetMessages. + */ + V2Transport(NodeId nodeid, bool initiating, int type_in, int version_in) noexcept; + + /** Construct a V2 transport with specified keys (test use only). */ + V2Transport(NodeId nodeid, bool initiating, int type_in, int version_in, const CKey& key, Span<const std::byte> ent32) noexcept; + + // Receive side functions. + bool ReceivedMessageComplete() const noexcept override EXCLUSIVE_LOCKS_REQUIRED(!m_recv_mutex); + bool ReceivedBytes(Span<const uint8_t>& msg_bytes) noexcept override EXCLUSIVE_LOCKS_REQUIRED(!m_recv_mutex, !m_send_mutex); + CNetMessage GetReceivedMessage(std::chrono::microseconds time, bool& reject_message) noexcept override EXCLUSIVE_LOCKS_REQUIRED(!m_recv_mutex); + + // Send side functions. + bool SetMessageToSend(CSerializedNetMsg& msg) noexcept override EXCLUSIVE_LOCKS_REQUIRED(!m_send_mutex); + BytesToSend GetBytesToSend(bool have_next_message) const noexcept override EXCLUSIVE_LOCKS_REQUIRED(!m_send_mutex); + void MarkBytesSent(size_t bytes_sent) noexcept override EXCLUSIVE_LOCKS_REQUIRED(!m_send_mutex); + size_t GetSendMemoryUsage() const noexcept override EXCLUSIVE_LOCKS_REQUIRED(!m_send_mutex); +}; + struct CNodeOptions { NetPermissionFlags permission_flags = NetPermissionFlags::None; -- cgit v1.2.3 From 8da8642062fa2c7aa2f49995b832c3d0897e37ed Mon Sep 17 00:00:00 2001 From: Pieter Wuille <pieter@wuille.net> Date: Tue, 29 Aug 2023 22:37:18 -0400 Subject: net: make V2Transport auto-detect incoming V1 and fall back to it --- src/net.h | 67 ++++++++++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 54 insertions(+), 13 deletions(-) (limited to 'src/net.h') diff --git a/src/net.h b/src/net.h index 27d141bc6e..9194a677b1 100644 --- a/src/net.h +++ b/src/net.h @@ -444,25 +444,40 @@ private: * an empty version packet contents is interpreted as no extensions supported. */ static constexpr std::array<std::byte, 0> VERSION_CONTENTS = {}; + /** The length of the V1 prefix to match bytes initially received by responders with to + * determine if their peer is speaking V1 or V2. */ + static constexpr size_t V1_PREFIX_LEN = 12; + // The sender side and receiver side of V2Transport are state machines that are transitioned // through, based on what has been received. The receive state corresponds to the contents of, // and bytes received to, the receive buffer. The send state controls what can be appended to - // the send buffer. + // the send buffer and what can be sent from it. /** State type that defines the current contents of the receive buffer and/or how the next * received bytes added to it will be interpreted. * * Diagram: * - * start /---------\ - * | | | - * v v | - * KEY -> GARB_GARBTERM -> GARBAUTH -> VERSION -> APP -> APP_READY + * start(responder) + * | + * | start(initiator) /---------\ + * | | | | + * v v v | + * KEY_MAYBE_V1 -> KEY -> GARB_GARBTERM -> GARBAUTH -> VERSION -> APP -> APP_READY + * | + * \-------> V1 */ enum class RecvState : uint8_t { + /** (Responder only) either v2 public key or v1 header. + * + * This is the initial state for responders, before data has been received to distinguish + * v1 from v2 connections. When that happens, the state becomes either KEY (for v2) or V1 + * (for v1). */ + KEY_MAYBE_V1, + /** Public key. * - * This is the initial state, during which the other side's public key is + * This is the initial state for initiators, during which the other side's public key is * received. When that information arrives, the ciphers get initialized and the state * becomes GARB_GARBTERM. */ KEY, @@ -502,23 +517,40 @@ private: * Nothing can be received in this state. When the message is retrieved by GetMessage, * the state becomes APP again. */ APP_READY, + + /** Nothing (this transport is using v1 fallback). + * + * All receive operations are redirected to m_v1_fallback. */ + V1, }; /** State type that controls the sender side. * * Diagram: * - * start - * | - * v - * AWAITING_KEY -> READY + * start(responder) + * | + * | start(initiator) + * | | + * v v + * MAYBE_V1 -> AWAITING_KEY -> READY + * | + * \-----> V1 */ enum class SendState : uint8_t { + /** (Responder only) Not sending until v1 or v2 is detected. + * + * This is the initial state for responders. The send buffer contains the public key to + * send, but nothing is sent in this state yet. When the receiver determines whether this + * is a V1 or V2 connection, the sender state becomes AWAITING_KEY (for v2) or V1 (for v1). + */ + MAYBE_V1, + /** Waiting for the other side's public key. * - * This is the initial state. The public key is sent out. When the receiver receives the - * other side's public key and transitions to GARB_GARBTERM, the sender state becomes - * READY. */ + * This is the initial state for initiators. The public key is sent out. When the receiver + * receives the other side's public key and transitions to GARB_GARBTERM, the sender state + * becomes READY. */ AWAITING_KEY, /** Normal sending state. @@ -528,6 +560,11 @@ private: * appended to the send buffer (in addition to the key which may still be there). In this * state a message can be provided if the send buffer is empty. */ READY, + + /** This transport is using v1 fallback. + * + * All send operations are redirected to m_v1_fallback. */ + V1, }; /** Cipher state. */ @@ -536,6 +573,8 @@ private: const bool m_initiating; /** NodeId (for debug logging). */ const NodeId m_nodeid; + /** Encapsulate a V1Transport to fall back to. */ + V1Transport m_v1_fallback; /** Lock for receiver-side fields. */ mutable Mutex m_recv_mutex ACQUIRED_BEFORE(m_send_mutex); @@ -575,6 +614,8 @@ private: static std::optional<std::string> GetMessageType(Span<const uint8_t>& contents) noexcept; /** Determine how many received bytes can be processed in one go (not allowed in V1 state). */ size_t GetMaxBytesToProcess() noexcept EXCLUSIVE_LOCKS_REQUIRED(m_recv_mutex); + /** Process bytes in m_recv_buffer, while in KEY_MAYBE_V1 state. */ + void ProcessReceivedMaybeV1Bytes() noexcept EXCLUSIVE_LOCKS_REQUIRED(m_recv_mutex, !m_send_mutex); /** Process bytes in m_recv_buffer, while in KEY state. */ void ProcessReceivedKeyBytes() noexcept EXCLUSIVE_LOCKS_REQUIRED(m_recv_mutex, !m_send_mutex); /** Process bytes in m_recv_buffer, while in GARB_GARBTERM state. */ -- cgit v1.2.3 From 3ffa5fb49ee4a6d9502aa957093bd94058630282 Mon Sep 17 00:00:00 2001 From: Pieter Wuille <pieter@wuille.net> Date: Sun, 30 Jul 2023 11:43:10 -0400 Subject: net: make V2Transport send uniformly random number garbage bytes --- src/net.h | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'src/net.h') diff --git a/src/net.h b/src/net.h index 9194a677b1..90f5c4b388 100644 --- a/src/net.h +++ b/src/net.h @@ -556,9 +556,9 @@ private: /** Normal sending state. * * In this state, the ciphers are initialized, so packets can be sent. When this state is - * entered, the garbage terminator, garbage authentication packet, and version packet are - * appended to the send buffer (in addition to the key which may still be there). In this - * state a message can be provided if the send buffer is empty. */ + * entered, the garbage, garbage terminator, garbage authentication packet, and version + * packet are appended to the send buffer (in addition to the key which may still be + * there). In this state a message can be provided if the send buffer is empty. */ READY, /** This transport is using v1 fallback. @@ -635,8 +635,8 @@ public: */ V2Transport(NodeId nodeid, bool initiating, int type_in, int version_in) noexcept; - /** Construct a V2 transport with specified keys (test use only). */ - V2Transport(NodeId nodeid, bool initiating, int type_in, int version_in, const CKey& key, Span<const std::byte> ent32) noexcept; + /** Construct a V2 transport with specified keys and garbage (test use only). */ + V2Transport(NodeId nodeid, bool initiating, int type_in, int version_in, const CKey& key, Span<const std::byte> ent32, Span<const uint8_t> garbage) noexcept; // Receive side functions. bool ReceivedMessageComplete() const noexcept override EXCLUSIVE_LOCKS_REQUIRED(!m_recv_mutex); -- cgit v1.2.3 From db9888feec48c6220a2fcf92865503bbbdab02a4 Mon Sep 17 00:00:00 2001 From: Pieter Wuille <pieter@wuille.net> Date: Tue, 5 Sep 2023 23:38:15 -0400 Subject: net: detect wrong-network V1 talking to V2Transport --- src/net.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/net.h') diff --git a/src/net.h b/src/net.h index 90f5c4b388..cf7a240202 100644 --- a/src/net.h +++ b/src/net.h @@ -617,7 +617,7 @@ private: /** Process bytes in m_recv_buffer, while in KEY_MAYBE_V1 state. */ void ProcessReceivedMaybeV1Bytes() noexcept EXCLUSIVE_LOCKS_REQUIRED(m_recv_mutex, !m_send_mutex); /** Process bytes in m_recv_buffer, while in KEY state. */ - void ProcessReceivedKeyBytes() noexcept EXCLUSIVE_LOCKS_REQUIRED(m_recv_mutex, !m_send_mutex); + bool ProcessReceivedKeyBytes() noexcept EXCLUSIVE_LOCKS_REQUIRED(m_recv_mutex, !m_send_mutex); /** Process bytes in m_recv_buffer, while in GARB_GARBTERM state. */ bool ProcessReceivedGarbageBytes() noexcept EXCLUSIVE_LOCKS_REQUIRED(m_recv_mutex); /** Process bytes in m_recv_buffer, while in GARBAUTH/VERSION/APP state. */ -- cgit v1.2.3