Introduced Managed DNS settings to Wireguard, which is redundant for nmcli. However, this same framework can be used for other protocols or implementations.
This commit is contained in:
parent
208cb29f77
commit
ff493455b3
13 changed files with 508 additions and 41 deletions
|
|
@ -98,3 +98,9 @@ class ConfigurationController:
|
|||
configuration.firewall = new_value
|
||||
configuration.save()
|
||||
|
||||
@staticmethod
|
||||
def change_dns(new_value):
|
||||
configuration = ConfigurationController.get_or_new()
|
||||
configuration.dns = new_value
|
||||
configuration.save()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +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.services.networking.systemwide.systemwide_utils import get_firewall_setting, get_dns_setting
|
||||
|
||||
from core.Errors import InvalidSubscriptionError, MissingSubscriptionError, ConnectionTerminationError, ProfileActivationError, ProfileDeactivationError, MissingLocationError, ConnectionUnprotectedError, EndpointVerificationError, ProfileStateConflictError
|
||||
from core.controllers.ApplicationController import ApplicationController
|
||||
|
|
@ -133,8 +133,7 @@ class ProfileController:
|
|||
raise ProfileDeactivationError('The profile could not be disabled.')
|
||||
|
||||
try:
|
||||
firewall_setting = get_firewall_setting()
|
||||
terminate_system_connection(firewall_setting)
|
||||
terminate_system_connection()
|
||||
except ConnectionTerminationError:
|
||||
raise ProfileDeactivationError('The profile could not be disabled.')
|
||||
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@ class SystemStateController:
|
|||
return SystemState.exists()
|
||||
|
||||
@staticmethod
|
||||
def create(profile_id: int, firewalled: bool) -> SystemState:
|
||||
return SystemState(profile_id, firewalled).save()
|
||||
def create(profile_id: int, firewalled: bool, dns_set: bool) -> SystemState:
|
||||
return SystemState(profile_id, firewalled, dns_set).save()
|
||||
|
||||
@staticmethod
|
||||
def update_or_create(system_state):
|
||||
|
|
|
|||
|
|
@ -57,6 +57,14 @@ class Configuration:
|
|||
)
|
||||
)
|
||||
|
||||
dns: 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'
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ class ResultError(Enum):
|
|||
SUBSCRIPTION = "subscription"
|
||||
NMCLI = "nmcli_issues"
|
||||
FIREWALL = "firewall"
|
||||
CLIENT_DNS = "client_dns"
|
||||
EXTERNAL_DNS = "external_dns"
|
||||
UNKNOWN = "unknown"
|
||||
|
||||
@dataclass
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import pathlib
|
|||
class SystemState:
|
||||
profile_id: int
|
||||
firewalled: bool
|
||||
dns_set: bool
|
||||
|
||||
def save(self: Self):
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
from core.Constants import Constants
|
||||
import os
|
||||
|
||||
def extract_endpoint_ip(profile_id: str):
|
||||
def extract_endpoint_ip(profile_id: str) -> str | bool:
|
||||
try:
|
||||
profile_path = Constants.HV_PROFILE_CONFIG_HOME + f'/{profile_id}'
|
||||
wg_conf_path = f'{profile_path}/wg.conf.bak'
|
||||
|
|
@ -17,3 +17,19 @@ def extract_endpoint_ip(profile_id: str):
|
|||
return None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def extract_dns(profile_id: str) -> str | bool:
|
||||
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('DNS = '):
|
||||
return line.strip().split(' = ')[1]
|
||||
return None
|
||||
except Exception:
|
||||
return None
|
||||
|
|
|
|||
90
core/services/networking/systemwide/dns.py
Normal file
90
core/services/networking/systemwide/dns.py
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
# custom
|
||||
from core.Constants import Constants
|
||||
from core.models.Result import Result, ResultError
|
||||
from core.errors.logger import logger
|
||||
|
||||
# generic
|
||||
import subprocess
|
||||
from subprocess import CalledProcessError
|
||||
|
||||
# temp location:
|
||||
DNS_SCRIPT = f"/opt/hydra-veil/dns_script"
|
||||
|
||||
|
||||
# requires script to have sudo
|
||||
def set_on(network_interface: str, dns_should_be: str) -> Result:
|
||||
result = subprocess.run(
|
||||
["sudo", DNS_SCRIPT, "set", network_interface, dns_should_be],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
logger.info("DNS setup successful")
|
||||
return Result(valid=True)
|
||||
else:
|
||||
logger.error(f"DNS setup failed: {result.stderr.strip()}")
|
||||
return Result(valid=False, message=result.stderr.strip())
|
||||
|
||||
# requires script to have sudo
|
||||
def revert() -> Result:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["sudo", DNS_SCRIPT, "revert"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
logger.info("DNS revert successful")
|
||||
return Result(valid=True)
|
||||
else:
|
||||
logger.error(f"DNS revert failed: {result.stderr.strip()}")
|
||||
return Result(valid=False, message=result.stderr.strip())
|
||||
except CalledProcessError as e:
|
||||
return Result(valid=False, message=e)
|
||||
|
||||
|
||||
# does NOT need sudo
|
||||
def is_systemd_enabled() -> Result:
|
||||
try:
|
||||
process = subprocess.Popen(('systemctl', 'is-enabled', 'systemd-resolved'), stdout=subprocess.PIPE)
|
||||
process_output = str(process.stdout.read())
|
||||
if 'enabled' in process_output:
|
||||
return Result(valid=True)
|
||||
else:
|
||||
return Result(valid=False, message=process_output)
|
||||
except CalledProcessError as e:
|
||||
return Result(valid=False, message=e)
|
||||
|
||||
|
||||
# does NOT need sudo
|
||||
def get_resolvctl_data(which_command: str) -> Result:
|
||||
try:
|
||||
process_output = subprocess.check_output(('resolvectl', which_command), text=True)
|
||||
return Result(valid=True, data=process_output)
|
||||
|
||||
except CalledProcessError as e:
|
||||
return Result(valid=False, message=e)
|
||||
|
||||
|
||||
# These are the raw Linux systemctl commands:
|
||||
"""
|
||||
# is it enabled?
|
||||
systemctl is-enabled systemd-resolved
|
||||
|
||||
# socket exists
|
||||
-S /run/systemd/resolve/io.systemd.Resolve
|
||||
|
||||
# Shows DNS servers per interface. Should spit back what was put in it for that interface.
|
||||
resolvectl dns
|
||||
|
||||
# Shows search domains per interface. should show ~. for that interface
|
||||
resolvectl domain
|
||||
|
||||
# Shows which is the default. yes/no for each
|
||||
resolvectl default-route
|
||||
|
||||
# Test DNS:
|
||||
resolvectl query example.com
|
||||
"""
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
|
||||
|
||||
from enum import Enum
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional, Any
|
||||
|
||||
"""
|
||||
Purpose:
|
||||
Searching for values in strings from the system's CLI,
|
||||
In it's current use context, systemctl
|
||||
"""
|
||||
|
||||
class SearchError(Enum):
|
||||
"""Classified error categories."""
|
||||
SUCCESS = "success"
|
||||
NO_INPUT = "no_input"
|
||||
INVALID_INPUT = "invalid_input"
|
||||
TARGET_MISSING = "target_missing"
|
||||
TARGET_NO_VALUE = "target_no_value"
|
||||
VALUE_DOESNT_MATCH = "value_doesnt_match"
|
||||
COMPETITORS_MATCH = "competitors_match"
|
||||
COMMAND_DIDNT_FINISH = "command_didnt_finish"
|
||||
UNKNOWN = "unknown"
|
||||
|
||||
@dataclass
|
||||
class SearchResult():
|
||||
valid: bool
|
||||
error_type: SearchError = SearchError.SUCCESS
|
||||
which_command_was_run: Optional[str] = None
|
||||
target_value: str = None
|
||||
competitors_count: Optional[int] = 0
|
||||
competitors_list: Optional[Any] = None
|
||||
message: Optional[str] = None
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
from core.services.networking.systemwide.dns_tools.SearchResult import SearchResult, SearchError
|
||||
from core.services.networking.systemwide.dns_tools.search_resolv_output import evaluating_resolv_output
|
||||
from core.services.networking.systemwide import dns
|
||||
from core.models.Result import Result, ResultError
|
||||
from core.errors.logger import logger
|
||||
|
||||
# This is for the future to potentially give errors to the end user:
|
||||
def turn_command_into_readable_string(which_command) -> str:
|
||||
if which_command == "default-route":
|
||||
human_readable_string = "The Systemctl's DNS Default Route"
|
||||
elif which_command == "domain":
|
||||
human_readable_string = "The Systemctl's DNS Domain, for which domains are routed to it"
|
||||
elif which_command == "dns":
|
||||
human_readable_string = "The Systemctl's Default DNS IP address, which should match the VPN configuration file"
|
||||
else:
|
||||
human_readable_string = "Unknown error"
|
||||
return human_readable_string
|
||||
|
||||
|
||||
def process_ONE_dns_result(which_command: str, target_interface: str) -> SearchResult:
|
||||
# get the output
|
||||
resolvctl_output = dns.get_resolvctl_data(which_command)
|
||||
|
||||
if not resolvctl_output.valid:
|
||||
return SearchResult(valid=False, error_type=SearchError.COMMAND_DIDNT_FINISH, message=resolvctl_output.message)
|
||||
|
||||
# search that output:
|
||||
return evaluating_resolv_output(which_command=which_command, target_interface=target_interface, resolv_output=resolvctl_output.data)
|
||||
|
||||
|
||||
def process_ALL_dns_results(target_interface: str) -> Result:
|
||||
|
||||
list_of_commands = ["domain", "default-route", "dns"]
|
||||
list_of_results_as_objects = []
|
||||
list_of_results_as_binary = []
|
||||
|
||||
# ================= START LOOP =====================
|
||||
for each_command in list_of_commands:
|
||||
each_SearchResult = process_ONE_dns_result(
|
||||
which_command=each_command,
|
||||
target_interface=target_interface,
|
||||
)
|
||||
# add the command to the SearchResult here, so it's only declared once,
|
||||
each_SearchResult.which_command_was_run = each_command
|
||||
|
||||
list_of_results_as_objects.append(each_SearchResult)
|
||||
list_of_results_as_binary.append(each_SearchResult.valid)
|
||||
|
||||
# ================= END LOOP =====================
|
||||
return list_of_results_as_objects, list_of_results_as_binary
|
||||
|
||||
|
||||
def get_quantity_of_false(list_of_results_as_binary: list) -> int:
|
||||
quantity_of_false = 0
|
||||
for each_result in list_of_results_as_binary:
|
||||
if each_result is False:
|
||||
quantity_of_false =+ 1
|
||||
return quantity_of_false
|
||||
|
||||
|
||||
def orchestrate_dns_check(target_interface: str) -> Result:
|
||||
"""
|
||||
Purpose:
|
||||
Coordinates DNS checks for resolvctl output, with actionable results
|
||||
|
||||
Rank:
|
||||
Module Orchestrator
|
||||
|
||||
Called by:
|
||||
systemwide_wireguard.__establish_system_connection
|
||||
"""
|
||||
|
||||
list_of_results_as_objects, list_of_results_as_binary = process_ALL_dns_results(
|
||||
target_interface=target_interface,
|
||||
)
|
||||
|
||||
quantity_of_checks = len(list_of_results_as_binary)
|
||||
quantity_of_false = get_quantity_of_false(list_of_results_as_binary)
|
||||
|
||||
if quantity_of_false == 0:
|
||||
logger.info(f"[DNS Orchestrator] Congrats, all {quantity_of_checks} worked.")
|
||||
return Result(valid=True)
|
||||
|
||||
else:
|
||||
logger.info(f"[DNS Orchestrator] Some of the DNS checks did NOT work. But this might be because the tunnel is down.")
|
||||
|
||||
targets_not_found = 0
|
||||
|
||||
for each_object in list_of_results_as_objects:
|
||||
if each_object.error_type == SearchError.TARGET_MISSING:
|
||||
target_not_found = target_not_found + 1
|
||||
|
||||
return Result(valid=False, data=targets_not_found)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# NOT YET INTEGRATED. This is for potentially showing errors to the end-user:
|
||||
|
||||
# if search_results.valid:
|
||||
# return Result(valid=True)
|
||||
|
||||
# resolvctl_name = turn_command_into_readable_string(which_command)
|
||||
# reason = search_results.error_type
|
||||
# not_leak_msg = "This is NOT a leak, because the firewall is still blocking all queries outside the VPN tunnel. But for queries to realize they are being blocked, it may cause (very small) speed delays on domain lookups. Do you want to reconnect now or just continue as is?"
|
||||
|
||||
# if reason == SearchError.COMPETITORS_MATCH:
|
||||
# error_log_msg = f"{resolvctl_name} had interfaces using DNS other than the VPN."
|
||||
# logger.error(error_log_msg)
|
||||
# final_human_message = f"{error_log_msg} {not_leak_msg}"
|
||||
# return Result(valid=False, error_type=ResultError.CLIENT_DNS, message=final_human_message)
|
||||
|
||||
# elif reason == SearchError.TARGET_NO_VALUE:
|
||||
# error_log_msg = f"{resolvctl_name} wasn't able to be set to the VPN."
|
||||
# logger.error(error_log_msg)
|
||||
# final_human_message = f"{error_log_msg} {not_leak_msg}"
|
||||
# return Result(valid=False, error_type=ResultError.CLIENT_DNS, message=final_human_message)
|
||||
|
||||
# else:
|
||||
# error_log_msg = "There was an Unknown error in setting your default DNS to the VPN."
|
||||
# logger.error(error_log_msg)
|
||||
# final_human_message = f"{error_log_msg} {not_leak_msg}"
|
||||
# return Result(valid=False, error_type=ResultError.UNKNOWN, message=final_human_message)
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
from core.services.networking.systemwide.dns_tools.SearchResult import SearchResult, SearchError
|
||||
from core.errors.logger import logger
|
||||
import re
|
||||
|
||||
def get_pattern_and_phrase(
|
||||
which_command: str,
|
||||
ip_address: str = "10.13.0.1"
|
||||
)-> str:
|
||||
"""figure out which re pattern & phrase to use, based on the search command"""
|
||||
|
||||
if which_command == "domain":
|
||||
target_phrase = "~."
|
||||
pattern = r'\(([^)]+)\):\s*(\S*)?'
|
||||
|
||||
elif which_command == "default-route":
|
||||
target_phrase = "yes"
|
||||
pattern = r'\(([^)]+)\):\s*(yes|no)'
|
||||
|
||||
elif which_command == "dns":
|
||||
target_phrase = ip_address
|
||||
pattern = r'\(([^)]+)\):\s*([\d.]+)'
|
||||
|
||||
else:
|
||||
target_phrase = None
|
||||
pattern = None
|
||||
return pattern, target_phrase
|
||||
|
||||
def evaluating_resolv_output(which_command: str, target_interface: str, resolv_output: str) -> SearchResult:
|
||||
if target_interface not in resolv_output:
|
||||
return SearchResult(valid=False, error_type=SearchError.TARGET_MISSING)
|
||||
|
||||
# ============= PREPARE SEARCH =============
|
||||
pattern, target_phrase = get_pattern_and_phrase(which_command)
|
||||
if not pattern or not target_phrase:
|
||||
return SearchResult(valid=False, error_type=SearchError.UNKNOWN, message=f"invalid which_command of {which_command}")
|
||||
|
||||
# Find all entries with pattern (name): value
|
||||
matches = re.findall(pattern, resolv_output)
|
||||
|
||||
# Separate target from competitors
|
||||
target_has_match = False
|
||||
competitors_with_match = []
|
||||
target_actually_had = None
|
||||
|
||||
# ============= LOOP =============
|
||||
for name, value in matches:
|
||||
if value == target_phrase:
|
||||
if name == target_interface:
|
||||
target_has_match = True
|
||||
else:
|
||||
competitors_with_match.append(name)
|
||||
|
||||
else:
|
||||
# Check if Target has the wrong value
|
||||
if name == target_interface:
|
||||
target_actually_had = value
|
||||
|
||||
# ============= EVALUATE =============
|
||||
competitors_match = len(competitors_with_match) > 0
|
||||
# logger.info(f"Target has {target_phrase}: {target_has_match}")
|
||||
# logger.info(f"Competitors have {target_phrase}: {competitors_match}")
|
||||
# logger.info(f"Competitor list: {competitors_with_match}")
|
||||
|
||||
# target has it, and nobody else:
|
||||
if target_has_match and not competitors_match:
|
||||
return SearchResult(valid=True, target_value=target_phrase)
|
||||
|
||||
if target_has_match and competitors_match:
|
||||
return SearchResult(valid=False, error_type=SearchError.COMPETITORS_MATCH, target_value=target_phrase)
|
||||
|
||||
if not target_has_match:
|
||||
# target has wrong value:
|
||||
if target_actually_had:
|
||||
return SearchResult(valid=False, error_type=SearchError.VALUE_DOESNT_MATCH, competitors_list=competitors_with_match, target_value=target_actually_had)
|
||||
# target has no value
|
||||
else:
|
||||
return SearchResult(valid=False, error_type=SearchError.TARGET_NO_VALUE, competitors_list=competitors_with_match, target_value=target_actually_had)
|
||||
|
||||
|
|
@ -7,6 +7,14 @@ from core.models.Configuration import get_setting
|
|||
from core.errors.logger import logger
|
||||
|
||||
|
||||
def get_dns_setting() -> bool:
|
||||
dns_setting = get_setting("dns")
|
||||
logger.info(f"DNS Setting is {dns_setting}")
|
||||
if dns_setting is None:
|
||||
return False
|
||||
return dns_setting
|
||||
|
||||
|
||||
def get_firewall_setting() -> bool:
|
||||
firewall_setting = get_setting("firewall")
|
||||
logger.info(f"Firewall Setting is {firewall_setting}")
|
||||
|
|
|
|||
|
|
@ -1,9 +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, 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.services.networking.systemwide.systemwide_utils import extract_wg_interface_name, check_system_prereqs, get_firewall_setting, get_dns_setting
|
||||
from core.services.networking.general_connection_tools.config_tools import extract_endpoint_ip, extract_dns
|
||||
from core.services.networking.systemwide import killswitch, dns
|
||||
from core.services.networking.systemwide.dns_tools.process_dns_result import orchestrate_dns_check
|
||||
from core.services.networking.systemwide.dns_tools.SearchResult import SearchResult, SearchError
|
||||
from core.errors.logger import logger
|
||||
|
||||
from core.Constants import Constants
|
||||
|
|
@ -24,7 +26,39 @@ import subprocess
|
|||
import time
|
||||
|
||||
|
||||
def turn_on_firewall(profile_id: str, process_output: str) -> Result:
|
||||
def set_dns(profile_id: str, network_interface: str) -> Result:
|
||||
do_they_even_use_systemd = dns.is_systemd_enabled()
|
||||
|
||||
if not do_they_even_use_systemd.valid:
|
||||
error_msg = "You don't use SystemD, so we are skipping setting your DNS."
|
||||
logger.info(error_msg)
|
||||
return Result(valid=True, message=error_msg) # true to not prompt errors
|
||||
|
||||
dns_should_be = extract_dns(profile_id)
|
||||
|
||||
logger.info(f"[CONNECT] About to set the DNS for {network_interface}..")
|
||||
return dns.set_on(network_interface=network_interface, dns_should_be=dns_should_be)
|
||||
|
||||
|
||||
|
||||
def revert_dns() -> Result:
|
||||
do_they_even_use_systemd = dns.is_systemd_enabled()
|
||||
|
||||
if not do_they_even_use_systemd.valid:
|
||||
error_msg = "You don't use SystemD, so we are skipping setting your DNS."
|
||||
logger.info(error_msg)
|
||||
return Result(valid=True, message=error_msg) # true to not prompt errors
|
||||
|
||||
logger.info("Reverting DNS Settings..")
|
||||
turn_off = dns.revert()
|
||||
logger.info(f"The result of the turn off is {turn_off.valid}")
|
||||
if not turn_off.valid:
|
||||
logger.error(f"Critical DNS Error! Could NOT turn off DNS. {turn_off.message}")
|
||||
return turn_off.valid
|
||||
|
||||
|
||||
|
||||
def turn_on_firewall(profile_id: str, wg_interface_name: str) -> Result:
|
||||
"""
|
||||
Purpose:
|
||||
This is a wireguard specific function, using the WG interface,
|
||||
|
|
@ -37,17 +71,9 @@ def turn_on_firewall(profile_id: str, process_output: str) -> Result:
|
|||
"""
|
||||
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}")
|
||||
if wg_interface_name != 'wg':
|
||||
logger.info(f"Note: The WG interface is NOT WG. It's {wg_interface_name}. Be careful about what we kill switch compared to what goes up. Although we kill the nftable rule regardless on down.")
|
||||
|
||||
return killswitch.arm(
|
||||
server_ip=server_ip_address,
|
||||
|
|
@ -55,39 +81,73 @@ def turn_on_firewall(profile_id: str, process_output: str) -> Result:
|
|||
internal_subnet=None
|
||||
)
|
||||
|
||||
|
||||
# This raises errors on failure.
|
||||
def check_and_kill_firewall():
|
||||
firewall_on = killswitch.status()
|
||||
logger.info(f"Inside check_and_kill_firewall the firewall status is {firewall_on}")
|
||||
|
||||
logger.info(f"Terminate connection ran check_and_kill_firewall which evaluated the status as {firewall_on}")
|
||||
firewall_on = killswitch.status() # produces a boolean
|
||||
logger.info(f"The Firewall is currently: {firewall_on}")
|
||||
|
||||
# is it even armed? if not, get lost,
|
||||
if firewall_on:
|
||||
# if it's armed, kill it,
|
||||
logger.info(f"Killing the firewall")
|
||||
disable_result_object = killswitch.disarm()
|
||||
logger.info(f"Killing the firewall..")
|
||||
disable_result_object = killswitch.disarm() # produces a Result Object
|
||||
|
||||
# did that work?
|
||||
if disable_result_object.valid:
|
||||
logger.info(f"We disabled the firewall correctly.")
|
||||
logger.info(f"Success! We disabled the firewall correctly.")
|
||||
return
|
||||
else:
|
||||
logger.error(f"[FIREWALL] CRITICAL ISSUE with the Firewall being disabled: {disable_result_object.message}")
|
||||
error_msg = f"Firewall couldn't be disabled: {disable_result_object.message}"
|
||||
error_msg = f"Firewall can't be disabled: {disable_result_object.message}"
|
||||
logger.error(error_msg)
|
||||
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:
|
||||
|
||||
# This is dead code right now
|
||||
def confirm_dns_is_reverted(wg_interface_name: str = 'wg'):
|
||||
do_they_even_use_systemd = dns.is_systemd_enabled()
|
||||
|
||||
if not do_they_even_use_systemd:
|
||||
return True
|
||||
|
||||
check_on_dns = orchestrate_dns_check(wg_interface_name)
|
||||
|
||||
if check_on_dns.valid:
|
||||
logger.error("CRITICAL DNS PROBLEM! We turned WG off, but the DNS check shows it's still on. Trying again..")
|
||||
return False
|
||||
else:
|
||||
logger.info("We turned WG off, and the DNS check shows it reverted also. This is great.")
|
||||
return True
|
||||
|
||||
|
||||
def terminate_system_connection(
|
||||
firewall_setting: Optional[bool] = get_firewall_setting(),
|
||||
dns_setting: Optional[bool] = get_dns_setting()
|
||||
):
|
||||
|
||||
if shutil.which('nmcli') is None:
|
||||
raise CommandNotFoundError('nmcli')
|
||||
|
||||
# print("inside terminate_system_connection about to do firewall and dns")
|
||||
|
||||
# if firewall_setting is None:
|
||||
# firewall_setting = get_firewall_setting()
|
||||
|
||||
# if dns_setting is None:
|
||||
# dns_setting = get_dns_setting()
|
||||
|
||||
print(f"Got firewall and dns: {firewall_setting} and {dns_setting}")
|
||||
|
||||
# ====== FIREWALL =======
|
||||
if firewall_setting:
|
||||
check_and_kill_firewall() # on failure, this raises errors, which then bubble up
|
||||
|
||||
# ====== DNS =======
|
||||
if dns_setting:
|
||||
reverting_dns_worked = revert_dns()
|
||||
|
||||
# ====== NMCLI =======
|
||||
if SystemStateController.exists():
|
||||
process = subprocess.Popen(('nmcli', 'connection', 'delete', 'wg'), stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
|
||||
|
|
@ -97,11 +157,18 @@ def terminate_system_connection(firewall_setting) -> Result:
|
|||
|
||||
subprocess.run(('nmcli', 'connection', 'delete', 'hv-ipv6-sink'), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
terminate_tor_connection()
|
||||
|
||||
# confirm check on dns:
|
||||
# did_dns_revert = confirm_dns_is_reverted('wg')
|
||||
# print(f"did_dns_revert is {did_dns_revert}")
|
||||
|
||||
# finally end that profile's JSON state:
|
||||
SystemState.dissolve()
|
||||
|
||||
else:
|
||||
raise ConnectionTerminationError('The connection could not be terminated.')
|
||||
|
||||
|
||||
def setup_nmcli_connection(wg_config_path: str) -> Result:
|
||||
try:
|
||||
process_output = subprocess.check_output(('nmcli', 'connection', 'import', '--temporary', 'type', 'wireguard', 'file', wg_config_path), text=True)
|
||||
|
|
@ -111,6 +178,7 @@ def setup_nmcli_connection(wg_config_path: str) -> Result:
|
|||
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:
|
||||
|
|
@ -139,6 +207,7 @@ def setup_ipv6_sinkhole(process_output: str) -> Result:
|
|||
def __establish_system_connection(
|
||||
profile: SystemProfile,
|
||||
firewall_setting: bool = False,
|
||||
dns_setting: bool = False,
|
||||
connection_observer: Optional[ConnectionObserver] = None):
|
||||
"""
|
||||
Purpose:
|
||||
|
|
@ -150,14 +219,15 @@ def __establish_system_connection(
|
|||
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.
|
||||
6) Set DNS (if enabled), and test it.
|
||||
7) Setup State (JSON file)
|
||||
8) Confirm Firewall is on (if enabled). Raises errors if not.
|
||||
9) Testing, confirming, displaying to the end user.
|
||||
"""
|
||||
|
||||
check_system_prereqs()
|
||||
logger.info("[CONNECT] Terming existing connections..")
|
||||
terminate_system_connection(firewall_setting)
|
||||
terminate_system_connection(firewall_setting, dns_setting)
|
||||
|
||||
# ============= INITIAL NMCLI CONNECTION =============
|
||||
wg_config_path = profile.get_wireguard_configuration_path()
|
||||
|
|
@ -175,18 +245,44 @@ def __establish_system_connection(
|
|||
if not sinkhole_result:
|
||||
raise ConnectionError('IPv6 Could not be protected.')
|
||||
|
||||
# ============= PREP SETTINGS =============
|
||||
if firewall_setting or dns_setting:
|
||||
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
|
||||
|
||||
# ============= FIREWALL =============
|
||||
firewall_tracker = False
|
||||
if firewall_setting:
|
||||
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
|
||||
wg_interface_name=wg_interface_name
|
||||
)
|
||||
logger.info(f"Results of firewall turn on are: {turn_on_result.valid}")
|
||||
if turn_on_result.valid:
|
||||
firewall_tracker = True
|
||||
|
||||
# ============= DNS =============
|
||||
if dns_setting:
|
||||
set_dns_result = set_dns(str(profile.id), wg_interface_name)
|
||||
logger.info(f"[CONNECT] The result of our setting the DNS is {set_dns_result.valid}")
|
||||
if not set_dns_result.valid:
|
||||
logger.error(f"[CONNECT] Setting the DNS DIDN'T work. Here's the reason given: {set_dns_result.message}")
|
||||
|
||||
# now check:
|
||||
test_dns_result = orchestrate_dns_check(wg_interface_name)
|
||||
if test_dns_result.valid:
|
||||
did_dns_work = True
|
||||
logger.info("[CONNECT] All of the DNS Checks Worked")
|
||||
else:
|
||||
did_dns_work = False
|
||||
logger.info("Skipping DNS setup, because it's off in settings")
|
||||
|
||||
# ============= SETUP STATE =============
|
||||
# Even if firewall is off, we want to save the fact we turned on the VPN tunnel, before we raise errors.
|
||||
|
|
@ -194,7 +290,8 @@ def __establish_system_connection(
|
|||
logger.info(f"Setting State JSON..")
|
||||
SystemStateController.create(
|
||||
profile_id=profile.id,
|
||||
firewalled=firewall_tracker
|
||||
firewalled=firewall_tracker,
|
||||
dns_set=did_dns_work
|
||||
)
|
||||
|
||||
# ============= CONFIRM ON FIREWALL =============
|
||||
|
|
@ -227,7 +324,7 @@ 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(firewall_setting)
|
||||
terminate_system_connection(firewall_setting, True)
|
||||
except ConnectionTerminationError:
|
||||
pass
|
||||
|
||||
|
|
@ -250,12 +347,16 @@ def establish_system_connection(
|
|||
if ConfigurationController.get_endpoint_verification_enabled():
|
||||
verify_wireguard_endpoint(profile, ignore=ignore)
|
||||
|
||||
# ================= CONNECT WG =================
|
||||
# ================= SETTINGS =================
|
||||
firewall_setting = get_firewall_setting()
|
||||
dns_setting = get_dns_setting()
|
||||
|
||||
# ================= CONNECT WG ================
|
||||
try:
|
||||
connection_result = __establish_system_connection(
|
||||
profile=profile,
|
||||
firewall_setting=firewall_setting,
|
||||
dns_setting=dns_setting,
|
||||
connection_observer=connection_observer
|
||||
)
|
||||
evaluate_connection_result(connection_result)
|
||||
|
|
@ -263,7 +364,7 @@ def establish_system_connection(
|
|||
except ConnectionError:
|
||||
# ================= RETRY ON FAILURE =================
|
||||
try:
|
||||
terminate_system_connection(firewall_setting)
|
||||
terminate_system_connection(firewall_setting, dns_setting)
|
||||
except ConnectionTerminationError:
|
||||
pass
|
||||
|
||||
|
|
@ -272,7 +373,7 @@ def establish_system_connection(
|
|||
except CalledProcessError:
|
||||
|
||||
try:
|
||||
terminate_system_connection(firewall_setting)
|
||||
terminate_system_connection(firewall_setting, dns_setting)
|
||||
except ConnectionTerminationError:
|
||||
pass
|
||||
|
||||
|
|
@ -281,6 +382,7 @@ def establish_system_connection(
|
|||
connection_result = __establish_system_connection(
|
||||
profile=profile,
|
||||
firewall_setting=firewall_setting,
|
||||
dns_setting=dns_setting,
|
||||
connection_observer=connection_observer
|
||||
)
|
||||
evaluate_connection_result(connection_result)
|
||||
|
|
@ -288,7 +390,7 @@ def establish_system_connection(
|
|||
except (ConnectionError, CalledProcessError):
|
||||
|
||||
try:
|
||||
terminate_system_connection(firewall_setting)
|
||||
terminate_system_connection(firewall_setting, dns_setting)
|
||||
except ConnectionTerminationError:
|
||||
pass
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue