systemwide wg goes connect -> test -> location check -> mtu check -> port check -> wg renegotiate
This commit is contained in:
parent
25d69d728c
commit
14e257d091
9 changed files with 643 additions and 256 deletions
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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.")
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
66
core/services/networking/mtu/mtu_orchestrator.py
Normal file
66
core/services/networking/mtu/mtu_orchestrator.py
Normal 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
|
||||
|
||||
|
|
@ -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.")
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Reference in a new issue