4
4
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
5
5
"""Test node responses to invalid network messages."""
6
6
import asyncio
7
- import struct
8
- import sys
9
7
10
8
from test_framework .messages import (
11
9
CBlockHeader ,
24
22
)
25
23
from test_framework .test_framework import BitcoinTestFramework
26
24
25
+ MSG_LIMIT = 4 * 1000 * 1000 # 4MB, per MAX_PROTOCOL_MESSAGE_LENGTH
26
+ VALID_DATA_LIMIT = MSG_LIMIT - 5 # Account for the 5-byte length prefix
27
27
28
28
class msg_unrecognized :
29
29
"""Nonsensical message. Modeled after similar types in test_framework.messages."""
@@ -46,114 +46,12 @@ def set_test_params(self):
46
46
self .setup_clean_chain = True
47
47
48
48
def run_test (self ):
49
- """
50
- . Test msg header
51
- 0. Send a bunch of large (4MB) messages of an unrecognized type. Check to see
52
- that it isn't an effective DoS against the node.
53
-
54
- 1. Send an oversized (4MB+) message and check that we're disconnected.
55
-
56
- 2. Send a few messages with an incorrect data size in the header, ensure the
57
- messages are ignored.
58
- """
59
49
self .test_magic_bytes ()
60
50
self .test_checksum ()
61
51
self .test_size ()
62
52
self .test_msgtype ()
63
53
self .test_large_inv ()
64
-
65
- node = self .nodes [0 ]
66
- self .node = node
67
- node .add_p2p_connection (P2PDataStore ())
68
- conn2 = node .add_p2p_connection (P2PDataStore ())
69
-
70
- msg_limit = 4 * 1000 * 1000 # 4MB, per MAX_PROTOCOL_MESSAGE_LENGTH
71
- valid_data_limit = msg_limit - 5 # Account for the 4-byte length prefix
72
-
73
- #
74
- # 0.
75
- #
76
- # Send as large a message as is valid, ensure we aren't disconnected but
77
- # also can't exhaust resources.
78
- #
79
- msg_at_size = msg_unrecognized (str_data = "b" * valid_data_limit )
80
- assert len (msg_at_size .serialize ()) == msg_limit
81
-
82
- self .log .info ("Sending a bunch of large, junk messages to test memory exhaustion. May take a bit..." )
83
-
84
- # Run a bunch of times to test for memory exhaustion.
85
- for _ in range (80 ):
86
- node .p2p .send_message (msg_at_size )
87
-
88
- # Check that, even though the node is being hammered by nonsense from one
89
- # connection, it can still service other peers in a timely way.
90
- for _ in range (20 ):
91
- conn2 .sync_with_ping (timeout = 2 )
92
-
93
- # Peer 1, despite serving up a bunch of nonsense, should still be connected.
94
- self .log .info ("Waiting for node to drop junk messages." )
95
- node .p2p .sync_with_ping (timeout = 400 )
96
- assert node .p2p .is_connected
97
-
98
- #
99
- # 1.
100
- #
101
- # Send an oversized message, ensure we're disconnected.
102
- #
103
- # Under macOS this test is skipped due to an unexpected error code
104
- # returned from the closing socket which python/asyncio does not
105
- # yet know how to handle.
106
- #
107
- if sys .platform != 'darwin' :
108
- msg_over_size = msg_unrecognized (str_data = "b" * (valid_data_limit + 1 ))
109
- assert len (msg_over_size .serialize ()) == (msg_limit + 1 )
110
-
111
- # An unknown message type (or *any* message type) over
112
- # MAX_PROTOCOL_MESSAGE_LENGTH should result in a disconnect.
113
- node .p2p .send_message (msg_over_size )
114
- node .p2p .wait_for_disconnect (timeout = 4 )
115
-
116
- node .disconnect_p2ps ()
117
- conn = node .add_p2p_connection (P2PDataStore ())
118
- conn .wait_for_verack ()
119
- else :
120
- self .log .info ("Skipping test p2p_invalid_messages/1 (oversized message) under macOS" )
121
-
122
- #
123
- # 2.
124
- #
125
- # Send messages with an incorrect data size in the header.
126
- #
127
- actual_size = 100
128
- msg = msg_unrecognized (str_data = "b" * actual_size )
129
-
130
- # TODO: handle larger-than cases. I haven't been able to pin down what behavior to expect.
131
- for wrong_size in (2 , 77 , 78 , 79 ):
132
- self .log .info ("Sending a message with incorrect size of {}" .format (wrong_size ))
133
-
134
- # Unmodified message should submit okay.
135
- node .p2p .send_and_ping (msg )
136
-
137
- # A message lying about its data size results in a disconnect when the incorrect
138
- # data size is less than the actual size.
139
- #
140
- # TODO: why does behavior change at 78 bytes?
141
- #
142
- node .p2p .send_raw_message (self ._tweak_msg_data_size (msg , wrong_size ))
143
-
144
- # For some reason unknown to me, we sometimes have to push additional data to the
145
- # peer in order for it to realize a disconnect.
146
- try :
147
- node .p2p .send_message (msg_ping (nonce = 123123 ))
148
- except IOError :
149
- pass
150
-
151
- node .p2p .wait_for_disconnect (timeout = 10 )
152
- node .disconnect_p2ps ()
153
- node .add_p2p_connection (P2PDataStore ())
154
-
155
- # Node is still up.
156
- conn = node .add_p2p_connection (P2PDataStore ())
54
+ self .test_resource_exhaustion ()
157
55
158
56
def test_magic_bytes (self ):
159
57
conn = self .nodes [0 ].add_p2p_connection (P2PDataStore ())
@@ -189,13 +87,9 @@ def test_checksum(self):
189
87
def test_size (self ):
190
88
conn = self .nodes [0 ].add_p2p_connection (P2PDataStore ())
191
89
with self .nodes [0 ].assert_debug_log (['' ]):
192
- msg = conn .build_message (msg_unrecognized (str_data = "d" ))
193
- cut_len = (
194
- 4 + # magic
195
- 12 # msgtype
196
- )
197
- # modify len to MAX_SIZE + 1
198
- msg = msg [:cut_len ] + struct .pack ("<I" , 0x02000000 + 1 ) + msg [cut_len + 4 :]
90
+ # Create a message with oversized payload
91
+ msg = msg_unrecognized (str_data = "d" * (VALID_DATA_LIMIT + 1 ))
92
+ msg = conn .build_message (msg )
199
93
self .nodes [0 ].p2p .send_raw_message (msg )
200
94
conn .wait_for_disconnect (timeout = 1 )
201
95
self .nodes [0 ].disconnect_p2ps ()
@@ -225,25 +119,28 @@ def test_large_inv(self):
225
119
conn .send_and_ping (msg )
226
120
self .nodes [0 ].disconnect_p2ps ()
227
121
228
- def _tweak_msg_data_size (self , message , wrong_size ):
229
- """
230
- Return a raw message based on another message but with an incorrect data size in
231
- the message header.
232
- """
233
- raw_msg = self .node .p2p .build_message (message )
234
-
235
- bad_size_bytes = struct .pack ("<I" , wrong_size )
236
- num_header_bytes_before_size = 4 + 12
237
-
238
- # Replace the correct data size in the message with an incorrect one.
239
- raw_msg_with_wrong_size = (
240
- raw_msg [:num_header_bytes_before_size ] +
241
- bad_size_bytes +
242
- raw_msg [(num_header_bytes_before_size + len (bad_size_bytes )):]
243
- )
244
- assert len (raw_msg ) == len (raw_msg_with_wrong_size )
245
-
246
- return raw_msg_with_wrong_size
122
+ def test_resource_exhaustion (self ):
123
+ conn = self .nodes [0 ].add_p2p_connection (P2PDataStore ())
124
+ conn2 = self .nodes [0 ].add_p2p_connection (P2PDataStore ())
125
+ msg_at_size = msg_unrecognized (str_data = "b" * VALID_DATA_LIMIT )
126
+ assert len (msg_at_size .serialize ()) == MSG_LIMIT
127
+
128
+ self .log .info ("Sending a bunch of large, junk messages to test memory exhaustion. May take a bit..." )
129
+
130
+ # Run a bunch of times to test for memory exhaustion.
131
+ for _ in range (80 ):
132
+ conn .send_message (msg_at_size )
133
+
134
+ # Check that, even though the node is being hammered by nonsense from one
135
+ # connection, it can still service other peers in a timely way.
136
+ for _ in range (20 ):
137
+ conn2 .sync_with_ping (timeout = 2 )
138
+
139
+ # Peer 1, despite being served up a bunch of nonsense, should still be connected.
140
+ self .log .info ("Waiting for node to drop junk messages." )
141
+ conn .sync_with_ping (timeout = 400 )
142
+ assert conn .is_connected
143
+ self .nodes [0 ].disconnect_p2ps ()
247
144
248
145
249
146
if __name__ == '__main__' :
0 commit comments