Changeset View
Changeset View
Standalone View
Standalone View
test/functional/p2p_invalid_messages.py
#!/usr/bin/env python3 | #!/usr/bin/env python3 | ||||
# Copyright (c) 2015-2019 The Bitcoin Core developers | # Copyright (c) 2015-2019 The Bitcoin Core developers | ||||
# Distributed under the MIT software license, see the accompanying | # Distributed under the MIT software license, see the accompanying | ||||
# file COPYING or http://www.opensource.org/licenses/mit-license.php. | # file COPYING or http://www.opensource.org/licenses/mit-license.php. | ||||
"""Test node responses to invalid network messages.""" | """Test node responses to invalid network messages.""" | ||||
import struct | import struct | ||||
import time | import time | ||||
from test_framework.messages import ( | from test_framework.messages import ( | ||||
CBlockHeader, | CBlockHeader, | ||||
CInv, | CInv, | ||||
MAX_HEADERS_RESULTS, | |||||
MAX_INV_SIZE, | |||||
MAX_PROTOCOL_MESSAGE_LENGTH, | |||||
msg_avahello, | msg_avahello, | ||||
msg_avapoll, | msg_avapoll, | ||||
msg_avaresponse, | msg_avaresponse, | ||||
msg_getdata, | msg_getdata, | ||||
msg_headers, | msg_headers, | ||||
msg_inv, | msg_inv, | ||||
msg_ping, | msg_ping, | ||||
MSG_TX, | MSG_TX, | ||||
ser_string, | ser_string, | ||||
) | ) | ||||
from test_framework.mininode import ( | from test_framework.mininode import ( | ||||
P2PDataStore, | P2PDataStore, | ||||
P2PInterface, | P2PInterface, | ||||
) | ) | ||||
from test_framework.test_framework import BitcoinTestFramework | from test_framework.test_framework import BitcoinTestFramework | ||||
from test_framework.util import ( | from test_framework.util import ( | ||||
hex_str_to_bytes, | hex_str_to_bytes, | ||||
assert_equal, | assert_equal, | ||||
wait_until, | wait_until, | ||||
) | ) | ||||
MSG_LIMIT = 2 * 1024 * 1024 # 2MB, per MAX_PROTOCOL_MESSAGE_LENGTH | # Account for the 5-byte length prefix | ||||
VALID_DATA_LIMIT = MSG_LIMIT - 5 # Account for the 5-byte length prefix | VALID_DATA_LIMIT = MAX_PROTOCOL_MESSAGE_LENGTH - 5 | ||||
class msg_unrecognized: | class msg_unrecognized: | ||||
"""Nonsensical message. Modeled after similar types in test_framework.messages.""" | """Nonsensical message. Modeled after similar types in test_framework.messages.""" | ||||
msgtype = b'badmsg' | msgtype = b'badmsg' | ||||
def __init__(self, *, str_data): | def __init__(self, *, str_data): | ||||
Show All 21 Lines | def run_test(self): | ||||
self.test_magic_bytes() | self.test_magic_bytes() | ||||
self.test_checksum() | self.test_checksum() | ||||
self.test_size() | self.test_size() | ||||
self.test_msgtype() | self.test_msgtype() | ||||
self.test_addrv2_empty() | self.test_addrv2_empty() | ||||
self.test_addrv2_no_addresses() | self.test_addrv2_no_addresses() | ||||
self.test_addrv2_too_long_address() | self.test_addrv2_too_long_address() | ||||
self.test_addrv2_unrecognized_network() | self.test_addrv2_unrecognized_network() | ||||
self.test_large_inv() | self.test_oversized_inv_msg() | ||||
self.test_oversized_getdata_msg() | |||||
self.test_oversized_headers_msg() | |||||
self.test_unsolicited_ava_messages() | self.test_unsolicited_ava_messages() | ||||
self.test_resource_exhaustion() | self.test_resource_exhaustion() | ||||
def test_buffer(self): | def test_buffer(self): | ||||
self.log.info( | self.log.info( | ||||
"Test message with header split across two buffers, should be received") | "Test message with header split across two buffers is received") | ||||
conn = self.nodes[0].add_p2p_connection(P2PDataStore()) | conn = self.nodes[0].add_p2p_connection(P2PDataStore()) | ||||
# Create valid message | # Create valid message | ||||
msg = conn.build_message(msg_ping(nonce=12345)) | msg = conn.build_message(msg_ping(nonce=12345)) | ||||
cut_pos = 12 # Chosen at an arbitrary position within the header | cut_pos = 12 # Chosen at an arbitrary position within the header | ||||
# Send message in two pieces | # Send message in two pieces | ||||
before = int(self.nodes[0].getnettotals()['totalbytesrecv']) | before = int(self.nodes[0].getnettotals()['totalbytesrecv']) | ||||
conn.send_raw_message(msg[:cut_pos]) | conn.send_raw_message(msg[:cut_pos]) | ||||
# Wait until node has processed the first half of the message | # Wait until node has processed the first half of the message | ||||
wait_until( | wait_until( | ||||
lambda: int( | lambda: int( | ||||
self.nodes[0].getnettotals()['totalbytesrecv']) != before) | self.nodes[0].getnettotals()['totalbytesrecv']) != before) | ||||
middle = int(self.nodes[0].getnettotals()['totalbytesrecv']) | middle = int(self.nodes[0].getnettotals()['totalbytesrecv']) | ||||
# If this assert fails, we've hit an unlikely race | # If this assert fails, we've hit an unlikely race | ||||
# where the test framework sent a message in between the two halves | # where the test framework sent a message in between the two halves | ||||
assert_equal(middle, before + cut_pos) | assert_equal(middle, before + cut_pos) | ||||
conn.send_raw_message(msg[cut_pos:]) | conn.send_raw_message(msg[cut_pos:]) | ||||
conn.sync_with_ping(timeout=1) | conn.sync_with_ping(timeout=1) | ||||
self.nodes[0].disconnect_p2ps() | self.nodes[0].disconnect_p2ps() | ||||
def test_magic_bytes(self): | def test_magic_bytes(self): | ||||
self.log.info("Test message with invalid magic bytes disconnects peer") | |||||
conn = self.nodes[0].add_p2p_connection(P2PDataStore()) | conn = self.nodes[0].add_p2p_connection(P2PDataStore()) | ||||
with self.nodes[0].assert_debug_log(['PROCESSMESSAGE: INVALID MESSAGESTART badmsg']): | with self.nodes[0].assert_debug_log(['PROCESSMESSAGE: INVALID MESSAGESTART badmsg']): | ||||
msg = conn.build_message(msg_unrecognized(str_data="d")) | msg = conn.build_message(msg_unrecognized(str_data="d")) | ||||
# modify magic bytes | # modify magic bytes | ||||
msg = b'\xff' * 4 + msg[4:] | msg = b'\xff' * 4 + msg[4:] | ||||
conn.send_raw_message(msg) | conn.send_raw_message(msg) | ||||
conn.wait_for_disconnect(timeout=1) | conn.wait_for_disconnect(timeout=1) | ||||
self.nodes[0].disconnect_p2ps() | self.nodes[0].disconnect_p2ps() | ||||
def test_checksum(self): | def test_checksum(self): | ||||
self.log.info("Test message with invalid checksum logs an error") | |||||
conn = self.nodes[0].add_p2p_connection(P2PDataStore()) | conn = self.nodes[0].add_p2p_connection(P2PDataStore()) | ||||
with self.nodes[0].assert_debug_log(['CHECKSUM ERROR (badmsg, 2 bytes), expected 78df0a04 was ffffffff']): | 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")) | msg = conn.build_message(msg_unrecognized(str_data="d")) | ||||
# Checksum is after start bytes (4B), message type (12B), len (4B) | # Checksum is after start bytes (4B), message type (12B), len (4B) | ||||
cut_len = 4 + 12 + 4 | cut_len = 4 + 12 + 4 | ||||
# modify checksum | # modify checksum | ||||
msg = msg[:cut_len] + b'\xff' * 4 + msg[cut_len + 4:] | msg = msg[:cut_len] + b'\xff' * 4 + msg[cut_len + 4:] | ||||
self.nodes[0].p2p.send_raw_message(msg) | conn.send_raw_message(msg) | ||||
conn.wait_for_disconnect() | conn.wait_for_disconnect() | ||||
self.nodes[0].disconnect_p2ps() | self.nodes[0].disconnect_p2ps() | ||||
def test_size(self): | def test_size(self): | ||||
self.log.info("Test message with oversized payload disconnects peer") | |||||
conn = self.nodes[0].add_p2p_connection(P2PDataStore()) | conn = self.nodes[0].add_p2p_connection(P2PDataStore()) | ||||
with self.nodes[0].assert_debug_log(['']): | with self.nodes[0].assert_debug_log(['']): | ||||
# Create a message with oversized payload | |||||
msg = msg_unrecognized(str_data="d" * (VALID_DATA_LIMIT + 1)) | msg = msg_unrecognized(str_data="d" * (VALID_DATA_LIMIT + 1)) | ||||
msg = conn.build_message(msg) | msg = conn.build_message(msg) | ||||
self.nodes[0].p2p.send_raw_message(msg) | conn.send_raw_message(msg) | ||||
conn.wait_for_disconnect(timeout=1) | conn.wait_for_disconnect(timeout=1) | ||||
self.nodes[0].disconnect_p2ps() | self.nodes[0].disconnect_p2ps() | ||||
def test_msgtype(self): | def test_msgtype(self): | ||||
self.log.info("Test message with invalid message type logs an error") | |||||
conn = self.nodes[0].add_p2p_connection(P2PDataStore()) | conn = self.nodes[0].add_p2p_connection(P2PDataStore()) | ||||
with self.nodes[0].assert_debug_log(['PROCESSMESSAGE: ERRORS IN HEADER']): | with self.nodes[0].assert_debug_log(['PROCESSMESSAGE: ERRORS IN HEADER']): | ||||
msg = msg_unrecognized(str_data="d") | msg = msg_unrecognized(str_data="d") | ||||
msg.msgtype = b'\xff' * 12 | msg.msgtype = b'\xff' * 12 | ||||
msg = conn.build_message(msg) | msg = conn.build_message(msg) | ||||
# Modify msgtype | # Modify msgtype | ||||
msg = msg[:7] + b'\x00' + msg[7 + 1:] | msg = msg[:7] + b'\x00' + msg[7 + 1:] | ||||
self.nodes[0].p2p.send_raw_message(msg) | conn.send_raw_message(msg) | ||||
conn.sync_with_ping(timeout=1) | conn.sync_with_ping(timeout=1) | ||||
self.nodes[0].disconnect_p2ps() | self.nodes[0].disconnect_p2ps() | ||||
def test_addrv2(self, label, required_log_messages, raw_addrv2): | def test_addrv2(self, label, required_log_messages, raw_addrv2): | ||||
node = self.nodes[0] | node = self.nodes[0] | ||||
conn = node.add_p2p_connection(SenderOfAddrV2()) | conn = node.add_p2p_connection(SenderOfAddrV2()) | ||||
# Make sure bitcoind signals support for ADDRv2, otherwise this test | # Make sure bitcoind signals support for ADDRv2, otherwise this test | ||||
▲ Show 20 Lines • Show All 90 Lines • ▼ Show 20 Lines | def test_addrv2_unrecognized_network(self): | ||||
'01' + | '01' + | ||||
# address length (COMPACTSIZE(4)) | # address length (COMPACTSIZE(4)) | ||||
'04' + | '04' + | ||||
# address | # address | ||||
'09' * 4 + | '09' * 4 + | ||||
# port | # port | ||||
'208d')) | '208d')) | ||||
def test_large_inv(self): | def test_oversized_msg(self, msg, size): | ||||
conn = self.nodes[0].add_p2p_connection(P2PInterface()) | msg_type = msg.msgtype.decode('ascii') | ||||
with self.nodes[0].assert_debug_log(['Misbehaving', '(0 -> 20): oversized-inv: message inv size() = 50001']): | self.log.info( | ||||
msg = msg_inv([CInv(MSG_TX, 1)] * 50001) | "Test {} message of size {} is logged as misbehaving".format( | ||||
conn.send_and_ping(msg) | msg_type, size)) | ||||
with self.nodes[0].assert_debug_log(['Misbehaving', '(20 -> 40): too-many-inv: message getdata size() = 50001']): | with self.nodes[0].assert_debug_log(['Misbehaving', '{} message size = {}'.format(msg_type, size)]): | ||||
msg = msg_getdata([CInv(MSG_TX, 1)] * 50001) | self.nodes[0].add_p2p_connection(P2PInterface()).send_and_ping(msg) | ||||
conn.send_and_ping(msg) | |||||
with self.nodes[0].assert_debug_log(['Misbehaving', '(40 -> 60): too-many-headers: headers message size = 2001']): | |||||
msg = msg_headers([CBlockHeader()] * 2001) | |||||
conn.send_and_ping(msg) | |||||
self.nodes[0].disconnect_p2ps() | self.nodes[0].disconnect_p2ps() | ||||
def test_oversized_inv_msg(self): | |||||
size = MAX_INV_SIZE + 1 | |||||
self.test_oversized_msg(msg_inv([CInv(MSG_TX, 1)] * size), size) | |||||
def test_oversized_getdata_msg(self): | |||||
size = MAX_INV_SIZE + 1 | |||||
self.test_oversized_msg(msg_getdata([CInv(MSG_TX, 1)] * size), size) | |||||
def test_oversized_headers_msg(self): | |||||
size = MAX_HEADERS_RESULTS + 1 | |||||
self.test_oversized_msg(msg_headers([CBlockHeader()] * size), size) | |||||
def test_unsolicited_ava_messages(self): | def test_unsolicited_ava_messages(self): | ||||
"""Node 0 has avalanche disabled by default. If a node does not | """Node 0 has avalanche disabled by default. If a node does not | ||||
advertise the avalanche service flag, it does not expect to receive | advertise the avalanche service flag, it does not expect to receive | ||||
any avalanche related message and should consider it as spam. | any avalanche related message and should consider it as spam. | ||||
""" | """ | ||||
conn = self.nodes[0].add_p2p_connection(P2PInterface()) | conn = self.nodes[0].add_p2p_connection(P2PInterface()) | ||||
with self.nodes[0].assert_debug_log( | with self.nodes[0].assert_debug_log( | ||||
['Misbehaving', '(0 -> 20): unsolicited-avahello']): | ['Misbehaving', '(0 -> 20): unsolicited-avahello']): | ||||
msg = msg_avahello() | msg = msg_avahello() | ||||
conn.send_and_ping(msg) | conn.send_and_ping(msg) | ||||
with self.nodes[0].assert_debug_log( | with self.nodes[0].assert_debug_log( | ||||
['Misbehaving', '(20 -> 40): unsolicited-avapoll']): | ['Misbehaving', '(20 -> 40): unsolicited-avapoll']): | ||||
msg = msg_avapoll() | msg = msg_avapoll() | ||||
conn.send_and_ping(msg) | conn.send_and_ping(msg) | ||||
with self.nodes[0].assert_debug_log( | with self.nodes[0].assert_debug_log( | ||||
['Misbehaving', '(40 -> 60): unsolicited-avaresponse']): | ['Misbehaving', '(40 -> 60): unsolicited-avaresponse']): | ||||
msg = msg_avaresponse() | msg = msg_avaresponse() | ||||
conn.send_and_ping(msg) | conn.send_and_ping(msg) | ||||
self.nodes[0].disconnect_p2ps() | self.nodes[0].disconnect_p2ps() | ||||
def test_resource_exhaustion(self): | def test_resource_exhaustion(self): | ||||
self.log.info("Test node stays up despite many large junk messages") | |||||
conn = self.nodes[0].add_p2p_connection(P2PDataStore()) | conn = self.nodes[0].add_p2p_connection(P2PDataStore()) | ||||
conn2 = 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) | msg_at_size = msg_unrecognized(str_data="b" * VALID_DATA_LIMIT) | ||||
assert len(msg_at_size.serialize()) == MSG_LIMIT | assert len(msg_at_size.serialize()) == MAX_PROTOCOL_MESSAGE_LENGTH | ||||
self.log.info( | self.log.info( | ||||
"Sending a bunch of large, junk messages to test memory exhaustion. May take a bit...") | "(a) Send 80 messages, each of maximum valid data size (2MB)") | ||||
# Run a bunch of times to test for memory exhaustion. | |||||
for _ in range(80): | for _ in range(80): | ||||
conn.send_message(msg_at_size) | conn.send_message(msg_at_size) | ||||
# Check that, even though the node is being hammered by nonsense from one | # Check that, even though the node is being hammered by nonsense from one | ||||
# connection, it can still service other peers in a timely way. | # connection, it can still service other peers in a timely way. | ||||
self.log.info("(b) Check node still services peers in a timely way") | |||||
for _ in range(20): | for _ in range(20): | ||||
conn2.sync_with_ping(timeout=2) | conn2.sync_with_ping(timeout=2) | ||||
# Peer 1, despite being served up a bunch of nonsense, should still be | self.log.info( | ||||
# connected. | "(c) Wait for node to drop junk messages, while remaining connected") | ||||
self.log.info("Waiting for node to drop junk messages.") | |||||
conn.sync_with_ping(timeout=400) | conn.sync_with_ping(timeout=400) | ||||
# Despite being served up a bunch of nonsense, the peers should still | |||||
# be connected. | |||||
assert conn.is_connected | assert conn.is_connected | ||||
assert conn2.is_connected | |||||
self.nodes[0].disconnect_p2ps() | self.nodes[0].disconnect_p2ps() | ||||
if __name__ == '__main__': | if __name__ == '__main__': | ||||
InvalidMessagesTest().main() | InvalidMessagesTest().main() |