diff --git a/README.md b/README.md index ec54fb4..d66d748 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,68 @@ -# hydra-veil — usage +# Hydra-Veil — June Major Update + +## Expanded Killswitch + +`hydraveil-killswitch.sh` was added to the installer: a wrapper around `nftables` that blocks all traffic not routed through the tunnel. It defaults to `tun0`, but supports a configurable interface IP that the operator will send when creating the profile. The IP is currently hardcoded; it will be removed in the next commit. + +## One Connection at a Time + +First in, first out. When a new connection is established, the previous one receives a signal (`SIGUSR1`) and closes cleanly before the new one takes over. + +## Unexpected Shutdowns + +If the terminal is closed or the machine is powered off, the process receives `SIGHUP` and performs a full disconnect: stops `sing-box`, clears state, and releases the network. No orphan processes. + +## Fewer Network Requests + +Unnecessary calls made when establishing and closing connections have been removed. + +--- + +## Repositories + +| Repo | Role | +|---|---| +| `core` | Business logic, controllers, models | +| `cli` | Command-line interface | +| `essentials` | Network modules (Tor, WireGuard, proxies) | +| `installer` | Installation scripts and killswitch | + +--- +--- + +# Hydra-Veil — Gran Actualización de Junio + +## Killswitch ampliado + +Se añadió `hydraveil-killswitch.sh` al installer: un wrapper sobre `nftables` que bloquea todo el tráfico que no pase por el túnel. Por defecto usa `tun0`, pero soporta una IP de interfaz configurable que el operador enviará al crear el perfil. Por ahora la IP está hardcodeada; se eliminará en el próximo commit. + +## Una conexión a la vez + +Primero en entrar, primero en salir. Cuando se establece una nueva conexión, la anterior recibe una señal (`SIGUSR1`) y se cierra limpiamente antes de que la nueva tome el control. + +## Cierres inesperados + +Si se cierra la terminal o se apaga el equipo, el proceso recibe `SIGHUP` y ejecuta la desconexión completa: para `sing-box`, limpia el estado y libera la red. Sin procesos huérfanos. + +## Menos solicitudes de red + +Se eliminaron llamadas innecesarias que se hacían al establecer y cerrar conexiones. + +--- + +## Repositorios + +| Repo | Rol | +|---|---| +| `core` | Lógica de negocio, controladores, modelos | +| `cli` | Interfaz de línea de comandos | +| `essentials` | Módulos de red (Tor, WireGuard, proxies) | +| `installer` | Scripts de instalación y killswitch | + +--- +--- + +## Uso rápido — Desde Core (innecesario, usar CLI) ## 1. Create billing code / Crear billing code diff --git a/core/Constants.py b/core/Constants.py index 1c7d4a0..a6e755a 100644 --- a/core/Constants.py +++ b/core/Constants.py @@ -51,7 +51,7 @@ class Constants: HV_SESSION_STATE_HOME: Final[str] = f'{HV_STATE_HOME}/sessions' HV_TOR_STATE_HOME: Final[str] = f'{HV_STATE_HOME}/tor' - # ── sing-box ────────────────────────────────────────────────────────────── +# ── sing-box ────────────────────────────────────────────────────────────── SINGBOX_WRAPPER: Final[str] = os.environ.get( 'SINGBOX_WRAPPER', '/usr/local/bin/hydraveil-singbox' ) @@ -61,6 +61,9 @@ class Constants: SINGBOX_CONFIG_DIR: Final[str] = f'{HV_DATA_HOME}/configs' SINGBOX_PID_FILE: Final[str] = f'{HV_RUNTIME_DATA_HOME}/singbox.pid' SINGBOX_LOG_FILE: Final[str] = f'{HV_RUNTIME_DATA_HOME}/singbox.log' + SINGBOX_TUN_IF: Final[str] = os.environ.get('SINGBOX_TUN_IF', 'tun0') + SINGBOX_INTERNAL_SUBNET: Final[str] = os.environ.get('SINGBOX_INTERNAL_SUBNET', '172.19.0.0/30') + SINGBOX_INTERNAL_ADDR: Final[str] = os.environ.get('SINGBOX_INTERNAL_ADDR', '172.19.0.1/30') # ── killswitch / dns wrappers ───────────────────────────────────────────── KILLSWITCH_WRAPPER: Final[str] = os.environ.get( @@ -68,4 +71,7 @@ class Constants: ) RESOLVECTL_WRAPPER: Final[str] = os.environ.get( 'RESOLVECTL_WRAPPER', '/usr/local/bin/hydraveil-resolvectl' - ) \ No newline at end of file + ) + + VLESS_DNS_ENABLED: Final[bool] = os.environ.get('VLESS_DNS_ENABLED', 'true').lower() == 'true' + HYSTERIA2_DNS_ENABLED: Final[bool] = os.environ.get('HYSTERIA2_DNS_ENABLED', 'true').lower() == 'true' \ No newline at end of file diff --git a/core/controllers/ConnectionController.py b/core/controllers/ConnectionController.py index 623dbdf..a6cedb5 100644 --- a/core/controllers/ConnectionController.py +++ b/core/controllers/ConnectionController.py @@ -19,6 +19,7 @@ import os import random import re import shutil +import signal import subprocess import sys import tempfile @@ -143,6 +144,7 @@ class ConnectionController: raise ConnectionError('The connection could not be established.') return None + @staticmethod def establish_session_connection(profile: SessionProfile, ignore: tuple[type[Exception]] = (), connection_observer: Optional[ConnectionObserver] = None): @@ -208,8 +210,32 @@ class ConnectionController: SessionStateController.update_or_create(session_state) return proxy_port_number or port_number + + @staticmethod + def _signal_previous_process() -> None: + + system_state = SystemStateController.get() + if system_state is None: + return + pid = system_state.pid + if pid is None or pid == os.getpid(): + return + try: + os.kill(pid, signal.SIGUSR1) + time.sleep(0.5) + except (ProcessLookupError, PermissionError): + pass + @staticmethod def establish_system_connection(profile: SystemProfile, ignore: tuple[type[Exception]] = (), connection_observer: Optional[ConnectionObserver] = None): + ConnectionController._signal_previous_process() + + system_state = SystemStateController.get() + if system_state is not None: + try: + ConnectionController.terminate_system_connection(connection_observer=connection_observer) + except ConnectionTerminationError: + pass if profile.connection.needs_operator_proxy(): operator_proxy_session = profile.get_operator_proxy_session() @@ -217,17 +243,29 @@ class ConnectionController: if protocol == 'vless': from core.controllers.encrypted_proxy.VlessController import VlessController + from core.services.encrypted_proxy.vless_service import parse_vless_link + import socket + server_ip = operator_proxy_session.server_ip + if server_ip is None: + vless = parse_vless_link(operator_proxy_session.links[0]) + server_ip = socket.gethostbyname(vless['host']) ok = VlessController(1080).enable( operator_proxy_session.links[0], operator_proxy_session.username, + server_ip, connection_observer ) elif protocol == 'hysteria2': from core.controllers.encrypted_proxy.HysteriaController import HysteriaController + import socket + server_ip = operator_proxy_session.server_ip + if server_ip is None: + server_ip = socket.gethostbyname(operator_proxy_session.operator_hysteria2_host) ok = HysteriaController(1080).enable( operator_proxy_session.username, operator_proxy_session.password, operator_proxy_session.operator_hysteria2_host, + server_ip, connection_observer ) else: @@ -236,8 +274,11 @@ class ConnectionController: if not ok: raise ConnectionError('The connection could not be established.') - SystemStateController.create(profile.id) + token = SystemStateController.create(profile.id) + if connection_observer is not None: + connection_observer.notify('connected_token', {'session_token': token}) return + if ConfigurationController.get_endpoint_verification_enabled(): ProfileController.verify_wireguard_endpoint(profile, ignore=ignore) @@ -267,6 +308,7 @@ class ConnectionController: ConnectionController.terminate_tor_connection() time.sleep(1.0) + @staticmethod def establish_tor_connection(connection_observer: Optional[ConnectionObserver] = None): @@ -484,7 +526,9 @@ class ConnectionController: except CalledProcessError: raise ConnectionError('The connection could not be established.') - SystemStateController.create(profile.id) + token = SystemStateController.create(profile.id) + if connection_observer is not None: + connection_observer.notify('connected_token', {'session_token': token}) try: ConnectionController.await_connection(connection_observer=connection_observer) @@ -557,22 +601,22 @@ class ConnectionController: return True return False - + @staticmethod def verify_singbox_installation(): import subprocess import os from core.Errors import SingboxNotInstalledException - + singbox_binary = '/usr/bin/sing-box' singbox_wrapper = '/usr/local/bin/hydraveil-singbox' - + if not os.path.exists(singbox_binary): raise SingboxNotInstalledException(f'sing-box binary not found at {singbox_binary}') - + if not os.path.exists(singbox_wrapper): raise SingboxNotInstalledException(f'sing-box wrapper not found at {singbox_wrapper}') - + try: subprocess.run([singbox_binary, 'version'], capture_output=True, timeout=5, check=True) except subprocess.CalledProcessError: diff --git a/core/controllers/SystemStateController.py b/core/controllers/SystemStateController.py index 18bb459..e5571c2 100644 --- a/core/controllers/SystemStateController.py +++ b/core/controllers/SystemStateController.py @@ -1,3 +1,5 @@ +import os +import uuid from core.models.system.SystemState import SystemState @@ -13,7 +15,10 @@ class SystemStateController: @staticmethod def create(profile_id): - return SystemState(profile_id).save() + token = str(uuid.uuid4()) + pid = os.getpid() + SystemState(profile_id, token, pid).save() + return token @staticmethod def update_or_create(system_state): @@ -21,4 +26,4 @@ class SystemStateController: @staticmethod def dissolve(): - return SystemState.dissolve() + return SystemState.dissolve() \ No newline at end of file diff --git a/core/controllers/encrypted_proxy/HysteriaController.py b/core/controllers/encrypted_proxy/HysteriaController.py index 233c282..5521ccb 100644 --- a/core/controllers/encrypted_proxy/HysteriaController.py +++ b/core/controllers/encrypted_proxy/HysteriaController.py @@ -5,7 +5,7 @@ class HysteriaController: self.socks5_port = socks5_port def enable(self, username: str, password: str, - server_host: str, observer=None) -> bool: + server_host: str, server_ip: str, observer=None) -> bool: if not username or not isinstance(username, str): if observer: observer.notify("error", "Invalid username") @@ -14,10 +14,15 @@ class HysteriaController: if observer: observer.notify("error", "Missing password or server_host") return False + if not server_ip: + if observer: + observer.notify("error", "Missing server_ip") + return False return enable_hysteria( username=username, password=password, server_host=server_host, + server_ip=server_ip, socks5_port=self.socks5_port, observer=observer, ) diff --git a/core/controllers/encrypted_proxy/VlessController.py b/core/controllers/encrypted_proxy/VlessController.py index a969cef..57f7f3e 100644 --- a/core/controllers/encrypted_proxy/VlessController.py +++ b/core/controllers/encrypted_proxy/VlessController.py @@ -4,7 +4,7 @@ class VlessController: def __init__(self, socks5_port: int): self.socks5_port = socks5_port - def enable(self, vless_link: str, username: str, observer=None) -> bool: + def enable(self, vless_link: str, username: str, server_ip: str, observer=None) -> bool: if not username or not isinstance(username, str): if observer: observer.notify("error", "Invalid username") @@ -13,9 +13,14 @@ class VlessController: if observer: observer.notify("error", "Invalid vless link") return False + if not server_ip: + if observer: + observer.notify("error", "Missing server_ip") + return False return enable_vless( vless_link=vless_link, username=username, + server_ip=server_ip, socks5_port=self.socks5_port, observer=observer, ) diff --git a/core/models/OperatorProxySession.py b/core/models/OperatorProxySession.py index 5bdfa6f..065d18d 100644 --- a/core/models/OperatorProxySession.py +++ b/core/models/OperatorProxySession.py @@ -16,4 +16,5 @@ class OperatorProxySession: operator_name: str operator_domain: Optional[str] = None operator_hysteria2_host: Optional[str] = None - operator_vless_host: Optional[str] = None \ No newline at end of file + operator_vless_host: Optional[str] = None + server_ip: Optional[str] = None # pre-resolved IP — avoids DNS leak at connect time \ No newline at end of file diff --git a/core/models/system/SystemState.py b/core/models/system/SystemState.py index 2187faa..d90efdc 100644 --- a/core/models/system/SystemState.py +++ b/core/models/system/SystemState.py @@ -1,8 +1,8 @@ from core.Constants import Constants -from dataclasses import dataclass +from dataclasses import dataclass, field from dataclasses_json import dataclass_json from pathlib import Path -from typing import Self +from typing import Optional, Self import json import os import pathlib @@ -11,6 +11,8 @@ import pathlib @dataclass class SystemState: profile_id: int + session_token: Optional[str] = field(default=None) + pid: Optional[int] = field(default=None) def save(self: Self): @@ -31,7 +33,6 @@ class SystemState: system_state_file_contents = open(f'{SystemState.__get_state_path()}/system.json', 'r').read() system_state_dict = json.loads(system_state_file_contents) - # noinspection PyUnresolvedReferences return SystemState.from_dict(system_state_dict) except (FileNotFoundError, ValueError, KeyError): @@ -53,4 +54,4 @@ class SystemState: @staticmethod def __get_state_path(): - return Constants.HV_STATE_HOME + return Constants.HV_STATE_HOME \ No newline at end of file diff --git a/core/services/encrypted_proxy/hysteria_service.py b/core/services/encrypted_proxy/hysteria_service.py index e49f1fd..f93a513 100644 --- a/core/services/encrypted_proxy/hysteria_service.py +++ b/core/services/encrypted_proxy/hysteria_service.py @@ -1,23 +1,14 @@ -import socket import time from pathlib import Path from core.Constants import Constants -from core.utils.encrypted_proxy.net import get_real_ip, verify_ip from core.utils.encrypted_proxy.singbox import SingboxRunner from core.utils.encrypted_proxy.dns import wait_for_tun, set_dns_on_tun, revert_dns_on_tun from core.utils.encrypted_proxy import killswitch -def _resolve(host: str) -> str: - return socket.gethostbyname(host) - - def build_hysteria_config(username: str, password: str, server_host: str, socks5_port: int, - server_ip: str = None) -> dict: - if server_ip is None: - server_ip = _resolve(server_host) - + server_ip: str) -> dict: return { "dns": { "servers": [{"tag": "tunnel-dns", "type": "udp", "server": "9.9.9.9"}], @@ -29,8 +20,8 @@ def build_hysteria_config(username: str, password: str, { "type": "tun", "tag": "tun-in", - "interface_name": "tun0", - "address": ["172.19.0.1/30"], + "interface_name": Constants.SINGBOX_TUN_IF, + "address": [Constants.SINGBOX_INTERNAL_ADDR], "mtu": 9000, "auto_route": True, "stack": "gvisor", @@ -61,7 +52,7 @@ def build_hysteria_config(username: str, password: str, "route": { "rules": [ {"protocol": "dns", "action": "hijack-dns"}, - {"ip_cidr": ["172.19.0.0/30"], "action": "hijack-dns"}, + {"ip_cidr": [Constants.SINGBOX_INTERNAL_SUBNET], "action": "hijack-dns"}, {"ip_cidr": [f"{server_ip}/32"], "outbound": "direct"}, {"ip_is_private": True, "outbound": "direct"}, {"ip_version": 6, "outbound": "block"}, @@ -91,64 +82,58 @@ def _cleanup_all() -> None: def enable_hysteria(username: str, password: str, server_host: str, - socks5_port: int, observer=None) -> bool: + server_ip: str, socks5_port: int, observer=None) -> bool: _cleanup_all() - server_ip = _resolve(server_host) - real_ip = get_real_ip() - - if not killswitch.arm(server_ip): - if observer: - observer.notify("error", "Failed to arm kill switch") - return False - - runner = SingboxRunner() - runner.stop() + runner = SingboxRunner() config_path = Path(Constants.SINGBOX_CONFIG_DIR) / f"{username}-sing-box.json" - config = build_hysteria_config(username, password, server_host, socks5_port, - server_ip=server_ip) + config = build_hysteria_config(username, password, server_host, + socks5_port, server_ip) + _connected = False try: + # Fase 1 — arrancar sing-box runner.write_config(config_path, config) - ok = runner.start(config_path) + if not runner.start(config_path): + if observer: + observer.notify("error", "sing-box not active after start") + return False + + # Fase 2 — esperar a que tun aparezca + if not wait_for_tun(timeout=15.0): + if observer: + observer.notify("error", f"{Constants.SINGBOX_TUN_IF} did not appear after 15s") + return False + + # Fase 3 — armar kill switch ahora que la interfaz existe + if not killswitch.arm(server_ip, Constants.SINGBOX_TUN_IF): + if observer: + observer.notify("error", "Failed to arm kill switch") + return False + + # Fase 4 — DNS + _C = Constants() + dns_ok = set_dns_on_tun() if _C.HYSTERIA2_DNS_ENABLED else False + + _connected = True + if observer: + observer.notify("connected", { + "tunnel_if": Constants.SINGBOX_TUN_IF, + "socks5_port": socks5_port, + "dns_enabled": _C.HYSTERIA2_DNS_ENABLED, + "dns_active": dns_ok, + }) + return True + except Exception as e: - killswitch.disarm() if observer: observer.notify("error", str(e)) return False - if not ok: - killswitch.disarm() - if observer: - observer.notify("error", "sing-box not active after start") - return False - - if not wait_for_tun(timeout=15.0): - killswitch.disarm() - runner.stop() - if observer: - observer.notify("error", "tun0 did not appear after 15s") - return False - - set_dns_on_tun() - - proxy_ip = verify_ip(socks5_port, retries=5, delay=3.0) - if not proxy_ip or proxy_ip == "unknown" or proxy_ip == real_ip: - killswitch.disarm() - revert_dns_on_tun() - runner.stop() - if observer: - observer.notify("error", "IP did not change — possible leak") - return False - - if observer: - observer.notify("connected", { - "real_ip": real_ip, - "proxy_ip": proxy_ip, - "socks5_port": socks5_port, - }) - return True + finally: + if not _connected: + _cleanup_all() def disable_hysteria(observer=None) -> bool: @@ -156,5 +141,5 @@ def disable_hysteria(observer=None) -> bool: SingboxRunner().stop() killswitch.disarm() if observer: - observer.notify("disconnected", {}) + 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 9930d55..0cdfa96 100644 --- a/core/services/encrypted_proxy/vless_service.py +++ b/core/services/encrypted_proxy/vless_service.py @@ -3,7 +3,6 @@ from pathlib import Path import socket import time from core.Constants import Constants -from core.utils.encrypted_proxy.net import get_real_ip, verify_ip from core.utils.encrypted_proxy.singbox import SingboxRunner from core.utils.encrypted_proxy.dns import wait_for_tun, set_dns_on_tun, revert_dns_on_tun from core.utils.encrypted_proxy import killswitch @@ -34,10 +33,7 @@ def parse_vless_link(link: str) -> dict: } -def build_vless_config(vless: dict, socks5_port: int, server_ip: str = None) -> dict: - if server_ip is None: - server_ip = socket.gethostbyname(vless["host"]) - +def build_vless_config(vless: dict, socks5_port: int, server_ip: str) -> dict: return { "dns": { "servers": [{"tag": "tunnel-dns", "type": "udp", "server": "9.9.9.9"}], @@ -49,8 +45,8 @@ def build_vless_config(vless: dict, socks5_port: int, server_ip: str = None) -> { "type": "tun", "tag": "tun-in", - "interface_name": "tun0", - "address": ["172.19.0.1/30"], + "interface_name": Constants.SINGBOX_TUN_IF, + "address": [Constants.SINGBOX_INTERNAL_ADDR], "mtu": 9000, "auto_route": True, "stack": "gvisor", @@ -86,7 +82,7 @@ def build_vless_config(vless: dict, socks5_port: int, server_ip: str = None) -> "route": { "rules": [ {"protocol": "dns", "action": "hijack-dns"}, - {"ip_cidr": ["172.19.0.0/30"], "action": "hijack-dns"}, + {"ip_cidr": [Constants.SINGBOX_INTERNAL_SUBNET], "action": "hijack-dns"}, {"ip_cidr": [f"{server_ip}/32"], "outbound": "direct"}, {"ip_is_private": True, "outbound": "direct"}, {"ip_version": 6, "outbound": "block"}, @@ -115,65 +111,55 @@ def _cleanup_all() -> None: time.sleep(1) -def enable_vless(vless_link: str, username: str, +def enable_vless(vless_link: str, username: str, server_ip: str, socks5_port: int, observer=None) -> bool: _cleanup_all() - vless = parse_vless_link(vless_link) - server_ip = socket.gethostbyname(vless["host"]) - real_ip = get_real_ip() - - if not killswitch.arm(server_ip): - if observer: - observer.notify("error", "Failed to arm kill switch") - return False - - runner = SingboxRunner() - runner.stop() + vless = parse_vless_link(vless_link) + runner = SingboxRunner() config_path = Path(Constants.SINGBOX_CONFIG_DIR) / f"{username}-sing-box.json" - config = build_vless_config(vless, socks5_port, server_ip=server_ip) + config = build_vless_config(vless, socks5_port, server_ip) + _connected = False try: runner.write_config(config_path, config) - ok = runner.start(config_path) + if not runner.start(config_path): + if observer: + observer.notify("error", "sing-box not active after start") + return False + + if not wait_for_tun(timeout=15.0): + if observer: + observer.notify("error", f"{Constants.SINGBOX_TUN_IF} did not appear after 15s") + return False + + if not killswitch.arm(server_ip, Constants.SINGBOX_TUN_IF): + if observer: + observer.notify("error", "Failed to arm kill switch") + return False + + _C = Constants() + dns_ok = set_dns_on_tun() if _C.VLESS_DNS_ENABLED else False + + _connected = True + if observer: + observer.notify("connected", { + "tunnel_if": Constants.SINGBOX_TUN_IF, + "socks5_port": socks5_port, + "dns_enabled": _C.VLESS_DNS_ENABLED, + "dns_active": dns_ok, + }) + return True + except Exception as e: - killswitch.disarm() if observer: observer.notify("error", str(e)) return False - if not ok: - killswitch.disarm() - if observer: - observer.notify("error", "sing-box not active after start") - return False - - if not wait_for_tun(timeout=15.0): - killswitch.disarm() - runner.stop() - if observer: - observer.notify("error", "tun0 did not appear after 15s") - return False - - set_dns_on_tun() - - proxy_ip = verify_ip(socks5_port, retries=5, delay=3.0) - if not proxy_ip or proxy_ip == "unknown" or proxy_ip == real_ip: - killswitch.disarm() - revert_dns_on_tun() - runner.stop() - if observer: - observer.notify("error", "IP did not change — possible leak") - return False - - if observer: - observer.notify("connected", { - "real_ip": real_ip, - "proxy_ip": proxy_ip, - "socks5_port": socks5_port, - }) - return True + finally: + if not _connected: + _cleanup_all() def disable_vless(observer=None) -> bool: @@ -181,5 +167,5 @@ def disable_vless(observer=None) -> bool: SingboxRunner().stop() killswitch.disarm() if observer: - observer.notify("disconnected", {}) + observer.notify("disconnected", {"tunnel_if": Constants.SINGBOX_TUN_IF}) return True \ No newline at end of file diff --git a/core/utils/encrypted_proxy/dns.py b/core/utils/encrypted_proxy/dns.py index 7b122b0..035939e 100644 --- a/core/utils/encrypted_proxy/dns.py +++ b/core/utils/encrypted_proxy/dns.py @@ -1,10 +1,9 @@ import os import subprocess import time +from core.Constants import Constants -RESOLVECTL_WRAPPER = "/usr/local/bin/hydraveil-resolvectl" -TUN_IFACE = "tun0" -TUN_DNS = "9.9.9.9" +_C = Constants() _SUDO_KW = dict( stdout=subprocess.DEVNULL, @@ -15,11 +14,24 @@ _SUDO_KW = dict( ) +def is_resolved_active() -> bool: + try: + result = subprocess.run( + ["systemctl", "is-active", "systemd-resolved"], + capture_output=True, + text=True, + timeout=5, + ) + return result.stdout.strip() == "active" + except (subprocess.TimeoutExpired, FileNotFoundError): + return False + + def wait_for_tun(timeout: float = 15.0, interval: float = 0.5) -> bool: elapsed = 0.0 while elapsed < timeout: result = subprocess.run( - ["ip", "link", "show", TUN_IFACE], + ["ip", "link", "show", _C.SINGBOX_TUN_IF], capture_output=True, ) if result.returncode == 0: @@ -30,22 +42,31 @@ def wait_for_tun(timeout: float = 15.0, interval: float = 0.5) -> bool: def set_dns_on_tun() -> bool: + + if not is_resolved_active(): + print("[DNS] systemd-resolved is not active — DNS on tun skipped") + return False + result = subprocess.run( - ["sudo", RESOLVECTL_WRAPPER, "set", TUN_DNS], + ["sudo", _C.RESOLVECTL_WRAPPER, "set", "9.9.9.9"], **_SUDO_KW, ) if result.returncode != 0: - print(f"[DNS] Failed to configure tun0: {result.stderr.strip()}") + print(f"[DNS] Failed to configure {_C.SINGBOX_TUN_IF}: {result.stderr.strip()}") return False return True def revert_dns_on_tun() -> bool: + + if not is_resolved_active(): + return True + result = subprocess.run( - ["sudo", RESOLVECTL_WRAPPER, "revert"], + ["sudo", _C.RESOLVECTL_WRAPPER, "revert"], **_SUDO_KW, ) if result.returncode != 0: - print(f"[DNS] Failed to revert tun0: {result.stderr.strip()}") + print(f"[DNS] Failed to revert {_C.SINGBOX_TUN_IF}: {result.stderr.strip()}") return False return True \ No newline at end of file diff --git a/core/utils/encrypted_proxy/killswitch.py b/core/utils/encrypted_proxy/killswitch.py index 09235bb..3629976 100644 --- a/core/utils/encrypted_proxy/killswitch.py +++ b/core/utils/encrypted_proxy/killswitch.py @@ -1,13 +1,14 @@ import subprocess +from core.Constants import Constants -KILLSWITCH_WRAPPER = "/usr/local/bin/hydraveil-killswitch" +_C = Constants() -def arm(server_ip: str) -> bool: +def arm(server_ip: str, tunnel_if: str) -> bool: result = subprocess.run( - ["sudo", KILLSWITCH_WRAPPER, "arm", server_ip], + ["sudo", _C.KILLSWITCH_WRAPPER, "arm", server_ip, tunnel_if], capture_output=True, - text=True + text=True, ) if result.returncode != 0: print(f"[killswitch] Failed to arm: {result.stderr.strip()}") @@ -17,9 +18,9 @@ def arm(server_ip: str) -> bool: def disarm() -> bool: result = subprocess.run( - ["sudo", KILLSWITCH_WRAPPER, "disarm"], + ["sudo", _C.KILLSWITCH_WRAPPER, "disarm"], capture_output=True, - text=True + text=True, ) if result.returncode != 0: print(f"[killswitch] Failed to disarm: {result.stderr.strip()}") @@ -29,8 +30,8 @@ def disarm() -> bool: def status() -> bool: result = subprocess.run( - ["sudo", KILLSWITCH_WRAPPER, "status"], + ["sudo", _C.KILLSWITCH_WRAPPER, "status"], capture_output=True, - text=True + text=True, ) return result.returncode == 0 and result.stdout.strip() == "armed" \ No newline at end of file