fix(wireguard): multiplatform support for Arch/Ubuntu/Fedora
- Activate wg interface explicitly if nmcli import does not do it automatically (Arch) - Apply DNS via hydraveil-resolvectl wrapper instead of resolvectl directly (avoids polkit auth dialog) - Add __is_resolved_active() helper to check systemd-resolved before applying DNS - Wait for socket.getaddrinfo to confirm DNS is routable before await_connection() - Replace subprocess test_connection with requests direct call + socket.getaddrinfo (respects systemd-resolved on all distros) - Use sudo install instead of pkexec install for wg.conf copy (uses sudoers, more reliable) - Add fallback to pkexec if sudo not available
This commit is contained in:
parent
9c5636eb8c
commit
fd386c496f
2 changed files with 124 additions and 23 deletions
|
|
@ -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.')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue