mirror of
https://github.com/bitcoin/bitcoin.git
synced 2025-02-08 10:31:50 -05:00
![Troy Giorshev](/assets/img/avatar_default.png)
Test 1 is a duplicate of test_size() later in the file. Inexplicably, this test does not work on macOS, whereas test_size() does. Test 2 is problematic for two reasons. First, it always fails with an invalid checksum, which is probably not what was intended. Second, it's not defined at this layer what the behavior should be. Hypothetically, if this test was fixed so that it gave messages with valid checksums, then the message would pass successfully thought the network layer and fail only in the processing layer. A priori the network layer has no idea what the size of a message "actually" is. The "Why does behavior change at 78 bytes" is because of the following: print(len(node.p2p.build_message(msg))) # 125 => Payload size = 125 - 24 = 101 If we take 77 bytes, then there are 101 - 77 = 24 left That's exactly the size of a header So, bitcoind deserializes the header and rejects it for some other reason (Almost always an invalid size (too large)) But, if we take 78 bytes, then there are 101 - 78 = 23 left That's not enough to fill a header, so the socket stays open waiting for more data. That's why we sometimes have to push additional data in order for the peer to disconnect. Additionally, both of these tests use the "conn" variable. For fun, go look at where it's declared. (Hint: test_large_inv(). Don't we all love python's idea of scope?)
164 lines
6.1 KiB
Python
Executable file
164 lines
6.1 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."""
|
|
import asyncio
|
|
import struct
|
|
|
|
from test_framework.messages import (
|
|
CBlockHeader,
|
|
CInv,
|
|
msg_getdata,
|
|
msg_headers,
|
|
msg_inv,
|
|
msg_ping,
|
|
MSG_TX,
|
|
ser_string,
|
|
)
|
|
from test_framework.mininode import (
|
|
NetworkThread,
|
|
P2PDataStore,
|
|
P2PInterface,
|
|
)
|
|
from test_framework.test_framework import BitcoinTestFramework
|
|
|
|
|
|
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):
|
|
"""
|
|
. Test msg header
|
|
0. Send a bunch of large (4MB) messages of an unrecognized type. Check to see
|
|
that it isn't an effective DoS against the node.
|
|
"""
|
|
self.test_magic_bytes()
|
|
self.test_checksum()
|
|
self.test_size()
|
|
self.test_msgtype()
|
|
self.test_large_inv()
|
|
|
|
node = self.nodes[0]
|
|
self.node = node
|
|
node.add_p2p_connection(P2PDataStore())
|
|
conn2 = node.add_p2p_connection(P2PDataStore())
|
|
|
|
msg_limit = 4 * 1000 * 1000 # 4MB, per MAX_PROTOCOL_MESSAGE_LENGTH
|
|
valid_data_limit = msg_limit - 5 # Account for the 4-byte length prefix
|
|
|
|
#
|
|
# 0.
|
|
#
|
|
# Send as large a message as is valid, ensure we aren't disconnected but
|
|
# also can't exhaust resources.
|
|
#
|
|
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):
|
|
node.p2p.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 serving up a bunch of nonsense, should still be connected.
|
|
self.log.info("Waiting for node to drop junk messages.")
|
|
node.p2p.sync_with_ping(timeout=400)
|
|
assert node.p2p.is_connected
|
|
|
|
def test_magic_bytes(self):
|
|
conn = self.nodes[0].add_p2p_connection(P2PDataStore())
|
|
|
|
async def swap_magic_bytes():
|
|
conn._on_data = lambda: None # Need to ignore all incoming messages from now, since they come with "invalid" magic bytes
|
|
conn.magic_bytes = b'\x00\x11\x22\x32'
|
|
|
|
# Call .result() to block until the atomic swap is complete, otherwise
|
|
# we might run into races later on
|
|
asyncio.run_coroutine_threadsafe(swap_magic_bytes(), NetworkThread.network_event_loop).result()
|
|
|
|
with self.nodes[0].assert_debug_log(['PROCESSMESSAGE: INVALID MESSAGESTART ping']):
|
|
conn.send_message(msg_ping(nonce=0xff))
|
|
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"))
|
|
cut_len = (
|
|
4 + # magic
|
|
12 + # msgtype
|
|
4 #len
|
|
)
|
|
# 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(['']):
|
|
msg = conn.build_message(msg_unrecognized(str_data="d"))
|
|
cut_len = (
|
|
4 + # magic
|
|
12 # msgtype
|
|
)
|
|
# modify len to MAX_SIZE + 1
|
|
msg = msg[:cut_len] + struct.pack("<I", 0x02000000 + 1) + msg[cut_len + 4:]
|
|
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', 'peer=4 (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', 'peer=4 (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', 'peer=4 (40 -> 60): headers message size = 2001']):
|
|
msg = msg_headers([CBlockHeader()] * 2001)
|
|
conn.send_and_ping(msg)
|
|
self.nodes[0].disconnect_p2ps()
|
|
|
|
|
|
if __name__ == '__main__':
|
|
InvalidMessagesTest().main()
|