frontiere : NAT des locataires par leur adresse publique (devis)
Sortie traduite avec l adresse attribuee par le site ; flux externe rediriges depuis cette adresse vers la machine du role. Gardes : pas de redirection en double, adresse du pool. Non applique : en attente de l accord de l exploitant. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
e84b155a5f
commit
e9b03399b9
3 changed files with 95 additions and 3 deletions
27
CHANGELOG.md
27
CHANGELOG.md
|
|
@ -1,5 +1,32 @@
|
|||
# CHANGELOG — Set-OPS
|
||||
|
||||
## 2026-09-29 (36) — Le NAT des locataires par leur adresse publique (devis ; application en attente)
|
||||
|
||||
**La règle** : les services publics d'un locataire passent par SON adresse publique, et
|
||||
sont redirigés par la frontière ; sa sortie est traduite avec cette même adresse.
|
||||
|
||||
**Ce que faisait la frontière.** Sortie : les deux locataires et le site sortaient tous par
|
||||
l'adresse du WAN (`wanip`, `.62`) — vus de l'Internet, une seule machine. Entrée : seule la
|
||||
redirection du site existait (DNS public 53) ; un flux `externe` de locataire (Postfix 25,
|
||||
Dovecot 993) produisait une règle de filtrage vers une adresse privée, sans redirection
|
||||
devant — une publication qui avait l'air faite et ne recevait rien.
|
||||
|
||||
**Le devis** (`devis_opnsense.py`) :
|
||||
- NAT sortant de chaque locataire vers l'adresse que le pool du site lui attribue ; le site
|
||||
garde `wanip` ; sans attribution, repli sur `wanip` (et une note).
|
||||
- Tout flux entrant `externe` d'un locataire — hors SSH de gestion, hors ICMP — devient une
|
||||
redirection DEPUIS son adresse publique vers la machine unique qui porte le rôle (plusieurs
|
||||
machines : aucune redirection, une note). La règle WAN existante voit l'adresse traduite.
|
||||
- Gardes : pas deux redirections sur la même adresse/protocole/port public ; l'adresse d'une
|
||||
redirection de locataire est celle que le pool lui attribue.
|
||||
- `appliquer_opnsense.py` : l'adresse publique entre dans l'identité d'une redirection (les
|
||||
deux locataires publient le 25) — sauf `wanip`, pour que celle du site ne soit pas recréée ;
|
||||
le plan l'affiche.
|
||||
|
||||
**Plan** (non appliqué) : NAT Chezlepro → `.61`, Technolibre → `.60` (2 remplacés) ;
|
||||
redirections `.61`/`.60` : 25 → `edge-mta-01`, 993 → `infra-mail-01`. Le pare-feu Proxmox reçoit
|
||||
déjà ces deux ports depuis `+t<i>-internet` (entrée 33).
|
||||
|
||||
## 2026-09-29 (35) — Le pool d'adresses publiques du site ; chaque locataire reçoit la sienne
|
||||
|
||||
**La règle** : l'hébergeur détient un pool d'adresses publiques, et c'est le site qui en
|
||||
|
|
|
|||
|
|
@ -136,7 +136,12 @@ def cle_nat(n: dict) -> str:
|
|||
|
||||
def cle_redirection(d: dict) -> str:
|
||||
"""Identite d'une redirection publique : ce que l'Internet frappe, et ou ca aboutit."""
|
||||
# L'ADRESSE PUBLIQUE ENTRE DANS L'IDENTITE (2026-09-29) — deux locataires publient le
|
||||
# meme port sur deux adresses. Omise pour `wanip` : la redirection du site garde sa cle,
|
||||
# et n'est pas recreee (ce qui couperait son DNS public le temps de l'operation).
|
||||
dest = str(d.get("destination") or "wanip")
|
||||
return (f"setopsrdr:{d['interface']}:{d['protocole']}:{d['port_public']}"
|
||||
f"{'' if dest == 'wanip' else '@' + dest}"
|
||||
f"->{d['cible']}:{d['port_local']}")
|
||||
|
||||
|
||||
|
|
@ -154,7 +159,8 @@ def _corps_redirection(d: dict, k: str) -> dict:
|
|||
"ipprotocol": "inet",
|
||||
"protocol": d["protocole"],
|
||||
"source": {"network": "any", "port": "", "not": "0"},
|
||||
"destination": {"network": "wanip", "port": str(d["port_public"]), "not": "0"},
|
||||
"destination": {"network": str(d.get("destination") or "wanip"),
|
||||
"port": str(d["port_public"]), "not": "0"},
|
||||
"target": d["cible"],
|
||||
"local-port": str(d["port_local"]),
|
||||
"natreflection": "disable",
|
||||
|
|
@ -440,8 +446,10 @@ def afficher(p: dict) -> bool:
|
|||
print(f" + NAT sortant {n['interface']:<5} {n['source'][:30]:<30} "
|
||||
f"-> {n['cible']}")
|
||||
for k, d in sorted(p["rdr_creer"].items()):
|
||||
_pub = f"{d.get('destination') or 'wanip'}:{d['port_public']}"
|
||||
print(f" + REDIRECTION PUBLIQUE {d['interface']:<5} {d['protocole']:<4} "
|
||||
f"{d['port_public']:<9} -> {d['cible']}:{d['port_local']} ({d.get('role', '')})")
|
||||
f"{_pub:<20} -> {d['cible']}:{d['port_local']} ({d.get('tenant') or 'SITE'}, "
|
||||
f"{d.get('role', '')})")
|
||||
for k, r in sorted(p["routes_creer"].items()):
|
||||
print(f" + route {r['reseau']:<18} -> {r['prochain_saut']:<14} "
|
||||
f"{r['tenant']}")
|
||||
|
|
|
|||
|
|
@ -550,6 +550,11 @@ def construire(tenants: list[tuple[str, str, dict]]) -> dict:
|
|||
# frontiere un alias qu'aucune regle ne reference : `appliquer_opnsense` l'aurait
|
||||
# cree, et personne n'aurait su a quoi il sert.
|
||||
|
||||
# L'ADRESSE PUBLIQUE DE CHAQUE LOCATAIRE (2026-09-29) — attribuee par le site dans son
|
||||
# pool (`opnsense_ips_publiques`). Elle traduit sa sortie ET porte ses services publics.
|
||||
ips_publiques = {str(qui): str(ip) for ip, qui in
|
||||
(_intr.get("opnsense_ips_publiques") or {}).items() if str(qui) != "libre"}
|
||||
|
||||
for nom, pfx, n in tenants:
|
||||
etiquette = f"{pfx}{n['index']}"
|
||||
alias_tenant = f"SETOPS_TENANT_{etiquette}"
|
||||
|
|
@ -692,6 +697,38 @@ def construire(tenants: list[tuple[str, str, dict]]) -> dict:
|
|||
"tenant": nom,
|
||||
"raison": fl.get("raison", ""),
|
||||
})
|
||||
# UN SERVICE PUBLIC D'UN LOCATAIRE EST JOINT PAR SON ADRESSE PUBLIQUE (2026-09-29).
|
||||
#
|
||||
# La regle WAN ci-dessus vise une adresse privee : sans redirection devant elle,
|
||||
# elle ne recoit rien. La redirection part de l'adresse que le site attribue au
|
||||
# locataire — jamais de celle du WAN, que tous partagent : deux locataires qui
|
||||
# publient le 25 ne se disputent rien. Le SSH de gestion n'est pas publie ; l'ICMP
|
||||
# n'a pas de port. Une cible, ou rien : une redirection porte UNE adresse.
|
||||
if (entrant and "externe" in _pairs(fl) and not ssh_gestion
|
||||
and role != ROLE_SOCLE
|
||||
and str(fl.get("protocole", "tcp")).lower() in ("tcp", "udp")):
|
||||
ip_pub = ips_publiques.get(nom)
|
||||
if not ip_pub:
|
||||
print(f"note : {nom} n'a pas d'adresse publique au pool du site — "
|
||||
f"{role} ({', '.join(_ports(fl))}) n'est pas redirige.", file=sys.stderr)
|
||||
elif len(cibles) != 1:
|
||||
print(f"note : {role} est porte par {len(cibles)} machine(s) de {nom} — une "
|
||||
f"redirection exige une cible unique ; aucune.", file=sys.stderr)
|
||||
else:
|
||||
_pub = fl.get("port_public")
|
||||
for _pl in _ports(fl):
|
||||
if not str(_pl).isdigit():
|
||||
continue
|
||||
redirections.append({
|
||||
"interface": if_wan,
|
||||
"protocole": fl.get("protocole", "tcp"),
|
||||
"destination": ip_pub,
|
||||
"port_public": int(_pub) if str(_pub or "").isdigit() else int(_pl),
|
||||
"cible": cibles[0],
|
||||
"port_local": int(_pl),
|
||||
"role": role,
|
||||
"tenant": nom,
|
||||
})
|
||||
|
||||
intrants = intrants_frontiere()
|
||||
# --- LES MACHINES DU SITE -------------------------------------------------
|
||||
|
|
@ -1257,12 +1294,17 @@ def construire(tenants: list[tuple[str, str, dict]]) -> dict:
|
|||
# du WAN. Le mode automatique d'OPNsense ne couvre QUE les reseaux directement attaches ;
|
||||
# un supernet joint par route statique en sort sans avertissement. Mesure du 2026-08-06 :
|
||||
# le filtre laissait passer, un etat s'ouvrait sans `nat_addr`, et rien ne revenait.
|
||||
#
|
||||
# ET CHAQUE LOCATAIRE SORT PAR SA PROPRE ADRESSE (2026-09-29) — celle que le site lui
|
||||
# attribue. Tous sortaient par l'adresse du WAN : vu de l'Internet, les deux locataires
|
||||
# et le site etaient une seule machine (reputation du courriel, listes d'acces, journaux
|
||||
# de ceux qu'on joint). Sans attribution, le repli reste l'adresse du WAN.
|
||||
nat = [
|
||||
{
|
||||
"interface": if_wan,
|
||||
"source": f"SETOPS_TENANT_{pfx}{n['index']}",
|
||||
"destination": "any",
|
||||
"cible": "wanip",
|
||||
"cible": ips_publiques.get(nom, "wanip"),
|
||||
"tenant": nom,
|
||||
}
|
||||
for nom, pfx, n in tenants
|
||||
|
|
@ -1850,6 +1892,21 @@ def verifier(devis: dict) -> tuple[bool, list[str]]:
|
|||
f"{d['port_local']} sans regle WAN correspondante.")
|
||||
if not ipaddress.ip_address(d["cible"]).is_private:
|
||||
erreurs.append(f"Redirection vers {d['cible']} : une cible de redirection est interne.")
|
||||
# UNE ADRESSE, UN PROTOCOLE, UN PORT PUBLIC : UNE SEULE DESTINATION. Deux redirections
|
||||
# identiques cote Internet, la premiere gagnerait sans que rien ne le dise.
|
||||
_vues: dict[tuple, str] = {}
|
||||
for d in devis.get("redirections") or []:
|
||||
c = (d.get("destination", "wanip"), d["protocole"], str(d["port_public"]))
|
||||
if c in _vues:
|
||||
erreurs.append(f"Redirection en double sur {c[0]} {c[1]}/{c[2]} : "
|
||||
f"{_vues[c]} et {d['cible']}:{d['port_local']}.")
|
||||
_vues[c] = f"{d['cible']}:{d['port_local']}"
|
||||
# L'ADRESSE D'UN LOCATAIRE EST LA SIENNE : celle que le pool du site lui attribue.
|
||||
_pool = {str(ip): str(q) for ip, q in (intrants_frontiere().get("opnsense_ips_publiques") or {}).items()}
|
||||
for d in devis.get("redirections") or []:
|
||||
if d.get("tenant") and _pool.get(str(d.get("destination"))) != d["tenant"]:
|
||||
erreurs.append(f"Redirection de {d['tenant']} depuis {d.get('destination')} : "
|
||||
f"le pool du site ne lui attribue pas cette adresse.")
|
||||
|
||||
# UN TUNNEL DE LOCATAIRE NE VOIT QUE SON LOCATAIRE (2026-09-17).
|
||||
#
|
||||
|
|
|
|||
Loading…
Reference in a new issue