From 4e8c4bc794c045beb678854e6e326fc04322c7c3 Mon Sep 17 00:00:00 2001 From: Henry Romp <151henry151@gmail.com> Date: Mon, 3 Aug 2026 18:31:01 -0400 Subject: [PATCH] test: classify SOCKS5 peers via getpeerinfo addrbind The SOCKS5 destinations factory classified connections by scanning debug.log for connection attempts to the requested address and port. The destination is not unique per connection, so the log cannot identify which attempt is being served: first-match returned a stale type when an automatic connection reused an address private broadcast had already used, and latest-match still breaks if two attempts to the same address overlap. Match the exact connection instead: the source addr:port of the proxy's client socket equals the node's addrbind for that peer, so looking it up in getpeerinfo identifies precisely the connection being served and returns its connection_type. This also stops treating debug.log contents as a stable interface. Co-authored-by: Greg Sanders --- test/functional/p2p_private_broadcast.py | 39 +++++++++---------- .../p2p_private_broadcast_retry_v1.py | 4 +- test/functional/test_framework/socks5.py | 10 +++-- 3 files changed, 27 insertions(+), 26 deletions(-) diff --git a/test/functional/p2p_private_broadcast.py b/test/functional/p2p_private_broadcast.py index 054058e946b..4ee6af72511 100755 --- a/test/functional/p2p_private_broadcast.py +++ b/test/functional/p2p_private_broadcast.py @@ -6,7 +6,6 @@ Test how locally submitted transactions are sent to the network when private broadcast is used. """ -import re import time import threading @@ -68,33 +67,32 @@ class P2PPrivateBroadcast(BitcoinTestFramework): self.trigger_no_relay_peer = False self.no_relay_peer = None - def find_connection_type_in_debug_log(to_addr, to_port): - """ - Scan the debug log of tx_originator for a connection attempt to to_addr:to_port. - Return the connection type (outbound-full-relay, private-broadcast, etc) or - None if there is no connection attempt to to_addr:to_port. - """ - with open(self.tx_originator_debug_log_path, mode="r", encoding="utf-8") as debug_log: - for line in debug_log.readlines(): - match = re.match(f".*trying v. connection \\((.+)\\) to \\[?{to_addr}]?:{to_port},.*", line) - if match: - return match.group(1) - return None - - def destinations_factory(requested_to_addr, requested_to_port): + def destinations_factory(requested_to_addr, requested_to_port, proxy_client): """ Instruct the SOCKS5 proxy to redirect connections: * The first automatic outbound connection -> P2PDataStore * The first private broadcast connection -> nodes[1] * Anything else -> P2PInterface + + proxy_client is the client's socket address as seen by the proxy (host:port), + equal to the node's addrbind for this connection. """ conn_type = None - def found_connection_in_debug_log(): - nonlocal conn_type - conn_type = find_connection_type_in_debug_log(requested_to_addr, requested_to_port) - return conn_type is not None + # SOCKS handlers run in separate threads, so each needs its own RPC connection. + rpc = self.nodes[0].create_new_rpc_connection() - self.wait_until(found_connection_in_debug_log) + def connection_type_found(): + nonlocal conn_type + # The proxy has already replied SUCCESS to the SOCKS5 request, so the node + # has finished ConnectNode and registered the peer (or is about to). + # The proxy client address equals the node's addrbind for this connection. + for peer in rpc.getpeerinfo(): + if peer.get("addrbind") == proxy_client: + conn_type = peer["connection_type"] + return True + return False + + self.wait_until(connection_type_found) with self.destinations_lock: i = len(self.destinations) @@ -233,7 +231,6 @@ class P2PPrivateBroadcast(BitcoinTestFramework): def run_test(self): tx_originator = self.nodes[0] - self.tx_originator_debug_log_path = tx_originator.debug_log_path tx_receiver = self.nodes[1] far_observer = tx_receiver.add_p2p_connection(P2PInterface()) diff --git a/test/functional/p2p_private_broadcast_retry_v1.py b/test/functional/p2p_private_broadcast_retry_v1.py index 87fa440a883..7b97119bba8 100755 --- a/test/functional/p2p_private_broadcast_retry_v1.py +++ b/test/functional/p2p_private_broadcast_retry_v1.py @@ -77,7 +77,7 @@ class P2PPrivateBroadcastRetryV1(BitcoinTestFramework): self.ipv4_via_tor_proxy_conn_versions.append(v2or1) def setup_nodes(self): - def destinations_factory_all_proxy(requested_to_addr, requested_to_port): + def destinations_factory_all_proxy(requested_to_addr, requested_to_port, _proxy_client): """ Instruct the SOCKS5 proxy to redirect all connections to newly created P2PInterface objects that claim support for P2P_V2. @@ -102,7 +102,7 @@ class P2PPrivateBroadcastRetryV1(BitcoinTestFramework): self.ipv4_via_tor_proxy_addr_port = None # Remember the first IPv4 address connected to via the Tor proxy. self.ipv4_via_tor_proxy_conn_versions = [] # Transport versions tried on that address. - def destinations_factory_tor_proxy(requested_to_addr, requested_to_port): + def destinations_factory_tor_proxy(requested_to_addr, requested_to_port, _proxy_client): """ Instruct the SOCKS5 proxy to redirect all connections to newly created P2PInterface, except the first connection to an IPv4 address and all subsequent connections to that diff --git a/test/functional/test_framework/socks5.py b/test/functional/test_framework/socks5.py index f7a1aabddd4..44d8e6fc82a 100644 --- a/test/functional/test_framework/socks5.py +++ b/test/functional/test_framework/socks5.py @@ -104,6 +104,7 @@ class Socks5Configuration(): # and it decides where the connection is redirected to. It is passed: # - the address the client requested to connect to # - the port the client requested to connect to + # - the client's socket address as seen by the proxy, formatted as host:port # It is supposed to return an object like: # { # "actual_to_addr": "127.0.0.1" @@ -140,8 +141,9 @@ class Socks5Connection(): """Handle socks5 request according to RFC1928.""" log_exception_prefix = "Socks5Connection.handle(): " try: + proxy_client = format_sock(self.conn, local=False) log_exception_prefix = ("Socks5Connection.handle(" - f"client={format_sock(self.conn, local=False)}, " + f"client={proxy_client}, " f"proxy={format_sock(self.conn, local=True)}): ") # Verify socks version @@ -193,7 +195,9 @@ class Socks5Connection(): port_hi,port_lo = recvall(self.conn, 2) port = (port_hi << 8) | port_lo - # Send dummy response + # Reply SUCCESS before calling destinations_factory, so the client can finish + # establishing the connection and register the peer; factories that consult + # getpeerinfo depend on that order. self.conn.sendall(bytearray([0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) cmdin = Socks5Command(cmd, atyp, addr, port, username, password) @@ -205,7 +209,7 @@ class Socks5Connection(): if self.serv.is_running(): if self.serv.conf.destinations_factory is not None: - dest = self.serv.conf.destinations_factory(requested_to_addr, port) + dest = self.serv.conf.destinations_factory(requested_to_addr, port, proxy_client) if dest is not None: logger.debug(f"Serving connection to {requested_to}, will redirect it to " f"{dest['actual_to_addr']}:{dest['actual_to_port']} instead")