gioser

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…

squid mudado — y la jaula arrancaba DEGRADADA cuando la arranca un init

El proxy de salida sirve desde la caja: `squid 7.7` en 2.29.29.217:1137 con la config, el `passwd`
de los tres usuarios y las dos ACL de gioser tal cual, dentro de una instancia qorpa y supervisado
por arje. El de gioser sigue vivo e intacto (7.6). Es un servicio con gente encima: el access.log
del origen mostraba CONNECT a api.anthropic.com y a claude.ai en el minuto anterior a mudarlo.

Control en los dos sentidos: desde una IP no autorizada, 407 igual que el original; desde localhost,
que su propia config permite, TCP_TUNNEL/200 a claude.ai y api.anthropic.com.

EL MURO QUE VALE, y es un bug de qorpa: **el rango de subuid se buscaba por `$USER`**. Una shell
interactiva lo trae; un init NO. Bajo arje la tarjeta arranca con PATH y HOME, el nombre salia
vacio, no habia rango, y la jaula caia al userns de UN SOLO ID — con lo que squid moria en
`setgid(15): (22) Invalid argument` y el supervisor lo reintentaba 80 veces. El aviso de qorpa
estaba impreso y era correcto («no hay rango para "" en /etc/subuid»), pero lo que se ve en el bucle
es el error de squid: **la degradacion silenciosa la paga el de mas abajo**. Y el MISMO comando a
mano funcionaba, porque la shell si pone `$USER`: el sintoma dependia de quien lo arrancaba.

⇒ `nombre_de_usuario()` deriva el nombre del UID REAL leyendo /etc/passwd y el entorno queda como
ultimo recurso, con la mitad pura (`nombre_en_passwd`) probada, control negativo incluido: un uid
que no esta NO inventa un nombre — devolver algo ahi haria buscar un rango ajeno.

Los otros tres muros quedan documentados en §6.27: provisionar ANTES de montar la config (pacman
aborta si el paquete no puede escribir sus defaults), `/dev/shm` que bwrap crea 0755 y squid no
puede usar al bajar a `proxy`, y el pidfile que en una jaula con `--unshare-pid` SIEMPRE parece
fresco porque cada corrida tiene su propio PID 2.

Y la pregunta que corresponde —¿no va en una receta?— con su respuesta: si, y sigue pendiente. La
jaula fue la via urgente. La diferencia con php-fpm importa: PHP no queremos que entre al corpus,
squid es C y su sitio natural es una receta como la de gitea.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

a1ccce2b65dc99a65189455719c45bdae3b08976 · por Sergio · (hace 1 mes) · padre e7369efe8 · ver el árbol en este commit

Delta semántico · 4

crates/takana-cli/src/qorpa.rs

2 archivos · +98 −3

diff --git a/crates/takana-cli/src/qorpa.rs b/crates/takana-cli/src/qorpa.rs
index fc587f5b..65230406 100644
--- a/crates/takana-cli/src/qorpa.rs
+++ b/crates/takana-cli/src/qorpa.rs
@@ -1335,6 +1335,40 @@ fn leer_subid(fichero: &str, usuario: &str) -> Option<Rango> {
     })
 }
 
+/// La mitad PURA de `nombre_de_usuario`: el primer nombre de `/etc/passwd` con ese uid.
+fn nombre_en_passwd(passwd: &str, uid: u32) -> Option<String> {
+    passwd.lines().find_map(|linea| {
+        let mut c = linea.split(':');
+        let (nombre, _, u) = (c.next()?, c.next()?, c.next()?);
+        (u.parse::<u32>().ok()? == uid).then(|| nombre.to_string())
+    })
+}
+
+/// El nombre del usuario que corre esto, para buscar su rango en `/etc/subuid`.
+///
+/// ⚠ MEDIDO el 2026-09-15: esto leía `$USER`/`$LOGNAME` y nada más. Una shell interactiva los trae;
+/// **un init NO**. Al poner `squid` bajo arje-zero, la tarjeta arrancó con `PATH`, `HOME` y poco
+/// más, el nombre salió vacío, no hubo rango, y la jaula cayó al **userns de un solo id** — con lo
+/// que squid murió en `setgid(15): (22) Invalid argument`. El aviso estaba y era correcto («no hay
+/// rango para "" en /etc/subuid»), pero el servicio se caía en bucle y el error que se veía era el
+/// de squid, no el nuestro: **la degradación silenciosa la paga el de más abajo**.
+///
+/// Ahora el nombre se deriva del **uid real** leyendo `/etc/passwd`, y el entorno es sólo un atajo.
+/// El uid es el hecho; `$USER` es una etiqueta que cualquiera puede no poner (ver la nota de
+/// `la-etiqueta-no-es-el-hecho`).
+fn nombre_de_usuario(uid: u32) -> String {
+    if let Ok(passwd) = std::fs::read_to_string("/etc/passwd") {
+        if let Some(n) = nombre_en_passwd(&passwd, uid) {
+            return n;
+        }
+    }
+    // Último recurso: el entorno. Sirve cuando `/etc/passwd` no describe a este uid (un rootfs
+    // pelado, un uid prestado), y no estorba cuando sí.
+    std::env::var("USER")
+        .or_else(|_| std::env::var("LOGNAME"))
+        .unwrap_or_default()
+}
+
 /// El namespace vivo, con su tenedor. Al soltarlo, se mata al tenedor y el namespace desaparece.
 struct UsernsMapeado {
     tenedor: std::process::Child,
@@ -1365,9 +1399,7 @@ fn userns_con_rango(uid_dentro: u32) -> Result<UsernsMapeado> {
     let gid = std::fs::metadata("/proc/self")
         .map(|m| std::os::unix::fs::MetadataExt::gid(&m))
         .unwrap_or(1000);
-    let usuario = std::env::var("USER")
-        .or_else(|_| std::env::var("LOGNAME"))
-        .unwrap_or_default();
+    let usuario = nombre_de_usuario(uid);
     let ru = leer_subid("/etc/subuid", &usuario)
         .with_context(|| format!("no hay rango para {usuario:?} en /etc/subuid"))?;
     let rg = leer_subid("/etc/subgid", &usuario)
@@ -2790,6 +2822,18 @@ mod tests {
         assert!(a.iter().any(|x| x == "/etc/resolv.conf"));
     }
 
+    #[test]
+    fn el_usuario_sale_del_uid_y_no_del_entorno() {
+        let passwd = "root:x:0:0::/root:/bin/sh\nhttp:x:33:33::/srv/http:/usr/bin/nologin\n";
+        assert_eq!(nombre_en_passwd(passwd, 0).as_deref(), Some("root"));
+        assert_eq!(nombre_en_passwd(passwd, 33).as_deref(), Some("http"));
+        // El control que importa: un uid que no está NO inventa un nombre. Devolver algo aquí
+        // haría buscar un rango de subuid ajeno, que es peor que no encontrar ninguno.
+        assert_eq!(nombre_en_passwd(passwd, 1000), None);
+        // Una línea rota no puede tumbar el arranque de una jaula.
+        assert_eq!(nombre_en_passwd("basura\n\n:::\n", 0), None);
+    }
+
     #[test]
     fn los_padres_que_faltan_se_crean_y_los_de_la_imagen_no_se_tocan() {
         // El control que importa es el NEGATIVO: si esto devolviera los directorios que la imagen
diff --git a/docs/28-servidor-de-produccion.md b/docs/28-servidor-de-produccion.md
index 89eab81f..1ce3794e 100644
--- a/docs/28-servidor-de-produccion.md
+++ b/docs/28-servidor-de-produccion.md
@@ -1593,6 +1593,57 @@ todavía no está**: el camino se abrió a mano en el `upper` de la instancia (`
   `projects` — y ahí vive la memoria, **indexada POR RUTA del repo**. Mudarlo es una decisión sobre
   qué se lleva, no una copia.
 
+### 6.27 🧦 `squid` mudado — y la jaula que arrancaba DEGRADADA cuando la arranca un init *(2026-09-15)*
+
+**El proxy de salida sirve desde la caja**: `squid 7.7` en `2.29.29.217:1137`, con la config, el
+`passwd` de los tres usuarios y las dos ACL de gioser tal cual, dentro de la instancia `squid` y
+supervisado por arje. **El de gioser sigue vivo e intacto** (7.6), así que los clientes que apuntan
+a la IP vieja no se enteraron.
+
+Es un servicio con gente encima: el `access.log` del origen mostraba `CONNECT api.anthropic.com` y
+`CONNECT claude.ai` **en el minuto anterior a mudarlo**, desde IPs de Venezuela y con tres usuarios
+autenticados. Por eso se mudó entero y sin reescribir nada.
+
+Control en los dos sentidos: desde una IP no autorizada, **407 Proxy Authentication Required**
+—igual que el original—; desde `localhost`, que su propia config permite, **TCP_TUNNEL/200 a
+claude.ai y a api.anthropic.com**.
+
+#### Los cuatro muros, y el cuarto es el que vale
+
+1. **`provision` con la config ya montada aborta**: pacman dice «squid.conf.default exists in
+   filesystem» porque el paquete quiere escribir sus defaults donde está la concesión. ⇒ provisionar
+   **primero**, montar **después**. Queda escrito en el manifiesto.
+2. **`/dev/shm` lo crea bwrap 0755 root**, y squid —que baja a `proxy`— muere con
+   `FATAL: shm_open(...): (13) Permission denied`. Un `chmod 1777` en el arranque.
+3. **El fichero de PID sobrevive entre corridas y SIEMPRE parece fresco**: cada `run` tiene su propio
+   namespace de PIDs, así que el `/run/squid.pid` de la corrida anterior dice `PID 2`… y en la nueva
+   hay un PID 2 vivo. Squid concluye «Squid is already running» y se niega. ⇒ `rm -f` en el arranque.
+   Es una trampa general de cualquier servicio con pidfile dentro de una jaula con `--unshare-pid`.
+4. 🧨 **El rango de subuid se buscaba por `$USER` — y un init no lo pone.** Bajo arje la tarjeta
+   arranca con `PATH` y `HOME` y poco más; el nombre salía **vacío**, no había rango, y la jaula caía
+   al **userns de un solo id**. Squid moría en `setgid(15): (22) Invalid argument` y el supervisor lo
+   reintentaba 80 veces. El aviso de qorpa era correcto y estaba impreso —«no hay rango para `""` en
+   /etc/subuid»— pero **lo que se ve en el bucle es el error de squid, no el nuestro: la degradación
+   silenciosa la paga el de más abajo**. Y el mismo comando a mano, desde una shell con `$USER`,
+   funcionaba perfecto: *el síntoma dependía de quién lo arrancaba*.
+   ⇒ `nombre_de_usuario()` deriva el nombre del **uid real** leyendo `/etc/passwd`, y el entorno pasa
+   a ser el último recurso. El uid es el hecho; `$USER` es una etiqueta ([[la-etiqueta-no-es-el-hecho]]).
+   Las tarjetas llevan además `USER`/`LOGNAME` explícitos, que era el arreglo del día.
+
+⚠ **`arjectl start` sobre un Ente ya vivo lo DUPLICA** (se vio con `gioser-php`: dos entes, uno
+peleando por el mismo `upper`). Y matar el proceso de adentro **no libera la instancia**: el
+`takana qorpa run` de afuera sigue vivo y el siguiente `run` choca. Para relanzar, `stop` y después
+`start`, mirando `status` en el medio.
+
+#### Y la pregunta que corresponde: ¿no va en una receta?
+
+**Sí, y sigue pendiente.** El propio §6.2 pone a `squid` en la lista de recetas que esta mudanza
+tiene que producir. Lo de hoy fue la vía URGENTE —el servicio estaba en uso y la jaula lo levanta en
+minutos, sin compilar nada—, no la respuesta final. La diferencia con `php-fpm` importa:
+**PHP no queremos que entre al corpus** (es un intérprete ajeno al proyecto, y la jaula es su lugar
+definitivo), mientras que **`squid` es C, se compila con el lab y su sitio natural es una receta**
+como la de `gitea`. Cuando esté, la instancia se tira: el manifiesto es descartable a propósito.
+
 ## 7. Reusar los scripts que ya existen, y no escribir de nuevo
 
 Pedido explícito del usuario. El inventario de lo que ya hace el trabajo: