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:
commit
74bfbe9437
42 changed files with 2989 additions and 0 deletions
26
.gitignore
vendored
Normal file
26
.gitignore
vendored
Normal 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/
|
||||||
282
01_investigacion/capa_identidad.md
Normal file
282
01_investigacion/capa_identidad.md
Normal 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
|
||||||
19
01_investigacion/fuentes.md
Normal file
19
01_investigacion/fuentes.md
Normal 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)
|
||||||
34
01_investigacion/legal_ue.md
Normal file
34
01_investigacion/legal_ue.md
Normal 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.
|
||||||
70
01_investigacion/mercado_y_players.md
Normal file
70
01_investigacion/mercado_y_players.md
Normal 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.
|
||||||
45
01_investigacion/stack_tecnico.md
Normal file
45
01_investigacion/stack_tecnico.md
Normal 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
112
02_device_lab/README.md
Normal 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, **12–20 instancias redroid** simultáneas es el
|
||||||
|
rango realista aquí. No son las 30–60 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)).
|
||||||
302
02_device_lab/ansible/README.md
Normal file
302
02_device_lab/ansible/README.md
Normal 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`).
|
||||||
18
02_device_lab/ansible/ansible.cfg
Normal file
18
02_device_lab/ansible/ansible.cfg
Normal 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
|
||||||
80
02_device_lab/ansible/group_vars/all.yml
Normal file
80
02_device_lab/ansible/group_vars/all.yml
Normal 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
|
||||||
70
02_device_lab/ansible/inventory/hosts.yml.example
Normal file
70
02_device_lab/ansible/inventory/hosts.yml.example
Normal 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
|
||||||
60
02_device_lab/ansible/playbooks/apk.yml
Normal file
60
02_device_lab/ansible/playbooks/apk.yml
Normal 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
|
||||||
19
02_device_lab/ansible/playbooks/coordinator.yml
Normal file
19
02_device_lab/ansible/playbooks/coordinator.yml
Normal 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
|
||||||
48
02_device_lab/ansible/playbooks/destroy.yml
Normal file
48
02_device_lab/ansible/playbooks/destroy.yml
Normal 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' }}.
|
||||||
23
02_device_lab/ansible/playbooks/devices.yml
Normal file
23
02_device_lab/ansible/playbooks/devices.yml
Normal 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
|
||||||
37
02_device_lab/ansible/playbooks/preflight.yml
Normal file
37
02_device_lab/ansible/playbooks/preflight.yml
Normal 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"
|
||||||
18
02_device_lab/ansible/playbooks/provision.yml
Normal file
18
02_device_lab/ansible/playbooks/provision.yml
Normal 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
|
||||||
53
02_device_lab/ansible/playbooks/status.yml
Normal file
53
02_device_lab/ansible/playbooks/status.yml
Normal 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
|
||||||
|
|
@ -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 2009–2011).
|
||||||
|
- **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.
|
||||||
38
02_device_lab/ansible/roles/base/tasks/main.yml
Normal file
38
02_device_lab/ansible/roles/base/tasks/main.yml
Normal 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
|
||||||
84
02_device_lab/ansible/roles/binder/tasks/main.yml
Normal file
84
02_device_lab/ansible/roles/binder/tasks/main.yml
Normal 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."
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
---
|
||||||
|
coordinator_dir: /opt/granja
|
||||||
61
02_device_lab/ansible/roles/coordinator/tasks/main.yml
Normal file
61
02_device_lab/ansible/roles/coordinator/tasks/main.yml
Normal 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>'"
|
||||||
|
|
@ -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 %}
|
||||||
135
02_device_lab/ansible/roles/coordinator/templates/fleet.sh.j2
Normal file
135
02_device_lab/ansible/roles/coordinator/templates/fleet.sh.j2
Normal 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
|
||||||
53
02_device_lab/ansible/roles/docker/tasks/main.yml
Normal file
53
02_device_lab/ansible/roles/docker/tasks/main.yml
Normal 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
|
||||||
75
02_device_lab/ansible/roles/preflight/tasks/main.yml
Normal file
75
02_device_lab/ansible/roles/preflight/tasks/main.yml
Normal 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' }}"
|
||||||
27
02_device_lab/ansible/roles/redroid/defaults/main.yml
Normal file
27
02_device_lab/ansible/roles/redroid/defaults/main.yml
Normal 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
|
||||||
115
02_device_lab/ansible/roles/redroid/tasks/main.yml
Normal file
115
02_device_lab/ansible/roles/redroid/tasks/main.yml
Normal 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 }})
|
||||||
|
|
@ -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 %}
|
||||||
29
02_device_lab/ansible/site.yml
Normal file
29
02_device_lab/ansible/site.yml
Normal 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
16
02_device_lab/connect.sh
Executable 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
|
||||||
40
02_device_lab/docker-compose.yml
Normal file
40
02_device_lab/docker-compose.yml
Normal 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"]
|
||||||
61
02_device_lab/hardware_m1000e.md
Normal file
61
02_device_lab/hardware_m1000e.md
Normal 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 (~1–1,5 GB), salen **~3 instancias redroid por blade**
|
||||||
|
→ **~48 instancias totales**. Casi el triple que el sobremesa (12–20).
|
||||||
|
|
||||||
|
### En contra
|
||||||
|
|
||||||
|
- **Consumo: 1,5–2,5 kW en marcha.** A precio doméstico en España, del orden de **160–350 €/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
19
02_device_lab/install-apk.sh
Executable 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
44
03_charla/README.md
Normal 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 (9–12 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
137
GUIA.md
Normal 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 2009–2011)
|
||||||
|
- 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,6–3,6 kW, extracción y tolerancia a 70–85 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
|
||||||
|
(9–12 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
151
README.md
Normal 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,6–3,6 kW · 70–85 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
25
docs/README.md
Normal 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,6–3,6 kW, más de lo que tiene
|
||||||
|
contratado una vivienda española, y hace 70–85 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
119
docs/costes.md
Normal 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 250–500 W |
|
||||||
|
| Consumo por blade con Android en marcha | 140 W — rango 100–200 W |
|
||||||
|
| Precio de la electricidad | **0,18 €/kWh** — rango 0,15–0,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** | **320–640 €** | Si son M620 (DDR3 ECC): ~10–20 € el módulo de 16 GB de segunda mano, 2 por blade. Si son M630 (DDR4): 800–1.300 € |
|
||||||
|
| SSD por blade, si faltan | ~400 € | 16 × ~25 € |
|
||||||
|
| PDU con tomas C19 + cableado | 50–150 € | Las fuentes del M1000e no usan C13 |
|
||||||
|
| Subida de potencia contratada | 10–50 € | Derecho de cambio, una vez |
|
||||||
|
| **Total realista** | **~400–1.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 + 400–1.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** | 396–1.364 $/dispositivo | Otro producto: son los únicos que pasan atestación de hardware. 45 dispositivos = 17.800–61.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
140
docs/requisitos.md
Normal 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,6–3,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 **208–240 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** | 70–85 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 2009–2011), 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: 2–4 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
218
docs/seguridad.md
Normal 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 2011–2016 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.
|
||||||
Loading…
Add table
Add a link
Reference in a new issue