tawasuyu

sergio/takana

> 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…

TOML 1534Shell 199Rust 109Markdown 993713 Definiciones

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

7d0dc72de03b00bd2010415f8b5df3c28732be69 · por Sergio · (hace 1 mes) · padre b12256eb2 · ver el árbol en este commit

Delta semántico · 3

scripts/test-atuq-ruteo.py

  • cambió por dentro fn main
  • cambió de firma fn oido
  • nueva fn oreja

1 archivos · +38 −14

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"])