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.

This commit is contained in:
SimplifiedPrivacy 2026-07-20 13:07:04 -04:00
parent d8273171b5
commit bd34f456b5
14 changed files with 291 additions and 184 deletions

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)
# # Validation
# if self.code not in SessionConnectionTypes:
# raise ValueError(f'Invalid code: {self.code}')
# if self.code not in ('system', 'tor', 'wireguard'): # @dataclass
# raise ValueError('Invalid connection code.') # 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" # has 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):