diff --git a/core/controllers/ConfigurationController.py b/core/controllers/ConfigurationController.py index 3dc0d09..f92b3d7 100644 --- a/core/controllers/ConfigurationController.py +++ b/core/controllers/ConfigurationController.py @@ -91,3 +91,10 @@ class ConfigurationController: @staticmethod def update_or_create(configuration): configuration.save() + + @staticmethod + def change_firewall(new_value): + configuration = ConfigurationController.get_or_new() + configuration.firewall = new_value + configuration.save() + diff --git a/core/controllers/ProfileController.py b/core/controllers/ProfileController.py index 5c257f3..c65935a 100644 --- a/core/controllers/ProfileController.py +++ b/core/controllers/ProfileController.py @@ -1,6 +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.services.networking.systemwide.systemwide_utils import get_firewall_setting from core.Errors import InvalidSubscriptionError, MissingSubscriptionError, ConnectionTerminationError, ProfileActivationError, ProfileDeactivationError, MissingLocationError, ConnectionUnprotectedError, EndpointVerificationError, ProfileStateConflictError from core.controllers.ApplicationController import ApplicationController @@ -132,7 +133,8 @@ class ProfileController: raise ProfileDeactivationError('The profile could not be disabled.') try: - terminate_system_connection() + firewall_setting = get_firewall_setting() + terminate_system_connection(firewall_setting) except ConnectionTerminationError: raise ProfileDeactivationError('The profile could not be disabled.') diff --git a/core/controllers/SystemStateController.py b/core/controllers/SystemStateController.py index 18bb459..0289801 100644 --- a/core/controllers/SystemStateController.py +++ b/core/controllers/SystemStateController.py @@ -12,8 +12,8 @@ class SystemStateController: return SystemState.exists() @staticmethod - def create(profile_id): - return SystemState(profile_id).save() + def create(profile_id: int, firewalled: bool) -> SystemState: + return SystemState(profile_id, firewalled).save() @staticmethod def update_or_create(system_state): diff --git a/core/models/Configuration.py b/core/models/Configuration.py index 6852a5a..b29f7b4 100644 --- a/core/models/Configuration.py +++ b/core/models/Configuration.py @@ -1,3 +1,5 @@ +from core.errors.logger import logger + from core.Constants import Constants from core.Helpers import write_atomically from dataclasses import dataclass, field @@ -47,6 +49,14 @@ class Configuration: ) ) + firewall: Optional[bool] = field( + default=False, + metadata=config( + undefined=dataclasses_json.Undefined.EXCLUDE, + exclude=lambda value: value is None + ) + ) + def save(self: Self): config_file_contents = f'{self.to_json(indent=4)}\n' @@ -82,3 +92,31 @@ class Configuration: def _from_iso_format(datetime_string: str): date_string = datetime_string.replace('Z', '+00:00') return datetime.fromisoformat(date_string) + + +def read_config(): + try: + config_file_contents = open(f'{Constants.HV_CONFIG_HOME}/config.json', 'r').read() + except FileNotFoundError: + return None + + try: + configuration = json.loads(config_file_contents) + except ValueError: + logger.error(f"[CONFIG] Can't load JSON config") + + return configuration + +def get_setting(looking_for): + config = read_config() + if not config: + logger.error(f"[CONFIG] Can't load the entire config") + return None + + if looking_for not in config: + logger.error(f"[CONFIG] What you want isn't in the config") + return None + + result = config[looking_for] + + return result diff --git a/core/models/session/SessionConnection.py b/core/models/session/SessionConnection.py index 745a683..85ead48 100644 --- a/core/models/session/SessionConnection.py +++ b/core/models/session/SessionConnection.py @@ -1,6 +1,5 @@ from core.models.BaseConnection import BaseConnection from dataclasses import dataclass -from enum import Enum @dataclass class SessionConnection(BaseConnection): @@ -26,6 +25,7 @@ class SessionConnection(BaseConnection): # from dataclasses import dataclass, field # from dataclasses_json import dataclass_json, config +# from enum import Enum # class SessionConnectionTypes(str, Enum): # WIREGUARD = "wireguard" @@ -49,9 +49,4 @@ class SessionConnection(BaseConnection): # # Validation # if self.code not in SessionConnectionTypes: -# raise ValueError(f'Invalid code: {self.code}') - -# @dataclass -# class SessionConnection(BaseConnection): -# code: SessionConnectionTypes -# masked: bool +# raise ValueError(f'Invalid code: {self.code}') \ No newline at end of file diff --git a/core/models/system/SystemState.py b/core/models/system/SystemState.py index 2187faa..8080e72 100644 --- a/core/models/system/SystemState.py +++ b/core/models/system/SystemState.py @@ -11,6 +11,7 @@ import pathlib @dataclass class SystemState: profile_id: int + firewalled: bool def save(self: Self): diff --git a/core/services/networking/general_connection_tools/connection_enable.py b/core/services/networking/general_connection_tools/connection_enable.py index b000c78..073eb61 100644 --- a/core/services/networking/general_connection_tools/connection_enable.py +++ b/core/services/networking/general_connection_tools/connection_enable.py @@ -119,7 +119,7 @@ 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.connection.code == "wireguard" and profile.has_wireguard_configuration(): if profile.subscription.has_been_activated(): return True else: diff --git a/core/services/networking/systemwide/killswitch.py b/core/services/networking/systemwide/killswitch.py index 58ff6a1..a94ab76 100644 --- a/core/services/networking/systemwide/killswitch.py +++ b/core/services/networking/systemwide/killswitch.py @@ -23,19 +23,21 @@ def arm(server_ip: str, tunnel_if: str, internal_subnet: str = None) -> Result: def disarm() -> bool: result = subprocess.run( - ["sudo", _C.KILLSWITCH_WRAPPER, "disarm"], + ["sudo", TESTING_KILLSWITCH, "disarm"], capture_output=True, text=True, ) if result.returncode != 0: - print(f"[killswitch] Failed to disarm: {result.stderr.strip()}") - return False - return True + error_msg = f"Failed to disarm: {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 status() -> bool: result = subprocess.run( - ["sudo", _C.KILLSWITCH_WRAPPER, "status"], + ["sudo", TESTING_KILLSWITCH, "status"], capture_output=True, text=True, ) diff --git a/core/services/networking/systemwide/systemwide_utils.py b/core/services/networking/systemwide/systemwide_utils.py index 4e249f3..d5a1319 100644 --- a/core/services/networking/systemwide/systemwide_utils.py +++ b/core/services/networking/systemwide/systemwide_utils.py @@ -3,6 +3,18 @@ from core.models.Result import Result, ResultError import re import shutil from core.Errors import CommandNotFoundError +from core.models.Configuration import get_setting +from core.errors.logger import logger + + +def get_firewall_setting() -> bool: + firewall_setting = get_setting("firewall") + logger.info(f"Firewall Setting is {firewall_setting}") + if firewall_setting is None: + return False + return firewall_setting + + def check_system_prereqs(): if shutil.which('dbus-send') is None: diff --git a/core/services/networking/systemwide/systemwide_wireguard.py b/core/services/networking/systemwide/systemwide_wireguard.py index ac3816d..b40f945 100644 --- a/core/services/networking/systemwide/systemwide_wireguard.py +++ b/core/services/networking/systemwide/systemwide_wireguard.py @@ -1,12 +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.systemwide.systemwide_utils import extract_wg_interface_name, check_system_prereqs, get_firewall_setting 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 from core.controllers.ConfigurationController import ConfigurationController @@ -24,13 +23,73 @@ import shutil import subprocess import time -def terminate_system_connection(): + +def turn_on_firewall(profile_id: str, process_output: str) -> Result: + """ + Purpose: + This is a wireguard specific function, using the WG interface, + but the underlying killswitch.arm function it uses is protocol neutral. + + Steps: + 1) Get the WG interface from the process output + 2) Get the server IP from the backup WG config + 3) Use the killswitch's arm + """ + logger.info(f"[SYSTEMWIDE WG] Turning on Firewall..") + + wg_interface_result = extract_wg_interface_name(process_output) + + if not wg_interface_result.valid: + error_msg = f"WG interface is in the wrong format. This is the raw output: {process_output}" + logger.error(f"[SYSTEMWIDE WG] Critical issue, the {error_msg}") + raise ConnectionError(error_msg) + # or handle the interface being wrong? + + wg_interface_name = wg_interface_result.data + server_ip_address = extract_endpoint_ip(profile_id) + # print(f"WG Inteface Name: {wg_interface_name}") + + return killswitch.arm( + server_ip=server_ip_address, + tunnel_if=wg_interface_name, + internal_subnet=None + ) + +# This raises errors on failure. +def check_and_kill_firewall(): + firewall_status = killswitch.status() + logger.info(f"Inside check_and_kill_firewall the firewall status is {firewall_status}") + + logger.info(f"Terminate connection ran check_and_kill_firewall which evaluated the status as {firewall_status}") + + # is it even armed? if not, get lost, + if firewall_status == "on": + # if it's armed, kill it, + logger.info(f"Killing the firewall") + firewall_results = killswitch.disarm() + + # did that work? + if firewall_results.valid: + logger.info(f"We disabled the firewall correctly.") + return + else: + logger.error(f"[FIREWALL] CRITICAL ISSUE with the Firewall being disabled: {firewall_results.message}") + error_msg = f"Firewall couldn't be disabled: {firewall_results.message}" + raise ConnectionTerminationError(error_msg) + else: + logger.info("We are skipping disabling the firewall, because it's already off.") + +def terminate_system_connection(firewall_setting) -> Result: if shutil.which('nmcli') is None: raise CommandNotFoundError('nmcli') - if SystemStateController.exists(): + # ====== FIREWALL ======= + if firewall_setting: + check_and_kill_firewall() # on failure, this raises errors, which then bubble up + # ====== 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) @@ -77,10 +136,28 @@ def setup_ipv6_sinkhole(process_output: str) -> Result: # raise ConnectionError('The connection could not be established.') -def __establish_system_connection(profile: SystemProfile, firewall_setting: bool = False, connection_observer: Optional[ConnectionObserver] = None): +def __establish_system_connection( + profile: SystemProfile, + firewall_setting: bool = False, + connection_observer: Optional[ConnectionObserver] = None): + """ + Purpose: + Orchestrates the full flow of the connection. + + Steps: + 1) Check Prereqs + 2) Disable any pre-existing system connections + 3) Turn on WG via nmcli + 4) Turn on IPv6 sinkhole + 5) Turn on Firewall (if enabled) + 6) Setup State (JSON file) + 7) Confirm Firewall is on (if enabled). Raises errors if not. + 8) Testing, confirming, displaying to the end user. + """ check_system_prereqs() - terminate_system_connection() + logger.info("[CONNECT] Terming existing connections..") + terminate_system_connection(firewall_setting) # ============= INITIAL NMCLI CONNECTION ============= wg_config_path = profile.get_wireguard_configuration_path() @@ -88,44 +165,55 @@ def __establish_system_connection(profile: SystemProfile, firewall_setting: bool if nmcli_result.valid: process_output = nmcli_result.data + logger.info("[CONNECT] Successfully turned on WG with nmcli") 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 ============= + firewall_tracker = False 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 + logger.info("Firewall setting is enabled, let's turn it on..") + turn_on_result = turn_on_firewall( + profile_id=str(profile.id), + process_output=process_output ) + logger.info(f"Results of firewall turn on are: {turn_on_result.valid}") + if turn_on_result.valid: + firewall_tracker = True - 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) + # ============= SETUP STATE ============= + # Even if firewall is off, we want to save the fact we turned on the VPN tunnel, before we raise errors. + logger.info(f"Setting State JSON..") + SystemStateController.create( + profile_id=profile.id, + firewalled=firewall_tracker + ) + + # ============= CONFIRM ON FIREWALL ============= + if firewall_setting: + logger.info(f"Evaluating Firewall Situation after we tried to turn on it on.") + firewall_status = killswitch.status() + logger.info(f""" +The firewall setting is on. +The result of arming it, {turn_on_result.valid} +The result of the status {firewall_status} + """) + if firewall_status: + logger.info("Firewall is on. All good.") + else: + logger.error("Firewall FAILED TO ARM") # TRIES TO RENEGOTIATE WITH DEAD INTERNET !! + # raise ConnectionError(f'Firewall failed to arm!! {turn_on_result.message}.') # if this triggers, it tries to renegotiate with dead internet. + + logger.info("If we made it this far, we have nmcli on, ipv6 sinkhole on, firewall on.") + # ============= TESTING ============= try: await_connection(connection_observer=connection_observer) @@ -139,18 +227,23 @@ 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() + terminate_system_connection(firewall_setting) 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. + Calls the private function with the full flow, so it can be retried on failure. + + Steps) + 1) Verify Endpoint's encryption + 2) Pass to private function + 3) Retry on failure """ # ================= VERIFY ENDPOINT ================= @@ -158,14 +251,19 @@ def establish_system_connection(profile: SystemProfile, ignore: tuple[type[Excep verify_wireguard_endpoint(profile, ignore=ignore) # ================= CONNECT WG ================= + firewall_setting = get_firewall_setting() try: - connection_result = __establish_system_connection(profile, False, connection_observer) + connection_result = __establish_system_connection( + profile=profile, + firewall_setting=firewall_setting, + connection_observer=connection_observer + ) evaluate_connection_result(connection_result) except ConnectionError: - + # ================= RETRY ON FAILURE ================= try: - terminate_system_connection() + terminate_system_connection(firewall_setting) except ConnectionTerminationError: pass @@ -174,19 +272,23 @@ def establish_system_connection(profile: SystemProfile, ignore: tuple[type[Excep except CalledProcessError: try: - terminate_system_connection() + terminate_system_connection(firewall_setting) except ConnectionTerminationError: pass # trying again.. try: - connection_result = __establish_system_connection(profile, False, connection_observer) + connection_result = __establish_system_connection( + profile=profile, + firewall_setting=firewall_setting, + connection_observer=connection_observer + ) evaluate_connection_result(connection_result) except (ConnectionError, CalledProcessError): try: - terminate_system_connection() + terminate_system_connection(firewall_setting) except ConnectionTerminationError: pass