Introduced MTU tools. MTU set for session configs automatically, for systemwide the testing is setup but not yet implemented. Also introduced server reply objects and a new v2 wg endpoint that doesn't require location codes.

This commit is contained in:
SimplifiedPrivacy 2026-10-11 20:18:52 -04:00
parent 10e46cd8ea
commit 9e4fd54eb6
6 changed files with 281 additions and 49 deletions

View file

@ -40,6 +40,7 @@ class ResultError(Enum):
WRONG_LOCATION = "wrong_location" WRONG_LOCATION = "wrong_location"
SERVER_SIDE = "server_side" SERVER_SIDE = "server_side"
UNPAID = "unpaid" UNPAID = "unpaid"
MTU = "mtu"
NAVIGATE_TO = "navigate_to" NAVIGATE_TO = "navigate_to"
UNKNOWN = "unknown" UNKNOWN = "unknown"

View file

@ -1,11 +1,19 @@
from core.Errors import InvalidSubscriptionError
from core.models.manage.validate_dataclass import validate_model from core.models.manage.validate_dataclass import validate_model
from core.models.pydantic_models.ServerReply import ServerReply, ServerError from core.models.pydantic_models.ServerReply import ServerReply, ServerError
from core.models.Result import Result, ResultError from core.models.Result import Result, ResultError
from core.services.networking.api_requests.ApiResponseModel import ApiResponse from core.services.networking.api_requests.ApiResponseModel import ApiResponse
from core.observers.ConnectionObserver import ConnectionObserver
from core.services.networking.httpx import connect
from core.errors.logger import logger from core.errors.logger import logger
from enum import Enum
from typing import Optional from typing import Optional
class ApiCall(Enum):
GET = "get"
POST = "post"
# Translate Server-side to client-side errors in THIS CONTEXT, # Translate Server-side to client-side errors in THIS CONTEXT,
TRANSLATION_DICT = { TRANSLATION_DICT = {
ServerError.WRONG_LOCATION: ResultError.WRONG_LOCATION, ServerError.WRONG_LOCATION: ResultError.WRONG_LOCATION,
@ -79,3 +87,81 @@ def invalid_server_format(error_msg: Optional[str] = None) -> Result:
error_msg = "The server gave an invalid response format." error_msg = "The server gave an invalid response format."
return Result(valid=False, error_type=ResultError.INVALID_API_REPLY, message=error_msg) return Result(valid=False, error_type=ResultError.INVALID_API_REPLY, message=error_msg)
def api_call_and_extraction(
desired_type: str,
method: ApiCall,
url: str,
connection_observer: ConnectionObserver,
payload: dict,
billing_code: Optional[str] = None
) -> Result:
"""Do an API call, extract the data, and if there's an error, then translate the server's reply to a Result Object"""
#-----------------------------
# SEND TO API
#-----------------------------
api_response = connect.single_endpoint(
method=method.value,
url=url,
observer=connection_observer,
payload=payload,
billing_code=billing_code
)
if not api_response:
error_msg = "Complete failure of the function to produce a valid api response"
logger.error(error_msg)
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=error_msg)
#-----------------------------
# EVALUATE CONNECTION FAILURE
#-----------------------------
# Layer 1: API failure:
if not api_response.valid:
# legacy system
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}")
error_msg = f"Connection/500s Error prevented the server from replying: {api_response.error_type} {api_response.message}"
logger.error(error_msg)
return Result(valid=False, error_type=ResultError.CONNECTION, message=error_msg)
#-----------------------------
# EVALUATE SERVER-SIDE ISSUES (assuming it replied)
#-----------------------------
# Layer 2: Validity of Server's Reply:
raw_reply = api_response.data
logger.info("Validating the Validity of the Server's Reply..")
worked, server_reply = validate_model(ServerReply, raw_reply)
error_msg = f"Invalid Structure for ServerReply! The raw data is {raw_reply}"
if not worked:
logger.error(error_msg)
return invalid_server_format()
if not server_reply:
logger.error(error_msg)
return invalid_server_format()
if not server_reply.valid:
error_message = f"Server reply was correctly structured, but the actual invoice failed to be created. The raw data is {raw_reply}"
logger.error(error_message)
# translate by enum error code:
return translate_server_failure(server_reply)
if not hasattr(server_reply, desired_type):
error_msg = "Developer Error: You're asking for a data type that isn't in the object!"
logger.error(error_msg)
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=error_msg)
data = getattr(server_reply, desired_type)
if data:
return Result(valid=True, data=data)
else:
return Result(valid=False, error_type=ResultError.MISSING_DATA, message="Server Replied it was valid, but failed to provide the data.")

View file

@ -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
conf: str = None
payment_status: Optional[bool] = None payment_status: Optional[bool] = None

View file

@ -1,23 +1,31 @@
from core.services.networking.httpx import connect
from core.services.networking.api_requests.ApiResponseModel import ErrorType from core.models.manage.server_reply_tools import api_call_and_extraction, ApiCall
from core.models.session.SessionProfile import SessionProfile # from core.services.networking.httpx import connect
from core.models.system.SystemProfile import SystemProfile #from core.services.networking.api_requests.ApiResponseModel import ErrorType
from core.Errors import MissingSubscriptionError, MissingLocationError, InvalidSubscriptionError from core.models.BaseProfile import BaseProfile as Profile
from core.models.BaseProfile import ProfileType
# from core.models.session.SessionProfile import SessionProfile
# from core.models.system.SystemProfile import SystemProfile
from core.Errors import MissingSubscriptionError, MissingLocationError
#, InvalidSubscriptionError
# from core.services.WebServiceApiService import WebServiceApiService # from core.services.WebServiceApiService import WebServiceApiService
# from core.controllers.ConnectionController import ConnectionController # from core.controllers.ConnectionController import ConnectionController
from core.observers.ConnectionObserver import ConnectionObserver from core.observers.ConnectionObserver import ConnectionObserver
from core.Constants import Constants from core.Constants import Constants
from core.errors.logger import logger from core.errors.logger import logger
from core.models.manage.server_reply_tools import format_server_response #from core.models.manage.server_reply_tools import format_server_response
# from core.models.pydantic_models.ServerReply import ServerReply, ServerError # from core.models.pydantic_models.ServerReply import ServerReply, ServerError
from core.models.Result import Result, ResultError from core.models.Result import Result, ResultError
from typing import Union, Optional from typing import Optional
import base64 import base64
import re import re
MIN_MTU = 1280
def register_wireguard_session( def register_wireguard_session(
profile: Union[SessionProfile, SystemProfile], profile: Profile,
connection_observer: Optional[ConnectionObserver] = None connection_observer: Optional[ConnectionObserver] = None
) -> Result: ) -> Result:
"""Register a WireGuard session for the given profile.""" """Register a WireGuard session for the given profile."""
@ -34,69 +42,40 @@ def register_wireguard_session(
logger.error("No Public Key Generated. Raising ValueError") logger.error("No Public Key Generated. Raising ValueError")
raise ValueError("No Public Key Generated.") raise ValueError("No Public Key Generated.")
country_code = profile.location.country_code
location_code = profile.location.code
billing_code = profile.subscription.billing_code billing_code = profile.subscription.billing_code
url = f'{Constants.SP_API_BASE_URL}/countries/{country_code}/locations/{location_code}/wireguard-sessions' url = f'{Constants.SP_BASE_URL}/v2/wireguard_config'
payload = { payload = {
'public_key': public_key 'public_key': public_key
} }
api_result = connect.single_endpoint( result = api_call_and_extraction(
method="post", desired_type="conf",
method=ApiCall.POST,
url=url, url=url,
observer=connection_observer, connection_observer=connection_observer,
payload=payload, payload=payload,
billing_code=billing_code 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: if not result.valid:
# KILLING THE RESULT FOR NOW. Later, replace this error raise with the return of data, return result
raise InvalidSubscriptionError(result.message)
wireguard_configuration = result.data wireguard_configuration = result.data
if wireguard_configuration is None: if wireguard_configuration is None:
raise InvalidSubscriptionError() return Result(valid=False, error_type=ResultError.SERVER_SIDE, message="Server gave a blank wireguard config.")
wireguard_configuration = _inject_private_key(wireguard_configuration, wireguard_keys.get('private')) wireguard_configuration = _inject_private_key(wireguard_configuration, wireguard_keys.get('private'))
if profile.type == ProfileType.SESSION:
logger.info("Injecting MTU..")
wireguard_configuration = insert_mtu(mtu=MIN_MTU, wireguard_config=wireguard_configuration)
profile.attach_wireguard_configuration(wireguard_configuration) profile.attach_wireguard_configuration(wireguard_configuration)
return Result(valid=True) return Result(valid=True)
# over-engineering:
# 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.")
def _generate_wireguard_keys() -> dict: def _generate_wireguard_keys() -> dict:
"""Generate WireGuard public/private key pair.""" """Generate WireGuard public/private key pair."""
from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives import serialization
@ -124,10 +103,84 @@ def _inject_private_key(config: str, private_key: str) -> str:
return re.sub(expression, r'\1 ' + private_key, config) return re.sub(expression, r'\1 ' + private_key, config)
def insert_mtu(mtu: int, wireguard_config: str) -> str:
# This regex finds the DNS line and the whitespace following it,
# specifically when that whitespace is followed by the [Peer] section.
# Group 1: The DNS assignment line
# Group 2: The whitespace (including newlines) between DNS and [Peer]
# Group 3: The [Peer] header
pattern = r"(DNS\s*=\s*[^\n]+)(\s*)(\[Peer\])"
# We insert the MTU line immediately after the DNS line (\1),
# then preserve the original whitespace (\2) and the [Peer] header (\3).
replacement = rf"\1\nMTU = {mtu}\2\3"
return re.sub(pattern, replacement, wireguard_config)
# If need it pretty spacing quickly:
def expand_newlines(text):
"""
Removes leading/trailing whitespace and ensures exactly one
empty line between every non-empty line of text.
"""
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: # 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( # ConnectionController.with_preferred_connection(
# profile.location.country_code, # profile.location.country_code,
# profile.location.code, # profile.location.code,

View file

@ -0,0 +1,86 @@
from core.utils.run_commands import run_generic_command
from core.models.Result import Result, ResultError
from core.errors.logger import logger
MAX_MTU = 1500
MIN_MTU = 1280
# Use integer division for the middle point
MIDDLE_MTU = MIN_MTU + ((MAX_MTU - MIN_MTU) // 2)
# Fallback list for reliability
URLS = ["217.156.66.35", "185.165.169.91", "9.9.9.9"]
def test_an_mtu(mtu_value: int, url: str, timeout: int) -> Result:
# Payload size = MTU - 28 bytes (IP + ICMP headers)
payload_size = mtu_value - 28
if payload_size < 0:
return Result(valid=False)
command = ["ping", "-M", "do", "-s", str(payload_size), "-c", "1", url]
human_readable_goal = f"Test the MTU value {mtu_value} against {url}."
return run_generic_command(command, human_readable_goal, timeout=timeout)
def find_optimal_mtu(low: int, high: int, url: str) -> int:
"""
Performs a binary search between a known working MTU (low)
and a known failing MTU (high).
"""
optimal = low
current_timeout = 6
while low <= high:
mid = (low + high) // 2
# We increase timeout slightly as we try larger packets
# to avoid false negatives due to congestion/latency
logger.info(f"Refining: Testing MTU {mid}...")
result = test_an_mtu(mid, url, timeout=current_timeout)
if result.valid:
optimal = mid
low = mid + 1
current_timeout += 1 # Increase timeout as we get closer to max
else:
high = mid - 1
return optimal
def loop_through_testing_mtu():
# The sequence to probe the range
INITIAL_TEST_LIST = [MAX_MTU, MIDDLE_MTU, MIN_MTU]
for url in URLS:
logger.info(f"Testing connectivity using URL: {url}")
working_mtu = None
failed_above = None
for each_mtu in INITIAL_TEST_LIST:
logger.info(f"Probing MTU {each_mtu}...")
result = test_an_mtu(mtu_value=each_mtu, url=url, timeout=6)
if result.valid:
working_mtu = each_mtu
# If MAX_MTU works immediately, we've found the ceiling
if each_mtu == MAX_MTU:
return MAX_MTU
break
if working_mtu:
# We found a baseline. Now we need to find the ceiling.
# The 'high' is the first value in our test list that failed.
# If MIDDLE worked, high is MAX. If MIN worked, high is MIDDLE.
if working_mtu == MIDDLE_MTU:
failed_above = MAX_MTU
elif working_mtu == MIN_MTU:
failed_above = MIDDLE_MTU
else:
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.")
return None

View file

@ -185,5 +185,10 @@ def parse_errors(error_output: str) -> ResultError:
elif "command not found" in error_output: elif "command not found" in error_output:
return ResultError.MISSING_DEPENDENCY return ResultError.MISSING_DEPENDENCY
elif "local error: message too long" in error_output:
return ResultError.MTU
else: else:
return ResultError.UNKNOWN return ResultError.UNKNOWN