1 #!/usr/bin/env python3
2 # Copyright (c) 2015-2019 The Limenka developers
3 # Distributed under the MIT software license, see the accompanying
4 # file COPYING or http://www.opensource.org/licenses/mit-license.php.
5 """Dummy Socks5 server for testing."""
6 7 import select
8 import socket
9 import threading
10 import queue
11 import logging
12 13 from .netutil import (
14 format_addr_port
15 )
16 17 logger = logging.getLogger("TestFramework.socks5")
18 19 # Protocol constants
20 class Command:
21 CONNECT = 0x01
22 23 class AddressType:
24 IPV4 = 0x01
25 DOMAINNAME = 0x03
26 IPV6 = 0x04
27 28 # Utility functions
29 def recvall(s, n):
30 """Receive n bytes from a socket, or fail."""
31 rv = bytearray()
32 while n > 0:
33 d = s.recv(n)
34 if not d:
35 raise IOError('Unexpected end of stream')
36 rv.extend(d)
37 n -= len(d)
38 return rv
39 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 76 # Implementation classes
77 class Socks5Configuration():
78 """Proxy configuration."""
79 def __init__(self):
80 self.addr = None # Bind address (must be set)
81 self.af = socket.AF_INET # Bind address family
82 self.unauth = False # Support unauthenticated
83 self.auth = False # Support authentication
84 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
98 99 class Socks5Command():
100 """Information about an incoming socks5 command."""
101 def __init__(self, cmd, atyp, addr, port, username, password):
102 self.cmd = cmd # Command (one of Command.*)
103 self.atyp = atyp # Address type (one of AddressType.*)
104 self.addr = addr # Address
105 self.port = port # Port to connect to
106 self.username = username
107 self.password = password
108 def __repr__(self):
109 return 'Socks5Command(%s,%s,%s,%s,%s,%s)' % (self.cmd, self.atyp, self.addr, self.port, self.username, self.password)
110 111 class Socks5Connection():
112 def __init__(self, serv, conn):
113 self.serv = serv
114 self.conn = conn
115 116 def handle(self):
117 """Handle socks5 request according to RFC1928."""
118 try:
119 # Verify socks version
120 ver = recvall(self.conn, 1)[0]
121 if ver != 0x05:
122 raise IOError('Invalid socks version %i' % ver)
123 # Choose authentication method
124 nmethods = recvall(self.conn, 1)[0]
125 methods = bytearray(recvall(self.conn, nmethods))
126 method = None
127 if 0x02 in methods and self.serv.conf.auth:
128 method = 0x02 # username/password
129 elif 0x00 in methods and self.serv.conf.unauth:
130 method = 0x00 # unauthenticated
131 if method is None:
132 raise IOError('No supported authentication method was offered')
133 # Send response
134 self.conn.sendall(bytearray([0x05, method]))
135 # Read authentication (optional)
136 username = None
137 password = None
138 if method == 0x02:
139 ver = recvall(self.conn, 1)[0]
140 if ver != 0x01:
141 raise IOError('Invalid auth packet version %i' % ver)
142 ulen = recvall(self.conn, 1)[0]
143 username = str(recvall(self.conn, ulen))
144 plen = recvall(self.conn, 1)[0]
145 password = str(recvall(self.conn, plen))
146 # Send authentication response
147 self.conn.sendall(bytearray([0x01, 0x00]))
148 149 # Read connect request
150 ver, cmd, _, atyp = recvall(self.conn, 4)
151 if ver != 0x05:
152 raise IOError('Invalid socks version %i in connect request' % ver)
153 if cmd != Command.CONNECT:
154 raise IOError('Unhandled command %i in connect request' % cmd)
155 156 if atyp == AddressType.IPV4:
157 addr = recvall(self.conn, 4)
158 elif atyp == AddressType.DOMAINNAME:
159 n = recvall(self.conn, 1)[0]
160 addr = recvall(self.conn, n)
161 elif atyp == AddressType.IPV6:
162 addr = recvall(self.conn, 16)
163 else:
164 raise IOError('Unknown address type %i' % atyp)
165 port_hi,port_lo = recvall(self.conn, 2)
166 port = (port_hi << 8) | port_lo
167 168 # Send dummy response
169 self.conn.sendall(bytearray([0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]))
170 171 cmdin = Socks5Command(cmd, atyp, addr, port, username, password)
172 self.serv.queue.put(cmdin)
173 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"Can't serve the connection to {requested_to}: the destinations factory returned None")
187 else:
188 logger.debug(f"Can't serve the connection to {requested_to}: no destinations factory")
189 190 # Fall through to disconnect
191 except Exception as e:
192 logger.exception("socks5 request handling failed.")
193 self.serv.queue.put(e)
194 finally:
195 if not self.serv.keep_alive:
196 self.conn.close()
197 else:
198 logger.debug("Keeping client connection alive")
199 200 class Socks5Server():
201 def __init__(self, conf):
202 self.conf = conf
203 self.s = socket.socket(conf.af)
204 self.s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
205 self.s.bind(conf.addr)
206 self.s.listen(5)
207 self.running = False
208 self.thread = None
209 self.queue = queue.Queue() # report connections and exceptions to client
210 self.keep_alive = conf.keep_alive
211 212 def run(self):
213 while self.running:
214 (sockconn, _) = self.s.accept()
215 if self.running:
216 conn = Socks5Connection(self, sockconn)
217 thread = threading.Thread(None, conn.handle)
218 thread.daemon = True
219 thread.start()
220 221 def start(self):
222 assert not self.running
223 self.running = True
224 self.thread = threading.Thread(None, self.run)
225 self.thread.daemon = True
226 self.thread.start()
227 228 def stop(self):
229 self.running = False
230 # connect to self to end run loop
231 s = socket.socket(self.conf.af)
232 s.connect(self.conf.addr)
233 s.close()
234 self.thread.join()
235