sp-hydra-veil-cli/cli/killswitch_monitor.py
2026-06-19 06:42:50 -05:00

130 lines
No EOL
3.9 KiB
Python

import subprocess
import threading
import time
from core.utils.encrypted_proxy import killswitch
from core.utils.encrypted_proxy.singbox import SingboxRunner
drop_state: dict = {"count": 0}
tunnel_state: dict = {"alive": True}
CAUSE_DISCONNECT = 'disconnect'
CAUSE_LEAK = 'leak'
CAUSE_DISPLACED = 'displaced'
class KillswitchMonitor:
POLL_INTERVAL = 5
LOG_PREFIX = "hydraveil-drop"
def __init__(self):
self._on_disconnect = None
self._on_leak = None
self._on_displaced = None
self._socks5_port = None
self._session_token = None
self._running = False
self._stop_event = threading.Event()
self._cause_event = threading.Event()
self._cause: str | None = None
self._last_poll_ts: str | None = None
def set_on_disconnect(self, fn):
self._on_disconnect = fn
def set_on_leak(self, fn):
self._on_leak = fn
def set_on_displaced(self, fn):
self._on_displaced = fn
def set_connection_data(self, socks5_port: int):
self._socks5_port = socks5_port
def set_session_token(self, token: str):
self._session_token = token
def _poll_drops(self) -> None:
since = self._last_poll_ts or "10 seconds ago"
try:
result = subprocess.run(
[
"journalctl", "-k", "--no-pager",
f"--grep={self.LOG_PREFIX}",
"--since", since,
"-o", "short-iso",
],
capture_output=True,
text=True,
timeout=5,
)
new_drops = result.stdout.count(self.LOG_PREFIX)
if new_drops:
drop_state["count"] += new_drops
except (subprocess.TimeoutExpired, FileNotFoundError):
pass
finally:
self._last_poll_ts = time.strftime("%Y-%m-%dT%H:%M:%S")
def stop(self):
self._running = False
self._stop_event.set()
def _trigger(self, cause: str):
self._cause = cause
self._running = False
self._cause_event.set()
self._stop_event.set()
def _monitor_loop(self):
while self._running:
interrupted = self._stop_event.wait(timeout=self.POLL_INTERVAL)
if interrupted or not self._running:
break
if not killswitch.status():
tunnel_state["alive"] = False
self._trigger(CAUSE_DISCONNECT)
return
singbox_alive = SingboxRunner().is_running()
tunnel_state["alive"] = singbox_alive
if not singbox_alive:
self._trigger(CAUSE_LEAK)
return
if self._session_token is not None:
from core.models.system.SystemState import SystemState
current = SystemState.get()
if current is None or current.session_token != self._session_token:
tunnel_state["alive"] = False
self._trigger(CAUSE_DISPLACED)
return
self._poll_drops()
def run_blocking(self):
self._running = True
self._stop_event.clear()
self._cause_event.clear()
self._cause = None
drop_state["count"] = 0
tunnel_state["alive"] = True
self._last_poll_ts = time.strftime("%Y-%m-%dT%H:%M:%S")
t = threading.Thread(target=self._monitor_loop, daemon=True)
t.start()
self._cause_event.wait()
t.join(timeout=2)
cause = self._cause
if cause == CAUSE_DISCONNECT:
if self._on_disconnect:
self._on_disconnect()
elif cause == CAUSE_LEAK:
if self._on_leak:
self._on_leak()
elif cause == CAUSE_DISPLACED:
if self._on_displaced:
self._on_displaced()