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 1572Shell 203Rust 109Markdown 993728 Definiciones

⎇ main scripts/test-foco-egress.sh Shell · 184 líneas · 9.5 KB Historia crudo

atuq §6.5: el foco por cgroup MEDIDO — y corregido el propio documento 212b304b6 · Sergio ·

1#!/usr/bin/env bash
2# ¿El «modo foco» del §6.5 (SDD 26) para de verdad al navegador? — la mitad que SÓLO se mide con root.
3#
4#   scripts/test-foco-egress.sh --rootfs alpine-minirootfs.tar.gz --artifacts artefactos-nft.tar.gz
5#
6# Desde el hub se lanza con `scripts/foco-egress-remoto.sh`, que empaqueta los artefactos VIGENTES.
7#
8# La tesis del §6.5 es que el foco NO lo sostiene una extensión —que el propio navegador puede
9# apagar— sino el cortafuegos del sistema: una regla de egress por **cgroup**. Esto lo comprueba como
10# lo viviría el usuario: un proceso dentro del cgroup del navegador NO abre una conexión y, control
11# que TIENE que pasar, el mismo binario en el cgroup de al lado SÍ la abre.
12#
13# ⚠ POR QUÉ NO CORRE EN EL HUB. Dos cosas piden privilegio y ninguna se puede fingir:
14#   · crear un cgroup (`/sys/fs/cgroup` es de root; medido: `mkdir` da EPERM incluso dentro de un
15#     userns con CAP_ALL, porque el permiso se chequea contra la jerarquía REAL);
16#   · y `nft -c` no es un chequeo de sintaxis: RESUELVE el path del cgroup contra la máquina viva y
17#     falla con «cgroupv2 path fails» si no existe.
18# Por eso corre donde somos root (hoy: el LXC prestado dev.gioser.net) y SIEMPRE dentro de `unshare`:
19# una regla mal puesta en el netns de verdad te saca de la máquina.
20#
21# ⚠⚠ Y TODO EL MONTAJE VA EN UN MOUNT NAMESPACE PROPIO, `--propagation private`. Esto no es
22# prolijidad: `/sys/fs/cgroup` está montado **shared**, así que desmontar la COPIA de un `--rbind`
23# se PROPAGA al montaje real. Medido el 2026-09-10 y a costa del worker: quedó sin `cgroup2`
24# montado —dos veces— sin que nada fallara ni avisara; los builds seguían, pero la máquina había
25# perdido su jerarquía de cgroups. Dentro de un namespace privado no hay nada que desmontar al
26# final: se va con el proceso.
27#
28# El `nft` es el NUESTRO —el artefacto sellado, musl— y por eso hace falta el rootfs alpine: la
29# máquina donde hay root es Debian/glibc y no tiene su intérprete.
30set -euo pipefail
31
32ROOTFS_TAR=""; ARTIFACTS_TAR=""; WORK="/root/foco-lab"; ROTO=0
33while [ $# -gt 0 ]; do
34    case "$1" in
35        --rootfs)    ROOTFS_TAR="$2"; shift 2 ;;
36        --artifacts) ARTIFACTS_TAR="$2"; shift 2 ;;
37        --work)      WORK="$2"; shift 2 ;;
38        # Rotura A PROPÓSITO: le abre egress TAMBIÉN al cgroup en foco. Con esto el guardián TIENE
39        # que fallar; si pasa, no está mirando nada. Es la mitad que a un guardián le falta siempre.
40        --broken-rules) ROTO=1; shift ;;
41        *) echo "opción desconocida: $1" >&2; exit 2 ;;
42    esac
43done
44[ -n "$ROOTFS_TAR" ] && [ -n "$ARTIFACTS_TAR" ] || { echo "faltan --rootfs y --artifacts" >&2; exit 2; }
45[ "$(id -u)" = "0" ] || { echo "✗ esto necesita root: crea cgroups y aplica reglas nft" >&2; exit 2; }
46grep -q cgroup2 /proc/mounts || { echo "✗ no hay cgroup2 montado" >&2; exit 2; }
47
48CG_RAIZ="$(awk '$3=="cgroup2"{print $2; exit}' /proc/mounts)"
49CG_FOCO="takana-foco-atuq"     # el navegador EN foco: sin egress
50CG_LIBRE="takana-foco-libre"   # el control: mismo binario, egress permitido
51
52limpiar() {
53    set +e
54    # Los cgroups son lo ÚNICO que vive fuera del namespace, y un cgroup con procesos adentro no se
55    # borra: primero se vacía moviendo lo que quede a la raíz.
56    for cg in "$CG_RAIZ/$CG_FOCO" "$CG_RAIZ/$CG_LIBRE"; do
57        [ -d "$cg" ] || continue
58        while read -r p; do [ -n "$p" ] && echo "$p" > "$CG_RAIZ/cgroup.procs" 2>/dev/null; done < "$cg/cgroup.procs"
59        rmdir "$cg" 2>/dev/null
60    done
61    # Sin montajes que desarmar: estaban en otro namespace. Pero se comprueba antes de borrar, que
62    # es lo que faltó la primera vez.
63    if grep -q " $WORK/" /proc/mounts; then
64        echo "⚠ quedó algo montado bajo $WORK: NO lo borro (mirá /proc/mounts)" >&2
65    else
66        rm -rf "$WORK"
67    fi
68}
69trap limpiar EXIT
70
71[ "$ROTO" = "1" ] && echo "== MODO ROTURA A PROPÓSITO: el reglaset deja salir también al cgroup en foco"
72echo "== armando el lab en $WORK"
73rm -rf "$WORK"; mkdir -p "$WORK/rootfs" "$WORK/art"
74tar xzf "$ROOTFS_TAR" -C "$WORK/rootfs"
75tar xzf "$ARTIFACTS_TAR" -C "$WORK/art"
76for a in "$WORK/art"/*; do cp -a "$a"/usr/. "$WORK/rootfs/usr/"; done
77[ -x "$WORK/rootfs/usr/sbin/nft" ] || { echo "✗ el artefacto no trae usr/sbin/nft" >&2; exit 1; }
78
79# ⚠ Que `nft` ARRANQUE se comprueba acá, primero. Si le falta una librería, el reglaset no se aplica
80# y las dos conexiones fallan — que desde afuera se lee EXACTAMENTE como «el foco funciona». Medido:
81# la primera corrida empaquetó un `gmp` viejo (sólo `libgmp.a`) y `libnftables.so` murió con
82# `__gmpz_com: symbol not found`. En el hub no se había notado porque ahí el lab tiene su propia
83# libgmp y la tapaba: el artefacto parecía autosuficiente y no lo era.
84if ! chroot "$WORK/rootfs" /usr/sbin/nft --version > "$WORK/version.txt" 2>&1; then
85    echo "✗ nuestro nft no arranca en este rootfs:"; sed 's/^/     /' "$WORK/version.txt"
86    echo "  (típico: falta una librería del cierre — mirá los NEEDED de usr/lib/libnftables.so)"
87    exit 1
88fi
89echo "   nft: $(cat "$WORK/version.txt")"
90
91mkdir -p "$CG_RAIZ/$CG_FOCO" "$CG_RAIZ/$CG_LIBRE"
92echo "   cgroups: $CG_FOCO (en foco) · $CG_LIBRE (control)"
93
94cat > "$WORK/rootfs/reglas.nft" <<EOF
95# El reglaset que el §6.5 le pide al cortafuegos: egress DENEGADO salvo lo que se nombra.
96table inet takana_foco {
97    chain egress {
98        type filter hook output priority 0; policy drop;
99        ct state established,related accept
100        socket cgroupv2 level 1 "$CG_LIBRE" accept
101$( [ "$ROTO" = "1" ] && echo "        socket cgroupv2 level 1 \"$CG_FOCO\" accept   # ROTURA A PROPÓSITO" )
102    }
103}
104EOF
105
106# ⚠ HEREDOC ENTRECOMILLADO (<<'EOF') y los nombres por ENTORNO. No es estilo: con el heredoc
107# abierto, la máquina de afuera expande `$(...)`, `$VAR` **y los backticks**, incluso dentro de un
108# COMENTARIO. Medido el 2026-09-10: un comentario que decía «en alpine `ip` e `ifconfig` son enlaces
109# de busybox» ejecutó los dos comandos EN EL HOST y pegó la salida de `ifconfig` —con las IPs de la
110# máquina— dentro del guión generado. Un comentario que corre.
111cat > "$WORK/rootfs/adentro.sh" <<'EOF'
112#!/bin/sh
113# Corre YA dentro del netns y del mount ns privados, y del chroot. Los cgroups llegan por entorno
114# (CG_FOCO / CG_LIBRE): chroot conserva el entorno, así que no hace falta interpolar nada.
115set -u
116# /sbin en el PATH: en alpine, ip e ifconfig son enlaces de busybox en /sbin, y sin eso el
117# "ip link set lo up" no existe — el loopback queda caído y NINGUNA conexión sale. La línea base lo
118# cazó; sin ella, ese silencio se habría contado como «el foco funciona».
119export PATH=/usr/sbin:/usr/bin:/sbin:/bin LD_LIBRARY_PATH=/usr/lib
120PUERTO=45999
121
122ip link set lo up 2>/dev/null || ifconfig lo up 2>/dev/null
123echo "LO $(ip addr show lo 2>/dev/null | tr '\n' ' ' | tr -s ' ')"
124
125# Un listener por intento: "nc -l ... -e" atiende UNA conexión y se va, y un listener muerto se lee
126# igual que una conexión bloqueada. El listener vive en el cgroup permitido, así sus respuestas
127# salen por "established".
128intento() {   # $1 = cgroup del cliente, $2 = etiqueta
129    sh -c "echo \$\$ > /sys/fs/cgroup/$CG_LIBRE/cgroup.procs; exec nc -l -p $PUERTO -e /bin/true" \
130        < /dev/null > /dev/null 2>&1 &
131    L=$!
132    sleep 1
133    sh -c "echo \$\$ > /sys/fs/cgroup/$1/cgroup.procs; exec nc -w 3 127.0.0.1 $PUERTO" \
134        < /dev/null > /dev/null 2>&1
135    echo "CONEXION $2 exit=$?"
136    kill $L 2>/dev/null
137    wait $L 2>/dev/null
138}
139
140# LÍNEA BASE, sin ninguna regla puesta: si esto ya falla, el harness está roto y cualquier veredicto
141# posterior sería un falso positivo del foco («no conectó» se lee como «el cortafuegos funciona»).
142intento "$CG_FOCO" linea-base
143
144nft -f /reglas.nft || { echo "RESULTADO nft-no-aplica"; exit 1; }
145echo "REGLAS $(nft list ruleset | tr '\n' ' ' | tr -s ' ')"
146
147intento "$CG_LIBRE" control
148intento "$CG_FOCO"  foco
149EOF
150chmod +x "$WORK/rootfs/adentro.sh"
151
152echo "== corriendo dentro de un netns y un mount ns privados"
153# Las montaduras van ADENTRO del namespace y se van solas: nada que desmontar, nada que propagar.
154CG_FOCO="$CG_FOCO" CG_LIBRE="$CG_LIBRE" unshare -m --propagation private -n sh -c "
155    mount --rbind /sys '$WORK/rootfs/sys'
156    mount -t proc proc '$WORK/rootfs/proc'
157    exec chroot '$WORK/rootfs' /adentro.sh
158" > "$WORK/adentro.log" 2>&1 || true
159SALIDA="$(cat "$WORK/adentro.log")"
160echo "$SALIDA" | sed 's/^/   /'
161
162base="$(echo  "$SALIDA" | sed -n 's/^CONEXION linea-base exit=//p')"
163libre="$(echo "$SALIDA" | sed -n 's/^CONEXION control exit=//p')"
164foco="$(echo  "$SALIDA" | sed -n 's/^CONEXION foco exit=//p')"
165[ -n "$base" ] && [ -n "$libre" ] && [ -n "$foco" ] || { echo "✗ la corrida no dejó los tres veredictos"; exit 1; }
166if [ "$base" != "0" ]; then
167    echo "✗ HARNESS ROTO: sin ninguna regla puesta tampoco se pudo conectar (exit=$base)."
168    echo "  Nada de lo que sigue mediría el foco: un «no conectó» se leería como éxito."
169    exit 1
170fi
171if [ "$libre" != "0" ]; then
172    echo "✗ CONTROL ROTO: con las reglas puestas, el cgroup PERMITIDO tampoco conectó (exit=$libre)"
173    echo "  o sea que el reglaset niega de más: el foco no se distingue de cortar todo."
174    exit 1
175fi
176if [ "$foco" = "0" ]; then
177    echo "✗ el cgroup EN FOCO conectó igual: el cortafuegos no está sosteniendo nada"
178    exit 1
179fi
180echo
181echo "✓ la línea base conecta: el harness mide algo (exit=0 sin reglas puestas)"
182echo "✓ en foco no hay salida: el proceso del cgroup del navegador no abrió la conexión (exit=$foco)"
183echo "✓ y el control PASA: el mismo binario, en el cgroup de al lado, conectó (exit=0)"
184echo "✓ o sea que el foco lo sostiene una regla del sistema — nada que el navegador pueda apagar"