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>
158 lines
5.0 KiB
Python
158 lines
5.0 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
WGSecure (WGS) — Interface graphique WireGuard avec MFA
|
|
Version 0.4.5
|
|
"""
|
|
import sys
|
|
import argparse
|
|
|
|
from PyQt6.QtWidgets import QApplication, QMessageBox, QInputDialog, QLineEdit, QSystemTrayIcon
|
|
from PyQt6.QtCore import Qt
|
|
|
|
from app.core.config import Config
|
|
from app.core import dns as dns_util
|
|
from app.core import log as conn_log
|
|
from app.core import shutdown as shutdown_guard
|
|
from app.ui.main_window import MainWindow
|
|
from app.ui.systray import SystemTray
|
|
from app.ui import icons
|
|
from app.utils.platform_utils import install_desktop_entry
|
|
|
|
|
|
def _install_taskbar_icon(app_icon) -> None:
|
|
"""Exporte l'icône en PNG 256 px et installe l'entrée .desktop."""
|
|
try:
|
|
import tempfile, os
|
|
tmp = os.path.join(tempfile.gettempdir(), "wgsecure_icon.png")
|
|
app_icon.pixmap(256, 256).save(tmp, "PNG")
|
|
install_desktop_entry(tmp)
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
def recover_dns_state(config: Config) -> None:
|
|
"""Répare la résolution DNS laissée détournée par une session précédente.
|
|
|
|
Si l'application (ou la machine) s'est arrêtée sans exécuter
|
|
`wg-quick down`, la configuration DNS du tunnel reste active alors que le
|
|
résolveur correspondant est injoignable : plus aucun nom ne se résout.
|
|
Le nettoyage n'a lieu que si un résidu est effectivement constaté.
|
|
"""
|
|
iface = config.wg.get("interface_name", "wgs0")
|
|
try:
|
|
repaired, msg = dns_util.repair_if_broken(iface, config.wg.get("dns", ""))
|
|
except Exception as e: # noqa: BLE001
|
|
repaired, msg = False, f"Vérification DNS impossible : {e}"
|
|
if msg:
|
|
conn_log.log_event(msg, "warning" if repaired else "error")
|
|
|
|
|
|
def parse_args() -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser(
|
|
prog="wgsecure",
|
|
description="WGSecure — WireGuard GUI avec MFA",
|
|
)
|
|
parser.add_argument(
|
|
"--admin",
|
|
action="store_true",
|
|
help="Lancer directement en mode Administrateur",
|
|
)
|
|
parser.add_argument(
|
|
"--no-tray",
|
|
action="store_true",
|
|
help="Désactiver l'icône systray",
|
|
)
|
|
return parser.parse_args()
|
|
|
|
|
|
def require_admin_auth(config: Config, app: QApplication) -> bool:
|
|
"""Vérifie le mot de passe admin si configuré. Retourne True si autorisé."""
|
|
if not config.has_admin_password():
|
|
return True
|
|
pw, ok = QInputDialog.getText(
|
|
None,
|
|
"Accès Administrateur",
|
|
"Mot de passe administrateur :",
|
|
QLineEdit.EchoMode.Password,
|
|
)
|
|
if not ok:
|
|
return False
|
|
if config.check_admin_password(pw):
|
|
return True
|
|
QMessageBox.warning(None, "WGSecure", "Mot de passe incorrect.")
|
|
return False
|
|
|
|
|
|
def main():
|
|
args = parse_args()
|
|
|
|
# Support DPI haute résolution
|
|
QApplication.setHighDpiScaleFactorRoundingPolicy(
|
|
Qt.HighDpiScaleFactorRoundingPolicy.PassThrough
|
|
)
|
|
app = QApplication(sys.argv)
|
|
app.setApplicationName("WGSecure")
|
|
app.setApplicationVersion("0.4.5")
|
|
app.setOrganizationName("WGS")
|
|
# Icône globale multi-tailles — barre des tâches + alt-tab
|
|
app_icon = icons.icon_app()
|
|
app.setWindowIcon(app_icon)
|
|
app.setDesktopFileName("wgsecure")
|
|
# Installe .desktop + PNG pour que GNOME/KDE affiche l'icône sur la taskbar
|
|
_install_taskbar_icon(app_icon)
|
|
# Empêche la fermeture quand toutes les fenêtres sont cachées (tray)
|
|
app.setQuitOnLastWindowClosed(False)
|
|
|
|
config = Config()
|
|
|
|
# Reste d'une session précédente mal terminée : DNS détourné vers un
|
|
# résolveur mort. À faire avant d'ouvrir la fenêtre pour rendre l'accès
|
|
# au réseau tout de suite.
|
|
recover_dns_state(config)
|
|
|
|
# Restauration de l'état réseau garantie sur toutes les sorties :
|
|
# Quitter, croix, SIGINT/SIGTERM, atexit, exception non rattrapée.
|
|
shutdown_guard.install(config, report=conn_log.log_event, app=app)
|
|
|
|
# Mode Admin direct (--admin)
|
|
if args.admin:
|
|
if not require_admin_auth(config, app):
|
|
sys.exit(0)
|
|
from app.ui.admin_window import AdminWindow
|
|
win = AdminWindow(config)
|
|
win.setWindowIcon(icons.icon_admin())
|
|
win.show()
|
|
sys.exit(app.exec())
|
|
|
|
# Mode User (défaut)
|
|
window = MainWindow(config)
|
|
|
|
# Systray
|
|
if not args.no_tray and QSystemTrayIcon_available():
|
|
tray = SystemTray(config, window)
|
|
tray.show()
|
|
# Démarrer réduit si déjà configuré
|
|
if config.configured:
|
|
window.show()
|
|
else:
|
|
window.show()
|
|
tray.showMessage(
|
|
"WGSecure",
|
|
"Bienvenue ! Ouvrez le panneau Administrateur pour configurer WireGuard.",
|
|
QSystemTrayIcon.MessageIcon.Information,
|
|
4000,
|
|
)
|
|
else:
|
|
window.show()
|
|
|
|
sys.exit(app.exec())
|
|
|
|
|
|
def QSystemTrayIcon_available() -> bool:
|
|
from PyQt6.QtWidgets import QSystemTrayIcon
|
|
return QSystemTrayIcon.isSystemTrayAvailable()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|