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:
+31
-8
@@ -24,6 +24,7 @@ import os
|
||||
import shutil
|
||||
import socket
|
||||
|
||||
from app.utils.helper_ipc import call_helper
|
||||
from app.utils.platform_utils import is_windows, run_command, run_privileged
|
||||
|
||||
# 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():
|
||||
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)
|
||||
|
||||
|
||||
if not script:
|
||||
return True, "Aucun domaine à configurer"
|
||||
|
||||
|
||||
# Exécuter avec élévation (nécessite admin pour modifier les règles NRPT)
|
||||
code, out, err = run_privileged(
|
||||
["powershell", "-NoProfile", "-NonInteractive", "-Command", script],
|
||||
timeout=15
|
||||
)
|
||||
|
||||
|
||||
if code == 0:
|
||||
return True, f"Règle split-DNS (NRPT) configurée pour {server} (domaines: {', '.join(domains)})"
|
||||
else:
|
||||
return False, f"Échec de la configuration split-DNS (NRPT): {err or out or 'Erreur inconnue'}"
|
||||
|
||||
|
||||
else:
|
||||
# Linux : utiliser systemd-resolved
|
||||
return _setup_split_dns_linux(server, domains, name)
|
||||
@@ -331,15 +345,24 @@ def cleanup_split_dns(cfg: Config) -> tuple[bool, str]:
|
||||
if is_windows():
|
||||
name = cfg.wg.get("interface_name", "wgs0")
|
||||
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)
|
||||
|
||||
|
||||
# Exécuter avec élévation
|
||||
code, out, err = run_privileged(
|
||||
["powershell", "-NoProfile", "-NonInteractive", "-Command", script],
|
||||
timeout=15
|
||||
)
|
||||
|
||||
|
||||
if code == 0:
|
||||
return True, "Règle split-DNS (NRPT) supprimée"
|
||||
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():
|
||||
return True, ""
|
||||
return False, f"Échec de la suppression split-DNS (NRPT): {err or out or 'Erreur inconnue'}"
|
||||
|
||||
|
||||
else:
|
||||
# Linux : supprimer le fichier de config systemd-resolved
|
||||
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 wg_handshake
|
||||
from app.core.config import Config
|
||||
from app.utils.helper_ipc import call_helper
|
||||
from app.utils.platform_utils import (
|
||||
get_config_dir,
|
||||
has_root_privileges,
|
||||
@@ -168,7 +169,14 @@ def _wg_show(name: str) -> str:
|
||||
`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
|
||||
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])
|
||||
if code != 0 or not out:
|
||||
return ""
|
||||
@@ -213,6 +221,36 @@ def interface_exists(cfg: Config) -> bool:
|
||||
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]:
|
||||
"""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
|
||||
vraie invite UAC.
|
||||
"""
|
||||
resp = _try_helper_tunnel_cmd(cmd)
|
||||
if resp is not None:
|
||||
return resp
|
||||
code, out, err = run_command(cmd, timeout)
|
||||
if code == 0:
|
||||
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
|
||||
est inaccessible (privilèges insuffisants, `wg` absent).
|
||||
"""
|
||||
code, out, _ = run_privileged_readonly([wg_exe(), "show", name, "latest-handshakes"], 10)
|
||||
if code != 0 or not out.strip():
|
||||
resp = call_helper({"cmd": "get_status", "name": name, "latest_handshakes": True})
|
||||
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
|
||||
newest = 0
|
||||
for line in out.splitlines():
|
||||
|
||||
Reference in New Issue
Block a user