- wg-quick down échoue silencieusement si l'interface a déjà disparu (crash, kill, veille) et ne retire donc jamais l'entrée DNS posée par wg-quick up : la résolution reste pointée sur un résolveur mort. Ajout d'un nettoyage DNS inconditionnel (app/core/dns.py) déclenché à la connexion, la déconnexion, la fermeture et au démarrage suivant. - is_connected() se basait sur `wg show`, qui rend un code 0 même en cas d'échec de permission pour un utilisateur non-root : l'app se croyait déconnectée en permanence et ne démontait donc jamais le tunnel à la fermeture. Détection réécrite via sysfs (sans privilèges). - Connexion/déconnexion et ping tournaient sur le thread Qt : gel de l'UI pendant les élévations de privilèges. Déportés dans des QThread (app/ui/worker.py). - Restauration réseau garantie sur toutes les sorties (croix, tray, SIGINT/SIGTERM, atexit, exception) via app/core/shutdown.py. - run_privileged() : la détection "sudo veut un mot de passe" ratait en session non-anglophone et sur les comptes ALL=(ALL) ALL ; réécrite pour se fier au message d'erreur réel plutôt qu'à `sudo -l`. - Écriture de la config WireGuard dans /etc/wireguard (root:root 0700) passe maintenant par install -D via la chaîne de privilèges complète au lieu d'un tee non autorisé par les sudoers. - Ajout d'un diagnostic des droits (make check-privileges, bouton Admin) et mise à jour de setup-sudoers pour couvrir toutes les commandes désormais utilisées.
132 lines
4.4 KiB
Python
132 lines
4.4 KiB
Python
"""Restauration garantie de l'état réseau à la fermeture de l'application.
|
|
|
|
Le tunnel WireGuard modifie deux choses globales au système : la table de
|
|
routage et la résolution DNS. Tant que l'application ne les rend pas, la
|
|
machine reste dans un état où « il n'y a plus internet » — c'est vrai sous
|
|
Linux (entrée resolvconf orpheline) comme sous Windows (service de tunnel
|
|
resté en place et règle NRPT résiduelle).
|
|
|
|
Ce module branche la même routine de restauration sur *toutes* les sorties :
|
|
|
|
* clic sur « Quitter » dans le systray ou fermeture de la fenêtre ;
|
|
* signal SIGINT / SIGTERM (Ctrl-C, arrêt de session, `kill`) ;
|
|
* SIGBREAK sous Windows (fermeture de la console) ;
|
|
* `atexit`, qui couvre `sys.exit()` et la fin normale de l'interpréteur ;
|
|
* exception non rattrapée, via `sys.excepthook`.
|
|
|
|
La restauration est idempotente : la déclencher plusieurs fois est sans effet.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import atexit
|
|
import signal
|
|
import sys
|
|
import threading
|
|
from typing import Callable
|
|
|
|
from app.core import wireguard as wg_core
|
|
from app.core.config import Config
|
|
|
|
_lock = threading.Lock()
|
|
_restored = False
|
|
_cfg: Config | None = None
|
|
_report: Callable[[str, str], None] | None = None
|
|
_installed = False
|
|
|
|
|
|
def _log(msg: str, kind: str = "info") -> None:
|
|
if _report is not None:
|
|
try:
|
|
_report(msg, kind)
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
def restore_network(reason: str = "") -> list[str]:
|
|
"""Rend le réseau à son état d'origine. Idempotent et sans exception.
|
|
|
|
Retourne la liste des actions effectuées (vide si rien n'était à faire).
|
|
"""
|
|
global _restored
|
|
with _lock:
|
|
if _restored or _cfg is None:
|
|
return []
|
|
_restored = True
|
|
cfg = _cfg
|
|
|
|
actions: list[str] = []
|
|
try:
|
|
if wg_core.is_connected(cfg):
|
|
ok, msg = wg_core.disconnect(cfg)
|
|
actions.append("tunnel arrêté" if ok else f"échec arrêt tunnel : {msg}")
|
|
_log(f"Fermeture{(' (' + reason + ')') if reason else ''} :"
|
|
f" {actions[-1]}", "info" if ok else "error")
|
|
else:
|
|
# Le tunnel n'est plus là, mais sa configuration DNS peut avoir
|
|
# survécu : c'est précisément le cas qui coupe la résolution.
|
|
actions += wg_core.force_cleanup(cfg)
|
|
if actions:
|
|
_log("Résidus réseau nettoyés à la fermeture : "
|
|
+ ", ".join(actions), "warning")
|
|
except Exception as e: # noqa: BLE001
|
|
_log(f"Restauration réseau incomplète : {e}", "error")
|
|
|
|
return actions
|
|
|
|
|
|
def _signal_handler(signum, _frame):
|
|
restore_network(f"signal {signum}")
|
|
# Réinstaller le comportement par défaut puis se renvoyer le signal :
|
|
# le code de sortie reste celui attendu par l'appelant (shell, systemd).
|
|
try:
|
|
signal.signal(signum, signal.SIG_DFL)
|
|
except (ValueError, OSError):
|
|
pass
|
|
sys.exit(128 + int(signum))
|
|
|
|
|
|
def _excepthook(exc_type, exc, tb):
|
|
restore_network("exception non rattrapée")
|
|
sys.__excepthook__(exc_type, exc, tb)
|
|
|
|
|
|
def install(cfg: Config, report: Callable[[str, str], None] | None = None,
|
|
app=None) -> None:
|
|
"""Branche la restauration sur toutes les voies de sortie du programme."""
|
|
global _cfg, _report, _installed
|
|
_cfg = cfg
|
|
_report = report
|
|
if _installed:
|
|
return
|
|
_installed = True
|
|
|
|
atexit.register(restore_network, "atexit")
|
|
sys.excepthook = _excepthook
|
|
|
|
for name in ("SIGINT", "SIGTERM", "SIGBREAK", "SIGHUP"):
|
|
sig = getattr(signal, name, None)
|
|
if sig is None:
|
|
continue
|
|
try:
|
|
signal.signal(sig, _signal_handler)
|
|
except (ValueError, OSError):
|
|
# Signal indisponible sur la plateforme ou hors thread principal.
|
|
pass
|
|
|
|
if app is not None:
|
|
app.aboutToQuit.connect(lambda: restore_network("aboutToQuit"))
|
|
# Qt bloque dans du code C : sans réveil périodique, les gestionnaires
|
|
# de signaux Python ne s'exécutent jamais pendant la boucle d'événements.
|
|
from PyQt6.QtCore import QTimer
|
|
heartbeat = QTimer(app)
|
|
heartbeat.timeout.connect(lambda: None)
|
|
heartbeat.start(400)
|
|
app._wgs_signal_heartbeat = heartbeat # garder une référence vivante
|
|
|
|
|
|
def arm() -> None:
|
|
"""Réarme la restauration après une reconnexion volontaire."""
|
|
global _restored
|
|
with _lock:
|
|
_restored = False
|