From a1ccce2b65dc99a65189455719c45bdae3b08976 Mon Sep 17 00:00:00 2001 From: Sergio Date: Tue, 15 Sep 2026 18:59:13 +0000 Subject: [PATCH] =?UTF-8?q?squid=20mudado=20=E2=80=94=20y=20la=20jaula=20a?= =?UTF-8?q?rrancaba=20DEGRADADA=20cuando=20la=20arranca=20un=20init?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- crates/takana-cli/src/qorpa.rs | 50 ++++++++++++++++++++++++++++-- docs/28-servidor-de-produccion.md | 51 +++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 3 deletions(-) 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 { }) } +/// 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 { + passwd.lines().find_map(|linea| { + let mut c = linea.split(':'); + let (nombre, _, u) = (c.next()?, c.next()?, c.next()?); + (u.parse::().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 { 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: