chore(release): v0.7.12
Ajoute wgsecure-helper, un service Windows privilégié (LocalSystem) qui installe/désinstalle le tunnel et pose/retire les règles NRPT sans invite d'authentification répétée. L'ACL du service WireGuard (v0.7.6) ne délègue que démarrer/arrêter un tunnel déjà installé, jamais le créer/supprimer, ce que WGSecure fait pourtant à chaque connexion/déconnexion — d'où les prompts qui persistaient malgré v0.7.6-v0.7.11. Corrige aussi le handshake, qui échouait pour la même raison (wg show sans élévation complète). Purement additif : l'app retombe sur son chemin élevé existant si le service est absent (composant optionnel dans l'installeur). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,14 @@ Ce projet suit le [Versionnage Sémantique](https://semver.org/lang/fr/).
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## [0.7.12] — 2026-09-04
|
||||||
|
|
||||||
|
### Ajouté
|
||||||
|
- **Service Windows privilégié `wgsecure-helper`.** Élimine les authentifications admin restantes à la connexion/déconnexion pour un compte non-admin. Tourne en LocalSystem, exécutable séparé jamais chargé par l'app principale ; expose cinq commandes fixes (`install_tunnel`, `uninstall_tunnel`, `set_split_dns`, `clear_split_dns`, `get_status`) via un pipe nommé sécurisé (SDDL explicite), chacune validant strictement ses paramètres — jamais de script/commande arbitraire accepté. L'app principale tente ce service en premier (client `ctypes` pur, sans nouvelle dépendance) et retombe sur le chemin élevé existant s'il est absent : purement additif, installation optionnelle. Corrige au passage le handshake qui ne se rafraîchissait jamais (`wg show` échouait systématiquement sans élévation complète, même avec les délégations de la v0.7.6).
|
||||||
|
- L'ACL du service WireGuard (bouton « Configurer les permissions ») ne délègue que démarrer/arrêter un tunnel déjà installé, jamais le créer/supprimer — WGSecure réinstalle pourtant le tunnel à chaque connexion/déconnexion. C'est ce qui rendait les invites d'authentification inévitables malgré la v0.7.6 à v0.7.11 ; le nouveau service couvre cette étape.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [0.7.11] — 2026-09-04
|
## [0.7.11] — 2026-09-04
|
||||||
|
|
||||||
### Corrigé
|
### Corrigé
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
## ──────────────────────────────────────────────
|
## ──────────────────────────────────────────────
|
||||||
|
|
||||||
APP := wgsecure
|
APP := wgsecure
|
||||||
VERSION := 0.7.11
|
VERSION := 0.7.12
|
||||||
VENV := .venv
|
VENV := .venv
|
||||||
PYTHON := $(VENV)/bin/python3
|
PYTHON := $(VENV)/bin/python3
|
||||||
PIP := $(VENV)/bin/pip
|
PIP := $(VENV)/bin/pip
|
||||||
@@ -27,7 +27,7 @@ PI_OPTS := \
|
|||||||
--hidden-import cryptography.hazmat.primitives.asymmetric.x25519 \
|
--hidden-import cryptography.hazmat.primitives.asymmetric.x25519 \
|
||||||
--collect-submodules PyQt6
|
--collect-submodules PyQt6
|
||||||
|
|
||||||
.PHONY: all linux windows installer-deps release publish 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 helper installer-deps release publish 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 ────────────────────────────────────────────────────────
|
# ── Cible par défaut ────────────────────────────────────────────────────────
|
||||||
all: linux
|
all: linux
|
||||||
@@ -108,6 +108,36 @@ windows: icon
|
|||||||
@echo " ✅ Binaire Windows → $(DIST)/$(APP).exe"
|
@echo " ✅ Binaire Windows → $(DIST)/$(APP).exe"
|
||||||
@echo ""
|
@echo ""
|
||||||
|
|
||||||
|
# ── Service Windows privilégié (wgsecure-helper.exe) ─────────────────────────
|
||||||
|
# Exécutable séparé de wgsecure.exe : installe/désinstalle le tunnel et pose/
|
||||||
|
# retire les règles NRPT (split-DNS) en LocalSystem, pour éviter une invite
|
||||||
|
# d'authentification à chaque connexion/déconnexion (cf. app/service/). Utilise
|
||||||
|
# pywin32, installé uniquement dans ce venv Wine — jamais dans .venv (Linux) ni
|
||||||
|
# dans les dépendances de wgsecure.exe. Cible explicite : ni `windows` ni
|
||||||
|
# `release` ne la déclenchent implicitement, ce composant restant optionnel.
|
||||||
|
# --hidden-import win32timezone : dépendance dynamique de pywin32 que
|
||||||
|
# l'analyse statique de PyInstaller ne détecte pas seule (service qui
|
||||||
|
# démarre mais plante à l'exécution sans ce forçage — cas connu).
|
||||||
|
helper:
|
||||||
|
@echo ""
|
||||||
|
@echo " 🛡️ Compilation du service (Wine + PyInstaller)…"
|
||||||
|
@echo ""
|
||||||
|
@which wine > /dev/null 2>&1 \
|
||||||
|
|| (echo " ❌ Wine non installé → sudo apt install wine"; exit 1)
|
||||||
|
@test -f "$(WINE_PYTHON)" \
|
||||||
|
|| (echo " ❌ Python Windows introuvable dans Wine → make setup-wine"; exit 1)
|
||||||
|
@wine $(WINE_PYTHON) -m pip install -r requirements-helper.txt --quiet
|
||||||
|
@wine $(WINE_PYTHON) -m PyInstaller \
|
||||||
|
--noconfirm \
|
||||||
|
--clean \
|
||||||
|
--name wgsecure-helper \
|
||||||
|
--onefile \
|
||||||
|
--hidden-import win32timezone \
|
||||||
|
app/service/helper_service.py
|
||||||
|
@echo ""
|
||||||
|
@echo " ✅ Service → $(DIST)/wgsecure-helper.exe"
|
||||||
|
@echo ""
|
||||||
|
|
||||||
# ── Prérequis de l'installeur Windows (wgsecure.iss) ─────────────────────────
|
# ── Prérequis de l'installeur Windows (wgsecure.iss) ─────────────────────────
|
||||||
# WireGuard n'est pas empaqueté avec WGSecure (c'est wg.exe et le pilote
|
# 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
|
# tunnel qui manquent, pas une simple DLL) : wgsecure.iss l'installe donc en
|
||||||
@@ -314,6 +344,7 @@ help:
|
|||||||
@echo " ║ make install Installe les dépendances ║"
|
@echo " ║ make install Installe les dépendances ║"
|
||||||
@echo " ║ make linux Binaire Linux (onefile) ║"
|
@echo " ║ make linux Binaire Linux (onefile) ║"
|
||||||
@echo " ║ make windows Binaire Windows (Wine) ║"
|
@echo " ║ make windows Binaire Windows (Wine) ║"
|
||||||
|
@echo " ║ make helper Service Windows (Wine) ║"
|
||||||
@echo " ║ make installer-deps Prérequis wgsecure.iss ║"
|
@echo " ║ make installer-deps Prérequis wgsecure.iss ║"
|
||||||
@echo " ║ make release Linux + nommage release ║"
|
@echo " ║ make release Linux + nommage release ║"
|
||||||
@echo " ║ make publish Tague + publie sur Gitea ║"
|
@echo " ║ make publish Tague + publie sur Gitea ║"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# 🛡️ WGSecure (WGS)
|
# 🛡️ WGSecure (WGS)
|
||||||
|
|
||||||
🚀 v0.7.11 · 🐍 · 🪟🐧 · 🔐 MFA TOTP · 🛡️ WireGuard
|
🚀 v0.7.12 · 🐍 · 🪟🐧 · 🔐 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).
|
||||||
|
|
||||||
@@ -210,4 +210,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.11"
|
__version__ = "0.7.12"
|
||||||
APP_NAME = "WGSecure"
|
APP_NAME = "WGSecure"
|
||||||
APP_SHORT = "WGS"
|
APP_SHORT = "WGS"
|
||||||
|
|||||||
+31
-8
@@ -24,6 +24,7 @@ import os
|
|||||||
import shutil
|
import shutil
|
||||||
import socket
|
import socket
|
||||||
|
|
||||||
|
from app.utils.helper_ipc import call_helper
|
||||||
from app.utils.platform_utils import is_windows, run_command, run_privileged
|
from app.utils.platform_utils import is_windows, run_command, run_privileged
|
||||||
|
|
||||||
# Domaine utilisé pour vérifier que la résolution DNS fonctionne encore.
|
# Domaine utilisé pour vérifier que la résolution DNS fonctionne encore.
|
||||||
@@ -282,22 +283,35 @@ def setup_split_dns(cfg: Config) -> tuple[bool, str]:
|
|||||||
|
|
||||||
if is_windows():
|
if is_windows():
|
||||||
rule_name = f"{_SPLIT_DNS_RULE_PREFIX}{name}"
|
rule_name = f"{_SPLIT_DNS_RULE_PREFIX}{name}"
|
||||||
|
|
||||||
|
# Délègue à wgsecure-helper (service SYSTEM) s'il est installé : la
|
||||||
|
# règle NRPT est posée sans invite d'authentification. `None` =
|
||||||
|
# service absent/indisponible, on retombe sur le chemin élevé.
|
||||||
|
resp = call_helper({
|
||||||
|
"cmd": "set_split_dns", "server": server,
|
||||||
|
"domains": domains, "rule_name": rule_name,
|
||||||
|
})
|
||||||
|
if resp is not None:
|
||||||
|
if resp.get("ok"):
|
||||||
|
return True, f"Règle split-DNS (NRPT) configurée pour {server} (domaines: {', '.join(domains)})"
|
||||||
|
return False, f"Échec de la configuration split-DNS (NRPT): {resp.get('message') or 'Erreur inconnue'}"
|
||||||
|
|
||||||
script = _nrpt_add_split_dns_ps(server, domains, rule_name)
|
script = _nrpt_add_split_dns_ps(server, domains, rule_name)
|
||||||
|
|
||||||
if not script:
|
if not script:
|
||||||
return True, "Aucun domaine à configurer"
|
return True, "Aucun domaine à configurer"
|
||||||
|
|
||||||
# Exécuter avec élévation (nécessite admin pour modifier les règles NRPT)
|
# Exécuter avec élévation (nécessite admin pour modifier les règles NRPT)
|
||||||
code, out, err = run_privileged(
|
code, out, err = run_privileged(
|
||||||
["powershell", "-NoProfile", "-NonInteractive", "-Command", script],
|
["powershell", "-NoProfile", "-NonInteractive", "-Command", script],
|
||||||
timeout=15
|
timeout=15
|
||||||
)
|
)
|
||||||
|
|
||||||
if code == 0:
|
if code == 0:
|
||||||
return True, f"Règle split-DNS (NRPT) configurée pour {server} (domaines: {', '.join(domains)})"
|
return True, f"Règle split-DNS (NRPT) configurée pour {server} (domaines: {', '.join(domains)})"
|
||||||
else:
|
else:
|
||||||
return False, f"Échec de la configuration split-DNS (NRPT): {err or out or 'Erreur inconnue'}"
|
return False, f"Échec de la configuration split-DNS (NRPT): {err or out or 'Erreur inconnue'}"
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Linux : utiliser systemd-resolved
|
# Linux : utiliser systemd-resolved
|
||||||
return _setup_split_dns_linux(server, domains, name)
|
return _setup_split_dns_linux(server, domains, name)
|
||||||
@@ -331,15 +345,24 @@ def cleanup_split_dns(cfg: Config) -> tuple[bool, str]:
|
|||||||
if is_windows():
|
if is_windows():
|
||||||
name = cfg.wg.get("interface_name", "wgs0")
|
name = cfg.wg.get("interface_name", "wgs0")
|
||||||
rule_name = f"{_SPLIT_DNS_RULE_PREFIX}{name}"
|
rule_name = f"{_SPLIT_DNS_RULE_PREFIX}{name}"
|
||||||
|
|
||||||
|
resp = call_helper({"cmd": "clear_split_dns", "rule_name": rule_name})
|
||||||
|
if resp is not None:
|
||||||
|
if resp.get("ok"):
|
||||||
|
return True, "Règle split-DNS (NRPT) supprimée"
|
||||||
|
msg = resp.get("message") or ""
|
||||||
|
if "cannot find" in msg.lower() or "not found" in msg.lower():
|
||||||
|
return True, ""
|
||||||
|
return False, f"Échec de la suppression split-DNS (NRPT): {msg or 'Erreur inconnue'}"
|
||||||
|
|
||||||
script = _nrpt_remove_split_dns_ps(rule_name)
|
script = _nrpt_remove_split_dns_ps(rule_name)
|
||||||
|
|
||||||
# Exécuter avec élévation
|
# Exécuter avec élévation
|
||||||
code, out, err = run_privileged(
|
code, out, err = run_privileged(
|
||||||
["powershell", "-NoProfile", "-NonInteractive", "-Command", script],
|
["powershell", "-NoProfile", "-NonInteractive", "-Command", script],
|
||||||
timeout=15
|
timeout=15
|
||||||
)
|
)
|
||||||
|
|
||||||
if code == 0:
|
if code == 0:
|
||||||
return True, "Règle split-DNS (NRPT) supprimée"
|
return True, "Règle split-DNS (NRPT) supprimée"
|
||||||
else:
|
else:
|
||||||
@@ -347,7 +370,7 @@ def cleanup_split_dns(cfg: Config) -> tuple[bool, str]:
|
|||||||
if "cannot find" in (err or out or "").lower() or "not found" in (err or out or "").lower():
|
if "cannot find" in (err or out or "").lower() or "not found" in (err or out or "").lower():
|
||||||
return True, ""
|
return True, ""
|
||||||
return False, f"Échec de la suppression split-DNS (NRPT): {err or out or 'Erreur inconnue'}"
|
return False, f"Échec de la suppression split-DNS (NRPT): {err or out or 'Erreur inconnue'}"
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Linux : supprimer le fichier de config systemd-resolved
|
# Linux : supprimer le fichier de config systemd-resolved
|
||||||
name = cfg.wg.get("interface_name", "wgs0")
|
name = cfg.wg.get("interface_name", "wgs0")
|
||||||
|
|||||||
+49
-2
@@ -7,6 +7,7 @@ from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey
|
|||||||
from app.core import dns as dns_util
|
from app.core import dns as dns_util
|
||||||
from app.core import wg_handshake
|
from app.core import wg_handshake
|
||||||
from app.core.config import Config
|
from app.core.config import Config
|
||||||
|
from app.utils.helper_ipc import call_helper
|
||||||
from app.utils.platform_utils import (
|
from app.utils.platform_utils import (
|
||||||
get_config_dir,
|
get_config_dir,
|
||||||
has_root_privileges,
|
has_root_privileges,
|
||||||
@@ -168,7 +169,14 @@ def _wg_show(name: str) -> str:
|
|||||||
`run_privileged_readonly` : sans lui, le handshake et les compteurs de
|
`run_privileged_readonly` : sans lui, le handshake et les compteurs de
|
||||||
trafic restent vides dès que l'application ne tourne plus en root (le cas
|
trafic restent vides dès que l'application ne tourne plus en root (le cas
|
||||||
normal une fois `make setup-sudoers` en place pour wg-quick).
|
normal une fois `make setup-sudoers` en place pour wg-quick).
|
||||||
|
|
||||||
|
Sous Windows, `wg.exe show` échoue même avec les délégations existantes
|
||||||
|
(ACL du service, LimitedOperatorUI) — délègue donc d'abord à
|
||||||
|
wgsecure-helper (service SYSTEM), lecture seule, si disponible.
|
||||||
"""
|
"""
|
||||||
|
resp = call_helper({"cmd": "get_status", "name": name})
|
||||||
|
if resp is not None:
|
||||||
|
return resp.get("message") or "" if resp.get("ok") else ""
|
||||||
code, out, err = run_privileged_readonly([wg_exe(), "show", name])
|
code, out, err = run_privileged_readonly([wg_exe(), "show", name])
|
||||||
if code != 0 or not out:
|
if code != 0 or not out:
|
||||||
return ""
|
return ""
|
||||||
@@ -213,6 +221,36 @@ def interface_exists(cfg: Config) -> bool:
|
|||||||
return dns_util.interface_exists(cfg.wg.get("interface_name", "wgs0"))
|
return dns_util.interface_exists(cfg.wg.get("interface_name", "wgs0"))
|
||||||
|
|
||||||
|
|
||||||
|
def _try_helper_tunnel_cmd(cmd: list[str]) -> tuple[int, str, str] | None:
|
||||||
|
"""Délègue install/uninstalltunnelservice à wgsecure-helper (service
|
||||||
|
SYSTEM) s'il est installé — aucune invite d'authentification, y compris
|
||||||
|
sans ACL accordée. `None` si le service est absent/indisponible :
|
||||||
|
l'appelant bascule alors sur le chemin existant (ACL puis élévation).
|
||||||
|
|
||||||
|
L'ACL du service WireGuard (bouton « Configurer les permissions ») ne
|
||||||
|
délègue que démarrer/arrêter un tunnel déjà installé, jamais le créer/
|
||||||
|
supprimer — `/installtunnelservice` et `/uninstalltunnelservice` (qui
|
||||||
|
font justement l'un ou l'autre à chaque connexion/déconnexion, cf.
|
||||||
|
`connect()`/`force_cleanup()`) continuaient donc à réclamer une
|
||||||
|
élévation même une fois l'ACL en place. D'où ce service séparé.
|
||||||
|
"""
|
||||||
|
if len(cmd) < 3:
|
||||||
|
return None
|
||||||
|
verb = cmd[1]
|
||||||
|
if verb == "/installtunnelservice":
|
||||||
|
config_path = cmd[2]
|
||||||
|
name = os.path.splitext(os.path.basename(config_path))[0]
|
||||||
|
request = {"cmd": "install_tunnel", "config_path": config_path, "name": name}
|
||||||
|
elif verb == "/uninstalltunnelservice":
|
||||||
|
request = {"cmd": "uninstall_tunnel", "name": cmd[2]}
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
resp = call_helper(request)
|
||||||
|
if resp is None:
|
||||||
|
return None
|
||||||
|
return (0 if resp.get("ok") else 1), "", str(resp.get("message") or "")
|
||||||
|
|
||||||
|
|
||||||
def _run_wireguard_service_cmd(cmd: list[str], timeout: int) -> tuple[int, str, str]:
|
def _run_wireguard_service_cmd(cmd: list[str], timeout: int) -> tuple[int, str, str]:
|
||||||
"""Lance `wireguard.exe /install…` ou `/uninstall…tunnelservice`.
|
"""Lance `wireguard.exe /install…` ou `/uninstall…tunnelservice`.
|
||||||
|
|
||||||
@@ -228,6 +266,9 @@ def _run_wireguard_service_cmd(cmd: list[str], timeout: int) -> tuple[int, str,
|
|||||||
`run_privileged`, seul à passer par ShellExecuteEx "runas" pour obtenir une
|
`run_privileged`, seul à passer par ShellExecuteEx "runas" pour obtenir une
|
||||||
vraie invite UAC.
|
vraie invite UAC.
|
||||||
"""
|
"""
|
||||||
|
resp = _try_helper_tunnel_cmd(cmd)
|
||||||
|
if resp is not None:
|
||||||
|
return resp
|
||||||
code, out, err = run_command(cmd, timeout)
|
code, out, err = run_command(cmd, timeout)
|
||||||
if code == 0:
|
if code == 0:
|
||||||
return code, out, err
|
return code, out, err
|
||||||
@@ -451,8 +492,14 @@ def last_handshake_age(name: str) -> int | None:
|
|||||||
Retourne -1 si aucun handshake n'a jamais eu lieu, None si l'information
|
Retourne -1 si aucun handshake n'a jamais eu lieu, None si l'information
|
||||||
est inaccessible (privilèges insuffisants, `wg` absent).
|
est inaccessible (privilèges insuffisants, `wg` absent).
|
||||||
"""
|
"""
|
||||||
code, out, _ = run_privileged_readonly([wg_exe(), "show", name, "latest-handshakes"], 10)
|
resp = call_helper({"cmd": "get_status", "name": name, "latest_handshakes": True})
|
||||||
if code != 0 or not out.strip():
|
if resp is not None:
|
||||||
|
out = resp.get("message") or "" if resp.get("ok") else ""
|
||||||
|
else:
|
||||||
|
code, out, _ = run_privileged_readonly([wg_exe(), "show", name, "latest-handshakes"], 10)
|
||||||
|
if code != 0:
|
||||||
|
out = ""
|
||||||
|
if not out.strip():
|
||||||
return None
|
return None
|
||||||
newest = 0
|
newest = 0
|
||||||
for line in out.splitlines():
|
for line in out.splitlines():
|
||||||
|
|||||||
@@ -0,0 +1,299 @@
|
|||||||
|
"""Service Windows privilégié pour WGSecure.
|
||||||
|
|
||||||
|
Installe/désinstalle le tunnel WireGuard, pose/retire les règles NRPT
|
||||||
|
(split-DNS) et lit l'état du tunnel (`wg show`) sur demande de l'app — non
|
||||||
|
privilégiée — pour éviter une invite d'authentification à chaque connexion
|
||||||
|
et déconnexion. Tourne en LocalSystem.
|
||||||
|
|
||||||
|
Exécutable séparé (`wgsecure-helper.exe`, build PyInstaller dédié) : jamais
|
||||||
|
importé par l'app principale (`wgsecure.exe`), qui lui parle uniquement via
|
||||||
|
le pipe nommé (`app/utils/helper_ipc.py`, ctypes pur, sans dépendance à
|
||||||
|
pywin32).
|
||||||
|
|
||||||
|
Invariant de sécurité central : le pipe reste accessible à tout process
|
||||||
|
d'un utilisateur local, pas seulement WGSecure — donc CINQ commandes fixes
|
||||||
|
seulement, jamais de script/commande arbitraire. Chaque commande valide
|
||||||
|
strictement ses paramètres avant d'exécuter quoi que ce soit.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import ipaddress
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
from logging.handlers import RotatingFileHandler
|
||||||
|
|
||||||
|
import pywintypes
|
||||||
|
import servicemanager
|
||||||
|
import win32event
|
||||||
|
import win32file
|
||||||
|
import win32pipe
|
||||||
|
import win32security
|
||||||
|
import win32service
|
||||||
|
import win32serviceutil
|
||||||
|
|
||||||
|
from app.core.dns import _nrpt_add_split_dns_ps, _nrpt_remove_split_dns_ps
|
||||||
|
from app.utils.platform_utils import get_wg_config_dir, wg_exe, wireguard_exe
|
||||||
|
|
||||||
|
_PIPE_NAME = r"\\.\pipe\WGSecureHelper"
|
||||||
|
# SYSTEM + Administrateurs : contrôle total. Utilisateurs interactifs
|
||||||
|
# locaux (IU) : lecture/écriture seulement (se connecter, envoyer une
|
||||||
|
# requête, lire la réponse) — jamais anonyme, jamais réseau (un pipe nommé
|
||||||
|
# n'est de toute façon jamais accessible à distance sans partage explicite,
|
||||||
|
# absent ici).
|
||||||
|
_PIPE_SDDL = "D:(A;;GA;;;SY)(A;;GA;;;BA)(A;;GRGW;;;IU)"
|
||||||
|
|
||||||
|
_INTERFACE_NAME_RE = re.compile(r"^[A-Za-z0-9_-]{1,15}$")
|
||||||
|
_RULE_NAME_RE = re.compile(r"^WGSecure-SplitDNS-[A-Za-z0-9_-]+$")
|
||||||
|
_HOSTNAME_LABEL = r"[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?"
|
||||||
|
_HOSTNAME_RE = re.compile(rf"^{_HOSTNAME_LABEL}(\.{_HOSTNAME_LABEL})*$")
|
||||||
|
_MAX_DOMAINS = 20
|
||||||
|
|
||||||
|
_LOG_DIR = os.path.join(os.environ.get("PROGRAMDATA", r"C:\ProgramData"), "WGSecure")
|
||||||
|
_LOG_FILE = os.path.join(_LOG_DIR, "helper.log")
|
||||||
|
|
||||||
|
|
||||||
|
def _get_logger() -> logging.Logger:
|
||||||
|
os.makedirs(_LOG_DIR, exist_ok=True)
|
||||||
|
logger = logging.getLogger("wgsecure-helper")
|
||||||
|
if not logger.handlers:
|
||||||
|
logger.setLevel(logging.INFO)
|
||||||
|
handler = RotatingFileHandler(_LOG_FILE, maxBytes=1_000_000,
|
||||||
|
backupCount=3, encoding="utf-8")
|
||||||
|
handler.setFormatter(logging.Formatter(
|
||||||
|
"%(asctime)s [%(levelname)s] %(message)s", "%Y-%m-%d %H:%M:%S"))
|
||||||
|
logger.addHandler(handler)
|
||||||
|
logger.propagate = False
|
||||||
|
return logger
|
||||||
|
|
||||||
|
|
||||||
|
class ValidationError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_interface_name(name) -> str:
|
||||||
|
if not isinstance(name, str) or not _INTERFACE_NAME_RE.match(name):
|
||||||
|
raise ValidationError(f"Nom d'interface invalide : {name!r}")
|
||||||
|
return name
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_rule_name(name) -> str:
|
||||||
|
if not isinstance(name, str) or not _RULE_NAME_RE.match(name):
|
||||||
|
raise ValidationError(f"Nom de règle NRPT invalide : {name!r}")
|
||||||
|
return name
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_domains(domains) -> list[str]:
|
||||||
|
if not isinstance(domains, list) or not domains or len(domains) > _MAX_DOMAINS:
|
||||||
|
raise ValidationError("Liste de domaines invalide")
|
||||||
|
out = []
|
||||||
|
for d in domains:
|
||||||
|
if not isinstance(d, str) or not _HOSTNAME_RE.match(d):
|
||||||
|
raise ValidationError(f"Domaine invalide : {d!r}")
|
||||||
|
out.append(d)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_server(server) -> str:
|
||||||
|
if not isinstance(server, str):
|
||||||
|
raise ValidationError("Serveur invalide")
|
||||||
|
try:
|
||||||
|
ipaddress.ip_address(server)
|
||||||
|
except ValueError:
|
||||||
|
raise ValidationError(f"Adresse IP invalide : {server!r}") from None
|
||||||
|
return server
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_config_path(config_path, name: str) -> str:
|
||||||
|
"""Le fichier doit résider exactement dans le dossier de configuration
|
||||||
|
WireGuard et s'appeler <name>.conf — jamais un chemin arbitraire. Le
|
||||||
|
service ne lit ni n'écrit ce fichier ; il vérifie seulement qu'il
|
||||||
|
pointe là où l'app (non privilégiée) est censée l'avoir déjà écrit
|
||||||
|
avant de demander à wireguard.exe de l'installer.
|
||||||
|
"""
|
||||||
|
if not isinstance(config_path, str):
|
||||||
|
raise ValidationError("Chemin de configuration invalide")
|
||||||
|
expected = os.path.join(get_wg_config_dir(), f"{name}.conf")
|
||||||
|
if os.path.normcase(os.path.abspath(config_path)) != os.path.normcase(os.path.abspath(expected)):
|
||||||
|
raise ValidationError(f"Chemin de configuration hors du dossier attendu : {config_path!r}")
|
||||||
|
return config_path
|
||||||
|
|
||||||
|
|
||||||
|
def _run(cmd: list[str], timeout: int = 30) -> tuple[int, str]:
|
||||||
|
"""Exécution directe : le service tourne déjà en SYSTEM, aucune
|
||||||
|
élévation à demander (contrairement à platform_utils.run_privileged,
|
||||||
|
conçu pour un process non privilégié)."""
|
||||||
|
try:
|
||||||
|
result = subprocess.run(cmd, capture_output=True, timeout=timeout,
|
||||||
|
encoding="utf-8", errors="replace")
|
||||||
|
return result.returncode, (result.stderr.strip() or result.stdout.strip())
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return -1, "Timeout"
|
||||||
|
except OSError as e:
|
||||||
|
return -1, str(e)
|
||||||
|
|
||||||
|
|
||||||
|
def _handle_install_tunnel(req: dict) -> dict:
|
||||||
|
name = _validate_interface_name(req.get("name"))
|
||||||
|
config_path = _validate_config_path(req.get("config_path"), name)
|
||||||
|
code, msg = _run([wireguard_exe(), "/installtunnelservice", config_path], timeout=90)
|
||||||
|
return {"ok": code == 0, "message": msg}
|
||||||
|
|
||||||
|
|
||||||
|
def _handle_uninstall_tunnel(req: dict) -> dict:
|
||||||
|
name = _validate_interface_name(req.get("name"))
|
||||||
|
code, msg = _run([wireguard_exe(), "/uninstalltunnelservice", name], timeout=60)
|
||||||
|
return {"ok": code == 0, "message": msg}
|
||||||
|
|
||||||
|
|
||||||
|
def _handle_set_split_dns(req: dict) -> dict:
|
||||||
|
server = _validate_server(req.get("server"))
|
||||||
|
domains = _validate_domains(req.get("domains"))
|
||||||
|
rule_name = _validate_rule_name(req.get("rule_name"))
|
||||||
|
script = _nrpt_add_split_dns_ps(server, domains, rule_name)
|
||||||
|
code, msg = _run(["powershell", "-NoProfile", "-NonInteractive", "-Command", script], timeout=20)
|
||||||
|
return {"ok": code == 0, "message": msg}
|
||||||
|
|
||||||
|
|
||||||
|
def _handle_clear_split_dns(req: dict) -> dict:
|
||||||
|
rule_name = _validate_rule_name(req.get("rule_name"))
|
||||||
|
script = _nrpt_remove_split_dns_ps(rule_name)
|
||||||
|
code, msg = _run(["powershell", "-NoProfile", "-NonInteractive", "-Command", script], timeout=20)
|
||||||
|
return {"ok": code == 0, "message": msg}
|
||||||
|
|
||||||
|
|
||||||
|
def _handle_get_status(req: dict) -> dict:
|
||||||
|
"""Lecture seule : "message" porte la sortie de `wg show`, pas une
|
||||||
|
erreur — "ok" reflète juste si la commande a pu s'exécuter.
|
||||||
|
`latest_handshakes` : ajoute le sous-argument `latest-handshakes`
|
||||||
|
(utilisé par `last_handshake_age` pour un horodatage exploitable,
|
||||||
|
plutôt que le texte humain « il y a X secondes » de `wg show` simple).
|
||||||
|
"""
|
||||||
|
name = _validate_interface_name(req.get("name"))
|
||||||
|
cmd = [wg_exe(), "show", name]
|
||||||
|
if req.get("latest_handshakes"):
|
||||||
|
cmd.append("latest-handshakes")
|
||||||
|
code, msg = _run(cmd, timeout=10)
|
||||||
|
return {"ok": code == 0, "message": msg}
|
||||||
|
|
||||||
|
|
||||||
|
_HANDLERS = {
|
||||||
|
"install_tunnel": _handle_install_tunnel,
|
||||||
|
"uninstall_tunnel": _handle_uninstall_tunnel,
|
||||||
|
"set_split_dns": _handle_set_split_dns,
|
||||||
|
"clear_split_dns": _handle_clear_split_dns,
|
||||||
|
"get_status": _handle_get_status,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _create_pipe_instance():
|
||||||
|
sd = win32security.ConvertStringSecurityDescriptorToSecurityDescriptor(
|
||||||
|
_PIPE_SDDL, win32security.SDDL_REVISION_1)
|
||||||
|
sa = win32security.SECURITY_ATTRIBUTES()
|
||||||
|
sa.SECURITY_DESCRIPTOR = sd
|
||||||
|
return win32pipe.CreateNamedPipe(
|
||||||
|
_PIPE_NAME,
|
||||||
|
win32pipe.PIPE_ACCESS_DUPLEX,
|
||||||
|
win32pipe.PIPE_TYPE_BYTE | win32pipe.PIPE_READMODE_BYTE | win32pipe.PIPE_WAIT,
|
||||||
|
win32pipe.PIPE_UNLIMITED_INSTANCES,
|
||||||
|
65536, 65536, 0, sa,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _handle_connection(handle, logger: logging.Logger) -> None:
|
||||||
|
resp: dict
|
||||||
|
try:
|
||||||
|
# Un seul message par connexion (le client ferme après avoir lu la
|
||||||
|
# réponse) : bornée à 64 Ko pour ne jamais bloquer sur un client
|
||||||
|
# qui n'enverrait jamais de fin de ligne.
|
||||||
|
data = b""
|
||||||
|
for _ in range(16):
|
||||||
|
_, chunk = win32file.ReadFile(handle, 4096)
|
||||||
|
data += chunk
|
||||||
|
if b"\n" in data or not chunk:
|
||||||
|
break
|
||||||
|
line = data.split(b"\n", 1)[0]
|
||||||
|
req = json.loads(line.decode("utf-8"))
|
||||||
|
cmd = req.get("cmd") if isinstance(req, dict) else None
|
||||||
|
handler = _HANDLERS.get(cmd)
|
||||||
|
if handler is None:
|
||||||
|
resp = {"ok": False, "message": f"Commande inconnue : {cmd!r}"}
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
resp = handler(req)
|
||||||
|
except ValidationError as e:
|
||||||
|
resp = {"ok": False, "message": str(e)}
|
||||||
|
logger.info("%s -> ok=%s", cmd, resp.get("ok"))
|
||||||
|
except (pywintypes.error, ValueError, OSError) as e:
|
||||||
|
logger.exception("Requête invalide ou erreur pipe")
|
||||||
|
resp = {"ok": False, "message": f"Erreur interne : {e}"}
|
||||||
|
try:
|
||||||
|
win32file.WriteFile(handle, (json.dumps(resp) + "\n").encode("utf-8"))
|
||||||
|
except pywintypes.error:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class WGSecureHelperService(win32serviceutil.ServiceFramework):
|
||||||
|
_svc_name_ = "WGSecureHelper"
|
||||||
|
_svc_display_name_ = "WGSecure Helper"
|
||||||
|
_svc_description_ = (
|
||||||
|
"Applique pour WGSecure, sans invite d'authentification répétée, "
|
||||||
|
"l'installation du tunnel WireGuard et les règles DNS (split-DNS) "
|
||||||
|
"qui exigent normalement des droits administrateur."
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, args):
|
||||||
|
win32serviceutil.ServiceFramework.__init__(self, args)
|
||||||
|
self._stop_event = win32event.CreateEvent(None, 0, 0, None)
|
||||||
|
self._running = True
|
||||||
|
|
||||||
|
def SvcStop(self):
|
||||||
|
self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
|
||||||
|
self._running = False
|
||||||
|
# Débloque un ConnectNamedPipe() en attente en se connectant à son
|
||||||
|
# propre pipe — plus simple et plus robuste qu'un I/O recouvrant
|
||||||
|
# pour un service à trafic très faible (quelques appels par
|
||||||
|
# connexion/déconnexion de tunnel).
|
||||||
|
try:
|
||||||
|
h = win32file.CreateFile(
|
||||||
|
_PIPE_NAME, win32file.GENERIC_READ, 0, None,
|
||||||
|
win32file.OPEN_EXISTING, 0, None)
|
||||||
|
win32file.CloseHandle(h)
|
||||||
|
except pywintypes.error:
|
||||||
|
pass
|
||||||
|
win32event.SetEvent(self._stop_event)
|
||||||
|
|
||||||
|
def SvcDoRun(self):
|
||||||
|
servicemanager.LogMsg(
|
||||||
|
servicemanager.EVENTLOG_INFORMATION_TYPE,
|
||||||
|
servicemanager.PYS_SERVICE_STARTED,
|
||||||
|
(self._svc_name_, ""),
|
||||||
|
)
|
||||||
|
logger = _get_logger()
|
||||||
|
logger.info("Service démarré")
|
||||||
|
while self._running:
|
||||||
|
try:
|
||||||
|
handle = _create_pipe_instance()
|
||||||
|
except pywintypes.error:
|
||||||
|
logger.exception("Création du pipe impossible")
|
||||||
|
break
|
||||||
|
try:
|
||||||
|
win32pipe.ConnectNamedPipe(handle, None)
|
||||||
|
if not self._running:
|
||||||
|
break
|
||||||
|
_handle_connection(handle, logger)
|
||||||
|
except pywintypes.error:
|
||||||
|
logger.exception("Erreur sur une connexion cliente")
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
win32pipe.DisconnectNamedPipe(handle)
|
||||||
|
except pywintypes.error:
|
||||||
|
pass
|
||||||
|
win32file.CloseHandle(handle)
|
||||||
|
logger.info("Service arrêté")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
win32serviceutil.HandleCommandLine(WGSecureHelperService)
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
"""Client IPC pour wgsecure-helper, le service Windows privilégié.
|
||||||
|
|
||||||
|
Utilise directement l'API Win32 via `ctypes` (même famille d'appels que
|
||||||
|
`_run_elevated_windows` dans `platform_utils.py`) plutôt que `pywin32`,
|
||||||
|
pour ne pas ajouter de dépendance à l'app principale — seul l'exécutable
|
||||||
|
séparé du service a besoin de `pywin32` (enregistrement SCM via
|
||||||
|
`win32serviceutil`), jamais `wgsecure.exe`.
|
||||||
|
|
||||||
|
Le service tourne en LocalSystem et applique, sans jamais demander
|
||||||
|
d'élévation UAC, les opérations que `platform_utils.run_privileged()`
|
||||||
|
demande sinon à chaque appel : installer/désinstaller le tunnel, poser/
|
||||||
|
retirer une règle NRPT (split-DNS), lire l'état du tunnel (`wg show`).
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
_PIPE_NAME = r"\\.\pipe\WGSecureHelper"
|
||||||
|
|
||||||
|
|
||||||
|
def call_helper(request: dict, timeout_ms: int = 1500) -> dict | None:
|
||||||
|
"""Envoie `request` (JSON) au service et attend sa réponse JSON.
|
||||||
|
|
||||||
|
Retourne `None` si le service est absent, indisponible, ou si l'échange
|
||||||
|
échoue pour n'importe quelle raison (pipe introuvable, occupé au-delà du
|
||||||
|
timeout, réponse malformée…) — l'appelant doit alors basculer sur son
|
||||||
|
chemin existant (élévation UAC). Ne lève jamais d'exception : cette
|
||||||
|
fonction est un pur best-effort, jamais un point de défaillance.
|
||||||
|
"""
|
||||||
|
if sys.platform != "win32":
|
||||||
|
return None
|
||||||
|
|
||||||
|
import ctypes
|
||||||
|
from ctypes import wintypes
|
||||||
|
|
||||||
|
GENERIC_READ = 0x80000000
|
||||||
|
GENERIC_WRITE = 0x40000000
|
||||||
|
OPEN_EXISTING = 3
|
||||||
|
FILE_ATTRIBUTE_NORMAL = 0x80
|
||||||
|
INVALID_HANDLE_VALUE = ctypes.c_void_p(-1).value
|
||||||
|
|
||||||
|
kernel32 = ctypes.windll.kernel32
|
||||||
|
kernel32.CreateFileW.restype = wintypes.HANDLE
|
||||||
|
kernel32.CreateFileW.argtypes = [
|
||||||
|
wintypes.LPCWSTR, wintypes.DWORD, wintypes.DWORD,
|
||||||
|
wintypes.LPVOID, wintypes.DWORD, wintypes.DWORD, wintypes.HANDLE,
|
||||||
|
]
|
||||||
|
|
||||||
|
# Un pipe dont toutes les instances sont déjà occupées fait échouer
|
||||||
|
# CreateFile immédiatement (ERROR_PIPE_BUSY) : WaitNamedPipe patiente
|
||||||
|
# qu'une instance se libère, dans la limite du timeout. Sans effet (et
|
||||||
|
# sans erreur bloquante) si le pipe n'existe pas du tout — CreateFile
|
||||||
|
# échouera juste ensuite avec ERROR_FILE_NOT_FOUND.
|
||||||
|
try:
|
||||||
|
kernel32.WaitNamedPipeW(_PIPE_NAME, timeout_ms)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
handle = kernel32.CreateFileW(
|
||||||
|
_PIPE_NAME, GENERIC_READ | GENERIC_WRITE, 0, None,
|
||||||
|
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, None,
|
||||||
|
)
|
||||||
|
if not handle or handle == INVALID_HANDLE_VALUE:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
payload = (json.dumps(request) + "\n").encode("utf-8")
|
||||||
|
written = wintypes.DWORD(0)
|
||||||
|
ok = kernel32.WriteFile(handle, payload, len(payload),
|
||||||
|
ctypes.byref(written), None)
|
||||||
|
if not ok:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Pipe en mode octet (pas mode message) des deux côtés : on lit
|
||||||
|
# jusqu'à trouver la fin de ligne qui termine la réponse JSON.
|
||||||
|
chunks: list[bytes] = []
|
||||||
|
buf = ctypes.create_string_buffer(4096)
|
||||||
|
read = wintypes.DWORD(0)
|
||||||
|
while b"\n" not in b"".join(chunks):
|
||||||
|
ok = kernel32.ReadFile(handle, buf, len(buf), ctypes.byref(read), None)
|
||||||
|
if not ok or read.value == 0:
|
||||||
|
break
|
||||||
|
chunks.append(buf.raw[:read.value])
|
||||||
|
|
||||||
|
data = b"".join(chunks)
|
||||||
|
if not data:
|
||||||
|
return None
|
||||||
|
line = data.split(b"\n", 1)[0]
|
||||||
|
resp = json.loads(line.decode("utf-8"))
|
||||||
|
return resp if isinstance(resp, dict) else None
|
||||||
|
except (OSError, ValueError):
|
||||||
|
return None
|
||||||
|
finally:
|
||||||
|
kernel32.CloseHandle(handle)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
pywin32>=306
|
||||||
+27
-1
@@ -5,7 +5,7 @@
|
|||||||
; ──────────────────────────────────────────────
|
; ──────────────────────────────────────────────
|
||||||
|
|
||||||
#define MyAppName "WGSecure"
|
#define MyAppName "WGSecure"
|
||||||
#define MyAppVersion "0.7.11"
|
#define MyAppVersion "0.7.12"
|
||||||
#define MyAppPublisher "WGSecure"
|
#define MyAppPublisher "WGSecure"
|
||||||
#define MyAppExeName "wgsecure.exe"
|
#define MyAppExeName "wgsecure.exe"
|
||||||
|
|
||||||
@@ -30,6 +30,13 @@ Name: "french"; MessagesFile: "compiler:Languages\French.isl"
|
|||||||
|
|
||||||
[Files]
|
[Files]
|
||||||
Source: "dist\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion
|
Source: "dist\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion
|
||||||
|
; Service privilégié optionnel (cf. app/service/, `make helper`) : installe/
|
||||||
|
; désinstalle le tunnel et pose/retire les règles NRPT (split-DNS) en
|
||||||
|
; LocalSystem, pour éviter une invite d'authentification à chaque connexion/
|
||||||
|
; déconnexion. skipifsourcedoesntexist : le build peut être absent (composant
|
||||||
|
; optionnel, pas encore généré sur toutes les machines de build) — dans ce
|
||||||
|
; cas l'app retombe simplement sur son chemin élevé existant.
|
||||||
|
Source: "dist\wgsecure-helper.exe"; DestDir: "{app}"; Flags: ignoreversion skipifsourcedoesntexist
|
||||||
; VC++ Redistributable x64 — téléchargé automatiquement par `make installer-deps` :
|
; 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
|
; 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
|
Source: "installer\VC_redist.x64.exe"; DestDir: "{tmp}"; Flags: deleteafterinstall
|
||||||
@@ -52,8 +59,20 @@ Filename: "{tmp}\VC_redist.x64.exe"; Parameters: "/install /quiet /norestart"; S
|
|||||||
; (PrivilegesRequired=admin ci-dessus), donc pas d'UAC supplémentaire non plus.
|
; (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.
|
; `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: "{tmp}\wireguard-installer.exe"; StatusMsg: "Installation de WireGuard…"; Check: WireGuardNeedsInstall; Flags: waituntilterminated
|
||||||
|
; Enregistre et démarre le service privilégié s'il a été fourni (cf. [Files]
|
||||||
|
; ci-dessus). "binPath= " avec l'espace après "=" est une exigence de la
|
||||||
|
; syntaxe sc.exe, pas d'Inno Setup — l'oublier fait échouer la commande.
|
||||||
|
Filename: "{sys}\sc.exe"; Parameters: "create WGSecureHelper binPath= ""{app}\wgsecure-helper.exe"" start= auto DisplayName= ""WGSecure Helper"""; StatusMsg: "Installation du service WGSecure Helper…"; Check: HelperServiceAvailable; Flags: runhidden
|
||||||
|
Filename: "{sys}\sc.exe"; Parameters: "start WGSecureHelper"; StatusMsg: "Démarrage du service WGSecure Helper…"; Check: HelperServiceAvailable; Flags: runhidden
|
||||||
Filename: "{app}\{#MyAppExeName}"; Description: "Lancer {#MyAppName}"; Flags: nowait postinstall skipifsilent
|
Filename: "{app}\{#MyAppExeName}"; Description: "Lancer {#MyAppName}"; Flags: nowait postinstall skipifsilent
|
||||||
|
|
||||||
|
[UninstallRun]
|
||||||
|
; Arrête puis supprime le service avant que ses fichiers ne disparaissent —
|
||||||
|
; sc.exe ignore silencieusement une commande sur un service déjà absent
|
||||||
|
; (ex. installeur généré sans le composant service), donc pas de Check ici.
|
||||||
|
Filename: "{sys}\sc.exe"; Parameters: "stop WGSecureHelper"; Flags: runhidden; RunOnceId: "StopWGSecureHelper"
|
||||||
|
Filename: "{sys}\sc.exe"; Parameters: "delete WGSecureHelper"; Flags: runhidden; RunOnceId: "DeleteWGSecureHelper"
|
||||||
|
|
||||||
[Code]
|
[Code]
|
||||||
function VCRedistNeedsInstall: Boolean;
|
function VCRedistNeedsInstall: Boolean;
|
||||||
var
|
var
|
||||||
@@ -70,3 +89,10 @@ begin
|
|||||||
// ArchitecturesInstallIn64BitMode=x64compatible ci-dessus.
|
// ArchitecturesInstallIn64BitMode=x64compatible ci-dessus.
|
||||||
Result := not FileExists(ExpandConstant('{pf}\WireGuard\wireguard.exe'));
|
Result := not FileExists(ExpandConstant('{pf}\WireGuard\wireguard.exe'));
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
function HelperServiceAvailable: Boolean;
|
||||||
|
begin
|
||||||
|
// wgsecure-helper.exe est optionnel (skipifsourcedoesntexist dans
|
||||||
|
// [Files]) : n'enregistrer le service que si le binaire a bien été copié.
|
||||||
|
Result := FileExists(ExpandConstant('{app}\wgsecure-helper.exe'));
|
||||||
|
end;
|
||||||
|
|||||||
Reference in New Issue
Block a user