From fe19044928599a4e66b3d273d65aac12693b4d63 Mon Sep 17 00:00:00 2001 From: SimplifiedPrivacy Date: Thu, 20 Aug 2026 14:00:17 -0400 Subject: [PATCH] Singbox enable now reads the output file to confirm it's up --- .../connection_enable.py | 1 + .../encrypted_proxy/singbox_runner.py | 52 ++++++++++++++++--- core/utils/search_tools.py | 49 +++++++++++++++++ 3 files changed, 96 insertions(+), 6 deletions(-) create mode 100644 core/utils/search_tools.py diff --git a/core/services/networking/general_connection_tools/connection_enable.py b/core/services/networking/general_connection_tools/connection_enable.py index 6c57ad5..ef6e76f 100644 --- a/core/services/networking/general_connection_tools/connection_enable.py +++ b/core/services/networking/general_connection_tools/connection_enable.py @@ -97,6 +97,7 @@ def launch_encrypted_proxy( return start_singbox( profile_id=profile.id, server_ip=server_ip, + protocol_choice=profile.connection.code, connection_observer=connection_observer ) diff --git a/core/services/networking/systemwide/encrypted_proxy/singbox_runner.py b/core/services/networking/systemwide/encrypted_proxy/singbox_runner.py index 0726671..558aeea 100644 --- a/core/services/networking/systemwide/encrypted_proxy/singbox_runner.py +++ b/core/services/networking/systemwide/encrypted_proxy/singbox_runner.py @@ -5,6 +5,9 @@ from core.services.networking.systemwide import killswitch from core.services.networking.systemwide.wireguard.wg_firewall_dns import revert_dns from core.utils.basic_operations import pid_tools from core.services.networking.general_connection_tools import ip +from core.utils.basic_operations.write_string_to_text_file import write_string_to_text_file +from core.utils.search_tools import search_for_phrase +from core.utils.basic_operations.does_file_exist import does_file_exist from core.models.Result import Result, ResultError from core.errors.logger import logger @@ -27,6 +30,24 @@ from typing import Optional QUANTITY_OF_ATTEMPTS = 2 +def confirm_proxy_started(protocol_choice: str) -> bool: + SINGBOX_OUTPUT = Constants.SINGBOX_OUTPUT + + if not does_file_exist(SINGBOX_OUTPUT): + return False + + if protocol_choice == "vless": + timeout = 13 + else: + timeout = 7 + + return search_for_phrase( + phrase="sing-box started", + textfile_path=SINGBOX_OUTPUT, + timeout=timeout + ) + + def set_dns_for_singbox(current_state: SystemState): do_they_even_use_systemd = dns.is_systemd_enabled() @@ -61,8 +82,8 @@ def launch_singbox_binary(profile_id: int) -> Result: process_id = int(activation_result.data) - logger.info(f"Waiting 6 seconds to see if the process id {process_id} is still alive..") - time.sleep(6) + logger.info(f"Waiting 2 seconds to see if the process id {process_id} is still alive..") + time.sleep(2) # Evaluate if running by that exact pid: active = pid_tools.is_running(pid=process_id, process_name="sing-box") @@ -175,6 +196,7 @@ def end_singbox() -> Result: def start_singbox( profile_id: int, server_ip: str, + protocol_choice: str, connection_observer: Optional[ConnectionObserver] = None ) -> Result: """ @@ -185,10 +207,13 @@ def start_singbox( 1) Validate inputs 2) Start the binary 3) Evaluate if the process id is still up. - 4) If it's up, setup the State JSON - 5) Turn on Firewall - 6) Turn on DNS - 7) Update the State + 4) Check if the interface is up (before we wait for timeout period for output). This gives immediate feedback. + 5) For a timeout period, we wait to confirm via singbox's output in a text file. + 6) If it's up, setup the State JSON + 7) Test the IP address to match the server via an external API + 8) Turn on Firewall + 9) Turn on DNS + 10) Update the State """ # ============= INPUT VALIDATION ============= requirements = [profile_id, server_ip] @@ -203,6 +228,15 @@ def start_singbox( return killed_pre_existing logger.info("Pre-existing process ended.") + # ========= WIPE PRE-EXISTING LOGS ======= + wiped = write_string_to_text_file(content_to_write="", file_path=Constants.SINGBOX_OUTPUT) + if not wiped: + error_msg = f"Critical Error with wiping the log at {Constants.SINGBOX_OUTPUT}." + if protocol_choice == "vless": + logger.error(f"{error_msg} which vless relies upon to see when the connection started.") + return Result(valid=False, error_type=ResultError.FILE_SYSTEM, message=error_msg) + logger.error(f"{error_msg} but that's okay, because {protocol_choice} doesn't use it.") + # ============= START BINARY ============= launched = _attempt_start_with_retry(profile_id=profile_id, quantity_of_attempts=2) if not launched.valid: @@ -229,6 +263,12 @@ def start_singbox( logger.error("Interface is DOWN") return Result(valid=False, error_type=ResultError.INTERFACE, data=process_id) + # ======= CONFIRM VIA SINGBOX OUTPUT ========== + its_up = confirm_proxy_started(protocol_choice=protocol_choice) + 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.") + logger.info("Singbox output confirming it's up.") + # ============= 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. diff --git a/core/utils/search_tools.py b/core/utils/search_tools.py new file mode 100644 index 0000000..b8c2326 --- /dev/null +++ b/core/utils/search_tools.py @@ -0,0 +1,49 @@ +import time + +def search_for_phrase( + phrase: str, + textfile_path: str, + timeout: int) -> bool: + """ + Monitor a text file for a phrase. + + Args: + phrase: The phrase to search for + textfile_path: Path to the text file to monitor + timeout: Maximum time in seconds to search + + Returns: + True if phrase is found, False if timeout exceeded + """ + start_time = time.monotonic() + last_position = 0 + + while True: + # Check if timeout has been exceeded + elapsed = time.monotonic() - start_time + if elapsed >= timeout: + return False + + try: + # Open file and read from last known position + with open(textfile_path, 'r') as f: + f.seek(last_position) + new_content = f.read() + + # Check if phrase is in new content + if phrase in new_content: + return True + + # Update position for next iteration + last_position = f.tell() + + except FileNotFoundError: + # File doesn't exist yet, continue trying + pass + except Exception as e: + # Handle other file errors gracefully + pass + + # Wait before checking again + time.sleep(0.2) +