diff --git a/CHANGELOG.md b/CHANGELOG.md index a5156be..9c63c40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # CHANGELOG — Set-OPS +## 2026-09-28 (22) — Pairs WireGuard de Technolibre appliqués ; un renommage n'est plus une création + +**Appliqué** (`vpn_admin.py appliquer --portee OPS-Technolibre`, à la demande de l'exploitant) : +`technolibre-mathieu-portable` devient `technolibre-responsable-portable`. `fabric` au vert : +les quatre plans de la fabric sont conformes. + +**Ce que l'application a révélé.** Le « nouveau » pair portait la MÊME clé publique que +l'ancien : le même appareil, renommé. Le script créait avant de retirer ; OPNsense a refusé +la création (« Public keys should be unique ») — mais le retrait, lui, était passé. La +configuration n'avait plus AUCUN pair pour ce tunnel ; seul le fait que l'échec empêche le +rechargement a gardé l'ancien en service. Rejoué aussitôt : le nouveau pair est créé, relu +en service. + +**Corrigé** : `rapprocher` reconnaît un renommage à la clé (à créer ET à retirer, même clé) +et le fait en MODIFICATION sur place (`set_client`) — l'accès n'est jamais coupé, la +frontière ne voit jamais deux fois la même clé. Un vrai remplacement (autre clé) reste une +création suivie d'un retrait. Éprouvé sur un état simulé, dans les deux cas. + ## 2026-09-28 (21) — La sonde « genome-a-jour » : le filet sous `make publier` `make publier` tient la forge du site à jour dans le geste courant. Reste le jour où l'on diff --git a/scripts/vpn_admin.py b/scripts/vpn_admin.py index d52fd1c..f3afcd8 100644 --- a/scripts/vpn_admin.py +++ b/scripts/vpn_admin.py @@ -183,18 +183,32 @@ def rapprocher(api: appl.Frontiere, vpn: dict) -> dict: return (str(p.get("pubkey")) != str(d.get("cle_publique")) or _choisi(p.get("tunneladdress")) != [str(d.get("adresse"))]) + creer = {n: d for n, d in declares.items() if n not in poses} + # UN PAIR ATTACHE A NOTRE INSTANCE QUE LE PLAN NE DECLARE PLUS : c'est un acces qui + # survit a la decision de le retirer. On le nomme, et `appliquer` le retire. + retirer = {n: u for n, (u, _p) in poses.items() if n not in declares or n in retires} + # UN RENOMMAGE N'EST NI UNE CREATION NI UN RETRAIT (2026-09-28). Le meme appareil + # renomme (`technolibre-mathieu-portable` -> `technolibre-responsable-portable`) garde + # sa cle. Le traiter en creation PUIS retrait echouait : OPNsense refuse deux pairs de + # meme cle (« Public keys should be unique »), et l'echec laissait la configuration + # sans aucun pair pour ce tunnel. On le reconnait a la cle, et on MODIFIE sur place. + cle_retiree = {str(poses[n][1].get("pubkey")): n for n in retirer} + renommer = {} + for n, d in list(creer.items()): + ancien = cle_retiree.get(str(d.get("cle_publique"))) + if ancien: + renommer[n] = (retirer.pop(ancien), ancien, d) + del creer[n] return { "nom": nom, "uuid_serveur": uuid_srv, "serveur_a_creer": uuid_srv is None, "serveur": srv.get(uuid_srv or "", {}), - "pairs_creer": {n: d for n, d in declares.items() if n not in poses}, + "pairs_creer": creer, "pairs_majer": {n: d for n, d in declares.items() if n in poses and different(n)}, + "pairs_renommer": renommer, "pairs_garder": {n for n in declares if n in poses and not different(n)}, - # UN PAIR ATTACHE A NOTRE INSTANCE QUE LE PLAN NE DECLARE PLUS : c'est un acces qui - # survit a la decision de le retirer. On le nomme, et `appliquer` le retire. - "pairs_retirer": {n: u for n, (u, _p) in poses.items() - if n not in declares or n in retires}, + "pairs_retirer": retirer, } @@ -210,12 +224,15 @@ def afficher(p: dict, vpn: dict) -> bool: print(f" + pair {n:<24} {d.get('adresse')}") for n, d in sorted(p["pairs_majer"].items()): print(f" ~ pair MODIFIE {n:<24} {d.get('adresse')}") + for n, (_u, ancien, d) in sorted(p["pairs_renommer"].items()): + print(f" ~ pair RENOMME {ancien} -> {n} {d.get('adresse')}") for n in sorted(p["pairs_retirer"]): print(f" - pair RETIRE {n}") print(f"\n a creer : {len(p['pairs_creer']) + (1 if p['serveur_a_creer'] else 0)}" f" | a retirer : {len(p['pairs_retirer'])}" f" | inchange : {len(p['pairs_garder'])}") - return bool(p["serveur_a_creer"] or p["pairs_creer"] or p["pairs_majer"] or p["pairs_retirer"]) + return bool(p["serveur_a_creer"] or p["pairs_creer"] or p["pairs_majer"] + or p["pairs_renommer"] or p["pairs_retirer"]) def _cle_privee() -> tuple[str, str]: @@ -271,6 +288,13 @@ def appliquer(api: appl.Frontiere, p: dict, vpn: dict) -> int: else: fait(api("/api/wireguard/client/add_client", corps), f"pair {nom_p}") + for nom_p, (u, _ancien, d) in sorted(p["pairs_renommer"].items()): + fait(api(f"/api/wireguard/client/set_client/{u}", {"client": { + "enabled": "1", "name": nom_p, "pubkey": str(d["cle_publique"]), "psk": "", + "tunneladdress": str(d["adresse"]), "keepalive": "25", + "servers": uuid_srv or "", + }}), f"renommage du pair {nom_p}") + for nom_p, u in sorted(p["pairs_retirer"].items()): fait(api(f"/api/wireguard/client/del_client/{u}", {}), f"retrait du pair {nom_p}")