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
This commit is contained in:
+38
-14
@@ -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"])
|
||||
|
||||
Reference in New Issue
Block a user