3 """Test that tincd works through proxies."""
9 import multiprocessing.connection as mp
15 from threading import Thread
16 from socketserver import ThreadingMixIn, TCPServer, StreamRequestHandler
17 from testlib import check, cmd, path, util
18 from testlib.proc import Tinc, Script
19 from testlib.test import Test
20 from testlib.util import random_string
21 from testlib.log import log
22 from testlib.feature import HAVE_SANDBOX
24 USERNAME = random_string(8)
25 PASSWORD = random_string(8)
27 proxy_stats = {"tx": 0}
32 REQUEST_GRANTED = 0x5A
37 METHOD_USERNAME_PASSWORD = 2
48 def send_all(sock: socket.socket, data: bytes) -> bool:
49 """Send all data to socket, retrying as necessary."""
53 while total < len(data):
54 sent = sock.send(data[total:])
59 return total == len(data)
62 def proxy_data(client: socket.socket, remote: socket.socket) -> None:
63 """Pipe data between the two sockets."""
66 read, _, _ = select.select([client, remote], [], [])
69 data = client.recv(4096)
70 proxy_stats["tx"] += len(data)
71 log.debug("received from client: '%s'", data)
72 if not data or not send_all(remote, data):
73 log.info("remote finished")
77 data = remote.recv(4096)
78 proxy_stats["tx"] += len(data)
79 log.debug("sending to client: '%s'", data)
80 if not data or not send_all(client, data):
81 log.info("client finished")
85 def error_response(address_type: int, error: int) -> bytes:
86 """Create error response for SOCKS client."""
87 return struct.pack("!BBBBIH", SOCKS_VERSION_5, error, 0, address_type, 0, 0)
90 def read_ipv4(sock: socket.socket) -> str:
91 """Read IPv4 address from socket and convert it into a string."""
92 ip_addr = sock.recv(4)
93 return socket.inet_ntoa(ip_addr)
96 def ip_to_int(addr: str) -> int:
97 """Convert address to integer."""
98 return struct.unpack("!I", socket.inet_aton(addr))[0]
101 def addr_response(address, port: T.Tuple[str, int]) -> bytes:
102 """Create address response. Format:
103 version rep rsv atyp bind_addr bind_port
116 class ProxyServer(StreamRequestHandler):
117 """Parent class for proxy server implementations."""
119 name: T.ClassVar[str] = ""
122 class ThreadingTCPServer(ThreadingMixIn, TCPServer):
123 """TCPServer which handles each request in a separate thread."""
126 class HttpProxy(ProxyServer):
127 """HTTP proxy server that handles CONNECT requests."""
130 _re = re.compile(r"CONNECT ([^:]+):(\d+) HTTP/1\.[01]")
132 def handle(self) -> None:
134 self._handle_connection()
136 self.server.close_request(self.request)
138 def _handle_connection(self) -> None:
139 """Handle a single proxy connection"""
141 while not data.endswith(b"\r\n\r\n"):
142 data += self.connection.recv(1)
143 log.info("got request: '%s'", data)
145 match = self._re.match(data.decode("utf-8"))
148 address, port = match.groups()
149 log.info("matched target address %s:%s", address, port)
151 with socket.socket() as sock:
152 sock.connect((address, int(port)))
153 log.info("connected to target")
155 self.connection.sendall(b"HTTP/1.1 200 OK\r\n\r\n")
156 log.info("sent successful response")
158 proxy_data(self.connection, sock)
161 class Socks4Proxy(ProxyServer):
162 """SOCKS 4 proxy server."""
167 def handle(self) -> None:
169 self._handle_connection()
171 self.server.close_request(self.request)
173 def _handle_connection(self) -> None:
174 """Handle a single proxy connection."""
176 version, command, port = struct.unpack("!BBH", self.connection.recv(4))
177 check.equals(SOCKS_VERSION_4, version)
178 check.equals(command, CMD_STREAM)
181 addr = read_ipv4(self.connection)
182 log.info("received address %s:%d", addr, port)
186 byte = self.connection.recv(1)
189 user += byte.decode("utf-8")
191 log.info("received username %s", user)
192 self._check_username(user)
194 with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as remote:
195 remote.connect((addr, port))
196 logging.info("connected to %s:%s", addr, port)
197 self._process_remote(remote)
199 def _check_username(self, user: str) -> bool:
200 """Authenticate by comparing socks4 username."""
201 return user == self.username
203 def _process_remote(self, sock: socket.socket) -> None:
204 """Process a single proxy connection."""
206 addr, port = sock.getsockname()
207 reply = struct.pack("!BBHI", 0, REQUEST_GRANTED, port, ip_to_int(addr))
208 log.info("sending reply %s", reply)
209 self.connection.sendall(reply)
211 proxy_data(self.connection, sock)
214 class AnonymousSocks4Proxy(Socks4Proxy):
215 """socks4 server without any authentication."""
217 def _check_username(self, user: str) -> bool:
221 class Socks5Proxy(ProxyServer):
222 """SOCKS 5 proxy server."""
226 def handle(self) -> None:
227 """Handle a proxy connection."""
229 self._process_connection()
231 self.server.close_request(self.request)
233 def _process_connection(self) -> None:
234 """Handle a proxy connection."""
236 methods = self._read_header()
237 if not self._authenticate(methods):
238 raise RuntimeError("authentication failed")
240 command, address_type = self._read_command()
241 address = self._read_address(address_type)
242 port = struct.unpack("!H", self.connection.recv(2))[0]
243 log.info("got address %s:%d", address, port)
245 if command != CMD_CONNECT:
246 raise RuntimeError(f"bad command {command}")
249 with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as remote:
250 remote.connect((address, port))
251 bind_address = remote.getsockname()
252 logging.info("connected to %s:%d", address, port)
254 reply = addr_response(*bind_address)
255 log.debug("sending address '%s'", reply)
256 self.connection.sendall(reply)
258 proxy_data(self.connection, remote)
259 except OSError as ex:
260 log.error("socks server failed", exc_info=ex)
261 reply = error_response(address_type, 5)
262 self.connection.sendall(reply)
265 def _read_address(self, address_type: int) -> str:
266 """Read target address."""
268 if address_type == ADDR_TYPE_IPV4:
269 return read_ipv4(self.connection)
271 if address_type == ADDR_TYPE_DOMAIN:
272 domain_len = self.connection.recv(1)[0]
273 domain = self.connection.recv(domain_len)
274 return socket.gethostbyname(domain.decode())
276 raise RuntimeError(f"unknown address type {address_type}")
278 def _read_command(self) -> T.Tuple[int, int]:
279 """Check protocol version and get command code and address type."""
281 version, command, _, address_type = struct.unpack(
282 "!BBBB", self.connection.recv(4)
284 check.equals(SOCKS_VERSION_5, version)
285 return command, address_type
288 def _method(self) -> int:
289 """Supported authentication method."""
290 return METHOD_USERNAME_PASSWORD
292 def _authenticate(self, methods: T.List[int]) -> bool:
293 """Perform client authentication."""
295 found = self._method in methods
296 choice = self._method if found else NO_METHODS
297 result = struct.pack("!BB", SOCKS_VERSION_5, choice)
299 log.debug("sending authentication result '%s'", result)
300 self.connection.sendall(result)
303 log.error("auth method not found in %s", methods)
306 if not self._read_creds():
307 log.error("could not verify credentials")
312 def _read_header(self) -> T.List[int]:
313 """Get the list of methods supported by the client."""
315 version, methods = struct.unpack("!BB", self.connection.recv(2))
316 check.equals(SOCKS_VERSION_5, version)
317 check.greater(methods, 0)
318 return [ord(self.connection.recv(1)) for _ in range(methods)]
320 def _read_creds(self) -> bool:
321 """Read and verify auth credentials."""
323 version = ord(self.connection.recv(1))
324 check.equals(1, version)
326 user_len = ord(self.connection.recv(1))
327 user = self.connection.recv(user_len).decode("utf-8")
329 passw_len = ord(self.connection.recv(1))
330 passw = self.connection.recv(passw_len).decode("utf-8")
332 log.info("got credentials '%s', '%s'", user, passw)
333 log.info("want credentials '%s', '%s'", USERNAME, PASSWORD)
335 passed = user == USERNAME and passw == PASSWORD
336 response = struct.pack("!BB", version, AUTH_OK if passed else AUTH_FAILURE)
337 self.connection.sendall(response)
342 class AnonymousSocks5Proxy(Socks5Proxy):
343 """SOCKS 5 server without authentication support."""
346 def _method(self) -> int:
349 def _read_creds(self) -> bool:
353 def init(ctx: Test) -> T.Tuple[Tinc, Tinc]:
354 """Create a new tinc node."""
356 foo, bar = ctx.node(), ctx.node()
359 set Address 127.0.0.1
367 set Address 127.0.0.1
376 def create_exec_proxy(port: int) -> str:
377 """Create a fake exec proxy program."""
381 import multiprocessing.connection as mp
383 with mp.Client(("127.0.0.1", {port}), family="AF_INET") as client:
384 client.send({{ **os.environ }})
386 return util.temp_file(code)
389 def test_proxy(ctx: Test, handler: T.Type[ProxyServer], user="", passw="") -> None:
390 """Test socks proxy support."""
395 for node in foo, bar:
396 node.cmd("set", "Sandbox", "high")
398 bar.add_script(Script.TINC_UP)
401 cmd.exchange(foo, bar)
402 foo.cmd("set", f"{bar}.Port", str(bar.port))
404 with ThreadingTCPServer(("127.0.0.1", 0), handler) as server:
405 _, port = server.server_address
407 worker = Thread(target=server.serve_forever)
410 foo.cmd("set", "Proxy", handler.name, f"127.0.0.1 {port} {user} {passw}")
412 foo.add_script(Script.TINC_UP)
414 foo[Script.TINC_UP].wait()
424 def test_proxy_exec(ctx: Test) -> None:
425 """Test that exec proxies work as expected."""
428 log.info("exec proxy without arguments fails")
429 foo.cmd("set", "Proxy", "exec")
430 _, stderr = foo.cmd("start", code=1)
431 check.is_in("Argument expected for proxy type", stderr)
433 log.info("exec proxy with correct arguments works")
435 cmd.exchange(foo, bar)
437 with mp.Listener(("127.0.0.1", 0), family="AF_INET") as listener:
438 port = int(listener.address[1])
439 proxy = create_exec_proxy(port)
441 foo.cmd("set", "Proxy", "exec", f"{path.PYTHON_INTERPRETER} {proxy}")
444 with listener.accept() as conn:
445 env: T.Dict[str, str] = conn.recv()
447 for var in "NAME", "REMOTEADDRESS", "REMOTEPORT":
448 check.true(env.get(var))
450 for var in "NODE", "NETNAME":
458 with Test("exec proxy") as context:
459 test_proxy_exec(context)
461 with Test("HTTP CONNECT proxy") as context:
462 proxy_stats["tx"] = 0
463 test_proxy(context, HttpProxy)
464 check.greater(proxy_stats["tx"], 0)
466 with Test("socks4 proxy with username") as context:
467 proxy_stats["tx"] = 0
468 test_proxy(context, Socks4Proxy, USERNAME)
469 check.greater(proxy_stats["tx"], 0)
471 with Test("anonymous socks4 proxy") as context:
472 proxy_stats["tx"] = 0
473 test_proxy(context, AnonymousSocks4Proxy)
474 check.greater(proxy_stats["tx"], 0)
476 with Test("authenticated socks5 proxy") as context:
477 proxy_stats["tx"] = 0
478 test_proxy(context, Socks5Proxy, USERNAME, PASSWORD)
479 check.greater(proxy_stats["tx"], 0)
481 with Test("anonymous socks5 proxy") as context:
482 proxy_stats["tx"] = 0
483 test_proxy(context, AnonymousSocks5Proxy)
484 check.greater(proxy_stats["tx"], 0)