diff --git a/docs/29-mudanza.md b/docs/29-mudanza.md index 238b99f1..3ef26526 100644 --- a/docs/29-mudanza.md +++ b/docs/29-mudanza.md @@ -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. É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 El plan ya decía «cambiar de servidor web obliga a REESCRIBIR la configuración entera». Es cierto y diff --git a/scripts/mudanza/censar.py b/scripts/mudanza/censar.py index 15249f3d..1ced5c69 100755 --- a/scripts/mudanza/censar.py +++ b/scripts/mudanza/censar.py @@ -222,8 +222,8 @@ def dominios(t): return sorted(nombres) -def sondear_dominio(d, ips_maquina): - """DNS + HTTP desde donde corre el censo. Determinista, sin juicio. +def sondear_dominio(d, ips_maquina, http=True): + """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: 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 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 = "" try: 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] except Exception: pass - code = "000" - if ip: + code = "" + if ip and http: try: r = subprocess.run(["curl", "-sk", "-o", "/dev/null", "-w", "%{http_code}", "--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" elif ip not in ips_maquina: 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")): clase = "vivo" else: @@ -467,7 +477,7 @@ def resumen(c): pd = {} for d in c["dominios"]: 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, []) if n: 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", 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") + 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("--data-root", action="append", default=[], help="raíz de datos a medir (repetible). Default: las habituales") @@ -516,7 +529,8 @@ def main(): e['paquete'] = dn.get(exe, '') pts = puertos(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: print(f" ⚠ {len(nombres)} dominio(s) declarados, SIN sondear: censando en local, y pedirle", file=sys.stderr) @@ -528,9 +542,9 @@ def main(): else: doms = [] 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 - 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"] dat = datos(t, raices) censo = { diff --git a/scripts/mudanza/planear.py b/scripts/mudanza/planear.py index f111ab6f..093ea722 100755 --- a/scripts/mudanza/planear.py +++ b/scripts/mudanza/planear.py @@ -76,8 +76,13 @@ def entradas(censo): if s.get("class") != "declarado-muerto": out.append(("servicio", s["name"], s)) for d in censo.get("dominio", []): - if d.get("class") != "fosil-sin-dns": - out.append(("dominio", d["domain"], d)) + # ⚠ Los `fosil-sin-dns` ENTRAN. Antes se filtraban con el argumento de que un dominio sin DNS + # 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", []): out.append(("datos", d["path"], d)) 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" if c == "backend-caido": 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": return "", "sin sondear (censado en local): hace falta mirarlo desde afuera" return "", ""