from collections.abc import Callable from concurrent.futures import ThreadPoolExecutor, TimeoutError as FutureTimeoutError from core.Constants import Constants from core.Errors import InvalidSubscriptionError, MissingSubscriptionError, ConnectionUnprotectedError, ConnectionTerminationError, CommandNotFoundError, TorServiceInitializationError from core.controllers.ConfigurationController import ConfigurationController from core.controllers.ProfileController import ProfileController from core.controllers.SessionStateController import SessionStateController from core.controllers.SystemStateController import SystemStateController from core.models.session.SessionProfile import SessionProfile from core.models.system.SystemProfile import SystemProfile from core.models.system.SystemState import SystemState from core.observers import ConnectionObserver from core.services.WebServiceApiService import WebServiceApiService from pathlib import Path from subprocess import CalledProcessError from typing import Union, Optional, Any import os import psutil import random import re import shutil import socket import stem import stem.control import stem.process import subprocess import sys import tempfile import time class ConnectionController: @staticmethod def with_preferred_connection(*args, task: Callable[..., Any], connection_observer: Optional[ConnectionObserver] = None, **kwargs): connection = ConfigurationController.get_connection() if connection == 'system': return task(*args, **kwargs) elif connection == 'tor': return ConnectionController.__with_tor_connection(*args, task=task, connection_observer=connection_observer, **kwargs) else: return None @staticmethod def establish_connection(profile: Union[SessionProfile, SystemProfile], ignore: tuple[type[Exception]] = (), connection_observer: Optional[ConnectionObserver] = None): connection = profile.connection if connection.needs_proxy_configuration() and not profile.has_proxy_configuration(): if profile.has_subscription(): if not profile.subscription.has_been_activated(): ProfileController.activate_subscription(profile, connection_observer=connection_observer) proxy_configuration = ConnectionController.with_preferred_connection(profile.subscription.billing_code, task=WebServiceApiService.get_proxy_configuration, connection_observer=connection_observer) if proxy_configuration is None: raise InvalidSubscriptionError() profile.attach_proxy_configuration(proxy_configuration) else: raise MissingSubscriptionError() if connection.needs_wireguard_configuration() and not profile.has_wireguard_configuration(): if profile.has_subscription(): if not profile.subscription.has_been_activated(): ProfileController.activate_subscription(profile, connection_observer=connection_observer) ProfileController.register_wireguard_session(profile, connection_observer=connection_observer) else: if profile.is_system_profile(): if ConnectionController.system_uses_wireguard_interface() and SystemStateController.exists(): try: ConnectionController.terminate_system_connection() except ConnectionTerminationError: pass raise MissingSubscriptionError() if profile.is_session_profile(): try: return ConnectionController.establish_session_connection(profile, ignore=ignore, connection_observer=connection_observer) except ConnectionError: if ConnectionController.__should_renegotiate(profile): ProfileController.register_wireguard_session(profile, connection_observer=connection_observer) return ConnectionController.establish_session_connection(profile, ignore=ignore, connection_observer=connection_observer) else: raise ConnectionError('The connection could not be established.') if profile.is_system_profile(): try: return ConnectionController.establish_system_connection(profile, ignore=ignore, connection_observer=connection_observer) except ConnectionError: if ConnectionController.__should_renegotiate(profile): ProfileController.register_wireguard_session(profile, connection_observer=connection_observer) return ConnectionController.establish_system_connection(profile, ignore=ignore, connection_observer=connection_observer) else: raise ConnectionError('The connection could not be established.') return None @staticmethod def establish_session_connection(profile: SessionProfile, ignore: tuple[type[Exception]] = (), connection_observer: Optional[ConnectionObserver] = None): session_directory = tempfile.mkdtemp(prefix='hv-') session_state = SessionStateController.get_or_new(profile.id) port_number = None proxy_port_number = None if profile.connection.is_unprotected(): if not ConnectionController.system_uses_wireguard_interface(): if not ConnectionUnprotectedError in ignore: raise ConnectionUnprotectedError('Connection unprotected while the system is not using a WireGuard interface.') else: ProfileController.disable(profile) if profile.connection.code == 'tor': port_number = ConnectionController.get_random_available_port_number() ConnectionController.establish_tor_session_connection(port_number, connection_observer=connection_observer) session_state.network_port_numbers.tor.append(port_number) elif profile.connection.code == 'wireguard': if ConfigurationController.get_endpoint_verification_enabled(): ProfileController.verify_wireguard_endpoint(profile, ignore=ignore) port_number = ConnectionController.get_random_available_port_number() ConnectionController.establish_wireguard_session_connection(profile, session_directory, port_number) session_state.network_port_numbers.wireguard.append(port_number) if profile.connection.masked: while proxy_port_number is None or proxy_port_number == port_number: proxy_port_number = ConnectionController.get_random_available_port_number() ConnectionController.establish_proxy_session_connection(profile, session_directory, port_number, proxy_port_number) session_state.network_port_numbers.proxy.append(proxy_port_number) if not profile.connection.is_unprotected(): ConnectionController.await_connection(proxy_port_number or port_number, connection_observer=connection_observer) SessionStateController.update_or_create(session_state) return proxy_port_number or port_number @staticmethod def establish_system_connection(profile: SystemProfile, ignore: tuple[type[Exception]] = (), connection_observer: Optional[ConnectionObserver] = None): if ConfigurationController.get_endpoint_verification_enabled(): ProfileController.verify_wireguard_endpoint(profile, ignore=ignore) try: ConnectionController.__establish_system_connection(profile, connection_observer) except ConnectionError: try: ConnectionController.terminate_system_connection() except ConnectionTerminationError: pass raise ConnectionError('The connection could not be established.') except CalledProcessError: try: ConnectionController.terminate_system_connection() except ConnectionTerminationError: pass try: ConnectionController.__establish_system_connection(profile, connection_observer) except (ConnectionError, CalledProcessError): try: ConnectionController.terminate_system_connection() except ConnectionTerminationError: pass raise ConnectionError('The connection could not be established.') time.sleep(1.0) @staticmethod def establish_tor_session_connection(port_number: int, connection_observer: Optional[ConnectionObserver] = None): try: controller = stem.control.Controller.from_socket_file(Constants.HV_TOR_CONTROL_SOCKET_PATH) controller.authenticate() except (FileNotFoundError, stem.SocketError, TypeError, IndexError): ConnectionController.establish_tor_connection(connection_observer=connection_observer) controller = stem.control.Controller.from_socket_file(Constants.HV_TOR_CONTROL_SOCKET_PATH) controller.authenticate() socks_port_numbers = [str(port_number) for port_number in controller.get_ports('socks')] socks_port_numbers.append(str(port_number)) controller.set_conf('SocksPort', socks_port_numbers) @staticmethod def terminate_tor_session_connection(port_number: int): try: controller = stem.control.Controller.from_socket_file(Constants.HV_TOR_CONTROL_SOCKET_PATH) controller.authenticate() socks_port_numbers = [str(port_number) for port_number in controller.get_ports('socks')] if len(socks_port_numbers) > 1: socks_port_numbers = [socks_port_number for socks_port_number in socks_port_numbers if socks_port_number != port_number] controller.set_conf('SocksPort', socks_port_numbers) else: controller.set_conf('SocksPort', '0') except (FileNotFoundError, stem.SocketError, TypeError, IndexError): pass @staticmethod def establish_tor_connection(connection_observer: Optional[ConnectionObserver] = None): Path(Constants.HV_TOR_SESSION_STATE_HOME).mkdir(exist_ok=True, mode=0o700) ConnectionController.terminate_tor_connection() if connection_observer is not None: connection_observer.notify('tor_bootstrapping') with ThreadPoolExecutor(max_workers=1) as executor: future = executor.submit( stem.process.launch_tor_with_config, config={ 'DataDirectory': Constants.HV_TOR_SESSION_STATE_HOME, 'ControlSocket': Constants.HV_TOR_CONTROL_SOCKET_PATH, 'PIDFile': Constants.HV_TOR_PROCESS_IDENTIFIER_PATH, 'SocksPort': '0' }, init_msg_handler=lambda contents: ConnectionController.__on_tor_initialization_message(contents, connection_observer) ) try: future.result(timeout=Constants.TOR_BOOTSTRAP_TIMEOUT) except FutureTimeoutError: ConnectionController.terminate_tor_connection() raise TorServiceInitializationError('The dedicated Tor service could not be initialized.') if connection_observer is not None: connection_observer.notify('tor_bootstrapped') try: controller = stem.control.Controller.from_socket_file(Constants.HV_TOR_CONTROL_SOCKET_PATH) controller.authenticate() except (FileNotFoundError, stem.SocketError, TypeError, IndexError): raise TorServiceInitializationError('The dedicated Tor service could not be initialized.') for session_state in SessionStateController.all(): for port_number in session_state.network_port_numbers.tor: ConnectionController.establish_tor_session_connection(port_number) @staticmethod def terminate_tor_connection(): process_identifier_file = Path(Constants.HV_TOR_PROCESS_IDENTIFIER_PATH) control_socket_file = Path(Constants.HV_TOR_CONTROL_SOCKET_PATH) try: process_identifier = int(process_identifier_file.read_text().strip()) except (OSError, ValueError): process_identifier = None if process_identifier is not None: try: process = psutil.Process(process_identifier) if process.is_running(): process.terminate() except psutil.NoSuchProcess: pass process_identifier_file.unlink(missing_ok=True) control_socket_file.unlink(missing_ok=True) @staticmethod def establish_wireguard_session_connection(profile: SessionProfile, session_directory: str, port_number: int): if not profile.has_wireguard_configuration(): raise FileNotFoundError('No valid WireGuard configuration file detected.') wireguard_session_directory = f'{session_directory}/wireguard' Path(wireguard_session_directory).mkdir(exist_ok=True, mode=0o700) wireproxy_configuration_file_path = f'{wireguard_session_directory}/wireproxy.conf' Path(wireproxy_configuration_file_path).touch(exist_ok=True, mode=0o600) with open(wireproxy_configuration_file_path, 'w') as wireproxy_configuration_file: wireproxy_configuration_file.write(f'WGConfig = {profile.get_wireguard_configuration_path()}\n\n[Socks5]\nBindAddress = 127.0.0.1:{str(port_number)}\n') wireproxy_configuration_file.close() return subprocess.Popen((f'{Constants.HV_RUNTIME_DATA_HOME}/wireproxy/wireproxy', '-c', wireproxy_configuration_file_path), stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT) @staticmethod def establish_proxy_session_connection(profile: SessionProfile, session_directory: str, port_number: int, proxy_port_number: int): if shutil.which('proxychains4') is None: raise CommandNotFoundError('proxychains4') if shutil.which('microsocks') is None: raise CommandNotFoundError('microsocks') if profile.has_proxy_configuration(): proxy_configuration = profile.get_proxy_configuration() else: raise FileNotFoundError('No valid proxy configuration file detected.') proxy_session_directory = f'{session_directory}/proxy' Path(proxy_session_directory).mkdir(parents=True, exist_ok=True, mode=0o700) proxychains_proxy_list = '' if port_number is not None: proxychains_proxy_list = f'socks5 127.0.0.1 {port_number}\n' proxychains_proxy_list = proxychains_proxy_list + f'socks5 {proxy_configuration.ip_address} {proxy_configuration.port_number} {proxy_configuration.username} {proxy_configuration.password}' proxychains_template_file_path = f'{Constants.HV_RUNTIME_DATA_HOME}/proxychains.ptpl' with open(proxychains_template_file_path, 'r') as proxychains_template_file: proxychains_configuration_file_path = f'{proxy_session_directory}/proxychains.conf' Path(proxychains_configuration_file_path).touch(exist_ok=True, mode=0o600) proxychains_configuration_file_contents = proxychains_template_file.read().format(proxy_list=proxychains_proxy_list) with open(proxychains_configuration_file_path, 'w') as proxychains_configuration_file: proxychains_configuration_file.write(proxychains_configuration_file_contents) proxychains_configuration_file.close() return subprocess.Popen(('proxychains4', '-f', proxychains_configuration_file_path, 'microsocks', '-p', str(proxy_port_number)), stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT) @staticmethod def terminate_system_connection(): if shutil.which('nmcli') is None: raise CommandNotFoundError('nmcli') if SystemStateController.exists(): process = subprocess.Popen(('nmcli', 'connection', 'delete', 'wg'), stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT) completed_successfully = not bool(os.waitpid(process.pid, 0)[1] >> 8) if completed_successfully or not ConnectionController.system_uses_wireguard_interface(): subprocess.run(('nmcli', 'connection', 'delete', 'hv-ipv6-sink'), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) SystemState.dissolve() else: raise ConnectionTerminationError('The connection could not be terminated.') @staticmethod def get_proxies(port_number: int): return dict( http=f'socks5h://127.0.0.1:{port_number}', https=f'socks5h://127.0.0.1:{port_number}' ) @staticmethod def get_random_available_port_number(): socket_instance = socket.socket() socket_instance.bind(('', 0)) port_number = socket_instance.getsockname()[1] socket_instance.close() return port_number @staticmethod def await_connection(port_number: Optional[int] = None, connection_observer: Optional[ConnectionObserver] = None): if port_number is None: ConnectionController.await_network_interface() for retry_count in range(Constants.MAX_CONNECTION_ATTEMPTS): if connection_observer is not None: connection_observer.notify('connecting', dict( retry_interval=Constants.CONNECTION_RETRY_INTERVAL, maximum_number_of_attempts=Constants.MAX_CONNECTION_ATTEMPTS, attempt_count=retry_count + 1 )) try: ConnectionController.__test_connection(port_number) return except ConnectionError: time.sleep(Constants.CONNECTION_RETRY_INTERVAL) retry_count += 1 raise ConnectionError('The connection could not be established.') @staticmethod def await_network_interface(): network_interface_is_activated = False retry_interval = .5 maximum_number_of_attempts = 10 attempt = 0 while not network_interface_is_activated and attempt < maximum_number_of_attempts: time.sleep(retry_interval) network_interface_is_activated = ConnectionController.system_uses_wireguard_interface() attempt += 1 if not network_interface_is_activated: raise ConnectionError('The network interface could not be activated.') @staticmethod def system_uses_wireguard_interface(): if shutil.which('ip') is None: raise CommandNotFoundError('ip') process = subprocess.Popen(('ip', 'route', 'get', '192.0.2.1'), stdout=subprocess.PIPE) process_output = str(process.stdout.read()) return bool(re.search('dev wg', str(process_output))) @staticmethod def __establish_system_connection(profile: SystemProfile, connection_observer: Optional[ConnectionObserver] = None): if shutil.which('nmcli') is None: raise CommandNotFoundError('nmcli') ConnectionController.terminate_system_connection() try: process_output = subprocess.check_output(('nmcli', 'connection', 'import', '--temporary', 'type', 'wireguard', 'file', profile.get_wireguard_configuration_path()), text=True) except CalledProcessError as exception: raise CalledProcessError(exception.returncode, 'nmcli') connection_id = (m := re.search(r'(?<=\()([a-f0-9-]+?)(?=\))', process_output)) and m.group(1) subprocess.run(('nmcli', 'connection', 'modify', connection_id, 'ipv4.dns-priority', '-1750', 'ipv4.ignore-auto-dns', 'yes'), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) try: ipv6_method = subprocess.check_output(('nmcli', '-g', 'ipv6.method', 'connection', 'show', connection_id), text=True).strip() except CalledProcessError: raise ConnectionError('The connection could not be established.') if ipv6_method in ('disabled', 'ignore'): subprocess.run(('nmcli', 'connection', 'add', 'type', 'dummy', 'save', 'no', 'con-name', 'hv-ipv6-sink', 'ifname', 'hvipv6sink0', 'ipv6.method', 'manual', 'ipv6.addresses', 'fd7a:fd4b:54e3:077c::/64', 'ipv6.gateway', 'fd7a:fd4b:54e3:077c::1', 'ipv6.route-metric', '72'), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) SystemStateController.create(profile.id) try: ConnectionController.await_connection(connection_observer=connection_observer) except ConnectionError: raise ConnectionError('The connection could not be established.') @staticmethod def __with_tor_connection(*args, task: Callable[..., Any], connection_observer: Optional[ConnectionObserver] = None, **kwargs): port_number = ConnectionController.get_random_available_port_number() ConnectionController.establish_tor_session_connection(port_number, connection_observer=connection_observer) ConnectionController.await_connection(port_number, connection_observer=connection_observer) task_output = task(*args, proxies=ConnectionController.get_proxies(port_number), **kwargs) ConnectionController.terminate_tor_session_connection(port_number) return task_output @staticmethod def __test_connection(port_number: Optional[int] = None, timeout: float = 4.0): request_urls = [Constants.PING_URL] proxies = None if os.environ.get('PING_URL') is None: request_urls.extend([ 'https://hc1.simplifiedprivacy.net', 'https://hc2.simplifiedprivacy.org', 'https://hc3.hydraveil.net' ]) random.shuffle(request_urls) if port_number is not None: proxies = ConnectionController.get_proxies(port_number) for request_url in request_urls: command = [ sys.executable, '-u', '-c', 'import requests, sys\n' 'try:\n' f' response = requests.get(\'{request_url}\', proxies={proxies}, timeout={timeout})\n' ' response.raise_for_status(); print(response.text)\n' 'except requests.exceptions.RequestException:\n' ' sys.exit(1)' ] try: _response = subprocess.check_output(command, text=True, timeout=timeout) return None except (subprocess.CalledProcessError, subprocess.TimeoutExpired): pass raise ConnectionError('The connection could not be established.') @staticmethod def __should_renegotiate(profile: Union[SessionProfile, SystemProfile]): if not profile.has_subscription(): raise MissingSubscriptionError() if profile.connection.needs_wireguard_configuration() and profile.has_wireguard_configuration(): if profile.subscription.has_been_activated(): return True return False @staticmethod def __on_tor_initialization_message(contents, connection_observer: Optional[ConnectionObserver] = None): if connection_observer is not None: if 'Bootstrapped ' in contents: progress = (m := re.search(r' (\d{1,3})% ', contents)) and int(m.group(1)) connection_observer.notify('tor_bootstrap_progressing', None, dict( progress=progress ))