Improved Connection Observer Feedback for all systemwide protocols

This commit is contained in:
SimplifiedPrivacy 2026-08-21 15:31:52 -04:00
parent 8968d89c5c
commit a69b87817c
4 changed files with 62 additions and 10 deletions

View file

@ -119,9 +119,14 @@ class ProfileController:
# ============================================================================ # ============================================================================
if profile.is_system_profile(): if profile.is_system_profile():
try: try:
establish_connection(profile, ignore=ignore, connection_observer=connection_observer) connection_result = establish_connection(profile, ignore=ignore, connection_observer=connection_observer)
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)
else:
logger.error(f"Couldn't enable the profile: {connection_result.error_type}")
error_msg = connection_result.message
raise ProfileActivationError(error_msg)
except FirewallError: except FirewallError:
raise raise
except ConnectionError: except ConnectionError:

View file

@ -193,7 +193,15 @@ def _establish_with_renegotiation(
except ConnectionError: except ConnectionError:
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..")
# KEY
if connection_observer:
connection_observer.notify("connecting", "Renegotiating WG Key..")
register_wireguard_session(profile, connection_observer=connection_observer) register_wireguard_session(profile, connection_observer=connection_observer)
# Reconnect:
if connection_observer:
connection_observer.notify("connecting", "Got New WG Key, Connecting..")
return establish_fn(profile, ignore=ignore, connection_observer=connection_observer, ticket_observer=ticket_observer) return establish_fn(profile, ignore=ignore, connection_observer=connection_observer, ticket_observer=ticket_observer)
raise ConnectionError('The connection could not be established.') raise ConnectionError('The connection could not be established.')
except FirewallError: except FirewallError:

View file

@ -238,6 +238,8 @@ def start_singbox(
logger.error(f"{error_msg} but that's okay, because {protocol_choice} doesn't use it.") logger.error(f"{error_msg} but that's okay, because {protocol_choice} doesn't use it.")
# ============= START BINARY ============= # ============= START BINARY =============
if connection_observer:
connection_observer.notify("connecting", "Starting Software..")
launched = _attempt_start_with_retry(profile_id=profile_id, quantity_of_attempts=2) launched = _attempt_start_with_retry(profile_id=profile_id, quantity_of_attempts=2)
if not launched.valid: if not launched.valid:
return launched return launched
@ -246,6 +248,8 @@ def start_singbox(
logger.info(f"Binary started with process id {process_id}.") logger.info(f"Binary started with process id {process_id}.")
# ============= CHECK INTERFACE ============= # ============= CHECK INTERFACE =============
if connection_observer:
connection_observer.notify("connecting", "Testing Interface..")
interface_result = interface_tools.get_output(Constants.SINGBOX_TUN_IF) interface_result = interface_tools.get_output(Constants.SINGBOX_TUN_IF)
if not interface_result.valid: if not interface_result.valid:
error_msg = f"We could not get the interface's output, despite having a process id of {process_id}" error_msg = f"We could not get the interface's output, despite having a process id of {process_id}"
@ -259,8 +263,13 @@ def start_singbox(
if interface_up: if interface_up:
logger.info("Interface is confirmed to be Up") logger.info("Interface is confirmed to be Up")
if connection_observer:
connection_observer.notify("connecting", "Interface looking healthy..")
else: else:
logger.error("Interface is DOWN") error_msg = "Interface is DOWN"
logger.error(error_msg)
if connection_observer:
connection_observer.notify("connecting", error_msg)
return Result(valid=False, error_type=ResultError.INTERFACE, data=process_id) return Result(valid=False, error_type=ResultError.INTERFACE, data=process_id)
# ======= CONFIRM VIA SINGBOX OUTPUT ========== # ======= CONFIRM VIA SINGBOX OUTPUT ==========
@ -268,6 +277,8 @@ def start_singbox(
if not its_up: if not its_up:
return Result(valid=False, error_type=ResultError.CONNECTION, message="After a timeout period, the singbox output is still not showing the connection having started.") return Result(valid=False, error_type=ResultError.CONNECTION, message="After a timeout period, the singbox output is still not showing the connection having started.")
logger.info("Singbox output confirming it's up.") logger.info("Singbox output confirming it's up.")
if connection_observer:
connection_observer.notify("connecting", "Software looking healthy so far..")
# ============= SETUP STATE ============= # ============= SETUP STATE =============
# Even if the connection is dead, firewall is off, we want to save the fact we turned Singbox on, before we raise errors. # Even if the connection is dead, firewall is off, we want to save the fact we turned Singbox on, before we raise errors.
@ -292,10 +303,15 @@ def start_singbox(
observed_ip_address = ip_result.data observed_ip_address = ip_result.data
if observed_ip_address == server_ip: if observed_ip_address == server_ip:
logger.info("Observed IP matches the intended server IP. Proceeding..") logger.info("Observed IP matches the intended server IP. Proceeding..")
if connection_observer:
connection_observer.notify("connecting", f"IP Matched! {server_ip}")
else: else:
error_msg = f"Critical Leak! IP address check of {observed_ip_address} does NOT match the intended {server_ip}." error_msg = f"Connection didn't work, it's leaking! IP address check of {observed_ip_address} does NOT match the intended {server_ip}."
logger.error(f"{error_msg} Killing Singbox pid {process_id}") logger.error(f"{error_msg} Killing Singbox pid {process_id}")
closed = orchestrate_closing(current_state=current_state, interface=Constants.SINGBOX_TUN_IF) closed = orchestrate_closing(current_state=current_state, interface=Constants.SINGBOX_TUN_IF)
error_for_user = f"Failed Connection! Test showed leak! Closed Status: {closed}"
if connection_observer:
connection_observer.notify("connecting", error_for_user)
return Result(valid=False, error_type=ResultError.LEAK_ISSUE, message=error_msg) return Result(valid=False, error_type=ResultError.LEAK_ISSUE, message=error_msg)
# ============= FIREWALL ============= # ============= FIREWALL =============
@ -313,9 +329,13 @@ def start_singbox(
current_state.dns_set = False current_state.dns_set = False
SystemStateController.update_or_create(current_state) SystemStateController.update_or_create(current_state)
raise FirewallError(firewall_result) # Now end the party. raise FirewallError(firewall_result) # Now end the party.
if connection_observer:
connection_observer.notify("connecting", "Firewall Enabled.")
# ============= DNS ============= # ============= DNS =============
dns_result = set_dns_for_singbox(current_state) dns_result = set_dns_for_singbox(current_state)
if dns_result and connection_observer:
connection_observer.notify("connecting", "DNS Changed.")
# raises error if not okay. # raises error if not okay.
# ============= CONCLUSION ============= # ============= CONCLUSION =============

View file

@ -112,6 +112,8 @@ def __establish_system_connection(
terminate_system_connection(firewall_setting, dns_setting) terminate_system_connection(firewall_setting, dns_setting)
# ============= INITIAL NMCLI CONNECTION ============= # ============= INITIAL NMCLI CONNECTION =============
if connection_observer:
connection_observer.notify("connecting", "Setting Up WireGuard..")
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)
@ -124,6 +126,8 @@ def __establish_system_connection(
raise ConnectionError(f'NMCLI could not be enabled. {nmcli_result.message}') raise ConnectionError(f'NMCLI could not be enabled. {nmcli_result.message}')
# ============= IPv6 SINKHOLE ============= # ============= IPv6 SINKHOLE =============
if connection_observer:
connection_observer.notify("connecting", "Setting Up IPv6 Protection..")
sinkhole_result = setup_ipv6_sinkhole(process_output) sinkhole_result = setup_ipv6_sinkhole(process_output)
if not sinkhole_result: if not sinkhole_result:
raise ConnectionError('IPv6 Could not be protected.') raise ConnectionError('IPv6 Could not be protected.')
@ -139,6 +143,8 @@ def __establish_system_connection(
) )
# ============= PREP SETTINGS ============= # ============= PREP SETTINGS =============
if connection_observer:
connection_observer.notify("connecting", "Getting Interface Name..")
if firewall_setting or dns_setting: if firewall_setting or dns_setting:
wg_interface_result = extract_wg_interface_name(process_output) wg_interface_result = extract_wg_interface_name(process_output)
if not wg_interface_result.valid: if not wg_interface_result.valid:
@ -154,6 +160,8 @@ def __establish_system_connection(
if firewall_setting: if firewall_setting:
logger.info("Firewall setting is enabled, attempting to enable...") logger.info("Firewall setting is enabled, attempting to enable...")
if connection_observer:
connection_observer.notify("connecting", "Turning Firewall On..")
enable_firewall_with_retry( enable_firewall_with_retry(
profile_id=str(profile.id), profile_id=str(profile.id),
wg_interface_name=wg_interface_name wg_interface_name=wg_interface_name
@ -162,6 +170,8 @@ def __establish_system_connection(
# ============= DNS ============= # ============= DNS =============
if dns_setting: if dns_setting:
if connection_observer:
connection_observer.notify("connecting", "Setting DNS Up..")
set_dns_result = set_dns(str(profile.id), wg_interface_name) set_dns_result = set_dns(str(profile.id), wg_interface_name)
if not set_dns_result.valid: if not set_dns_result.valid:
logger.error(f"[CONNECT] Setting the DNS DIDN'T work. {set_dns_result.message}") logger.error(f"[CONNECT] Setting the DNS DIDN'T work. {set_dns_result.message}")
@ -178,6 +188,8 @@ 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: try:
if connection_observer:
connection_observer.notify("connecting", "Testing Connection Health..")
await_connection(connection_observer=connection_observer) await_connection(connection_observer=connection_observer)
except ConnectionError: except ConnectionError:
@ -193,11 +205,13 @@ def __establish_system_connection(
def evaluate_connection_result(connection_result: Result) -> None: def evaluate_connection_result(connection_result: Result) -> Result:
"""Validate the connection result. Raises if invalid.""" """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}")
raise ConnectionError(f"Connection validation failed: {connection_result.message}") raise ConnectionError(f"Connection validation failed: {connection_result.message}")
else:
return connection_result
def _cleanup_on_error(firewall_setting: bool, dns_setting: bool) -> None: def _cleanup_on_error(firewall_setting: bool, dns_setting: bool) -> None:
@ -213,12 +227,14 @@ def _establish_connection_with_retry(
firewall_setting: bool, firewall_setting: bool,
dns_setting: bool, dns_setting: bool,
connection_observer: Optional[ConnectionObserver], connection_observer: Optional[ConnectionObserver],
max_retries: int = 1 max_retries: int = 2
) -> Result: ) -> Result:
"""Establish connection with retry logic for transient errors.""" """Establish connection with retry logic for transient errors."""
last_error = None last_error = None
for attempt in range(1, max_retries + 1): for attempt in range(1, max_retries):
if connection_observer:
connection_observer.notify("connecting", f"Connecting attempt {attempt} of {max_retries}.. ")
try: try:
connection_result = __establish_system_connection( connection_result = __establish_system_connection(
profile=profile, profile=profile,
@ -259,6 +275,8 @@ def establish_system_connection(
3) Retry on failure 3) Retry on failure
""" """
# ================= VERIFY ENDPOINT ================= # ================= VERIFY ENDPOINT =================
if connection_observer:
connection_observer.notify("connecting", "Verifying endpoint..")
if ConfigurationController.get_endpoint_verification_enabled(): if ConfigurationController.get_endpoint_verification_enabled():
verify_wireguard_endpoint(profile, ignore=ignore) verify_wireguard_endpoint(profile, ignore=ignore)
@ -268,7 +286,7 @@ def establish_system_connection(
# ================= CONNECT WG ================ # ================= CONNECT WG ================
try: try:
_establish_connection_with_retry( final_result = _establish_connection_with_retry(
profile=profile, profile=profile,
firewall_setting=firewall_setting, firewall_setting=firewall_setting,
dns_setting=dns_setting, dns_setting=dns_setting,
@ -280,3 +298,4 @@ def establish_system_connection(
terminate_tor_connection() terminate_tor_connection()
time.sleep(1.0) time.sleep(1.0)
return final_result