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
|
# 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
|
## 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
|
**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:
|
def cle_redirection(d: dict) -> str:
|
||||||
"""Identite d'une redirection publique : ce que l'Internet frappe, et ou ca aboutit."""
|
"""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']}"
|
return (f"setopsrdr:{d['interface']}:{d['protocole']}:{d['port_public']}"
|
||||||
|
f"{'' if dest == 'wanip' else '@' + dest}"
|
||||||
f"->{d['cible']}:{d['port_local']}")
|
f"->{d['cible']}:{d['port_local']}")
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -154,7 +159,8 @@ def _corps_redirection(d: dict, k: str) -> dict:
|
||||||
"ipprotocol": "inet",
|
"ipprotocol": "inet",
|
||||||
"protocol": d["protocole"],
|
"protocol": d["protocole"],
|
||||||
"source": {"network": "any", "port": "", "not": "0"},
|
"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"],
|
"target": d["cible"],
|
||||||
"local-port": str(d["port_local"]),
|
"local-port": str(d["port_local"]),
|
||||||
"natreflection": "disable",
|
"natreflection": "disable",
|
||||||
|
|
@ -440,8 +446,10 @@ def afficher(p: dict) -> bool:
|
||||||
print(f" + NAT sortant {n['interface']:<5} {n['source'][:30]:<30} "
|
print(f" + NAT sortant {n['interface']:<5} {n['source'][:30]:<30} "
|
||||||
f"-> {n['cible']}")
|
f"-> {n['cible']}")
|
||||||
for k, d in sorted(p["rdr_creer"].items()):
|
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} "
|
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()):
|
for k, r in sorted(p["routes_creer"].items()):
|
||||||
print(f" + route {r['reseau']:<18} -> {r['prochain_saut']:<14} "
|
print(f" + route {r['reseau']:<18} -> {r['prochain_saut']:<14} "
|
||||||
f"{r['tenant']}")
|
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
|
# frontiere un alias qu'aucune regle ne reference : `appliquer_opnsense` l'aurait
|
||||||
# cree, et personne n'aurait su a quoi il sert.
|
# 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:
|
for nom, pfx, n in tenants:
|
||||||
etiquette = f"{pfx}{n['index']}"
|
etiquette = f"{pfx}{n['index']}"
|
||||||
alias_tenant = f"SETOPS_TENANT_{etiquette}"
|
alias_tenant = f"SETOPS_TENANT_{etiquette}"
|
||||||
|
|
@ -692,6 +697,38 @@ def construire(tenants: list[tuple[str, str, dict]]) -> dict:
|
||||||
"tenant": nom,
|
"tenant": nom,
|
||||||
"raison": fl.get("raison", ""),
|
"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()
|
intrants = intrants_frontiere()
|
||||||
# --- LES MACHINES DU SITE -------------------------------------------------
|
# --- 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 ;
|
# 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 :
|
# 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.
|
# 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 = [
|
nat = [
|
||||||
{
|
{
|
||||||
"interface": if_wan,
|
"interface": if_wan,
|
||||||
"source": f"SETOPS_TENANT_{pfx}{n['index']}",
|
"source": f"SETOPS_TENANT_{pfx}{n['index']}",
|
||||||
"destination": "any",
|
"destination": "any",
|
||||||
"cible": "wanip",
|
"cible": ips_publiques.get(nom, "wanip"),
|
||||||
"tenant": nom,
|
"tenant": nom,
|
||||||
}
|
}
|
||||||
for nom, pfx, n in tenants
|
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.")
|
f"{d['port_local']} sans regle WAN correspondante.")
|
||||||
if not ipaddress.ip_address(d["cible"]).is_private:
|
if not ipaddress.ip_address(d["cible"]).is_private:
|
||||||
erreurs.append(f"Redirection vers {d['cible']} : une cible de redirection est interne.")
|
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).
|
# UN TUNNEL DE LOCATAIRE NE VOIT QUE SON LOCATAIRE (2026-09-17).
|
||||||
#
|
#
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue