qorpa §Orden 6: pressure-vessel ANIDA bajo la jaula — sin cuenta, sin juego y sin pantalla
El paso 6 quedaba parcial por esta frase: «pressure-vessel con un juego real sigue sin ejercitarse», porque el runtime sniper sólo se baja al instalar un juego y eso pide credenciales. Era un techo falso: el depot está publicado y ahora pineado, así que se coloca a mano y la pregunta que ordenaba D6 se responde entera. **La evidencia**, dentro de una instancia qorpa sobre Ubuntu base, con `nesting` y la jaula puesta: os-release Ubuntu 24.04.3 LTS → Steam Runtime 3 (sniper) ns de montaje mnt:[4026532468] → mnt:[4026532526] /usr/lib/x86_64-linux-gnu → 675 libs de sniper, libSDL2 incluida Contenedor de Valve anidado dentro del nuestro, con el runtime real adentro. **Y la concesión resultó de verdad:** con `nesting = false` el mismo comando muere en `bwrap: Creating new namespace failed: Operation not permitted`. Queda como guardián (`scripts/qorpa/pressure-vessel-probe.sh`), que exige las DOS mitades — sin la negativa, «no salió sniper» lo cumpliría también un cuelgue. Tres cicatrices del camino, todas en los comentarios: 1. **`ldd --version` es la comprobación equivocada** y es la primera que uno escribe: pressure-vessel importa la libc del host cuando es más nueva que la del runtime, así que ver la glibc de afuera adentro es lo correcto y no prueba nada. El veredicto es `os-release`. 2. **`SALIDA=$(timeout … qorpa run …)` se cuelga para siempre** aunque timeout mate al hijo: la sustitución no espera al PROCESO, espera a que se cierre el PIPE, y pressure-vessel deja descendientes con el fd abierto. A fichero termina y devuelve su código. 3. **Dos `run` seguidos sobre la misma instancia fallaban** con `Can't make overlay mount … Device or resource busy`. El kernel niega dos overlays vivos con el mismo `upper` porque eso corrompe la capa — o sea que el EBUSY es un guardián correcto a destiempo: la corrida anterior ya devolvió el prompt y su namespace no terminó de reaparse. `run` reintenta acotado y, si sigue tomado, dice la causa en vez de soltar el mensaje crudo de bwrap. El punto 3 salió porque el guardián exige evidencia POSITIVA de la denegación: con «no apareció sniper» habría dado OK tapando un fallo distinto. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QZjGRqvjWij9dew7NVFu4Q
This commit is contained in:
@@ -1295,18 +1295,33 @@ fn run_instance(
|
|||||||
// `exec` si no es CLOEXEC. En vez de arrastrar una dep de C para un `fcntl`, lo abre la shell:
|
// `exec` si no es CLOEXEC. En vez de arrastrar una dep de C para un `fcntl`, lo abre la shell:
|
||||||
// sus redirecciones no son CLOEXEC por definición, y de paso el `--dry-run` puede mostrar
|
// sus redirecciones no son CLOEXEC por definición, y de paso el `--dry-run` puede mostrar
|
||||||
// exactamente lo que pasa.
|
// exactamente lo que pasa.
|
||||||
let st = match &mapeado {
|
// ── EL OVERLAY PUEDE SEGUIR TOMADO POR LA CORRIDA ANTERIOR ─────────────────────────────────
|
||||||
Some(m) => Command::new("sh")
|
// MEDIDO: dos `run` seguidos sobre la MISMA instancia y el segundo muere con
|
||||||
.arg("-c")
|
// `bwrap: Can't make overlay mount … Device or resource busy`; el tercero anda.
|
||||||
.arg(r#"exec 3<"$1"; shift; exec bwrap --userns 3 "$@""#)
|
//
|
||||||
.arg("qorpa")
|
// No es un capricho de overlayfs: el kernel se NIEGA a montar dos overlays vivos que compartan
|
||||||
.arg(&m.ns_path)
|
// `upperdir`/`workdir` porque eso corrompe la capa. O sea que el EBUSY es un GUARDIÁN, no una
|
||||||
.args(&args)
|
// molestia — lo que está mal es el momento: la corrida anterior ya devolvió el prompt y su
|
||||||
.status()
|
// namespace todavía no terminó de reaparse, así que el usuario ve una denegación correcta por
|
||||||
.context("no pude ejecutar sh/bwrap")?,
|
// una razón que ya no existe. Se reintenta acotado; si de verdad hay otra `run` viva, los
|
||||||
None => Command::new("bwrap").args(&args).status()
|
// reintentos se agotan y se dice cuál es la causa en vez de dejar el mensaje crudo de bwrap.
|
||||||
.context("no pude ejecutar bwrap — ¿está en el PATH? (recipes/bwrap.toml)")?,
|
const INTENTOS: u32 = 6;
|
||||||
};
|
let mut st = None;
|
||||||
|
for intento in 1..=INTENTOS {
|
||||||
|
let (status, overlay_tomado) = lanzar_bwrap(&mapeado, &args)?;
|
||||||
|
if status.success() || !overlay_tomado {
|
||||||
|
st = Some(status);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if intento == INTENTOS {
|
||||||
|
bail!(
|
||||||
|
"el overlay de `{id}` sigue tomado tras {INTENTOS} intentos.\n ¿Hay otra `hammer qorpa run {id}` viva? Dos overlays con el mismo `upper` \n corromperían la capa mutable, y por eso el kernel lo niega."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
eprintln!(" el overlay lo tiene todavía la corrida anterior — reintento {intento}/{INTENTOS}");
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(200 * u64::from(intento)));
|
||||||
|
}
|
||||||
|
let st = st.expect("el bucle sale con estado o con bail");
|
||||||
match st.code() {
|
match st.code() {
|
||||||
Some(0) => Ok(()),
|
Some(0) => Ok(()),
|
||||||
Some(c) => std::process::exit(c),
|
Some(c) => std::process::exit(c),
|
||||||
@@ -1314,6 +1329,60 @@ fn run_instance(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Lanza bwrap y devuelve su estado junto con **si el fallo fue el overlay tomado**.
|
||||||
|
///
|
||||||
|
/// El stderr se PASA A TRAVÉS mientras se mira, byte a byte y sin esperar renglones: hay que leerlo
|
||||||
|
/// para distinguir ese fallo de cualquier otro, pero tragárselo sería peor que el bug —adentro
|
||||||
|
/// corre un shell interactivo y su prompt sale por stderr sin salto de línea—. Sólo se guardan los
|
||||||
|
/// primeros KB para buscar la firma; el resto se reenvía y se olvida.
|
||||||
|
fn lanzar_bwrap(
|
||||||
|
mapeado: &Option<UsernsMapeado>, args: &[String],
|
||||||
|
) -> Result<(std::process::ExitStatus, bool)> {
|
||||||
|
let mut c = match mapeado {
|
||||||
|
Some(m) => {
|
||||||
|
let mut c = Command::new("sh");
|
||||||
|
c.arg("-c")
|
||||||
|
.arg(r#"exec 3<"$1"; shift; exec bwrap --userns 3 "$@""#)
|
||||||
|
.arg("qorpa")
|
||||||
|
.arg(&m.ns_path)
|
||||||
|
.args(args);
|
||||||
|
c
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
let mut c = Command::new("bwrap");
|
||||||
|
c.args(args);
|
||||||
|
c
|
||||||
|
}
|
||||||
|
};
|
||||||
|
c.stderr(std::process::Stdio::piped());
|
||||||
|
let mut hijo = c
|
||||||
|
.spawn()
|
||||||
|
.context("no pude ejecutar bwrap — ¿está en el PATH? (recipes/bwrap.toml)")?;
|
||||||
|
let mut err = hijo.stderr.take().expect("stderr pedido arriba");
|
||||||
|
let hilo = std::thread::spawn(move || {
|
||||||
|
use std::io::Write;
|
||||||
|
let mut principio = Vec::new();
|
||||||
|
let mut buf = [0u8; 4096];
|
||||||
|
loop {
|
||||||
|
match err.read(&mut buf) {
|
||||||
|
Ok(0) | Err(_) => break,
|
||||||
|
Ok(n) => {
|
||||||
|
if principio.len() < 8192 {
|
||||||
|
principio.extend_from_slice(&buf[..n]);
|
||||||
|
}
|
||||||
|
let _ = std::io::stderr().write_all(&buf[..n]);
|
||||||
|
let _ = std::io::stderr().flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let t = String::from_utf8_lossy(&principio).into_owned();
|
||||||
|
t.contains("Can't make overlay mount") && t.contains("busy")
|
||||||
|
});
|
||||||
|
let st = hijo.wait().context("esperando a bwrap")?;
|
||||||
|
let overlay_tomado = hilo.join().unwrap_or(false);
|
||||||
|
Ok((st, overlay_tomado))
|
||||||
|
}
|
||||||
|
|
||||||
// ── prune: la poda (§Orden 7) ───────────────────────────────────────────────────────────────────
|
// ── prune: la poda (§Orden 7) ───────────────────────────────────────────────────────────────────
|
||||||
//
|
//
|
||||||
// Nace CON el subsistema y no después, a propósito: un rootfs son cientos de MB o varios GB, y no
|
// Nace CON el subsistema y no después, a propósito: un rootfs son cientos de MB o varios GB, y no
|
||||||
|
|||||||
@@ -505,6 +505,14 @@ Se escriben acá para que no se descubran en producción.
|
|||||||
`recreate` / `run`. El overlay lo monta bwrap (`--overlay-src` + `--overlay`) dentro de su
|
`recreate` / `run`. El overlay lo monta bwrap (`--overlay-src` + `--overlay`) dentro de su
|
||||||
propio namespace: sin root y sin montar nada en el host. `run` entra con `--clearenv` y con
|
propio namespace: sin root y sin montar nada en el host. `run` entra con `--clearenv` y con
|
||||||
`--unshare-net` salvo que se declare `network` — el entorno del host también es una concesión.
|
`--unshare-net` salvo que se declare `network` — el entorno del host también es una concesión.
|
||||||
|
|
||||||
|
**Corregido el mismo día, y el bug era una verdad a destiempo:** dos `run` seguidos sobre la
|
||||||
|
misma instancia y el segundo moría con `Can't make overlay mount … Device or resource busy`; el
|
||||||
|
tercero andaba. El kernel se **niega a propósito** a montar dos overlays vivos que compartan
|
||||||
|
`upperdir`/`workdir` —eso corrompe la capa—, así que el EBUSY es un guardián correcto: lo que
|
||||||
|
estaba mal era el momento, porque la corrida anterior ya devolvió el prompt y su namespace
|
||||||
|
todavía no terminó de reaparse. Ahora `run` reintenta acotado y, si el overlay sigue tomado,
|
||||||
|
dice **la causa** («¿hay otra `run` viva?») en vez de dejar pasar el mensaje crudo de bwrap.
|
||||||
4. ✅ **HECHO 2026-09-03.** Concesiones → política de harkaq. `harkaq-exec` entra como último
|
4. ✅ **HECHO 2026-09-03.** Concesiones → política de harkaq. `harkaq-exec` entra como último
|
||||||
eslabón dentro de bwrap (cruza un binario **estático**, no una librería ⇒ D2 sigue en pie), y
|
eslabón dentro de bwrap (cruza un binario **estático**, no una librería ⇒ D2 sigue en pie), y
|
||||||
aporta lo que bwrap no da: **seccomp** —hoy una instancia podría `io_uring`, `bpf`, `ptrace`,
|
aporta lo que bwrap no da: **seccomp** —hoy una instancia podría `io_uring`, `bpf`, `ptrace`,
|
||||||
@@ -524,6 +532,34 @@ Se escriben acá para que no se descubran en producción.
|
|||||||
**Lo que NO se pudo, y por qué:** la máquina no tiene ninguna sesión gráfica (`/dev/dri` sí,
|
**Lo que NO se pudo, y por qué:** la máquina no tiene ninguna sesión gráfica (`/dev/dri` sí,
|
||||||
socket Wayland no), así que Steam no dibuja; y el runtime *sniper* sólo se baja al instalar un
|
socket Wayland no), así que Steam no dibuja; y el runtime *sniper* sólo se baja al instalar un
|
||||||
juego, lo que exige credenciales. **pressure-vessel con un juego real sigue sin ejercitarse.**
|
juego, lo que exige credenciales. **pressure-vessel con un juego real sigue sin ejercitarse.**
|
||||||
|
|
||||||
|
**✅ AMPLIADO 2026-09-03: pressure-vessel SÍ se ejercitó — sin cuenta, sin juego y sin pantalla.**
|
||||||
|
Lo que lo destrabó fue pinear el depot (D8): `SteamLinuxRuntime_sniper.tar.xz` es lo que Steam
|
||||||
|
despliega, y colocado a mano no hace falta comprar nada. Corrido dentro de una instancia qorpa
|
||||||
|
*sobre Ubuntu base*, con `nesting` y la jaula puesta:
|
||||||
|
|
||||||
|
| evidencia | fuera de pressure-vessel | dentro |
|
||||||
|
|---|---|---|
|
||||||
|
| `/etc/os-release` | `Ubuntu 24.04.3 LTS` | **`Steam Runtime 3 (sniper)`** |
|
||||||
|
| namespace de montaje | `mnt:[4026532468]` | **`mnt:[4026532526]`** |
|
||||||
|
| `/usr/lib/x86_64-linux-gnu` | el de Ubuntu | **675 libs de sniper, `libSDL2-2.0.so.0` incluida** |
|
||||||
|
|
||||||
|
O sea: **contenedor de Valve anidado dentro de nuestra jaula, con el runtime real adentro.** Es
|
||||||
|
la pregunta que ordenaba D6 y ya no está abierta.
|
||||||
|
|
||||||
|
**Y la concesión resultó ser de verdad, no un adorno:** con `nesting = false` el mismo comando
|
||||||
|
muere en `bwrap: Creating new namespace failed: Operation not permitted`. Una concesión que no
|
||||||
|
se puede apagar no es una concesión.
|
||||||
|
|
||||||
|
Tres detalles que sólo salen corriéndolo: pressure-vessel avisa `Cannot determine ld.so for
|
||||||
|
i386-linux-gnu` porque Ubuntu base no trae multilib —para juegos la base es Arch, que es
|
||||||
|
justamente por lo que Arch está en la terna—; `ldd --version` sigue diciendo la glibc de la
|
||||||
|
instancia y **está bien**, porque pressure-vessel importa la libc del host cuando es más nueva
|
||||||
|
que la del runtime, así que la prueba de que el runtime entró es `os-release`, no `ldd`; y no
|
||||||
|
hay ICD de Vulkan porque no se concedió `/dev/dri` — la jaula no regala dispositivos.
|
||||||
|
|
||||||
|
**Lo que sigue faltando, y ahora es sólo esto:** un juego real dibujando, que pide sesión
|
||||||
|
gráfica y credenciales. Ninguna de las dos es una pregunta de diseño.
|
||||||
7. ✅ **HECHO 2026-09-03.** `hammer qorpa prune` + el renglón en
|
7. ✅ **HECHO 2026-09-03.** `hammer qorpa prune` + el renglón en
|
||||||
[SDD 20](../20-catalogo-publicable-y-completa.md#imágenes-ajenas-qorpa-fuera-del-catálogo-y-por-escrito).
|
[SDD 20](../20-catalogo-publicable-y-completa.md#imágenes-ajenas-qorpa-fuera-del-catálogo-y-por-escrito).
|
||||||
Poda restos de pulls a medias, imágenes que ninguna instancia usa (se re-traen por digest: es la
|
Poda restos de pulls a medias, imágenes que ninguna instancia usa (se re-traen por digest: es la
|
||||||
|
|||||||
Executable
+98
@@ -0,0 +1,98 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Prueba que pressure-vessel —el contenedor con el que Valve corre los juegos— arranca ANIDADO
|
||||||
|
# dentro de una instancia qorpa enjaulada. ADR 0015 §Orden 6 / D6.
|
||||||
|
#
|
||||||
|
# ── POR QUÉ ESTE GUARDIÁN ───────────────────────────────────────────────────────────────────────
|
||||||
|
# El paso 6 del ADR quedó PARCIAL con esta frase: «pressure-vessel con un juego real sigue sin
|
||||||
|
# ejercitarse», porque el runtime sniper sólo se baja al instalar un juego y eso pide credenciales.
|
||||||
|
# Falso techo: el depot está publicado y pineado (`docs/state/qorpa-imagenes.toml`), así que se
|
||||||
|
# coloca a mano y **no hace falta cuenta, ni juego, ni pantalla** para responder la pregunta que
|
||||||
|
# ordenaba el diseño — ¿nuestra jaula deja anidar la de Valve?
|
||||||
|
#
|
||||||
|
# ── QUÉ MIDE, Y POR QUÉ ESO Y NO OTRA COSA ──────────────────────────────────────────────────────
|
||||||
|
# El veredicto es `/etc/os-release` dentro del contenedor: si dice «Steam Runtime 3 (sniper)», el
|
||||||
|
# runtime REAL se montó. NO se mide con `ldd --version`, que es la comprobación que uno escribiría
|
||||||
|
# primero y sale mal: pressure-vessel **importa la libc del host cuando es más nueva** que la del
|
||||||
|
# runtime, así que ver la glibc de la instancia adentro es lo correcto y no prueba nada.
|
||||||
|
#
|
||||||
|
# Y mide la contraparte: con `nesting = false` el mismo comando tiene que MORIR. Una concesión que
|
||||||
|
# no se puede apagar no es una concesión — sin este segundo tramo, el guardián diría «anida» aunque
|
||||||
|
# la jaula no estuviera haciendo nada.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
QORPA_ROOT="${HAMMER_QORPA_ROOT:-/var/lib/hammer/qorpa}"
|
||||||
|
HAMMER="${HAMMER_BIN:-./target/release/hammer}"
|
||||||
|
ID="${1:-sniper-probe}"
|
||||||
|
|
||||||
|
UBUNTU_URL="https://cdimage.ubuntu.com/ubuntu-base/releases/24.04/release/ubuntu-base-24.04.3-base-amd64.tar.gz"
|
||||||
|
UBUNTU_SHA="6bc2cde3930ad088b3bb46fa45279e96d25bc3810f209850ecbe4722711874f9"
|
||||||
|
DEPOT_URL="https://repo.steampowered.com/steamrt-images-sniper/snapshots/3.0.20260805.254768/SteamLinuxRuntime_sniper.tar.xz"
|
||||||
|
DEPOT_SHA="e264f0639ab775338311036f207b35cebe99bc417b016b53931ebca8b30b3d94"
|
||||||
|
|
||||||
|
export HAMMER_QORPA_ROOT="$QORPA_ROOT"
|
||||||
|
echo "== qorpa root: $QORPA_ROOT"
|
||||||
|
|
||||||
|
# 1. Las dos piezas, por digest. `pull` verifica ANTES de desempacar y es idempotente.
|
||||||
|
"$HAMMER" qorpa pull "$UBUNTU_URL" --sha256 "$UBUNTU_SHA" --label ubuntu-base-24.04.3
|
||||||
|
# El depot NO es un rootfs (trae `run`, `pressure-vessel/` y la imagen adentro): se trae verificado
|
||||||
|
# y sin desempacar. Traerlo a secas FALLA el anclaje por estructura, que es lo que tiene que hacer.
|
||||||
|
"$HAMMER" qorpa pull "$DEPOT_URL" --sha256 "$DEPOT_SHA" --label steamlinuxruntime-sniper --verify-only
|
||||||
|
|
||||||
|
INST="$QORPA_ROOT/instances/$ID"
|
||||||
|
if [ ! -f "$INST/instance.toml" ]; then
|
||||||
|
"$HAMMER" qorpa create "$ID" --base "$UBUNTU_SHA" --distro ubuntu-24.04
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 2. El depot va al `upper`, que es caché por D3: si esto se pierde, se rehace con este script.
|
||||||
|
# `--delay-directory-restore` no es un adorno — sin él, tar sin ser root no puede terminar de
|
||||||
|
# llenar un directorio cuyo modo final es de sólo lectura.
|
||||||
|
if [ ! -x "$INST/upper/opt/SteamLinuxRuntime_sniper/run" ]; then
|
||||||
|
mkdir -p "$INST/upper/opt"
|
||||||
|
tar -xf "$QORPA_ROOT/images/$DEPOT_SHA/archive" --delay-directory-restore -C "$INST/upper/opt"
|
||||||
|
fi
|
||||||
|
|
||||||
|
DENTRO='cd /opt/SteamLinuxRuntime_sniper && ./run -- /bin/sh -c "grep ^PRETTY_NAME= /etc/os-release; readlink /proc/self/ns/mnt"'
|
||||||
|
LOG=$(mktemp -d)
|
||||||
|
trap 'rm -rf "$LOG"' EXIT
|
||||||
|
|
||||||
|
# ── LA SALIDA VA A FICHERO, NO A `$( )` ─────────────────────────────────────────────────────────
|
||||||
|
# MEDIDO acá, y cuesta una hora si no se sabe: `SALIDA=$(timeout … hammer qorpa run …)` **se cuelga
|
||||||
|
# para siempre** aunque `timeout` mate al hijo. La sustitución de comandos no espera al PROCESO,
|
||||||
|
# espera a que se cierre el PIPE, y pressure-vessel deja descendientes (su logger) con el fd
|
||||||
|
# abierto. Con redirección a fichero el mismo comando termina y devuelve su código. Es la misma
|
||||||
|
# familia que la cicatriz del preflight: el guardián que se cuelga o miente es peor que no tenerlo.
|
||||||
|
corre() { # corre <fichero-de-salida> <segundos>
|
||||||
|
timeout -k 5 "$2" "$HAMMER" qorpa run "$ID" -- /bin/sh -c "$DENTRO" > "$1" 2>&1 < /dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
# 3. Con la concesión: tiene que aparecer el runtime de Valve.
|
||||||
|
sed -i 's/^nesting = false/nesting = true/' "$INST/instance.toml"
|
||||||
|
echo "== con nesting = true"
|
||||||
|
corre "$LOG/si" 300
|
||||||
|
grep -E "PRETTY_NAME|mnt:" "$LOG/si" || true
|
||||||
|
if ! grep -q "Steam Runtime 3 (sniper)" "$LOG/si"; then
|
||||||
|
tail -20 "$LOG/si"
|
||||||
|
echo "FALLA: pressure-vessel no montó el runtime sniper" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 4. Sin la concesión: tiene que MORIR. Si esto pasa, la jaula no está haciendo nada.
|
||||||
|
sed -i 's/^nesting = true/nesting = false/' "$INST/instance.toml"
|
||||||
|
echo "== con nesting = false (tiene que fallar)"
|
||||||
|
corre "$LOG/no" 120
|
||||||
|
sed -i 's/^nesting = false/nesting = true/' "$INST/instance.toml"
|
||||||
|
if grep -q "Steam Runtime 3 (sniper)" "$LOG/no"; then
|
||||||
|
echo "FALLA: anidó SIN la concesión — \`nesting\` no está conteniendo nada" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# NO alcanza con «no salió sniper»: un cuelgue, un timeout o una ruta mal escrita también dejan un
|
||||||
|
# log sin esa línea, y el guardián diría OK sin haber probado nada. Se exige la EVIDENCIA POSITIVA
|
||||||
|
# de la denegación — el mismo criterio de evidencia negativa de harkaq.
|
||||||
|
if ! grep -q "Creating new namespace failed" "$LOG/no"; then
|
||||||
|
tail -20 "$LOG/no"
|
||||||
|
echo "FALLA: sin \`nesting\` no salió el rechazo esperado del kernel — no se probó nada" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
grep -o "bwrap: Creating new namespace failed.*" "$LOG/no" | head -1
|
||||||
|
|
||||||
|
echo "OK: pressure-vessel anida bajo la jaula con \`nesting\`, y NO anida sin ella"
|
||||||
Reference in New Issue
Block a user