June Major Update
This commit is contained in:
parent
abe350294d
commit
309ac7bd02
4 changed files with 190 additions and 36 deletions
|
|
@ -14,6 +14,8 @@ from cli.observers import profile_observer, application_version_observer, connec
|
||||||
from core.Errors import SingboxNotInstalledException
|
from core.Errors import SingboxNotInstalledException
|
||||||
from cli.killswitch_monitor import KillswitchMonitor
|
from cli.killswitch_monitor import KillswitchMonitor
|
||||||
import pprint
|
import pprint
|
||||||
|
import signal
|
||||||
|
import sys
|
||||||
|
|
||||||
NAME = 'profile'
|
NAME = 'profile'
|
||||||
|
|
||||||
|
|
@ -165,8 +167,28 @@ def handle(arguments, main_parser):
|
||||||
print()
|
print()
|
||||||
_do_disable()
|
_do_disable()
|
||||||
|
|
||||||
|
def _on_displaced():
|
||||||
|
_stop_timer()
|
||||||
|
monitor.stop()
|
||||||
|
print('\n')
|
||||||
|
print(' ⚡ Connection terminated: taken over by another session.')
|
||||||
|
print()
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
def _sigusr1_handler(signum, frame):
|
||||||
|
_on_displaced()
|
||||||
|
|
||||||
|
signal.signal(signal.SIGUSR1, _sigusr1_handler)
|
||||||
|
|
||||||
|
def _sighup_handler(signum, frame):
|
||||||
|
_do_disable()
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
signal.signal(signal.SIGHUP, _sighup_handler)
|
||||||
|
|
||||||
monitor.set_on_disconnect(_do_disable)
|
monitor.set_on_disconnect(_do_disable)
|
||||||
monitor.set_on_leak(_on_leak)
|
monitor.set_on_leak(_on_leak)
|
||||||
|
monitor.set_on_displaced(_on_displaced)
|
||||||
obs._monitor = monitor
|
obs._monitor = monitor
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,32 @@
|
||||||
import time
|
import subprocess
|
||||||
import threading
|
import threading
|
||||||
|
import time
|
||||||
from core.utils.encrypted_proxy import killswitch
|
from core.utils.encrypted_proxy import killswitch
|
||||||
from core.utils.encrypted_proxy.net import get_proxied_ip
|
|
||||||
|
drop_state: dict = {"count": 0}
|
||||||
|
tunnel_state: dict = {"alive": True}
|
||||||
|
|
||||||
|
CAUSE_DISCONNECT = 'disconnect'
|
||||||
|
CAUSE_LEAK = 'leak'
|
||||||
|
CAUSE_DISPLACED = 'displaced'
|
||||||
|
|
||||||
|
|
||||||
class KillswitchMonitor:
|
class KillswitchMonitor:
|
||||||
|
|
||||||
POLL_INTERVAL = 10
|
POLL_INTERVAL = 5
|
||||||
|
LOG_PREFIX = "hydraveil-drop"
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._on_disconnect = None
|
self._on_disconnect = None
|
||||||
self._on_leak = None
|
self._on_leak = None
|
||||||
self._socks5_port = None
|
self._on_displaced = None
|
||||||
self._real_ip = None
|
self._socks5_port = None
|
||||||
self._running = False
|
self._session_token = None
|
||||||
self._stop_event = threading.Event()
|
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):
|
def set_on_disconnect(self, fn):
|
||||||
self._on_disconnect = fn
|
self._on_disconnect = fn
|
||||||
|
|
@ -22,33 +34,105 @@ class KillswitchMonitor:
|
||||||
def set_on_leak(self, fn):
|
def set_on_leak(self, fn):
|
||||||
self._on_leak = fn
|
self._on_leak = fn
|
||||||
|
|
||||||
def set_connection_data(self, socks5_port, real_ip):
|
def set_on_displaced(self, fn):
|
||||||
|
self._on_displaced = fn
|
||||||
|
|
||||||
|
def set_connection_data(self, socks5_port: int):
|
||||||
self._socks5_port = socks5_port
|
self._socks5_port = socks5_port
|
||||||
self._real_ip = real_ip
|
|
||||||
|
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):
|
def stop(self):
|
||||||
self._running = False
|
self._running = False
|
||||||
self._stop_event.set()
|
self._stop_event.set()
|
||||||
|
|
||||||
def run_blocking(self):
|
def _trigger(self, cause: str):
|
||||||
self._running = True
|
self._cause = cause
|
||||||
self._stop_event.clear()
|
self._running = False
|
||||||
|
self._cause_event.set()
|
||||||
|
self._stop_event.set()
|
||||||
|
|
||||||
|
def _monitor_loop(self):
|
||||||
while self._running:
|
while self._running:
|
||||||
interrupted = self._stop_event.wait(timeout=self.POLL_INTERVAL)
|
interrupted = self._stop_event.wait(timeout=self.POLL_INTERVAL)
|
||||||
if interrupted or not self._running:
|
if interrupted or not self._running:
|
||||||
break
|
break
|
||||||
|
|
||||||
if not killswitch.status():
|
if not killswitch.status():
|
||||||
if self._on_disconnect:
|
tunnel_state["alive"] = False
|
||||||
self._on_disconnect()
|
self._trigger(CAUSE_DISCONNECT)
|
||||||
self._running = False
|
|
||||||
return
|
return
|
||||||
|
|
||||||
if self._socks5_port and self._real_ip:
|
try:
|
||||||
proxied = get_proxied_ip(self._socks5_port, timeout=10)
|
result = subprocess.run(
|
||||||
if proxied == "unknown" or proxied == self._real_ip:
|
['pgrep', '-x', 'sing-box'],
|
||||||
if self._on_leak:
|
capture_output=True,
|
||||||
self._on_leak()
|
timeout=3,
|
||||||
self._running = False
|
)
|
||||||
return
|
singbox_alive = result.returncode == 0
|
||||||
|
except (OSError, subprocess.TimeoutExpired):
|
||||||
|
singbox_alive = True
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
from core.utils.encrypted_proxy.net import get_proxied_ip
|
|
||||||
from core.utils.encrypted_proxy import killswitch
|
from core.utils.encrypted_proxy import killswitch
|
||||||
from core.controllers.ApplicationController import ApplicationController
|
from core.controllers.ApplicationController import ApplicationController
|
||||||
from core.observers.ApplicationVersionObserver import ApplicationVersionObserver
|
from core.observers.ApplicationVersionObserver import ApplicationVersionObserver
|
||||||
|
|
@ -6,8 +5,9 @@ from core.observers.ClientObserver import ClientObserver
|
||||||
from core.observers.ConnectionObserver import ConnectionObserver
|
from core.observers.ConnectionObserver import ConnectionObserver
|
||||||
from core.observers.InvoiceObserver import InvoiceObserver
|
from core.observers.InvoiceObserver import InvoiceObserver
|
||||||
from core.observers.ProfileObserver import ProfileObserver
|
from core.observers.ProfileObserver import ProfileObserver
|
||||||
from cli.ui import Spinner, label_ok, label_error, label_warn, label_connected, label_disconnect, progress_bar, label_killswitch
|
from cli.ui import Spinner, label_ok, label_error, label_warn, label_connected, label_disconnect, progress_bar, label_killswitch, label_dns
|
||||||
from cli.helpers import sanitize_profile
|
from cli.helpers import sanitize_profile
|
||||||
|
from cli.killswitch_monitor import drop_state
|
||||||
import pprint
|
import pprint
|
||||||
|
|
||||||
application_version_observer = ApplicationVersionObserver()
|
application_version_observer = ApplicationVersionObserver()
|
||||||
|
|
@ -20,6 +20,7 @@ _spinner = None
|
||||||
_monitor = None
|
_monitor = None
|
||||||
_timer_state = None
|
_timer_state = None
|
||||||
|
|
||||||
|
|
||||||
application_version_observer.subscribe('downloading', lambda event: print(f'Downloading {ApplicationController.get(event.subject.application_code).name}, version {event.subject.version_number}...'))
|
application_version_observer.subscribe('downloading', lambda event: print(f'Downloading {ApplicationController.get(event.subject.application_code).name}, version {event.subject.version_number}...'))
|
||||||
application_version_observer.subscribe('download_progressing', lambda event: print(f'Current progress: {event.meta.get("progress"):.2f}%', flush=True, end='\r'))
|
application_version_observer.subscribe('download_progressing', lambda event: print(f'Current progress: {event.meta.get("progress"):.2f}%', flush=True, end='\r'))
|
||||||
application_version_observer.subscribe('downloaded', lambda event: print('\n'))
|
application_version_observer.subscribe('downloaded', lambda event: print('\n'))
|
||||||
|
|
@ -55,16 +56,28 @@ def _on_connected(event):
|
||||||
if _timer_state:
|
if _timer_state:
|
||||||
_timer_state['_retrying'] = False
|
_timer_state['_retrying'] = False
|
||||||
d = event.subject or {}
|
d = event.subject or {}
|
||||||
label_connected(f"proxy={d.get('proxy_ip','?')} real={d.get('real_ip','?')} port={d.get('socks5_port','?')}")
|
label_connected(f"proxy={d.get('proxy_ip','?')} tunnel={d.get('tunnel_if','?')} port={d.get('socks5_port','?')}")
|
||||||
label_killswitch(killswitch.status())
|
label_killswitch(killswitch.status())
|
||||||
|
label_dns(
|
||||||
|
enabled=d.get('dns_enabled', True),
|
||||||
|
active=d.get('dns_active', False),
|
||||||
|
)
|
||||||
if _monitor is not None:
|
if _monitor is not None:
|
||||||
_monitor.set_connection_data(
|
_monitor.set_connection_data(
|
||||||
socks5_port=d.get('socks5_port'),
|
socks5_port=d.get('socks5_port'),
|
||||||
real_ip=d.get('real_ip')
|
|
||||||
)
|
)
|
||||||
if _timer_state is not None:
|
if _timer_state is not None:
|
||||||
from cli.ui import session_timer
|
from cli.ui import session_timer
|
||||||
_timer_state['stop'] = session_timer()
|
from cli.killswitch_monitor import tunnel_state
|
||||||
|
_timer_state['stop'] = session_timer(drop_state=drop_state, tunnel_state=tunnel_state)
|
||||||
|
|
||||||
|
|
||||||
|
def _on_connected_token(event):
|
||||||
|
global _monitor
|
||||||
|
if _monitor is not None:
|
||||||
|
token = (event.subject or {}).get('session_token')
|
||||||
|
if token:
|
||||||
|
_monitor.set_session_token(token)
|
||||||
|
|
||||||
|
|
||||||
def _on_disconnected(event):
|
def _on_disconnected(event):
|
||||||
|
|
@ -79,7 +92,7 @@ def _on_disconnected(event):
|
||||||
_monitor.stop()
|
_monitor.stop()
|
||||||
_monitor = None
|
_monitor = None
|
||||||
d = event.subject or {}
|
d = event.subject or {}
|
||||||
label_disconnect(f"real={d.get('ip','?')}")
|
label_disconnect(f"tunnel={d.get('tunnel_if','?')}")
|
||||||
|
|
||||||
|
|
||||||
def _on_error(event):
|
def _on_error(event):
|
||||||
|
|
@ -118,6 +131,7 @@ def _on_tor_bootstrapped(event):
|
||||||
|
|
||||||
connection_observer.subscribe('connecting', _on_connecting)
|
connection_observer.subscribe('connecting', _on_connecting)
|
||||||
connection_observer.subscribe('connected', _on_connected)
|
connection_observer.subscribe('connected', _on_connected)
|
||||||
|
connection_observer.subscribe('connected_token', _on_connected_token)
|
||||||
connection_observer.subscribe('disconnected', _on_disconnected)
|
connection_observer.subscribe('disconnected', _on_disconnected)
|
||||||
connection_observer.subscribe('error', _on_error)
|
connection_observer.subscribe('error', _on_error)
|
||||||
connection_observer.subscribe('tor_bootstrapping', _on_tor_bootstrapping)
|
connection_observer.subscribe('tor_bootstrapping', _on_tor_bootstrapping)
|
||||||
|
|
|
||||||
46
cli/ui.py
46
cli/ui.py
|
|
@ -123,7 +123,6 @@ class Spinner:
|
||||||
|
|
||||||
|
|
||||||
def wait_with_spinner(msg: str, fn, *args, result_msg: str = "", **kwargs):
|
def wait_with_spinner(msg: str, fn, *args, result_msg: str = "", **kwargs):
|
||||||
|
|
||||||
result_box = [None, None]
|
result_box = [None, None]
|
||||||
|
|
||||||
def _run():
|
def _run():
|
||||||
|
|
@ -142,14 +141,28 @@ def wait_with_spinner(msg: str, fn, *args, result_msg: str = "", **kwargs):
|
||||||
if result_box[1] is not None:
|
if result_box[1] is not None:
|
||||||
raise result_box[1]
|
raise result_box[1]
|
||||||
return result_box[0]
|
return result_box[0]
|
||||||
|
|
||||||
|
|
||||||
|
def label_dns(enabled: bool, active: bool):
|
||||||
|
if not enabled:
|
||||||
|
print(f" {BYELLOW}[ DNS ]{RESET} DNS management {BYELLOW}disabled{RESET} — using system resolver")
|
||||||
|
elif active:
|
||||||
|
print(f" {BGREEN}[ DNS ]{RESET} DNS over {BGREEN}tun0{RESET} active {DIM}(9.9.9.9){RESET}")
|
||||||
|
else:
|
||||||
|
print(f" {BRED}[ DNS ]{RESET} DNS on tun0 {BRED}FAILED{RESET} — possible DNS leak")
|
||||||
|
|
||||||
|
|
||||||
def label_killswitch(armed: bool):
|
def label_killswitch(armed: bool):
|
||||||
if armed:
|
if armed:
|
||||||
print(f" {BGREEN}[ KS ]{RESET} Kill switch active")
|
print(f" {BGREEN}[ KS ]{RESET} Kill switch active")
|
||||||
else:
|
else:
|
||||||
print(f" {BRED}[ KS ]{RESET} Kill switch {BRED}INACTIVE — connection unprotected{RESET}")
|
print(f" {BRED}[ KS ]{RESET} Kill switch {BRED}INACTIVE — connection unprotected{RESET}")
|
||||||
def session_timer() -> threading.Event:
|
|
||||||
|
|
||||||
|
def session_timer(drop_state: dict | None = None, tunnel_state: dict | None = None) -> threading.Event:
|
||||||
|
|
||||||
stop_event = threading.Event()
|
stop_event = threading.Event()
|
||||||
start = time.time()
|
start = time.time()
|
||||||
|
|
||||||
def _run():
|
def _run():
|
||||||
while not stop_event.is_set():
|
while not stop_event.is_set():
|
||||||
|
|
@ -157,17 +170,38 @@ def session_timer() -> threading.Event:
|
||||||
h = elapsed // 3600
|
h = elapsed // 3600
|
||||||
m = (elapsed % 3600) // 60
|
m = (elapsed % 3600) // 60
|
||||||
s = elapsed % 60
|
s = elapsed % 60
|
||||||
|
time_str = f"{h:02d}:{m:02d}:{s:02d}"
|
||||||
|
|
||||||
|
alive = True if tunnel_state is None else tunnel_state.get("alive", True)
|
||||||
|
dot_color = BGREEN if alive else BRED
|
||||||
|
status_word = "Protected" if alive else "Disconnected"
|
||||||
|
|
||||||
|
if drop_state is not None:
|
||||||
|
count = drop_state["count"]
|
||||||
|
drop_color = BRED if count > 0 else DIM
|
||||||
|
drop_str = (
|
||||||
|
f" {DIM}│{RESET} "
|
||||||
|
f"{drop_color}drops: {count}{RESET}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
drop_str = ""
|
||||||
|
|
||||||
sys.stdout.write(
|
sys.stdout.write(
|
||||||
f"\r {BGREEN}●{RESET} {BWHITE}Protected{RESET} "
|
f"\r {dot_color}●{RESET} {BWHITE}{status_word}{RESET} "
|
||||||
f"{DIM}[{h:02d}:{m:02d}:{s:02d}]{RESET} "
|
f"{DIM}[{time_str}]{RESET}"
|
||||||
|
f"{drop_str}"
|
||||||
|
f" "
|
||||||
)
|
)
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
sys.stdout.write("\r" + " " * 50 + "\r")
|
|
||||||
|
sys.stdout.write("\r" + " " * 70 + "\r")
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
|
|
||||||
threading.Thread(target=_run, daemon=True).start()
|
threading.Thread(target=_run, daemon=True).start()
|
||||||
return stop_event
|
return stop_event
|
||||||
|
|
||||||
|
|
||||||
def connecting_spinner() -> threading.Event:
|
def connecting_spinner() -> threading.Event:
|
||||||
stop_event = threading.Event()
|
stop_event = threading.Event()
|
||||||
frames = ("─", "\\", "│", "/")
|
frames = ("─", "\\", "│", "/")
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue