feat: support split-DNS, infobulles UI et corrections Windows

- Ajout du support complet du split-DNS pour Linux (systemd-resolved) et Windows (NRPT)
- Bouton de configuration des permissions ACL pour WireGuard sous Windows
- Suppression de tous les appels à sc.exe (remplacés par PowerShell + wireguard.exe)
- UAC gérée par wireguard.exe lui-même (pas par WGSecure)
- Infobulles ajoutées sur tous les champs de configuration principaux
- Correction du blocage du handshake lié au redémarrage de systemd-resolved
- Incrémentation de la version à 0.7.3

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
2026-09-03 07:20:13 +02:00
parent e747f2a1ea
commit 38b48fc428
9 changed files with 326 additions and 11 deletions
+6 -1
View File
@@ -33,10 +33,15 @@ _WG_DEFAULT: dict[str, Any] = {
"allowed_ips": "10.8.0.0/24",
"keepalive": 25,
"mtu": 0,
# Split-DNS : serveur DNS dédié pour les noms du réseau distant,
# avec liste de domaines à résoudre via ce serveur (ex: ["local", "internal"]).
# Laisser vide pour utiliser le DNS par défaut pour tout le trafic.
"split_dns_server": "",
"split_dns_domains": [],
}
_DEFAULT: dict[str, Any] = {
"version": "0.7.2",
"version": "0.7.3",
"admin_password_hash": "",
"admin_salt": "",
"mfa_enabled": False,
+229
View File
@@ -169,6 +169,235 @@ _NRPT_CLEAN_PS = (
"-ErrorAction SilentlyContinue }"
)
# Split-DNS : rule name template for per-tunnel DNS rules
_SPLIT_DNS_RULE_PREFIX = "WGSecure-SplitDNS-"
def _nrpt_add_split_dns_ps(server: str, domains: list[str], rule_name: str) -> str:
"""PowerShell script to add a NRPT rule for split-DNS.
Args:
server: DNS server IP address
domains: List of domain suffixes (e.g., ["local", "internal"])
rule_name: Name for the NRPT rule
"""
if not domains:
return ""
# Build namespace list for NRPT rule
namespace_list = ", ".join(f"'{d.strip().strip('.')}'" for d in domains if d.strip())
return (
f"$rule = Get-DnsClientNrptRule -Name '{rule_name}' -ErrorAction SilentlyContinue; "
f"if ($rule) {{ Remove-DnsClientNrptRule -Name '{rule_name}' -Force -ErrorAction SilentlyContinue }}; "
f"Add-DnsClientNrptRule -Name '{rule_name}' -Namespace @({namespace_list}) "
f"-NameServer '{server}' -Comment 'WGSecure Split-DNS' -ErrorAction SilentlyContinue"
)
def _nrpt_remove_split_dns_ps(rule_name: str) -> str:
"""PowerShell script to remove a NRPT rule for split-DNS."""
return (
f"Remove-DnsClientNrptRule -Name '{rule_name}' -Force -ErrorAction SilentlyContinue"
)
def _systemd_resolved_available() -> bool:
"""Vérifie si systemd-resolved est actif (Linux)."""
if is_windows():
return False
import subprocess
try:
# Vérifier que systemd-resolved est actif
code = subprocess.run(
["systemctl", "is-active", "systemd-resolved"],
capture_output=True, text=True, timeout=5
).returncode
return code == 0
except Exception:
return False
def _setup_split_dns_linux(server: str, domains: list[str], iface: str) -> tuple[bool, str]:
"""Configure le split-DNS sous Linux via systemd-resolved.
Crée un fichier dans /etc/systemd/resolved.conf.d/ pour router
les domaines vers le serveur DNS spécifié.
Note: Le redémarrage de systemd-resolved est nécessaire pour appliquer
la configuration, mais il est effectué de manière asynchrone pour
éviter de bloquer le handshake WireGuard.
Args:
server: Adresse IP du serveur DNS (ex: 192.168.1.210)
domains: Liste de domaines (ex: ["local", "internal"])
iface: Nom de l'interface WireGuard
Returns:
(success, message)
"""
if not _systemd_resolved_available():
return False, "systemd-resolved non disponible. Utilisez resolvconf ou configurez manuellement."
# Créer le nom du fichier de config
config_file = f"/etc/systemd/resolved.conf.d/wgsecure-{iface}.conf"
# Contenu du fichier
domain_str = ", ".join(f"~{d.strip()}" for d in domains if d.strip())
content = f"[Resolve]\nDNS={server}\nDomains={domain_str}\n"
# Écrire le fichier (nécessite sudo)
import tempfile
import os
import subprocess
# Créer un fichier temporaire
with tempfile.NamedTemporaryFile(mode='w', suffix='.conf', delete=False) as f:
f.write(content)
temp_path = f.name
try:
# Copier le fichier temporaire vers /etc/systemd/resolved.conf.d/
code, out, err = run_privileged(
["sudo", "cp", temp_path, config_file],
timeout=10
)
if code != 0:
return False, f"Échec de la copie du fichier: {err or out}"
# Redémarrer systemd-resolved en arrière-plan pour ne pas bloquer
# Utiliser systemd-run pour un redémarrage asynchrone
restart_cmd = ["sudo", "systemctl", "restart", "systemd-resolved"]
try:
# Lancer en arrière-plan avec nohup pour éviter de bloquer
subprocess.Popen(
restart_cmd,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)
except Exception:
pass # Ignorer l'erreur, le fichier est déjà en place
return True, (
f"Split-DNS configuré pour {server} (domaines: {', '.join(domains)}). "
f"Redémarrez systemd-resolved manuellement si nécessaire: "
f"`sudo systemctl restart systemd-resolved`"
)
finally:
try:
os.unlink(temp_path)
except Exception:
pass
def setup_split_dns(cfg: Config) -> tuple[bool, str]:
"""Configure le split-DNS pour Windows (NRPT) ou Linux (systemd-resolved).
Args:
cfg: Configuration contenant split_dns_server et split_dns_domains
Returns:
(success, message)
"""
server = (cfg.wg.get("split_dns_server") or "").strip()
domains = cfg.wg.get("split_dns_domains") or []
name = cfg.wg.get("interface_name", "wgs0")
if not server or not domains:
return True, "Aucune configuration split-DNS"
# Nettoyer les domaines
domains = [d.strip() for d in domains if d.strip()]
if not domains:
return True, "Aucun domaine valide à configurer"
if is_windows():
rule_name = f"{_SPLIT_DNS_RULE_PREFIX}{name}"
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)
def _cleanup_split_dns_linux(iface: str) -> tuple[bool, str]:
"""Supprime la configuration split-DNS sous Linux.
Note: Ne redémarre PAS systemd-resolved pour éviter de casser
la résolution DNS pendant une reconnexion. Le service sera
redémarré manuellement par l'utilisateur si nécessaire.
Args:
iface: Nom de l'interface WireGuard
Returns:
(success, message)
"""
config_file = f"/etc/systemd/resolved.conf.d/wgsecure-{iface}.conf"
# Supprimer le fichier de config (nécessite sudo)
import os
if not os.path.exists(config_file):
return True, ""
code, out, err = run_privileged(
["sudo", "rm", "-f", config_file],
timeout=10
)
if code != 0:
return False, f"Échec de la suppression du fichier split-DNS: {err or out}"
return True, "Configuration split-DNS supprimée (redémarrez systemd-resolved si nécessaire)"
def cleanup_split_dns(cfg: Config) -> tuple[bool, str]:
"""Supprime la configuration split-DNS pour Windows (NRPT) ou Linux.
Args:
cfg: Configuration contenant l'interface name
Returns:
(success, message)
"""
if is_windows():
name = cfg.wg.get("interface_name", "wgs0")
rule_name = f"{_SPLIT_DNS_RULE_PREFIX}{name}"
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:
# Ignorer l'erreur si la règle n'existait pas
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")
return _cleanup_split_dns_linux(name)
def _powershell(script: str, timeout: int = 20) -> tuple[int, str, str]:
return run_command(
+23 -3
View File
@@ -61,9 +61,20 @@ def build_client_config(cfg: Config) -> str:
# `wg-quick up` en plein milieu de son exécution : l'interface est déjà
# créée, le trap de nettoyage laisse un état partiel. Mieux vaut monter le
# tunnel sans toucher au DNS système.
dns_value = (wg.get("dns") or "").strip()
if dns_value and dns_util.has_dns_backend():
lines.append(f"DNS = {dns_value}")
# Split-DNS : si configuré, on ne met pas de DNS dans la config WireGuard
# car les règles NRPT (Windows) ou systemd-resolved (Linux) géreront le routage.
split_dns_server = (wg.get("split_dns_server") or "").strip()
split_dns_domains = wg.get("split_dns_domains") or []
if split_dns_server and split_dns_domains and is_windows():
# Sous Windows avec split-DNS : pas de DNS dans la config, tout sera géré par NRPT
pass
else:
# Cas normal : utiliser le DNS principal
dns_value = (wg.get("dns") or "").strip()
if dns_value and dns_util.has_dns_backend():
lines.append(f"DNS = {dns_value}")
mtu = int(wg.get("mtu") or 0)
if mtu:
lines.append(f"MTU = {mtu}")
@@ -222,6 +233,8 @@ def force_cleanup(cfg: Config) -> list[str]:
if dns_util.interface_exists(name):
cmd = [wireguard_exe(), "/uninstalltunnelservice", name]
run_command(cmd, timeout=60)
# Nettoyer les règles split-DNS
dns_util.cleanup_split_dns(cfg)
actions += dns_util.cleanup(name)
return actions
@@ -237,6 +250,9 @@ def force_cleanup(cfg: Config) -> list[str]:
# ne disparaît pas avec le lien. C'est la cause du « plus d'internet ».
# Sans effet, et sans dialogue d'authentification, si rien ne traîne.
actions += dns_util.cleanup(name)
# Nettoyer les règles split-DNS
dns_util.cleanup_split_dns(cfg)
return actions
@@ -286,6 +302,8 @@ def connect(cfg: Config) -> tuple[bool, str]:
# démarrage effectif du tunnel est asynchrone. Tester l'état
# immédiatement conclurait « échec » sur un tunnel qui monte.
if code == 0 and _wait_connected(cfg, timeout=15):
# Configurer le split-DNS si un serveur dédié est défini
dns_util.setup_split_dns(cfg)
return True, "Tunnel WireGuard activé"
force_cleanup(cfg)
return False, err or f"Échec de l'installation du tunnel (code {code})"
@@ -302,6 +320,8 @@ def connect(cfg: Config) -> tuple[bool, str]:
# Passer le nom d'interface (pas le chemin) : AppArmor autorise /etc/wireguard/ seulement
code, _, err = run_privileged(["wg-quick", "up", name], timeout=90)
if code == 0:
# Configurer le split-DNS si un serveur dédié est défini
dns_util.setup_split_dns(cfg)
return True, "Tunnel WireGuard activé"
# Échec : wg-quick a pu s'arrêter après set_dns (ou être tué par le