This commit is contained in:
tuxgyver
2026-07-21 10:21:13 +02:00
parent ea2e6d772b
commit d03a75057e
5 changed files with 187 additions and 80 deletions
+8 -3
View File
@@ -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 ──────────────────────────
+29 -7
View File
@@ -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")
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
View File
@@ -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
+17 -14
View File
@@ -62,9 +62,13 @@ 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)
@@ -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,
)
if result.returncode == 0:
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)
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\""
)
+52
View File
@@ -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;