Direct httpx2 IP address checking, instead of subprocess for connection tests.

This commit is contained in:
SimplifiedPrivacy 2026-10-19 21:36:22 -04:00
parent a7b733db0d
commit ed517f4568
6 changed files with 102 additions and 25 deletions

View file

@ -5,7 +5,8 @@ from core.services.networking.general_connection_tools.testing_evaluating import
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.endpoint_verification import verify_wireguard_endpoint from core.services.keys_and_verifications.endpoint_verification import verify_wireguard_endpoint
from core.observers.TicketObserver import TicketObserver from core.observers.TicketObserver import TicketObserver
from core.services.networking.systemwide.general_tools.test_any_protocol import test_session_ip
from core.models.enum_models import ProtocolChoice
from collections.abc import Callable from collections.abc import Callable
from core.Constants import Constants from core.Constants import Constants
@ -79,8 +80,6 @@ class ConnectionController:
else: else:
from core.services.profiles.profile_connections import disable_profile from core.services.profiles.profile_connections import disable_profile
disable_profile(profile=profile, connection_observer=connection_observer, ticket_observer=ticket_observer) disable_profile(profile=profile, connection_observer=connection_observer, ticket_observer=ticket_observer)
# from core.controllers.ProfileController import ProfileController
# ProfileController.disable(profile=profile, connection_observer=connection_observer, ticket_observer=ticket_observer)
if profile.connection.code == 'tor': if profile.connection.code == 'tor':
@ -107,14 +106,28 @@ class ConnectionController:
ConnectionController.establish_proxy_session_connection(profile, session_directory, port_number, proxy_port_number) ConnectionController.establish_proxy_session_connection(profile, session_directory, port_number, proxy_port_number)
session_state.network_port_numbers.proxy.append(proxy_port_number) session_state.network_port_numbers.proxy.append(proxy_port_number)
if not profile.connection.is_unprotected(): # if not profile.connection.is_unprotected():
if profile.connection.code == 'wireguard':
test_result = test_session_ip(port=port_number, profile_id=profile.id, protocol=ProtocolChoice.WIREGUARD, connection_observer=connection_observer)
if test_result.valid:
logger.info("Connection established.")
if connection_observer:
connection_observer.notify("connected", "Confirmed")
else:
logger.error(test_result.message)
raise ConnectionError(test_result.message)
elif profile.connection.code == 'tor':
await_connection(proxy_port_number or port_number, connection_observer=connection_observer) await_connection(proxy_port_number or port_number, connection_observer=connection_observer)
SessionStateController.update_or_create(session_state) SessionStateController.update_or_create(session_state)
return proxy_port_number or port_number return proxy_port_number or port_number
@staticmethod @staticmethod
def establish_tor_connection(connection_observer: Optional[ConnectionObserver] = None): def establish_tor_connection(connection_observer: Optional[ConnectionObserver] = None):
try: try:

View file

@ -1,21 +1,36 @@
from core.models.BaseProfile import ProfileType
from core.Constants import Constants from core.Constants import Constants
import os from core.errors.logger import logger
def extract_endpoint_ip(profile_id: str) -> str | bool: import os
try: from typing import Optional
profile_path = Constants.HV_PROFILE_CONFIG_HOME + f'/{profile_id}'
def extract_endpoint_ip(profile_id: str, profile_type: Optional[ProfileType] = None) -> str | bool:
profile_path = Constants.HV_PROFILE_CONFIG_HOME + f'/{profile_id}'
if not profile_type: # assumes systemwide:
wg_conf_path = f'{profile_path}/wg.conf.bak' wg_conf_path = f'{profile_path}/wg.conf.bak'
print(f"wg_conf_path is {wg_conf_path}") elif profile_type == ProfileType.SESSION:
if not os.path.exists(wg_conf_path): wg_conf_path = f'{profile_path}/wg.conf'
return None else:
return None
if not os.path.exists(wg_conf_path):
logger.error(f"Path doesn't exist: {wg_conf_path}")
return None
try:
with open(wg_conf_path, 'r') as f: with open(wg_conf_path, 'r') as f:
content = f.read() content = f.read()
for line in content.split('\n'): for line in content.split('\n'):
if line.strip().startswith('Endpoint = '): if line.strip().startswith('Endpoint = '):
endpoint = line.strip().split(' = ')[1] endpoint = line.strip().split(' = ')[1]
return endpoint.split(':')[0] return endpoint.split(':')[0]
logger.error("Found no content.")
return None return None
except Exception: except Exception as e:
logger.error(f"Unable to parse WG config: {str(e)}")
return None return None
def extract_dns(profile_id: str) -> str | bool: def extract_dns(profile_id: str) -> str | bool:

View file

@ -4,7 +4,6 @@ from core.services.networking.httpx.classify_response import classify_response
from core.models.Result import Result, ResultError from core.models.Result import Result, ResultError
from core.services.networking.api_requests.ApiResponseModel import ApiResponse, ErrorType from core.services.networking.api_requests.ApiResponseModel import ApiResponse, ErrorType
# errors/loggers/observers
from core.Constants import Constants from core.Constants import Constants
from core.errors.logger import logger from core.errors.logger import logger
from essentials.observers.ConnectionObserver import ConnectionObserver from essentials.observers.ConnectionObserver import ConnectionObserver
@ -13,6 +12,10 @@ from essentials.observers.ConnectionObserver import ConnectionObserver
import time import time
import random import random
import httpx import httpx
from httpx_socks import AsyncProxyTransport
import httpx_socks
from httpx_socks import ProxyType
from typing import Optional from typing import Optional
API_ENDPOINT_FORMATS = { API_ENDPOINT_FORMATS = {
@ -26,7 +29,7 @@ API_ENDPOINT_FORMATS = {
MAX_ATTEMPTS = 3 MAX_ATTEMPTS = 3
def test(connection_observer: Optional[ConnectionObserver]) -> Result: def test(connection_observer: Optional[ConnectionObserver], port: Optional[int] = None) -> Result:
""" """
Cycles through 3 possible endpoints, Cycles through 3 possible endpoints,
to attempt to get an IP address for the systemwide connection. to attempt to get an IP address for the systemwide connection.
@ -35,7 +38,16 @@ def test(connection_observer: Optional[ConnectionObserver]) -> Result:
endpoint_index = 0 endpoint_index = 0
endpoint_list = list(API_ENDPOINT_FORMATS.keys()) endpoint_list = list(API_ENDPOINT_FORMATS.keys())
_ip_test_client = httpx.Client(http2=True, timeout=6) if port:
transport = httpx_socks.SyncProxyTransport(
proxy_type=ProxyType.SOCKS5,
proxy_host="127.0.0.1",
proxy_port=port,
)
_ip_test_client = httpx.Client(transport=transport, http2=True, timeout=6)
else:
_ip_test_client = httpx.Client(http2=True, timeout=6)
while attempt < MAX_ATTEMPTS: while attempt < MAX_ATTEMPTS:
endpoint_url = endpoint_list[attempt] endpoint_url = endpoint_list[attempt]

View file

@ -2,6 +2,7 @@
from core.services.networking.general_connection_tools.config_tools import extract_endpoint_ip from core.services.networking.general_connection_tools.config_tools import extract_endpoint_ip
from core.services.networking.general_connection_tools import ip from core.services.networking.general_connection_tools import ip
from essentials.observers.ConnectionObserver import ConnectionObserver from essentials.observers.ConnectionObserver import ConnectionObserver
from core.models.BaseProfile import ProfileType
from core.models.enum_models import ProtocolChoice from core.models.enum_models import ProtocolChoice
from core.utils.basic_operations.write_or_read_from_json import get_value_from_json_file from core.utils.basic_operations.write_or_read_from_json import get_value_from_json_file
from core.models.Result import Result, ResultError from core.models.Result import Result, ResultError
@ -11,6 +12,29 @@ from core.Constants import Constants
import os import os
def test_session_ip(port: int, profile_id: int, protocol: ProtocolChoice, connection_observer: ConnectionObserver) -> Result:
"""Compare the real vs expected IP address."""
if protocol == ProtocolChoice.WIREGUARD:
expected_ip_address = extract_endpoint_ip(profile_id=profile_id, profile_type=ProfileType.SESSION)
else:
error_msg = "Not currently supported protocol for test_session_ip"
logger.error(error_msg)
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=error_msg)
if not expected_ip_address:
error_msg = "Could not get the expected IP address from local files."
logger.error(error_msg)
return Result(valid=False, error_type=ResultError.MISSING_DATA, message=error_msg)
# reality:
logger.info(f"testing on port {port}")
ip_result = ip.test(connection_observer=connection_observer, port=port)
return compare_ips(ip_result=ip_result, expected_ip_address=expected_ip_address)
def get_singbox_ip(profile_id: int) -> str|None: def get_singbox_ip(profile_id: int) -> str|None:
"""Get the IP address for a Singbox based profile based on local disk.""" """Get the IP address for a Singbox based profile based on local disk."""
config_path = f'{Constants.HV_SYSTEM_PROFILE_CONFIG_PATH}/{profile_id}/proxy.json' config_path = f'{Constants.HV_SYSTEM_PROFILE_CONFIG_PATH}/{profile_id}/proxy.json'
@ -26,9 +50,6 @@ def get_singbox_ip(profile_id: int) -> str|None:
def test_ip_systemwide_for_any_protocol(profile_id: int, protocol: ProtocolChoice, connection_observer: ConnectionObserver) -> Result: def test_ip_systemwide_for_any_protocol(profile_id: int, protocol: ProtocolChoice, connection_observer: ConnectionObserver) -> Result:
"""Compare the real vs expected IP address.""" """Compare the real vs expected IP address."""
# protocol = profile.type
# profile_id = profile.id
# what's expected: # what's expected:
if protocol == ProtocolChoice.WIREGUARD: if protocol == ProtocolChoice.WIREGUARD:
expected_ip_address = extract_endpoint_ip(profile_id) expected_ip_address = extract_endpoint_ip(profile_id)
@ -45,6 +66,12 @@ def test_ip_systemwide_for_any_protocol(profile_id: int, protocol: ProtocolChoic
# reality: # reality:
ip_result = ip.test(connection_observer) ip_result = ip.test(connection_observer)
return compare_ips(ip_result=ip_result, expected_ip_address=expected_ip_address)
def compare_ips(ip_result: Result, expected_ip_address: str) -> Result:
"""Compare the expected to the real IP address."""
if not ip_result.valid: if not ip_result.valid:
logger.error(ip_result.message) logger.error(ip_result.message)
return ip_result # result object return ip_result # result object

View file

@ -5,6 +5,8 @@ from core.services.keys_and_verifications.endpoint_verification import verify_wi
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
from core.observers.Notifications import Notifications, EventType from core.observers.Notifications import Notifications, EventType
from core.services.networking.systemwide.general_tools.test_any_protocol import test_ip_systemwide_for_any_protocol
# 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
@ -198,13 +200,16 @@ 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.")
try: if connection_observer:
if connection_observer: connection_observer.notify("connecting", "Testing Connection Health..")
connection_observer.notify("connecting", "Testing Connection Health..")
await_connection(connection_observer=connection_observer)
except ConnectionError: test_result = test_ip_systemwide_for_any_protocol(
raise ConnectionError('The connection could not be established.') profile_id=profile.id,
protocol=ProtocolChoice.WIREGUARD,
connection_observer=connection_observer
)
if not test_result.valid:
return test_result
# ============= UPDATE STATE ============= # ============= UPDATE STATE =============
if dns_setting and not did_dns_work: if dns_setting and not did_dns_work:
@ -212,6 +217,9 @@ def __establish_system_connection(
current_state.dns_set = did_dns_work current_state.dns_set = did_dns_work
SystemStateController.update_or_create(current_state) SystemStateController.update_or_create(current_state)
if connection_observer:
connection_observer.notify("connected", "Confirmed")
Notifications.emit(EventType.SYSTEM_ENABLE, profile.id) Notifications.emit(EventType.SYSTEM_ENABLE, profile.id)
return Result(valid=True, data=current_state) return Result(valid=True, data=current_state)
@ -221,6 +229,7 @@ def evaluate_connection_result(connection_result: Result) -> Result:
"""In the future, this will raise other error types or filter replies.""" """In the future, this will raise other error types or filter replies."""
if not connection_result.valid: if not connection_result.valid:
logger.error(f"[SYSTEMWIDE WG] Critical issue, could not establish a connection: {connection_result.message}") 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}") raise ConnectionError(f"Connection validation failed: {connection_result.message}")
else: else:
return connection_result return connection_result
@ -309,5 +318,5 @@ def establish_system_connection(
raise raise
terminate_tor_connection() terminate_tor_connection()
time.sleep(1.0) time.sleep(0.3)
return final_result return final_result

View file

@ -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
def wipe_ticket_cycle() -> Result: def wipe_ticket_cycle() -> Result:
"""Wipe the entire global ticketing cycle & all associated tickets.""" """Wipe the entire global ticketing cycle & all associated tickets."""