Files
WGSecure/app/ui/main_window.py
T
tuxgyver 1b74719204 fix: DNS coupée après arrêt du tunnel, gel UI, dialogues sudo à répétition
- wg-quick down échoue silencieusement si l'interface a déjà disparu
  (crash, kill, veille) et ne retire donc jamais l'entrée DNS posée par
  wg-quick up : la résolution reste pointée sur un résolveur mort.
  Ajout d'un nettoyage DNS inconditionnel (app/core/dns.py) déclenché à
  la connexion, la déconnexion, la fermeture et au démarrage suivant.
- is_connected() se basait sur `wg show`, qui rend un code 0 même en cas
  d'échec de permission pour un utilisateur non-root : l'app se croyait
  déconnectée en permanence et ne démontait donc jamais le tunnel à la
  fermeture. Détection réécrite via sysfs (sans privilèges).
- Connexion/déconnexion et ping tournaient sur le thread Qt : gel de
  l'UI pendant les élévations de privilèges. Déportés dans des
  QThread (app/ui/worker.py).
- Restauration réseau garantie sur toutes les sorties (croix, tray,
  SIGINT/SIGTERM, atexit, exception) via app/core/shutdown.py.
- run_privileged() : la détection "sudo veut un mot de passe" ratait en
  session non-anglophone et sur les comptes ALL=(ALL) ALL ; réécrite
  pour se fier au message d'erreur réel plutôt qu'à `sudo -l`.
- Écriture de la config WireGuard dans /etc/wireguard (root:root 0700)
  passe maintenant par install -D via la chaîne de privilèges complète
  au lieu d'un tee non autorisé par les sudoers.
- Ajout d'un diagnostic des droits (make check-privileges, bouton
  Admin) et mise à jour de setup-sudoers pour couvrir toutes les
  commandes désormais utilisées.
2026-08-31 11:16:04 +02:00

745 lines
31 KiB
Python

from __future__ import annotations
from PyQt6.QtWidgets import (
QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QLabel, QPushButton, QFrame, QMessageBox, QInputDialog,
QLineEdit, QListWidget, QListWidgetItem, QApplication,
QComboBox, QSizePolicy,
)
from PyQt6.QtCore import Qt, QTimer, QDateTime
from PyQt6.QtGui import QFont, QCloseEvent, QColor
from app.core.config import Config
from app.core import wireguard as wg_core
from app.core import dns as dns_util
from app.core import log as conn_log
from app.core import history as hist
from app.core import shutdown as shutdown_guard
from app.ui.worker import TaskWorker, ValueWorker
from app.ui.mfa_dialog import MFADialog
from app.ui.admin_window import AdminWindow
from app.ui.bw_graph import BandwidthGraph
from app.ui.history_dialog import HistoryDialog
from app.ui import icons
_DARK = "#1c2833"
_DARK2 = "#17202a"
_CARD = "#212f3d"
_LOG_COLORS = {
"success": "#a9dfbf",
"error": "#f1948a",
"warning": "#f9e79f",
"info": "#aed6f1",
}
_PING_CSS = {
"good": "background:#1e8449; color:white;",
"medium": "background:#d4ac0d; color:black;",
"bad": "background:#922b21; color:white;",
"offline": "background:#2e4057; color:#5d6d7e;",
}
class MainWindow(QMainWindow):
def __init__(self, config: Config, parent=None):
super().__init__(parent)
self._cfg = config
self._connecting = False
self._connected_since: QDateTime | None = None
self._prev_rx: int | None = None
self._prev_tx: int | None = None
self._auto_reconnect_active = False
self._session_id: int | None = None
self._worker: TaskWorker | None = None
self._ping_worker: ValueWorker | None = None
self._is_connected = False
self._reconnect_failures = 0
self._quitting = False
self._warned_foreign: set[str] = set()
self.setWindowTitle("WGSecure")
self.setFixedSize(430, 680)
self.setWindowFlags(
Qt.WindowType.Window
| Qt.WindowType.WindowTitleHint
| Qt.WindowType.WindowCloseButtonHint
| Qt.WindowType.WindowMinimizeButtonHint
)
self.setWindowIcon(icons.icon_app())
self._build_ui()
# Timers
self._status_timer = QTimer(self)
self._status_timer.timeout.connect(self._refresh_status)
self._status_timer.start(3000)
self._clock_timer = QTimer(self)
self._clock_timer.timeout.connect(self._tick_duration)
self._clock_timer.start(1000)
self._bw_timer = QTimer(self)
self._bw_timer.timeout.connect(self._update_bw)
self._bw_timer.start(3000)
self._ping_timer = QTimer(self)
self._ping_timer.timeout.connect(self._update_ping)
self._ping_timer.start(10000)
self._reconnect_timer = QTimer(self)
self._reconnect_timer.timeout.connect(self._check_reconnect)
self._refresh_status()
self._reload_log()
self._reload_profiles()
self._warn_foreign_interfaces()
if self._cfg.get("ui", "auto_connect_on_startup") and self._cfg.configured:
QTimer.singleShot(500, self._on_connect)
# ------------------------------------------------------------------ #
# Construction UI
# ------------------------------------------------------------------ #
def _build_ui(self):
central = QWidget()
central.setStyleSheet(f"background: {_DARK};")
self.setCentralWidget(central)
root = QVBoxLayout(central)
root.setContentsMargins(0, 0, 0, 0)
root.setSpacing(0)
# ── En-tête ──────────────────────────────────────────────────
header = QWidget()
header.setFixedHeight(72)
header.setStyleSheet(f"background: {_DARK2};")
h = QHBoxLayout(header)
h.setContentsMargins(14, 8, 14, 8)
self._icon_label = QLabel()
self._icon_label.setFixedSize(40, 40)
self._icon_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
h.addWidget(self._icon_label)
title_col = QVBoxLayout()
title_col.setSpacing(1)
title_lbl = QLabel("WGSecure")
title_lbl.setStyleSheet(
"color: white; font-size: 16px; font-weight: bold; background: transparent;"
)
self._profile_combo = QComboBox()
self._profile_combo.setFixedHeight(22)
self._profile_combo.setStyleSheet(
"QComboBox { background: #2e4057; color: #aed6f1; border: none;"
" border-radius: 3px; font-size: 10px; padding: 0 6px; }"
"QComboBox::drop-down { border: none; }"
"QComboBox QAbstractItemView { background: #2e4057; color: white; }"
)
self._profile_combo.currentTextChanged.connect(self._on_profile_changed)
title_col.addWidget(title_lbl)
title_col.addWidget(self._profile_combo)
h.addLayout(title_col)
h.addStretch()
# Ping badge
self._ping_badge = QLabel("— ms")
self._ping_badge.setFixedSize(62, 22)
self._ping_badge.setAlignment(Qt.AlignmentFlag.AlignCenter)
self._ping_badge.setStyleSheet(
f"border-radius: 11px; font-size: 10px; font-weight: bold; {_PING_CSS['offline']}"
)
h.addWidget(self._ping_badge)
# Copier clé publique
self._btn_copy_key = QPushButton("📋 Clé pub.")
self._btn_copy_key.setFixedHeight(26)
self._btn_copy_key.setStyleSheet(
"QPushButton { background: #2471a3; color: white; border-radius: 5px;"
" font-size: 10px; padding: 0 8px; border: none; }"
"QPushButton:hover { background: #1a5276; }"
"QPushButton:disabled { background: #2e4057; color: #5d6d7e; }"
)
self._btn_copy_key.clicked.connect(self._copy_public_key)
h.addWidget(self._btn_copy_key)
root.addWidget(header)
# ── Statut ───────────────────────────────────────────────────
status_area = QWidget()
status_area.setFixedHeight(80)
status_area.setStyleSheet(f"background: {_CARD};")
s = QVBoxLayout(status_area)
s.setContentsMargins(10, 6, 10, 6)
s.setAlignment(Qt.AlignmentFlag.AlignCenter)
self._status_badge = QLabel("● Déconnecté")
self._status_badge.setAlignment(Qt.AlignmentFlag.AlignCenter)
self._status_badge.setStyleSheet(
"color: #e74c3c; font-size: 15px; font-weight: bold; background: transparent;"
)
s.addWidget(self._status_badge)
self._duration_label = QLabel("")
self._duration_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self._duration_label.setStyleSheet(
"color: #5d6d7e; font-size: 11px; background: transparent;"
)
s.addWidget(self._duration_label)
root.addWidget(status_area)
root.addWidget(self._hsep())
# ── Infos ─────────────────────────────────────────────────────
info_frame = QFrame()
info_frame.setStyleSheet(f"background: {_DARK};")
info_frame.setAutoFillBackground(True)
il = QVBoxLayout(info_frame)
il.setContentsMargins(14, 8, 14, 8)
il.setSpacing(5)
self._info_labels: dict[str, QLabel] = {}
for key, icon_txt, label in [
("server", "🌐", "Serveur"),
("iface", "🔌", "Interface"),
("addr", "📍", "Adresse IP"),
("mfa", "🔐", "MFA"),
("rx_tx", "↕️ ", "Transfert"),
("handshake", "🤝", "Handshake"),
]:
rw = QWidget()
rw.setStyleSheet("background: transparent;")
rl = QHBoxLayout(rw)
rl.setContentsMargins(0, 0, 0, 0)
rl.setSpacing(5)
rl.addWidget(self._lbl(icon_txt, fixed=20, color="#5d6d7e", size=11))
rl.addWidget(self._lbl(f"{label} :", fixed=100, color="#5d6d7e", size=12))
val = self._lbl("—", color="#aed6f1", size=12)
val.setWordWrap(True)
self._info_labels[key] = val
rl.addWidget(val, 1)
il.addWidget(rw)
root.addWidget(info_frame)
root.addWidget(self._hsep())
# ── Graphique bande passante ───────────────────────────────────
bw_header = self._section_header("📊 Bande passante")
root.addWidget(bw_header)
self._bw_graph = BandwidthGraph()
self._bw_graph.setFixedHeight(75)
root.addWidget(self._bw_graph)
root.addWidget(self._hsep())
# ── Journal ───────────────────────────────────────────────────
log_hdr = self._section_header("📋 Journal", btn_text="Effacer",
btn_slot=self._clear_log)
root.addWidget(log_hdr)
self._log_list = QListWidget()
self._log_list.setFixedHeight(78)
self._log_list.setStyleSheet(
f"QListWidget {{ background: {_DARK2}; color: #aed6f1;"
" border: none; font-size: 10px; font-family: Courier; }"
"QListWidget::item { padding: 1px 6px; }"
)
self._log_list.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
root.addWidget(self._log_list)
root.addWidget(self._hsep())
# ── Boutons ───────────────────────────────────────────────────
btn_area = QWidget()
btn_area.setStyleSheet(f"background: {_DARK2};")
bl = QVBoxLayout(btn_area)
bl.setContentsMargins(14, 10, 14, 12)
bl.setSpacing(7)
self._btn_connect = QPushButton("Se connecter")
self._btn_connect.setFixedHeight(42)
self._btn_connect.setStyleSheet(
"QPushButton { background: #1e8449; color: white; font-size: 14px;"
" font-weight: bold; border-radius: 6px; border: none; }"
"QPushButton:hover { background: #27ae60; }"
"QPushButton:disabled { background: #2e4057; color: #5d6d7e; }"
)
self._btn_connect.clicked.connect(self._on_connect)
bl.addWidget(self._btn_connect)
secondary_row = QHBoxLayout()
secondary_row.setSpacing(6)
btn_admin = QPushButton("⚙️ Administrateur")
btn_admin.setFixedHeight(30)
btn_admin.setStyleSheet(
"QPushButton { background: #2e4057; color: white; border-radius: 6px;"
" font-size: 11px; border: none; }"
"QPushButton:hover { background: #3d5166; }"
)
btn_admin.clicked.connect(self._open_admin)
secondary_row.addWidget(btn_admin)
btn_history = QPushButton("📊 Historique")
btn_history.setFixedHeight(30)
btn_history.setStyleSheet(
"QPushButton { background: #2e4057; color: white; border-radius: 6px;"
" font-size: 11px; border: none; }"
"QPushButton:hover { background: #3d5166; }"
)
btn_history.clicked.connect(self._open_history)
secondary_row.addWidget(btn_history)
bl.addLayout(secondary_row)
root.addWidget(btn_area)
# ── Helpers UI ───────────────────────────────────────────────────────
def _lbl(self, text: str, fixed: int = 0, color: str = "white",
size: int = 12) -> QLabel:
lbl = QLabel(text)
lbl.setStyleSheet(
f"color: {color}; font-size: {size}px; background: transparent;"
)
if fixed:
lbl.setFixedWidth(fixed)
return lbl
def _hsep(self) -> QFrame:
f = QFrame()
f.setFrameShape(QFrame.Shape.HLine)
f.setFixedHeight(1)
f.setStyleSheet(f"background: {_DARK2};")
return f
def _section_header(self, title: str, btn_text: str = "",
btn_slot=None) -> QWidget:
w = QWidget()
w.setFixedHeight(24)
w.setStyleSheet(f"background: {_DARK2};")
hl = QHBoxLayout(w)
hl.setContentsMargins(12, 0, 12, 0)
t = QLabel(title)
t.setStyleSheet("color: #5d6d7e; font-size: 10px; font-weight: bold;"
" background: transparent;")
hl.addWidget(t)
hl.addStretch()
if btn_text and btn_slot:
b = QPushButton(btn_text)
b.setFixedHeight(18)
b.setStyleSheet(
"QPushButton { background: transparent; color: #5d6d7e;"
" font-size: 9px; border: none; padding: 0 4px; }"
"QPushButton:hover { color: white; }"
)
b.clicked.connect(btn_slot)
hl.addWidget(b)
return w
# ------------------------------------------------------------------ #
# Profils
# ------------------------------------------------------------------ #
def _reload_profiles(self):
self._profile_combo.blockSignals(True)
self._profile_combo.clear()
self._profile_combo.addItem(f"⭐ {self._cfg.active_profile} (actif)")
for name in self._cfg.list_profiles():
marker = "✓ " if name == self._cfg.active_profile else " "
self._profile_combo.addItem(f"{marker}{name}")
self._profile_combo.blockSignals(False)
def _on_profile_changed(self, text: str):
if "(actif)" in text:
return
name = text.lstrip("✓ ").strip()
if not name or name == self._cfg.active_profile:
return
if self._cfg.load_profile(name):
self._add_log(f"Profil chargé : {name}", "info")
self._reload_profiles()
self._refresh_status()
# ------------------------------------------------------------------ #
# Journal
# ------------------------------------------------------------------ #
def _reload_log(self):
self._log_list.clear()
for ev in conn_log.get_events(30):
self._add_log_item(ev["ts"], ev["msg"], ev["kind"])
self._log_list.scrollToBottom()
def _clear_log(self):
conn_log.clear()
self._log_list.clear()
def _add_log(self, msg: str, kind: str = "info"):
conn_log.log_event(msg, kind)
ts = QDateTime.currentDateTime().toString("yyyy-MM-dd hh:mm:ss")
self._add_log_item(ts, msg, kind)
def _add_log_item(self, ts: str, msg: str, kind: str):
color = _LOG_COLORS.get(kind, "#aed6f1")
item = QListWidgetItem(f"[{ts}] {msg}")
item.setForeground(QColor(color))
self._log_list.addItem(item)
self._log_list.scrollToBottom()
# ------------------------------------------------------------------ #
# Timers
# ------------------------------------------------------------------ #
def _tick_duration(self):
if self._connected_since is None:
return
secs = self._connected_since.secsTo(QDateTime.currentDateTime())
h, rem = divmod(secs, 3600)
m, s = divmod(rem, 60)
self._duration_label.setText(f"Connecté depuis {h:02d}h {m:02d}m {s:02d}s")
def _update_bw(self):
iface = self._cfg.wg.get("interface_name", "wgs0")
result = wg_core.get_interface_bytes(iface)
if result is None or self._prev_rx is None:
self._prev_rx, self._prev_tx = (result or (0, 0))
return
rx_now, tx_now = result
dt = 3.0
rx_bps = max(0, rx_now - self._prev_rx) / dt
tx_bps = max(0, tx_now - self._prev_tx) / dt
self._prev_rx, self._prev_tx = rx_now, tx_now
# `_is_connected` est rafraîchi par _refresh_status : inutile de
# relancer un `wg show` (un sous-processus) toutes les 3 secondes.
self._bw_graph.push(rx_bps if self._is_connected else 0,
tx_bps if self._is_connected else 0)
def _update_ping(self):
host = self._cfg.wg.get("server_endpoint", "")
if not host or not self._is_connected:
self._set_ping_badge(None, "— ms", "offline")
return
if self._ping_worker is not None and self._ping_worker.isRunning():
return
# `ping` bloque jusqu'à 4 s : exécuté dans le thread Qt, il faisait
# saccader toute l'interface toutes les 10 secondes.
self._ping_worker = ValueWorker(wg_core.ping_server, host, 2, parent=self)
self._ping_worker.done.connect(self._on_ping_done)
# Détruire le QThread depuis `finished` et non depuis `done` : `done`
# est émis avant la sortie de run(), un deleteLater() à ce moment
# provoque « QThread: Destroyed while thread is still running ».
self._ping_worker.finished.connect(self._ping_worker.deleteLater)
self._ping_worker.start()
def _on_ping_done(self, ms):
self._ping_worker = None
if not self._is_connected:
self._set_ping_badge(None, "— ms", "offline")
elif ms is None:
self._set_ping_badge(None, "hors ligne", "bad")
elif ms < 50:
self._set_ping_badge(ms, f"{ms} ms", "good")
elif ms < 200:
self._set_ping_badge(ms, f"{ms} ms", "medium")
else:
self._set_ping_badge(ms, f"{ms} ms", "bad")
def _set_ping_badge(self, _ms, text: str, css_key: str):
self._ping_badge.setText(text)
self._ping_badge.setStyleSheet(
"border-radius: 11px; font-size: 10px; font-weight: bold; "
+ _PING_CSS[css_key]
)
def _check_reconnect(self):
if not self._cfg.get("ui", "auto_reconnect") or self._quitting:
return
if self._worker is not None:
return # une (dé)connexion est déjà en cours
if self._cfg.mfa_enabled:
# Rouvrir une fenêtre MFA modale toutes les 30 s rendrait
# l'application inutilisable : la reconnexion doit rester manuelle.
return
if not self._cfg.configured or self._is_connected:
self._reconnect_failures = 0
return
if self._reconnect_failures >= 5:
# Arrêt du harcèlement : chaque tentative demande des privilèges.
if self._reconnect_failures == 5:
self._add_log("Auto-reconnexion suspendue après 5 échecs", "error")
self._reconnect_failures += 1
return
self._add_log("Auto-reconnexion…", "warning")
self._start_connect(silent=True)
# ------------------------------------------------------------------ #
# Statut WireGuard
# ------------------------------------------------------------------ #
def _refresh_status(self):
wg = self._cfg.wg
srv = wg.get("server_endpoint", "")
port = wg.get("server_port", "")
self._info_labels["server"].setText(f"{srv}:{port}" if srv else "—")
self._info_labels["iface"].setText(wg.get("interface_name", "wgs0"))
self._info_labels["addr"].setText(wg.get("client_address", "—"))
self._info_labels["mfa"].setText(
"Activé ✓" if self._cfg.mfa_enabled else "Désactivé"
)
self._btn_copy_key.setEnabled(bool(wg.get("client_public_key", "")))
# Auto-reconnect timer
if self._cfg.get("ui", "auto_reconnect"):
interval = int(self._cfg.get("ui", "reconnect_interval") or 30) * 1000
if not self._reconnect_timer.isActive():
self._reconnect_timer.start(interval)
else:
self._reconnect_timer.stop()
try:
info = wg_core.get_status_info(self._cfg)
connected = info["connected"]
except Exception:
connected = False
info = {}
self._is_connected = connected
if connected:
self._reconnect_failures = 0
if connected:
if self._connected_since is None:
self._connected_since = QDateTime.currentDateTime()
self._status_badge.setText("● Connecté")
self._status_badge.setStyleSheet(
"color: #27ae60; font-size: 15px; font-weight: bold; background: transparent;"
)
self._btn_connect.setText("Se déconnecter")
self._btn_connect.setStyleSheet(
"QPushButton { background: #922b21; color: white; font-size: 14px;"
" font-weight: bold; border-radius: 6px; border: none; }"
"QPushButton:hover { background: #c0392b; }"
)
self._info_labels["rx_tx"].setText(
f"↓ {info.get('rx_bytes','—')}{info.get('tx_bytes','—')}"
)
self._info_labels["handshake"].setText(info.get("last_handshake", "—"))
self._duration_label.setStyleSheet(
"color: #a9dfbf; font-size: 11px; background: transparent;"
)
self.setWindowIcon(icons.icon_connected())
else:
if self._connected_since is not None:
self._connected_since = None
self._duration_label.setText("")
self._bw_graph.reset()
self._prev_rx = self._prev_tx = None
self._status_badge.setText("● Déconnecté")
self._status_badge.setStyleSheet(
"color: #e74c3c; font-size: 15px; font-weight: bold; background: transparent;"
)
self._btn_connect.setText("Se connecter")
self._btn_connect.setStyleSheet(
"QPushButton { background: #1e8449; color: white; font-size: 14px;"
" font-weight: bold; border-radius: 6px; border: none; }"
"QPushButton:hover { background: #27ae60; }"
)
self._info_labels["rx_tx"].setText("—")
self._info_labels["handshake"].setText("—")
self._duration_label.setStyleSheet(
"color: #5d6d7e; font-size: 11px; background: transparent;"
)
self.setWindowIcon(icons.icon_disconnected())
pix = (icons.icon_connected() if connected else icons.icon_disconnected()).pixmap(36, 36)
self._icon_label.setPixmap(pix)
# ------------------------------------------------------------------ #
# Actions
# ------------------------------------------------------------------ #
def _copy_public_key(self):
pub = self._cfg.wg.get("client_public_key", "")
if pub:
QApplication.clipboard().setText(pub)
self._add_log("Clé publique copiée dans le presse-papiers", "info")
def _on_connect(self):
"""Bascule connexion/déconnexion. Le travail réel part dans un thread."""
if self._worker is not None:
return # opération déjà en cours
if self._is_connected or wg_core.is_connected(self._cfg):
self._start_disconnect()
else:
self._start_connect(silent=False)
# ── Connexion ────────────────────────────────────────────────────────
def _start_connect(self, silent: bool = False):
if self._worker is not None:
return
if not self._cfg.configured:
if not silent:
QMessageBox.information(
self, "Configuration manquante",
"WireGuard n'est pas encore configuré.\n"
"Ouvrez le panneau Administrateur."
)
return
if self._cfg.mfa_enabled:
if not self._cfg.mfa_secret:
if not silent:
QMessageBox.warning(self, "MFA non configuré",
"MFA activé mais aucun secret défini.")
return
if silent:
return # jamais de fenêtre MFA sur minuterie
dlg = MFADialog(self._cfg.mfa_secret, self)
if dlg.exec() != MFADialog.DialogCode.Accepted or not dlg.is_verified():
self._add_log("MFA annulé ou échoué", "warning")
return
self._add_log("Authentification MFA réussie ✓", "success")
self._begin_task("Connexion…")
shutdown_guard.arm() # le tunnel devra être rendu à la sortie
self._worker = TaskWorker(wg_core.connect, self._cfg,
tag="connect" if not silent else "connect-silent",
parent=self)
self._worker.done.connect(self._on_task_done)
self._worker.finished.connect(self._worker.deleteLater)
self._worker.start()
# ── Déconnexion ──────────────────────────────────────────────────────
def _start_disconnect(self):
if self._worker is not None:
return
self._begin_task("Déconnexion…")
self._worker = TaskWorker(wg_core.disconnect, self._cfg,
tag="disconnect", parent=self)
self._worker.done.connect(self._on_task_done)
self._worker.finished.connect(self._worker.deleteLater)
self._worker.start()
def _begin_task(self, label: str):
self._connecting = True
self._btn_connect.setEnabled(False)
self._btn_connect.setText(label)
def _on_task_done(self, ok: bool, msg: str, tag: str):
self._worker = None
self._connecting = False
self._btn_connect.setEnabled(True)
silent = tag.endswith("-silent")
if tag.startswith("connect"):
if ok:
self._reconnect_failures = 0
self._connected_since = QDateTime.currentDateTime()
endpoint = self._cfg.wg.get("server_endpoint", "")
self._session_id = hist.start_session(endpoint,
self._cfg.active_profile)
self._add_log(f"Connexion établie → {endpoint}", "success")
QTimer.singleShot(5000, self._update_ping)
else:
self._reconnect_failures += 1
self._add_log(f"Erreur : {msg}", "error")
if not silent:
QMessageBox.warning(self, "Erreur de connexion", msg)
else: # disconnect
if ok:
if self._session_id is not None:
hist.end_session(self._session_id,
self._prev_rx or 0, self._prev_tx or 0)
self._session_id = None
self._add_log("Tunnel WireGuard désactivé", "warning")
else:
self._add_log(f"Erreur déconnexion : {msg}", "error")
if not silent:
QMessageBox.warning(self, "Erreur", msg)
self._check_dns_health()
self._warn_foreign_interfaces()
self._refresh_status()
def _warn_foreign_interfaces(self):
"""Signale un tunnel WireGuard monté hors de WGSecure.
Il impose son propre DNS au système et WGSecure ne le démontera pas :
c'est une cause fréquente de « plus d'accès internet » après coup.
"""
try:
foreign = wg_core.foreign_wg_interfaces(self._cfg)
except Exception:
return
for iface in foreign:
if iface in self._warned_foreign:
continue
self._warned_foreign.add(iface)
self._add_log(
f"Tunnel « {iface} » actif hors de WGSecure "
f"(interface configurée : {self._cfg.wg.get('interface_name','wgs0')})",
"warning",
)
def _check_dns_health(self):
"""Signale — et répare — une résolution DNS restée détournée."""
iface = self._cfg.wg.get("interface_name", "wgs0")
repaired, msg = dns_util.repair_if_broken(iface, self._cfg.wg.get("dns", ""))
if msg:
self._add_log(msg, "warning" if repaired else "error")
def _open_history(self):
HistoryDialog(self).exec()
def _open_admin(self):
if self._cfg.has_admin_password():
pw, ok = QInputDialog.getText(
self, "Accès Administrateur",
"Mot de passe administrateur :",
QLineEdit.EchoMode.Password,
)
if not ok:
return
if not self._cfg.check_admin_password(pw):
QMessageBox.warning(self, "Refusé", "Mot de passe incorrect.")
return
dlg = AdminWindow(self._cfg, self)
dlg.exec()
self._reload_profiles()
self._refresh_status()
# ------------------------------------------------------------------ #
# Fermeture — restitution de l'état réseau
# ------------------------------------------------------------------ #
def prepare_quit(self) -> None:
"""Arrête minuteries et threads, puis rend le réseau à son état initial.
Appelé par toutes les voies de sortie (croix de la fenêtre, « Quitter »
du systray, signal système). Synchrone : la restauration doit être
terminée avant que le processus ne disparaisse, sinon la configuration
DNS du tunnel survit à l'application et la machine perd la résolution
de noms.
"""
if self._quitting:
return
self._quitting = True
for timer in (self._status_timer, self._clock_timer, self._bw_timer,
self._ping_timer, self._reconnect_timer):
timer.stop()
# Laisser les threads en cours se terminer avant de démonter le tunnel :
# une connexion et une déconnexion simultanées laisseraient un état mixte.
for worker in (self._worker, self._ping_worker):
try:
if worker is not None and worker.isRunning():
worker.wait(15000)
except RuntimeError:
pass # objet Qt déjà détruit
self._worker = None
self._ping_worker = None
if self._session_id is not None:
hist.end_session(self._session_id, self._prev_rx or 0,
self._prev_tx or 0)
self._session_id = None
shutdown_guard.restore_network("fermeture de l'application")
def closeEvent(self, event: QCloseEvent):
if self._cfg.get("ui", "minimize_to_tray") and not self._quitting:
# Simple masquage : ce n'est pas une fermeture, le tunnel reste actif.
event.ignore()
self.hide()
return
self.prepare_quit()
event.accept()