From c3fad1f29df093e8fd03d70eb43f25ee9d531bf7 Mon Sep 17 00:00:00 2001 From: Pieter Wuille 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.cpp | 43 ++++++++++++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 15 deletions(-) (limited to 'src/net.cpp') diff --git a/src/net.cpp b/src/net.cpp index 4addca0982..eaa99e6601 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -867,20 +867,22 @@ bool V1Transport::SetMessageToSend(CSerializedNetMsg& msg) noexcept return true; } -Transport::BytesToSend V1Transport::GetBytesToSend() const noexcept +Transport::BytesToSend V1Transport::GetBytesToSend(bool have_next_message) const noexcept { AssertLockNotHeld(m_send_mutex); LOCK(m_send_mutex); if (m_sending_header) { return {Span{m_header_to_send}.subspan(m_bytes_sent), - // We have more to send after the header if the message has payload. - !m_message_to_send.data.empty(), + // We have more to send after the header if the message has payload, or if there + // is a next message after that. + have_next_message || !m_message_to_send.data.empty(), m_message_to_send.m_type }; } else { return {Span{m_message_to_send.data}.subspan(m_bytes_sent), - // We never have more to send after this message's payload. - false, + // We only have more to send after this message's payload if there is another + // message. + have_next_message, m_message_to_send.m_type }; } @@ -916,6 +918,7 @@ std::pair CConnman::SocketSendData(CNode& node) const auto it = node.vSendMsg.begin(); size_t nSentSize = 0; bool data_left{false}; //!< second return value (whether unsent data remains) + std::optional expected_more; while (true) { if (it != node.vSendMsg.end()) { @@ -928,7 +931,12 @@ std::pair CConnman::SocketSendData(CNode& node) const ++it; } } - const auto& [data, more, msg_type] = node.m_transport->GetBytesToSend(); + const auto& [data, more, msg_type] = node.m_transport->GetBytesToSend(it != node.vSendMsg.end()); + // We rely on the 'more' value returned by GetBytesToSend to correctly predict whether more + // bytes are still to be sent, to correctly set the MSG_MORE flag. As a sanity check, + // verify that the previously returned 'more' was correct. + if (expected_more.has_value()) Assume(!data.empty() == *expected_more); + expected_more = more; data_left = !data.empty(); // will be overwritten on next loop if all of data gets sent int nBytes = 0; if (!data.empty()) { @@ -941,9 +949,7 @@ std::pair CConnman::SocketSendData(CNode& node) const } int flags = MSG_NOSIGNAL | MSG_DONTWAIT; #ifdef MSG_MORE - // We have more to send if either the transport itself has more, or if we have more - // messages to send. - if (more || it != node.vSendMsg.end()) { + if (more) { flags |= MSG_MORE; } #endif @@ -1323,9 +1329,10 @@ Sock::EventsPerSock CConnman::GenerateWaitSockets(Span nodes) { LOCK(pnode->cs_vSend); // Sending is possible if either there are bytes to send right now, or if there will be - // once a potential message from vSendMsg is handed to the transport. - const auto& [to_send, _more, _msg_type] = pnode->m_transport->GetBytesToSend(); - select_send = !to_send.empty() || !pnode->vSendMsg.empty(); + // once a potential message from vSendMsg is handed to the transport. GetBytesToSend + // determines both of these in a single call. + const auto& [to_send, more, _msg_type] = pnode->m_transport->GetBytesToSend(!pnode->vSendMsg.empty()); + select_send = !to_send.empty() || more; } if (!select_recv && !select_send) continue; @@ -3007,7 +3014,10 @@ void CConnman::PushMessage(CNode* pnode, CSerializedNetMsg&& msg) size_t nBytesSent = 0; { LOCK(pnode->cs_vSend); - const auto& [to_send, _more, _msg_type] = pnode->m_transport->GetBytesToSend(); + // Check if the transport still has unsent bytes, and indicate to it that we're about to + // give it a message to send. + const auto& [to_send, more, _msg_type] = + pnode->m_transport->GetBytesToSend(/*have_next_message=*/true); const bool queue_was_empty{to_send.empty() && pnode->vSendMsg.empty()}; // Update memory usage of send buffer. @@ -3016,10 +3026,13 @@ void CConnman::PushMessage(CNode* pnode, CSerializedNetMsg&& msg) // Move message to vSendMsg queue. pnode->vSendMsg.push_back(std::move(msg)); - // If there was nothing to send before, attempt "optimistic write": + // If there was nothing to send before, and there is now (predicted by the "more" value + // returned by the GetBytesToSend call above), attempt "optimistic write": // because the poll/select loop may pause for SELECT_TIMEOUT_MILLISECONDS before actually // doing a send, try sending from the calling thread if the queue was empty before. - if (queue_was_empty) { + // With a V1Transport, more will always be true here, because adding a message always + // results in sendable bytes there. + if (queue_was_empty && more) { std::tie(nBytesSent, std::ignore) = SocketSendData(*pnode); } } -- cgit v1.2.3 From 13a7f01557272db652b3f333af3f06af6897253f Mon Sep 17 00:00:00 2001 From: Pieter Wuille 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.cpp | 427 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 425 insertions(+), 2 deletions(-) (limited to 'src/net.cpp') diff --git a/src/net.cpp b/src/net.cpp index eaa99e6601..f5425bf50e 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -913,6 +913,427 @@ size_t V1Transport::GetSendMemoryUsage() const noexcept return m_message_to_send.GetMemoryUsage(); } +V2Transport::V2Transport(NodeId nodeid, bool initiating, int type_in, int version_in) noexcept : + m_cipher{}, m_initiating{initiating}, m_nodeid{nodeid}, + m_recv_type{type_in}, m_recv_version{version_in}, + m_recv_state{RecvState::KEY}, + m_send_state{SendState::AWAITING_KEY} +{ + // Initialize the send buffer with ellswift pubkey. + m_send_buffer.resize(EllSwiftPubKey::size()); + std::copy(std::begin(m_cipher.GetOurPubKey()), std::end(m_cipher.GetOurPubKey()), MakeWritableByteSpan(m_send_buffer).begin()); +} + +V2Transport::V2Transport(NodeId nodeid, bool initiating, int type_in, int version_in, const CKey& key, Span ent32) noexcept : + m_cipher{key, ent32}, m_initiating{initiating}, m_nodeid{nodeid}, + m_recv_type{type_in}, m_recv_version{version_in}, + m_recv_state{RecvState::KEY}, + m_send_state{SendState::AWAITING_KEY} +{ + // Initialize the send buffer with ellswift pubkey. + m_send_buffer.resize(EllSwiftPubKey::size()); + std::copy(std::begin(m_cipher.GetOurPubKey()), std::end(m_cipher.GetOurPubKey()), MakeWritableByteSpan(m_send_buffer).begin()); +} + +void V2Transport::SetReceiveState(RecvState recv_state) noexcept +{ + AssertLockHeld(m_recv_mutex); + // Enforce allowed state transitions. + switch (m_recv_state) { + case RecvState::KEY: + Assume(recv_state == RecvState::GARB_GARBTERM); + break; + case RecvState::GARB_GARBTERM: + Assume(recv_state == RecvState::GARBAUTH); + break; + case RecvState::GARBAUTH: + Assume(recv_state == RecvState::VERSION); + break; + case RecvState::VERSION: + Assume(recv_state == RecvState::APP); + break; + case RecvState::APP: + Assume(recv_state == RecvState::APP_READY); + break; + case RecvState::APP_READY: + Assume(recv_state == RecvState::APP); + break; + } + // Change state. + m_recv_state = recv_state; +} + +void V2Transport::SetSendState(SendState send_state) noexcept +{ + AssertLockHeld(m_send_mutex); + // Enforce allowed state transitions. + switch (m_send_state) { + case SendState::AWAITING_KEY: + Assume(send_state == SendState::READY); + break; + case SendState::READY: + Assume(false); // Final state + break; + } + // Change state. + m_send_state = send_state; +} + +bool V2Transport::ReceivedMessageComplete() const noexcept +{ + AssertLockNotHeld(m_recv_mutex); + LOCK(m_recv_mutex); + return m_recv_state == RecvState::APP_READY; +} + +void V2Transport::ProcessReceivedKeyBytes() noexcept +{ + AssertLockHeld(m_recv_mutex); + AssertLockNotHeld(m_send_mutex); + Assume(m_recv_state == RecvState::KEY); + Assume(m_recv_buffer.size() <= EllSwiftPubKey::size()); + if (m_recv_buffer.size() == EllSwiftPubKey::size()) { + // Other side's key has been fully received, and can now be Diffie-Hellman combined with + // our key to initialize the encryption ciphers. + + // Initialize the ciphers. + EllSwiftPubKey ellswift(MakeByteSpan(m_recv_buffer)); + LOCK(m_send_mutex); + m_cipher.Initialize(ellswift, m_initiating); + + // Switch receiver state to GARB_GARBTERM. + SetReceiveState(RecvState::GARB_GARBTERM); + m_recv_buffer.clear(); + + // Switch sender state to READY. + SetSendState(SendState::READY); + + // Append the garbage terminator to the send buffer. + m_send_buffer.resize(m_send_buffer.size() + BIP324Cipher::GARBAGE_TERMINATOR_LEN); + std::copy(m_cipher.GetSendGarbageTerminator().begin(), + m_cipher.GetSendGarbageTerminator().end(), + MakeWritableByteSpan(m_send_buffer).last(BIP324Cipher::GARBAGE_TERMINATOR_LEN).begin()); + + // Construct garbage authentication packet in the send buffer. + m_send_buffer.resize(m_send_buffer.size() + BIP324Cipher::EXPANSION); + m_cipher.Encrypt( + /*contents=*/{}, + /*aad=*/{}, /* empty garbage for now */ + /*ignore=*/false, + /*output=*/MakeWritableByteSpan(m_send_buffer).last(BIP324Cipher::EXPANSION)); + + // Construct version packet in the send buffer. + m_send_buffer.resize(m_send_buffer.size() + BIP324Cipher::EXPANSION + VERSION_CONTENTS.size()); + m_cipher.Encrypt( + /*contents=*/VERSION_CONTENTS, + /*aad=*/{}, + /*ignore=*/false, + /*output=*/MakeWritableByteSpan(m_send_buffer).last(BIP324Cipher::EXPANSION + VERSION_CONTENTS.size())); + } else { + // We still have to receive more key bytes. + } +} + +bool V2Transport::ProcessReceivedGarbageBytes() noexcept +{ + AssertLockHeld(m_recv_mutex); + Assume(m_recv_state == RecvState::GARB_GARBTERM); + Assume(m_recv_buffer.size() <= MAX_GARBAGE_LEN + BIP324Cipher::GARBAGE_TERMINATOR_LEN); + if (m_recv_buffer.size() >= BIP324Cipher::GARBAGE_TERMINATOR_LEN) { + if (MakeByteSpan(m_recv_buffer).last(BIP324Cipher::GARBAGE_TERMINATOR_LEN) == m_cipher.GetReceiveGarbageTerminator()) { + // Garbage terminator received. Switch to receiving garbage authentication packet. + m_recv_garbage = std::move(m_recv_buffer); + m_recv_garbage.resize(m_recv_garbage.size() - BIP324Cipher::GARBAGE_TERMINATOR_LEN); + m_recv_buffer.clear(); + SetReceiveState(RecvState::GARBAUTH); + } else if (m_recv_buffer.size() == MAX_GARBAGE_LEN + BIP324Cipher::GARBAGE_TERMINATOR_LEN) { + // We've reached the maximum length for garbage + garbage terminator, and the + // terminator still does not match. Abort. + LogPrint(BCLog::NET, "V2 transport error: missing garbage terminator, peer=%d\n", m_nodeid); + return false; + } else { + // We still need to receive more garbage and/or garbage terminator bytes. + } + } else { + // We have less than GARBAGE_TERMINATOR_LEN (16) bytes, so we certainly need to receive + // more first. + } + return true; +} + +bool V2Transport::ProcessReceivedPacketBytes() noexcept +{ + AssertLockHeld(m_recv_mutex); + Assume(m_recv_state == RecvState::GARBAUTH || m_recv_state == RecvState::VERSION || + m_recv_state == RecvState::APP); + + // The maximum permitted contents length for a packet, consisting of: + // - 0x00 byte: indicating long message type encoding + // - 12 bytes of message type + // - payload + static constexpr size_t MAX_CONTENTS_LEN = + 1 + CMessageHeader::COMMAND_SIZE + + std::min(MAX_SIZE, MAX_PROTOCOL_MESSAGE_LENGTH); + + if (m_recv_buffer.size() == BIP324Cipher::LENGTH_LEN) { + // Length descriptor received. + m_recv_len = m_cipher.DecryptLength(MakeByteSpan(m_recv_buffer)); + if (m_recv_len > MAX_CONTENTS_LEN) { + LogPrint(BCLog::NET, "V2 transport error: packet too large (%u bytes), peer=%d\n", m_recv_len, m_nodeid); + return false; + } + } else if (m_recv_buffer.size() > BIP324Cipher::LENGTH_LEN && m_recv_buffer.size() == m_recv_len + BIP324Cipher::EXPANSION) { + // Ciphertext received, decrypt it into m_recv_decode_buffer. + // Note that it is impossible to reach this branch without hitting the branch above first, + // as GetMaxBytesToProcess only allows up to LENGTH_LEN into the buffer before that point. + m_recv_decode_buffer.resize(m_recv_len); + bool ignore{false}; + Span aad; + if (m_recv_state == RecvState::GARBAUTH) aad = MakeByteSpan(m_recv_garbage); + bool ret = m_cipher.Decrypt( + /*input=*/MakeByteSpan(m_recv_buffer).subspan(BIP324Cipher::LENGTH_LEN), + /*aad=*/aad, + /*ignore=*/ignore, + /*contents=*/MakeWritableByteSpan(m_recv_decode_buffer)); + if (!ret) { + LogPrint(BCLog::NET, "V2 transport error: packet decryption failure (%u bytes), peer=%d\n", m_recv_len, m_nodeid); + return false; + } + // Feed the last 4 bytes of the Poly1305 authentication tag (and its timing) into our RNG. + RandAddEvent(ReadLE32(m_recv_buffer.data() + m_recv_buffer.size() - 4)); + + // At this point we have a valid packet decrypted into m_recv_decode_buffer. Depending on + // the current state, decide what to do with it. + switch (m_recv_state) { + case RecvState::GARBAUTH: + // Ignore flag does not matter for garbage authentication. Any valid packet functions + // as authentication. Receive and process the version packet next. + SetReceiveState(RecvState::VERSION); + m_recv_garbage = {}; + break; + case RecvState::VERSION: + if (!ignore) { + // Version message received; transition to application phase. The contents is + // ignored, but can be used for future extensions. + SetReceiveState(RecvState::APP); + } + break; + case RecvState::APP: + if (!ignore) { + // Application message decrypted correctly. It can be extracted using GetMessage(). + SetReceiveState(RecvState::APP_READY); + } + break; + default: + // Any other state is invalid (this function should not have been called). + Assume(false); + } + // Wipe the receive buffer where the next packet will be received into. + m_recv_buffer = {}; + // In all but APP_READY state, we can wipe the decoded contents. + if (m_recv_state != RecvState::APP_READY) m_recv_decode_buffer = {}; + } else { + // We either have less than 3 bytes, so we don't know the packet's length yet, or more + // than 3 bytes but less than the packet's full ciphertext. Wait until those arrive. + } + return true; +} + +size_t V2Transport::GetMaxBytesToProcess() noexcept +{ + AssertLockHeld(m_recv_mutex); + switch (m_recv_state) { + case RecvState::KEY: + // During the KEY state, we only allow the 64-byte key into the receive buffer. + Assume(m_recv_buffer.size() <= EllSwiftPubKey::size()); + // As long as we have not received the other side's public key, don't receive more than + // that (64 bytes), as garbage follows, and locating the garbage terminator requires the + // key exchange first. + return EllSwiftPubKey::size() - m_recv_buffer.size(); + case RecvState::GARB_GARBTERM: + // Process garbage bytes one by one (because terminator may appear anywhere). + return 1; + case RecvState::GARBAUTH: + case RecvState::VERSION: + case RecvState::APP: + // These three states all involve decoding a packet. Process the length descriptor first, + // so that we know where the current packet ends (and we don't process bytes from the next + // packet or decoy yet). Then, process the ciphertext bytes of the current packet. + if (m_recv_buffer.size() < BIP324Cipher::LENGTH_LEN) { + return BIP324Cipher::LENGTH_LEN - m_recv_buffer.size(); + } else { + // Note that BIP324Cipher::EXPANSION is the total difference between contents size + // and encoded packet size, which includes the 3 bytes due to the packet length. + // When transitioning from receiving the packet length to receiving its ciphertext, + // the encrypted packet length is left in the receive buffer. + return BIP324Cipher::EXPANSION + m_recv_len - m_recv_buffer.size(); + } + case RecvState::APP_READY: + // No bytes can be processed until GetMessage() is called. + return 0; + } + Assume(false); // unreachable + return 0; +} + +bool V2Transport::ReceivedBytes(Span& msg_bytes) noexcept +{ + AssertLockNotHeld(m_recv_mutex); + LOCK(m_recv_mutex); + // Process the provided bytes in msg_bytes in a loop. In each iteration a nonzero number of + // bytes (decided by GetMaxBytesToProcess) are taken from the beginning om msg_bytes, and + // appended to m_recv_buffer. Then, depending on the receiver state, one of the + // ProcessReceived*Bytes functions is called to process the bytes in that buffer. + while (!msg_bytes.empty()) { + // Decide how many bytes to copy from msg_bytes to m_recv_buffer. + size_t max_read = GetMaxBytesToProcess(); + // Can't read more than provided input. + max_read = std::min(msg_bytes.size(), max_read); + // Copy data to buffer. + m_recv_buffer.insert(m_recv_buffer.end(), UCharCast(msg_bytes.data()), UCharCast(msg_bytes.data() + max_read)); + msg_bytes = msg_bytes.subspan(max_read); + + // Process data in the buffer. + switch (m_recv_state) { + case RecvState::KEY: + ProcessReceivedKeyBytes(); + break; + + case RecvState::GARB_GARBTERM: + if (!ProcessReceivedGarbageBytes()) return false; + break; + + case RecvState::GARBAUTH: + case RecvState::VERSION: + case RecvState::APP: + if (!ProcessReceivedPacketBytes()) return false; + break; + + case RecvState::APP_READY: + return true; + } + // Make sure we have made progress before continuing. + Assume(max_read > 0); + } + + return true; +} + +std::optional V2Transport::GetMessageType(Span& contents) noexcept +{ + if (contents.size() == 0) return std::nullopt; // Empty contents + uint8_t first_byte = contents[0]; + contents = contents.subspan(1); // Strip first byte. + + if (first_byte != 0) return std::nullopt; // TODO: implement short encoding + + if (contents.size() < CMessageHeader::COMMAND_SIZE) { + return std::nullopt; // Long encoding needs 12 message type bytes. + } + + size_t msg_type_len{0}; + while (msg_type_len < CMessageHeader::COMMAND_SIZE && contents[msg_type_len] != 0) { + // Verify that message type bytes before the first 0x00 are in range. + if (contents[msg_type_len] < ' ' || contents[msg_type_len] > 0x7F) { + return {}; + } + ++msg_type_len; + } + std::string ret{reinterpret_cast(contents.data()), msg_type_len}; + while (msg_type_len < CMessageHeader::COMMAND_SIZE) { + // Verify that message type bytes after the first 0x00 are also 0x00. + if (contents[msg_type_len] != 0) return {}; + ++msg_type_len; + } + // Strip message type bytes of contents. + contents = contents.subspan(CMessageHeader::COMMAND_SIZE); + return {std::move(ret)}; +} + +CNetMessage V2Transport::GetReceivedMessage(std::chrono::microseconds time, bool& reject_message) noexcept +{ + AssertLockNotHeld(m_recv_mutex); + LOCK(m_recv_mutex); + Assume(m_recv_state == RecvState::APP_READY); + Span contents{m_recv_decode_buffer}; + auto msg_type = GetMessageType(contents); + CDataStream ret(m_recv_type, m_recv_version); + CNetMessage msg{std::move(ret)}; + // Note that BIP324Cipher::EXPANSION also includes the length descriptor size. + msg.m_raw_message_size = m_recv_decode_buffer.size() + BIP324Cipher::EXPANSION; + if (msg_type) { + reject_message = false; + msg.m_type = std::move(*msg_type); + msg.m_time = time; + msg.m_message_size = contents.size(); + msg.m_recv.resize(contents.size()); + std::copy(contents.begin(), contents.end(), UCharCast(msg.m_recv.data())); + } else { + LogPrint(BCLog::NET, "V2 transport error: invalid message type (%u bytes contents), peer=%d\n", m_recv_decode_buffer.size(), m_nodeid); + reject_message = true; + } + m_recv_decode_buffer = {}; + SetReceiveState(RecvState::APP); + + return msg; +} + +bool V2Transport::SetMessageToSend(CSerializedNetMsg& msg) noexcept +{ + AssertLockNotHeld(m_send_mutex); + LOCK(m_send_mutex); + // We only allow adding a new message to be sent when in the READY state (so the packet cipher + // is available) and the send buffer is empty. This limits the number of messages in the send + // buffer to just one, and leaves the responsibility for queueing them up to the caller. + if (!(m_send_state == SendState::READY && m_send_buffer.empty())) return false; + // Construct contents (encoding message type + payload). + // Initialize with zeroes, and then write the message type string starting at offset 1. + // This means contents[0] and the unused positions in contents[1..13] remain 0x00. + std::vector contents(1 + CMessageHeader::COMMAND_SIZE + msg.data.size(), 0); + std::copy(msg.m_type.begin(), msg.m_type.end(), contents.data() + 1); + std::copy(msg.data.begin(), msg.data.end(), contents.begin() + 1 + CMessageHeader::COMMAND_SIZE); + // Construct ciphertext in send buffer. + m_send_buffer.resize(contents.size() + BIP324Cipher::EXPANSION); + m_cipher.Encrypt(MakeByteSpan(contents), {}, false, MakeWritableByteSpan(m_send_buffer)); + m_send_type = msg.m_type; + // Release memory + msg.data = {}; + return true; +} + +Transport::BytesToSend V2Transport::GetBytesToSend(bool have_next_message) const noexcept +{ + AssertLockNotHeld(m_send_mutex); + LOCK(m_send_mutex); + Assume(m_send_pos <= m_send_buffer.size()); + return { + Span{m_send_buffer}.subspan(m_send_pos), + // We only have more to send after the current m_send_buffer if there is a (next) + // message to be sent, and we're capable of sending packets. */ + have_next_message && m_send_state == SendState::READY, + m_send_type + }; +} + +void V2Transport::MarkBytesSent(size_t bytes_sent) noexcept +{ + AssertLockNotHeld(m_send_mutex); + LOCK(m_send_mutex); + m_send_pos += bytes_sent; + Assume(m_send_pos <= m_send_buffer.size()); + if (m_send_pos == m_send_buffer.size()) { + m_send_pos = 0; + m_send_buffer = {}; + } +} + +size_t V2Transport::GetSendMemoryUsage() const noexcept +{ + AssertLockNotHeld(m_send_mutex); + LOCK(m_send_mutex); + return sizeof(m_send_buffer) + memusage::DynamicUsage(m_send_buffer); +} + std::pair CConnman::SocketSendData(CNode& node) const { auto it = node.vSendMsg.begin(); @@ -923,7 +1344,8 @@ std::pair CConnman::SocketSendData(CNode& node) const while (true) { if (it != node.vSendMsg.end()) { // If possible, move one message from the send queue to the transport. This fails when - // there is an existing message still being sent. + // there is an existing message still being sent, or (for v2 transports) when the + // handshake has not yet completed. size_t memusage = it->GetMemoryUsage(); if (node.m_transport->SetMessageToSend(*it)) { // Update memory usage of send buffer (as *it will be deleted). @@ -3031,7 +3453,8 @@ void CConnman::PushMessage(CNode* pnode, CSerializedNetMsg&& msg) // because the poll/select loop may pause for SELECT_TIMEOUT_MILLISECONDS before actually // doing a send, try sending from the calling thread if the queue was empty before. // With a V1Transport, more will always be true here, because adding a message always - // results in sendable bytes there. + // results in sendable bytes there, but with V2Transport this is not the case (it may + // still be in the handshake). if (queue_was_empty && more) { std::tie(nBytesSent, std::ignore) = SocketSendData(*pnode); } -- cgit v1.2.3 From 8da8642062fa2c7aa2f49995b832c3d0897e37ed Mon Sep 17 00:00:00 2001 From: Pieter Wuille Date: Tue, 29 Aug 2023 22:37:18 -0400 Subject: net: make V2Transport auto-detect incoming V1 and fall back to it --- src/net.cpp | 100 +++++++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 93 insertions(+), 7 deletions(-) (limited to 'src/net.cpp') diff --git a/src/net.cpp b/src/net.cpp index f5425bf50e..dbdbbf9d4e 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -915,9 +915,9 @@ size_t V1Transport::GetSendMemoryUsage() const noexcept V2Transport::V2Transport(NodeId nodeid, bool initiating, int type_in, int version_in) noexcept : m_cipher{}, m_initiating{initiating}, m_nodeid{nodeid}, - m_recv_type{type_in}, m_recv_version{version_in}, - m_recv_state{RecvState::KEY}, - m_send_state{SendState::AWAITING_KEY} + m_v1_fallback{nodeid, type_in, version_in}, m_recv_type{type_in}, m_recv_version{version_in}, + m_recv_state{initiating ? RecvState::KEY : RecvState::KEY_MAYBE_V1}, + m_send_state{initiating ? SendState::AWAITING_KEY : SendState::MAYBE_V1} { // Initialize the send buffer with ellswift pubkey. m_send_buffer.resize(EllSwiftPubKey::size()); @@ -926,9 +926,9 @@ V2Transport::V2Transport(NodeId nodeid, bool initiating, int type_in, int versio V2Transport::V2Transport(NodeId nodeid, bool initiating, int type_in, int version_in, const CKey& key, Span ent32) noexcept : m_cipher{key, ent32}, m_initiating{initiating}, m_nodeid{nodeid}, - m_recv_type{type_in}, m_recv_version{version_in}, - m_recv_state{RecvState::KEY}, - m_send_state{SendState::AWAITING_KEY} + m_v1_fallback{nodeid, type_in, version_in}, m_recv_type{type_in}, m_recv_version{version_in}, + m_recv_state{initiating ? RecvState::KEY : RecvState::KEY_MAYBE_V1}, + m_send_state{initiating ? SendState::AWAITING_KEY : SendState::MAYBE_V1} { // Initialize the send buffer with ellswift pubkey. m_send_buffer.resize(EllSwiftPubKey::size()); @@ -940,6 +940,9 @@ void V2Transport::SetReceiveState(RecvState recv_state) noexcept AssertLockHeld(m_recv_mutex); // Enforce allowed state transitions. switch (m_recv_state) { + case RecvState::KEY_MAYBE_V1: + Assume(recv_state == RecvState::KEY || recv_state == RecvState::V1); + break; case RecvState::KEY: Assume(recv_state == RecvState::GARB_GARBTERM); break; @@ -958,6 +961,9 @@ void V2Transport::SetReceiveState(RecvState recv_state) noexcept case RecvState::APP_READY: Assume(recv_state == RecvState::APP); break; + case RecvState::V1: + Assume(false); // V1 state cannot be left + break; } // Change state. m_recv_state = recv_state; @@ -968,11 +974,15 @@ void V2Transport::SetSendState(SendState send_state) noexcept AssertLockHeld(m_send_mutex); // Enforce allowed state transitions. switch (m_send_state) { + case SendState::MAYBE_V1: + Assume(send_state == SendState::V1 || send_state == SendState::AWAITING_KEY); + break; case SendState::AWAITING_KEY: Assume(send_state == SendState::READY); break; case SendState::READY: - Assume(false); // Final state + case SendState::V1: + Assume(false); // Final states break; } // Change state. @@ -983,9 +993,48 @@ bool V2Transport::ReceivedMessageComplete() const noexcept { AssertLockNotHeld(m_recv_mutex); LOCK(m_recv_mutex); + if (m_recv_state == RecvState::V1) return m_v1_fallback.ReceivedMessageComplete(); + return m_recv_state == RecvState::APP_READY; } +void V2Transport::ProcessReceivedMaybeV1Bytes() noexcept +{ + AssertLockHeld(m_recv_mutex); + AssertLockNotHeld(m_send_mutex); + Assume(m_recv_state == RecvState::KEY_MAYBE_V1); + // We still have to determine if this is a v1 or v2 connection. The bytes being received could + // be the beginning of either a v1 packet (network magic + "version\x00"), or of a v2 public + // key. BIP324 specifies that a mismatch with this 12-byte string should trigger sending of the + // key. + std::array v1_prefix = {0, 0, 0, 0, 'v', 'e', 'r', 's', 'i', 'o', 'n', 0}; + std::copy(std::begin(Params().MessageStart()), std::end(Params().MessageStart()), v1_prefix.begin()); + Assume(m_recv_buffer.size() <= v1_prefix.size()); + if (!std::equal(m_recv_buffer.begin(), m_recv_buffer.end(), v1_prefix.begin())) { + // Mismatch with v1 prefix, so we can assume a v2 connection. + SetReceiveState(RecvState::KEY); // Convert to KEY state, leaving received bytes around. + // Transition the sender to AWAITING_KEY state (if not already). + LOCK(m_send_mutex); + SetSendState(SendState::AWAITING_KEY); + } else if (m_recv_buffer.size() == v1_prefix.size()) { + // Full match with the v1 prefix, so fall back to v1 behavior. + LOCK(m_send_mutex); + Span feedback{m_recv_buffer}; + // Feed already received bytes to v1 transport. It should always accept these, because it's + // less than the size of a v1 header, and these are the first bytes fed to m_v1_fallback. + bool ret = m_v1_fallback.ReceivedBytes(feedback); + Assume(feedback.empty()); + Assume(ret); + SetReceiveState(RecvState::V1); + SetSendState(SendState::V1); + // Reset v2 transport buffers to save memory. + m_recv_buffer = {}; + m_send_buffer = {}; + } else { + // We have not received enough to distinguish v1 from v2 yet. Wait until more bytes come. + } +} + void V2Transport::ProcessReceivedKeyBytes() noexcept { AssertLockHeld(m_recv_mutex); @@ -1143,6 +1192,15 @@ size_t V2Transport::GetMaxBytesToProcess() noexcept { AssertLockHeld(m_recv_mutex); switch (m_recv_state) { + case RecvState::KEY_MAYBE_V1: + // During the KEY_MAYBE_V1 state we do not allow more than the length of v1 prefix into the + // receive buffer. + Assume(m_recv_buffer.size() <= V1_PREFIX_LEN); + // As long as we're not sure if this is a v1 or v2 connection, don't receive more than what + // is strictly necessary to distinguish the two (12 bytes). If we permitted more than + // the v1 header size (24 bytes), we may not be able to feed the already-received bytes + // back into the m_v1_fallback V1 transport. + return V1_PREFIX_LEN - m_recv_buffer.size(); case RecvState::KEY: // During the KEY state, we only allow the 64-byte key into the receive buffer. Assume(m_recv_buffer.size() <= EllSwiftPubKey::size()); @@ -1171,6 +1229,10 @@ size_t V2Transport::GetMaxBytesToProcess() noexcept case RecvState::APP_READY: // No bytes can be processed until GetMessage() is called. return 0; + case RecvState::V1: + // Not allowed (must be dealt with by the caller). + Assume(false); + return 0; } Assume(false); // unreachable return 0; @@ -1180,6 +1242,8 @@ bool V2Transport::ReceivedBytes(Span& msg_bytes) noexcept { AssertLockNotHeld(m_recv_mutex); LOCK(m_recv_mutex); + if (m_recv_state == RecvState::V1) return m_v1_fallback.ReceivedBytes(msg_bytes); + // Process the provided bytes in msg_bytes in a loop. In each iteration a nonzero number of // bytes (decided by GetMaxBytesToProcess) are taken from the beginning om msg_bytes, and // appended to m_recv_buffer. Then, depending on the receiver state, one of the @@ -1195,6 +1259,11 @@ bool V2Transport::ReceivedBytes(Span& msg_bytes) noexcept // Process data in the buffer. switch (m_recv_state) { + case RecvState::KEY_MAYBE_V1: + ProcessReceivedMaybeV1Bytes(); + if (m_recv_state == RecvState::V1) return true; + break; + case RecvState::KEY: ProcessReceivedKeyBytes(); break; @@ -1211,6 +1280,11 @@ bool V2Transport::ReceivedBytes(Span& msg_bytes) noexcept case RecvState::APP_READY: return true; + + case RecvState::V1: + // We should have bailed out before. + Assume(false); + break; } // Make sure we have made progress before continuing. Assume(max_read > 0); @@ -1254,6 +1328,8 @@ CNetMessage V2Transport::GetReceivedMessage(std::chrono::microseconds time, bool { AssertLockNotHeld(m_recv_mutex); LOCK(m_recv_mutex); + if (m_recv_state == RecvState::V1) return m_v1_fallback.GetReceivedMessage(time, reject_message); + Assume(m_recv_state == RecvState::APP_READY); Span contents{m_recv_decode_buffer}; auto msg_type = GetMessageType(contents); @@ -1282,6 +1358,7 @@ bool V2Transport::SetMessageToSend(CSerializedNetMsg& msg) noexcept { AssertLockNotHeld(m_send_mutex); LOCK(m_send_mutex); + if (m_send_state == SendState::V1) return m_v1_fallback.SetMessageToSend(msg); // We only allow adding a new message to be sent when in the READY state (so the packet cipher // is available) and the send buffer is empty. This limits the number of messages in the send // buffer to just one, and leaves the responsibility for queueing them up to the caller. @@ -1305,6 +1382,11 @@ Transport::BytesToSend V2Transport::GetBytesToSend(bool have_next_message) const { AssertLockNotHeld(m_send_mutex); LOCK(m_send_mutex); + if (m_send_state == SendState::V1) return m_v1_fallback.GetBytesToSend(have_next_message); + + // We do not send anything in MAYBE_V1 state (as we don't know if the peer is v1 or v2), + // despite there being data in the send buffer in that state. + if (m_send_state == SendState::MAYBE_V1) return {{}, false, m_send_type}; Assume(m_send_pos <= m_send_buffer.size()); return { Span{m_send_buffer}.subspan(m_send_pos), @@ -1319,6 +1401,8 @@ void V2Transport::MarkBytesSent(size_t bytes_sent) noexcept { AssertLockNotHeld(m_send_mutex); LOCK(m_send_mutex); + if (m_send_state == SendState::V1) return m_v1_fallback.MarkBytesSent(bytes_sent); + m_send_pos += bytes_sent; Assume(m_send_pos <= m_send_buffer.size()); if (m_send_pos == m_send_buffer.size()) { @@ -1331,6 +1415,8 @@ size_t V2Transport::GetSendMemoryUsage() const noexcept { AssertLockNotHeld(m_send_mutex); LOCK(m_send_mutex); + if (m_send_state == SendState::V1) return m_v1_fallback.GetSendMemoryUsage(); + return sizeof(m_send_buffer) + memusage::DynamicUsage(m_send_buffer); } -- cgit v1.2.3 From 0be752d9f8ca27320bc3e82498c7640fabd7e8de Mon Sep 17 00:00:00 2001 From: Pieter Wuille Date: Sun, 30 Jul 2023 21:25:10 -0400 Subject: net: add short message encoding/decoding support to V2Transport --- src/net.cpp | 97 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 91 insertions(+), 6 deletions(-) (limited to 'src/net.cpp') diff --git a/src/net.cpp b/src/net.cpp index dbdbbf9d4e..25f390961b 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -913,6 +913,74 @@ size_t V1Transport::GetSendMemoryUsage() const noexcept return m_message_to_send.GetMemoryUsage(); } +namespace { + +/** List of short messages as defined in BIP324, in order. + * + * Only message types that are actually implemented in this codebase need to be listed, as other + * messages get ignored anyway - whether we know how to decode them or not. + */ +const std::array V2_MESSAGE_IDS = { + "", // 12 bytes follow encoding the message type like in V1 + NetMsgType::ADDR, + NetMsgType::BLOCK, + NetMsgType::BLOCKTXN, + NetMsgType::CMPCTBLOCK, + NetMsgType::FEEFILTER, + NetMsgType::FILTERADD, + NetMsgType::FILTERCLEAR, + NetMsgType::FILTERLOAD, + NetMsgType::GETBLOCKS, + NetMsgType::GETBLOCKTXN, + NetMsgType::GETDATA, + NetMsgType::GETHEADERS, + NetMsgType::HEADERS, + NetMsgType::INV, + NetMsgType::MEMPOOL, + NetMsgType::MERKLEBLOCK, + NetMsgType::NOTFOUND, + NetMsgType::PING, + NetMsgType::PONG, + NetMsgType::SENDCMPCT, + NetMsgType::TX, + NetMsgType::GETCFILTERS, + NetMsgType::CFILTER, + NetMsgType::GETCFHEADERS, + NetMsgType::CFHEADERS, + NetMsgType::GETCFCHECKPT, + NetMsgType::CFCHECKPT, + NetMsgType::ADDRV2, + // Unimplemented message types that are assigned in BIP324: + "", + "", + "", + "" +}; + +class V2MessageMap +{ + std::unordered_map m_map; + +public: + V2MessageMap() noexcept + { + for (size_t i = 1; i < std::size(V2_MESSAGE_IDS); ++i) { + m_map.emplace(V2_MESSAGE_IDS[i], i); + } + } + + std::optional operator()(const std::string& message_name) const noexcept + { + auto it = m_map.find(message_name); + if (it == m_map.end()) return std::nullopt; + return it->second; + } +}; + +const V2MessageMap V2_MESSAGE_MAP; + +} // namespace + V2Transport::V2Transport(NodeId nodeid, bool initiating, int type_in, int version_in) noexcept : m_cipher{}, m_initiating{initiating}, m_nodeid{nodeid}, m_v1_fallback{nodeid, type_in, version_in}, m_recv_type{type_in}, m_recv_version{version_in}, @@ -1299,7 +1367,16 @@ std::optional V2Transport::GetMessageType(Span& cont uint8_t first_byte = contents[0]; contents = contents.subspan(1); // Strip first byte. - if (first_byte != 0) return std::nullopt; // TODO: implement short encoding + if (first_byte != 0) { + // Short (1 byte) encoding. + if (first_byte < std::size(V2_MESSAGE_IDS)) { + // Valid short message id. + return V2_MESSAGE_IDS[first_byte]; + } else { + // Unknown short message id. + return std::nullopt; + } + } if (contents.size() < CMessageHeader::COMMAND_SIZE) { return std::nullopt; // Long encoding needs 12 message type bytes. @@ -1364,11 +1441,19 @@ bool V2Transport::SetMessageToSend(CSerializedNetMsg& msg) noexcept // buffer to just one, and leaves the responsibility for queueing them up to the caller. if (!(m_send_state == SendState::READY && m_send_buffer.empty())) return false; // Construct contents (encoding message type + payload). - // Initialize with zeroes, and then write the message type string starting at offset 1. - // This means contents[0] and the unused positions in contents[1..13] remain 0x00. - std::vector contents(1 + CMessageHeader::COMMAND_SIZE + msg.data.size(), 0); - std::copy(msg.m_type.begin(), msg.m_type.end(), contents.data() + 1); - std::copy(msg.data.begin(), msg.data.end(), contents.begin() + 1 + CMessageHeader::COMMAND_SIZE); + std::vector contents; + auto short_message_id = V2_MESSAGE_MAP(msg.m_type); + if (short_message_id) { + contents.resize(1 + msg.data.size()); + contents[0] = *short_message_id; + std::copy(msg.data.begin(), msg.data.end(), contents.begin() + 1); + } else { + // Initialize with zeroes, and then write the message type string starting at offset 1. + // This means contents[0] and the unused positions in contents[1..13] remain 0x00. + contents.resize(1 + CMessageHeader::COMMAND_SIZE + msg.data.size(), 0); + std::copy(msg.m_type.begin(), msg.m_type.end(), contents.data() + 1); + std::copy(msg.data.begin(), msg.data.end(), contents.begin() + 1 + CMessageHeader::COMMAND_SIZE); + } // Construct ciphertext in send buffer. m_send_buffer.resize(contents.size() + BIP324Cipher::EXPANSION); m_cipher.Encrypt(MakeByteSpan(contents), {}, false, MakeWritableByteSpan(m_send_buffer)); -- cgit v1.2.3 From 3ffa5fb49ee4a6d9502aa957093bd94058630282 Mon Sep 17 00:00:00 2001 From: Pieter Wuille Date: Sun, 30 Jul 2023 11:43:10 -0400 Subject: net: make V2Transport send uniformly random number garbage bytes --- src/net.cpp | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) (limited to 'src/net.cpp') diff --git a/src/net.cpp b/src/net.cpp index 25f390961b..e75089c8e9 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -987,20 +987,26 @@ V2Transport::V2Transport(NodeId nodeid, bool initiating, int type_in, int versio m_recv_state{initiating ? RecvState::KEY : RecvState::KEY_MAYBE_V1}, m_send_state{initiating ? SendState::AWAITING_KEY : SendState::MAYBE_V1} { - // Initialize the send buffer with ellswift pubkey. - m_send_buffer.resize(EllSwiftPubKey::size()); + // Construct garbage (including its length) using a FastRandomContext. + FastRandomContext rng; + size_t garbage_len = rng.randrange(MAX_GARBAGE_LEN + 1); + // Initialize the send buffer with ellswift pubkey + garbage. + m_send_buffer.resize(EllSwiftPubKey::size() + garbage_len); std::copy(std::begin(m_cipher.GetOurPubKey()), std::end(m_cipher.GetOurPubKey()), MakeWritableByteSpan(m_send_buffer).begin()); + rng.fillrand(MakeWritableByteSpan(m_send_buffer).subspan(EllSwiftPubKey::size())); } -V2Transport::V2Transport(NodeId nodeid, bool initiating, int type_in, int version_in, const CKey& key, Span ent32) noexcept : +V2Transport::V2Transport(NodeId nodeid, bool initiating, int type_in, int version_in, const CKey& key, Span ent32, Span garbage) noexcept : m_cipher{key, ent32}, m_initiating{initiating}, m_nodeid{nodeid}, m_v1_fallback{nodeid, type_in, version_in}, m_recv_type{type_in}, m_recv_version{version_in}, m_recv_state{initiating ? RecvState::KEY : RecvState::KEY_MAYBE_V1}, m_send_state{initiating ? SendState::AWAITING_KEY : SendState::MAYBE_V1} { - // Initialize the send buffer with ellswift pubkey. - m_send_buffer.resize(EllSwiftPubKey::size()); + assert(garbage.size() <= MAX_GARBAGE_LEN); + // Initialize the send buffer with ellswift pubkey + provided garbage. + m_send_buffer.resize(EllSwiftPubKey::size() + garbage.size()); std::copy(std::begin(m_cipher.GetOurPubKey()), std::end(m_cipher.GetOurPubKey()), MakeWritableByteSpan(m_send_buffer).begin()); + std::copy(garbage.begin(), garbage.end(), m_send_buffer.begin() + EllSwiftPubKey::size()); } void V2Transport::SetReceiveState(RecvState recv_state) noexcept @@ -1126,16 +1132,18 @@ void V2Transport::ProcessReceivedKeyBytes() noexcept SetSendState(SendState::READY); // Append the garbage terminator to the send buffer. + size_t garbage_len = m_send_buffer.size() - EllSwiftPubKey::size(); m_send_buffer.resize(m_send_buffer.size() + BIP324Cipher::GARBAGE_TERMINATOR_LEN); std::copy(m_cipher.GetSendGarbageTerminator().begin(), m_cipher.GetSendGarbageTerminator().end(), MakeWritableByteSpan(m_send_buffer).last(BIP324Cipher::GARBAGE_TERMINATOR_LEN).begin()); - // Construct garbage authentication packet in the send buffer. + // Construct garbage authentication packet in the send buffer (using the garbage data which + // is still there). m_send_buffer.resize(m_send_buffer.size() + BIP324Cipher::EXPANSION); m_cipher.Encrypt( /*contents=*/{}, - /*aad=*/{}, /* empty garbage for now */ + /*aad=*/MakeByteSpan(m_send_buffer).subspan(EllSwiftPubKey::size(), garbage_len), /*ignore=*/false, /*output=*/MakeWritableByteSpan(m_send_buffer).last(BIP324Cipher::EXPANSION)); @@ -1490,7 +1498,10 @@ void V2Transport::MarkBytesSent(size_t bytes_sent) noexcept m_send_pos += bytes_sent; Assume(m_send_pos <= m_send_buffer.size()); - if (m_send_pos == m_send_buffer.size()) { + // Only wipe the buffer when everything is sent in the READY state. In the AWAITING_KEY state + // we still need the garbage that's in the send buffer to construct the garbage authentication + // packet. + if (m_send_state == SendState::READY && m_send_pos == m_send_buffer.size()) { m_send_pos = 0; m_send_buffer = {}; } -- cgit v1.2.3 From 297c8889975a18258d6cc39b1ec1e94fed6630fb Mon Sep 17 00:00:00 2001 From: Pieter Wuille Date: Sun, 30 Jul 2023 10:51:12 -0400 Subject: net: make V2Transport preallocate receive buffer space --- src/net.cpp | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) (limited to 'src/net.cpp') diff --git a/src/net.cpp b/src/net.cpp index e75089c8e9..bd3ba3f6f8 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -1317,6 +1317,9 @@ size_t V2Transport::GetMaxBytesToProcess() noexcept bool V2Transport::ReceivedBytes(Span& msg_bytes) noexcept { AssertLockNotHeld(m_recv_mutex); + /** How many bytes to allocate in the receive buffer at most above what is received so far. */ + static constexpr size_t MAX_RESERVE_AHEAD = 256 * 1024; + LOCK(m_recv_mutex); if (m_recv_state == RecvState::V1) return m_v1_fallback.ReceivedBytes(msg_bytes); @@ -1327,6 +1330,40 @@ bool V2Transport::ReceivedBytes(Span& msg_bytes) noexcept while (!msg_bytes.empty()) { // Decide how many bytes to copy from msg_bytes to m_recv_buffer. size_t max_read = GetMaxBytesToProcess(); + + // Reserve space in the buffer if there is not enough. + if (m_recv_buffer.size() + std::min(msg_bytes.size(), max_read) > m_recv_buffer.capacity()) { + switch (m_recv_state) { + case RecvState::KEY_MAYBE_V1: + case RecvState::KEY: + case RecvState::GARB_GARBTERM: + // During the initial states (key/garbage), allocate once to fit the maximum (4111 + // bytes). + m_recv_buffer.reserve(MAX_GARBAGE_LEN + BIP324Cipher::GARBAGE_TERMINATOR_LEN); + break; + case RecvState::GARBAUTH: + case RecvState::VERSION: + case RecvState::APP: { + // During states where a packet is being received, as much as is expected but never + // more than MAX_RESERVE_AHEAD bytes in addition to what is received so far. + // This means attackers that want to cause us to waste allocated memory are limited + // to MAX_RESERVE_AHEAD above the largest allowed message contents size, and to + // MAX_RESERVE_AHEAD more than they've actually sent us. + size_t alloc_add = std::min(max_read, msg_bytes.size() + MAX_RESERVE_AHEAD); + m_recv_buffer.reserve(m_recv_buffer.size() + alloc_add); + break; + } + case RecvState::APP_READY: + // The buffer is empty in this state. + Assume(m_recv_buffer.empty()); + break; + case RecvState::V1: + // Should have bailed out above. + Assume(false); + break; + } + } + // Can't read more than provided input. max_read = std::min(msg_bytes.size(), max_read); // Copy data to buffer. -- cgit v1.2.3 From db9888feec48c6220a2fcf92865503bbbdab02a4 Mon Sep 17 00:00:00 2001 From: Pieter Wuille Date: Tue, 5 Sep 2023 23:38:15 -0400 Subject: net: detect wrong-network V1 talking to V2Transport --- src/net.cpp | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) (limited to 'src/net.cpp') diff --git a/src/net.cpp b/src/net.cpp index bd3ba3f6f8..3955005dfa 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -1109,12 +1109,29 @@ void V2Transport::ProcessReceivedMaybeV1Bytes() noexcept } } -void V2Transport::ProcessReceivedKeyBytes() noexcept +bool V2Transport::ProcessReceivedKeyBytes() noexcept { AssertLockHeld(m_recv_mutex); AssertLockNotHeld(m_send_mutex); Assume(m_recv_state == RecvState::KEY); Assume(m_recv_buffer.size() <= EllSwiftPubKey::size()); + + // As a special exception, if bytes 4-16 of the key on a responder connection match the + // corresponding bytes of a V1 version message, but bytes 0-4 don't match the network magic + // (if they did, we'd have switched to V1 state already), assume this is a peer from + // another network, and disconnect them. They will almost certainly disconnect us too when + // they receive our uniformly random key and garbage, but detecting this case specially + // means we can log it. + static constexpr std::array MATCH = {'v', 'e', 'r', 's', 'i', 'o', 'n', 0, 0, 0, 0, 0}; + static constexpr size_t OFFSET = sizeof(CMessageHeader::MessageStartChars); + if (!m_initiating && m_recv_buffer.size() >= OFFSET + MATCH.size()) { + if (std::equal(MATCH.begin(), MATCH.end(), m_recv_buffer.begin() + OFFSET)) { + LogPrint(BCLog::NET, "V2 transport error: V1 peer with wrong MessageStart %s\n", + HexStr(Span(m_recv_buffer).first(OFFSET))); + return false; + } + } + if (m_recv_buffer.size() == EllSwiftPubKey::size()) { // Other side's key has been fully received, and can now be Diffie-Hellman combined with // our key to initialize the encryption ciphers. @@ -1157,6 +1174,7 @@ void V2Transport::ProcessReceivedKeyBytes() noexcept } else { // We still have to receive more key bytes. } + return true; } bool V2Transport::ProcessReceivedGarbageBytes() noexcept @@ -1378,7 +1396,7 @@ bool V2Transport::ReceivedBytes(Span& msg_bytes) noexcept break; case RecvState::KEY: - ProcessReceivedKeyBytes(); + if (!ProcessReceivedKeyBytes()) return false; break; case RecvState::GARB_GARBTERM: -- cgit v1.2.3