From 74bfbe9437d8971619104d126339036fa18b0ad8 Mon Sep 17 00:00:00 2001 From: s1to Date: Sat, 8 Aug 2026 19:42:14 +0200 Subject: [PATCH] =?UTF-8?q?Rebeli=C3=B3n=20en=20la=20granja=20=E2=80=94=20?= =?UTF-8?q?investigaci=C3=B3n=20y=20laboratorio=20de=20dispositivos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .gitignore | 26 ++ 01_investigacion/capa_identidad.md | 282 ++++++++++++++++ 01_investigacion/fuentes.md | 19 ++ 01_investigacion/legal_ue.md | 34 ++ 01_investigacion/mercado_y_players.md | 70 ++++ 01_investigacion/stack_tecnico.md | 45 +++ 02_device_lab/README.md | 112 +++++++ 02_device_lab/ansible/README.md | 302 ++++++++++++++++++ 02_device_lab/ansible/ansible.cfg | 18 ++ 02_device_lab/ansible/group_vars/all.yml | 80 +++++ .../ansible/inventory/hosts.yml.example | 70 ++++ 02_device_lab/ansible/playbooks/apk.yml | 60 ++++ .../ansible/playbooks/coordinator.yml | 19 ++ 02_device_lab/ansible/playbooks/destroy.yml | 48 +++ 02_device_lab/ansible/playbooks/devices.yml | 23 ++ 02_device_lab/ansible/playbooks/preflight.yml | 37 +++ 02_device_lab/ansible/playbooks/provision.yml | 18 ++ 02_device_lab/ansible/playbooks/status.yml | 53 +++ .../templates/preflight-report.md.j2 | 28 ++ .../ansible/roles/base/tasks/main.yml | 38 +++ .../ansible/roles/binder/tasks/main.yml | 84 +++++ .../roles/coordinator/defaults/main.yml | 2 + .../ansible/roles/coordinator/tasks/main.yml | 61 ++++ .../coordinator/templates/devices.txt.j2 | 18 ++ .../roles/coordinator/templates/fleet.sh.j2 | 135 ++++++++ .../ansible/roles/docker/tasks/main.yml | 53 +++ .../ansible/roles/preflight/tasks/main.yml | 75 +++++ .../ansible/roles/redroid/defaults/main.yml | 27 ++ .../ansible/roles/redroid/tasks/main.yml | 115 +++++++ .../redroid/templates/docker-compose.yml.j2 | 38 +++ 02_device_lab/ansible/site.yml | 29 ++ 02_device_lab/connect.sh | 16 + 02_device_lab/docker-compose.yml | 40 +++ 02_device_lab/hardware_m1000e.md | 61 ++++ 02_device_lab/install-apk.sh | 19 ++ 03_charla/README.md | 44 +++ GUIA.md | 137 ++++++++ README.md | 151 +++++++++ docs/README.md | 25 ++ docs/costes.md | 119 +++++++ docs/requisitos.md | 140 ++++++++ docs/seguridad.md | 218 +++++++++++++ 42 files changed, 2989 insertions(+) create mode 100644 .gitignore create mode 100644 01_investigacion/capa_identidad.md create mode 100644 01_investigacion/fuentes.md create mode 100644 01_investigacion/legal_ue.md create mode 100644 01_investigacion/mercado_y_players.md create mode 100644 01_investigacion/stack_tecnico.md create mode 100644 02_device_lab/README.md create mode 100644 02_device_lab/ansible/README.md create mode 100644 02_device_lab/ansible/ansible.cfg create mode 100644 02_device_lab/ansible/group_vars/all.yml create mode 100644 02_device_lab/ansible/inventory/hosts.yml.example create mode 100644 02_device_lab/ansible/playbooks/apk.yml create mode 100644 02_device_lab/ansible/playbooks/coordinator.yml create mode 100644 02_device_lab/ansible/playbooks/destroy.yml create mode 100644 02_device_lab/ansible/playbooks/devices.yml create mode 100644 02_device_lab/ansible/playbooks/preflight.yml create mode 100644 02_device_lab/ansible/playbooks/provision.yml create mode 100644 02_device_lab/ansible/playbooks/status.yml create mode 100644 02_device_lab/ansible/playbooks/templates/preflight-report.md.j2 create mode 100644 02_device_lab/ansible/roles/base/tasks/main.yml create mode 100644 02_device_lab/ansible/roles/binder/tasks/main.yml create mode 100644 02_device_lab/ansible/roles/coordinator/defaults/main.yml create mode 100644 02_device_lab/ansible/roles/coordinator/tasks/main.yml create mode 100644 02_device_lab/ansible/roles/coordinator/templates/devices.txt.j2 create mode 100644 02_device_lab/ansible/roles/coordinator/templates/fleet.sh.j2 create mode 100644 02_device_lab/ansible/roles/docker/tasks/main.yml create mode 100644 02_device_lab/ansible/roles/preflight/tasks/main.yml create mode 100644 02_device_lab/ansible/roles/redroid/defaults/main.yml create mode 100644 02_device_lab/ansible/roles/redroid/tasks/main.yml create mode 100644 02_device_lab/ansible/roles/redroid/templates/docker-compose.yml.j2 create mode 100644 02_device_lab/ansible/site.yml create mode 100755 02_device_lab/connect.sh create mode 100644 02_device_lab/docker-compose.yml create mode 100644 02_device_lab/hardware_m1000e.md create mode 100755 02_device_lab/install-apk.sh create mode 100644 03_charla/README.md create mode 100644 GUIA.md create mode 100644 README.md create mode 100644 docs/README.md create mode 100644 docs/costes.md create mode 100644 docs/requisitos.md create mode 100644 docs/seguridad.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d3ea4e3 --- /dev/null +++ b/.gitignore @@ -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/ diff --git a/01_investigacion/capa_identidad.md b/01_investigacion/capa_identidad.md new file mode 100644 index 0000000..b3d5f9d --- /dev/null +++ b/01_investigacion/capa_identidad.md @@ -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 diff --git a/01_investigacion/fuentes.md b/01_investigacion/fuentes.md new file mode 100644 index 0000000..5566be8 --- /dev/null +++ b/01_investigacion/fuentes.md @@ -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) diff --git a/01_investigacion/legal_ue.md b/01_investigacion/legal_ue.md new file mode 100644 index 0000000..643e17a --- /dev/null +++ b/01_investigacion/legal_ue.md @@ -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. diff --git a/01_investigacion/mercado_y_players.md b/01_investigacion/mercado_y_players.md new file mode 100644 index 0000000..1841d7e --- /dev/null +++ b/01_investigacion/mercado_y_players.md @@ -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. diff --git a/01_investigacion/stack_tecnico.md b/01_investigacion/stack_tecnico.md new file mode 100644 index 0000000..4443331 --- /dev/null +++ b/01_investigacion/stack_tecnico.md @@ -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. diff --git a/02_device_lab/README.md b/02_device_lab/README.md new file mode 100644 index 0000000..45f6ca7 --- /dev/null +++ b/02_device_lab/README.md @@ -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/.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)). diff --git a/02_device_lab/ansible/README.md b/02_device_lab/ansible/README.md new file mode 100644 index 0000000..5eed637 --- /dev/null +++ b/02_device_lab/ansible/README.md @@ -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-.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 `:`, 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`). diff --git a/02_device_lab/ansible/ansible.cfg b/02_device_lab/ansible/ansible.cfg new file mode 100644 index 0000000..ecfa4c5 --- /dev/null +++ b/02_device_lab/ansible/ansible.cfg @@ -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 diff --git a/02_device_lab/ansible/group_vars/all.yml b/02_device_lab/ansible/group_vars/all.yml new file mode 100644 index 0000000..eae8d27 --- /dev/null +++ b/02_device_lab/ansible/group_vars/all.yml @@ -0,0 +1,80 @@ +# --------------------------------------------------------------------------- +# Mandos del operador. Lo que se toca normalmente está aquí. +# Los defaults internos de cada rol están en roles//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 : +# 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 diff --git a/02_device_lab/ansible/inventory/hosts.yml.example b/02_device_lab/ansible/inventory/hosts.yml.example new file mode 100644 index 0000000..9a87b07 --- /dev/null +++ b/02_device_lab/ansible/inventory/hosts.yml.example @@ -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 diff --git a/02_device_lab/ansible/playbooks/apk.yml b/02_device_lab/ansible/playbooks/apk.yml new file mode 100644 index 0000000..c8d9c7f --- /dev/null +++ b/02_device_lab/ansible/playbooks/apk.yml @@ -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 diff --git a/02_device_lab/ansible/playbooks/coordinator.yml b/02_device_lab/ansible/playbooks/coordinator.yml new file mode 100644 index 0000000..86db773 --- /dev/null +++ b/02_device_lab/ansible/playbooks/coordinator.yml @@ -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 diff --git a/02_device_lab/ansible/playbooks/destroy.yml b/02_device_lab/ansible/playbooks/destroy.yml new file mode 100644 index 0000000..007d240 --- /dev/null +++ b/02_device_lab/ansible/playbooks/destroy.yml @@ -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' }}. diff --git a/02_device_lab/ansible/playbooks/devices.yml b/02_device_lab/ansible/playbooks/devices.yml new file mode 100644 index 0000000..b10d227 --- /dev/null +++ b/02_device_lab/ansible/playbooks/devices.yml @@ -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 diff --git a/02_device_lab/ansible/playbooks/preflight.yml b/02_device_lab/ansible/playbooks/preflight.yml new file mode 100644 index 0000000..8f6e135 --- /dev/null +++ b/02_device_lab/ansible/playbooks/preflight.yml @@ -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" diff --git a/02_device_lab/ansible/playbooks/provision.yml b/02_device_lab/ansible/playbooks/provision.yml new file mode 100644 index 0000000..a3dad9f --- /dev/null +++ b/02_device_lab/ansible/playbooks/provision.yml @@ -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 diff --git a/02_device_lab/ansible/playbooks/status.yml b/02_device_lab/ansible/playbooks/status.yml new file mode 100644 index 0000000..e54481f --- /dev/null +++ b/02_device_lab/ansible/playbooks/status.yml @@ -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 diff --git a/02_device_lab/ansible/playbooks/templates/preflight-report.md.j2 b/02_device_lab/ansible/playbooks/templates/preflight-report.md.j2 new file mode 100644 index 0000000..8b3d0d8 --- /dev/null +++ b/02_device_lab/ansible/playbooks/templates/preflight-report.md.j2 @@ -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. diff --git a/02_device_lab/ansible/roles/base/tasks/main.yml b/02_device_lab/ansible/roles/base/tasks/main.yml new file mode 100644 index 0000000..70ac10d --- /dev/null +++ b/02_device_lab/ansible/roles/base/tasks/main.yml @@ -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 diff --git a/02_device_lab/ansible/roles/binder/tasks/main.yml b/02_device_lab/ansible/roles/binder/tasks/main.yml new file mode 100644 index 0000000..d3f311a --- /dev/null +++ b/02_device_lab/ansible/roles/binder/tasks/main.yml @@ -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." diff --git a/02_device_lab/ansible/roles/coordinator/defaults/main.yml b/02_device_lab/ansible/roles/coordinator/defaults/main.yml new file mode 100644 index 0000000..f97c39f --- /dev/null +++ b/02_device_lab/ansible/roles/coordinator/defaults/main.yml @@ -0,0 +1,2 @@ +--- +coordinator_dir: /opt/granja diff --git a/02_device_lab/ansible/roles/coordinator/tasks/main.yml b/02_device_lab/ansible/roles/coordinator/tasks/main.yml new file mode 100644 index 0000000..0254137 --- /dev/null +++ b/02_device_lab/ansible/roles/coordinator/tasks/main.yml @@ -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 · fleet shell ''" diff --git a/02_device_lab/ansible/roles/coordinator/templates/devices.txt.j2 b/02_device_lab/ansible/roles/coordinator/templates/devices.txt.j2 new file mode 100644 index 0000000..8373fda --- /dev/null +++ b/02_device_lab/ansible/roles/coordinator/templates/devices.txt.j2 @@ -0,0 +1,18 @@ +# GENERADO POR ANSIBLE — no editar a mano. +# Una dirección adb por línea: : +# 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 %} diff --git a/02_device_lab/ansible/roles/coordinator/templates/fleet.sh.j2 b/02_device_lab/ansible/roles/coordinator/templates/fleet.sh.j2 new file mode 100644 index 0000000..b017725 --- /dev/null +++ b/02_device_lab/ansible/roles/coordinator/templates/fleet.sh.j2 @@ -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 instala un APK en toda la flota + fleet uninstall desinstala un paquete de toda la flota + fleet shell '' 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 }" + [[ -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 }" + 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 ''}" + 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 diff --git a/02_device_lab/ansible/roles/docker/tasks/main.yml b/02_device_lab/ansible/roles/docker/tasks/main.yml new file mode 100644 index 0000000..56641b2 --- /dev/null +++ b/02_device_lab/ansible/roles/docker/tasks/main.yml @@ -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 diff --git a/02_device_lab/ansible/roles/preflight/tasks/main.yml b/02_device_lab/ansible/roles/preflight/tasks/main.yml new file mode 100644 index 0000000..c3d9a40 --- /dev/null +++ b/02_device_lab/ansible/roles/preflight/tasks/main.yml @@ -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' }}" diff --git a/02_device_lab/ansible/roles/redroid/defaults/main.yml b/02_device_lab/ansible/roles/redroid/defaults/main.yml new file mode 100644 index 0000000..8dc3cd2 --- /dev/null +++ b/02_device_lab/ansible/roles/redroid/defaults/main.yml @@ -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 diff --git a/02_device_lab/ansible/roles/redroid/tasks/main.yml b/02_device_lab/ansible/roles/redroid/tasks/main.yml new file mode 100644 index 0000000..e024f49 --- /dev/null +++ b/02_device_lab/ansible/roles/redroid/tasks/main.yml @@ -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 }}) diff --git a/02_device_lab/ansible/roles/redroid/templates/docker-compose.yml.j2 b/02_device_lab/ansible/roles/redroid/templates/docker-compose.yml.j2 new file mode 100644 index 0000000..4599816 --- /dev/null +++ b/02_device_lab/ansible/roles/redroid/templates/docker-compose.yml.j2 @@ -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 %} diff --git a/02_device_lab/ansible/site.yml b/02_device_lab/ansible/site.yml new file mode 100644 index 0000000..d832ee4 --- /dev/null +++ b/02_device_lab/ansible/site.yml @@ -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 diff --git a/02_device_lab/connect.sh b/02_device_lab/connect.sh new file mode 100755 index 0000000..7e3887c --- /dev/null +++ b/02_device_lab/connect.sh @@ -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 diff --git a/02_device_lab/docker-compose.yml b/02_device_lab/docker-compose.yml new file mode 100644 index 0000000..4ded6dc --- /dev/null +++ b/02_device_lab/docker-compose.yml @@ -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"] diff --git a/02_device_lab/hardware_m1000e.md b/02_device_lab/hardware_m1000e.md new file mode 100644 index 0000000..014b189 --- /dev/null +++ b/02_device_lab/hardware_m1000e.md @@ -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 diff --git a/02_device_lab/install-apk.sh b/02_device_lab/install-apk.sh new file mode 100755 index 0000000..4c35cfd --- /dev/null +++ b/02_device_lab/install-apk.sh @@ -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 }" +[[ -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 diff --git a/03_charla/README.md b/03_charla/README.md new file mode 100644 index 0000000..4d6d5c5 --- /dev/null +++ b/03_charla/README.md @@ -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 diff --git a/GUIA.md b/GUIA.md new file mode 100644 index 0000000..c6d9a9c --- /dev/null +++ b/GUIA.md @@ -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 | diff --git a/README.md b/README.md new file mode 100644 index 0000000..3d4a14c --- /dev/null +++ b/README.md @@ -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). diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..707ee70 --- /dev/null +++ b/docs/README.md @@ -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. diff --git a/docs/costes.md b/docs/costes.md new file mode 100644 index 0000000..05de800 --- /dev/null +++ b/docs/costes.md @@ -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`. diff --git a/docs/requisitos.md b/docs/requisitos.md new file mode 100644 index 0000000..302df28 --- /dev/null +++ b/docs/requisitos.md @@ -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 diff --git a/docs/seguridad.md b/docs/seguridad.md new file mode 100644 index 0000000..21f281a --- /dev/null +++ b/docs/seguridad.md @@ -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:" +``` + +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.