dns public : chaque zone designe aussi le serveur de noms du site pair
Le contrat du site porte dns_public_noms_pairs : nom et adresse publique du serveur de chaque pair actif, lus dans son plan et sa carte de frontiere par les memes fonctions que pour le site lui-meme. Le gabarit de zone ecrit un NS par serveur ; la zone qui contient un de ces noms en porte le A, a l'adresse de son site. Sans pair, le rendu est identique a l'ancien. Garde au role (nom, adresse publique, jamais redeclare) ; la preuve du DNS public refuse un pair actif sans dns_public_nom ; dns_bascule compte les serveurs designes et sonde la colle de chacun. Deploye chez les deux locataires : les deux serveurs publics servent chezlepro.ca et technolibre.ca au meme serial, deux NS signes. CHANGELOG 151. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
ee5aad5f41
commit
d152e32328
7 changed files with 148 additions and 36 deletions
30
CHANGELOG.md
30
CHANGELOG.md
|
|
@ -1,3 +1,33 @@
|
||||||
|
## 2026-10-14 (151) — Le second serveur de noms, désigné : `dns1.technolibre.ca`
|
||||||
|
|
||||||
|
**95 preuves.** `make verifier` passe (site de Chezlepro), `make test` passe. Témoin de bout en
|
||||||
|
bout, depuis l'Internet : les deux serveurs publics (`69.70.26.62` et `51.79.45.193`, par le
|
||||||
|
relais) servent `chezlepro.ca` et `technolibre.ca` au même serial, chacune avec deux NS signés,
|
||||||
|
`dns1.chezlepro.ca` et `dns1.technolibre.ca`, et chaque `dns1` à l'adresse de son site. Un second
|
||||||
|
passage chez les deux locataires ne change rien. Témoins qui mordent : une adresse faussée chez
|
||||||
|
OPS-Technolibre rend `site-intrants-verifier` NON CONFORME ; le nom retiré du plan de
|
||||||
|
TechnoLibre fait refuser la preuve du DNS public.
|
||||||
|
|
||||||
|
**Le serveur de noms du site de TechnoLibre s'appelle `dns1.technolibre.ca`** — la nomenclature
|
||||||
|
de `dns1.chezlepro.ca` : `dns1` sous le domaine de l'organisation qui tient le site.
|
||||||
|
|
||||||
|
**Chaque zone publique désigne le serveur de son site ET celui de chaque pair.**
|
||||||
|
- Le contrat du site porte `dns_public_noms_pairs` : {nom : adresse publique} de chaque pair
|
||||||
|
actif, lus dans son plan (`dns_public_nom`) et sa carte de frontière (`opnsense_wan_ip`) par
|
||||||
|
les mêmes fonctions que pour le site lui-même, sous `sur_le_site`. Vérifié contre les
|
||||||
|
intrants du locataire, et déposé dans sa fiche.
|
||||||
|
- Le gabarit de zone écrit un NS par serveur ; la zone qui contient un de ces noms en porte le
|
||||||
|
A, à l'adresse de SON site. Sans pair, le rendu est identique octet pour octet à l'ancien.
|
||||||
|
- Gardé au rôle : nom bien formé, adresse publique, distinct du nôtre, jamais redéclaré.
|
||||||
|
- La preuve du DNS public refuse un pair actif qui ne nomme pas son serveur : ses zones n'en
|
||||||
|
désigneraient qu'un, en silence, alors qu'il les sert déjà.
|
||||||
|
- `dns_bascule.py` compte les serveurs désignés et sonde la colle de chacun.
|
||||||
|
|
||||||
|
**Reste** : la colle de `dns1.technolibre.ca` chez le registraire de technolibre.ca, et celle de
|
||||||
|
`dns1.chezlepro.ca` chez celui de chezlepro.ca, au moment des bascules. Au site de TechnoLibre,
|
||||||
|
`maurice` pousse encore ses journaux en HTTP : la racine du site (`ac-racine-site.crt`) n'est
|
||||||
|
pas publiée à côté de sa carte.
|
||||||
|
|
||||||
## 2026-10-14 (150) — La simulation de `genome-pousser` dit ce que la poussée ferait
|
## 2026-10-14 (150) — La simulation de `genome-pousser` dit ce que la poussée ferait
|
||||||
|
|
||||||
**95 preuves.** `make verifier` passe (site de Chezlepro), `make test` passe. Témoin : contre
|
**95 preuves.** `make verifier` passe (site de Chezlepro), `make test` passe. Témoin : contre
|
||||||
|
|
|
||||||
|
|
@ -105,6 +105,9 @@ serveur_powerdns_ip_publique: "{{ ip_publique_site | default('') }}"
|
||||||
serveur_powerdns_ip_expositions: "{{ ip_publique | default(ip_publique_site) | default('') }}"
|
serveur_powerdns_ip_expositions: "{{ ip_publique | default(ip_publique_site) | default('') }}"
|
||||||
# Le nom du serveur de noms du site (contrat du site) : NS et SOA de chaque zone publique.
|
# Le nom du serveur de noms du site (contrat du site) : NS et SOA de chaque zone publique.
|
||||||
serveur_powerdns_dns_public_nom: "{{ dns_public_nom | default('') }}"
|
serveur_powerdns_dns_public_nom: "{{ dns_public_nom | default('') }}"
|
||||||
|
# LES SERVEURS DE NOMS DES SITES PAIRS (2026-10-14) : {nom : adresse publique}, du contrat du
|
||||||
|
# site. Chaque zone publique les designe en plus du sien ; celle qui contient un nom en porte le A.
|
||||||
|
serveur_powerdns_dns_public_noms_pairs: "{{ dns_public_noms_pairs | default({}) }}"
|
||||||
# Le NOM de la cle TSIG, identique des deux cotes — derive du nom du depot du locataire,
|
# Le NOM de la cle TSIG, identique des deux cotes — derive du nom du depot du locataire,
|
||||||
# par la meme regle que `site_inventaire.py` (`tsig-<depot sans ops->`). `realpath`, parce
|
# par la meme regle que `site_inventaire.py` (`tsig-<depot sans ops->`). `realpath`, parce
|
||||||
# que sur le poste l'inventaire passe par le lien `instance`.
|
# que sur le poste l'inventaire passe par le lien `instance`.
|
||||||
|
|
|
||||||
|
|
@ -82,13 +82,26 @@
|
||||||
# PERSONNE NE LE REDECLARE : un enregistrement a ce nom lui donnerait une seconde
|
# PERSONNE NE LE REDECLARE : un enregistrement a ce nom lui donnerait une seconde
|
||||||
# adresse, ou un CNAME a cote de son A.
|
# adresse, ou un CNAME a cote de son A.
|
||||||
- (serveur_powerdns_dns_public_nom | trim | regex_replace('\\.$', '')) not in (serveur_powerdns_fqdn_declares | from_json)
|
- (serveur_powerdns_dns_public_nom | trim | regex_replace('\\.$', '')) not in (serveur_powerdns_fqdn_declares | from_json)
|
||||||
|
# LES SERVEURS DES SITES PAIRS, SOUS LES MEMES REGLES : un nom bien forme, une adresse
|
||||||
|
# publique, distinct du notre, et que personne ne redeclare.
|
||||||
|
- serveur_powerdns_dns_public_noms_pairs is mapping
|
||||||
|
- (serveur_powerdns_dns_public_noms_pairs | dict2items
|
||||||
|
| rejectattr('key', 'search', '^[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)+\.?$') | list | length) == 0
|
||||||
|
- (serveur_powerdns_dns_public_noms_pairs.values()
|
||||||
|
| select('search', '^(10\.|127\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[01])\.|169\.254\.)') | list | length) == 0
|
||||||
|
- (serveur_powerdns_dns_public_noms_pairs.values()
|
||||||
|
| reject('search', '^\d{1,3}(\.\d{1,3}){3}$') | list | length) == 0
|
||||||
|
- (serveur_powerdns_dns_public_noms_pairs.keys() | map('trim') | map('regex_replace', '\.$', '')
|
||||||
|
| intersect([serveur_powerdns_dns_public_nom | trim | regex_replace('\.$', '')]
|
||||||
|
+ (serveur_powerdns_fqdn_declares | from_json)) | list | length) == 0
|
||||||
fail_msg: >-
|
fail_msg: >-
|
||||||
Zones publiques refusees : zone `.internal` declaree publique, adresse publique
|
Zones publiques refusees : zone `.internal` declaree publique, adresse publique
|
||||||
absente ou privee ({{ serveur_powerdns_ip_publique | default('?') }}), serveur public
|
absente ou privee ({{ serveur_powerdns_ip_publique | default('?') }}), serveur public
|
||||||
du site liste dans `allow-axfr-ips` (TSIG deviendrait decoratif), ou
|
du site liste dans `allow-axfr-ips` (TSIG deviendrait decoratif), ou
|
||||||
`vault_tsig_dns_public` absent de la voute, `dns_public_nom` absent ou malforme
|
`vault_tsig_dns_public` absent de la voute, `dns_public_nom` absent ou malforme
|
||||||
({{ serveur_powerdns_dns_public_nom | default('?') }}), ou redeclare dans les
|
({{ serveur_powerdns_dns_public_nom | default('?') }}), ou redeclare dans les
|
||||||
enregistrements d'une zone.
|
enregistrements d'une zone ; ou un serveur de site pair malforme, a une adresse non
|
||||||
|
publique, ou redeclare ({{ serveur_powerdns_dns_public_noms_pairs | default({}) }}).
|
||||||
quiet: true
|
quiet: true
|
||||||
when: serveur_powerdns_zones_publiques | length > 0
|
when: serveur_powerdns_zones_publiques | length > 0
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,10 +21,19 @@ $TTL {{ serveur_powerdns_ttl }}
|
||||||
de 256 bits ne revele rien d'elle. #}
|
de 256 bits ne revele rien d'elle. #}
|
||||||
{% set _cle = lookup('vars', 'vault_dnssec_' ~ (zone_publique | lower | regex_replace('[^a-z0-9]', '_')), default='') %}
|
{% set _cle = lookup('vars', 'vault_dnssec_' ~ (zone_publique | lower | regex_replace('[^a-z0-9]', '_')), default='') %}
|
||||||
; DNSSEC : {{ ('signee, cle ' ~ (_cle | hash('sha256'))[:12]) if ((domaines_publics[zone_publique].dnssec | default(false) | bool) and _cle) else 'non signee' }}
|
; DNSSEC : {{ ('signee, cle ' ~ (_cle | hash('sha256'))[:12]) if ((domaines_publics[zone_publique].dnssec | default(false) | bool) and _cle) else 'non signee' }}
|
||||||
@ IN NS {{ _ns }}.
|
{# ET LE SERVEUR DE CHAQUE SITE PAIR (2026-10-14). Il replique la zone par le tunnel entre les
|
||||||
{% if _ns.endswith('.' ~ zone_publique) %}
|
sites ; sans son NS, il la servirait sans que personne le designe. Meme regle pour son A :
|
||||||
{{ _ns[:-(zone_publique | length + 1)] }} IN A {{ serveur_powerdns_ip_publique }}
|
seule la zone qui contient son nom le porte, a l'adresse publique de SON site. #}
|
||||||
{% endif %}
|
{% set _ns_tous = {_ns: serveur_powerdns_ip_publique} %}
|
||||||
|
{% for _n, _ip in (serveur_powerdns_dns_public_noms_pairs | default({})).items() %}
|
||||||
|
{% set _ = _ns_tous.update({(_n | trim | regex_replace('\\.$', '')): _ip}) %}
|
||||||
|
{% endfor %}
|
||||||
|
{% for _n in _ns_tous %}
|
||||||
|
@ IN NS {{ _n }}.
|
||||||
|
{% endfor %}
|
||||||
|
{% for _n, _ip in _ns_tous.items() if _n.endswith('.' ~ zone_publique) %}
|
||||||
|
{{ _n[:-(zone_publique | length + 1)] }} IN A {{ _ip }}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
{# UNE ZONE PUBLIQUE NE PORTE QUE DES ADRESSES PUBLIQUES.
|
{# UNE ZONE PUBLIQUE NE PORTE QUE DES ADRESSES PUBLIQUES.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -93,8 +93,13 @@ def _normaliser(rtype: str, rdata: str) -> str:
|
||||||
return rdata
|
return rdata
|
||||||
|
|
||||||
|
|
||||||
def _du_plan(zone: str, conf: dict, ns: str, ip_publique: str) -> dict[tuple[str, str], set[str]]:
|
def _du_plan(zone: str, conf: dict, ns: str, ip_publique: str,
|
||||||
"""Ce que la zone rendue portera, par (FQDN, type). SOA et NS exceptes."""
|
pairs: dict[str, str] | None = None) -> dict[tuple[str, str], set[str]]:
|
||||||
|
"""Ce que la zone rendue portera, par (FQDN, type). SOA et NS exceptes.
|
||||||
|
|
||||||
|
`pairs` : {nom : adresse publique} des serveurs des sites pairs (contrat du site). Comme le
|
||||||
|
gabarit, la zone qui contient l'un de ces noms en porte le A.
|
||||||
|
"""
|
||||||
attendu: dict[tuple[str, str], set[str]] = {}
|
attendu: dict[tuple[str, str], set[str]] = {}
|
||||||
for e in conf.get("enregistrements") or []:
|
for e in conf.get("enregistrements") or []:
|
||||||
t = str(e["type"]).upper()
|
t = str(e["type"]).upper()
|
||||||
|
|
@ -104,8 +109,9 @@ def _du_plan(zone: str, conf: dict, ns: str, ip_publique: str) -> dict[tuple[str
|
||||||
elif t == "CAA":
|
elif t == "CAA":
|
||||||
v = f"0 {e['etiquette']} {v}"
|
v = f"0 {e['etiquette']} {v}"
|
||||||
attendu.setdefault((_rel_en_fqdn(zone, e["nom"]), t), set()).add(_normaliser(t, v))
|
attendu.setdefault((_rel_en_fqdn(zone, e["nom"]), t), set()).add(_normaliser(t, v))
|
||||||
if ns.endswith("." + zone):
|
for nom, ip in {ns: ip_publique, **(pairs or {})}.items():
|
||||||
attendu.setdefault((ns, "A"), set()).add(ip_publique)
|
if nom.endswith("." + zone):
|
||||||
|
attendu.setdefault((nom, "A"), set()).add(ip)
|
||||||
return attendu
|
return attendu
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -153,10 +159,12 @@ def _intrants(base: Path) -> dict:
|
||||||
|
|
||||||
|
|
||||||
def devis_zone(zone: str, conf: dict, ns: str, ip_publique: str, resolveur: str,
|
def devis_zone(zone: str, conf: dict, ns: str, ip_publique: str, resolveur: str,
|
||||||
serveur: str | None) -> tuple[int, int, list[str]]:
|
serveur: str | None, pairs: dict[str, str] | None = None
|
||||||
|
) -> tuple[int, int, list[str]]:
|
||||||
"""Rend (pertes, mesures_impossibles, lignes)."""
|
"""Rend (pertes, mesures_impossibles, lignes)."""
|
||||||
lignes, echecs = [], 0
|
lignes, echecs = [], 0
|
||||||
attendu = _du_plan(zone, conf, ns, ip_publique)
|
pairs = {k.rstrip("."): v for k, v in (pairs or {}).items()}
|
||||||
|
attendu = _du_plan(zone, conf, ns, ip_publique, pairs)
|
||||||
noms = sorted({fq for fq, _ in attendu} | {_rel_en_fqdn(zone, n) for n in NOMS_SONDES})
|
noms = sorted({fq for fq, _ in attendu} | {_rel_en_fqdn(zone, n) for n in NOMS_SONDES})
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -204,23 +212,29 @@ def devis_zone(zone: str, conf: dict, ns: str, ip_publique: str, resolveur: str,
|
||||||
|
|
||||||
# DEUX SERVEURS DE NOMS, OU PAS DE BASCULE. Le registre des `.ca` (CIRA) en exige deux,
|
# DEUX SERVEURS DE NOMS, OU PAS DE BASCULE. Le registre des `.ca` (CIRA) en exige deux,
|
||||||
# et un seul serveur fait de la moindre panne du site une disparition du domaine. La zone
|
# et un seul serveur fait de la moindre panne du site une disparition du domaine. La zone
|
||||||
# n'en designe qu'un tant que le serveur du site pair n'est pas actif.
|
# n'en designe qu'un tant que le serveur du site pair n'est pas actif — et nomme
|
||||||
|
# (`dns_public_nom` du pair, recu par le contrat : `dns_public_noms_pairs`).
|
||||||
|
serveurs = {ns: ip_publique, **pairs}
|
||||||
|
if len(serveurs) < 2:
|
||||||
lignes.append(f" PREALABLE : la zone ne designe qu'un serveur de noms ({ns}) ; il en faut deux "
|
lignes.append(f" PREALABLE : la zone ne designe qu'un serveur de noms ({ns}) ; il en faut deux "
|
||||||
f"— celui du site pair, quand `dns_public_pairs` sera actif.")
|
f"— celui du site pair, quand `dns_public_pairs` sera actif.")
|
||||||
pertes += 1
|
pertes += 1
|
||||||
|
|
||||||
# LA COLLE. Le serveur de noms doit resoudre AVANT qu'un registraire le designe : sinon
|
|
||||||
# la zone basculee pointe vers un nom que personne ne trouve.
|
|
||||||
try:
|
|
||||||
a_ns = _interroger(resolveur, ns, "A")
|
|
||||||
if a_ns == {ip_publique}:
|
|
||||||
lignes.append(f" {ns} resout deja vers {ip_publique} : le registraire peut le designer.")
|
|
||||||
else:
|
else:
|
||||||
lignes.append(f" PREALABLE : {ns} ne resout pas publiquement vers {ip_publique} "
|
lignes.append(f" serveurs de noms designes : {', '.join(serveurs)}")
|
||||||
|
|
||||||
|
# LA COLLE. Chaque serveur de noms doit resoudre AVANT qu'un registraire le designe :
|
||||||
|
# sinon la zone basculee pointe vers un nom que personne ne trouve.
|
||||||
|
for nom, ip in serveurs.items():
|
||||||
|
try:
|
||||||
|
a_ns = _interroger(resolveur, nom, "A")
|
||||||
|
if a_ns == {ip}:
|
||||||
|
lignes.append(f" {nom} resout deja vers {ip} : le registraire peut le designer.")
|
||||||
|
else:
|
||||||
|
lignes.append(f" PREALABLE : {nom} ne resout pas publiquement vers {ip} "
|
||||||
f"(en ligne : {sorted(a_ns) or 'rien'}). La zone qui le contient doit "
|
f"(en ligne : {sorted(a_ns) or 'rien'}). La zone qui le contient doit "
|
||||||
f"le publier d'abord, et son registraire en tenir la colle.")
|
f"le publier d'abord, et son registraire en tenir la colle.")
|
||||||
# Pour la zone qui CONTIENT le nom, c'est la bascule elle-meme qui le publie.
|
# Pour la zone qui CONTIENT le nom, c'est la bascule elle-meme qui le publie.
|
||||||
pertes += 0 if ns.endswith("." + zone) else 1
|
pertes += 0 if nom.endswith("." + zone) else 1
|
||||||
except MesureImpossible as exc:
|
except MesureImpossible as exc:
|
||||||
echecs += 1
|
echecs += 1
|
||||||
lignes.append(f" colle : MESURE IMPOSSIBLE — {exc}")
|
lignes.append(f" colle : MESURE IMPOSSIBLE — {exc}")
|
||||||
|
|
@ -277,7 +291,8 @@ def main() -> int:
|
||||||
total_pertes = total_echecs = 0
|
total_pertes = total_echecs = 0
|
||||||
print(f"DEVIS DE BASCULE DNS — {base.name}, reference {args.resolveur}, serveur de noms {ns}")
|
print(f"DEVIS DE BASCULE DNS — {base.name}, reference {args.resolveur}, serveur de noms {ns}")
|
||||||
for zone, conf in sorted(zones.items()):
|
for zone, conf in sorted(zones.items()):
|
||||||
pertes, echecs, lignes = devis_zone(zone, conf, ns, ip_publique, args.resolveur, args.serveur)
|
pertes, echecs, lignes = devis_zone(zone, conf, ns, ip_publique, args.resolveur, args.serveur,
|
||||||
|
intrants.get("dns_public_noms_pairs") or {})
|
||||||
verdict = ("MESURE INCOMPLETE" if echecs else
|
verdict = ("MESURE INCOMPLETE" if echecs else
|
||||||
"rien ne serait perdu" if not pertes else f"{pertes} ecart(s) a regler")
|
"rien ne serait perdu" if not pertes else f"{pertes} ecart(s) a regler")
|
||||||
print(f"\n{zone} — {verdict}")
|
print(f"\n{zone} — {verdict}")
|
||||||
|
|
|
||||||
|
|
@ -4514,11 +4514,19 @@ def _dns_zones_servies(attendu: dict, servies: set, a_un_serveur: bool) -> list[
|
||||||
return fautes
|
return fautes
|
||||||
|
|
||||||
|
|
||||||
def _dns_paires(declarations: dict) -> list[str]:
|
def _dns_paires(declarations: dict, noms: dict | None = None) -> list[str]:
|
||||||
"""{site: [(pair, etat)]} -> fautes. Une relation vaut des deux cotes, ou pas du tout."""
|
"""{site: [(pair, etat)]} -> fautes. Une relation vaut des deux cotes, ou pas du tout.
|
||||||
|
|
||||||
|
`noms` ({site: dns_public_nom}) : un pair ACTIF doit nommer son serveur (2026-10-14) — les
|
||||||
|
zones de l'autre site le designent en second NS. Sans nom, elles n'en designeraient qu'un,
|
||||||
|
en silence, alors que le serveur les sert deja.
|
||||||
|
"""
|
||||||
fautes = []
|
fautes = []
|
||||||
for site, liens in declarations.items():
|
for site, liens in declarations.items():
|
||||||
for pair, etat in liens:
|
for pair, etat in liens:
|
||||||
|
if noms is not None and etat == "actif" and pair in noms and not noms[pair]:
|
||||||
|
fautes.append(f"{site} replique avec {pair}, qui ne nomme pas son serveur "
|
||||||
|
f"(`dns_public_nom`) — les zones de {site} ne le designeraient pas")
|
||||||
retour = dict(declarations.get(pair, []))
|
retour = dict(declarations.get(pair, []))
|
||||||
if site not in retour:
|
if site not in retour:
|
||||||
fautes.append(f"{site} declare une replication avec {pair}, qui ne la declare pas — "
|
fautes.append(f"{site} declare une replication avec {pair}, qui ne la declare pas — "
|
||||||
|
|
@ -4618,13 +4626,15 @@ def preuve_dns_public_tient() -> tuple[bool, str]:
|
||||||
if ip.startswith(_PRIVEE):
|
if ip.startswith(_PRIVEE):
|
||||||
fautes.append(f"{eco} : `ip_publique_site` vaut {ip}, une adresse privee")
|
fautes.append(f"{eco} : `ip_publique_site` vaut {ip}, une adresse privee")
|
||||||
|
|
||||||
declarations = {}
|
declarations, noms = {}, {}
|
||||||
for d in sorted(RACINE.parent.glob("SITE-*")):
|
for d in sorted(RACINE.parent.glob("SITE-*")):
|
||||||
liens = _yaml_ou_vide(d / "plan" / "10-intrants.yml").get("dns_public_pairs") or []
|
intrants_site = _yaml_ou_vide(d / "plan" / "10-intrants.yml")
|
||||||
|
noms[d.name] = str(intrants_site.get("dns_public_nom") or "").strip()
|
||||||
|
liens = intrants_site.get("dns_public_pairs") or []
|
||||||
if liens:
|
if liens:
|
||||||
declarations[d.name] = [(str(x.get("site")), str(x.get("etat", "actif")))
|
declarations[d.name] = [(str(x.get("site")), str(x.get("etat", "actif")))
|
||||||
for x in liens if isinstance(x, dict)]
|
for x in liens if isinstance(x, dict)]
|
||||||
fautes += _dns_paires(declarations)
|
fautes += _dns_paires(declarations, noms)
|
||||||
|
|
||||||
gabarit = RACINE / "roles" / "serveur_powerdns" / "templates" / "pdns-public.conf.j2"
|
gabarit = RACINE / "roles" / "serveur_powerdns" / "templates" / "pdns-public.conf.j2"
|
||||||
lignes = [l.strip() for l in gabarit.read_text(encoding="utf-8").splitlines()
|
lignes = [l.strip() for l in gabarit.read_text(encoding="utf-8").splitlines()
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,30 @@ def _ip_publique_site() -> str:
|
||||||
return str(_opnsense().get("opnsense_wan_ip") or "")
|
return str(_opnsense().get("opnsense_wan_ip") or "")
|
||||||
|
|
||||||
|
|
||||||
|
def _noms_dns_public_des_pairs() -> dict[str, str]:
|
||||||
|
"""{nom : adresse publique} du serveur de noms de chaque site pair ACTIF (2026-10-14).
|
||||||
|
|
||||||
|
LE SECOND NS D'UNE ZONE. Chaque zone publique designe le serveur de son site ET celui de
|
||||||
|
chaque pair qui la replique. Le nom vient du plan du pair (`dns_public_nom`), l'adresse de
|
||||||
|
sa frontiere (`opnsense_wan_ip`) — lus par les MEMES fonctions que pour ce site-ci, sous
|
||||||
|
`sur_le_site`. La zone qui CONTIENT un de ces noms en porte le A.
|
||||||
|
|
||||||
|
Un pair sans nom ou sans adresse ne rend rien : la preuve du DNS public le refuse a part.
|
||||||
|
"""
|
||||||
|
out: dict[str, str] = {}
|
||||||
|
for nom in underlay_mod.sites_pairs_actifs():
|
||||||
|
try:
|
||||||
|
with underlay_mod.sur_le_site(nom):
|
||||||
|
ns = str((underlay_mod.lire_plan_site("10-intrants.yml") or {})
|
||||||
|
.get("dns_public_nom") or "").strip().rstrip(".")
|
||||||
|
ip = _ip_publique_site()
|
||||||
|
except FileNotFoundError:
|
||||||
|
continue
|
||||||
|
if ns and ip:
|
||||||
|
out[ns] = ip
|
||||||
|
return dict(sorted(out.items()))
|
||||||
|
|
||||||
|
|
||||||
LIBRE = "libre"
|
LIBRE = "libre"
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -172,6 +196,9 @@ def contrat() -> dict:
|
||||||
"ip_publique": ip_publique_de(instance_courante().name),
|
"ip_publique": ip_publique_de(instance_courante().name),
|
||||||
# SOUS QUEL NOM LE SITE FAIT AUTORITE — les NS et le SOA de chaque zone publique.
|
# SOUS QUEL NOM LE SITE FAIT AUTORITE — les NS et le SOA de chaque zone publique.
|
||||||
"dns_public_nom": str(intrants.get("dns_public_nom") or ""),
|
"dns_public_nom": str(intrants.get("dns_public_nom") or ""),
|
||||||
|
# ET LES NOMS DES SERVEURS DES SITES PAIRS (2026-10-14), avec leur adresse publique : les
|
||||||
|
# NS de plus de chaque zone, et le A de celui qui vit sous une zone du locataire.
|
||||||
|
"dns_public_noms_pairs": _noms_dns_public_des_pairs(),
|
||||||
# QUI A LE DROIT D'ENTRER EN SSH
|
# QUI A LE DROIT D'ENTRER EN SSH
|
||||||
# QUI A LE DROIT D'ENTRER EN SSH : le plan d'administration du site ET le tunnel
|
# QUI A LE DROIT D'ENTRER EN SSH : le plan d'administration du site ET le tunnel
|
||||||
# WireGuard des admins, derive de sa declaration (jamais recopie).
|
# WireGuard des admins, derive de sa declaration (jamais recopie).
|
||||||
|
|
@ -197,6 +224,7 @@ OU_LE_LOCATAIRE_LE_DIT = {
|
||||||
"ip_publique_site": ("all/10-intrants.yml", "ip_publique_site"),
|
"ip_publique_site": ("all/10-intrants.yml", "ip_publique_site"),
|
||||||
"ip_publique": ("all/10-intrants.yml", "ip_publique"),
|
"ip_publique": ("all/10-intrants.yml", "ip_publique"),
|
||||||
"dns_public_nom": ("all/10-intrants.yml", "dns_public_nom"),
|
"dns_public_nom": ("all/10-intrants.yml", "dns_public_nom"),
|
||||||
|
"dns_public_noms_pairs": ("all/10-intrants.yml", "dns_public_noms_pairs"),
|
||||||
# CES DEUX-LA MANQUAIENT, ET LE SILENCE A COUTE (2026-09-13). Le site en expose SEPT ;
|
# CES DEUX-LA MANQUAIENT, ET LE SILENCE A COUTE (2026-09-13). Le site en expose SEPT ;
|
||||||
# cette table n'en comparait que CINQ. Le controle rapportait donc « CONFORME » avec
|
# cette table n'en comparait que CINQ. Le controle rapportait donc « CONFORME » avec
|
||||||
# un aplomb complet, sur un locataire dont le plan d'administration pointait encore
|
# un aplomb complet, sur un locataire dont le plan d'administration pointait encore
|
||||||
|
|
@ -223,6 +251,10 @@ def _normaliser(v) -> str:
|
||||||
"""
|
"""
|
||||||
if isinstance(v, (list, tuple)):
|
if isinstance(v, (list, tuple)):
|
||||||
return " ".join(sorted(str(x).strip().rstrip("/") for x in v))
|
return " ".join(sorted(str(x).strip().rstrip("/") for x in v))
|
||||||
|
# Un dictionnaire (`dns_public_noms_pairs`) : l'ordre des cles ne porte aucun sens non plus.
|
||||||
|
if isinstance(v, dict):
|
||||||
|
return " ".join(sorted(f"{str(k).strip().rstrip('.')}={str(x).strip()}"
|
||||||
|
for k, x in v.items()))
|
||||||
return str(v).strip().rstrip("/")
|
return str(v).strip().rstrip("/")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue