Compare commits

...

5 commits

14 changed files with 923 additions and 299 deletions

View file

@ -1,8 +1,21 @@
# Major Change Log: # Major Change Log:
# 2.4.2
### Surgery on ConnectionController
Huge separation of ConnectionController & ProfileController into naked function modules for enabling a connection, registering wireguard keys, coordinating proxy configs, subscriptions, endpoint verification, and the logistical flow. Additionally enums were used for type checking, instead of polymorphic functions. This version is stable and confirmed working for systemwide and session wireguard, but not yet proxies.
<br/>
# 2.4.1
### Enums for Profile types.
July 18, 2026
Introduce Base profile types on load, which gradually will switch over on save/edits. Transition Connections to using Enum instead of raw strings. End goal here is to transition ConnectionController to use the Enums.
<br/>
# 2.4.0 # 2.4.0
July 15, 2026 July 15, 2026
Robust Database Error Handling & Migrations System. For New Client upgrades (Operational Errors) & Old Clients with New JSON keys from an API (TypeErrors). We also added an SQL generic handling wrapper for use across any project or function. Robust Database Error Handling & Migrations System. For New Client upgrades (Operational Errors) & Old Clients with New JSON keys from an API (TypeErrors). We also added an SQL generic handling wrapper for use across any project or function.
<br/>
# 2.3.9 # 2.3.9
July 11, 2026 July 11, 2026

View file

@ -1,13 +1,19 @@
from core.errors.exceptions import * from core.errors.exceptions import *
from core.errors.logger import logger from core.errors.logger import logger
from core.services.networking.general_connection_tools.testing_evaluating import await_connection, system_uses_wireguard_interface, await_network_interface
from core.services.networking.systemwide.systemwide_wireguard import establish_system_connection, terminate_system_connection
from core.services.keys_and_verifications.endpoint_verification import verify_wireguard_endpoint
from collections.abc import Callable from collections.abc import Callable
from core.Constants import Constants from core.Constants import Constants
from core.Errors import InvalidSubscriptionError, MissingSubscriptionError, ConnectionUnprotectedError, ConnectionTerminationError, CommandNotFoundError from core.Errors import InvalidSubscriptionError, MissingSubscriptionError, ConnectionUnprotectedError, ConnectionTerminationError, CommandNotFoundError
from core.controllers.ConfigurationController import ConfigurationController from core.controllers.ConfigurationController import ConfigurationController
from core.controllers.ProfileController import ProfileController # from core.controllers.ProfileController import ProfileController
from core.controllers.SessionStateController import SessionStateController from core.controllers.SessionStateController import SessionStateController
from core.controllers.SystemStateController import SystemStateController from core.controllers.SystemStateController import SystemStateController
from core.models.BaseProfile import ProfileType
from core.models.session.SessionProfile import SessionProfile from core.models.session.SessionProfile import SessionProfile
from core.models.system.SystemProfile import SystemProfile from core.models.system.SystemProfile import SystemProfile
from core.models.system.SystemState import SystemState from core.models.system.SystemState import SystemState
@ -20,12 +26,15 @@ from subprocess import CalledProcessError
from typing import Union, Optional, Any from typing import Union, Optional, Any
import os import os
import random import random
import re
# import re
import shutil import shutil
import subprocess import subprocess
import sys # import sys
import tempfile import tempfile
import time import time
from enum import Enum
class ConnectionController: class ConnectionController:
@ -51,85 +60,87 @@ class ConnectionController:
else: else:
return None return None
@staticmethod # @staticmethod
def establish_connection(profile: Union[SessionProfile, SystemProfile], ignore: tuple[type[Exception]] = (), connection_observer: Optional[ConnectionObserver] = None): # def establish_connection(profile: Union[SessionProfile, SystemProfile], ignore: tuple[type[Exception]] = (), connection_observer: Optional[ConnectionObserver] = None):
connection = profile.connection # connection = profile.connection
# needs_proxy_configuration comes from the child connection models # # needs_proxy_configuration comes from the child connection models
# for the system it returns false blindly. for the session is checks if the connection type is masked. # # for the system it returns false blindly. for the session is checks if the connection type is masked.
if connection.needs_proxy_configuration() and not profile.has_proxy_configuration(): # # if connection.masked and not profile.has_proxy_configuration():
# if connection.needs_proxy_configuration() and not profile.has_proxy_configuration():
if profile.has_subscription(): # if profile.has_subscription():
if not profile.subscription.has_been_activated(): # if not profile.subscription.has_been_activated():
ProfileController.activate_subscription(profile, connection_observer=connection_observer) # 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) # proxy_configuration = ConnectionController.with_preferred_connection(profile.subscription.billing_code, task=WebServiceApiService.get_proxy_configuration, connection_observer=connection_observer)
if proxy_configuration is None: # if proxy_configuration is None:
raise InvalidSubscriptionError() # raise InvalidSubscriptionError()
profile.attach_proxy_configuration(proxy_configuration) # profile.attach_proxy_configuration(proxy_configuration)
else: # else:
raise MissingSubscriptionError() # raise MissingSubscriptionError()
# The needs_wireguard_configuration comes from the connection model, and can be transitioned to get it directly from the object's data # # The needs_wireguard_configuration comes from the connection model,
# The has_wireguard_configuration comes from each polymorph object doing an os check on if the wg config exists # # and can be transitioned to get it directly from the object's data
if connection.needs_wireguard_configuration() and not profile.has_wireguard_configuration(): # # The has_wireguard_configuration comes from each polymorph object doing an os check on if the wg config exists
# if connection.needs_wireguard_configuration() and not profile.has_wireguard_configuration():
if profile.has_subscription(): # if profile.has_subscription():
if not profile.subscription.has_been_activated(): # if not profile.subscription.has_been_activated():
ProfileController.activate_subscription(profile, connection_observer=connection_observer) # ProfileController.activate_subscription(profile, connection_observer=connection_observer)
ProfileController.register_wireguard_session(profile, connection_observer=connection_observer) # ProfileController.register_wireguard_session(profile, connection_observer=connection_observer)
else: # else:
if profile.is_system_profile(): # if profile.is_system_profile():
if ConnectionController.system_uses_wireguard_interface() and SystemStateController.exists(): # if system_uses_wireguard_interface() and SystemStateController.exists():
try: # try:
ConnectionController.terminate_system_connection() # terminate_system_connection()
except ConnectionTerminationError: # except ConnectionTerminationError:
pass # pass
raise MissingSubscriptionError() # raise MissingSubscriptionError()
if profile.is_session_profile(): # if profile.is_session_profile():
try: # try:
return ConnectionController.establish_session_connection(profile, ignore=ignore, connection_observer=connection_observer) # return ConnectionController.establish_session_connection(profile, ignore=ignore, connection_observer=connection_observer)
except ConnectionError: # except ConnectionError:
if ConnectionController.__should_renegotiate(profile): # if ConnectionController.__should_renegotiate(profile):
ProfileController.register_wireguard_session(profile, connection_observer=connection_observer) # ProfileController.register_wireguard_session(profile, connection_observer=connection_observer)
return ConnectionController.establish_session_connection(profile, ignore=ignore, connection_observer=connection_observer) # return ConnectionController.establish_session_connection(profile, ignore=ignore, connection_observer=connection_observer)
else: # else:
raise ConnectionError('The connection could not be established.') # raise ConnectionError('The connection could not be established.')
if profile.is_system_profile(): # if profile.is_system_profile():
try: # try:
return ConnectionController.establish_system_connection(profile, ignore=ignore, connection_observer=connection_observer) # return establish_system_connection(profile, ignore=ignore, connection_observer=connection_observer)
except ConnectionError: # except ConnectionError:
if ConnectionController.__should_renegotiate(profile): # if ConnectionController.__should_renegotiate(profile):
ProfileController.register_wireguard_session(profile, connection_observer=connection_observer) # ProfileController.register_wireguard_session(profile, connection_observer=connection_observer)
return ConnectionController.establish_system_connection(profile, ignore=ignore, connection_observer=connection_observer) # return establish_system_connection(profile, ignore=ignore, connection_observer=connection_observer)
else: # else:
raise ConnectionError('The connection could not be established.') # raise ConnectionError('The connection could not be established.')
return None # return None
@staticmethod @staticmethod
def establish_session_connection(profile: SessionProfile, ignore: tuple[type[Exception]] = (), connection_observer: Optional[ConnectionObserver] = None): def establish_session_connection(profile: SessionProfile, ignore: tuple[type[Exception]] = (), connection_observer: Optional[ConnectionObserver] = None):
@ -143,11 +154,12 @@ class ConnectionController:
# this is a check from SessionConnection of if there's a systemwide with mask # this is a check from SessionConnection of if there's a systemwide with mask
if profile.connection.is_unprotected(): if profile.connection.is_unprotected():
if not ConnectionController.system_uses_wireguard_interface(): if not system_uses_wireguard_interface():
if not ConnectionUnprotectedError in ignore: if not ConnectionUnprotectedError in ignore:
raise ConnectionUnprotectedError('Connection unprotected while the system is not using a WireGuard interface.') raise ConnectionUnprotectedError('Connection unprotected while the system is not using a WireGuard interface.')
else: else:
from core.controllers.ProfileController import ProfileController
ProfileController.disable(profile) ProfileController.disable(profile)
if profile.connection.code == 'tor': if profile.connection.code == 'tor':
@ -160,7 +172,7 @@ class ConnectionController:
elif profile.connection.code == 'wireguard': elif profile.connection.code == 'wireguard':
if ConfigurationController.get_endpoint_verification_enabled(): if ConfigurationController.get_endpoint_verification_enabled():
ProfileController.verify_wireguard_endpoint(profile, ignore=ignore) verify_wireguard_endpoint(profile, ignore=ignore)
port_number = ConnectionService.get_random_available_port_number() port_number = ConnectionService.get_random_available_port_number()
ConnectionController.establish_wireguard_session_connection(profile, session_directory, port_number) ConnectionController.establish_wireguard_session_connection(profile, session_directory, port_number)
@ -175,57 +187,51 @@ class ConnectionController:
session_state.network_port_numbers.proxy.append(proxy_port_number) session_state.network_port_numbers.proxy.append(proxy_port_number)
if not profile.connection.is_unprotected(): if not profile.connection.is_unprotected():
ConnectionController.await_connection(proxy_port_number or port_number, connection_observer=connection_observer) await_connection(proxy_port_number or port_number, connection_observer=connection_observer)
SessionStateController.update_or_create(session_state) SessionStateController.update_or_create(session_state)
return proxy_port_number or port_number return proxy_port_number or port_number
@staticmethod # @staticmethod
def establish_system_connection(profile: SystemProfile, ignore: tuple[type[Exception]] = (), connection_observer: Optional[ConnectionObserver] = None): # def establish_system_connection(profile: SystemProfile, ignore: tuple[type[Exception]] = (), connection_observer: Optional[ConnectionObserver] = None):
if ConfigurationController.get_endpoint_verification_enabled(): # if ConfigurationController.get_endpoint_verification_enabled():
ProfileController.verify_wireguard_endpoint(profile, ignore=ignore) # ProfileController.verify_wireguard_endpoint(profile, ignore=ignore)
try: # try:
ConnectionController.__establish_system_connection(profile, connection_observer) # ConnectionController.__establish_system_connection(profile, connection_observer)
except ConnectionError: # except ConnectionError:
try: # try:
ConnectionController.terminate_system_connection() # ConnectionController.terminate_system_connection()
except ConnectionTerminationError: # except ConnectionTerminationError:
pass # pass
raise ConnectionError('The connection could not be established.') # raise ConnectionError('The connection could not be established.')
except CalledProcessError: # except CalledProcessError:
try: # try:
ConnectionController.terminate_system_connection() # ConnectionController.terminate_system_connection()
except ConnectionTerminationError: # except ConnectionTerminationError:
pass # pass
try: # try:
ConnectionController.__establish_system_connection(profile, connection_observer) # ConnectionController.__establish_system_connection(profile, connection_observer)
except (ConnectionError, CalledProcessError): # except (ConnectionError, CalledProcessError):
try: # try:
ConnectionController.terminate_system_connection() # ConnectionController.terminate_system_connection()
except ConnectionTerminationError: # except ConnectionTerminationError:
pass # pass
raise ConnectionError('The connection could not be established.') # raise ConnectionError('The connection could not be established.')
ConnectionController.terminate_tor_connection() # ConnectionController.terminate_tor_connection()
time.sleep(1.0) # time.sleep(1.0)
@staticmethod
def test_observer(connection_observer: Optional[ConnectionObserver] = None):
if connection_observer is not None:
connection_observer.notify('custom_message', "HELLO")
@staticmethod @staticmethod
@ -242,11 +248,11 @@ class ConnectionController:
if connection_observer is not None: if connection_observer is not None:
connection_observer.notify('custom_message', "Tor Can't Initialize") connection_observer.notify('custom_message', "Tor Can't Initialize")
@staticmethod # @staticmethod
def terminate_tor_connection(): # def terminate_tor_connection():
tor_module = TorModule(Constants.HV_TOR_STATE_HOME) # tor_module = TorModule(Constants.HV_TOR_STATE_HOME)
tor_module.stop_service() # tor_module.stop_service()
@staticmethod @staticmethod
def establish_tor_session_connection(port_number: int, connection_observer: Optional[ConnectionObserver] = None): def establish_tor_session_connection(port_number: int, connection_observer: Optional[ConnectionObserver] = None):
@ -320,26 +326,28 @@ class ConnectionController:
return subprocess.Popen(('proxychains4', '-f', proxychains_configuration_file_path, 'microsocks', '-p', str(proxy_port_number)), stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT) return subprocess.Popen(('proxychains4', '-f', proxychains_configuration_file_path, 'microsocks', '-p', str(proxy_port_number)), stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
@staticmethod # @staticmethod
def terminate_system_connection(): # def terminate_system_connection():
if shutil.which('nmcli') is None: # if shutil.which('nmcli') is None:
raise CommandNotFoundError('nmcli') # raise CommandNotFoundError('nmcli')
if SystemStateController.exists(): # if SystemStateController.exists():
process = subprocess.Popen(('nmcli', 'connection', 'delete', 'wg'), stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT) # process = subprocess.Popen(('nmcli', 'connection', 'delete', 'wg'), stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
completed_successfully = not bool(os.waitpid(process.pid, 0)[1] >> 8) # completed_successfully = not bool(os.waitpid(process.pid, 0)[1] >> 8)
if completed_successfully or not ConnectionController.system_uses_wireguard_interface(): # if completed_successfully or not ConnectionController.system_uses_wireguard_interface():
subprocess.run(('nmcli', 'connection', 'delete', 'hv-ipv6-sink'), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) # subprocess.run(('nmcli', 'connection', 'delete', 'hv-ipv6-sink'), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
ConnectionController.terminate_tor_connection() # ConnectionController.terminate_tor_connection()
SystemState.dissolve() # SystemState.dissolve()
else: # else:
raise ConnectionTerminationError('The connection could not be terminated.') # raise ConnectionTerminationError('The connection could not be terminated.')
# Stays here for Tor based only. Will move with Tor later.
@staticmethod @staticmethod
def get_proxies(port_number: int): def get_proxies(port_number: int):
@ -348,107 +356,107 @@ class ConnectionController:
https=f'socks5h://127.0.0.1:{port_number}' https=f'socks5h://127.0.0.1:{port_number}'
) )
@staticmethod # @staticmethod
def await_connection(port_number: Optional[int] = None, connection_observer: Optional[ConnectionObserver] = None): # def await_connection(port_number: Optional[int] = None, connection_observer: Optional[ConnectionObserver] = None):
if port_number is None: # if port_number is None:
ConnectionController.await_network_interface() # ConnectionController.await_network_interface()
for retry_count in range(Constants.MAX_CONNECTION_ATTEMPTS): # for retry_count in range(Constants.MAX_CONNECTION_ATTEMPTS):
if connection_observer is not None: # if connection_observer is not None:
connection_observer.notify('connecting', dict( # connection_observer.notify('connecting', dict(
retry_interval=Constants.CONNECTION_RETRY_INTERVAL, # retry_interval=Constants.CONNECTION_RETRY_INTERVAL,
maximum_number_of_attempts=Constants.MAX_CONNECTION_ATTEMPTS, # maximum_number_of_attempts=Constants.MAX_CONNECTION_ATTEMPTS,
attempt_count=retry_count + 1 # attempt_count=retry_count + 1
)) # ))
try: # try:
ConnectionController.__test_connection(port_number) # ConnectionController.__test_connection(port_number)
return # return
except ConnectionError: # except ConnectionError:
time.sleep(Constants.CONNECTION_RETRY_INTERVAL) # time.sleep(Constants.CONNECTION_RETRY_INTERVAL)
retry_count += 1 # retry_count += 1
raise ConnectionError('The connection could not be established.') # raise ConnectionError('The connection could not be established.')
@staticmethod # @staticmethod
def await_network_interface(): # def await_network_interface():
network_interface_is_activated = False # network_interface_is_activated = False
retry_interval = .5 # retry_interval = .5
maximum_number_of_attempts = 10 # maximum_number_of_attempts = 10
attempt = 0 # attempt = 0
while not network_interface_is_activated and attempt < maximum_number_of_attempts: # while not network_interface_is_activated and attempt < maximum_number_of_attempts:
time.sleep(retry_interval) # time.sleep(retry_interval)
network_interface_is_activated = ConnectionController.system_uses_wireguard_interface() # network_interface_is_activated = ConnectionController.system_uses_wireguard_interface()
attempt += 1 # attempt += 1
if not network_interface_is_activated: # if not network_interface_is_activated:
raise ConnectionError('The network interface could not be activated.') # raise ConnectionError('The network interface could not be activated.')
@staticmethod # @staticmethod
def system_uses_wireguard_interface(): # def system_uses_wireguard_interface():
if shutil.which('ip') is None: # if shutil.which('ip') is None:
raise CommandNotFoundError('ip') # raise CommandNotFoundError('ip')
process = subprocess.Popen(('ip', 'route', 'get', '192.0.2.1'), stdout=subprocess.PIPE) # process = subprocess.Popen(('ip', 'route', 'get', '192.0.2.1'), stdout=subprocess.PIPE)
process_output = str(process.stdout.read()) # process_output = str(process.stdout.read())
return bool(re.search('dev wg', str(process_output))) # return bool(re.search('dev wg', str(process_output)))
@staticmethod # @staticmethod
def __establish_system_connection(profile: SystemProfile, connection_observer: Optional[ConnectionObserver] = None): # def __establish_system_connection(profile: SystemProfile, connection_observer: Optional[ConnectionObserver] = None):
if shutil.which('dbus-send') is None: # if shutil.which('dbus-send') is None:
raise CommandNotFoundError('dbus-send') # raise CommandNotFoundError('dbus-send')
if shutil.which('nmcli') is None: # if shutil.which('nmcli') is None:
raise CommandNotFoundError('nmcli') # raise CommandNotFoundError('nmcli')
ConnectionController.terminate_system_connection() # ConnectionController.terminate_system_connection()
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.')
try: # 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.')
SystemStateController.create(profile.id) # SystemStateController.create(profile.id)
try: # try:
ConnectionController.await_connection(connection_observer=connection_observer) # ConnectionController.await_connection(connection_observer=connection_observer)
except ConnectionError: # except ConnectionError:
raise ConnectionError('The connection could not be established.') # raise ConnectionError('The connection could not be established.')
@staticmethod @staticmethod
def __with_tor_connection(*args, task: Callable[..., Any], connection_observer: Optional[ConnectionObserver] = None, **kwargs): def __with_tor_connection(*args, task: Callable[..., Any], connection_observer: Optional[ConnectionObserver] = None, **kwargs):
@ -456,62 +464,62 @@ class ConnectionController:
port_number = ConnectionService.get_random_available_port_number() port_number = ConnectionService.get_random_available_port_number()
ConnectionController.establish_tor_session_connection(port_number, connection_observer=connection_observer) ConnectionController.establish_tor_session_connection(port_number, connection_observer=connection_observer)
ConnectionController.await_connection(port_number, connection_observer=connection_observer) await_connection(port_number, connection_observer=connection_observer)
task_output = task(*args, proxies=ConnectionController.get_proxies(port_number), **kwargs) task_output = task(*args, proxies=ConnectionController.get_proxies(port_number), **kwargs)
ConnectionController.terminate_tor_session_connection(port_number) ConnectionController.terminate_tor_session_connection(port_number)
return task_output return task_output
@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):
request_urls = [Constants.PING_URL] # request_urls = [Constants.PING_URL]
proxies = None # proxies = None
if os.environ.get('PING_URL') is None: # if os.environ.get('PING_URL') is None:
request_urls.extend([ # request_urls.extend([
'https://hc1.simplifiedprivacy.net', # 'https://hc1.simplifiedprivacy.net',
'https://hc2.simplifiedprivacy.org', # 'https://hc2.simplifiedprivacy.org',
'https://hc3.hydraveil.net' # 'https://hc3.hydraveil.net'
]) # ])
random.shuffle(request_urls) # random.shuffle(request_urls)
if port_number is not None: # if port_number is not None:
proxies = ConnectionController.get_proxies(port_number) # proxies = ConnectionController.get_proxies(port_number)
for request_url in request_urls: # for request_url in request_urls:
command = [ # command = [
sys.executable, '-u', '-c', 'import requests, sys\n' # sys.executable, '-u', '-c', 'import requests, sys\n'
'try:\n' # 'try:\n'
f' response = requests.get(\'{request_url}\', proxies={proxies}, timeout={timeout})\n' # f' response = requests.get(\'{request_url}\', proxies={proxies}, timeout={timeout})\n'
' response.raise_for_status(); print(response.text)\n' # ' response.raise_for_status(); print(response.text)\n'
'except requests.exceptions.RequestException:\n' # 'except requests.exceptions.RequestException:\n'
' sys.exit(1)' # ' sys.exit(1)'
] # ]
try: # try:
_response = subprocess.check_output(command, text=True, timeout=timeout) # _response = subprocess.check_output(command, text=True, timeout=timeout)
return None # return None
except (subprocess.CalledProcessError, subprocess.TimeoutExpired): # except (subprocess.CalledProcessError, subprocess.TimeoutExpired):
pass # pass
raise ConnectionError('The connection could not be established.') # raise ConnectionError('The connection could not be established.')
@staticmethod # @staticmethod
def __should_renegotiate(profile: Union[SessionProfile, SystemProfile]): # def __should_renegotiate(profile: Union[SessionProfile, SystemProfile]):
if not profile.has_subscription(): # if not profile.has_subscription():
raise MissingSubscriptionError() # raise MissingSubscriptionError()
if profile.connection.needs_wireguard_configuration() and profile.has_wireguard_configuration(): # if profile.connection.needs_wireguard_configuration() and profile.has_wireguard_configuration():
if profile.subscription.has_been_activated(): # if profile.subscription.has_been_activated():
return True # return True
return False # return False

View file

@ -1,3 +1,7 @@
from core.services.networking.systemwide.systemwide_wireguard import terminate_system_connection
from core.services.networking.general_connection_tools.testing_evaluating import system_uses_wireguard_interface
from core.services.networking.general_connection_tools.connection_enable import establish_connection
from core.Errors import InvalidSubscriptionError, MissingSubscriptionError, ConnectionTerminationError, ProfileActivationError, ProfileDeactivationError, MissingLocationError, ConnectionUnprotectedError, EndpointVerificationError, ProfileStateConflictError from core.Errors import InvalidSubscriptionError, MissingSubscriptionError, ConnectionTerminationError, ProfileActivationError, ProfileDeactivationError, MissingLocationError, ConnectionUnprotectedError, EndpointVerificationError, ProfileStateConflictError
from core.controllers.ApplicationController import ApplicationController from core.controllers.ApplicationController import ApplicationController
from core.controllers.ApplicationVersionController import ApplicationVersionController from core.controllers.ApplicationVersionController import ApplicationVersionController
@ -37,20 +41,31 @@ class ProfileController:
@staticmethod @staticmethod
def enable(profile: Union[SessionProfile, SystemProfile], ignore: tuple[type[Exception]] = (), pristine: bool = False, asynchronous: bool = False, profile_observer: ProfileObserver = None, application_version_observer: ApplicationVersionObserver = None, connection_observer: ConnectionObserver = None): def enable(
profile: Union[SessionProfile, SystemProfile],
ignore: tuple[type[Exception]] = (),
pristine: bool = False,
asynchronous: bool = False,
profile_observer: ProfileObserver = None,
application_version_observer: ApplicationVersionObserver = None,
connection_observer: ConnectionObserver = None):
from core.controllers.ConnectionController import ConnectionController from core.controllers.ConnectionController import ConnectionController
# =========== ALREADY ENABLED ============
if ProfileController.is_enabled(profile): if ProfileController.is_enabled(profile):
if not ProfileStateConflictError in ignore: if not ProfileStateConflictError in ignore:
raise ProfileStateConflictError('The profile is already enabled or its session was not properly terminated.') raise ProfileStateConflictError('The profile is already enabled or its session was not properly terminated.')
else: else:
ProfileController.disable(profile) ProfileController.disable(profile)
# =========== PRISTINE ============
if pristine: if pristine:
profile.delete_data() profile.delete_data()
# ============================================================================
# SESSION
# ============================================================================
if profile.is_session_profile(): if profile.is_session_profile():
application_version = profile.application_version application_version = profile.application_version
@ -59,7 +74,7 @@ class ProfileController:
ApplicationVersionController.install(application_version, application_version_observer=application_version_observer, connection_observer=connection_observer) ApplicationVersionController.install(application_version, application_version_observer=application_version_observer, connection_observer=connection_observer)
try: try:
port_number = ConnectionController.establish_connection(profile, ignore=ignore, connection_observer=connection_observer) port_number = establish_connection(profile, ignore=ignore, connection_observer=connection_observer)
except ConnectionError: except ConnectionError:
raise ProfileActivationError('The profile could not be enabled.') raise ProfileActivationError('The profile could not be enabled.')
@ -68,10 +83,12 @@ class ProfileController:
ApplicationController.launch(application_version, profile, port_number, asynchronous=asynchronous, profile_observer=profile_observer) ApplicationController.launch(application_version, profile, port_number, asynchronous=asynchronous, profile_observer=profile_observer)
# ============================================================================
# SYSTEMWIDE
# ============================================================================
if profile.is_system_profile(): if profile.is_system_profile():
try: try:
ConnectionController.establish_connection(profile, ignore=ignore, connection_observer=connection_observer) establish_connection(profile, ignore=ignore, connection_observer=connection_observer)
except ConnectionError: except ConnectionError:
raise ProfileActivationError('The profile could not be enabled.') raise ProfileActivationError('The profile could not be enabled.')
@ -115,7 +132,7 @@ class ProfileController:
raise ProfileDeactivationError('The profile could not be disabled.') raise ProfileDeactivationError('The profile could not be disabled.')
try: try:
ConnectionController.terminate_system_connection() terminate_system_connection()
except ConnectionTerminationError: except ConnectionTerminationError:
raise ProfileDeactivationError('The profile could not be disabled.') raise ProfileDeactivationError('The profile could not be disabled.')
@ -177,7 +194,7 @@ class ProfileController:
system_state = SystemStateController.get() system_state = SystemStateController.get()
if system_state is not None and system_state.profile_id is profile.id: if system_state is not None and system_state.profile_id is profile.id:
return ConnectionController.system_uses_wireguard_interface() return system_uses_wireguard_interface()
return False return False
@ -211,28 +228,28 @@ class ProfileController:
def has_proxy_configuration(profile: Union[SessionProfile, SystemProfile]): def has_proxy_configuration(profile: Union[SessionProfile, SystemProfile]):
profile.has_proxy_configuration() profile.has_proxy_configuration()
@staticmethod # @staticmethod
def register_wireguard_session(profile: Union[SessionProfile, SystemProfile], connection_observer: Optional[ConnectionObserver] = None): # def register_wireguard_session(profile: Union[SessionProfile, SystemProfile], connection_observer: Optional[ConnectionObserver] = None):
from core.controllers.ConnectionController import ConnectionController # from core.controllers.ConnectionController import ConnectionController
if not profile.has_subscription(): # if not profile.has_subscription():
raise MissingSubscriptionError() # raise MissingSubscriptionError()
if not profile.has_location(): # if not profile.has_location():
raise MissingLocationError() # raise MissingLocationError()
wireguard_keys = ProfileController.__generate_wireguard_keys() # wireguard_keys = ProfileController.__generate_wireguard_keys()
wireguard_configuration = ConnectionController.with_preferred_connection(profile.location.country_code, profile.location.code, profile.subscription.billing_code, wireguard_keys.get('public'), task=WebServiceApiService.post_wireguard_session, connection_observer=connection_observer) # wireguard_configuration = ConnectionController.with_preferred_connection(profile.location.country_code, profile.location.code, profile.subscription.billing_code, wireguard_keys.get('public'), task=WebServiceApiService.post_wireguard_session, connection_observer=connection_observer)
if wireguard_configuration is None: # if wireguard_configuration is None:
raise InvalidSubscriptionError() # raise InvalidSubscriptionError()
expression = re.compile(r'^(PrivateKey =)\s?$', re.MULTILINE) # expression = re.compile(r'^(PrivateKey =)\s?$', re.MULTILINE)
wireguard_configuration = re.sub(expression, r'\1 ' + wireguard_keys.get('private'), wireguard_configuration) # wireguard_configuration = re.sub(expression, r'\1 ' + wireguard_keys.get('private'), wireguard_configuration)
profile.attach_wireguard_configuration(wireguard_configuration) # profile.attach_wireguard_configuration(wireguard_configuration)
@staticmethod @staticmethod
def get_wireguard_configuration_path(profile: Union[SessionProfile, SystemProfile]): def get_wireguard_configuration_path(profile: Union[SessionProfile, SystemProfile]):
@ -242,65 +259,65 @@ class ProfileController:
def has_wireguard_configuration(profile: Union[SessionProfile, SystemProfile]): def has_wireguard_configuration(profile: Union[SessionProfile, SystemProfile]):
return profile.has_wireguard_configuration() return profile.has_wireguard_configuration()
@staticmethod # @staticmethod
def verify_wireguard_endpoint(profile: Union[SessionProfile, SystemProfile], ignore: tuple[type[Exception]] = ()): # def verify_wireguard_endpoint(profile: Union[SessionProfile, SystemProfile], ignore: tuple[type[Exception]] = ()):
try: # try:
ProfileController.__verify_wireguard_endpoint(profile) # ProfileController.__verify_wireguard_endpoint(profile)
except EndpointVerificationError as error: # except EndpointVerificationError as error:
if not EndpointVerificationError in ignore: # if not EndpointVerificationError in ignore:
profile.address_security_incident() # profile.address_security_incident()
raise error # raise error
@staticmethod # @staticmethod
def __verify_wireguard_endpoint(profile: Union[SessionProfile, SystemProfile]): # def __verify_wireguard_endpoint(profile: Union[SessionProfile, SystemProfile]):
from cryptography.hazmat.primitives.asymmetric import ed25519 # from cryptography.hazmat.primitives.asymmetric import ed25519
import base64 # import base64
signature = profile.get_wireguard_configuration_metadata('Signature') # signature = profile.get_wireguard_configuration_metadata('Signature')
wireguard_public_keys = profile.get_wireguard_public_keys() # wireguard_public_keys = profile.get_wireguard_public_keys()
operator = profile.location.operator # operator = profile.location.operator
if signature is None: # if signature is None:
raise EndpointVerificationError('The WireGuard endpoint\'s signature could not be determined.') # raise EndpointVerificationError('The WireGuard endpoint\'s signature could not be determined.')
if not wireguard_public_keys: # if not wireguard_public_keys:
raise EndpointVerificationError('The WireGuard endpoint\'s public key could not be determined.') # raise EndpointVerificationError('The WireGuard endpoint\'s public key could not be determined.')
if operator is None: # if operator is None:
raise EndpointVerificationError('The WireGuard endpoint\'s operator could not be determined.') # raise EndpointVerificationError('The WireGuard endpoint\'s operator could not be determined.')
try: # try:
operator_public_key = ed25519.Ed25519PublicKey.from_public_bytes(bytes.fromhex(operator.public_key)) # operator_public_key = ed25519.Ed25519PublicKey.from_public_bytes(bytes.fromhex(operator.public_key))
for wireguard_public_key in wireguard_public_keys: # for wireguard_public_key in wireguard_public_keys:
operator_public_key.verify(base64.b64decode(signature), wireguard_public_key.encode('utf-8')) # operator_public_key.verify(base64.b64decode(signature), wireguard_public_key.encode('utf-8'))
except Exception: # except Exception:
raise EndpointVerificationError('The WireGuard endpoint could not be verified.') # raise EndpointVerificationError('The WireGuard endpoint could not be verified.')
@staticmethod # @staticmethod
def __generate_wireguard_keys(): # def __generate_wireguard_keys():
from cryptography.hazmat.primitives import serialization # from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey # from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey
raw_private_key = X25519PrivateKey.generate() # raw_private_key = X25519PrivateKey.generate()
public_key = raw_private_key.public_key().public_bytes( # public_key = raw_private_key.public_key().public_bytes(
encoding=serialization.Encoding.Raw, format=serialization.PublicFormat.Raw # encoding=serialization.Encoding.Raw, format=serialization.PublicFormat.Raw
) # )
private_key = raw_private_key.private_bytes( # private_key = raw_private_key.private_bytes(
encoding=serialization.Encoding.Raw, format=serialization.PrivateFormat.Raw, encryption_algorithm=serialization.NoEncryption() # encoding=serialization.Encoding.Raw, format=serialization.PrivateFormat.Raw, encryption_algorithm=serialization.NoEncryption()
) # )
return dict( # return dict(
private=base64.b64encode(private_key).decode(), # private=base64.b64encode(private_key).decode(),
public=base64.b64encode(public_key).decode() # public=base64.b64encode(public_key).decode()
) # )

View file

@ -1,19 +1,21 @@
from dataclasses import dataclass from dataclasses import dataclass
from dataclasses_json import dataclass_json from dataclasses_json import dataclass_json
@dataclass_json @dataclass_json
@dataclass @dataclass
class BaseConnection: class BaseConnection:
code: str code: str
# called by: connection controller for basic type checks on wireguard code # Called by: ConnectionController
# it uses it for basic type checks on wireguard code
def needs_wireguard_configuration(self): def needs_wireguard_configuration(self):
return self.code == 'wireguard' return self.code == 'wireguard'
# Called By SubscriptionPlan
def is_session_connection(self): def is_session_connection(self):
return type(self).__name__ == 'SessionConnection' return type(self).__name__ == 'SessionConnection'
# Not called. Dead code
def is_system_connection(self): def is_system_connection(self):
return type(self).__name__ == 'SystemConnection' return type(self).__name__ == 'SystemConnection'

View file

@ -28,6 +28,7 @@ import re
import shutil import shutil
import tempfile import tempfile
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from enum import Enum
@safe_db_operation @safe_db_operation
def execute_location_sql(country_code: str, city_code: str, session: Session) -> DatabaseOperation: def execute_location_sql(country_code: str, city_code: str, session: Session) -> DatabaseOperation:
@ -51,6 +52,9 @@ def get_profile_location_data(country_code: str, city_code: str) -> Location:
return None return None
class ProfileType(str, Enum):
SESSION = "session"
SYSTEM = "system"
@dataclass_json @dataclass_json
@dataclass @dataclass
@ -61,6 +65,7 @@ class BaseProfile(ABC):
name: str name: str
subscription: Optional[Subscription] subscription: Optional[Subscription]
location: Optional[Location] = field(metadata=config(exclude=Exclude.ALWAYS)) # SQLAlchemy object location: Optional[Location] = field(metadata=config(exclude=Exclude.ALWAYS)) # SQLAlchemy object
type: ProfileType
# legacy version included it to be serialized, but now it's an SQLAlchemy object. # legacy version included it to be serialized, but now it's an SQLAlchemy object.
# location: Optional[Location] # location: Optional[Location]
@ -237,16 +242,9 @@ class BaseProfile(ABC):
# this needs error handling if there's no location or malconformed config. # this needs error handling if there's no location or malconformed config.
# legacy phased out since SQLAlchemy handles the time_zone. # =========== SESSION ===========
# if location is not None:
# if profile['location'].get('time_zone') is not None:
# location.time_zone = profile['location']['time_zone']
# profile['location'] = location
if 'application_version' in profile: if 'application_version' in profile:
profile['type'] = ProfileType.SESSION
if profile['application_version'] is not None: if profile['application_version'] is not None:
application_version = ApplicationVersion.find(profile['application_version']['application_code'] or None, profile['application_version']['version_number'] or None) application_version = ApplicationVersion.find(profile['application_version']['application_code'] or None, profile['application_version']['version_number'] or None)
@ -258,7 +256,10 @@ class BaseProfile(ABC):
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
profile = SessionProfile.from_dict(profile) profile = SessionProfile.from_dict(profile)
# =========== SYSTEM ===========
else: else:
profile['type'] = ProfileType.SYSTEM
from core.models.system.SystemProfile import SystemProfile from core.models.system.SystemProfile import SystemProfile
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
@ -299,3 +300,10 @@ class BaseProfile(ABC):
@staticmethod @staticmethod
def __get_data_path(id: int): def __get_data_path(id: int):
return f'{Constants.HV_PROFILE_DATA_HOME}/{str(id)}' return f'{Constants.HV_PROFILE_DATA_HOME}/{str(id)}'
# legacy phased out since SQLAlchemy handles the time_zone.
# if location is not None:
# if profile['location'].get('time_zone') is not None:
# location.time_zone = profile['location']['time_zone']
# profile['location'] = location

View file

@ -1,20 +1,32 @@
from core.models.BaseConnection import BaseConnection from core.models.BaseConnection import BaseConnection
from dataclasses import dataclass from dataclasses import dataclass
from enum import Enum
class SessionConnectionTypes(str, Enum):
WIREGUARD = "wireguard"
TOR = "tor"
@dataclass @dataclass
class SessionConnection(BaseConnection): class SessionConnection(BaseConnection):
masked: bool = False code: SessionConnectionTypes
masked: bool
def __post_init__(self): # Called by: ConnectionController
if self.code not in ('system', 'tor', 'wireguard'):
raise ValueError('Invalid connection code.')
# called by connection controller
# doesn't even make sense, it's checking if the Session is code system. this will always be false. # doesn't even make sense, it's checking if the Session is code system. this will always be false.
def is_unprotected(self): def is_unprotected(self):
return self.code == 'system' and self.masked is False return self.code == 'system' and self.masked is False
# Called by: SessionProfile.determine_timezone
def needs_proxy_configuration(self): def needs_proxy_configuration(self):
return self.masked is True return self.masked is True
# legacy:
# @dataclass
# class SessionConnection(BaseConnection):
# masked: bool = False
# def __post_init__(self):
# if self.code not in ('system', 'tor', 'wireguard'):
# raise ValueError('Invalid connection code.')

View file

@ -1,16 +1,28 @@
from core.models.BaseConnection import BaseConnection from core.models.BaseConnection import BaseConnection
from dataclasses import dataclass from dataclasses import dataclass
from enum import Enum
from typing import Literal
class SystemConnectionTypes(str, Enum):
WIREGUARD = "wireguard"
HYSTERIA2 = "hysteria2"
VLESS = "vless"
@dataclass @dataclass
class SystemConnection (BaseConnection): class SystemConnection(BaseConnection):
code: SystemConnectionTypes
masked: Literal[False] = False
def __post_init__(self):
if self.code not in ('vless', 'hysteria2', 'wireguard'):
raise ValueError('Invalid connection code.')
@staticmethod @staticmethod
def needs_proxy_configuration(): def needs_proxy_configuration():
return False return False
# legacy:
# @dataclass
# class SystemConnection (BaseConnection):
# def __post_init__(self):
# if self.code not in ('vless', 'hysteria2', 'wireguard'):
# raise ValueError('Invalid connection code.')

View file

@ -0,0 +1,48 @@
from core.models.session.SessionProfile import SessionProfile
from core.models.system.SystemProfile import SystemProfile
from core.models.BaseProfile import BaseProfile as Profile
from core.Errors import InvalidSubscriptionError, MissingSubscriptionError, ConnectionTerminationError, ProfileActivationError, ProfileDeactivationError, MissingLocationError, ConnectionUnprotectedError, EndpointVerificationError, ProfileStateConflictError
from typing import Union, Optional
import base64
def verify_wireguard_endpoint(profile: Union[SessionProfile, SystemProfile], ignore: tuple[type[Exception]] = ()):
try:
__verify_wireguard_endpoint(profile)
except EndpointVerificationError as error:
if not EndpointVerificationError in ignore:
profile.address_security_incident()
raise error
def __verify_wireguard_endpoint(profile: Union[SessionProfile, SystemProfile]):
from cryptography.hazmat.primitives.asymmetric import ed25519
import base64
signature = profile.get_wireguard_configuration_metadata('Signature')
wireguard_public_keys = profile.get_wireguard_public_keys()
operator = profile.location.operator
if signature is None:
raise EndpointVerificationError('The WireGuard endpoint\'s signature could not be determined.')
if not wireguard_public_keys:
raise EndpointVerificationError('The WireGuard endpoint\'s public key could not be determined.')
if operator is None:
raise EndpointVerificationError('The WireGuard endpoint\'s operator could not be determined.')
try:
operator_public_key = ed25519.Ed25519PublicKey.from_public_bytes(bytes.fromhex(operator.public_key))
for wireguard_public_key in wireguard_public_keys:
operator_public_key.verify(base64.b64decode(signature), wireguard_public_key.encode('utf-8'))
except Exception:
raise EndpointVerificationError('The WireGuard endpoint could not be verified.')

View file

@ -0,0 +1,66 @@
from core.models.session.SessionProfile import SessionProfile
from core.models.system.SystemProfile import SystemProfile
from core.Errors import MissingSubscriptionError, MissingLocationError, InvalidSubscriptionError
from core.services.WebServiceApiService import WebServiceApiService
from core.controllers.ConnectionController import ConnectionController
from core.observers.ConnectionObserver import ConnectionObserver
from typing import Union, Optional
import base64
import re
def register_wireguard_session(
profile: Union[SessionProfile, SystemProfile],
connection_observer: Optional[ConnectionObserver] = None
):
"""Register a WireGuard session for the given profile."""
if not profile.has_subscription():
raise MissingSubscriptionError()
if not profile.has_location():
raise MissingLocationError()
wireguard_keys = _generate_wireguard_keys()
wireguard_configuration = ConnectionController.with_preferred_connection(
profile.location.country_code,
profile.location.code,
profile.subscription.billing_code,
wireguard_keys.get('public'),
task=WebServiceApiService.post_wireguard_session,
connection_observer=connection_observer
)
if wireguard_configuration is None:
raise InvalidSubscriptionError()
wireguard_configuration = _inject_private_key(wireguard_configuration, wireguard_keys.get('private'))
profile.attach_wireguard_configuration(wireguard_configuration)
def _generate_wireguard_keys() -> dict:
"""Generate WireGuard public/private key pair."""
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey
raw_private_key = X25519PrivateKey.generate()
public_key = raw_private_key.public_key().public_bytes(
encoding=serialization.Encoding.Raw, format=serialization.PublicFormat.Raw
)
private_key = raw_private_key.private_bytes(
encoding=serialization.Encoding.Raw, format=serialization.PrivateFormat.Raw, encryption_algorithm=serialization.NoEncryption()
)
return dict(
private=base64.b64encode(private_key).decode(),
public=base64.b64encode(public_key).decode()
)
def _inject_private_key(config: str, private_key: str) -> str:
"""Inject private key into WireGuard config."""
expression = re.compile(r'^(PrivateKey =)\s?$', re.MULTILINE)
return re.sub(expression, r'\1 ' + private_key, config)

View file

@ -0,0 +1,129 @@
from core.services.networking.general_connection_tools.testing_evaluating import system_uses_wireguard_interface
from core.services.networking.systemwide.systemwide_wireguard import establish_system_connection, terminate_system_connection
from core.services.keys_and_verifications.wireguard_keys import register_wireguard_session
from core.models.session.SessionConnection import SessionConnectionTypes
from core.models.system.SystemConnection import SystemConnectionTypes
from core.errors.logger import logger
from core.errors.exceptions import *
from typing import Union, Optional, Callable
from core.models.session.SessionProfile import SessionProfile
from core.models.system.SystemProfile import SystemProfile
from core.Errors import ConnectionTerminationError, InvalidSubscriptionError
from core.services.WebServiceApiService import WebServiceApiService
from core.controllers.ConnectionController import ConnectionController
from core.models.BaseProfile import ProfileType
from core.services.subscriptions.subscriptions import activate_subscription
from core.observers.ConnectionObserver import ConnectionObserver
from core.controllers.SystemStateController import SystemStateController
wireguard_types = [SystemConnectionTypes.WIREGUARD, SessionConnectionTypes.WIREGUARD]
def establish_connection(
profile: Union[SessionProfile, SystemProfile],
ignore: tuple[type[Exception]] = (),
connection_observer: Optional[ConnectionObserver] = None,
):
"""Establish a connection for the given profile."""
logger.info(f"[CONNECTION] Checking subscription..")
_ensure_subscription_ready(profile, connection_observer)
logger.info(f"[CONNECTION] Checking proxy configuration..")
_ensure_proxy_configured(profile, connection_observer)
logger.info(f"[CONNECTION] Checking Wireguard configuration..")
_ensure_wireguard_configured(profile, connection_observer)
establish_fn = {
ProfileType.SESSION: ConnectionController.establish_session_connection,
ProfileType.SYSTEM: establish_system_connection,
}[profile.type]
return _establish_with_renegotiation(profile, establish_fn, ignore, connection_observer)
def _ensure_subscription_ready(
profile: Union[SessionProfile, SystemProfile],
connection_observer: Optional[ConnectionObserver],
):
"""Activate subscription (idempotent)."""
activate_subscription(profile, connection_observer=connection_observer)
def _ensure_proxy_configured(
profile: Union[SessionProfile, SystemProfile],
connection_observer: Optional[ConnectionObserver],
):
"""Setup proxy config if needed."""
if not profile.connection.needs_proxy_configuration():
return
_ensure_subscription_ready(profile, connection_observer)
proxy_config = ConnectionController.with_preferred_connection(
profile.subscription.billing_code,
task=WebServiceApiService.get_proxy_configuration,
connection_observer=connection_observer
)
if proxy_config is None:
raise InvalidSubscriptionError()
profile.attach_proxy_configuration(proxy_config)
def _ensure_wireguard_configured(
profile: Union[SessionProfile, SystemProfile],
connection_observer: Optional[ConnectionObserver],
):
"""Setup WireGuard config if needed."""
logger.info(f"[CONNECTION] Checking if this profile needs a wg config. The profile.connection.code is {profile.connection.code}")
if profile.connection.code not in wireguard_types:
logger.info(f"[CONNECTION] This profile {profile.id} does NOT need a wireguard config")
return
if profile.type == ProfileType.SYSTEM:
if system_uses_wireguard_interface() and SystemStateController.exists():
logger.info(f"[CONNECTION] There is a systemwide profile that is already is active.")
try:
terminate_system_connection()
except ConnectionTerminationError as e:
logger.error(f"[CONNECTION] Previous Systemwide Wireguard connection could not be disabled. {e}")
_ensure_subscription_ready(profile, connection_observer)
logger.info(f"[CONNECTION] Checking if the profile already has a wg config..")
if not profile.has_wireguard_configuration():
logger.info(f"[CONNECTION] Profile does NOT have WG keys and it needs it. Registering a NEW wg session..")
register_wireguard_session(profile, connection_observer=connection_observer)
def _establish_with_renegotiation(
profile: Union[SessionProfile, SystemProfile],
establish_fn: Callable,
ignore: tuple[type[Exception]],
connection_observer: Optional[ConnectionObserver],
):
"""Attempt connection, renegotiate WireGuard once if needed."""
try:
logger.info(f"[CONNECTION] Connecting..")
return establish_fn(profile, ignore=ignore, connection_observer=connection_observer)
except ConnectionError:
if __should_renegotiate(profile):
logger.info(f"[CONNECTION] Renegotiating a new wg key session with API..")
register_wireguard_session(profile, connection_observer=connection_observer)
return establish_fn(profile, ignore=ignore, connection_observer=connection_observer)
raise ConnectionError('The connection could not be established.')
def __should_renegotiate(profile: Union[SessionProfile, SystemProfile]):
if not profile.has_subscription():
raise MissingSubscriptionError()
if profile.connection.code in wireguard_types and profile.has_wireguard_configuration():
if profile.subscription.has_been_activated():
return True
else:
return False

View file

@ -0,0 +1,125 @@
from core.Constants import Constants
from core.Errors import CommandNotFoundError
from core.observers.ConnectionObserver import ConnectionObserver
from essentials.modules.TorModule import TorModule
from typing import Optional
import os
import random
import re
import shutil
import subprocess
import sys
import time
def terminate_tor_connection():
tor_module = TorModule(Constants.HV_TOR_STATE_HOME)
tor_module.stop_service()
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 = system_uses_wireguard_interface()
attempt += 1
if not network_interface_is_activated:
raise ConnectionError('The network interface could not be activated.')
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)))
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 = 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.')
# Kept a duplicate in connection controller, because Tor uses it. and circular imports are an issue for now. Refactor later.
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}'
)
def await_connection(port_number: Optional[int] = None, connection_observer: Optional[ConnectionObserver] = None):
if port_number is None:
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:
__test_connection(port_number)
return
except ConnectionError:
time.sleep(Constants.CONNECTION_RETRY_INTERVAL)
retry_count += 1
raise ConnectionError('The connection could not be established.')

View file

@ -0,0 +1,123 @@
from core.services.networking.general_connection_tools.testing_evaluating import await_connection, system_uses_wireguard_interface, terminate_tor_connection
from core.services.keys_and_verifications.endpoint_verification import verify_wireguard_endpoint
from core.Constants import Constants
from core.Errors import ConnectionTerminationError, CommandNotFoundError
from core.controllers.ConfigurationController import ConfigurationController
from core.controllers.SystemStateController import SystemStateController
from core.models.BaseProfile import ProfileType
from core.models.session.SessionProfile import SessionProfile
from core.models.system.SystemProfile import SystemProfile
from core.models.system.SystemState import SystemState
from core.observers.ConnectionObserver import ConnectionObserver
from subprocess import CalledProcessError
from typing import Optional
import os
import re
import shutil
import subprocess
import time
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 system_uses_wireguard_interface():
subprocess.run(('nmcli', 'connection', 'delete', 'hv-ipv6-sink'), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
terminate_tor_connection()
SystemState.dissolve()
else:
raise ConnectionTerminationError('The connection could not be terminated.')
def __establish_system_connection(profile: SystemProfile, connection_observer: Optional[ConnectionObserver] = None):
if shutil.which('dbus-send') is None:
raise CommandNotFoundError('dbus-send')
if shutil.which('nmcli') is None:
raise CommandNotFoundError('nmcli')
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:
raise ConnectionError('The connection could not be established.')
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()
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)
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)
except CalledProcessError:
raise ConnectionError('The connection could not be established.')
SystemStateController.create(profile.id)
try:
await_connection(connection_observer=connection_observer)
except ConnectionError:
raise ConnectionError('The connection could not be established.')
def establish_system_connection(profile: SystemProfile, ignore: tuple[type[Exception]] = (), connection_observer: Optional[ConnectionObserver] = None):
if ConfigurationController.get_endpoint_verification_enabled():
verify_wireguard_endpoint(profile, ignore=ignore)
try:
__establish_system_connection(profile, connection_observer)
except ConnectionError:
try:
terminate_system_connection()
except ConnectionTerminationError:
pass
raise ConnectionError('The connection could not be established.')
except CalledProcessError:
try:
terminate_system_connection()
except ConnectionTerminationError:
pass
try:
__establish_system_connection(profile, connection_observer)
except (ConnectionError, CalledProcessError):
try:
terminate_system_connection()
except ConnectionTerminationError:
pass
raise ConnectionError('The connection could not be established.')
terminate_tor_connection()
time.sleep(1.0)

View file

@ -0,0 +1,61 @@
from typing import Union, Optional
from core.models.session.SessionProfile import SessionProfile
from core.models.system.SystemProfile import SystemProfile
from core.Errors import MissingSubscriptionError, InvalidSubscriptionError
from core.services.WebServiceApiService import WebServiceApiService
from core.controllers.ConnectionController import ConnectionController
from core.observers.ConnectionObserver import ConnectionObserver
def activate_subscription(
profile: Union[SessionProfile, SystemProfile],
connection_observer: Optional[ConnectionObserver] = None
) -> bool:
"""
Purpose:
Ensure subscription is ready.
Features:
Idempotent.
Checks local first
Confusion:
This returns True both if was already active or just activated.
Returns:
Returns True if subscription was already active.
Returns True if subscription was just activated.
Errors:
True.
Raises if subscription is missing or invalid. (not false)
"""
if not profile.has_subscription():
raise MissingSubscriptionError()
# Already activated—nothing to do
if profile.subscription.has_been_activated():
return True
# Fetch and activate
subscription = ConnectionController.with_preferred_connection(
profile.subscription.billing_code,
task=WebServiceApiService.get_subscription,
connection_observer=connection_observer
)
if subscription is None:
raise InvalidSubscriptionError()
profile.subscription = subscription
profile.save()
return True
def is_subscription_ready(profile: Union[SessionProfile, SystemProfile]) -> bool:
"""Check if subscription exists and is activated (no side effects)."""
return (
profile.has_subscription()
and profile.subscription.has_been_activated()
)

View file

@ -1,6 +1,6 @@
[project] [project]
name = "sp-hydra-veil-core" name = "sp-hydra-veil-core"
version = "2.4.0" version = "2.4.2"
authors = [ authors = [
{ name = "Simplified Privacy" }, { name = "Simplified Privacy" },
] ]