diff options
author | fanquake <fanquake@gmail.com> | 2023-09-08 10:06:32 +0100 |
---|---|---|
committer | fanquake <fanquake@gmail.com> | 2023-09-08 10:24:03 +0100 |
commit | 4e1a38c6df91f96ca8a2ef07413ffdb1d59c30cc (patch) | |
tree | b34b18a1ff5cd9d7e1d534951090d75dd5c5eda0 /src/test | |
parent | 238d29aff9b43234e340a9cf17742b2be5d1e97d (diff) | |
parent | db9888feec48c6220a2fcf92865503bbbdab02a4 (diff) |
Merge bitcoin/bitcoin#28196: BIP324 connection support
db9888feec48c6220a2fcf92865503bbbdab02a4 net: detect wrong-network V1 talking to V2Transport (Pieter Wuille)
91e1ef8684997fb4b3e8b64ef3935a936445066b test: add unit tests for V2Transport (Pieter Wuille)
297c8889975a18258d6cc39b1ec1e94fed6630fb net: make V2Transport preallocate receive buffer space (Pieter Wuille)
3ffa5fb49ee4a6d9502aa957093bd94058630282 net: make V2Transport send uniformly random number garbage bytes (Pieter Wuille)
0be752d9f8ca27320bc3e82498c7640fabd7e8de net: add short message encoding/decoding support to V2Transport (Pieter Wuille)
8da8642062fa2c7aa2f49995b832c3d0897e37ed net: make V2Transport auto-detect incoming V1 and fall back to it (Pieter Wuille)
13a7f01557272db652b3f333af3f06af6897253f net: add V2Transport class with subset of BIP324 functionality (Pieter Wuille)
dc2d7eb810ef95b06620f334c198687579916435 crypto: Spanify EllSwiftPubKey constructor (Pieter Wuille)
5f4b2c6d79e81ee0445752ad558fcc17831f4b2f net: remove unused Transport::SetReceiveVersion (Pieter Wuille)
c3fad1f29df093e8fd03d70eb43f25ee9d531bf7 net: add have_next_message argument to Transport::GetBytesToSend() (Pieter Wuille)
Pull request description:
This is part of #27634.
This implements the BIP324 v2 transport (which implements all of what the BIP calls transport layer *and* application layer), though in a non-exposed way. It is tested through an extensive fuzz test, which verifies that v2 transports can talk to v2 transports, and v1 transports can talk to v2 transports, and a unit test that exercises a number of unusual scenarios. The transport is functionally complete, including:
* Autodetection of incoming V1 connections.
* Garbage, both sending and receiving.
* Short message type IDs, both sending and receiving.
* Ignore packets (receiving only, but tested in a unit test).
* Session IDs are visible in `getpeerinfo` output (for manual comparison).
Things that are not included, left for future PRs, are:
* Actually using the v2 transport for connections.
* Support for the `NODE_P2P_V2` service flag.
* Retrying downgrade to V1 when attempted outbound V2 connections immediately fail.
* P2P functional and unit tests
ACKs for top commit:
naumenkogs:
ACK db9888feec48c6220a2fcf92865503bbbdab02a4
theStack:
re-ACK db9888feec48c6220a2fcf92865503bbbdab02a4
mzumsande:
Code Review ACK db9888feec48c6220a2fcf92865503bbbdab02a4
Tree-SHA512: 8906ac1e733a99e1f31c9111055611f706d80bbfc2edf6a07fa6e47b21bb65baacd1ff17993cbbf588063b2f5ad30b3af674a50c7bc8e8ebf4671483a21bbfeb
Diffstat (limited to 'src/test')
-rw-r--r-- | src/test/bip324_tests.cpp | 10 | ||||
-rw-r--r-- | src/test/denialofservice_tests.cpp | 4 | ||||
-rw-r--r-- | src/test/fuzz/p2p_transport_serialization.cpp | 91 | ||||
-rw-r--r-- | src/test/net_tests.cpp | 527 | ||||
-rw-r--r-- | src/test/util/net.cpp | 4 |
5 files changed, 614 insertions, 22 deletions
diff --git a/src/test/bip324_tests.cpp b/src/test/bip324_tests.cpp index 04472611ec..1ed7e23bcf 100644 --- a/src/test/bip324_tests.cpp +++ b/src/test/bip324_tests.cpp @@ -38,14 +38,8 @@ void TestBIP324PacketVector( { // Convert input from hex to char/byte vectors/arrays. const auto in_priv_ours = ParseHex(in_priv_ours_hex); - const auto in_ellswift_ours_vec = ParseHex<std::byte>(in_ellswift_ours_hex); - assert(in_ellswift_ours_vec.size() == 64); - std::array<std::byte, 64> in_ellswift_ours; - std::copy(in_ellswift_ours_vec.begin(), in_ellswift_ours_vec.end(), in_ellswift_ours.begin()); - const auto in_ellswift_theirs_vec = ParseHex<std::byte>(in_ellswift_theirs_hex); - assert(in_ellswift_theirs_vec.size() == 64); - std::array<std::byte, 64> in_ellswift_theirs; - std::copy(in_ellswift_theirs_vec.begin(), in_ellswift_theirs_vec.end(), in_ellswift_theirs.begin()); + const auto in_ellswift_ours = ParseHex<std::byte>(in_ellswift_ours_hex); + const auto in_ellswift_theirs = ParseHex<std::byte>(in_ellswift_theirs_hex); const auto in_contents = ParseHex<std::byte>(in_contents_hex); const auto in_aad = ParseHex<std::byte>(in_aad_hex); const auto mid_send_garbage = ParseHex<std::byte>(mid_send_garbage_hex); diff --git a/src/test/denialofservice_tests.cpp b/src/test/denialofservice_tests.cpp index 7f5d587cf6..8c1182b5e1 100644 --- a/src/test/denialofservice_tests.cpp +++ b/src/test/denialofservice_tests.cpp @@ -86,7 +86,7 @@ BOOST_AUTO_TEST_CASE(outbound_slow_chain_eviction) { LOCK(dummyNode1.cs_vSend); - const auto& [to_send, _more, _msg_type] = dummyNode1.m_transport->GetBytesToSend(); + const auto& [to_send, _more, _msg_type] = dummyNode1.m_transport->GetBytesToSend(false); BOOST_CHECK(!to_send.empty()); } connman.FlushSendBuffer(dummyNode1); @@ -97,7 +97,7 @@ BOOST_AUTO_TEST_CASE(outbound_slow_chain_eviction) BOOST_CHECK(peerman.SendMessages(&dummyNode1)); // should result in getheaders { LOCK(dummyNode1.cs_vSend); - const auto& [to_send, _more, _msg_type] = dummyNode1.m_transport->GetBytesToSend(); + const auto& [to_send, _more, _msg_type] = dummyNode1.m_transport->GetBytesToSend(false); BOOST_CHECK(!to_send.empty()); } // Wait 3 more minutes diff --git a/src/test/fuzz/p2p_transport_serialization.cpp b/src/test/fuzz/p2p_transport_serialization.cpp index 2fa5de5008..6e3ef2a707 100644 --- a/src/test/fuzz/p2p_transport_serialization.cpp +++ b/src/test/fuzz/p2p_transport_serialization.cpp @@ -25,6 +25,7 @@ std::vector<std::string> g_all_messages; void initialize_p2p_transport_serialization() { + ECC_Start(); SelectParams(ChainType::REGTEST); g_all_messages = getAllNetMessageTypes(); std::sort(g_all_messages.begin(), g_all_messages.end()); @@ -92,7 +93,7 @@ FUZZ_TARGET(p2p_transport_serialization, .init = initialize_p2p_transport_serial assert(queued); std::optional<bool> known_more; while (true) { - const auto& [to_send, more, _msg_type] = send_transport.GetBytesToSend(); + const auto& [to_send, more, _msg_type] = send_transport.GetBytesToSend(false); if (known_more) assert(!to_send.empty() == *known_more); if (to_send.empty()) break; send_transport.MarkBytesSent(to_send.size()); @@ -124,11 +125,13 @@ void SimulationTest(Transport& initiator, Transport& responder, R& rng, FuzzedDa // Vectors with bytes last returned by GetBytesToSend() on transport[i]. std::array<std::vector<uint8_t>, 2> to_send; - // Last returned 'more' values (if still relevant) by transport[i]->GetBytesToSend(). - std::array<std::optional<bool>, 2> last_more; + // Last returned 'more' values (if still relevant) by transport[i]->GetBytesToSend(), for + // both have_next_message false and true. + std::array<std::optional<bool>, 2> last_more, last_more_next; - // Whether more bytes to be sent are expected on transport[i]. - std::array<std::optional<bool>, 2> expect_more; + // Whether more bytes to be sent are expected on transport[i], before and after + // SetMessageToSend(). + std::array<std::optional<bool>, 2> expect_more, expect_more_next; // Function to consume a message type. auto msg_type_fn = [&]() { @@ -177,18 +180,27 @@ void SimulationTest(Transport& initiator, Transport& responder, R& rng, FuzzedDa // Wrapper around transport[i]->GetBytesToSend() that performs sanity checks. auto bytes_to_send_fn = [&](int side) -> Transport::BytesToSend { - const auto& [bytes, more, msg_type] = transports[side]->GetBytesToSend(); + // Invoke GetBytesToSend twice (for have_next_message = {false, true}). This function does + // not modify state (it's const), and only the "more" return value should differ between + // the calls. + const auto& [bytes, more_nonext, msg_type] = transports[side]->GetBytesToSend(false); + const auto& [bytes_next, more_next, msg_type_next] = transports[side]->GetBytesToSend(true); // Compare with expected more. if (expect_more[side].has_value()) assert(!bytes.empty() == *expect_more[side]); + // Verify consistency between the two results. + assert(bytes == bytes_next); + assert(msg_type == msg_type_next); + if (more_nonext) assert(more_next); // Compare with previously reported output. assert(to_send[side].size() <= bytes.size()); assert(to_send[side] == Span{bytes}.first(to_send[side].size())); to_send[side].resize(bytes.size()); std::copy(bytes.begin(), bytes.end(), to_send[side].begin()); - // Remember 'more' result. - last_more[side] = {more}; + // Remember 'more' results. + last_more[side] = {more_nonext}; + last_more_next[side] = {more_next}; // Return. - return {bytes, more, msg_type}; + return {bytes, more_nonext, msg_type}; }; // Function to make side send a new message. @@ -199,7 +211,8 @@ void SimulationTest(Transport& initiator, Transport& responder, R& rng, FuzzedDa CSerializedNetMsg msg = next_msg[side].Copy(); bool queued = transports[side]->SetMessageToSend(msg); // Update expected more data. - expect_more[side] = std::nullopt; + expect_more[side] = expect_more_next[side]; + expect_more_next[side] = std::nullopt; // Verify consistency of GetBytesToSend after SetMessageToSend bytes_to_send_fn(/*side=*/side); if (queued) { @@ -223,6 +236,7 @@ void SimulationTest(Transport& initiator, Transport& responder, R& rng, FuzzedDa // If all to-be-sent bytes were sent, move last_more data to expect_more data. if (send_now == bytes.size()) { expect_more[side] = last_more[side]; + expect_more_next[side] = last_more_next[side]; } // Remove the bytes from the last reported to-be-sent vector. assert(to_send[side].size() >= send_now); @@ -251,6 +265,7 @@ void SimulationTest(Transport& initiator, Transport& responder, R& rng, FuzzedDa // Clear cached expected 'more' information: if certainly no more data was to be sent // before, receiving bytes makes this uncertain. if (expect_more[!side] == false) expect_more[!side] = std::nullopt; + if (expect_more_next[!side] == false) expect_more_next[!side] = std::nullopt; // Verify consistency of GetBytesToSend after ReceivedBytes bytes_to_send_fn(/*side=*/!side); bool progress = to_recv.size() < old_len; @@ -320,6 +335,40 @@ std::unique_ptr<Transport> MakeV1Transport(NodeId nodeid) noexcept return std::make_unique<V1Transport>(nodeid, SER_NETWORK, INIT_PROTO_VERSION); } +template<typename RNG> +std::unique_ptr<Transport> MakeV2Transport(NodeId nodeid, bool initiator, RNG& rng, FuzzedDataProvider& provider) +{ + // Retrieve key + auto key = ConsumePrivateKey(provider); + if (!key.IsValid()) return {}; + // Construct garbage + size_t garb_len = provider.ConsumeIntegralInRange<size_t>(0, V2Transport::MAX_GARBAGE_LEN); + std::vector<uint8_t> garb; + if (garb_len <= 64) { + // When the garbage length is up to 64 bytes, read it directly from the fuzzer input. + garb = provider.ConsumeBytes<uint8_t>(garb_len); + garb.resize(garb_len); + } else { + // If it's longer, generate it from the RNG. This avoids having large amounts of + // (hopefully) irrelevant data needing to be stored in the fuzzer data. + for (auto& v : garb) v = uint8_t(rng()); + } + // Retrieve entropy + auto ent = provider.ConsumeBytes<std::byte>(32); + ent.resize(32); + // Use as entropy SHA256(ent || garbage). This prevents a situation where the fuzzer manages to + // include the garbage terminator (which is a function of both ellswift keys) in the garbage. + // This is extremely unlikely (~2^-116) with random keys/garbage, but the fuzzer can choose + // both non-randomly and dependently. Since the entropy is hashed anyway inside the ellswift + // computation, no coverage should be lost by using a hash as entropy, and it removes the + // possibility of garbage that happens to contain what is effectively a hash of the keys. + CSHA256().Write(UCharCast(ent.data()), ent.size()) + .Write(garb.data(), garb.size()) + .Finalize(UCharCast(ent.data())); + + return std::make_unique<V2Transport>(nodeid, initiator, SER_NETWORK, INIT_PROTO_VERSION, key, ent, garb); +} + } // namespace FUZZ_TARGET(p2p_transport_bidirectional, .init = initialize_p2p_transport_serialization) @@ -332,3 +381,25 @@ FUZZ_TARGET(p2p_transport_bidirectional, .init = initialize_p2p_transport_serial if (!t1 || !t2) return; SimulationTest(*t1, *t2, rng, provider); } + +FUZZ_TARGET(p2p_transport_bidirectional_v2, .init = initialize_p2p_transport_serialization) +{ + // Test with two V2 transports talking to each other. + FuzzedDataProvider provider{buffer.data(), buffer.size()}; + XoRoShiRo128PlusPlus rng(provider.ConsumeIntegral<uint64_t>()); + auto t1 = MakeV2Transport(NodeId{0}, true, rng, provider); + auto t2 = MakeV2Transport(NodeId{1}, false, rng, provider); + if (!t1 || !t2) return; + SimulationTest(*t1, *t2, rng, provider); +} + +FUZZ_TARGET(p2p_transport_bidirectional_v1v2, .init = initialize_p2p_transport_serialization) +{ + // Test with a V1 initiator talking to a V2 responder. + FuzzedDataProvider provider{buffer.data(), buffer.size()}; + XoRoShiRo128PlusPlus rng(provider.ConsumeIntegral<uint64_t>()); + auto t1 = MakeV1Transport(NodeId{0}); + auto t2 = MakeV2Transport(NodeId{1}, false, rng, provider); + if (!t1 || !t2) return; + SimulationTest(*t1, *t2, rng, provider); +} diff --git a/src/test/net_tests.cpp b/src/test/net_tests.cpp index 295cb78b36..900e311d22 100644 --- a/src/test/net_tests.cpp +++ b/src/test/net_tests.cpp @@ -15,6 +15,7 @@ #include <serialize.h> #include <span.h> #include <streams.h> +#include <test/util/random.h> #include <test/util/setup_common.h> #include <test/util/validation.h> #include <timedata.h> @@ -1005,4 +1006,530 @@ BOOST_AUTO_TEST_CASE(advertise_local_address) RemoveLocal(addr_cjdns); } +namespace { + +/** A class for scenario-based tests of V2Transport + * + * Each V2TransportTester encapsulates a V2Transport (the one being tested), and can be told to + * interact with it. To do so, it also encapsulates a BIP324Cipher to act as the other side. A + * second V2Transport is not used, as doing so would not permit scenarios that involve sending + * invalid data, or ones scenarios using BIP324 features that are not implemented on the sending + * side (like decoy packets). + */ +class V2TransportTester +{ + V2Transport m_transport; //!< V2Transport being tested + BIP324Cipher m_cipher; //!< Cipher to help with the other side + bool m_test_initiator; //!< Whether m_transport is the initiator (true) or responder (false) + + std::vector<uint8_t> m_sent_garbage; //!< The garbage we've sent to m_transport. + std::vector<uint8_t> m_to_send; //!< Bytes we have queued up to send to m_transport. + std::vector<uint8_t> m_received; //!< Bytes we have received from m_transport. + std::deque<CSerializedNetMsg> m_msg_to_send; //!< Messages to be sent *by* m_transport to us. + +public: + /** Construct a tester object. test_initiator: whether the tested transport is initiator. */ + V2TransportTester(bool test_initiator) : + m_transport(0, test_initiator, SER_NETWORK, INIT_PROTO_VERSION), + m_test_initiator(test_initiator) {} + + /** Data type returned by Interact: + * + * - std::nullopt: transport error occurred + * - otherwise: a vector of + * - std::nullopt: invalid message received + * - otherwise: a CNetMessage retrieved + */ + using InteractResult = std::optional<std::vector<std::optional<CNetMessage>>>; + + /** Send/receive scheduled/available bytes and messages. + * + * This is the only function that interacts with the transport being tested; everything else is + * scheduling things done by Interact(), or processing things learned by it. + */ + InteractResult Interact() + { + std::vector<std::optional<CNetMessage>> ret; + while (true) { + bool progress{false}; + // Send bytes from m_to_send to the transport. + if (!m_to_send.empty()) { + Span<const uint8_t> to_send = Span{m_to_send}.first(1 + InsecureRandRange(m_to_send.size())); + size_t old_len = to_send.size(); + if (!m_transport.ReceivedBytes(to_send)) { + return std::nullopt; // transport error occurred + } + if (old_len != to_send.size()) { + progress = true; + m_to_send.erase(m_to_send.begin(), m_to_send.begin() + (old_len - to_send.size())); + } + } + // Retrieve messages received by the transport. + if (m_transport.ReceivedMessageComplete() && (!progress || InsecureRandBool())) { + bool reject{false}; + auto msg = m_transport.GetReceivedMessage({}, reject); + if (reject) { + ret.push_back(std::nullopt); + } else { + ret.push_back(std::move(msg)); + } + progress = true; + } + // Enqueue a message to be sent by the transport to us. + if (!m_msg_to_send.empty() && (!progress || InsecureRandBool())) { + if (m_transport.SetMessageToSend(m_msg_to_send.front())) { + m_msg_to_send.pop_front(); + progress = true; + } + } + // Receive bytes from the transport. + const auto& [recv_bytes, _more, _msg_type] = m_transport.GetBytesToSend(!m_msg_to_send.empty()); + if (!recv_bytes.empty() && (!progress || InsecureRandBool())) { + size_t to_receive = 1 + InsecureRandRange(recv_bytes.size()); + m_received.insert(m_received.end(), recv_bytes.begin(), recv_bytes.begin() + to_receive); + progress = true; + m_transport.MarkBytesSent(to_receive); + } + if (!progress) break; + } + return ret; + } + + /** Expose the cipher. */ + BIP324Cipher& GetCipher() { return m_cipher; } + + /** Schedule bytes to be sent to the transport. */ + void Send(Span<const uint8_t> data) + { + m_to_send.insert(m_to_send.end(), data.begin(), data.end()); + } + + /** Send V1 version message header to the transport. */ + void SendV1Version(const CMessageHeader::MessageStartChars& magic) + { + CMessageHeader hdr(magic, "version", 126 + InsecureRandRange(11)); + CDataStream ser(SER_NETWORK, CLIENT_VERSION); + ser << hdr; + m_to_send.insert(m_to_send.end(), UCharCast(ser.data()), UCharCast(ser.data() + ser.size())); + } + + /** Schedule bytes to be sent to the transport. */ + void Send(Span<const std::byte> data) { Send(MakeUCharSpan(data)); } + + /** Schedule our ellswift key to be sent to the transport. */ + void SendKey() { Send(m_cipher.GetOurPubKey()); } + + /** Schedule specified garbage to be sent to the transport. */ + void SendGarbage(Span<const uint8_t> garbage) + { + // Remember the specified garbage (so we can use it for constructing the garbage + // authentication packet). + m_sent_garbage.assign(garbage.begin(), garbage.end()); + // Schedule it for sending. + Send(m_sent_garbage); + } + + /** Schedule garbage (of specified length) to be sent to the transport. */ + void SendGarbage(size_t garbage_len) + { + // Generate random garbage and send it. + SendGarbage(g_insecure_rand_ctx.randbytes<uint8_t>(garbage_len)); + } + + /** Schedule garbage (with valid random length) to be sent to the transport. */ + void SendGarbage() + { + SendGarbage(InsecureRandRange(V2Transport::MAX_GARBAGE_LEN + 1)); + } + + /** Schedule a message to be sent to us by the transport. */ + void AddMessage(std::string m_type, std::vector<uint8_t> payload) + { + CSerializedNetMsg msg; + msg.m_type = std::move(m_type); + msg.data = std::move(payload); + m_msg_to_send.push_back(std::move(msg)); + } + + /** Expect ellswift key to have been received from transport and process it. + * + * Many other V2TransportTester functions cannot be called until after ReceiveKey() has been + * called, as no encryption keys are set up before that point. + */ + void ReceiveKey() + { + // When processing a key, enough bytes need to have been received already. + BOOST_REQUIRE(m_received.size() >= EllSwiftPubKey::size()); + // Initialize the cipher using it (acting as the opposite side of the tested transport). + m_cipher.Initialize(MakeByteSpan(m_received).first(EllSwiftPubKey::size()), !m_test_initiator); + // Strip the processed bytes off the front of the receive buffer. + m_received.erase(m_received.begin(), m_received.begin() + EllSwiftPubKey::size()); + } + + /** Schedule an encrypted packet with specified content/aad/ignore to be sent to transport + * (only after ReceiveKey). */ + void SendPacket(Span<const uint8_t> content, Span<const uint8_t> aad = {}, bool ignore = false) + { + // Use cipher to construct ciphertext. + std::vector<std::byte> ciphertext; + ciphertext.resize(content.size() + BIP324Cipher::EXPANSION); + m_cipher.Encrypt( + /*contents=*/MakeByteSpan(content), + /*aad=*/MakeByteSpan(aad), + /*ignore=*/ignore, + /*output=*/ciphertext); + // Schedule it for sending. + Send(ciphertext); + } + + /** Schedule garbage terminator and authentication packet to be sent to the transport (only + * after ReceiveKey). */ + void SendGarbageTermAuth(size_t garb_auth_data_len = 0, bool garb_auth_ignore = false) + { + // Generate random data to include in the garbage authentication packet (ignored by peer). + auto garb_auth_data = g_insecure_rand_ctx.randbytes<uint8_t>(garb_auth_data_len); + // Schedule the garbage terminator to be sent. + Send(m_cipher.GetSendGarbageTerminator()); + // Schedule the garbage authentication packet to be sent. + SendPacket(/*content=*/garb_auth_data, /*aad=*/m_sent_garbage, /*ignore=*/garb_auth_ignore); + } + + /** Schedule version packet to be sent to the transport (only after ReceiveKey). */ + void SendVersion(Span<const uint8_t> version_data = {}, bool vers_ignore = false) + { + SendPacket(/*content=*/version_data, /*aad=*/{}, /*ignore=*/vers_ignore); + } + + /** Expect a packet to have been received from transport, process it, and return its contents + * (only after ReceiveKey). By default, decoys are skipped. */ + std::vector<uint8_t> ReceivePacket(Span<const std::byte> aad = {}, bool skip_decoy = true) + { + std::vector<uint8_t> contents; + // Loop as long as there are ignored packets that are to be skipped. + while (true) { + // When processing a packet, at least enough bytes for its length descriptor must be received. + BOOST_REQUIRE(m_received.size() >= BIP324Cipher::LENGTH_LEN); + // Decrypt the content length. + size_t size = m_cipher.DecryptLength(MakeByteSpan(Span{m_received}.first(BIP324Cipher::LENGTH_LEN))); + // Check that the full packet is in the receive buffer. + BOOST_REQUIRE(m_received.size() >= size + BIP324Cipher::EXPANSION); + // Decrypt the packet contents. + contents.resize(size); + bool ignore{false}; + bool ret = m_cipher.Decrypt( + /*input=*/MakeByteSpan( + Span{m_received}.first(size + BIP324Cipher::EXPANSION).subspan(BIP324Cipher::LENGTH_LEN)), + /*aad=*/aad, + /*ignore=*/ignore, + /*contents=*/MakeWritableByteSpan(contents)); + BOOST_CHECK(ret); + // Strip the processed packet's bytes off the front of the receive buffer. + m_received.erase(m_received.begin(), m_received.begin() + size + BIP324Cipher::EXPANSION); + // Stop if the ignore bit is not set on this packet, or if we choose to not honor it. + if (!ignore || !skip_decoy) break; + } + return contents; + } + + /** Expect garbage, garbage terminator, and garbage auth packet to have been received, and + * process them (only after ReceiveKey). */ + void ReceiveGarbage() + { + // Figure out the garbage length. + size_t garblen; + for (garblen = 0; garblen <= V2Transport::MAX_GARBAGE_LEN; ++garblen) { + BOOST_REQUIRE(m_received.size() >= garblen + BIP324Cipher::GARBAGE_TERMINATOR_LEN); + auto term_span = MakeByteSpan(Span{m_received}.subspan(garblen, BIP324Cipher::GARBAGE_TERMINATOR_LEN)); + if (term_span == m_cipher.GetReceiveGarbageTerminator()) break; + } + // Copy the garbage to a buffer. + std::vector<uint8_t> garbage(m_received.begin(), m_received.begin() + garblen); + // Strip garbage + garbage terminator off the front of the receive buffer. + m_received.erase(m_received.begin(), m_received.begin() + garblen + BIP324Cipher::GARBAGE_TERMINATOR_LEN); + // Process the expected garbage authentication packet. Such a packet still functions as one + // even when its ignore bit is set to true, so we do not skip decoy packets here. + ReceivePacket(/*aad=*/MakeByteSpan(garbage), /*skip_decoy=*/false); + } + + /** Expect version packet to have been received, and process it (only after ReceiveKey). */ + void ReceiveVersion() + { + auto contents = ReceivePacket(); + // Version packets from real BIP324 peers are expected to be empty, despite the fact that + // this class supports *sending* non-empty version packets (to test that BIP324 peers + // correctly ignore version packet contents). + BOOST_CHECK(contents.empty()); + } + + /** Expect application packet to have been received, with specified short id and payload. + * (only after ReceiveKey). */ + void ReceiveMessage(uint8_t short_id, Span<const uint8_t> payload) + { + auto ret = ReceivePacket(); + BOOST_CHECK(ret.size() == payload.size() + 1); + BOOST_CHECK(ret[0] == short_id); + BOOST_CHECK(Span{ret}.subspan(1) == payload); + } + + /** Expect application packet to have been received, with specified 12-char message type and + * payload (only after ReceiveKey). */ + void ReceiveMessage(const std::string& m_type, Span<const uint8_t> payload) + { + auto ret = ReceivePacket(); + BOOST_REQUIRE(ret.size() == payload.size() + 1 + CMessageHeader::COMMAND_SIZE); + BOOST_CHECK(ret[0] == 0); + for (unsigned i = 0; i < 12; ++i) { + if (i < m_type.size()) { + BOOST_CHECK(ret[1 + i] == m_type[i]); + } else { + BOOST_CHECK(ret[1 + i] == 0); + } + } + BOOST_CHECK(Span{ret}.subspan(1 + CMessageHeader::COMMAND_SIZE) == payload); + } + + /** Schedule an encrypted packet with specified message type and payload to be sent to + * transport (only after ReceiveKey). */ + void SendMessage(std::string mtype, Span<const uint8_t> payload) + { + // Construct contents consisting of 0x00 + 12-byte message type + payload. + std::vector<uint8_t> contents(1 + CMessageHeader::COMMAND_SIZE + payload.size()); + std::copy(mtype.begin(), mtype.end(), reinterpret_cast<char*>(contents.data() + 1)); + std::copy(payload.begin(), payload.end(), contents.begin() + 1 + CMessageHeader::COMMAND_SIZE); + // Send a packet with that as contents. + SendPacket(contents); + } + + /** Schedule an encrypted packet with specified short message id and payload to be sent to + * transport (only after ReceiveKey). */ + void SendMessage(uint8_t short_id, Span<const uint8_t> payload) + { + // Construct contents consisting of short_id + payload. + std::vector<uint8_t> contents(1 + payload.size()); + contents[0] = short_id; + std::copy(payload.begin(), payload.end(), contents.begin() + 1); + // Send a packet with that as contents. + SendPacket(contents); + } + + /** Introduce a bit error in the data scheduled to be sent. */ + void Damage() + { + m_to_send[InsecureRandRange(m_to_send.size())] ^= (uint8_t{1} << InsecureRandRange(8)); + } +}; + +} // namespace + +BOOST_AUTO_TEST_CASE(v2transport_test) +{ + // A mostly normal scenario, testing a transport in initiator mode. + for (int i = 0; i < 10; ++i) { + V2TransportTester tester(true); + auto ret = tester.Interact(); + BOOST_REQUIRE(ret && ret->empty()); + tester.SendKey(); + tester.SendGarbage(); + tester.ReceiveKey(); + tester.SendGarbageTermAuth(); + tester.SendVersion(); + ret = tester.Interact(); + BOOST_REQUIRE(ret && ret->empty()); + tester.ReceiveGarbage(); + tester.ReceiveVersion(); + auto msg_data_1 = g_insecure_rand_ctx.randbytes<uint8_t>(InsecureRandRange(100000)); + auto msg_data_2 = g_insecure_rand_ctx.randbytes<uint8_t>(InsecureRandRange(1000)); + tester.SendMessage(uint8_t(4), msg_data_1); // cmpctblock short id + tester.SendMessage(0, {}); // Invalidly encoded message + tester.SendMessage("tx", msg_data_2); // 12-character encoded message type + ret = tester.Interact(); + BOOST_REQUIRE(ret && ret->size() == 3); + BOOST_CHECK((*ret)[0] && (*ret)[0]->m_type == "cmpctblock" && Span{(*ret)[0]->m_recv} == MakeByteSpan(msg_data_1)); + BOOST_CHECK(!(*ret)[1]); + BOOST_CHECK((*ret)[2] && (*ret)[2]->m_type == "tx" && Span{(*ret)[2]->m_recv} == MakeByteSpan(msg_data_2)); + + // Then send a message with a bit error, expecting failure. + tester.SendMessage("bad", msg_data_1); + tester.Damage(); + ret = tester.Interact(); + BOOST_CHECK(!ret); + } + + // Normal scenario, with a transport in responder node. + for (int i = 0; i < 10; ++i) { + V2TransportTester tester(false); + tester.SendKey(); + tester.SendGarbage(); + auto ret = tester.Interact(); + BOOST_REQUIRE(ret && ret->empty()); + tester.ReceiveKey(); + tester.SendGarbageTermAuth(); + tester.SendVersion(); + ret = tester.Interact(); + BOOST_REQUIRE(ret && ret->empty()); + tester.ReceiveGarbage(); + tester.ReceiveVersion(); + auto msg_data_1 = g_insecure_rand_ctx.randbytes<uint8_t>(InsecureRandRange(100000)); + auto msg_data_2 = g_insecure_rand_ctx.randbytes<uint8_t>(InsecureRandRange(1000)); + tester.SendMessage(uint8_t(14), msg_data_1); // inv short id + tester.SendMessage(uint8_t(19), msg_data_2); // pong short id + ret = tester.Interact(); + BOOST_REQUIRE(ret && ret->size() == 2); + BOOST_CHECK((*ret)[0] && (*ret)[0]->m_type == "inv" && Span{(*ret)[0]->m_recv} == MakeByteSpan(msg_data_1)); + BOOST_CHECK((*ret)[1] && (*ret)[1]->m_type == "pong" && Span{(*ret)[1]->m_recv} == MakeByteSpan(msg_data_2)); + + // Then send a too-large message. + auto msg_data_3 = g_insecure_rand_ctx.randbytes<uint8_t>(4005000); + tester.SendMessage(uint8_t(11), msg_data_3); // getdata short id + ret = tester.Interact(); + BOOST_CHECK(!ret); + } + + // Various valid but unusual scenarios. + for (int i = 0; i < 50; ++i) { + /** Whether an initiator or responder is being tested. */ + bool initiator = InsecureRandBool(); + /** Use either 0 bytes or the maximum possible (4095 bytes) garbage length. */ + size_t garb_len = InsecureRandBool() ? 0 : V2Transport::MAX_GARBAGE_LEN; + /** Sometimes, use non-empty contents in the garbage authentication packet (which is to be ignored). */ + size_t garb_auth_data_len = InsecureRandBool() ? 0 : InsecureRandRange(100000); + /** Whether to set the ignore bit on the garbage authentication packet (it still functions as garbage authentication). */ + bool garb_ignore = InsecureRandBool(); + /** How many decoy packets to send before the version packet. */ + unsigned num_ignore_version = InsecureRandRange(10); + /** What data to send in the version packet (ignored by BIP324 peers, but reserved for future extensions). */ + auto ver_data = g_insecure_rand_ctx.randbytes<uint8_t>(InsecureRandBool() ? 0 : InsecureRandRange(1000)); + /** Whether to immediately send key and garbage out (required for responders, optional otherwise). */ + bool send_immediately = !initiator || InsecureRandBool(); + /** How many decoy packets to send before the first and second real message. */ + unsigned num_decoys_1 = InsecureRandRange(1000), num_decoys_2 = InsecureRandRange(1000); + V2TransportTester tester(initiator); + if (send_immediately) { + tester.SendKey(); + tester.SendGarbage(garb_len); + } + auto ret = tester.Interact(); + BOOST_REQUIRE(ret && ret->empty()); + if (!send_immediately) { + tester.SendKey(); + tester.SendGarbage(garb_len); + } + tester.ReceiveKey(); + tester.SendGarbageTermAuth(garb_auth_data_len, garb_ignore); + for (unsigned v = 0; v < num_ignore_version; ++v) { + size_t ver_ign_data_len = InsecureRandBool() ? 0 : InsecureRandRange(1000); + auto ver_ign_data = g_insecure_rand_ctx.randbytes<uint8_t>(ver_ign_data_len); + tester.SendVersion(ver_ign_data, true); + } + tester.SendVersion(ver_data, false); + ret = tester.Interact(); + BOOST_REQUIRE(ret && ret->empty()); + tester.ReceiveGarbage(); + tester.ReceiveVersion(); + for (unsigned d = 0; d < num_decoys_1; ++d) { + auto decoy_data = g_insecure_rand_ctx.randbytes<uint8_t>(InsecureRandRange(1000)); + tester.SendPacket(/*content=*/decoy_data, /*aad=*/{}, /*ignore=*/true); + } + auto msg_data_1 = g_insecure_rand_ctx.randbytes<uint8_t>(InsecureRandRange(4000000)); + tester.SendMessage(uint8_t(28), msg_data_1); + for (unsigned d = 0; d < num_decoys_2; ++d) { + auto decoy_data = g_insecure_rand_ctx.randbytes<uint8_t>(InsecureRandRange(1000)); + tester.SendPacket(/*content=*/decoy_data, /*aad=*/{}, /*ignore=*/true); + } + auto msg_data_2 = g_insecure_rand_ctx.randbytes<uint8_t>(InsecureRandRange(1000)); + tester.SendMessage(uint8_t(13), msg_data_2); // headers short id + // Send invalidly-encoded message + tester.SendMessage(std::string("blocktxn\x00\x00\x00a", CMessageHeader::COMMAND_SIZE), {}); + tester.SendMessage("foobar", {}); // test receiving unknown message type + tester.AddMessage("barfoo", {}); // test sending unknown message type + ret = tester.Interact(); + BOOST_REQUIRE(ret && ret->size() == 4); + BOOST_CHECK((*ret)[0] && (*ret)[0]->m_type == "addrv2" && Span{(*ret)[0]->m_recv} == MakeByteSpan(msg_data_1)); + BOOST_CHECK((*ret)[1] && (*ret)[1]->m_type == "headers" && Span{(*ret)[1]->m_recv} == MakeByteSpan(msg_data_2)); + BOOST_CHECK(!(*ret)[2]); + BOOST_CHECK((*ret)[3] && (*ret)[3]->m_type == "foobar" && (*ret)[3]->m_recv.empty()); + tester.ReceiveMessage("barfoo", {}); + } + + // Too long garbage (initiator). + { + V2TransportTester tester(true); + auto ret = tester.Interact(); + BOOST_REQUIRE(ret && ret->empty()); + tester.SendKey(); + tester.SendGarbage(V2Transport::MAX_GARBAGE_LEN + 1); + tester.ReceiveKey(); + tester.SendGarbageTermAuth(); + ret = tester.Interact(); + BOOST_CHECK(!ret); + } + + // Too long garbage (responder). + { + V2TransportTester tester(false); + tester.SendKey(); + tester.SendGarbage(V2Transport::MAX_GARBAGE_LEN + 1); + auto ret = tester.Interact(); + BOOST_REQUIRE(ret && ret->empty()); + tester.ReceiveKey(); + tester.SendGarbageTermAuth(); + ret = tester.Interact(); + BOOST_CHECK(!ret); + } + + // Send garbage that includes the first 15 garbage terminator bytes somewhere. + { + V2TransportTester tester(true); + auto ret = tester.Interact(); + BOOST_REQUIRE(ret && ret->empty()); + tester.SendKey(); + tester.ReceiveKey(); + /** The number of random garbage bytes before the included first 15 bytes of terminator. */ + size_t len_before = InsecureRandRange(V2Transport::MAX_GARBAGE_LEN - 16 + 1); + /** The number of random garbage bytes after it. */ + size_t len_after = InsecureRandRange(V2Transport::MAX_GARBAGE_LEN - 16 - len_before + 1); + // Construct len_before + 16 + len_after random bytes. + auto garbage = g_insecure_rand_ctx.randbytes<uint8_t>(len_before + 16 + len_after); + // Replace the designed 16 bytes in the middle with the to-be-sent garbage terminator. + auto garb_term = MakeUCharSpan(tester.GetCipher().GetSendGarbageTerminator()); + std::copy(garb_term.begin(), garb_term.begin() + 16, garbage.begin() + len_before); + // Introduce a bit error in the last byte of that copied garbage terminator, making only + // the first 15 of them match. + garbage[len_before + 15] ^= (uint8_t(1) << InsecureRandRange(8)); + tester.SendGarbage(garbage); + tester.SendGarbageTermAuth(); + tester.SendVersion(); + ret = tester.Interact(); + BOOST_REQUIRE(ret && ret->empty()); + tester.ReceiveGarbage(); + tester.ReceiveVersion(); + auto msg_data_1 = g_insecure_rand_ctx.randbytes<uint8_t>(4000000); // test that receiving 4M payload works + auto msg_data_2 = g_insecure_rand_ctx.randbytes<uint8_t>(4000000); // test that sending 4M payload works + tester.SendMessage(uint8_t(InsecureRandRange(223) + 33), {}); // unknown short id + tester.SendMessage(uint8_t(2), msg_data_1); // "block" short id + tester.AddMessage("blocktxn", msg_data_2); // schedule blocktxn to be sent to us + ret = tester.Interact(); + BOOST_REQUIRE(ret && ret->size() == 2); + BOOST_CHECK(!(*ret)[0]); + BOOST_CHECK((*ret)[1] && (*ret)[1]->m_type == "block" && Span{(*ret)[1]->m_recv} == MakeByteSpan(msg_data_1)); + tester.ReceiveMessage(uint8_t(3), msg_data_2); // "blocktxn" short id + } + + // Send correct network's V1 header + { + V2TransportTester tester(false); + tester.SendV1Version(Params().MessageStart()); + auto ret = tester.Interact(); + BOOST_CHECK(ret); + } + + // Send wrong network's V1 header + { + V2TransportTester tester(false); + tester.SendV1Version(CChainParams::Main()->MessageStart()); + auto ret = tester.Interact(); + BOOST_CHECK(!ret); + } +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/src/test/util/net.cpp b/src/test/util/net.cpp index 5696f8d13c..dc64c0b4c1 100644 --- a/src/test/util/net.cpp +++ b/src/test/util/net.cpp @@ -78,7 +78,7 @@ void ConnmanTestMsg::FlushSendBuffer(CNode& node) const node.vSendMsg.clear(); node.m_send_memusage = 0; while (true) { - const auto& [to_send, _more, _msg_type] = node.m_transport->GetBytesToSend(); + const auto& [to_send, _more, _msg_type] = node.m_transport->GetBytesToSend(false); if (to_send.empty()) break; node.m_transport->MarkBytesSent(to_send.size()); } @@ -90,7 +90,7 @@ bool ConnmanTestMsg::ReceiveMsgFrom(CNode& node, CSerializedNetMsg&& ser_msg) co assert(queued); bool complete{false}; while (true) { - const auto& [to_send, _more, _msg_type] = node.m_transport->GetBytesToSend(); + const auto& [to_send, _more, _msg_type] = node.m_transport->GetBytesToSend(false); if (to_send.empty()) break; NodeReceiveMsgBytes(node, to_send, complete); node.m_transport->MarkBytesSent(to_send.size()); |