Set-OPS-Public/scripts/tests/test_face_publique_relais.py

93 lines
4.7 KiB
Python
Raw Normal View History

2026-10-13 21:20:04 -04:00
#!/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())