diff --git a/core/controllers/ProfileController.py b/core/controllers/ProfileController.py index 7d08a5c..37af907 100644 --- a/core/controllers/ProfileController.py +++ b/core/controllers/ProfileController.py @@ -77,31 +77,14 @@ class ProfileController: if profile.is_system_profile(): - if profile.connection.needs_operator_proxy(): - from core.controllers.ConnectionController import ConnectionController - if not profile.has_operator_proxy_session(): - ProfileController.activate_subscription(profile, connection_observer=connection_observer) - from core.services.WebServiceApiService import WebServiceApiService - operator_proxy_session = ConnectionController.with_preferred_connection( - profile.subscription.billing_code, - profile.subscription.operator_id, - profile.connection.get_protocol(), - task=WebServiceApiService.post_operator_proxy, - connection_observer=connection_observer - ) - if operator_proxy_session is None: - raise ProfileActivationError('Could not obtain operator proxy session.') - profile.attach_operator_proxy_session(operator_proxy_session) - - ok = ConnectionController.establish_encrypted_proxy_connection( - profile, socks5_port=1080, observer=connection_observer - ) - if not ok: - raise ProfileActivationError('The profile could not be enabled.') + if profile.connection.code in ('vless', 'hysteria2'): + ProfileController._enable_encrypted_proxy(profile, connection_observer=connection_observer) if profile_observer is not None: profile_observer.notify('enabled', profile) return + # WireGuard and other system profiles — legacy flow + from core.controllers.ConnectionController import ConnectionController try: ConnectionController.establish_connection(profile, ignore=ignore, connection_observer=connection_observer) except ConnectionError: @@ -110,6 +93,37 @@ class ProfileController: if profile_observer is not None: profile_observer.notify('enabled', profile) + @staticmethod + def _enable_encrypted_proxy(profile, connection_observer=None) -> None: + + from core.controllers.ConnectionController import ConnectionController + from core.controllers.SystemStateController import SystemStateController + from core.services.WebServiceApiService import WebServiceApiService + + if not profile.has_operator_proxy_session(): + ProfileController.activate_subscription(profile, connection_observer=connection_observer) + operator_proxy_session = ConnectionController.with_preferred_connection( + profile.subscription.billing_code, + profile.subscription.operator_id, + profile.connection.code, + task=WebServiceApiService.post_operator_proxy, + connection_observer=connection_observer + ) + if operator_proxy_session is None: + raise ProfileActivationError('Could not obtain operator proxy session.') + profile.attach_operator_proxy_session(operator_proxy_session) + + ConnectionController._signal_previous_process() + ok = ConnectionController.establish_encrypted_proxy_connection( + profile, socks5_port=1080, observer=connection_observer + ) + if not ok: + raise ProfileActivationError('The profile could not be enabled.') + + token = SystemStateController.create(profile.id) + if connection_observer is not None: + connection_observer.notify('connected_token', {'session_token': token}) + @staticmethod def run_monitor(profile: Union[SessionProfile, SystemProfile], monitor) -> None: diff --git a/core/models/BaseProfile.py b/core/models/BaseProfile.py index 75cd58d..aca6119 100644 --- a/core/models/BaseProfile.py +++ b/core/models/BaseProfile.py @@ -65,8 +65,8 @@ class BaseProfile(ABC): return type(self).__name__ == 'SystemProfile' def save(self: Self): - config_dict = self.to_dict() # Get dict, not JSON string - location_dict = self.location.to_dict() if self.location else None if self.location else None # this is from SQLAlchemy, and not JSON-models, that's why it's separate. + config_dict = self.to_dict() + location_dict = self.location.to_dict() if self.location else None config_dict["location"] = location_dict diff --git a/core/services/WebServiceApiService.py b/core/services/WebServiceApiService.py index 484f8e3..13e4d33 100644 --- a/core/services/WebServiceApiService.py +++ b/core/services/WebServiceApiService.py @@ -209,7 +209,7 @@ class WebServiceApiService: else: headers = None - return requests.get(Constants.SP_API_BASE_URL + path, headers=headers, proxies=proxies) + return requests.get(Constants.SP_API_BASE_URL + path, headers=headers, proxies=proxies, timeout=30) @staticmethod def __post(path, billing_code: Optional[str] = None, body: Optional[dict] = None, proxies: Optional[dict] = None): @@ -221,7 +221,7 @@ class WebServiceApiService: else: headers = None - return requests.post(Constants.SP_API_BASE_URL + path, headers=headers, json=body, proxies=proxies) + return requests.post(Constants.SP_API_BASE_URL + path, headers=headers, json=body, proxies=proxies, timeout=30) @staticmethod def get_cached_sync(proxies: Optional[dict] = None): diff --git a/core/services/encrypted_proxy/hysteria_service.py b/core/services/encrypted_proxy/hysteria_service.py index 44e30a0..b28fcca 100644 --- a/core/services/encrypted_proxy/hysteria_service.py +++ b/core/services/encrypted_proxy/hysteria_service.py @@ -134,10 +134,41 @@ def enable_hysteria(username: str, password: str, server_host: str, _cleanup_all() +def _force_kill_singbox() -> None: + import subprocess + try: + result = subprocess.run(['pgrep', '-x', 'sing-box'], capture_output=True, text=True) + if result.returncode == 0: + for pid in result.stdout.strip().splitlines(): + subprocess.run( + ['sudo', 'kill', '-9', pid.strip()], + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=5 + ) + except Exception: + pass + + def disable_hysteria(observer=None) -> bool: - revert_dns_on_tun() - SingboxRunner().stop() - killswitch.disarm() + try: + revert_dns_on_tun() + except Exception: + pass + try: + SingboxRunner().stop() + except Exception: + pass + try: + import subprocess, time + time.sleep(0.5) + result = subprocess.run(['pgrep', '-x', 'sing-box'], capture_output=True, text=True) + if result.returncode == 0: + _force_kill_singbox() + except Exception: + pass + try: + killswitch.disarm() + except Exception: + pass if observer: observer.notify("disconnected", {"tunnel_if": Constants.SINGBOX_TUN_IF}) return True \ No newline at end of file diff --git a/core/services/encrypted_proxy/vless_service.py b/core/services/encrypted_proxy/vless_service.py index 3980db0..1a778cd 100644 --- a/core/services/encrypted_proxy/vless_service.py +++ b/core/services/encrypted_proxy/vless_service.py @@ -163,10 +163,41 @@ def enable_vless(vless_link: str, username: str, server_ip: str, _cleanup_all() +def _force_kill_singbox() -> None: + import subprocess + try: + result = subprocess.run(['pgrep', '-x', 'sing-box'], capture_output=True, text=True) + if result.returncode == 0: + for pid in result.stdout.strip().splitlines(): + subprocess.run( + ['sudo', 'kill', '-9', pid.strip()], + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=5 + ) + except Exception: + pass + + def disable_vless(observer=None) -> bool: - revert_dns_on_tun() - SingboxRunner().stop() - killswitch.disarm() + try: + revert_dns_on_tun() + except Exception: + pass + try: + SingboxRunner().stop() + except Exception: + pass + try: + import subprocess, time + time.sleep(0.5) + result = subprocess.run(['pgrep', '-x', 'sing-box'], capture_output=True, text=True) + if result.returncode == 0: + _force_kill_singbox() + except Exception: + pass + try: + killswitch.disarm() + except Exception: + pass if observer: observer.notify("disconnected", {"tunnel_if": Constants.SINGBOX_TUN_IF}) return True \ No newline at end of file diff --git a/core/utils/encrypted_proxy/singbox.py b/core/utils/encrypted_proxy/singbox.py index 60de1dd..1985a5c 100644 --- a/core/utils/encrypted_proxy/singbox.py +++ b/core/utils/encrypted_proxy/singbox.py @@ -24,8 +24,8 @@ class SingboxRunner: _START_POLL_S = 0.5 _START_INITIAL_S = 2.0 - _ORPHAN_GRACEFUL_TIMEOUT_S = 4.0 - _ORPHAN_FINAL_VERIFY_TIMEOUT_S = 2.0 + _ORPHAN_GRACEFUL_TIMEOUT_S = 2.0 + _ORPHAN_FINAL_VERIFY_TIMEOUT_S = 1.0 _ORPHAN_POLL_S = 0.3 def __init__(self): @@ -83,7 +83,7 @@ class SingboxRunner: stderr=subprocess.DEVNULL, stdin=subprocess.DEVNULL, env={**os.environ, 'SUDO_ASKPASS': '/bin/false'}, - timeout=15, + timeout=5, ) except (subprocess.TimeoutExpired, FileNotFoundError, OSError): logger.warning("Graceful stop failed, attempting emergency kill") @@ -190,7 +190,7 @@ class SingboxRunner: stderr=subprocess.DEVNULL, stdin=subprocess.DEVNULL, env={**os.environ, 'SUDO_ASKPASS': '/bin/false'}, - timeout=15, + timeout=5, ) except (OSError, subprocess.TimeoutExpired): pass