Skip to content

Commit ebe42c0

Browse files
committed
test: extend the SOCKS5 Python proxy to actually connect to a destination
If requested, make the SOCKS5 Python proxy redirect each connection to a given destination. 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.
1 parent ba621ff commit ebe42c0

File tree

2 files changed

+77
-0
lines changed

2 files changed

+77
-0
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/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.")

0 commit comments

Comments
 (0)