Introduced Location & MTU testing on systemwide Wireguard. As well as IP address testing instead of the previous method. Additionally, 400 status APIs now pass through the APIResponse layer as True to be evaluated by the SeverReply Layer. Finally notifications are better integrated to convey the testing rounds to the end user.
This commit is contained in:
parent
d2e75ca0e1
commit
2208cedb7e
19 changed files with 595 additions and 244 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
|
from core.services.networking.api_requests.ApiResponseModel import BackoffStrategy
|
||||||
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
@ -51,6 +51,9 @@ class ResultError(Enum):
|
||||||
SERVER_SIDE = "server_side"
|
SERVER_SIDE = "server_side"
|
||||||
MTU = "mtu"
|
MTU = "mtu"
|
||||||
TOR_NEEDED = "tor_needed"
|
TOR_NEEDED = "tor_needed"
|
||||||
|
API_DOWN = "api_down"
|
||||||
|
NODE_ERROR = "node_error"
|
||||||
|
NO_INTERNET = "no_internet"
|
||||||
NAVIGATE_TO = "navigate_to"
|
NAVIGATE_TO = "navigate_to"
|
||||||
UNKNOWN = "unknown"
|
UNKNOWN = "unknown"
|
||||||
|
|
||||||
|
|
@ -62,14 +65,15 @@ class Result():
|
||||||
data: Optional[Any] = None
|
data: Optional[Any] = None
|
||||||
message: Optional[str] = None
|
message: Optional[str] = None
|
||||||
goal: Optional[str] = None
|
goal: Optional[str] = None
|
||||||
|
backoff_strategy: Optional[BackoffStrategy] = None
|
||||||
function_choice: Optional[partial] = None
|
function_choice: Optional[partial] = None
|
||||||
no_option: Optional[partial] = None
|
no_option: Optional[partial] = None
|
||||||
navigate_to: Optional[str] = None
|
navigate_to: Optional[str] = None
|
||||||
|
|
||||||
def user_message(self) -> str:
|
# def user_message(self) -> str:
|
||||||
"""Human-readable error for the UI."""
|
# """Human-readable error for the UI."""
|
||||||
messages = {
|
# messages = {
|
||||||
ResultError.SUCCESS: "Operation completed successfully.",
|
# ResultError.SUCCESS: "Operation completed successfully.",
|
||||||
ResultError.UNKNOWN: f"Error: {self.message}",
|
# ResultError.UNKNOWN: f"Error: {self.message}",
|
||||||
}
|
# }
|
||||||
return messages.get(self.error_type, "Unknown error")
|
# return messages.get(self.error_type, "Unknown error")
|
||||||
|
|
|
||||||
|
|
@ -118,7 +118,9 @@ def api_call_and_extraction(
|
||||||
# EVALUATE CONNECTION FAILURE
|
# EVALUATE CONNECTION FAILURE
|
||||||
#-----------------------------
|
#-----------------------------
|
||||||
# Layer 1: API failure:
|
# Layer 1: API failure:
|
||||||
if not api_response.valid:
|
print(f"api_response is {api_response}")
|
||||||
|
print(f"api_response is {api_response.data}")
|
||||||
|
if not api_response.valid and not api_response.error_type in (ErrorType.AUTHENTICATION_ERROR, ErrorType.INVALID_REQUEST):
|
||||||
|
|
||||||
# legacy system
|
# legacy system
|
||||||
if api_response.error_type == ErrorType.AUTHENTICATION_ERROR:
|
if api_response.error_type == ErrorType.AUTHENTICATION_ERROR:
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ class ServerReply():
|
||||||
message: Optional[str] = None
|
message: Optional[str] = None
|
||||||
model_version: int = 1
|
model_version: int = 1
|
||||||
data: Optional[dict] = None
|
data: Optional[dict] = None
|
||||||
|
boolean_result: Optional[bool] = None
|
||||||
conf: Optional[str] = None
|
conf: Optional[str] = None
|
||||||
payment_status: Optional[bool] = None
|
payment_status: Optional[bool] = None
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ class EventType(Enum):
|
||||||
PROFILE_CREATED = auto()
|
PROFILE_CREATED = auto()
|
||||||
PROFILE_DELETED = auto()
|
PROFILE_DELETED = auto()
|
||||||
STATUS_UPDATE = auto()
|
STATUS_UPDATE = auto()
|
||||||
|
SHOW_POPUP = auto()
|
||||||
RESUME_ACTIONS = auto()
|
RESUME_ACTIONS = auto()
|
||||||
|
|
||||||
class Notifications:
|
class Notifications:
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
|
from core.utils.get_raw_string import get_string
|
||||||
from core.models.manage.server_reply_tools import api_call_and_extraction, ApiCall
|
from core.models.manage.server_reply_tools import api_call_and_extraction, ApiCall
|
||||||
# from core.services.networking.httpx import connect
|
# from core.services.networking.httpx import connect
|
||||||
#from core.services.networking.api_requests.ApiResponseModel import ErrorType
|
#from core.services.networking.api_requests.ApiResponseModel import ErrorType
|
||||||
|
|
@ -118,6 +118,62 @@ def insert_mtu(mtu: int, wireguard_config: str) -> str:
|
||||||
return re.sub(pattern, replacement, wireguard_config)
|
return re.sub(pattern, replacement, wireguard_config)
|
||||||
|
|
||||||
|
|
||||||
|
def insert_mtu_with_replacement(mtu: int, wireguard_config: str) -> str:
|
||||||
|
# 1. Attempt to replace an existing MTU line.
|
||||||
|
# ^MTU matches "MTU" at the start of a line (requires re.MULTILINE).
|
||||||
|
# \s*=\s*\d+ matches the equals sign and the numeric value.
|
||||||
|
new_config, count = re.subn(
|
||||||
|
r"^MTU\s*=\s*\d+",
|
||||||
|
f"MTU = {mtu}",
|
||||||
|
wireguard_config,
|
||||||
|
flags=re.MULTILINE
|
||||||
|
)
|
||||||
|
|
||||||
|
# If count > 0, an existing MTU was found and replaced.
|
||||||
|
if count > 0:
|
||||||
|
return new_config
|
||||||
|
|
||||||
|
# 2. If no MTU existed, use the original insertion logic.
|
||||||
|
pattern = r"(DNS\s*=\s*[^\n]+)(\s*)(\[Peer\])"
|
||||||
|
replacement = rf"\1\nMTU = {mtu}\2\3"
|
||||||
|
|
||||||
|
return re.sub(pattern, replacement, new_config)
|
||||||
|
|
||||||
|
|
||||||
|
def insert_mtu_into_config_on_disk(profile: Profile, mtu: int) -> bool:
|
||||||
|
"""Inserts an MTU into a Systemwide Profile"""
|
||||||
|
file_location = f"/etc/hydra-veil/profiles/{str(profile.id)}/wg.conf"
|
||||||
|
wireguard_config = get_string(file_location)
|
||||||
|
new_config = insert_mtu_with_replacement(mtu=mtu, wireguard_config=wireguard_config)
|
||||||
|
try:
|
||||||
|
profile.attach_wireguard_configuration(new_config)
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.error({str(e)})
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _parse_for_mtu(wireguard_config: str) -> Optional[int]:
|
||||||
|
# This pattern searches for 'MTU', optional whitespace, an equals sign,
|
||||||
|
# and captures the digits that follow.
|
||||||
|
pattern = r"MTU\s*=\s*(\d+)"
|
||||||
|
|
||||||
|
match = re.search(pattern, wireguard_config)
|
||||||
|
|
||||||
|
if match:
|
||||||
|
# Return the captured digit group as an integer.
|
||||||
|
return int(match.group(1))
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# ASSUMES SYSTEM-WIDE:
|
||||||
|
def get_mtu(profile_id: int) -> Optional[int]:
|
||||||
|
"""Gets the MTU of a Systemwide Profile"""
|
||||||
|
file_location = f"/etc/hydra-veil/profiles/{profile_id}/wg.conf"
|
||||||
|
wireguard_config = get_string(file_location)
|
||||||
|
return _parse_for_mtu(wireguard_config)
|
||||||
|
|
||||||
|
|
||||||
# If need it pretty spacing quickly:
|
# If need it pretty spacing quickly:
|
||||||
def expand_newlines(text):
|
def expand_newlines(text):
|
||||||
"""
|
"""
|
||||||
|
|
@ -125,67 +181,3 @@ def expand_newlines(text):
|
||||||
empty line between every non-empty line of text.
|
empty line between every non-empty line of text.
|
||||||
"""
|
"""
|
||||||
return "\n\n".join([line for line in text.splitlines() if line.strip()])
|
return "\n\n".join([line for line in text.splitlines() if line.strip()])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# over-engineering:
|
|
||||||
|
|
||||||
# api_result = connect.single_endpoint(
|
|
||||||
# method="post",
|
|
||||||
# url=url,
|
|
||||||
# observer=connection_observer,
|
|
||||||
# payload=payload,
|
|
||||||
# billing_code=billing_code
|
|
||||||
# )
|
|
||||||
|
|
||||||
# if not api_result.valid:
|
|
||||||
# if api_result.error_type == ErrorType.AUTHENTICATION_ERROR:
|
|
||||||
# logger.error(f"Server is giving an Authentication error for billing ID: {billing_code}")
|
|
||||||
# raise InvalidSubscriptionError(f"Invalid Subscription for {billing_code}")
|
|
||||||
# else:
|
|
||||||
# error_msg = f"Could Not Connect to API: {api_result.error_type}"
|
|
||||||
# logger.error(error_msg)
|
|
||||||
# raise ConnectionError(error_msg)
|
|
||||||
|
|
||||||
# # 2nd Layer Translation:
|
|
||||||
# result = format_server_response(api_response=api_result, desired_type="data")
|
|
||||||
# if not result.valid:
|
|
||||||
# # KILLING THE RESULT FOR NOW. Later, replace this error raise with the return of data,
|
|
||||||
# raise InvalidSubscriptionError(result.message)
|
|
||||||
|
|
||||||
|
|
||||||
# print(f"wireguard_configuration type is {type(wireguard_configuration)}")
|
|
||||||
# # print(f"wireguard_configuration value is {wireguard_configuration}")
|
|
||||||
|
|
||||||
# if isinstance(wireguard_configuration, dict):
|
|
||||||
# print(f"wireguard_configuration is {type(wireguard_configuration)}")
|
|
||||||
# print(f"wireguard_configuration is {wireguard_configuration}")
|
|
||||||
# logger.error("Server isn't returning a WG config, it's trying to tell you a ServerReply message...")
|
|
||||||
# return format_server_response(wireguard_configuration, "message")
|
|
||||||
|
|
||||||
|
|
||||||
# def handle_config_error(wireguard_configuration: str|dict) -> Result:
|
|
||||||
# """The server gave the wireguard config in an invalid format. First convert it to get the error."""
|
|
||||||
# server_reply = format_server_response(wireguard_configuration, "message")
|
|
||||||
# if server_reply.valid:
|
|
||||||
# return server_reply
|
|
||||||
# else:
|
|
||||||
# return Result(valid=False, error_type=ResultError.NODE_ERROR, message="server-side error with wireguard config format.")
|
|
||||||
|
|
||||||
|
|
||||||
# legacy:
|
|
||||||
|
|
||||||
# country_code = profile.location.country_code
|
|
||||||
# location_code = profile.location.code
|
|
||||||
# url = f'{Constants.SP_API_BASE_URL}/countries/{country_code}/locations/{location_code}/wireguard-sessions'
|
|
||||||
|
|
||||||
# ConnectionController.with_preferred_connection(
|
|
||||||
# profile.location.country_code,
|
|
||||||
# profile.location.code,
|
|
||||||
# profile.subscription.billing_code,
|
|
||||||
# wireguard_keys.get('public'),
|
|
||||||
# task=WebServiceApiService.post_wireguard_session,
|
|
||||||
# connection_observer=connection_observer
|
|
||||||
# )
|
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@ class BackoffStrategy(Enum):
|
||||||
class ErrorType(Enum):
|
class ErrorType(Enum):
|
||||||
"""Classified error categories."""
|
"""Classified error categories."""
|
||||||
SUCCESS = "success"
|
SUCCESS = "success"
|
||||||
|
APPLICATION_ERROR = "application_error"
|
||||||
|
|
||||||
INVALID_ENDPOINT = "invalid_endpoint" # 404 - could be wrong URL or middleware rejecting
|
INVALID_ENDPOINT = "invalid_endpoint" # 404 - could be wrong URL or middleware rejecting
|
||||||
INVALID_REQUEST = "invalid_request" # 400, 405 - bad method/body
|
INVALID_REQUEST = "invalid_request" # 400, 405 - bad method/body
|
||||||
AUTHENTICATION_ERROR = "authentication_error" # 401 - need credentials
|
AUTHENTICATION_ERROR = "authentication_error" # 401 - need credentials
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ def extract_endpoint_ip(profile_id: str, profile_type: Optional[ProfileType] = N
|
||||||
|
|
||||||
profile_path = Constants.HV_PROFILE_CONFIG_HOME + f'/{profile_id}'
|
profile_path = Constants.HV_PROFILE_CONFIG_HOME + f'/{profile_id}'
|
||||||
|
|
||||||
if not profile_type: # assumes systemwide:
|
if not profile_type or profile_type == ProfileType.SYSTEM:
|
||||||
wg_conf_path = f'{profile_path}/wg.conf.bak'
|
wg_conf_path = f'{profile_path}/wg.conf.bak'
|
||||||
elif profile_type == ProfileType.SESSION:
|
elif profile_type == ProfileType.SESSION:
|
||||||
wg_conf_path = f'{profile_path}/wg.conf'
|
wg_conf_path = f'{profile_path}/wg.conf'
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
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.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.systemwide.systemwide_wireguard import establish_system_connection, terminate_system_connection
|
from core.services.networking.systemwide.systemwide_wireguard import establish_system_connection, terminate_system_connection
|
||||||
from core.services.keys_and_verifications.wireguard_keys import register_wireguard_session
|
from core.services.keys_and_verifications.wireguard_keys import register_wireguard_session
|
||||||
|
|
@ -197,6 +199,9 @@ def _ensure_wireguard_configured(
|
||||||
logger.info(f"[CONNECTION] Profile does NOT have WG keys and it needs it. Registering a NEW wg session..")
|
logger.info(f"[CONNECTION] Profile does NOT have WG keys and it needs it. Registering a NEW wg session..")
|
||||||
registry_result = register_wireguard_session(profile, connection_observer=connection_observer)
|
registry_result = register_wireguard_session(profile, connection_observer=connection_observer)
|
||||||
|
|
||||||
|
logger.info(f"registry_result is {registry_result}")
|
||||||
|
if not registry_result.valid:
|
||||||
|
return registry_result
|
||||||
|
|
||||||
def _establish_with_renegotiation(
|
def _establish_with_renegotiation(
|
||||||
profile: Union[SessionProfile, SystemProfile],
|
profile: Union[SessionProfile, SystemProfile],
|
||||||
|
|
@ -209,7 +214,8 @@ def _establish_with_renegotiation(
|
||||||
try:
|
try:
|
||||||
logger.info(f"[CONNECTION] Connecting..")
|
logger.info(f"[CONNECTION] Connecting..")
|
||||||
return establish_fn(profile, ignore=ignore, connection_observer=connection_observer)
|
return establish_fn(profile, ignore=ignore, connection_observer=connection_observer)
|
||||||
except ConnectionError:
|
except ConnectionError as e:
|
||||||
|
logger.error(str(e))
|
||||||
if __should_renegotiate(profile):
|
if __should_renegotiate(profile):
|
||||||
logger.info(f"[CONNECTION] Renegotiating a new wg key session with API..")
|
logger.info(f"[CONNECTION] Renegotiating a new wg key session with API..")
|
||||||
|
|
||||||
|
|
@ -245,7 +251,6 @@ def __should_renegotiate(profile: Union[SessionProfile, SystemProfile]):
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Enums
|
# Enums
|
||||||
# if profile.type == ProfileType.SYSTEM:
|
# if profile.type == ProfileType.SYSTEM:
|
||||||
# print("This is a system profile")
|
# print("This is a system profile")
|
||||||
|
|
|
||||||
|
|
@ -15,10 +15,11 @@ import httpx
|
||||||
from httpx_socks import AsyncProxyTransport
|
from httpx_socks import AsyncProxyTransport
|
||||||
import httpx_socks
|
import httpx_socks
|
||||||
from httpx_socks import ProxyType
|
from httpx_socks import ProxyType
|
||||||
|
import subprocess
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
API_ENDPOINT_FORMATS = {
|
API_ENDPOINT_FORMATS = {
|
||||||
|
f"{Constants.SP_BASE_URL}/v2/get_ip": 'ip',
|
||||||
"https://api.ipify.org?format=json": "ip",
|
"https://api.ipify.org?format=json": "ip",
|
||||||
"https://api.myip.com": "ip",
|
"https://api.myip.com": "ip",
|
||||||
"https://wtfismyip.com/json": "YourFuckingIPAddress",
|
"https://wtfismyip.com/json": "YourFuckingIPAddress",
|
||||||
|
|
@ -106,3 +107,24 @@ def extract_ip_from_response(response: dict, api_url: str) -> str | bool:
|
||||||
final_value = response[desired_key]
|
final_value = response[desired_key]
|
||||||
return final_value
|
return final_value
|
||||||
|
|
||||||
|
|
||||||
|
def ping_ip(ip: str) -> Result:
|
||||||
|
command = ["ping", "-c", "1", "-W", "2", ip]
|
||||||
|
human_readable_goal = f"Pinging {ip}"
|
||||||
|
return run_generic_command(command, human_readable_goal, timeout=5)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def ping_ip_worked(ip: str) -> bool:
|
||||||
|
try:
|
||||||
|
# -c 1: send only 1 packet
|
||||||
|
# -W 2: wait 2 seconds for a response
|
||||||
|
output = subprocess.run(
|
||||||
|
["ping", "-c", "1", "-W", "2", ip],
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
text=True
|
||||||
|
)
|
||||||
|
return output.returncode == 0
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
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.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
|
||||||
|
from core.models.BaseProfile import BaseProfile as Profile
|
||||||
|
from core.models.BaseProfile import ProfileType
|
||||||
|
from core.observers.ConnectionObserver import ConnectionObserver
|
||||||
|
from core.Constants import Constants
|
||||||
|
from core.errors.logger import logger
|
||||||
|
from core.models.Result import Result, ResultError
|
||||||
|
from core.utils.type_checker import type_checker
|
||||||
|
from core.models.enum_models import ProtocolChoice
|
||||||
|
|
||||||
|
|
||||||
|
@type_checker
|
||||||
|
def get_profile_ip(profile_id: int, protocol: ProtocolChoice, profile_type: ProfileType) -> str|None|bool:
|
||||||
|
if protocol == ProtocolChoice.WIREGUARD:
|
||||||
|
print("getting wireguard IP")
|
||||||
|
return extract_endpoint_ip(profile_id=profile_id, profile_type=profile_type)
|
||||||
|
|
||||||
|
elif protocol in (ProtocolChoice.VLESS, ProtocolChoice.HYSTERIA2) and profile_type == ProfileType.SYSTEM:
|
||||||
|
return get_singbox_ip(profile_id=profile_id)
|
||||||
|
else:
|
||||||
|
logger.error("Singbox profiles not yet offered for session profiles")
|
||||||
|
return None
|
||||||
|
|
||||||
|
@type_checker
|
||||||
|
def check_if_location_is_up(profile: Profile, connection_observer: ConnectionObserver) -> Result:
|
||||||
|
|
||||||
|
profile_id = profile.id
|
||||||
|
location_id = profile.location.id
|
||||||
|
location_name = profile.location.country_name
|
||||||
|
profile_type = profile.type
|
||||||
|
string_protocol = profile.connection.code
|
||||||
|
protocol = ProtocolChoice(string_protocol)
|
||||||
|
|
||||||
|
print(f"""
|
||||||
|
profile_id: {profile_id}
|
||||||
|
location_id: {location_id}
|
||||||
|
location_name: {location_name}
|
||||||
|
profile_type: {profile_type}
|
||||||
|
protocol: {protocol}
|
||||||
|
""")
|
||||||
|
|
||||||
|
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)
|
||||||
|
print(f"Can {location_name} be pinged? {can_be_pinged}")
|
||||||
|
else:
|
||||||
|
logger.error("Skipping IP Address Test.")
|
||||||
|
can_be_pinged = False
|
||||||
|
|
||||||
|
if not can_be_pinged:
|
||||||
|
do_we_have_internet = test_internet()
|
||||||
|
print(f"Do you have the internet? {do_we_have_internet}")
|
||||||
|
|
||||||
|
if not do_we_have_internet:
|
||||||
|
error_msg = "Basic Internet Connection Issues"
|
||||||
|
return Result(valid=False, backoff_strategy=BackoffStrategy.NO_RETRY, error_type=ResultError.NO_INTERNET, message=error_msg)
|
||||||
|
|
||||||
|
# Now check vs official API:
|
||||||
|
url = f'{Constants.SP_BASE_URL}/v2/check_location'
|
||||||
|
payload = {
|
||||||
|
'location_id': str(location_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
result = api_call_and_extraction(
|
||||||
|
desired_type="boolean_result",
|
||||||
|
method=ApiCall.POST,
|
||||||
|
url=url,
|
||||||
|
connection_observer=connection_observer,
|
||||||
|
payload=payload
|
||||||
|
)
|
||||||
|
|
||||||
|
if not result.valid and not can_be_pinged:
|
||||||
|
error_msg = "Can't ping the VPN node or even reach the API. Your connection is unstable."
|
||||||
|
return Result(valid=False, backoff_strategy=BackoffStrategy.NO_RETRY, error_type=ResultError.NO_INTERNET, message=error_msg)
|
||||||
|
|
||||||
|
location_up = result.data
|
||||||
|
|
||||||
|
if location_up:
|
||||||
|
return Result(valid=True, backoff_strategy=BackoffStrategy.RETRY_IMMEDIATE)
|
||||||
|
else:
|
||||||
|
error_msg = f"The {location_name} node is currently down."
|
||||||
|
return Result(valid=False, backoff_strategy=BackoffStrategy.NO_RETRY, error_type=ResultError.NODE_ERROR, message=error_msg)
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
from core.errors.logger import logger
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
import asyncio
|
||||||
|
import httpx_socks
|
||||||
|
from httpx_socks import ProxyType
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
DEFAULT_URLS = [
|
||||||
|
"https://www.duckduckgo.com",
|
||||||
|
"https://www.github.com",
|
||||||
|
"https://www.wikipedia.org",
|
||||||
|
"https://www.grapheneos.org"
|
||||||
|
]
|
||||||
|
|
||||||
|
async def _async_test_internet(urls, port: Optional[int] = None):
|
||||||
|
# Configure transport if a port is provided for the SOCKS5 proxy
|
||||||
|
transport = None
|
||||||
|
if port:
|
||||||
|
transport = httpx_socks.AsyncProxyTransport(
|
||||||
|
proxy_type=ProxyType.SOCKS5,
|
||||||
|
proxy_host="127.0.0.1",
|
||||||
|
proxy_port=port,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Use the transport in the AsyncClient if it was created
|
||||||
|
async with httpx.AsyncClient(transport=transport, http2=True, timeout=6) as client:
|
||||||
|
tasks = [asyncio.create_task(client.get(url)) for url in urls]
|
||||||
|
|
||||||
|
done, pending = await asyncio.wait(
|
||||||
|
tasks,
|
||||||
|
return_when=asyncio.FIRST_COMPLETED,
|
||||||
|
timeout=6
|
||||||
|
)
|
||||||
|
|
||||||
|
for task in pending:
|
||||||
|
task.cancel()
|
||||||
|
|
||||||
|
for task in done:
|
||||||
|
try:
|
||||||
|
response = task.result()
|
||||||
|
logger.info(f"✓ Got response: {response.status_code}")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"✗ Failed: {e}")
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def test_internet(port: Optional[int] = None):
|
||||||
|
return asyncio.run(_async_test_internet(DEFAULT_URLS, port))
|
||||||
|
|
@ -26,6 +26,7 @@ def classify_response(response) -> ApiResponse:
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(f"Classifying HTTP response: {response.status_code}")
|
logger.info(f"Classifying HTTP response: {response.status_code}")
|
||||||
|
logger.info(f"payload: {response.json()}")
|
||||||
|
|
||||||
# Success case (2xx)
|
# Success case (2xx)
|
||||||
if 200 <= response.status_code < 300:
|
if 200 <= response.status_code < 300:
|
||||||
|
|
@ -49,13 +50,22 @@ def classify_response(response) -> ApiResponse:
|
||||||
if 400 <= response.status_code < 500:
|
if 400 <= response.status_code < 500:
|
||||||
try:
|
try:
|
||||||
resp_json = response.json()
|
resp_json = response.json()
|
||||||
|
if "valid" in resp_json and "error_type" in resp_json:
|
||||||
|
return ApiResponse(
|
||||||
|
valid=True,
|
||||||
|
error_type=ErrorType.APPLICATION_ERROR,
|
||||||
|
data=resp_json,
|
||||||
|
message="Server replied with an error.",
|
||||||
|
backoff_strategy=BackoffStrategy.NO_RETRY
|
||||||
|
)
|
||||||
|
except ValueError:
|
||||||
|
error_msg = response.text
|
||||||
|
|
||||||
error_msg = (
|
error_msg = (
|
||||||
resp_json.get("message")
|
resp_json.get("message")
|
||||||
or resp_json.get("error")
|
or resp_json.get("error")
|
||||||
or resp_json.get("error_code")
|
or resp_json.get("error_code")
|
||||||
)
|
)
|
||||||
except ValueError:
|
|
||||||
error_msg = response.text
|
|
||||||
|
|
||||||
# Map status codes to semantic error types and retry strategies
|
# Map status codes to semantic error types and retry strategies
|
||||||
error_map = {
|
error_map = {
|
||||||
|
|
|
||||||
|
|
@ -350,6 +350,8 @@ def start_singbox(
|
||||||
# raises error if not okay.
|
# raises error if not okay.
|
||||||
|
|
||||||
Notifications.emit(EventType.SYSTEM_ENABLE, profile_id)
|
Notifications.emit(EventType.SYSTEM_ENABLE, profile_id)
|
||||||
|
notification = "Connected."
|
||||||
|
Notifications.emit(EventType.STATUS_UPDATE, notification)
|
||||||
# ============= CONCLUSION =============
|
# ============= CONCLUSION =============
|
||||||
return Result(valid=True, data=process_id)
|
return Result(valid=True, data=process_id)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
|
from core.services.keys_and_verifications.wireguard_keys import get_mtu, insert_mtu_into_config_on_disk
|
||||||
|
from core.services.networking.systemwide.wireguard.mtu_tools import loop_through_testing_mtu, mtu_is_fine, MIN_MTU, MAX_MTU
|
||||||
|
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
|
from core.models.enum_models import ProtocolChoice, SINGBOX_PROTOCOLS
|
||||||
from core.controllers.profile_state.get_profile_data import get_protocol_enum
|
from core.controllers.profile_state.get_profile_data import get_protocol_enum
|
||||||
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 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
|
from core.services.networking.systemwide.systemwide_utils import extract_wg_interface_name, check_system_prereqs
|
||||||
|
|
@ -11,30 +15,31 @@ from core.services.networking.systemwide.general_tools.test_any_protocol import
|
||||||
# from core.services.networking.general_connection_tools.config_tools import extract_endpoint_ip, extract_dns
|
# 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
|
||||||
from core.services.networking.systemwide.dns_tools.process_dns_result import orchestrate_dns_check
|
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.services.networking.systemwide.dns_tools.SearchResult import SearchResult, SearchError
|
||||||
from core.services.networking.systemwide.wireguard.nmcli_tools import setup_nmcli_connection, setup_ipv6_sinkhole, manual_nmcli_enable
|
from core.services.networking.systemwide.wireguard.nmcli_tools import setup_nmcli_connection, setup_ipv6_sinkhole, manual_nmcli_enable
|
||||||
from core.services.networking.systemwide.wireguard.wg_firewall_dns import set_dns, revert_dns, turn_on_firewall, check_and_kill_firewall, enable_firewall_with_retry
|
from core.services.networking.systemwide.wireguard.wg_firewall_dns import set_dns, revert_dns, check_and_kill_firewall, enable_firewall_with_retry
|
||||||
from core.errors.logger import logger
|
from core.errors.logger import logger
|
||||||
from core.errors.exceptions import FirewallError
|
# from core.errors.exceptions import FirewallError
|
||||||
from core.utils.run_commands import run_generic_command
|
from core.utils.run_commands import run_generic_command
|
||||||
from core.observers.TicketObserver import TicketObserver
|
from core.observers.TicketObserver import TicketObserver
|
||||||
|
|
||||||
from core.Constants import Constants
|
# from core.Constants import Constants
|
||||||
from core.Errors import ConnectionTerminationError, CommandNotFoundError
|
from core.Errors import ConnectionTerminationError, CommandNotFoundError
|
||||||
from core.controllers.ConfigurationController import ConfigurationController
|
from core.controllers.ConfigurationController import ConfigurationController
|
||||||
from core.controllers.SystemStateController import SystemStateController
|
from core.controllers.SystemStateController import SystemStateController
|
||||||
from core.models.BaseProfile import ProfileType
|
# from core.models.BaseProfile import ProfileType
|
||||||
from core.models.session.SessionProfile import SessionProfile
|
# from core.models.session.SessionProfile import SessionProfile
|
||||||
from core.models.system.SystemProfile import SystemProfile
|
from core.models.system.SystemProfile import SystemProfile
|
||||||
from core.models.system.SystemState import SystemState
|
from core.models.system.SystemState import SystemState
|
||||||
from core.observers.ConnectionObserver import ConnectionObserver
|
from core.observers.ConnectionObserver import ConnectionObserver
|
||||||
from subprocess import CalledProcessError
|
from subprocess import CalledProcessError
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
import os
|
# import os
|
||||||
import re
|
# import re
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
# import subprocess
|
||||||
import time
|
import time
|
||||||
|
import asyncio
|
||||||
|
|
||||||
def terminate_system_connection(
|
def terminate_system_connection(
|
||||||
firewall_setting: Optional[bool] = None,
|
firewall_setting: Optional[bool] = None,
|
||||||
|
|
@ -53,12 +58,12 @@ def terminate_system_connection(
|
||||||
|
|
||||||
# ====== FIREWALL =======
|
# ====== FIREWALL =======
|
||||||
if firewall_setting:
|
if firewall_setting:
|
||||||
disable_result_object = killswitch.disarm() # raw blind kill
|
_ = killswitch.disarm() # raw blind kill
|
||||||
|
|
||||||
# ====== DNS =======
|
# ====== DNS =======
|
||||||
if dns_setting:
|
if dns_setting:
|
||||||
reverting_dns_worked = revert_dns()
|
_ = revert_dns()
|
||||||
|
|
||||||
# ====== NMCLI =======
|
# ====== NMCLI =======
|
||||||
if SystemStateController.exists():
|
if SystemStateController.exists():
|
||||||
|
|
||||||
|
|
@ -98,6 +103,54 @@ def terminate_system_connection(
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def evaluate_location_and_internet(profile: SystemProfile, connection_observer: ConnectionObserver) -> Result:
|
||||||
|
"""Checks if location is up and returns the backoff strategy."""
|
||||||
|
notification = "Checking If Location is Up.."
|
||||||
|
Notifications.emit(EventType.STATUS_UPDATE, notification)
|
||||||
|
logger.info(notification)
|
||||||
|
|
||||||
|
location_result = check_if_location_is_up(profile=profile, connection_observer=connection_observer)
|
||||||
|
|
||||||
|
if location_result.backoff_strategy == BackoffStrategy.RETRY_IMMEDIATE:
|
||||||
|
Notifications.emit(EventType.STATUS_UPDATE, "Retrying after Location Check")
|
||||||
|
|
||||||
|
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 _finish_function(final_result: Result) -> Result:
|
||||||
|
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(
|
def __establish_system_connection(
|
||||||
profile: SystemProfile,
|
profile: SystemProfile,
|
||||||
firewall_setting: bool = False,
|
firewall_setting: bool = False,
|
||||||
|
|
@ -118,12 +171,16 @@ def __establish_system_connection(
|
||||||
8) Testing, confirming, displaying to the end user.
|
8) Testing, confirming, displaying to the end user.
|
||||||
"""
|
"""
|
||||||
check_system_prereqs()
|
check_system_prereqs()
|
||||||
logger.info("[CONNECT] Terming existing connections..")
|
notification = "Terming existing connections.."
|
||||||
|
Notifications.emit(EventType.STATUS_UPDATE, notification)
|
||||||
|
logger.info(notification)
|
||||||
terminate_system_connection(firewall_setting, dns_setting)
|
terminate_system_connection(firewall_setting, dns_setting)
|
||||||
|
|
||||||
# ============= INITIAL NMCLI CONNECTION =============
|
# ============= INITIAL NMCLI CONNECTION =============
|
||||||
if connection_observer:
|
notification = "Setting Up WireGuard.."
|
||||||
connection_observer.notify("connecting", "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()
|
wg_config_path = profile.get_wireguard_configuration_path()
|
||||||
nmcli_result = setup_nmcli_connection(wg_config_path)
|
nmcli_result = setup_nmcli_connection(wg_config_path)
|
||||||
|
|
||||||
|
|
@ -147,7 +204,7 @@ def __establish_system_connection(
|
||||||
# ============= 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.
|
||||||
|
|
||||||
logger.info(f"Setting State JSON..")
|
logger.info("Setting State JSON..")
|
||||||
current_state = SystemStateController.create(
|
current_state = SystemStateController.create(
|
||||||
profile_id=profile.id,
|
profile_id=profile.id,
|
||||||
firewalled=firewall_setting, # intended setting, not result yet
|
firewalled=firewall_setting, # intended setting, not result yet
|
||||||
|
|
@ -200,15 +257,19 @@ def __establish_system_connection(
|
||||||
|
|
||||||
# ============= TESTING =============
|
# ============= TESTING =============
|
||||||
logger.info("If we made it this far, we have nmcli on, ipv6 sinkhole on, firewall on.")
|
logger.info("If we made it this far, we have nmcli on, ipv6 sinkhole on, firewall on.")
|
||||||
if connection_observer:
|
# if connection_observer:
|
||||||
connection_observer.notify("connecting", "Testing Connection Health..")
|
# connection_observer.notify("connecting", "Testing Connection Health..")
|
||||||
|
|
||||||
|
notification = "Testing IP address.."
|
||||||
|
Notifications.emit(EventType.STATUS_UPDATE, notification)
|
||||||
test_result = test_ip_systemwide_for_any_protocol(
|
test_result = test_ip_systemwide_for_any_protocol(
|
||||||
profile_id=profile.id,
|
profile_id=profile.id,
|
||||||
protocol=ProtocolChoice.WIREGUARD,
|
protocol=ProtocolChoice.WIREGUARD,
|
||||||
connection_observer=connection_observer
|
connection_observer=connection_observer
|
||||||
)
|
)
|
||||||
if not test_result.valid:
|
if not test_result.valid:
|
||||||
|
notification = "Test Failed."
|
||||||
|
Notifications.emit(EventType.STATUS_UPDATE, notification)
|
||||||
return test_result
|
return test_result
|
||||||
|
|
||||||
# ============= UPDATE STATE =============
|
# ============= UPDATE STATE =============
|
||||||
|
|
@ -221,65 +282,11 @@ def __establish_system_connection(
|
||||||
connection_observer.notify("connected", "Confirmed")
|
connection_observer.notify("connected", "Confirmed")
|
||||||
|
|
||||||
Notifications.emit(EventType.SYSTEM_ENABLE, profile.id)
|
Notifications.emit(EventType.SYSTEM_ENABLE, profile.id)
|
||||||
|
notification = "Connected."
|
||||||
|
Notifications.emit(EventType.STATUS_UPDATE, notification)
|
||||||
return Result(valid=True, data=current_state)
|
return Result(valid=True, data=current_state)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def evaluate_connection_result(connection_result: Result) -> Result:
|
|
||||||
"""In the future, this will raise other error types or filter replies."""
|
|
||||||
if not connection_result.valid:
|
|
||||||
logger.error(f"[SYSTEMWIDE WG] Critical issue, could not establish a connection: {connection_result.message}")
|
|
||||||
time.sleep(1)
|
|
||||||
raise ConnectionError(f"Connection validation failed: {connection_result.message}")
|
|
||||||
else:
|
|
||||||
return connection_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 _establish_connection_with_retry(
|
|
||||||
profile: SystemProfile,
|
|
||||||
firewall_setting: bool,
|
|
||||||
dns_setting: bool,
|
|
||||||
connection_observer: Optional[ConnectionObserver],
|
|
||||||
max_retries: int = 2
|
|
||||||
) -> Result:
|
|
||||||
"""Establish connection with retry logic for transient errors."""
|
|
||||||
last_error = None
|
|
||||||
|
|
||||||
for attempt in range(1, max_retries):
|
|
||||||
if connection_observer:
|
|
||||||
connection_observer.notify("connecting", f"Connecting attempt {attempt} of {max_retries}.. ")
|
|
||||||
try:
|
|
||||||
connection_result = __establish_system_connection(
|
|
||||||
profile=profile,
|
|
||||||
firewall_setting=firewall_setting,
|
|
||||||
dns_setting=dns_setting,
|
|
||||||
connection_observer=connection_observer
|
|
||||||
)
|
|
||||||
evaluate_connection_result(connection_result)
|
|
||||||
return connection_result
|
|
||||||
|
|
||||||
except ConnectionError as e:
|
|
||||||
# Unrecoverable error, fail immediately
|
|
||||||
logger.error(str(e))
|
|
||||||
raise
|
|
||||||
|
|
||||||
except CalledProcessError as e:
|
|
||||||
# Potentially transient, retry once
|
|
||||||
last_error = e
|
|
||||||
logger.warning(f"Connection attempt {attempt}/{max_retries} failed with CalledProcessError: {e}")
|
|
||||||
if attempt == max_retries:
|
|
||||||
raise ConnectionError('The connection could not be established.') from e
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def establish_system_connection(
|
def establish_system_connection(
|
||||||
profile: SystemProfile,
|
profile: SystemProfile,
|
||||||
ignore: tuple[type[Exception]] = (),
|
ignore: tuple[type[Exception]] = (),
|
||||||
|
|
@ -287,13 +294,13 @@ def establish_system_connection(
|
||||||
ticket_observer: Optional[TicketObserver] = None # Passed in only for assassin's link to tickets in ConnectionController.establish_session_connection
|
ticket_observer: Optional[TicketObserver] = None # Passed in only for assassin's link to tickets in ConnectionController.establish_session_connection
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Purpose:
|
Establish connection with a maximum of 2 retries.
|
||||||
Calls the private function with the full flow, so it can be retried on failure.
|
|
||||||
|
|
||||||
Steps)
|
Steps)
|
||||||
1) Verify Endpoint's encryption
|
1) Verify Endpoint's encryption
|
||||||
2) Pass to private function
|
2) get settings
|
||||||
3) Retry on failure
|
3) Attempt 1 -> (Fail) -> Location Check
|
||||||
|
4) Attempt 2 -> (Fail) -> MTU Check -> Attempt 3
|
||||||
"""
|
"""
|
||||||
# ================= VERIFY ENDPOINT =================
|
# ================= VERIFY ENDPOINT =================
|
||||||
if connection_observer:
|
if connection_observer:
|
||||||
|
|
@ -306,17 +313,88 @@ def establish_system_connection(
|
||||||
dns_setting = ConfigurationController.get_dns_setting()
|
dns_setting = ConfigurationController.get_dns_setting()
|
||||||
|
|
||||||
# ================= CONNECT WG ================
|
# ================= CONNECT WG ================
|
||||||
try:
|
# We allow 3 total attempts (Initial + 2 retries)
|
||||||
final_result = _establish_connection_with_retry(
|
for attempt in range(1, 4):
|
||||||
profile=profile,
|
try:
|
||||||
firewall_setting=firewall_setting,
|
if connection_observer:
|
||||||
dns_setting=dns_setting,
|
connection_observer.notify("connecting", f"Connecting attempt {attempt} of 3.. ")
|
||||||
connection_observer=connection_observer
|
|
||||||
)
|
|
||||||
except (ConnectionError, CalledProcessError):
|
|
||||||
_cleanup_on_error(firewall_setting, dns_setting)
|
|
||||||
raise
|
|
||||||
|
|
||||||
terminate_tor_connection()
|
# Attempt the actual system connection
|
||||||
time.sleep(0.3)
|
connection_result = __establish_system_connection(
|
||||||
return final_result
|
profile=profile,
|
||||||
|
firewall_setting=firewall_setting,
|
||||||
|
dns_setting=dns_setting,
|
||||||
|
connection_observer=connection_observer
|
||||||
|
)
|
||||||
|
|
||||||
|
if connection_result.valid:
|
||||||
|
return _finish_function(connection_result)
|
||||||
|
|
||||||
|
# If not valid, we treat it as a failure to trigger the except block logic
|
||||||
|
raise ConnectionError("Connection result invalid")
|
||||||
|
|
||||||
|
except (ConnectionError, CalledProcessError) as e:
|
||||||
|
_cleanup_on_error(firewall_setting, dns_setting)
|
||||||
|
|
||||||
|
if attempt == 1:
|
||||||
|
# First failure: Evaluate Location and Internet without connection,
|
||||||
|
terminate_system_connection(firewall_setting, dns_setting)
|
||||||
|
location_result = evaluate_location_and_internet(
|
||||||
|
profile=profile,
|
||||||
|
connection_observer=connection_observer
|
||||||
|
)
|
||||||
|
|
||||||
|
if location_result.backoff_strategy == BackoffStrategy.NO_RETRY:
|
||||||
|
logger.error(f"Location check failed: {location_result.message}")
|
||||||
|
bottom_left = "Node Down"
|
||||||
|
Notifications.emit(EventType.STATUS_UPDATE, bottom_left)
|
||||||
|
notification = location_result.message
|
||||||
|
Notifications.emit(EventType.SHOW_POPUP, notification)
|
||||||
|
return location_result
|
||||||
|
# raise ConnectionError(location_result.message) from e
|
||||||
|
|
||||||
|
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
|
||||||
|
current_mtu = get_mtu(profile_id=profile.id)
|
||||||
|
|
||||||
|
if current_mtu != MAX_MTU or not current_mtu:
|
||||||
|
logger.info("MTU is already at the max, no point to change it.")
|
||||||
|
raise ConnectionError("Failed to connect.")
|
||||||
|
# return Result(valid=False, error_type=ResultError.CONNECTION, message="Failed to connect.")
|
||||||
|
|
||||||
|
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
|
||||||
|
raise ConnectionError("Failed to connect.")
|
||||||
|
# return Result(valid=False, error_type=ResultError.CONNECTION, message="Failed to connect.")
|
||||||
|
|
||||||
|
if changed_mtu(profile=profile, current_mtu=current_mtu):
|
||||||
|
logger.info("MTU changed. Retrying connection...")
|
||||||
|
continue # Proceed to attempt 3
|
||||||
|
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)
|
||||||
|
else:
|
||||||
|
logger.info("Skipping MTU testing, it's not relevant here.")
|
||||||
|
|
||||||
|
# If MTU wasn't changed or wasn't applicable, don't bother with attempt 3
|
||||||
|
raise ConnectionError("Failed to connect.")
|
||||||
|
# return Result(valid=False, error_type=ResultError.CONNECTION, message="Failed to connect.")
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Attempt 3 failed
|
||||||
|
logger.error("Maximum retries reached. Connection failed.")
|
||||||
|
raise ConnectionError("Failed to connect.")
|
||||||
|
# return Result(valid=False, error_type=ResultError.CONNECTION, message="Failed to connect.")
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,21 @@
|
||||||
|
from core.observers.Notifications import Notifications, EventType
|
||||||
from core.utils.run_commands import run_generic_command
|
from core.utils.run_commands import run_generic_command
|
||||||
from core.models.Result import Result, ResultError
|
from core.models.Result import Result, ResultError
|
||||||
from core.errors.logger import logger
|
from core.errors.logger import logger
|
||||||
|
|
||||||
MAX_MTU = 1500
|
from typing import Optional
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
MAX_MTU = 1420
|
||||||
MIN_MTU = 1280
|
MIN_MTU = 1280
|
||||||
# Use integer division for the middle point
|
# Use integer division for the middle point
|
||||||
MIDDLE_MTU = MIN_MTU + ((MAX_MTU - MIN_MTU) // 2)
|
MIDDLE_MTU = MIN_MTU + ((MAX_MTU - MIN_MTU) // 2)
|
||||||
|
|
||||||
# Fallback list for reliability
|
# Fallback list for reliability
|
||||||
URLS = ["217.156.66.35", "185.165.169.91", "9.9.9.9"]
|
URLS = ["9.9.9.9", "217.156.66.35", "185.165.169.91"]
|
||||||
|
|
||||||
def test_an_mtu(mtu_value: int, url: str, timeout: int) -> Result:
|
|
||||||
|
def test_mtu_without_async(mtu_value: int, url: str, timeout: int) -> Result:
|
||||||
# Payload size = MTU - 28 bytes (IP + ICMP headers)
|
# Payload size = MTU - 28 bytes (IP + ICMP headers)
|
||||||
payload_size = mtu_value - 28
|
payload_size = mtu_value - 28
|
||||||
if payload_size < 0:
|
if payload_size < 0:
|
||||||
|
|
@ -20,67 +25,108 @@ def test_an_mtu(mtu_value: int, url: str, timeout: int) -> Result:
|
||||||
human_readable_goal = f"Test the MTU value {mtu_value} against {url}."
|
human_readable_goal = f"Test the MTU value {mtu_value} against {url}."
|
||||||
return run_generic_command(command, human_readable_goal, timeout=timeout)
|
return run_generic_command(command, human_readable_goal, timeout=timeout)
|
||||||
|
|
||||||
def find_optimal_mtu(low: int, high: int, url: str) -> int:
|
|
||||||
"""
|
def mtu_is_fine(current_mtu: Optional[int] = None) -> bool:
|
||||||
Performs a binary search between a known working MTU (low)
|
if current_mtu:
|
||||||
and a known failing MTU (high).
|
testing_mtu = current_mtu
|
||||||
"""
|
else:
|
||||||
|
testing_mtu = MAX_MTU
|
||||||
|
|
||||||
|
notification = f"Testing MTU {testing_mtu}..."
|
||||||
|
Notifications.emit(EventType.STATUS_UPDATE, notification)
|
||||||
|
|
||||||
|
for url in URLS:
|
||||||
|
result = test_mtu_without_async(mtu_value=testing_mtu, url=url, timeout=6)
|
||||||
|
if result.valid:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
async def test_an_mtu(mtu_value: int, url: str, timeout: int) -> Result:
|
||||||
|
payload_size = mtu_value - 28
|
||||||
|
if payload_size < 0:
|
||||||
|
return Result(valid=False)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Use asyncio subprocess for non-blocking OS calls
|
||||||
|
proc = await asyncio.create_subprocess_exec(
|
||||||
|
"ping", "-M", "do", "-s", str(payload_size), "-c", "1", url,
|
||||||
|
stdout=asyncio.subprocess.DEVNULL,
|
||||||
|
stderr=asyncio.subprocess.DEVNULL
|
||||||
|
)
|
||||||
|
|
||||||
|
# Wait for completion or timeout
|
||||||
|
await asyncio.wait_for(proc.wait(), timeout=timeout)
|
||||||
|
return Result(valid=(proc.returncode == 0))
|
||||||
|
|
||||||
|
except (asyncio.TimeoutError, Exception):
|
||||||
|
if 'proc' in locals():
|
||||||
|
try:
|
||||||
|
proc.kill()
|
||||||
|
except ProcessLookupError:
|
||||||
|
pass
|
||||||
|
return Result(valid=False)
|
||||||
|
|
||||||
|
async def find_optimal_mtu_binary_search(low: int, high: int, url: str) -> int:
|
||||||
optimal = low
|
optimal = low
|
||||||
current_timeout = 6
|
current_timeout = 6
|
||||||
|
|
||||||
while low <= high:
|
while low <= high:
|
||||||
mid = (low + high) // 2
|
mid = (low + high) // 2
|
||||||
# We increase timeout slightly as we try larger packets
|
notification = f"Binary searching: Testing MTU {mid} against {url}..."
|
||||||
# to avoid false negatives due to congestion/latency
|
Notifications.emit(EventType.STATUS_UPDATE, notification)
|
||||||
logger.info(f"Refining: Testing MTU {mid}...")
|
logger.info(notification)
|
||||||
result = test_an_mtu(mid, url, timeout=current_timeout)
|
|
||||||
|
result = await test_an_mtu(mid, url, timeout=current_timeout)
|
||||||
|
|
||||||
if result.valid:
|
if result.valid:
|
||||||
optimal = mid
|
optimal = mid
|
||||||
low = mid + 1
|
low = mid + 1
|
||||||
current_timeout += 1 # Increase timeout as we get closer to max
|
current_timeout += 1
|
||||||
else:
|
else:
|
||||||
high = mid - 1
|
high = mid - 1
|
||||||
|
|
||||||
return optimal
|
return optimal
|
||||||
|
|
||||||
def loop_through_testing_mtu():
|
async def loop_through_testing_mtu():
|
||||||
# The sequence to probe the range
|
|
||||||
INITIAL_TEST_LIST = [MAX_MTU, MIDDLE_MTU, MIN_MTU]
|
INITIAL_TEST_LIST = [MAX_MTU, MIDDLE_MTU, MIN_MTU]
|
||||||
|
|
||||||
for url in URLS:
|
for each_mtu in INITIAL_TEST_LIST:
|
||||||
logger.info(f"Testing connectivity using URL: {url}")
|
notification = f"Probing MTU {each_mtu} across all IPs..."
|
||||||
|
Notifications.emit(EventType.STATUS_UPDATE, notification)
|
||||||
|
logger.info(notification)
|
||||||
|
|
||||||
|
# Create tasks for all URLs for the current MTU value
|
||||||
|
async def wrapped_test(u):
|
||||||
|
res = await test_an_mtu(each_mtu, u, timeout=6)
|
||||||
|
return res, u
|
||||||
|
|
||||||
|
tasks = [asyncio.create_task(wrapped_test(url)) for url in URLS]
|
||||||
|
|
||||||
working_mtu = None
|
try:
|
||||||
failed_above = None
|
# Process results as they come in
|
||||||
|
for coro in asyncio.as_completed(tasks):
|
||||||
for each_mtu in INITIAL_TEST_LIST:
|
result, url = await coro
|
||||||
logger.info(f"Probing MTU {each_mtu}...")
|
if result.valid:
|
||||||
result = test_an_mtu(mtu_value=each_mtu, url=url, timeout=6)
|
# Scenario A: Maximum possible MTU worked immediately
|
||||||
|
if each_mtu == MAX_MTU:
|
||||||
if result.valid:
|
logger.info(f"MAX_MTU {MAX_MTU} worked on {url}!")
|
||||||
working_mtu = each_mtu
|
return MAX_MTU
|
||||||
# If MAX_MTU works immediately, we've found the ceiling
|
|
||||||
if each_mtu == MAX_MTU:
|
# Scenario B: We found a baseline, now binary search for the ceiling
|
||||||
return MAX_MTU
|
if each_mtu == MIDDLE_MTU:
|
||||||
break
|
failed_above = MAX_MTU
|
||||||
|
else: # MIN_MTU
|
||||||
if working_mtu:
|
failed_above = MIDDLE_MTU
|
||||||
# We found a baseline. Now we need to find the ceiling.
|
|
||||||
# The 'high' is the first value in our test list that failed.
|
logger.info(f"Baseline found at {each_mtu} on {url}. Searching for optimal...")
|
||||||
# If MIDDLE worked, high is MAX. If MIN worked, high is MIDDLE.
|
return await find_optimal_mtu_binary_search(each_mtu, failed_above, url)
|
||||||
if working_mtu == MIDDLE_MTU:
|
finally:
|
||||||
failed_above = MAX_MTU
|
# Kill all other pending pings for this MTU level immediately
|
||||||
elif working_mtu == MIN_MTU:
|
for t in tasks:
|
||||||
failed_above = MIDDLE_MTU
|
if not t.done():
|
||||||
else:
|
t.cancel()
|
||||||
failed_above = MAX_MTU + 1
|
|
||||||
|
|
||||||
logger.info(f"Baseline found at {working_mtu}. Searching for optimal...")
|
|
||||||
return find_optimal_mtu(working_mtu, failed_above, url)
|
|
||||||
|
|
||||||
# If we reach here, the MIN_MTU failed for this URL
|
|
||||||
logger.error(f"Could not reach {url} even with minimum MTU. Trying next URL.")
|
|
||||||
|
|
||||||
logger.critical("All URLs failed MTU tests. Check your WireGuard connection.")
|
logger.critical("All URLs failed MTU tests. Check your WireGuard connection.")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ from core.Constants import Constants
|
||||||
from core.models.Result import Result, ResultError
|
from core.models.Result import Result, ResultError
|
||||||
from core.errors.logger import logger
|
from core.errors.logger import logger
|
||||||
from core.errors.error_shortcuts import not_supported
|
from core.errors.error_shortcuts import not_supported
|
||||||
|
from core.observers.Notifications import Notifications, EventType
|
||||||
|
|
||||||
|
|
||||||
def _mark_profile_as_paid(profile_id: int):
|
def _mark_profile_as_paid(profile_id: int):
|
||||||
|
|
@ -62,6 +63,8 @@ def mark_as_paid() -> Result:
|
||||||
|
|
||||||
# finally:
|
# finally:
|
||||||
payment_cycle.wipe()
|
payment_cycle.wipe()
|
||||||
|
notification = "Paid!"
|
||||||
|
Notifications.emit(EventType.STATUS_UPDATE, notification)
|
||||||
return Result(valid=True)
|
return Result(valid=True)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
from core.observers.Notifications import Notifications, EventType
|
||||||
from core.controllers.ConnectionController import ConnectionController
|
from core.controllers.ConnectionController import ConnectionController
|
||||||
from core.controllers.ProfileController import ProfileController
|
from core.controllers.ProfileController import ProfileController
|
||||||
from core.services.networking.systemwide.systemwide_wireguard import terminate_system_connection
|
from core.services.networking.systemwide.systemwide_wireguard import terminate_system_connection
|
||||||
|
|
@ -152,21 +153,32 @@ def enable_profile(
|
||||||
if profile.is_system_profile():
|
if profile.is_system_profile():
|
||||||
try:
|
try:
|
||||||
connection_result = establish_connection(profile, ignore=ignore, connection_observer=connection_observer)
|
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 connection_result.valid:
|
if connection_result.valid:
|
||||||
if profile_observer is not None:
|
if profile_observer is not None:
|
||||||
profile_observer.notify('enabled', profile)
|
profile_observer.notify('enabled', profile)
|
||||||
|
notification = "Enabled."
|
||||||
|
Notifications.emit(EventType.STATUS_UPDATE, notification)
|
||||||
|
|
||||||
return Result(valid=True, data=profile.type)
|
return Result(valid=True, data=profile.type)
|
||||||
else:
|
else:
|
||||||
logger.error(f"Couldn't enable the profile: {connection_result.error_type}")
|
|
||||||
error_msg = connection_result.message
|
error_msg = connection_result.message
|
||||||
raise ProfileActivationError(error_msg)
|
logger.error(error_msg)
|
||||||
|
notification = "Connection Failed."
|
||||||
|
Notifications.emit(EventType.STATUS_UPDATE, notification)
|
||||||
|
return Result(valid=False, error_type=ResultError.UNKNOWN, message=error_msg)
|
||||||
|
# raise ProfileActivationError(error_msg)
|
||||||
except FirewallError:
|
except FirewallError:
|
||||||
raise
|
return Result(valid=False, error_type=ResultError.FIREWALL, message="Firewall Error.")
|
||||||
except ConnectionError:
|
except ConnectionError as e:
|
||||||
raise ProfileActivationError('The profile could not be enabled.')
|
return Result(valid=False, error_type=ResultError.CONNECTION, message=f"Connection Error. {str(e)}")
|
||||||
except ValueError:
|
except ValueError as e:
|
||||||
raise ProfileActivationError('The profile could not be enabled.')
|
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=f"Value Error. {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
def disable_profile(
|
def disable_profile(
|
||||||
|
|
|
||||||
|
|
@ -272,10 +272,6 @@ def return_pre_existing_invoice(billing_code: str, connection_observer: Connecti
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def get_ticket_invoice(connection_observer: ConnectionObserver) -> Result:
|
def get_ticket_invoice(connection_observer: ConnectionObserver) -> Result:
|
||||||
"""Evaluates if it needs a new ticket invoice, and then returns pre-existing"""
|
"""Evaluates if it needs a new ticket invoice, and then returns pre-existing"""
|
||||||
|
|
||||||
|
|
@ -443,20 +439,6 @@ def paying_for_tickets() -> bool:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
# def start_new_process_if_already(profile_id: int)
|
|
||||||
# # Is there currently a process?
|
|
||||||
# payment_cycle = OverallPaymentCycle()
|
|
||||||
|
|
||||||
# if not payment_cycle and not category:
|
|
||||||
# error_msg = "There is no current billing cycle AND no category provided."
|
|
||||||
# return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=error_msg)
|
|
||||||
|
|
||||||
# billing_data = payment_cycle.billing_data
|
|
||||||
|
|
||||||
# if not billing_data:
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def get_current_billing_data(category: Optional[PaymentCategory] = None, profile_id: Optional[int] = None) -> Result:
|
def get_current_billing_data(category: Optional[PaymentCategory] = None, profile_id: Optional[int] = None) -> Result:
|
||||||
"""
|
"""
|
||||||
Goal:
|
Goal:
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,9 @@
|
||||||
from core.Constants import Constants
|
from core.Constants import Constants
|
||||||
|
from core.errors.logger import logger
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
from typing import Optional
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
|
||||||
# gets a raw string, but unlike the other function, does not convert it to a JSON.
|
# gets a raw string, but unlike the other function, does not convert it to a JSON.
|
||||||
|
|
@ -23,3 +27,50 @@ def get_raw_string(which_ticket, name_of_file_and_folder):
|
||||||
return raw_string_content
|
return raw_string_content
|
||||||
except:
|
except:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _read_with_privileges(filepath: str) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Attempts to read a file using pkexec or sudo to bypass PermissionErrors.
|
||||||
|
"""
|
||||||
|
# pkexec is preferred for desktop environments (GUI prompt)
|
||||||
|
# sudo is the fallback for CLI environments
|
||||||
|
for cmd_prefix in ["pkexec", "sudo"]:
|
||||||
|
try:
|
||||||
|
logger.info(f"Attempting to read {filepath} using {cmd_prefix}...")
|
||||||
|
# Using a list instead of shell=True prevents shell injection vulnerabilities
|
||||||
|
result = subprocess.run(
|
||||||
|
[cmd_prefix, "cat", filepath],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=True
|
||||||
|
)
|
||||||
|
return result.stdout
|
||||||
|
except (subprocess.CalledProcessError, FileNotFoundError) as e:
|
||||||
|
logger.debug(f"{cmd_prefix} failed or is not installed: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_string(filepath: str) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Reads a text file. If a PermissionError occurs on Linux,
|
||||||
|
attempts to escalate privileges via pkexec or sudo.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with open(filepath, "r", encoding="utf-8") as f:
|
||||||
|
return f.read()
|
||||||
|
|
||||||
|
except PermissionError:
|
||||||
|
logger.warning(f"Permission denied for {filepath}. Attempting escalation...")
|
||||||
|
return _read_with_privileges(filepath)
|
||||||
|
except FileNotFoundError:
|
||||||
|
logger.error(f"File not found: {filepath}")
|
||||||
|
except IsADirectoryError:
|
||||||
|
logger.error(f"Path is a directory, not a file: {filepath}")
|
||||||
|
except UnicodeDecodeError as e:
|
||||||
|
logger.error(f"Encoding error reading file {filepath}: {e}")
|
||||||
|
except IOError as e:
|
||||||
|
logger.error(f"IO Error reading file {filepath}: {e}")
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue