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:
+1
-1
@@ -1,3 +1,3 @@
|
||||
__version__ = "0.7.0"
|
||||
__version__ = "0.7.1"
|
||||
APP_NAME = "WGSecure"
|
||||
APP_SHORT = "WGS"
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user