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:
co-authored by
Claude Opus 5
parent
09724f4e4f
commit
9d785c00cc
+119
-17
@@ -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 à 1280–1420 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} {escape(st["label"])}</span>'
|
||||
f'<br><span style="color:rgba(255,255,255,0.65); font-size:11px;">'
|
||||
f' {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;"
|
||||
|
||||
Reference in New Issue
Block a user