fix(windows): suppression de sc.exe, bouton de configuration des permissions
- Supprime tous les appels à sc.exe (remplacés par PowerShell Get-Service + wireguard.exe) - wireguard.exe appelé via run_command() sous Windows (UAC gérée par son manifeste) - Supprime le double appel à uninstalltunnelservice (évite l'erreur 'service inexistant') - Ajoute un bouton dans l'onglet Élévation pour configurer les ACLs du service WireGuard (permet à un utilisateur standard de gérer ses tunnels sans UAC après configuration admin) - Incrémente la version à 0.7.2 Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
@@ -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
|
## [0.7.1] — 2026-09-01
|
||||||
|
|
||||||
### Ajouté
|
### Ajouté
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
## ──────────────────────────────────────────────
|
## ──────────────────────────────────────────────
|
||||||
|
|
||||||
APP := wgsecure
|
APP := wgsecure
|
||||||
VERSION := 0.7.1
|
VERSION := 0.7.2
|
||||||
VENV := .venv
|
VENV := .venv
|
||||||
PYTHON := $(VENV)/bin/python3
|
PYTHON := $(VENV)/bin/python3
|
||||||
PIP := $(VENV)/bin/pip
|
PIP := $(VENV)/bin/pip
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# 🛡️ WGSecure (WGS)
|
# 🛡️ 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).
|
**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
|
## 👤 Auteur
|
||||||
|
|
||||||
Développé par **Johnny** — [JT-Tools](https://github.com/JT-Tools)
|
Développé par **Johnny** — [JT-Tools](https://github.com/JT-Tools)
|
||||||
Version :  — Septembre 2026
|
Version :  — Septembre 2026
|
||||||
|
|||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
__version__ = "0.7.1"
|
__version__ = "0.7.2"
|
||||||
APP_NAME = "WGSecure"
|
APP_NAME = "WGSecure"
|
||||||
APP_SHORT = "WGS"
|
APP_SHORT = "WGS"
|
||||||
|
|||||||
+1
-1
@@ -36,7 +36,7 @@ _WG_DEFAULT: dict[str, Any] = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_DEFAULT: dict[str, Any] = {
|
_DEFAULT: dict[str, Any] = {
|
||||||
"version": "0.7.1",
|
"version": "0.7.2",
|
||||||
"admin_password_hash": "",
|
"admin_password_hash": "",
|
||||||
"admin_salt": "",
|
"admin_salt": "",
|
||||||
"mfa_enabled": False,
|
"mfa_enabled": False,
|
||||||
|
|||||||
+6
-2
@@ -69,8 +69,12 @@ def interface_exists(iface: str) -> bool:
|
|||||||
if is_windows():
|
if is_windows():
|
||||||
# Le service du tunnel survit à la fenêtre WireGuard : c'est lui qui
|
# Le service du tunnel survit à la fenêtre WireGuard : c'est lui qui
|
||||||
# fait foi, pas la présence de l'adaptateur.
|
# fait foi, pas la présence de l'adaptateur.
|
||||||
code, out, _ = run_command(["sc", "query", f"WireGuardTunnel${iface}"], 10)
|
code, out, _ = run_command(
|
||||||
return code == 0 and "1060" not in out
|
["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}")
|
return os.path.isdir(f"/sys/class/net/{iface}")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+22
-21
@@ -177,8 +177,12 @@ def is_connected(cfg: Config) -> bool:
|
|||||||
"""
|
"""
|
||||||
name = cfg.wg.get("interface_name", "wgs0")
|
name = cfg.wg.get("interface_name", "wgs0")
|
||||||
if is_windows():
|
if is_windows():
|
||||||
code, out, _ = run_command(["sc", "query", f"WireGuardTunnel${name}"])
|
code, out, _ = run_command(
|
||||||
return code == 0 and "RUNNING" in out
|
["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)
|
return dns_util.interface_is_up(name)
|
||||||
|
|
||||||
|
|
||||||
@@ -213,16 +217,11 @@ def force_cleanup(cfg: Config) -> list[str]:
|
|||||||
actions: list[str] = []
|
actions: list[str] = []
|
||||||
|
|
||||||
if is_windows():
|
if is_windows():
|
||||||
# Le service du tunnel restaure routes et DNS en s'arrêtant ; s'il est
|
# Le service du tunnel restaure routes et DNS en s'arrêtant.
|
||||||
# bloqué, on le supprime avant de purger les règles NRPT résiduelles.
|
# Sous Windows, wireguard.exe gère lui-même l'élévation via son manifeste UAC.
|
||||||
if dns_util.interface_exists(name):
|
if dns_util.interface_exists(name):
|
||||||
svc = f"WireGuardTunnel${name}"
|
cmd = [wireguard_exe(), "/uninstalltunnelservice", name]
|
||||||
run_privileged([wireguard_exe(), "/uninstalltunnelservice", name],
|
run_command(cmd, timeout=60)
|
||||||
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}")
|
|
||||||
actions += dns_util.cleanup(name)
|
actions += dns_util.cleanup(name)
|
||||||
return actions
|
return actions
|
||||||
|
|
||||||
@@ -276,9 +275,13 @@ def connect(cfg: Config) -> tuple[bool, str]:
|
|||||||
return True, "Tunnel WireGuard déjà actif"
|
return True, "Tunnel WireGuard déjà actif"
|
||||||
if dns_util.interface_exists(name):
|
if dns_util.interface_exists(name):
|
||||||
force_cleanup(cfg)
|
force_cleanup(cfg)
|
||||||
code, _, err = run_privileged(
|
# Sous Windows, wireguard.exe gère lui-même l'élévation via son manifeste UAC.
|
||||||
[wireguard_exe(), "/installtunnelservice", result], timeout=90
|
# 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
|
# /installtunnelservice enregistre le service et rend la main : le
|
||||||
# démarrage effectif du tunnel est asynchrone. Tester l'état
|
# démarrage effectif du tunnel est asynchrone. Tester l'état
|
||||||
# immédiatement conclurait « échec » sur un tunnel qui monte.
|
# 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")
|
name = cfg.wg.get("interface_name", "wgs0")
|
||||||
|
|
||||||
if is_windows():
|
if is_windows():
|
||||||
code, _, err = run_privileged(
|
# force_cleanup() appelle wireguard.exe /uninstalltunnelservice si le service existe,
|
||||||
[wireguard_exe(), "/uninstalltunnelservice", name], timeout=60
|
# puis nettoie les règles DNS résiduelles (NRPT).
|
||||||
)
|
|
||||||
# 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(cfg)
|
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 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 = ""
|
err = ""
|
||||||
if is_connected(cfg):
|
if is_connected(cfg):
|
||||||
|
|||||||
+99
-4
@@ -15,7 +15,7 @@ from app.core import dns as dns_util
|
|||||||
from app.core import mfa as mfa_core
|
from app.core import mfa as mfa_core
|
||||||
from app.ui import theme
|
from app.ui import theme
|
||||||
from app.ui.worker import ValueWorker
|
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):
|
class AdminWindow(QDialog):
|
||||||
def __init__(self, config: Config, parent=None):
|
def __init__(self, config: Config, parent=None):
|
||||||
@@ -669,6 +669,21 @@ class AdminWindow(QDialog):
|
|||||||
"UAC à chaque connexion."
|
"UAC à chaque connexion."
|
||||||
))
|
))
|
||||||
btn = QPushButton("🛡️ Vérifier l'élévation")
|
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:
|
else:
|
||||||
grp = QGroupBox("🔐 Droits d'exécution (sudo)")
|
grp = QGroupBox("🔐 Droits d'exécution (sudo)")
|
||||||
g = QVBoxLayout(grp)
|
g = QVBoxLayout(grp)
|
||||||
@@ -678,9 +693,10 @@ class AdminWindow(QDialog):
|
|||||||
))
|
))
|
||||||
btn = QPushButton("🔐 Vérifier les droits")
|
btn = QPushButton("🔐 Vérifier les droits")
|
||||||
|
|
||||||
btn.setStyleSheet(theme.primary_button_style())
|
if not is_windows():
|
||||||
btn.clicked.connect(self._check_privileges)
|
btn.setStyleSheet(theme.primary_button_style())
|
||||||
g.addWidget(btn)
|
btn.clicked.connect(self._check_privileges)
|
||||||
|
g.addWidget(btn)
|
||||||
|
|
||||||
self._priv_result = QTextEdit()
|
self._priv_result = QTextEdit()
|
||||||
self._priv_result.setReadOnly(True)
|
self._priv_result.setReadOnly(True)
|
||||||
@@ -1632,3 +1648,82 @@ class AdminWindow(QDialog):
|
|||||||
self._admin_pw2.clear()
|
self._admin_pw2.clear()
|
||||||
msg = "Mot de passe supprimé." if not p1 else "Mot de passe administrateur mis à jour."
|
msg = "Mot de passe supprimé." if not p1 else "Mot de passe administrateur mis à jour."
|
||||||
QMessageBox.information(self, "Succès", msg)
|
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"))
|
||||||
|
|||||||
@@ -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]:
|
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.
|
"""Relance `cmd` derrière une invite UAC et attend sa fin.
|
||||||
|
|
||||||
`wireguard.exe /installtunnelservice` et `sc create/delete` exigent les
|
`wireguard.exe /installtunnelservice` et `wireguard.exe /uninstalltunnelservice`
|
||||||
droits administrateur : lancés depuis une session standard, ils échouaient
|
exigent les droits administrateur : lancés depuis une session standard, ils
|
||||||
sur « Access is denied » sans que l'application le sache. ShellExecuteEx
|
é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
|
avec le verbe « runas » est le seul moyen d'obtenir l'élévation depuis un
|
||||||
processus déjà démarré.
|
processus déjà démarré.
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
Executable
BIN
Binary file not shown.
+1
-1
@@ -5,7 +5,7 @@
|
|||||||
; ──────────────────────────────────────────────
|
; ──────────────────────────────────────────────
|
||||||
|
|
||||||
#define MyAppName "WGSecure"
|
#define MyAppName "WGSecure"
|
||||||
#define MyAppVersion "0.7.1"
|
#define MyAppVersion "0.7.2"
|
||||||
#define MyAppPublisher "WGSecure"
|
#define MyAppPublisher "WGSecure"
|
||||||
#define MyAppExeName "wgsecure.exe"
|
#define MyAppExeName "wgsecure.exe"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user