censar: sonda DNS-only (--probe-dns) — y los dominios fósiles vuelven a la lista de decisiones

Censo real de gioser (204.168.193.248, identificado por IP y no por hostname, que dice «momento»):
19 vivos, 18 NO-DECLARADOS, 85 declarados-muertos, 29 dominios.

**El HTTP toca al origen; el DNS no.** Sondear los dominios desde la propia máquina disparó su
fail2ban y la dejó incomunicada, así que el censo en local los salteaba — y quedaban 29 de 66 ítems
SIN recomendación por una precaución correcta aplicada de más. Lo que dispara la jaula es el HTTP:
`getent` le pregunta al DNS, no al servidor. `--probe-dns` sondea sólo el nombre, sin UNA SOLA
petición a gioser, y contesta la pregunta que más pesa en una mudanza: cuáles siguen apuntando acá y
cuáles son fósiles. 29/29 clasificados: 12 apuntan acá, 4 ya se mudaron, 13 NO RESUELVEN.

Lo que el DNS solo no puede decir es si el backend contesta, así que ésos quedan en `apunta-aca`,
clase propia y NO `vivo`: decir «vivo» sin haber pedido una página sería la respuesta falsa con forma
de respuesta que este censo existe para evitar.

**Y un hueco de verdad: los `fosil-sin-dns` quedaban fuera de `entradas()`**, con el argumento de que
un dominio sin DNS no necesita ningún paso. Cierto para el DNS, FALSO para lo que arrastra: son 13 de
29, y `terapeuta.ec` tenía 279 M de contenido y su bloque en el servidor web. Fuera de la lista eran
invisibles, así que nadie decidía borrarlos y sus datos viajaban a la caja nueva por omisión — el
«mudar fósiles» que este plan existe para evitar, y contra su propia regla 2 («lo que muere se dice
por su nombre y con su tamaño, ANTES de borrar nada»). Ahora entran, con recomendación `muere` y el
aviso de revisar qué dejaron en disco.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
This commit is contained in:
Sergio
2026-09-11 21:21:03 +00:00
co-authored by Claude Opus 5
parent 21e7cc7159
commit 73f75d45ed
3 changed files with 74 additions and 11 deletions
+33
View File
@@ -301,6 +301,39 @@ reconstruyen el servidor desde cero**:
Ninguno de los tres es un log de lo que se hizo: los tres son entradas que se vuelven a ejecutar. Ninguno de los tres es un log de lo que se hizo: los tres son entradas que se vuelven a ejecutar.
Ésa es la diferencia entre mudar un servidor y poder mudarlo otra vez. Ésa es la diferencia entre mudar un servidor y poder mudarlo otra vez.
### 4.0 septies · El censo real de gioser, y dos huecos que destapó
Corrido contra `204.168.193.248` — que es `gioser.net`, identificado **por IP y no por hostname**,
porque `hostname` dice `momento`:
```
── servicios ── ── dominios (sonda DNS) ──
vivo 19 apunta-aca 12
⚠ no-declarado 18 ya-mudado 4
declarado-muerto 85 fosil-sin-dns 13
```
#### El HTTP toca al origen; el DNS no
Sondear los dominios desde la propia máquina disparó su `fail2ban` y la dejó incomunicada — por eso
el censo en local los salteaba, y quedaban 29 de 66 ítems **sin recomendación**. Pero lo que dispara
la jaula es el HTTP: `getent` le pregunta al DNS, no al servidor. Con `--probe-dns` se sondea sólo el
nombre, **sin una sola petición a gioser**, y eso ya contesta la pregunta que más pesa: cuáles siguen
apuntando acá y cuáles son fósiles. 29 de 29 clasificados; sólo **12 hay que reapuntar**.
Lo que el DNS solo no puede decir es si el backend contesta, así que esos quedan en `apunta-aca`
— clase propia, **no `vivo`**. Decir «vivo» sin haber pedido una página sería la respuesta falsa con
forma de respuesta que este censo existe para evitar.
#### Los fósiles quedaban fuera de la lista, y por eso sus datos viajaban
`entradas()` filtraba los `fosil-sin-dns` con el argumento de que un dominio sin DNS no necesita
ningún paso. Es cierto para el DNS y **falso para lo que arrastra**: son 13 de 29, y `terapeuta.ec`
tenía 279 M de contenido y su bloque en el servidor web. Fuera de la lista eran **invisibles**, así
que nadie decidía borrarlos y sus datos viajaban a la caja nueva por omisión — exactamente el «mudar
fósiles» que este plan existe para evitar, y contra la regla 2, que dice que lo que muere se nombra
**antes** de borrar nada.
### 4.0 sexies · El centro ENGANCHADO al plan ### 4.0 sexies · El centro ENGANCHADO al plan
El plan ya decía «cambiar de servidor web obliga a REESCRIBIR la configuración entera». Es cierto y El plan ya decía «cambiar de servidor web obliga a REESCRIBIR la configuración entera». Es cierto y
+23 -9
View File
@@ -222,8 +222,8 @@ def dominios(t):
return sorted(nombres) return sorted(nombres)
def sondear_dominio(d, ips_maquina): def sondear_dominio(d, ips_maquina, http=True):
"""DNS + HTTP desde donde corre el censo. Determinista, sin juicio. """DNS (+ HTTP opcional) desde donde corre el censo. Determinista, sin juicio.
⚠⚠ **NO se hace contra la máquina que uno está censando desde ella misma.** «Sólo lee» era falso: ⚠⚠ **NO se hace contra la máquina que uno está censando desde ella misma.** «Sólo lee» era falso:
leer POR RED tiene efectos. Medido el 2026-09-11 — censar gioser en modo `--local` disparó 29 leer POR RED tiene efectos. Medido el 2026-09-11 — censar gioser en modo `--local` disparó 29
@@ -234,7 +234,15 @@ def sondear_dominio(d, ips_maquina):
Y además es metodológicamente mejor: lo que interesa es **qué ve el mundo**, no qué ve la máquina Y además es metodológicamente mejor: lo que interesa es **qué ve el mundo**, no qué ve la máquina
de sí misma. Por eso la sonda se hace desde donde corre el censo —el operador o la caja nueva— y de sí misma. Por eso la sonda se hace desde donde corre el censo —el operador o la caja nueva— y
se SALTA cuando se censa en local, salvo `--probe` explícito.""" se SALTA cuando se censa en local, salvo `--probe` explícito.
── PERO EL DNS NO TOCA AL ORIGEN ───────────────────────────────────────────────────────────
Lo que dispara el fail2ban es el HTTP, no la resolución: `getent` le pregunta al DNS, no al
servidor. Con `--probe-dns` se sondea SÓLO el nombre, y eso ya contesta la pregunta que más
pesa en una mudanza — **cuáles de estos dominios siguen apuntando acá y cuáles son fósiles**.
Lo que el DNS solo NO puede decir es si el backend contesta, así que esos dominios quedan en
`apunta-aca`, que es una clase propia y no `vivo`: decir «vivo» sin haber pedido una página
sería exactamente la respuesta falsa con forma de respuesta que este censo existe para evitar."""
ip = "" ip = ""
try: try:
r = subprocess.run(["getent", "ahostsv4", d], capture_output=True, text=True, timeout=10) r = subprocess.run(["getent", "ahostsv4", d], capture_output=True, text=True, timeout=10)
@@ -242,8 +250,8 @@ def sondear_dominio(d, ips_maquina):
ip = r.stdout.split()[0] ip = r.stdout.split()[0]
except Exception: except Exception:
pass pass
code = "000" code = ""
if ip: if ip and http:
try: try:
r = subprocess.run(["curl", "-sk", "-o", "/dev/null", "-w", "%{http_code}", r = subprocess.run(["curl", "-sk", "-o", "/dev/null", "-w", "%{http_code}",
"--max-time", "8", f"https://{d}/"], capture_output=True, text=True, timeout=20) "--max-time", "8", f"https://{d}/"], capture_output=True, text=True, timeout=20)
@@ -254,6 +262,8 @@ def sondear_dominio(d, ips_maquina):
clase = "fosil-sin-dns" clase = "fosil-sin-dns"
elif ip not in ips_maquina: elif ip not in ips_maquina:
clase = "ya-mudado" # resuelve a otra máquina: el bloque de acá es fósil clase = "ya-mudado" # resuelve a otra máquina: el bloque de acá es fósil
elif not http:
clase = "apunta-aca" # resuelve acá; si SIRVE no se preguntó, y no se inventa
elif code.startswith(("2", "3")): elif code.startswith(("2", "3")):
clase = "vivo" clase = "vivo"
else: else:
@@ -467,7 +477,7 @@ def resumen(c):
pd = {} pd = {}
for d in c["dominios"]: for d in c["dominios"]:
pd.setdefault(d["class"], []).append(d["domain"]) pd.setdefault(d["class"], []).append(d["domain"])
for cl in ("vivo", "ya-mudado", "backend-caido", "fosil-sin-dns", "sin-sondear"): for cl in ("vivo", "apunta-aca", "ya-mudado", "backend-caido", "fosil-sin-dns", "sin-sondear"):
n = pd.get(cl, []) n = pd.get(cl, [])
if n: if n:
print(f" {cl:16} {len(n):3} {' '.join(sorted(n)[:6])}{'' if len(n) > 6 else ''}") print(f" {cl:16} {len(n):3} {' '.join(sorted(n)[:6])}{'' if len(n) > 6 else ''}")
@@ -488,6 +498,9 @@ def main():
ap.add_argument("--probe", action="store_true", ap.add_argument("--probe", action="store_true",
help="sondear los dominios por DNS+HTTP. Por defecto SÓLO con --host: hacerlo " help="sondear los dominios por DNS+HTTP. Por defecto SÓLO con --host: hacerlo "
"contra uno mismo puede disparar el fail2ban de la propia máquina") "contra uno mismo puede disparar el fail2ban de la propia máquina")
ap.add_argument("--probe-dns", action="store_true",
help="sondear SÓLO el DNS (sin HTTP): no toca al origen, así que es seguro "
"contra uno mismo. Dice qué dominios son fósiles y cuáles apuntan acá")
ap.add_argument("--no-probe", action="store_true", help="no sondear dominios") ap.add_argument("--no-probe", action="store_true", help="no sondear dominios")
ap.add_argument("--data-root", action="append", default=[], ap.add_argument("--data-root", action="append", default=[],
help="raíz de datos a medir (repetible). Default: las habituales") help="raíz de datos a medir (repetible). Default: las habituales")
@@ -516,7 +529,8 @@ def main():
e['paquete'] = dn.get(exe, '') e['paquete'] = dn.get(exe, '')
pts = puertos(t) pts = puertos(t)
nombres = dominios(t) nombres = dominios(t)
sondear = (a.probe or bool(a.host)) and not a.no_probe sondear = (a.probe or a.probe_dns or bool(a.host)) and not a.no_probe
http_ok = not a.probe_dns
if nombres and not sondear: if nombres and not sondear:
print(f"{len(nombres)} dominio(s) declarados, SIN sondear: censando en local, y pedirle", print(f"{len(nombres)} dominio(s) declarados, SIN sondear: censando en local, y pedirle",
file=sys.stderr) file=sys.stderr)
@@ -528,9 +542,9 @@ def main():
else: else:
doms = [] doms = []
for i, d in enumerate(nombres): for i, d in enumerate(nombres):
if i: if i and http_ok:
time.sleep(0.5) # no parecer un escaneo ante el propio fail2ban del objetivo time.sleep(0.5) # no parecer un escaneo ante el propio fail2ban del objetivo
doms.append(sondear_dominio(d, set(ident["ips"]))) doms.append(sondear_dominio(d, set(ident["ips"]), http_ok))
raices = a.data_root or ["/var/www", "/var/lib", "/srv", "/opt", "/home", "/store"] raices = a.data_root or ["/var/www", "/var/lib", "/srv", "/opt", "/home", "/store"]
dat = datos(t, raices) dat = datos(t, raices)
censo = { censo = {
+18 -2
View File
@@ -76,8 +76,13 @@ def entradas(censo):
if s.get("class") != "declarado-muerto": if s.get("class") != "declarado-muerto":
out.append(("servicio", s["name"], s)) out.append(("servicio", s["name"], s))
for d in censo.get("dominio", []): for d in censo.get("dominio", []):
if d.get("class") != "fosil-sin-dns": # ⚠ Los `fosil-sin-dns` ENTRAN. Antes se filtraban con el argumento de que un dominio sin DNS
out.append(("dominio", d["domain"], d)) # no necesita ningún paso — y es cierto para el DNS, pero no para lo que arrastra: en gioser
# son 13 de 29, y `terapeuta.ec` tenía 279 M de contenido y su bloque en el caddy. Sacarlos
# de la lista los volvía INVISIBLES, así que nadie decidía borrarlos y sus datos viajaban a
# la caja nueva por omisión. Es exactamente el «mudar fósiles» que este plan existe para
# evitar, y la regla 2 dice que lo que muere se nombra ANTES de borrar nada.
out.append(("dominio", d["domain"], d))
for d in censo.get("datos", []): for d in censo.get("datos", []):
out.append(("datos", d["path"], d)) out.append(("datos", d["path"], d))
return out return out
@@ -333,6 +338,17 @@ def recomendar(tipo, e, datos_que_mueren=()):
return "muere", f"su DNS ya apunta a {e.get('dns','otra máquina')}: el bloque de acá es fósil" return "muere", f"su DNS ya apunta a {e.get('dns','otra máquina')}: el bloque de acá es fósil"
if c == "backend-caido": if c == "backend-caido":
return "muere", f"resuelve acá pero devuelve {e.get('http','5xx')}: no hay backend" return "muere", f"resuelve acá pero devuelve {e.get('http','5xx')}: no hay backend"
if c == "apunta-aca":
# El DNS dice que apunta acá; si SIRVE no se preguntó. Recomendar «muda» sería tratar el
# silencio como una respuesta afirmativa, que es el error que este censo evita en todos
# los demás lugares. Se recomienda mudar porque el DNS hay que reapuntarlo igual, pero
# se DICE lo que no se miró.
return "muda", (f"su DNS apunta acá ({e.get('dns','?')}), así que hay que reapuntarlo. "
"⚠ NO se sondeó por HTTP: si el backend estaba caído, esto muda un fósil")
if c == "fosil-sin-dns":
return "muere", ("no resuelve en ningún DNS: nadie puede llegar a él. Revisá si dejó "
"contenido en disco o un bloque en el servidor web — eso sí viaja si "
"nadie lo borra")
if c == "sin-sondear": if c == "sin-sondear":
return "", "sin sondear (censado en local): hace falta mirarlo desde afuera" return "", "sin sondear (censado en local): hace falta mirarlo desde afuera"
return "", "" return "", ""