diff --git a/Makefile b/Makefile index c1fa783..57b4434 100644 --- a/Makefile +++ b/Makefile @@ -27,7 +27,7 @@ PI_OPTS := \ --hidden-import cryptography.hazmat.primitives.asymmetric.x25519 \ --collect-submodules PyQt6 -.PHONY: all linux windows release version changelog install install-gnome uninstall-gnome run run-admin icon reset-password setup-sudoers check-privileges clean clean-all help venv +.PHONY: all linux windows installer-deps release version changelog install install-gnome uninstall-gnome run run-admin icon reset-password setup-sudoers check-privileges clean clean-all help venv # ── Cible par défaut ──────────────────────────────────────────────────────── all: linux @@ -69,6 +69,7 @@ linux: install icon $(PYTHON) -m PyInstaller $(PI_OPTS) \ --onefile \ --icon=$(ICON) \ + --add-data "CHANGELOG.md:." \ $(SRC) @echo "" @echo " ✅ Binaire Linux → $(DIST)/$(APP)" @@ -97,6 +98,7 @@ windows: icon --icon=$(ICON) \ --add-data "$(WINE_QT_PLUGINS)/platforms;PyQt6/Qt6/plugins/platforms" \ --add-data "$(WINE_QT_PLUGINS)/styles;PyQt6/Qt6/plugins/styles" \ + --add-data "CHANGELOG.md;." \ $(SRC) \ || (echo ""; \ echo " ❌ Échec : Python Windows introuvable dans Wine."; \ @@ -106,6 +108,25 @@ windows: icon @echo " ✅ Binaire Windows → $(DIST)/$(APP).exe" @echo "" +# ── Prérequis de l'installeur Windows (wgsecure.iss) ───────────────────────── +# WireGuard n'est pas empaqueté avec WGSecure (c'est wg.exe et le pilote +# tunnel qui manquent, pas une simple DLL) : wgsecure.iss l'installe donc en +# silencieux à partir de ce binaire, téléchargé ici une bonne fois pour +# toutes plutôt que d'imposer une étape manuelle avant chaque compilation. +WIREGUARD_URL := https://download.wireguard.com/windows-client/wireguard-installer.exe +VCREDIST_URL := https://aka.ms/vs/17/release/vc_redist.x64.exe + +installer/wireguard-installer.exe: + @echo " ⬇️ Téléchargement de l'installeur WireGuard…" + @curl -L --fail -o $@ $(WIREGUARD_URL) + +installer/VC_redist.x64.exe: + @echo " ⬇️ Téléchargement du Visual C++ Redistributable…" + @curl -L --fail -o $@ $(VCREDIST_URL) + +installer-deps: installer/wireguard-installer.exe installer/VC_redist.x64.exe + @echo " ✅ Prérequis de l'installeur prêts dans installer/" + # ── Installation de Python dans Wine ───────────────────────────────────────── WINE_PY_VER := 3.11.9 WINE_PY_URL := https://www.python.org/ftp/python/$(WINE_PY_VER)/python-$(WINE_PY_VER)-amd64.exe @@ -272,6 +293,7 @@ help: @echo " ║ make install Installe les dépendances ║" @echo " ║ make linux Binaire Linux (onefile) ║" @echo " ║ make windows Binaire Windows (Wine) ║" + @echo " ║ make installer-deps Prérequis wgsecure.iss ║" @echo " ║ make release Linux + nommage release ║" @echo " ║ make version Affiche la version ║" @echo " ║ make changelog Dernière entrée changelog║" diff --git a/app/app_info.py b/app/app_info.py index 4c4b587..f390cd9 100644 --- a/app/app_info.py +++ b/app/app_info.py @@ -7,12 +7,21 @@ seul endroit où elle est bumpée à chaque release — jamais recopiée en dur """ import os +import sys from app import __version__ -_CHANGELOG_PATH = os.path.join( - os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "CHANGELOG.md" -) +# En développement, CHANGELOG.md vit à la racine du dépôt, deux niveaux +# au-dessus de ce fichier. Une fois empaqueté par PyInstaller (--onefile), +# `__file__` pointe vers l'intérieur de l'exécutable et ce chemin n'existe +# plus : le fichier doit alors être cherché dans `sys._MEIPASS`, le dossier +# temporaire où l'exe extrait les données embarquées via `--add-data`. +if getattr(sys, "frozen", False): + _CHANGELOG_PATH = os.path.join(getattr(sys, "_MEIPASS", ""), "CHANGELOG.md") +else: + _CHANGELOG_PATH = os.path.join( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "CHANGELOG.md" + ) APP_NAME = "WGSecure" TAGLINE = "Client WireGuard avec surcouche MFA TOTP, pour Linux et Windows" diff --git a/app/core/wireguard.py b/app/core/wireguard.py index 7fb40c4..8b47743 100644 --- a/app/core/wireguard.py +++ b/app/core/wireguard.py @@ -994,6 +994,34 @@ def ping_server(host: str, timeout: int = 2) -> int | None: return 0 +#: Au-delà de cet âge (s), un handshake est considéré trop ancien pour +#: garantir que le tunnel répond encore (keepalive par défaut : 25 s). +_HANDSHAKE_STALE_S = 150 + + +def ping_or_handshake(cfg: Config, timeout: int = 2) -> tuple[int | None, str]: + """Latence ICMP vers l'endpoint, avec repli sur l'âge du handshake. + + De nombreux serveurs (ou un pare-feu en chemin) bloquent l'ICMP echo + tout en laissant le tunnel WireGuard fonctionner parfaitement : sans ce + repli, le badge de statut affichait « hors ligne » en continu sur ces + configurations, alors que le handshake était récent et le trafic RX/TX + bien réel. Retourne (valeur, source) où `source` vaut "ping" (latence + ICMP), "handshake" (âge du dernier handshake, tunnel sain), "stale" + (handshake trop ancien) ou "down" (aucune des deux sondes n'aboutit). + """ + host = cfg.wg.get("server_endpoint", "") + ms = ping_server(host, timeout) if host else None + if ms is not None: + return ms, "ping" + age = last_handshake_age(cfg.wg.get("interface_name", "wgs0")) + if age is not None and 0 <= age <= _HANDSHAKE_STALE_S: + return age, "handshake" + if age is not None and age > _HANDSHAKE_STALE_S: + return age, "stale" + return None, "down" + + # ── Test DNS leak ───────────────────────────────────────────────────────── def dns_leak_test(cfg: Config) -> dict: diff --git a/app/ui/main_window.py b/app/ui/main_window.py index 2467128..a727cb6 100644 --- a/app/ui/main_window.py +++ b/app/ui/main_window.py @@ -414,7 +414,7 @@ class MainWindow(QMainWindow): return # `ping` bloque jusqu'à 4 s : exécuté dans le thread Qt, il faisait # saccader toute l'interface toutes les 10 secondes. - self._ping_worker = ValueWorker(wg_core.ping_server, host, 2, parent=self) + self._ping_worker = ValueWorker(wg_core.ping_or_handshake, self._cfg, 2, parent=self) self._ping_worker.done.connect(self._on_ping_done) # Détruire le QThread depuis `finished` et non depuis `done` : `done` # est émis avant la sortie de run(), un deleteLater() à ce moment @@ -422,12 +422,20 @@ class MainWindow(QMainWindow): self._ping_worker.finished.connect(self._ping_worker.deleteLater) self._ping_worker.start() - def _on_ping_done(self, ms): + def _on_ping_done(self, result): self._ping_worker = None + ms, source = result if result else (None, "down") if not self._is_connected: self._set_ping_badge(None, "— ms", "offline") - elif ms is None: + elif source == "down": self._set_ping_badge(None, "hors ligne", "bad") + elif source == "stale": + self._set_ping_badge(None, "handshake ancien", "bad") + elif source == "handshake": + # ICMP bloqué par le serveur/pare-feu mais handshake WireGuard + # récent : le tunnel fonctionne, la latence réelle est juste + # invisible depuis ce test. + self._set_ping_badge(None, f"actif ({ms}s)", "medium") elif ms < 50: self._set_ping_badge(ms, f"{ms} ms", "good") elif ms < 200: diff --git a/wgsecure.iss b/wgsecure.iss index 2ebd34f..f219b42 100644 --- a/wgsecure.iss +++ b/wgsecure.iss @@ -1,6 +1,7 @@ ; ────────────────────────────────────────────── ; WGSecure (WGS) — Script Inno Setup -; Installeur Windows (bundle VC++ Redistributable) +; Installeur Windows complet : WGSecure + WireGuard + +; Visual C++ Redistributable, chacun installé en silence si absent. ; ────────────────────────────────────────────── #define MyAppName "WGSecure" @@ -29,9 +30,15 @@ Name: "french"; MessagesFile: "compiler:Languages\French.isl" [Files] Source: "dist\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion -; VC++ Redistributable x64 — à télécharger avant compilation : -; https://aka.ms/vs/17/release/vc_redist.x64.exe → installer\vc_redist.x64.exe -Source: "installer\vc_redist.x64.exe"; DestDir: "{tmp}"; Flags: deleteafterinstall +; VC++ Redistributable x64 — téléchargé automatiquement par `make installer-deps` : +; https://aka.ms/vs/17/release/vc_redist.x64.exe → installer\VC_redist.x64.exe +Source: "installer\VC_redist.x64.exe"; DestDir: "{tmp}"; Flags: deleteafterinstall +; WireGuard pour Windows — prérequis à WGSecure (fournit wg.exe, le pilote +; tunnel et le service WireGuardTunnel$ que wireguard.py pilote). Téléchargé +; automatiquement par `make installer-deps` : +; https://download.wireguard.com/windows-client/wireguard-installer.exe +; → installer\wireguard-installer.exe +Source: "installer\wireguard-installer.exe"; DestDir: "{tmp}"; Flags: deleteafterinstall [Icons] Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" @@ -39,7 +46,12 @@ Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" [Run] ; Installation silencieuse du VC++ Redistributable si absent (corrige "Failed to load Python DLL: python312.dll") -Filename: "{tmp}\vc_redist.x64.exe"; Parameters: "/install /quiet /norestart"; StatusMsg: "Installation du composant Visual C++ Redistributable…"; Check: VCRedistNeedsInstall +Filename: "{tmp}\VC_redist.x64.exe"; Parameters: "/install /quiet /norestart"; StatusMsg: "Installation du composant Visual C++ Redistributable…"; Check: VCRedistNeedsInstall +; L'installeur officiel WireGuard s'exécute déjà sans assistant (juste une +; fenêtre de progression) : notre propre installeur tourne en admin +; (PrivilegesRequired=admin ci-dessus), donc pas d'UAC supplémentaire non plus. +; `waituntilterminated` : l'app ne doit pas démarrer avant que wg.exe existe. +Filename: "{tmp}\wireguard-installer.exe"; StatusMsg: "Installation de WireGuard…"; Check: WireGuardNeedsInstall; Flags: waituntilterminated Filename: "{app}\{#MyAppExeName}"; Description: "Lancer {#MyAppName}"; Flags: nowait postinstall skipifsilent [Code] @@ -50,3 +62,11 @@ begin // Clé présente si VC++ 2015-2022 x64 (v14+) déjà installé Result := not RegQueryStringValue(HKLM64, 'SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\X64', 'Version', Version); end; + +function WireGuardNeedsInstall: Boolean; +begin + // WireGuard s'installe toujours dans le Program Files 64 bits, quelle que + // soit l'architecture de WGSecure : {pf} y pointe bien puisque + // ArchitecturesInstallIn64BitMode=x64compatible ci-dessus. + Result := not FileExists(ExpandConstant('{pf}\WireGuard\wireguard.exe')); +end;