TunnelMonitor with is_tunnel_alive(), add WireGuardMonitor, arm/disarm killswitch on WireGuard connect/disconnect
This commit is contained in:
parent
a4bbfa4ec5
commit
9c5636eb8c
8 changed files with 159 additions and 17 deletions
|
|
@ -441,6 +441,11 @@ class ConnectionController:
|
||||||
if completed_successfully or not ConnectionController.system_uses_wireguard_interface():
|
if completed_successfully or not ConnectionController.system_uses_wireguard_interface():
|
||||||
subprocess.run(('nmcli', 'connection', 'delete', 'hv-ipv6-sink'), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
subprocess.run(('nmcli', 'connection', 'delete', 'hv-ipv6-sink'), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||||
ConnectionController.terminate_tor_connection()
|
ConnectionController.terminate_tor_connection()
|
||||||
|
try:
|
||||||
|
from core.utils.encrypted_proxy import killswitch
|
||||||
|
killswitch.disarm()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
SystemState.dissolve()
|
SystemState.dissolve()
|
||||||
if connection_observer is not None:
|
if connection_observer is not None:
|
||||||
connection_observer.notify('disconnected', {})
|
connection_observer.notify('disconnected', {})
|
||||||
|
|
@ -549,6 +554,15 @@ class ConnectionController:
|
||||||
except CalledProcessError:
|
except CalledProcessError:
|
||||||
raise ConnectionError('The connection could not be established.')
|
raise ConnectionError('The connection could not be established.')
|
||||||
|
|
||||||
|
# Arm killswitch for WireGuard
|
||||||
|
try:
|
||||||
|
wg_server_ip = ConnectionController.__extract_wireguard_endpoint(profile)
|
||||||
|
if wg_server_ip:
|
||||||
|
from core.utils.encrypted_proxy import killswitch
|
||||||
|
killswitch.arm(wg_server_ip, 'wg')
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
token = SystemStateController.create(profile.id)
|
token = SystemStateController.create(profile.id)
|
||||||
if connection_observer is not None:
|
if connection_observer is not None:
|
||||||
connection_observer.notify('connected_token', {'session_token': token})
|
connection_observer.notify('connected_token', {'session_token': token})
|
||||||
|
|
@ -559,6 +573,23 @@ class ConnectionController:
|
||||||
except ConnectionError:
|
except ConnectionError:
|
||||||
raise ConnectionError('The connection could not be established.')
|
raise ConnectionError('The connection could not be established.')
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def __extract_wireguard_endpoint(profile):
|
||||||
|
import re, socket
|
||||||
|
try:
|
||||||
|
config = profile.get_wireguard_configuration()
|
||||||
|
if config is None:
|
||||||
|
return None
|
||||||
|
match = re.search(r'Endpoint\s*=\s*([^\s:]+):\d+', config)
|
||||||
|
if not match:
|
||||||
|
return None
|
||||||
|
host = match.group(1)
|
||||||
|
if re.match(r'^(\d{1,3}\.){3}\d{1,3}$', host):
|
||||||
|
return host
|
||||||
|
return socket.gethostbyname(host)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __with_tor_connection(*args, task: Callable[..., Any], connection_observer: Optional[ConnectionObserver] = None, **kwargs):
|
def __with_tor_connection(*args, task: Callable[..., Any], connection_observer: Optional[ConnectionObserver] = None, **kwargs):
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -104,7 +104,7 @@ def enable_hysteria(username: str, password: str, server_host: str,
|
||||||
observer.notify("error", f"{Constants.SINGBOX_TUN_IF} did not appear after 15s")
|
observer.notify("error", f"{Constants.SINGBOX_TUN_IF} did not appear after 15s")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if not killswitch.arm(server_ip, Constants.SINGBOX_TUN_IF):
|
if not killswitch.arm(server_ip, Constants.SINGBOX_TUN_IF, Constants.SINGBOX_INTERNAL_SUBNET):
|
||||||
if observer:
|
if observer:
|
||||||
observer.notify("error", "Failed to arm kill switch")
|
observer.notify("error", "Failed to arm kill switch")
|
||||||
return False
|
return False
|
||||||
|
|
|
||||||
|
|
@ -134,7 +134,7 @@ def enable_vless(vless_link: str, username: str, server_ip: str,
|
||||||
observer.notify("error", f"{Constants.SINGBOX_TUN_IF} did not appear after 15s")
|
observer.notify("error", f"{Constants.SINGBOX_TUN_IF} did not appear after 15s")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if not killswitch.arm(server_ip, Constants.SINGBOX_TUN_IF):
|
if not killswitch.arm(server_ip, Constants.SINGBOX_TUN_IF, Constants.SINGBOX_INTERNAL_SUBNET):
|
||||||
if observer:
|
if observer:
|
||||||
observer.notify("error", "Failed to arm kill switch")
|
observer.notify("error", "Failed to arm kill switch")
|
||||||
return False
|
return False
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,11 @@ from core.Constants import Constants
|
||||||
_C = Constants()
|
_C = Constants()
|
||||||
|
|
||||||
|
|
||||||
def arm(server_ip: str, tunnel_if: str) -> bool:
|
def arm(server_ip: str, tunnel_if: str, internal_subnet: str = None) -> bool:
|
||||||
result = subprocess.run(
|
cmd = ["sudo", _C.KILLSWITCH_WRAPPER, "arm", server_ip, tunnel_if]
|
||||||
["sudo", _C.KILLSWITCH_WRAPPER, "arm", server_ip, tunnel_if],
|
if internal_subnet:
|
||||||
capture_output=True,
|
cmd.append(internal_subnet)
|
||||||
text=True,
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||||
)
|
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
print(f"[killswitch] Failed to arm: {result.stderr.strip()}")
|
print(f"[killswitch] Failed to arm: {result.stderr.strip()}")
|
||||||
return False
|
return False
|
||||||
|
|
|
||||||
|
|
@ -10,12 +10,13 @@ tunnel_state: dict = {"alive": True}
|
||||||
|
|
||||||
|
|
||||||
class KillswitchMonitor(SingboxProcessMonitor):
|
class KillswitchMonitor(SingboxProcessMonitor):
|
||||||
"""CLI-specific monitor: adds killswitch + drops detection."""
|
"""CLI-specific monitor for sing-box (vless/hysteria2): checks sing-box process health."""
|
||||||
|
|
||||||
LOG_PREFIX = "hydraveil-drop"
|
LOG_PREFIX = "hydraveil-drop"
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__(SingboxRunner())
|
super().__init__()
|
||||||
|
self._runner = SingboxRunner()
|
||||||
self._on_disconnect = None
|
self._on_disconnect = None
|
||||||
self._on_leak = None
|
self._on_leak = None
|
||||||
self._on_displaced = None
|
self._on_displaced = None
|
||||||
|
|
@ -43,6 +44,9 @@ class KillswitchMonitor(SingboxProcessMonitor):
|
||||||
|
|
||||||
# ── Abstract impl ─────────────────────────────────────────────────────────
|
# ── Abstract impl ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def is_tunnel_alive(self) -> bool:
|
||||||
|
return self._runner.is_running()
|
||||||
|
|
||||||
def on_process_alive(self) -> None:
|
def on_process_alive(self) -> None:
|
||||||
tunnel_state["alive"] = True
|
tunnel_state["alive"] = True
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -270,10 +270,9 @@ class SingboxProcessMonitor(ABC):
|
||||||
threading model (threading.Event for CLI, Qt signals for GUI).
|
threading model (threading.Event for CLI, Qt signals for GUI).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
POLL_INTERVAL = 5
|
POLL_INTERVAL = 10
|
||||||
|
|
||||||
def __init__(self, runner: SingboxRunner):
|
def __init__(self):
|
||||||
self._runner = runner
|
|
||||||
self._lock = threading.Lock()
|
self._lock = threading.Lock()
|
||||||
self._running = False
|
self._running = False
|
||||||
self._stop_event = threading.Event()
|
self._stop_event = threading.Event()
|
||||||
|
|
@ -284,6 +283,11 @@ class SingboxProcessMonitor(ABC):
|
||||||
self._running = False
|
self._running = False
|
||||||
self._stop_event.set()
|
self._stop_event.set()
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def is_tunnel_alive(self) -> bool:
|
||||||
|
"""Return True if the tunnel is healthy."""
|
||||||
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def on_process_alive(self) -> None:
|
def on_process_alive(self) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
@ -306,7 +310,7 @@ class SingboxProcessMonitor(ABC):
|
||||||
if interrupted:
|
if interrupted:
|
||||||
break
|
break
|
||||||
|
|
||||||
alive = self._runner.is_running()
|
alive = self.is_tunnel_alive()
|
||||||
if alive:
|
if alive:
|
||||||
self.on_process_alive()
|
self.on_process_alive()
|
||||||
else:
|
else:
|
||||||
|
|
@ -327,5 +331,5 @@ class SingboxProcessMonitor(ABC):
|
||||||
self._running = False
|
self._running = False
|
||||||
|
|
||||||
t.join(timeout=5)
|
t.join(timeout=5)
|
||||||
self._runner.stop()
|
# tunnel teardown handled by subclass
|
||||||
self.on_monitor_stopped("user_requested")
|
self.on_monitor_stopped("user_requested")
|
||||||
0
core/utils/wireguard/__init__.py
Normal file
0
core/utils/wireguard/__init__.py
Normal file
104
core/utils/wireguard/wireguard_monitor.py
Normal file
104
core/utils/wireguard/wireguard_monitor.py
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
import subprocess
|
||||||
|
import threading
|
||||||
|
from core.utils.encrypted_proxy.singbox import SingboxProcessMonitor
|
||||||
|
from core.utils.encrypted_proxy.singbox import CAUSE_DISCONNECT, CAUSE_DISPLACED
|
||||||
|
|
||||||
|
|
||||||
|
class WireGuardMonitor(SingboxProcessMonitor):
|
||||||
|
"""Monitor for WireGuard system connections.
|
||||||
|
Checks if the wg interface is alive instead of a sing-box process.
|
||||||
|
Shared between CLI and GUI.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self._session_token = None
|
||||||
|
self._on_disconnect = None
|
||||||
|
self._on_displaced = 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_displaced(self, fn):
|
||||||
|
self._on_displaced = fn
|
||||||
|
|
||||||
|
def set_session_token(self, token: str):
|
||||||
|
self._session_token = token
|
||||||
|
|
||||||
|
# ── Tunnel health check ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def is_tunnel_alive(self) -> bool:
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
['ip', 'link', 'show', 'wg'],
|
||||||
|
capture_output=True,
|
||||||
|
timeout=5,
|
||||||
|
)
|
||||||
|
return result.returncode == 0
|
||||||
|
except (subprocess.TimeoutExpired, FileNotFoundError):
|
||||||
|
return False
|
||||||
|
|
||||||
|
# ── Abstract impl ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def on_process_alive(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def on_process_dead(self) -> None:
|
||||||
|
self._trigger(CAUSE_DISCONNECT)
|
||||||
|
|
||||||
|
def on_monitor_stopped(self, cause: str) -> None:
|
||||||
|
if cause == CAUSE_DISCONNECT and self._on_disconnect:
|
||||||
|
self._on_disconnect()
|
||||||
|
elif cause == CAUSE_DISPLACED and self._on_displaced:
|
||||||
|
self._on_displaced()
|
||||||
|
|
||||||
|
# ── Monitor loop override ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
if not self.is_tunnel_alive():
|
||||||
|
self._trigger(CAUSE_DISCONNECT)
|
||||||
|
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:
|
||||||
|
self._trigger(CAUSE_DISPLACED)
|
||||||
|
return
|
||||||
|
|
||||||
|
def _trigger(self, cause: str):
|
||||||
|
self._cause = cause
|
||||||
|
with self._lock:
|
||||||
|
self._running = False
|
||||||
|
self._cause_event.set()
|
||||||
|
self._stop_event.set()
|
||||||
|
|
||||||
|
# ── run_blocking override ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def run_blocking(self) -> None:
|
||||||
|
with self._lock:
|
||||||
|
self._running = True
|
||||||
|
|
||||||
|
self._stop_event.clear()
|
||||||
|
self._cause_event.clear()
|
||||||
|
self._cause = None
|
||||||
|
|
||||||
|
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")
|
||||||
Loading…
Reference in a new issue