diff --git a/CHANGELOG.md b/CHANGELOG.md index ae8e645..167b4b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,17 @@ Ce projet suit le [Versionnage Sémantique](https://semver.org/lang/fr/). --- +## [0.7.2] — 2026-09-02 + +### Ajouté +- **Bouton de configuration des permissions Windows.** Dans l'onglet Tests & diagnostic → Élévation, un nouveau bouton permet à l'administrateur de configurer les ACLs du service WireGuard pour autoriser l'utilisateur actuel à créer/supprimer ses tunnels sans invite UAC. Utilise PowerShell pour modifier les SDDL du service et redémarrer celui-ci. + +### Corrigé +- **Appels à sc.exe supprimés sous Windows.** Remplacés par PowerShell `Get-Service` pour la détection, et `wireguard.exe` pour la gestion des services. Supprime les messages d'erreur "service inexistant" et évite les doubles appels à `uninstalltunnelservice`. +- **UAC déclenchée par WGSecure sous Windows.** Désormais, `wireguard.exe` est appelé via `run_command()` (pas `run_privileged`) et gère lui-même l'élévation via son manifeste natif. Si l'utilisateur a les permissions ACLs, aucune UAC n'est affichée. + +--- + ## [0.7.1] — 2026-09-01 ### Ajouté diff --git a/Makefile b/Makefile index 3fb369d..c7447dd 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ ## ────────────────────────────────────────────── APP := wgsecure -VERSION := 0.7.1 +VERSION := 0.7.2 VENV := .venv PYTHON := $(VENV)/bin/python3 PIP := $(VENV)/bin/pip diff --git a/README.md b/README.md index e07468e..a4377b1 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # 🛡️ WGSecure (WGS) -🚀 v0.7.1 · 🐍 · 🪟🐧 · 🔐 MFA TOTP · 🛡️ WireGuard +🚀 v0.7.2 · 🐍 · 🪟🐧 · 🔐 MFA TOTP · 🛡️ WireGuard **WGSecure** est une interface graphique multiplateforme (Windows & Linux) pour gérer une connexion WireGuard avec une surcouche MFA (authentification à deux facteurs TOTP). @@ -208,4 +208,4 @@ WGSecure/ ## 👤 Auteur Développé par **Johnny** — [JT-Tools](https://github.com/JT-Tools) -Version : ![v0.7.1](https://img.shields.io/badge/v0.7.1-septembre%202026-2980b9?style=flat-square) — Septembre 2026 +Version : ![v0.7.2](https://img.shields.io/badge/v0.7.2-septembre%202026-2980b9?style=flat-square) — Septembre 2026 diff --git a/app/__init__.py b/app/__init__.py index ddce92e..b6ae49c 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,3 +1,3 @@ -__version__ = "0.7.1" +__version__ = "0.7.2" APP_NAME = "WGSecure" APP_SHORT = "WGS" diff --git a/app/core/config.py b/app/core/config.py index 1d1d6a0..afec6d6 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -36,7 +36,7 @@ _WG_DEFAULT: dict[str, Any] = { } _DEFAULT: dict[str, Any] = { - "version": "0.7.1", + "version": "0.7.2", "admin_password_hash": "", "admin_salt": "", "mfa_enabled": False, diff --git a/app/core/dns.py b/app/core/dns.py index d43ec67..d418fa0 100644 --- a/app/core/dns.py +++ b/app/core/dns.py @@ -69,8 +69,12 @@ def interface_exists(iface: str) -> bool: if is_windows(): # Le service du tunnel survit à la fenêtre WireGuard : c'est lui qui # fait foi, pas la présence de l'adaptateur. - code, out, _ = run_command(["sc", "query", f"WireGuardTunnel${iface}"], 10) - return code == 0 and "1060" not in out + code, out, _ = run_command( + ["powershell", "-NoProfile", "-NonInteractive", "-Command", + f"Get-Service -Name 'WireGuardTunnel${iface}' -ErrorAction SilentlyContinue"], + 10 + ) + return code == 0 and out.strip() return os.path.isdir(f"/sys/class/net/{iface}") diff --git a/app/core/wireguard.py b/app/core/wireguard.py index f02a5bf..6bd3630 100644 --- a/app/core/wireguard.py +++ b/app/core/wireguard.py @@ -177,8 +177,12 @@ def is_connected(cfg: Config) -> bool: """ name = cfg.wg.get("interface_name", "wgs0") if is_windows(): - code, out, _ = run_command(["sc", "query", f"WireGuardTunnel${name}"]) - return code == 0 and "RUNNING" in out + code, out, _ = run_command( + ["powershell", "-NoProfile", "-NonInteractive", "-Command", + f"(Get-Service -Name 'WireGuardTunnel${name}' -ErrorAction SilentlyContinue).Status -eq 'Running'"], + timeout=10 + ) + return code == 0 and "True" in out return dns_util.interface_is_up(name) @@ -213,16 +217,11 @@ def force_cleanup(cfg: Config) -> list[str]: actions: list[str] = [] if is_windows(): - # Le service du tunnel restaure routes et DNS en s'arrêtant ; s'il est - # bloqué, on le supprime avant de purger les règles NRPT résiduelles. + # Le service du tunnel restaure routes et DNS en s'arrêtant. + # Sous Windows, wireguard.exe gère lui-même l'élévation via son manifeste UAC. if dns_util.interface_exists(name): - svc = f"WireGuardTunnel${name}" - run_privileged([wireguard_exe(), "/uninstalltunnelservice", name], - timeout=60) - if dns_util.interface_exists(name): - run_privileged(["sc", "stop", svc], timeout=30) - run_privileged(["sc", "delete", svc], timeout=30) - actions.append(f"sc delete {svc}") + cmd = [wireguard_exe(), "/uninstalltunnelservice", name] + run_command(cmd, timeout=60) actions += dns_util.cleanup(name) return actions @@ -276,9 +275,13 @@ def connect(cfg: Config) -> tuple[bool, str]: return True, "Tunnel WireGuard déjà actif" if dns_util.interface_exists(name): force_cleanup(cfg) - code, _, err = run_privileged( - [wireguard_exe(), "/installtunnelservice", result], timeout=90 - ) + # Sous Windows, wireguard.exe gère lui-même l'élévation via son manifeste UAC. + # Sous Linux, wg-quick nécessite sudo. + cmd = [wireguard_exe(), "/installtunnelservice", result] + if is_windows(): + code, _, err = run_command(cmd, timeout=90) + else: + code, _, err = run_privileged(cmd, timeout=90) # /installtunnelservice enregistre le service et rend la main : le # démarrage effectif du tunnel est asynchrone. Tester l'état # immédiatement conclurait « échec » sur un tunnel qui monte. @@ -312,15 +315,13 @@ def disconnect(cfg: Config) -> tuple[bool, str]: name = cfg.wg.get("interface_name", "wgs0") if is_windows(): - code, _, err = run_privileged( - [wireguard_exe(), "/uninstalltunnelservice", name], timeout=60 - ) - # Nettoyage inconditionnel : si le service reste ou qu'une règle NRPT - # survit, la résolution DNS resterait détournée vers le DNS du tunnel. + # force_cleanup() appelle wireguard.exe /uninstalltunnelservice si le service existe, + # puis nettoie les règles DNS résiduelles (NRPT). force_cleanup(cfg) - if code == 0 or not dns_util.interface_exists(name): + if not dns_util.interface_exists(name): return True, "Déconnecté" - return False, err or "Erreur lors de la déconnexion" + # Si le service existe toujours, c'est qu'il n'a pas pu être supprimé. + return False, "Erreur lors de la déconnexion : le service n'a pas pu être arrêté" err = "" if is_connected(cfg): diff --git a/app/ui/admin_window.py b/app/ui/admin_window.py index a1632f4..2a2c6e8 100644 --- a/app/ui/admin_window.py +++ b/app/ui/admin_window.py @@ -15,7 +15,7 @@ from app.core import dns as dns_util from app.core import mfa as mfa_core from app.ui import theme from app.ui.worker import ValueWorker -from app.utils.platform_utils import is_windows +from app.utils.platform_utils import is_windows, run_command, run_privileged class AdminWindow(QDialog): def __init__(self, config: Config, parent=None): @@ -669,6 +669,21 @@ class AdminWindow(QDialog): "UAC à chaque connexion." )) btn = QPushButton("🛡️ Vérifier l'élévation") + btn.setStyleSheet(theme.primary_button_style()) + btn.clicked.connect(self._check_privileges) + g.addWidget(btn) + + # Bouton pour configurer les permissions de l'utilisateur actuel + self._btn_grant_permissions = QPushButton( + "🔧 Autoriser l'utilisateur à gérer ses tunnels" + ) + self._btn_grant_permissions.setStyleSheet(theme.secondary_button_style()) + self._btn_grant_permissions.clicked.connect(self._grant_wireguard_permissions) + self._btn_grant_permissions.setToolTip( + "Configure les permissions pour permettre à l'utilisateur actuel de " + "créer et supprimer des tunnels WireGuard sans UAC. Nécessite des droits admin." + ) + g.addWidget(self._btn_grant_permissions) else: grp = QGroupBox("🔐 Droits d'exécution (sudo)") g = QVBoxLayout(grp) @@ -678,9 +693,10 @@ class AdminWindow(QDialog): )) btn = QPushButton("🔐 Vérifier les droits") - btn.setStyleSheet(theme.primary_button_style()) - btn.clicked.connect(self._check_privileges) - g.addWidget(btn) + if not is_windows(): + btn.setStyleSheet(theme.primary_button_style()) + btn.clicked.connect(self._check_privileges) + g.addWidget(btn) self._priv_result = QTextEdit() self._priv_result.setReadOnly(True) @@ -1632,3 +1648,82 @@ class AdminWindow(QDialog): self._admin_pw2.clear() msg = "Mot de passe supprimé." if not p1 else "Mot de passe administrateur mis à jour." QMessageBox.information(self, "Succès", msg) + + def _grant_wireguard_permissions(self): + """Configure les ACLs du service WireGuard pour permettre à l'utilisateur actuel + de gérer ses propres tunnels sans UAC. + + Nécessite des droits administrateur. + """ + if not is_windows(): + return + + msg_box = QMessageBox(self) + msg_box.setIcon(QMessageBox.Icon.Question) + msg_box.setWindowTitle("Confirmer l'opération administrative") + msg_box.setText( + "Cette opération va configurer les permissions du service WireGuard " + "pour permettre à l'utilisateur actuel de créer et supprimer des tunnels " + "sans invite UAC.\n\n" + "⚠️ Nécessite des droits administrateur et redémarre le service WireGuard." + ) + msg_box.setStandardButtons( + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No + ) + msg_box.setDefaultButton(QMessageBox.StandardButton.No) + + if msg_box.exec() != QMessageBox.StandardButton.Yes: + return + + # Commande PowerShell pour modifier les ACLs du service WireGuard + # Ajoute à l'utilisateur actuel les droits de créer/supprimer des services WireGuardTunnel* + ps_script = ''' +$serviceName = "WireGuard" +$currentUserSid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value + +# Récupérer la SDDL actuelle +$currentSddl = (Get-WmiObject -Class Win32_Service -Filter "Name='$serviceName'").GetSecurityDescriptor().Descriptor.DACL.ToString() + +# Ajouter l'utilisateur actuel avec les permissions nécessaires +# CC = Create Child (créer des services enfants) +# LC = List Child (lister les services enfants) +# SW = Self Write (modifier ses propres services) +# LO = List Object (lister) +# CR = Control (contrôle) +$newAcl = "D:(A;;CCLCSWLOCRRC;;;$currentUserSid)" + +# Appliquer la nouvelle SDDL (en ajoutant aux ACLs existantes) +$fullSddl = $currentSddl + $newAcl +sc sdset $serviceName $fullSddl + +# Redémarrer le service pour appliquer les changements +Restart-Service -Name $serviceName -Force -ErrorAction SilentlyContinue + +Write-Output "Permissions configurées pour l'utilisateur $currentUserSid" +''' + + self._btn_grant_permissions.setEnabled(False) + self._priv_result.setPlainText("Configuration des permissions en cours…") + + # Exécuter avec élévation (nécessite admin) + code, out, err = run_privileged( + ["powershell", "-NoProfile", "-NonInteractive", "-Command", ps_script], + timeout=30 + ) + + self._btn_grant_permissions.setEnabled(True) + + if code == 0: + self._priv_result.setPlainText( + f"✅ Permissions configurées avec succès !\n\n" + f"L'utilisateur actuel peut désormais gérer ses tunnels WireGuard " + f"sans invite UAC (si le service WireGuard est démarré).\n\n" + f"Détails : {out.strip()}" + ) + self._priv_result.setStyleSheet(theme.result_view_style("ok")) + else: + error_msg = err.strip() or out.strip() or "Erreur inconnue" + self._priv_result.setPlainText( + f"❌ Échec de la configuration des permissions:\n\n{error_msg}" + ) + self._priv_result.setStyleSheet(theme.result_view_style("fail")) diff --git a/app/utils/platform_utils.py b/app/utils/platform_utils.py index ebb1164..96da80b 100644 --- a/app/utils/platform_utils.py +++ b/app/utils/platform_utils.py @@ -165,9 +165,9 @@ def _sudo_refused_auth(err: str) -> bool: def _run_elevated_windows(cmd: list[str], timeout: int = 60) -> tuple[int, str, str]: """Relance `cmd` derrière une invite UAC et attend sa fin. - `wireguard.exe /installtunnelservice` et `sc create/delete` exigent les - droits administrateur : lancés depuis une session standard, ils échouaient - sur « Access is denied » sans que l'application le sache. ShellExecuteEx + `wireguard.exe /installtunnelservice` et `wireguard.exe /uninstalltunnelservice` + exigent les droits administrateur : lancés depuis une session standard, ils + échouaient sur « Access is denied » sans que l'application le sache. ShellExecuteEx avec le verbe « runas » est le seul moyen d'obtenir l'élévation depuis un processus déjà démarré. diff --git a/installer/wireguard-installer.exe b/installer/wireguard-installer.exe new file mode 100644 index 0000000..23372ae Binary files /dev/null and b/installer/wireguard-installer.exe differ diff --git a/wgsecure-0.7.1-linux-x86_64 b/wgsecure-0.7.1-linux-x86_64 new file mode 100755 index 0000000..2238bf6 Binary files /dev/null and b/wgsecure-0.7.1-linux-x86_64 differ diff --git a/wgsecure.iss b/wgsecure.iss index cd57372..ac8d880 100644 --- a/wgsecure.iss +++ b/wgsecure.iss @@ -5,7 +5,7 @@ ; ────────────────────────────────────────────── #define MyAppName "WGSecure" -#define MyAppVersion "0.7.1" +#define MyAppVersion "0.7.2" #define MyAppPublisher "WGSecure" #define MyAppExeName "wgsecure.exe"