feat(update): mise à jour servie par le serveur, depuis l'intérieur du tunnel
Les binaires ne peuvent pas venir du dépôt Gitea : il est privé, et un jeton
embarqué dans une application distribuée est un jeton donné à tous ses
utilisateurs. Ils viennent donc du serveur WGSecure, où le contrôle d'accès
existe déjà — l'API n'est joignable que par le tunnel, et on y parvient parce
que sa clé WireGuard est déclarée. Aucun secret à embarquer.
Le serveur expose GET /api/version, qui rend la version publiée et l'empreinte
de chaque artefact, et /api/version/download/{clé} qui les sert. Le manifeste
est déposé par `make publish-updates`, avec les condensats calculés une fois à
la publication : les recalculer à chaque appel bloquerait l'API sur 125 Mo.
`ANNOUNCE=` permet d'annoncer les artefacts sous un autre numéro, pour exercer
la chaîne sans compiler une seconde version.
Rien ne s'installe sans un clic. La vérification est automatique — au montage
du tunnel puis toutes les demi-heures, car ne la faire qu'au montage laissait
un serveur momentanément injoignable annuler toute proposition pour la session
— mais le téléchargement attend le bouton. Cette application monte un VPN, et
une version défaillante qui se propagerait seule couperait l'accès d'un parc
entier sans que personne ne l'ait demandé.
L'artefact est vérifié contre son empreinte SHA-256 avant d'être mis en place,
et le fichier temporaire est détruit à la moindre anomalie. Le canal est déjà
authentifié par WireGuard ; le condensat couvre ce qu'il ne couvre pas — un
téléchargement tronqué, un disque plein, un artefact mal publié.
Sous Linux, le binaire est remplacé par renommage, sur le même système de
fichiers que sa destination. Un déplacement depuis /tmp se rabattait sur une
copie, donc sur une écriture dans l'exécutable en cours, que le noyau refuse
(ETXTBSY). Un renommage ne touche qu'une entrée de répertoire : l'ancien inode
reste vivant pour le processus, qui continue jusqu'à sa fermeture — et
l'application propose désormais de redémarrer plutôt que de le laisser deviner.
Sous Windows, l'installation passe par le service, qui tourne en LocalSystem et
écrit dans Program Files sans invite d'élévation. La commande `apply_update` ne
prend aucun paramètre : le service lit le manifeste, télécharge et vérifie
lui-même. Lui passer un fichier déjà téléchargé aurait donné à tout compte du
poste — le pipe est ouvert aux utilisateurs interactifs — le moyen de faire
exécuter ce qu'il veut avec les privilèges du système.
L'installeur orchestre le remplacement, ce qu'il ne faisait pas : les deux
exécutables à remplacer tournent au moment de la mise à jour, et Windows
verrouille l'image d'un processus vivant. Il ferme donc l'application par le
gestionnaire de redémarrage, arrête les tunnels et le service avant la copie —
en attendant la libération réelle du fichier, `sc stop` rendant la main avant
la fin de l'arrêt — et garde `restartreplace` en secours. Le service, lui,
lance l'installeur détaché et ne l'attend pas : il lui demande de se remplacer
lui-même.
Durcissement du service au passage. Le premier appelant devient propriétaire et
son SID est retenu ; ensuite seuls ce compte et les administrateurs sont
servis. Le pipe étant ouvert à tout utilisateur interactif, n'importe quel
compte du poste pouvait jusqu'ici couper le tunnel d'un autre ou poser une
règle NRPT valable pour toute la machine. Le contenu d'un `.conf` est également
vérifié — seul son chemin l'était — et les directives exécutables y sont
refusées.
Corrige enfin un défaut de compilation : Linux et Windows partageaient le
répertoire de travail de PyInstaller, que `--clean` vide au démarrage. Lancées
à la suite, les deux cibles effaçaient mutuellement leurs fichiers
intermédiaires, produisant un .exe gonflé de 44 Mo et un binaire Linux tronqué
dont l'archive ne se décompressait plus.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
414144560e
commit
e520e30716
@@ -14,6 +14,12 @@ BUILD := build
|
||||
ICON := /tmp/wgsecure_icon_build.png
|
||||
|
||||
# Options PyInstaller communes aux deux plateformes
|
||||
# `--clean` vide le répertoire de travail au démarrage. Linux et Windows
|
||||
# portant le même `--name`, ils partageaient `build/wgsecure/` : lancés à la
|
||||
# suite, chacun effaçait les fichiers intermédiaires de l'autre en plein
|
||||
# travail. Résultat observé le 8 septembre 2026 — un .exe Windows gonflé de
|
||||
# 44 Mo et un binaire Linux tronqué dont l'archive ne se décompressait plus
|
||||
# (« decompression resulted in return code -3 »). D'où un workpath par cible.
|
||||
PI_OPTS := \
|
||||
--noconfirm \
|
||||
--clean \
|
||||
@@ -26,7 +32,7 @@ PI_OPTS := \
|
||||
--hidden-import cryptography.hazmat.primitives.asymmetric.x25519 \
|
||||
--collect-submodules PyQt6
|
||||
|
||||
.PHONY: all linux windows helper windows-release check-windows-release installer-deps release publish version changelog install install-gnome uninstall-gnome run run-admin icon reset-password setup-sudoers check-privileges clean clean-all help venv
|
||||
.PHONY: all linux windows helper windows-release check-windows-release installer-deps release publish publish-updates version changelog install install-gnome uninstall-gnome run run-admin icon reset-password setup-sudoers check-privileges clean clean-all help venv
|
||||
|
||||
# ── Cible par défaut ────────────────────────────────────────────────────────
|
||||
all: linux
|
||||
@@ -66,6 +72,7 @@ linux: install icon
|
||||
@echo " 🐧 Compilation Linux (PyInstaller onefile)…"
|
||||
@echo ""
|
||||
$(PYTHON) -m PyInstaller $(PI_OPTS) \
|
||||
--workpath build/linux \
|
||||
--onefile \
|
||||
--icon=$(ICON) \
|
||||
--add-data "CHANGELOG.md:." \
|
||||
@@ -92,6 +99,7 @@ windows: icon
|
||||
|| (echo " ❌ qwindows.dll introuvable dans $(WINE_QT_PLUGINS)/platforms"; \
|
||||
echo " → Installez PyQt6 dans Wine avec : make setup-wine"; exit 1)
|
||||
@wine $(WINE_PYTHON) -m PyInstaller $(PI_OPTS) \
|
||||
--workpath build/windows \
|
||||
--onefile \
|
||||
--windowed \
|
||||
--icon=$(ICON) \
|
||||
@@ -129,6 +137,7 @@ helper:
|
||||
@wine $(WINE_PYTHON) -m PyInstaller \
|
||||
--noconfirm \
|
||||
--clean \
|
||||
--workpath build/helper \
|
||||
--name wgsecure-helper \
|
||||
--onefile \
|
||||
--hidden-import win32timezone \
|
||||
@@ -248,6 +257,51 @@ publish: release
|
||||
done; \
|
||||
bash "$(GITEA_RELEASE_SH)" --version $(VERSION) $$ASSETS
|
||||
|
||||
# ── Publication des artefacts vers le serveur de mise à jour ─────────────────
|
||||
# Le client va chercher ses mises à jour sur le serveur WGSecure, pas sur
|
||||
# Gitea : le dépôt est privé, et un jeton embarqué dans une application
|
||||
# distribuée est un jeton donné à tous ses utilisateurs. Cette cible dépose
|
||||
# les binaires et le manifeste que la route /api/version publie.
|
||||
#
|
||||
# Le condensat est calculé ici, une fois, et non par le serveur à chaque
|
||||
# appel : un SHA-256 sur 125 Mo à chaque interrogation bloquerait l'API.
|
||||
UPDATE_HOST := root@192.168.1.206
|
||||
UPDATE_DIR := /opt/WGSecure-Serveur/artifacts
|
||||
|
||||
# ANNOUNCE permet d'annoncer les artefacts sous un autre numéro que celui
|
||||
# qu'ils portent : `make publish-updates ANNOUNCE=9.9.9` fait croire au parc
|
||||
# qu'une version existe, sans avoir à en compiler une seconde. Sert à exercer
|
||||
# la chaîne de mise à jour — détection, téléchargement, empreinte, bascule —
|
||||
# avec les binaires déjà construits.
|
||||
ANNOUNCE ?= $(VERSION)
|
||||
|
||||
publish-updates:
|
||||
@test -f "$(DIST)/$(APP)-$(VERSION)-linux-x86_64" \
|
||||
|| (echo " ❌ Artefact Linux absent — lancez d'abord : make release"; exit 1)
|
||||
@echo ""
|
||||
@echo " 📤 Publication des artefacts $(VERSION) vers $(UPDATE_HOST)…"
|
||||
@FILES="$(DIST)/$(APP)-$(VERSION)-linux-x86_64"; \
|
||||
[ -f "$(DIST)/$(APP)-$(VERSION)-setup.exe" ] \
|
||||
&& FILES="$$FILES $(DIST)/$(APP)-$(VERSION)-setup.exe" \
|
||||
|| echo " ⚠️ Installeur Windows absent : seuls les postes Linux seront servis"; \
|
||||
scp -q $$FILES $(UPDATE_HOST):$(UPDATE_DIR)/
|
||||
@$(PYTHON) -c "import json, hashlib, os, sys; \
|
||||
v='$(VERSION)'; a='$(ANNOUNCE)'; d='$(DIST)'; app='$(APP)'; \
|
||||
spec=[('linux-x86_64', f'{app}-{v}-linux-x86_64'), ('windows-setup', f'{app}-{v}-setup.exe')]; \
|
||||
arts=[{'key': k, 'filename': n, 'size': os.path.getsize(f'{d}/{n}'), \
|
||||
'sha256': hashlib.sha256(open(f'{d}/{n}','rb').read()).hexdigest()} \
|
||||
for k, n in spec if os.path.isfile(f'{d}/{n}')]; \
|
||||
notes=open('CHANGELOG.md', encoding='utf-8').read().split('## [')[1]; \
|
||||
json.dump({'version': a, 'notes': '## [' + notes.split('\n---')[0].strip(), \
|
||||
'artifacts': arts}, open('/tmp/wgs-manifest.json','w', encoding='utf-8'), \
|
||||
ensure_ascii=False, indent=2); \
|
||||
print(f' 🔑 {len(arts)} artefact(s) de {v}, annonces comme {a}')"
|
||||
@scp -q /tmp/wgs-manifest.json $(UPDATE_HOST):$(UPDATE_DIR)/manifest.json
|
||||
@ssh $(UPDATE_HOST) "chown -R wgsecure:wgsecure $(UPDATE_DIR)"
|
||||
@rm -f /tmp/wgs-manifest.json
|
||||
@echo " ✅ Mise à jour $(ANNOUNCE) proposée aux clients"
|
||||
@echo ""
|
||||
|
||||
# ── Version courante ──────────────────────────────────────────────────────────
|
||||
version:
|
||||
@echo "$(VERSION)"
|
||||
|
||||
Reference in New Issue
Block a user