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:
Zenaku 2026-07-31 05:11:55 -05:00
parent 9c5636eb8c
commit fd386c496f
2 changed files with 124 additions and 23 deletions

View file

@ -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.')

View file

@ -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