La primera version dejaba afuera al guardian de notificaciones (`dunst-headless.sh --via-atuq`) con
la excusa de que vive en el arnes de sway, y lo escribia en la cabecera «para que su ausencia no se
lea como cobertura». Eso dura tres dias: al cuarto la lista ES la cobertura y el parrafo no lo lee
nadie. Corrido aparte: verde en 180 s. Entra.
Es ademas el que mide lo que ningun auditor de ELF puede ver: una pagina llama `new
Notification(...)`, `libxul` hace `dlopen("libnotify.so.4")` —que no es NEEDED de nada—, el bus
activa dunst y dunst DIBUJA. Cinco piezas y la unica forma de saber que estan las cinco es verlo.
Para que entrara, la lista pasa a llevar el COMANDO de cada guardian en vez de su nombre de fichero:
lo que lo tenia afuera era el despacho por extension, no una decision.
Corrida entera de los diecinueve sobre `b3:e556024b`: **17 verdes, 2 rojos, 32,2 min**, que es
exactamente lo que la cabecera predice — y la cabecera es el control, no un adorno.
Los caros: sct 242 s, archivo semantico 238 s, ia 182 s, instalacion 180 s, notificaciones 180 s.
Los cuatro primeros salen en 5 s y no tocan el navegador.
Del `access.log`, sin ambigüedad. La IP del usuario (`45.234.61.160`, autenticada como `sergio`,
2709 peticiones) tuvo tres silencios: **93,2 min (11:59:37 → 13:32:49 UTC)**, 50,1 min (10:44:58 →
11:35:06) y 10,0 min (11:45:06 → 11:55:05). Los tres son el `timeout` de 10 minutos del ban
disparándose una y otra vez: cada vez que volvía, su primera ráfaga lo baneaba de nuevo. El corte
grande terminó exactamente cuando vacié los sets.
🧨 Y LA EXENCIÓN QUE TENÍA NO SERVÍA: la ACL histórica de squid exime `45.234.60.0/24` y él entra
desde `45.234.61.160` — **una red de al lado**. Parecía cubierto y no lo estaba. La pertenencia se
MIDE (`ip in red`), no se deduce del parecido del prefijo.
DECISIÓN DE FONDO: en un servicio que YA AUTORIZA no va límite por tasa. El uso de esta caja es
INTENSIVO —varias sesiones de agente en paralelo, cada una abriendo túneles a la vez— y un límite
calibrado para «una persona normal navegando» no protege de nada: el atacante ajusta su ritmo, el
usuario no puede trabajar más despacio. Squid ya pide usuario y contraseña y tiene su ACL. El ban del
puerto queda apagado (60000/min, ráfaga 1000) y contra un flood queda el tope GLOBAL
`syn_nuevas_por_seg`, que no distingue usuarios ni banea a nadie por ser rápido.
Control tras aplicar: su túnel pasando —`45.234.61.160 … CONNECT api.anthropic.com:443 sergio`— y
`45.234.61.160 ∈ 45.234.60.0/23` comprobado con aritmética de redes, no a ojo.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Preguntaron si el proxy estaba caído. NO lo estaba: el ente llevaba horas corriendo con ↻ 0 y
contestando 407. Lo caído era el ACCESO — el cortafuegos de ayer baneaba a los autorizados.
La prueba en una línea: **154.197.1.2, una IP que está en la ACL de squid (gente con contraseña),
apareció en `@ban4`**, con el contador del ban en 38.246 paquetes tirados.
LA CAUSA NO ES LA TASA, ES LA RÁFAGA: `limit rate over 300/minute burst 5 packets` tolera cinco
conexiones por encima del promedio, y un navegador o un agente detrás de un proxy abre DECENAS de
CONNECT en el mismo instante aunque su promedio sea bajísimo. Y como @ban4 es UN SOLO set consultado
antes que todo, caer por el puerto del proxy te tira también el web y el git.
⚠ El tipo ya traía el campo `rafaga` (default 5) con un comentario que dice «para un servicio web va
alto (100-200)». Otra vez leí el .ron de EJEMPLO y no el TIPO.
Hecho, en orden de urgencia: flush de los sets (servicio restaurado en el acto) · los 21 IPs y 5
redes de la ACL de squid a `confiables` · `rafaga` por servicio según su clientela (ssh 5, git 20,
web 100, proxy 200) · proxy a 1200/min con ban de 60 s, que es red de contención contra un flood y
no control de uso. Control después: CERO autorizados en el set, 24 baneados (escáneres) y tráfico
real pasando —`154.194.14.37 … CONNECT api.deepseek.com:443 sigma`, 298 K tunelados—.
⚠⚠ Y un fallo de método que casi lo tapa: **`nft -f` SUMA a la tabla existente**. Cargué el reglaset
corregido encima del viejo y quedaron 30 reglas con las VIEJAS ADELANTE, así que los `confiables`
nuevos no se evaluaban nunca. Por eso `cortafuegos apply-input` borra la tabla antes de cargar.
La lección: un cortafuegos correcto y uno usable no son lo mismo, y la diferencia NO se ve al
aplicarlo. El control que lo habría cazado es el que no corrí: una ráfaga de conexiones desde una IP
que no esté en `confiables`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
La prueba de la caja recién instalada —`cargo build` en un proyecto nuevo, sin flags ni rutas a
mano— falló con `linker `cc` not found`, y destapó que la pieza que había empaquetado era falsa:
**cargo NO LEE `/etc/cargo/config.toml`**. Sólo lee `$CARGO_HOME/config.toml`, los
`.cargo/config.toml` del proyecto y sus padres, y lo que se le pase con `--config` — que es lo que yo
venía pasando a mano en cada prueba, y por eso «funcionaba».
Y el arreglo no era mover el fichero: **el que enlaza es `rustc`, y rustc no lee la config de
cargo**. Las flags van DENTRO del compilador, en el spec del triple:
· `linker = "ld.lld"` + `linker_flavor = Gnu(Cc::No, Lld::Yes)` — sin esto rustc busca `cc`, que una
caja takana no tiene.
· `crt_static_default = true` — sin esto enlaza dinámico y pide `-lgcc`, que no existe (la distro se
construye con zig, que trae compiler-rt).
· `crt_static_allows_dylibs = true` — su pareja obligatoria: sin ella `crt-static` apaga los dylibs
y con ellos los PROC-MACROS (serde_derive, clap_derive, las 44 recetas `cargo-*` con `derive`).
El compilador en sí sigue dinámico: el `bootstrap.toml` fija `crt-static = false` para el build, y
eso gana sobre el default del spec.
⇒ `cargo-config` (receta, árbol y copia en scripts/) se van enteros: un paquete que instala un
fichero que nadie abre es peor que no tenerlo.
Y de paso, `lld21` instala los cuatro alias como SYMLINKS y no como copias: cmake los duplicaba, 5 ×
96 M = 478 M de artefacto para 97 M de enlazador. Ahora 108 M, y `ld.lld` sigue despachando por
`argv[0]`, que es como upstream lo diseñó.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sale `rust-toolchain-bin` —799 M de bytes AJENOS sellados tal cual (`foreign`)—, que era el escalón 1
del SDD 31 y cumplió su función: con él se construyó el escalón 2, y el escalón 2 ya compila,
enlaza y corre.
Entran TRES, que son el mismo hecho («esta caja compila Rust») partido en piezas:
· `rust` (353 M) — el compilador y cargo, de la granja, desde fuente.
· `lld21` — el enlazador. NO es opcional: una caja takana no tiene `cc`, y sin enlazador `rustc`
compila objetos y no produce un ejecutable.
· `cargo-config` — receta nueva que publica `/etc/cargo/config.toml` con las tres flags que unen a
los dos. Sin ellas `cargo build` muere con «linker `cc` not found»: el toolchain estaría completo
y MUDO, que es [[subcomando-sin-driver]] otra vez. Es config de SISTEMA y no de usuario a
propósito — un `~/.cargo/config.toml` funcionaría para quien lo escribió y no para el siguiente.
⚠ Y `rust` se re-hashea otra vez (702094a8 → fe277b32) por una línea que descubrió el control de
PATH: **x.py instala en `/usr/local` por defecto, y el PATH de una caja takana es
`/bin:/usr/bin:/sbin:/usr/sbin`** — medido en la caja. O sea que el perfil habría proyectado un
`rustc` perfecto que nadie puede invocar: «existe ≠ se encuentra». Ahora `[install] prefix = "/usr"`,
como el resto del corpus.
Control previo que sí pasó: `rustc` y `cargo` corren SIN `LD_LIBRARY_PATH` — su `RUNPATH` es
`$ORIGIN/../lib`, así que la proyección del artefacto basta.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Un cambio de dos líneas en `llvm21` re-selló tres artefactos y los molió la granja sola, en orden,
por dependencia: llvm21 95956a16→bd4a4094, lld21 27be53c1→95a4794b, rust 015a07fb→702094a8.
llc --version Stack dump: + segfault → LLVM 21.1.2, Optimized build
opt --version ídem → LLVM 21.1.2
ld.lld «not built with zlib» → ENLAZA
rustc --version → 1.97.0 (built from a source tarball)
--print target-list → x86_64-alpine-linux-musl
cargo build + correr → 0,16 s y el binario habla
La config del sitio pasa a usar `lld` (más rápido); el `ld` de binutils queda documentado como
alternativa que también funciona.
LA REGLA QUE SE GANÓ SU LUGAR: **`link = "static"` en el encabezado decide cosas que la receta no
dice**. Van tres, todas medidas: apagó los threads de openssl, le mintió a libtool, y acá rompió los
registros estáticos de LLVM (`cl::opt`, `TargetRegistry`) porque el `static-pie` descarta sus
constructores globales. Con C++ grande la postura por defecto pasa a ser `dynamic` y medirlo — y el
control no es que selle: es CORRER UNA HERRAMIENTA del artefacto.
Y el corolario de por qué nadie lo vio en meses: el único consumidor de llvm21 era rust, que usa
`llvm-config` y las `.a`, nunca una herramienta. **Un artefacto puede estar roto en todo lo que nadie
usa** — como los 44 `cargo-*` sin cargo y los 23 binarios sin cargador.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
$ cargo build --offline
Compiling prueba v0.1.0 … Finished in 0.44s
$ ./target/debug/prueba
cargo, rustc y ld: los tres nuestros
`cargo` y `rustc` son los que construyó la granja; el enlazador es el `ld` de nuestro `binutils`.
EL MURO 8 SE CERRÓ SIN `lld`. La cadena, que es lo que vale: `-C linker-flavor=ld.lld` → «linker
`lld` not found» (el build con LLVM externo no genera rust-lld); `rust.lld = true` → el bootstrap lo
RECHAZA; `lld21` propio → crasheaba con cualquier entrada; `lld21` con zlib → **no se puede encender
desde ahí**, porque `LLVMConfig.cmake` dice `LLVM_ENABLE_ZLIB 0` y el build standalone lo hereda.
Lo que faltaba NO era un enlazador: era decirle a rustc que enlace en ESTÁTICO. Con `+crt-static`,
rustc usa su propio libunwind/compiler_builtins y deja de pedir `-lgcc` — que es lo que rompía el
intento con `ld`, porque la distro se construye con zig (compiler-rt) y libgcc no existe. Y estático
es el modo natural de musl.
⇒ `scripts/servidor/cargo-config.toml` (instalado en `/etc/cargo/config.toml`) cierra el pendiente
«config del sitio» del §5, con las tres flags y el porqué de cada una.
🧨 Y `lld21` destapó algo que nadie había visto: **las 79 herramientas de `llvm21` están ROTAS**.
`llc --version` crashea igual —también en el worker— porque salen `static-pie` y el enlace estático
descarta los constructores globales de los que dependen `cl::opt` y `TargetRegistry`. Funciona lo
que no los necesita (`llvm-ar`, `llvm-config`) y revienta lo que sí; nadie lo notó porque `rust` usa
`llvm-config` y las `.a`, nunca una herramienta. Queda como deuda DECLARADA: arreglarlo (o encender
zlib) re-hashea `llvm21` y con él `rust`. Hoy no bloquea nada.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
353 M, construido por la granja desde `llvm21` del corpus y el stage0 del lab. Los controles, en la
caja (que sí tiene cargador musl; el worker no):
rustc --version → 1.97.0 … (built from a source tarball) ← NUESTRO
rustc --print target-list → x86_64-alpine-linux-musl ← EL PARCHE, DENTRO
cargo --version → 1.97.0 … (built from a source tarball)
Y compila: `--emit=obj` da un objeto de 3,5 K y `--crate-type=rlib` una biblioteca, sin enlazador.
🧱 MURO 8, YA MEDIDO: el compilador no trae con qué ENLAZAR. En una caja sin `cc` —cualquier takana
que no sea un hub— `-C linker-flavor=ld.lld` da «linker `lld` not found» y el `ld` de binutils muere
con «cannot find -lgcc». La causa estaba en una línea del log que parece informativa:
skipping llvm-tools (x86_64-alpine-linux-musl): external LLVM
Al usar LLVM externo —que es lo que queremos— x.py se salta las herramientas de LLVM, y `rust-lld`
es una de ellas. El prebuilt oficial sí la trae, y por eso el escalón 1 enlazaba. Y `llvm21` no
sirve de repuesto: NO publica ningún `lld` (medido sobre sus 1,6 G).
Dos salidas: (a) `[rust] lld = true`, que construye rust-lld desde el llvm-project vendoreado y deja
el toolchain autosuficiente; (b) publicar `lld` desde llvm21 y fijar el linker en un
`/etc/cargo/config.toml` del sitio.
Y la lección de método: el artefacto SELLÓ, reproduce y NO ENLAZA. Es [[subcomando-sin-driver]] en
el corazón del toolchain. Un `build ok` no es la prueba: la prueba es el objeto en el disco.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`table inet tawasuyu_input` con policy drop, 15 reglas de puerto, y el set `ban4` llenandose solo:
a los pocos minutos, 27 IPs baneadas y paquetes tirados por el contador. El reemplazo de fail2ban
funcionando sobre trafico real, dentro del kernel y sin daemon.
🧨 EL PRIMER BANEADO FUI YO, EN SEGUNDOS. Aplicada la primera version: ssh y https bien, y `http`,
proxy y git-ssh muertos; al minuto la caja entera dejo de contestarme. La causa no es una regla mal
escrita: **el set @ban4 es UNO SOLO para todos los servicios** y `ip saddr @ban4 drop` esta antes
que todo, asi que pasarse de tasa en UN puerto te tira en TODOS — y el `nuevas_por_min: 5` del
puerto de administracion es exactamente lo que me pasa a mi, porque cada comando remoto es una
conexion nueva. Lo devolvio el INTERRUPTOR DE HOMBRE MUERTO (un `nft flush ruleset` programado a
180 s que sólo se desarma si la verificacion externa sale bien). Sin consola serie, esa es la
diferencia entre un susto y un rescue.
⚠ Y el tipo ya lo avisaba: `PoliticaEntrada` tiene un campo `confiables` cuyo comentario describe
palabra por palabra lo que me paso. Copie el .ron de EJEMPLO en vez de leer el TIPO. La cura fue una
linea con el hub y el worker, que se aceptan ANTES de la logica de tasa.
⚠ `nft -c` rechazo 5 reglas antes de aplicar nada: `ct count over N` es CONFIG_NFT_CONNLIMIT y este
kernel no lo trae. Quedan COMENTADAS y no borradas en el fichero que se aplica — lo que la politica
declara y el kernel no puede tiene que verse. El simbolo ya entro a la receta del kernel.
Y para que sobreviva al reinicio, `nftables` declara su [[service]] con `lifecycle = "oneshot"`:
cargar un reglaset no es un daemon, es un acto. Va PRIMERO en el genesis — entre que la red esta y
que las reglas cargan, la caja esta abierta. Probado con `nft flush ruleset` + `arjectl start`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`rust` en la cola del worker llegó MUCHO más lejos: compiló las 386 crates del stage1 y murió al
empezar `Building stage1 library artifacts (stage1 -> stage1)`:
error: error loading target specification: could not find specification for target
"x86_64-alpine-linux-musl"
Es la OTRA MITAD del muro 4 y sólo se ve en el stage 2: usar el triple de Alpine funciona mientras
el compilador que manda es el del LAB —Alpine le PARCHEA esa especificación a su rustc— y deja de
funcionar en cuanto toma el relevo el rustc recién construido, que es upstream puro y no la conoce.
El triple que hace falta para ARRANCAR el build es el que el PRODUCTO del build no tiene.
Tres salidas, con la elegida: parchear `rustc_target` para registrar el triple (lo que hace Alpine;
queda DENTRO del compilador y vale en todos los stages). El `RUST_TARGET_PATH` + JSON es más barato
pero un target por JSON como HOST es terreno que rustc no soporta bien. Y volver al canónico no
tiene candidato a stage0: el prebuilt oficial trae esa std pero no puede compilar proc-macros.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`nft list ruleset` contesta vacío y sin error: nf_tables funciona por primera vez en una máquina
takana. Comprobado con el mecanismo que reemplaza a fail2ban y no con una regla de adorno: un set
`flags dynamic` con `add @ban4 { ip saddr timeout 10s limit rate over 5/minute } drop`, aceptado por
el kernel y releído tal cual.
LO QUE EL REINICIO PROBÓ DE PASO, y vale más que el kernel: la Semilla levantó los OCHO entes sola,
todos con ↻ 0 — incluidos los DOS enjaulados en qorpa, que arrancan desde el genesis con su
newuidmap, su overlay y su harkaq igual que un binario nativo. Todo lo que se editó en
/ente/seed.card.json a lo largo del día era hasta ahora una declaración SIN PROBAR: un reinicio es
el único examen que toma. Desde fuera, al minuto: siete dominios en 200, el proxy en 407 y
`git ls-remote` por SSH:2345 devolviendo el commit. Cero intervención.
⚠ `reboot` NO reinicia una caja con arje-zero: le pide a PID 1 que lo haga y arje-zero no implementa
ese protocolo — el comando vuelve, no dice nada, y la máquina sigue arriba (`up 4 days` después de
«reiniciar»). Funciona `reboot -f`, que llama a reboot(2) saltándose al init. Falta la pieza: un
init que gobierna producción tiene que atender un apagado ordenado.
🧨 Y CASI REINICIO EL HUB POR UN BACKTICK: el aviso decía «no atiende el `reboot` normal» dentro de
comillas DOBLES, y la shell ejecutó lo de entre backticks — `reboot`, en gioser. No pasó nada porque
la sesión no es root y OpenRC contestó «you must be root». Es la TERCERA vez en el día que un
backtick dentro de un texto se ejecuta; las otras dos sólo se comieron un comentario. La regla deja
de ser de estilo: nunca un backtick dentro de comillas dobles en una shell.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`atuq` es `source.dir` entero, asi que el commit de la boveda de esta manana (`6f0ba974`) movio el
ArtifactHash y dejo el artefacto vigente SIN SELLAR. Durante esas nueve horas ningun guardian podia
medir: se niegan a correr contra uno que no sea el vigente, que es lo correcto. Pero un guardian que
se niega solo grita CUANDO alguien lo corre, y nadie lo corria — el repo se veia sano y la unica
senal era un `hash --check` que nadie tenia motivo para teclear.
Resellado en 3,7 s (es derivado y el firefox vigente estaba en el store) y corridos los dieciocho
sobre `b3:e556024b`: 16 en verde y 2 en ROJO, los dos por la MISMA causa —los tres verbos de la
boveda del §7.quinquies—. O sea que la unidad 12 no rompio nada de al lado, que es lo que habia que
saber antes de seguir: toco `atuq.cfg`, la politica y el empaquetado de extensiones.
`scripts/test-atuq-suite.sh` deja eso en un comando, en serie (cuatro nucleos sin swap; dos
navegadores a la vez es un OOM) y cada guardian a SU fichero, porque dos corridas sobre el mismo log
se mezclan y el resultado parece un pase. Lo primero que mira es el sello y NO corre nada si falta:
verificado contra un store vacio, sale por ahi y devuelve 1.
Dos cosas quedan escritas como control y no como adorno:
· el numero de rojos esperados va en la cabecera. Un cuadro entero en verde puede ser un producto
sano o un runner que no corre nada, y se ven igual;
· esa cabecera decia «un rojo» —prediccion escrita antes de correr— y la primera corrida dijo dos:
el guardian de coherencia de la boveda lleva el mismo chequeo del cable adentro.
Coste medido en gioser: ~29 min. Los cuatro primeros salen en 10 s y no tocan el navegador.