> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…
test-atuq-ruteo: los puertos fijos daban un fallo del producto que no existía
Esta prueba dio «NO concluyente — la pestaña sin contenedor no salió directa»
dos veces seguidas, y la causa no tenía nada que ver con atuq: **otro frente de
esta misma máquina tenía levantado un `python3 -m http.server 8099`** desde hacía
dos horas. La oreja del destino no podía atarse, no llegaba nada, y el informe
publicaba un fallo inexistente. En un repo que comparten varios agentes, un
puerto fijo es estado compartido sin dueño.
Y había un segundo bug que es el que lo hizo dañino: el `bind` vivía DENTRO del
hilo de la oreja, donde `fatal()` no puede matar el proceso — `SystemExit` en un
hilo secundario sólo termina ese hilo. Así que la prueba imprimía «el puerto está
ocupado, la medición no valdría nada» y **seguía adelante hasta publicar un
veredicto**. Un guardián que avisa de que no puede medir y mide igual es peor que
uno que no mide.
Ahora las dos orejas se atan en el hilo principal, con el puerto 0: lo elige el
kernel. Si no hay puertos, no hay prueba.
Con eso, y contra el atuq de hoy (d36ae188, el del arreglo del LD_LIBRARY_PATH):
positivo destino 40843 · proxy 38503 → GET /directo al destino, SOCKS5 al proxy
control destino 40917 · proxy 37113 → las dos directas, cero al proxy
O sea que el ruteo por contenedor del §6.8 sigue en pie y no lo rompió nada de
hoy.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NNFzrqFBEt6W7maGJKf2Xs
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"])