fix: DNS coupée après arrêt du tunnel, gel UI, dialogues sudo à répétition

- 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.
This commit is contained in:
tuxgyver
2026-08-31 11:16:04 +02:00
parent 2fa50a5873
commit 1b74719204
11 changed files with 1295 additions and 233 deletions
+97
View File
@@ -9,6 +9,7 @@ from PyQt6.QtGui import QFont
from app.core.config import Config
from app.core import wireguard as wg_core
from app.core import dns as dns_util
from app.core import mfa as mfa_core
# CSS sombre commun à tous les onglets
@@ -465,6 +466,13 @@ class AdminWindow(QDialog):
btn_dns = QPushButton("🔍 Analyser le DNS")
btn_dns.clicked.connect(self._run_dns_test)
g2.addWidget(btn_dns)
btn_dns_fix = QPushButton("🛠️ Réparer le DNS (tunnel arrêté)")
btn_dns_fix.setToolTip(
"Retire la configuration DNS laissée par un tunnel arrêté "
"brutalement, qui empêche toute résolution de noms."
)
btn_dns_fix.clicked.connect(self._repair_dns)
g2.addWidget(btn_dns_fix)
self._dns_result = QLabel("")
self._dns_result.setWordWrap(True)
self._dns_result.setStyleSheet(
@@ -472,6 +480,24 @@ class AdminWindow(QDialog):
)
g2.addWidget(self._dns_result)
lay.addWidget(grp2)
# Droits d'exécution
grp3 = QGroupBox("🔐 Droits d'exécution")
g3 = QVBoxLayout(grp3)
g3.addWidget(QLabel(
"Sans règle sudo, chaque connexion ouvre un dialogue d'authentification."
))
btn_priv = QPushButton("🔐 Vérifier les droits")
btn_priv.clicked.connect(self._check_privileges)
g3.addWidget(btn_priv)
self._priv_result = QLabel("")
self._priv_result.setWordWrap(True)
self._priv_result.setStyleSheet(
"color: white; padding: 8px; border-radius: 6px; font-size: 11px;"
)
g3.addWidget(self._priv_result)
lay.addWidget(grp3)
lay.addStretch()
return w
@@ -948,6 +974,12 @@ class AdminWindow(QDialog):
elif status == "leak":
lines.append("⚠️ Fuite DNS potentielle !")
css = "background: rgba(231,76,60,0.2); color: #f1948a;"
elif status == "down":
lines.append("🚫 Résolution DNS hors service — utilisez « Réparer le DNS »")
css = "background: rgba(231,76,60,0.2); color: #f1948a;"
elif not r.get("tunnel_up", False):
lines.append("️ Tunnel inactif : la comparaison n'a pas de sens")
css = "color: #aed6f1;"
else:
lines.append("❓ Statut inconnu")
css = "color: #aed6f1;"
@@ -962,6 +994,71 @@ class AdminWindow(QDialog):
f"padding: 8px; border-radius: 6px; font-size: 11px; {css}"
)
def _check_privileges(self):
"""Liste les commandes privilégiées et celles qui exigeront un dialogue."""
from PyQt6.QtWidgets import QApplication
from app.utils.platform_utils import privilege_report
self._priv_result.setText("Vérification en cours…")
QApplication.processEvents()
rows = privilege_report()
if not rows:
self._priv_result.setText(
"✅ Exécution avec les privilèges root : rien à configurer."
)
css = "background: rgba(39,174,96,0.2); color: #a9dfbf;"
else:
lines = [
("" if ok else "⚠️ ") + f"{name}{role}"
+ ("" if ok else " (dialogue à chaque appel)")
for name, ok, role in rows
]
missing = [n for n, ok, _ in rows if not ok]
if missing:
lines.append("")
lines.append("Pour supprimer ces dialogues : make setup-sudoers")
css = "background: rgba(241,196,15,0.2); color: #f9e79f;"
else:
css = "background: rgba(39,174,96,0.2); color: #a9dfbf;"
self._priv_result.setText("\n".join(lines))
self._priv_result.setStyleSheet(
f"padding: 8px; border-radius: 6px; font-size: 11px; {css}"
)
def _repair_dns(self):
"""Retire la configuration DNS résiduelle d'un tunnel mal arrêté.
`wg-quick down` refuse de s'exécuter dès que l'interface a disparu et
ne retire donc jamais l'entrée DNS qu'il avait posée : la résolution
reste dirigée vers un serveur injoignable.
"""
from PyQt6.QtWidgets import QApplication
self._dns_result.setText("Réparation en cours…")
QApplication.processEvents()
actions = wg_core.force_cleanup(self._cfg)
works = dns_util.resolution_works()
if works:
msg = "✅ Résolution DNS fonctionnelle"
if actions:
msg += "\nActions : " + ", ".join(actions)
css = "background: rgba(39,174,96,0.2); color: #a9dfbf;"
elif actions:
msg = ("⚠️ Résidus retirés (" + ", ".join(actions) +
") mais la résolution échoue toujours.\n"
"Vérifiez la connexion réseau physique.")
css = "background: rgba(241,196,15,0.2); color: #f9e79f;"
else:
msg = ("🚫 Aucun résidu du tunnel détecté et la résolution échoue :"
" le problème vient du réseau, pas de WGSecure.")
css = "background: rgba(231,76,60,0.2); color: #f1948a;"
self._dns_result.setText(msg)
self._dns_result.setStyleSheet(
f"padding: 8px; border-radius: 6px; font-size: 11px; {css}"
)
def _set_admin_password(self):
p1 = self._admin_pw1.text()
p2 = self._admin_pw2.text()