Audit du parcours complet — montage du tunnel, sondage de l'état, saisie
du code, ouverture de l'accès. Neuf défauts, dont plusieurs laissaient le
poste en quarantaine sans moyen visible d'en sortir : exactement ce que le
bouton « Saisir le code d'accès » existe pour éviter depuis la v0.8.2.
Adresse de l'API. api_base_url retranchait le dernier octet de l'adresse
du client, ce qui suppose un /24 et ignore les IPs autorisées. Or une
adresse client en /32 est la forme la plus courante et ne décrit aucun
réseau : un client en 10.6.1.7/32 sur un 10.6.0.0/16 était expédié vers
10.6.1.1, qui n'existe pas. La déduction passe désormais par
wireguard.tunnel_gateway, qui servait déjà au diagnostic et gère /32,
IPv6 et le repli sur les IPs autorisées. Une adresse IPv6 est encadrée de
crochets, sans quoi son « : » se confondait avec celui du port.
Révocation invisible. Le client ne sondait qu'au retour du tunnel : une
révocation avant terme — redémarrage du serveur, purge de sa table de
sessions, décision d'un administrateur — n'était annoncée par rien et
l'application affichait « Ouvert ✓ » devant un réseau muet, bouton
masqué, jusqu'à la chute du tunnel. Sans échéance annoncée, indéfiniment.
L'accès est reconfirmé chaque minute tant qu'il est cru ouvert.
Authentification perdue. « Plus tard » restait actif pendant la
vérification, Échap et la croix aussi. Fermer là rendait la main sur un
refus alors que le serveur pouvait encore accepter : sa réponse arrivait
après la sortie d'exec() et n'était lue par personne. L'accès était
ouvert côté serveur, l'application affichait la quarantaine, et le code
saisi était consommé pour rien. La fenêtre ne se ferme plus tant qu'une
requête est en vol.
Fenêtre survivante. Ni accept() ni reject() ne passent par closeEvent :
le minuteur de 500 ms continuait à battre après la fermeture, et la
fenêtre — parentée à la principale — survivait à sa disparition.
dispose() l'arrête et programme la destruction ; _access_dialog est remis
à None, son thread n'étant plus attendu à l'arrêt de l'application.
Repli 404 trompeur. La sonde d'accessibilité tenait pour preuve toute
réponse du serveur DNS du split-DNS, sans vérifier qu'il était joint par
le tunnel. Une adresse RFC1918 routée par le réseau local du poste
répondait donc hors tunnel, et le repli censé éviter le blocage le
provoquait : accès déclaré ouvert sans échéance, bouton masqué. La route
est vérifiée d'abord, sans émettre de paquet — connect() sur une socket
UDP suffit à connaître l'adresse source retenue par le noyau.
Proxy. urlopen emploie l'ouvreur par défaut, dont le ProxyHandler lit
http_proxy dans l'environnement : sur un poste d'entreprise la requête
vers l'adresse privée du tunnel partait au proxy, dont la réponse — 404
souvent — était prise pour celle du serveur, déclenchant le repli
ci-dessus. Panne intermittente parfaite : selon l'environnement de
lancement, ça passait ou non.
Silence pris pour un refus. Un ok faux couvrait « le serveur refuse » et
« le serveur n'a rien dit », traités pareil : un hoquet au montage du
tunnel faisait surgir la fenêtre et brûler un code alors que l'accès
était peut-être déjà ouvert. AuthResult porte désormais authoritative.
Un refus du serveur referme du premier coup, il en est seul juge ; un
silence déclenche un second sondage avant de déranger l'utilisateur, et
n'entame l'accès qu'après trois minutes sans réponse.
Annulation destructrice. _open_access_dialog écrivait l'état d'accès
quoi qu'il arrive. Un sondage abouti pendant que la fenêtre était ouverte
— les minuteries tournent dans la boucle imbriquée d'exec() — pouvait
établir que l'accès l'était, que « Plus tard » remettait aussitôt à
fermé. Renoncer à saisir un code ne referme rien côté serveur ; plus rien
ici non plus.
api_url. Le schéma est vérifié plutôt que repris tel quel : identifiant
et code partent dans cette URL. Reste ouvert, et hors de ce dépôt : l'API
par défaut est en clair et le serveur n'est pas authentifié.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
prepare_quit() attend les threads en cours avant toute destruction, mais sa
liste en oubliait deux : le sondage d'état ajouté en 0.8.6 — ma régression —
et le thread de validation de la fenêtre de saisie, parenté à cette fenêtre
depuis la v0.8.2 et jamais attendu. Fermer la fenêtre de code pendant qu'une
requête est en vol suffisait à avorter le process, ce qui arrive dès que le
serveur tarde. Reproduit avec un serveur qui accepte sans jamais répondre,
puis vérifié corrigé sur les deux chemins.
Le sondage est ramené à 4 s : l'arrêt l'attend désormais, et dix secondes y
seraient une interface figée. Son échec ne coûte rien, contrairement à une
validation ratée qui gâche un code déjà saisi.
Ajoute le temps restant sur la ligne Accès distant, et un repli pour les
serveurs sans GET /api/session : sur un 404, le client observe si le trafic
atteint le réseau distant plutôt que de réclamer un code déjà validé. Un 403
reste une réponse qui fait autorité.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Le client tenait pour acquis que l'autorisation ne survivait pas à la chute
du tunnel — un commentaire l'affirmait noir sur blanc. C'est l'inverse : le
serveur autorise l'adresse de tunnel du pair jusqu'à l'échéance enregistrée,
et cette adresse ne change pas d'une reconnexion à l'autre. Se reconnecter
ne révoque rien ; seul le client oubliait, et redemandait un code que le
serveur avait déjà accepté.
Il pose maintenant la question au lieu de la supposer, via GET /api/session
(serveur >= 0.3.0), dès que le tunnel monte : démarrage sur un tunnel déjà
actif, reconnexion automatique, ou avant toute invite de saisie. Le serveur
identifie l'appelant à l'IP source, aucun identifiant ne circule.
Serveur injoignable, réponse illisible ou serveur antérieur à la 0.3.0 ne
valent pas « accès ouvert » : sans information, le client ne conclut rien et
laisse le bouton disponible — le sens sûr, et le comportement de la 0.8.5.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
L'autorisation obtenue auprès du serveur a un terme, mais le client n'en
tenait aucun compte : `access_until`, pourtant renvoyé à chaque validation,
ne servait qu'à composer une ligne de journal. L'accès était marqué ouvert
jusqu'à la chute du tunnel, si bien qu'une fois le délai écoulé l'interface
affichait « Ouvert ✓ » et masquait le bouton comme l'entrée de systray —
laissant l'utilisateur devant un réseau muet, sans issue visible, dans la
situation même que ce bouton existe pour couvrir depuis la v0.8.2.
L'échéance remonte désormais jusqu'à la fenêtre principale, qui la relit
sans jamais la calculer : sa durée est un réglage serveur, que le poste n'a
pas à deviner. Un serveur qui n'en annonce pas laisse l'accès ouvert jusqu'à
la déconnexion, plutôt que de se voir imposer un terme inventé ici.
Corrige au passage l'heure de fin, annoncée en UTC mais présentée comme
locale — deux heures d'avance en été.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Le bouton et l'entrée de systray étaient conditionnés à un champ vide par
défaut depuis son introduction en v0.8.0 : un poste tenu en quarantaine par
le serveur n'affichait donc aucun moyen d'en sortir. La saisie ne dépend
plus que de l'état du tunnel ; la fenêtre demande l'identifiant quand il
manque et ne l'enregistre qu'une fois le serveur l'ayant accepté.
L'état « Non géré par le serveur » disparaît au passage : le client
l'affirmait à partir de sa seule configuration locale, sans rien vérifier.
« Non vérifié » est la vérité tant qu'aucun code n'a été validé.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Le bouton « Saisir le code d'accès » était créé et placé, mais sous le bord
inférieur : rendre un widget visible ne met pas les layouts à jour dans la
foulée — Qt poste un LayoutRequest traité au tour de boucle suivant. La
fenêtre, de hauteur fixe recalculée à chaque changement de contenu, se
mesurait avant ce traitement et obtenait une hauteur en retard d'un cycle.
Le symptôme était déjà visible dans les tests de la v0.8.2 : la hauteur
montait bien de 258 à 298 px à l'apparition du bouton, mais ne redescendait
jamais à sa disparition. J'avais mis cette asymétrie sur le compte du
backend graphique de test au lieu d'y voir le retard lui-même.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
La fenêtre de saisie ne s'ouvrait qu'une fois, juste après une connexion
manuelle réussie. Un refus, une fenêtre fermée, une reconnexion automatique
— qui n'ouvre volontairement aucune modale — ou l'expiration de
l'autorisation au bout de 12 h laissaient un tunnel monté, un réseau muet
et aucun élément d'interface pour ressaisir un code.
Un bouton sous l'action de connexion et une entrée de systray apparaissent
désormais dès qu'une saisie a un sens : identifiant VPN configuré, tunnel
monté, accès non ouvert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
La v0.8.0 laissait cohabiter deux vérifications du code à 6 chiffres : la
fenêtre MFA locale, qui contrôlait un code contre un secret détenu par le
poste lui-même — et ne verrouillait donc que sa propre interface — puis la
validation par le serveur, seule à décider de l'accès réseau. L'ancienne
disparaît, avec le secret TOTP qu'elle stockait en clair dans config.json
(effacé au premier lancement) et l'onglet MFA du panneau Administrateur.
La saisie passe par une fenêtre dédiée qui relaie le refus exact du serveur
et laisse réessayer, la requête partant dans un thread pour qu'un serveur
muet ne fige pas l'application. Une ligne « Accès distant » distingue
désormais l'état de l'autorisation de celui du tunnel : monté sans code
validé, celui-ci ne mène nulle part.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
Ajoute un fichier log texte rotatif (wgsecure.log) dans le dossier
applicatif, en complément du journal JSON interne, pour diagnostiquer
un échec (ex. split-DNS) sans dépendre de l'UI. Corrige le panneau
Journal qui coupait les messages longs sans moyen de les lire en
entier (défilement horizontal réactivé + infobulle).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- CHANGELOG.md n'était pas empaqueté par PyInstaller (datas=[]) et son
chemin, calculé depuis __file__, ne survivait pas à l'extraction
onefile : la page « À propos » restait vide une fois compilée. Le
fichier est désormais embarqué (--add-data) et son chemin bascule sur
sys._MEIPASS en mode frozen.
- Le badge de latence se basait uniquement sur un ping ICMP vers
l'endpoint : de nombreux serveurs/pare-feux le bloquent alors que le
tunnel WireGuard fonctionne, d'où « hors ligne » permanent. Repli sur
l'âge du dernier handshake réel quand l'ICMP échoue.
- wgsecure.iss installe désormais WireGuard for Windows en silencieux
(prérequis manquant jusqu'ici), au même titre que le VC++
Redistributable ; make installer-deps télécharge les deux binaires.
- Instance unique : un second lancement réveille l'instance active.
- Autostart et reconnexion auto activés par défaut (nouvelle installation).
- Page « À propos » refaite en 5 onglets (contenu séparé dans app_info.py).
- Corrige le handshake/RX-TX qui ne s'affichaient plus (wg show sans élévation).
- Corrige le diagnostic sudo trompeur (sondes qui ne matchaient jamais les règles).
- Corrige make setup-sudoers bloqué hors session graphique (pkexec -> sudo), idempotent.
- Corrige le README : Kill Switch inexistant retiré, ordre d'élévation réel documenté.
- 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.