diff --git a/scripts/test-atuq-ruteo.py b/scripts/test-atuq-ruteo.py index 84b184ef..185e18d5 100755 --- a/scripts/test-atuq-ruteo.py +++ b/scripts/test-atuq-ruteo.py @@ -7,13 +7,17 @@ Es la afirmación central del SDD 26 §6.8, y la que la v0.5 dejó sin probar. Esta prueba **no le pregunta nada al navegador**: le pone dos oídos en la red y mira a cuál de los dos llama. - 127.0.0.1:8099 el destino de verdad; contesta un HTTP 200 - 127.0.0.1:9099 el «proxy» de «Banco»; no habla SOCKS, sólo anota quién lo saludó + destino el de verdad; contesta un HTTP 200 + proxy el de «Banco»; no habla SOCKS, sólo anota quién lo saludó + +Los dos puertos los elige el KERNEL (bind al 0) y se atan en el hilo principal antes de empezar: +fijos, chocaban con lo que otro frente tuviera levantado en la misma máquina, y el choque se leía +como un fallo del navegador. Dos pestañas piden la MISMA url. Si el ruteo funciona: - · la pestaña SIN contenedor ⇒ llega un `GET /directo` al 8099 - · la pestaña en «Banco» ⇒ llega un saludo SOCKS5 (`\\x05…`) al 9099, y NADA al 8099 + · la pestaña SIN contenedor ⇒ llega un `GET /directo` al DESTINO + · la pestaña en «Banco» ⇒ llega un saludo SOCKS5 (`\\x05…`) al PROXY, y NADA al destino EL CONTROL NEGATIVO NO ES UN EXTRA. Con `--negative-control` no se le pone proxy a «Banco» y se exige el resultado CONTRARIO: que las dos pestañas salgan directas. Una prueba que nunca falló no se @@ -21,7 +25,7 @@ sabe si sirve — sin este modo, una prueba rota que siempre dice «ok» se ve i funciona. Los dos modos tienen que correr: el positivo caza que el ruteo se apague, el negativo caza que la prueba se haya vuelto ciega. -**El saludo SOCKS5 en el 9099 es la prueba.** Sólo puede aparecer si Gecko decidió hablar con un +**El saludo SOCKS5 en el puerto del proxy es la prueba.** Sólo puede aparecer si Gecko decidió hablar con un proxy para esa petición, y el único que se lo pudo indicar es el `proxy.onRequest` de `extensions/proxy/` mirando el `cookieStoreId`. La pestaña sin contenedor **no es decorativa**: es el control. Sin ella, «todo fue por el proxy» y «el ruteo anda» se verían iguales, igual que dos @@ -54,8 +58,13 @@ RFS = os.environ.get("RFS", "/mnt/cosecha/escritorios/atuq-rootfs") ALPINE = os.path.join(ROOT, ".dev-fs/alpine") HAMMER = os.path.join(ROOT, "target/release/hammer") STORE = os.path.join(ROOT, "store") -PUERTO_DESTINO, PUERTO_PROXY = 8099, 9099 -URL = "http://127.0.0.1:%d" % PUERTO_DESTINO +# ── LOS PUERTOS LOS ELIGE EL SISTEMA, Y NO ES UN DETALLE ────────────────────────────────────── +# Estaban fijos en 8099/9099 y el 2026-09-07 esta prueba dio «NO concluyente» durante media hora por +# una razón que no tenía nada que ver con atuq: **otro agente de la misma máquina tenía levantado un +# `python3 -m http.server 8099`**. La oreja del destino no podía atarse, no llegaba nada, y el +# informe decía que la pestaña sin contenedor no había salido directa — o sea, un fallo del producto +# que no existía. En un repo que comparten varios frentes, un puerto fijo es estado compartido sin +# dueño. Ahora se pide el 0 y el kernel da uno libre. visto = {"destino": [], "proxy": []} @@ -80,14 +89,23 @@ def artefacto(receta, nombre): return d -def oido(puerto, etiqueta, responder, hasta): +def oreja(etiqueta): + """Un socket ATADO a un puerto libre, elegido por el kernel. Se ata acá, en el hilo principal. + + Antes se ataba dentro del hilo, y ahí `fatal()` no puede matar el proceso: `SystemExit` en un + hilo secundario **sólo termina ese hilo**. El error se imprimía y la prueba seguía adelante + midiendo un puerto que nunca escuchó — que es peor que no medir, porque publica un veredicto.""" s = socket.socket() s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) try: - s.bind(("127.0.0.1", puerto)) + s.bind(("127.0.0.1", 0)) except OSError as e: - fatal(f"el puerto {puerto} está ocupado ({e}) — la medición no valdría nada") + fatal(f"no pude abrir la oreja de {etiqueta} ({e}) — la medición no valdría nada") s.listen(8) + return s, s.getsockname()[1] + + +def oido(s, etiqueta, responder, hasta): s.settimeout(1.0) while time.time() < hasta: try: @@ -181,6 +199,12 @@ def main(): atuq = artefacto("atuq", "atuq") print(f"artefacto: {os.path.basename(atuq)}") + # Las dos orejas se atan ANTES de tocar nada: si no hay puertos, no hay prueba. + sock_destino, PUERTO_DESTINO = oreja("destino") + sock_proxy, PUERTO_PROXY = oreja("proxy") + URL = "http://127.0.0.1:%d" % PUERTO_DESTINO + print(f" orejas: destino {PUERTO_DESTINO} · proxy {PUERTO_PROXY}") + tmp = tempfile.mkdtemp(prefix="atuq-ruteo-") perfil = os.path.join(tmp, "salida", "perfil") os.makedirs(os.path.join(tmp, "salida")) @@ -196,8 +220,8 @@ def main(): json.dump(pol, open(os.path.join(pol_dir, "policies.json"), "w"), indent=2) hasta = time.time() + 240 - for p, et, resp in ((PUERTO_DESTINO, "destino", True), (PUERTO_PROXY, "proxy", False)): - threading.Thread(target=oido, args=(p, et, resp, hasta), daemon=True).start() + for sk, et, resp in ((sock_destino, "destino", True), (sock_proxy, "proxy", False)): + threading.Thread(target=oido, args=(sk, et, resp, hasta), daemon=True).start() time.sleep(0.5) print("== pasada 1: crear el perfil (la política crea los contenedores)") @@ -238,9 +262,9 @@ def main(): visto["proxy"].clear() correr(tmp, atuq, 45) - print(f" al DESTINO (8099) llegaron {len(visto['destino'])}: " + print(f" al DESTINO ({PUERTO_DESTINO}) llegaron {len(visto['destino'])}: " f"{[d[:24] for d in visto['destino']]}") - print(f" al PROXY (9099) llegaron {len(visto['proxy'])}: " + print(f" al PROXY ({PUERTO_PROXY}) llegaron {len(visto['proxy'])}: " f"{[d[:24] for d in visto['proxy']]}") directo = any(b"/directo" in d for d in visto["destino"])