#!/usr/bin/env python3 """ WGSecure (WGS) — Interface graphique WireGuard avec MFA """ import sys import argparse from PyQt6.QtWidgets import QApplication, QMessageBox, QInputDialog, QLineEdit, QSystemTrayIcon from PyQt6.QtCore import Qt from app import __version__ 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.core.single_instance import SingleInstanceGuard 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(__version__) app.setOrganizationName("WGS") # Une seule instance à la fois : un second lancement réveille la # première plutôt que d'ouvrir une deuxième fenêtre sur le même tunnel. guard = SingleInstanceGuard() if not guard.try_acquire(): sys.exit(0) # 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() guard.on_activate_requested(lambda: _raise_window(win)) sys.exit(app.exec()) # Mode User (défaut) window = MainWindow(config) guard.on_activate_requested(lambda: _raise_window(window)) # 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 _raise_window(window) -> None: """Ramène une fenêtre existante au premier plan (second lancement).""" window.showNormal() window.raise_() window.activateWindow() def QSystemTrayIcon_available() -> bool: from PyQt6.QtWidgets import QSystemTrayIcon return QSystemTrayIcon.isSystemTrayAvailable() if __name__ == "__main__": main()