Skip to content

Commit 97b790e

Browse files
committed
Merge bitcoin#29420: test: extend the SOCKS5 Python proxy to actually connect to a destination
57529ac test: set P2PConnection.p2p_connected_to_node in peer_connect_helper() (Vasil Dimov) 22cd0e8 test: support WTX INVs from P2PDataStore and fix a comment (Vasil Dimov) ebe42c0 test: extend the SOCKS5 Python proxy to actually connect to a destination (Vasil Dimov) ba621ff test: improve debug log message from P2PConnection::connection_made() (Vasil Dimov) Pull request description: If requested, make the SOCKS5 Python proxy redirect connections to a set of given destinations. Actually act as a real proxy, connecting the client to a destination, except that the destination is not what the client asked for. This would enable us to "connect" to Tor addresses from the functional tests. Plus a few other minor improvements in the test framework as individual commits. --- These changes are part of bitcoin#29415 but they make sense on their own and would be good to have them, regardless of the fate of bitcoin#29415. Also, if this is merged, that would reduce the size of bitcoin#29415, thus the current standalone PR. ACKs for top commit: jonatack: Approach ACK 57529ac achow101: ACK 57529ac tdb3: CR and test ACK 57529ac mzumsande: Code review / tested ACK 57529ac Tree-SHA512: a2892c97bff2d337b37455c409c6136cb62423ce6cc32b197b36f220c1eec9ca046b599135b9a2603c0eb6c1ac4d9795e73831ef0f04378aeea8b245ea733399
2 parents 6b73eb9 + 57529ac commit 97b790e

File tree

4 files changed

+88
-6
lines changed

4 files changed

+88
-6
lines changed

test/functional/test_framework/netutil.py

+7
Original file line numberDiff line numberDiff line change
@@ -167,3 +167,10 @@ def test_unix_socket():
167167
return False
168168
else:
169169
return True
170+
171+
def format_addr_port(addr, port):
172+
'''Return either "addr:port" or "[addr]:port" based on whether addr looks like an IPv6 address.'''
173+
if ":" in addr:
174+
return f"[{addr}]:{port}"
175+
else:
176+
return f"{addr}:{port}"

test/functional/test_framework/p2p.py

+11-4
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ def peer_connect_helper(self, dstaddr, dstport, net, timeout_factor):
188188
self.on_connection_send_msg = None
189189
self.recvbuf = b""
190190
self.magic_bytes = MAGIC_BYTES[net]
191+
self.p2p_connected_to_node = dstport != 0
191192

192193
def peer_connect(self, dstaddr, dstport, *, net, timeout_factor, supports_v2_p2p):
193194
self.peer_connect_helper(dstaddr, dstport, net, timeout_factor)
@@ -217,7 +218,12 @@ def peer_disconnect(self):
217218
def connection_made(self, transport):
218219
"""asyncio callback when a connection is opened."""
219220
assert not self._transport
220-
logger.debug("Connected & Listening: %s:%d" % (self.dstaddr, self.dstport))
221+
info = transport.get_extra_info("socket")
222+
us = info.getsockname()
223+
them = info.getpeername()
224+
logger.debug(f"Connected: us={us[0]}:{us[1]}, them={them[0]}:{them[1]}")
225+
self.dstaddr = them[0]
226+
self.dstport = them[1]
221227
self._transport = transport
222228
# in an inbound connection to the TestNode with P2PConnection as the initiator, [TestNode <---- P2PConnection]
223229
# send the initial handshake immediately
@@ -803,12 +809,13 @@ def __init__(self):
803809
self.getdata_requests = []
804810

805811
def on_getdata(self, message):
806-
"""Check for the tx/block in our stores and if found, reply with an inv message."""
812+
"""Check for the tx/block in our stores and if found, reply with MSG_TX or MSG_BLOCK."""
807813
for inv in message.inv:
808814
self.getdata_requests.append(inv.hash)
809-
if (inv.type & MSG_TYPE_MASK) == MSG_TX and inv.hash in self.tx_store.keys():
815+
invtype = inv.type & MSG_TYPE_MASK
816+
if (invtype == MSG_TX or invtype == MSG_WTX) and inv.hash in self.tx_store.keys():
810817
self.send_message(msg_tx(self.tx_store[inv.hash]))
811-
elif (inv.type & MSG_TYPE_MASK) == MSG_BLOCK and inv.hash in self.block_store.keys():
818+
elif invtype == MSG_BLOCK and inv.hash in self.block_store.keys():
812819
self.send_message(msg_block(self.block_store[inv.hash]))
813820
else:
814821
logger.debug('getdata message type {} received.'.format(hex(inv.type)))

test/functional/test_framework/socks5.py

+70
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,16 @@
44
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
55
"""Dummy Socks5 server for testing."""
66

7+
import select
78
import socket
89
import threading
910
import queue
1011
import logging
1112

13+
from .netutil import (
14+
format_addr_port
15+
)
16+
1217
logger = logging.getLogger("TestFramework.socks5")
1318

1419
# Protocol constants
@@ -32,6 +37,42 @@ def recvall(s, n):
3237
n -= len(d)
3338
return rv
3439

40+
def sendall(s, data):
41+
"""Send all data to a socket, or fail."""
42+
sent = 0
43+
while sent < len(data):
44+
_, wlist, _ = select.select([], [s], [])
45+
if len(wlist) > 0:
46+
n = s.send(data[sent:])
47+
if n == 0:
48+
raise IOError('send() on socket returned 0')
49+
sent += n
50+
51+
def forward_sockets(a, b):
52+
"""Forward data received on socket a to socket b and vice versa, until EOF is received on one of the sockets."""
53+
# Mark as non-blocking so that we do not end up in a deadlock-like situation
54+
# where we block and wait on data from `a` while there is data ready to be
55+
# received on `b` and forwarded to `a`. And at the same time the application
56+
# at `a` is not sending anything because it waits for the data from `b` to
57+
# respond.
58+
a.setblocking(False)
59+
b.setblocking(False)
60+
sockets = [a, b]
61+
done = False
62+
while not done:
63+
rlist, _, xlist = select.select(sockets, [], sockets)
64+
if len(xlist) > 0:
65+
raise IOError('Exceptional condition on socket')
66+
for s in rlist:
67+
data = s.recv(4096)
68+
if data is None or len(data) == 0:
69+
done = True
70+
break
71+
if s == a:
72+
sendall(b, data)
73+
else:
74+
sendall(a, data)
75+
3576
# Implementation classes
3677
class Socks5Configuration():
3778
"""Proxy configuration."""
@@ -41,6 +82,19 @@ def __init__(self):
4182
self.unauth = False # Support unauthenticated
4283
self.auth = False # Support authentication
4384
self.keep_alive = False # Do not automatically close connections
85+
# This function is called whenever a new connection arrives to the proxy
86+
# and it decides where the connection is redirected to. It is passed:
87+
# - the address the client requested to connect to
88+
# - the port the client requested to connect to
89+
# It is supposed to return an object like:
90+
# {
91+
# "actual_to_addr": "127.0.0.1"
92+
# "actual_to_port": 28276
93+
# }
94+
# or None.
95+
# If it returns an object then the connection is redirected to actual_to_addr:actual_to_port.
96+
# If it returns None, or destinations_factory itself is None then the connection is closed.
97+
self.destinations_factory = None
4498

4599
class Socks5Command():
46100
"""Information about an incoming socks5 command."""
@@ -117,6 +171,22 @@ def handle(self):
117171
cmdin = Socks5Command(cmd, atyp, addr, port, username, password)
118172
self.serv.queue.put(cmdin)
119173
logger.debug('Proxy: %s', cmdin)
174+
175+
requested_to_addr = addr.decode("utf-8")
176+
requested_to = format_addr_port(requested_to_addr, port)
177+
178+
if self.serv.conf.destinations_factory is not None:
179+
dest = self.serv.conf.destinations_factory(requested_to_addr, port)
180+
if dest is not None:
181+
logger.debug(f"Serving connection to {requested_to}, will redirect it to "
182+
f"{dest['actual_to_addr']}:{dest['actual_to_port']} instead")
183+
with socket.create_connection((dest["actual_to_addr"], dest["actual_to_port"])) as conn_to:
184+
forward_sockets(self.conn, conn_to)
185+
else:
186+
logger.debug(f"Closing connection to {requested_to}: the destinations factory returned None")
187+
else:
188+
logger.debug(f"Closing connection to {requested_to}: no destinations factory")
189+
120190
# Fall through to disconnect
121191
except Exception as e:
122192
logger.exception("socks5 request handling failed.")

test/functional/test_framework/test_node.py

-2
Original file line numberDiff line numberDiff line change
@@ -715,7 +715,6 @@ def add_p2p_connection(self, p2p_conn, *, wait_for_verack=True, send_version=Tru
715715
if supports_v2_p2p is None:
716716
supports_v2_p2p = self.use_v2transport
717717

718-
p2p_conn.p2p_connected_to_node = True
719718
if self.use_v2transport:
720719
kwargs['services'] = kwargs.get('services', P2P_SERVICES) | NODE_P2P_V2
721720
supports_v2_p2p = self.use_v2transport and supports_v2_p2p
@@ -782,7 +781,6 @@ def addconnection_callback(address, port):
782781
self.log.debug("Connecting to %s:%d %s" % (address, port, connection_type))
783782
self.addconnection('%s:%d' % (address, port), connection_type, advertise_v2_p2p)
784783

785-
p2p_conn.p2p_connected_to_node = False
786784
if supports_v2_p2p is None:
787785
supports_v2_p2p = self.use_v2transport
788786
if advertise_v2_p2p is None:

0 commit comments

Comments
 (0)