systemwide wg goes connect -> test -> location check -> mtu check -> port check -> wg renegotiate

This commit is contained in:
SimplifiedPrivacy 2026-10-25 14:25:09 -04:00
parent 25d69d728c
commit 14e257d091
9 changed files with 643 additions and 256 deletions

View file

@ -57,6 +57,7 @@ class ResultError(Enum):
NODE_ERROR = "node_error"
NO_INTERNET = "no_internet"
PORT = "port"
RENEGOTIATION = "renegotiation"
NAVIGATE_TO = "navigate_to"
UNKNOWN = "unknown"

View file

@ -62,6 +62,12 @@ def establish_connection(
if profile.connection.code == "wireguard":
_ensure_wireguard_configured(profile, connection_observer)
if profile.connection.code == "wireguard" and profile.type == ProfileType.SYSTEM:
logger.info("Doing a systemwide WG connection..")
return establish_system_connection(profile, ignore, connection_observer, ticket_observer)
else:
establish_fn = {
ProfileType.SESSION: ConnectionController.establish_session_connection,
ProfileType.SYSTEM: establish_system_connection,

View file

@ -1,4 +1,8 @@
from core.services.networking.api_requests.subtools.extract_domain import extract_domain
from core.services.networking.httpx.classify_response import classify_response
from core.observers.Notifications import Notifications, EventType
from core.utils.run_commands import run_generic_command
# models
from core.models.Result import Result, ResultError
@ -6,7 +10,9 @@ from core.services.networking.api_requests.ApiResponseModel import ApiResponse,
from core.Constants import Constants
from core.errors.logger import logger
from essentials.observers.ConnectionObserver import ConnectionObserver
# from essentials.observers.ConnectionObserver import ConnectionObserver
from core.observers.ConnectionObserver import ConnectionObserver
# generic
import time
@ -31,69 +37,339 @@ API_ENDPOINT_FORMATS = {
MAX_ATTEMPTS = 3
import time
import socket
import multiprocessing
from threading import Thread
from queue import Queue, Empty
def is_dns_problem_worker(url: str, queue: multiprocessing.Queue):
"""Worker function for a separate PROCESS"""
domain = extract_domain(url)
try:
socket.gethostbyname(domain)
queue.put((url, False)) # Resolved
except Exception:
queue.put((url, True)) # Failed
def test(connection_observer: Optional[ConnectionObserver], port: Optional[int] = None) -> Result:
if connection_observer:
connection_observer.notify("connecting", "Testing IP..")
return asyncio.run(_test(connection_observer, port))
async def _test(connection_observer: Optional[ConnectionObserver], port: Optional[int] = None) -> Result:
endpoint_list = list(API_ENDPOINT_FORMATS.keys())[:MAX_ATTEMPTS]
start_time = time.time()
total_budget = 5.0
# 1. NUCLEAR DNS PRE-FLIGHT (Using Processes, not Threads)
if port is None:
dns_queue = multiprocessing.Queue()
processes = []
for url in endpoint_list:
p = multiprocessing.Process(target=is_dns_problem_worker, args=(url, dns_queue))
p.start()
processes.append(p)
any_resolved = False
dns_timeout = 2
try:
# Wait for the first successful resolution
while time.time() - start_time < dns_timeout:
try:
# Non-blocking check of the queue
url, is_problem = dns_queue.get(timeout=0.1)
if not is_problem:
any_resolved = True
break
except Empty:
continue
finally:
# THE NUCLEAR STRIKE:
# We kill every single process immediately.
# The OS kernel wipes them from memory regardless of where they are stuck.
for p in processes:
p.terminate()
p.join() # Clean up the zombie process entry
if not any_resolved:
logger.error("Aggressive DNS check failed: No internet.")
return Result(valid=False, error_type=ResultError.CONNECTION, message="No internet connection.")
# 2. HTTP REQUEST PHASE (Using Daemon Threads)
transport = None
if port:
transport = httpx_socks.AsyncProxyTransport(
transport = httpx_socks.ProxyTransport(
proxy_type=ProxyType.SOCKS5,
proxy_host="127.0.0.1",
proxy_port=port,
)
client = httpx.AsyncClient(transport=transport, http2=True, timeout=6)
else:
client = httpx.AsyncClient(http2=True, timeout=6)
async def attempt_request(url: str, index: int):
logger.info(f"Attempt {index + 1}: Trying {url}")
# if connection_observer:
# connection_observer.notify("connecting", f"Testing IP. Attempt {index + 1}")
# We use a Queue to get the first successful Result back from daemon threads
result_queue = Queue()
def attempt_request(url: str, index: int, client: httpx.Client):
try:
raw_response = await client.get(url)
raw_response = client.get(url)
filtered_response = classify_response(raw_response)
if filtered_response.valid:
ip_address = extract_ip_from_response(
response=filtered_response.data,
api_url=url
)
if ip_address is not None:
return Result(valid=True, data=ip_address)
ip_address = extract_ip_from_response(response=filtered_response.data, api_url=url)
if ip_address:
result_queue.put(Result(valid=True, data=ip_address))
return
except Exception:
pass
logger.error(f"Skipping {url}")
except Exception as e:
logger.error(f"Skipping {url}. Invalid API reply or failed connection {str(e)}")
with httpx.Client(transport=transport, http2=False, timeout=3.0) as client:
for i, url in enumerate(endpoint_list):
# daemon=True is CRITICAL.
# It tells Python: "If the main program exits, kill this thread immediately."
t = Thread(target=attempt_request, args=(url, i, client), daemon=True)
t.start()
return Result(valid=False)
# Wait for the first success or the remaining budget
while True:
elapsed = time.time() - start_time
remaining = total_budget - elapsed
if remaining <= 0:
break
try:
# Create tasks for all endpoints to run them concurrently
tasks = [asyncio.create_task(attempt_request(url, i)) for i, url in enumerate(endpoint_list)]
# Process tasks as they complete
for future in asyncio.as_completed(tasks):
result = await future
if result.valid:
# Cancel all other pending tasks immediately
for t in tasks:
if not t.done():
t.cancel()
return result
finally:
await client.aclose()
# Block for the remaining budget
return result_queue.get(timeout=remaining)
except Empty:
break
logger.error(f"We went through {len(endpoint_list)} attempts and could not get a reply.")
return Result(valid=False, error_type=ResultError.CONNECTION, message="Could not connect.")
# import time
# import socket
# from concurrent.futures import ThreadPoolExecutor, wait, FIRST_COMPLETED
# def is_dns_problem(url: str) -> bool:
# """Regular DNS check for domains"""
# domain = extract_domain(url)
# try:
# socket.gethostbyname(domain)
# return False
# except (socket.gaierror, Exception):
# return True
# def test(connection_observer: Optional[ConnectionObserver], port: Optional[int] = None) -> Result:
# endpoint_list = list(API_ENDPOINT_FORMATS.keys())[:MAX_ATTEMPTS]
# start_time = time.time()
# total_budget = 4.0
# # 1. AGGRESSIVE DNS PRE-FLIGHT
# if port is None:
# # IMPORTANT: Do NOT use 'with' here. 'with' calls shutdown(wait=True),
# # which causes the hang you are seeing.
# dns_executor = ThreadPoolExecutor(max_workers=len(endpoint_list))
# dns_futures = {dns_executor.submit(is_dns_problem, url): url for url in endpoint_list}
# dns_done, _ = wait(dns_futures, timeout=1.2, return_when=FIRST_COMPLETED)
# any_resolved = False
# for future in dns_done:
# if future.result() is False:
# any_resolved = True
# break
# if not any_resolved:
# logger.error("Aggressive DNS check failed: No internet or DNS unreachable.")
# # Return immediately. The dns_executor threads will stay alive
# # in the background until the OS times them out, but the
# # main thread is now free.
# return Result(valid=False, error_type=ResultError.CONNECTION, message="No internet connection.")
# # 2. HTTP REQUEST PHASE
# transport = None
# if port:
# transport = httpx_socks.ProxyTransport(
# proxy_type=ProxyType.SOCKS5,
# proxy_host="127.0.0.1",
# proxy_port=port,
# )
# with httpx.Client(transport=transport, http2=False, timeout=3.0) as client:
# def attempt_request(url: str, index: int):
# try:
# raw_response = client.get(url)
# filtered_response = classify_response(raw_response)
# if filtered_response.valid:
# ip_address = extract_ip_from_response(response=filtered_response.data, api_url=url)
# if ip_address:
# return Result(valid=True, data=ip_address)
# except Exception:
# pass
# return Result(valid=False)
# # IMPORTANT: Do NOT use 'with' here either.
# http_executor = ThreadPoolExecutor(max_workers=len(endpoint_list))
# http_futures = {http_executor.submit(attempt_request, url, i): url for i, url in enumerate(endpoint_list)}
# while http_futures:
# elapsed = time.time() - start_time
# remaining = total_budget - elapsed
# if remaining <= 0:
# break
# done, _ = wait(http_futures, timeout=remaining, return_when=FIRST_COMPLETED)
# if not done:
# break
# for future in done:
# result = future.result()
# if result.valid:
# return result
# http_futures.remove(future)
# logger.error(f"We went through {len(endpoint_list)} attempts and could not get a reply.")
# return Result(valid=False, error_type=ResultError.CONNECTION, message="Could not connect.")
# async
# async def _test(connection_observer: Optional[ConnectionObserver], port: Optional[int] = None) -> Result:
# endpoint_list = list(API_ENDPOINT_FORMATS.keys())[:MAX_ATTEMPTS]
# # Using 'async with' ensures the client and its connection pool are
# # closed before the event loop starts shutting down.
# async with httpx.AsyncClient(
# transport=httpx_socks.AsyncProxyTransport(
# proxy_type=ProxyType.SOCKS5,
# proxy_host="127.0.0.1",
# proxy_port=port,
# ) if port else None,
# http2=True,
# timeout=4
# ) as client:
# async def attempt_request(url: str, index: int):
# logger.info(f"Attempt {index + 1}: Trying {url}")
# try:
# raw_response = await client.get(url)
# filtered_response = classify_response(raw_response)
# if filtered_response.valid:
# ip_address = extract_ip_from_response(
# response=filtered_response.data,
# api_url=url
# )
# if ip_address is not None:
# return Result(valid=True, data=ip_address)
# logger.error(f"Skipping {url}")
# except Exception as e:
# logger.error(f"Skipping {url}. Invalid API reply or failed connection {str(e)}")
# return Result(valid=False)
# # Create tasks for concurrent execution
# tasks = [asyncio.create_task(attempt_request(url, i)) for i, url in enumerate(endpoint_list)]
# try:
# for future in asyncio.as_completed(tasks):
# result = await future
# if result.valid:
# # 1. Signal all other pending tasks to stop
# for t in tasks:
# if not t.done():
# t.cancel()
# # 2. CRITICAL: Wait for the cancelled tasks to actually finish.
# # return_exceptions=True prevents CancelledError from being raised here.
# await asyncio.gather(*tasks, return_exceptions=True)
# return result
# except Exception as e:
# logger.error(f"Unexpected error during request processing: {e}")
# # This part is reached only if no task returned a valid Result
# logger.error(f"We went through {len(endpoint_list)} attempts and could not get a reply.")
# return Result(valid=False, error_type=ResultError.CONNECTION, message="Could not connect.")
# def test(connection_observer: Optional[ConnectionObserver], port: Optional[int] = None) -> Result:
# if connection_observer:
# connection_observer.notify("connecting", "Testing IP..")
# return asyncio.run(_test(connection_observer, port))
# async def _test(connection_observer: Optional[ConnectionObserver], port: Optional[int] = None) -> Result:
# endpoint_list = list(API_ENDPOINT_FORMATS.keys())[:MAX_ATTEMPTS]
# if port:
# transport = httpx_socks.AsyncProxyTransport(
# proxy_type=ProxyType.SOCKS5,
# proxy_host="127.0.0.1",
# proxy_port=port,
# )
# client = httpx.AsyncClient(transport=transport, http2=True, timeout=4)
# else:
# client = httpx.AsyncClient(http2=True, timeout=4)
# async def attempt_request(url: str, index: int):
# logger.info(f"Attempt {index + 1}: Trying {url}")
# try:
# raw_response = await client.get(url)
# filtered_response = classify_response(raw_response)
# if filtered_response.valid:
# ip_address = extract_ip_from_response(
# response=filtered_response.data,
# api_url=url
# )
# if ip_address is not None:
# return Result(valid=True, data=ip_address)
# logger.error(f"Skipping {url}")
# except Exception as e:
# logger.error(f"Skipping {url}. Invalid API reply or failed connection {str(e)}")
# return Result(valid=False)
# try:
# # Create tasks for all endpoints to run them concurrently
# tasks = [asyncio.create_task(attempt_request(url, i)) for i, url in enumerate(endpoint_list)]
# # Process tasks as they complete
# for future in asyncio.as_completed(tasks):
# result = await future
# if result.valid:
# # Cancel all other pending tasks immediately
# for t in tasks:
# if not t.done():
# t.cancel()
# return result
# finally:
# await client.aclose()
# logger.error(f"We went through {len(endpoint_list)} attempts and could not get a reply.")
# return Result(valid=False, error_type=ResultError.CONNECTION, message="Could not connect.")
def extract_ip_from_response(response: dict, api_url: str) -> str | bool:
"""
Purpose:
@ -124,6 +400,7 @@ def extract_ip_from_response(response: dict, api_url: str) -> str | bool:
def ping_ip(ip: str) -> Result:
command = ["ping", "-c", "1", "-W", "2", ip]
human_readable_goal = f"Pinging {ip}"
Notifications.emit(EventType.STATUS_UPDATE, human_readable_goal)
return run_generic_command(command, human_readable_goal, timeout=5)

View file

@ -1,6 +1,6 @@
from core.services.networking.api_requests.ApiResponseModel import BackoffStrategy
from core.services.networking.general_connection_tools.test_internet import test_internet
from core.services.networking.general_connection_tools.ip import ping_ip_worked
from core.services.networking.general_connection_tools.ip import ping_ip
from core.services.networking.systemwide.general_tools.test_any_protocol import get_singbox_ip
from core.services.networking.general_connection_tools.config_tools import extract_endpoint_ip
from core.models.manage.server_reply_tools import api_call_and_extraction, ApiCall
@ -47,7 +47,8 @@ def check_if_location_is_up(profile: Profile, connection_observer: ConnectionObs
ip_address = get_profile_ip(profile_id=profile_id, protocol=protocol, profile_type=profile_type)
if ip_address:
can_be_pinged = ping_ip_worked(ip_address)
ping_result = ping_ip(ip_address)
can_be_pinged = ping_result.valid
print(f"Can {location_name} be pinged? {can_be_pinged}")
else:
logger.error("Skipping IP Address Test.")

View file

@ -1,3 +1,5 @@
from core.utils.run_commands import run_generic_command
from core.Constants import Constants
from core.Errors import CommandNotFoundError
from core.observers.ConnectionObserver import ConnectionObserver
@ -37,15 +39,33 @@ def await_network_interface():
raise ConnectionError('The network interface could not be activated.')
def system_uses_wireguard_interface():
if shutil.which('ip') is None:
raise CommandNotFoundError('ip')
process = subprocess.Popen(('ip', 'route', 'get', '192.0.2.1'), stdout=subprocess.PIPE)
process_output = str(process.stdout.read())
command = ['ip', 'route', 'get', '192.0.2.1']
human_readable_goal = "Getting WG interface"
get_output = run_generic_command(command, human_readable_goal, timeout=8)
if not get_output.valid:
return get_output
return bool(re.search('dev wg', str(process_output)))
output = get_output.data
print(f"output is {output}")
return bool(re.search('dev wg', str(output)))
# def system_uses_wireguard_interface():
# if shutil.which('ip') is None:
# raise CommandNotFoundError('ip')
# process = subprocess.Popen(('ip', 'route', 'get', '192.0.2.1'), stdout=subprocess.PIPE)
# process_output = str(process.stdout.read())
# return bool(re.search('dev wg', str(process_output)))
def __test_connection(port_number: Optional[int] = None, timeout: float = 4.0):

View file

@ -0,0 +1,66 @@
from core.services.keys_and_verifications.wireguard_keys import get_mtu, insert_mtu_into_config_on_disk
from core.services.networking.mtu.mtu_tools import loop_through_testing_mtu, mtu_is_fine, MIN_MTU, MAX_MTU
from core.observers.Notifications import Notifications, EventType
from core.models.BaseProfile import BaseProfile as Profile
from core.models.system.SystemProfile import SystemProfile
from core.errors.logger import logger
import asyncio
from typing import Optional
def _changed_mtu(profile: SystemProfile, current_mtu: Optional[int]) -> bool:
notification = "Testing MTU.."
Notifications.emit(EventType.STATUS_UPDATE, notification)
# optimal_mtu = loop_through_testing_mtu()
optimal_mtu = asyncio.run(loop_through_testing_mtu())
logger.info(f"Optimal MTU is {optimal_mtu}")
if optimal_mtu == current_mtu:
return False
# Assuming it isn't already at the max, there's no need to reset it to the max,
if current_mtu != MAX_MTU and optimal_mtu == MAX_MTU:
return False
notification = f"Updating WG MTU with {optimal_mtu}"
Notifications.emit(EventType.STATUS_UPDATE, notification)
return insert_mtu_into_config_on_disk(profile=profile, mtu=optimal_mtu)
def should_we_even_test_mtu(profile_id: int):
# Evaluate MTU WITH connection on
current_mtu = get_mtu(profile_id=profile_id)
if not current_mtu:
return True, current_mtu
if current_mtu > MIN_MTU:
logger.info("Skipping MTU testing, because MTU as at the minimum, so it's not relevant here.")
return False, current_mtu
else:
logger.info("MTU isn't at the min, so we can potentially improve")
return True, current_mtu
def was_mtu_changed(profile: Profile, current_mtu: int):
notification = "Testing MTU..."
Notifications.emit(EventType.STATUS_UPDATE, notification)
logger.info(notification)
if mtu_is_fine(current_mtu):
notification = "MTU is NOT the issue."
Notifications.emit(EventType.STATUS_UPDATE, notification)
logger.info(notification)
# MTU wasn't changed or wasn't applicable,
return False
if _changed_mtu(profile=profile, current_mtu=current_mtu):
logger.info("MTU changed. We should retry the connection")
return True
else:
logger.warning("MTU check performed but no change was made or user rejected it.")
notification = "MTU was NOT changed."
Notifications.emit(EventType.STATUS_UPDATE, notification)
return False

View file

@ -1,5 +1,7 @@
from core.services.keys_and_verifications.wireguard_keys import get_mtu, insert_mtu_into_config_on_disk, offer_port_change, should_we_try_to_change_port, port_was_changed_already_today
from core.services.networking.systemwide.wireguard.mtu_tools import loop_through_testing_mtu, mtu_is_fine, MIN_MTU, MAX_MTU
from core.services.keys_and_verifications.wireguard_keys import register_wireguard_session
from core.services.keys_and_verifications.wireguard_keys import offer_port_change, should_we_try_to_change_port #, port_was_changed_already_today
from core.services.networking.mtu.mtu_orchestrator import should_we_even_test_mtu, was_mtu_changed
from core.services.networking.general_connection_tools.location_evaluation import check_if_location_is_up
from core.services.networking.api_requests.ApiResponseModel import BackoffStrategy
from core.models.enum_models import ProtocolChoice, SINGBOX_PROTOCOLS
@ -31,19 +33,20 @@ from core.controllers.SystemStateController import SystemStateController
from core.models.system.SystemProfile import SystemProfile
from core.models.system.SystemState import SystemState
from core.observers.ConnectionObserver import ConnectionObserver
from subprocess import CalledProcessError
# from subprocess import CalledProcessError
from typing import Optional
# import os
# import re
import shutil
# import subprocess
import time
import asyncio
# import asyncio
from functools import partial
def terminate_system_connection(
firewall_setting: Optional[bool] = None,
dns_setting: Optional[bool] = None
):
) -> Result:
# ====== PREREQS =======
if firewall_setting is None:
@ -63,12 +66,11 @@ def terminate_system_connection(
if dns_setting:
_ = revert_dns()
# ====== NMCLI =======
if SystemStateController.exists():
# ====== SINGBOX =======
# Check if the user is switching from Singbox to Wireguard:
if SystemStateController.exists():
current_state = SystemState.get()
if current_state:
if get_protocol_enum(current_state.profile_id) in SINGBOX_PROTOCOLS:
logger.info("Ending Singbox protocols first..")
from core.services.networking.systemwide.encrypted_proxy.singbox_runner import end_singbox
@ -78,8 +80,9 @@ def terminate_system_connection(
command = ['nmcli', 'connection', 'delete', 'wg']
human_readable_goal = "Deleting NMCLI wg interface"
delete_completed = run_generic_command(command, human_readable_goal, timeout=8)
print(f"delete_completed output: {delete_completed}")
if delete_completed.valid or not system_uses_wireguard_interface():
if not system_uses_wireguard_interface():
command = ['nmcli', 'connection', 'delete', 'hv-ipv6-sink']
human_readable_goal = "Deleting NMCLI IPv6 Sink"
sinkhole_deleted = run_generic_command(command, human_readable_goal, timeout=8)
@ -91,14 +94,15 @@ def terminate_system_connection(
# confirm firewall is off.
if firewall_setting:
check_and_kill_firewall() # on failure, this raises errors, which then bubble up
# else:
# error_log = f"Error with deleting the connection: {delete_completed.error_type} {delete_completed.message}"
# logger.error(error_log)
# raise ConnectionTerminationError(f'The connection could not be terminated. {error_log}')
# finally end that profile's JSON state:
if SystemStateController.exists():
SystemState.dissolve()
else:
error_log = f"Error with deleting the connection: {delete_completed.error_type} {delete_completed.message}"
logger.error(error_log)
raise ConnectionTerminationError(f'The connection could not be terminated. {error_log}')
return Result(valid=True)
@ -115,45 +119,17 @@ def evaluate_location_and_internet(profile: SystemProfile, connection_observer:
return location_result
def _cleanup_on_error(firewall_setting: bool, dns_setting: bool) -> None:
"""Safely terminate connection on error, swallowing cleanup errors."""
try:
terminate_system_connection(firewall_setting, dns_setting)
except ConnectionTerminationError:
pass
# def _cleanup_on_error(firewall_setting: bool, dns_setting: bool) -> None:
# """Safely terminate connection on error, swallowing cleanup errors."""
# terminate_system_connection(firewall_setting, dns_setting)
def _finish_success(final_result: Result, profile_id: int) -> Result:
Notifications.emit(EventType.SYSTEM_ENABLE, profile_id)
notification = "Connected."
Notifications.emit(EventType.STATUS_UPDATE, notification)
terminate_tor_connection()
time.sleep(0.3)
return final_result
def changed_mtu(profile: SystemProfile, current_mtu: Optional[int]) -> bool:
notification = "Testing MTU.."
Notifications.emit(EventType.STATUS_UPDATE, notification)
# optimal_mtu = loop_through_testing_mtu()
optimal_mtu = asyncio.run(loop_through_testing_mtu())
logger.info(f"Optimal MTU is {optimal_mtu}")
if optimal_mtu == current_mtu:
return False
# Assuming it isn't already at the max, there's no need to reset it to the max,
if current_mtu != MAX_MTU and optimal_mtu == MAX_MTU:
return False
notification = f"Updating WG MTU with {optimal_mtu}"
Notifications.emit(EventType.STATUS_UPDATE, notification)
return insert_mtu_into_config_on_disk(profile=profile, mtu=optimal_mtu)
def __establish_system_connection(
profile: SystemProfile,
firewall_setting: bool = False,
@ -164,8 +140,6 @@ def __establish_system_connection(
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) Setup State (JSON file)
@ -173,17 +147,10 @@ def __establish_system_connection(
7) Set DNS (if enabled), and test it. Let's it slide even if failed.
8) Testing, confirming, displaying to the end user.
"""
check_system_prereqs()
notification = "Terming existing connections.."
Notifications.emit(EventType.STATUS_UPDATE, notification)
logger.info(notification)
terminate_system_connection(firewall_setting, dns_setting)
# ============= INITIAL NMCLI CONNECTION =============
notification = "Setting Up WireGuard.."
Notifications.emit(EventType.STATUS_UPDATE, notification)
# if connection_observer:
# connection_observer.notify("connecting", notification)
wg_config_path = profile.get_wireguard_configuration_path()
nmcli_result = setup_nmcli_connection(wg_config_path)
@ -204,17 +171,6 @@ def __establish_system_connection(
if not sinkhole_result:
raise ConnectionError('IPv6 Could not be protected.')
# ============= 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("Setting State JSON..")
current_state = SystemStateController.create(
profile_id=profile.id,
firewalled=firewall_setting, # intended setting, not result yet
dns_set=dns_setting, # intended setting, not result yet,
protocol=ProtocolChoice.WIREGUARD
)
# ============= PREP SETTINGS =============
if connection_observer:
connection_observer.notify("connecting", "Getting Interface Name..")
@ -273,18 +229,28 @@ def __establish_system_connection(
if not test_result.valid:
notification = "Test Failed."
Notifications.emit(EventType.STATUS_UPDATE, notification)
logger.error("Returning that the test failed..")
return test_result
# ============= UPDATE STATE =============
if dns_setting and not did_dns_work:
current_state.firewalled = firewall_tracker
current_state.dns_set = did_dns_work
SystemStateController.update_or_create(current_state)
notification = "DNS Set Did Not Work."
Notifications.emit(EventType.STATUS_UPDATE, notification)
# if dns_setting and not did_dns_work:
# current_state.firewalled = firewall_tracker
# current_state.dns_set = did_dns_work
# SystemStateController.update_or_create(current_state)
# notification = "DNS Set Did Not Work."
# Notifications.emit(EventType.STATUS_UPDATE, notification)
# ============= SETUP STATE =============
logger.info("Setting State JSON..")
current_state = SystemStateController.create(
profile_id=profile.id,
firewalled=firewall_setting,
dns_set=did_dns_work,
protocol=ProtocolChoice.WIREGUARD
)
return Result(valid=True, data=current_state)
import time
def establish_system_connection(
profile: SystemProfile,
@ -296,11 +262,14 @@ def establish_system_connection(
Establish connection with a maximum of 2 retries.
Steps)
1) Verify Endpoint's encryption
2) get settings
3) Attempt 1 -> (Fail) -> Location Check
4) Attempt 2 -> (Fail) -> MTU Check -> Attempt 3
1) Check Prereqs
2) Verify Endpoint's encryption
3) get settings
4) Disable any pre-existing system connections
"""
# ================= PRE-REQS =================
check_system_prereqs()
# ================= VERIFY ENDPOINT =================
if connection_observer:
connection_observer.notify("connecting", "Verifying endpoint..")
@ -314,13 +283,29 @@ def establish_system_connection(
# ================= CONNECT WG ================
# We allow 3 total attempts (Initial + 2 retries)
for attempt in range(1, 4):
try:
# if connection_observer:
# connection_observer.notify("connecting", f"Connecting attempt {attempt} of 3.. ")
# =================================
# PRE-EXISTING
# =================================
logger.info("Terminating pre-existing")
terminated = terminate_system_connection(firewall_setting, dns_setting)
if terminated.valid:
logger.info("Terminated pre-existing")
else:
logger.error(f"Failed to terminate pre-existing {terminated.message}")
return terminated
notification = f"Connecting attempt {attempt} of 3.. "
# func_choice = partial(register_wireguard_session, profile=profile, connection_observer=connection_observer)
# return Result(valid=False, function_choice=func_choice, error_type=ResultError.RENEGOTIATION, message="Would you like to try Wireguard key renegotiation? (Note: this invalidates other profiles with the same subscription.)")
print("sleeping")
time.sleep(5)
notification = f"Connecting attempt {attempt}.. "
Notifications.emit(EventType.STATUS_UPDATE, notification)
# =================================
# CONNECT
# =================================
# Attempt the actual system connection
connection_result = __establish_system_connection(
profile=profile,
@ -329,30 +314,29 @@ def establish_system_connection(
connection_observer=connection_observer
)
print("sleeping")
time.sleep(5)
# =================================
# EVALUATE
# =================================
if connection_result.valid:
return _finish_success(final_result=connection_result, profile_id=profile.id)
# If not valid, we treat it as a failure to trigger the except block logic
notification = f"{attempt} attempt failed.."
Notifications.emit(EventType.STATUS_UPDATE, notification)
raise ConnectionError(notification)
except (ConnectionError, CalledProcessError) as e:
# single time port is offered forever.
if port_was_changed_already_today():
# permanent config modify:
ConfigurationController.change_port_prompts(False)
# =================================
# ATTEMPT ONE: LOCATION UP?
# =================================
if attempt == 1:
# First failure: Evaluate Location and Internet without connection,
_cleanup_on_error(firewall_setting, dns_setting)
logger.info("Terminating system connection to check location")
terminate_system_connection(firewall_setting, dns_setting)
location_result = evaluate_location_and_internet(
profile=profile,
connection_observer=connection_observer
)
#============== NO RETRY #==============
if location_result.backoff_strategy == BackoffStrategy.NO_RETRY:
logger.error(f"Location check failed: {location_result.message}")
bottom_left = "Node Down"
@ -360,63 +344,91 @@ def establish_system_connection(
notification = location_result.message
Notifications.emit(EventType.SHOW_POPUP, notification)
return location_result
# raise ConnectionError(location_result.message) from e
#============== NO SEEMS FINE #==============
notification = "Location check passed. Retrying immediately..."
Notifications.emit(EventType.STATUS_UPDATE, notification)
logger.info(notification)
continue # Proceed to attempt 2
elif attempt == 2:
# Second failure: Evaluate MTU WITH connection on
current_mtu = get_mtu(profile_id=profile.id)
# =================================
# ATTEMPT 2+ MTU, PORT, WG KEY
# =================================
if current_mtu != MAX_MTU or not current_mtu:
logger.info("MTU is already at the max, no point to change it.")
_cleanup_on_error(firewall_setting, dns_setting)
raise ConnectionError("Failed to connect.")
# return Result(valid=False, error_type=ResultError.CONNECTION, message="Failed to connect.")
# Connection is still On for MTU testing:
if not current_mtu or current_mtu > MIN_MTU:
notification = "Testing MTU..."
Notifications.emit(EventType.STATUS_UPDATE, notification)
logger.info(notification)
if mtu_is_fine(current_mtu):
notification = "MTU is NOT the issue."
Notifications.emit(EventType.STATUS_UPDATE, notification)
logger.info(notification)
# If MTU wasn't changed or wasn't applicable, don't bother with attempt 3
_cleanup_on_error(firewall_setting, dns_setting)
raise ConnectionError("Failed to connect.")
# return Result(valid=False, error_type=ResultError.CONNECTION, message="Failed to connect.")
# =================================
# MTU
# =================================
should_test_mtu, current_mtu = should_we_even_test_mtu(profile.id)
if changed_mtu(profile=profile, current_mtu=current_mtu):
logger.info("MTU changed. Retrying connection...")
continue # Proceed to attempt 3
if should_test_mtu:
logger.info("We should test (and potentially change) the MTU..")
if was_mtu_changed(profile=profile, current_mtu=current_mtu):
logger.info("MTU was changed, so we're trying again.")
continue
else:
logger.warning("MTU check performed but no change was made or user rejected it.")
notification = "MTU was NOT changed."
Notifications.emit(EventType.STATUS_UPDATE, notification)
logger.info("MTU was tested but NOT changed.")
else:
logger.info("Skipping MTU testing, it's not relevant here.")
logger.info("MTU was NOT tested.")
# If Port & MTU wasn't changed or wasn't applicable, don't bother with attempt 3
# PORT CHANGE:
# Take down connection for the rest of the tests:
terminated = terminate_system_connection(firewall_setting, dns_setting)
if terminated.valid:
logger.info("Took down the system connection for the other tests.")
else:
return terminated
# =================================
# PORT
# =================================
if should_we_try_to_change_port():
print("We should try to change port")
_cleanup_on_error(firewall_setting, dns_setting)
logger.info("We should try to change port")
# Offer the choice to the end user:
return offer_port_change(profile)
else:
notification = "Failed to connect."
Notifications.emit(EventType.STATUS_UPDATE, notification)
_cleanup_on_error(firewall_setting, dns_setting)
raise ConnectionError("Failed to connect.")
logger.info("Skipping port choice as an offer.")
else:
# Attempt 3 failed
logger.error("Maximum retries reached. Connection failed.")
notification = "Failed to connect."
Notifications.emit(EventType.STATUS_UPDATE, notification)
_cleanup_on_error(firewall_setting, dns_setting)
raise ConnectionError(notification)
# return Result(valid=False, error_type=ResultError.CONNECTION, message="Failed to connect.")
# =================================
# WIREGUARD RENEGOTIATION
# =================================
terminate_system_connection(firewall_setting, dns_setting)
func_choice = partial(register_wireguard_session, profile=profile, connection_observer=connection_observer)
return Result(valid=False, function_choice=func_choice, error_type=ResultError.RENEGOTIATION, message="Would you like to try Wireguard key renegotiation? (Note: this invalidates other profiles with the same subscription.)")
# except (ConnectionError, CalledProcessError) as e:
# # single time port is offered forever.
# if port_was_changed_already_today():
# # permanent config modify:
# ConfigurationController.change_port_prompts(False)
# else:
# notification = "Failed to connect."
# Notifications.emit(EventType.STATUS_UPDATE, notification)
# _cleanup_on_error(firewall_setting, dns_setting)
# raise ConnectionError("Failed to connect.")
# else:
# # Attempt 3 failed
# logger.error("Maximum retries reached. Connection failed.")
# notification = "Failed to connect."
# Notifications.emit(EventType.STATUS_UPDATE, notification)
# _cleanup_on_error(firewall_setting, dns_setting)
# raise ConnectionError(notification)
# # return Result(valid=False, error_type=ResultError.CONNECTION, message="Failed to connect.")
# First take
# _cleanup_on_error(firewall_setting, dns_setting)
# _cleanup_on_error(firewall_setting, dns_setting)
# # return Result(valid=False, error_type=ResultError.CONNECTION, message="Failed to connect.")

View file

@ -156,18 +156,22 @@ def enable_profile(
connection_result = establish_connection(profile, ignore=ignore, connection_observer=connection_observer)
if not connection_result:
logger.error("Connection result is empty.")
notification = "Connection Failed"
Notifications.emit(EventType.STATUS_UPDATE, notification)
return
if not connection_result.valid:
return connection_result
logger.info(f"Exiting establish connection with {connection_result}")
if profile_observer is not None:
profile_observer.notify('enabled', profile)
notification = "Enabled."
if connection_result.valid:
# if profile_observer is not None:
# profile_observer.notify('enabled', profile)
Notifications.emit(EventType.SYSTEM_ENABLE, profile.id)
notification = "Connected."
Notifications.emit(EventType.STATUS_UPDATE, notification)
return Result(valid=True, data=profile.type)
else:
return connection_result
except FirewallError:
return Result(valid=False, error_type=ResultError.FIREWALL, message="Firewall Error.")
except ConnectionError as e: