143 lines
No EOL
4.9 KiB
Python
143 lines
No EOL
4.9 KiB
Python
import subprocess
|
|
import threading
|
|
import time
|
|
from core.utils.encrypted_proxy import killswitch
|
|
from core.utils.encrypted_proxy.singbox import SingboxRunner, SingboxProcessMonitor
|
|
from core.utils.encrypted_proxy.singbox import CAUSE_DISCONNECT, CAUSE_LEAK, CAUSE_DISPLACED
|
|
|
|
drop_state: dict = {"count": 0}
|
|
tunnel_state: dict = {"alive": True}
|
|
|
|
|
|
class KillswitchMonitor(SingboxProcessMonitor):
|
|
"""CLI-specific monitor: adds killswitch + drops detection."""
|
|
|
|
LOG_PREFIX = "hydraveil-drop"
|
|
|
|
def __init__(self):
|
|
super().__init__(SingboxRunner())
|
|
self._on_disconnect = None
|
|
self._on_leak = None
|
|
self._on_displaced = None
|
|
self._socks5_port = None
|
|
self._session_token = None
|
|
self._cause_event = threading.Event()
|
|
self._cause: str | None = None
|
|
|
|
# ── Setters ───────────────────────────────────────────────────────────────
|
|
|
|
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
|
|
|
|
# ── Abstract impl ─────────────────────────────────────────────────────────
|
|
|
|
def on_process_alive(self) -> None:
|
|
tunnel_state["alive"] = True
|
|
|
|
def on_process_dead(self) -> None:
|
|
tunnel_state["alive"] = False
|
|
|
|
def on_monitor_stopped(self, cause: str) -> None:
|
|
if cause == CAUSE_DISCONNECT and self._on_disconnect:
|
|
self._on_disconnect()
|
|
elif cause == CAUSE_LEAK and self._on_leak:
|
|
self._on_leak()
|
|
elif cause == CAUSE_DISPLACED and self._on_displaced:
|
|
self._on_displaced()
|
|
|
|
# ── Override monitor loop to add CLI-specific checks ──────────────────────
|
|
|
|
def _monitor_loop(self) -> None:
|
|
while True:
|
|
with self._lock:
|
|
if not self._running:
|
|
break
|
|
|
|
interrupted = self._stop_event.wait(timeout=self.POLL_INTERVAL)
|
|
if interrupted:
|
|
break
|
|
|
|
# Killswitch check
|
|
if not killswitch.status():
|
|
tunnel_state["alive"] = False
|
|
self._trigger(CAUSE_DISCONNECT)
|
|
return
|
|
|
|
# Sing-box check
|
|
alive = self._runner.is_running()
|
|
tunnel_state["alive"] = alive
|
|
if not alive:
|
|
self._trigger(CAUSE_LEAK)
|
|
return
|
|
|
|
# Session token check
|
|
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 _trigger(self, cause: str):
|
|
self._cause = cause
|
|
with self._lock:
|
|
self._running = False
|
|
self._cause_event.set()
|
|
self._stop_event.set()
|
|
|
|
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")
|
|
|
|
# ── Override run_blocking to handle cause_event ───────────────────────────
|
|
|
|
def run_blocking(self) -> None:
|
|
with self._lock:
|
|
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)
|
|
|
|
self.on_monitor_stopped(self._cause or "user_requested") |