scieee AI-readable full text Open interactive document viewer

Herramienta para el despliegue de laboratorios virtuales mediante Docker

Castro García, Álvaro de

Abstract

En el ecosistema digital actual, las redes de computadores son cada vez más complejas y su funcionamiento es cada vez más difícil de comprender sin ver a las mismas en ejecución. Para solucionar este problema, existen múltiples de simuladores, emuladores y otras herramientas que permiten comprobar el funcionamiento de una red sin necesidad de montarla físicamente. El presente documento tiene como objetivo presentar una alternativa a las herramientas actuales de despliegue de laboratorios virtuales, dockerlab, la cual reúne funcionalidades de algunas de las más populares y consta de una sintaxis reducida para su funcionamiento, ofreciéndole así al usuario la posibilidad de trabajar con redes de computadores virtuales con mucha más facilidad que mediante medios convencionales. En el presente documento, además, se puede encontrar un análisis de la situación actual con respecto a las tecnologías con finalidades similares, que servirá para verificar la utilidad que presenta el desarrollo de dockerlab frente al uso de herramientas convencionales.

Full text

I Equation Chapter 1 Section 1 Trabajo Fin de Grado Grado en Ingeniería de las Tecnologías de Telecomunicación Herramienta para el despliegue de laboratorios virtuales mediante Docker Autor: Álvaro de Castro García Tutor: Vicente Jesús Mayor Gallego Dpto. Ingeniería Telemática Escuela Técnica Superior de Ingeniería Universidad de Sevilla Sevilla, 2023 III Trabajo Fin de Grado Grado en Ingeniería de las Tecnologías de Telecomunicación Herramienta para el despliegue de laboratorios virtuales mediante Docker Autor: Álvaro de Castro García Tutor: Vicente Jesús Mayor Gallego Profesor Ayudante Doctor Dpto. de Ingeniería Telemática Escuela Técnica Superior de Ingeniería Universidad de Sevilla Sevilla, 2023 V Trabajo Fin de Grado: Herramienta para el despliegue de laboratorios virtuales mediante Docker Autor: Álvaro de Castro García Tutor: Vicente Jesús Mayor Gallego El tribunal nombrado para juzgar el Proyecto arriba indicado, compuesto por los siguientes miembros: Presidente: Vocales: Secretario: Acuerdan otorgarle la calificación de: Sevilla, 2013 El Secretario del Tribunal VII A mi familia A mis maestros IX Agradecimientos e acerca el final de una de las etapas más enriquecedoras de mi vida, en la que he descubierto mi pasión por el mundo de las telecomunicaciones. Nada de esto habría sido posible sin la ayuda y el apoyo de toda la gente que ha estado a mi lado a lo largo de estos años y, cuyo interés por mi formación ha culminado en el presente documento. Debo agradecer a mi familia por haberme ofrecido la oportunidad de formarme en las materias que me apasionan y que, a pesar de todas las noches de insomnio y semanas sin levantar la cabeza del pupitre, me han ayudado a ver el mundo desde otros ojos y a estar más cerca de dedicar mi futuro profesional a los ámbitos que realmente me interesan. En segundo lugar, me gustaría agradecer a mi tutor, Vicente, por haberme apoyado tanto en la realización de este trabajo, en el que no sólo se ven volcadas mis horas de trabajo y de estudio de la materia, sino también las incontables tutorías y correos electrónicos que hemos intercambiado a lo largo de todos estos meses, sin los cuales nada de esto habría sido posible. Por último, me gustaría agradecer a todos mis compañeros de titulación y a los demás profesores que me han ayudado a obtener los conocimientos y habilidades necesarios para llegar hasta este punto. A todos ellos, y a toda la demás gente que me han ayudado a lo largo de estos años, gracias. Álvaro de Castro García Sevilla, 2023 S 6 Referencias 41 7 Anexo: Código del script Dockerlab 43 XVII ÍNDICE DE TABLAS Tabla 1: Comparación entre VirtualBox y VMware Workstation 4 Tabla 2: Comparación entre las distintas herramientas de despliegue de Docker 8 Tabla 3: Diferencias entre Network simulator y GNS3 13 XIX ÍNDICE DE FIGURAS Ilustración 1: Arquitectura de una máquina virtual 3 Ilustración 2: Funcionamiento de docker swarm. Se aprecia como un mismo servicio tiene varias réplicas de una tarea, las cuales son acarreadas por uno o varios contenedores. 7 Ilustración 3: Funcionamiento de kubernetes. Se aprecian los nodos que forman un clúster de kubernetes, así como los servicios que estos ofrecen y los pods que componen cada servicio. 8 Ilustración 4: Ejemplo de uso de Compose Generator 10 Ilustración 5: Proyecto de ejemplo en GNS3 13 Ilustración 6: Funcionamiento básico de dockerlab 15 Ilustración 7: Diferencias entre contenedores y Máquinas virtuales. 16 Ilustración 8: Funcionamiento resumido de dockerlab 18 Ilustración 9: Clasificación de los archivos de la aplicación 19 Ilustración 10: Extracto de config.yml 20 Ilustración 11: Archivo docker-compose.yml generado tras la ejecución de dockerlab 21 Ilustración 12: Subprocesos generados al construir el archivo docker-compose.yml 23 Ilustración 13: Procesos generados durante la ejecución del software 24 Ilustración 14: Procesos generados para la gestión de entrada de usuario 24 Ilustración 15: Resultado de ejecutar la bandera “-h” o “--help" 25 Ilustración 16: Ejemplo de pasos previos a la ejecución del software 25 Ilustración 17: Ejemplo de ejecución de docker-compose mediante dockerlab 26 Ilustración 18: Ejemplo de salida al detener la ejecución de los contenedores 26 Ilustración 19: Indicación de que se ha detenido la ejecución de los contenedores correctamente 26 Ilustración 20: Ventana de "utilización" antes de recibir ningún dato 27 Ilustración 21: Ventana de "utilización" una vez hay datos que mostrar 27 Ilustración 22: Fragmento de output.pcap 27 Ilustración 23: Escenario 1. Red Industrial MQTT 29 Ilustración 24: Árbol de ficheros empleados en el caso 1 30 Ilustración 25: Comparación entre el archivo config.yml y docker-compose.yml generado en el escenario 1 31 Ilustración 26: Captura del tráfico generado en el escenario 1 32 Ilustración 27: Utilización de recursos de cada uno de los contenedores del escenario 1 32 Ilustración 28: Salida estándar de la ejecución del escenario 1 33 Ilustración 29: Escenario 2. Generación de dataset 33 Ilustración 30: Árbol de ficheros empleado en el escenario 2 34 Ilustración 31: Utilización de recursos de cada uno de los contenedores del escenario 2 35 Ilustración 32: Utilización de recursos del IDS del escenario 2 35 Ilustración 33: Captura del tráfico de red durante el ataque de ICMP flooding. 36 Ilustración 34: Log generado por el IDS tras analizar el archivo de captura de tráfico de la red 36 Ilustración 35: Comparación entre el archivo de configuración y el docker-compose.yml generados en el escenario 2. 37 XXI ÍNDICE DE CÓDIGOS Código 1: entrada de composerize para comprobar su funcionamiento 9 Código 2: Archivo docker-compose.yml generado por composerize 9 Código 3: Comandos a ejecutar para actualizar la lista de repositorios y los paquetes instalados 22 Código 4: Comandos para instalar el software del que depende dockerlab 22 Código 5: Comando para instalar las dependencias de Python 22 Código 6: Dockerfile del broker MQTT del caso de uso 1 30 Código 7: Contenido de mosquitto.conf del caso de uso 1 30 Código 8: Script a ejecutar por los clientes MQTT del caso de uso 1 30 Código 9: Dockerfile de la imagen de Kali Linux personalizada empleado en el caso de uso 2 34 Código 10: Script ejecutado por el contenedor "Hacker" del caso de uso 2 34 Código 11: Script ejecutado por el contenedor "Víctima" del caso de uso 2 34 Código 12: Script ejecutado por el contenedor "IDS" en el caso de uso 2 35 1 1 INTRODUCCIÓN a posibilidad de conocer el comportamiento de una red antes de su despliegue es una de las bases de la ingeniería de redes. A lo largo de los años, han surgido múltiples herramientas que permiten el despliegue o simulación de redes de computadores en entornos controlados, sin embargo, el funcionamiento de las herramientas más potentes es a veces tedioso, y su configuración puede tomar mucho más tiempo del que realmente sería necesario cuando se pretende recrear escenarios sencillos. Este trabajo propone el diseño e implementación de nueva herramienta, llamada dockerlab, que facilita el despliegue de laboratorios virtuales. Por ejemplo, la herramienta podría ser utilizada para la reproducción de escenarios en redes de área local; facilitando las pruebas de nuevo software (e.g., IDS), la generación de datasets (logs o capturas de tráfico), y la evaluación del rendimiento. A continuación, se describen los principales objetivos y limitaciones de la herramienta descrita en este documento. 1.1 Objetivos A pesar de que existen herramientas que podrían llegar a realizar las funciones de dockerlab, su uso implicaría un gran conocimiento por parte del usuario, ya que, al ser multipropósito, éstas ofrecen muchas opciones no relevantes en los casos de uso anteriormente mencionados o, simplemente, su configuración es compleja, monótona o incluso repetitiva. Dockerlab es una herramienta por línea de comandos que simplifica el despliegue de laboratorios virtuales, apoyándose en algunas de las tecnologías ya existentes. Gracias a sus diversas opciones de funcionamiento, puede ser empleado para realizar cualquiera de las tareas expuestas anteriormente individualmente o cualquier combinación de ellas, lo cual es logrado haciendo uso de una serie de banderas y opciones que se verán con mayor detenimiento en capítulos posteriores. Los principales objetivos que se han buscado satisfacer en el diseño de dockerlab son los siguientes: - Sintaxis textual sencilla: Un software como dockerlab requiere de una simplificación de la sintaxis con respecto a las herramientas que utiliza para proporcionar el resultado esperado para ser más amigable al usuario. - Abstracción: Ligado al objetivo anterior, se desea que el usuario no requiera de especiales conocimientos sobre las tecnologías subyacentes. - Reproducibilidad: Se busca que dockerlab genere un archivo que permita ejecutar el laboratorio virtual en cualquier lugar, sin necesidad de que sea en la misma máquina que lo ha generado. - Uso interactivo: El usuario debe poder interactuar con el software para decidir cuándo ejecutar el laboratorio virtual, cuándo pararlo y cuándo dar por finalizada la sesión. - Monitorización de recursos: Dockerlab debe proporcionar al usuario la opción de obtener información acerca del consumo de recursos de cada uno de los nodos que está ejecutando en todo momento. - Captura automática del tráfico de red: Se debe ofrecer al usuario también la posibilidad de monitorizar el tráfico de red generado por los contenedores y almacenar el mismo en un archivo para su posterior estudio. - Bajo consumo de recursos: Para ofrecer una ventaja con respecto al resto de herramientas en el mercado, se debe ofrecer un menor consumo de recursos con respecto a otras herramientas de simulación, emulación o virtualización. L Introducción 2 1.2 Limitaciones En lo que respecta a las limitaciones de dockerlab derivadas de las decisiones tomadas para su diseño, caben destacar: - Funcionamiento sólo sobre ethernet: Por la naturaleza de las herramientas de las que dockerlab hace uso, no se pueden diseñar sistemas que utilicen tarjetas de red que no sean ethernet, ya que las máquinas harán uso de puentes ethernet virtuales para comunicarse entre sí. - No permite ejecución multinodo: Debido a que se ha decidido no utilizar la tecnología “Docker swarm” (se verá en detalle en apartados posteriores), la complejidad del escenario quedará limitada por los recursos hardware de la máquina en la que se ejecute el software. En un futuro, dockerlab podría ser ampliado para incorporar las funcionalidades de “docker swarm”. - Monitorización de recursos limitada: Dockerlab pretende realizar una monitorización de recursos básicas en la que se muestren algunos de los parámetros más relevantes de cada uno de los contenedores, sin embargo, no consta de herramientas de análisis avanzadas. - Diseñada para Linux: Debido al funcionamiento del motor Docker, algunas funcionalidades, como la captura del tráfico desde el equipo anfitrión, podría no ser compatible con otros sistemas operativos (e.g., MacOS). 3 2 TECNOLOGÍAS Y TRABAJOS RELACIONADOS ste capítulo tiene como objetivo la revisión de herramientas disponibles en el mercado – preferentemente herramientas open source o gratuitas – que ofrezcan funcionalidades similares o relacionadas con la simulación, despliegue y monitorización de laboratorios virtuales, con el fin de tomar las mejores decisiones en el diseño de la misma. Es importante destacar que, aunque estas constituyan algunas de las herramientas más populares, existe una amplia gama de software diseñado con el mismo propósito y que, por tanto, el propósito de este capítulo no es más que ilustrar brevemente las alternativas disponibles a la herramienta expuesta en el presente documento, y no se trata de una revisión exhaustiva. Para ello, se propone la revisión de tecnologías en tres secciones: la primera presenta el uso de máquinas virtuales como alternativa, la segunda introduce el uso de contenedores, y la tercera trata el uso de simuladores de red. Finalmente, se ofrece una conclusión del análisis realizado. 2.1 Máquinas virtuales El despliegue de laboratorios virtuales puede verse como la ejecución una red de equipos completa en el interior de una máquina anfitriona. Para lograr esto, la primera tecnología que puede venir a la mente son las máquinas virtuales, software que es capaz de cargar en su interior otro sistema operativo, sin embargo, esta no es una solución óptima para la aplicación deseada. En el presente apartado se estudiará en detenimiento qué son estas “máquinas virtuales”, sus utilidades y los motivos por el que se ha decantado por utilizar una solución distinta. 2.1.1 ¿Qué es una máquina virtual? Tras la breve introducción expuesta anteriormente, se puede entender a una máquina virtual como un software que proporciona la misma funcionalidad que ordenadores físicos, esto es, ejecutan aplicaciones y un sistema operativo. Sin embargo, a pesar de ejecutarse sobre un ordenador físico, el hecho de que se comporten como otro hace que cada máquina virtual que se ejecute en un mismo dispositivo se comporte como un sistema informático totalmente independiente. Aplicación … Aplicación Sistema Operativo Sistema Operativo Hardware Virtual Hardware Virtual Máquina Virtual Software de virtualización Sistema Operativo Anfitrión Hardware físico Ilustración 1: Arquitectura de una máquina virtual Es importante destacar que existen dos tipos de máquinas virtuales según su funcionalidad: máquinas virtuales de sistema y máquinas virtuales de proceso, aunque, normalmente, cuando se hace referencia al concepto de “máquina virtual”, se está refiriendo a las de sistema. E Tecnologías y Trabajos Relacionados 10 10 Ilustración 4: Ejemplo de uso de Compose Generator En la imagen anterior se muestra un ejemplo de uso de Compose Generator, en el que se pone en funcionamiento una configuración de Docker Compose para un stack con Angular, Node.js, MySQL y PhpMyAdmin en unos pocos seguros. Se trata de un proyecto parecido a dockerlab, sin embargo, no facilita la configuración avanzada de cada uno de los contenedores ni proporciona un servicio de monitorización en detalle del sistema. 2.2.3 Ventajas e inconvenientes Al utilizar contenedores en un proyecto, exponemos al mismo a una serie de ventajas y desafíos que deben ser considerados según las necesidades y objetivos específicos del mismo. En lo que respecta a las ventajas, a continuación, se muestran algunas de las principales: - Aislamiento: Al igual que con las máquinas virtuales, los contenedores proporcionan un alto nivel de aislamiento entre las aplicaciones y sus dependencias. Gracias a que cada contenedor es independiente y encapsula su entorno de ejecución, se evitan conflictos y problemas de compatibilidad. - Portabilidad: Mediante contenedores, una aplicación contenida se ejecutará de la misma manera en diferentes entornos, lo cual facilita la migración y el despliegue multiplataforma. - Eficiencia de recursos: Una de las principales ventajas que ofrecen con respecto al uso de máquinas virtuales. Los contenedores comparten el kernel del sistema operativo anfitrión, lo que los hace más ligeros y eficientes en términos de recursos, permitiendo así que se ejecuten múltiples contenedores en un solo servidor físico. 11 11 Herramienta para el despliegue de laboratorios virtuales mediante Docker - Rápido despliegue: A diferencia de las máquinas virtuales, los contenedores se pueden crear y desplegar rápidamente, acelerando así el proceso de desarrollo y entrega de aplicaciones. - Orquestación: Existen herramientas como Docker Compose, Docker Swarm o Kubernetes (se estudiarán en apartados posteriores) que simplifican la administración, escalabilidad y automatización de contenedores en entornos complejos. Sin embargo, los contenedores no son herramientas perfectas y constan también de algunos inconvenientes a la hora de su utilización, algunos de los cuales serán expuestos a continuación: - Complejidad Inicial: Los contenedores son una herramienta muy poderosa pero también compleja, por lo que su adopción requiere de un aprendizaje inicial y de una correcta configuración de redes, volúmenes y mecanismos de orquestación. - Seguridad: A diferencia de con las máquinas virtuales, si no se configuran y gestionan adecuadamente, los contenedores pueden introducir riesgos de seguridad, especialmente si se utilizan imágenes no confiables. - Gestión de redes: La configuración de redes y puentes virtuales puede resultar compleja, y la conexión entre los contenedores y el mundo exterior puede requerir conocimientos adicionales. - Compatibilidad de Sistemas operativos: Algunas aplicaciones pueden requerir de sistemas operativos específicos que no sean compatibles con contenedores, limitando su uso en ciertos casos. - Tamaño de imágenes: En caso de no gestionarse adecuadamente, las imágenes de contenedores pueden volverse grandes, lo cual puede afectar al tiempo de descarga y al espacio en disco. 2.3 Simuladores de red Llamamos simulador de red al software que permite probar el funcionamiento de un sistema de comunicación entre distintos dispositivos sin necesidad de montar dicho sistema en la vida real. “La tecnología de redes de computadores está sujeta a cambios constantes e innovaciones. […] Debido a su complejidad y necesidad de compatibilidad con versiones anteriores, surgen muchos desafíos al desarrollar, implementar, probar y comprender estas tecnologías. Aquí es donde entra en juego la simulación de redes.” [3] Debido a que el software que se expone en el presente documento tiene como finalidad ser una herramienta para el despliegue de redes virtuales, es importante comprender las funcionalidades ofrecidas por otras herramientas para el estudio de redes. 2.3.1 ns: Simuladores de red en tiempo discreto Network Simulator 2 y 3 (ns-2 y ns-3) son simuladores de redes basados en eventos discretos empleados principalmente en entornos educativos y de investigación, siendo especialmente utilizados en el entorno de la investigación de redes móviles ad-hoc. “Network Simulator […] se ha convertido en un estándar de facto debido a su amplia utilización. Su estructura permite obtener una visión global de las redes que facilita la relación de conceptos de distintas áreas como podría ser la propagación de señales en medios inalámbricos con el desarrollo de nuevos mecanismos de comunicación. A pesar de estas ventajas, presenta como principal inconveniente la dificultad asociada al desarrollo de nuevos protocolos para este entorno.” [4] Ns comenzó como una variante del simulador de redes “REAL” en 1989 y su evolución a lo largo de las últimas décadas lo ha convertido en una de las herramientas de simulación más empleadas en entornos académicos y de investigación. Sus últimas versiones (ns-2 y ns-3) son empleadas por universidades e investigadores independientes de todo el mundo por su versatilidad y la cantidad de opciones de configuración que ofrecen. Tecnologías y Trabajos Relacionados 12 12 2.3.1.1 ns-2 “Network Simulator Versión 2 […] es, simplemente, una herramienta de simulación basada en eventos que ha demostrado ser útil para estudiar la naturaleza dinámica de las redes de comunicación. La simulación de funciones y protocolos de redes cableadas e inalámbricas (por ejemplo, algoritmos de enrutamiento, TCP, UDP) se puede realizar utilizando NS2. En general, NS2 proporciona a los usuarios una forma de especificar dichos protocolos de red y simular sus comportamientos correspondientes.” [5] ns-2 ha sido desarrollado en C++ y proporciona una interfaz de simulación a través de una versión Orientada a Objetos de Tcl (Tool Command Language, lenguaje de herramientas de comando para el desarrollo rápido de prototipos) llamada OTcl. Para utilizarlo, el usuario deberá definir una topología de red por medio de scripts OTcl para que, posteriormente, ns-2 realice las simulaciones pertinentes sobre dicha topología teniendo en cuenta los parámetros definidos. La última versión del software fue publicada en 2011 y ha sido reemplazado mayoritariamente por su sucesor, ns-3. 2.3.1.2 ns-3 “Durante muchos años, […] ns–2 fue el estándar de facto para la investigación académica en protocolos de redes y métodos de comunicación. Innumerables artículos de investigación fueron escritos informando resultados obtenidos utilizando ns–2, y cientos de nuevos modelos fueron escritos y contribuidos a la base de código de ns–2. A pesar de esta popularidad, […], los autores y otros investigadores emprendieron un proyecto en 2005 para diseñar un nuevo simulador de redes que reemplazara a ns–2 en la investigación en redes.” [6] ns-3 fue desarrollado desde cero, utilizando el lenguaje C++ y basándose en el paquete yans (Yet Another Network Simulator). Haciendo uso de ns-3 se pueden obtener modelos de simulación de alto desempeño, permitiendo así ser utilizado como emulador. ns-3 soporta simulaciones de multitud de tipos de redes, tanto IP, LTE, Wi-fi o WiMAX, así como varios protocolos de enrutamiento. Es gratuito, de código abierto, consta de una licencia GNU GPLv2 y es mantenido por una amplia comunidad. Su última versión (ns-3.39) publicada el 5 de julio de 2023 proporciona actualizaciones del modelo Wi-Fi para los estándares de Wi-Fi 6 y 7, soporte de escaneo de huérfanos IEE 802.15.4 LR-WPAN y extensiones de modelos de pérdidas de propagación para construir pérdidas de penetración, así como otras mejoras a sus características y arreglos de errores. 2.3.2 GNS3: Entorno virtual de simulación de redes en tiempo real En palabras de Jeremy Grossmann, cofundador de GNS3, “Para entender, diseñar y manejar las redes complejas actuales, los profesionales de las redes deben no solo dominar la teoría, si no también la práctica y validar conceptos en estos entornos constantemente cambiantes. Es aquí donde entra en juego GNS3: provee al usuario una inmensa flexibilidad para construir sus propios laboratorios de redes, permitiéndoles experimentar con nuevos elementos de red, capturar paquetes para diseccionar protocolos y verificar configuraciones para su posterior despliegue en equipos reales.” [7] 13 13 Herramienta para el despliegue de laboratorios virtuales mediante Docker Ilustración 5: Proyecto de ejemplo en GNS3 GNS3 es un simulador gráfico de red que permite al usuario diseñar y configurar redes complejas y ejecutar simulaciones con las mismas, permitiendo interconectar dispositivos reales y virtuales. Dicha hazaña es posible gracias a las aplicaciones asociadas a este software, tales como Dynamips (emulador de IOS), Dynagen (front-end de Dynamips), Qemu, VirtualBox (permiten emplear máquinas virtuales como un firewall), VPCS (emulador de PC con funcionalidades básicas de networking) o IOU (compilaciones del IOS de Cisco específicas para correr en sistemas UNIX). GNS3 es empleado por miles de ingenieros de redes alrededor del mundo para emular, configurar, hacer tests y corregir errores de redes físicas y virtuales. GNS3 permite al usuario ejecutar una pequeña topología compuesta por unos pocos equipos en su ordenador portátil o una topología compuesta por un gran número de equipos en múltiples servidores o, incluso, en la nube. Se trata de una herramienta gratuita y de código abierto disponible para cualquier usuario que desee instalarla. En la siguiente tabla podemos apreciar las principales diferencias entre el software de simulación de redes mencionado hasta este punto para comprender los casos de uso que abarca cada uno: ns-2, ns-3 GNS3 Tipo de simulador Discreto, sin entorno gráfico Continuo, con entorno gráfico Objetivo Monitorización y diseño de redes Diseño y configuración de redes Funcionalidad Basado en scripts Ejecuta el firmware real Lenguaje C++ y Python Python Tabla 3: Diferencias entre Network simulator y GNS3 2.3.3 Ventajas e Inconvenientes Al igual que con respecto a las demás tecnologías, los simuladores de red tienen una serie de ventajas que los convierten en herramientas esenciales para la investigación, desarrollo y capacitación en el campo de las redes de computadores. Algunas de estas son: - Experimentación segura: Gracias que los simuladores de red no ejecutan realmente el software sobre equipos reales, se pueden probar configuraciones y escenarios de red sin necesidad de afectar a una red real, lo cual ayuda a identificar errores graves o problemas de seguridad. - Coste reducido: Debido a que no se requieren de dispositivos reales, la experimentación con simuladores es mucho más económica a la hora de crear y mantener una infraestructura de red que las pruebas en equipos reales. Tecnologías y Trabajos Relacionados 14 14 - Control total: Ya que se trata de simulaciones, los parámetros de las pruebas realizadas pueden ser totalmente controlados, facilitando la creación de escenarios específicos. - Reproducibilidad: Los experimentos en simuladores son altamente reproducibles, facilitando así el contraste de resultados y la realización de pruebas rigurosas. El uso de simuladores presenta, por el contrario, una serie de inconvenientes derivados de su naturaleza y que también hay que tener en cuenta antes de decantarse por su uso. A continuación, se muestran algunos de los principales: - Simplificaciones de la realidad: A pesar de que los simuladores pretenden replicar entornos reales, estas simulaciones no son perfectas y no pueden tener en cuenta todos los parámetros existentes en la vida real, lo cual puede desembocar en resultados que no se correspondan totalmente con lo que pueda encontrarse en la práctica. - Recursos necesarios: Existen múltiples simuladores, tales como ns-2 o ns-3, que en algunos casos pueden resultar intensivos en recursos para las máquinas que ejecuten las simulaciones, por lo que, en algunos casos, se puede requerir de hardware lo suficientemente potente para poder realizar simulaciones complejas de forma eficiente. - Curva de aprendizaje: No es trivial manejar simuladores como los que se han expuesto en el presente apartado, ya que su funcionamiento interno es complejo y se requiere de grandes cantidades de tiempo y esfuerzo para dominar su manejo. - Limitaciones de modelos matemáticos: Los simuladores utilizan modelos matemáticos y del comportamiento de los diversos protocolos que pueden no estar completamente adaptados a la realidad, por lo que las simulaciones pueden no resultar completamente precisas en algunos casos. 2.4 Conclusiones Tras haber estudiado las diversas tecnologías disponibles en el mercado, su funcionamiento y los casos de uso para las cuales están diseñadas se ha optado por emplear tecnología de contenedores, en concreto, se ha decidido utilizar Docker y docker-compose como herramientas subyacentes para mayor compatibilidad, y se ha decidido hacer un parser orientado al despliegue de escenarios según los requisitos expuestos en el capítulo 1, el cual consta de una sintaxis de configuración sencilla y que ofrece funcionalidades adicionales, tales como la gestión de validación y el parseo del fichero de configuración, asistiendo posteriormente de forma interactiva al despliegue y monitorización. En el capítulo siguiente, se estudiará en mayor detenimiento las tecnologías a emplear en el desarrollo del software. 15 3 DISEÑO E IMPLEMENTACIÓN n el presente capítulo, se expone el funcionamiento interno de cada uno de los elementos que componen la aplicación, así como las herramientas empleadas para lograr el comportamiento final que se espera de la misma. Se explicará en detalle todo lo correspondiente al funcionamiento de dichos elementos, así como la forma de interactuar con el sistema desde el punto de vista del usuario para lograr un correcto funcionamiento de este. Dockerlab es una herramienta que permite el despliegue de laboratorios virtuales haciendo uso del motor Docker. Su funcionamiento queda representado en la Ilustración 6, tomando como entrada un fichero de configuración en formato YAML con una sintaxis reducida que describe el laboratorio virtual a desplegar. A continuación, Dockerlab interpretará la configuración aportada y asistirá al usuario tanto en el despliegue como en la monitorización del escenario, además de proporcionar un archivo docker-compose.yml que permitirá la reproducibilidad del escenario en cualquier máquina. Ilustración 6: Funcionamiento básico de dockerlab Se adjunta, además, el código del script principal de la aplicación, “dockerlab.py”, a modo de anexo al final del presente documento. 3.1 Tecnologías empleadas Para el diseño y realización de la aplicación, se ha trabajado con diferentes tecnologías, las cuales serán expuestas en los siguientes subapartados para así tener un correcto entendimiento de la capacidad de estas y del motivo por el cual se han decidido emplear. 3.1.1 Docker: Despliegue de aplicaciones en contenedores Docker es una plataforma de virtualización ligera y flexible que permite crear, distribuir y ejecutar aplicaciones de manera eficiente y reproducible. Mediante Docker, se pueden encapsular aplicaciones y todas sus dependencias en contenedores. En un contenedor Docker, se crea un entorno ligero y aislado que opera sobre el sistema operativo anfitrión, utilizando facilidades proporcionadas por el kernel Linux como los espacios de nombres (namespaces) y los grupos de control (cgroups). Esto permite que cada contenedor tenga su propio sistema de archivos, su propia configuración de red y sus propios recursos, al tiempo que comparte el kernel del sistema operativo anfitrión. La principal diferencia entre un contenedor Docker y una máquina virtual, por tanto, es que el primero se basa en las funcionalidades del kernel y utiliza el aislamiento de recursos y espacios de nombres separados para aislar la vista de una aplicación del sistema operativo anfitrión, mientras que una máquina virtual ejecuta un sistema operativo completo sobre un hipervisor que puede operar en distintos niveles. E Diseño e Implementación 16 16 Ilustración 7: Diferencias entre contenedores y Máquinas virtuales. El funcionamiento de Docker se basa en el uso de imágenes, a partir de las cuáles se crean los contenedores. Una imagen de Docker es un paquete autónomo y autocontenido que incluye todo lo necesario para ejecutar una aplicación, desde el código fuente hasta las bibliotecas y las dependencias. Estas imágenes se crean a partir de un archivo llamado Dockerfile, que describe paso a paso los comandos necesarios para construir la imagen. Tras construir una imagen Docker, ésta se puede ejecutar en uno o varios contenedores a la vez. El contenedor ofrecerá un entorno aislado y consistente para la ejecución de aplicaciones, garantizando que tanto las dependencias como las configuraciones necesarias estén presentes y sean coherentes en cada ejecución. Cabe destacar que las imágenes Docker pueden llegar a ser altamente portables, ya que no son completamente independientes de la infraestructura subyacente, pero se pueden diseñar Dockerfiles multiplataforma, los cuales pueden ser ejecutadas en cualquier entorno que tenga Docker instalado, ya sea en una máquina local, en un servidor de desarrollo o en la nube. Esto permite la creación de entornos de desarrollos consistentes y facilita la implementación de aplicaciones en diferentes entornos sin que las diferencias de configuración supongan ningún problema. Docker también permite la creación de redes y volúmenes para facilitar tanto la comunicación entre contenedores como la persistencia de datos, lo cual es especialmente útil en entornos de aplicaciones distribuidas, donde diferentes componentes de una aplicación necesitan comunicarse entre sí. En lo que respecta a la ejecución, Docker proporciona un motor que se ocupa de la gestión de los contenedores y las imágenes, el cual se comunica con el kernel del sistema operativo para crear y gestionar los contenedores de manera eficiente. A su vez, Docker proporciona una interfaz de línea de comandos (CLI) y una API que permite a sus usuarios interactuar con los contenedores e imágenes, lo cual facilita la automatización e integración con otras herramientas y sistemas. 3.1.2 Docker Compose: Definición de aplicaciones multicontenedor Para la definición y el manejo de aplicaciones compuestas por varios contenedores Docker interconectados, existen varias alternativas de herramientas disponibles. Se utilizará Docker Compose, cuyo objetivo principal es simplificar la administración de aplicaciones complejas que consten de múltiples servicios (tales como bases de datos, servidores web o microservicios), al permitir que todos estos componentes funcionen juntos de manera coordinada entre ellos. La principal funcionalidad ofrecida por Docker Compose está basada en la descripción de la configuración de la aplicación en un archivo YAML llamado “docker-compose.yml”, en el cual se definen los servicios, propiedades, dependencias y configuraciones de la aplicación. Cada servicio se configurará a partir de una imagen de Docker que, o bien puede existir anteriormente, o se puede crear mediante un Dockerfile personalizado. Los siguientes pasos tienen lugar a lo largo del proceso de uso de Docker Compose: 17 17 Herramienta para el despliegue de laboratorios virtuales mediante Docker 1. Creación y definición de los archivos de configuración: Las directrices de construcción y ejecución de los contenedores que conforman la aplicación serán encapsuladas en el archivo “dockercompose.yml”. Esta definición incluirá todos los servicios, imágenes, volúmenes, redes y configuraciones necesarias para la aplicación. 2. Ejecución de servicios: Haciendo uso del comando “docker-compose up”, el archivo de configuración será interpretado por la herramienta, la cual desplegará los contenedores según las indicaciones provistas en el archivo previamente mencionado. Los servicios serán arrancados en orden secuencial, mientras que las redes serán configuradas automáticamente para que los distintos contenedores puedan comunicarse entre sí. 3. Supervisión y gestión: Con los servicios en ejecución, se pueden emplear múltiples comandos para estudiar el estado de estos. Empleando “docker-compose ps”, se puede verificar el estado de cada uno de los contenedores, al igual que empleando “docker-compose logs” se pueden consultar los registros de cada servicio. Por último, empleando “docker-compose down” se detendrán y eliminarán los contenedores que se hayan creado anteriormente (en caso de que se desee almacenar información de estos de forma persistente, se deberán utilizar volúmenes). 4. Actualización de servicios: En lo referente a actualizaciones y ajustes de configuración, en caso de desear modificar la imágenes o configuraciones empleadas, se deberá modificar el archivo “dockercompose.yml” y, posteriormente, volver a ejecutar la orden “docker-compose up”. 3.1.3 Tshark: Analizador de protocolos de red En lo que respecta a herramientas de análisis de protocolos, algunas de las más populares son Wireshark o Ethereal, ambas utilizadas mediante entorno gráfico y que permiten analizar capturas de tráfico de red, sin embargo, debido a la naturaleza del presente proyecto, se ha decidido hacer uso de Tshark, una herramienta para el análisis de protocolos que se utiliza, a diferencia de las mencionadas anteriormente, por línea de comandos. Se trata de la versión CLI (command line interface) de Wireshark, proporcionando funcionalidades muy similares a esta. Debido a que pertenece al conjunto de herramientas Wireshark, Tshark se presenta como una alternativa versátil y eficiente para capturar, decodificar y analizar paquetes de red en tiempo real, almacenar la información recibida en ficheros pcap o pcapng o analizar el contenido de dichos ficheros, siendo una herramienta esencial para profesionales de seguridad de red, administradores de sistema o analistas de tráfico. Tshark permite la captura de paquetes de red en vivo desde una o varias interfaces de red, tales como adaptadores Ethernet o interfaces inalámbricas (Wifi, Bluetooth, etcétera). Dichas capturas podrán abarcar diversos tipos de tráfico, permitiendo realizar un profundo análisis sobre el comportamiento de la red. Gracias a su capacidad para decodificar los paquetes recibidos, Tshark ofrece los detalles y contenidos de cada paquete traducidos a un formato legible y estructurado, permitiendo aplicar filtros para seleccionar y visualizar los paquetes que se deseen en función de ciertos criterios específicos. Dichos filtros pueden estar basados en direcciones de origen o destino, puertos, protocolos empleados u otros atributos, agilizando así el proceso de análisis y permitiendo ignorar los paquetes que no se consideren como relevantes para un escenario particular. Por último, Tshark permite plasmar los resultados obtenidos de la captura y el análisis en diversos formatos, tales como texto plano, CSV, JSON o XML, lo cual facilita su integración con otras herramientas tales como la que se ha recogido en el presente documento. 3.2 Diseño y Arquitectura En lo que respecta al software desarrollado, en este apartado se estudiará la utilización, así como la arquitectura interna que permiten obtener el resultado deseado. Se estudiarán, además, las diversas opciones disponibles al usuario mediante las cuales se puede obtener un comportamiento concreto. Diseño e Implementación 18 18 3.2.1 Visión general del sistema La contribución principal de este trabajo ha sido llamada ‘dockerlab’, y tiene como objetivo facilitar el despliegue, ejecución y monitorización de laboratorios virtuales simplificando en gran medida la complejidad de los ficheros docker-compose. Para ello, puede llamarse por línea de comandos indicando un fichero de configuración (en adelante, config.yml), y unas banderas de ejecución, iniciando un proceso interactivo. La Ilustración 8 representa el diagrama de flujo que sigue la aplicación. En el siguiente diagrama se expone de forma resumida el funcionamiento de la aplicación dockerlab. Ilustración 8: Funcionamiento resumido de dockerlab Dockerlab puede ser ejecutado con cuatro banderas distintas (build, execute, monitor y usage) las cuales definirán el comportamiento del programa, ya sea si se desea generar un archivo docker-compose.yml a partir del archivo de configuración proporcionado (config.yml), si se desea ejecutar Docker Compose a partir de dicho archivo, o si se desea algún tipo de monitorización (de red o de recursos) a lo largo de dicha ejecución. 19 19 Herramienta para el despliegue de laboratorios virtuales mediante Docker 3.2.2 Elementos de entrada Para utilizar la aplicación, primero se debe comprender el funcionamiento del sistema de ficheros que la componen. A continuación de muestra una imagen con el contenido de la carpeta en la que está situado el script “dockerlab.py”. Cada uno de los archivos que componen la aplicación tiene un propósito, algunos no han de ser modificados debido a que definen el modo de funcionamiento del software, mientras que otros sirven de archivos de configuración del escenario de ejemplo. Estos últimos deberán ser modificados o sustituidos por los archivos de configuración del escenario que deseemos ejecutar. Ilustración 9: Clasificación de los archivos de la aplicación En lo que respecta a los archivos base necesarios para el funcionamiento del software, tienen el siguiente propósito: - Carpeta “Img”: contiene el icono (monitor_icon.png) de las ventanas que se mostrará en caso de desear ejecutar la aplicación en modo “monitor”. - dockerlab.py: script troncal de la aplicación. Más adelante se estudiará cómo manejarlo, ya que es el archivo que ha de ser interpretado por Python para permitir la ejecución del software. - README.md: archivo “léeme” escrito en Markdown. Contiene toda la información necesaria para comprender el funcionamiento de la aplicación, así como su manejo. - requirements.txt: archivo que contiene una lista con las librerías de Python de las que depende el software. Más adelante se estudiará Por otro lado, los archivos de configuración del escenario de ejemplo son los siguientes: - config.yml: archivo de configuración en formato YAML, el cual contendrá las especificaciones del laboratorio virtual a desplegar. Se estudiará su contenido más adelante, sin embargo, cabe destacar que la existencia de este archivo es obligatoria para el correcto funcionamiento del sistema. - Carpeta “custom_build”: en caso de querer basar un nodo en un contenedor definido por el usuario, se incluirá una carpeta en el directorio actual que contenga los archivos necesarios para su despliegue y se indicará en esta directiva su nombre. • Dockerfile: para construir una imagen personalizada para el contenedor, se deberá incluir un archivo “Dockerfile” en el cual se especifique la configuración de dicha imagen. • Archivos conf: archivo(s) de configuración necesarios para la imagen “custom_build”. - script.sh: se trata de un script de ejemplo. Contiene las órdenes que ejecutarán los diversos contenedores una vez comience a funcionar la aplicación. Diseño e Implementación 26 26 Ilustración 17: Ejemplo de ejecución de docker-compose mediante dockerlab Una vez comienza la ejecución de los contenedores, esta podrá ser detenida al pulsar la tecla “s”, mandando así una señal simultánea a cada uno de dichos contenedores para terminar su ejecución. Ilustración 18: Ejemplo de salida al detener la ejecución de los contenedores Ilustración 19: Indicación de que se ha detenido la ejecución de los contenedores correctamente En caso de haber ejecutado dockerlab con la bandera “-u”, cuando se pulse “r” por primera vez, saltará una ventana como la que se muestra a continuación, la cual podrá ser refrescada al pulsar el botón de “Refrescar” situado en la esquina inferior izquierda de la misma. 27 27 Herramienta para el despliegue de laboratorios virtuales mediante Docker Ilustración 20: Ventana de "utilización" antes de recibir ningún dato El programa refrescará automáticamente los datos a mostrar en la ventana anterior periódicamente, indicando al usuario que hay nuevos datos disponibles mediante el mensaje “Valores de monitorización refrescados”. Ilustración 21: Ventana de "utilización" una vez hay datos que mostrar Por último, cabe destacar que el uso de la bandera “-m” generará un archivo “output.pcap” en el que se recogerá todo el tráfico de red generado entre los contenedores. Para poder observar su contenido, se podrán utilizar herramientas como Wireshark, las cuales mostrarán la información contenida en cada uno de los paquetes intercambiados. Ilustración 22: Fragmento de output.pcap Diseño e Implementación 28 28 29 4 CASOS DE USO okerlab es una herramienta que puede ser adaptada para diversos escenarios. En el siguiente apartado se estudiarán distintos casos de uso, mostrándose el resultado de la ejecución de la herramienta en distintos escenarios y analizando la información obtenida de la ejecución de cada uno de ellos. 4.1 Red MQTT El siguiente caso de estudio tratará una red MQTT, en la que cinco clientes envían mensajes en un tema determinado a un contenedor que cumplirá la función de bróker. Se empleará una imagen personalizada de Eclipse Mosquitto para el bróker y la imagen general para los clientes. Ilustración 23: Escenario 1. Red Industrial MQTT El presente escenario tiene su inspiración en replicar las necesidades del artículo “Smart home anomaly-based IDS: Architecture proposal and case study” [8], en el que se partía de una serie de ficheros csv que indicaban el contenido y el timestamp de los paquetes que tenían lugar en una smarthome, sin embargo, la complejidad no estaba ahí, si no que radicaba en la recreación del tráfico MQTT. D Casos de Uso 30 30 Ilustración 24: Árbol de ficheros empleados en el caso 1 La imagen personalizada que se ha empleado para el bróker viene definida en el archivo Dockerfile, dentro de la carpeta “custom_broker”. Importa la imagen oficial de Eclipse Mosquitto y le añade el archivo de configuración necesario, tal y como se ve a continuación: FROM eclipse-mosquitto:latest ADD mosquitto.conf /mosquitto/config/mosquitto.conf Código 6: Dockerfile del broker MQTT del caso de uso 1 El archivo de configuración empleado sirve para permitir conexiones anónimas (esto es, no se requiere de autenticación de los clientes) y pone a escuchar al bróker en el puerto 1883. Su contenido es el siguiente: persistence true persistence_location /mosquitto/data/ log_dest file /mosquitto/log/mosquitto.log listener 1883 0.0.0.0 allow_anonymous true Código 7: Contenido de mosquitto.conf del caso de uso 1 En lo que respecta al script que van a ejecutar los clientes para enviar los mensajes periódicamente, tiene el contenido expuesto a continuación: #!/bin/bash echo REPLICA_ID = $REPLICA_ID for i in `seq 1 60` do mosquitto_pub -h mqtt_broker -t /test/message -m "Mensaje número ${i}" && \ echo "Cliente $REPLICA_ID envió el mensaje número $i" || \ echo "Cliente $REPLICA_ID no pudo envíar el mensaje número $i" sleep $((REPLICA_ID + 1)) done Código 8: Script a ejecutar por los clientes MQTT del caso de uso 1 31 31 Herramienta para el despliegue de laboratorios virtuales mediante Docker En este ejemplo en concreto, además, se puede apreciar cómo el uso de dockerlab ha ayudado a reducir considerablemente la cantidad configuración requerida por parte del usuario, transformando un archivo de configuración de la aplicación de 12 líneas en un docker-compose.yml de más de 70. Ilustración 25: Comparación entre el archivo config.yml y docker-compose.yml generado en el escenario 1 Casos de Uso 32 32 En lo que respecta al tráfico generado, se pueden apreciar fácilmente los mensajes MQTT enviados en ambos sentidos gracias a la captura de tráfico realizada por dockerlab. Ilustración 26: Captura del tráfico generado en el escenario 1 A continuación, se muestran capturas tanto de la utilización de recursos de los distintos contenedores durante su ejecución como de su salida estándar visto desde la consola de dockerlab. Ilustración 27: Utilización de recursos de cada uno de los contenedores del escenario 1 33 33 Herramienta para el despliegue de laboratorios virtuales mediante Docker Ilustración 28: Salida estándar de la ejecución del escenario 1 4.2 Generación de dataset En el siguiente apartado se tratará el caso de una generación de dataset simple, en el que un equipo con Kali Linux realiza un ataque de ICMP flooding sobre una máquina mientras que un sistema de detección de intrusos (IDS) realiza un log de dicho ataque. Ilustración 29: Escenario 2. Generación de dataset En el presente ejemplo, se ha generado una imagen personalizada de Kali Linux que cumpla el perfil de Hacker (atacante) haciendo uso de la herramienta hping3, la cual permitirá realizar el ataque deseado sobre la máquina objetivo. El IDS detectará el ataque y generará un log con información de este una vez haya terminado. Casos de Uso 34 34 Ilustración 30: Árbol de ficheros empleado en el escenario 2 Para generar la imagen personalizada de Kali Linux, se ha empleado un Dockerfile con el contenido expuesto a continuación, el cual adapta la imagen oficial de Kali Linux instalándole la herramienta a utilizar en este ejemplo. FROM kalilinux/kali-rolling RUN apt-get update RUN apt-get -y upgrade RUN apt-get -y install hping3 Código 9: Dockerfile de la imagen de Kali Linux personalizada empleado en el caso de uso 2 Del mismo modo, para hacer uso de dicha herramienta, el script “script_kali.sh” empleará la siguiente orden: #!/bin/sh timeout 6 hping3 --rand-source victim -1 --faster Código 10: Script ejecutado por el contenedor "Hacker" del caso de uso 2 El script que define el comportamiento de la víctima, “script_victim.sh”, hará que esta esté en espera mientras recibe el ataque #!/bin/bash sleep infinite Código 11: Script ejecutado por el contenedor "Víctima" del caso de uso 2 Por último, el script que ejecuta el IDS, “script_snort.sh”, generará el log con información a cerca del ataque recibido. Su contenido es el siguiente: 35 35 Herramienta para el despliegue de laboratorios virtuales mediante Docker #!/bin/bash sleep 10 cd /workspace echo "Running snort" /home/snorty/snort3/bin/snort -c \ /home/snorty/snort3/etc/snort/snort.lua -q --talos \ -r output.pcap > snort.log echo "Log generated at $PWD/snort.log" Código 12: Script ejecutado por el contenedor "IDS" en el caso de uso 2 Se muestra, a continuación, una captura en la que se puede apreciar el consumo de recursos de cada uno de los dispositivos individuales. Cabe destacar que el contenedor “Hacker” hacer el mayor uso de recursos debido a que utiliza toda su capacidad para inundar la red de mensajes ICMP e impedir la comunicación entre ambos equipos. Ilustración 31: Utilización de recursos de cada uno de los contenedores del escenario 2 Una vez realizado el ataque, el contenedor “Hacker” detendrá su ejecución automáticamente y el mayor consumo de recursos pasará a ser por parte del IDS. Ilustración 32: Utilización de recursos del IDS del escenario 2 Referencias 42 42 43 7 ANEXO: CÓDIGO DEL SCRIPT DOCKERLAB """ El presente repositorio muestra una herramienta para automatizar el despliegue de laboratorios virtuales mediante Docker. Se trata de un Trabajo de Fin de Grado (TFG) desarrollado en el curso 2022/23 para el Grado en Ingeniería de las Tecnologías de Telecomunicación de la Universidad de Sevilla. """ from yaml import load, dump try: from yaml import CLoader as Loader, CDumper as Dumper except ImportError: from yaml import Loader, Dumper from colorama import Fore, Back from typing import TypedDict, Optional from ipaddress import ip_address, ip_network, IPv4Address, IPv4Network import copy import argparse import subprocess import shlex from pynput import keyboard from threading import Thread import re import PySimpleGUI as sg import time import json import psutil ############################### # DEFINICIÓN DE TYPED DICTS # ############################### class Arguments(TypedDict): build: bool execute: bool monitor: bool usage: bool class Compose(TypedDict): version: str # name:str services: dict networks: dict class Service(TypedDict): build: Optional[str] image: Optional[str] entrypoint: Optional[str] depends_on: Optional[list] deploy: Optional[dict] environment: Optional[list] volumes: list networks: list class Docker_Network_List(TypedDict): network_id_short: str name: str driver: str scope: str Anexo: Código del script Dockerlab 44 44 class Docker_Network(TypedDict): Name: str Id: str Created: str Scope: str Driver: str EnableIPv6: bool IPAM: dict Internal: bool Attachable: bool Ingress: bool ConfigFrom: dict ConfigOnly: bool Containers: dict Options: dict Labels: dict ############################### # DEFINICIÓN DE VAR GLOBALES # ############################### tshark_process: subprocess.Popen = None continue_monitor: bool = True compose_name: str = "" ############################### # DEFINICIÓN DE EXCEPCIONES # ############################### class ReaderException(Exception): def __init__(self, *args: object) -> None: super().__init__(*args) class ParseNodeException(Exception): def __init__(self, *args: object) -> None: super().__init__(*args) class IpAddrException(Exception): def __init__(self, *args: object) -> None: super().__init__(*args) 45 45 Herramienta para el despliegue de laboratorios virtuales mediante Docker ############################### # DEFINICIÓN DE FUNCIONES # ############################### def reader(config: dict, compose: Compose, *args, **conf) -> tuple: """ Función "reader" que tomará como parámetro un diccionario que contenga la definición del laboratorio de "config.yml" y el diccionario mediante el cual se generará el docker-compose. Comprobará que sólo hay una clave definida en dicho diccionario y dividirá su contenido en dos diccionarios, "network" y "nodes". Guardará el nombre de la clave en el diccionario "compose". """ network: IPv4Network = ip_network("10.0.0.0/8") nodes: dict = {} # Comprobamos que sólo hay una clave en "config" if len(config) != 1: raise ( ReaderException( f"'config' contiene {len(config)} elementos. Sólo se admite 1." ) ) else: if conf["debug"]: print( f"{Fore.BLUE}Contenido del laboratorio: ”+ f"{config[list(config)[0]]}{Fore.RESET}" ) # Comprobamos el contenido de "config", de momento, # sólo se admiten las cláusulas "network" y "nodes" lab = config[list(config)[0]] if len(lab) > 2: raise ( ReaderException( f"Se han definido más parámetros de los admitidos: {list(lab)}" ) ) else: global compose_name compose_name = list(config)[0] try: network = ip_network(lab["network"]) print(f"{Fore.GREEN}\tRed [{network}] añadida{Fore.RESET}") except KeyError: if "debug" in conf and conf["debug"]: print( f"{Fore.BLUE}'network' no está definido. "+ f"Proporcionando el valor por defecto.{Fore.RESET}" ) try: nodes = lab["nodes"] for node in nodes: print(f"{Fore.GREEN}\tNodo [{node}] añadido{Fore.RESET}") except KeyError: if "debug" in conf and conf["debug"]: print( f"{Fore.BLUE}'nodes' no está definido. "+ f"Proporcionando el valor por defecto.{Fore.RESET}" ) return (network, nodes) Anexo: Código del script Dockerlab 46 46 def list_networks(is_debugging=False, *args, **conf) -> list[Docker_Network_List]: """ Función que devuelve una lista con un resumen de cada una de las redes actualmente definidas por docker. """ network_list: list[Docker_Network_List] = [] header: bool = True with subprocess.Popen( shlex.split("docker network list"), stdout=subprocess.PIPE, universal_newlines=True, ) as p: for line in p.stdout: if not header: network: Docker_Network_List = {} split_line: list[str] = re.split(r"\s{2,}", line) network["network_id_short"] = split_line[0] network["name"] = split_line[1] network["driver"] = split_line[2] network["scope"] = split_line[3] network_list.append(network) header = False if is_debugging: print( f"{Fore.BLUE}Código de retorno (Network listing): "+ f"{p.wait()}{Fore.RESET}" ) return network_list def get_network_data( docker_network: Docker_Network_List, is_debugging: bool = False, *args, **conf ) -> Docker_Network: """ Función que, proporcionada la información resumida de una red, devuelve la información completa de la misma. """ result: Docker_Network = None if is_debugging: print( f"{Fore.BLUE}Ejecutando 'docker network inspect"+ f" {docker_network['name']}'...{Fore.RESET}" ) with subprocess.Popen( shlex.split(f"docker network inspect {docker_network['name']}"), stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, ) as p: stdout, stderr = p.communicate() # Verifica si hubo errores en la salida estándar if p.returncode == 0: # Intenta parsear la salida como JSON try: result = json.loads(stdout) except json.JSONDecodeError as e: print(f"{Fore.RED}Error al parsear la salida JSON:{e}{Fore.RESET}") else: print(f"{Fore.RED}Error al ejecutar el comando Docker: "+ f"{stderr}{Fore.RESET}") if is_debugging: print( f"{Fore.BLUE}Código de retorno (Network specification): "+ f"{p.wait()}{Fore.RESET}" ) return result[0] 47 47 Herramienta para el despliegue de laboratorios virtuales mediante Docker def create_network(network: IPv4Network, name: str, *args, **conf) -> bool: """ Función que realiza los comandos necesarios para la creación de una red de docker en función de la red y el nombre proporcionados """ network_created: bool = True with subprocess.Popen( shlex.split( "docker network create --driver=bridge " + "--opt com.docker.network.bridge.name=br-dockerlab " + "--opt com.docker.network.bridge.enable_icc=true " + "--opt com.docker.network.bridge.enable_ip_masquerade=true " + "--opt com.docker.network.bridge.host_binding_ipv4=0.0.0.0 " + f"--subnet {network} {name}" ), stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, ) as p: for err in p.stderr: network_created = False if "network with name" not in err and "already exists" not in err: print(f"{Fore.RED}{err}{Fore.RESET}", end="") if "debug" in conf and conf["debug"]: print(f"Código de retorno (Network creation): {p.wait()}") return network_created def generate_network(network: IPv4Network, compose: Compose, *args, **conf) -> None: """ Función "generate_network", que genera la red que utilizaremos en nuestro compose. """ is_debugging = False name = f"{compose_name}_network" compose["networks"] = {f"{name}": {"external": True}} # "name":f"{name}", if "debug" in conf and conf["debug"]: print(f"{Fore.BLUE}compose['networks']={compose['networks']}{Fore.RESET}") if not create_network(network, name): # Red creada anteriormente o red con la misma IP if "debug" in conf and conf["debug"]: print(f"{Fore.BLUE}network, name = {network}, {name}{Fore.RESET}") is_debugging = True for i in list_networks(is_debugging): if "debug" in conf and conf["debug"]: print(f"{Fore.BLUE}i (list_networks)={i}{Fore.RESET}") docker_network: Docker_Network = get_network_data(i, is_debugging) if "debug" in conf and conf["debug"]: print(f"{Fore.BLUE}docker_network={docker_network}{Fore.RESET}") if ( docker_network["Name"] not in {"none", "host", "bridge"} and len(docker_network["IPAM"]["Config"]) > 0 and "Subnet" in docker_network["IPAM"]["Config"][0] and ( ip_network(docker_network["IPAM"] ["Config"][0]["Subnet"]).subnet_of( network ) or ip_network( docker_network["IPAM"]["Config"][0]["Subnet"] ).supernet_of(network) ) ) or docker_network["Name"] == name: with subprocess.Popen( shlex.split(f"docker network remove {docker_network['Name']}"), stdout=subprocess.PIPE, stderr=subprocess.PIPE, Anexo: Código del script Dockerlab 48 48 universal_newlines=True, ) as p: for err in p.stderr: print(f"{Fore.RED}{err}{Fore.RESET}", end="") if "debug" in conf and conf["debug"]: print(f"Código de retorno (Network removal): {p.wait()}") create_network(network, name) if "debug" in conf and conf["debug"]: print(f"{Fore.BLUE}\tnetworks: {compose['networks']}{Fore.RESET}") def new_ip_addr( network: IPv4Network, ip_list: set[IPv4Address], n: int ) -> list[IPv4Address]: """ Función "new_ip_addr". Toma como parámetros una red IPv4, una lista de direcciones IP y un número de direcciones a obtener. Lanza una excepción en caso de que no hayan suficientes direcciones disponibles en la subred. """ result: list[IPv4Address] = [] if n >= 1: for addr in network: if ( not (addr in ip_list) # Si la IP no está en la lista and addr.packed[3:] != b"\x00" # Si la IP no acaba en '.0' and addr.packed[3:] != b"\x01" # Si la IP no acaba en '.1' (Host) and addr.packed[3:] != b"\xff" ): # Si la IP no acaba en '.255' result.append(addr) n -= 1 if n < 1: break if n > 1: raise ( IpAddrException( "no hay suficientes direcciones IP disponibles en la "+ f"subred {network}" ) ) return result 49 49 Herramienta para el despliegue de laboratorios virtuales mediante Docker def parse_node( nodes: dict, compose: Compose, network: IPv4Network, *args, **conf ) -> None: """ Función "parse_node" que, para cada nodo, parseará su contenido en el diccionario "compose" """ compose["services"] = {} ip_list = set() for node in nodes: case1, case2 = False, False replicas: int = 1 if "build" in nodes[node]: case1 = True if "image" in nodes[node]: case2 = True if case1 and case2: raise ParseNodeException( f"El nodo {node} contiene cláusulas 'build': "+ f"{nodes[node]['build']} e 'image':{nodes[node]['image']}" ) elif not (case1 or case2): raise ParseNodeException("El nodo no contiene cláusulas "+ "'build' ni 'image'") else: service: Service = {} service_replica: list[Service] = [] if case1: # Caso 1: contiene "build" service["build"] = nodes[node]["build"] elif case2: # Caso 2: contiene "image" service["image"] = nodes[node]["image"] service["volumes"] = ["./:/workspace"] if "needs" in nodes[node]: service["depends_on"] = nodes[node]["needs"] if "script" in nodes[node]: service["entrypoint"] = f'/bin/sh /workspace/{nodes[node]["script"]}' ###################################################################################### # Existen varias opciones posibles: # 1. El nodo está replicado y tiene una IP asignada -> ❌ ERROR # 2. El nodo está replicado y NO tiene una IP asignada: # 2.1. El nodo tiene una subred definida: # 2.1.1. La subred pertenece al rango del laboratorio -> # ✅ Se le asignan a los nodos IPs en dicho rango # 2.1.2. La subred NO pertenece al rango del laboratorio -> # ❌ ERROR # 2.2. El nodo no tiene una subred definida -> # ✅ Se le asignan a los nodos IPs en el rango del laboratorio # 3. El nodo NO está replicado y tiene una IP asignada: # 3.1. La IP pertenece al rango del laboratorio: # 3.1.1. La IP está repetida -> ❌ ERROR # 3.1.2. La IP NO está repetida -> ✅ Se le asigna dicha IP # 3.2. La IP NO pertenece al rango del laboratorio ->❌ ERROR # 4. El nodo NO está replicado y NO tiene una IP asignada -> # ✅ Se le asigna una IP en el rango del laboratorio # A la hora de asignar direcciones IP, SIEMPRE se verificará que # queden suficientes direcciones disponibles en el rango en cuestión, # en caso contrario, se lanzará una excepción. ###################################################################################### Anexo: Código del script Dockerlab 50 50 if "replicas" in nodes[node]: replicas = nodes[node][ "replicas" ] # Si tiene más de una réplica, se crean varios nodos similares for i in range(replicas): service_replica.append(copy.deepcopy(service)) else: replicas = 1 if "debug" in conf and conf["debug"]: print( f"{Fore.BLUE}\nEl nodo {node} tiene "+ f"{replicas} réplica(s){Fore.RESET}" ) if replicas > 1: if "ip" in nodes[node]: # [1.] raise ParseNodeException( f"No se puede replicar un nodo al que se le ha asignado"+ f" IP: {node}" ) else: # [2.] if "network" in nodes[node]: # [2.1.] node_network: IPv4Network = ip_network(nodes[node]["network"]) if node_network.subnet_of(network): # [2.1.1.] ips: list[IPv4Address] = new_ip_addr( node_network, ip_list, replicas ) for i in range(replicas): ip_list.add(ips[i]) service_replica[i]["networks"] = { list(compose["networks"])[0]: { "ipv4_address": f"{ips[i]}" } } service_replica[i]["environment"] = [f"REPLICA_ID={i}"] else: # [2.1.2.] raise ParseNodeException( f"La subred {node_network} no pertenece a la "+ f"red del laboratorio ({network})" ) else: # [2.2.] ips: list[IPv4Address] = new_ip_addr(network, ip_list, replicas) for i in range(replicas): ip_list.add(ips[i]) service_replica[i]["networks"] = { list(compose["networks"])[0]: { "ipv4_address": f"{ips[i]}" } } service_replica[i]["environment"] = [f"REPLICA_ID={i}"] elif "ip" in nodes[node]: # [3.] ip: IPv4Address = ip_address(nodes[node]["ip"]) if ip in ip_list: # [3.1.1.] raise ParseNodeException(f"La ip {ip} está repetida") elif not (ip in network): # [3.2.] raise ParseNodeException( f"La ip {ip} no está contenida en el rango {network}" ) else: # [3.1.2.] ip_list.add(ip) service["networks"] = { list(compose["networks"])[0]: {"ipv4_address": f"{ip}"} } else: # [4.] 51 51 Herramienta para el despliegue de laboratorios virtuales mediante Docker ip: IPv4Address = new_ip_addr(network, ip_list, 1)[0] ip_list.add(ip) service["networks"] = { list(compose["networks"])[0]: {"ipv4_address": f"{ip}"} } if replicas == 1: compose["services"][node] = service if "debug" in conf and conf["debug"]: print(f"{Fore.BLUE}\tservice "+ f"'{node}':\n\t\t{service}{Fore.RESET}") else: for i in range(replicas): compose["services"][f"{node}_{i}"] = service_replica[i] if "debug" in conf and conf["debug"]: print( f"{Fore.BLUE}\tservice "+ f"'{node}_{i}':\n\t\t{service_replica[i]}{Fore.RESET}" ) def read_output(proc) -> None: """ Función 'read_output', que imprime por pantalla la salida estándar de un subproceso y su código de retorno al finalizar. """ for line in proc.stdout: print(line, end="") # espera a que el subproceso termine y obtiene su código de retorno return_code = proc.wait() # imprime el código de retorno del subproceso print(f"Código de retorno (read output): {return_code}") def stop_compose() -> None: with subprocess.Popen( shlex.split("docker-compose stop"), stdout=subprocess.PIPE, universal_newlines=True, ) as p: t = Thread(target=read_output, args=(p,)) t.start()