#!/usr/bin/env python3 """Une face publique par relais : chaque entree y repart par ou elle est arrivee. POURQUOI (2026-10-13). Le site de TechnoLibre recoit ses adresses fixes par un tunnel vers un VPS (le relais), sur sa propre interface. Trois pings depuis l'Internet sont arrives sur `wg0` de la frontiere et n'ont recu AUCUNE reponse : OPNsense ne pose `reply-to` que sur une interface dont la passerelle amont est declaree, et la reponse repartait par la route par defaut — le WAN. Une seule regle oubliee suffit a publier un service qui ne repond jamais. Sur le VRAI devis (sans site visible, le test se saute) : - si une face publique est declaree (`if_publique`) : * toute regle ENTRANTE de cette interface porte `replyto` ; * toute sortie vers l'Internet d'un locataire au pool porte la passerelle du relais, et sa traduction sortante est sur la face publique ; * aucune regle `reject` n'y repond (elle est l'Internet, comme le WAN) ; * le WAN ne porte plus aucune entree `pass` (tout ce qui est publie a demenage) ; * aucune redirection n'y vise `wanip` (l'adresse du tunnel) ; - sinon (temoin : le site de Chezlepro), AUCUNE regle ne porte `replyto` ni passerelle — la nouveaute ne deborde pas sur un site qui ne l'a pas declaree. """ from __future__ import annotations import json import subprocess import sys from pathlib import Path RACINE = Path(__file__).resolve().parents[2] def main() -> int: r = subprocess.run([sys.executable, "scripts/devis_opnsense.py", "--json"], capture_output=True, text=True, cwd=RACINE) if r.returncode != 0 or not r.stdout.strip().startswith("{"): print("SAUTE le devis de la frontiere ne se genere pas ici (site invisible)") return 0 devis = json.loads(r.stdout) regles = devis.get("regles") or [] pub, gw, wan = devis.get("if_publique"), devis.get("passerelle_publique"), devis.get("if_wan") echecs: list[str] = [] if not pub: portant = [x for x in regles if x.get("replyto") or x.get("passerelle")] if portant: echecs.append(f"{len(portant)} regle(s) portent reply-to/passerelle sans face " f"publique declaree") for e in echecs: print("ECHEC " + e) if not echecs: print("OK aucune face publique declaree : aucune regle ne porte reply-to ni " "passerelle (temoin)") return 1 if echecs else 0 entrees = [x for x in regles if x.get("interface") == pub and x.get("sens") == "in" and str(x.get("action") or "pass") == "pass"] bloquees = [x for x in regles if x.get("interface") == pub and x.get("action") == "block" and x.get("replyto")] for x in bloquees: echecs.append(f"{pub} : un blocage ({x.get('role')}) porte reply-to") sans = [x for x in entrees if x.get("replyto") != gw] if not entrees: echecs.append(f"aucune regle entrante sur la face publique {pub}") for x in sans: echecs.append(f"{pub} : {x.get('role')} -> {x.get('destination')} sans reply-to {gw}") relayes = {str(n.get("tenant")) for n in devis.get("nat") or [] if n.get("interface") == pub} sorties = [x for x in regles if x.get("sens") == "out" and x.get("tenant") in relayes and str(x.get("destination") or "").startswith("!")] for x in sorties: if x.get("passerelle") != gw: echecs.append(f"sortie de {x.get('tenant')} ({x.get('role')}) sans passerelle {gw}") # Sur un relais, `wanip` est l'adresse du TUNNEL : une redirection qui la vise ne # recoit jamais rien (trouve le 2026-10-13, au premier chargement sur requin). for d in devis.get("redirections") or []: if d.get("interface") == pub and str(d.get("destination") or "wanip") == "wanip": echecs.append(f"redirection {d.get('role')} {d.get('protocole')}/{d.get('port_public')} " f"vise `wanip` sur {pub} : l'adresse du tunnel, que l'Internet ne frappe pas") if any(x.get("action") == "reject" and x.get("interface") == pub for x in regles): echecs.append(f"un `reject` sur {pub} : la face publique repondrait aux balayages") restes = [x for x in regles if x.get("interface") == wan and x.get("sens") == "in" and str(x.get("action") or "pass") == "pass"] for x in restes: echecs.append(f"entree encore sur le WAN : {x.get('role')} -> {x.get('destination')}") for e in echecs: print("ECHEC " + e) if echecs: return 1 print(f"OK face publique {pub} : {len(entrees)} entree(s), toutes reply-to {gw} ; " f"{len(sorties)} sortie(s) de {len(relayes)} locataire(s) par {gw} ; WAN sans entree.") return 0 if __name__ == "__main__": sys.exit(main())