This commit is contained in:
2026-06-01 23:22:18 +02:00
parent f061da654d
commit d23d0c15be
14 changed files with 1858 additions and 202 deletions
+483 -47
View File
@@ -2,6 +2,7 @@ from PyQt6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QTabWidget, QWidget,
QLabel, QLineEdit, QPushButton, QSpinBox, QCheckBox,
QGroupBox, QFormLayout, QTextEdit, QMessageBox, QFrame,
QFileDialog, QSizePolicy,
)
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QFont
@@ -127,12 +128,14 @@ class AdminWindow(QDialog):
font-weight: bold; }
QTabBar::tab:hover { background: #253545; color: white; }
""")
tabs.addTab(self._tab_wireguard(), "WireGuard")
tabs.addTab(self._tab_keys(), "Clés")
tabs.addTab(self._tab_mfa(), "MFA")
tabs.addTab(self._tab_test(), "Test connexion")
tabs.addTab(self._tab_admin(), "Sécurité")
tabs.addTab(self._tab_about(), "À propos")
tabs.addTab(self._tab_wireguard(), "WireGuard")
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")
layout.addWidget(tabs)
# Barre de boutons
@@ -165,41 +168,79 @@ class AdminWindow(QDialog):
# ------------------------------------------------------------------ #
# Onglet WireGuard
# ------------------------------------------------------------------ #
@staticmethod
def _row(form: QFormLayout, text: str, widget,
label_w: int = 148, field_h: int = 28) -> None:
"""Ligne de formulaire : label largeur fixe + champ hauteur uniforme.
widget peut être un QWidget ou un QHBoxLayout (sans setFixedHeight)."""
from PyQt6.QtWidgets import QLayout
lbl = QLabel(text)
lbl.setFixedWidth(label_w)
lbl.setFixedHeight(field_h)
lbl.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
lbl.setStyleSheet("color: rgba(255,255,255,0.75); background: transparent;")
if not isinstance(widget, QLayout):
widget.setFixedHeight(field_h)
form.addRow(lbl, widget)
def _tab_wireguard(self) -> QWidget:
w, lay = self._dark_page("Configuration WireGuard", "#154360")
w, lay = self._dark_page("🛡️ Configuration WireGuard", "#154360")
# Barre import / export
io_row = QHBoxLayout()
btn_import = QPushButton("📂 Importer un .conf")
btn_import.clicked.connect(self._import_conf)
btn_export = QPushButton("💾 Exporter en .conf")
btn_export.clicked.connect(self._export_conf)
io_row.addWidget(btn_import)
io_row.addWidget(btn_export)
io_row.addStretch()
lay.addLayout(io_row)
grp = QGroupBox("Serveur WireGuard")
form = QFormLayout(grp)
form.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.ExpandingFieldsGrow)
self._srv_endpoint = QLineEdit()
self._srv_endpoint.setPlaceholderText("vpn.exemple.com ou 1.2.3.4")
form.addRow("Adresse serveur :", self._srv_endpoint)
self._row(form, "Adresse serveur :", self._srv_endpoint)
self._srv_port = QSpinBox()
self._srv_port.setRange(1, 65535)
self._srv_port.setValue(51820)
form.addRow("Port UDP :", self._srv_port)
self._srv_port.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
self._row(form, "Port UDP :", self._srv_port)
self._srv_pubkey = QLineEdit()
self._srv_pubkey.setPlaceholderText("Clé publique du serveur (base64)")
form.addRow("Clé publique serveur :", self._srv_pubkey)
self._row(form, "Clé publique serveur :", self._srv_pubkey)
lay.addWidget(grp)
grp2 = QGroupBox("Interface client")
form2 = QFormLayout(grp2)
form2.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.ExpandingFieldsGrow)
self._iface_name = QLineEdit()
self._iface_name.setPlaceholderText("wgs0")
form2.addRow("Nom interface :", self._iface_name)
self._row(form2, "Nom interface :", self._iface_name)
self._client_addr = QLineEdit()
self._client_addr.setPlaceholderText("10.8.0.2/24")
form2.addRow("Adresse IP client :", self._client_addr)
self._row(form2, "Adresse IP client :", self._client_addr)
self._dns = QLineEdit()
self._dns.setPlaceholderText("1.1.1.1")
form2.addRow("DNS :", self._dns)
self._row(form2, "DNS :", self._dns)
self._allowed_ips = QLineEdit()
self._allowed_ips.setPlaceholderText("0.0.0.0/0")
form2.addRow("IPs autorisées :", self._allowed_ips)
self._row(form2, "IPs autorisées :", self._allowed_ips)
self._keepalive = QSpinBox()
self._keepalive.setRange(0, 300)
self._keepalive.setValue(25)
form2.addRow("Keepalive (s) :", self._keepalive)
self._keepalive.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
self._row(form2, "Keepalive (s) :", self._keepalive)
lay.addWidget(grp2)
lay.addStretch()
return w
@@ -208,15 +249,18 @@ class AdminWindow(QDialog):
# Onglet Clés
# ------------------------------------------------------------------ #
def _tab_keys(self) -> QWidget:
w, lay = self._dark_page("Gestion des clés Curve25519", "#1a3a52")
w, lay = self._dark_page("🔑 Gestion des clés Curve25519", "#1a3a52")
grp = QGroupBox("Paire de clés client")
form = QFormLayout(grp)
form.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.ExpandingFieldsGrow)
self._priv_key = QLineEdit()
self._priv_key.setEchoMode(QLineEdit.EchoMode.Password)
self._priv_key.setPlaceholderText("(générer ou coller)")
self._priv_key.setFixedHeight(28)
show_priv = QPushButton("Afficher")
show_priv.setFixedWidth(80)
show_priv.setFixedSize(70, 28)
show_priv.setCheckable(True)
show_priv.toggled.connect(
lambda checked: self._priv_key.setEchoMode(
@@ -224,16 +268,19 @@ class AdminWindow(QDialog):
)
)
priv_row = QHBoxLayout()
priv_row.setSpacing(6)
priv_row.addWidget(self._priv_key)
priv_row.addWidget(show_priv)
form.addRow("Clé privée :", priv_row)
self._row(form, "Clé privée :", priv_row)
self._pub_key = QLineEdit()
self._pub_key.setReadOnly(True)
self._pub_key.setPlaceholderText("(dérivée automatiquement)")
form.addRow("Clé publique :", self._pub_key)
self._row(form, "Clé publique :", self._pub_key)
btn_gen = QPushButton("Générer une nouvelle paire de clés")
btn_gen.clicked.connect(self._generate_keys)
form.addRow("", btn_gen)
btn_gen.setFixedHeight(28)
self._row(form, "", btn_gen)
lay.addWidget(grp)
grp2 = QGroupBox("Aperçu config WireGuard")
@@ -241,12 +288,28 @@ class AdminWindow(QDialog):
self._config_preview = QTextEdit()
self._config_preview.setReadOnly(True)
self._config_preview.setFont(QFont("Courier", 9))
self._config_preview.setFixedHeight(150)
self._config_preview.setFixedHeight(130)
v2.addWidget(self._config_preview)
btn_preview = QPushButton("Rafraîchir l'aperçu")
btn_row2 = QHBoxLayout()
btn_preview = QPushButton("🔄 Rafraîchir")
btn_preview.clicked.connect(self._refresh_preview)
v2.addWidget(btn_preview)
btn_qr = QPushButton("📱 Exporter QR Code")
btn_qr.clicked.connect(self._export_qr)
btn_row2.addWidget(btn_preview)
btn_row2.addWidget(btn_qr)
v2.addLayout(btn_row2)
lay.addWidget(grp2)
# Config serveur
grp3 = QGroupBox("🖥️ Génération config serveur + client")
v3 = QVBoxLayout(grp3)
note = QLabel("Génère une paire complète prête à déployer (nouvelles clés, PSK).")
note.setStyleSheet("color: rgba(255,255,255,0.7); font-size: 11px;")
v3.addWidget(note)
btn_gen_srv = QPushButton("⚡ Générer config serveur + client")
btn_gen_srv.clicked.connect(self._generate_server_config)
v3.addWidget(btn_gen_srv)
lay.addWidget(grp3)
lay.addStretch()
return w
@@ -254,7 +317,7 @@ class AdminWindow(QDialog):
# Onglet MFA
# ------------------------------------------------------------------ #
def _tab_mfa(self) -> QWidget:
w, lay = self._dark_page("Authentification Multi-Facteurs (MFA / TOTP)", "#0e6655")
w, lay = self._dark_page("🔐 Authentification Multi-Facteurs (MFA / TOTP)", "#0e6655")
self._mfa_enabled_cb = QCheckBox("Activer la vérification MFA avant connexion")
self._mfa_enabled_cb.setStyleSheet("color: white; font-weight: bold;")
@@ -267,12 +330,13 @@ class AdminWindow(QDialog):
grp = QGroupBox("Secret TOTP")
form = QFormLayout(grp)
form.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.ExpandingFieldsGrow)
self._mfa_secret = QLineEdit()
self._mfa_secret.setPlaceholderText("(générer ou coller votre secret TOTP)")
form.addRow("Secret :", self._mfa_secret)
self._row(form, "Secret :", self._mfa_secret)
btn_gen_secret = QPushButton("Générer un nouveau secret")
btn_gen_secret.clicked.connect(self._generate_mfa_secret)
form.addRow("", btn_gen_secret)
self._row(form, "", btn_gen_secret)
lay.addWidget(grp)
grp2 = QGroupBox("QR Code — Scanner avec Google Authenticator / Aegis")
@@ -296,39 +360,227 @@ class AdminWindow(QDialog):
# Onglet Test connexion
# ------------------------------------------------------------------ #
def _tab_test(self) -> QWidget:
w, lay = self._dark_page("Test de connectivité réseau", "#1a4a7a")
w, lay = self._dark_page("📡 Test de connectivité réseau", "#1a4a7a")
info = QLabel(
"Teste la joignabilité du serveur WireGuard (port UDP) avant d'établir le tunnel."
)
info.setWordWrap(True)
info.setStyleSheet("color: white; font-size: 13px;")
lay.addWidget(info)
btn_test = QPushButton("Lancer le test de connexion")
# Test UDP serveur
grp1 = QGroupBox("Test UDP serveur")
g1 = QVBoxLayout(grp1)
g1.addWidget(QLabel("Vérifie la joignabilité UDP du serveur WireGuard."))
btn_test = QPushButton("🚀 Lancer le test de connexion")
btn_test.setStyleSheet(
"QPushButton { padding: 10px; background: #1e8449; color: white;"
"QPushButton { padding: 9px; background: #1e8449; color: white;"
" border-radius: 5px; font-weight: bold; border: none; }"
"QPushButton:hover { background: #27ae60; }"
)
btn_test.clicked.connect(self._run_test)
lay.addWidget(btn_test)
g1.addWidget(btn_test)
self._test_result = QLabel("")
self._test_result.setWordWrap(True)
self._test_result.setAlignment(Qt.AlignmentFlag.AlignCenter)
self._test_result.setStyleSheet(
"color: white; padding: 12px; border-radius: 6px; font-size: 13px;"
"color: white; padding: 8px; border-radius: 6px; font-size: 12px;"
)
lay.addWidget(self._test_result)
g1.addWidget(self._test_result)
lay.addWidget(grp1)
# Test DNS leak
grp2 = QGroupBox("🔍 Test de fuite DNS")
g2 = QVBoxLayout(grp2)
g2.addWidget(QLabel("Vérifie que le DNS passe bien par le tunnel VPN."))
btn_dns = QPushButton("🔍 Analyser le DNS")
btn_dns.clicked.connect(self._run_dns_test)
g2.addWidget(btn_dns)
self._dns_result = QLabel("")
self._dns_result.setWordWrap(True)
self._dns_result.setStyleSheet(
"color: white; padding: 8px; border-radius: 6px; font-size: 11px;"
)
g2.addWidget(self._dns_result)
lay.addWidget(grp2)
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
for name in self._cfg.list_profiles():
marker = "" if name == active else " "
self._profile_list.addItem(f"{marker} {name}")
def _save_profile(self):
from PyQt6.QtWidgets import QInputDialog
self._save_values()
name, ok = QInputDialog.getText(self, "Sauvegarder le profil",
"Nom du profil :")
if not ok or not name.strip():
return
self._cfg.save_profile(name.strip())
self._refresh_profile_list()
QMessageBox.information(self, "Profil sauvegardé",
f"Profil « {name.strip()} » sauvegardé.")
def _load_profile(self):
item = self._profile_list.currentItem()
if not item:
return
name = item.text().lstrip("").strip()
if self._cfg.load_profile(name):
self._load_values()
self._refresh_profile_list()
QMessageBox.information(self, "Profil chargé",
f"Profil « {name} » chargé.")
def _delete_profile(self):
item = self._profile_list.currentItem()
if not item:
return
name = item.text().lstrip("").strip()
reply = QMessageBox.question(
self, "Confirmer",
f"Supprimer le profil « {name} » ?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
)
if reply == QMessageBox.StandardButton.Yes:
self._cfg.delete_profile(name)
self._refresh_profile_list()
# ------------------------------------------------------------------ #
# Onglet Paramètres
# ------------------------------------------------------------------ #
def _tab_settings(self) -> QWidget:
w, lay = self._dark_page("⚙️ Paramètres de l'application", "#2a1a4a")
grp1 = QGroupBox("Comportement")
form1 = QFormLayout(grp1)
self._chk_tray = QCheckBox("Réduire dans le systray à la fermeture")
self._chk_tray.setStyleSheet("color: white;")
self._chk_tray.setChecked(bool(self._cfg.get("ui", "minimize_to_tray")))
form1.addRow("", self._chk_tray)
self._chk_autostart = QCheckBox("Lancer WGSecure au démarrage du système")
self._chk_autostart.setStyleSheet("color: white;")
self._chk_autostart.setChecked(bool(self._cfg.get("ui", "autostart")))
form1.addRow("", self._chk_autostart)
lay.addWidget(grp1)
grp_ks = QGroupBox("🔒 Kill Switch (Linux)")
g_ks = QVBoxLayout(grp_ks)
note_ks = QLabel(
"Bloque tout trafic internet si le tunnel VPN tombe.\n"
"Nécessite des droits root (iptables)."
)
note_ks.setWordWrap(True)
note_ks.setStyleSheet("color: rgba(255,255,255,0.7); font-size: 11px;")
g_ks.addWidget(note_ks)
ks_row = QHBoxLayout()
self._btn_ks_enable = QPushButton("🔒 Activer")
self._btn_ks_enable.clicked.connect(self._enable_kill_switch)
self._btn_ks_disable = QPushButton("🔓 Désactiver")
self._btn_ks_disable.setStyleSheet(
"QPushButton { background: #6e2e1c; border-radius: 5px; padding: 7px 12px;"
" color: white; border: none; } QPushButton:hover { background: #922b21; }"
)
self._btn_ks_disable.clicked.connect(self._disable_kill_switch)
self._ks_status = QLabel("")
self._ks_status.setStyleSheet("font-size: 11px; color: #aed6f1;")
ks_row.addWidget(self._btn_ks_enable)
ks_row.addWidget(self._btn_ks_disable)
ks_row.addWidget(self._ks_status)
ks_row.addStretch()
g_ks.addLayout(ks_row)
lay.addWidget(grp_ks)
self._refresh_ks_status()
grp2 = QGroupBox("Auto-reconnexion")
form2 = QFormLayout(grp2)
self._chk_autorecon = QCheckBox("Reconnecter automatiquement si le tunnel tombe")
self._chk_autorecon.setStyleSheet("color: white;")
self._chk_autorecon.setChecked(bool(self._cfg.get("ui", "auto_reconnect")))
form2.addRow("", self._chk_autorecon)
self._spin_interval = QSpinBox()
self._spin_interval.setRange(10, 300)
self._spin_interval.setValue(int(self._cfg.get("ui", "reconnect_interval") or 30))
self._spin_interval.setSuffix(" s")
self._spin_interval.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
self._row(form2, "Intervalle :", self._spin_interval)
lay.addWidget(grp2)
btn_apply = QPushButton("✅ Appliquer les paramètres")
btn_apply.clicked.connect(self._apply_settings)
lay.addWidget(btn_apply)
lay.addStretch()
return w
def _apply_settings(self):
self._cfg.set("ui", "minimize_to_tray", self._chk_tray.isChecked())
self._cfg.set("ui", "auto_reconnect", self._chk_autorecon.isChecked())
self._cfg.set("ui", "reconnect_interval", self._spin_interval.value())
autostart = self._chk_autostart.isChecked()
if autostart != bool(self._cfg.get("ui", "autostart")):
self._cfg.set_autostart(autostart)
else:
self._cfg.save()
QMessageBox.information(self, "Paramètres", "Paramètres enregistrés.")
# ------------------------------------------------------------------ #
# Onglet Sécurité
# ------------------------------------------------------------------ #
def _tab_admin(self) -> QWidget:
w, lay = self._dark_page("Sécurité — Accès Administrateur", "#6e2e1c")
w, lay = self._dark_page("🔒 Sécurité — Accès Administrateur", "#6e2e1c")
note = QLabel(
"Définissez un mot de passe pour protéger l'accès au panneau Administrateur.\n"
@@ -340,22 +592,26 @@ class AdminWindow(QDialog):
grp = QGroupBox("Mot de passe administrateur")
form = QFormLayout(grp)
form.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.ExpandingFieldsGrow)
self._admin_pw1 = QLineEdit()
self._admin_pw1.setEchoMode(QLineEdit.EchoMode.Password)
self._admin_pw1.setPlaceholderText("Nouveau mot de passe")
form.addRow("Mot de passe :", self._admin_pw1)
self._row(form, "Mot de passe :", self._admin_pw1)
self._admin_pw2 = QLineEdit()
self._admin_pw2.setEchoMode(QLineEdit.EchoMode.Password)
self._admin_pw2.setPlaceholderText("Confirmer")
form.addRow("Confirmation :", self._admin_pw2)
self._row(form, "Confirmation :", self._admin_pw2)
btn_set_pw = QPushButton("Définir le mot de passe")
btn_set_pw.setStyleSheet(
"QPushButton { padding: 8px; background: #922b21; color: white;"
"QPushButton { background: #922b21; color: white;"
" border-radius: 5px; border: none; }"
"QPushButton:hover { background: #c0392b; }"
)
btn_set_pw.clicked.connect(self._set_admin_password)
form.addRow("", btn_set_pw)
self._row(form, "", btn_set_pw)
lay.addWidget(grp)
lay.addStretch()
return w
@@ -385,7 +641,7 @@ class AdminWindow(QDialog):
name_lbl.setStyleSheet("color: white;")
layout.addWidget(name_lbl)
version_lbl = QLabel("Version 0.1.0")
version_lbl = QLabel("Version 0.3.0")
version_lbl.setAlignment(Qt.AlignmentFlag.AlignCenter)
version_lbl.setStyleSheet("color: rgba(255,255,255,0.7); font-size: 13px;")
layout.addWidget(version_lbl)
@@ -468,6 +724,54 @@ class AdminWindow(QDialog):
# ------------------------------------------------------------------ #
# Actions
# ------------------------------------------------------------------ #
def _import_conf(self):
path, _ = QFileDialog.getOpenFileName(
self, "Importer un fichier WireGuard",
"", "WireGuard Config (*.conf);;Tous les fichiers (*)"
)
if not path:
return
values = wg_core.parse_conf_file(path)
if not values:
QMessageBox.warning(self, "Erreur", "Impossible de lire ce fichier .conf")
return
mapping = {
"server_endpoint": self._srv_endpoint,
"server_public_key": self._srv_pubkey,
"client_address": self._client_addr,
"dns": self._dns,
"allowed_ips": self._allowed_ips,
"client_private_key": self._priv_key,
"client_public_key": self._pub_key,
}
for k, widget in mapping.items():
if k in values:
widget.setText(str(values[k]))
if "server_port" in values:
self._srv_port.setValue(int(values["server_port"]))
if "keepalive" in values:
self._keepalive.setValue(int(values["keepalive"]))
self._save_values()
QMessageBox.information(
self, "Import réussi",
f"Configuration importée depuis :\n{path}"
)
def _export_conf(self):
self._save_values()
path, _ = QFileDialog.getSaveFileName(
self, "Exporter la configuration WireGuard",
f"{self._cfg.wg.get('interface_name','wgs0')}.conf",
"WireGuard Config (*.conf)"
)
if not path:
return
ok, result = wg_core.export_conf_file(self._cfg, path)
if ok:
QMessageBox.information(self, "Export réussi", f"Config exportée :\n{result}")
else:
QMessageBox.warning(self, "Erreur", f"Export échoué :\n{result}")
def _generate_keys(self):
priv, pub = wg_core.generate_keypair()
self._priv_key.setText(priv)
@@ -522,6 +826,138 @@ class AdminWindow(QDialog):
"background: rgba(231,76,60,0.25); color: #f1948a;"
)
def _export_qr(self):
"""Exporte la config client comme QR Code pour import mobile."""
self._save_values()
if not self._cfg.configured:
QMessageBox.warning(self, "Erreur", "Config WireGuard incomplète.")
return
from PyQt6.QtWidgets import QDialog, QLabel, QVBoxLayout
import qrcode, io
from PyQt6.QtGui import QImage, QPixmap
content = wg_core.build_client_config(self._cfg)
qr = qrcode.QRCode(box_size=5, border=2,
error_correction=qrcode.constants.ERROR_CORRECT_L)
qr.add_data(content)
qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white")
buf = io.BytesIO()
img.save(buf, "PNG")
buf.seek(0)
pix = QPixmap.fromImage(QImage.fromData(buf.read()))
dlg = QDialog(self)
dlg.setWindowTitle("QR Code — Config WireGuard client")
dlg.setStyleSheet("background: #1c2833; color: white;")
v = QVBoxLayout(dlg)
lbl = QLabel()
lbl.setPixmap(pix.scaled(300, 300, Qt.AspectRatioMode.KeepAspectRatio,
Qt.TransformationMode.SmoothTransformation))
lbl.setAlignment(Qt.AlignmentFlag.AlignCenter)
note = QLabel("Scannez avec l'application WireGuard sur mobile.")
note.setAlignment(Qt.AlignmentFlag.AlignCenter)
note.setStyleSheet("color: rgba(255,255,255,0.7); font-size: 11px;")
v.addWidget(lbl)
v.addWidget(note)
dlg.exec()
def _generate_server_config(self):
"""Génère et affiche une paire complète server + client."""
from PyQt6.QtWidgets import QDialog, QVBoxLayout, QTabWidget, QTextEdit, QPushButton
result = wg_core.generate_server_config(
server_port=self._srv_port.value() if hasattr(self, '_srv_port') else 51820
)
dlg = QDialog(self)
dlg.setWindowTitle("Config générée — Server + Client")
dlg.setMinimumSize(600, 420)
dlg.setStyleSheet("background: #1c2833; color: white;")
v = QVBoxLayout(dlg)
tabs = QTabWidget()
tabs.setStyleSheet(
"QTabBar::tab { background:#2e4057; color:white; padding:6px 14px; }"
"QTabBar::tab:selected { background:#2471a3; }"
)
for title, content in [("🖥️ Serveur", result["server_conf"]),
("💻 Client", result["client_conf"])]:
te = QTextEdit()
te.setReadOnly(True)
te.setFont(QFont("Courier", 9))
te.setPlainText(content)
te.setStyleSheet(
"background: #17202a; color: #a8d8ea; border: none; font-size: 11px;"
)
tabs.addTab(te, title)
v.addWidget(tabs)
note = QLabel(
f"🔑 Clé pub. serveur : {result['server_pub'][:32]}\n"
f"🔑 Clé pub. client : {result['client_pub'][:32]}\n"
"⚠️ Remplacez <SERVER_IP> dans la config client par l'IP publique du serveur."
)
note.setWordWrap(True)
note.setStyleSheet("color: rgba(255,255,255,0.7); font-size: 10px; padding: 6px;")
v.addWidget(note)
btn_close = QPushButton("Fermer")
btn_close.setStyleSheet(
"QPushButton { background:#2471a3; color:white; border-radius:5px;"
" padding:7px 16px; border:none; } QPushButton:hover { background:#1a5276; }"
)
btn_close.clicked.connect(dlg.accept)
v.addWidget(btn_close)
dlg.exec()
def _run_dns_test(self):
from PyQt6.QtWidgets import QApplication
self._dns_result.setText("Analyse en cours…")
QApplication.processEvents()
r = wg_core.dns_leak_test(self._cfg)
lines = []
status = r["status"]
if status == "ok":
lines.append("✅ Aucune fuite DNS détectée")
css = "background: rgba(39,174,96,0.2); color: #a9dfbf;"
elif status == "leak":
lines.append("⚠️ Fuite DNS potentielle !")
css = "background: rgba(231,76,60,0.2); color: #f1948a;"
else:
lines.append("❓ Statut inconnu")
css = "color: #aed6f1;"
if r["resolvers"]:
lines.append("Serveurs DNS actifs : " + "".join(r["resolvers"]))
if r["expected"]:
lines.append(f"DNS configuré (VPN) : {r['expected']}")
if r.get("probe_ip"):
lines.append(f"IP retournée par whoami.akamai.net : {r['probe_ip']}")
self._dns_result.setText("\n".join(lines))
self._dns_result.setStyleSheet(
f"padding: 8px; border-radius: 6px; font-size: 11px; {css}"
)
def _refresh_ks_status(self):
active = wg_core.kill_switch_active()
if active:
self._ks_status.setText("🔒 Actif")
self._ks_status.setStyleSheet("color: #a9dfbf; font-size: 11px;")
else:
self._ks_status.setText("🔓 Inactif")
self._ks_status.setStyleSheet("color: #f1948a; font-size: 11px;")
def _enable_kill_switch(self):
self._save_values()
ok, msg = wg_core.enable_kill_switch(self._cfg)
self._refresh_ks_status()
if ok:
QMessageBox.information(self, "Kill Switch", msg)
else:
QMessageBox.warning(self, "Kill Switch", msg)
def _disable_kill_switch(self):
ok, msg = wg_core.disable_kill_switch(self._cfg)
self._refresh_ks_status()
QMessageBox.information(self, "Kill Switch", msg)
def _set_admin_password(self):
p1 = self._admin_pw1.text()
p2 = self._admin_pw2.text()