Compare commits

..

2 commits

16 changed files with 298 additions and 185 deletions

View file

@ -1,6 +1,12 @@
# Major Change Log: # Major Change Log:
# 2.4.3
### Systemwide Killswitch Introduced for Enable
### July 20, 2026
Introduced IP-interface-based killswitch support for systemwide wireguard, on enable only. This version is stable and working for enable only, with a "test" script in the fileslot. And fixed GUI's object save creation. Additionally rolled back Connection data models to previous non-enum version.
<br/>
# 2.4.2 # 2.4.2
### Surgery on ConnectionController ### 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. 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.

View file

@ -65,3 +65,8 @@ class Constants:
HV_SESSION_STATE_HOME: Final[str] = f'{HV_STATE_HOME}/sessions' HV_SESSION_STATE_HOME: Final[str] = f'{HV_STATE_HOME}/sessions'
HV_TOR_STATE_HOME: Final[str] = f'{HV_STATE_HOME}/tor' HV_TOR_STATE_HOME: Final[str] = f'{HV_STATE_HOME}/tor'
# ── killswitch / dns wrappers ─────────────────────────────────────────────
KILLSWITCH_WRAPPER: Final[str] = os.environ.get(
'KILLSWITCH_WRAPPER', '/opt/hydra-veil/killswitch'
)

View file

@ -113,7 +113,7 @@ def get_unused_tickets(ticket_observer: TicketObserver) -> dict:
does_the_file_exist, path_of_file = does_ticket_tracker_exist() does_the_file_exist, path_of_file = does_ticket_tracker_exist()
if does_the_file_exist == False: if does_the_file_exist == False:
error_msg = f"The ticket tracker organizer file does not exist. Check the folder {path_of_file}" error_msg = f"The ticket tracker organizer file does not exist. Check the folder {path_of_file}"
ticket_observer.notify("failed_input", subject=error_msg) # ticket_observer.notify("failed_input", subject=error_msg)
return {"valid": False, "message": error_msg} return {"valid": False, "message": error_msg}
# Use the Model: # Use the Model:

View file

@ -11,6 +11,10 @@ from core.Helpers import write_atomically
from core.models.manage.wrapper import safe_db_operation, WrapperRollback from core.models.manage.wrapper import safe_db_operation, WrapperRollback
from core.models.DatabaseOperation import DatabaseOperation, DBErrorType from core.models.DatabaseOperation import DatabaseOperation, DBErrorType
# If switching to enums for Connection:
# from core.models.system.SystemConnection import SystemConnection, SystemConnectionTypes
# from core.models.session.SessionConnection import SessionConnection, SessionConnectionTypes
from core.controllers.LocationController import LocationController from core.controllers.LocationController import LocationController
from core.models.orm_models.Location import Location from core.models.orm_models.Location import Location
from core.models.orm_models.Operator import Operator from core.models.orm_models.Operator import Operator
@ -64,8 +68,8 @@ class BaseProfile(ABC):
) )
name: str name: str
subscription: Optional[Subscription] subscription: Optional[Subscription]
location: Optional[Location] = field(metadata=config(exclude=Exclude.ALWAYS)) # SQLAlchemy object
type: ProfileType type: ProfileType
location: Optional[Location] = field(metadata=config(exclude=Exclude.ALWAYS)) # SQLAlchemy object
# 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]
@ -98,13 +102,15 @@ class BaseProfile(ABC):
def save(self: Self): def save(self: Self):
# === SERIALIZATION ===
config_dict = self.to_dict() config_dict = self.to_dict()
location_dict = self.location.to_dict() # this is from SQLAlchemy, and not JSON-models, that's why it's separate. # === LOCATION ===
location_dict = self.location.convert_to_dict() # this is from SQLAlchemy, and not JSON-models, that's why it's separate.
if self.location: if self.location:
config_dict["location"] = location_dict config_dict["location"] = location_dict
# === FILE I/O ===
config_file_contents = json.dumps(config_dict, indent=4) + '\n' config_file_contents = json.dumps(config_dict, indent=4) + '\n'
# legacy version: # legacy version:

View file

@ -11,10 +11,14 @@ class ResultError(Enum):
INVALID_INPUT = "invalid_input" INVALID_INPUT = "invalid_input"
CONNECTION = "connection" CONNECTION = "connection"
DATABASE = "database" DATABASE = "database"
PERMISSION = "permission"
SUBSCRIPTION = "subscription"
NMCLI = "nmcli_issues"
FIREWALL = "firewall"
UNKNOWN = "unknown" UNKNOWN = "unknown"
@dataclass @dataclass
class Result: class Result():
valid: bool valid: bool
error_type: ResultError = ResultError.SUCCESS error_type: ResultError = ResultError.SUCCESS
data: Optional[Any] = None data: Optional[Any] = None

View file

@ -46,12 +46,12 @@ class Location(BaseModel):
is_hysteria2_capable: Mapped[Optional[bool]] = mapped_column(String, nullable=True, default=None) is_hysteria2_capable: Mapped[Optional[bool]] = mapped_column(String, nullable=True, default=None)
is_vless_capable: Mapped[Optional[bool]] = mapped_column(String, nullable=True, default=None) is_vless_capable: Mapped[Optional[bool]] = mapped_column(String, nullable=True, default=None)
def to_dict(self): def convert_to_dict(self):
return { return {
"country_code": self.country_code, "country_code": self.country_code,
"code": self.code, "code": self.code,
"time_zone": self.time_zone "time_zone": self.time_zone
} }
# to use: # to use:

View file

@ -2,14 +2,15 @@ from core.models.BaseConnection import BaseConnection
from dataclasses import dataclass from dataclasses import dataclass
from enum import Enum from enum import Enum
class SessionConnectionTypes(str, Enum):
WIREGUARD = "wireguard"
TOR = "tor"
@dataclass @dataclass
class SessionConnection(BaseConnection): class SessionConnection(BaseConnection):
code: SessionConnectionTypes masked: bool = False
masked: bool
def __post_init__(self):
if self.code not in ('system', 'tor', 'wireguard'):
raise ValueError('Invalid connection code.')
# Called by: ConnectionController # Called by: ConnectionController
# 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.
@ -21,12 +22,36 @@ class SessionConnection(BaseConnection):
return self.masked is True return self.masked is True
# legacy: # Potential refactor
# from dataclasses import dataclass, field
# from dataclasses_json import dataclass_json, config
# class SessionConnectionTypes(str, Enum):
# WIREGUARD = "wireguard"
# TOR = "tor"
# @dataclass_json
# @dataclass # @dataclass
# class SessionConnection(BaseConnection): # class SessionConnection:
# code: SessionConnectionTypes = field(
# metadata=config(
# encoder=lambda x: x.value, # Convert enum to string on save
# decoder=lambda x: SessionConnectionTypes(x) # Convert string to enum on load
# )
# )
# masked: bool = False # masked: bool = False
# def __post_init__(self): # def __post_init__(self):
# # Convert string to enum for deserialization
# if isinstance(self.code, str):
# self.code = SessionConnectionTypes(self.code)
# if self.code not in ('system', 'tor', 'wireguard'): # # Validation
# raise ValueError('Invalid connection code.') # if self.code not in SessionConnectionTypes:
# raise ValueError(f'Invalid code: {self.code}')
# @dataclass
# class SessionConnection(BaseConnection):
# code: SessionConnectionTypes
# masked: bool

View file

@ -16,7 +16,7 @@ import shutil
class SessionProfile(BaseProfile): class SessionProfile(BaseProfile):
resolution: str resolution: str
application_version: Optional[ApplicationVersion] application_version: Optional[ApplicationVersion]
connection: Optional[SessionConnection] connection: Optional[SessionConnection] = None
def has_connection(self): def has_connection(self):
return self.connection is not None return self.connection is not None

View file

@ -1,28 +1,29 @@
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"
# legacy:
@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: # Potential refactor:
# 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.')

View file

@ -0,0 +1,19 @@
from core.Constants import Constants
import os
def extract_endpoint_ip(profile_id: str):
try:
profile_path = Constants.HV_PROFILE_CONFIG_HOME + f'/{profile_id}'
wg_conf_path = f'{profile_path}/wg.conf.bak'
print(f"wg_conf_path is {wg_conf_path}")
if not os.path.exists(wg_conf_path):
return None
with open(wg_conf_path, 'r') as f:
content = f.read()
for line in content.split('\n'):
if line.strip().startswith('Endpoint = '):
endpoint = line.strip().split(' = ')[1]
return endpoint.split(':')[0]
return None
except Exception:
return None

View file

@ -1,10 +1,14 @@
from core.services.networking.general_connection_tools.testing_evaluating import system_uses_wireguard_interface 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.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.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.services.subscriptions.subscriptions import activate_subscription from core.services.subscriptions.subscriptions import activate_subscription
# If refactored to enums:
# from core.models.session.SessionConnection import SessionConnectionTypes
# from core.models.system.SystemConnection import SystemConnectionTypes
# wireguard_types = [SystemConnectionTypes.WIREGUARD, SessionConnectionTypes.WIREGUARD]
from core.errors.logger import logger from core.errors.logger import logger
from core.errors.exceptions import * from core.errors.exceptions import *
from typing import Union, Optional, Callable from typing import Union, Optional, Callable
@ -17,8 +21,6 @@ from core.models.BaseProfile import ProfileType
from core.observers.ConnectionObserver import ConnectionObserver from core.observers.ConnectionObserver import ConnectionObserver
from core.controllers.SystemStateController import SystemStateController from core.controllers.SystemStateController import SystemStateController
wireguard_types = [SystemConnectionTypes.WIREGUARD, SessionConnectionTypes.WIREGUARD]
def establish_connection( def establish_connection(
profile: Union[SessionProfile, SystemProfile], profile: Union[SessionProfile, SystemProfile],
ignore: tuple[type[Exception]] = (), ignore: tuple[type[Exception]] = (),
@ -75,7 +77,7 @@ def _ensure_wireguard_configured(
"""Setup WireGuard config if needed.""" """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}") 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: if profile.connection.code != "wireguard":
logger.info(f"[CONNECTION] This profile {profile.id} does NOT need a wireguard config") logger.info(f"[CONNECTION] This profile {profile.id} does NOT need a wireguard config")
return return

View file

@ -1,118 +0,0 @@
#!/bin/bash
set -euo pipefail
# root check
if [[ "$EUID" -ne 0 ]]; then
echo "[killswitch] ERROR: must be run as root" >&2
exit 1
fi
ACTION="${1:-}"
SERVER_IP="${2:-}"
TUNNEL_IF="${3:-}"
TABLE="hydraveil"
# Confirm the action is among the valid choices:
if [[ "$ACTION" != "arm" && "$ACTION" != "disarm" && "$ACTION" != "status" ]]; then
echo "[killswitch] ERROR: invalid action '$ACTION'. Usage: arm <server_ip> <tunnel_if> | disarm | status" >&2
exit 1
fi
# This function gets the default route. The function is used during the arm phase later on.
_default_iface() {
ip route show default 2>/dev/null | awk 'NR==1{print $5}'
}
# Log changes in the rules:
_log() {
local level="$1"; shift
echo "[killswitch] [$level] $*$(date '+%Y-%m-%d %H:%M:%S')" >&2
}
# use the presence of the table to detect if the rules are active, which we define as "armed"
if [[ "$ACTION" == "status" ]]; then
if nft list table inet "$TABLE" &>/dev/null 2>&1; then
echo "armed"
else
echo "disarmed"
fi
exit 0
fi
# If disarm is chosen, then check if the table rules exist, and if so, delete the table. Then log it.
if [[ "$ACTION" == "disarm" ]]; then
if nft list table inet "$TABLE" &>/dev/null 2>&1; then
nft delete table inet "$TABLE"
_log "INFO" "disarmed"
else
_log "INFO" "already disarmed"
fi
exit 0
fi
# Check the pre-reqs exist. We need a server IP and a tunnel interface.
[[ -z "$SERVER_IP" ]] && { _log "ERROR" "server_ip required for arm"; exit 1; }
[[ -z "$TUNNEL_IF" ]] && { _log "ERROR" "tunnel_if required for arm"; exit 1; }
# Confirm the IPv4 address is a valid format. But does NOT check if it's actually a live server.
# Just that it looks like an IPv4 address.
if ! [[ "$SERVER_IP" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
_log "ERROR" "invalid IPv4: $SERVER_IP"
exit 1
fi
IFS='.' read -r o1 o2 o3 o4 <<< "$SERVER_IP"
for oct in "$o1" "$o2" "$o3" "$o4"; do
if (( oct > 255 )); then
_log "ERROR" "invalid IPv4 octet ($oct) in $SERVER_IP"
exit 1
fi
done
# This uses the function at the top of the file to get the default WAN interface, and then checks that it got a result.
WAN_IFACE=$(_default_iface)
if [[ -z "$WAN_IFACE" ]]; then
_log "ERROR" "could not detect primary network interface"
exit 1
fi
# Either list out the table rules and delete them, or return True.
nft list table inet "$TABLE" &>/dev/null 2>&1 && nft delete table inet "$TABLE" || true
table inet ${TABLE} {
chain output {
type filter hook output priority filter; policy drop;
# Allow all loopback traffic (localhost)
oifname "lo" accept
# Drop all IPv6 except loopback (::1). This blocks IPv6 from leaving the system entirely
ip6 daddr != ::1 drop
# Allow IPv4 traffic to the server IP on the WAN interface
oifname "${WAN_IFACE}" ip daddr ${SERVER_IP} accept
# Allow all traffic (IPv4 and IPv6) on the WireGuard tunnel interface
oifname "${TUNNEL_IF}" accept
}
chain input {
type filter hook input priority filter; policy drop;
# Allow all loopback traffic (localhost)
iifname "lo" accept
# Drop all IPv6 except loopback (::1). This blocks IPv6 from entering the system entirely
ip6 saddr != ::1 drop
# Allow established and related connections (stateful firewall tracking)
ct state established,related accept
# Allow IPv4 traffic from the server IP on the WAN interface
iifname "${WAN_IFACE}" ip saddr ${SERVER_IP} accept
# Allow all traffic (IPv4 and IPv6) from the WireGuard tunnel interface
iifname "${TUNNEL_IF}" accept
}
chain forward {
type filter hook forward priority filter; policy drop;
# Drop all forwarded traffic (nothing is routed through this system)
}
}

View file

@ -0,0 +1,42 @@
import subprocess
from core.Constants import Constants
from core.models.Result import Result, ResultError
from core.errors.logger import logger
_C = Constants()
# _C.KILLSWITCH_WRAPPER
TESTING_KILLSWITCH = "/opt/hydra-veil/test" # needs chmod 755
def arm(server_ip: str, tunnel_if: str, internal_subnet: str = None) -> Result:
cmd = ["sudo", TESTING_KILLSWITCH, "arm", server_ip, tunnel_if]
if internal_subnet:
cmd.append(internal_subnet)
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
error_log = f"Failed to arm firewall: {result.stderr.strip()}"
logger.error(f"[KILLSWITCH] {error_log}")
return Result(valid=False, error_type=ResultError.FIREWALL, message=error_log)
else:
return Result(valid=True)
def disarm() -> bool:
result = subprocess.run(
["sudo", _C.KILLSWITCH_WRAPPER, "disarm"],
capture_output=True,
text=True,
)
if result.returncode != 0:
print(f"[killswitch] Failed to disarm: {result.stderr.strip()}")
return False
return True
def status() -> bool:
result = subprocess.run(
["sudo", _C.KILLSWITCH_WRAPPER, "status"],
capture_output=True,
text=True,
)
return result.returncode == 0 and result.stdout.strip() == "armed"

View file

@ -0,0 +1,48 @@
from core.models.Result import Result, ResultError
import re
import shutil
from core.Errors import CommandNotFoundError
def check_system_prereqs():
if shutil.which('dbus-send') is None:
raise CommandNotFoundError('dbus-send')
if shutil.which('nmcli') is None:
raise CommandNotFoundError('nmcli')
def extract_wg_interface_name(text: str) -> Result:
"""
Purpose:
Extracts the wg interface name, which should be 'wg'
Pattern:
"Connection" followed by text in single quotes
Example:
"Connection 'wg' (jgfdgfdjgfdjfdg"
"""
pattern = r"Connection '([^']*)'"
match = re.search(pattern, text)
if match:
# Return what's between the quotes
result = match.group(1)
return Result(valid=True, data=result)
else:
error_msg = "Error: String doesn't contain 'Connection' and text within single quotes"
return Result(valid=True, error_type=ResultError.NMCLI, message=error_msg)
######### ISOLATION TESTING ########
# result1 = extract_connection_name("Connection 'wg' (jgfdgfdjgfdjfdg")
# print(result1) # Output: wg

View file

@ -1,5 +1,11 @@
from core.services.networking.general_connection_tools.testing_evaluating import await_connection, system_uses_wireguard_interface, terminate_tor_connection 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.services.keys_and_verifications.endpoint_verification import verify_wireguard_endpoint
from core.models.Result import Result, ResultError
from core.services.networking.systemwide.systemwide_utils import extract_wg_interface_name, check_system_prereqs
from core.services.networking.general_connection_tools.config_tools import extract_endpoint_ip
from core.services.networking.systemwide import killswitch
from core.errors.logger import logger
from core.Constants import Constants from core.Constants import Constants
from core.Errors import ConnectionTerminationError, CommandNotFoundError from core.Errors import ConnectionTerminationError, CommandNotFoundError
@ -37,42 +43,87 @@ def terminate_system_connection():
else: else:
raise ConnectionTerminationError('The connection could not be terminated.') raise ConnectionTerminationError('The connection could not be terminated.')
def setup_nmcli_connection(wg_config_path: str) -> Result:
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: 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', wg_config_path), text=True)
except CalledProcessError: return Result(valid=True, data=process_output)
raise ConnectionError('The connection could not be established.')
except CalledProcessError as e:
return Result(valid=False, error_type=ResultError.NMCLI, message=f"Called Process error with nmcli: {e}")
# raise ConnectionError('The connection could not be established.')
def setup_ipv6_sinkhole(process_output: str) -> Result:
error_info = "with nmcli setting up the IPv6 sinkhole, error is "
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 as e:
except CalledProcessError: return Result(valid=False, error_type=ResultError.NMCLI, message=f"Called Process error {error_info}: {e}")
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 as e:
raise ConnectionError('The connection could not be established.') return Result(valid=False, error_type=ResultError.NMCLI, message=f"Called Process error {error_info}: {e}")
# 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: return Result(valid=True)
raise ConnectionError('The connection could not be established.')
except CalledProcessError:
return Result(valid=False, error_type=ResultError.NMCLI, message=f"Called Process error {error_info}")
# raise ConnectionError('The connection could not be established.')
def __establish_system_connection(profile: SystemProfile, firewall_setting: bool = False, connection_observer: Optional[ConnectionObserver] = None):
check_system_prereqs()
terminate_system_connection()
# ============= INITIAL NMCLI CONNECTION =============
wg_config_path = profile.get_wireguard_configuration_path()
nmcli_result = setup_nmcli_connection(wg_config_path)
if nmcli_result.valid:
process_output = nmcli_result.data
else:
logger.error(f"[SYSTEMWIDE WG] Critical issue, the initial nmcli command failed {nmcli_result.message}.")
return nmcli_result
wg_interface_result = extract_wg_interface_name(process_output)
if not wg_interface_result.valid:
logger.error(f"[SYSTEMWIDE WG] Critical issue, the wg interface is in the wrong format. This is the raw output: {process_output}")
raise ConnectionError('The connection could not be established.')
# handle the interface being wrong
# ============= IPv6 SINKHOLE =============
sinkhole_result = setup_ipv6_sinkhole(process_output)
if not sinkhole_result:
raise ConnectionError('IPv6 Could not be protected.')
# ============= FIREWALL =============
if firewall_setting:
logger.info(f"[SYSTEMWIDE WG] Turning on Firewall")
wg_interface_name = wg_interface_result.data
server_ip_address = extract_endpoint_ip(str(profile.id))
print(f"WG Inteface Name: {wg_interface_name}")
print(f"Server IP address: {server_ip_address}")
firewall_results = killswitch.arm(
server_ip=server_ip_address,
tunnel_if=wg_interface_name,
internal_subnet=None
)
if not firewall_results.valid:
print(f"FAILED to setup Firewall. firewall_results is {firewall_results.message}")
# raise ConnectionError('Firewall could not be enabled.')
# ============= SETUP STATE & TEST =============
SystemStateController.create(profile.id) SystemStateController.create(profile.id)
try: try:
@ -81,15 +132,35 @@ def __establish_system_connection(profile: SystemProfile, connection_observer: O
except ConnectionError: except ConnectionError:
raise ConnectionError('The connection could not be established.') raise ConnectionError('The connection could not be established.')
return Result(valid=True)
def evaluate_connection_result(connection_result: Result):
if not connection_result.valid:
logger.error(f"[SYSTEMWIDE WG] Critical issue, could not establish a connection: {connection_result.message}")
try:
terminate_system_connection()
except ConnectionTerminationError:
pass
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):
"""
Rank:
Public Interface Orchestrator
Purpose:
Tries to enable the systemwide connection, retries on failure.
"""
# ================= VERIFY ENDPOINT =================
if ConfigurationController.get_endpoint_verification_enabled(): if ConfigurationController.get_endpoint_verification_enabled():
verify_wireguard_endpoint(profile, ignore=ignore) verify_wireguard_endpoint(profile, ignore=ignore)
# ================= CONNECT WG =================
try: try:
__establish_system_connection(profile, connection_observer) connection_result = __establish_system_connection(profile, False, connection_observer)
evaluate_connection_result(connection_result)
except ConnectionError: except ConnectionError:
@ -107,8 +178,10 @@ def establish_system_connection(profile: SystemProfile, ignore: tuple[type[Excep
except ConnectionTerminationError: except ConnectionTerminationError:
pass pass
# trying again..
try: try:
__establish_system_connection(profile, connection_observer) connection_result = __establish_system_connection(profile, False, connection_observer)
evaluate_connection_result(connection_result)
except (ConnectionError, CalledProcessError): except (ConnectionError, CalledProcessError):

View file

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