Rebelión en la granja — investigación y laboratorio de dispositivos

Investigación sobre granjas de dispositivos y automatización completa para
levantar un laboratorio Android sobre un chasis de blades Debian.

Conclusión que ordena el repositorio: el cuello de botella nunca fue el
cómputo sino la identidad. Desde que la atestación de dispositivo pasó de
estadística a criptográfica, un servidor no puede hacerse pasar por un
teléfono — le falta una clave grabada en fábrica que ningún software produce.

Contenido:

- 01_investigacion/ — capa de identidad (verificación telefónica, eSIM,
  reputación de IP, atestación por hardware, correlación entre cuentas),
  mercado y players, stack técnico, marco legal en la UE
- 02_device_lab/ansible/ — Ansible para 16 blades: preflight de solo lectura
  que identifica el hardware por DMI, despliegue de redroid en Docker y
  control de flota por adb. Solo ansible.builtin, sin colecciones externas
- docs/ — requisitos, costes y repaso de seguridad con 11 hallazgos
- 03_charla/ — guion para HackMadrid / Hackmeeting València

Estado: la automatización está escrita pero SIN ejecutar contra hardware.
YAML validado, plantillas renderizadas en ambos modos de red y fleet.sh
comprobado con bash -n. El modelo de blade sigue sin confirmar.
This commit is contained in:
s1to 2026-08-08 19:42:14 +02:00
commit 74bfbe9437
42 changed files with 2989 additions and 0 deletions

26
.gitignore vendored Normal file
View file

@ -0,0 +1,26 @@
# Inventario real: IPs de los blades y posibles credenciales.
# Solo se versiona hosts.yml.example
02_device_lab/ansible/inventory/hosts.yml
# Informes de preflight: inventario real del hardware
02_device_lab/ansible/reports/
# Secretos
*.vault
vault.yml
vault.yaml
.vault_pass
*.retry
# Volúmenes de las instancias Android y binarios
data/
*.apk
*.img
# Ruido del sistema y de editores
.DS_Store
*.swp
*~
.idea/
.vscode/
.claude/

View file

@ -0,0 +1,282 @@
# La capa de identidad: verificación, reputación de IP y atestación
El documento central del proyecto. Explica **por qué la identidad, y no el cómputo, es el único
cuello de botella real** de una granja — y cómo las tres capas que la componen se integran entre
sí para derrotarla.
> **Alcance.** Esto describe cómo funcionan los mecanismos y por qué las granjas pierden contra
> ellos. No es un manual operativo de alta masiva de cuentas: no hay proveedores de SIM, ni
> técnicas de evasión de fingerprint, ni calendarios de maduración. Esa parte es fraude de
> identidad a escala y está bajo persecución activa en la UE — ver [legal_ue.md](legal_ue.md).
> Lo que sigue sirve para investigar, para defender y para dar la charla; para lo otro, no.
---
## 0 · La tesis en una frase
Una plataforma no te pregunta *quién eres*. Te pregunta tres cosas a la vez —
**¿controlas un número?**, **¿desde dónde llegas?**, **¿qué dispositivo eres?** — y luego,
la que de verdad mata: **¿en qué te pareces a los otros 44?**
Las tres primeras se pueden atacar con dinero. La cuarta no, y es la que hunde el negocio.
---
## 1 · Verificación telefónica
### Qué se está comprobando de verdad
El OTP por SMS no verifica tu identidad. Verifica que **controlas un número en el momento del
alta**. Es una prueba de posesión, no de identidad. Pero detrás del número hay una cadena que sí
lleva a una persona:
```
número → operador → contrato o prepago registrado → KYC → persona
```
En la UE, esa cadena existe casi siempre. En España el registro de prepago es obligatorio desde
2007. Es el motivo por el que el eslabón telefónico es el que rompe las investigaciones: la
plataforma no sabe quién eres, pero el operador sí, y a la policía se lo cuenta.
### Por qué el mercado usa SIM físicas
Las granjas no compran números uno a uno: compran **acceso a números por API**. Detrás hay
SIM-boxes — bastidores con cientos de ranuras de SIM físicas y un módem por ranura — con una capa
de software que expone "dame un número de país X" y "dame el SMS que acaba de llegar al número Y".
El suministro es prepago comprado a granel en jurisdicciones donde el registro es débil o
evitable. Eso es lo que se revende como *número como servicio*.
### La eSIM: por qué NO es la respuesta
Es la pregunta que más se repite y la respuesta es contraintuitiva. **La eSIM es peor para una
granja que la SIM física**, y hay que entender el aprovisionamiento para ver por qué.
Una eSIM no es un fichero que se copia. Es un **perfil** que se descarga a un chip concreto:
```
eUICC (chip físico soldado, con su EID único de fábrica)
│ descarga de perfil autenticada contra el EID
SM-DP+ del operador ← contrato / alta ← KYC
```
Tres consecuencias que lo descartan:
1. **El EID es una identidad de hardware.** Cada descarga de perfil queda asociada a un chip
físico concreto, registrado por el operador y por el SM-DP+. Donde la SIM prepago de plástico
es anónima hasta el registro, la eSIM nace **atada a un identificador de hardware trazable**.
2. **El aprovisionamiento deja rastro contractual.** Necesitas un código de activación emitido por
un operador, y para eso, una relación con él. Justo lo que una granja quiere evitar.
3. **No escala como el plástico.** Un SIM-box acepta cientos de tarjetas de plástico intercambiables
en segundos. Los perfiles eSIM se aprovisionan uno a uno contra EIDs distintos, y moverlos entre
dispositivos depende de que el operador lo permita.
Resumen: la eSIM cambia un consumible anónimo y barato por un identificador de hardware trazable
con contrato detrás. **Por eso el mercado sigue con plástico** — no por inercia, por diseño.
### Y aquí está el problema de fondo para un rack
Un contenedor redroid **no tiene banda base**. No hay módem, no hay IMEI legítimo, no puede
recibir un SMS. Nunca. Así que el número y el dispositivo están **necesariamente desacoplados**:
el SMS llega a un SIM-box en otro sitio y alguien lo teclea en el Android.
Ese desacople es en sí mismo una señal. La plataforma puede comprobar la coherencia entre el
prefijo del número, el operador que lo emite, la geolocalización de la IP, el idioma del sistema,
la zona horaria y el teclado. En un móvil real de una persona real, todo eso concuerda solo. En una
granja, concuerda solo si alguien lo hace concordar, en cada cuenta, cada vez.
### Por qué es el eslabón que cae
**Operación SIMCARTEL**, Europol, octubre de 2025: 1.200 SIM-boxes, 40.000 SIMs activas,
49 millones de cuentas falsas, 7 detenidos, gogetsms.com y apisim.com incautados.
No cayeron por la parte técnica. Cayeron por **la capa SIM**: las tarjetas tienen KYC en origen y
el dinero de comprarlas deja rastro. Detalle en [legal_ue.md](legal_ue.md).
---
## 2 · Reputación de IP
La segunda pregunta: *¿desde dónde llegas?*
### Las tres clases de dirección
Toda IP pública cae en una de tres categorías, y las plataformas las tratan de forma radicalmente
distinta:
| Clase | Qué es | Cómo la trata la plataforma |
|---|---|---|
| **Datacenter** | AWS, Hetzner, OVH, cualquier hosting | Bandera roja inmediata en un alta de consumidor. Nadie se abre Instagram desde un rack |
| **Residencial** | ISP doméstico — Movistar fibra, Vodafone | Normal. Es de donde viene la gente |
| **Móvil** | Red celular — datos de Movistar, Orange | **La más limpia de todas.** Y por una razón estructural |
### Por qué la IP móvil es el producto premium
No es que sea "más confiable". Es que **la plataforma no puede castigarla**.
Los operadores móviles usan **CGNAT**: miles de abonados comparten una misma IP pública. Bloquear
esa IP significa bloquear a miles de clientes legítimos a la vez. Ninguna plataforma puede
permitírselo.
Ese es todo el secreto del mercado de proxies móviles: no vende anonimato, vende **inmunidad al
bloqueo por daño colateral**.
### De dónde salen los proxies residenciales
Aquí está la parte turbia del sector, y es material de charla. La mayoría del inventario
residencial no viene de servidores: viene de **SDKs embebidos en apps gratuitas y VPNs
"gratuitas"**, que convierten el móvil del usuario en un nodo de salida a cambio de que la app sea
gratis. El consentimiento suele estar enterrado en unos términos que nadie lee.
Es decir: cuando alguien compra tráfico residencial, muy probablemente está saliendo por la
conexión de una persona que no lo sabe. Eso ha generado litigios y es uno de los ángulos
periodísticos más fértiles del tema.
### Qué mira la plataforma, más allá de la clase
La clasificación es solo el principio. Hay un mercado entero de datos de reputación —
MaxMind, IPQualityScore, Spur, IPinfo — que vende exactamente esto:
- **Clasificación de ASN**: si el rango pertenece a un hosting, se sabe
- **Detección de proxy/VPN conocido**: los rangos de los proveedores comerciales están catalogados
- **Velocidad**: cuántas cuentas se han dado de alta desde esa IP y en cuánto tiempo
- **Distribución**: una IP residencial con 40 cuentas es más rara que 40 IPs con una cada una
- **Coherencia geográfica**: IP en Letonia, número español, dispositivo en inglés, zona horaria de
Vietnam. Cada desajuste es una señal
- **Huella de transporte**: JA3/JA4 (el fingerprint del handshake TLS), orden de cabeceras HTTP,
versión de la librería de red. Identifica al cliente por debajo de la aplicación
Ese último punto es el que sorprende: **no hace falta mirar la app para saber que no es la app.**
---
## 3 · Atestación de dispositivo
La tercera pregunta: *¿qué dispositivo eres?* Y es donde la partida se decidió del todo.
### De SafetyNet a Play Integrity
SafetyNet Attestation, la generación anterior, era básicamente software comprobando software: se
podía derrotar con suficiente trabajo, y por eso existió un mercado de bypasses. Google la retiró
y la sustituyó por la **Play Integrity API**, que devuelve tres veredictos escalonados:
| Veredicto | Qué afirma |
|---|---|
| `MEETS_BASIC_INTEGRITY` | Algo parecido a un Android, sin garantías fuertes |
| `MEETS_DEVICE_INTEGRITY` | Dispositivo Android real, con Play Services y arranque verificado |
| `MEETS_STRONG_INTEGRITY` | Lo anterior **más prueba respaldada por hardware** |
### Qué significa "respaldada por hardware"
Aquí está la clave técnica del proyecto entero, y merece entenderla bien.
Un Android certificado sale de fábrica con una **clave privada de atestación grabada en su elemento
seguro** — el TEE, o StrongBox si hay chip dedicado. Esa clave nunca sale del hardware. Cuando la
plataforma pide una prueba, el dispositivo genera una clave dentro del TEE y devuelve una **cadena
de certificados** que se remonta, eslabón a eslabón, hasta **una raíz firmada por Google**.
En la cadena viaja además el estado de **Verified Boot**: si el arranque está verificado
(`GREEN`), si el bootloader está desbloqueado (`ORANGE`), si hay algo modificado.
Entonces, ¿qué le falta a un contenedor redroid, o a cualquier emulador?
**La clave privada.** No es que sea difícil de imitar: es que **no la tiene y no puede tenerla**.
Está grabada en silicio en la fábrica de un dispositivo certificado. No hay software que la
produzca, porque su valor consiste precisamente en que no se puede producir por software.
Junto a eso, todo lo demás cae por su propio peso: sin TEE, sin arranque verificado, sin banda
base, sin IMEI legítimo, con cadenas de GPU que dicen SwiftShader, sin acelerómetro que dé ruido
real, sin sensor de luz que varíe.
### Por qué esto ya no es una carrera armamentística
Este es el punto que hay que subrayar, porque cambia la naturaleza del problema:
> La detección de emulador dejó de ser **estadística** y pasó a ser **criptográfica**.
Antes, detectar un emulador era acumular indicios y decidir con un umbral — y todo umbral se puede
esquivar. Ahora es verificar una firma contra una raíz de confianza. **O tienes la clave o no la
tienes.** No hay evasión posible, solo robo de claves de dispositivos reales, que es otro delito y
otro mercado.
Y ahí está la explicación de la pregunta que abrió toda esta investigación: **por qué GenFarmer te
vende cajas con teléfonos físicos de 400 $ en vez de venderte software para tu servidor.** No es
que no sepan hacer software. Es que lo que venden no es cómputo, es **el único sitio donde vive la
clave**.
---
## 4 · Cómo se integra todo: la correlación
Las tres capas anteriores se pueden atacar por separado con dinero: SIM-box, proxies móviles,
teléfonos físicos. Caras, pero atacables. Lo que hunde el negocio es la cuarta pregunta, y es de
otra naturaleza.
### La plataforma no evalúa cuentas, evalúa grupos
Una cuenta se mira sola solo en el alta. A partir de ahí, los sistemas antiabuso hacen
**clustering**: buscan grupos de cuentas que se parecen más entre sí de lo que se parecerían por
azar.
Las aristas que unen el grafo:
- **Dispositivo**: identificadores, huellas de hardware, versiones exactas de SO y app
- **Red**: rangos de IP, ASN, huellas TLS, patrones de tránsito
- **Comportamiento**: horas de actividad, velocidad de escritura, ritmo de desplazamiento, tiempo
entre acciones, sesiones
- **Grafo social**: a quién siguen, en qué orden, qué contenido tocan
- **Temporal**: cohortes de registro. 45 cuentas dadas de alta el mismo martes por la tarde ya son
un grupo, aunque todo lo demás esté impecable
### La trampa matemática
Fabricar **una** cuenta indistinguible de la de una persona real es caro pero factible.
Fabricar **cuarenta y cinco cuentas mutuamente independientes** es un problema distinto: no basta
con que cada una parezca real, hace falta que **no se parezcan entre sí más de lo normal**. Cada
recurso compartido — un rango de IP, un patrón horario, una plantilla de comportamiento, el momento
del alta — es una arista que las une.
De ahí sale la conclusión económica que explica todo el mercado:
> El coste por cuenta **crece** con el tamaño de la flota, en vez de bajar.
Es lo contrario de una economía de escala. Y por eso el sector es un flujo constante de operadores
pequeños quemando cuentas, en lugar de unos pocos grandes con márgenes estables. La única forma de
que salgan las cuentas es externalizar el riesgo: comprar las SIMs a nombre de otros, sacar los
proxies de los móviles de terceros. Que es exactamente **lo que persiguen y por lo que caen**.
---
## 5 · Qué significa para este proyecto
Sin rodeos, y es la conclusión que ordena todo el repositorio:
**El rack no sirve para las plataformas sociales, y no por falta de potencia.** No tiene la clave.
Ninguna cantidad de blades, RAM o ancho de banda produce una clave de atestación grabada en
fábrica. La detección no es un umbral que se pueda esquivar, es una firma que no se puede
falsificar.
**El rack sí sirve, y muy bien, como laboratorio de QA propio.** Probar `oasis_mobile` en una
matriz de versiones de Android, y el escenario multi-dispositivo de los dos APKs que se tienen que
ver entre sí, es exactamente para lo que 45 Android en red interna son inmejorables. Ahí no hay
ninguna atestación que superar: es tu app en tus dispositivos.
**Y para la charla, esto es la tesis.** Ver [../03_charla/](../03_charla/):
> La granja de bots perdió la guerra técnica el día que la atestación pasó de estadística a
> criptográfica. Lo que queda no es un problema de cómputo, es un problema de identidad — y quien
> controla las SIMs controla el mercado. Por eso ahí es exactamente donde pega la policía.
---
## Fuentes y lecturas
- [Play Integrity API](https://developer.android.com/google/play/integrity) — los tres veredictos
- [Android Key Attestation](https://developer.android.com/privacy-and-security/security-key-attestation) — la cadena de certificados hasta la raíz de Google
- [Verified Boot](https://source.android.com/docs/security/features/verifiedboot) — estados de arranque
- [GSMA eSIM / arquitectura de aprovisionamiento remoto](https://www.gsma.com/esim/) — EID, SM-DP+, perfiles
- Europol, Operación SIMCARTEL — ver [fuentes.md](fuentes.md)
- Informes de transparencia DSA de TikTok y Meta — cifras de cuentas falsas bloqueadas

View file

@ -0,0 +1,19 @@
# Fuentes
## Vendedores analizados
- [GenFarmer](https://genfarmer.com/) — proveedor vietnamita, Android Device Lab + CloudPhone + GenRouter
- [PhoneFarmBox](https://www.phonefarmbox.com/) — tienda opaca, contacto por Telegram/WhatsApp
## Stack técnico
- [redroid](https://github.com/remote-android/redroid-doc) — Android en Docker
- [Cuttlefish (AOSP)](https://source.android.com/docs/devices/cuttlefish) — dispositivo virtual sobre KVM
- [Waydroid](https://waydro.id/) — LXC + binder
- [GADS](https://github.com/shamanec/GADS) — orquestación de device lab, iOS + Android
- [OpenSTF/DeviceFarmer, alternativas](https://devicelab.dev/blog/openstf-devicefarmer-alternative)
- [Running Android on Kubernetes](https://realz.medium.com/running-android-on-kubernetes-be73b940833f)
## Legal y casos
- [Europol desmantela red de SIM farms — The Hacker News](https://thehackernews.com/2025/10/europol-dismantles-sim-farm-network.html)
- [SIM farm dismantled in Europe, seven arrested — SecurityWeek](https://www.securityweek.com/sim-farm-dismantled-in-europe-seven-arrested/)
- [Europe SIM farms raided: Latvia, Austria, Estonia — The Record](https://therecord.media/europe-sim-farms-raided-latvia-austria-estonia)
- [Comisión Europea — DSA, hallazgos preliminares TikTok y Meta](https://ec.europa.eu/commission/presscorner/detail/en/ip_25_2503)

View file

@ -0,0 +1,34 @@
# Riesgo legal en la UE
## DSA
Las plataformas están bajo procedimiento formal de la Comisión —
[TikTok y Meta con hallazgos preliminares de incumplimiento](https://ec.europa.eu/commission/presscorner/detail/en/ip_25_2503).
Esto no afecta directamente a quien monta la granja, pero significa que las plataformas están
obligadas a endurecer detección y a cooperar. La presión regulatoria empuja hacia abajo.
## España
- Comprar SIMs a nombre de terceros o con datos falsos para registrar cuentas es **falsedad
documental y usurpación de identidad**, no incumplimiento de ToS.
- Vender engagement falso a clientes es **publicidad engañosa** y potencialmente **estafa**.
## El precedente: Operación SIMCARTEL (octubre 2025)
Europol, en Austria, Estonia, Finlandia y Letonia:
- 1.200 SIM-boxes, 40.000 SIMs activas, 5 servidores
- **49 millones de cuentas falsas** creadas
- 7 detenidos
- €431.000 congelados, €266.000 en cripto incautados, coches embargados
- €4,5M en pérdidas por fraude asociadas
- gogetsms.com y apisim.com incautados, sirviendo banner policial
**Cayeron por la capa SIM.** Es el eslabón que identifica: KYC en origen y rastro del dinero de
compra.
## Línea que este proyecto no cruza
Sourcing de SIMs para verificación, evasión de fingerprint y warming de cuentas en masa quedan
fuera. No es "granja de bots", es fraude de identidad a escala, y está bajo persecución activa.

View file

@ -0,0 +1,70 @@
# Mercado y players
> Investigación del 2026-08-07. Punto de partida: [phonefarmbox.com](https://www.phonefarmbox.com/)
> y [genfarmer.com](https://genfarmer.com/).
## 1. Tres negocios distintos bajo el mismo nombre
"Phone farm" tapa tres cosas que no tienen nada que ver entre sí:
| Modelo | Qué es | Legalidad | Márgenes |
|---|---|---|---|
| **Reward farming** | N dispositivos propios corriendo Swagbucks, Mistplay, AttaPoll, apps de ads… cobras por impresión/tiempo | Gris (viola ToS, no es delito) | Muy bajos, mercado saturado |
| **Device lab / QA** | N dispositivos como infra de testing de apps | Legal, aburrido, real | Estable, B2B |
| **Engagement / account farming** | N cuentas falsas generando views, follows, votos, airdrops | Fraude en la UE | Altos hasta que te caen encima |
**phonefarmbox.com** y **genfarmer.com** venden hardware que sirve para los tres y se posicionan
deliberadamente en la ambigüedad.
- **GenFarmer** es el más maduro: "Android Device Lab" (teléfonos modificados con USB/red
reforzados), CloudPhone de alquiler, GenRouter, plataforma no-code con OCR y reconocimiento de
imagen, 300+ dispositivos simultáneos, marketplace de mini-apps. Dispositivos de $396 a $1.364.
El tell de su origen: soportan **Zalo** — es un proveedor vietnamita.
- **PhoneFarmBox** es más pequeño, tienda opaca, contacto solo por Telegram/WhatsApp. El nivel de
formalidad del negocio te lo dice eso solo.
## 2. Quién juega, y quién juega en Europa
Europa **no fabrica el hardware** — eso es Shenzhen y Vietnam. Lo que Europa tiene es la capa de
software e infraestructura de red:
- **Antidetect / multi-cuenta**: Multilogin (Estonia), GoLogin (Lituania), Incogniton (Países Bajos).
Es el segmento europeo más consolidado.
- **Proxies móviles/residenciales**: Oxylabs y Decodo/Smartproxy (Lituania), Bright Data.
Lituania es el hub real de esto en la UE.
- **Cloud phones**: VMOS Cloud, GeeLark, BitBrowser — todos asiáticos.
- **SIM farms**: ver abajo, es la parte interesante.
## 3. El tema de las SIMs (y por qué la eSIM no es la respuesta)
En octubre de 2025 Europol desmanteló la **Operación SIMCARTEL** en Austria, Estonia, Finlandia y
Letonia: 1.200 SIM-boxes, 40.000 SIMs activas, 5 servidores, 7 detenidos, y **49 millones de cuentas
falsas** creadas. Los dos servicios que vendían los números — gogetsms.com y apisim.com — están
incautados y sirven banner policial. Pérdidas asociadas: €4,5M en fraude.
Esa es la respuesta factual a la pregunta de la eSIM:
> **El mercado usa SIMs físicas reales.** Prepago comprada a granel en países con registro débil,
> metidas en SIM-boxes y revendida como "número como servicio" por API.
La eSIM no funciona bien para esto porque el aprovisionamiento deja rastro contractual con el
operador — el desarrollo completo, con la arquitectura EID / SM-DP+ y por qué es *peor* que el
plástico para una granja, está en [capa_identidad.md](capa_identidad.md). Y esa capa entera es ahora objetivo prioritario de Europol — es el eslabón que te
identifica, porque las SIMs tienen KYC en origen y el dinero de compra deja rastro. Es el punto
exacto por el que cayeron los siete de SIMCARTEL.
## 4. TikTok vs Instagram: por qué las estrategias divergen
A nivel estructural, sin entrar en operativa:
- **TikTok** es un sistema de recomendación basado en señal de contenido. La cuenta importa poco,
el grafo social importa poco. Un vídeo malo de una cuenta grande muere. Esto hace que la
manipulación por volumen de cuentas rinda mal — lo que se manipula es la señal temprana
(watch-time, completación), y TikTok lo sabe y lo pondera. Su propio reporte DSA declara
**143,8 millones de cuentas falsas bloqueadas**.
- **Instagram** es grafo social + señal. La cuenta y sus conexiones sí valen. Por eso el mercado de
cuentas envejecidas y de follows es mucho más grande en Meta que en TikTok, y por eso Meta
invierte en detección de comportamiento coordinado más que en detección de contenido.
Consecuencia práctica: en TikTok la única palanca que escala es **hacer contenido**, no multiplicar
cuentas.

View file

@ -0,0 +1,45 @@
# Stack técnico: qué se puede correr en un rack Linux
## Opciones de software libre
- **[redroid](https://github.com/remote-android/redroid-doc)** — Android real en contenedores
Docker, GPU acelerada, arm64 y amd64, orquestable con Kubernetes. Es lo mejor para densidad:
~2 GB RAM por instancia, de 30 a 60 instancias en un nodo de 32 cores/128 GB. No ensucia el host
y soporta acceso remoto.
- **[Cuttlefish](https://source.android.com/docs/devices/cuttlefish)** (Google/AOSP) — dispositivo
virtual configurable sobre KVM, imágenes AOSP oficiales. Más pesado, pero es el estándar si
importa la fidelidad al SO. Hay
[patrones de escalado sobre Kubernetes](https://realz.medium.com/running-android-on-kubernetes-be73b940833f)
publicados.
- **[Waydroid](https://waydro.id/)** — LXC + binder, rendimiento nativo sin encoding de vídeo, pero
pensado para una instancia en escritorio, no para densidad.
## Orquestación y control
- **[GADS](https://github.com/shamanec/GADS)** — activo, iOS + Android, integra Appium.
- **DeviceFarmer / STF** — fork comunitario de OpenSTF; Google lo abandonó en 2020 y va lento con
versiones nuevas de Android.
- Encima: **Appium/UIAutomator2** para conducir, **scrcpy** para ver, **ADB sobre TCP** para todo.
## El jarro de agua fría
Ese rack **no sirve** para el caso TikTok/Instagram, y esa es exactamente la razón por la que
GenFarmer vende cajas con teléfonos físicos de $400 en vez de vender software para tu servidor.
Play Integrity con atestación por hardware, ausencia de sensores reales, strings de GPU, falta de
baseband e IMEI legítimo — la detección de emulador es prácticamente determinista desde 2025.
> El cuello de botella nunca fue cómputo, fue **identidad y confianza del dispositivo**.
> Un rack te da CPU infinita y cero confianza.
Por eso el mercado real es hardware caro y lento, no servidores.
El mecanismo exacto — por qué a un contenedor le falta una clave grabada en fábrica que ningún
software puede producir — está en **[capa_identidad.md](capa_identidad.md)**.
Donde el rack sí es imbatible: **laboratorio de testing de tu propia app**. Con `oasis_mobile` hay
APK que necesita probarse en matriz de versiones de Android y en escenarios multi-dispositivo —
dos APKs que se tienen que ver entre sí. redroid + Appium en un rack resuelve eso de verdad,
incluido el bloqueo de capa 1 anotado.
Ver [../02_device_lab/](../02_device_lab/) para el montaje concreto.

112
02_device_lab/README.md Normal file
View file

@ -0,0 +1,112 @@
# Device lab con redroid
La vía 1: laboratorio de dispositivos Android propio sobre el rack. Legal, reutilizable, y el
primer uso real es probar `oasis_mobile` a escala — incluido el escenario de dos APKs que se tienen
que ver entre sí (el bloqueo de capa 1).
**No hay móviles físicos.** Cada "dispositivo" es un contenedor redroid: Android real sobre el
kernel de una máquina Debian.
## Dos caminos
| | Para qué |
|---|---|
| **[ansible/](ansible/)** | **La granja de verdad**: los 16 blades del M1000e, provisionados y controlados desde un sitio. Es lo que hay que usar. |
| Este documento | Un solo nodo, a mano. Sirve para entender las piezas y para trastear en el sobremesa. |
El hardware está descrito en [hardware_m1000e.md](hardware_m1000e.md).
## Host
Comprobado el 2026-08-08 sobre esta máquina:
| | |
|---|---|
| CPU | 16 cores |
| RAM | 62 GB |
| SO | Debian 12 bookworm, kernel 6.1.0-51-amd64 |
| Docker | **no instalado** |
| binder | `binder_linux.ko` presente, pero `CONFIG_ANDROID_BINDERFS` **no está activado** |
Con 2 GB por instancia y dejando margen al host, **1220 instancias redroid** simultáneas es el
rango realista aquí. No son las 3060 de un nodo de 32c/128 GB.
## Prerrequisitos
### 1. binder — el punto donde esto se atasca
El kernel de Debian trae `binder_linux` como módulo pero **sin binderfs**, así que redroid no puede
crear los devices por sí mismo. Hay que cargarlos a mano con los tres nombres que Android espera:
```bash
sudo modprobe binder_linux devices=binder,hwbinder,vndbinder
ls -l /dev/binder /dev/hwbinder /dev/vndbinder # deben existir
```
Para que persista entre reinicios:
```bash
echo binder_linux | sudo tee /etc/modules-load.d/redroid.conf
echo "options binder_linux devices=binder,hwbinder,vndbinder" | sudo tee /etc/modprobe.d/redroid.conf
```
Si `modprobe` falla, la alternativa es el módulo DKMS de Waydroid
(`binder_linux-dkms`), que sí aporta binderfs.
### 2. Docker
```bash
sudo apt install docker.io docker-compose-v2
sudo usermod -aG docker $USER # requiere volver a entrar en sesión
```
### 3. adb
```bash
sudo apt install adb
```
## Arrancar
```bash
docker compose up -d
./connect.sh # conecta adb a las instancias levantadas
adb devices # deben salir 127.0.0.1:5555 .. 5558
```
Instalar un APK en todas:
```bash
./install-apk.sh ~/COFRE/CODERS/OASIS_APK/<algo>.apk
```
Ver una instancia:
```bash
sudo apt install scrcpy
scrcpy -s 127.0.0.1:5555
```
## Escalar
`docker-compose.yml` trae 4 instancias como punto de partida. Para más, replicar el bloque
cambiando nombre y puerto. A partir de ~8 conviene generar el compose con un script en vez de
mantenerlo a mano, o pasar a GADS.
## Para la flota entera
Todo lo de arriba, pero en 16 blades y sin hacerlo a mano: **[ansible/](ansible/)**.
Ahí está el preflight que identifica el modelo de blade solo, el despliegue completo, y `fleet`
para hablar con todos los Android desde un único sitio.
## Siguiente capa: GADS
[GADS](https://github.com/shamanec/GADS) da UI web, gestión de dispositivos y Appium integrado
encima de esto. Merece la pena cuando el número de instancias pase de una decena o cuando haga
falta lanzar tests automatizados en vez de trastear a mano.
## Limitación que hay que tener presente
Estas instancias **son detectables como emulador**. Sirven para probar tu propia app, no para
interactuar con plataformas que hacen atestación de dispositivo (ver
[../01_investigacion/stack_tecnico.md](../01_investigacion/stack_tecnico.md)).

View file

@ -0,0 +1,302 @@
# Ansible — provisionar la granja Android sobre los blades Debian
Automatiza el montaje completo: de blades Debian pelados a una flota de Android
virtuales controlables por `adb` desde un único sitio.
**No hay móviles físicos en esta granja.** Cada "dispositivo" es un contenedor
[redroid](https://github.com/remote-android/redroid-doc) — Android real corriendo sobre el kernel
del blade. Es la única forma sensata de hacer esto con el hardware que hay.
## La arquitectura
```
Dell PowerEdge M1000e — 16 blades
├── blade-01 ................ COORDINADOR
│ └── adb + fleet.sh único punto de control de toda la flota
└── blade-02 .. blade-16 .... NODOS DE CARGA (15)
└── Debian 12 pelado
└── Docker
├── redroid-blade-NN-01 Android 13 ~2 GB
├── redroid-blade-NN-02 Android 13 ~2 GB
└── redroid-blade-NN-03 Android 13 ~2 GB
Con 8 GB por blade → 3 instancias → ~45 Android en total
```
Debian va **directo sobre el blade**. Sin Proxmox, sin VMs, sin LXC anidado: con 8 GB por blade,
cada capa de hipervisor se come una instancia Android entera.
---
## Requisitos
**Nodo de control** (tu sobremesa):
```bash
sudo apt install ansible
```
**Blades**:
- Debian 12 bookworm instalado
- Acceso SSH desde el nodo de control, con sudo o como root
- Salida a internet, o un mirror local de `download.docker.com` y `deb.debian.org`
- Kernel con `binder_linux.ko` (el estándar de Debian lo trae)
**Ejecuta siempre desde este directorio** (`ansible/`) — `ansible.cfg` y las rutas de roles son
relativas a él.
---
## Puesta en marcha
### 1. Inventario
```bash
cp inventory/hosts.yml.example inventory/hosts.yml
$EDITOR inventory/hosts.yml # pon las IPs reales de los blades
```
### 2. Preflight — esto va primero, siempre
```bash
ansible-playbook playbooks/preflight.yml
```
**Es de solo lectura, no toca nada.** Contesta lo que ahora mismo no sabemos:
- **Qué blade hay dentro del chasis** (lo lee de DMI — no hace falta ir a mirar el frontal)
- Si esas CPUs tienen las instrucciones que Android x86_64 necesita
- Si el kernel trae binder
- Cuántas instancias caben en cada blade según su RAM real
Deja un informe en `reports/preflight-<fecha>.md` con la tabla de toda la flota y el total de
capacidad. Si algún blade sale **NO APTO**, lo dice ahí y no cuando estés a medio despliegue.
### 3. Montar la granja
```bash
ansible-playbook site.yml
```
Idempotente: relanzable sin romper nada.
### 4. Comprobar
```bash
ansible-playbook playbooks/status.yml
```
### 5. Instalar un APK en toda la flota
```bash
ansible-playbook playbooks/apk.yml -e apk_local_path=~/COFRE/CODERS/OASIS_APK/oasis.apk
```
---
## Control diario desde el coordinador
Entra por SSH al coordinador y usa `fleet`:
```bash
fleet connect # conecta adb a toda la flota
fleet list # qué hay conectado
fleet install app.apk # instala en todos
fleet shell 'getprop ro.build.version.release'
fleet ip # IP de cada Android — útil para el caso de dos APKs
fleet reboot
```
---
## Los dos modos de red
Se controla con `redroid_network_mode` en `group_vars/all.yml`.
### `portmap` (por defecto)
Cada Android detrás de NAT de Docker; adb en `<ip_blade>:<puerto>`, empezando en 5555.
Funciona en cualquier sitio sin saber nada de la LAN. Es el modo con el que empezar.
### `macvlan` — el que quieres para `oasis_mobile`
Cada Android coge una **IP real de la LAN** a través del switch FlexIO del chasis. Los dos APKs
que se tienen que ver entre sí se ven como **pares de red de verdad**, no a través de traducciones.
Es lo más cerca de "dos móviles en la misma wifi" sin tener dos móviles.
```yaml
redroid_network_mode: macvlan
redroid_macvlan_parent: eno1
redroid_macvlan_subnet: 10.0.0.0/24
redroid_macvlan_gateway: 10.0.0.1
redroid_macvlan_ip_prefix: "10.0.0."
```
Y en el inventario, un `redroid_macvlan_ip_offset` por blade (el ejemplo ya trae 20, 30, 40…).
> **El gotcha de macvlan.** Un anfitrión **no puede hablar con sus propios contenedores macvlan**.
> Es una limitación del driver, no un fallo de configuración. Por eso el coordinador es un blade
> **distinto**: desde fuera sí se llega. Si algún día pones adb en un nodo de carga y no ve sus
> propios Android, es esto.
>
> Reserva también un rango de IPs para los Android en el DHCP de la LAN, o acabarás con colisiones.
---
## El binder, que es donde esto se atasca
Debian 12 compila binder como módulo pero **sin binderfs**, así que redroid no puede crear sus
devices solo. El rol `binder` lo resuelve: carga `binder_linux` con los tres nombres que Android
espera (`binder`, `hwbinder`, `vndbinder`) y lo deja persistente para los siguientes arranques.
Los contenedores de un blade **comparten ese binder del kernel anfitrión**. Si no está, no arranca
ni una instancia.
**Si el playbook falla con "faltan devices binder"**: el módulo estaba ya cargado y en uso, así que
no se pudo recargar con los parámetros nuevos. La configuración persistente ya quedó escrita —
reinicia ese blade y relanza. Es un fallo de una sola vez.
---
## Ajustes que vas a querer tocar
Todo en `group_vars/all.yml`.
| Variable | Por defecto | Para qué |
|---|---|---|
| `redroid_image` | `redroid/redroid:13.0.0-latest` | Versión de Android. Hay 11, 12, 13 y 14 |
| `redroid_instances` | `0` (auto) | Instancias por blade. 0 = calculado desde la RAM real |
| `redroid_mem_mb` | `2048` | RAM por Android |
| `redroid_host_reserve_mb` | `1500` | RAM que se le deja a Debian + Docker |
| `redroid_gpu_mode` | `guest` | Sin GPU en los blades → render por software |
| `redroid_network_mode` | `portmap` | `portmap` o `macvlan` |
Cambiar la versión de Android en toda la flota, sin editar nada:
```bash
ansible-playbook playbooks/devices.yml -e redroid_image=redroid/redroid:14.0.0-latest
```
### La palanca real: la RAM
El cuello de botella son **los 8 GB por blade, no la CPU**. Si los blades resultan ser M620
(DDR3 ECC), ampliar a 32 GB cuesta poco en segunda mano:
```bash
ansible-playbook playbooks/devices.yml -e redroid_instances=14
```
De ~45 Android a **~210**, por menos de lo que cuesta un mes de luz del chasis.
---
## Playbooks
| Playbook | Qué hace | Toca algo |
|---|---|---|
| `playbooks/preflight.yml` | Reconocimiento: modelo, CPU, RAM, binder, capacidad | **No** |
| `site.yml` | La granja entera de cero | Sí |
| `playbooks/provision.yml` | Solo el hierro: base + binder + Docker | Sí |
| `playbooks/devices.yml` | Solo los contenedores Android | Sí |
| `playbooks/coordinator.yml` | Regenera el listado de flota y `fleet` | Sí |
| `playbooks/apk.yml` | Instala un APK en toda la flota | Sí |
| `playbooks/status.yml` | Estado real de la granja | **No** |
| `playbooks/destroy.yml` | Para y borra los contenedores | **Sí, destructivo** |
`destroy.yml` exige `-e confirmar=si`, y conserva los `/data` salvo que añadas `-e borrar_datos=si`.
---
## Probarlo antes de tocar el chasis
`inventory/hosts.yml.example` trae un bloque comentado de **piloto de un nodo**: el sobremesa hace
de coordinador y de nodo de carga a la vez. Descoméntalo, comenta el bloque `granja`, y tienes el
flujo entero contra una sola máquina. Con 62 GB de RAM salen unas 8 instancias (tope
`redroid_max_instances`).
Es la forma sensata de empezar: valida todo el pipeline sin encender 2 kW.
---
## Estructura
```
ansible/
├── ansible.cfg
├── site.yml playbook maestro
├── inventory/hosts.yml.example 16 blades + piloto de un nodo
├── group_vars/all.yml los mandos del operador
├── playbooks/
│ ├── preflight.yml reconocimiento (solo lectura)
│ ├── provision.yml base + binder + docker
│ ├── devices.yml contenedores Android
│ ├── coordinator.yml listado de flota + fleet
│ ├── apk.yml instalar APK en la flota
│ ├── status.yml estado (solo lectura)
│ ├── destroy.yml desmontar (destructivo)
│ └── templates/preflight-report.md.j2
├── roles/
│ ├── preflight/ DMI, flags de CPU, binder, capacidad
│ ├── base/ paquetes, hora
│ ├── binder/ los tres devices, persistentes
│ ├── docker/ Docker CE + compose v2
│ ├── redroid/ genera el compose y levanta Android
│ └── coordinator/ adb + devices.txt + fleet.sh
└── reports/ informes de preflight (se genera solo)
```
---
## Estado de verificación
Escrito el 2026-08-08. **No se ha ejecutado contra hardware real** — el chasis todavía no está
accesible y el sobremesa no tiene Docker instalado.
Lo que sí está comprobado:
- Los 18 ficheros YAML parsean correctamente
- La plantilla de `docker-compose` renderiza YAML válido **en los dos modos de red**, con los
puertos y las IPs bien asignados
- `fleet.sh` pasa `bash -n` una vez renderizado, sin Jinja pendiente
Lo que **no** está comprobado y solo dirá la realidad:
- Que las imágenes x86_64 de Android arranquen en la generación de CPU de estos blades
(el riesgo real si resultan ser M610 — preflight lo detecta antes de gastar tiempo)
- El comportamiento de macvlan sobre los switches FlexIO concretos del chasis
- Que 3 instancias por blade sean cómodas con 8 GB en carga real, no solo en el papel
---
## Siguiente capa: GADS
[GADS](https://github.com/shamanec/GADS) da UI web y Appium integrado encima de esto. No está
automatizado aquí a propósito: montar un despliegue de Node que no puedo probar sería añadir
complejidad sin verificar. Merece la pena cuando la flota pase de una decena de dispositivos o
cuando haga falta lanzar tests automatizados en vez de trastear con `fleet shell`.
---
## Seguridad — lee esto antes de encender
El repaso completo está en **[../../docs/seguridad.md](../../docs/seguridad.md)** (11 hallazgos).
Lo que no se puede saltar:
**adb no tiene autenticación**, las imágenes de redroid permiten `adb root`, y los contenedores son
`privileged` porque redroid lo exige. Encadenado: **quien alcance el puerto 5555 de un blade acaba
siendo root en ese blade**, sin exploit ninguno. Con 45 dispositivos son 45 puertas.
Mitigación parcial ya en el código — ata adb solo a la interfaz interna del chasis:
```yaml
blade-02: { ansible_host: 10.0.0.12, redroid_adb_bind_ip: 10.0.0.12 }
```
Lo que lo cierra de verdad es una **VLAN aislada** para la granja. En modo `macvlan`,
`redroid_adb_bind_ip` no aplica (adb escucha en la IP de LAN del propio Android) y la VLAN es la
única defensa.
Y antes de nada: **cambiar las credenciales de fábrica del CMC y de los 16 iDRAC** (`root`/`calvin`).

View file

@ -0,0 +1,18 @@
[defaults]
inventory = inventory/hosts.yml
roles_path = roles
host_key_checking = False
retry_files_enabled = False
stdout_callback = yaml
interpreter_python = auto_silent
# 20 > 16 blades: toda la flota en paralelo
forks = 20
timeout = 30
[ssh_connection]
pipelining = True
ssh_args = -o ControlMaster=auto -o ControlPersist=120s
[privilege_escalation]
become = True
become_method = sudo

View file

@ -0,0 +1,80 @@
# ---------------------------------------------------------------------------
# Mandos del operador. Lo que se toca normalmente está aquí.
# Los defaults internos de cada rol están en roles/<rol>/defaults/main.yml
# ---------------------------------------------------------------------------
# --- Android ---------------------------------------------------------------
# Imagen de redroid. Tags: 11.0.0-latest, 12.0.0-latest, 13.0.0-latest, 14.0.0-latest
# Android 13 es el punto dulce: memfd en vez de ashmem (un módulo de kernel menos)
# y todavía ligero.
redroid_image: "redroid/redroid:13.0.0-latest"
# RAM por instancia. 2 GB es el mínimo cómodo para Android 13.
redroid_mem_mb: 2048
# RAM que se le deja al anfitrión (Debian + Docker + margen).
redroid_host_reserve_mb: 1500
# Instancias por nodo. 0 = automático a partir de la RAM real del blade:
# (RAM_total - reserva) / redroid_mem_mb
# Con 8 GB por blade eso da 3. Súbelo a mano solo si sabes lo que haces.
redroid_instances: 0
# Tope duro por si un blade tiene mucha RAM y te comes los cores.
redroid_max_instances: 8
# Pantalla de las instancias.
redroid_width: 720
redroid_height: 1280
redroid_dpi: 320
# Sin GPU en los blades -> render por software. Con GPU pasable: "host".
redroid_gpu_mode: guest
# --- Red -------------------------------------------------------------------
# portmap -> cada Android detrás de NAT, adb en <ip_blade>:<puerto>
# Funciona en cualquier sitio. Es el default sensato.
#
# macvlan -> cada Android coge una IP REAL de la LAN a través del switch
# FlexIO del chasis. Los APKs se ven como pares de red de verdad.
# Es lo que quieres para el caso de los dos APKs de oasis_mobile.
# Ojo al gotcha documentado en el README.
redroid_network_mode: portmap
# --- solo si redroid_network_mode == macvlan ---
redroid_macvlan_network: redroid-lan
redroid_macvlan_parent: "" # p.ej. eno1 (interfaz física del blade)
redroid_macvlan_subnet: "" # p.ej. 10.0.0.0/24
redroid_macvlan_gateway: "" # p.ej. 10.0.0.1
redroid_macvlan_ip_prefix: "" # p.ej. "10.0.0." (el offset va por host en el inventario)
# --- solo si redroid_network_mode == portmap ---
# Primer puerto adb en el blade. Las instancias ocupan base, base+1, base+2...
redroid_adb_base_port: 5555
# A qué interfaz se ata adb. 0.0.0.0 = TODAS, y adb no tiene autenticación:
# quien llegue al puerto es root en el Android, y desde un contenedor privilegiado
# eso es una vía hacia el anfitrión. Ver docs/seguridad.md (hallazgo S1).
#
# Pon aquí la IP interna del blade en la red del chasis para que adb no escuche
# en nada más. Por host, en el inventario:
# blade-02: { ansible_host: 10.0.0.12, redroid_adb_bind_ip: 10.0.0.12 }
redroid_adb_bind_ip: "0.0.0.0"
# --- Rutas -----------------------------------------------------------------
redroid_base_dir: /opt/redroid
# --- Preflight -------------------------------------------------------------
# Flags de CPU que Android x86_64 necesita. Si un blade no las tiene,
# preflight lo marca como NO APTO en vez de dejarte descubrirlo a medio despliegue.
preflight_required_cpu_flags:
- sse4_2
- ssse3
- popcnt

View file

@ -0,0 +1,70 @@
# Inventario de la granja. Copia esto a hosts.yml y ajusta IPs.
#
# cp inventory/hosts.yml.example inventory/hosts.yml
#
# Reparto: 1 blade coordinador (adb + control) + 15 blades de carga.
# El coordinador NO corre instancias Android, solo dirige.
all:
children:
granja:
vars:
ansible_user: root
# Si entras con usuario normal + sudo, cambia a:
# ansible_user: sito
# ansible_become_password: "{{ vault_sudo_password }}"
children:
# ---------------------------------------------------------------
# Coordinador: adb, scripts de flota, punto único de control
# ---------------------------------------------------------------
coordinator:
hosts:
blade-01:
ansible_host: 10.0.0.11
# ---------------------------------------------------------------
# Nodos de carga: aquí corren los contenedores redroid
# ---------------------------------------------------------------
redroid_nodes:
hosts:
blade-02: { ansible_host: 10.0.0.12, redroid_macvlan_ip_offset: 20 }
blade-03: { ansible_host: 10.0.0.13, redroid_macvlan_ip_offset: 30 }
blade-04: { ansible_host: 10.0.0.14, redroid_macvlan_ip_offset: 40 }
blade-05: { ansible_host: 10.0.0.15, redroid_macvlan_ip_offset: 50 }
blade-06: { ansible_host: 10.0.0.16, redroid_macvlan_ip_offset: 60 }
blade-07: { ansible_host: 10.0.0.17, redroid_macvlan_ip_offset: 70 }
blade-08: { ansible_host: 10.0.0.18, redroid_macvlan_ip_offset: 80 }
blade-09: { ansible_host: 10.0.0.19, redroid_macvlan_ip_offset: 90 }
blade-10: { ansible_host: 10.0.0.20, redroid_macvlan_ip_offset: 100 }
blade-11: { ansible_host: 10.0.0.21, redroid_macvlan_ip_offset: 110 }
blade-12: { ansible_host: 10.0.0.22, redroid_macvlan_ip_offset: 120 }
blade-13: { ansible_host: 10.0.0.23, redroid_macvlan_ip_offset: 130 }
blade-14: { ansible_host: 10.0.0.24, redroid_macvlan_ip_offset: 140 }
blade-15: { ansible_host: 10.0.0.25, redroid_macvlan_ip_offset: 150 }
blade-16: { ansible_host: 10.0.0.26, redroid_macvlan_ip_offset: 160 }
# -------------------------------------------------------------------------
# PILOTO DE UN NODO
# -------------------------------------------------------------------------
# Para probar todo esto en el sobremesa antes de tocar el chasis, comenta
# el bloque `granja` de arriba y descomenta este. El sobremesa hace de
# coordinador y de nodo de carga a la vez.
#
# ansible-playbook playbooks/preflight.yml
# ansible-playbook site.yml
#
# all:
# children:
# granja:
# children:
# coordinator:
# hosts:
# localhost:
# ansible_connection: local
# redroid_nodes:
# hosts:
# localhost:
# ansible_connection: local
# redroid_macvlan_ip_offset: 20

View file

@ -0,0 +1,60 @@
---
# Instala un APK en toda la flota, desde el coordinador.
#
# ansible-playbook playbooks/apk.yml -e apk_local_path=~/COFRE/CODERS/OASIS_APK/oasis.apk
#
# Se ejecuta desde el coordinador a propósito: en modo macvlan un blade NO puede
# hablar con sus propios contenedores, así que instalar desde cada nodo fallaría.
# Desde el coordinador funcionan los dos modos de red.
- name: Instalar APK en la flota
hosts: coordinator
gather_facts: false
vars:
apk_local_path: ""
tasks:
- name: Exigir la ruta del APK
ansible.builtin.assert:
that: apk_local_path | length > 0
fail_msg: "Falta -e apk_local_path=/ruta/al/app.apk"
- name: Comprobar que el APK existe en el nodo de control
ansible.builtin.stat:
path: "{{ apk_local_path }}"
delegate_to: localhost
become: false
register: apk_local
- name: Abortar si no está
ansible.builtin.assert:
that: apk_local.stat.exists
fail_msg: "No existe: {{ apk_local_path }}"
- name: Copiar el APK al coordinador
ansible.builtin.copy:
src: "{{ apk_local_path }}"
dest: "{{ coordinator_dir }}/apks/{{ apk_local_path | basename }}"
owner: root
group: root
mode: "0644"
- name: Conectar adb a la flota
ansible.builtin.command:
cmd: "{{ coordinator_dir }}/fleet.sh connect"
register: apk_connect
changed_when: false
- name: Resultado de la conexión
ansible.builtin.debug:
var: apk_connect.stdout_lines
- name: Instalar el APK en todos los dispositivos
ansible.builtin.command:
cmd: "{{ coordinator_dir }}/fleet.sh install {{ coordinator_dir }}/apks/{{ apk_local_path | basename }}"
register: apk_install
changed_when: true
- name: Resultado de la instalación
ansible.builtin.debug:
var: apk_install.stdout_lines

View file

@ -0,0 +1,19 @@
---
# Regenera solo el coordinador (listado de flota + script fleet).
#
# ansible-playbook playbooks/coordinator.yml
#
# La primera play recoge facts de los nodos de carga sin tocarlos: el listado
# de dispositivos se calcula a partir de su RAM real.
- name: Recoger facts de los nodos de carga
hosts: redroid_nodes
gather_facts: true
become: false
tasks: []
- name: Configurar el coordinador
hosts: coordinator
gather_facts: true
roles:
- role: coordinator

View file

@ -0,0 +1,48 @@
---
# DESTRUCTIVO. Para y borra los contenedores Android.
#
# ansible-playbook playbooks/destroy.yml -e confirmar=si
#
# Por defecto conserva los volúmenes /data (el estado de cada Android).
# Para borrarlos también — esto no tiene vuelta atrás:
#
# ansible-playbook playbooks/destroy.yml -e confirmar=si -e borrar_datos=si
- name: Desmontar la flota Android
hosts: redroid_nodes
gather_facts: false
vars:
confirmar: "no"
borrar_datos: "no"
tasks:
- name: Exigir confirmación explícita
ansible.builtin.assert:
that: confirmar == "si"
fail_msg: >-
Esto para y borra los contenedores Android de {{ inventory_hostname }}.
Si es lo que quieres, relanza con -e confirmar=si
- name: Comprobar que hay un compose desplegado
ansible.builtin.stat:
path: "{{ redroid_base_dir }}/docker-compose.yml"
register: destroy_compose
- name: Parar y eliminar los contenedores
ansible.builtin.command:
cmd: docker compose down
chdir: "{{ redroid_base_dir }}"
when: destroy_compose.stat.exists
changed_when: true
- name: Borrar los volúmenes /data
ansible.builtin.file:
path: "{{ redroid_base_dir }}/data"
state: absent
when: borrar_datos == "si"
- name: Qué ha quedado
ansible.builtin.debug:
msg: >-
{{ inventory_hostname }}: contenedores eliminados.
Datos {{ 'BORRADOS' if borrar_datos == 'si' else 'conservados en ' ~ redroid_base_dir ~ '/data' }}.

View file

@ -0,0 +1,23 @@
---
# Despliega o redespliega solo los contenedores Android.
# Da por hecho que provision.yml ya pasó.
#
# ansible-playbook playbooks/devices.yml
#
# Cambiar la versión de Android en toda la flota:
# ansible-playbook playbooks/devices.yml -e redroid_image=redroid/redroid:14.0.0-latest
#
# Subir instancias por nodo (si has ampliado la RAM):
# ansible-playbook playbooks/devices.yml -e redroid_instances=6
- name: Contenedores Android
hosts: redroid_nodes
gather_facts: true
roles:
- role: redroid
- name: Regenerar el listado de la flota en el coordinador
hosts: coordinator
gather_facts: true
roles:
- role: coordinator

View file

@ -0,0 +1,37 @@
---
# SOLO LECTURA. No cambia nada en los blades.
#
# ansible-playbook playbooks/preflight.yml
#
# Contesta lo que ahora mismo no sabemos: qué modelo de blade hay dentro del
# chasis, si esas CPUs pueden con Android x86_64, y cuántas instancias salen.
# Lánzalo ANTES de site.yml.
- name: Reconocimiento de la granja
hosts: granja
gather_facts: true
become: true
roles:
- role: preflight
- name: Escribir informe en el nodo de control
hosts: localhost
connection: local
gather_facts: true
become: false
tasks:
- name: Crear directorio de informes
ansible.builtin.file:
path: "{{ playbook_dir }}/../reports"
state: directory
mode: "0755"
- name: Generar informe
ansible.builtin.template:
src: preflight-report.md.j2
dest: "{{ playbook_dir }}/../reports/preflight-{{ ansible_date_time.date }}.md"
mode: "0644"
- name: Dónde ha quedado
ansible.builtin.debug:
msg: "Informe en ansible/reports/preflight-{{ ansible_date_time.date }}.md"

View file

@ -0,0 +1,18 @@
---
# Deja los blades listos, pero sin levantar Android todavía.
# Útil para separar "preparar el hierro" de "desplegar la flota".
#
# ansible-playbook playbooks/provision.yml
- name: Base del sistema
hosts: granja
gather_facts: true
roles:
- role: base
- name: Binder y Docker en los nodos de carga
hosts: redroid_nodes
gather_facts: true
roles:
- role: binder
- role: docker

View file

@ -0,0 +1,53 @@
---
# Estado real de la granja. Solo lectura.
#
# ansible-playbook playbooks/status.yml
- name: Estado de los nodos de carga
hosts: redroid_nodes
gather_facts: true
tasks:
- name: Contenedores en marcha
ansible.builtin.command:
cmd: docker ps --filter "name=redroid-" --format '{% raw %}{{.Names}}\t{{.Status}}{% endraw %}'
register: status_ps
changed_when: false
failed_when: false
- name: Devices binder presentes
ansible.builtin.stat:
path: "/dev/{{ item }}"
loop: [binder, hwbinder, vndbinder]
register: status_binder
- name: Memoria libre
ansible.builtin.command:
cmd: free -m
register: status_mem
changed_when: false
- name: Resumen del nodo
ansible.builtin.debug:
msg:
- "--- {{ inventory_hostname }} ---"
- "binder : {{ 'ok' if (status_binder.results | rejectattr('stat.exists') | list | length == 0) else 'FALTAN devices' }}"
- "ram libre : {{ status_mem.stdout_lines[1].split()[6] | default('?') }} MB disponibles de {{ ansible_memtotal_mb }} MB"
- "contenedores:"
- "{{ status_ps.stdout_lines | default(['(ninguno)']) }}"
- name: Estado de la flota adb
hosts: coordinator
gather_facts: false
tasks:
- name: Conectar y listar
ansible.builtin.shell:
cmd: "{{ coordinator_dir }}/fleet.sh connect >/dev/null 2>&1; {{ coordinator_dir }}/fleet.sh list"
register: status_fleet
changed_when: false
failed_when: false
- name: Flota
ansible.builtin.debug:
var: status_fleet.stdout_lines

View file

@ -0,0 +1,28 @@
# Informe de preflight — {{ ansible_date_time.date }}
Generado por `ansible-playbook playbooks/preflight.yml`. Solo lectura: no se tocó nada.
## Flota
| Nodo | Modelo | CPU | vCPU | RAM | Kernel | binder | Flags CPU | Capacidad | Veredicto |
|---|---|---|---|---|---|---|---|---|---|
{% for h in groups['granja'] | sort %}
{% set hv = hostvars[h] %}
| {{ h }} | {{ hv.preflight_vendor | default('?') }} {{ hv.preflight_model | default('?') }} | {{ hv.ansible_processor[2] | default('?') }} | {{ hv.ansible_processor_vcpus | default('?') }} | {{ hv.ansible_memtotal_mb | default('?') }} MB | {{ hv.ansible_kernel | default('?') }} | {{ 'sí' if hv.preflight_binder_ko.stat.exists | default(false) else 'NO' }} | {{ 'ok' if (hv.preflight_missing_flags | default([]) | length) == 0 else 'faltan: ' ~ (hv.preflight_missing_flags | join(', ')) }} | {{ hv.preflight_capacity | default('?') }} | {{ 'APTO' if hv.preflight_ok | default(false) else '**NO APTO**' }} |
{% endfor %}
## Totales
{% set aptos = groups['granja'] | map('extract', hostvars) | selectattr('preflight_ok', 'defined') | selectattr('preflight_ok') | list %}
{% set nodos_carga = groups['redroid_nodes'] | default([]) | map('extract', hostvars) | selectattr('preflight_ok', 'defined') | selectattr('preflight_ok') | list %}
- Nodos aptos: **{{ aptos | length }}** de {{ groups['granja'] | length }}
- Capacidad total en nodos de carga: **{{ nodos_carga | map(attribute='preflight_capacity') | map('int') | sum }} instancias Android**
- RAM total de la flota: {{ groups['granja'] | map('extract', hostvars) | selectattr('ansible_memtotal_mb', 'defined') | map(attribute='ansible_memtotal_mb') | map('int') | sum }} MB
## Cómo leer esto
- **binder = NO** → ese blade no puede correr redroid con su kernel actual. Ver `roles/binder/tasks/main.yml`.
- **Flags CPU faltantes** → CPU demasiado antigua para las imágenes x86_64 de Android. Es el riesgo
real si los blades resultan ser M610 (Xeon 20092011).
- **Capacidad** sale de `(RAM - {{ redroid_host_reserve_mb }} MB de reserva) / {{ redroid_mem_mb }} MB por instancia`.
Si el número es bajo, la palanca es ampliar RAM por blade, no añadir blades.

View file

@ -0,0 +1,38 @@
---
- name: Actualizar índice de apt
ansible.builtin.apt:
update_cache: true
cache_valid_time: 3600
- name: Paquetes base
ansible.builtin.apt:
name:
- ca-certificates
- curl
- gnupg
- htop
- iproute2
- python3
- rsync
state: present
- name: Leer zona horaria actual
ansible.builtin.command:
cmd: timedatectl show -p Timezone --value
register: base_tz
changed_when: false
failed_when: false
- name: Fijar zona horaria
ansible.builtin.command:
cmd: "timedatectl set-timezone {{ granja_timezone | default('Europe/Madrid') }}"
when: base_tz.stdout | default('') != (granja_timezone | default('Europe/Madrid'))
changed_when: true
failed_when: false
- name: Sincronización horaria activa
ansible.builtin.systemd:
name: systemd-timesyncd
enabled: true
state: started
failed_when: false

View file

@ -0,0 +1,84 @@
---
# El punto donde esto se atasca en Debian.
#
# Debian 12 compila binder como módulo (CONFIG_ANDROID_BINDER_IPC=m) pero SIN
# binderfs (CONFIG_ANDROID_BINDERFS no está activado). Sin binderfs, redroid no
# puede crear los devices por sí mismo: hay que cargarlos a mano y con los TRES
# nombres que Android espera — binder, hwbinder, vndbinder.
#
# Los contenedores comparten este binder del kernel anfitrión. Si no está, no
# arranca ni una instancia.
- name: Comprobar que el kernel trae binder_linux
ansible.builtin.stat:
path: "/lib/modules/{{ ansible_kernel }}/kernel/drivers/android/binder_linux.ko"
register: binder_ko
- name: Abortar si no hay módulo binder
ansible.builtin.assert:
that: binder_ko.stat.exists
fail_msg: >-
Este kernel ({{ ansible_kernel }}) no trae binder_linux.ko.
Sin binder no hay redroid. Salidas: instalar el kernel estándar de Debian,
o el módulo DKMS de Waydroid (binder_linux-dkms), que además aporta binderfs.
- name: Cargar binder_linux en cada arranque
ansible.builtin.copy:
content: "binder_linux\n"
dest: /etc/modules-load.d/redroid.conf
owner: root
group: root
mode: "0644"
- name: Fijar los tres devices como parámetro del módulo
ansible.builtin.copy:
content: "options binder_linux devices=binder,hwbinder,vndbinder\n"
dest: /etc/modprobe.d/redroid.conf
owner: root
group: root
mode: "0644"
- name: Ver qué devices binder existen ya
ansible.builtin.stat:
path: "/dev/{{ item }}"
loop: [binder, hwbinder, vndbinder]
register: binder_devs_before
- name: ¿Falta alguno?
ansible.builtin.set_fact:
binder_needs_load: "{{ binder_devs_before.results | rejectattr('stat.exists') | list | length > 0 }}"
# Si el módulo ya está cargado pero con los parámetros equivocados, modprobe no
# lo recarga solo. Hay que descargarlo primero.
- name: Descargar binder_linux si está cargado mal
ansible.builtin.command:
cmd: modprobe -r binder_linux
when: binder_needs_load
register: binder_rmmod
failed_when: false
changed_when: binder_rmmod.rc == 0
- name: Cargar binder_linux con los tres devices
ansible.builtin.command:
cmd: modprobe binder_linux devices=binder,hwbinder,vndbinder
when: binder_needs_load
register: binder_modprobe
failed_when: false
changed_when: binder_modprobe.rc == 0
- name: Verificar los devices binder
ansible.builtin.stat:
path: "/dev/{{ item }}"
loop: [binder, hwbinder, vndbinder]
register: binder_devs_after
- name: Confirmar que los tres devices están
ansible.builtin.assert:
that: binder_devs_after.results | rejectattr('stat.exists') | list | length == 0
fail_msg: >-
Faltan devices binder tras el modprobe:
{{ binder_devs_after.results | rejectattr('stat.exists') | map(attribute='item') | join(', ') }}.
Causa habitual: el módulo estaba cargado y en uso, así que no se pudo recargar
con los parámetros nuevos. La configuración persistente YA está escrita, así que
un reinicio del blade lo arregla. Reinicia y relanza este playbook.
success_msg: "binder, hwbinder y vndbinder presentes."

View file

@ -0,0 +1,2 @@
---
coordinator_dir: /opt/granja

View file

@ -0,0 +1,61 @@
---
# El coordinador es el único punto desde el que se habla con toda la flota.
# No corre instancias Android: corre adb y los scripts.
#
# Importante en modo macvlan: un anfitrión NO puede hablar con sus propios
# contenedores macvlan. Por eso adb vive aquí, en un blade distinto — desde
# fuera sí se llega. Ver el README.
- name: Instalar herramientas de control
ansible.builtin.apt:
name:
- adb
- jq
state: present
- name: Crear directorios del coordinador
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: root
group: root
mode: "0755"
loop:
- "{{ coordinator_dir }}"
- "{{ coordinator_dir }}/apks"
- name: Escribir el listado de dispositivos de la flota
ansible.builtin.template:
src: devices.txt.j2
dest: "{{ coordinator_dir }}/devices.txt"
owner: root
group: root
mode: "0644"
- name: Instalar el script de flota
ansible.builtin.template:
src: fleet.sh.j2
dest: "{{ coordinator_dir }}/fleet.sh"
owner: root
group: root
mode: "0755"
- name: Enlazar fleet en el PATH
ansible.builtin.file:
src: "{{ coordinator_dir }}/fleet.sh"
dest: /usr/local/bin/fleet
state: link
- name: Contar dispositivos de la flota
ansible.builtin.command:
cmd: "grep -cv '^#' {{ coordinator_dir }}/devices.txt"
register: coordinator_count
changed_when: false
failed_when: false
- name: Resumen
ansible.builtin.debug:
msg:
- "Coordinador listo en {{ inventory_hostname }}."
- "Flota declarada: {{ coordinator_count.stdout | default('0') | trim }} dispositivos."
- "Uso: fleet connect · fleet list · fleet install <apk> · fleet shell '<cmd>'"

View file

@ -0,0 +1,18 @@
# GENERADO POR ANSIBLE — no editar a mano.
# Una dirección adb por línea: <host>:<puerto>
# Regenerar con: ansible-playbook playbooks/coordinator.yml
{% for h in groups['redroid_nodes'] | sort %}
{% set hv = hostvars[h] %}
{% set count = hv.redroid_instance_count | default(
[ (((hv.ansible_memtotal_mb | int) - (redroid_host_reserve_mb | int))
/ (redroid_mem_mb | int)) | int,
redroid_max_instances | int ] | min ) %}
# --- {{ h }} ({{ count }} instancias) ---
{% for i in range(count | int) %}
{% if (hv.redroid_network_mode | default(redroid_network_mode)) == 'macvlan' %}
{{ hv.redroid_macvlan_ip_prefix | default(redroid_macvlan_ip_prefix) }}{{ (hv.redroid_macvlan_ip_offset | int) + i }}:5555
{% else %}
{{ hv.ansible_host | default(h) }}:{{ (hv.redroid_adb_base_port | default(redroid_adb_base_port) | int) + i }}
{% endif %}
{% endfor %}
{% endfor %}

View file

@ -0,0 +1,135 @@
#!/usr/bin/env bash
# GENERADO POR ANSIBLE — no editar a mano.
# Fuente: roles/coordinator/templates/fleet.sh.j2
#
# Control de la flota Android de la granja desde un único sitio.
set -euo pipefail
DEVICES_FILE="{{ coordinator_dir }}/devices.txt"
APK_DIR="{{ coordinator_dir }}/apks"
usage() {
cat <<'USAGE'
fleet — control de la flota Android
fleet connect conecta adb a toda la flota
fleet disconnect desconecta todo
fleet list dispositivos conectados y su estado
fleet install <apk> instala un APK en toda la flota
fleet uninstall <pkg> desinstala un paquete de toda la flota
fleet shell '<cmd>' ejecuta un comando de shell en toda la flota
fleet ip IP de cada dispositivo (útil para el caso de dos APKs)
fleet reboot reinicia toda la flota
El listado de dispositivos sale de devices.txt, que genera Ansible.
USAGE
}
declared_devices() {
grep -v '^#' "$DEVICES_FILE" | grep -v '^[[:space:]]*$'
}
connected_devices() {
adb devices | awk '/\tdevice$/ {print $1}'
}
require_connected() {
local n
n=$(connected_devices | wc -l)
if [[ "$n" -eq 0 ]]; then
echo "No hay dispositivos conectados. Lanza 'fleet connect' primero." >&2
exit 1
fi
}
cmd_connect() {
local ok=0 fail=0
while read -r dev; do
if adb connect "$dev" 2>&1 | grep -qE 'connected to'; then
ok=$((ok + 1))
else
echo " fallo: $dev" >&2
fail=$((fail + 1))
fi
done < <(declared_devices)
echo "conectados: $ok · fallidos: $fail"
}
cmd_disconnect() {
adb disconnect || true
}
cmd_list() {
local declared connected
declared=$(declared_devices | wc -l)
connected=$(connected_devices | wc -l)
echo "declarados: $declared · conectados: $connected"
echo
adb devices -l
}
cmd_install() {
local apk="${1:?uso: fleet install <apk>}"
[[ -f "$apk" ]] || { echo "no existe: $apk" >&2; exit 1; }
require_connected
local ok=0 fail=0
while read -r dev; do
if adb -s "$dev" install -r "$apk" >/dev/null 2>&1; then
echo " ok $dev"
ok=$((ok + 1))
else
echo " FALLO $dev" >&2
fail=$((fail + 1))
fi
done < <(connected_devices)
echo "instalado en $ok · fallos $fail"
[[ "$fail" -eq 0 ]]
}
cmd_uninstall() {
local pkg="${1:?uso: fleet uninstall <paquete>}"
require_connected
while read -r dev; do
printf ' %s: ' "$dev"
adb -s "$dev" uninstall "$pkg" || true
done < <(connected_devices)
}
cmd_shell() {
local command="${1:?uso: fleet shell '<comando>'}"
require_connected
while read -r dev; do
echo "== $dev =="
adb -s "$dev" shell "$command" || true
done < <(connected_devices)
}
cmd_ip() {
require_connected
while read -r dev; do
printf '%-24s %s\n' "$dev" \
"$(adb -s "$dev" shell ip -4 -o addr show scope global 2>/dev/null \
| awk '{print $4}' | paste -sd, - || echo '?')"
done < <(connected_devices)
}
cmd_reboot() {
require_connected
while read -r dev; do
echo " reiniciando $dev"
adb -s "$dev" reboot || true
done < <(connected_devices)
}
case "${1:-}" in
connect) cmd_connect ;;
disconnect) cmd_disconnect ;;
list) cmd_list ;;
install) shift; cmd_install "$@" ;;
uninstall) shift; cmd_uninstall "$@" ;;
shell) shift; cmd_shell "$@" ;;
ip) cmd_ip ;;
reboot) cmd_reboot ;;
*) usage; exit 1 ;;
esac

View file

@ -0,0 +1,53 @@
---
# Docker desde el repo oficial. El `docker.io` de Debian 12 va con un compose
# antiguo (v1, sintaxis `docker-compose`), y las plantillas de aquí usan
# `docker compose` (v2). Requiere que los blades lleguen a internet o a un
# mirror local de download.docker.com.
- name: Directorio de keyrings de apt
ansible.builtin.file:
path: /etc/apt/keyrings
state: directory
owner: root
group: root
mode: "0755"
- name: Clave GPG de Docker
ansible.builtin.get_url:
url: https://download.docker.com/linux/debian/gpg
dest: /etc/apt/keyrings/docker.asc
owner: root
group: root
mode: "0644"
- name: Repositorio de Docker
ansible.builtin.apt_repository:
repo: >-
deb [arch={{ 'amd64' if ansible_architecture == 'x86_64' else ansible_architecture }}
signed-by=/etc/apt/keyrings/docker.asc]
https://download.docker.com/linux/debian {{ ansible_distribution_release }} stable
filename: docker
state: present
update_cache: true
- name: Instalar Docker Engine y el plugin de compose
ansible.builtin.apt:
name:
- docker-ce
- docker-ce-cli
- containerd.io
- docker-compose-plugin
state: present
- name: Servicio de Docker activo y arrancado
ansible.builtin.systemd:
name: docker
enabled: true
state: started
- name: Añadir el usuario de despliegue al grupo docker
ansible.builtin.user:
name: "{{ docker_user }}"
groups: docker
append: true
when: docker_user | default('') | length > 0

View file

@ -0,0 +1,75 @@
---
# Preflight es de SOLO LECTURA. No cambia nada en los blades.
# Su trabajo es contestar dos preguntas antes de gastar un minuto:
# 1. ¿Qué blade es esto exactamente?
# 2. ¿Puede correr Android x86_64, y cuántas instancias?
- name: Leer identidad DMI del blade
ansible.builtin.slurp:
src: "/sys/class/dmi/id/{{ item }}"
loop:
- sys_vendor
- product_name
register: preflight_dmi
failed_when: false
changed_when: false
- name: Registrar modelo de blade
ansible.builtin.set_fact:
preflight_vendor: "{{ (preflight_dmi.results[0].content | default('') | b64decode | trim) or 'desconocido' }}"
preflight_model: "{{ (preflight_dmi.results[1].content | default('') | b64decode | trim) or 'desconocido' }}"
- name: Leer flags de CPU
ansible.builtin.shell:
cmd: "set -o pipefail; grep -m1 '^flags' /proc/cpuinfo | cut -d: -f2"
executable: /bin/bash
register: preflight_cpuflags
changed_when: false
- name: Detectar flags de CPU que faltan
ansible.builtin.set_fact:
preflight_missing_flags: >-
{{ preflight_required_cpu_flags
| reject('in', preflight_cpuflags.stdout.split())
| list }}
- name: Comprobar módulo binder en el kernel
ansible.builtin.stat:
path: "/lib/modules/{{ ansible_kernel }}/kernel/drivers/android/binder_linux.ko"
register: preflight_binder_ko
changed_when: false
- name: Comprobar si el kernel trae binderfs
ansible.builtin.shell:
cmd: "set -o pipefail; grep -c '^CONFIG_ANDROID_BINDERFS=y' /boot/config-{{ ansible_kernel }} || true"
executable: /bin/bash
register: preflight_binderfs
changed_when: false
failed_when: false
- name: Calcular instancias que caben en este blade
ansible.builtin.set_fact:
preflight_capacity: >-
{{ [ (((ansible_memtotal_mb | int) - (redroid_host_reserve_mb | int))
/ (redroid_mem_mb | int)) | int,
redroid_max_instances | int ] | min }}
- name: Veredicto del nodo
ansible.builtin.set_fact:
preflight_ok: >-
{{ (preflight_missing_flags | length == 0)
and preflight_binder_ko.stat.exists
and (preflight_capacity | int) >= 1 }}
- name: Resumen del blade
ansible.builtin.debug:
msg:
- "modelo : {{ preflight_vendor }} {{ preflight_model }}"
- "cpu : {{ ansible_processor[2] | default('?') }} ({{ ansible_processor_vcpus }} vcpus)"
- "ram : {{ ansible_memtotal_mb }} MB"
- "kernel : {{ ansible_kernel }} / {{ ansible_distribution }} {{ ansible_distribution_version }}"
- "binder .ko : {{ 'sí' if preflight_binder_ko.stat.exists else 'NO — este blade no puede correr redroid' }}"
- "binderfs : {{ 'sí' if (preflight_binderfs.stdout | default('0') | int) > 0 else 'no (se cargan devices a mano)' }}"
- "flags CPU : {{ 'ok' if preflight_missing_flags | length == 0 else 'FALTAN ' ~ (preflight_missing_flags | join(', ')) }}"
- "capacidad : {{ preflight_capacity }} instancias"
- "veredicto : {{ 'APTO' if preflight_ok else 'NO APTO' }}"

View file

@ -0,0 +1,27 @@
---
# Defaults del rol. Lo que se toca normalmente vive en group_vars/all.yml,
# que tiene precedencia sobre esto.
redroid_image: "redroid/redroid:13.0.0-latest"
redroid_mem_mb: 2048
redroid_host_reserve_mb: 1500
redroid_instances: 0
redroid_max_instances: 8
redroid_width: 720
redroid_height: 1280
redroid_dpi: 320
redroid_gpu_mode: guest
redroid_base_dir: /opt/redroid
redroid_network_mode: portmap
redroid_adb_base_port: 5555
redroid_adb_bind_ip: "0.0.0.0"
redroid_macvlan_network: redroid-lan
redroid_macvlan_parent: ""
redroid_macvlan_subnet: ""
redroid_macvlan_gateway: ""
redroid_macvlan_ip_prefix: ""
redroid_macvlan_ip_offset: 20

View file

@ -0,0 +1,115 @@
---
# Despliega N contenedores Android en el blade. N sale de la RAM real salvo
# que se fije redroid_instances a mano.
- name: Calcular cuántas instancias caben
ansible.builtin.set_fact:
redroid_instance_count: >-
{{ (redroid_instances | int) if (redroid_instances | int) > 0
else [ (((ansible_memtotal_mb | int) - (redroid_host_reserve_mb | int))
/ (redroid_mem_mb | int)) | int,
redroid_max_instances | int ] | min }}
- name: Abortar si no cabe ni una instancia
ansible.builtin.assert:
that: (redroid_instance_count | int) >= 1
fail_msg: >-
{{ inventory_hostname }} tiene {{ ansible_memtotal_mb }} MB de RAM. Con
{{ redroid_host_reserve_mb }} MB de reserva y {{ redroid_mem_mb }} MB por
instancia no cabe ninguna. Baja redroid_mem_mb, baja la reserva, o amplía
la RAM del blade (que es la palanca buena — ver hardware_m1000e.md).
- name: Validar configuración de macvlan
ansible.builtin.assert:
that:
- redroid_macvlan_parent | length > 0
- redroid_macvlan_subnet | length > 0
- redroid_macvlan_gateway | length > 0
- redroid_macvlan_ip_prefix | length > 0
- redroid_macvlan_ip_offset is defined
fail_msg: >-
redroid_network_mode es 'macvlan' pero faltan datos de red. Necesito
redroid_macvlan_parent / _subnet / _gateway / _ip_prefix en group_vars/all.yml
y redroid_macvlan_ip_offset por host en el inventario.
when: redroid_network_mode == 'macvlan'
- name: Crear árbol de directorios
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: root
group: root
mode: "0755"
loop:
- "{{ redroid_base_dir }}"
- "{{ redroid_base_dir }}/data"
- "{{ redroid_base_dir }}/apks"
- name: Crear el volumen /data de cada instancia
ansible.builtin.file:
path: "{{ redroid_base_dir }}/data/device-{{ '%02d' % (item + 1) }}"
state: directory
owner: root
group: root
mode: "0755"
loop: "{{ range(redroid_instance_count | int) | list }}"
# --- red macvlan -----------------------------------------------------------
- name: Listar redes docker existentes
ansible.builtin.command:
cmd: docker network ls --format '{% raw %}{{.Name}}{% endraw %}'
register: redroid_networks
changed_when: false
when: redroid_network_mode == 'macvlan'
- name: Crear la red macvlan
ansible.builtin.command:
cmd: >-
docker network create -d macvlan
--subnet={{ redroid_macvlan_subnet }}
--gateway={{ redroid_macvlan_gateway }}
-o parent={{ redroid_macvlan_parent }}
{{ redroid_macvlan_network }}
when:
- redroid_network_mode == 'macvlan'
- redroid_macvlan_network not in redroid_networks.stdout_lines
changed_when: true
# --- contenedores ----------------------------------------------------------
- name: Escribir el docker-compose del nodo
ansible.builtin.template:
src: docker-compose.yml.j2
dest: "{{ redroid_base_dir }}/docker-compose.yml"
owner: root
group: root
mode: "0644"
register: redroid_compose
- name: Descargar la imagen de redroid
ansible.builtin.command:
cmd: docker compose pull
chdir: "{{ redroid_base_dir }}"
register: redroid_pull
changed_when: "'Downloaded' in redroid_pull.stderr or 'Pulled' in redroid_pull.stderr"
- name: Levantar las instancias Android
ansible.builtin.command:
cmd: docker compose up -d --remove-orphans
chdir: "{{ redroid_base_dir }}"
register: redroid_up
changed_when: "'Started' in redroid_up.stderr or 'Created' in redroid_up.stderr or 'Recreated' in redroid_up.stderr"
- name: Contar contenedores en marcha
ansible.builtin.command:
cmd: docker compose ps --status running --format '{% raw %}{{.Name}}{% endraw %}'
chdir: "{{ redroid_base_dir }}"
register: redroid_running
changed_when: false
- name: Resumen del nodo
ansible.builtin.debug:
msg: >-
{{ inventory_hostname }}: {{ redroid_running.stdout_lines | length }}/{{ redroid_instance_count }}
instancias en marcha ({{ redroid_network_mode }})

View file

@ -0,0 +1,38 @@
# GENERADO POR ANSIBLE — no editar a mano.
# Fuente: roles/redroid/templates/docker-compose.yml.j2
# Nodo: {{ inventory_hostname }} · {{ ansible_memtotal_mb }} MB RAM · {{ redroid_instance_count }} instancias
name: redroid-{{ inventory_hostname }}
services:
{% for i in range(redroid_instance_count | int) %}
{% set idx = '%02d' % (i + 1) %}
device-{{ idx }}:
image: {{ redroid_image }}
container_name: redroid-{{ inventory_hostname }}-{{ idx }}
privileged: true
restart: unless-stopped
mem_limit: {{ redroid_mem_mb }}m
volumes:
- {{ redroid_base_dir }}/data/device-{{ idx }}:/data
{% if redroid_network_mode == 'macvlan' %}
networks:
redroid_lan:
ipv4_address: {{ redroid_macvlan_ip_prefix }}{{ (redroid_macvlan_ip_offset | int) + i }}
{% else %}
ports:
- "{{ redroid_adb_bind_ip }}:{{ (redroid_adb_base_port | int) + i }}:5555"
{% endif %}
command:
- androidboot.redroid_width={{ redroid_width }}
- androidboot.redroid_height={{ redroid_height }}
- androidboot.redroid_dpi={{ redroid_dpi }}
- androidboot.redroid_gpu_mode={{ redroid_gpu_mode }}
{% endfor %}
{% if redroid_network_mode == 'macvlan' %}
networks:
redroid_lan:
external: true
name: {{ redroid_macvlan_network }}
{% endif %}

View file

@ -0,0 +1,29 @@
---
# Playbook maestro: provisiona la granja entera de cero.
#
# ansible-playbook site.yml
#
# Antes de esto, SIEMPRE:
# ansible-playbook playbooks/preflight.yml
#
# Es idempotente: se puede relanzar sin romper nada.
- name: Base del sistema en toda la granja
hosts: granja
gather_facts: true
roles:
- role: base
- name: Nodos de carga — binder, Docker y contenedores Android
hosts: redroid_nodes
gather_facts: true
roles:
- role: binder
- role: docker
- role: redroid
- name: Coordinador — adb y scripts de flota
hosts: coordinator
gather_facts: true
roles:
- role: coordinator

16
02_device_lab/connect.sh Executable file
View file

@ -0,0 +1,16 @@
#!/usr/bin/env bash
# Conecta adb a todas las instancias redroid levantadas por docker-compose.yml
set -euo pipefail
PORTS=(5555 5556 5557 5558)
for port in "${PORTS[@]}"; do
if ss -ltn "sport = :$port" 2>/dev/null | grep -q LISTEN; then
adb connect "127.0.0.1:$port" || true
else
echo "puerto $port sin escuchar — instancia no levantada, salto" >&2
fi
done
echo
adb devices

View file

@ -0,0 +1,40 @@
# redroid — 4 instancias Android 13 como punto de partida.
# Requiere binder cargado en el host: ver README.md
#
# Cada instancia expone adb en un puerto distinto (5555..5558).
x-redroid: &redroid
image: redroid/redroid:13.0.0-latest
privileged: true
restart: unless-stopped
mem_limit: 2048m
command:
- androidboot.redroid_width=720
- androidboot.redroid_height=1280
- androidboot.redroid_dpi=320
- androidboot.redroid_gpu_mode=guest # software; cambiar a "host" si hay GPU pasable
services:
device-01:
<<: *redroid
container_name: redroid-01
ports: ["5555:5555"]
volumes: ["./data/device-01:/data"]
device-02:
<<: *redroid
container_name: redroid-02
ports: ["5556:5555"]
volumes: ["./data/device-02:/data"]
device-03:
<<: *redroid
container_name: redroid-03
ports: ["5557:5555"]
volumes: ["./data/device-03:/data"]
device-04:
<<: *redroid
container_name: redroid-04
ports: ["5558:5555"]
volumes: ["./data/device-04:/data"]

View file

@ -0,0 +1,61 @@
# El hardware: Dell PowerEdge M1000e
Identificado el 2026-08-08. Fuente: `~/Downloads/poweredge-m1000e-spec-sheet.pdf`
(spec sheet oficial Dell EMC, 6 de diciembre de 2016).
No es un servidor suelto: es un **chasis de blades**. Cada blade es una máquina independiente
con su CPU y su RAM.
## Especificaciones del chasis
| | |
|---|---|
| Factor de forma | Modular 10U — 44,0 alto × 44,7 ancho × 75,4 cm fondo |
| Capacidad | **16 blades de media altura** (8 arriba + 8 abajo) · u 8 de altura completa · o 32 de cuarto |
| Alimentación | Hasta 6 fuentes de 3.000 W / 2.700 W (config 3+3, 2+2, 2+1…) |
| Refrigeración | 9 módulos de ventiladores redundantes |
| Gestión | CMC (Chassis Management Controller) + iDRAC por blade; web SSL, SSH/Telnet, LDAP/AD |
| Red interna | Hasta 6 módulos I/O FlexIO — los blades se ven entre sí a 1/10 GbE |
| Peso | 44 kg vacío · 179 kg cargado |
## Configuración de esta máquina
Según lo que recuerda el usuario:
- **16 blades**, 8 arriba y 8 abajo
- **8 GB de RAM por blade** → 128 GB totales, pero **repartidos, no agrupados**
- Modelo de blade: **desconocido** (M610 / M620 / M630 — pendiente de confirmar en el frontal)
- Estado / ubicación / accesibilidad de red: **pendiente**
## Qué implica para el device lab
### A favor
- 16 máquinas independientes con **red interna de chasis**. Mejor topología que un solo host para
el escenario de `oasis_mobile` con **dos APKs que se tienen que ver entre sí**: el switch FlexIO
los pone en la misma L2 sin montar nada.
- Con 8 GB por blade menos Debian + Docker (~11,5 GB), salen **~3 instancias redroid por blade**
**~48 instancias totales**. Casi el triple que el sobremesa (1220).
### En contra
- **Consumo: 1,52,5 kW en marcha.** A precio doméstico en España, del orden de **160350 €/mes**
si está 24/7. Este es el factor que decide la viabilidad, no los cores.
- **Ruido**: no es máquina de casa. Necesita sala con extracción.
- **Sin GPU** en los blades → `redroid_gpu_mode=guest` (render por software). Vale para test
funcional, no para nada gráfico.
- **16 provisiones separadas**: cada blade necesita su Debian + Docker + módulo binder. A partir de
4-5 blades esto pide Ansible o netboot, no hacerlo a mano.
### La palanca real
El cuello de botella son **los 8 GB por blade, no la CPU**. Si los blades son M620 (DDR3 ECC),
ampliar a 32 GB por blade cuesta poco en segunda mano y sube de ~3 a **~14 instancias por blade**.
Eso multiplica por 4-5 la capacidad del proyecto entero por menos de lo que cuesta un mes de luz.
## Pendiente de averiguar
- [ ] **Modelo de blade** (serigrafiado en el frontal) → determina CPU, generación y tipo de RAM
- [ ] ¿Está encendido y accesible el CMC en red? Si responde, da el inventario completo
- [ ] Ubicación física y quién asume el coste eléctrico
- [ ] Con eso: decidir si se levantan los 16 blades o solo 2-3 como piloto

19
02_device_lab/install-apk.sh Executable file
View file

@ -0,0 +1,19 @@
#!/usr/bin/env bash
# Instala un APK en todas las instancias redroid conectadas.
# Uso: ./install-apk.sh ruta/al/app.apk
set -euo pipefail
APK="${1:?uso: $0 <ruta.apk>}"
[[ -f "$APK" ]] || { echo "no existe: $APK" >&2; exit 1; }
mapfile -t DEVICES < <(adb devices | awk '/^127\.0\.0\.1:[0-9]+\tdevice$/ {print $1}')
if [[ ${#DEVICES[@]} -eq 0 ]]; then
echo "no hay dispositivos conectados — ejecuta ./connect.sh primero" >&2
exit 1
fi
for dev in "${DEVICES[@]}"; do
echo "== $dev =="
adb -s "$dev" install -r "$APK"
done

44
03_charla/README.md Normal file
View file

@ -0,0 +1,44 @@
# Charla: la economía de las granjas de bots
Vía 2. Ángulo defensivo/periodístico, no operativo. Hay demanda real de esto y encaja con
HackMadrid, los hacklabs y **Hackmeeting València (912 oct 2026)**.
## Tesis
La granja de bots perdió la guerra técnica y la ganó la económica. La detección de emulador es
determinista desde 2025 (Play Integrity con atestación por hardware), así que el negocio se vio
forzado a volver al hardware físico, caro y lento. Lo que queda no es un problema de cómputo sino
de identidad: **quien controla las SIMs controla el mercado** — y por eso ahí es donde pega la
policía.
## Guion propuesto
1. **Las tres cosas que llamamos "phone farm"** — reward farming / device lab / account farming.
Desmontar la confusión de entrada.
2. **El catálogo real** — GenFarmer y PhoneFarmBox como material de clase: qué venden, a qué
precio, qué revela su marketing (Zalo, Telegram-only, "300+ dispositivos").
3. **Por qué no puedes hacerlo con un servidor** — atestación por hardware, sensores, baseband.
El momento en que el cómputo dejó de ser el cuello de botella.
4. **La capa SIM** — el eslabón débil, con KYC en origen y rastro de dinero.
5. **SIMCARTEL** — 1.200 SIM-boxes, 40.000 SIMs, 49 millones de cuentas falsas, 7 detenidos.
El caso que cierra el argumento.
6. **Qué significa para quien defiende** — cómo se ve una granja desde el otro lado y qué señales
deja.
## Material
El sustrato de los puntos 3, 4 y 5 está entero en
**[../01_investigacion/capa_identidad.md](../01_investigacion/capa_identidad.md)** — verificación
telefónica, por qué la eSIM no vale, reputación de IP, atestación por hardware y la correlación
entre cuentas. Es el documento del que sale la tesis.
El resto en [../01_investigacion/](../01_investigacion/); las fuentes citables en
[../01_investigacion/fuentes.md](../01_investigacion/fuentes.md).
## Pendiente
- [ ] Decidir formato y duración
- [ ] Contactar HackMadrid / CFP de Hackmeeting València
- [ ] Buscar cifras actualizadas de 2026 (los datos son de oct 2025)
- [ ] ¿Demo en vivo? El device lab de [../02_device_lab/](../02_device_lab/) sirve para enseñar
cómo se detecta un emulador

137
GUIA.md Normal file
View file

@ -0,0 +1,137 @@
# Guía de trabajo
Cómo seguir con esto. Estado real, qué está verificado y qué no, y por dónde continuar.
---
## Estado a 2026-08-08
| Parte | Estado |
|---|---|
| Investigación | **Cerrada.** Cinco documentos en `01_investigacion/` |
| Automatización Ansible | **Escrita, sin ejecutar** contra hardware |
| Documentación de requisitos, costes y seguridad | **Cerrada** |
| Hardware | **Sin acceso.** Modelo de blade sin confirmar |
| Decisión de qué vía se ejecuta | **Pendiente** |
### Qué está verificado
- Los 18 ficheros YAML parsean correctamente
- La plantilla de `docker-compose` renderiza YAML válido en **los dos modos de red**, con puertos
e IPs bien asignados
- `fleet.sh` pasa `bash -n` una vez renderizado, sin Jinja pendiente
- El M1000e está identificado por su spec sheet oficial (Dell EMC, dic 2016)
### Qué NO está verificado
Y conviene tenerlo claro antes de fiarse de nada:
- **Nada se ha ejecutado contra hardware real.** Ni un blade, ni el sobremesa
- Que las imágenes x86_64 de Android arranquen en la generación de CPU de estos blades —
el riesgo real si resultan ser **M610** (Xeon 20092011)
- El comportamiento de `macvlan` sobre los switches FlexIO concretos del chasis
- Que 3 instancias por blade sean cómodas con 8 GB en carga real, no solo en el papel
- **Las cifras de consumo son estimaciones.** El CMC da el dato real; media hora ahí sustituye
todo `docs/costes.md` por medidas
---
## Las tres vías
Ninguna elegida todavía.
### 1 · Device lab en el rack
Infra legal y reutilizable. Primer uso real: probar `oasis_mobile` a escala, incluido el escenario
de **dos APKs que se tienen que ver entre sí**. La automatización ya está escrita.
**Bloqueante**: sitio con 2,63,6 kW, extracción y tolerancia a 7085 dB. Ver
[docs/requisitos.md](docs/requisitos.md).
### 2 · Charla sobre la economía de los bots
Ángulo defensivo y periodístico, con demanda real. HackMadrid, hacklabs, **Hackmeeting València
(912 oct 2026)**. El material está entero en `01_investigacion/`; el guion en
[03_charla/](03_charla/).
**Es la vía con menos fricción**: no necesita encender nada.
### 3 · Reward farming
Ingreso pasivo, legal-gris. Márgenes malos en 2026. Documentado por completitud, no recomendado.
---
## Preguntas abiertas
Por orden de lo que más desbloquea:
1. **¿Qué modelo de blade hay en el chasis?** M610 / M620 / M630. Determina CPU, generación y tipo
de RAM — y si el proyecto es viable. → `ansible-playbook playbooks/preflight.yml`
2. **¿Responde el CMC en red?** Si sí, da el inventario completo y el **consumo real**
3. **¿Dónde va el chasis y quién asume la factura?** Es lo que decide, más que ninguna
consideración técnica
4. **¿Cuánta utilización real va a tener?** Por debajo del ~11 % la nube sale más barata que
tenerlo 24/7. Ver [docs/costes.md](docs/costes.md)
---
## Siguientes pasos concretos
### Sin tocar el hardware (se puede hacer hoy)
- [ ] Lanzar el **piloto de un nodo** en un sobremesa: valida el pipeline entero por ~20 €/mes.
El bloque está comentado en `02_device_lab/ansible/inventory/hosts.yml.example`
- [ ] Con eso, confirmar que redroid arranca, que `fleet` habla con los contenedores y que un APK
de `oasis_mobile` se instala en toda la flota
- [ ] Preparar la charla — el material ya está
### Cuando haya acceso al chasis
- [ ] Credenciales de fábrica del **CMC y de los 16 iDRAC** (`root`/`calvin`) —
[docs/seguridad.md](docs/seguridad.md) S2
- [ ] VLAN aislada para la granja y **otra** para gestión — S1 y S3
- [ ] `preflight.yml` sobre los 16 blades → informe en `ansible/reports/`
- [ ] Medir consumo real en el CMC y corregir `docs/costes.md`
- [ ] Decidir sobre la ampliación de RAM: es lo único que cambia la economía del proyecto
(7,6 € → 1,7 € por dispositivo/mes)
### Mejoras pendientes en el código
- [ ] Fijar la imagen de redroid **por digest** en vez de por tag mutable — S6
- [ ] Cortafuegos (nftables) en los blades: 5555+ solo desde el coordinador — S1
- [ ] `ansible-vault` para secretos — S7
- [ ] Rol de GADS, cuando la flota pase de una decena de dispositivos. No está automatizado a
propósito: montar un despliegue de Node sin poder probarlo es complejidad sin verificar
- [ ] Provisión de Debian por PXE, para no instalar 16 blades a mano
---
## Convenciones
**Documentación en castellano.** Es un repo de trabajo, no un producto.
**Todo lo verificable, verificado, y lo demás dicho.** Si algo no se ha ejecutado, el documento lo
dice. Las estimaciones van etiquetadas como estimaciones y con sus supuestos a la vista. Si
encuentras una afirmación sin respaldo, es un fallo — corrígela o márcala.
**Los hallazgos de seguridad se numeran** (S1…S11 en [docs/seguridad.md](docs/seguridad.md)) y se
referencian por número desde el resto del repositorio.
**Nada operativo de alta masiva de cuentas.** Ni proveedores de SIM, ni evasión de fingerprint, ni
maduración. No es una postura moral: es que está bajo persecución activa en la UE y **no funciona**
con este hardware, por lo que explica
[01_investigacion/capa_identidad.md](01_investigacion/capa_identidad.md).
---
## Qué NO se sube al repositorio
Está en `.gitignore`, pero conviene saber por qué:
| | |
|---|---|
| `inventory/hosts.yml` | IPs reales de los blades y posibles credenciales. Solo va el `.example` |
| `ansible/reports/` | Informes de preflight con el inventario real del hardware |
| `*.apk` | Binarios |
| `vault.yml`, `*.vault` | Secretos de ansible-vault |

151
README.md Normal file
View file

@ -0,0 +1,151 @@
# Rebelión en la granja
Investigación y laboratorio sobre **granjas de dispositivos**: qué son realmente, quién las
fabrica, qué se puede montar con hardware propio y qué no, y por qué la partida técnica ya está
decidida.
Incluye la automatización completa para levantar un **laboratorio de dispositivos Android** sobre
un chasis de blades Debian — legal, reutilizable y sin un solo móvil físico.
Arrancado el 2026-08-07 a raíz de dos webs del sector:
[phonefarmbox.com](https://www.phonefarmbox.com/) y [genfarmer.com](https://genfarmer.com/).
---
## La conclusión, por delante
> **El cuello de botella nunca fue el cómputo. Fue la identidad.**
>
> Desde que la atestación de dispositivo pasó de ser estadística a ser **criptográfica**, un
> servidor no puede hacerse pasar por un teléfono: le falta una clave grabada en fábrica que
> ningún software produce. Por eso el mercado vende cajas con teléfonos físicos de 400 $ en lugar
> de software para tu rack.
>
> Lo que queda es un problema de SIMs, de reputación de IP y de correlación entre cuentas. Y ahí es
> exactamente donde pega la policía.
El desarrollo está en **[01_investigacion/capa_identidad.md](01_investigacion/capa_identidad.md)**,
que es el documento central del repositorio.
---
## Qué hay aquí
### 📄 [docs/](docs/) — léelo antes de encender nada
| | |
|---|---|
| [requisitos.md](docs/requisitos.md) | Eléctrico, físico, hardware, red, tiempo. Con checklist de bloqueantes |
| [costes.md](docs/costes.md) | La luz es el coste real: ~340 €/mes 24/7, ~56 €/mes por ventanas |
| [seguridad.md](docs/seguridad.md) | 11 hallazgos, uno crítico. Incluye los que introduce este propio código |
### 🔍 [01_investigacion/](01_investigacion/) — el análisis
| | |
|---|---|
| [capa_identidad.md](01_investigacion/capa_identidad.md) | **El documento central.** Verificación telefónica, por qué la eSIM no vale, reputación de IP, atestación por hardware y la correlación entre cuentas |
| [mercado_y_players.md](01_investigacion/mercado_y_players.md) | Los tres negocios bajo el mismo nombre, quién juega en Europa, TikTok vs Instagram |
| [stack_tecnico.md](01_investigacion/stack_tecnico.md) | redroid, Cuttlefish, Waydroid, GADS — y por qué el rack no resuelve el caso social |
| [legal_ue.md](01_investigacion/legal_ue.md) | DSA, Operación SIMCARTEL, riesgo penal en España |
| [fuentes.md](01_investigacion/fuentes.md) | Enlaces |
### 🔧 [02_device_lab/](02_device_lab/) — el laboratorio
| | |
|---|---|
| [ansible/](02_device_lab/ansible/) | **La automatización completa**: preflight, despliegue de los 16 blades y control de flota |
| [hardware_m1000e.md](02_device_lab/hardware_m1000e.md) | El chasis, sus límites y la palanca (RAM) |
| [README.md](02_device_lab/README.md) | Montaje de un solo nodo, a mano, para entender las piezas |
### 🎤 [03_charla/](03_charla/) — divulgación
Guion de 6 puntos para HackMadrid / Hackmeeting València.
---
## Especificaciones
### Arquitectura
```
Dell PowerEdge M1000e — chasis de blades, 16 slots (8 arriba + 8 abajo)
├── blade-01 ................ COORDINADOR
│ └── adb + fleet único punto de control de toda la flota
└── blade-02 .. blade-16 .... NODOS DE CARGA (15)
└── Debian 12 pelado
└── Docker
└── N × redroid Android 13 en contenedor
8 GB/blade → 3 instancias → ~45 Android
32 GB/blade → 14 instancias → ~210 Android
```
**No hay móviles físicos.** Cada "dispositivo" es un contenedor
[redroid](https://github.com/remote-android/redroid-doc): Android real sobre el kernel del blade.
Debian va **directo sobre el hierro**. Sin Proxmox, sin VMs, sin LXC anidado — con 8 GB por blade,
cada capa de hipervisor se come una instancia Android entera.
### Stack
| Capa | Tecnología |
|---|---|
| Hardware | Dell PowerEdge M1000e, 16 blades, 8 GB/blade |
| SO | Debian 12 bookworm |
| Kernel | `binder_linux` con devices `binder,hwbinder,vndbinder` (Debian no trae binderfs) |
| Contenedores | Docker CE + Compose v2 |
| Android | redroid 13 (memfd, sin ashmem) |
| Orquestación | Ansible — solo `ansible.builtin`, sin colecciones externas |
| Control | adb sobre TCP + `fleet` |
| Red | `portmap` (NAT) o `macvlan` (IP real de LAN por el switch FlexIO) |
### Números
| | |
|---|---|
| Capacidad | ~45 Android (~210 con la RAM ampliada) |
| Consumo | 2,63,6 kW · 7085 dB · 179 kg · 10U |
| Coste 24/7 | ~340 €/mes (~4.100 €/año) |
| Coste por ventanas de 4 h/día | ~56 €/mes |
| Coste por dispositivo | 7,6 €/mes → **1,7 €/mes** con la RAM ampliada |
---
## Arranque rápido
```bash
git clone https://gitea.laenre.net/hacklab/REBELION_EN_LA_GRANJA.git
cd REBELION_EN_LA_GRANJA/02_device_lab/ansible
sudo apt install ansible
cp inventory/hosts.yml.example inventory/hosts.yml # pon las IPs reales
ansible-playbook playbooks/preflight.yml # SOLO LECTURA — no toca nada
ansible-playbook site.yml # monta la granja
ansible-playbook playbooks/status.yml
```
**Empieza por el preflight siempre.** Te dice qué blades hay dentro del chasis leyéndolo del DMI,
si esas CPUs pueden con Android x86_64, y cuántas instancias caben. Antes de gastar un minuto.
**Y antes de encender el chasis**, el inventario trae un bloque de **piloto de un nodo** para
probar el pipeline entero en un sobremesa, por 20 €/mes y sin ruido.
Guía de trabajo y siguientes pasos: **[GUIA.md](GUIA.md)**.
---
## Aviso
Este repositorio documenta cómo funcionan las granjas de dispositivos y **por qué fracasan** contra
las plataformas. La automatización que contiene levanta un laboratorio de QA para probar apps
propias.
No contiene, ni va a contener, operativa de alta masiva de cuentas: proveedores de SIM, evasión de
fingerprint o maduración de cuentas. Eso es fraude de identidad a escala, en la UE está bajo
persecución activa, y además **no funcionaría** con este hardware por lo que se explica en
[capa_identidad.md](01_investigacion/capa_identidad.md).
Contexto legal: [legal_ue.md](01_investigacion/legal_ue.md).

25
docs/README.md Normal file
View file

@ -0,0 +1,25 @@
# Docs
Lo que hay que leer antes de encender nada.
| | |
|---|---|
| **[requisitos.md](requisitos.md)** | Qué hace falta: eléctrico, físico, hardware, software, red, accesos y tiempo. Con checklist de bloqueantes. |
| **[costes.md](costes.md)** | Lo que cuesta tenerlo en marcha, por escenario y por dispositivo. La luz es el coste real del proyecto. |
| **[seguridad.md](seguridad.md)** | Repaso de seguridad con 11 hallazgos, incluidos los que introduce el código de este proyecto. |
## Las tres conclusiones, en corto
**Requisitos** — el bloqueante no es técnico: el chasis pide 2,63,6 kW, más de lo que tiene
contratado una vivienda española, y hace 7085 dB. Sin un sitio con potencia, extracción y
tolerancia al ruido, la vía es el piloto en el sobremesa.
**Costes** — la granja completa 24/7 son **~340 €/mes**, unos 4.100 € al año: entre tres y ocho
veces lo que vale el hierro de segunda mano. Encendida **por ventanas** baja a ~56 €/mes. Y con
8 GB por blade, el sobremesa sale más barato por dispositivo que el chasis; solo la ampliación de
RAM invierte eso.
**Seguridad** — hay un hallazgo **crítico**: adb no tiene autenticación y los contenedores son
privilegiados, así que quien alcance el puerto 5555 de un blade acaba siendo root en él. Se ha
mitigado a medias en el código (`redroid_adb_bind_ip`); lo que lo cierra de verdad es una **VLAN
aislada**. Y antes de eso, cambiar las credenciales de fábrica del CMC y de los 16 iDRAC.

119
docs/costes.md Normal file
View file

@ -0,0 +1,119 @@
# Costes
El hardware ya está. El coste real de este proyecto es **la luz**, y no es pequeño.
> **Todas las cifras de consumo son estimaciones.** El M1000e reporta consumo real por chasis y por
> blade desde el CMC — antes de comprometerse con nada, hay que mirar ahí. Es media hora de trabajo
> y sustituye todo este documento por datos.
---
## Supuestos
| | |
|---|---|
| Consumo fijo del chasis | 350 W (ventiladores, CMC, switches, pérdidas de las fuentes) — rango 250500 W |
| Consumo por blade con Android en marcha | 140 W — rango 100200 W |
| Precio de la electricidad | **0,18 €/kWh** — rango 0,150,22 según tarifa y tramo |
| Horas al mes | 730 |
| Reparto | 1 blade coordinador + N de carga × 3 instancias |
---
## Coste mensual por escenario, 24/7
| Escenario | Blades | Potencia | kWh/mes | **€/mes** | Android | **€/Android/mes** |
|---|---|---|---|---|---|---|
| Solo sobremesa, chasis apagado | 0 | 150 W | 110 | **20 €** | 8 | **2,5 €** |
| Piloto en el chasis | 3 | 770 W | 562 | **101 €** | 6 | **16,9 €** |
| Media granja | 8 | 1.470 W | 1.073 | **193 €** | 21 | **9,2 €** |
| Granja completa | 16 | 2.590 W | 1.891 | **340 €** | 45 | **7,6 €** |
| Completa + RAM a 32 GB | 16 | 2.750 W | 2.008 | **361 €** | **210** | **1,7 €** |
Granja completa 24/7: **~4.100 €/año**.
### Tres cosas que dice esta tabla
**El sobremesa gana al chasis por dispositivo — hasta que se amplíe la RAM.** 2,5 €/Android en el
sobremesa contra 7,6 € en el chasis completo. Con 8 GB por blade, encender el M1000e para hacer lo
que ya hace el sobremesa es pagar tres veces más por dispositivo.
**Los pilotos en el chasis son el peor negocio de todos.** Los 350 W fijos se pagan igual con 3
blades que con 16, así que el piloto sale a 16,9 € por Android — casi siete veces el sobremesa. Si
se enciende el chasis, se enciende entero; y para probar, se usa el sobremesa.
**La ampliación de RAM es lo único que cambia la ecuación.** De 7,6 € a 1,7 € por Android, por el
mismo recibo de la luz. No ahorra dinero: multiplica por 4,7 lo que se obtiene por el dinero que ya
se está gastando.
---
## El modo que sí sale a cuenta: encendido bajo demanda
El CMC permite encender y apagar blades individualmente. Un laboratorio de QA no necesita 45
Android a las cuatro de la mañana.
| Uso | Potencia | kWh/mes | €/mes | €/Android/mes |
|---|---|---|---|---|
| 24/7 | 2.590 W | 1.891 | 340 € | 7,6 € |
| **4 h/día** | 2.590 W | 311 | **56 €** | **1,2 €** |
| 8 h/día laborables | 2.590 W | 456 | 82 € | 1,8 € |
**Ventanas de prueba en vez de servicio permanente**: el coste cae seis veces y el proyecto pasa de
inviable a trivial. Es como debería operarse esto salvo que aparezca una razón para lo contrario.
---
## Costes de una vez
| Concepto | Coste | Notas |
|---|---|---|
| **RAM 8 → 32 GB × 16 blades** | **320640 €** | Si son M620 (DDR3 ECC): ~1020 € el módulo de 16 GB de segunda mano, 2 por blade. Si son M630 (DDR4): 8001.300 € |
| SSD por blade, si faltan | ~400 € | 16 × ~25 € |
| PDU con tomas C19 + cableado | 50150 € | Las fuentes del M1000e no usan C13 |
| Subida de potencia contratada | 1050 € | Derecho de cambio, una vez |
| **Total realista** | **~4001.200 €** | Sin contar los SSD si los blades ya tienen disco |
Coste fijo adicional: subir de 3,45 a 6,9 kW contratados añade **~130 €/año** solo en término de
potencia, se use o no.
---
## Comparación con las alternativas
| Opción | Coste | Cuándo tiene sentido |
|---|---|---|
| **Sobremesa, 8 Android** | ~20 €/mes | Desarrollo diario y pruebas de `oasis_mobile`. Silencioso, ya encendido. |
| **Chasis bajo demanda, 45 Android** | ~56 €/mes + 4001.200 € de entrada | Matrices de prueba grandes, escenarios multi-dispositivo |
| **Chasis 24/7, 45 Android** | ~340 €/mes | Solo si algo tiene que estar servido permanentemente |
| **Device farm en la nube** | ~0,09 €/dispositivo-hora | Uso esporádico. 45 dispositivos 24/7 saldrían por ~2.950 €/mes: diez veces el chasis |
| **Teléfonos físicos tipo GenFarmer** | 3961.364 $/dispositivo | Otro producto: son los únicos que pasan atestación de hardware. 45 dispositivos = 17.80061.400 $ |
**El punto de equilibrio con la nube** está en torno al **11 % de utilización**: si cada dispositivo
se usa más de ~84 horas al mes, el chasis 24/7 sale más barato. Por debajo, la nube gana. Y por
debajo del 11 %, lo que gana de verdad es encender el chasis solo cuando haga falta.
---
## El dato incómodo
Un M1000e con 16 blades vale hoy de segunda mano entre **500 y 1.500 €**.
Tenerlo encendido 24/7 durante un año cuesta **~4.100 € de luz**.
**La electricidad de un año cuesta entre tres y ocho veces lo que vale el hierro.** Eso no es un
argumento para tirarlo — es un argumento para no dejarlo encendido por defecto.
---
## Recomendación
1. **Medir antes que estimar.** El CMC da el consumo real del chasis y de cada blade. Es lo primero.
2. **Empezar en el sobremesa.** El piloto de un nodo valida el pipeline entero por 20 €/mes y sin
ruido. Ver [ansible/README.md](../02_device_lab/ansible/README.md).
3. **Encender el chasis solo con un caso concreto que el sobremesa no cubra** — matriz de versiones
de Android, o el escenario multi-dispositivo de los dos APKs a escala real.
4. **Si se enciende, encenderlo entero y por ventanas.** Los 350 W fijos hacen que los pilotos
parciales sean el peor negocio.
5. **La RAM antes que nada más.** 400 € que multiplican por 4,7 la capacidad sin tocar el recibo.
Pero primero hay que saber qué blades son — lo dice `playbooks/preflight.yml`.

140
docs/requisitos.md Normal file
View file

@ -0,0 +1,140 @@
# Requisitos
Todo lo que hace falta para levantar la granja. Lo eléctrico y lo físico está primero a propósito:
es lo que descarta el proyecto antes que ninguna consideración técnica.
---
## 1 · Eléctrico y físico — lee esto primero
| Requisito | Detalle |
|---|---|
| **Potencia contratada** | El chasis completo tira **2,63,6 kW**. Un domicilio español típico tiene **3,45 kW contratados**: no da. Hace falta subir a 6,9 kW o más, o un local con suministro trifásico. |
| **Circuito** | Dedicado. Nada de compartir con el resto de la instalación. |
| **Tensión y conectores** | Dell recomienda **208240 V** para el M1000e. España a 230 V vale. Fuentes con conectores **C19/C20**, no los C13 normales. |
| **Espacio** | **10U** de rack, con raíles capaces de aguantar el peso. |
| **Peso** | **179 kg cargado.** El rack tiene que soportarlo y el suelo también. |
| **Refrigeración** | 2,6 kW de consumo son 2,6 kW de calor: como dos radiadores eléctricos a tope. Necesita extracción real, no una ventana. |
| **Ruido** | 7085 dB con los 9 ventiladores en marcha. **No es una máquina de vivienda.** |
> **Esto es lo que decide.** Si no hay sitio con potencia, extracción y tolerancia al ruido, el
> resto de este documento no importa: la vía es el piloto en el sobremesa.
---
## 2 · Hardware
### El chasis
Dell PowerEdge M1000e — ver [hardware_m1000e.md](../02_device_lab/hardware_m1000e.md).
- 16 blades de media altura (8 arriba + 8 abajo)
- Al menos 2 fuentes (mejor 3+3 en redundancia)
- Módulos de I/O FlexIO con conectividad entre blades y hacia la LAN
- CMC accesible
### Por blade
| | Mínimo | Recomendado |
|---|---|---|
| RAM | 8 GB (→ 3 instancias) | **32 GB (→ 14 instancias)** ← la palanca del proyecto |
| Disco | 1 disco, 60 GB libres | SSD; ~10 GB por instancia con margen |
| CPU | x86_64 con `sse4_2`, `ssse3`, `popcnt` | Cualquier Xeon de M620 en adelante |
| Red | 1 GbE por el FlexIO | 10 GbE si se va a mover mucho APK |
**Sin verificar todavía**: el modelo exacto de blade. Lo resuelve `playbooks/preflight.yml`
leyendo el DMI — no hace falta ir a mirar el frontal. Si salen **M610** (Xeon 20092011), hay
riesgo real de que las imágenes x86_64 de Android no arranquen; el preflight lo dice antes de
gastar tiempo.
### Nodo de control
El sobremesa actual (16 cores, 62 GB, Debian 12) sobra. Solo necesita SSH a los blades.
---
## 3 · Software
### En los blades
- **Debian 12 bookworm** instalado y arrancando
- Kernel con `binder_linux.ko` — el estándar de Debian lo trae
- SSH accesible
- Nada más: Docker y el resto lo pone Ansible
### En el nodo de control
```bash
sudo apt install ansible
```
Sin colecciones externas: todo el código usa solo `ansible.builtin`.
### Se descarga durante el despliegue
- Docker CE desde `download.docker.com`
- Imagen `redroid/redroid:13.0.0-latest` desde Docker Hub (~1 GB, una vez por blade)
- Paquetes de `deb.debian.org`
---
## 4 · Red
| | |
|---|---|
| **Salida a internet** desde los blades | O un mirror local de Docker y Debian |
| **VLAN aislada** para la granja | No opcional — ver [seguridad.md](seguridad.md) S1 y S3 |
| **VLAN de gestión** aparte | Para CMC e iDRAC, separada de la anterior |
| SSH del control a los blades | Puerto 22 |
| adb del coordinador a los nodos | 5555+ , solo dentro de la VLAN de granja |
| Reserva DHCP | Solo en modo `macvlan`: un rango para los Android, o habrá colisiones |
---
## 5 · Accesos
- Clave SSH del nodo de control a los 16 blades
- sudo o root en los blades
- **Credenciales del CMC** — y cambiarlas si siguen siendo las de fábrica ([seguridad.md](seguridad.md) S2)
- Acceso físico para el primer arranque y para meter RAM
---
## 6 · Conocimientos y tiempo
| | |
|---|---|
| Ansible | Nivel usuario: editar inventario y lanzar playbooks. No hace falta escribir roles. |
| Docker | Poco: los contenedores los gestiona Ansible |
| Redes | **Aquí sí hace falta**: VLANs, macvlan y el switching del chasis es la parte que más muerde |
| Dell OME/CMC | Básico: encender blades, mirar consumo, actualizar firmware |
Tiempo estimado, con el hierro ya en su sitio y encendido:
- Instalar Debian en 16 blades: **1 día** (menos con PXE)
- Red y VLANs: **medio día**
- Ansible y despliegue: **2 horas** (la automatización ya está escrita)
- Ajuste y verificación: **medio día**
Mantenimiento posterior: 24 h al mes.
---
## 7 · Checklist antes de empezar
**Bloqueantes** — si alguno falla, el proyecto no sale:
- [ ] Sitio con potencia suficiente, extracción y tolerancia al ruido
- [ ] Alguien asume la factura eléctrica (ver [costes.md](costes.md))
- [ ] Rack de 10U que aguante 179 kg
- [ ] Los blades arrancan y el CMC responde
**Antes de encender**:
- [ ] Credenciales de CMC e iDRAC cambiadas
- [ ] VLANs de granja y gestión, separadas
- [ ] `playbooks/preflight.yml` en verde en todos los blades
**Antes de tocar el chasis**:
- [ ] El piloto de un nodo funciona en el sobremesa — valida el pipeline entero sin encender 2 kW

218
docs/seguridad.md Normal file
View file

@ -0,0 +1,218 @@
# Repaso de seguridad
Revisión del montaje completo — arquitectura, hardware y el código Ansible de
[`02_device_lab/ansible/`](../02_device_lab/ansible/). Fecha: 2026-08-08.
Incluye los fallos que introduce el propio código de este proyecto, no solo los del entorno.
## Resumen
| | Hallazgo | Severidad | Estado |
|---|---|---|---|
| **S1** | adb sin autenticación + contenedor privilegiado → root del anfitrión | **Crítico** | Mitigado a medias |
| **S2** | Credenciales por defecto del CMC / iDRAC (`root`/`calvin`) | **Alto** | Sin verificar |
| **S3** | macvlan mete 45 Android en la LAN de producción | **Alto** | Por diseño, hay que aislar |
| **S4** | `host_key_checking = False` en `ansible.cfg` | Medio | Aceptado, documentado |
| **S5** | SSH como root en el inventario de ejemplo | Medio | Documentado |
| **S6** | Imagen de Android por tag mutable | Medio | Abierto |
| **S7** | Sin gestión de secretos | Medio | Abierto |
| **S8** | Estado persistente en `/data` sin cifrar | Medio | Abierto |
| **S9** | `fleet install` no verifica el APK | Bajo | Abierto |
| **S10** | Microcódigo antiguo en CPUs de 2011-2016 | Bajo | Aceptado |
| **S11** | Infraestructura de doble uso | Contextual | Ver [legal_ue.md](../01_investigacion/legal_ue.md) |
---
## S1 · adb sin autenticación sobre contenedor privilegiado — **crítico**
El fallo más serio, y es una cadena de tres eslabones que por separado parecen asumibles:
1. **adb no tiene autenticación** cuando escucha en TCP. Quien llega al puerto, entra.
2. **Las imágenes de redroid son `userdebug`**, así que `adb root` funciona: shell de root dentro
del Android.
3. **El contenedor es `privileged: true`** — redroid lo exige para acceder a binder. Un privilegiado
ve los devices del anfitrión y puede montar su sistema de ficheros.
Encadenado: **cualquiera que alcance el puerto 5555 de un blade acaba siendo root en ese blade.**
No hace falta ningún exploit, solo `adb connect`.
Y con 45 dispositivos son 45 puertas.
**Qué se ha corregido hoy.** El compose ataba adb a `0.0.0.0` — todas las interfaces. Ahora hay
`redroid_adb_bind_ip`, así que se puede atar solo a la interfaz interna del chasis:
```yaml
blade-02: { ansible_host: 10.0.0.12, redroid_adb_bind_ip: 10.0.0.12 }
```
**Qué falta, y es lo que de verdad cierra el agujero:**
- **Red de laboratorio aislada.** VLAN propia para la granja, sin ruta hacia la LAN de casa ni
hacia internet salvo lo imprescindible. Es la medida que importa; el resto son parches.
- **Cortafuegos en cada blade** (nftables) que solo acepte 5555+ desde la IP del coordinador.
- **Nunca exponer estos puertos a internet.** Ni con reenvío de puertos, ni con VPN mal segmentada.
En modo `macvlan` el problema es peor: adb queda escuchando en la IP de LAN del propio Android,
donde `redroid_adb_bind_ip` no aplica. Ahí la única defensa es la VLAN.
---
## S2 · Credenciales por defecto del CMC y de los iDRAC — **alto**
Los Dell PowerEdge salen de fábrica con **`root` / `calvin`** en iDRAC, y el CMC del M1000e con las
mismas. Es la forma clásica en que se toma uno de estos chasis: no hay que romper nada, se entra.
El M1000e es de 2016 o anterior, así que el firmware de gestión arrastra años de CVEs conocidas y
TLS antiguo.
Antes de enchufarlo a nada:
- Cambiar las credenciales del CMC **y de cada uno de los 16 iDRAC**, no solo del CMC.
- Actualizar firmware de CMC e iDRAC hasta donde Dell lo permita para esa generación.
- **VLAN de gestión separada**, sin salida a internet. La gestión de un chasis nunca va en la
misma red que el resto.
- Desactivar IPMI sobre LAN si no se usa (`cipher 0` es un clásico de acceso sin autenticar).
Esto es independiente de la granja: aplica al hierro desde el momento en que se encienda.
---
## S3 · macvlan expone 45 Android en la LAN — **alto**
El modo `macvlan` es el que hace falta para el caso de los dos APKs de `oasis_mobile`, pero pone
cada Android **con IP real en la red**, cada uno con S1 encima.
- VLAN dedicada para la granja, con el coordinador como único puente.
- Reservar el rango de IPs en el DHCP, o habrá colisiones con máquinas reales.
- No usar la LAN de casa. Nunca.
---
## S4 · `host_key_checking = False` — medio
Lo puse yo en `ansible.cfg` por comodidad de arranque. El coste: si alguien se hace pasar por un
blade, Ansible se conecta igual y le entrega **comandos de root y, si se usa, la contraseña de
sudo**.
Es asumible en el primer aprovisionamiento de una red aislada. Después:
```bash
ssh-keyscan -H 10.0.0.11 10.0.0.12 ... >> ~/.ssh/known_hosts
```
y poner `host_key_checking = True`.
---
## S5 · SSH como root — medio
`inventory/hosts.yml.example` trae `ansible_user: root`. Funciona, pero:
- Usuario dedicado con sudo en vez de root directo
- **Solo clave, sin contraseña**: `PasswordAuthentication no`
- `PermitRootLogin no` en los blades
- Clave separada para la granja, no la personal
---
## S6 · Imagen de Android por tag mutable — medio
`redroid/redroid:13.0.0-latest` es un tag que **se mueve**. Consecuencias reales:
- Dos blades provisionados con una semana de diferencia acaban con imágenes distintas, y depurar
eso es un infierno.
- Cadena de suministro: si la cuenta del registro se compromete, el siguiente `docker compose pull`
se trae otra cosa. Y entra en 45 contenedores privilegiados.
Fijar por digest:
```yaml
redroid_image: "redroid/redroid@sha256:<digest>"
```
Con `docker buildx imagetools inspect redroid/redroid:13.0.0-latest` se saca el digest actual.
Se actualiza a mano cuando se quiera subir de versión, que es justo lo que se quiere.
---
## S7 · Sin gestión de secretos — medio
El inventario de ejemplo menciona `ansible_become_password` sin montar `ansible-vault`. Si se pone
una contraseña ahí en claro, queda en el disco y en cualquier copia del proyecto.
```bash
ansible-vault create group_vars/granja/vault.yml
ansible-playbook site.yml --ask-vault-pass
```
Mejor aún: sudo sin contraseña para el usuario de despliegue, restringido a los comandos que
necesita, y ninguna contraseña en ningún fichero.
---
## S8 · Estado persistente sin cifrar — medio
Cada Android guarda su `/data` en `{{ redroid_base_dir }}/data/device-NN` del blade, sin cifrar.
Si en las pruebas se entra con cuentas reales — cuentas SSB, credenciales de Gitea, lo que sea —
esas credenciales quedan en el disco del blade en claro.
`destroy.yml` **conserva los datos por defecto**, a propósito, así que sobreviven a un
redespliegue y a un cambio de manos del hardware.
- Cuentas de prueba desechables, nunca las reales.
- Cifrado de disco en los blades si van a manejar algo sensible.
- Al dar de baja un blade: `destroy.yml -e confirmar=si -e borrar_datos=si` **y** borrado del disco.
---
## S9 · `fleet install` no verifica nada — bajo
`fleet install app.apk` mete el APK en los 45 dispositivos sin comprobar firma ni hash. Como los
APK son propios (`oasis_mobile`), el riesgo es bajo, pero el patrón es malo: un fichero equivocado
se propaga a toda la flota de una vez.
Comprobar el hash antes, y a ser posible la firma con `apksigner verify --print-certs`.
---
## S10 · Microcódigo antiguo — bajo
CPUs de 20112016 con microcódigo sin actualizar: las mitigaciones de Spectre/Meltdown dependen de
microcódigo + kernel. En un laboratorio aislado que solo corre código propio, el riesgo es bajo.
Subiría si algún día se corre software de terceros no confiable en los Android.
`apt install intel-microcode` en los blades, y aun así esa generación no recibe todo.
---
## S11 · Infraestructura de doble uso — contextual
Técnicamente, esto es indistinguible de una granja de cuentas: 45 Android automatizados, con
control central y despliegue masivo de APKs. Lo que separa un laboratorio de QA de una granja de
fraude no es la infraestructura, son **tres cosas que no están aquí**: SIMs, proxies residenciales
y cuentas de plataforma.
Para que siga siendo lo que es:
- No conectar la granja a proveedores de SIM ni de números por API.
- No meter proxies residenciales o móviles.
- Documentar el propósito con evidencia — este repositorio ya lo hace.
El contexto legal está en [legal_ue.md](../01_investigacion/legal_ue.md). Y recuerda el hallazgo de
la investigación: estos Android **son detectables como emulador**, así que para las plataformas
sociales no servirían aunque se quisiera.
---
## Mínimos antes de encender
Por orden:
1. Cambiar credenciales del **CMC y de los 16 iDRAC** (S2)
2. **VLAN aislada** para granja y gestión, separadas entre sí (S1, S2, S3)
3. `redroid_adb_bind_ip` a la IP interna de cada blade (S1)
4. Claves SSH, sin root, sin contraseñas (S5)
5. Poblar `known_hosts` y `host_key_checking = True` (S4)
6. Fijar la imagen por digest (S6)
Los tres primeros no son opcionales. Sin ellos, la granja es 45 puertas abiertas a la red.