feat(windows): tunnel installé une fois, puis démarré — et code validé par le serveur

Deux changements liés, qui achèvent le déplacement de la sécurité vers
le serveur.

1. Le tunnel n'est plus recréé à chaque connexion. Créer ou supprimer un
   service Windows n'est jamais délégable à un utilisateur standard, le
   démarrer peut l'être : c'est cette réinstallation systématique qui
   imposait une authentification administrateur à chaque connexion ET à
   chaque déconnexion. Désormais connect() démarre le service s'il
   existe déjà, disconnect() se contente de l'arrêter, et le tunnel
   reste installé entre les deux.

   Trois voies en cascade pour ce démarrage/arrêt : sc.exe sans
   élévation, puis le service wgsecure-helper, puis l'élévation en
   dernier recours. Le helper gagne donc deux commandes
   (start/stop_tunnel_service). Cette cascade tient quelle que soit la
   configuration du poste — le test en session standard a montré que
   l'ACL posée sur le service WireGuard ne donne aucun droit sur le
   service du tunnel, qui est un objet distinct.

2. Après le montage, l'application demande le code à 6 chiffres et le
   fait valider PAR LE SERVEUR (app/core/vpn_session.py). Jusqu'ici le
   code était vérifié localement, contre un secret que l'application
   détenait : elle validait donc un code qu'elle pouvait produire, et ne
   verrouillait que sa propre interface. L'ordre est imposé par le
   réseau — l'API n'est joignable que depuis le tunnel, qui sert de
   réseau de quarantaine tant que le code n'est pas passé.

   L'URL de l'API est déduite de l'adresse du client (10.6.0.6 →
   10.6.0.1) faute de valeur explicite : c'est l'adresse du serveur
   lui-même, la seule jamais filtrée depuis la quarantaine.

Sans identifiant VPN configuré, l'étape est ignorée : le serveur peut
tourner en mode permissif, où un compte non enrôlé garde son accès, et
imposer la saisie bloquerait des installations qui fonctionnent.

Chemins Linux inchangés (branches is_windows()).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-06 12:40:34 +02:00
co-authored by Claude Opus 5
parent e272b9397e
commit a7ac6d1375
6 changed files with 264 additions and 11 deletions
+38
View File
@@ -9,6 +9,7 @@ from PyQt6.QtCore import Qt, QTimer, QDateTime
from PyQt6.QtGui import QFont, QCloseEvent, QColor
from app.core.config import Config
from app.core import vpn_session
from app.core import wireguard as wg_core
from app.core import dns as dns_util
from app.core import log as conn_log
@@ -692,6 +693,38 @@ class MainWindow(QMainWindow):
self._worker.finished.connect(self._worker.deleteLater)
self._worker.start()
def _authenticate_with_server(self, silent: bool):
"""Demande le code à 6 chiffres et le fait valider par le serveur.
Sans identifiant VPN configuré, l'étape est ignorée : le serveur
peut très bien tourner en mode permissif, où un compte non enrôlé
conserve son accès. Imposer la saisie dans ce cas bloquerait des
installations qui fonctionnent parfaitement.
"""
if not (self._cfg.wg.get("vpn_username") or "").strip():
return
if silent:
# Reconnexion automatique : ouvrir une fenêtre modale sans
# personne devant l'écran laisserait l'application figée.
self._add_log("Code d'accès requis — reconnectez-vous manuellement",
"warning")
return
code, accepted = QInputDialog.getText(
self, "Code d'accès",
"Le tunnel est monté, mais l'accès aux ressources distantes\n"
"reste fermé tant que votre code n'est pas validé.\n\n"
"Code à 6 chiffres :",
)
if not accepted or not code.strip():
self._add_log("Authentification annulée — accès distant fermé", "warning")
return
ok, message = vpn_session.authenticate(self._cfg, code.strip())
self._add_log(message, "success" if ok else "error")
if not ok:
QMessageBox.warning(self, "Accès refusé", message)
def _begin_task(self, label: str):
self._connecting = True
self._btn_connect.setEnabled(False)
@@ -717,6 +750,11 @@ class MainWindow(QMainWindow):
base, _, extra = msg.partition("\n")
if extra:
self._add_log(extra, "warning")
# Le tunnel est monté mais ne mène encore nulle part : le
# serveur garde le pair en quarantaine tant qu'un code n'a
# pas été validé. C'est seulement maintenant que l'API est
# joignable — elle ne l'est que depuis l'intérieur du tunnel.
self._authenticate_with_server(silent)
QTimer.singleShot(5000, self._update_ping)
else:
self._reconnect_failures += 1