v0.7.0 : instance unique, handshake ré-affiché, sudoers fiabilisé, À propos refaite
- Instance unique : un second lancement réveille l'instance active. - Autostart et reconnexion auto activés par défaut (nouvelle installation). - Page « À propos » refaite en 5 onglets (contenu séparé dans app_info.py). - Corrige le handshake/RX-TX qui ne s'affichaient plus (wg show sans élévation). - Corrige le diagnostic sudo trompeur (sondes qui ne matchaient jamais les règles). - Corrige make setup-sudoers bloqué hors session graphique (pkexec -> sudo), idempotent. - Corrige le README : Kill Switch inexistant retiré, ordre d'élévation réel documenté.
This commit is contained in:
+8
-4
@@ -36,7 +36,7 @@ _WG_DEFAULT: dict[str, Any] = {
|
||||
}
|
||||
|
||||
_DEFAULT: dict[str, Any] = {
|
||||
"version": "0.6.0",
|
||||
"version": "0.7.0",
|
||||
"admin_password_hash": "",
|
||||
"admin_salt": "",
|
||||
"mfa_enabled": False,
|
||||
@@ -46,10 +46,10 @@ _DEFAULT: dict[str, Any] = {
|
||||
"wg": deepcopy(_WG_DEFAULT),
|
||||
"ui": {
|
||||
"minimize_to_tray": True,
|
||||
"autostart": False,
|
||||
"autostart": True,
|
||||
"auto_connect_on_startup": False,
|
||||
"auto_reconnect": False,
|
||||
"reconnect_interval": 30,
|
||||
"auto_reconnect": True,
|
||||
"reconnect_interval": 15,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -69,7 +69,11 @@ class Config:
|
||||
except Exception:
|
||||
self._data = deepcopy(_DEFAULT)
|
||||
else:
|
||||
# Première exécution : le défaut « autostart activé » n'a d'effet
|
||||
# que si on l'applique réellement (fichier .desktop / clé de
|
||||
# registre), sinon la case reste cochée sans rien avoir installé.
|
||||
self._data = deepcopy(_DEFAULT)
|
||||
self._apply_autostart(self._data["ui"]["autostart"])
|
||||
|
||||
def save(self):
|
||||
os.makedirs(os.path.dirname(self._path), exist_ok=True)
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
"""Garantit une seule instance de WGSecure en cours d'exécution.
|
||||
|
||||
Avec le lancement automatique au démarrage désormais activé par défaut
|
||||
([[wg-autostart-reconnect]]), un second lancement manuel (icône du menu,
|
||||
raccourci double-cliqué) peut facilement se superposer à l'instance déjà
|
||||
présente en systray — deux fenêtres sur le même tunnel, deux timers de
|
||||
reconnexion concurrents. Le second lancement doit simplement réveiller le
|
||||
premier et s'arrêter.
|
||||
|
||||
Implémenté via un socket local nommé (QLocalServer/QLocalSocket) : le
|
||||
mécanisme Qt standard pour ça, portable Linux/Windows sans dépendance
|
||||
supplémentaire. Le premier lancement écoute ; tout lancement suivant s'y
|
||||
connecte, signale sa présence, puis quitte.
|
||||
"""
|
||||
|
||||
from PyQt6.QtNetwork import QLocalServer, QLocalSocket
|
||||
|
||||
_SERVER_NAME = "wgsecure-single-instance"
|
||||
|
||||
|
||||
class SingleInstanceGuard:
|
||||
def __init__(self):
|
||||
self._server: QLocalServer | None = None
|
||||
self._on_activate = None
|
||||
|
||||
def try_acquire(self) -> bool:
|
||||
"""True si cette instance est la première et doit continuer.
|
||||
|
||||
False si une autre tourne déjà : celle-ci lui a signalé sa présence
|
||||
et doit se terminer sans ouvrir la moindre fenêtre.
|
||||
"""
|
||||
probe = QLocalSocket()
|
||||
probe.connectToServer(_SERVER_NAME)
|
||||
if probe.waitForConnected(200):
|
||||
probe.write(b"activate")
|
||||
probe.waitForBytesWritten(200)
|
||||
probe.disconnectFromServer()
|
||||
return False
|
||||
|
||||
# Un crash précédent peut laisser le socket enregistré sans qu'aucun
|
||||
# serveur ne l'écoute : listen() échouerait indéfiniment sinon.
|
||||
QLocalServer.removeServer(_SERVER_NAME)
|
||||
self._server = QLocalServer()
|
||||
self._server.newConnection.connect(self._handle_connection)
|
||||
self._server.listen(_SERVER_NAME)
|
||||
return True
|
||||
|
||||
def on_activate_requested(self, callback) -> None:
|
||||
"""`callback` (sans argument) est appelé quand un second lancement
|
||||
tente de démarrer — à connecter au réaffichage de la fenêtre."""
|
||||
self._on_activate = callback
|
||||
|
||||
def _handle_connection(self) -> None:
|
||||
sock = self._server.nextPendingConnection()
|
||||
if sock is None:
|
||||
return
|
||||
sock.readyRead.connect(lambda: self._consume(sock))
|
||||
|
||||
def _consume(self, sock: QLocalSocket) -> None:
|
||||
sock.readAll()
|
||||
if self._on_activate:
|
||||
self._on_activate()
|
||||
sock.disconnectFromServer()
|
||||
@@ -14,6 +14,7 @@ from app.utils.platform_utils import (
|
||||
is_windows,
|
||||
run_command,
|
||||
run_privileged,
|
||||
run_privileged_readonly,
|
||||
wg_available,
|
||||
wg_exe,
|
||||
wg_quick_available,
|
||||
@@ -150,12 +151,14 @@ def write_client_config(cfg: Config) -> tuple[bool, str]:
|
||||
def _wg_show(name: str) -> str:
|
||||
"""Sortie de `wg show <iface>`, ou "" si les privilèges manquent.
|
||||
|
||||
Attention : sans CAP_NET_ADMIN, `wg show` écrit « Unable to access
|
||||
interface: Operation not permitted » sur stderr **et sort avec le code 0**.
|
||||
Un simple test `code == 0` conclut donc « déconnecté » pour tout
|
||||
utilisateur non root — c'était le cas de l'application entière.
|
||||
`wg show` exige CAP_NET_ADMIN (root) pour lire l'état d'une interface,
|
||||
même en lecture seule — un utilisateur ordinaire reçoit « Unable to
|
||||
access interface: Operation not permitted ». D'où l'appel via
|
||||
`run_privileged_readonly` : sans lui, le handshake et les compteurs de
|
||||
trafic restent vides dès que l'application ne tourne plus en root (le cas
|
||||
normal une fois `make setup-sudoers` en place pour wg-quick).
|
||||
"""
|
||||
code, out, err = run_command([wg_exe(), "show", name])
|
||||
code, out, err = run_privileged_readonly([wg_exe(), "show", name])
|
||||
if code != 0 or not out:
|
||||
return ""
|
||||
if "not permitted" in err.lower() or "unable to access" in err.lower():
|
||||
@@ -402,7 +405,7 @@ def last_handshake_age(name: str) -> int | None:
|
||||
Retourne -1 si aucun handshake n'a jamais eu lieu, None si l'information
|
||||
est inaccessible (privilèges insuffisants, `wg` absent).
|
||||
"""
|
||||
code, out, _ = run_command([wg_exe(), "show", name, "latest-handshakes"], 10)
|
||||
code, out, _ = run_privileged_readonly([wg_exe(), "show", name, "latest-handshakes"], 10)
|
||||
if code != 0 or not out.strip():
|
||||
return None
|
||||
newest = 0
|
||||
|
||||
Reference in New Issue
Block a user