diff --git a/core/controllers/ConnectionController.py b/core/controllers/ConnectionController.py index 5f35851..2104bcd 100644 --- a/core/controllers/ConnectionController.py +++ b/core/controllers/ConnectionController.py @@ -529,32 +529,91 @@ class ConnectionController: ConnectionController.terminate_system_connection() + # ── 1. Import wg config ─────────────────────────────────────────────── try: - process_output = subprocess.check_output(('nmcli', 'connection', 'import', '--temporary', 'type', 'wireguard', 'file', profile.get_wireguard_configuration_path()), text=True) + process_output = subprocess.check_output( + ('nmcli', 'connection', 'import', '--temporary', 'type', 'wireguard', 'file', + profile.get_wireguard_configuration_path()), text=True + ) except CalledProcessError: raise ConnectionError('The connection could not be established.') + # ── 2. Activate if nmcli import did not do it automatically (distro-dependent) ── try: + wg_up = subprocess.run(('ip', 'link', 'show', 'wg'), capture_output=True) + if wg_up.returncode != 0: + subprocess.check_output(('nmcli', 'connection', 'up', 'wg'), text=True) + for _ in range(10): + time.sleep(0.5) + if subprocess.run(('ip', 'link', 'show', 'wg'), capture_output=True).returncode == 0: + break + else: + raise ConnectionError('The connection could not be established.') + except CalledProcessError: + raise ConnectionError('The connection could not be established.') + # ── 3. IPv6 method check and sink ──────────────────────────────────── + try: connection_id = (m := re.search(r'(?<=\()([a-f0-9-]+?)(?=\))', process_output)) and m.group(1) - ipv6_method = subprocess.check_output(('nmcli', '-g', 'ipv6.method', 'connection', 'show', connection_id), text=True).strip() - + 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'): - try: - subprocess.run(('dbus-send', '--system', '--print-reply', '--dest=org.freedesktop.NetworkManager', '/org/freedesktop/NetworkManager', 'org.freedesktop.DBus.Properties.Set', 'string:org.freedesktop.NetworkManager', 'string:ConnectivityCheckEnabled', 'variant:boolean:false'), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True) + subprocess.run( + ('dbus-send', '--system', '--print-reply', + '--dest=org.freedesktop.NetworkManager', '/org/freedesktop/NetworkManager', + 'org.freedesktop.DBus.Properties.Set', + 'string:org.freedesktop.NetworkManager', + 'string:ConnectivityCheckEnabled', 'variant:boolean:false'), + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True + ) except CalledProcessError: raise ConnectionError('The connection could not be established.') try: - 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.dns', '::1', 'ipv6.route-metric', '72'), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True) + 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.dns', '::1', 'ipv6.route-metric', '72'), + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True + ) except CalledProcessError: raise ConnectionError('The connection could not be established.') - # Arm killswitch for WireGuard + + # ── 4. Apply DNS via resolvectl wrapper (handles polkit correctly) ─── + try: + if ConnectionController.__is_resolved_active(): + wg_config = profile.get_wireguard_configuration() + if wg_config: + dns_match = re.search(r'^DNS\s*=\s*(.+)$', wg_config, re.MULTILINE) + if dns_match: + dns_server = dns_match.group(1).split(',')[0].strip() + subprocess.run( + ('sudo', Constants.RESOLVECTL_WRAPPER, 'set', dns_server, 'wg'), + check=False, timeout=5, + env={**os.environ, 'SUDO_ASKPASS': '/bin/false'}, + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL + ) + deadline = time.monotonic() + 8.0 + while time.monotonic() < deadline: + result = subprocess.run( + ('resolvectl', 'status', 'wg'), + capture_output=True, text=True, timeout=2 + ) + if 'Current DNS Server' in result.stdout: + break + time.sleep(0.3) + except Exception: + pass + # ── 5. Arm killswitch ───────────────────────────────────────────────── try: wg_server_ip = ConnectionController.__extract_wireguard_endpoint(profile) if wg_server_ip: @@ -563,6 +622,20 @@ class ConnectionController: except Exception: pass + # Wait for tunnel to be fully routable before testing connectivity + import socket as _socket + deadline = time.monotonic() + 10.0 + tunnel_ready = False + while time.monotonic() < deadline: + try: + _socket.getaddrinfo('hc1.simplifiedprivacy.net', 443, _socket.AF_INET, _socket.SOCK_STREAM) + tunnel_ready = True + break + except (_socket.gaierror, OSError): + time.sleep(0.5) + if not tunnel_ready: + raise ConnectionError('The connection could not be established.') + token = SystemStateController.create(profile.id) if connection_observer is not None: connection_observer.notify('connected_token', {'session_token': token}) @@ -573,6 +646,17 @@ class ConnectionController: except ConnectionError: raise ConnectionError('The connection could not be established.') + @staticmethod + def __is_resolved_active() -> bool: + try: + result = subprocess.run( + ('systemctl', 'is-active', 'systemd-resolved'), + capture_output=True, text=True, timeout=5 + ) + return result.stdout.strip() == 'active' + except Exception: + return False + @staticmethod def __extract_wireguard_endpoint(profile): import re, socket @@ -606,6 +690,10 @@ class ConnectionController: @staticmethod def __test_connection(port_number: Optional[int] = None, timeout: float = 4.0): + import requests as _requests + import socket as _socket + from urllib.parse import urlparse as _urlparse + request_urls = [Constants.PING_URL] proxies = None @@ -624,21 +712,32 @@ class ConnectionController: 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) + if proxies is None: + parsed = _urlparse(request_url) + hostname = parsed.hostname + port = parsed.port or (443 if parsed.scheme == 'https' else 80) + try: + results = _socket.getaddrinfo(hostname, port, _socket.AF_INET, _socket.SOCK_STREAM) + if not results: + continue + resolved_ip = results[0][4][0] + resolved_url = request_url.replace(hostname, resolved_ip, 1) + response = _requests.get( + resolved_url, + timeout=timeout, + headers={'Host': hostname}, + verify=False + ) + except (_socket.gaierror, OSError): + continue + else: + response = _requests.get(request_url, proxies=proxies, timeout=timeout) + response.raise_for_status() return None - - except (subprocess.CalledProcessError, subprocess.TimeoutExpired): + except (_requests.exceptions.RequestException, OSError): + pass + except Exception: pass raise ConnectionError('The connection could not be established.') diff --git a/core/models/system/SystemProfile.py b/core/models/system/SystemProfile.py index 0a1de60..c0b8a8e 100644 --- a/core/models/system/SystemProfile.py +++ b/core/models/system/SystemProfile.py @@ -24,15 +24,17 @@ class SystemProfile(BaseProfile): super().save() def attach_wireguard_configuration(self, wireguard_configuration): - if shutil.which('pkexec') is None: - raise CommandNotFoundError('pkexec') wireguard_configuration_file_backup_path = f'{self.get_config_path()}/wg.conf.bak' with open(wireguard_configuration_file_backup_path, 'w') as wireguard_configuration_file: wireguard_configuration_file.write(wireguard_configuration) wireguard_configuration_is_attached = False failed_attempt_count = 0 + # Try sudo first (configured via sudoers by installer), fall back to pkexec + install_cmd = 'sudo' if shutil.which('sudo') else 'pkexec' + if install_cmd == 'pkexec' and shutil.which('pkexec') is None: + raise CommandNotFoundError('pkexec') while not wireguard_configuration_is_attached and failed_attempt_count < 3: - process = subprocess.Popen(('pkexec', 'install', '-D', wireguard_configuration_file_backup_path, self.get_wireguard_configuration_path(), '-o', 'root', '-m', '744')) + process = subprocess.Popen((install_cmd, 'install', '-D', wireguard_configuration_file_backup_path, self.get_wireguard_configuration_path(), '-o', 'root', '-m', '744')) wireguard_configuration_is_attached = not bool(os.waitpid(process.pid, 0)[1] >> 8) if not wireguard_configuration_is_attached: failed_attempt_count += 1