From 8e40812f76471ecd4217cde18d586b96f7abc9cd Mon Sep 17 00:00:00 2001 From: Johnny Date: Tue, 1 Sep 2026 22:05:20 +0200 Subject: [PATCH] =?UTF-8?q?v0.7.1=20:=20diagnostics=20non-bloquants,=20DNS?= =?UTF-8?q?=20hors=20tunnel=20d=C3=A9tect=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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. --- CHANGELOG.md | 11 ++++++ Makefile | 2 +- README.md | 4 +- app/__init__.py | 2 +- app/core/config.py | 2 +- app/core/wireguard.py | 37 ++++++++++++++++++ app/ui/admin_window.py | 85 ++++++++++++++++++++++++++++++++---------- wgsecure.iss | 2 +- 8 files changed, 119 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e2a31a..ae8e645 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,17 @@ Ce projet suit le [Versionnage Sémantique](https://semver.org/lang/fr/). --- +## [0.7.1] — 2026-09-01 + +### Ajouté +- **Diagnostic : DNS hors des IPs autorisées.** Sur un 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 (typiquement un résolveur public comme 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 continue de fonctionner (d'où un `ping` sur une IP qui réussit à côté d'un `ping` sur un nom qui échoue). Le test de connexion signale maintenant ce cas précis. + +### Corrigé +- **CHANGELOG.md absent de l'exécutable compilé.** `datas=[]` dans le build PyInstaller n'embarquait jamais le fichier, et son chemin — calculé depuis `__file__` — ne survivait de toute façon pas à l'extraction onefile : la page « À propos » restait vide une fois l'application compilée. Le fichier est désormais ajouté via `--add-data`, et son chemin bascule sur `sys._MEIPASS` en mode figé (frozen). +- **Badge de latence toujours « hors ligne » alors que le tunnel fonctionne.** Il se basait uniquement sur un ping ICMP vers l'endpoint du serveur ; de nombreux serveurs ou pare-feux bloquent l'ICMP echo tout en laissant le tunnel WireGuard fonctionner normalement. Repli sur l'âge du dernier handshake réel quand l'ICMP échoue. +- **Outils de diagnostic figeant l'application (« Ne répond plus »).** Le test de connexion, l'analyse de fuite DNS et la réparation DNS exécutaient leurs sondes (ping, `dig`, handshake, élévation de privilèges) directement sur le thread graphique — jusqu'à plusieurs secondes de gel total. Déportés sur des threads dédiés, comme le reste de l'application. +- **Installeur Windows incomplet.** WireGuard for Windows, prérequis de WGSecure, n'était jamais installé automatiquement. `wgsecure.iss` l'installe désormais en silence s'il est absent, au même titre que le Visual C++ Redistributable ; `make installer-deps` télécharge les deux binaires. + ## [0.7.0] — 2026-09-01 ### Ajouté diff --git a/Makefile b/Makefile index 57b4434..3fb369d 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ ## ────────────────────────────────────────────── APP := wgsecure -VERSION := 0.7.0 +VERSION := 0.7.1 VENV := .venv PYTHON := $(VENV)/bin/python3 PIP := $(VENV)/bin/pip diff --git a/README.md b/README.md index fa3e9fa..e07468e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # 🛡️ WGSecure (WGS) -🚀 v0.7.0 · 🐍 · 🪟🐧 · 🔐 MFA TOTP · 🛡️ WireGuard +🚀 v0.7.1 · 🐍 · 🪟🐧 · 🔐 MFA TOTP · 🛡️ WireGuard **WGSecure** est une interface graphique multiplateforme (Windows & Linux) pour gérer une connexion WireGuard avec une surcouche MFA (authentification à deux facteurs TOTP). @@ -208,4 +208,4 @@ WGSecure/ ## 👤 Auteur Développé par **Johnny** — [JT-Tools](https://github.com/JT-Tools) -Version : ![v0.7.0](https://img.shields.io/badge/v0.7.0-septembre%202026-2980b9?style=flat-square) — Septembre 2026 +Version : ![v0.7.1](https://img.shields.io/badge/v0.7.1-septembre%202026-2980b9?style=flat-square) — Septembre 2026 diff --git a/app/__init__.py b/app/__init__.py index ecea1fa..ddce92e 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,3 +1,3 @@ -__version__ = "0.7.0" +__version__ = "0.7.1" APP_NAME = "WGSecure" APP_SHORT = "WGS" diff --git a/app/core/config.py b/app/core/config.py index 1886611..1d1d6a0 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -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, diff --git a/app/core/wireguard.py b/app/core/wireguard.py index 8b47743..f02a5bf 100644 --- a/app/core/wireguard.py +++ b/app/core/wireguard.py @@ -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 diff --git a/app/ui/admin_window.py b/app/ui/admin_window.py index e46f537..a1632f4 100644 --- a/app/ui/admin_window.py +++ b/app/ui/admin_window.py @@ -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( 'Diagnostic en cours…' ) 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" diff --git a/wgsecure.iss b/wgsecure.iss index f219b42..cd57372 100644 --- a/wgsecure.iss +++ b/wgsecure.iss @@ -5,7 +5,7 @@ ; ────────────────────────────────────────────── #define MyAppName "WGSecure" -#define MyAppVersion "0.7.0" +#define MyAppVersion "0.7.1" #define MyAppPublisher "WGSecure" #define MyAppExeName "wgsecure.exe"