atuq §6.5: el foco por cgroup MEDIDO — y corregido el propio documento
La fila de la tabla se leía como «bloquear sitios». El cortafuegos NO sabe de sitios: su política de egress tiene UNA dimensión, qué cgroup sale, y ninguna de destino (leído en UnidadRed, no supuesto). El foco que estas piezas dan es «el navegador no sale», con todo lo local andando — promesa más honesta, además: una lista de dominios se esquiva con un espejo; un cgroup sin egress no. Medido con nuestro propio nft, en el LXC donde somos root: linea-base exit=0 · control exit=0 · foco exit=1 Tres medidas y no una: la línea base porque un harness roto se lee igual que un foco que funciona, y el control porque un reglaset que niega de más tampoco se distingue. `--broken-rules` abre egress al cgroup en foco y el guardián falla nombrándolo (sale 1). Verificado en los dos sentidos. Restricciones reales que salieron de medir: crear un cgroup pide root (EPERM incluso en un userns con CAP_ALL), y `nft -c` no es sintaxis — resuelve el path del cgroup contra la máquina viva. ⚠ Y lo que rompí en el camino, documentado: `/sys/fs/cgroup` está montado SHARED, así que desmontar la copia de un --rbind se propaga al montaje real. Dejé al worker sin cgroup2 dos veces, en silencio. Remontado y jerarquía intacta. Ahora todo va en un mount namespace propio (--propagation private), sin `umount -l` antes de un rm -rf, y con /proc/mounts consultado antes de borrar. Tres falsos positivos más que cazó el harness, todos con cara de éxito: un gmp viejo tomado por `ls store/*-gmp` (sin .so ⇒ nada conectaba, y el lab del hub lo tapaba), /sbin fuera del PATH del chroot (sin `ip`, loopback caído), y un COMENTARIO que se ejecutó — backticks dentro de un heredoc sin comillas corrieron `ip`/`ifconfig` en el host y pegaron su salida en el guión generado.
Semántico sayk'uy
Mana ima sutichaypas t'ikrakurqanchu: qillqalla, yachachiylla utaq willaylla t'ikrakurqan.
3 qillqakuna · +313 −0
- docs/26-atuq-envoltorio-gecko.md +80 −0
- scripts/foco-egress-remoto.sh +49 −0
- scripts/test-foco-egress.sh +184 −0
diff --git a/docs/26-atuq-envoltorio-gecko.md b/docs/26-atuq-envoltorio-gecko.md
index 5193ef6f..87b9b60b 100644
--- a/docs/26-atuq-envoltorio-gecko.md
+++ b/docs/26-atuq-envoltorio-gecko.md
@@ -681,6 +681,86 @@ eso; el modo incógnito es teatro. ⚠ Con la advertencia que el propio SDD de `
**hay que repetir acá, no esconder**: en una máquina con swap sin cifrar, el documento no aplica.
Se promete lo que se puede probar.
+### 6.5 Foco por `cortafuegos` — la mitad del sistema, MEDIDA (2026-09-10)
+
+**La primera corrección es a este propio documento.** La fila de la tabla dice «foco por
+`cortafuegos`» y es fácil leerla como «bloquear sitios que distraen». **El cortafuegos de tawasuyu no
+sabe de sitios**: su política de egress (`cortafuegos-core::Politica`) tiene exactamente una
+dimensión —**qué cgroup puede salir**— y ninguna de destino; ni dominio, ni IP, ni puerto. Leído en
+la fuente (`UnidadRed { nombre, cgroup_path, concesion }`), no supuesto. Así que el foco que estas
+piezas permiten no es «Twitter no carga»: es **«el navegador no sale»**, con todo lo local
+—incluido el archivo personal del §6.3 y el CAS del §6.2— funcionando igual. Que es, además, una
+promesa más honesta: una lista de dominios se esquiva con un espejo, y un cgroup sin egress no.
+
+**Y la diferencia con una extensión no es de grado.** Un bloqueador de extensión lo apaga en dos
+clics la misma persona que quería no distraerse; un reglaset `nft` que aplicó root no lo toca el
+navegador ni sus extensiones. Por eso la fila decía «nadie lo tiene»: nadie es dueño del navegador
+*y* del sistema.
+
+**Lo que hizo falta traer.** `nftables` estaba en el grafo como `wanted` y nadie había puesto la
+cadena: entran `recipes/libmnl.toml`, `recipes/libnftnl.toml` y `recipes/nftables.toml` (1.1.6, la
+versión contra la que el SDD del cortafuegos validó `socket cgroupv2` en metal). Dos cosas venían de
+fábrica y están en los comentarios de la receta: nftables **no reproduce** —`MAKE_STAMP` es
+`$(shell date +%s)` horneado en el binario, la familia del `BuildID` de waterfox— y su
+`config.status` trae un bashismo que busybox rechaza. Las dos se arreglan en el parche, y el control
+existe: con el sello vivo, dos builds de la misma fuente **divergen** en `libnftables.so`
+(`why-differs`: `.data`, `.text`); con el parche, `verificar-repro.sh` da REPRODUCE.
+
+**La medición, que es la unidad de trabajo de verdad** (`scripts/test-foco-egress.sh`, lanzado con
+`scripts/foco-egress-remoto.sh`):
+
+```
+nft: nftables v1.1.6 ← el NUESTRO, musl, desde el artefacto sellado
+CONEXION linea-base exit=0 ← sin reglas: el harness mide algo
+CONEXION control exit=0 ← con reglas: el cgroup permitido SÍ conecta
+CONEXION foco exit=1 ← el cgroup del navegador NO
+```
+
+Tres medidas y no una: la **línea base** existe porque un harness roto («no conectó») se lee
+exactamente igual que un foco que funciona, y el **control** porque un reglaset que niega de más no
+se distingue de uno que niega bien. Con `--broken-rules` —que le abre egress también al cgroup en
+foco— el guardián falla diciendo `el cgroup EN FOCO conectó igual`, y sale 1.
+
+**Por qué no corre en el hub, y es una restricción real, no comodidad.** Crear un cgroup pide root:
+medido, `mkdir /sys/fs/cgroup/...` da EPERM **incluso dentro de un userns con `--cap-add ALL`**,
+porque el permiso se chequea contra la jerarquía real. Y `nft -c` **no es un chequeo de sintaxis**:
+resuelve el path del cgroup contra la máquina viva y falla con `cgroupv2 path fails` si no existe. O
+sea que ni siquiera *verificar* una política del cortafuegos se puede sin los cgroups puestos. Corre
+en el LXC prestado, y todo va dentro de `unshare -m --propagation private -n`.
+
+⚠ **Y ese `--propagation private` se pagó caro.** `/sys/fs/cgroup` está montado **shared**, así que
+desmontar la COPIA de un `--rbind` **se propaga al montaje real**: dos corridas dejaron al worker
+**sin `cgroup2` montado**, en silencio —los builds seguían— hasta que el guardián siguiente dijo «no
+hay cgroup2 montado». Se remontó y la jerarquía volvió intacta (los cgroups viven en el kernel, no en
+el montaje). Tres lecciones que quedan en el guión: los montajes van en un **mount namespace propio**
+y se van solos; **nunca `umount -l`** antes de un `rm -rf` (un desmontaje perezoso desaparece de
+`/proc/mounts` mientras el árbol sigue ahí, así que la guarda pasa y el borrado entra en el `/sys` de
+la máquina); y antes de borrar, **preguntarle a `/proc/mounts`**.
+
+⚠ **Tres falsos positivos más que cazó el propio harness, y todos se veían como éxito:**
+
+1. un `gmp` **viejo** empaquetado por `ls store/*-gmp` (sólo `libgmp.a`) ⇒ `libnftables.so` no
+ relocaba y **ninguna** conexión salía. En el hub no se había notado porque ahí el lab tiene su
+ propia libgmp y la tapaba: el artefacto parecía autosuficiente y no lo era. Ahora los artefactos
+ se resuelven por `takana hash` (vigente), y el guardián corre `nft --version` **antes** de todo;
+2. `/sbin` fuera del `PATH` del chroot ⇒ no había `ip`, el loopback quedaba caído y todo fallaba;
+3. y el mejor: **un comentario que se ejecutó**. El guión interior se escribía con un heredoc sin
+ comillas, y un comentario que mencionaba `` `ip` `` y `` `ifconfig` `` entre backticks los corrió
+ **en el host** y pegó la salida —con las IPs de la máquina— dentro del guión generado. Ahora el
+ heredoc va entrecomillado y los nombres entran por entorno.
+
+**Lo que falta para que esto sea un botón, y por qué no se inventó de paso.** Dos piezas, ninguna del
+navegador:
+
+- **quién pone a `atuq` en un cgroup.** Hoy nadie: los cgroups de la distro los crea arje para sus
+ servicios, y una app de escritorio la lanza el usuario. Hace falta delegación de un subárbol a la
+ sesión, que es decisión de arje (SDD 10), no de este documento;
+- **quién aplica la política.** `cortafuegos apply` pide root. Un demonio que lo haga a pedido del
+ navegador es superficie nueva: se decide, no se agrega de paso.
+
+Mientras eso no exista, lo que `atuq` puede tener —y tiene, §6.5.bis— es la mitad honesta: **ver** el
+estado del foco y **no poder apagarlo**.
+
### 6.6 Medios fuera del navegador — HECHO (2026-09-10)
Una navegación de PRIMER NIVEL a un medio (`Content-Type: video/*` o `audio/*`) se cancela y la URL se
diff --git a/scripts/foco-egress-remoto.sh b/scripts/foco-egress-remoto.sh
new file mode 100755
index 00000000..cc0c4a5f
--- /dev/null
+++ b/scripts/foco-egress-remoto.sh
@@ -0,0 +1,49 @@
+#!/usr/bin/env bash
+# Corre `test-foco-egress.sh` donde SÍ hay root: empaqueta los artefactos VIGENTES, los manda al
+# worker y trae el veredicto.
+#
+# scripts/foco-egress-remoto.sh [--broken-rules] [host] (host default: 1º de scripts/farm/.fleet)
+#
+# ⚠ Los artefactos se resuelven con `takana hash`, NO con `ls store/*-gmp`. La diferencia no es
+# estética: el store guarda artefactos VIEJOS con el mismo nombre, y la primera corrida de esto
+# empaquetó un `gmp` de otra época que sólo traía `libgmp.a` ⇒ `libnftables.so` no relocaba. En el
+# hub no se veía porque el lab tiene su propia libgmp y la tapaba. Sellado ≠ vigente.
+set -euo pipefail
+cd "$(dirname "$0")/.."
+
+ROTO=""
+if [ "${1:-}" = "--broken-rules" ]; then ROTO="--broken-rules"; shift; fi
+HOST="${1:-}"
+if [ -z "$HOST" ]; then
+ HOST="$(awk 'NR==1{print $1}' scripts/farm/.fleet 2>/dev/null || true)"
+ [ -n "$HOST" ] || { echo "✗ no hay host: pasalo por argumento o poné uno en scripts/farm/.fleet" >&2; exit 2; }
+fi
+LLAVE="${SSH_KEY:-$HOME/.ssh/github5}"
+TAKANA=./target/release/takana
+TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
+
+echo "== artefactos vigentes"
+DIRS=()
+for r in nftables libmnl libnftnl gmp; do
+ h="$($TAKANA --store ./store hash "recipes/$r.toml" | tail -1)"; h="${h#b3:}"
+ d="store/$h-$r"
+ [ -d "$d" ] || { echo "✗ $r vigente no está sellado ($d) — construilo antes" >&2; exit 1; }
+ [ -n "$(ls -A "$d")" ] || { echo "✗ $r vigente está VACÍO: eso no es un artefacto, es un nombre" >&2; exit 1; }
+ echo " $r → ${h:0:12}"
+ DIRS+=("$h-$r")
+done
+tar czf "$TMP/artefactos-nft.tar.gz" -C store "${DIRS[@]}"
+
+echo "== enviando a $HOST"
+scp -i "$LLAVE" -o StrictHostKeyChecking=no -q \
+ scripts/test-foco-egress.sh .dev-fs/alpine-minirootfs.tar.gz "$TMP/artefactos-nft.tar.gz" \
+ "root@$HOST:/root/"
+# `set -e` mataría el script antes de leer $? de un ssh que falla, y entonces el veredicto ROJO del
+# guardián llegaría como si nunca hubiera corrido. Se captura explícitamente.
+SALIDA=0
+ssh -i "$LLAVE" -o BatchMode=yes "root@$HOST" \
+ "bash /root/test-foco-egress.sh --rootfs /root/alpine-minirootfs.tar.gz --artifacts /root/artefactos-nft.tar.gz $ROTO" \
+ || SALIDA=$?
+ssh -i "$LLAVE" -o BatchMode=yes "root@$HOST" \
+ 'rm -f /root/test-foco-egress.sh /root/alpine-minirootfs.tar.gz /root/artefactos-nft.tar.gz' || true
+exit $SALIDA
diff --git a/scripts/test-foco-egress.sh b/scripts/test-foco-egress.sh
new file mode 100755
index 00000000..5789b627
--- /dev/null
+++ b/scripts/test-foco-egress.sh
@@ -0,0 +1,184 @@
+#!/usr/bin/env bash
+# ¿El «modo foco» del §6.5 (SDD 26) para de verdad al navegador? — la mitad que SÓLO se mide con root.
+#
+# scripts/test-foco-egress.sh --rootfs alpine-minirootfs.tar.gz --artifacts artefactos-nft.tar.gz
+#
+# Desde el hub se lanza con `scripts/foco-egress-remoto.sh`, que empaqueta los artefactos VIGENTES.
+#
+# La tesis del §6.5 es que el foco NO lo sostiene una extensión —que el propio navegador puede
+# apagar— sino el cortafuegos del sistema: una regla de egress por **cgroup**. Esto lo comprueba como
+# lo viviría el usuario: un proceso dentro del cgroup del navegador NO abre una conexión y, control
+# que TIENE que pasar, el mismo binario en el cgroup de al lado SÍ la abre.
+#
+# ⚠ POR QUÉ NO CORRE EN EL HUB. Dos cosas piden privilegio y ninguna se puede fingir:
+# · crear un cgroup (`/sys/fs/cgroup` es de root; medido: `mkdir` da EPERM incluso dentro de un
+# userns con CAP_ALL, porque el permiso se chequea contra la jerarquía REAL);
+# · y `nft -c` no es un chequeo de sintaxis: RESUELVE el path del cgroup contra la máquina viva y
+# falla con «cgroupv2 path fails» si no existe.
+# Por eso corre donde somos root (hoy: el LXC prestado dev.gioser.net) y SIEMPRE dentro de `unshare`:
+# una regla mal puesta en el netns de verdad te saca de la máquina.
+#
+# ⚠⚠ Y TODO EL MONTAJE VA EN UN MOUNT NAMESPACE PROPIO, `--propagation private`. Esto no es
+# prolijidad: `/sys/fs/cgroup` está montado **shared**, así que desmontar la COPIA de un `--rbind`
+# se PROPAGA al montaje real. Medido el 2026-09-10 y a costa del worker: quedó sin `cgroup2`
+# montado —dos veces— sin que nada fallara ni avisara; los builds seguían, pero la máquina había
+# perdido su jerarquía de cgroups. Dentro de un namespace privado no hay nada que desmontar al
+# final: se va con el proceso.
+#
+# El `nft` es el NUESTRO —el artefacto sellado, musl— y por eso hace falta el rootfs alpine: la
+# máquina donde hay root es Debian/glibc y no tiene su intérprete.
+set -euo pipefail
+
+ROOTFS_TAR=""; ARTIFACTS_TAR=""; WORK="/root/foco-lab"; ROTO=0
+while [ $# -gt 0 ]; do
+ case "$1" in
+ --rootfs) ROOTFS_TAR="$2"; shift 2 ;;
+ --artifacts) ARTIFACTS_TAR="$2"; shift 2 ;;
+ --work) WORK="$2"; shift 2 ;;
+ # Rotura A PROPÓSITO: le abre egress TAMBIÉN al cgroup en foco. Con esto el guardián TIENE
+ # que fallar; si pasa, no está mirando nada. Es la mitad que a un guardián le falta siempre.
+ --broken-rules) ROTO=1; shift ;;
+ *) echo "opción desconocida: $1" >&2; exit 2 ;;
+ esac
+done
+[ -n "$ROOTFS_TAR" ] && [ -n "$ARTIFACTS_TAR" ] || { echo "faltan --rootfs y --artifacts" >&2; exit 2; }
+[ "$(id -u)" = "0" ] || { echo "✗ esto necesita root: crea cgroups y aplica reglas nft" >&2; exit 2; }
+grep -q cgroup2 /proc/mounts || { echo "✗ no hay cgroup2 montado" >&2; exit 2; }
+
+CG_RAIZ="$(awk '$3=="cgroup2"{print $2; exit}' /proc/mounts)"
+CG_FOCO="takana-foco-atuq" # el navegador EN foco: sin egress
+CG_LIBRE="takana-foco-libre" # el control: mismo binario, egress permitido
+
+limpiar() {
+ set +e
+ # Los cgroups son lo ÚNICO que vive fuera del namespace, y un cgroup con procesos adentro no se
+ # borra: primero se vacía moviendo lo que quede a la raíz.
+ for cg in "$CG_RAIZ/$CG_FOCO" "$CG_RAIZ/$CG_LIBRE"; do
+ [ -d "$cg" ] || continue
+ while read -r p; do [ -n "$p" ] && echo "$p" > "$CG_RAIZ/cgroup.procs" 2>/dev/null; done < "$cg/cgroup.procs"
+ rmdir "$cg" 2>/dev/null
+ done
+ # Sin montajes que desarmar: estaban en otro namespace. Pero se comprueba antes de borrar, que
+ # es lo que faltó la primera vez.
+ if grep -q " $WORK/" /proc/mounts; then
+ echo "⚠ quedó algo montado bajo $WORK: NO lo borro (mirá /proc/mounts)" >&2
+ else
+ rm -rf "$WORK"
+ fi
+}
+trap limpiar EXIT
+
+[ "$ROTO" = "1" ] && echo "== MODO ROTURA A PROPÓSITO: el reglaset deja salir también al cgroup en foco"
+echo "== armando el lab en $WORK"
+rm -rf "$WORK"; mkdir -p "$WORK/rootfs" "$WORK/art"
+tar xzf "$ROOTFS_TAR" -C "$WORK/rootfs"
+tar xzf "$ARTIFACTS_TAR" -C "$WORK/art"
+for a in "$WORK/art"/*; do cp -a "$a"/usr/. "$WORK/rootfs/usr/"; done
+[ -x "$WORK/rootfs/usr/sbin/nft" ] || { echo "✗ el artefacto no trae usr/sbin/nft" >&2; exit 1; }
+
+# ⚠ Que `nft` ARRANQUE se comprueba acá, primero. Si le falta una librería, el reglaset no se aplica
+# y las dos conexiones fallan — que desde afuera se lee EXACTAMENTE como «el foco funciona». Medido:
+# la primera corrida empaquetó un `gmp` viejo (sólo `libgmp.a`) y `libnftables.so` murió con
+# `__gmpz_com: symbol not found`. En el hub no se había notado porque ahí el lab tiene su propia
+# libgmp y la tapaba: el artefacto parecía autosuficiente y no lo era.
+if ! chroot "$WORK/rootfs" /usr/sbin/nft --version > "$WORK/version.txt" 2>&1; then
+ echo "✗ nuestro nft no arranca en este rootfs:"; sed 's/^/ /' "$WORK/version.txt"
+ echo " (típico: falta una librería del cierre — mirá los NEEDED de usr/lib/libnftables.so)"
+ exit 1
+fi
+echo " nft: $(cat "$WORK/version.txt")"
+
+mkdir -p "$CG_RAIZ/$CG_FOCO" "$CG_RAIZ/$CG_LIBRE"
+echo " cgroups: $CG_FOCO (en foco) · $CG_LIBRE (control)"
+
+cat > "$WORK/rootfs/reglas.nft" <<EOF
+# El reglaset que el §6.5 le pide al cortafuegos: egress DENEGADO salvo lo que se nombra.
+table inet takana_foco {
+ chain egress {
+ type filter hook output priority 0; policy drop;
+ ct state established,related accept
+ socket cgroupv2 level 1 "$CG_LIBRE" accept
+$( [ "$ROTO" = "1" ] && echo " socket cgroupv2 level 1 \"$CG_FOCO\" accept # ROTURA A PROPÓSITO" )
+ }
+}
+EOF
+
+# ⚠ HEREDOC ENTRECOMILLADO (<<'EOF') y los nombres por ENTORNO. No es estilo: con el heredoc
+# abierto, la máquina de afuera expande `$(...)`, `$VAR` **y los backticks**, incluso dentro de un
+# COMENTARIO. Medido el 2026-09-10: un comentario que decía «en alpine `ip` e `ifconfig` son enlaces
+# de busybox» ejecutó los dos comandos EN EL HOST y pegó la salida de `ifconfig` —con las IPs de la
+# máquina— dentro del guión generado. Un comentario que corre.
+cat > "$WORK/rootfs/adentro.sh" <<'EOF'
+#!/bin/sh
+# Corre YA dentro del netns y del mount ns privados, y del chroot. Los cgroups llegan por entorno
+# (CG_FOCO / CG_LIBRE): chroot conserva el entorno, así que no hace falta interpolar nada.
+set -u
+# /sbin en el PATH: en alpine, ip e ifconfig son enlaces de busybox en /sbin, y sin eso el
+# "ip link set lo up" no existe — el loopback queda caído y NINGUNA conexión sale. La línea base lo
+# cazó; sin ella, ese silencio se habría contado como «el foco funciona».
+export PATH=/usr/sbin:/usr/bin:/sbin:/bin LD_LIBRARY_PATH=/usr/lib
+PUERTO=45999
+
+ip link set lo up 2>/dev/null || ifconfig lo up 2>/dev/null
+echo "LO $(ip addr show lo 2>/dev/null | tr '\n' ' ' | tr -s ' ')"
+
+# Un listener por intento: "nc -l ... -e" atiende UNA conexión y se va, y un listener muerto se lee
+# igual que una conexión bloqueada. El listener vive en el cgroup permitido, así sus respuestas
+# salen por "established".
+intento() { # $1 = cgroup del cliente, $2 = etiqueta
+ sh -c "echo \$\$ > /sys/fs/cgroup/$CG_LIBRE/cgroup.procs; exec nc -l -p $PUERTO -e /bin/true" \
+ < /dev/null > /dev/null 2>&1 &
+ L=$!
+ sleep 1
+ sh -c "echo \$\$ > /sys/fs/cgroup/$1/cgroup.procs; exec nc -w 3 127.0.0.1 $PUERTO" \
+ < /dev/null > /dev/null 2>&1
+ echo "CONEXION $2 exit=$?"
+ kill $L 2>/dev/null
+ wait $L 2>/dev/null
+}
+
+# LÍNEA BASE, sin ninguna regla puesta: si esto ya falla, el harness está roto y cualquier veredicto
+# posterior sería un falso positivo del foco («no conectó» se lee como «el cortafuegos funciona»).
+intento "$CG_FOCO" linea-base
+
+nft -f /reglas.nft || { echo "RESULTADO nft-no-aplica"; exit 1; }
+echo "REGLAS $(nft list ruleset | tr '\n' ' ' | tr -s ' ')"
+
+intento "$CG_LIBRE" control
+intento "$CG_FOCO" foco
+EOF
+chmod +x "$WORK/rootfs/adentro.sh"
+
+echo "== corriendo dentro de un netns y un mount ns privados"
+# Las montaduras van ADENTRO del namespace y se van solas: nada que desmontar, nada que propagar.
+CG_FOCO="$CG_FOCO" CG_LIBRE="$CG_LIBRE" unshare -m --propagation private -n sh -c "
+ mount --rbind /sys '$WORK/rootfs/sys'
+ mount -t proc proc '$WORK/rootfs/proc'
+ exec chroot '$WORK/rootfs' /adentro.sh
+" > "$WORK/adentro.log" 2>&1 || true
+SALIDA="$(cat "$WORK/adentro.log")"
+echo "$SALIDA" | sed 's/^/ /'
+
+base="$(echo "$SALIDA" | sed -n 's/^CONEXION linea-base exit=//p')"
+libre="$(echo "$SALIDA" | sed -n 's/^CONEXION control exit=//p')"
+foco="$(echo "$SALIDA" | sed -n 's/^CONEXION foco exit=//p')"
+[ -n "$base" ] && [ -n "$libre" ] && [ -n "$foco" ] || { echo "✗ la corrida no dejó los tres veredictos"; exit 1; }
+if [ "$base" != "0" ]; then
+ echo "✗ HARNESS ROTO: sin ninguna regla puesta tampoco se pudo conectar (exit=$base)."
+ echo " Nada de lo que sigue mediría el foco: un «no conectó» se leería como éxito."
+ exit 1
+fi
+if [ "$libre" != "0" ]; then
+ echo "✗ CONTROL ROTO: con las reglas puestas, el cgroup PERMITIDO tampoco conectó (exit=$libre)"
+ echo " o sea que el reglaset niega de más: el foco no se distingue de cortar todo."
+ exit 1
+fi
+if [ "$foco" = "0" ]; then
+ echo "✗ el cgroup EN FOCO conectó igual: el cortafuegos no está sosteniendo nada"
+ exit 1
+fi
+echo
+echo "✓ la línea base conecta: el harness mide algo (exit=0 sin reglas puestas)"
+echo "✓ en foco no hay salida: el proceso del cgroup del navegador no abrió la conexión (exit=$foco)"
+echo "✓ y el control PASA: el mismo binario, en el cgroup de al lado, conectó (exit=0)"
+echo "✓ o sea que el foco lo sostiene una regla del sistema — nada que el navegador pueda apagar"