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
def update_or_create(configuration):
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.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.')

View file

@ -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):

View file

@ -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

View file

@ -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"
@ -50,8 +50,3 @@ 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

View file

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

View file

@ -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:

View file

@ -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,
)

View file

@ -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:

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.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")
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
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
# ============= 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
)
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)
# ============= 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