[test] Reconnect using v1 P2P when v2 P2P terminates due to magic byte mismatch

- When a v2 TestNode makes an outbound connection to a P2PInterface node
which doesn't support v2 but is advertised as v2 by some malicious
intermediary, the TestNode sends 64 bytes ellswift. The v1 node doesn't
understand this and disconnects. Then the v2 TestNode reconnects by
sending a v1/version message.
This commit is contained in:
stratospher 2022-12-10 12:12:37 +05:30
parent a94e350ac0
commit 382894c3ac
2 changed files with 27 additions and 9 deletions

View file

@ -168,6 +168,7 @@ class P2PConnection(asyncio.Protocol):
# p2p_lock must not be acquired after _send_lock as it could result in deadlocks.
self._send_lock = threading.Lock()
self.v2_state = None # EncryptedP2PState object needed for v2 p2p connections
self.reconnect = False # set if reconnection needs to happen
@property
def is_connected(self):
@ -197,8 +198,9 @@ class P2PConnection(asyncio.Protocol):
coroutine = loop.create_connection(lambda: self, host=self.dstaddr, port=self.dstport)
return lambda: loop.call_soon_threadsafe(loop.create_task, coroutine)
def peer_accept_connection(self, connect_id, connect_cb=lambda: None, *, net, timeout_factor, supports_v2_p2p):
def peer_accept_connection(self, connect_id, connect_cb=lambda: None, *, net, timeout_factor, supports_v2_p2p, reconnect):
self.peer_connect_helper('0', 0, net, timeout_factor)
self.reconnect = reconnect
if supports_v2_p2p:
self.v2_state = EncryptedP2PState(initiating=False, net=net)
@ -222,14 +224,16 @@ class P2PConnection(asyncio.Protocol):
send_handshake_bytes = self.v2_state.initiate_v2_handshake()
self.send_raw_message(send_handshake_bytes)
# if v2 connection, send `on_connection_send_msg` after initial v2 handshake.
if self.on_connection_send_msg and not self.supports_v2_p2p:
# if reconnection situation, send `on_connection_send_msg` after version message is received in `on_version()`.
if self.on_connection_send_msg and not self.supports_v2_p2p and not self.reconnect:
self.send_message(self.on_connection_send_msg)
self.on_connection_send_msg = None # Never used again
self.on_open()
def connection_lost(self, exc):
"""asyncio callback when a connection is closed."""
if exc:
# don't display warning if reconnection needs to be attempted using v1 P2P
if exc and not self.reconnect:
logger.warning("Connection lost to {}:{} due to {}".format(self.dstaddr, self.dstport, exc))
else:
logger.debug("Closed connection to: %s:%d" % (self.dstaddr, self.dstport))
@ -279,9 +283,9 @@ class P2PConnection(asyncio.Protocol):
if not is_mac_auth:
raise ValueError("invalid v2 mac tag in handshake authentication")
self.recvbuf = self.recvbuf[length:]
while self.v2_state.tried_v2_handshake and self.queue_messages:
message = self.queue_messages.pop(0)
self.send_message(message)
if self.v2_state.tried_v2_handshake and self.on_connection_send_msg:
self.send_message(self.on_connection_send_msg)
self.on_connection_send_msg = None
# Socket read methods
@ -350,7 +354,8 @@ class P2PConnection(asyncio.Protocol):
self._log_message("receive", t)
self.on_message(t)
except Exception as e:
logger.exception('Error reading message:', repr(e))
if not self.reconnect:
logger.exception('Error reading message:', repr(e))
raise
def on_message(self, message):
@ -549,6 +554,12 @@ class P2PInterface(P2PConnection):
def on_version(self, message):
assert message.nVersion >= MIN_P2P_VERSION_SUPPORTED, "Version {} received. Test framework only supports versions greater than {}".format(message.nVersion, MIN_P2P_VERSION_SUPPORTED)
# reconnection using v1 P2P has happened since version message can be processed, previously unsent version message is sent using v1 P2P here
if self.reconnect:
if self.on_connection_send_msg:
self.send_message(self.on_connection_send_msg)
self.on_connection_send_msg = None
self.reconnect = False
if message.nVersion >= 70016 and self.wtxidrelay:
self.send_message(msg_wtxidrelay())
if self.support_addrv2:
@ -721,6 +732,11 @@ class NetworkThread(threading.Thread):
if addr is None:
addr = '127.0.0.1'
def exception_handler(loop, context):
if not p2p.reconnect:
loop.default_exception_handler(context)
cls.network_event_loop.set_exception_handler(exception_handler)
coroutine = cls.create_listen_server(addr, port, callback, p2p)
cls.network_event_loop.call_soon_threadsafe(cls.network_event_loop.create_task, coroutine)
@ -734,7 +750,9 @@ class NetworkThread(threading.Thread):
protocol function from that dict, and returns it so the event loop
can start executing it."""
response = cls.protos.get((addr, port))
cls.protos[(addr, port)] = None
# remove protocol function from dict only when reconnection doesn't need to happen/already happened
if not proto.reconnect:
cls.protos[(addr, port)] = None
return response
if (addr, port) not in cls.listeners:

View file

@ -702,7 +702,7 @@ class TestNode():
self.addconnection('%s:%d' % (address, port), connection_type)
p2p_conn.p2p_connected_to_node = False
p2p_conn.peer_accept_connection(connect_cb=addconnection_callback, connect_id=p2p_idx + 1, net=self.chain, timeout_factor=self.timeout_factor, supports_v2_p2p=supports_v2_p2p, **kwargs)()
p2p_conn.peer_accept_connection(connect_cb=addconnection_callback, connect_id=p2p_idx + 1, net=self.chain, timeout_factor=self.timeout_factor, supports_v2_p2p=supports_v2_p2p, reconnect=False, **kwargs)()
if connection_type == "feeler":
# feeler connections are closed as soon as the node receives a `version` message