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:
SimplifiedPrivacy 2026-07-22 14:04:50 -04:00
parent 208cb29f77
commit ff493455b3
13 changed files with 508 additions and 41 deletions

View file

@ -98,3 +98,9 @@ class ConfigurationController:
configuration.firewall = new_value configuration.firewall = new_value
configuration.save() configuration.save()
@staticmethod
def change_dns(new_value):
configuration = ConfigurationController.get_or_new()
configuration.dns = new_value
configuration.save()

View file

@ -1,7 +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.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.Errors import InvalidSubscriptionError, MissingSubscriptionError, ConnectionTerminationError, ProfileActivationError, ProfileDeactivationError, MissingLocationError, ConnectionUnprotectedError, EndpointVerificationError, ProfileStateConflictError
from core.controllers.ApplicationController import ApplicationController from core.controllers.ApplicationController import ApplicationController
@ -133,8 +133,7 @@ class ProfileController:
raise ProfileDeactivationError('The profile could not be disabled.') raise ProfileDeactivationError('The profile could not be disabled.')
try: try:
firewall_setting = get_firewall_setting() terminate_system_connection()
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: int, firewalled: bool) -> SystemState: def create(profile_id: int, firewalled: bool, dns_set: bool) -> SystemState:
return SystemState(profile_id, firewalled).save() return SystemState(profile_id, firewalled, dns_set).save()
@staticmethod @staticmethod
def update_or_create(system_state): def update_or_create(system_state):

View file

@ -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): def save(self: Self):
config_file_contents = f'{self.to_json(indent=4)}\n' config_file_contents = f'{self.to_json(indent=4)}\n'

View file

@ -15,6 +15,8 @@ class ResultError(Enum):
SUBSCRIPTION = "subscription" SUBSCRIPTION = "subscription"
NMCLI = "nmcli_issues" NMCLI = "nmcli_issues"
FIREWALL = "firewall" FIREWALL = "firewall"
CLIENT_DNS = "client_dns"
EXTERNAL_DNS = "external_dns"
UNKNOWN = "unknown" UNKNOWN = "unknown"
@dataclass @dataclass

View file

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

View file

@ -1,7 +1,7 @@
from core.Constants import Constants from core.Constants import Constants
import os import os
def extract_endpoint_ip(profile_id: str): def extract_endpoint_ip(profile_id: str) -> str | bool:
try: try:
profile_path = Constants.HV_PROFILE_CONFIG_HOME + f'/{profile_id}' profile_path = Constants.HV_PROFILE_CONFIG_HOME + f'/{profile_id}'
wg_conf_path = f'{profile_path}/wg.conf.bak' wg_conf_path = f'{profile_path}/wg.conf.bak'
@ -17,3 +17,19 @@ def extract_endpoint_ip(profile_id: str):
return None return None
except Exception: except Exception:
return None 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

View 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
"""

View file

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

View file

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

View file

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

View file

@ -7,6 +7,14 @@ from core.models.Configuration import get_setting
from core.errors.logger import logger 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: def get_firewall_setting() -> bool:
firewall_setting = get_setting("firewall") firewall_setting = get_setting("firewall")
logger.info(f"Firewall Setting is {firewall_setting}") logger.info(f"Firewall Setting is {firewall_setting}")

View file

@ -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.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, get_firewall_setting 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 from core.services.networking.general_connection_tools.config_tools import extract_endpoint_ip, extract_dns
from core.services.networking.systemwide import killswitch 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.errors.logger import logger
from core.Constants import Constants from core.Constants import Constants
@ -24,7 +26,39 @@ import subprocess
import time 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: Purpose:
This is a wireguard specific function, using the WG interface, 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..") 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) 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( return killswitch.arm(
server_ip=server_ip_address, server_ip=server_ip_address,
@ -55,39 +81,73 @@ def turn_on_firewall(profile_id: str, process_output: str) -> Result:
internal_subnet=None internal_subnet=None
) )
# This raises errors on failure. # This raises errors on failure.
def check_and_kill_firewall(): def check_and_kill_firewall():
firewall_on = killswitch.status() firewall_on = killswitch.status() # produces a boolean
logger.info(f"Inside check_and_kill_firewall the firewall status is {firewall_on}") logger.info(f"The Firewall is currently: {firewall_on}")
logger.info(f"Terminate connection ran check_and_kill_firewall which evaluated the status as {firewall_on}")
# is it even armed? if not, get lost, # is it even armed? if not, get lost,
if firewall_on: if firewall_on:
# if it's armed, kill it, # if it's armed, kill it,
logger.info(f"Killing the firewall") logger.info(f"Killing the firewall..")
disable_result_object = killswitch.disarm() disable_result_object = killswitch.disarm() # produces a Result Object
# did that work? # did that work?
if disable_result_object.valid: if disable_result_object.valid:
logger.info(f"We disabled the firewall correctly.") logger.info(f"Success! We disabled the firewall correctly.")
return return
else: else:
logger.error(f"[FIREWALL] CRITICAL ISSUE with the Firewall being disabled: {disable_result_object.message}") error_msg = f"Firewall can't be disabled: {disable_result_object.message}"
error_msg = f"Firewall couldn't be disabled: {disable_result_object.message}" logger.error(error_msg)
raise ConnectionTerminationError(error_msg) raise ConnectionTerminationError(error_msg)
else: else:
logger.info("We are skipping disabling the firewall, because it's already off.") 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: if shutil.which('nmcli') is None:
raise CommandNotFoundError('nmcli') 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 ======= # ====== FIREWALL =======
if firewall_setting: if firewall_setting:
check_and_kill_firewall() # on failure, this raises errors, which then bubble up check_and_kill_firewall() # on failure, this raises errors, which then bubble up
# ====== DNS =======
if dns_setting:
reverting_dns_worked = revert_dns()
# ====== NMCLI ======= # ====== NMCLI =======
if SystemStateController.exists(): 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)
@ -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) subprocess.run(('nmcli', 'connection', 'delete', 'hv-ipv6-sink'), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
terminate_tor_connection() 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() SystemState.dissolve()
else: else:
raise ConnectionTerminationError('The connection could not be terminated.') raise ConnectionTerminationError('The connection could not be terminated.')
def setup_nmcli_connection(wg_config_path: str) -> Result: def setup_nmcli_connection(wg_config_path: str) -> Result:
try: try:
process_output = subprocess.check_output(('nmcli', 'connection', 'import', '--temporary', 'type', 'wireguard', 'file', wg_config_path), text=True) 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}") return Result(valid=False, error_type=ResultError.NMCLI, message=f"Called Process error with nmcli: {e}")
# raise ConnectionError('The connection could not be established.') # raise ConnectionError('The connection could not be established.')
def setup_ipv6_sinkhole(process_output: str) -> Result: def setup_ipv6_sinkhole(process_output: str) -> Result:
error_info = "with nmcli setting up the IPv6 sinkhole, error is " error_info = "with nmcli setting up the IPv6 sinkhole, error is "
try: try:
@ -139,6 +207,7 @@ def setup_ipv6_sinkhole(process_output: str) -> Result:
def __establish_system_connection( def __establish_system_connection(
profile: SystemProfile, profile: SystemProfile,
firewall_setting: bool = False, firewall_setting: bool = False,
dns_setting: bool = False,
connection_observer: Optional[ConnectionObserver] = None): connection_observer: Optional[ConnectionObserver] = None):
""" """
Purpose: Purpose:
@ -150,14 +219,15 @@ def __establish_system_connection(
3) Turn on WG via nmcli 3) Turn on WG via nmcli
4) Turn on IPv6 sinkhole 4) Turn on IPv6 sinkhole
5) Turn on Firewall (if enabled) 5) Turn on Firewall (if enabled)
6) Setup State (JSON file) 6) Set DNS (if enabled), and test it.
7) Confirm Firewall is on (if enabled). Raises errors if not. 7) Setup State (JSON file)
8) Testing, confirming, displaying to the end user. 8) Confirm Firewall is on (if enabled). Raises errors if not.
9) Testing, confirming, displaying to the end user.
""" """
check_system_prereqs() check_system_prereqs()
logger.info("[CONNECT] Terming existing connections..") logger.info("[CONNECT] Terming existing connections..")
terminate_system_connection(firewall_setting) terminate_system_connection(firewall_setting, dns_setting)
# ============= INITIAL NMCLI CONNECTION ============= # ============= INITIAL NMCLI CONNECTION =============
wg_config_path = profile.get_wireguard_configuration_path() wg_config_path = profile.get_wireguard_configuration_path()
@ -175,18 +245,44 @@ def __establish_system_connection(
if not sinkhole_result: if not sinkhole_result:
raise ConnectionError('IPv6 Could not be protected.') 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 =============
firewall_tracker = False firewall_tracker = False
if firewall_setting: if firewall_setting:
logger.info("Firewall setting is enabled, let's turn it on..") logger.info("Firewall setting is enabled, let's turn it on..")
turn_on_result = turn_on_firewall( turn_on_result = turn_on_firewall(
profile_id=str(profile.id), 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}") logger.info(f"Results of firewall turn on are: {turn_on_result.valid}")
if turn_on_result.valid: if turn_on_result.valid:
firewall_tracker = True 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 ============= # ============= SETUP STATE =============
# Even if firewall is off, we want to save the fact we turned on the VPN tunnel, before we raise errors. # 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..") logger.info(f"Setting State JSON..")
SystemStateController.create( SystemStateController.create(
profile_id=profile.id, profile_id=profile.id,
firewalled=firewall_tracker firewalled=firewall_tracker,
dns_set=did_dns_work
) )
# ============= CONFIRM ON FIREWALL ============= # ============= CONFIRM ON FIREWALL =============
@ -227,7 +324,7 @@ 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(firewall_setting) terminate_system_connection(firewall_setting, True)
except ConnectionTerminationError: except ConnectionTerminationError:
pass pass
@ -250,12 +347,16 @@ def establish_system_connection(
if ConfigurationController.get_endpoint_verification_enabled(): if ConfigurationController.get_endpoint_verification_enabled():
verify_wireguard_endpoint(profile, ignore=ignore) verify_wireguard_endpoint(profile, ignore=ignore)
# ================= CONNECT WG ================= # ================= SETTINGS =================
firewall_setting = get_firewall_setting() firewall_setting = get_firewall_setting()
dns_setting = get_dns_setting()
# ================= CONNECT WG ================
try: try:
connection_result = __establish_system_connection( connection_result = __establish_system_connection(
profile=profile, profile=profile,
firewall_setting=firewall_setting, firewall_setting=firewall_setting,
dns_setting=dns_setting,
connection_observer=connection_observer connection_observer=connection_observer
) )
evaluate_connection_result(connection_result) evaluate_connection_result(connection_result)
@ -263,7 +364,7 @@ def establish_system_connection(
except ConnectionError: except ConnectionError:
# ================= RETRY ON FAILURE ================= # ================= RETRY ON FAILURE =================
try: try:
terminate_system_connection(firewall_setting) terminate_system_connection(firewall_setting, dns_setting)
except ConnectionTerminationError: except ConnectionTerminationError:
pass pass
@ -272,7 +373,7 @@ def establish_system_connection(
except CalledProcessError: except CalledProcessError:
try: try:
terminate_system_connection(firewall_setting) terminate_system_connection(firewall_setting, dns_setting)
except ConnectionTerminationError: except ConnectionTerminationError:
pass pass
@ -281,6 +382,7 @@ def establish_system_connection(
connection_result = __establish_system_connection( connection_result = __establish_system_connection(
profile=profile, profile=profile,
firewall_setting=firewall_setting, firewall_setting=firewall_setting,
dns_setting=dns_setting,
connection_observer=connection_observer connection_observer=connection_observer
) )
evaluate_connection_result(connection_result) evaluate_connection_result(connection_result)
@ -288,7 +390,7 @@ def establish_system_connection(
except (ConnectionError, CalledProcessError): except (ConnectionError, CalledProcessError):
try: try:
terminate_system_connection(firewall_setting) terminate_system_connection(firewall_setting, dns_setting)
except ConnectionTerminationError: except ConnectionTerminationError:
pass pass