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:
@@ -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
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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 "", ""
|
||||||
|
|||||||
Reference in New Issue
Block a user