chore(release): v0.4.5

fix: la clé pré-partagée n'était écrite dans aucune config client

L'assistant de génération produisait une PresharedKey affichée à l'écran mais
stockée nulle part, et l'import d'un .conf ignorait la ligne. Face à un serveur
qui en attend une, le tunnel montait et l'interface existait — sans qu'aucun
handshake n'aboutisse jamais. La PSK devient un champ à part entière.

fix: options de ligne de commande wireguard.exe incorrectes sous Windows

/installtunnel et /uninstalltunnel n'existent pas ; les vrais verbes sont
/installtunnelservice et /uninstalltunnelservice. S'y ajoutaient l'absence de
résolution du chemin d'installation (WireGuard n'est pas dans le PATH),
l'absence totale d'élévation UAC alors que ces commandes l'exigent, et une
détection d'installation qui sondait une application graphique avec --help.

feat: diagnostic de bout en bout de la chaîne de connexion

Le bouton de test s'arrêtait à la joignabilité UDP et concluait « serveur
joignable » sur un tunnel qui n'échangeait rien. Il déroule désormais quatorze
étapes, de la configuration locale jusqu'au DNS dans le tunnel, et désigne
l'étape qui bloque.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
tuxgyver
2026-08-31 15:41:43 +02:00
co-authored by Claude Opus 5
parent 09724f4e4f
commit 9d785c00cc
10 changed files with 777 additions and 64 deletions
+119 -17
View File
@@ -283,6 +283,16 @@ class AdminWindow(QDialog):
self._srv_pubkey = QLineEdit()
self._srv_pubkey.setPlaceholderText("Clé publique du serveur (base64)")
self._row(form, "Clé publique serveur :", self._srv_pubkey)
self._psk = QLineEdit()
self._psk.setEchoMode(QLineEdit.EchoMode.Password)
self._psk.setPlaceholderText("(vide si le serveur n'en utilise pas)")
self._psk.setToolTip(
"Clé pré-partagée (PresharedKey). Doit être identique des deux côtés :\n"
"si le serveur en attend une et qu'elle manque ici, le tunnel monte\n"
"mais aucun handshake n'aboutit."
)
self._row(form, "Clé pré-partagée :", self._psk)
lay.addWidget(grp)
# ── Interface client ─────────────────────────────────────────────
@@ -311,6 +321,16 @@ class AdminWindow(QDialog):
self._keepalive.setValue(25)
self._keepalive.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
self._row(form2, "Keepalive (s) :", self._keepalive)
self._mtu = QSpinBox()
self._mtu.setRange(0, 9000)
self._mtu.setSpecialValueText("automatique")
self._mtu.setToolTip(
"0 = laisser WireGuard décider. Abaisser à 12801420 si le tunnel\n"
"monte mais que les gros paquets ne passent pas."
)
self._mtu.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
self._row(form2, "MTU :", self._mtu)
lay.addWidget(grp2)
lay.addStretch()
@@ -439,9 +459,13 @@ class AdminWindow(QDialog):
w, lay = self._dark_page("📡 Test de connectivité réseau", "#1a4a7a")
# Test UDP serveur
grp1 = QGroupBox("Test UDP serveur")
grp1 = QGroupBox("Diagnostic de la chaîne de connexion")
g1 = QVBoxLayout(grp1)
g1.addWidget(QLabel("Vérifie la joignabilité UDP du serveur WireGuard."))
g1.addWidget(QLabel(
"Vérifie toute la chaîne : configuration, clés, route, port UDP,\n"
"montage du tunnel, handshake, trafic et DNS — et désigne l'étape\n"
"qui bloque."
))
btn_test = QPushButton("🚀 Lancer le test de connexion")
btn_test.setStyleSheet(
"QPushButton { padding: 9px; background: #1e8449; color: white;"
@@ -452,7 +476,13 @@ class AdminWindow(QDialog):
g1.addWidget(btn_test)
self._test_result = QLabel("")
self._test_result.setWordWrap(True)
self._test_result.setAlignment(Qt.AlignmentFlag.AlignCenter)
self._test_result.setTextFormat(Qt.TextFormat.RichText)
self._test_result.setAlignment(
Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop
)
self._test_result.setTextInteractionFlags(
Qt.TextInteractionFlag.TextSelectableByMouse
)
self._test_result.setStyleSheet(
"color: white; padding: 8px; border-radius: 6px; font-size: 12px;"
)
@@ -675,7 +705,7 @@ class AdminWindow(QDialog):
name_lbl.setStyleSheet("color: white;")
layout.addWidget(name_lbl)
version_lbl = QLabel("Version 0.4.3")
version_lbl = QLabel("Version 0.4.5")
version_lbl.setAlignment(Qt.AlignmentFlag.AlignCenter)
version_lbl.setStyleSheet("color: rgba(255,255,255,0.7); font-size: 13px;")
layout.addWidget(version_lbl)
@@ -725,11 +755,13 @@ class AdminWindow(QDialog):
self._srv_endpoint.setText(wg.get("server_endpoint", ""))
self._srv_port.setValue(int(wg.get("server_port", 51820)))
self._srv_pubkey.setText(wg.get("server_public_key", ""))
self._psk.setText(wg.get("preshared_key", ""))
self._iface_name.setText(wg.get("interface_name", "wgs0"))
self._client_addr.setText(wg.get("client_address", "10.8.0.2/24"))
self._dns.setText(wg.get("dns", "1.1.1.1"))
self._allowed_ips.setText(wg.get("allowed_ips", "0.0.0.0/0"))
self._keepalive.setValue(int(wg.get("keepalive", 25)))
self._mtu.setValue(int(wg.get("mtu") or 0))
self._priv_key.setText(wg.get("client_private_key", ""))
self._pub_key.setText(wg.get("client_public_key", ""))
self._mfa_enabled_cb.setChecked(self._cfg.mfa_enabled)
@@ -739,11 +771,13 @@ class AdminWindow(QDialog):
self._cfg.set("wg", "server_endpoint", self._srv_endpoint.text().strip())
self._cfg.set("wg", "server_port", self._srv_port.value())
self._cfg.set("wg", "server_public_key", self._srv_pubkey.text().strip())
self._cfg.set("wg", "preshared_key", self._psk.text().strip())
self._cfg.set("wg", "interface_name", self._iface_name.text().strip() or "wgs0")
self._cfg.set("wg", "client_address", self._client_addr.text().strip())
self._cfg.set("wg", "dns", self._dns.text().strip())
self._cfg.set("wg", "allowed_ips", self._allowed_ips.text().strip())
self._cfg.set("wg", "keepalive", self._keepalive.value())
self._cfg.set("wg", "mtu", self._mtu.value())
self._cfg.set("wg", "client_private_key", self._priv_key.text().strip())
self._cfg.set("wg", "client_public_key", self._pub_key.text().strip())
self._cfg.mfa_enabled = self._mfa_enabled_cb.isChecked()
@@ -792,6 +826,7 @@ class AdminWindow(QDialog):
"allowed_ips": self._allowed_ips,
"client_private_key": self._priv_key,
"client_public_key": self._pub_key,
"preshared_key": self._psk,
}
for k, widget in mapping.items():
if k in values:
@@ -800,6 +835,8 @@ class AdminWindow(QDialog):
self._srv_port.setValue(int(values["server_port"]))
if "keepalive" in values:
self._keepalive.setValue(int(values["keepalive"]))
if "mtu" in values:
self._mtu.setValue(int(values["mtu"]))
# Sauvegarder en tant que nouveau profil
self._save_values()
@@ -857,28 +894,66 @@ class AdminWindow(QDialog):
)
self._mfa_uri_label.setText(mfa_core.get_provisioning_uri(secret))
#: Rendu d'une étape de diagnostic : puce, couleur du libellé.
_STEP_STYLE = {
"ok": ("", "#a9dfbf"),
"warn": ("⚠️", "#f7dc6f"),
"fail": ("", "#f1948a"),
"skip": ("⏭️", "rgba(255,255,255,0.45)"),
}
def _run_test(self):
from PyQt6.QtWidgets import QApplication
self._save_values()
self._test_result.setText("Test en cours…")
self._test_result.setText("Diagnostic en cours…")
self._test_result.setStyleSheet(
"color: rgba(255,255,255,0.6); padding: 12px; border-radius: 6px; font-size: 13px;"
)
from PyQt6.QtWidgets import QApplication
QApplication.processEvents()
ok, msg = wg_core.test_connection(self._cfg)
if ok:
self._test_result.setText(f"{msg}")
self._test_result.setStyleSheet(
"padding: 12px; border-radius: 6px; font-size: 13px; font-weight: bold;"
"background: rgba(39,174,96,0.25); color: #a9dfbf;"
)
steps = wg_core.diagnose_connection(self._cfg)
self._test_result.setText(self._format_diagnostic(steps))
states = [s["state"] for s in steps]
if "fail" in states:
css = "background: rgba(231,76,60,0.20); color: #f1948a;"
elif "warn" in states or "skip" in states:
css = "background: rgba(241,196,15,0.15); color: #f7dc6f;"
else:
self._test_result.setText(f"{msg}")
self._test_result.setStyleSheet(
"padding: 12px; border-radius: 6px; font-size: 13px; font-weight: bold;"
"background: rgba(231,76,60,0.25); color: #f1948a;"
css = "background: rgba(39,174,96,0.20); color: #a9dfbf;"
self._test_result.setStyleSheet(
"padding: 12px; border-radius: 6px; font-size: 12px; " + css
)
def _format_diagnostic(self, steps: list[dict]) -> str:
"""Met en forme les étapes ; la première en échec porte le verdict."""
from html import escape
rows = []
for st in steps:
bullet, color = self._STEP_STYLE.get(st["state"], ("", "white"))
weight = "bold" if st["state"] == "fail" else "normal"
detail = escape(st["detail"])
rows.append(
f'<div style="margin-bottom:5px;">'
f'<span style="color:{color}; font-weight:{weight};">'
f'{bullet}&nbsp;{escape(st["label"])}</span>'
f'<br><span style="color:rgba(255,255,255,0.65); font-size:11px;">'
f'&nbsp;&nbsp;&nbsp;&nbsp;{detail}</span></div>'
)
failed = next((s for s in steps if s["state"] == "fail"), None)
if failed:
verdict = (f'<div style="margin-bottom:9px; font-weight:bold;">'
f'⛔ Blocage : {escape(failed["label"])}</div>')
elif any(s["state"] == "skip" for s in steps):
verdict = ('<div style="margin-bottom:9px; font-weight:bold;">'
'➡️ Chaîne validée jusqu\'au tunnel — reste à le monter'
'</div>')
else:
verdict = ('<div style="margin-bottom:9px; font-weight:bold;">'
'✅ Chaîne de connexion complète</div>')
return verdict + "".join(rows)
def _export_qr(self):
"""Exporte la config client comme QR Code pour import mobile."""
self._save_values()
@@ -952,6 +1027,33 @@ class AdminWindow(QDialog):
note.setStyleSheet("color: rgba(255,255,255,0.7); font-size: 10px; padding: 6px;")
v.addWidget(note)
# Sans ce bouton, la PSK générée n'existait que dans ce texte : elle
# n'était reportée nulle part, et le tunnel monté avec les clés
# affichées ne négociait jamais avec le serveur.
btn_apply = QPushButton("📥 Appliquer le volet client à cette configuration")
btn_apply.setStyleSheet(
"QPushButton { background:#1e8449; color:white; border-radius:5px;"
" padding:7px 16px; border:none; } QPushButton:hover { background:#27ae60; }"
)
def _apply():
self._priv_key.setText(result["client_priv"])
self._pub_key.setText(result["client_pub"])
self._srv_pubkey.setText(result["server_pub"])
self._psk.setText(result["psk"])
self._refresh_preview()
dlg.accept()
QMessageBox.information(
self, "Config client appliquée",
"Clés client, clé publique du serveur et clé pré-partagée "
"reportées dans la configuration.\n\n"
"⚠ Déployez le volet serveur sur la machine distante et "
"renseignez son adresse publique."
)
btn_apply.clicked.connect(_apply)
v.addWidget(btn_apply)
btn_close = QPushButton("Fermer")
btn_close.setStyleSheet(
"QPushButton { background:#2471a3; color:white; border-radius:5px;"