$ 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>
El primer `lld21` selló, arrancó y se moría en cuanto hacía algo: `--version` y `--help` contestaban
bien, y CUALQUIER enlace —incluso uno que sólo debía dar un error, como un fichero de entrada
inexistente— terminaba en SIGSEGV con el banner de LLVM y un «Stack dump:» vacío.
🧨 Y no era culpa de esta receta: **las 79 herramientas que publica `llvm21` tienen el mismo defecto**
—`llc --version` también crashea, y también en el worker— porque salen `static-pie` y el enlace
estático descarta los constructores globales de los que dependen los registros de LLVM
(`cl::opt`, `TargetRegistry`). Lo que funciona es lo que no los necesita (`llvm-ar`, `llvm-config`),
y por eso nadie lo había visto: `rust` usa `llvm-config` y las `.a`, nunca una herramienta.
⇒ `link = "dynamic"` en esta receta (como `openssl-threads`, y por lo mismo: el flag global decide
cosas que la receta no dice). Control en los dos sentidos: con `static`, SIGSEGV; con `dynamic`,
`ld.lld: error: cannot open /no/existe.o: No such file or directory` — un error limpio.
Y el segundo, ya enlazando de verdad con nuestro rustc:
lld: error: …/self-contained/crtn.o:(.debug_line) is compressed with ELFCOMPRESS_ZLIB,
but lld is not built with zlib support
Los objetos `crt*` del sysroot de rust llevan las secciones de depuración COMPRIMIDAS. Para `llvm21`
apagar zlib era gratis; para el ENLAZADOR no lo es. ⇒ `-DLLVM_ENABLE_ZLIB` encendido y `zlib` a las
deps — copiar los flags del hermano sin preguntarse qué hace cada uno es lo que lo causó.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Medido hoy al encolar `lld21` junto a `rust`: el ciclo decía «2 recetas en recipes/incoming» y
construía UNA. La causa está en el filtro de «ya sellado en el hub»: acumula en `filtrada` uniendo
con ESPACIOS, y más abajo la cola se le pasa a `xargs -I{}`, que trata **una línea = un elemento**.
Unidas por espacios, N recetas llegan como UN SOLO argumento:
printf '%s\n' " a.toml b.toml c.toml" | xargs -I{} sh -c 'echo [$1]' _ {}
recibe: [a.toml b.toml c.toml] ← uno solo
⇒ la fase paralela fallaba SIEMPRE con «No such file», y la fase 1b —el reintento serial— salvaba
exactamente UNA receta: la última, porque `basename` de esa ristra da el último nombre. El log
parecía normal, con su ✗ seguido de un ✓ (reintento serial), y ese patrón está en TODAS las colas
(yambar, spidermonkey, rust…). O sea que el reintento serial, que existe para conciliar colisiones
de concurrencia, venía tapando el bug desde que se escribió el filtro.
El arreglo son dos líneas: acumular con salto de línea y descartar la línea vacía con la que arranca
el acumulador (una línea vacía también es un elemento para `xargs -I{}`). Probado en chiquito, en
los dos sentidos: antes 1 elemento, después 3.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
La vía (a) está CERRADA, y lo dice el propio bootstrap en una línea:
Cannot enable LLD with `rust.lld = true` when using external llvm-config.
Había leído el paso `Lld` —que compila desde el `src/llvm-project/lld` del tarball y reusa el
`llvm-config` externo— y me faltó mirar la VALIDACIÓN de config, que rechaza la combinación antes de
llegar a ese paso. La guarda tiene sentido: LLD y LLVM comparten ABI de C++, y con un llvm-config
externo el bootstrap no puede garantizar que casen. O sea: mientras usemos el `llvm21` del corpus
—que es lo que queremos— el toolchain NO se va a construir su propio `rust-lld`.
⇒ `recipes/lld21.toml` construye el MISMO `lld`, del mismo `llvm-project` (mismo tarball y mismo
sha256 que `llvm21`, byte por byte), pero SÓLO el subproyecto: `cmake -S lld` contra el LLVM ya
instalado por la dep. Minutos en vez de horas, y sin re-hashear `llvm21` —que encender
`LLVM_ENABLE_PROJECTS=lld` ahí dentro habría arrastrado a `rust` con él.
⚠ Y al revertir el flag aprendí algo que vale para todas las recetas con heredoc: el texto del
`bootstrap.toml` que la fase escribe **es parte de la fase, o sea ENTRADA DE HASH**. Mi comentario
explicando el fallo, puesto ahí adentro, cambiaba el hash del compilador entero: hora y media de
granja por un párrafo. Va fuera, como comentario de la receta, y el hash vuelve a `015a07fb…` — el
artefacto que ya está sellado.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Muro 8, cerrado por la vía (a): el toolchain se construye su propio `rust-lld` en vez de depender de
un `cc` que en una caja takana no existe.
Lo que lo destraba es una línea del log que parece informativa: `skipping llvm-tools (…): external
LLVM`. Al usar un LLVM externo —que es lo que queremos, porque `llvm21` es del corpus— 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 el 2 no.
Comprobado ANTES de encolar dos horas de build, leyendo el bootstrap y midiendo los artefactos:
· el paso `Lld` compila desde `src/llvm-project/lld`, que YA VIENE en el tarball (1,1 G de fuente);
· usa el `llvm-config`/cmake del LLVM EXTERNO y NO dispara un build de LLVM entero;
· `llvm21` publica `lib/cmake/llvm/LLVMConfig.cmake`, que es justo lo que ese paso necesita;
· `dist.rs` copia `rust-lld` al sysroot SÓLO `if builder.config.lld_enabled`;
· y `llvm21` no servía de repuesto: sobre sus 1,6 G no publica NINGÚN `lld`.
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>
El parche del triple FUNCIONÓ: el build pasó el stage 2 —donde moría— y llegó hasta las herramientas.
Ahí cortó otra cosa:
error: failed to run custom build command for `openssl-sys v0.9.114`
The system library `openssl` required by crate `openssl-sys` was not found.
`tools = ["cargo"]` arrastra `openssl-sys`, y eso pasa DESPUÉS de compilar las ~400 crates del
compilador, o sea a la hora y media de build.
La dep que entra es `openssl-threads` y NO la canónica, aunque las dos publiquen sólo `.a`: el
`Configure` de openssl APAGA LOS THREADS en cuanto ve `-static` en LDFLAGS, y la canónica se
construye así. Un cargo —masivamente multihilo— enlazado contra un openssl sin soporte de hilos es
la clase de fallo que no se ve al construir ni al arrancar. La variante ya existía por exactamente
lo mismo para `python3`, y el worker la tiene sellada con el mismo hash.
Van también `OPENSSL_STATIC=1` y `OPENSSL_DIR=/usr`: el lab publica `.a` y ningún `.so`, y sin
decírselo `openssl-sys` busca la dinámica y falla con un mensaje que habla de pkg-config.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`recipes/incoming/linux-generic.toml` entró a la cola y salió el mismo día. El motivo es concreto:
la cola del worker se muele EN SERIE y `rust` está horas adentro, así que el kernel —que era lo
urgente, porque sin él no hay cortafuegos— habría esperado a que rust terminara. Se construyó en la
propia caja (4 cores, 4,6 G libres, CPU ociosa) en 21 minutos.
Dejarlo encolado no era gratis: el worker no tiene ese artefacto en su store —la cosecha va
worker→hub y nunca al revés— así que lo habría reconstruido entero para llegar al mismo b3.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`recipes/rust-alpine-target.patch` registra `x86_64-alpine-linux-musl` en `rustc_target`: un spec
copiado del canónico y una línea en `supported_targets!`. Es lo que hace Alpine en su APKBUILD, y es
la única forma que vale para el triple del HOST (un `.json` por `RUST_TARGET_PATH` no alcanza).
La única diferencia real con el spec canónico es `crt_static_default = false`, que es lo que habilita
dylibs y con ellos los PROC-MACROS: un rustc con crt-static por default no compila `serde_derive` ni
`clap_derive` — ni las 44 recetas `cargo-*` del corpus que usan `derive`.
⚠ PROBADO EN SECO ANTES DE ENCOLAR UNA HORA DE BUILD (`patch -p1 --dry-run` contra el árbol real del
worker), y no de primera: la cabecera `@@ -0,0 +1,38 @@` del fichero nuevo tenía 37 líneas y `patch`
murió con «malformed patch», y el hunk de `mod.rs` lo escribí con números a ojo y falló. El segundo
lo generé con `diff -u` de verdad contra una copia. Un parche se COMPRUEBA, no se redacta.
⚠ Y EL `.patch` HAY QUE ENCOLARLO TAMBIÉN: `source.patches` resuelve contra el directorio de la
receta, así que con la receta enlazada en `recipes/incoming/` el parche se busca AHÍ. Es exactamente
el corolario que `farm-worker-loop.sh` documenta («los `.patch` NO viajan con la receta»), visto
desde el otro lado. Con los dos enlaces, el hash de la cola y el canónico coinciden: `b3:53c7f991…`,
y el worker calcula el mismo.
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>
`b3:63fdd57c…` sellado EN LA CAJA (gioser tiene 2 G disponibles y SWAP 0; el worker está horas con
rust) en 21 minutos. Los dos controles, sobre el `.config` SELLADO y no sobre la receta:
· lo que se quería encender: NETFILTER_ADVANCED=y, NF_TABLES=y, NF_TABLES_INET=y, NFT_CT=y,
NFT_LIMIT=y, NFT_LOG=y, NFT_REJECT{,_INET,_IPV4,_IPV6}=y, NFT_SYNPROXY=y.
· lo que NO podía romperse (el muro 1 del §6.4, que dejó una caja sin ver su disco): SCSI_VIRTIO=y,
VIRTIO_BLK=y, VIRTIO_NET=y, VIRTIO_PCI=y, SCSI=y, EXT4_FS=y. Los seis siguen.
⚠ `NFT_COUNTER` salió AUSENTE — y está bien: en 7.1 ese símbolo **ya no existe** (los contadores son
parte del core de nf_tables), así que no aparece en el `.config` ni como `# … is not set`. La línea
`-e NFT_COUNTER` es inerte y se deja escrita CON LA NOTA, porque sin ella el próximo que compare la
receta con el `.config` va a leer esa ausencia como un fallo. El comentario no re-hashea: el hash
sigue en `63fdd57c…`, comprobado.
El kernel nuevo queda EN `/boot/bzImage.nuevo`, al lado y no en su sitio, con el vigente copiado a
`/boot/bzImage.sin-nftables`. Hasta que alguien decida, la caja arranca lo de siempre — y ese
camino de vuelta es el único que hay: **hcloud no tiene consola serie**, así que un kernel que no
arranca se arregla con rescue + restaurar el bzImage, no apretando una tecla en el menú de grub.
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.
Dos cosas a moler, las dos en el worker (que tiene 6 cores, 16 G y 94 G libres), encoladas por
ENLACE a la receta canónica y no por copia: `takana hash` da el MISMO b3 por el enlace que por el
fichero real, así que la cola no puede divergir de lo canónico — que es justo el defecto que el
propio `farm-worker-loop.sh` documenta de las colas viejas («su copia aparcada era la versión
ANTERIOR y seguía leyéndose como deuda abierta»).
· `linux-generic` (b3:63fdd57c…, antes 23f1cc43…): `NETFILTER_ADVANCED`, `NF_TABLES`,
`NF_TABLES_INET`, `NFT_CT`, `NFT_LIMIT`, `NFT_COUNTER`, `NFT_LOG`, `NFT_REJECT{,_INET}` y
`NETFILTER_SYNPROXY`/`NFT_SYNPROXY`. Es lo que pide el cortafuegos de tawasuyu (SDD-ENTRADA): la
tabla `inet`, el estado de conexión y el `limit rate` POR ELEMENTO de set, que es el ban por IP
dentro del kernel y el reemplazo de fail2ban. SYNPROXY va ahora porque encenderlo después cuesta
otro kernel y otro reinicio de producción.
· `rust` (b3:aa5d81e8…): el escalón 2 del SDD 31, que estaba en `never`. El worker ya tiene `llvm21`
sellado con el MISMO hash que el hub (95956a16…), así que arranca desde ahí y no lo reconstruye.
Los dos hashes coinciden hub↔worker, comprobado antes de sembrar: el worker va a sellar exactamente
lo que sellaría acá.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Decisión del usuario sobre lo que quedaba del censo:
· `qdrant` (:6333/:6334) — es la base vectorial de `gioser_api`, y ese backend es un FÓSIL
(api.gioser.net da 502 permanente desde el §6.9). Una base sin consumidor no se muda. La receta
queda en el catálogo, que no es lo mismo que la imagen, y eso NO es deuda.
· `act_runner` (:41027) — runner de CI, binario suelto que nadie provee. Si mañana hace falta CI,
entra por receta y no por un binario copiado.
· `fail2ban` — **tawasuyu ya tiene el sustituto y es mejor**: `shared/cortafuegos` (SDD-ENTRADA §1)
no mira logs con un daemon, usa DOS SETS DINÁMICOS DEL KERNEL (`ban4`/`ban6`, `flags dynamic`,
`timeout`) con `limit rate over N/minute` POR IP de origen. El ban lo aplica nftables sin proceso
y sin latencia.
🧨 PERO EL SUSTITUTO NO ARRANCA: `nft list ruleset` da «cache initialization failed: Invalid
argument». `nft 1.1.6` está instalado y sellado; lo que falta está un piso más abajo, en el
`.config` que publica el artefacto del kernel:
CONFIG_NETFILTER=y · CONFIG_NF_CONNTRACK=y · **# CONFIG_NF_TABLES is not set**
y la causa: **# CONFIG_NETFILTER_ADVANCED is not set** (NF_TABLES cuelga de ella)
Es la lección del §6.4 muro 1 otra vez: la receta dice lo que se CAMBIÓ; sólo el `.config` sellado
dice lo que QUEDÓ. `linux-generic` sale de `make defconfig` y defconfig apaga NETFILTER_ADVANCED.
⇒ HOY LA CAJA NO TIENE CORTAFUEGOS DE NINGUNA CLASE, con :22022, :2345, :1137, :80 y :443
públicos. Hay que decirlo entero. La cura es un kernel con NETFILTER_ADVANCED + NF_TABLES (trabajo
del SDD 22) y termina en un reinicio de producción.
MITIGACIÓN HECHA HOY, que no depende del kernel: el `sshd` ofrecía `password` y
`keyboard-interactive` —su config eran CUATRO líneas y ninguna hablaba de autenticación, así que
regía el default de OpenSSH— que es justo la puerta que fail2ban cerraba. Ahora es sólo claves, con
cuatro controles: la clave entra, :22022 y :2345 contestan `Permission denied (publickey)` sin
ofrecer contraseña, y el `git ls-remote` por SSH sigue andando.
La lección: una defensa se muda con su CAPACIDAD, no con su nombre. Decidir «fail2ban muere porque
tenemos algo mejor» es correcto Y deja un hueco abierto hasta que ese algo mejor pueda ejecutarse.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`planear.py --revisar` sobre el censo ordena lo que queda, y lo primero no eran los servicios del
usuario sino TRES CAPACIDADES que ninguna metrica reclama: el disparador periodico, la hora y la
rotacion de logs. Los tres paquetes ya estaban sellados y declarados en `perfil.servidor` desde que
ese perfil nacio — lo que faltaba era el eslabon que los ARRANCA, igual que con squid.
· `chronyd`: la caja iba **15 s atrasada** y nadie la corregia. Ahora stratum 3 contra los NTP de
Hetzner, 0,000005 s de NTP. Sin hora no hay TLS ni firmas, y el sintoma no se parece a la causa.
· `crond`: no existia el latido. Verificado con una entrada `* * * * *` en el crontab REAL.
· `logrotate`: `/var/log/squid` sin rotar. Diario, con `squid -k rotate` (squid mantiene los
ficheros abiertos: un rename a secas lo deja escribiendo en un inode que nadie puede leer).
· `scripts/respaldo-gitea.sh` + `17 3 * * *`: cierra el hueco del §6.15 — los 44 repos vivian en UNA
copia y el respaldo se hacia a mano. Sube 23+21 repos (1,5 G) y un `gitea.db` de 332 M tomado con
`.backup`, consistente con el servidor vivo. El control no es que el script salga 0: es CONTAR los
repos de los dos lados.
`chrony` y `cronie` NO declaraban `[[service]]` — el paquete llegaba a la imagen y no lo arrancaba
nadie. Ahora lo declaran (fuera de `hash_inputs`: los hashes no se movieron) y el perfil los habilita.
TRES MEDICIONES que valen mas que el resultado:
1. **La deriva era de gioser, no de la caja**: despues de sincronizar, el HUB quedo 3 s adelantado.
Medir «contra el otro» sin un tercero no dice quien esta mal.
2. **El spool de cronie es `/var/spool/cron/<usuario>`**, no `…/crontabs/<usuario>`: `crontab -l`
mostraba las entradas y `crontabs/` estaba vacio. Un crontab restaurado en el sitio equivocado es
un fichero perfecto que nadie lee.
3. 🧨 **`sqlite3` seguia INERTE en la caja** (`compressBound: symbol not found`): el ultimo de los 23
del §6.4. Le faltaba `zlib-shared`, sellado y declarado en `perfil.base` desde el 11-09 — pero la
caja se instalo antes. Sin el no habia snapshot consistente. Un paquete declarado no es un
paquete instalado: en una caja viva vale lo que el `upgrade` proyecto.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`b3:932ba096…` proyectado en la caja y arrancado por arje SIN bwrap: `pid 11548 · uid=968 ·
exe=/usr/sbin/squid`, con la cuenta `proxy` que declara la receta. La instancia `qorpa/squid` se
borró — el manifiesto es descartable por diseño (ADR 0015 D3) y esto es para lo que existía.
El arreglo de esta tanda: **`--disable-log-daemon-helpers` compilaba perfecto y no arrancaba**. El
default de `access_log` es `daemon:`, asi que sin `log_file_daemon` squid muere con `FATAL:
logfile_daemon /usr/lib/squid/log_file_daemon: (2) No such file or directory`. Sellaba, hasheaba y
pasaba `-k parse` — se ve SOLO levantando el servicio con la config real. Familia
[[subcomando-sin-driver]].
El control que lo caza, y que corre en el hub antes de tocar produccion: con la config REAL del
origen y en un puerto aparte, arranque completo + CONNECT a claude.ai y api.anthropic.com con
TCP_TUNNEL/200 + el access.log escrito por el log daemon + `basic_ncsa_auth` leyendo el `passwd` de
verdad (control negativo: clave mala ⇒ `ERR Wrong password`). Los DOS primeros intentos de esta
receta habrian llegado a produccion sin ese control: el binario estaba sellado y el hash era estable
las dos veces.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`b3:54070b26…`, 8,8 M, **estático de verdad** (binario y los tres helpers), y probado con la CONFIG
REAL de produccion: parsea sin FATAL, tunela `CONNECT claude.ai` y `CONNECT api.anthropic.com` con
TCP_TUNNEL/200, y su `basic_ncsa_auth` lee el `passwd` de los tres usuarios (control negativo: con
una clave mala contesta `ERR Wrong password`).
Es la respuesta definitiva a lo de ayer: el proxy se levanto primero en una jaula qorpa porque
estaba EN USO y eso lo destrabo en minutos, pero squid es C++ y el lab lo construye. La diferencia
con php-fpm importa: PHP no queremos que entre al corpus, squid si.
⚠ LA FUENTE NO SALE DE squid-cache.org: esa URL devuelve **200 con una pagina HTML de 8985 bytes**,
no el tarball. Pinear su sha256 habria anclado la receta a una pagina de error. Va el release de
GitHub, que ademas trae `configure` ya generado.
🧨 EL MURO, y es del lab: el primer sellado salio con `usr/sbin/squid` **dinamico y `NEEDED
libc.so`** mientras sus helpers salian estaticos — o sea inerte en cualquier imagen sin cargador
([[needed-colgante-libstdcxx]]) pese a `link = "static"`. La cadena: `squid_LDFLAGS` trae
`-export-dynamic` (para modulos eCAP, que estan apagados) ⇒ **el wrapper del lab quita `-static` de
cualquier enlace que mencione `--export-dynamic`**, a proposito, para poder enlazar las `.so`
dlopen-ables de Python y companhia. Y no alcanza con sacarlo de la variable: **libtool lo vuelve a
anhadir solo** en cuanto hay un `-dlopen`. Se vacia `export_dynamic_flag_spec` en el `libtool`
GENERADO (local al build, sin tocar la fuente) y con guarda: si el campo no aparece, aborta — un
`sed` que no acierta deja pasar el problema y el binario sale dinamico sin que nada falle.
⚠⚠ Y `-dlopen force` NO sobra, aunque lo parezca: vaciar `squid_LDFLAGS` entero rompe el enlace con
`undefined symbol: lt__PROGRAM__LTX_preloaded_symbols`, que emite el propio libtool **porque hay un
`-dlopen`**. De los dos flags, el que estorba es uno solo. Distinguirlos costo un build; adivinar
cuesta lo mismo y no ensenha nada.
Declarado en `perfil.servidor` como paquete Y en `servicios`, con su `[[user]] proxy` y su
`[[service]]` (SDD 30) — que comprueba la config del sitio y la cuenta, y sale 78 nombrando cual
falta en vez de dejar un bucle de reinicios. El hash NO cambio al declararlos: `[[user]]` y
`[[service]]` estan fuera de `hash_inputs`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
El commit anterior puso la comprobación en el arnés y el párrafo no entró (el ancla del texto no
casó y el commit salió con un solo fichero: `git show --stat` lo dijo en el segundo, que es para lo
que se mira). Va ahora: los 180 s son de ESTA máquina, con el anfitrión cargado el arranque puede no
llegar al gancho, y como el sello se lee antes y después el arnés avisa en vez de envenenar la
corrida siguiente en silencio.
Los 180 s de `PISO_SELLO` son de esta máquina (TCG sin KVM y con la carga que tenga el anfitrión,
igual que los +97/+120/+137 de la pintura). Con el anfitrión cargado el mismo arranque puede no
llegar al gancho que sella el éxito, y entonces la corrida suma al contador de caídas y envenena la
siguiente EN SILENCIO — que es exactamente como empezó todo esto.
Ahora que `toolkit.startup.last_success` se lee antes y después, eso se puede decir, así que se dice:
✓ el arranque SELLÓ el éxito (last_success X → Y): esta corrida no envenena la siguiente
⚠⚠ el sello NO se movió: este arranque no terminó ⇒ la corrida SUMA al contador. Subí PISO_SELLO
Es la diferencia entre un piso confiado y un piso comprobado, y cuesta cuatro líneas porque el dato
ya estaba medido y guardado.
Leyendo el perfil en cada captura, el sello (`toolkit.startup.last_success`) y la limpieza del
contador llegan al disco entre +100 s y +137 s DESDE EL LANZAMIENTO, no desde el primer cuadro. Y es
UN gancho y no dos: el mismo arranque que limpia escribe el sello, con su propia hora de arranque.
Las dos cotas salen de corridas independientes, que es lo que hace al intervalo creíble:
· a los +100 s NO había sellado — la corrida S1 vivió 100 s desde el lanzamiento (primer cuadro a
+97 s, fin del proceso tres segundos después, medido con los tiempos de sus propios ficheros);
· a los +137 s YA había sellado — la corrida con `--watch-prefs` lo ve aparecer en esa captura.
S1 parecía la excepción del modelo y resulta ser su cota inferior: si el reloj fuera el de la pintura
tendría que haber sellado, y no selló.
La consecuencia es contraintuitiva y es la que muerde: cuanto MÁS RÁPIDO pinta, más fácil es
envenenar el perfil, porque `--until-paint` corta antes. La serie de ayer no se rompió por ser larga
sino por ser rápida. Por eso la regla va en el arnés y no en un runbook: `PISO_SELLO = 180`, margen
sobre la cota alta, y `--until-paint` no corta antes de ese piso. El comentario del piso lleva los
dos números medidos al lado — un piso que se lee como elegido lo baja el próximo que lo vea.
Las corridas `cierre-1`/`cuanto-1`, el mando `--watch-prefs` y el piso los midió y escribió la otra
sesión del frente; acá van con la cota de S1 que los cierra por abajo.
El par aísla la variable —mismo pin, mismo perfil, misma imagen, y la única diferencia es cuánto vive
el navegador después del primer cuadro—:
S1 +97 s, muerta en el acto (`--until-paint`) ⇒ contador después: 1
cierre-1 +120 s, ~360 s de vida después de pintar ⇒ contador después: AUSENTE
Con eso queda contestada la pregunta que dos secciones atrás este documento daba por cerrada y no lo
estaba, y A deja de ser una anomalía: también siguió viva minutos después del primer cuadro.
Se dice además lo que la tabla NO prueba: si ese arranque además SELLÓ el éxito. El volcado del
«después» sólo grepeaba el contador; ahora `toolkit.startup.last_success` va en los DOS volcados y se
guarda en la medición (`last_success_antes` / `_despues`), que es lo que faltaba para no repetir la
corrida. Si el sello se mueve, es un gancho que sella y limpia a la vez; si no, son dos.
Y queda escrito que el 16 sigue sin explicación, ahora con una contradicción medida encima:
desapareció en una corrida diálogo + ATUQ-EXIT=0, y las fases k2/k3 del umbral fueron diálogo +
ATUQ-EXIT=0 y no limpiaron nada. La respuesta buena no tapa la sucia.
La corrida `cierre-1` y el mando `--watch-prefs` son de la otra sesión del frente.