v0.7.1 : diagnostics non-bloquants, DNS hors tunnel détecté

- Nouveau diagnostic : signale un DNS non couvert par les IPs autorisées
  sur un tunnel scindé (résolution forcée via une interface qui n'a pas
  de route vers ce serveur).
- Corrige le test de connexion, l'analyse DNS et la réparation DNS qui
  gelaient l'application (sondes lancées sur le thread graphique).
- Corrige le CHANGELOG absent de l'exécutable compilé (page « À propos »
  vide) et le badge de latence bloqué sur « hors ligne » quand l'ICMP est
  filtré alors que le tunnel fonctionne.
- wgsecure.iss installe désormais WireGuard for Windows automatiquement.
This commit is contained in:
2026-09-01 22:05:20 +02:00
parent ce4df4edb4
commit 8e40812f76
8 changed files with 119 additions and 26 deletions
+1 -1
View File
@@ -1,3 +1,3 @@
__version__ = "0.7.0"
__version__ = "0.7.1"
APP_NAME = "WGSecure"
APP_SHORT = "WGS"
+1 -1
View File
@@ -36,7 +36,7 @@ _WG_DEFAULT: dict[str, Any] = {
}
_DEFAULT: dict[str, Any] = {
"version": "0.7.0",
"version": "0.7.1",
"admin_password_hash": "",
"admin_salt": "",
"mfa_enabled": False,
+37
View File
@@ -580,6 +580,43 @@ def _diag_config(cfg: Config) -> list[dict]:
steps.append(_step(_OK if valid else _FAIL, "Clé pré-partagée",
"Activée (doit être identique côté serveur)" if valid
else "Format invalide : 32 octets en base64 attendus"))
# En tunnel scindé (AllowedIPs ≠ 0.0.0.0/0), NetworkManager/systemd-resolved
# associe le DNS configuré à l'interface WireGuard avec un domaine de
# routage global (`~.`) : toute résolution de nom est alors tentée via
# cette interface, qui n'a de route que vers les réseaux listés dans les
# IPs autorisées. Un DNS hors de cette liste (ex. 1.1.1.1 sur un tunnel qui
# ne route que des sous-réseaux internes) devient injoignable par ce
# chemin — la résolution échoue en bloc alors que le reste d'Internet,
# non concerné par ce DNS, continue de fonctionner (d'où un `ping` sur une
# IP qui réussit à côté d'un `ping` sur un nom qui échoue).
dns_value = (wg.get("dns") or "").strip()
if dns_value and dns_util.has_dns_backend():
nets = _networks(wg.get("allowed_ips", ""))
full_tunnel = any(n.prefixlen == 0 for n in nets)
if nets and not full_tunnel:
uncovered = []
for part in dns_value.split(","):
part = part.strip()
if not part:
continue
try:
addr = ipaddress.ip_address(part)
except ValueError:
continue
if not any(addr in n for n in nets if n.version == addr.version):
uncovered.append(part)
if uncovered:
steps.append(_step(
_WARN, "Serveur DNS routé par le tunnel",
"DNS " + ", ".join(uncovered) + " absent des IPs "
"autorisées : une fois le tunnel monté, le système tente "
"de le joindre via l'interface WireGuard, qui n'a pourtant "
"aucune route vers cette adresse — toute résolution de "
"nom échoue, même si le reste d'Internet fonctionne. "
"Utilisez un DNS compris dans les IPs autorisées (ex. un "
"résolveur interne au réseau distant), ou ajoutez son "
"réseau à la liste."))
return steps
+65 -20
View File
@@ -14,12 +14,20 @@ from app.core import wireguard as wg_core
from app.core import dns as dns_util
from app.core import mfa as mfa_core
from app.ui import theme
from app.ui.worker import ValueWorker
from app.utils.platform_utils import is_windows
class AdminWindow(QDialog):
def __init__(self, config: Config, parent=None):
super().__init__(parent)
self._cfg = config
# Sondes réseau (ping, `dig`, handshake, sudo…) : jusqu'à plusieurs
# secondes chacune. Lancées telles quelles depuis un clic, elles
# gelaient toute la fenêtre (« Ne répond plus ») le temps de leur
# exécution — ces workers les déportent hors du thread Qt.
self._diag_worker: ValueWorker | None = None
self._dns_worker: ValueWorker | None = None
self._dns_fix_worker: ValueWorker | None = None
self.setWindowTitle("WGSecure — Panneau Administrateur")
self.setWindowFlags(
Qt.WindowType.Dialog
@@ -570,10 +578,10 @@ class AdminWindow(QDialog):
"montage du tunnel, handshake, trafic et DNS — et désigne l'étape\n"
"qui bloque."
))
btn = QPushButton("🚀 Lancer le test de connexion")
btn.setStyleSheet(theme.primary_button_style())
btn.clicked.connect(self._run_test)
g.addWidget(btn)
self._btn_diag_test = QPushButton("🚀 Lancer le test de connexion")
self._btn_diag_test.setStyleSheet(theme.primary_button_style())
self._btn_diag_test.clicked.connect(self._run_test)
g.addWidget(self._btn_diag_test)
# QLabel auparavant : le rapport fait une dizaine d'étapes avec un
# détail par étape, et un QLabel se laisse écraser par le layout —
@@ -604,17 +612,17 @@ class AdminWindow(QDialog):
grp = QGroupBox("🔍 Test de fuite DNS")
g = QVBoxLayout(grp)
g.addWidget(QLabel("Vérifie que le DNS passe bien par le tunnel VPN."))
btn = QPushButton("🔍 Analyser le DNS")
btn.setStyleSheet(theme.primary_button_style())
btn.clicked.connect(self._run_dns_test)
g.addWidget(btn)
btn_fix = QPushButton("🛠️ Réparer le DNS (tunnel arrêté)")
btn_fix.setToolTip(
self._btn_dns_test = QPushButton("🔍 Analyser le DNS")
self._btn_dns_test.setStyleSheet(theme.primary_button_style())
self._btn_dns_test.clicked.connect(self._run_dns_test)
g.addWidget(self._btn_dns_test)
self._btn_dns_fix = QPushButton("🛠️ Réparer le DNS (tunnel arrêté)")
self._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_fix.clicked.connect(self._repair_dns)
g.addWidget(btn_fix)
self._btn_dns_fix.clicked.connect(self._repair_dns)
g.addWidget(self._btn_dns_fix)
self._dns_result = QTextEdit()
self._dns_result.setReadOnly(True)
@@ -1271,15 +1279,24 @@ class AdminWindow(QDialog):
}
def _run_test(self):
from PyQt6.QtWidgets import QApplication
if self._diag_worker is not None:
return
self._save_values()
self._test_result.setHtml(
'<span style="color:rgba(255,255,255,0.6);">Diagnostic en cours…</span>'
)
self._test_result.setStyleSheet(theme.result_view_style("idle"))
QApplication.processEvents()
self._btn_diag_test.setEnabled(False)
steps = wg_core.diagnose_connection(self._cfg)
self._diag_worker = ValueWorker(wg_core.diagnose_connection, self._cfg, parent=self)
self._diag_worker.done.connect(self._on_diag_done)
self._diag_worker.finished.connect(self._diag_worker.deleteLater)
self._diag_worker.start()
def _on_diag_done(self, steps: list[dict] | None):
self._diag_worker = None
self._btn_diag_test.setEnabled(True)
steps = steps or []
self._test_result.setHtml(self._format_diagnostic(steps))
states = [s["state"] for s in steps]
@@ -1441,10 +1458,20 @@ class AdminWindow(QDialog):
dlg.exec()
def _run_dns_test(self):
from PyQt6.QtWidgets import QApplication
if self._dns_worker is not None:
return
self._dns_result.setPlainText("Analyse en cours…")
QApplication.processEvents()
r = wg_core.dns_leak_test(self._cfg)
self._btn_dns_test.setEnabled(False)
self._dns_worker = ValueWorker(wg_core.dns_leak_test, self._cfg, parent=self)
self._dns_worker.done.connect(self._on_dns_test_done)
self._dns_worker.finished.connect(self._dns_worker.deleteLater)
self._dns_worker.start()
def _on_dns_test_done(self, r: dict | None):
self._dns_worker = None
self._btn_dns_test.setEnabled(True)
r = r or {"status": "unknown", "resolvers": [], "expected": ""}
lines = []
status = r["status"]
if status == "ok":
@@ -1551,12 +1578,30 @@ class AdminWindow(QDialog):
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
if self._dns_fix_worker is not None:
return
self._dns_result.setPlainText("Réparation en cours…")
QApplication.processEvents()
self._btn_dns_fix.setEnabled(False)
self._dns_fix_worker = ValueWorker(self._do_repair_dns, parent=self)
self._dns_fix_worker.done.connect(self._on_repair_dns_done)
self._dns_fix_worker.finished.connect(self._dns_fix_worker.deleteLater)
self._dns_fix_worker.start()
def _do_repair_dns(self) -> tuple[list[str], bool]:
"""Corps bloquant de `_repair_dns` — exécuté hors du thread Qt.
`force_cleanup` élève ses privilèges (sudo/askpass/pkexec) et peut
donc rester en attente d'une saisie utilisateur.
"""
actions = wg_core.force_cleanup(self._cfg)
works = dns_util.resolution_works()
return actions, works
def _on_repair_dns_done(self, result: tuple[list[str], bool] | None):
self._dns_fix_worker = None
self._btn_dns_fix.setEnabled(True)
actions, works = result if result else ([], False)
if works:
msg = "✅ Résolution DNS fonctionnelle"