A message can be broken across two buffers, with the split inside its header. Usually this will occur when sending many messages, such that the first buffer fills. This test uses the RPC to verify that the message is actually being received in two pieces. There is a very rare chance of a race condition where the test framework sends a message in between the two halves of the message under test. In this case the peer will almost certainly disconnect and the test will fail. An assert has been added to help debugging that rare case.
160 lines
6.4 KiB
Python
Executable file
160 lines
6.4 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
# Copyright (c) 2015-2020 The Bitcoin Core developers
|
|
# Distributed under the MIT software license, see the accompanying
|
|
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
|
"""Test node responses to invalid network messages."""
|
|
|
|
from test_framework.messages import (
|
|
CBlockHeader,
|
|
CInv,
|
|
msg_getdata,
|
|
msg_headers,
|
|
msg_inv,
|
|
msg_ping,
|
|
MSG_TX,
|
|
ser_string,
|
|
)
|
|
from test_framework.mininode import (
|
|
P2PDataStore,
|
|
P2PInterface,
|
|
)
|
|
from test_framework.test_framework import BitcoinTestFramework
|
|
from test_framework.util import (
|
|
assert_equal,
|
|
wait_until,
|
|
)
|
|
|
|
MSG_LIMIT = 4 * 1000 * 1000 # 4MB, per MAX_PROTOCOL_MESSAGE_LENGTH
|
|
VALID_DATA_LIMIT = MSG_LIMIT - 5 # Account for the 5-byte length prefix
|
|
|
|
class msg_unrecognized:
|
|
"""Nonsensical message. Modeled after similar types in test_framework.messages."""
|
|
|
|
msgtype = b'badmsg'
|
|
|
|
def __init__(self, *, str_data):
|
|
self.str_data = str_data.encode() if not isinstance(str_data, bytes) else str_data
|
|
|
|
def serialize(self):
|
|
return ser_string(self.str_data)
|
|
|
|
def __repr__(self):
|
|
return "{}(data={})".format(self.msgtype, self.str_data)
|
|
|
|
|
|
class InvalidMessagesTest(BitcoinTestFramework):
|
|
def set_test_params(self):
|
|
self.num_nodes = 1
|
|
self.setup_clean_chain = True
|
|
|
|
def run_test(self):
|
|
self.test_buffer()
|
|
self.test_magic_bytes()
|
|
self.test_checksum()
|
|
self.test_size()
|
|
self.test_msgtype()
|
|
self.test_large_inv()
|
|
self.test_resource_exhaustion()
|
|
|
|
def test_buffer(self):
|
|
self.log.info("Test message with header split across two buffers, should be received")
|
|
conn = self.nodes[0].add_p2p_connection(P2PDataStore())
|
|
# Create valid message
|
|
msg = conn.build_message(msg_ping(nonce=12345))
|
|
cut_pos = 12 # Chosen at an arbitrary position within the header
|
|
# Send message in two pieces
|
|
before = int(self.nodes[0].getnettotals()['totalbytesrecv'])
|
|
conn.send_raw_message(msg[:cut_pos])
|
|
# Wait until node has processed the first half of the message
|
|
wait_until(lambda: int(self.nodes[0].getnettotals()['totalbytesrecv']) != before)
|
|
middle = int(self.nodes[0].getnettotals()['totalbytesrecv'])
|
|
# If this assert fails, we've hit an unlikely race
|
|
# where the test framework sent a message in between the two halves
|
|
assert_equal(middle, before + cut_pos)
|
|
conn.send_raw_message(msg[cut_pos:])
|
|
conn.sync_with_ping(timeout=1)
|
|
self.nodes[0].disconnect_p2ps()
|
|
|
|
def test_magic_bytes(self):
|
|
conn = self.nodes[0].add_p2p_connection(P2PDataStore())
|
|
with self.nodes[0].assert_debug_log(['PROCESSMESSAGE: INVALID MESSAGESTART badmsg']):
|
|
msg = conn.build_message(msg_unrecognized(str_data="d"))
|
|
# modify magic bytes
|
|
msg = b'\xff' * 4 + msg[4:]
|
|
conn.send_raw_message(msg)
|
|
conn.wait_for_disconnect(timeout=1)
|
|
self.nodes[0].disconnect_p2ps()
|
|
|
|
def test_checksum(self):
|
|
conn = self.nodes[0].add_p2p_connection(P2PDataStore())
|
|
with self.nodes[0].assert_debug_log(['CHECKSUM ERROR (badmsg, 2 bytes), expected 78df0a04 was ffffffff']):
|
|
msg = conn.build_message(msg_unrecognized(str_data="d"))
|
|
# Checksum is after start bytes (4B), message type (12B), len (4B)
|
|
cut_len = 4 + 12 + 4
|
|
# modify checksum
|
|
msg = msg[:cut_len] + b'\xff' * 4 + msg[cut_len + 4:]
|
|
self.nodes[0].p2p.send_raw_message(msg)
|
|
conn.sync_with_ping(timeout=1)
|
|
self.nodes[0].disconnect_p2ps()
|
|
|
|
def test_size(self):
|
|
conn = self.nodes[0].add_p2p_connection(P2PDataStore())
|
|
with self.nodes[0].assert_debug_log(['']):
|
|
# Create a message with oversized payload
|
|
msg = msg_unrecognized(str_data="d" * (VALID_DATA_LIMIT + 1))
|
|
msg = conn.build_message(msg)
|
|
self.nodes[0].p2p.send_raw_message(msg)
|
|
conn.wait_for_disconnect(timeout=1)
|
|
self.nodes[0].disconnect_p2ps()
|
|
|
|
def test_msgtype(self):
|
|
conn = self.nodes[0].add_p2p_connection(P2PDataStore())
|
|
with self.nodes[0].assert_debug_log(['PROCESSMESSAGE: ERRORS IN HEADER']):
|
|
msg = msg_unrecognized(str_data="d")
|
|
msg.msgtype = b'\xff' * 12
|
|
msg = conn.build_message(msg)
|
|
# Modify msgtype
|
|
msg = msg[:7] + b'\x00' + msg[7 + 1:]
|
|
self.nodes[0].p2p.send_raw_message(msg)
|
|
conn.sync_with_ping(timeout=1)
|
|
self.nodes[0].disconnect_p2ps()
|
|
|
|
def test_large_inv(self):
|
|
conn = self.nodes[0].add_p2p_connection(P2PInterface())
|
|
with self.nodes[0].assert_debug_log(['Misbehaving', '(0 -> 20): message inv size() = 50001']):
|
|
msg = msg_inv([CInv(MSG_TX, 1)] * 50001)
|
|
conn.send_and_ping(msg)
|
|
with self.nodes[0].assert_debug_log(['Misbehaving', '(20 -> 40): message getdata size() = 50001']):
|
|
msg = msg_getdata([CInv(MSG_TX, 1)] * 50001)
|
|
conn.send_and_ping(msg)
|
|
with self.nodes[0].assert_debug_log(['Misbehaving', '(40 -> 60): headers message size = 2001']):
|
|
msg = msg_headers([CBlockHeader()] * 2001)
|
|
conn.send_and_ping(msg)
|
|
self.nodes[0].disconnect_p2ps()
|
|
|
|
def test_resource_exhaustion(self):
|
|
conn = self.nodes[0].add_p2p_connection(P2PDataStore())
|
|
conn2 = self.nodes[0].add_p2p_connection(P2PDataStore())
|
|
msg_at_size = msg_unrecognized(str_data="b" * VALID_DATA_LIMIT)
|
|
assert len(msg_at_size.serialize()) == MSG_LIMIT
|
|
|
|
self.log.info("Sending a bunch of large, junk messages to test memory exhaustion. May take a bit...")
|
|
|
|
# Run a bunch of times to test for memory exhaustion.
|
|
for _ in range(80):
|
|
conn.send_message(msg_at_size)
|
|
|
|
# Check that, even though the node is being hammered by nonsense from one
|
|
# connection, it can still service other peers in a timely way.
|
|
for _ in range(20):
|
|
conn2.sync_with_ping(timeout=2)
|
|
|
|
# Peer 1, despite being served up a bunch of nonsense, should still be connected.
|
|
self.log.info("Waiting for node to drop junk messages.")
|
|
conn.sync_with_ping(timeout=400)
|
|
assert conn.is_connected
|
|
self.nodes[0].disconnect_p2ps()
|
|
|
|
|
|
if __name__ == '__main__':
|
|
InvalidMessagesTest().main()
|