Set-OPS-Public/scripts/tests/test_frontiere_refus.py
Daniel Allaire f9e45b1676 relais : la face publique d'un site sans adresse fixe, et le site de reprise de TechnoLibre
Le site de TechnoLibre n'a qu'une adresse DHCP. Trois adresses fixes sont louees sur un VPS
(le relais), qui repond a l'ARP pour elles et les route en /32 dans un tunnel WireGuard que la
frontiere initie. Aucune traduction : l'adresse du client arrive intacte.

- underlay.py : `tunnel: wireguard` dispense de VLAN ; role d'hote `relais`.
- devis_opnsense.py : `opnsense_face_publique`. Les entrees publiques s'y posent avec
  reply-to ; les sorties des locataires du pool passent par le relais ; une redirection n'y
  vise plus `wanip`. Le devis de Chezlepro est identique au caractere pres.
- appliquer_opnsense.py : replyto et gateway transmis et dans l'identite (seulement s'ils
  existent) ; le NAT sortant en mode automatic arrete l'application au lieu de dupliquer.
- role serveur_relais, inventorie depuis la carte comme un hyperviseur ; cle privee dans la
  voute du site. Deploye, idempotent.
- site_inventaire.py : les hyperviseurs ne sont plus filtres sur 192.168.11. ; site-appliquer
  charge la voute designee par SETOPS_UNDERLAY ; voute.py n'exige plus d'un locataire un
  secret du site.
- prouver.py : P67 lit le plan par underlay.plan_site() ; P53 couvre la face publique.
- Tests : test_underlay_tunnel, test_face_publique_relais, test_mode_nat_sortant.

95 preuves (make verifier), make test passe. CHANGELOG 144 et 145.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 21:20:04 -04:00

64 lines
2.9 KiB
Python

#!/usr/bin/env python3
"""La frontiere refuse A VOIX HAUTE a l'interieur, et se tait sur l'Internet.
POURQUOI (2026-10-01). Les machines refusaient deja en le disant (nftables, Proxmox) ; la
frontiere jetait en silence sur TOUTES ses interfaces. Entre deux zones, un flux mal declare
expirait au lieu d'echouer — la sonde `connectivite` l'a mesure au site. Chaque interface
interne recoit desormais un `reject` final. Le WAN, lui, reste muet : y repondre servirait
les balayages d'Internet (P53).
Sur le VRAI devis (il faut voir le site : sans `underlay.yml`, le test se saute) :
- aucun `reject` sur le WAN ;
- chaque interface interne qui porte des regles porte un `reject` ;
- ce `reject` vient APRES toute autre regle de son interface (OPNsense evalue en `quick`).
"""
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 []
# `if_wan`, la cle que le devis produit. Le test lisait `interfaces.wan`, qui n'existe
# pas : il retombait sur « wan », juste au site par chance, faux sur le modele public.
wan = str(devis.get("if_wan") or "wan")
# La face publique par relais (2026-10-13) est l'Internet, comme le WAN : muette aussi.
muettes = {wan} | ({str(devis["if_publique"])} if devis.get("if_publique") else set())
echecs = []
refus = [x for x in regles if x.get("action") == "reject"]
for m in sorted(muettes):
if any(x.get("interface") == m for x in refus):
echecs.append(f"un `reject` sur {m} : la bordure repondrait aux balayages")
interfaces = {x.get("interface") for x in regles} - muettes
for itf in sorted(i for i in interfaces if i):
siens = [x for x in regles if x.get("interface") == itf]
rj = [x for x in siens if x.get("action") == "reject"]
if not rj:
echecs.append(f"{itf} : aucun refus audible")
continue
haut = max([int(x.get("sequence") or 1) for x in siens if x.get("action") != "reject"] or [0])
if int(rj[0].get("sequence") or 1) <= haut:
echecs.append(f"{itf} : le `reject` ({rj[0].get('sequence')}) ne vient pas apres "
f"toutes les autres regles ({haut})")
for e in echecs:
print("ECHEC " + e)
if echecs:
return 1
print(f"OK {len(refus)} interface(s) interne(s) refusent a voix haute, chacune en dernier ; "
f"WAN muet.")
return 0
if __name__ == "__main__":
sys.exit(main())