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: