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"
SERVER_SIDE = "server_side"
UNPAID = "unpaid"
MTU = "mtu"
NAVIGATE_TO = "navigate_to"
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.pydantic_models.ServerReply import ServerReply, ServerError
from core.models.Result import Result, ResultError
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 enum import Enum
from typing import Optional
class ApiCall(Enum):
GET = "get"
POST = "post"
# Translate Server-side to client-side errors in THIS CONTEXT,
TRANSLATION_DICT = {
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."
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
model_version: int = 1
data: Optional[dict] = None
conf: str = 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.session.SessionProfile import SessionProfile
from core.models.system.SystemProfile import SystemProfile
from core.Errors import MissingSubscriptionError, MissingLocationError, InvalidSubscriptionError
from core.models.manage.server_reply_tools import api_call_and_extraction, ApiCall
# from core.services.networking.httpx import connect
#from core.services.networking.api_requests.ApiResponseModel import ErrorType
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.controllers.ConnectionController import ConnectionController
from core.observers.ConnectionObserver import ConnectionObserver
from core.Constants import Constants
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.Result import Result, ResultError
from typing import Union, Optional
from typing import Optional
import base64
import re
MIN_MTU = 1280
def register_wireguard_session(
profile: Union[SessionProfile, SystemProfile],
profile: Profile,
connection_observer: Optional[ConnectionObserver] = None
) -> Result:
"""Register a WireGuard session for the given profile."""
@ -34,69 +42,40 @@ def register_wireguard_session(
logger.error("No Public Key Generated. Raising ValueError")
raise ValueError("No Public Key Generated.")
country_code = profile.location.country_code
location_code = profile.location.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 = {
'public_key': public_key
}
api_result = connect.single_endpoint(
method="post",
result = api_call_and_extraction(
desired_type="conf",
method=ApiCall.POST,
url=url,
observer=connection_observer,
connection_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)
return result
wireguard_configuration = result.data
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'))
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)
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:
"""Generate WireGuard public/private key pair."""
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)
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:
# 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,

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:
return ResultError.MISSING_DEPENDENCY
elif "local error: message too long" in error_output:
return ResultError.MTU
else:
return ResultError.UNKNOWN