chore(release): v0.4.5
fix: la clé pré-partagée n'était écrite dans aucune config client L'assistant de génération produisait une PresharedKey affichée à l'écran mais stockée nulle part, et l'import d'un .conf ignorait la ligne. Face à un serveur qui en attend une, le tunnel montait et l'interface existait — sans qu'aucun handshake n'aboutisse jamais. La PSK devient un champ à part entière. fix: options de ligne de commande wireguard.exe incorrectes sous Windows /installtunnel et /uninstalltunnel n'existent pas ; les vrais verbes sont /installtunnelservice et /uninstalltunnelservice. S'y ajoutaient l'absence de résolution du chemin d'installation (WireGuard n'est pas dans le PATH), l'absence totale d'élévation UAC alors que ces commandes l'exigent, et une détection d'installation qui sondait une application graphique avec --help. feat: diagnostic de bout en bout de la chaîne de connexion Le bouton de test s'arrêtait à la joignabilité UDP et concluait « serveur joignable » sur un tunnel qui n'échangeait rien. Il déroule désormais quatorze étapes, de la configuration locale jusqu'au DNS dans le tunnel, et désigne l'étape qui bloque. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
09724f4e4f
commit
9d785c00cc
+122
-3
@@ -29,6 +29,45 @@ def get_wg_config_dir() -> str:
|
||||
return "/etc/wireguard"
|
||||
|
||||
|
||||
def _windows_wg_dir() -> str:
|
||||
"""Répertoire d'installation de WireGuard for Windows."""
|
||||
for base in (os.environ.get("ProgramFiles", r"C:\Program Files"),
|
||||
os.environ.get("ProgramW6432", r"C:\Program Files"),
|
||||
os.environ.get("ProgramFiles(x86)", "")):
|
||||
if not base:
|
||||
continue
|
||||
path = os.path.join(base, "WireGuard")
|
||||
if os.path.isdir(path):
|
||||
return path
|
||||
return ""
|
||||
|
||||
|
||||
def _windows_exe(name: str) -> str:
|
||||
"""Chemin absolu de wireguard.exe / wg.exe.
|
||||
|
||||
L'installateur WireGuard n'ajoute pas son répertoire au PATH : invoquer
|
||||
« wireguard » ou « wg » tel quel remonte « Commande introuvable » sur une
|
||||
machine où WireGuard est pourtant installé.
|
||||
"""
|
||||
import shutil as _sh
|
||||
d = _windows_wg_dir()
|
||||
if d:
|
||||
path = os.path.join(d, name)
|
||||
if os.path.isfile(path):
|
||||
return path
|
||||
return _sh.which(name) or name
|
||||
|
||||
|
||||
def wireguard_exe() -> str:
|
||||
"""Binaire du gestionnaire WireGuard (installe/désinstalle les tunnels)."""
|
||||
return _windows_exe("wireguard.exe") if is_windows() else "wg-quick"
|
||||
|
||||
|
||||
def wg_exe() -> str:
|
||||
"""Binaire `wg` (interrogation d'un tunnel actif)."""
|
||||
return _windows_exe("wg.exe") if is_windows() else "wg"
|
||||
|
||||
|
||||
def has_root_privileges() -> bool:
|
||||
if is_windows():
|
||||
try:
|
||||
@@ -123,9 +162,84 @@ def _sudo_refused_auth(err: str) -> bool:
|
||||
return any(marker in low for marker in _SUDO_AUTH_MARKERS)
|
||||
|
||||
|
||||
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.
|
||||
|
||||
`wireguard.exe /installtunnelservice` et `sc create/delete` exigent les
|
||||
droits administrateur : lancés depuis une session standard, ils é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
|
||||
processus déjà démarré.
|
||||
|
||||
Un processus élevé n'hérite pas de nos tubes : stdout/stderr sont perdus,
|
||||
seul le code de sortie remonte.
|
||||
"""
|
||||
import ctypes
|
||||
from ctypes import wintypes
|
||||
|
||||
SEE_MASK_NOCLOSEPROCESS = 0x00000040
|
||||
SEE_MASK_NOASYNC = 0x00000100
|
||||
SW_HIDE = 0
|
||||
ERROR_CANCELLED = 1223
|
||||
WAIT_TIMEOUT = 0x00000102
|
||||
|
||||
class SHELLEXECUTEINFOW(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("cbSize", wintypes.DWORD),
|
||||
("fMask", ctypes.c_ulong),
|
||||
("hwnd", wintypes.HWND),
|
||||
("lpVerb", wintypes.LPCWSTR),
|
||||
("lpFile", wintypes.LPCWSTR),
|
||||
("lpParameters", wintypes.LPCWSTR),
|
||||
("lpDirectory", wintypes.LPCWSTR),
|
||||
("nShow", ctypes.c_int),
|
||||
("hInstApp", wintypes.HINSTANCE),
|
||||
("lpIDList", ctypes.c_void_p),
|
||||
("lpClass", wintypes.LPCWSTR),
|
||||
("hkeyClass", wintypes.HKEY),
|
||||
("dwHotKey", wintypes.DWORD),
|
||||
("hIconOrMonitor", wintypes.HANDLE),
|
||||
("hProcess", wintypes.HANDLE),
|
||||
]
|
||||
|
||||
info = SHELLEXECUTEINFOW()
|
||||
info.cbSize = ctypes.sizeof(info)
|
||||
info.fMask = SEE_MASK_NOCLOSEPROCESS | SEE_MASK_NOASYNC
|
||||
info.lpVerb = "runas"
|
||||
info.lpFile = cmd[0]
|
||||
info.lpParameters = subprocess.list2cmdline(cmd[1:])
|
||||
info.nShow = SW_HIDE
|
||||
|
||||
if not ctypes.windll.shell32.ShellExecuteExW(ctypes.byref(info)):
|
||||
err = ctypes.get_last_error() or ctypes.GetLastError()
|
||||
if err == ERROR_CANCELLED:
|
||||
return -1, "", "Élévation refusée par l'utilisateur"
|
||||
return -1, "", f"Élévation impossible (code {err}) : {cmd[0]}"
|
||||
|
||||
handle = info.hProcess
|
||||
if not handle:
|
||||
# Commande lancée sans handle exploitable : succès non vérifiable.
|
||||
return 0, "", ""
|
||||
try:
|
||||
if ctypes.windll.kernel32.WaitForSingleObject(
|
||||
handle, int(timeout * 1000)
|
||||
) == WAIT_TIMEOUT:
|
||||
return -1, "", "Timeout"
|
||||
code = wintypes.DWORD()
|
||||
ctypes.windll.kernel32.GetExitCodeProcess(handle, ctypes.byref(code))
|
||||
return code.value, "", ""
|
||||
finally:
|
||||
ctypes.windll.kernel32.CloseHandle(handle)
|
||||
|
||||
|
||||
def run_privileged(cmd: list[str], timeout: int = 60) -> tuple[int, str, str]:
|
||||
if is_windows():
|
||||
return run_command(cmd, timeout)
|
||||
if has_root_privileges():
|
||||
return run_command(cmd, timeout)
|
||||
try:
|
||||
return _run_elevated_windows(cmd, timeout)
|
||||
except Exception as e:
|
||||
return -1, "", str(e)
|
||||
if has_root_privileges():
|
||||
return run_command(cmd, timeout)
|
||||
|
||||
@@ -217,14 +331,19 @@ def privilege_report() -> list[tuple[str, bool, str]]:
|
||||
|
||||
|
||||
def wg_available() -> bool:
|
||||
if is_windows():
|
||||
return os.path.isfile(wg_exe())
|
||||
code, _, _ = run_command(["wg", "--version"])
|
||||
return code == 0
|
||||
|
||||
|
||||
def wg_quick_available() -> bool:
|
||||
# wireguard.exe est une application graphique : elle n'accepte pas
|
||||
# « --help » et n'écrit rien sur stdout. Le sonder comme un outil console
|
||||
# concluait « WireGuard absent » sur toute machine Windows. Sa présence sur
|
||||
# le disque est le seul test fiable.
|
||||
if is_windows():
|
||||
code, _, _ = run_command(["wireguard", "--help"])
|
||||
return code == 0
|
||||
return os.path.isfile(wireguard_exe())
|
||||
code, _, _ = run_command(["wg-quick", "--help"])
|
||||
return code == 0
|
||||
|
||||
|
||||
Reference in New Issue
Block a user