Integration of Firewall feature to Systemwide Wireguard

This commit is contained in:
SimplifiedPrivacy 2026-07-21 09:44:03 -04:00
parent 9ba6d7e783
commit c4ffe9be3c
10 changed files with 216 additions and 57 deletions

View file

@ -91,3 +91,10 @@ class ConfigurationController:
@staticmethod @staticmethod
def update_or_create(configuration): def update_or_create(configuration):
configuration.save() configuration.save()
@staticmethod
def change_firewall(new_value):
configuration = ConfigurationController.get_or_new()
configuration.firewall = new_value
configuration.save()

View file

@ -1,6 +1,7 @@
from core.services.networking.systemwide.systemwide_wireguard import terminate_system_connection 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.testing_evaluating import system_uses_wireguard_interface
from core.services.networking.general_connection_tools.connection_enable import establish_connection 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.Errors import InvalidSubscriptionError, MissingSubscriptionError, ConnectionTerminationError, ProfileActivationError, ProfileDeactivationError, MissingLocationError, ConnectionUnprotectedError, EndpointVerificationError, ProfileStateConflictError
from core.controllers.ApplicationController import ApplicationController from core.controllers.ApplicationController import ApplicationController
@ -132,7 +133,8 @@ class ProfileController:
raise ProfileDeactivationError('The profile could not be disabled.') raise ProfileDeactivationError('The profile could not be disabled.')
try: try:
terminate_system_connection() firewall_setting = get_firewall_setting()
terminate_system_connection(firewall_setting)
except ConnectionTerminationError: except ConnectionTerminationError:
raise ProfileDeactivationError('The profile could not be disabled.') raise ProfileDeactivationError('The profile could not be disabled.')

View file

@ -12,8 +12,8 @@ class SystemStateController:
return SystemState.exists() return SystemState.exists()
@staticmethod @staticmethod
def create(profile_id): def create(profile_id: int, firewalled: bool) -> SystemState:
return SystemState(profile_id).save() return SystemState(profile_id, firewalled).save()
@staticmethod @staticmethod
def update_or_create(system_state): def update_or_create(system_state):

View file

@ -1,3 +1,5 @@
from core.errors.logger import logger
from core.Constants import Constants from core.Constants import Constants
from core.Helpers import write_atomically from core.Helpers import write_atomically
from dataclasses import dataclass, field 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): def save(self: Self):
config_file_contents = f'{self.to_json(indent=4)}\n' config_file_contents = f'{self.to_json(indent=4)}\n'
@ -82,3 +92,31 @@ class Configuration:
def _from_iso_format(datetime_string: str): def _from_iso_format(datetime_string: str):
date_string = datetime_string.replace('Z', '+00:00') date_string = datetime_string.replace('Z', '+00:00')
return datetime.fromisoformat(date_string) 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

View file

@ -1,6 +1,5 @@
from core.models.BaseConnection import BaseConnection from core.models.BaseConnection import BaseConnection
from dataclasses import dataclass from dataclasses import dataclass
from enum import Enum
@dataclass @dataclass
class SessionConnection(BaseConnection): class SessionConnection(BaseConnection):
@ -26,6 +25,7 @@ class SessionConnection(BaseConnection):
# from dataclasses import dataclass, field # from dataclasses import dataclass, field
# from dataclasses_json import dataclass_json, config # from dataclasses_json import dataclass_json, config
# from enum import Enum
# class SessionConnectionTypes(str, Enum): # class SessionConnectionTypes(str, Enum):
# WIREGUARD = "wireguard" # WIREGUARD = "wireguard"
@ -49,9 +49,4 @@ class SessionConnection(BaseConnection):
# # Validation # # Validation
# if self.code not in SessionConnectionTypes: # if self.code not in SessionConnectionTypes:
# raise ValueError(f'Invalid code: {self.code}') # raise ValueError(f'Invalid code: {self.code}')
# @dataclass
# class SessionConnection(BaseConnection):
# code: SessionConnectionTypes
# masked: bool

View file

@ -11,6 +11,7 @@ import pathlib
@dataclass @dataclass
class SystemState: class SystemState:
profile_id: int profile_id: int
firewalled: bool
def save(self: Self): def save(self: Self):

View file

@ -119,7 +119,7 @@ def __should_renegotiate(profile: Union[SessionProfile, SystemProfile]):
if not profile.has_subscription(): if not profile.has_subscription():
raise MissingSubscriptionError() 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(): if profile.subscription.has_been_activated():
return True return True
else: else:

View file

@ -23,19 +23,21 @@ def arm(server_ip: str, tunnel_if: str, internal_subnet: str = None) -> Result:
def disarm() -> bool: def disarm() -> bool:
result = subprocess.run( result = subprocess.run(
["sudo", _C.KILLSWITCH_WRAPPER, "disarm"], ["sudo", TESTING_KILLSWITCH, "disarm"],
capture_output=True, capture_output=True,
text=True, text=True,
) )
if result.returncode != 0: if result.returncode != 0:
print(f"[killswitch] Failed to disarm: {result.stderr.strip()}") error_msg = f"Failed to disarm: {result.stderr.strip()}"
return False logger.error(f"[KILLSWITCH] {error_log}")
return True return Result(valid=False, error_type=ResultError.FIREWALL, message=error_log)
else:
return Result(valid=True)
def status() -> bool: def status() -> bool:
result = subprocess.run( result = subprocess.run(
["sudo", _C.KILLSWITCH_WRAPPER, "status"], ["sudo", TESTING_KILLSWITCH, "status"],
capture_output=True, capture_output=True,
text=True, text=True,
) )

View file

@ -3,6 +3,18 @@ from core.models.Result import Result, ResultError
import re import re
import shutil import shutil
from core.Errors import CommandNotFoundError 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(): def check_system_prereqs():
if shutil.which('dbus-send') is None: if shutil.which('dbus-send') is None:

View file

@ -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.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.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.general_connection_tools.config_tools import extract_endpoint_ip
from core.services.networking.systemwide import killswitch from core.services.networking.systemwide import killswitch
from core.errors.logger import logger 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
from core.controllers.ConfigurationController import ConfigurationController from core.controllers.ConfigurationController import ConfigurationController
@ -24,13 +23,73 @@ import shutil
import subprocess import subprocess
import time 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: if shutil.which('nmcli') is None:
raise CommandNotFoundError('nmcli') 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) process = subprocess.Popen(('nmcli', 'connection', 'delete', 'wg'), stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
completed_successfully = not bool(os.waitpid(process.pid, 0)[1] >> 8) completed_successfully = not bool(os.waitpid(process.pid, 0)[1] >> 8)
@ -77,10 +136,28 @@ def setup_ipv6_sinkhole(process_output: str) -> Result:
# raise ConnectionError('The connection could not be established.') # 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() check_system_prereqs()
terminate_system_connection() logger.info("[CONNECT] Terming existing connections..")
terminate_system_connection(firewall_setting)
# ============= INITIAL NMCLI CONNECTION ============= # ============= INITIAL NMCLI CONNECTION =============
wg_config_path = profile.get_wireguard_configuration_path() 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: if nmcli_result.valid:
process_output = nmcli_result.data process_output = nmcli_result.data
logger.info("[CONNECT] Successfully turned on WG with nmcli")
else: else:
logger.error(f"[SYSTEMWIDE WG] Critical issue, the initial nmcli command failed {nmcli_result.message}.") logger.error(f"[SYSTEMWIDE WG] Critical issue, the initial nmcli command failed {nmcli_result.message}.")
return nmcli_result 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 ============= # ============= IPv6 SINKHOLE =============
sinkhole_result = setup_ipv6_sinkhole(process_output) sinkhole_result = setup_ipv6_sinkhole(process_output)
if not sinkhole_result: if not sinkhole_result:
raise ConnectionError('IPv6 Could not be protected.') raise ConnectionError('IPv6 Could not be protected.')
# ============= FIREWALL ============= # ============= FIREWALL =============
firewall_tracker = False
if firewall_setting: if firewall_setting:
logger.info(f"[SYSTEMWIDE WG] Turning on Firewall") logger.info("Firewall setting is enabled, let's turn it on..")
turn_on_result = turn_on_firewall(
wg_interface_name = wg_interface_result.data profile_id=str(profile.id),
server_ip_address = extract_endpoint_ip(str(profile.id)) process_output=process_output
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(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 ============= # ============= SETUP STATE =============
SystemStateController.create(profile.id) # 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: try:
await_connection(connection_observer=connection_observer) await_connection(connection_observer=connection_observer)
@ -139,18 +227,23 @@ def evaluate_connection_result(connection_result: Result):
if not connection_result.valid: if not connection_result.valid:
logger.error(f"[SYSTEMWIDE WG] Critical issue, could not establish a connection: {connection_result.message}") logger.error(f"[SYSTEMWIDE WG] Critical issue, could not establish a connection: {connection_result.message}")
try: try:
terminate_system_connection() terminate_system_connection(firewall_setting)
except ConnectionTerminationError: except ConnectionTerminationError:
pass 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: 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 ================= # ================= VERIFY ENDPOINT =================
@ -158,14 +251,19 @@ def establish_system_connection(profile: SystemProfile, ignore: tuple[type[Excep
verify_wireguard_endpoint(profile, ignore=ignore) verify_wireguard_endpoint(profile, ignore=ignore)
# ================= CONNECT WG ================= # ================= CONNECT WG =================
firewall_setting = get_firewall_setting()
try: 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) evaluate_connection_result(connection_result)
except ConnectionError: except ConnectionError:
# ================= RETRY ON FAILURE =================
try: try:
terminate_system_connection() terminate_system_connection(firewall_setting)
except ConnectionTerminationError: except ConnectionTerminationError:
pass pass
@ -174,19 +272,23 @@ def establish_system_connection(profile: SystemProfile, ignore: tuple[type[Excep
except CalledProcessError: except CalledProcessError:
try: try:
terminate_system_connection() terminate_system_connection(firewall_setting)
except ConnectionTerminationError: except ConnectionTerminationError:
pass pass
# trying again.. # trying again..
try: 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) evaluate_connection_result(connection_result)
except (ConnectionError, CalledProcessError): except (ConnectionError, CalledProcessError):
try: try:
terminate_system_connection() terminate_system_connection(firewall_setting)
except ConnectionTerminationError: except ConnectionTerminationError:
pass pass