Integration of Firewall feature to Systemwide Wireguard
This commit is contained in:
parent
9ba6d7e783
commit
c4ffe9be3c
10 changed files with 216 additions and 57 deletions
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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.')
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import pathlib
|
|||
@dataclass
|
||||
class SystemState:
|
||||
profile_id: int
|
||||
firewalled: bool
|
||||
|
||||
def save(self: Self):
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue