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:
@@ -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
|
## [0.7.0] — 2026-09-01
|
||||||
|
|
||||||
### Ajouté
|
### Ajouté
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
## ──────────────────────────────────────────────
|
## ──────────────────────────────────────────────
|
||||||
|
|
||||||
APP := wgsecure
|
APP := wgsecure
|
||||||
VERSION := 0.7.0
|
VERSION := 0.7.1
|
||||||
VENV := .venv
|
VENV := .venv
|
||||||
PYTHON := $(VENV)/bin/python3
|
PYTHON := $(VENV)/bin/python3
|
||||||
PIP := $(VENV)/bin/pip
|
PIP := $(VENV)/bin/pip
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# 🛡️ WGSecure (WGS)
|
# 🛡️ 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).
|
**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
|
## 👤 Auteur
|
||||||
|
|
||||||
Développé par **Johnny** — [JT-Tools](https://github.com/JT-Tools)
|
Développé par **Johnny** — [JT-Tools](https://github.com/JT-Tools)
|
||||||
Version :  — Septembre 2026
|
Version :  — Septembre 2026
|
||||||
|
|||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
__version__ = "0.7.0"
|
__version__ = "0.7.1"
|
||||||
APP_NAME = "WGSecure"
|
APP_NAME = "WGSecure"
|
||||||
APP_SHORT = "WGS"
|
APP_SHORT = "WGS"
|
||||||
|
|||||||
+1
-1
@@ -36,7 +36,7 @@ _WG_DEFAULT: dict[str, Any] = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_DEFAULT: dict[str, Any] = {
|
_DEFAULT: dict[str, Any] = {
|
||||||
"version": "0.7.0",
|
"version": "0.7.1",
|
||||||
"admin_password_hash": "",
|
"admin_password_hash": "",
|
||||||
"admin_salt": "",
|
"admin_salt": "",
|
||||||
"mfa_enabled": False,
|
"mfa_enabled": False,
|
||||||
|
|||||||
@@ -580,6 +580,43 @@ def _diag_config(cfg: Config) -> list[dict]:
|
|||||||
steps.append(_step(_OK if valid else _FAIL, "Clé pré-partagée",
|
steps.append(_step(_OK if valid else _FAIL, "Clé pré-partagée",
|
||||||
"Activée (doit être identique côté serveur)" if valid
|
"Activée (doit être identique côté serveur)" if valid
|
||||||
else "Format invalide : 32 octets en base64 attendus"))
|
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
|
return steps
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+65
-20
@@ -14,12 +14,20 @@ from app.core import wireguard as wg_core
|
|||||||
from app.core import dns as dns_util
|
from app.core import dns as dns_util
|
||||||
from app.core import mfa as mfa_core
|
from app.core import mfa as mfa_core
|
||||||
from app.ui import theme
|
from app.ui import theme
|
||||||
|
from app.ui.worker import ValueWorker
|
||||||
from app.utils.platform_utils import is_windows
|
from app.utils.platform_utils import is_windows
|
||||||
|
|
||||||
class AdminWindow(QDialog):
|
class AdminWindow(QDialog):
|
||||||
def __init__(self, config: Config, parent=None):
|
def __init__(self, config: Config, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self._cfg = config
|
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.setWindowTitle("WGSecure — Panneau Administrateur")
|
||||||
self.setWindowFlags(
|
self.setWindowFlags(
|
||||||
Qt.WindowType.Dialog
|
Qt.WindowType.Dialog
|
||||||
@@ -570,10 +578,10 @@ class AdminWindow(QDialog):
|
|||||||
"montage du tunnel, handshake, trafic et DNS — et désigne l'étape\n"
|
"montage du tunnel, handshake, trafic et DNS — et désigne l'étape\n"
|
||||||
"qui bloque."
|
"qui bloque."
|
||||||
))
|
))
|
||||||
btn = QPushButton("🚀 Lancer le test de connexion")
|
self._btn_diag_test = QPushButton("🚀 Lancer le test de connexion")
|
||||||
btn.setStyleSheet(theme.primary_button_style())
|
self._btn_diag_test.setStyleSheet(theme.primary_button_style())
|
||||||
btn.clicked.connect(self._run_test)
|
self._btn_diag_test.clicked.connect(self._run_test)
|
||||||
g.addWidget(btn)
|
g.addWidget(self._btn_diag_test)
|
||||||
|
|
||||||
# QLabel auparavant : le rapport fait une dizaine d'étapes avec un
|
# QLabel auparavant : le rapport fait une dizaine d'étapes avec un
|
||||||
# détail par étape, et un QLabel se laisse écraser par le layout —
|
# 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")
|
grp = QGroupBox("🔍 Test de fuite DNS")
|
||||||
g = QVBoxLayout(grp)
|
g = QVBoxLayout(grp)
|
||||||
g.addWidget(QLabel("Vérifie que le DNS passe bien par le tunnel VPN."))
|
g.addWidget(QLabel("Vérifie que le DNS passe bien par le tunnel VPN."))
|
||||||
btn = QPushButton("🔍 Analyser le DNS")
|
self._btn_dns_test = QPushButton("🔍 Analyser le DNS")
|
||||||
btn.setStyleSheet(theme.primary_button_style())
|
self._btn_dns_test.setStyleSheet(theme.primary_button_style())
|
||||||
btn.clicked.connect(self._run_dns_test)
|
self._btn_dns_test.clicked.connect(self._run_dns_test)
|
||||||
g.addWidget(btn)
|
g.addWidget(self._btn_dns_test)
|
||||||
btn_fix = QPushButton("🛠️ Réparer le DNS (tunnel arrêté)")
|
self._btn_dns_fix = QPushButton("🛠️ Réparer le DNS (tunnel arrêté)")
|
||||||
btn_fix.setToolTip(
|
self._btn_dns_fix.setToolTip(
|
||||||
"Retire la configuration DNS laissée par un tunnel arrêté "
|
"Retire la configuration DNS laissée par un tunnel arrêté "
|
||||||
"brutalement, qui empêche toute résolution de noms."
|
"brutalement, qui empêche toute résolution de noms."
|
||||||
)
|
)
|
||||||
btn_fix.clicked.connect(self._repair_dns)
|
self._btn_dns_fix.clicked.connect(self._repair_dns)
|
||||||
g.addWidget(btn_fix)
|
g.addWidget(self._btn_dns_fix)
|
||||||
|
|
||||||
self._dns_result = QTextEdit()
|
self._dns_result = QTextEdit()
|
||||||
self._dns_result.setReadOnly(True)
|
self._dns_result.setReadOnly(True)
|
||||||
@@ -1271,15 +1279,24 @@ class AdminWindow(QDialog):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def _run_test(self):
|
def _run_test(self):
|
||||||
from PyQt6.QtWidgets import QApplication
|
if self._diag_worker is not None:
|
||||||
|
return
|
||||||
self._save_values()
|
self._save_values()
|
||||||
self._test_result.setHtml(
|
self._test_result.setHtml(
|
||||||
'<span style="color:rgba(255,255,255,0.6);">Diagnostic en cours…</span>'
|
'<span style="color:rgba(255,255,255,0.6);">Diagnostic en cours…</span>'
|
||||||
)
|
)
|
||||||
self._test_result.setStyleSheet(theme.result_view_style("idle"))
|
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))
|
self._test_result.setHtml(self._format_diagnostic(steps))
|
||||||
|
|
||||||
states = [s["state"] for s in steps]
|
states = [s["state"] for s in steps]
|
||||||
@@ -1441,10 +1458,20 @@ class AdminWindow(QDialog):
|
|||||||
dlg.exec()
|
dlg.exec()
|
||||||
|
|
||||||
def _run_dns_test(self):
|
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…")
|
self._dns_result.setPlainText("Analyse en cours…")
|
||||||
QApplication.processEvents()
|
self._btn_dns_test.setEnabled(False)
|
||||||
r = wg_core.dns_leak_test(self._cfg)
|
|
||||||
|
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 = []
|
lines = []
|
||||||
status = r["status"]
|
status = r["status"]
|
||||||
if status == "ok":
|
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
|
ne retire donc jamais l'entrée DNS qu'il avait posée : la résolution
|
||||||
reste dirigée vers un serveur injoignable.
|
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…")
|
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)
|
actions = wg_core.force_cleanup(self._cfg)
|
||||||
works = dns_util.resolution_works()
|
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:
|
if works:
|
||||||
msg = "✅ Résolution DNS fonctionnelle"
|
msg = "✅ Résolution DNS fonctionnelle"
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@
|
|||||||
; ──────────────────────────────────────────────
|
; ──────────────────────────────────────────────
|
||||||
|
|
||||||
#define MyAppName "WGSecure"
|
#define MyAppName "WGSecure"
|
||||||
#define MyAppVersion "0.7.0"
|
#define MyAppVersion "0.7.1"
|
||||||
#define MyAppPublisher "WGSecure"
|
#define MyAppPublisher "WGSecure"
|
||||||
#define MyAppExeName "wgsecure.exe"
|
#define MyAppExeName "wgsecure.exe"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user