Singbox enable now reads the output file to confirm it's up

This commit is contained in:
SimplifiedPrivacy 2026-08-20 14:00:17 -04:00
parent bdfb933548
commit fe19044928
3 changed files with 96 additions and 6 deletions

View file

@ -97,6 +97,7 @@ def launch_encrypted_proxy(
return start_singbox( return start_singbox(
profile_id=profile.id, profile_id=profile.id,
server_ip=server_ip, server_ip=server_ip,
protocol_choice=profile.connection.code,
connection_observer=connection_observer connection_observer=connection_observer
) )

View file

@ -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.services.networking.systemwide.wireguard.wg_firewall_dns import revert_dns
from core.utils.basic_operations import pid_tools from core.utils.basic_operations import pid_tools
from core.services.networking.general_connection_tools import ip 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.models.Result import Result, ResultError
from core.errors.logger import logger from core.errors.logger import logger
@ -27,6 +30,24 @@ from typing import Optional
QUANTITY_OF_ATTEMPTS = 2 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): def set_dns_for_singbox(current_state: SystemState):
do_they_even_use_systemd = dns.is_systemd_enabled() 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) process_id = int(activation_result.data)
logger.info(f"Waiting 6 seconds to see if the process id {process_id} is still alive..") logger.info(f"Waiting 2 seconds to see if the process id {process_id} is still alive..")
time.sleep(6) time.sleep(2)
# Evaluate if running by that exact pid: # Evaluate if running by that exact pid:
active = pid_tools.is_running(pid=process_id, process_name="sing-box") active = pid_tools.is_running(pid=process_id, process_name="sing-box")
@ -175,6 +196,7 @@ def end_singbox() -> Result:
def start_singbox( def start_singbox(
profile_id: int, profile_id: int,
server_ip: str, server_ip: str,
protocol_choice: str,
connection_observer: Optional[ConnectionObserver] = None connection_observer: Optional[ConnectionObserver] = None
) -> Result: ) -> Result:
""" """
@ -185,10 +207,13 @@ def start_singbox(
1) Validate inputs 1) Validate inputs
2) Start the binary 2) Start the binary
3) Evaluate if the process id is still up. 3) Evaluate if the process id is still up.
4) If it's up, setup the State JSON 4) Check if the interface is up (before we wait for timeout period for output). This gives immediate feedback.
5) Turn on Firewall 5) For a timeout period, we wait to confirm via singbox's output in a text file.
6) Turn on DNS 6) If it's up, setup the State JSON
7) Update the State 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 ============= # ============= INPUT VALIDATION =============
requirements = [profile_id, server_ip] requirements = [profile_id, server_ip]
@ -203,6 +228,15 @@ def start_singbox(
return killed_pre_existing return killed_pre_existing
logger.info("Pre-existing process ended.") 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 ============= # ============= START BINARY =============
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:
@ -229,6 +263,12 @@ def start_singbox(
logger.error("Interface is DOWN") logger.error("Interface is DOWN")
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 ==========
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 ============= # ============= 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.

View file

@ -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)