From bd34f456b56cd21274bd67396d2981dc55455016 Mon Sep 17 00:00:00 2001 From: SimplifiedPrivacy Date: Mon, 20 Jul 2026 13:07:04 -0400 Subject: [PATCH] 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. --- core/Constants.py | 5 + .../tickets/UseTicketController.py | 2 +- core/models/BaseProfile.py | 12 +- core/models/Result.py | 6 +- core/models/orm_models/Location.py | 4 +- core/models/session/SessionConnection.py | 45 +++++-- core/models/session/SessionProfile.py | 2 +- core/models/system/SystemConnection.py | 37 +++--- .../general_connection_tools/config_tools.py | 19 +++ .../connection_enable.py | 12 +- .../systemwide/firewall_isolated.sh | 118 ----------------- .../networking/systemwide/killswitch.py | 42 ++++++ .../networking/systemwide/systemwide_utils.py | 48 +++++++ .../systemwide/systemwide_wireguard.py | 123 ++++++++++++++---- 14 files changed, 291 insertions(+), 184 deletions(-) create mode 100644 core/services/networking/general_connection_tools/config_tools.py delete mode 100644 core/services/networking/systemwide/firewall_isolated.sh create mode 100644 core/services/networking/systemwide/killswitch.py create mode 100644 core/services/networking/systemwide/systemwide_utils.py diff --git a/core/Constants.py b/core/Constants.py index 20dc285..026e0b8 100644 --- a/core/Constants.py +++ b/core/Constants.py @@ -65,3 +65,8 @@ class Constants: HV_SESSION_STATE_HOME: Final[str] = f'{HV_STATE_HOME}/sessions' 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' + ) diff --git a/core/controllers/tickets/UseTicketController.py b/core/controllers/tickets/UseTicketController.py index a1473ad..0341528 100644 --- a/core/controllers/tickets/UseTicketController.py +++ b/core/controllers/tickets/UseTicketController.py @@ -113,7 +113,7 @@ def get_unused_tickets(ticket_observer: TicketObserver) -> dict: does_the_file_exist, path_of_file = does_ticket_tracker_exist() if does_the_file_exist == False: 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} # Use the Model: diff --git a/core/models/BaseProfile.py b/core/models/BaseProfile.py index e38e13b..7f560cb 100644 --- a/core/models/BaseProfile.py +++ b/core/models/BaseProfile.py @@ -11,6 +11,10 @@ from core.Helpers import write_atomically from core.models.manage.wrapper import safe_db_operation, WrapperRollback 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.models.orm_models.Location import Location from core.models.orm_models.Operator import Operator @@ -64,8 +68,8 @@ class BaseProfile(ABC): ) name: str subscription: Optional[Subscription] - location: Optional[Location] = field(metadata=config(exclude=Exclude.ALWAYS)) # SQLAlchemy object 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. # location: Optional[Location] @@ -98,13 +102,15 @@ class BaseProfile(ABC): def save(self: Self): + # === SERIALIZATION === 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: config_dict["location"] = location_dict + # === FILE I/O === config_file_contents = json.dumps(config_dict, indent=4) + '\n' # legacy version: diff --git a/core/models/Result.py b/core/models/Result.py index 4246e6a..012d4f4 100644 --- a/core/models/Result.py +++ b/core/models/Result.py @@ -11,10 +11,14 @@ class ResultError(Enum): INVALID_INPUT = "invalid_input" CONNECTION = "connection" DATABASE = "database" + PERMISSION = "permission" + SUBSCRIPTION = "subscription" + NMCLI = "nmcli_issues" + FIREWALL = "firewall" UNKNOWN = "unknown" @dataclass -class Result: +class Result(): valid: bool error_type: ResultError = ResultError.SUCCESS data: Optional[Any] = None diff --git a/core/models/orm_models/Location.py b/core/models/orm_models/Location.py index 68ce7a5..13e2ff1 100644 --- a/core/models/orm_models/Location.py +++ b/core/models/orm_models/Location.py @@ -46,12 +46,12 @@ class Location(BaseModel): 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) - def to_dict(self): + def convert_to_dict(self): return { "country_code": self.country_code, "code": self.code, "time_zone": self.time_zone - } + } # to use: diff --git a/core/models/session/SessionConnection.py b/core/models/session/SessionConnection.py index c9626e3..745a683 100644 --- a/core/models/session/SessionConnection.py +++ b/core/models/session/SessionConnection.py @@ -2,14 +2,15 @@ from core.models.BaseConnection import BaseConnection from dataclasses import dataclass from enum import Enum -class SessionConnectionTypes(str, Enum): - WIREGUARD = "wireguard" - TOR = "tor" - @dataclass class SessionConnection(BaseConnection): - code: SessionConnectionTypes - masked: bool + masked: bool = False + + def __post_init__(self): + + if self.code not in ('system', 'tor', 'wireguard'): + raise ValueError('Invalid connection code.') + # Called by: ConnectionController # 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 -# 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 -# 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 # 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'): -# raise ValueError('Invalid connection code.') +# @dataclass +# class SessionConnection(BaseConnection): +# code: SessionConnectionTypes +# masked: bool diff --git a/core/models/session/SessionProfile.py b/core/models/session/SessionProfile.py index b2e7f9c..d5e784c 100644 --- a/core/models/session/SessionProfile.py +++ b/core/models/session/SessionProfile.py @@ -16,7 +16,7 @@ import shutil class SessionProfile(BaseProfile): resolution: str application_version: Optional[ApplicationVersion] - connection: Optional[SessionConnection] + connection: Optional[SessionConnection] = None def has_connection(self): return self.connection is not None diff --git a/core/models/system/SystemConnection.py b/core/models/system/SystemConnection.py index cc9371e..7de09f2 100644 --- a/core/models/system/SystemConnection.py +++ b/core/models/system/SystemConnection.py @@ -1,28 +1,29 @@ from core.models.BaseConnection import BaseConnection from dataclasses import dataclass -from enum import Enum -from typing import Literal - -class SystemConnectionTypes(str, Enum): - WIREGUARD = "wireguard" - HYSTERIA2 = "hysteria2" - VLESS = "vless" +# legacy: @dataclass -class SystemConnection(BaseConnection): - code: SystemConnectionTypes - masked: Literal[False] = False +class SystemConnection (BaseConnection): + + def __post_init__(self): + + if self.code not in ('vless', 'hysteria2', 'wireguard'): + raise ValueError('Invalid connection code.') @staticmethod def needs_proxy_configuration(): return False -# legacy: +# Potential refactor: +# from enum import Enum +# from typing import Literal + +# class SystemConnectionTypes(str, Enum): +# WIREGUARD = "wireguard" +# HYSTERIA2 = "hysteria2" +# VLESS = "vless" + # @dataclass -# class SystemConnection (BaseConnection): - - -# def __post_init__(self): - -# if self.code not in ('vless', 'hysteria2', 'wireguard'): -# raise ValueError('Invalid connection code.') +# class SystemConnection(BaseConnection): +# code: SystemConnectionTypes +# masked: Literal[False] = False diff --git a/core/services/networking/general_connection_tools/config_tools.py b/core/services/networking/general_connection_tools/config_tools.py new file mode 100644 index 0000000..12c4d33 --- /dev/null +++ b/core/services/networking/general_connection_tools/config_tools.py @@ -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 diff --git a/core/services/networking/general_connection_tools/connection_enable.py b/core/services/networking/general_connection_tools/connection_enable.py index a350f30..b000c78 100644 --- a/core/services/networking/general_connection_tools/connection_enable.py +++ b/core/services/networking/general_connection_tools/connection_enable.py @@ -1,10 +1,14 @@ 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.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.exceptions import * from typing import Union, Optional, Callable @@ -17,8 +21,6 @@ from core.models.BaseProfile import ProfileType 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]] = (), @@ -75,7 +77,7 @@ def _ensure_wireguard_configured( """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: + if profile.connection.code != "wireguard": logger.info(f"[CONNECTION] This profile {profile.id} does NOT need a wireguard config") return diff --git a/core/services/networking/systemwide/firewall_isolated.sh b/core/services/networking/systemwide/firewall_isolated.sh deleted file mode 100644 index 6ae9a34..0000000 --- a/core/services/networking/systemwide/firewall_isolated.sh +++ /dev/null @@ -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 | 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) - } -} diff --git a/core/services/networking/systemwide/killswitch.py b/core/services/networking/systemwide/killswitch.py new file mode 100644 index 0000000..d6c84f5 --- /dev/null +++ b/core/services/networking/systemwide/killswitch.py @@ -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" \ No newline at end of file diff --git a/core/services/networking/systemwide/systemwide_utils.py b/core/services/networking/systemwide/systemwide_utils.py new file mode 100644 index 0000000..4e249f3 --- /dev/null +++ b/core/services/networking/systemwide/systemwide_utils.py @@ -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 diff --git a/core/services/networking/systemwide/systemwide_wireguard.py b/core/services/networking/systemwide/systemwide_wireguard.py index c926c36..ac3816d 100644 --- a/core/services/networking/systemwide/systemwide_wireguard.py +++ b/core/services/networking/systemwide/systemwide_wireguard.py @@ -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.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.Errors import ConnectionTerminationError, CommandNotFoundError @@ -37,42 +43,87 @@ def terminate_system_connection(): 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() - +def setup_nmcli_connection(wg_config_path: str) -> Result: 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.') + process_output = subprocess.check_output(('nmcli', 'connection', 'import', '--temporary', 'type', 'wireguard', 'file', wg_config_path), text=True) + return Result(valid=True, data=process_output) + 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: - 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.') + except CalledProcessError as e: + return Result(valid=False, error_type=ResultError.NMCLI, message=f"Called Process error {error_info}: {e}") + # 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.') + except CalledProcessError as e: + 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: 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.') + return Result(valid=True) + 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) try: @@ -81,15 +132,35 @@ def __establish_system_connection(profile: SystemProfile, connection_observer: O except ConnectionError: 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): + """ + Rank: + Public Interface Orchestrator + Purpose: + Tries to enable the systemwide connection, retries on failure. + """ + + # ================= VERIFY ENDPOINT ================= if ConfigurationController.get_endpoint_verification_enabled(): verify_wireguard_endpoint(profile, ignore=ignore) + # ================= CONNECT WG ================= try: - __establish_system_connection(profile, connection_observer) + connection_result = __establish_system_connection(profile, False, connection_observer) + evaluate_connection_result(connection_result) except ConnectionError: @@ -107,8 +178,10 @@ def establish_system_connection(profile: SystemProfile, ignore: tuple[type[Excep except ConnectionTerminationError: pass + # trying again.. try: - __establish_system_connection(profile, connection_observer) + connection_result = __establish_system_connection(profile, False, connection_observer) + evaluate_connection_result(connection_result) except (ConnectionError, CalledProcessError):