scieee AI-readable full text Open interactive document viewer

Emulador de comunicaciones de sistemas de control industriales

Rosendo Alonso, Juan José

Abstract

Este Trabajo de Fin de Grado se centra en el desarrollo de un emulador de tráfico Modbus, una herramienta diseñada para simular la comunicación entre dispositivos industriales. El principal objetivo es proporcionar un entorno seguro y práctico que permita probar, depurar y analizar sistemas basados en el protocolo Modbus sin necesidad de interactuar con equipos reales, evitando así riesgos asociados a su manipulación. El proyecto abarca desde el diseño de una arquitectura basada en contenedores Docker hasta la implementación de una interfaz gráfica intuitiva. El sistema utiliza el patrón Modelo-Vista-Controlador (MVC) y está compuesto por tres partes principales: Core, que gestiona la emulación mediante Docker y Tcpdump; Backend, encargado de coordinar las acciones del sistema; y Frontend, que ofrece al usuario la posibilidad de configurar escenarios de red de manera interactiva. La solución desarrollada permite emular múltiples dispositivos Modbus TCP, configurados como maestros o esclavos, en una red virtual. Se destacan funcionalidades como la validación de configuraciones de red, la personalización del tráfico emulado y la captura de paquetes en formato PCAP para su análisis posterior. Además, el sistema facilita la creación, carga, guardado y ejecución de escenarios de red personalizados. Este emulador no solo tiene aplicaciones prácticas en la seguridad y análisis de redes industriales, sino que también puede ser utilizado en entornos educativos y en la generación de datasets para entrenar algoritmos de inteligencia artificial destinados a la detección de ciberataques.

Full text

Proyecto Fin de Carrera Ingeniería de Telecomunicación Formato de Publicación de la Escuela Técnica Superior de Ingeniería Autor: F. Javier Payán Somet Tutor: Juan José Murillo Fuentes Dep. Teoría de la Señal y Comunicaciones Escuela Técnica Superior de Ingeniería Universidad de Sevilla Sevilla, 2013 Trabajo Fin de Grado Grado en Ingeniería de las Tecnologías de Telecomunicación Emulador de comunicaciones de sistemas de control industriales Autor: Juan José Rosendo Alonso Tutor: Antonio Estepa Alonso Dpto. Ingeniería Telemática Escuela Técnica Superior de Ingeniería Universidad de Sevilla Sevilla, 2025 Trabajo Fin de Grado Grado en Ingeniería de las Tecnologías de Telecomunicación Emulador de comunicaciones de sistemas de control industriales Autor: Juan José Rosendo Alonso Tutor: Antonio Estepa Alonso Doctor Dpto. Ingeniería Telemática Escuela Técnica Superior de Ingeniería Universidad de Sevilla Sevilla, 2025 Trabajo Fin de Grado: Emulador de comunicaciones de sistemas de control industriales Autor: Juan José Rosendo Alonso Tutor: Antonio Estepa Alonso El tribunal nombrado para juzgar el trabajo arriba indicado, compuesto por los siguientes profesores: Presidente: Vocal/es: Secretario: acuerdan otorgarle la calificación de: El Secretario del Tribunal Fecha: Agradecimientos Alo largo de estos años de formación, he tenido la fortuna de contar con personas increíbles, tanto quienes se han sumado recientemente como quienes llevan tiempo a mi lado. Este éxito no es solo mío, sino también de ellos: A mis compañeros de clase. Gracias por la ayuda, la compañía y los buenos momentos. A mi familia. Gracias por el apoyo incondicional, el sacrificio y los ánimos. A mis profesores. Gracias por el conocimiento, la dedicación y la inspiración. A mi tutor. Gracias por la paciencia, el tiempo y la confianza. A mi pareja. Gracias por el cariño, la comprensión y el apoyo constante. Gracias de corazón a todos los que habéis participado en esta etapa. Sin vosotros, no habría sido posible. Juan José Rosendo Alonso Sevilla, 2024 I Resumen Este Trabajo de Fin de Grado se centra en el desarrollo de un emulador de tráfico Modbus, una herramienta diseñada para simular la comunicación entre dispositivos industriales. El principal objetivo es proporcionar un entorno seguro y práctico que permita probar, depurar y analizar sistemas basados en el protocolo Modbus sin necesidad de interactuar con equipos reales, evitando así riesgos asociados a su manipulación. El proyecto abarca desde el diseño de una arquitectura basada en contenedores Docker hasta la implementación de una interfaz gráfica intuitiva. El sistema utiliza el patrón Modelo-Vista-Controlador (MVC) y está compuesto por tres partes principales: Core, que gestiona la emulación mediante Docker y Tcpdump; Backend, encargado de coordinar las acciones del sistema; y Frontend, que ofrece al usuario la posibilidad de configurar escenarios de red de manera interactiva. La solución desarrollada permite emular múltiples dispositivos Modbus TCP, configurados como maestros o esclavos, en una red virtual. Se destacan funcionalidades como la validación de configuraciones de red, la personalización del tráfico emulado y la captura de paquetes en formato PCAP para su análisis posterior. Además, el sistema facilita la creación, carga, guardado y ejecución de escenarios de red personalizados. Este emulador no solo tiene aplicaciones prácticas en la seguridad y análisis de redes industriales, sino que también puede ser utilizado en entornos educativos y en la generación de datasets para entrenar algoritmos de inteligencia artificial destinados a la detección de ciberataques. III 1 Introducción 1.1 Motivación En el entorno de los sistemas industriales modernos, la ciberseguridad ha adquirido una importancia crítica debido al creciente número de amenazas a los que se enfrentan las infraestructuras de control. El protocolo Modbus, ampliamente utilizado en sistemas Industrial Control System (ICS), fue diseñado originalmente sin medidas de seguridad intrínsecas, lo que lo hace vulnerable a una variedad de ataques. Estos ataques comprometen los tres atributos más importantes de la información: integridad, disponibilidad y confidencialidad[1]. En particular, comprometer la información de estas comunicaciones pone en riesgo infraestructuras críticas como lo es la red eléctrica de España. La necesidad de contar con herramientas que faciliten la labor de los profesionales de la ciberseguridad es vital. Aquí es donde entra la emulación de tráfico realista, donde se puede evaluar la seguridad de una red ante ciberataques para posteriormente prepararse adecuadamente. Este proyecto busca llenar ese vacío, proporcionando un emulador que no solo facilite el análisis de una red, sino que también permita realizar pruebas prácticas y realistas en un entorno seguro y controlado. La motivación última de un proyecto de estas características es la de generar datasets que permitan entrenar algoritmos de inteligencia artificial para la detección de ciberataques en sistemas ICS. 1.2 Objetivos Siendo extensa el área de la ciberseguridad, se ha tratado de llegar a un compromiso donde el alcance y la facilidad de uso no se vean comprometidos. Por ello, se han establecido los siguientes objetivos: 1. Desarollar un software que emule de forma precisa la comunicación entre múltiples dispositivos Modbus en su implementación Transmission Control Protocol (TCP). 2. Desarrollar una interfaz de usuario que permita la configuración gráfica de los escenarios sin conocimiento de la tecnología subyacente. En el proyecto se han desarrollado más funcionalidades que las propuestas inicialmente, descritas en el capítulo de resultados. 1 2 Conocimientos previos Un proyecto de estas características tiene una alta carga de conocimientos previos tanto en el ámbito de la gestión de servicios como en el de la programación. En este capítulo se expondrán los conceptos básicos necesarios para entender el proyecto. 2.1 Modbus Modbus es un protocolo de comunicación originado en el año 1979 por Modicon (actualmente parte de Schneider Electric), diseñado inicialmente para la automatización industrial y la interconexión de dispositivos en sistemas de control distribuido. Su ámbito de aplicación reside en la gestión de dispositivos en fábricas y plantas industriales. A lo largo de su historia, se han propuesto diversas soluciones para mejorar la baja seguridad inherente al protocolo, como la implementación de capas de cifrado y autenticación. Sin embargo, debido a la filosofía "si funciona no lo toques" y a la necesidad de mantener la compatibilidad con equipos existentes, estas soluciones no están en funcionamiento. [2] Modbus tiene tres variantes principales: Modbus Remote Terminal Unit (RTU), Modbus TCP, y Modbus User Datagram Protocol (UDP), cada una adaptada a diferentes entornos de comunicación. Modbus RTU utiliza una comunicación serie, mientras que Modbus TCP y Modbus UDP operan sobre redes Internet Protocol (IP), permitiendo una integración más sencilla en redes modernas.[3] Modbus se basa en un paradigma de comunicación maestro-esclavo, donde un dispositivo maestro (generalmente un controlador) envía comandos a uno o varios dispositivos esclavos (sensores, actuadores, etc.), que responden a estas solicitudes de acuerdo con su función específica.[4] En este paradigma el maestro es el que inicia la comunicación, y el esclavo responde a las solicitudes del maestro. En algunos protocolos, como es el caso de DNP3 o IEC 60870-5-104 (comúnmente conocido como IEC 104), se permite que además el esclavo actualice al maestro del estado de sus registros sin una solicitud explícita previa. La estructura de la cabecera de Modbus se muestra en la Figura 2.1, donde se destacan los campos de la cabecera fijos. Los campos variables dependen del Function Code (FC) que se esté utilizando, añadiendo si lo necesita un campo que referencie un valor (1 Byte) o una dirección de registro (2 Bytes). Dado que el objetivo de este apartado es introducir la cabecera y no explicarla en profundidad se dejan de lado el resto de campos dependientes del FC que puedan aparecer en la cabecera. Figura 2.1 Campos de la cabecera del protocolo Modbus. 3 4Capítulo 2. Conocimientos previos Existen diversos FC implementados en el protocolo Modbus, cada uno con una función específica. En la Tabla 2.1 se muestran algunos de los FC más comunes. Tabla 2.1 Códigos de función Modbus. Function Code Nombre Descripción 01 Read Coils Lee el estado de las bobinas en el rango especificado. 02 Read Discrete Inputs Lee el estado de las entradas digitales en el rango especificado. 03 Read Holding Registers Lee el contenido de los registros de retención. 04 Read Input Registers Lee el contenido de los registros de entrada. 05 Write Single Coil Escribe un valor en una sola bobina. 06 Write Single Register Escribe un valor en un solo registro. 15 Write Multiple Coils Escribe valores en múltiples bobinas. 16 Write Multiple Registers Escribe valores en múltiples registros. 43 Read Device Information Lee información específica del dispositivo, como identificadores. El protocolo define cuatro tipos de registros, los cuales tienen un espacio de direcciones distinto y viene dado por el FC, esto es, dos registros pueden estar en la misma dirección mientras sean de distinto tipo. Los cuatro tipos de registros se diferencian por su función y acceso: •Coil (Bobina): Registros de tipo bit diseñados para operaciones de salida. Permiten lectura (FC 1) y escritura (FC 5 para un bit o FC 15 para múltiples bits). Se utilizan típicamente para controlar actuadores como relés o válvulas. •Discrete Input (Entrada Discreta): Registros de solo lectura (FC 2) que representan estados binarios de entradas físicas, como sensores digitales o interruptores. No permiten escritura. •Holding Register (Registro de Retención): Registros de 16 bits para almacenar datos numéricos. Son accesibles para lectura (FC 3) y escritura (FC 6 para un registro o FC 16 para múltiples). Se emplean en parámetros configurables como setpoints o valores de control. •Input Register (Registro de Entrada): Registros de 16 bits de solo lectura (FC 4), usados para datos analógicos como mediciones de temperatura, presión o valores de sensores que no requieren escritura. Esta división garantiza coherencia funcional: los registros Input (2 y 4) son de solo lectura para monitorización, mientras los Holding yCoils (3 y 1) permiten configuración activa. La tabla 2.2 resume los FCs asociados a cada tipo de registro. Tabla 2.2 Tipos de Registros en Modbus. Tipo Tipo Dato FC lectura FC escritura Coil Bit 1 5, 15 Discrete Input Bit 2 - Holding Register 16-bit Word 3 6, 16 Input Register 16-bit Word 4 - Un aspecto crítico es la codificación de datos en los registros de 16 bits. Mientras los Holding eInput Registers almacenan valores en formato binario (enteros sin signo, complemento a dos o punto flotante según estándar IEC 61131-3), los Coils yDiscrete Inputs usan valores booleanos (0x0000=Falso, 0xFF00=Verdadero). Esta estructura permite interoperabilidad entre dispositivos mediante una semántica predecible. 2.2 Docker Docker es una tecnología desarrollada por Docker Inc. en el año 2013 cuyo fin es facilitar la creación, despliegue y ejecución de aplicaciones mediante contenedores. Los contenedores permiten a los desarrolladores empaquetar una aplicación con todas sus dependencias en un entorno aislado y consistente, mejorando la 2.2 Docker 5 portabilidad y la eficiencia. En este contexto se habla de imágenes, que son plantillas inmutables a partir de las cuales se instancian los contenedores. Una semejanza rápida sería comparar las imágenes y contenedores a programas y procesos, respectivamente: un contenedor es una imagen en ejecución. Para la gestión de imágenes contamos con los comandos principales mostrados en la tabla 2.3. Tabla 2.3 Comandos comunes para la gestión de imágenes en Docker. Comando Uso docker pull Descargar una imagen desde un repositorio docker build Construir una imagen a partir de un Dockerfile docker images Listar todas las imágenes locales docker rmi Eliminar una imagen local Para el aprovisionamiento de imágenes se puede o bien reusar una ya existente o bien crearla. La creación de una imagen personalizada se realiza mediante un Dockerfile, que es un archivo de texto con instrucciones que describen cómo construir la imagen. Las instrucciones más importantes que se pueden usar dentro de un Dockerfile están recogidos en la tabla 2.4. Tabla 2.4 Instrucciones comunes de Dockerfile. Instrucción Uso FROM Especificar la imagen base para la construcción RUN Ejecutar comandos en el contenedor durante la construcción COPY Copiar archivos/directorios al sistema de archivos del contenedor CMD Especificar el comando por defecto a ejecutar EXPOSE Declarar el puerto en el que la aplicación escuchará ENV Establecer variables de entorno Un Docker Registry es un servicio centralizado que permite almacenar, gestionar y distribuir imágenes Docker. Estas imágenes pueden ser públicas o privadas, dependiendo de las necesidades del proyecto. El registro más conocido es Docker Hub, que ofrece una gran variedad de imágenes preconstruidas listas para su uso. Sin embargo, también es posible configurar registros privados, como Amazon Elastic Container Registry (ECR) o Azure Container Registry (ACR), para mantener un control más estricto sobre el acceso y la distribución de las imágenes. Los registros facilitan la colaboración entre equipos al proporcionar un repositorio centralizado desde donde se pueden descargar las imágenes necesarias, y además, soportan procesos de CI/CD al integrarse fácilmente en los flujos de desarrollo. A modo de resumen se incluye la figura 2.2 2.2.1 Docker Compose Docker Compose es una herramienta de Docker que facilita la gestión de los contenedores. Mediante un archivo YAML Ain’t Markup Language (YAML) de nombre por defecto docker-compose.yml, se puede especificar cómo deben ser configurados y relacionados varios servicios, simplificando el proceso de despliegue y gestión de aplicaciones complejas. YAML es un formato de serialización de datos fácil de leer y escribir. En este caso, el YAML deberá tener obligatoriamente un campo services y opcionalmente si así lo requiere, otro de networks yvolumes. Services El bloque services define los distintos servicios que forman la aplicación. Cada servicio representa un contenedor que puede incluir configuraciones como la imagen a usar, variables de entorno, puertos expuestos, volúmenes, y comandos a ejecutar. 6Capítulo 2. Conocimientos previos Figura 2.2 Arquitectura de Docker. Networks El bloque networks permite definir redes personalizadas para facilitar la comunicación entre servicios de forma aislada. Esto ofrece un mayor control sobre la conectividad entre los contenedores y permite crear entornos más seguros y organizados. Volumes El bloque volumes permite definir volúmenes que pueden ser utilizados por los servicios para almacenar datos de manera persistente. Esto es útil cuando es necesario mantener datos más allá del ciclo de vida de un contenedor. Los volúmenes pueden ser configurados para mapear una parte específica del sistema de archivos del anfitrión a una ubicación dentro del contenedor, asegurando que los datos permanezcan accesibles incluso si los contenedores se eliminan o reinician. Ejemplo Un ejemplo de un docker-compose.yml con dos servicios con poca configuración sería el descrito en 2.1. Aquí se definen dos servicios web ydb. El primero usa la imagen nginx:latest y expone el puerto 80 del contenedor en el puerto 8080 del anfitrión. El segundo usa la imagen postgres:latest y expone el puerto 5432 del contenedor en el puerto 5432 del anfitrión. Además, se definen variables de entorno para configurar la base de datos. Código 2.1 Ejemplo básico docker-compose.yml. 1services : 2web: 3image: nginx: latest 4ports : 5−"8080:80" 6 7db: 8image: postgres : latest 9environment: 10 POSTGRES_USER: exampleuser 11 POSTGRES_PASSWORD: examplepass 12 POSTGRES_DB: exampledb 13 ports : 14 −"5432:5432" 2.3 Patrón MVC 7 2.3 Patrón MVC El patrón Modelo-Vista-Controlador (MVC) es una arquitectura de software utilizada ampliamente en el desarrollo de aplicaciones web, móviles y de escritorio [5]. Su principal objetivo es organizar el código en tres componentes claramente diferenciados: Modelo, Vista y Controlador. Esto promueve la separación de responsabilidades, permitiendo mejorar la modularidad, escalabilidad, mantenimiento y reutilización del código. Cada uno de estos componentes desempeña un rol fundamental dentro del patrón: 1. Modelo: Representa los datos y la lógica de negocio de la aplicación. Se encarga de realizar cálculos, gestionar la persistencia de datos (por ejemplo, mediante acceso a bases de datos) y procesar las reglas de negocio. El Modelo debe estar desacoplado de los detalles específicos de presentación y enfocarse exclusivamente en la funcionalidad de la aplicación. 2. Vista: Es la capa responsable de la presentación de los datos y de la interacción con el usuario. Muestra la información proveniente del modelo a través de interfaces gráficas o visuales, ya sea en navegadores web, aplicaciones de escritorio o dispositivos móviles. La Vista debe estar diseñada para minimizar los problemas de integración y facilitar la definición de la interfaz de usuario dentro del sistema. 3. Controlador: Actúa como intermediario entre la Vista y el Modelo. Procesa las peticiones del usuario, las interpreta y las traduce en acciones que interactúan con el Modelo. Posteriormente, el Controlador toma la respuesta del Modelo y la pasa a la Vista, asegurando que los datos sean presentados correctamente. Figura 2.3 Aplicación del patrón Modelo-Vista-Controlador. 2.4 REST API En el contexto de aplicaciones web modernas, Representational State Transfer (REST) es un estilo arquitectónico ampliamente adoptado para diseñar servicios web que permitan la comunicación entre sistemas distribuidos. REST no es un protocolo, sino un conjunto de buenas prácticas que promueven la uniformidad en la comunicación entre componentes.[6] REST se basa en el uso de estándares web, como Hypertext Transfer Protocol (HTTP) o Constrained Application Protocol (CoAP), para la transferencia de recursos identificados por Uniform Resource Identifier (URI)s. Cada recurso es representado en formatos estandarizados como JavaScript Object Notation (JSON) o Extensible Markup Language (XML), facilitando la interoperabilidad entre sistemas heterogéneos. Las principales características de una REST Application Programming Interface (API) son: •Cliente-Servidor: La arquitectura se divide en dos partes independientes, el cliente y el servidor, que interactúan a través de una interfaz bien definida. 8Capítulo 2. Conocimientos previos •Interfaz uniforme: Los recursos son accedidos a través de un conjunto de operaciones Create, Read, Update, Delete (CRUD). •Sin estado: Cada petición al servidor debe contener toda la información necesaria para procesarla, sin depender de un estado almacenado entre peticiones. El diseño de una REST API HTTP efectiva está guiado por buenas prácticas que incluyen:[7] •Diseño orientado a recursos: Cada URI debe representar un recurso específico, nombrado con sustantivos (por ejemplo, /usuarios o/pedidos). •Uso adecuado de los métodos HTTP: Como se detalla en la tabla 2.5, los métodos HTTP están asociados a operaciones CRUD estandarizadas. •Manejo de códigos de estado HTTP: Las respuestas deben incluir códigos de estado como 200 OK, 404 Not Found o500 Internal Server Error para informar al cliente sobre el resultado de la operación. Tabla 2.5 Equivalencia entre operaciones CRUD y métodos HTTP. Operación CRUD Método HTTP Uso Create POST Crea un nuevo recurso Read GET Recupera información de un recurso existente Update PUT Modifica un recurso existente Delete DELETE Elimina un recurso existente En el contexto del patrón MVC, las REST APIs suelen desempeñar un papel fundamental al actuar como interfaz entre el Modelo y la Vista. Por ejemplo, cuando un usuario interactúa con la interfaz de una aplicación web, las peticiones generadas por la Vista (normalmente utilizando Asynchronous JavaScript And XML (AJAX) o similar) son enviadas al Controlador. Este, a su vez, interactúa con el Modelo para obtener o modificar datos y devuelve una respuesta estructurada (en formato JSON o XML) que es procesada y mostrada por la Vista. 2.4.1 Ejemplo práctico de REST API Un ejemplo típico de REST API para la gestión de usuarios podría incluir un endpoint para obtener información de un usuario específico: 1GET /usuarios/123 HTTP/1.1 2Host: api.ejemplo.com 3Authorization: Bearer token123 4 5HTTP/1.1 200 OK 6Content-Type: application/json 7 8{ 9"id": 123, 10 "nombre":"Juan Pérez", 11 "email":"[email protected]" 12 } En este ejemplo, el cliente realiza una petición GET al recurso /usuarios/123 para obtener información de un usuario con el identificador 123. El servidor responde con un código de estado 200 y un cuerpo en formato JSON que contiene los datos solicitados. 2.4.2 Errores comunes y desafíos en REST API A pesar de su simplicidad, el diseño de una REST API presenta varios retos: •Uso incorrecto de métodos HTTP: Es frecuente observar endpoints que no respetan las convenciones REST, como /obtenerUsuario en lugar de /usuarios. 2.4 REST API 9 •Endpoints poco intuitivos: Diseñar URLs claras y orientadas a recursos puede ser un desafío, especialmente en aplicaciones complejas. •Autenticación y autorización: La gestión de permisos a través de sistemas como OAuth2 o tokens JSON Web Token (JWT) requiere una implementación cuidadosa. Abordar estos problemas es fundamental para garantizar que las APIs sean fáciles de usar, mantener y escalar. Además, las buenas prácticas en el diseño REST contribuyen a alinear el desarrollo con los principios del patrón MVC, promoviendo la modularidad y la claridad en la arquitectura del sistema. 16 Capítulo 4. Diseño e Implementación de la Solución Figura 4.1 Diagrama de componentes: Backend y Core. en el directorio src. Esta estructura modular no solo permite una fácil extensión del sistema para soportar nuevos protocolos y escenarios, sino que también facilita la implementación y el uso en diversos entornos gracias a la combinación de Docker y una interfaz web intuitiva. 4.4 Core El Core es el componente más importante del sistema, ya que es el encargado de dar la funcionalidad del sistema. En este desarrollo se le ha dado mucha importancia a la claridad y a la modularidad del código, lo cual ha facilitado la codificación y depuración de errores. Con la estructura identificada, la implementación se ve enormemente facilitada. Esta vez vamos a empezar desde el principio, especificando la implementación y justificando las decisiones tomadas. 4.4.1 Docker Compose El primer paso es definir el formato que tendrá el fichero docker-compose.yml de nuestro proyecto. A grandes rasgos consta de dos secciones relevantes para este proyecto: networks yservices Ahora se va a entrar en detalle en cada sección. Networks Este bloque tiene como función ubicar a todos los contenedores en la misma subred para que cuando se capture el tráfico, tenga las direcciones MAC e IP correctas. Un ejemplo de definición sería el expuesto en el listado 4.1. Código 4.1 Ejemplo de network. 1networks: 2icscommemulator: 3ipam: 4config : 5−subnet: 192.168.45.0/24 6name: icscommemulator 4.4 Core 17 La configuración es bastante directa e intuitiva. Networks es la palabra reservada en el docker-compose.yml que sirve para comenzar la lista de redes definidas. En este caso solo hay una con el nombre icscommemulator, que se guardará con dicho nombre. Ipam (IP Address Management)[14], especifica que la subred que se quiere es la 192.168.45.0/24. Services Este es el bloque principal de un fichero docker-compose.yml. En él se definen los servicios que se van a lanzar. En nuestro caso, los contenedores que emularán los dispositivos Modbus. Cada servicio tiene un nombre, que será el identificador del nodo, y una serie de configuraciones. En nuestro caso hace falta configurar todos los contenedores de forma específica. Se muestra la configuración de un nodo maestro en el listado 4.2. Código 4.2 Ejemplo de nodo maestro. 1modbus_master_0: 2build : 3context : ./ protocols /modbus/master 4dockerfile : Dockerfile .master 5container_name: modbus_master_container_0 6depends_on: 7modbus_slave_0: 8condition : service_healthy 9modbus_slave_1: 10 condition : service_healthy 11 environment: 12 −PYTHONUNBUFFERED=1 13 image: modbus_master_image 14 networks: 15 icscommemulator: 16 ipv4_address: 192.168.45.10 17 mac_address: E4:A8:DF:D1:11:0A 18 volumes: 19 −/tmp/ICSCommEmulator/masters/0/master.csv:/app/master.csv:ro En el listado se identifican muchos campos que se deben explicar: •modbus_master_0: Este es el nombre del servicio. En este caso, se trata del nodo maestro Modbus. •build: – context: Especifica el directorio de contexto para la construcción de la imagen Docker. En este caso, es ./protocols/modbus/master. – dockerfile: Indica el archivo Dockerfile a utilizar para construir la imagen. Aquí se usa Dockerfile.master. •container_name: Define el nombre del contenedor. En este ejemplo, es modbus_master_container_0. •depends_on: Especifica las dependencias del servicio. El nodo maestro depende de los nodos esclavos modbus_slave_0 ymodbus_slave_1, y solo se iniciará cuando estos estén en un estado saludable (service_healthy). •environment: Define las variables de entorno para el contenedor. Aquí se establece PYTHONUNBUFFERED=1, lo que desactiva el almacenamiento en búfer de la salida estándar de Python. •image: Especifica la imagen Docker a utilizar. En este caso, es modbus_master_image. •networks: Configura la red a la que se conectará el contenedor. En este ejemplo, se conecta a la red icscommemulator con una dirección IPv4 específica (192.168.45.10) y una dirección MAC (E4:A8:DF:D1:11:0A). •volumes: Monta un volumen en el contenedor. En este caso, se monta el archivo de configuración del nodo maestro. También contamos con la configuración de un esclavo, la cual se muestra en en el listado 4.3 Código 4.3 Ejemplo de nodo esclavo. 1modbus_slave_0: 18 Capítulo 4. Diseño e Implementación de la Solución 2build : 3context : ./ protocols /modbus/slave 4dockerfile : Dockerfile . slave 5container_name: modbus_slave_container_0 6environment: 7−PYTHONUNBUFFERED=1 8expose: 9−’502’ 10 healthcheck : 11 interval : 10s 12 retries : 3 13 start_period : 10s 14 test : 15 −CMD−SHELL 16 −test −f /app/app_running.lock 17 timeout: 5s 18 image: modbus_slave_image 19 networks: 20 icscommemulator: 21 ipv4_address: 192.168.45.101 22 mac_address: E4:A8:DF:D1:11:1B 23 volumes: 24 −/tmp/ICSCommEmulator/slaves/0/slave.yaml:/app/ slave .yaml:ro Las diferencias con el nodo maestro son: •depends_on: El nodo esclavo no tiene dependencias especificadas, mientras que el nodo maestro depende de todos los nodos esclavos. •expose: El nodo esclavo expone el puerto 502, puerto estándar de Mobus, lo que permite que otros servicios se comuniquen con él a través de este puerto. El nodo maestro no tiene esta configuración. •healthcheck: El nodo esclavo incluye una configuración de verificación de salud (healthcheck) que define cómo y cuándo se verifica el estado del contenedor. Esta configuración incluye: – interval: El intervalo de tiempo entre verificaciones, en este caso, 10 segundos. – retries: El número de intentos fallidos antes de considerar que el contenedor no está saludable, aquí son 3 intentos. – start_period: El período de tiempo durante el cual las verificaciones fallidas no cuentan hacia el límite de intentos, en este caso, 10 segundos. – test: El comando que se ejecuta para verificar el estado del contenedor. Aquí se verifica la existencia del archivo /app/app_running.lock. – timeout: El tiempo máximo que se espera para que el comando de verificación se complete, en este caso, 5 segundos. •volumes: El nodo esclavo monta un archivo de configuración diferente (slave.yaml) en comparación con el nodo maestro (master.csv). 4.4.2 Docker Images En la sección anterior se ha explicado cuál va a ser el formato del docker-compose. Aquí se va a entrar en detalle en las dos imágenes desarrolladas en el proyecto. Como se observa en la figura 4.1, tenemos un directorio donde se encuentra el protocolo Modbus. Dentro de este directorio, se encuentran dos subdirectorios, uno para el maestro y otro para el esclavo. En cada uno de estos directorios se encuentra un Dockerfile que se encarga de construir la imagen y un script principal. Dockerfiles A fin de tener la mayor cantidad de contenedores corriendo, estos Dockerfiles se han diseñado para generar imágenes del menor tamaño posible, por lo que se han seguido las recomendaciones de Docker para ello. Se ha minimizado el espacio ocupado en disco por las imágenes, siendo de 52.7MB y 55.3MB para el maestro y el esclavo, respectivamente. Esto es inferior a otras imágenes que se pueden encontrar en Docker Hub, con tamaños de entre 60 y 100 MB. En el listado 4.4 se muestra el Dockerfile del nodo maestro y en el listado 4.5 el del nodo esclavo. 4.4 Core 19 Código 4.4 Dockerfile del maestro. 1# Use an official Python runtime as a parent image 2FROM python:3.10.0-alpine 3 4# Set the working directory in the container to /app 5WORKDIR /app 6 7# Add the main script into the container at /app 8COPY ./master.py /app 9 10 # Install any needed packages specified in requirements.txt 11 RUN pip install --no-cache-dir pymodbus 12 13 # Run master.py when the container launches 14 ENTRYPOINT ["python","master.py"] Código 4.5 Dockerfile del esclavo. 1# Use an official Python runtime as a parent image 2FROM python:3.10.0-alpine 3 4# Set the working directory in the container to /app 5WORKDIR /app 6 7# Add the main script into the container at /app 8COPY ./slave.py /app 9 10 # Install any needed packages 11 RUN pip install --no-cache-dir pymodbus pyyaml 12 13 # Run slave.py when the container launches 14 ENTRYPOINT ["python","slave.py"] Ambas imágenes son muy parecidas, ya que solo se diferencian en el script que se copia y en las librerías que se instalan. En el caso del nodo maestro, solo se necesita la librería pymodbus, mientras que en el nodo esclavo se necesita también pyyaml. Scripts El script del maestro es muy sencillo, ya que solo se encarga de enviar mensajes Modbus. Esos mensajes están descritos en un archivo CSV que se monta en el contenedor. En el listado 4.6 se muestra un ejemplo de este archivo. Código 4.6 Ejemplo de master.csv. 1count,function_code,interval,ip,port,recurrent,slave_id,start_address,timestamp,values 23,3,5,192.168.45.100,502,True,1,0,0,[] 32,1,4,192.168.45.100,502,True,1,0,1,[] 41,3,5,192.168.45.101,502,True,2,0,0,[] 53,1,4,192.168.45.101,502,True,2,0,1,[] El script del esclavo requiere algo más de configuración, ya que se tiene que configurar la respuesta a los mensajes del maestro. Estos mensajes están descritos en un archivo YAML que se monta en el contenedor. En el listado 4.7 se muestra un ejemplo de este archivo. Código 4.7 Ejemplo de slave.yaml. 1coils : 2type: sequential 3values : 4−1 5−0 6−1 7discrete_inputs : 8type: sequential 9values : ’’ 10 holding_registers : 11 type: sequential 12 values : 13 −3 14 identity : 15 major_minor_revision: ’2.5’ 16 model_name: MagicMaster 3000 20 Capítulo 4. Diseño e Implementación de la Solución 17 product_code: TF98765 18 product_name: Controlador de Automatizacin Mgica 19 user_application_name : Control de Procesos Mgicos 20 vendor_name: TechnoFantasy Inc. 21 vendor_url : https :// www.technofantasy.com 22 input_registers : 23 type: sequential 24 values : ’’ 25 ip: 192.168.45.101 26 mac: E4:A8:DF:D1:11:1B 27 port : 502 28 slave_id : 2 El script diferencia entre registros definidos de forma secuencial o dispersa. En el caso de los registros secuenciales, se especifica el valor de cada uno de ellos. En el caso de los registros dispersos, se crea un diccionario "dirección:valor". La librería utilizada para gestionar los mensajes de Modbus en Python (Pymodbus) presenta un comportamiento indeseado al definir los registros en el esclavo: los índices empiezan en 1 en lugar de en 0. Es decir, cuando se guarda un registro con índice N (posición N) en Python, lo estará guardando en el índice N (posición N-1) en Pymodbus. Al recuperarlos recupera lo esperado (si se le pide M, recupera M+1) De este modo, a la hora de guardar los registros hay que tener en cuenta este detalle e incrementar en uno la dirección de los registros. 4.4.3 Módulo docker_compose_generator.py Este módulo tiene un papel muy importante, que es pasar de la configuración del escenario, descrito más adelante en 4.5.2, a un fichero docker-compose.yml. Para ello se ha considerado apropiada una programación orientada a objetos en este módulo. La interfaz con el módulo es muy sencilla, ya que solo se necesita crear una instancia de la clase DockerComposeGenerator y llamar al método parse con la configuración del escenario. A continuación, se detallan los componentes y funcionalidades clave del módulo: Clase DockerComposeGenerator La clase DockerComposeGenerator es la encargada de generar y validar archivos de configuración de Docker Compose. Los atributos principales de la clase incluyen: •services (dict): Almacena la configuración de los servicios. •networks (dict): Almacena la configuración de las redes. •protocol (str): Nombre del protocolo utilizado en la configuración de Docker Compose. •ip_base (ipaddress.IPv4Address): Dirección IP base para la red. •path (str): Ruta al archivo Docker Compose. •config_path (str): Ruta a los archivos de configuración. •last_ip (ipaddress.IPv4Address): Última dirección IP asignada para la asignación dinámica. Métodos de la Clase •__init__: Inicializa la clase con el protocolo, la ruta del archivo y la ruta de configuración. •add_network: Añade una red a la configuración de Docker Compose. •add_node: Añade un nodo (servicio) a la configuración de Docker Compose. •generate: Genera el archivo YAML de Docker Compose. •validate: Valida el archivo de Docker Compose generado. •validate_file: Método estático para validar un archivo de Docker Compose dado. •_validate_file: Método estático auxiliar para validar un archivo de Docker Compose utilizando la CLI de Docker Compose. •parse: Genera una configuración de Docker Compose basada en el escenario proporcionado. •get_dependencies: Obtiene las dependencias para los nodos maestros. •is_master: Verifica si un nodo es un nodo maestro. 4.4 Core 21 •is_slave: Verifica si un nodo es un nodo esclavo. Ejemplo de Uso A continuación, se muestra un ejemplo de cómo utilizar la clase DockerComposeGenerator para generar un archivo docker-compose.yml a partir de una configuración de escenario: Código 4.8 Ejemplo de uso de docker-compose-generator.py. 1if __name__ == "__main__": 2scenario = { 3"protocol":"modbus", 4"ip_network":"172.28.0.0/16", 5"nodes": [ 6{"role":"master","ip":"172.28.0.2"}, 7{"role":"slave","ip":"172.28.0.3"}, 8{"role":"slave","ip":"172.28.0.4"}, 9], 10 } 11 generator = DockerComposeGenerator( 12 "modbus","docker-compose.yml","/tmp/ICSCommEmulator" 13 ) 14 generator.parse(scenario) Este ejemplo crea una instancia de DockerComposeGenerator con el protocolo "modbus", la ruta del archivo docker-compose.yml y la ruta de configuración /tmp/ICSCommEmulator. Luego, llama al método parse con la configuración del escenario para generar el archivo de Docker Compose. 4.4.4 Módulo scenario_config_generator.py En el diseño se identificó la necesidad de gestionar los archivos de configuración de los nodos maestros y esclavos. Para ello, se ha desarrollado el módulo scenario-config-generator.py, que se encarga de generar y validar estos archivos. Tras estudiar las posibilidades, se consideró que se debía usar una estructura de directorios bajo /tmp/ICSCommEmulator para almacenar los archivos de configuración. En este directorio se crean subdirectorios para cada nodo, donde se almacenan los archivos de configuración específicos. Cuando se mencionó la figura 4.1, se dijo que faltaba una parte referente a los ficheros de configuración específicos de cada contenedor. Esta sección se encarga de explicar cómo se han implementado. Para ello en la figura 4.2 se especifica la estructura de directorios generada por el módulo. Figura 4.2 Estructura de directorios de /tmp/ICSCommEmulator. 22 Capítulo 4. Diseño e Implementación de la Solución Para el uso de este módulo, se ha desarrollado la clase ScenarioConfigGenerator, que se encarga de generar los archivos de configuración para los nodos maestros y esclavos. A continuación, se detallan los componentes y funcionalidades clave del módulo: Clase ScenarioConfigGenerator La clase ScenarioConfigGenerator es la encargada de generar los archivos de configuración para los nodos maestros y esclavos. Los atributos principales de la clase incluyen: •scenario (dict): Diccionario que contiene la configuración del escenario. •config_path (str): Ruta a los archivos de configuración. Métodos de la Clase •__init__: Inicializa la clase con el escenario y la ruta de configuración. •_convert_to_int: Método estático para convertir una cadena a un entero si es posible. •_craft_master: Crea archivos de configuración para los nodos maestros. •_craft_slave: Crea archivos de configuración para los nodos esclavos. •clean: Limpia la ruta de configuración eliminando los archivos existentes. •generate: Genera los archivos de configuración para el escenario. Ejemplo de Uso A continuación, se muestra un ejemplo de cómo utilizar la clase ScenarioConfigGenerator para generar archivos de configuración a partir de una configuración de escenario: Código 4.9 Ejemplo de uso de scenario-config-generator.py. 1scenario = { 2"nodes": [ 3{"role":"master","messages": [...]}, 4{"role":"slave", ...} 5] 6} 7generator = ScenarioConfigGenerator(scenario, "/tmp/ICSCommEmulator") 8generator.generate() Este ejemplo crea una instancia de ScenarioConfigGenerator con la configuración del escenario y la ruta de configuración. Luego, llama al método generate para generar los archivos de configuración. Para evitar repetición en la documentación, el formato que ha de tener cada nodo es un diccionario con el formato expuesto de cada archivo de configuración. En el caso del nodo maestro, código 4.6, y en el del esclavo, código 4.7. 4.4.5 Módulo runner.py Aunque no se haya dicho de forma explícita, se necesita un script que sirva de interfaz para la ejecución de Docker Compose y Tcpdump de forma coordinada. De esta necesidad nace el módulo runner.py. Este módulo usa una clase singleton, ScenarioRunner, la cual solo permite que haya un escenario corriendo a la vez. Se ha decidido que solo se pueda ejecutar un escenario a la vez para evitar problemas de concurrencia. Este módulo tiene tres funciones que actúan de interfaz para el resto de módulos. start En esta función asegura que solo se ejecutará el escenario propuesto si no se está ejecutando ningún escenario. Recibe como parámetro la ruta hacia el fichero docker compose, el tiempo de simulación y la ruta hacia el fichero de salida. Con estos parámetros configura ScenarioRunner y lo corre. Para no bloquear el hilo que llama a esta función, se lanza otro hilo donde correrá la emulación. Para lanzar el escenario se realizan las siguientes acciones: 4.5 Backend 23 1. Asegurar seguridad de hilos con un candado. 2. Marcar que se está corriendo un escenario. 3. Lanzar Docker Compose. 4. Lanzar Tcpdump en la interfaz de Docker. 5. Esperar a que la emulación termine. 6. Parar Docker Compose. 7. Parar Tcpdump. Debe añadirse que Docker Compose borra los contenedores que crea pero no borra la red. Es por eso que se ha añadido una función que limpia la red creada. Por último, Docker cursa tráfico adicional por las redes, como el protocolo Multicast DNS (mDNS) o Simple Service Discovery Protocol (SSDP). mDNS es un servicio diseñado para llevar a cabo la resolución de nombres en redes más pequeñas[15]. SSDP es un protocolo que sirve para la búsqueda de dispositivos Universal Plug and Play (UPnP) en una red. Utiliza UDP en unicast o multicast en el puerto 1900 para anunciar los servicios de un dispositivo[16]. Dado que nuestro objetivo es el realismo de la red, se ha configurado Tcpdump para que no escuche esos protocolos. stop Esta función solo se llama si el usuario pide desde la interfaz gráfica detener el escenario. La función fuerza la detención del escenario que se esté ejecutando, por lo que no recibe parámetros. El proceso es primero parar Docker Compose y luego Tcpdump. status En la configuración de Tcpdump se especfició que se guardase el tráfico en el fichero de salida periódicamente. De este modo se puede devolver información relevante para saber el estado de la emulación como el tamaño del pcap. En este caso se han considerado necesarios los campos: •Tiempo transcurrido. •Tiempo total de emulación. •Tamaño actual del pcap. •Si el escenario está corriendo. 4.5 Backend El Backend es el componente que coordina la vista con el modelo, es decir, la interfaz gráfica con el núcleo de la funcionalidad. Dado que este fue el segundo componente en ser codificado, se mantuvo el espíritu de modularidad anteriormente expuesto en el core. 4.5.1 Módulo web.py Este módulo es el encargado de crear la aplicación web que ofrece sus servicios tanto de REST API como de interfaz gráfica. Para ello, como se ha mencionado anteriormente, se ha usado Flask, un framework minimalista de Python. Anteriormente ya se nombraron los distintos endpoints, aquí se explicará en detalle cómo se han implementado. Lo primero es entender la estructura de directorios que genera Flask. Como se puede ver en la figura 4.1, hay una carpeta web en la raíz del proyecto, donde se encuentran los ficheros de la aplicación web. La subcarpeta templates es una carpeta reservada por el framework para colocar los ficheros Hypertext Markup Language (HTML). Esto es así porque Flask permite la renderización del HTML con código Python dentro1, similar a JavaServer Pages (JSP), visto a lo largo de la carrera. También contamos con la subcarpeta static, donde se encuentran el código JavaScript, el CSS y las imágenes utilizadas. Como se ha mencionado anteriormente, se han usado las librerías Cytoscape.js yipaddr.js 1https://www.geeksforgeeks.org/flask-rendering-templates/ 24 Capítulo 4. Diseño e Implementación de la Solución en el Frontend. Al ser archivos de JavaScript que carga el usuario junto al HTML, para permitir que la aplicación pueda correr sin necesidad de acceso a internet, se han descargado y situado en la carpeta asignada para JavaScript. A continuación se describen brevemente los endpoints desarrollados por este proyecto, ya vistos en la tabla 4.1. GET /api/networks/ •Descripción: Devuelve una lista de redes disponibles en el sistema. •Parámetros: Ninguno. •Respuesta: –200 OK: Lista de redes. Ejemplo: [ "demo", "prueba" ] –500 Internal Server Error: Error inesperado en el servidor. •Funcionamiento: Este endpoint usa el módulo scenario_handler.py para saber todos los escenarios que existen. POST /api/networks/ •Descripción: Crea un escenario con los parámetros especificados. •Parámetros: –projectName: Nombre del proyecto. –ipSubrange: Rango IP de los nodos en el proyecto. –protocol: Protocolo que tendrá el escenario. –masterNodes: Número de maestros inicialmente en el escenario. –slaveNodes: Número de esclavos inicialmente en el escenario. •Respuesta: –200 OK: Escenario creado con éxito. –400 Bad Request: El escenario tiene parámetros inválidos. –500 Internal Server Error: Error inesperado en el servidor. •Funcionamiento: El escenario comprueba secuencialmente si: –El escenario no existe. –El valor de maestros o esclavos es correcto. –Están todos los parámetros en la petición. –El rango IP es válido. Una vez comprobado, crea el escenario usando el módulo cytoscape_adapter.py para la creación de la representación de la red y el módulo scenario_handler.py para guardarlo. GET /api/networks/name •Descripción: Devuelve la representación JSON del proyecto. •Parámetros: –name: Nombre del proyecto. •Respuesta: –200 OK: Escenario en representación JSON de Cytoscape.js. –404 Not Found: No se ha encontrado el escenario. –500 Internal Server Error: Error inesperado en el servidor. •Funcionamiento: Este endpoint consulta la base de datos para obtener todas las redes disponibles. Si se proporciona un parámetro de filtro, las redes se filtrarán en consecuencia. 4.5 Backend 25 PUT /api/networks/name •Descripción: Actualiza la configuración de un escenario. •Parámetros: El escenario en la representación JSON que es capaz de generar y restaurar Cytoscape.js. •Respuesta: –200 OK: Escenario actualizado con éxito. –400 Bad Request: Escenario inválido. –500 Internal Server Error: Error inesperado en el servidor. •Funcionamiento: Se valida el escenario usando el módulo cytoscape_adapter.py y posteriormente se guarda el escenario usando el módulo scenario_handler.py. GET /api/run/ •Descripción: Obtiene las métricas de la ejecución actual. •Parámetros: Ninguno •Respuesta: –200 OK: Métricas de la ejecución. Ejemplo: [ "elapsed_seconds": 2, "total_seconds": 15, "pcap_size": 3093, # bytes "running": true, ] –500 Internal Server Error: Error inesperado en el servidor. •Funcionamiento: Llama a la api ofrecida por runner.py y devuelve sus resultados. POST /api/run/name •Descripción: Arranca el escenario especificado. •Parámetros: –name: Nombre del proyecto. •Respuesta: –200 OK: Información de la ejecucición: [ "message": "Scenario running", "simulation_time": simulation_time, "file_path": file_path, ] –404 Not Found: Escenario no encontrado. –500 Internal Server Error: Error inesperado del servidor. •Funcionamiento: Usando los módulos docker_compose_generator.py,scenario_config_generator.py yrunner.py, se crea el escenario, se generan los ficheros de configuración y se arranca la emulación. DELETE /api/run/ •Descripción: Fuerza la detención del escenario actual. •Parámetros: Ninguno. •Respuesta: –200 OK: OK. –500 Internal Server Error: Error inesperado en el servidor. •Funcionamiento: Usa la api de runner.py para detener los recursos utilizados por la emulación, como Docker Compose y Tcpdump. GET /index.html •Descripción: Devuelve la página principal. 32 Capítulo 4. Diseño e Implementación de la Solución En caso de error no se le permite al usuario continuar, ya que daría lugar a una configuración inválida. En caso de que sean alertas se le permite continuar. 4.6.5 Ejecución Para comenzar la ejecución se le pide al usuario que introduzca el tiempo que durará la emulación. Esto es así porque al tener la posibilidad de mandar mensajes periódicos, no se puede determinar un tiempo de emulación. Una vez comenzada la ejecución el script le pide información acerca de la ejecución al servidor y con ello se muestra en una pestaña la siguiente información: •Tiempo actual de emulación. •Tiempo total de emulación. •Tamaño del pcap en bytes. 4.7 Casos de Uso Dadas las funciones principales, tenemos los siguientes casos de uso: •Creación de un nuevo escenario. •Carga de un escenario previamente guardado. •Guardado del escenario cargado. •Emulación del escenario cargado. •Detención de la emulación actual. 4.7.1 Creación de un Nuevo Escenario La creación de un escenario es una funcionalidad esencial en el proyecto. El usuario completa un formulario con los campos descritos en la tabla 4.9. Al confirmar, el escenario se guarda el ficheor y se redirige a la págin web del escenario automáticamente. La lógica del proceso está representada en el diagrama de la figura 4.5. Tabla 4.9 Campos requeridos para la creación de un escenario. Campo Tipo Descripción Nombre del proyecto Cadena de texto Identificador único del escenario Subred IP Formato IP CIDR Rango de direcciones IP para el escenario Protocolo Cadena de texto Campo reservado para compatibilidad futura Número de maestros Número entero Cantidad inicial de nodos maestros Número de esclavos Número entero Cantidad inicial de nodos esclavos 4.7.2 Carga de un Escenario Esta funcionalidad permite al usuario cargar un escenario previamente guardado desde la interfaz gráfica. Al seleccionar la opción correspondiente, se muestra una lista de escenarios disponibles. Tras elegir uno, este se carga automáticamente. El diagrama de la figura 4.6 detalla el proceso. 4.7.3 Guardado de un Escenario Cargado Una vez cargado un escenario, el usuario puede modificarlo y guardarlo. La información del escenario (nodos, configuraciones, posiciones, nombres, etc.) se almacena en archivos cuyo formato depende de las librerías utilizadas en el frontend. Aunque el guardado está influenciado por estas herramientas, la lógica subyacente es independiente, como se muestra en la figura 4.7. 4.7 Casos de Uso 33 Figura 4.5 Diagrama de secuencia para la creación de un escenario. Figura 4.6 Diagrama de secuencia para la carga de un escenario. 4.7.4 Emulación del Escenario Cargado Una vez guardado el escenario, el usuario puede iniciar su emulación, para lo que especifica la duración del proceso. La lógica de esta funcionalidad se divide en dos etapas, ilustradas en las figuras 4.8 y 4.9. 4.7.5 Detención de la Emulación Por último, un escenario en ejecución se puede detener en cualquier momento. La figura 4.10 muestra el proceso de detención de la emulación. 34 Capítulo 4. Diseño e Implementación de la Solución Figura 4.7 Diagrama de secuencia para el guardado de un escenario. Figura 4.8 Diagrama de secuencia para la ejecución de un escenario. 4.7 Casos de Uso 35 Figura 4.9 Diagrama de secuencia para la generación de archivos necesarios para la emulación. Figura 4.10 Diagrama de secuencia para la detención de la emulación. 5 Puesta en Ejecución y Ejemplos de Uso 5.1 Instalación 5.1.1 Obtención del código fuente El primer paso es obtener el código fuente, que aunque puede ser obtenido de múltiples formas, la más sencilla es descargarlo desde el repositorio de GitHub: curl -L0 \ https://api.github.com/repos/Nelnitorian/icscommemulator/tarball \ -o icscommemulator.tar.gz Descomprimimos el archivo descargado: tar -xvzf icscommemulator.tar.gz 5.1.2 Instalación de dependencias Para instalar las dependencias necesarias se recomienda instalar mediante el script install.sh, el cual instala: •La última versión de Docker y Docker Compose. •Tcpdump. •Python3. •Dependencias de librerías de Python, que se incluyen en el archivo requirements.txt. 5.1.3 Ejecución del proyecto Para ejecutar el proyecto, se debe ejecutar el script de python main.py. Para ello, ejecutamos en la terminal: python3 main.py Esto nos abrirá un servidor http en 127.0.0.1:8080, al que podremos acceder desde un navegador web entrando en la dirección http://127.0.0.1:8080. 5.2 Pruebas Las pruebas son esenciales en cualquier proyecto porque garantizan la funcionalidad y la calidad del sistema desarrollado. Al probar el código, se minimizan errores que podrían comprometer el rendimiento o la experiencia del usuario, especialmente en proyectos con múltiples componentes interdependientes. Además, las pruebas son fundamentales para verificar que los requisitos iniciales se cumplan y que los cambios futuros no introduzcan problemas inesperados, asegurando la sostenibilidad del proyecto a largo plazo. En algunos casos, realizar pruebas exhaustivas en todos los componentes puede ser innecesario y poco eficiente, especialmente si el proyecto tiene recursos limitados o plazos ajustados. Focalizar las pruebas únicamente en los componentes más complejos o críticos del sistema puede ser suficiente para garantizar la 37 38 Capítulo 5. Puesta en Ejecución y Ejemplos de Uso estabilidad general. Al centrarse en estos puntos, se detectan los posibles fallos de mayor impacto sin invertir un tiempo excesivo en partes que son más simples o menos propensas a errores. Este enfoque balancea el costo y el beneficio, asegurando una validación adecuada del sistema sin comprometer la viabilidad del proyecto. Puesto que el diseño del proyecto se hizo con la idea de ser modular, durante una ejecución del código principal es fácilmente identificable dónde está el fallo. Esto ha reducido los requisitos de pruebas unitarias, ya que la modularidad del código permite una depuración más sencilla. 5.2.1 Pruebas unitarias En el proyecto se ha identificado como necesario testear los componentes sobre los que se tiene menos control y podrían dificultar la depuración. En este caso se han realizado pruebas unitarias sobre el generador de ficheros de configuración de Docker Compose y la comunicación entre un maestro y un esclavo. Estas pruebas están alojadas en el directorio tests/ y se ejecutan con el comando python3 -m pytest. Docker Compose Generator En esta prueba unitaria se usa el generador de Docker Composes para crear un archivo de configuración y añadirle nodos de forma controlada. Posteriormente, se comprueba que en el fichero están los nodos generados correctamente. Finalmente, se le pide a Docker Compose que use su validador para terminar la prueba. Comunicación Maestro - Esclavo En esta prueba unitaria se lanza el maestro escrito en protocols/modbus/master/master.py y el esclavo de protocols/modbus/slave/slave.py. Posteriormente se comprueba que el esclavo inicia y responde correctamente a los mensajes. Figura 5.1 Ejecución exitosa de los tests unitarios. 5.3 Ejemplos de uso En este apartado presenta la interfaz gráfica así como algunos resultados interesantes. Previamente habrá sido necesario seguir los pasos descritos en sección 5.1. Para acompañar la demostración se ha creado un vídeo donde se prepara el mismo escenario. El vídeo se puede encontrar en https://drive.google.com/file/d/1VYJP0eNjIukOdCXdtUinA2j5hwsCZtj4/. 5.3.1 Creación de Escenario Para crear un escenario nos dirigimos a http://127.0.0.1:8080 y pulsamos en el botón de crear escenario. Como ya se ha visto en 4.6.1, nos encontramos con un formulario en el que tendremos que especificar algunos datos para la creación del escenario. Para el escenario de prueba vamos a crear dos esclavos y dos maestros en la subred 192.168.45.0/24. Llamaremos al escenario demo. De esta forma nos queda el formulario como se muestra en 5.2. 5.3 Ejemplos de uso 39 Figura 5.2 Formulario de creación de escenario. 5.3.2 Carga de Escenario Tras crear el escenario en el apartado anterior se nos redirigió automáticamente hacia http://127.0.0.1:8080/network/demo. Si accidentalmente hubiéramos cerrado la pestaña del navegador y quisiéramos volver a abrirla, nos dirigiríamos a http://127.0.0.1:8080, y esta vez pulsaríamos en el botón de cargar escenario. De esta forma nos salen todos los escenarios creados y podemos seleccionar el que queramos cargar, como se muestra en la figura 5.3. Figura 5.3 Formulario de carga de escenario. 5.3.3 Pantalla de red y utilidades En el momento de o bien cargar o bien crear nuestro escenario llamado demo, se redirige automáticamente a http://127.0.0.1:8080/network/demo. Aquí nos encontramos con múltiples elementos relevantes, los cuales se describen en 4.6.2. En la figura 5.4 se muestran en azul las instrucciones básicas para el uso de la aplicación. Además, en verde se muestran los botones de zoom in y zoom out. En amarillo se muestran los botones de ejecutar y guardar. Por último, nos encontramos con un lienzo con nodos. La funcionalidad del lienzo se irá detallando en las siguientes secciones. 40 Capítulo 5. Puesta en Ejecución y Ejemplos de Uso Figura 5.4 Pantalla de red. 5.3.4 Configuración de Maestros Todos los nodos tienen una configuración común, la cual se compone de un nombre, comentario, rol, ip y mac, tal y como se vio en 4.6.2. Los nodos se guardan su configuración automáticamente al cerrar la pestaña. Los nodos vienen preconfigurados con un nombre y una ip, pero se pueden modificar a través de la interfaz gráfica. En este caso vamos a modificar los maestros para que uno se llame "maestro_insistente" y otro "maestro_relajado". Los nombres no afectan a la emulación y se pueden repetir. Además, para hacer más interesante la configuración de los maestros vamos a cambiarles la dirección IP para que el maestro insistente esté en 192.168.45.10 y el relajado en 192.168.45.11. Para ilustrar las virtudes del proyecto, se cambiará la MAC del maestro insistente a e4:a8:df:d1:11:0a y la del maestro relajado a e4:a8:df:d1:11:0b. Figura 5.5 Configuración de los maestros. 5.3.5 Configuración de Esclavos Los esclavos tienen más campos de configuración que los maestros porque estamos ante un nodo más complejo. En este ejemplo de uso nombramos a los esclavos esclavo1 yesclavo2 y cambiamos sus respectivas IPs a 192.168.45.100 y192.168.45.101, así como sus respectivas MACs a e4:a8:df:d1:11:1a ye4:a8:df:d1:11:1b. Yendo a la configuración específica del protocolo, vamos a dejar el puerto 502 para facilitar la decodificación del protocolo de Wireshark. En cuanto al ID de esclavo, vamos a asignarle a cada esclavo su ID correspondiente, 1y2. 5.3 Ejemplos de uso 41 Registros Como ya se vio en 2.1, hay cuatro tipo de registros. Podemos definir los registros utilizados de dos formas distintas: dispersa o secuencial. Dado que el modo de uso más habitual es el sequencial, vamos a usar este. Para esta demostración vamos a usar Holding Registers yCoils. Ambos esclavos van a definir algunos registros de ambos tipos. En cuanto al esclavo1, para los Holding Registers vamos a definir los primeros tres registros (0-2) con los valores 0, 1, 2. Para los Coils vamos a definir los primeros dos registros (0-1) con los valores 0, 1. En cuanto al esclavo2, para los Holding Registers vamos a definir únicamente el primer registro (0) con el valor 3. Para los Coils vamos a definir los primeros tres registros (0-2) con los valores 1, 0, 1. De este modo las configuraciones quedan como se muestra en las figuras 5.6 y 5.7. Figura 5.6 Configuración de esclavo1. Figura 5.7 Configuración de esclavo2. Identidad Como ya se vio en 4.6.2, el último apartado de configuración de los esclavos es la identidad. En este apartado se configura el modelo, la versión y el fabricante del esclavo. Para hacer una demostración completa de todas las capacidades del software, se le va a configurar al esclavo2 la identidad. Se le configurará con los siguientes datos: •Nombre del Vendedor: TechnoFantasy Inc. •Código de Producto: TF98765 •Revisión: 2.5 48 Capítulo 6. Conclusiones Modelo a lo largo de esta memoria. En segundo lugar, ha quedado manifiesto en la demostración que la interfaz es intuitiva y abstrae al usuario de la tecnología subyacente. Además, gracias a la fase de diseño se han podido identificar necesidades funcionales a tiempo, permitiendo incluirlas con facilidad adaptando la arquitectura. Por otro lado, se ha podido comprobar que la arquitectura propuesta es escalable y flexible, permitiendo así la inclusión de nuevas funcionalidades sin necesidad de rehacer los módulos. A lo largo del proyecto se han codificado 3037 líneas de código en ficheros .py y .js. Esta suma no incluye las librerías usadas ni archivos de configuración, como los diversos archivos YAML, JSON, CSV o los archivos de configuración de Docker. Por último, se han creado imágenes de Docker para este escenario, reduciendo su peso en disco y en memoria con respecto a las disponibles públicamente en Docker Hub. 7 Limitaciones y Líneas Futuras I believe we’ve reached the end of our journey. All that remains is to collapse the innumerable possibilities before us. Are you ready to learn what comes next? Solanum, Outer Wilds (Mobius Digital, 2019) El proyecto comenzó con una serie de objetivos que se han alcanzado con éxito, marcando un sólido punto de partida. Aunque algunas funcionalidades iniciales quedaron fuera del alcance debido al tiempo disponible, esto abre oportunidades de mejora interesantes. A continuación, se presentan algunas líneas futuras que podrían potenciar aún más el proyecto. 7.1 Incrementar cantidad de protocolos La primera limitación viene dada porque este trabajo solo soporta el protocolo Modbus TCP, lo que restringe su aplicabilidad a otros protocolos industriales como DNP3 o IEC 104. Aunque el código es personalizable, se puede mejorar la automatización de los ataques a Modbus. La conveniencia de ampliar el número de protocolos ya se ha contemplado en este trabajo durante la fase de diseño, lo que facilitará la futura implementación. La arquitectura se diseñó para que se pudiese implementar cualquier otro protocolo únicamente buscando la librería y haciendo cambios leves a la interfaz gráfica. Las librerías que se proponen de los protocolos mencionados son pyiec61850 ydnp3-python. 7.2 Retroalimentación durante la ejecución Uno de los principales motivos por los cuales se escogió la representación en grafo fue para poder representar el tráfico generado por la red en tiempo real. Actualmente no está implementado, pero estuvo presente la posibilidad de añadirlo hasta las últimas fases del proyecto. Este punto fue descartado por falta de tiempo. La librería utilizada para el frontend permite la congelación de los nodos, evitando la interacción con el usuario. En este estado, las peticiones que se realizan al backend para actualizar sobre el estado de la emulación se pueden hacer con mayor frecuencia y que lleven información de los mensajes mandados por cada nodo. Desde el core, se tendría que añadir un módulo que recogiese los mensajes mandados por cada nodo. Una de las formas más sencillas sería inspeccionando el socket de Docker, aunque no se ha estudiado con la suficiente profundidad como para establecerla como la mejor solución. 7.3 Velocidad de enlace no infinita Otra limitación es que el enlace de datos no tiene condiciones realistas (p.ej., retardo constante o cero), lo que puede afectar a la representatividad de los escenarios simulados frente a redes reales con latencias y 49 50 Capítulo 7. Limitaciones y Líneas Futuras pérdidas de paquetes. Una posible línea de trabajo es añadir una opción para limitar la velocidad de enlace entre los nodos. 7.4 Dependencia de Docker Desde el punto de vista de la arquitectura, el sistema depende de Docker para la virtualización de los dispositivos, lo que puede generar sobrecarga en sistemas con recursos limitados. Esto podría llegar a ser una limitación pero hay que tener en cuenta que en el trabajo ya se ha intentado paliar esta limitación reduciendo el tamaño de las imágenes, aunque como futura línea podría profundizarse más en esta línea. Índice de Figuras 2.1 Campos de la cabecera del protocolo Modbus 3 2.2 Arquitectura de Docker 6 2.3 Aplicación del patrón Modelo-Vista-Controlador 7 4.1 Diagrama de componentes: Backend y Core 16 4.2 Estructura de directorios de /tmp/ICSCommEmulator 21 4.3 Diagrama de Secuencia para Crear un Escenario 29 4.4 Diagrama de Secuencia para Cargar un Escenario 29 4.5 Diagrama de secuencia para la creación de un escenario 33 4.6 Diagrama de secuencia para la carga de un escenario 33 4.7 Diagrama de secuencia para el guardado de un escenario 34 4.8 Diagrama de secuencia para la ejecución de un escenario 34 4.9 Diagrama de secuencia para la generación de archivos necesarios para la emulación 35 4.10 Diagrama de secuencia para la detención de la emulación 35 5.1 Ejecución exitosa de los tests unitarios 38 5.2 Formulario de creación de escenario 39 5.3 Formulario de carga de escenario 39 5.4 Pantalla de red 40 5.5 Configuración de los maestros 40 5.6 Configuración de esclavo1 41 5.7 Configuración de esclavo2 41 5.8 Configuración de mensajes del maestro insistente al esclavo1 42 5.9 Configuración de mensajes del maestro insistente al esclavo2 42 5.10 Configuración de mensajes del maestro relajado al esclavo2 42 5.11 Mensaje de éxito al guardar el escenario 43 5.12 Parámetros de ejecución de la emulación 43 5.13 Progreso de la emulación 44 5.14 Resultado de la emulación 44 5.15 Tráfico en Wireshark 45 51 Índice de Tablas 2.1 Códigos de función Modbus 4 2.2 Tipos de Registros en Modbus 4 2.3 Comandos comunes para la gestión de imágenes en Docker 5 2.4 Instrucciones comunes de Dockerfile 5 2.5 Equivalencia entre operaciones CRUD y métodos HTTP 8 3.1 Comparativa entre Software 12 4.1 Endpoints de la REST API 14 4.2 Librerías Usadas en el Proyecto 15 4.3 Herramientas Externas Usadas en el Proyecto 15 4.4 Validaciones realizadas para un escenario 26 4.5 Campos de creación de un escenario 28 4.6 Campos de configuración de un nodo 30 4.7 Campos de configuración específicos de un esclavo 30 4.8 Validaciones realizadas para los parámetros de comunicación 31 4.9 Campos requeridos para la creación de un escenario 32 5.1 Mensajes enviados esperados 44 53 Índice de Códigos 2.1 Ejemplo básico docker-compose.yml 6 4.1 Ejemplo de network 16 4.2 Ejemplo de nodo maestro 17 4.3 Ejemplo de nodo esclavo 17 4.4 Dockerfile del maestro 18 4.5 Dockerfile del esclavo 19 4.6 Ejemplo de master.csv 19 4.7 Ejemplo de slave.yaml 19 4.8 Ejemplo de uso de docker-compose-generator.py 21 4.9 Ejemplo de uso de scenario-config-generator.py 22 4.10 Ejemplo de configuración YAML 26 55 Bibliografía [1] I. N. Fovino, A. Carcano, M. Masera, and A. Trom-Betta, “Design and implementation of a secure modbus protocol,” IFIP Advances in Information and Communication Technology, 2009, cited by: 111; All Open Access, Bronze Open Access. [Online]. Available: https://www.scopus.com/inward/record.uri?eid=2-s2.0-84891808534&doi=10.1007%2f978-3642-04798-5_6&partnerID=40&md5=ec190d7dad22b95152062417b0d9a9a7 [2] INCIBE. (2020) Evolucionando a modbus seguro. [Online]. Available: https://www.incibe.es/incibecert/blog/evolucionando-modbus-seguro [3] Modbus Organization, Inc., MODBUS Application Protocol Specification V1.1b3, Apr. 2012. [Online]. Available: https://www.modbus.org/docs/Modbus_Application_Protocol_V1_1b3.pdf [4] S. Electric. (2020) Protocolo modbus para interruptores masterpact nt/nw. Schneider Electric. [Online]. Available: https://product-help.schneider-electric.com/ED/ES_Power/NT-NW_Modbus_IEC_ Guide/EDMS/DOCA0054EN/DOCA0054xx/Master_NS_Modbus_Protocol/Master_NS_Modbus_ Protocol-2.htm [5] Wikipedia. (2009) Model-view-controller. [Online]. Available: https://en.wikipedia.org/wiki/Modelview-controller [6] S. O. Blog, “Best practices for rest api design,” 2020. [Online]. Available: https://stackoverflow.blog/ 2020/03/02/best-practices-for-rest-api-design/ [7] M. Masse, REST API Design Rulebook. Sebastopol, CA: O’Reilly Media, 2011. [8] I. McGregor, “The relationship between simulation and emulation,” in Proceedings of the Winter Simulation Conference, vol. 2, 2002, pp. 1683–1688 vol.2. [9] Wikipedia. (2024) Packet generators. [Online]. Available: https://en.wikipedia.org/wiki/Packet_ generator [10] D. Antonioli and N. O. Tippenhauer, “Minicps: A toolkit for security research on CPS networks,” CoRR, vol. abs/1507.04860, 2015. [Online]. Available: http://arxiv.org/abs/1507.04860 [11] C. Queiroz, A. Mahmood, and Z. Tari, “Scadasim—a framework for building scada simulations,” IEEE Transactions on Smart Grid, vol. 2, no. 4, pp. 589–597, 2011. [12] A. Dehlaghi-Ghadim, A. Balador, M. H. Moghadam, H. Hansson, and M. Conti, “Icssim — a framework for building industrial control systems security testbeds,” Computers in Industry, vol. 148, p. 103906, 2023. [Online]. Available: https://www.sciencedirect.com/science/article/pii/ S0166361523000568 [13] A. Candia and C. Cappo, “Tinyics: An industrial control system simulator based on ns-3,” in 2024 43rd International Conference of the Chilean Computer Science Society (SCCC), 2024, pp. 1–8. [14] D. D. Team. (2016) Docker networking design philosophy. [Online]. Available: https://www.docker. com/blog/docker-networking-design-philosophy/ [15] IONOS. (2021) Multicast dns: qué es y cómo funciona. [Online]. Available: https://www.ionos.es/ digitalguide/servidores/know-how/multicast-dns/ [16] Wikipedia. (2010) Simple service discovery protocol (ssdp). [Online]. Available: https://es.wikipedia. org/wiki/SSDP 57