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
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
23 USERNAME = random_string(8)
24 PASSWORD = random_string(8)
26 proxy_stats = {"tx": 0}
31 REQUEST_GRANTED = 0x5A
36 METHOD_USERNAME_PASSWORD = 2
47 def send_all(sock: socket.socket, data: bytes) -> bool:
48 """Send all data to socket, retrying as necessary."""
52 while total < len(data):
53 sent = sock.send(data[total:])
58 return total == len(data)
61 def proxy_data(client: socket.socket, remote: socket.socket) -> None:
62 """Pipe data between the two sockets."""
65 read, _, _ = select.select([client, remote], [], [])
68 data = client.recv(4096)
69 proxy_stats["tx"] += len(data)
70 log.debug("received from client: '%s'", data)
71 if not data or not send_all(remote, data):
72 log.info("remote finished")
76 data = remote.recv(4096)
77 proxy_stats["tx"] += len(data)
78 log.debug("sending to client: '%s'", data)
79 if not data or not send_all(client, data):
80 log.info("client finished")
84 def error_response(address_type: int, error: int) -> bytes:
85 """Create error response for SOCKS client."""
86 return struct.pack("!BBBBIH", SOCKS_VERSION_5, error, 0, address_type, 0, 0)
89 def read_ipv4(sock: socket.socket) -> str:
90 """Read IPv4 address from socket and convert it into a string."""
91 ip_addr = sock.recv(4)
92 return socket.inet_ntoa(ip_addr)
95 def ip_to_int(addr: str) -> int:
96 """Convert address to integer."""
97 return struct.unpack("!I", socket.inet_aton(addr))[0]
100 def addr_response(address, port: T.Tuple[str, int]) -> bytes:
101 """Create address response. Format:
102 version rep rsv atyp bind_addr bind_port
115 class ProxyServer(StreamRequestHandler):
116 """Parent class for proxy server implementations."""
118 name: T.ClassVar[str] = ""
121 class ThreadingTCPServer(ThreadingMixIn, TCPServer):
122 """TCPServer which handles each request in a separate thread."""
125 class HttpProxy(ProxyServer):
126 """HTTP proxy server that handles CONNECT requests."""
129 _re = re.compile(r"CONNECT ([^:]+):(\d+) HTTP/1\.[01]")
131 def handle(self) -> None:
133 self._handle_connection()
135 self.server.close_request(self.request)
137 def _handle_connection(self) -> None:
138 """Handle a single proxy connection"""
140 while not data.endswith(b"\r\n\r\n"):
141 data += self.connection.recv(1)
142 log.info("got request: '%s'", data)
144 match = self._re.match(data.decode("utf-8"))
147 address, port = match.groups()
148 log.info("matched target address %s:%s", address, port)
150 with socket.socket() as sock:
151 sock.connect((address, int(port)))
152 log.info("connected to target")
154 self.connection.sendall(b"HTTP/1.1 200 OK\r\n\r\n")
155 log.info("sent successful response")
157 proxy_data(self.connection, sock)
160 class Socks4Proxy(ProxyServer):
161 """SOCKS 4 proxy server."""
166 def handle(self) -> None:
168 self._handle_connection()
170 self.server.close_request(self.request)
172 def _handle_connection(self) -> None:
173 """Handle a single proxy connection."""
175 version, command, port = struct.unpack("!BBH", self.connection.recv(4))
176 check.equals(SOCKS_VERSION_4, version)
177 check.equals(command, CMD_STREAM)
180 addr = read_ipv4(self.connection)
181 log.info("received address %s:%d", addr, port)
185 byte = self.connection.recv(1)
188 user += byte.decode("utf-8")
190 log.info("received username %s", user)
191 self._check_username(user)
193 with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as remote:
194 remote.connect((addr, port))
195 logging.info("connected to %s:%s", addr, port)
196 self._process_remote(remote)
198 def _check_username(self, user: str) -> bool:
199 """Authenticate by comparing socks4 username."""
200 return user == self.username
202 def _process_remote(self, sock: socket.socket) -> None:
203 """Process a single proxy connection."""
205 addr, port = sock.getsockname()
206 reply = struct.pack("!BBHI", 0, REQUEST_GRANTED, port, ip_to_int(addr))
207 log.info("sending reply %s", reply)
208 self.connection.sendall(reply)
210 proxy_data(self.connection, sock)
213 class AnonymousSocks4Proxy(Socks4Proxy):
214 """socks4 server without any authentication."""
216 def _check_username(self, user: str) -> bool:
220 class Socks5Proxy(ProxyServer):
221 """SOCKS 5 proxy server."""
225 def handle(self) -> None:
226 """Handle a proxy connection."""
228 self._process_connection()
230 self.server.close_request(self.request)
232 def _process_connection(self) -> None:
233 """Handle a proxy connection."""
235 methods = self._read_header()
236 if not self._authenticate(methods):
237 raise RuntimeError("authentication failed")
239 command, address_type = self._read_command()
240 address = self._read_address(address_type)
241 port = struct.unpack("!H", self.connection.recv(2))[0]
242 log.info("got address %s:%d", address, port)
244 if command != CMD_CONNECT:
245 raise RuntimeError(f"bad command {command}")
248 with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as remote:
249 remote.connect((address, port))
250 bind_address = remote.getsockname()
251 logging.info("connected to %s:%d", address, port)
253 reply = addr_response(*bind_address)
254 log.debug("sending address '%s'", reply)
255 self.connection.sendall(reply)
257 proxy_data(self.connection, remote)
258 except OSError as ex:
259 log.error("socks server failed", exc_info=ex)
260 reply = error_response(address_type, 5)
261 self.connection.sendall(reply)
264 def _read_address(self, address_type: int) -> str:
265 """Read target address."""
267 if address_type == ADDR_TYPE_IPV4:
268 return read_ipv4(self.connection)
270 if address_type == ADDR_TYPE_DOMAIN:
271 domain_len = self.connection.recv(1)[0]
272 domain = self.connection.recv(domain_len)
273 return socket.gethostbyname(domain.decode())
275 raise RuntimeError(f"unknown address type {address_type}")
277 def _read_command(self) -> T.Tuple[int, int]:
278 """Check protocol version and get command code and address type."""
280 version, command, _, address_type = struct.unpack(
281 "!BBBB", self.connection.recv(4)
283 check.equals(SOCKS_VERSION_5, version)
284 return command, address_type
287 def _method(self) -> int:
288 """Supported authentication method."""
289 return METHOD_USERNAME_PASSWORD
291 def _authenticate(self, methods: T.List[int]) -> bool:
292 """Perform client authentication."""
294 found = self._method in methods
295 choice = self._method if found else NO_METHODS
296 result = struct.pack("!BB", SOCKS_VERSION_5, choice)
298 log.debug("sending authentication result '%s'", result)
299 self.connection.sendall(result)
302 log.error("auth method not found in %s", methods)
305 if not self._read_creds():
306 log.error("could not verify credentials")
311 def _read_header(self) -> T.List[int]:
312 """Get the list of methods supported by the client."""
314 version, methods = struct.unpack("!BB", self.connection.recv(2))
315 check.equals(SOCKS_VERSION_5, version)
316 check.greater(methods, 0)
317 return [ord(self.connection.recv(1)) for _ in range(methods)]
319 def _read_creds(self) -> bool:
320 """Read and verify auth credentials."""
322 version = ord(self.connection.recv(1))
323 check.equals(1, version)
325 user_len = ord(self.connection.recv(1))
326 user = self.connection.recv(user_len).decode("utf-8")
328 passw_len = ord(self.connection.recv(1))
329 passw = self.connection.recv(passw_len).decode("utf-8")
331 log.info("got credentials '%s', '%s'", user, passw)
332 log.info("want credentials '%s', '%s'", USERNAME, PASSWORD)
334 passed = user == USERNAME and passw == PASSWORD
335 response = struct.pack("!BB", version, AUTH_OK if passed else AUTH_FAILURE)
336 self.connection.sendall(response)
341 class AnonymousSocks5Proxy(Socks5Proxy):
342 """SOCKS 5 server without authentication support."""
345 def _method(self) -> int:
348 def _read_creds(self) -> bool:
352 def init(ctx: Test) -> T.Tuple[Tinc, Tinc]:
353 """Create a new tinc node."""
355 foo, bar = ctx.node(), ctx.node()
358 set Address 127.0.0.1
366 set Address 127.0.0.1
375 def create_exec_proxy(port: int) -> str:
376 """Create a fake exec proxy program."""
380 import multiprocessing.connection as mp
382 with mp.Client(("127.0.0.1", {port}), family="AF_INET") as client:
383 client.send({{ **os.environ }})
386 file = tempfile.mktemp()
387 with open(file, "w", encoding="utf-8") as f:
393 def test_proxy(ctx: Test, handler: T.Type[ProxyServer], user="", passw="") -> None:
394 """Test socks proxy support."""
398 bar.add_script(foo.script_up)
399 bar.add_script(Script.TINC_UP)
402 cmd.exchange(foo, bar)
403 foo.cmd("set", f"{bar}.Port", str(bar.port))
405 with ThreadingTCPServer(("127.0.0.1", 0), handler) as server:
406 _, port = server.server_address
408 worker = Thread(target=server.serve_forever)
411 foo.cmd("set", "Proxy", handler.name, f"127.0.0.1 {port} {user} {passw}")
413 bar[foo.script_up].wait()
422 def test_proxy_exec(ctx: Test) -> None:
423 """Test that exec proxies work as expected."""
426 log.info("exec proxy without arguments fails")
427 foo.cmd("set", "Proxy", "exec")
428 _, stderr = foo.cmd("start", code=1)
429 check.is_in("Argument expected for proxy type", stderr)
431 log.info("exec proxy with correct arguments works")
433 cmd.exchange(foo, bar)
435 with mp.Listener(("127.0.0.1", 0), family="AF_INET") as listener:
436 port = int(listener.address[1])
437 proxy = create_exec_proxy(port)
439 foo.cmd("set", "Proxy", "exec", f"{path.PYTHON_PATH} {path.PYTHON_CMD} {proxy}")
442 with listener.accept() as conn:
443 env: T.Dict[str, str] = conn.recv()
445 for var in "NAME", "REMOTEADDRESS", "REMOTEPORT":
446 check.true(env.get(var))
448 for var in "NODE", "NETNAME":
456 with Test("exec proxy") as context:
457 test_proxy_exec(context)
459 with Test("HTTP CONNECT proxy") as context:
460 proxy_stats["tx"] = 0
461 test_proxy(context, HttpProxy)
462 check.greater(proxy_stats["tx"], 0)
464 with Test("socks4 proxy with username") as context:
465 proxy_stats["tx"] = 0
466 test_proxy(context, Socks4Proxy, USERNAME)
467 check.greater(proxy_stats["tx"], 0)
469 with Test("anonymous socks4 proxy") as context:
470 proxy_stats["tx"] = 0
471 test_proxy(context, AnonymousSocks4Proxy)
472 check.greater(proxy_stats["tx"], 0)
474 with Test("authenticated socks5 proxy") as context:
475 proxy_stats["tx"] = 0
476 test_proxy(context, Socks5Proxy, USERNAME, PASSWORD)
477 check.greater(proxy_stats["tx"], 0)
479 with Test("anonymous socks5 proxy") as context:
480 proxy_stats["tx"] = 0
481 test_proxy(context, AnonymousSocks5Proxy)
482 check.greater(proxy_stats["tx"], 0)