Windows
This commit is contained in:
@@ -154,12 +154,17 @@ setup-sudoers:
|
||||
@$(PYTHON) -c "\
|
||||
import os, subprocess; \
|
||||
user = os.environ.get('USER', os.path.basename(os.path.expanduser('~'))); \
|
||||
rule = user + ' ALL=(ALL) NOPASSWD: /usr/bin/wg-quick\n'; \
|
||||
rules = ( \
|
||||
user + ' ALL=(ALL) NOPASSWD: /usr/bin/wg-quick\n' \
|
||||
+ user + ' ALL=(ALL) NOPASSWD: /usr/bin/tee /etc/wireguard/*\n' \
|
||||
+ user + ' ALL=(ALL) NOPASSWD: /bin/chmod 600 /etc/wireguard/*\n' \
|
||||
+ user + ' ALL=(ALL) NOPASSWD: /usr/bin/chmod 600 /etc/wireguard/*\n' \
|
||||
); \
|
||||
tmp = '/tmp/wgsecure_sudoers_tmp'; \
|
||||
open(tmp, 'w').write(rule); \
|
||||
open(tmp, 'w').write(rules); \
|
||||
r = subprocess.run(['pkexec', 'bash', '-c', 'cp ' + tmp + ' /etc/sudoers.d/wgsecure && chmod 440 /etc/sudoers.d/wgsecure']); \
|
||||
os.unlink(tmp); \
|
||||
print(' ✅ /etc/sudoers.d/wgsecure configuré — WGSecure n\\'a plus besoin de dialogue admin.' if r.returncode == 0 else ' ❌ Échec. Exécutez manuellement : sudo bash -c \"echo ' + user + ' ALL=(ALL) NOPASSWD: /usr/bin/wg-quick > /etc/sudoers.d/wgsecure && chmod 440 /etc/sudoers.d/wgsecure\"')"
|
||||
print(' ✅ /etc/sudoers.d/wgsecure configuré — plus de dialogue admin.' if r.returncode == 0 else ' ❌ Échec.')"
|
||||
@echo ""
|
||||
|
||||
# ── Réinitialisation du mot de passe administrateur ──────────────────────────
|
||||
|
||||
+30
-8
@@ -48,7 +48,9 @@ def build_client_config(cfg: Config) -> str:
|
||||
|
||||
def get_client_config_path(cfg: Config) -> str:
|
||||
name = cfg.wg.get("interface_name", "wgs0")
|
||||
return os.path.join(get_config_dir(), f"{name}.conf")
|
||||
if is_windows():
|
||||
return os.path.join(get_config_dir(), f"{name}.conf")
|
||||
return os.path.join(get_wg_config_dir(), f"{name}.conf") # /etc/wireguard/
|
||||
|
||||
|
||||
def write_client_config(cfg: Config) -> tuple[bool, str]:
|
||||
@@ -61,8 +63,30 @@ def write_client_config(cfg: Config) -> tuple[bool, str]:
|
||||
if not is_windows():
|
||||
os.chmod(path, 0o600)
|
||||
return True, path
|
||||
except Exception as e:
|
||||
return False, str(e)
|
||||
except PermissionError:
|
||||
# sudo -n tee (NOPASSWD si configuré, sinon pkexec cp)
|
||||
import subprocess as _sp
|
||||
try:
|
||||
r = _sp.run(["sudo", "-n", "tee", path],
|
||||
input=content, text=True,
|
||||
capture_output=True, timeout=10)
|
||||
if r.returncode == 0:
|
||||
_sp.run(["sudo", "-n", "chmod", "600", path],
|
||||
capture_output=True, timeout=5)
|
||||
return True, path
|
||||
except Exception:
|
||||
pass
|
||||
# Repli sur pkexec cp (dialogue graphique)
|
||||
tmp = os.path.join(get_config_dir(), "wgs_tmp.conf")
|
||||
with open(tmp, "w") as f:
|
||||
f.write(content)
|
||||
os.chmod(tmp, 0o600)
|
||||
code, _, err = run_privileged(["cp", tmp, path])
|
||||
os.unlink(tmp)
|
||||
if code == 0:
|
||||
run_privileged(["chmod", "600", path])
|
||||
return True, path
|
||||
return False, err
|
||||
|
||||
|
||||
def is_connected(cfg: Config) -> bool:
|
||||
@@ -88,7 +112,8 @@ def connect(cfg: Config) -> tuple[bool, str]:
|
||||
code, _, err = run_command(["wireguard", "/installtunnel", result])
|
||||
return (code == 0), (err or "Connecté")
|
||||
else:
|
||||
code, _, err = run_privileged(["wg-quick", "up", result])
|
||||
# Passer le nom d'interface (pas le chemin) : AppArmor autorise /etc/wireguard/ seulement
|
||||
code, _, err = run_privileged(["wg-quick", "up", name])
|
||||
if code == 0:
|
||||
return True, "Tunnel WireGuard activé"
|
||||
return False, err or "Erreur lors de la connexion"
|
||||
@@ -101,10 +126,7 @@ def disconnect(cfg: Config) -> tuple[bool, str]:
|
||||
code, _, err = run_command(["wireguard", "/uninstalltunnel", name])
|
||||
return (code == 0), (err or "Déconnecté")
|
||||
else:
|
||||
path = get_client_config_path(cfg)
|
||||
if not os.path.exists(path):
|
||||
path = name
|
||||
code, _, err = run_privileged(["wg-quick", "down", path])
|
||||
code, _, err = run_privileged(["wg-quick", "down", name])
|
||||
if code == 0:
|
||||
return True, "Tunnel WireGuard désactivé"
|
||||
return False, err or "Erreur lors de la déconnexion"
|
||||
|
||||
+79
-54
@@ -132,7 +132,6 @@ class AdminWindow(QDialog):
|
||||
tabs.addTab(self._tab_keys(), "Clés")
|
||||
tabs.addTab(self._tab_mfa(), "MFA")
|
||||
tabs.addTab(self._tab_test(), "Test connexion")
|
||||
tabs.addTab(self._tab_profiles(), "Profils")
|
||||
tabs.addTab(self._tab_settings(), "Paramètres")
|
||||
tabs.addTab(self._tab_admin(), "Sécurité")
|
||||
tabs.addTab(self._tab_about(), "À propos")
|
||||
@@ -184,19 +183,88 @@ class AdminWindow(QDialog):
|
||||
form.addRow(lbl, widget)
|
||||
|
||||
def _tab_wireguard(self) -> QWidget:
|
||||
w, lay = self._dark_page("🛡️ Configuration WireGuard", "#154360")
|
||||
from PyQt6.QtWidgets import QScrollArea, QListWidget
|
||||
|
||||
# Barre import / export
|
||||
outer = QWidget()
|
||||
outer.setStyleSheet(_TAB_CSS)
|
||||
outer_lay = QVBoxLayout(outer)
|
||||
outer_lay.setContentsMargins(0, 0, 0, 0)
|
||||
outer_lay.setSpacing(0)
|
||||
|
||||
hdr = QLabel(" 🛡️ WireGuard — Configuration & Profils")
|
||||
hdr.setFixedHeight(36)
|
||||
hdr.setStyleSheet(
|
||||
"background: #154360; color: white; font-size: 13px;"
|
||||
" font-weight: bold; padding-left: 8px;"
|
||||
)
|
||||
outer_lay.addWidget(hdr)
|
||||
|
||||
scroll = QScrollArea()
|
||||
scroll.setWidgetResizable(True)
|
||||
scroll.setStyleSheet(
|
||||
"QScrollArea { border: none; background: #1c2833; }"
|
||||
"QScrollBar:vertical { background: #1c2833; width: 8px; border: none; }"
|
||||
"QScrollBar::handle:vertical { background: #2e4057; border-radius: 4px; }"
|
||||
"QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { height: 0; }"
|
||||
)
|
||||
|
||||
content = QWidget()
|
||||
content.setStyleSheet("background: #1c2833;")
|
||||
lay = QVBoxLayout(content)
|
||||
lay.setContentsMargins(14, 12, 14, 12)
|
||||
lay.setSpacing(10)
|
||||
|
||||
# ── Profils ──────────────────────────────────────────────────────
|
||||
grp_p = QGroupBox("Profils")
|
||||
gp = QVBoxLayout(grp_p)
|
||||
gp.setSpacing(6)
|
||||
gp.setContentsMargins(10, 14, 10, 10)
|
||||
|
||||
self._profile_list = QListWidget()
|
||||
self._profile_list.setFixedHeight(72)
|
||||
self._profile_list.setStyleSheet(
|
||||
"QListWidget { background: rgba(255,255,255,0.07); color: white;"
|
||||
" border: 1px solid rgba(255,255,255,0.2); border-radius: 4px; }"
|
||||
"QListWidget::item:selected { background: #2471a3; }"
|
||||
)
|
||||
gp.addWidget(self._profile_list)
|
||||
|
||||
p_btn_row = QHBoxLayout()
|
||||
p_btn_row.setSpacing(6)
|
||||
btn_save_p = QPushButton("💾 Sauvegarder sous…")
|
||||
btn_save_p.setFixedHeight(26)
|
||||
btn_save_p.clicked.connect(self._save_profile)
|
||||
btn_load_p = QPushButton("✅ Charger")
|
||||
btn_load_p.setFixedHeight(26)
|
||||
btn_load_p.clicked.connect(self._load_profile)
|
||||
btn_del_p = QPushButton("🗑️ Supprimer")
|
||||
btn_del_p.setFixedHeight(26)
|
||||
btn_del_p.setStyleSheet(
|
||||
"QPushButton { background: #6e2e1c; color: white; border-radius: 5px;"
|
||||
" padding: 0 12px; border: none; }"
|
||||
"QPushButton:hover { background: #922b21; }"
|
||||
)
|
||||
btn_del_p.clicked.connect(self._delete_profile)
|
||||
p_btn_row.addWidget(btn_save_p)
|
||||
p_btn_row.addWidget(btn_load_p)
|
||||
p_btn_row.addWidget(btn_del_p)
|
||||
gp.addLayout(p_btn_row)
|
||||
lay.addWidget(grp_p)
|
||||
|
||||
# ── Import / Export ──────────────────────────────────────────────
|
||||
io_row = QHBoxLayout()
|
||||
btn_import = QPushButton("📂 Importer un .conf")
|
||||
btn_import.setFixedHeight(26)
|
||||
btn_import.clicked.connect(self._import_conf)
|
||||
btn_export = QPushButton("💾 Exporter en .conf")
|
||||
btn_export.setFixedHeight(26)
|
||||
btn_export.clicked.connect(self._export_conf)
|
||||
io_row.addWidget(btn_import)
|
||||
io_row.addWidget(btn_export)
|
||||
io_row.addStretch()
|
||||
lay.addLayout(io_row)
|
||||
|
||||
# ── Serveur ──────────────────────────────────────────────────────
|
||||
grp = QGroupBox("Serveur WireGuard")
|
||||
form = QFormLayout(grp)
|
||||
form.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.ExpandingFieldsGrow)
|
||||
@@ -216,6 +284,7 @@ class AdminWindow(QDialog):
|
||||
self._row(form, "Clé publique serveur :", self._srv_pubkey)
|
||||
lay.addWidget(grp)
|
||||
|
||||
# ── Interface client ─────────────────────────────────────────────
|
||||
grp2 = QGroupBox("Interface client")
|
||||
form2 = QFormLayout(grp2)
|
||||
form2.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.ExpandingFieldsGrow)
|
||||
@@ -233,7 +302,7 @@ class AdminWindow(QDialog):
|
||||
self._row(form2, "DNS :", self._dns)
|
||||
|
||||
self._allowed_ips = QLineEdit()
|
||||
self._allowed_ips.setPlaceholderText("0.0.0.0/0")
|
||||
self._allowed_ips.setPlaceholderText("10.8.0.0/24")
|
||||
self._row(form2, "IPs autorisées :", self._allowed_ips)
|
||||
|
||||
self._keepalive = QSpinBox()
|
||||
@@ -243,7 +312,12 @@ class AdminWindow(QDialog):
|
||||
self._row(form2, "Keepalive (s) :", self._keepalive)
|
||||
lay.addWidget(grp2)
|
||||
lay.addStretch()
|
||||
return w
|
||||
|
||||
scroll.setWidget(content)
|
||||
outer_lay.addWidget(scroll)
|
||||
|
||||
self._refresh_profile_list()
|
||||
return outer
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Onglet Clés
|
||||
@@ -401,55 +475,6 @@ class AdminWindow(QDialog):
|
||||
lay.addStretch()
|
||||
return w
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Onglet Profils
|
||||
# ------------------------------------------------------------------ #
|
||||
def _tab_profiles(self) -> QWidget:
|
||||
from PyQt6.QtWidgets import QListWidget, QListWidgetItem, QInputDialog
|
||||
w, lay = self._dark_page("👤 Gestion des profils", "#1a4a2a")
|
||||
|
||||
info = QLabel(
|
||||
"Sauvegardez la configuration WireGuard courante sous un nom de profil,\n"
|
||||
"puis basculez entre profils sans passer par la configuration."
|
||||
)
|
||||
info.setWordWrap(True)
|
||||
info.setStyleSheet("color: rgba(255,255,255,0.75); font-size: 12px;")
|
||||
lay.addWidget(info)
|
||||
|
||||
grp = QGroupBox("Profils sauvegardés")
|
||||
gv = QVBoxLayout(grp)
|
||||
|
||||
self._profile_list = QListWidget()
|
||||
self._profile_list.setFixedHeight(130)
|
||||
self._profile_list.setStyleSheet(
|
||||
"QListWidget { background: rgba(255,255,255,0.07); color: white;"
|
||||
" border: 1px solid rgba(255,255,255,0.2); border-radius: 4px; }"
|
||||
"QListWidget::item:selected { background: #2471a3; }"
|
||||
)
|
||||
gv.addWidget(self._profile_list)
|
||||
|
||||
btn_row = QHBoxLayout()
|
||||
btn_save_p = QPushButton("💾 Sauvegarder sous…")
|
||||
btn_save_p.clicked.connect(self._save_profile)
|
||||
btn_load_p = QPushButton("✅ Charger")
|
||||
btn_load_p.clicked.connect(self._load_profile)
|
||||
btn_del_p = QPushButton("🗑️ Supprimer")
|
||||
btn_del_p.setStyleSheet(
|
||||
"QPushButton { background: #6e2e1c; color: white; border-radius: 5px;"
|
||||
" padding: 7px 12px; border: none; }"
|
||||
"QPushButton:hover { background: #922b21; }"
|
||||
)
|
||||
btn_del_p.clicked.connect(self._delete_profile)
|
||||
btn_row.addWidget(btn_save_p)
|
||||
btn_row.addWidget(btn_load_p)
|
||||
btn_row.addWidget(btn_del_p)
|
||||
gv.addLayout(btn_row)
|
||||
lay.addWidget(grp)
|
||||
lay.addStretch()
|
||||
|
||||
self._refresh_profile_list()
|
||||
return w
|
||||
|
||||
def _refresh_profile_list(self):
|
||||
self._profile_list.clear()
|
||||
active = self._cfg.active_profile
|
||||
|
||||
+18
-15
@@ -62,10 +62,14 @@ def run_privileged(cmd: list[str], timeout: int = 60) -> tuple[int, str, str]:
|
||||
if has_root_privileges():
|
||||
return run_command(cmd, timeout)
|
||||
|
||||
# 1. pkexec — dialogue graphique polkit (GNOME/KDE), timeout long pour la saisie
|
||||
code, out, err = run_command(["pkexec"] + cmd, timeout)
|
||||
if code != -1 or "introuvable" not in err:
|
||||
# 1. sudo -n (NOPASSWD configuré dans sudoers — sans aucun dialogue)
|
||||
code, out, err = run_command(["sudo", "-n"] + cmd, timeout)
|
||||
if code == 0:
|
||||
return code, out, err
|
||||
# Si sudo a tourné mais la commande a échoué (pas un problème d'auth), retourner l'erreur
|
||||
if code != 1 or ("password" not in err.lower() and "passwd" not in err.lower()):
|
||||
if "sudo:" not in err.lower() and code not in (-1,):
|
||||
return code, out, err
|
||||
|
||||
# 2. sudo avec programme askpass graphique (pas de TTY dans une app Qt)
|
||||
askpass = _find_askpass()
|
||||
@@ -78,24 +82,23 @@ def run_privileged(cmd: list[str], timeout: int = 60) -> tuple[int, str, str]:
|
||||
capture_output=True, text=True,
|
||||
timeout=timeout, env=env,
|
||||
)
|
||||
return result.returncode, result.stdout.strip(), result.stderr.strip()
|
||||
except subprocess.TimeoutExpired:
|
||||
return -1, "", "Timeout élévation sudo"
|
||||
except Exception as e:
|
||||
return -1, "", str(e)
|
||||
if result.returncode == 0:
|
||||
return result.returncode, result.stdout.strip(), result.stderr.strip()
|
||||
except (subprocess.TimeoutExpired, Exception):
|
||||
pass
|
||||
|
||||
# 3. sudo classique (fonctionne si NOPASSWD configuré dans sudoers)
|
||||
code, out, err = run_command(["sudo", "-n"] + cmd, 10)
|
||||
if code != -1:
|
||||
# 3. pkexec — dialogue graphique polkit (GNOME/KDE)
|
||||
code, out, err = run_command(["pkexec"] + cmd, timeout)
|
||||
if code != -1 or "introuvable" not in err:
|
||||
return code, out, err
|
||||
|
||||
return (
|
||||
-1, "",
|
||||
"Élévation de privilèges impossible.\n"
|
||||
"Solutions :\n"
|
||||
" • Installer pkexec (polkit) pour le dialogue graphique\n"
|
||||
" • Ou ajouter dans /etc/sudoers :\n"
|
||||
f" {os.environ.get('USER','<user>')} ALL=(ALL) NOPASSWD: /usr/bin/wg-quick"
|
||||
"Exécutez make setup-sudoers pour configurer wg-quick sans dialogue.\n"
|
||||
"Ou manuellement :\n"
|
||||
f" sudo bash -c \"echo '{os.environ.get('USER','<user>')} ALL=(ALL) NOPASSWD: /usr/bin/wg-quick'"
|
||||
" > /etc/sudoers.d/wgsecure && chmod 440 /etc/sudoers.d/wgsecure\""
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
; ──────────────────────────────────────────────
|
||||
; WGSecure (WGS) — Script Inno Setup
|
||||
; Installeur Windows (bundle VC++ Redistributable)
|
||||
; ──────────────────────────────────────────────
|
||||
|
||||
#define MyAppName "WGSecure"
|
||||
#define MyAppVersion "0.4.0"
|
||||
#define MyAppPublisher "WGSecure"
|
||||
#define MyAppExeName "wgsecure.exe"
|
||||
|
||||
[Setup]
|
||||
AppId={{8F3B2C1A-7D4E-4A9B-9C6F-1E2D3F4A5B6C}
|
||||
AppName={#MyAppName}
|
||||
AppVersion={#MyAppVersion}
|
||||
AppPublisher={#MyAppPublisher}
|
||||
DefaultDirName={autopf}\{#MyAppName}
|
||||
DefaultGroupName={#MyAppName}
|
||||
DisableProgramGroupPage=yes
|
||||
OutputDir=dist
|
||||
OutputBaseFilename=wgsecure-{#MyAppVersion}-setup
|
||||
Compression=lzma2
|
||||
SolidCompression=yes
|
||||
ArchitecturesAllowed=x64compatible
|
||||
ArchitecturesInstallIn64BitMode=x64compatible
|
||||
PrivilegesRequired=admin
|
||||
|
||||
[Languages]
|
||||
Name: "french"; MessagesFile: "compiler:Languages\French.isl"
|
||||
|
||||
[Files]
|
||||
Source: "dist\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion
|
||||
; VC++ Redistributable x64 — à télécharger avant compilation :
|
||||
; 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
|
||||
|
||||
[Icons]
|
||||
Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
|
||||
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
|
||||
|
||||
[Run]
|
||||
; Installation silencieuse du VC++ Redistributable si absent (corrige "Failed to load Python DLL: python312.dll")
|
||||
Filename: "{tmp}\vc_redist.x64.exe"; Parameters: "/install /quiet /norestart"; StatusMsg: "Installation du composant Visual C++ Redistributable…"; Check: VCRedistNeedsInstall
|
||||
Filename: "{app}\{#MyAppExeName}"; Description: "Lancer {#MyAppName}"; Flags: nowait postinstall skipifsilent
|
||||
|
||||
[Code]
|
||||
function VCRedistNeedsInstall: Boolean;
|
||||
var
|
||||
Version: String;
|
||||
begin
|
||||
// Clé présente si VC++ 2015-2022 x64 (v14+) déjà installé
|
||||
Result := not RegQueryStringValue(HKLM64, 'SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\X64', 'Version', Version);
|
||||
end;
|
||||
Reference in New Issue
Block a user