Files
WGSecure/Makefile
T
tuxgyverandClaude Opus 5 cc9f521a95 feat(security): retire le MFA local, le code d'accès est celui du serveur (v0.8.1)
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>
2026-09-06 13:06:17 +02:00

367 lines
19 KiB
Makefile

## ──────────────────────────────────────────────
## WGSecure (WGS) — Makefile
## Compilation Linux & Windows via PyInstaller
## ──────────────────────────────────────────────
APP := wgsecure
VERSION := 0.8.1
VENV := .venv
PYTHON := $(VENV)/bin/python3
PIP := $(VENV)/bin/pip
SRC := main.py
DIST := dist
BUILD := build
ICON := /tmp/wgsecure_icon_build.png
# Options PyInstaller communes aux deux plateformes
PI_OPTS := \
--noconfirm \
--clean \
--name $(APP) \
--hidden-import qrcode \
--hidden-import qrcode.image.pil \
--hidden-import PIL \
--hidden-import PIL.Image \
--hidden-import cryptography \
--hidden-import cryptography.hazmat.primitives.asymmetric.x25519 \
--collect-submodules PyQt6
.PHONY: all linux windows helper 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
# ── Cible par défaut ────────────────────────────────────────────────────────
all: linux
# ── Environnement virtuel ────────────────────────────────────────────────────
# Le venv est reconstruit si la version de Python du système a changé : un venv
# créé pour python3.13 pointe vers .venv/lib/python3.13/site-packages, que
# python3.14 n'ouvre pas. L'interpréteur démarrait alors sans aucune de ses
# dépendances (« No module named PyQt6 ») sans que make ne le remarque.
venv:
@if [ -x $(PYTHON) ] && \
[ "$$($(PYTHON) -c 'import sys;print("%d.%d"%sys.version_info[:2])' 2>/dev/null)" \
= "$$(python3 -c 'import sys;print("%d.%d"%sys.version_info[:2])')" ] && \
$(PYTHON) -c 'import PyQt6' >/dev/null 2>&1; then \
echo " ✅ Environnement virtuel valide ($(VENV))"; \
else \
echo " ♻️ (Re)création de l'environnement virtuel $(VENV)…"; \
rm -rf $(VENV); \
python3 -m venv $(VENV); \
$(PIP) install --upgrade pip --quiet; \
$(PIP) install -r requirements.txt --quiet; \
fi
# ── Génération de l'icône PNG (utilisée par PyInstaller) ────────────────────
icon:
@$(PYTHON) -c "\
import sys; sys.path.insert(0,'.');\
from PyQt6.QtWidgets import QApplication; app=QApplication(sys.argv);\
from app.ui.icons import icon_app;\
pix=icon_app().pixmap(256,256); pix.save('$(ICON)','PNG');\
print(' Icône générée →', '$(ICON)')" 2>/dev/null
@test -f $(ICON) || (echo " Génération icône échouée"; exit 1)
# ── Binaire Linux ────────────────────────────────────────────────────────────
linux: install icon
@echo ""
@echo " 🐧 Compilation Linux (PyInstaller onefile)…"
@echo ""
$(PYTHON) -m PyInstaller $(PI_OPTS) \
--onefile \
--icon=$(ICON) \
--add-data "CHANGELOG.md:." \
$(SRC)
@echo ""
@echo " ✅ Binaire Linux → $(DIST)/$(APP)"
@echo ""
# ── Binaire Windows (via Wine + Python Windows) ──────────────────────────────
# Le hook PyQt6 de pyinstaller-hooks-contrib introspecte QLibraryInfo pour
# localiser Qt6/plugins/platforms au moment du build : sous Wine cette
# introspection échoue silencieusement, donc qwindows.dll n'est jamais copié
# et l'exe plante au lancement avec « no Qt platform plugin could be
# initialized ». On force sa copie via --add-data.
WINE_QT_PLUGINS := $(HOME)/.wine/drive_c/users/$(USER)/AppData/Local/Programs/Python/Python311/Lib/site-packages/PyQt6/Qt6/plugins
windows: icon
@echo ""
@echo " 🪟 Compilation Windows (Wine + PyInstaller)…"
@echo ""
@which wine > /dev/null 2>&1 \
|| (echo " ❌ Wine non installé → sudo apt install wine"; exit 1)
@test -f "$(WINE_QT_PLUGINS)/platforms/qwindows.dll" \
|| (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) \
--onefile \
--windowed \
--icon=$(ICON) \
--add-data "$(WINE_QT_PLUGINS)/platforms;PyQt6/Qt6/plugins/platforms" \
--add-data "$(WINE_QT_PLUGINS)/styles;PyQt6/Qt6/plugins/styles" \
--add-data "CHANGELOG.md;." \
$(SRC) \
|| (echo ""; \
echo " ❌ Échec : Python Windows introuvable dans Wine."; \
echo " → Installez-le avec : make setup-wine"; \
echo ""; exit 1)
@echo ""
@echo " ✅ Binaire Windows → $(DIST)/$(APP).exe"
@echo ""
# ── Service Windows privilégié (wgsecure-helper.exe) ─────────────────────────
# Exécutable séparé de wgsecure.exe : installe/désinstalle le tunnel et pose/
# retire les règles NRPT (split-DNS) en LocalSystem, pour éviter une invite
# d'authentification à chaque connexion/déconnexion (cf. app/service/). Utilise
# pywin32, installé uniquement dans ce venv Wine — jamais dans .venv (Linux) ni
# dans les dépendances de wgsecure.exe. Cible explicite : ni `windows` ni
# `release` ne la déclenchent implicitement, ce composant restant optionnel.
# --hidden-import win32timezone : dépendance dynamique de pywin32 que
# l'analyse statique de PyInstaller ne détecte pas seule (service qui
# démarre mais plante à l'exécution sans ce forçage — cas connu).
helper:
@echo ""
@echo " 🛡️ Compilation du service (Wine + PyInstaller)…"
@echo ""
@which wine > /dev/null 2>&1 \
|| (echo " ❌ Wine non installé → sudo apt install wine"; exit 1)
@test -f "$(WINE_PYTHON)" \
|| (echo " ❌ Python Windows introuvable dans Wine → make setup-wine"; exit 1)
@wine $(WINE_PYTHON) -m pip install -r requirements-helper.txt --quiet
@wine $(WINE_PYTHON) -m PyInstaller \
--noconfirm \
--clean \
--name wgsecure-helper \
--onefile \
--hidden-import win32timezone \
app/service/helper_service.py
@echo ""
@echo " ✅ Service → $(DIST)/wgsecure-helper.exe"
@echo ""
# ── Prérequis de l'installeur Windows (wgsecure.iss) ─────────────────────────
# WireGuard n'est pas empaqueté avec WGSecure (c'est wg.exe et le pilote
# tunnel qui manquent, pas une simple DLL) : wgsecure.iss l'installe donc en
# silencieux à partir de ce binaire, téléchargé ici une bonne fois pour
# toutes plutôt que d'imposer une étape manuelle avant chaque compilation.
WIREGUARD_URL := https://download.wireguard.com/windows-client/wireguard-installer.exe
VCREDIST_URL := https://aka.ms/vs/17/release/vc_redist.x64.exe
installer/wireguard-installer.exe:
@echo " ⬇️ Téléchargement de l'installeur WireGuard…"
@curl -L --fail -o $@ $(WIREGUARD_URL)
installer/VC_redist.x64.exe:
@echo " ⬇️ Téléchargement du Visual C++ Redistributable…"
@curl -L --fail -o $@ $(VCREDIST_URL)
installer-deps: installer/wireguard-installer.exe installer/VC_redist.x64.exe
@echo " ✅ Prérequis de l'installeur prêts dans installer/"
# ── Installation de Python dans Wine ─────────────────────────────────────────
WINE_PY_VER := 3.11.9
WINE_PY_URL := https://www.python.org/ftp/python/$(WINE_PY_VER)/python-$(WINE_PY_VER)-amd64.exe
WINE_PY_EXE := /tmp/python-$(WINE_PY_VER)-amd64.exe
WINE_PYTHON := $(HOME)/.wine/drive_c/users/$(USER)/AppData/Local/Programs/Python/Python311/python.exe
setup-wine:
@echo " 📦 Installation Python $(WINE_PY_VER) Windows dans Wine…"
@which wine > /dev/null 2>&1 \
|| (echo " ❌ wine non installé → sudo apt install wine"; exit 1)
@test -f $(WINE_PY_EXE) \
|| (echo " ⬇️ Téléchargement Python Windows…" \
&& curl -L -o $(WINE_PY_EXE) $(WINE_PY_URL))
@echo " 🔧 Installation silencieuse…"
wine $(WINE_PY_EXE) /quiet InstallAllUsers=0 PrependPath=1
@echo " 📦 Installation des dépendances…"
wine $(WINE_PYTHON) -m pip install --upgrade pip
wine $(WINE_PYTHON) -m pip install -r requirements.txt
wine $(WINE_PYTHON) -m pip install pyinstaller
@echo " ✅ Python Windows prêt dans Wine"
# ── Release (Linux renommé avec version) ─────────────────────────────────────
# Ne fait pas `clean` : celui-ci vide tout $(DIST), y compris les artefacts
# Windows produits juste avant (compilés sous Wine, plusieurs minutes) que
# `publish` doit justement joindre à la release. Seul l'artefact Linux est
# retiré, ce qui suffit à garantir sa fraîcheur.
release: linux
@rm -f $(DIST)/$(APP)-*-linux-x86_64
@mv $(DIST)/$(APP) $(DIST)/$(APP)-$(VERSION)-linux-x86_64
@echo " 📦 Release → $(DIST)/$(APP)-$(VERSION)-linux-x86_64"
# ── Publication de la release sur Gitea ───────────────────────────────────────
# Rebuild l'artefact Linux (fraîcheur garantie avec VERSION), joint aussi les
# artefacts Windows présents dans dist/ s'ils ont déjà été produits pour cette
# version (binaire + installeur), puis tague et publie sur Gitea. Réutilise
# gitea_release.sh (BorisSkill) plutôt que de réimplémenter la création de
# tag/release/upload d'assets déjà gérée par ce script (jeton, détection du
# remote, notes issues du CHANGELOG).
GITEA_RELEASE_SH := $(HOME)/.claude/skills/BorisSkill/scripts/gitea_release.sh
publish: release
@test -f "$(GITEA_RELEASE_SH)" \
|| (echo " ❌ gitea_release.sh introuvable ($(GITEA_RELEASE_SH))"; \
echo " → Nécessite BorisSkill (Claude Code)."; exit 1)
@ASSETS="--asset $(DIST)/$(APP)-$(VERSION)-linux-x86_64"; \
for f in "$(DIST)/$(APP)-$(VERSION)-windows-x86_64.exe" "$(DIST)/$(APP)-$(VERSION)-setup.exe"; do \
[ -f "$$f" ] && ASSETS="$$ASSETS --asset $$f"; \
done; \
bash "$(GITEA_RELEASE_SH)" --version $(VERSION) $$ASSETS
# ── Version courante ──────────────────────────────────────────────────────────
version:
@echo "$(VERSION)"
# ── Dernière entrée du changelog ─────────────────────────────────────────────
changelog:
@awk '/^## \[/{if(n++)exit} n' CHANGELOG.md
# ── Installation GNOME (utilisateur local, sans sudo) ────────────────────────
INSTALL_BIN := $(HOME)/.local/bin
INSTALL_ICON := $(HOME)/.local/share/icons/hicolor/256x256/apps
INSTALL_APPS := $(HOME)/.local/share/applications
install-gnome: linux
@echo ""
@echo " 🐧 Installation de WGSecure pour GNOME…"
@mkdir -p $(INSTALL_BIN) $(INSTALL_ICON) $(INSTALL_APPS)
@cp $(DIST)/$(APP) $(INSTALL_BIN)/$(APP)
@chmod +x $(INSTALL_BIN)/$(APP)
@echo " ✅ Binaire → $(INSTALL_BIN)/$(APP)"
@cp $(ICON) $(INSTALL_ICON)/$(APP).png
@echo " ✅ Icône → $(INSTALL_ICON)/$(APP).png"
@printf '[Desktop Entry]\nType=Application\nName=WGSecure\nComment=WireGuard GUI avec MFA TOTP\nExec=%s\nIcon=%s\nCategories=Network;Security;\nStartupWMClass=wgsecure\nTerminal=false\n' \
"$(INSTALL_BIN)/$(APP)" "$(APP)" \
> $(INSTALL_APPS)/$(APP).desktop
@echo " ✅ Lanceur → $(INSTALL_APPS)/$(APP).desktop"
@gtk-update-icon-cache -f -t $(HOME)/.local/share/icons/hicolor 2>/dev/null || true
@update-desktop-database $(INSTALL_APPS) 2>/dev/null || true
@echo ""
@echo " 🎉 Installation terminée. WGSecure est disponible dans le menu GNOME."
@echo ""
uninstall-gnome:
@echo " 🗑️ Désinstallation de WGSecure…"
@rm -f $(INSTALL_BIN)/$(APP)
@rm -f $(INSTALL_ICON)/$(APP).png
@rm -f $(INSTALL_APPS)/$(APP).desktop
@gtk-update-icon-cache -f -t $(HOME)/.local/share/icons/hicolor 2>/dev/null || true
@update-desktop-database $(INSTALL_APPS) 2>/dev/null || true
@echo " ✅ WGSecure désinstallé"
# ── Dépendances Python ────────────────────────────────────────────────────────
install: venv
$(PIP) install -r requirements.txt
$(PIP) install pyinstaller
@echo " ✅ Dépendances installées dans $(VENV)"
# ── Diagnostic des droits ────────────────────────────────────────────────────
check-privileges:
@echo ""
@echo " 🔐 Droits requis par WGSecure :"
@$(PYTHON) -c "\
import sys; sys.path.insert(0, '.'); \
from app.utils.platform_utils import privilege_report; \
rows = privilege_report(); \
print(' (exécution en root : aucun droit à configurer)') if not rows else None; \
[print(' ' + ('✅' if ok else '⚠️ ') + ' ' + name.ljust(12) + ('sans mot de passe' if ok else 'dialogue polkit à chaque appel').ljust(32) + '— ' + role) for name, ok, role in rows]; \
print() if not rows else print(chr(10) + ' ⚠️ → lancez make setup-sudoers pour supprimer les dialogues.' if any(not ok for _, ok, _ in rows) else chr(10) + ' ✅ Tout est configuré.')"
@echo ""
# ── Droits sudo wg-quick sans dialogue de mot de passe ───────────────────────
# `pkexec` (utilisé jusqu'ici) exige un agent d'authentification polkit
# graphique : absent d'un terminal simple (SSH, tmux, session sans agent), il
# reste bloqué en attente d'une réponse qui n'arrive jamais. `sudo` n'a pas ce
# problème : lancé depuis un terminal, il y invite directement le mot de
# passe, avec ou sans polkit. La cible est idempotente : elle recalcule les
# règles à chaque lancement et n'invite pour un mot de passe que si le
# contenu déjà installé diffère (donc jamais lors d'une relance sans
# changement).
setup-sudoers:
@echo ""
@echo " 🔧 Configuration sudoers (wg-quick + nettoyage réseau, sans mot de passe)…"
@$(PYTHON) -c "\
import os, subprocess, tempfile, shutil; \
user = os.environ.get('SUDO_USER') or os.environ.get('USER') or os.path.basename(os.path.expanduser('~')); \
w = lambda n: shutil.which(n) or ''; \
cmds = [c for c in ( \
w('wg-quick'), \
w('wg'), \
w('ip') + ' link delete dev *' if w('ip') else '', \
w('resolvconf') + ' -d *' if w('resolvconf') else '', \
w('resolvectl') + ' revert *' if w('resolvectl') else '', \
w('resolvectl') + ' dns *' if w('resolvectl') else '', \
w('resolvectl') + ' domain *' if w('resolvectl') else '', \
w('install') + ' -D -m 600 -o root -g root * /etc/wireguard/*' if w('install') else '', \
) if c]; \
rules = ''.join(user + ' ALL=(ALL) NOPASSWD: ' + c + chr(10) for c in cmds); \
dest = '/etc/sudoers.d/wgsecure'; \
current = subprocess.run(['sudo', '-n', 'cat', dest], capture_output=True, text=True); \
already_ok = current.returncode == 0 and current.stdout == rules; \
print(' ✅ Déjà configuré pour ' + user + ' — rien à faire.') if already_ok else None; \
import sys; sys.exit(0) if already_ok else None; \
fd, tmp = tempfile.mkstemp(prefix='wgsecure_sudoers_'); \
os.write(fd, rules.encode()); os.close(fd); os.chmod(tmp, 0o600); \
chk = subprocess.run(['visudo', '-cqf', tmp], capture_output=True); \
ok = chk.returncode == 0; \
r = subprocess.run(['sudo', 'install', '-m', '440', '-o', 'root', '-g', 'root', tmp, dest]) if ok else None; \
os.unlink(tmp); \
print(' ❌ Règles sudoers invalides, rien installé : ' + chk.stderr.decode().strip()) if not ok else \
print(' ✅ ' + dest + ' configuré pour ' + user + ' — plus de dialogue admin.' if r.returncode == 0 else ' ❌ Échec.')"
@echo ""
# ── Réinitialisation du mot de passe administrateur ──────────────────────────
reset-password:
@echo ""
@echo " ⚠️ Réinitialisation du mot de passe administrateur WGSecure…"
@$(PYTHON) -c "\
import sys; sys.path.insert(0,'.');\
from app.core.config import Config;\
cfg = Config();\
cfg.set_admin_password('');\
print(' ✅ Mot de passe supprimé — accès admin sans restriction au prochain démarrage.')"
@echo ""
# ── Lancer l'application ─────────────────────────────────────────────────────
run: venv
DISPLAY=:0 $(PYTHON) $(SRC)
run-admin: venv
DISPLAY=:0 $(PYTHON) $(SRC) --admin
# ── Nettoyage ────────────────────────────────────────────────────────────────
clean:
@echo " 🧹 Nettoyage…"
@rm -rf $(BUILD) $(DIST) *.spec
@find . -name "__pycache__" -type d -exec rm -rf {} + 2>/dev/null; true
@find . -name "*.pyc" -delete 2>/dev/null; true
@echo " Artefacts supprimés"
clean-all: clean
@rm -rf $(VENV)
@echo " Venv supprimé"
# ── Aide ─────────────────────────────────────────────────────────────────────
help:
@echo ""
@echo " ╔══════════════════════════════════════════════╗"
@echo " 🛡️ WGSecure v$(VERSION) Makefile "
@echo " ╠══════════════════════════════════════════════╣"
@echo " make install Installe les dépendances "
@echo " make linux Binaire Linux (onefile) "
@echo " make windows Binaire Windows (Wine) "
@echo " make helper Service Windows (Wine) "
@echo " make installer-deps Prérequis wgsecure.iss "
@echo " make release Linux + nommage release "
@echo " make publish Tague + publie sur Gitea "
@echo " make version Affiche la version "
@echo " make changelog Dernière entrée changelog║"
@echo " make setup-wine Python Windows dans Wine "
@echo " make install-gnome Installe dans GNOME "
@echo " make uninstall-gnome Désinstalle de GNOME "
@echo " make setup-sudoers wg-quick sans sudo "
@echo " make reset-password Supprime le MDP admin "
@echo " make run Lance l'application "
@echo " make run-admin Lance en mode admin "
@echo " make clean Supprime les artefacts "
@echo " ╚══════════════════════════════════════════════╝"
@echo ""