Add Remote DNS
This commit is contained in:
+73
-98
@@ -175,7 +175,7 @@ _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"])
|
||||
@@ -183,13 +183,23 @@ def _nrpt_add_split_dns_ps(server: str, domains: list[str], rule_name: str) -> s
|
||||
"""
|
||||
if not domains:
|
||||
return ""
|
||||
# Build namespace list for NRPT rule
|
||||
namespace_list = ", ".join(f"'{d.strip().strip('.')}'" for d in domains if d.strip())
|
||||
clean = [d.strip().strip(".") for d in domains if d.strip()]
|
||||
# NRPT : un namespace sans point de tête ('h3adm.lan') ne matche que ce
|
||||
# nom exact, jamais ses sous-domaines ; un namespace avec point de tête
|
||||
# ('.h3adm.lan') ne matche que les sous-domaines, jamais le domaine lui-
|
||||
# même. Les deux formes sont nécessaires pour couvrir toute la zone —
|
||||
# comme `~domaine` sous systemd-resolved (Linux) couvre déjà les deux.
|
||||
namespace_list = ", ".join(f"'{d}', '.{d}'" for d in clean)
|
||||
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"
|
||||
f"-NameServer '{server}' -Comment 'WGSecure Split-DNS'; "
|
||||
# Sans ce contrôle, une erreur réelle d'Add-DnsClientNrptRule restait
|
||||
# invisible : le script se terminait quand même avec le code 0, et
|
||||
# `setup_split_dns` rapportait un succès alors que la règle n'avait
|
||||
# jamais été posée.
|
||||
"if (-not $?) { exit 1 }"
|
||||
)
|
||||
|
||||
|
||||
@@ -217,76 +227,36 @@ def _systemd_resolved_available() -> bool:
|
||||
|
||||
|
||||
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)
|
||||
"""Configure le split-DNS sous Linux via systemd-resolved, par lien.
|
||||
|
||||
Écrivait auparavant un fichier global dans /etc/systemd/resolved.conf.d/
|
||||
et redémarrait tout le service — deux privilèges que `make setup-sudoers`
|
||||
ne couvrait pas (échec silencieux ou dialogue à chaque connexion), pour
|
||||
un réglage qui entrait en concurrence avec la configuration éventuelle
|
||||
d'autres liens et survivait à un arrêt brutal du tunnel.
|
||||
|
||||
`resolvectl dns`/`resolvectl domain` posent la même route mais rattachée
|
||||
à l'interface WireGuard elle-même — comme `wg-quick` le fait déjà pour le
|
||||
DNS non split. `resolvectl revert <iface>` (déjà autorisé sans mot de
|
||||
passe, cf. `make setup-sudoers`) l'annule d'un coup, et systemd-resolved
|
||||
la retire de lui-même dès que l'interface disparaît : plus de résidu à
|
||||
surveiller après un crash.
|
||||
"""
|
||||
if not shutil.which("resolvectl"):
|
||||
return False, "resolvectl introuvable : split-DNS indisponible sur ce système"
|
||||
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
|
||||
return False, "systemd-resolved non actif : split-DNS indisponible"
|
||||
|
||||
code, out, err = run_privileged(["resolvectl", "dns", iface, server], timeout=10)
|
||||
if code != 0:
|
||||
return False, f"Échec de configuration du DNS distant : {err or out or 'erreur inconnue'}"
|
||||
|
||||
routing_domains = [f"~{d}" for d in domains]
|
||||
code, out, err = run_privileged(["resolvectl", "domain", iface, *routing_domains], timeout=10)
|
||||
if code != 0:
|
||||
return False, f"Échec de configuration du domaine distant : {err or out or 'erreur inconnue'}"
|
||||
|
||||
return True, f"Split-DNS configuré pour {server} (domaines : {', '.join(domains)})"
|
||||
|
||||
|
||||
def setup_split_dns(cfg: Config) -> tuple[bool, str]:
|
||||
@@ -334,34 +304,19 @@ def setup_split_dns(cfg: Config) -> tuple[bool, str]:
|
||||
|
||||
|
||||
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)
|
||||
"""Annule la config split-DNS par-lien de cette interface.
|
||||
|
||||
`force_cleanup()` appelle ceci après avoir déjà supprimé l'interface la
|
||||
plupart du temps (systemd-resolved retire alors sa config par-lien tout
|
||||
seul) : `resolvectl revert` échoue simplement sur une interface déjà
|
||||
partie, ce qui n'est pas une erreur — rien à nettoyer, pas un échec.
|
||||
"""
|
||||
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):
|
||||
if not shutil.which("resolvectl"):
|
||||
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)"
|
||||
code, out, err = run_privileged(["resolvectl", "revert", iface], timeout=10)
|
||||
if code == 0:
|
||||
return True, "Configuration split-DNS retirée"
|
||||
return True, ""
|
||||
|
||||
|
||||
def cleanup_split_dns(cfg: Config) -> tuple[bool, str]:
|
||||
@@ -488,6 +443,26 @@ def resolution_works(timeout: float = 3.0) -> bool:
|
||||
socket.setdefaulttimeout(old)
|
||||
|
||||
|
||||
def resolve_host(host: str, timeout: float = 3.0) -> tuple[bool, list[str]]:
|
||||
"""Résout un nom via le résolveur système. Retourne (succès, IPs uniques).
|
||||
|
||||
Sert au split-DNS : `resolution_works()` ne confirme qu'un résolveur
|
||||
quelconque répond, jamais que les noms du réseau distant (le domaine
|
||||
configuré en split-DNS, ou un hôte à l'intérieur) résolvent bien vers ce
|
||||
réseau plutôt que vers un NXDOMAIN ou une réponse publique inattendue.
|
||||
"""
|
||||
old = socket.getdefaulttimeout()
|
||||
socket.setdefaulttimeout(timeout)
|
||||
try:
|
||||
infos = socket.getaddrinfo(host, None)
|
||||
ips = list(dict.fromkeys(info[4][0] for info in infos))
|
||||
return True, ips
|
||||
except OSError:
|
||||
return False, []
|
||||
finally:
|
||||
socket.setdefaulttimeout(old)
|
||||
|
||||
|
||||
def repair_if_broken(iface: str, tunnel_dns: str = "") -> tuple[bool, str]:
|
||||
"""Répare le DNS si le tunnel est absent mais une entrée résiduelle traîne.
|
||||
|
||||
|
||||
+35
-8
@@ -268,6 +268,20 @@ def _wait_connected(cfg: Config, timeout: int = 15) -> bool:
|
||||
time.sleep(0.5)
|
||||
|
||||
|
||||
def _post_connect_message(cfg: Config) -> str:
|
||||
"""Message de succès de connexion, complété d'un avertissement si le
|
||||
split-DNS configuré n'a pas pu être appliqué.
|
||||
|
||||
Son échec était auparavant ignoré silencieusement (`setup_split_dns`
|
||||
appelé sans regarder son retour) : le tunnel se déclarait « activé »
|
||||
même quand les noms du réseau distant ne se résolvaient jamais.
|
||||
"""
|
||||
ok, msg = dns_util.setup_split_dns(cfg)
|
||||
if not ok:
|
||||
return f"Tunnel WireGuard activé\n⚠ {msg}"
|
||||
return "Tunnel WireGuard activé"
|
||||
|
||||
|
||||
def connect(cfg: Config) -> tuple[bool, str]:
|
||||
if not cfg.configured:
|
||||
return False, "WireGuard non configuré. Ouvrez le panneau Admin."
|
||||
@@ -302,9 +316,7 @@ 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é"
|
||||
return True, _post_connect_message(cfg)
|
||||
force_cleanup(cfg)
|
||||
return False, err or f"Échec de l'installation du tunnel (code {code})"
|
||||
|
||||
@@ -320,9 +332,7 @@ 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é"
|
||||
return True, _post_connect_message(cfg)
|
||||
|
||||
# Échec : wg-quick a pu s'arrêter après set_dns (ou être tué par le
|
||||
# dépassement de délai avant son trap de nettoyage). On démonte
|
||||
@@ -862,8 +872,25 @@ def _diag_tunnel(cfg: Config) -> list[dict]:
|
||||
steps.append(_step(_FAIL, "Résolution DNS dans le tunnel",
|
||||
"Plus aucun nom ne se résout — utilisez "
|
||||
"« Réparer le DNS »"))
|
||||
else:
|
||||
steps.append(_step(_OK, "Résolution DNS dans le tunnel", "Fonctionnelle"))
|
||||
return steps
|
||||
steps.append(_step(_OK, "Résolution DNS dans le tunnel", "Fonctionnelle"))
|
||||
|
||||
# Un résolveur qui répond ne dit rien sur le split-DNS lui-même : le nom
|
||||
# peut très bien se résoudre... vers une réponse publique inattendue au
|
||||
# lieu du réseau distant. On vérifie donc explicitement le(s) domaine(s)
|
||||
# configurés, pas seulement qu'une résolution quelconque fonctionne.
|
||||
domains = wg.get("split_dns_domains") or []
|
||||
for domain in domains:
|
||||
ok, ips = dns_util.resolve_host(domain, timeout=3.0)
|
||||
if ok:
|
||||
steps.append(_step(_OK, f"Résolution distante ({domain})",
|
||||
"→ " + ", ".join(ips)))
|
||||
else:
|
||||
steps.append(_step(
|
||||
_WARN, f"Résolution distante ({domain})",
|
||||
"Ne se résout pas — vérifiez le serveur DNS distant "
|
||||
f"({wg.get('split_dns_server', '?')}) et les règles NRPT/"
|
||||
"systemd-resolved du split-DNS"))
|
||||
|
||||
return steps
|
||||
|
||||
|
||||
Reference in New Issue
Block a user