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()
|
ConnectionController.terminate_system_connection()
|
||||||
|
|
||||||
|
# ── 1. Import wg config ───────────────────────────────────────────────
|
||||||
try:
|
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:
|
except CalledProcessError:
|
||||||
raise ConnectionError('The connection could not be established.')
|
raise ConnectionError('The connection could not be established.')
|
||||||
|
|
||||||
|
# ── 2. Activate if nmcli import did not do it automatically (distro-dependent) ──
|
||||||
try:
|
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)
|
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:
|
except CalledProcessError:
|
||||||
raise ConnectionError('The connection could not be established.')
|
raise ConnectionError('The connection could not be established.')
|
||||||
|
|
||||||
if ipv6_method in ('disabled', 'ignore'):
|
if ipv6_method in ('disabled', 'ignore'):
|
||||||
|
|
||||||
try:
|
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:
|
except CalledProcessError:
|
||||||
raise ConnectionError('The connection could not be established.')
|
raise ConnectionError('The connection could not be established.')
|
||||||
|
|
||||||
try:
|
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:
|
except CalledProcessError:
|
||||||
raise ConnectionError('The connection could not be established.')
|
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:
|
try:
|
||||||
wg_server_ip = ConnectionController.__extract_wireguard_endpoint(profile)
|
wg_server_ip = ConnectionController.__extract_wireguard_endpoint(profile)
|
||||||
if wg_server_ip:
|
if wg_server_ip:
|
||||||
|
|
@ -563,6 +622,20 @@ class ConnectionController:
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
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)
|
token = SystemStateController.create(profile.id)
|
||||||
if connection_observer is not None:
|
if connection_observer is not None:
|
||||||
connection_observer.notify('connected_token', {'session_token': token})
|
connection_observer.notify('connected_token', {'session_token': token})
|
||||||
|
|
@ -573,6 +646,17 @@ class ConnectionController:
|
||||||
except ConnectionError:
|
except ConnectionError:
|
||||||
raise ConnectionError('The connection could not be established.')
|
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
|
@staticmethod
|
||||||
def __extract_wireguard_endpoint(profile):
|
def __extract_wireguard_endpoint(profile):
|
||||||
import re, socket
|
import re, socket
|
||||||
|
|
@ -606,6 +690,10 @@ class ConnectionController:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __test_connection(port_number: Optional[int] = None, timeout: float = 4.0):
|
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]
|
request_urls = [Constants.PING_URL]
|
||||||
proxies = None
|
proxies = None
|
||||||
|
|
||||||
|
|
@ -624,21 +712,32 @@ class ConnectionController:
|
||||||
|
|
||||||
for request_url in request_urls:
|
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:
|
try:
|
||||||
|
if proxies is None:
|
||||||
_response = subprocess.check_output(command, text=True, timeout=timeout)
|
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
|
return None
|
||||||
|
except (_requests.exceptions.RequestException, OSError):
|
||||||
except (subprocess.CalledProcessError, subprocess.TimeoutExpired):
|
pass
|
||||||
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
raise ConnectionError('The connection could not be established.')
|
raise ConnectionError('The connection could not be established.')
|
||||||
|
|
|
||||||
|
|
@ -24,15 +24,17 @@ class SystemProfile(BaseProfile):
|
||||||
super().save()
|
super().save()
|
||||||
|
|
||||||
def attach_wireguard_configuration(self, wireguard_configuration):
|
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'
|
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:
|
with open(wireguard_configuration_file_backup_path, 'w') as wireguard_configuration_file:
|
||||||
wireguard_configuration_file.write(wireguard_configuration)
|
wireguard_configuration_file.write(wireguard_configuration)
|
||||||
wireguard_configuration_is_attached = False
|
wireguard_configuration_is_attached = False
|
||||||
failed_attempt_count = 0
|
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:
|
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)
|
wireguard_configuration_is_attached = not bool(os.waitpid(process.pid, 0)[1] >> 8)
|
||||||
if not wireguard_configuration_is_attached:
|
if not wireguard_configuration_is_attached:
|
||||||
failed_attempt_count += 1
|
failed_attempt_count += 1
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue