Containernet
Containernet
de virtualización ligera
para la evaluación de
redes de comunicaciones
16 de marzo de 2022
Índice general
Índice de figuras . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
Listado de ejemplos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
Glosario de términos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
Agradecimientos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
1 Introducción 9
1.1 Contexto del trabajo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
1.2 Objetivos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
1.3 Resumen capı́tulos de la memoria . . . . . . . . . . . . . . . . . . . . . . . . . . 10
2
TFE: Enrique Fernández Sánchez ÍNDICE GENERAL
8 Bibliografı́a 100
Enlaces y referencias . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100
Imagenes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102
3
TFE: Enrique Fernández Sánchez
Índice de figuras
4
TFE: Enrique Fernández Sánchez ÍNDICE DE FIGURAS
5
TFE: Enrique Fernández Sánchez
Listado de ejemplos
6
TFE: Enrique Fernández Sánchez
Glosario de términos
Linux. Sistema operativo tipo UNIX, de código abierto, multiplataforma, multiusuario y mul-
titarea.
PID (Process Identifier ). Identificador de procesos que están ejecutándose bajo un sistema
tipo Linux.
UID (User identifier ). Encontrado normalmente como un número o palabra, supone un iden-
tificador de usuario dentro del sistema Linux.
GID (Group Identifier ). Al igual que sucede con el UID, suele aparecer como un número o
palabra y se refiere al identificador de grupo dentro del sistema de Linux.
DHCP (Dynamic Host Configuration Protocol ). Protocolo de red que permite a un servidor
asignar dinámicamente direcciones IP y otros parámetros, a cada dispositivo de la red en la
que se encuentre.
7
TFE: Enrique Fernández Sánchez
Agradecimientos
En primer lugar, agradecer a mi tutor, José Marı́a Malgosa Sanahuja, por presentarme este
proyecto y animarme en todo momento para su realización. Agradezco mucho su tiempo, su
dedicación y su ayuda, que han sido una parte clave para la realización de este trabajo.
Por otro lado, me gustarı́a agradecer a mi familia, en especial a mis padres, Encarnación y
Miguel Ángel, y a mi hermano y a mi cuñada, Miguel y Yolanda; ellos han supuesto un pi-
lar fundamental en este camino que ha sido para mi la ingenierı́a, ellos han sido mi principal
apoyo, y sin él, no hubiera llegado hasta donde estoy hoy. También agradecer a mis padrinos,
Ana y Anastasio, ya que siempre han tenido palabras de ánimo para mi, incluso en este último
año, en el que han sabido transmitir esa fortaleza, tan propia de ellos. Especial mención a mis
abuelos, estoy seguro que estarı́an muy orgullosos de mi camino y de cómo su nieto es ahora
ingeniero. Agradecer también a mis suegros, Marı́a Dolores y Francisco, ya que suponen otra
fuente importante de apoyo y confianza.
Especial mención a Lucı́a Francoso Fernández, compañera del grado en sistemas de telecomu-
nicación, con la cual he tenido la suerte de emprender codo con codo muchos de los proyectos
que he llevado entre manos estos años. Fuente de apoyo incondicional, una mina de creatividad
y una brillante ingeniera. Sin ella, mi paso por la universidad no hubiera sido el mismo.
Por último, agradecer a mis compañeros del Free Open Source Club ([Link]
ya que gracias a ellos he puesto en valor el movimiento Open Source. Me han servido de tram-
polı́n para entrar a un mundo muy bonito que es el conocimiento libre y poder vivir experiencias
únicas. Con ellos, he pasado muchas horas de trastear y aprender sobre cualquier cosa que se
nos ocurrı́a, lo que nosotros amistosamente llamamos “hackear ”.
8
TFE: Enrique Fernández Sánchez
Capı́tulo 1
Introducción
Con el fin de concluir los estudios de grado en ingenierı́a telemática, es necesaria la investiga-
ción y el posterior desarrollo del Trabajo Fin de Estudios (TFE). Dicho trabajo, tiene como
objetivo enfrentar al alumno a un proceso de investigación en el que pueda aplicar muchos de
los conceptos que ha ido aprendiendo durante su paso por el grado, pudiendo añadir puntos de
innovación, y aportar soluciones nuevas a un proyecto especı́fico.
En este documento recojo lo que serı́a mi memoria en relación al TFE. En él se detallarán las
diferentes investigaciones realizadas sobre el concepto de virtualización en sistemas Linux, el
funcionamiento de los contenedores y la utilización de la virtualización ligera para la evaluación
de redes de comunicaciones, en nuestro caso, de conmutación de paquetes.
Por otra parte, muchas de las herramientas de evaluación y simulación de redes de comunica-
ciones son costosas en términos de medios a utilizar (tanto hardware como software), lo que
limita seriamente su escalabilidad a la hora de evaluar redes de cierta envergadura. La virtua-
lización de redes (en especial la virtualización ligera) nos permitirá desarrollar simuladores de
redes más complejas sin consumir excesivos recursos computacionales. Por ello, en este proyecto
particularizaremos estas tecnologı́as y las acercamos al campo de conocimiento de la telemática
para utilizarlas con el fin de evaluar y simular redes de conmutación de tipo IP.
9
TFE: Enrique Fernández Sánchez 1.2. OBJETIVOS
1.2 Objetivos
Como bien hemos adelantado en el apartado anterior, el objetivo principal de este proyecto es
el de estudiar propuestas en el ámbito de simulación y evaluación de redes de comunicación,
basadas en virtualización ligera. Por lo tanto, se pretende:
• Estudiar, dentro del sistema operativo Linux, las diferentes tecnologı́as que nos permiten
adoptar soluciones NFV.
• Definir los espacios de nombres (namespaces) y cómo podemos aplicarlo para virtualizar
redes.
• Aportar una serie de ejemplos que puedan servir de guı́a para la evaluación de redes de
comunicaciones, utilizando virtualización ligera.
10
TFE: Enrique Fernández Sánchez 1.3. RESUMEN CAPÍTULOS DE LA MEMORIA
11
TFE: Enrique Fernández Sánchez
Capı́tulo 2
• Servicios que requieren una instalación manual o que necesitan una intervención manual.
• Problemas relacionados con el negocio de las operadoras de red, como puede ser la reduc-
ción de costes del servicio.
Para solucionar todos estos problemas, desde los grupos de trabajo de la ITU se empezó a
trabajar en nuevas propuestas con el fin de aportar nuevas alternativas. La solución propuesta
con más apoyo serı́a “la virtualización de funciones de red”, que tendrı́a su punto de partida en
octubre de 2012, en un grupo de trabajo de la ITU, formado por 13 operadoras internacionales,
dando lugar a un paper informativo [1] en el que se detallaba de forma teórica la solución de
NFV.
12
TFE: Enrique Fernández Sánchez
Figura 2.1: Comparativa enfoque clásico de las redes contra el enfoque virtualizado.
En el enfoque clásico, las funciones de red están basadas en tener un software y hardware es-
pecı́fico para cada dispositivo. Sin embargo, NFV nos permite desplegar esos mismos recursos
software y hardware en servidores fı́sicos de propósito general, dando lugar a una mejor gestión
de los recursos y un mayor aprovechamiento. Por lo tanto, un mismo nodo fı́sico, puede ser
DHCP, router o Firewall. [2]
La virtualización de funciones de red supone una oportunidad para reducir costes y acelerar el
desarrollo de servicios para los operadores de red.
Tal y como podemos ver en la comparativa de la Figura 2.1, sustituimos electrónica de red
especı́fica, como podrı́an ser router, switches, etc; por máquinas virtualizadas que se despliegan
en servidores de carácter general, dando lugar a un mayor control y escalabilidad de los siste-
mas fı́sicos. A consecuencia de esto, podemos ver como las redes toman un camino diferente,
dejando a atrás el hardware y software propietario, para centrarse en un enfoque basado en el
software.
13
TFE: Enrique Fernández Sánchez 2.1. TECNOLOGÍAS IMPLICADAS
A modo de resumen, Software Defined Networks (SDN) desacopla el plano de control y el plano
de datos de los routers y switches de una red. El controlador SDN es el encargado de programar
los dispositivos de la red vı́a software. En entornos SDN, el switch/router solo sabe encaminar
paquetes de datos de la forma en la que el controlador SDN lo ha programado. Hasta el mo-
mento no hay nada nuevo, ya que esto mismo se consigue -por ejemplo- mediante el protocolo
SNMP. La novedad que introduce SDN consiste en que la programación de los routers/switches
es dinámica, permitiendo que se vaya adaptando automáticamente a las condiciones de la red.
Para programar bajo el paradigma de SDN, se utilizan APIs llamadas respectivamente South-
bound y Northbound. La primera es una API de muy bajo nivel que permite comunicar el
controlador con el equipo de red. La segunda es de alto nivel y permite que el administrador
de la red pueda programar los equipos de forma fácil y amigable.
14
TFE: Enrique Fernández Sánchez 2.2. VIRTUALIZACIÓN LIGERA
Por lo tanto, las ventajas destacables sobre la virtualización ligera frente a la virtualización
“dura” serı́an:
A consecuencia de tener que utilizar un mismo kernel para todas las instancias, tenemos que
profundizar sobre los conceptos de interfaces de red virtuales [3] y espacios de nombres [4], ya
que serán piezas clave para tener nuestras instancias aisladas entre sı́, pero a su vez conectadas
con los diferentes recursos en red que nosotros definamos.
15
TFE: Enrique Fernández Sánchez
Capı́tulo 3
Linux dispone de una selección muy diferente de interfaces de red que nos permiten, de manera
sencilla, el uso de máquinas virtuales o contenedores. En este apartado vamos a mencionar las
interfaces más relevantes de cara a la virtualización ligera que proponemos para el despliegue
de una red virtualizada. Para obtener una lista completa de las interfaces disponibles, podemos
ejecutar el comando ip link help.
• TUN/TAP
Además, udev también se encarga de gestionar los nodos de dispositivos ubicados en el di-
rectorio /dev añadiéndolos, encadenándolos simbólicamente y renombrándolos. Por otro lado,
una consideración a tener en cuenta es que udev maneja los eventos de manera concurrente, lo
que aporta una mejora de rendimiento, pero a la vez puede dar problemas a la hora de que el
16
TFE: Enrique Fernández Sánchez 3.1. NOMBRADO PREDECIBLE DE DISPOSITIVOS
orden de carga de los módulos del kernel no se conserva entre los arranques. Un ejemplo de esto
podrı́a ser que en el caso de tener dos discos duros (uno llamado /dev/sda y otro /dev/sdb),
en el siguiente arranque el orden de de arranque puede variar, generando que ambos identifi-
cadores se intercambien entre sı́, desencadenando una serie de problemas en nuestro sistema. [5]
A modo de ejemplo, el usuario puede crear sus propias reglas, de modo que puede realizar las
acciones que ya hemos comentado, de acuerdo a sus propias necesidades. A modo de ejemplo,
podemos ver en la siguiente captura (Figura 3.1) como hemos creado un archivo en la ruta
/etc/udev/rules.d/ en el que definimos la regla para un dispositivo fı́sico en especı́fico
(en este caso un USB). Identificamos el dispositivo que queremos a través de los atributos
idVendor e idProduct (atributos necesarios para cualquier dispositivo USB), después le
asignamos un ”MODE”que corresponde con los permisos que le queremos asignar al dispositivo
(en modo numérico) y el grupo al que permitimos acceder al dispositivo.
En el caso de las interfaces fı́sicas de red, vamos a suponer que estamos utilizando el etiquetado
de interfaces de red tradicional, tales como eth0, eth1, etc. En la últimas versiones del kernel,
se ha cambiado la forma en la que las interfaces de red son nombradas por Linux (systemd
networkd v197 [6]). Es por este motivo por lo que antes podı́amos tener interfaces del tipo
eth0 y ahora nos encontramos con la siguiente nomenclatura enps30. Este cambio surge ya
que anteriormente se nombraban las diferentes interfaces durante el boot del sistema, por lo
que podrı́a pasar que lo que en un primer momento se nombró como eth1, en el próximo
arranque fuera eth0, dando lugar a incontables errores en configuración del sistema. Es por
esto por lo que se empezó a trabajar en soluciones alternativas. Por ejemplo, la que acabamos de
comentar (udev), utiliza la información aportada por la BIOS del dispositivo para catalogarlo
en diferentes categorı́as, con su formato de nombre para cada categorı́as. Dichas clasificación
corresponde con las siguientes:
3. Nombres que incorporan una localización fı́sica de un conector hardware. (ejemplo: enp2s0)
5. Sistema clásico e impredecible, asignación de nombres nativa del kernel. (ejemplo: eth0)
17
TFE: Enrique Fernández Sánchez 3.2. MAC COMPARTIDA: ENP2S0:{0,1,2...}
Sin embargo, el comando iproute2 admite esta misma funcionalidad sin tener que crear
interfaces de red extra. Para ello, solo tenemos que asociar cada IP con la interfaz de red
deseada.
18
TFE: Enrique Fernández Sánchez 3.4. VLAN 802.1AD: ENP2S0.{0,1,2...}.{0,1,2...}
Para configurar un enlace utilizando este estándar, tenemos que ejecutar los siguientes comandos
[9]:
$ ip link add link eth0 eth0.1000 type vlan proto 802.1ad id 1000
$ ip link add link eth0.1000 eth0.1000.1000 type vlan proto
802.1q id 1000
De esta manera, lo que hacemos es asociar una primera VLAN a una interfaz de red, utilizando
802.1ad, después a esa misma interfaz con identificador, podemos asignar otra nueva VLAN,
pero esta vez utilizando el estándar 802.1q. Por lo tanto, al final nos quedarı́a una interfaz
similar a eth0.1000.1000 en la que podemos distinguir dos identificadores de red virtual.
19
TFE: Enrique Fernández Sánchez 3.5. PARES ETHERNET VIRTUALES
Los paquetes transmitidos por un extremo del ethernet virtual se reciben inmediatamente en
el otro extremo. Si alguno de ellos se encuentra apagado, decimos que el link de la pareja está
también apagado. A modo de ejemplo, nos fijamos en una estructura básica en la que dos apli-
caciones se comunican utilizando veth, tal y como vemos en la figura (3.4)
Las interfaces virtuales ethernet se inventaron con el fin de comunicar diferentes network na-
mespaces. Aunque profundizaremos en ello más adelante, los namespace de Linux permiten
encapsular recursos globales del sistema de forma aislada, evitando que puedan interferir con
procesos que estén fuera del namespace.
La configuración necesaria para implementar el ejemplo de la figura 3.4 serı́a el siguiente [10]:
De esta manera, tendrı́amos creados los namespaces app1 y app2, que estarı́an interconec-
tados entre sı́. Ahora procedemos a asignar una IP a cada interfaz.
20
TFE: Enrique Fernández Sánchez 3.5. PARES ETHERNET VIRTUALES
Para comprobar que hay conectividad entre las diferentes aplicaciones (app1 y app2) utiliza-
mos la función del comando ip para ejecutar programas dentro de un network namespace,
en este caso realizar un ping entre ambas aplicaciones:
Por otro lado, si quisiéramos una topologı́a más compleja, como por ejemplo que varios namespaces
puedan hacer uso de una interfaz fı́sica, tendrı́amos que añadir un elemento extra a nuestro
sistema. El diagrama de la topologı́a podrı́a ser tal que ası́:
Figura 3.5: Ejemplo avanzado de utilización de pares virtuales ethernet, utilizando bridge
Como podemos comprobar en la figura 3.5, es necesario que utilicemos un “bridge”para que
podamos conectar ambas interfaces virtuales a una interfaz fı́sica, para replicar dicha topologı́a,
ejecutaremos los siguientes comandos:
Para definir un bridge entre las diferentes interfaces virtuales que hemos creado, utilizaremos
una interfaz tipo bridge de Linux, o bien podemos configurar dicho bridge usando Open vSwitch.
Open vSwitch es un programa de código abierto diseñado para ser utilizado como un switch
multi-capa virtual [11][12].
$ ip link add veth1_br type veth peer name veth0 netns app1
$ ip link add veth2_br type veth peer name veth0 netns app2
$ ovs-vsctl add-br ovsbr0
$ ovs-vsctl add-port ovsbr0 veth1_br
$ ovs-vsctl add-port ovsbr0 veth2_br
$ ovs-vsctl add-port ovsbr0 eth0
21
TFE: Enrique Fernández Sánchez 3.6. TUN/TAP
De esta manera, ya tendrı́amos la topologı́a configurada con conectividad entre las diferen-
tes aplicaciones, además de cada aplicación con una interfaz fı́sica de la máquina, todo esto
utilizando una interfaz tipo bridge.
3.6 TUN/TAP
TUN/TAP son dos interfaces de red virtuales de Linux, que permiten dar conectividad entre
programas dentro del espacio de usuario, es decir permiten conectar aplicaciones a través de un
socket de red. Esta interfaz es expuesta al usuario mediante la ruta /dev/net/tun. Como bien
hemos mencionado, existen dos tipos de interfaces virtuales controladas por /dev/net/tun:
• TAP. Interfaces encargadas de transportar paquetes Ethernet (trabaja sobre la capa 2).
TUN (Capa 3)
Las interfaces TUN (IFF TUN) transportan paquetes PDU (Protocol Data Units) de la capa 3
[13]:
• No hay capa 2 en esta interfaz, por lo que los mecanismos que se ejecutan en esta capa
no estarán presentes en la comunicación. Por ejemplo, no tenemos ARP.
22
TFE: Enrique Fernández Sánchez 3.6. TUN/TAP
TAP (Capa 2)
• En la práctica, transporta frames Ethernet, por lo tanto, actuarı́a como si fuera un adap-
tador virtual de Ethernet (“bridge virtual”).
Si nos fijamos en la figura 3.6, podemos ver las diferencias entre ambas interfaces. Aún ası́, hay
una que se utiliza mucho más que la otra, esta interfaz es TAP. Las interfaces tipo TAP son
ampliamente utilizadas ya que nos permiten añadir a nuestro sistema tantas tarjetas ethernet
virtuales (software) como se necesiten. Posteriormente -utilizando los comandos Linux tradi-
cionales para configurar redes- se le asigna una IP y ya pueden ser utilizadas por cualquier
aplicación -incluso de forma compartida- como si fuera una interfaz de red hardware.
23
TFE: Enrique Fernández Sánchez 3.6. TUN/TAP
En el caso de que queramos crear interfaces TUN/TAP fuera de una aplicación, es decir, desde
la lı́nea de comandos, tendremos que utilizar los programas tunctl o ip. Para ello, podemos
revisar los ejemplos 3.1 y 3.2 donde se comentan algunos de los comandos más importantes
para trabajar con estar interfaces en cada uno de los programas mencionados. [14]
24
TFE: Enrique Fernández Sánchez 3.6. TUN/TAP
Por otro lado, otra manera de trabajar con estas interfaces, es dejar que el programa que es-
temos utilizando cree dichas interfaces. Es por esto por lo que dentro del programa podemos
definir que se refiera a la ruta /dev/net/tun para crear la interfaz necesaria. A modo de
ejemplo, se implementa un programa en C que crea una interfaz TUN/TAP (en el programa
elegimos cual de las dos queremos) y que devuelve el tamaño de los paquetes que reciba en di-
cha interfaz. Este ejemplo nos sirve para comprobar con una aplicación puede crear y gestionar
una interfaz, y además, mediante un programa externo de captura de paquetes (Wireshark,
tcpdump, etc...) ver los paquetes que llegan a la interfaz y su estructura.
El código utilizado serı́a el que vemos en [3.3]. Es importante comentar que en la linea 80
podemos modificar el tipo de interfaz que vamos a crear, usaremos IFF TUN para crear un
TUN y IFF TAP para crear un TAP. [15]
6 #include <net/if.h>
7 #include <sys/ioctl.h>
8 #include <sys/stat.h>
9 #include <fcntl.h>
10 #include <string.h>
11 #include <sys/types.h>
12 #include <linux/if_tun.h>
13 #include <stdlib.h>
14 #include <stdio.h>
15
16 int tun_alloc(int flags)
17 {
18
19 struct ifreq ifr;
20 int fd, err;
21 char *clonedev = "/dev/net/tun";
22
23 if ((fd = open(clonedev, O_RDWR)) < 0) {
24 return fd;
25 }
26
27 memset(&ifr, 0, sizeof(ifr));
28 ifr.ifr_flags = flags;
29
25
TFE: Enrique Fernández Sánchez 3.6. TUN/TAP
40 int main()
41 {
42
43 int tun_fd, nread;
44 char buffer[1500];
45
46 /* Flags: IFF_TUN - TUN device (no Ethernet headers)
47 * IFF_TAP - TAP device
48 * IFF_NO_PI - Do not provide packet information
49 */
50 tun_fd = tun_alloc(IFF_TAP | IFF_NO_PI);
51
52 if (tun_fd < 0) {
53 perror("Allocating interface");
54 exit(1);
55 }
56
57 while (1) {
58 nread = read(tun_fd, buffer, sizeof(buffer));
59 if (nread < 0) {
60 perror("Reading from interface");
61 close(tun_fd);
62 exit(1);
63 }
64
65 printf("Read %d bytes from tun/tap device\n", nread);
66 }
67 return 0;
68 }
69
9 ### Terminal 2
10 # Asignamos una IP a la interfaz recien creada
11 ip addr add [Link]/24 dev tun0
12 ip link set dev tun0 up
13
14 # Capturamos el trafico que viaja por la interfaz
15 tcpdump -i tun0
16
17 ### Terminal 3
18 # Mandamos trafico a la interfaz creada, un ping por ejemplo:
19 ping -c 4 [Link] -I tun0
20
26
TFE: Enrique Fernández Sánchez 3.6. TUN/TAP
En este ejemplo, queremos comprobar el funcionamiento de una interfaz tap, pero poniéndonos
en el caso de que el usuario cree dicha interfaz mediante el comando ip y un programa externo
se asocie a dicha interfaz. Para ello, vamos a utilizar la aplicación sock [16], que nos servirá
para crear un socket IP en un puerto especı́fico.
Ejemplo 3.5: Compilar e instalar programa sock
1 mkdir ˜/tmp
2 cd tmp
3 tar zxvf [Link]
4 cd sock-0.3.2
5 ./configure
6 make
7 sudo make install
8
Una vez tenemos el programa instalado, podemos proceder a crear la interfaz tap.
Ejemplo 3.6: Creación interfaz TAP
1 ip tuntap add dev tap0 mode tap
2 ip address add [Link]/24 dev tap0
3 ip link set tap0 up
4 ip route add [Link] dev tap0
5
Una vez ya tenemos creada la interfaz, podemos proceder a comprobar la funcionalidad utili-
zando el programa sock. Para ello, vamos a crear dos instancias cliente-servidor, cada una con
un puerto asociado diferente.
Ejemplo 3.7: Uso de aplicacion sock para crear cliente servidor asociado a un puerto
1 sock -s [Link] 1025 # Terminal 1 (servidor)
2 sock [Link] 1025 # Terminal 2 (cliente)
3
4 sock -s [Link] 1026 # Terminal 3 (servidor)
5 sock [Link] 1026 # Terminal 4 (cliente)
6
De esta manera, podemos escribir en cada una de las terminales, y comprobar que solo son
recibidas por el servidor, o cliente, asociado al puerto de la terminal en cuestión. Es decir, en
las interfaces tuntap tenemos aislamiento entre los diferentes puertos, por lo que funcionan
de manera equivalente a un enlace ethernet fı́sico. En la figura (3.7) podemos ver el ejemplo
realizado.
27
TFE: Enrique Fernández Sánchez 3.6. TUN/TAP
Por último, podemos comprobar la estructura de los paquetes enviados utilizando programas
como Wireshark, tcpdump, etc. Es importante comentar que aunque los paquetes deberı́an
viajar por la interfaz tap0, los kernels modernos los redirigen todos por loopback. En la
figura 3.9 podemos ver un ejemplo de captura de un paquete enviado por el tap0.
28
TFE: Enrique Fernández Sánchez
Capı́tulo 4
El objetivo de cada namespaces es adquirir una caracterı́stica global del sistema como una
abstracción que haga parecer a los procesos de dentro del namespace que tienen su propia
instancia aislada del recurso global.
29
TFE: Enrique Fernández Sánchez 4.3. ¿CÓMO CREAR/ACCEDER A UN NAMESPACE?
• sethostname()
• setdomainname()
• uname()
En una situación normal sin namespaces, se modificarı́a una variable global; sin embargo, si es-
tamos dentro de un namespace, los procesos asociados tienen su propia variable global asignada.
Un ejemplo muy básico de uso de este namespaces podrı́a ser el siguiente [19]:
• Tenemos que especificar la ruta del ejecutable que queremos aislar. Si no se especifica, se
asume bash como proceso por defecto.
30
TFE: Enrique Fernández Sánchez 4.4. UTS NAMESPACE
Si quisiéramos mantener “vivo” un namespace, serı́a necesario que lo asociemos con un archivo
del sistema, y después volver a “crear” el namespace con la herramienta nsenter apuntando
a dicho archivo. A modo de ejemplo, serı́a tal que ası́:
Ejemplo 4.2: Ejemplo de un persistencia namespace
1 touch /root/ns-uts # Creamos un archivo
2 unshare --uts=/root/ns-uts /bin/bash # Asociamos namespace UTS al archivo
3 hostname FooBar
4
5 # Salimos del namespace
6 exit
7
8
9 # Volvemos a entrar al namespace
10 nsenter --uts=/root/ns-uts /bin/bash
11 hostname # Nos devuelve ’FooBar’
12
Tal y como vemos en el ejemplo (4.2), utilizamos los comandos unshare y nsenter para
manipular un namespace de tipo UTS (hostname). Lo importante es comprobar que si asociamos
un namespace a un archivo, podemos recuperar dicho namespace si utilizamos el comando
nsenter. Además, si quisiéramos eliminar permanentemente dicho namespace, tendrı́amos
que hacer uso del comando umount.
31
TFE: Enrique Fernández Sánchez 4.5. MOUNT NAMESPACE
Una vez iniciamos el sistema, solo existe un único mount namespace, al que llamamos “na-
mespace inicial” (default). Los nuevos mount namespaces que creemos los haremos uti-
lizando la llamada al sistema clone(), para crear un nuevo proceso asociado en el nuevo
namespaces; o bien utilizando el comando unshare, para mover un proceso dentro del nuevo
namespace. Por defecto, cuando creamos un nuevo mount namespace, éste recibe una copia
del punto de montaje inicial, replicado por el namespace que lo ha llamado. [21]
Un concepto importante es que los mount namespaces tienen activado la funcionalidad del
kernel llamada shared subtree. Esto permite que cada punto de montaje que tengamos en
nuestra máquina pueda tener su propio tipo de propagación asociado. Esta información permite
que nuevos puntos de montaje en ciertas rutas se propaguen a otros puntos de montaje. A modo
de ejemplo, si conectamos un USB a nuestro sistema, y este se monta automáticamente en la
ruta /MI USB, el contenido solo estará visible en otros namespaces si la propagación está
configurada correctamente. [22]
Por lo tanto, un namespace de tipo mount nos permite modificar un sistema de archivos en con-
creto, sin que otros namespaces puedan ver y/o acceder a dicho sistema de archivos. Podrı́amos
concluir que el objetivo de este espacio de nombres es el de permitir que diferentes procesos
puedan tener “vistas” diferentes de los puntos del montaje de nuestro sistema.
Como vemos en el ejemplo, dentro del namespaces lo que hacemos es realizar un mount de
un disco duro, dentro de este encontramos un archivo cp. Sin embargo, una vez salimos del
namespace, podemos comprobar como no podemos acceder al montaje que realizamos dentro
del namespace. Por lo tanto, solo desde el namespace que realizó el montaje tendremos acceso
a la información del disco duro.
32
TFE: Enrique Fernández Sánchez 4.5. MOUNT NAMESPACE
$ mdkir /mnt/HDD1
$ mount /dev/sdb2 /mnt/HDD1
$ ls /mnt/HDD1
[Link] images/ storage_shared/
Podemos ver como dentro del sistema de archivos del disco duro, encontramos un archivo y
un par de carpetas. Si verificamos la lista de todos los dispositivos que tenemos montados,
utilizando el comando findmnt --real, obtenemos la siguiente salida:
Como podemos ver, en este momento hay cuatro dispositivos montados en nuestro sistema,
incluido el disco duro (el que montamos en /mnt/HDD1). Cada dispositivo tiene asociado un
directorio, por el cual podremos acceder a sus archivos. Procedemos a desmontar el disco duro:
$ umount /dev/HDD1
Sin embargo, también podemos realizar un montaje de un directorio sobre otro directorio,
para ello utilizaremos el comando mount --bind. Podemos entender el montaje bind co-
mo un alias, por ejemplo, si realizamos un montaje bind del directorio /tmp/dir1 so-
bre /tmp/disk, ambos tendrán el mismo contenido. Podremos acceder a los archivos de
/tmp/dir1 desde /tmp/disk, y viceversa.
Cualquier directorio puede ser utilizado en un montaje bind. Al igual que si el directorio origen
es un dispositivo, el dispositivo formará un montaje bind en la ruta de destino. Además, es
importante tener en cuenta que cuando utilizamos el parámetro bind, los puntos de montaje
que estaban dentro de la carpeta origen no son remontados, por lo que si queremos que el
directorio origen y todos sus montajes de dentro de ese directorio, se monten con el bind, ten-
dremos que utilizar el parámetro --rbind (recursive bind ). Por último, recalcar que después de
realizar un montaje tipo bind, no tendremos acceso al contenido original del directorio destino.
33
TFE: Enrique Fernández Sánchez 4.5. MOUNT NAMESPACE
$ mkdir /tmp/dir1
$ touch /tmp/dir1/bind_example
$ touch /tmp/dir1/mount_example
$ mkdir /tmp/disk
$ mount --bind /tmp/dir1 /tmp/disk
$ ls -l /tmp/dir1
total 40K
-rw-r--r-- 1 rani rani 0 Mar 10 18:46 bind_example
-rw-r--r-- 1 rani rani 0 Mar 10 18:47 mount_example
$ ls -l /tmp/disk
total 40K
-rw-r--r-- 1 rani rani 0 Mar 10 18:46 bind_example
-rw-r--r-- 1 rani rani 0 Mar 10 18:47 mount_example
Ambos directorios contienen los mismos archivos, podemos comprobarlo creando un archivo en
uno de ellos:
Al igual que hicimos en el ejemplo anterior, podemos utilizar el comando findmnt --real
para verificar los montajes del sistema.
$ umount /tmp/disk
34
TFE: Enrique Fernández Sánchez 4.5. MOUNT NAMESPACE
Para acceder al archivo oculto, podemos utilizar un montaje tipo bind. Para ello, creamos una
carpeta /tmp/storage y realizamos el montaje:
$ mkdir /tmp/storage
$ mount --bind /mnt /tmp/storage
$ ls /tmp/storage/storage
$ cat /tmp/storage/hidden_file
test_hidden
Como podemos comprobar, se ha utilizado el directorio /mnt ya que son los submounts los
que no se montan usando el bind. Si hubiéramos realizado el montaje bind sobre el directorio
/mnt/storage, no hubiéramos podido acceder al archivo oculto. Para terminar, desmontamos
los respectivos puntos de montaje:
Otro ejemplo de uso de los montajes tipo bind es el de montar directorios dentro de un entorno
chroot (un programa que permite un aislamiento limitado de procesos). Utilizando chroot,
podemos elegir el directorio raı́z en donde ejecutaremos nuestro programa. Por ejemplo, pode-
mos utilizar chroot para ejecutar httpd en el directorio raı́z /home/apache. Esto lo que
hará es convertir /home/apache en el directorio raı́z de ese programa, es decir, directorio /. Si
quisiéramos acceder al directorio /home/apache/www serı́a equivalente a /www. Esto permite
35
TFE: Enrique Fernández Sánchez 4.5. MOUNT NAMESPACE
a que el proceso httpd no pueda acceder a ningún archivo fuera del directorio /home/apache.
Sin embargo, si el proceso necesita acceder a archivos de fuera del chroot, por ejemplo, ciertas
librerı́as del sistema, podemos utilizar un montaje tipo bind para hacer accesibles los directo-
rios necesarios dentro del directorio al que hemos hecho chroot.
Un montaje bind es utilizado para aplicar una modificación sobre los permisos, o opciones del
montaje, de una parte del tree en especı́fico. Esto lo podemos ver con un ejemplo: queremos
que ciertas carpetas de nuestra aplicación tengan permisos de solo lectura, para ello podemos
ejecutar los siguientes comandos:
$ mount --bind /mnt/MI_USB /home/user/MI_USB
$ mount -o remount,ro,bind /home/user/MI_USB
Por otro lado, otro uso del montaje tipo bind, es la posibilidad realizar un montaje sobre un
mismo directorio. Por ejemplo, lo realizamos sobre el directorio /mnt/MI USB:
$ mount --bind /mnt/MI_USB /mnt/MI_USB
Al ejecutar dicho comando, lo que estamos haciendo es convertir un directorio del tree en un
device o dispositivo de nuestro sistema, es decir, a partir de ahora (mientras esté montado)
la ruta /mnt/MI USB será tratado como un dispositivo montado sobre ese directorio. Esto
mismo nos aporta cierta ventajas, ya que podemos aplicar las opciones que hemos comentado
anteriormente, tales como modificar los permisos de escritura o lectura, o cambiar las opciones
de montaje.
36
TFE: Enrique Fernández Sánchez 4.5. MOUNT NAMESPACE
tmpfs
De cara al siguiente ejemplo, es interesante que repasemos la funcionalidad del kernel del Linux
llamada tmpfs. Dicha funcionalidad permite crear un sistema de archivos temporal dentro de
nuestro sistema. El sistema de archivo creado reside completamente en la memoria y/o “swap”
de nuestro sistema. Este tipo de montajes suele ser muy interesante cuando necesitamos agilizar
el acceso a ciertos archivos, ya que al estar en la memoria RAM, la lectura es mucho más rápida
que si lo comparamos con un disco duro e incluso un disco duro de estado sólido. Sin embargo,
el inconveniente que tiene es que el contenido de dicho sistema de archivos se elimina una vez
reiniciemos el sistema, ya que este está almacenado en la memoria RAM de nuestro dispositivo.
[25]
Este tipo de archivos es comúnmente utilizado en los siguientes directorios: /tmp, /var/lock,
/var/run, entre otros muchos. Desde el punto de vista de los namespaces, tiene la ventaja de
que -si el sistema de archivo no es muy grande- permite crear un punto de montaje de forma
fácil y rápida.
37
TFE: Enrique Fernández Sánchez 4.5. MOUNT NAMESPACE
shared subtrees
Supongamos que un proceso quiere clonar su propio namespace, pero quiere mantener el acceso
a un USB que previamente hemos montado en nuestro sistema. Para satisfacer esta situación,
podemos utilizar la funcionalidad shared subtrees, ya que nos permite la propagación au-
tomática y controlada de eventos de montaje y desmontaje entre diferentes namespaces [27]
shared subtrees nos aporta cuatro maneras diferentes de realizar un montaje. Dichas op-
ciones son las siguientes:
• shared mount. Un montaje de este tipo puede ser replicado en tantos puntos de mon-
taje como se quieran y todas las réplicas seguirán siendo exactamente iguales. Es decir,
una vez enlazados los directorios origen y destino a través de la opción bind, cualquier
montaje nuevo que se haga en el directorio origen se reflejará en el directorio destino (y
viceversa).
• slave mount. Un montaje de tipo esclavo funciona como un montaje compartido, me-
nos para los eventos asociados al montaje y desmontaje, que solo se propagarán hacia él.
Es decir, una vez enlazados los directorios origen y destino a través de la opción bind,
cualquier montaje nuevo que se haga en el directorio origen se reflejará en el directorio
destino pero no a la inversa.
• private mount. Un montaje de tipo privado no permite realizar ni recibir ningún tipo
de propagación.
Si quisiéramos asignar alguno de estos tipos de montajes a nuestro dispositivo, tendrı́amos que
hacer uso del comando mount con los siguientes parámetros de estado:
Por otro lado, si quisiéramos aplicar los cambios anteriores de manera recursiva para que se
cambie el tipo de montaje para todos los montajes por debajo de la jerarquı́a (recursivo) del
directorio que determinemos, tendrı́amos que utilizar los comandos siguientes: [28]
38
TFE: Enrique Fernández Sánchez 4.5. MOUNT NAMESPACE
2. Supongamos que un proceso quiere que su montaje sea invisible para otros procesos, pero
que a la vez pueda ver el resto de montajes del sistema. [27]
Ejemplo 4.6: Caso de uso de slave mount
1 # El administrador permite que todo el sistema sea un subtree de tipo
shared
2 $ mount --make-rshared /
3
4 # Un proceso puede clonar el sistema de archivos. En un momento,
5 # marca parte de sus archivos como subtree tipo slave
6 $ mount --make-rslave /mitreeprivado
7
8 # Por lo tanto, cualquier montaje realizado en /mitreeprivado no aparecera
en
9 # el resto de namespaces. Sin embargo, los montajes que se realicen en
10 # ese directorio pero desde el namespaces default si que se progaran
11 # al proceso asociado
12
3. Supongamos que queremos realizar un montaje en el que podamos “vincular” dos direc-
torios, de modo que todo lo que hagamos en un directorio se refleje en el otro. [27]
Ejemplo 4.7: Caso de uso de bind y shared subtree
1 # El administrador permite que un directorio pueda ser replicado utilizando
el tipo shared
2 $ mount --make-shared /mnt
3
4 # Asignamos un montaje tipo bind a dicho montaje, y lo dirigimos a otro
directorio
5 $ mount --bind /mnt /tmp
6
7 # Los montajes /mnt y /tmp se compartiran mutuamente. Cualquier montaje
8 # o desmontaje que realicemos dentro de esos directorios se veran
9 # reflejados en el resto de montajes
10
39
TFE: Enrique Fernández Sánchez 4.6. PROCESS ID NAMESPACE
Concretando, aı́sla el namespace de la ID del proceso asignado, dando lugar a que, por ejemplo,
otros namespaces puedan tener el mismo PID. Esto nos lleva a la situación de que un proceso
dentro de un PID namespace piense que tiene asignado el ID ”1”, mientras que en la realidad
(en la máquina host) tiene otro ID asignado. [30]
Si ejecutamos el ejemplo, lo que podemos comprobar es que el ID del proceso que está dentro
del namespaces (echo $$), no coincide con el proceso que podemos ver de la máquina host (ps
-ef | grep /bin/sh). Más concretamente, el primer proceso creado en un PID namespace
recibirá el pid número 1, además de un tratamiento especial ya que supone un init process
dentro de ese namespace [19].
40
TFE: Enrique Fernández Sánchez 4.7. NETWORK NAMESPACE
Para crear un namespace de tipo network, y que este sea persistente, utilizamos la tool ip (del
package iproute2).
Ejemplo 4.9: Creación de network namespace persistente
1 $ ip netns add ns1
2
Este comando creará un network namespace llamado ns1. Cuando se crea dicho namespace,
el comando ip realiza un montaje tipo bind en la ruta /var/run/netns, permitiendo que el
namespace sea persistente aún sin tener un proceso asociado.
Ejemplo 4.10: Comprobar network namespaces existentes
1 $ ls /var/run/netns
2 or
3 $ ip netns
4
Como ejemplo, podemos proceder a añadir una interfaz de loopback al namespace que previa-
mente hemos creado:
Ejemplo 4.11: Asignar interfaz loopback a un namespace
1 $ ip netns exec ns1 ip link dev lo up
2 $ ip netns exec ns1 ping [Link]
3 > PING [Link] ([Link]) 56(84) bytes of data.
4 > 64 bytes from [Link]: icmp_seq=1 ttl=64 time=0.115 ms
5
La primera lı́nea de este ejemplo, corresponde con la directiva que le dice al namespace que
”levante”la interfaz de loopback. La segunda lı́nea, vemos como el namespace ns1 ejecuta el
ping a la interfaz de loopback (el loopback de ese namespace).
Es importante mencionar, que aunque existen más comandos para gestionar las redes dentro de
Linux (como pueden ser ifconfig, route, etc), el comando ip es el considerado sucesor de todos
estos, y los anteriores mencionados, dejarán de formar parte de Linux en versiones posterio-
res. Un detalle a tener en cuenta con el comando ip, es que es necesario tener privilegios de
administrador para poder usarlo, por lo que deberemos ser root o utilizar sudo.
Por lo tanto, utilizando el comando ip, podemos recapitular que si utilizamos la siguiente
directiva, podemos ejecutar el comando que nosotros indiquemos, pero dentro del network
namespace que previamente hemos creado.
Ejemplo 4.12: Ejecutar cualquier programa con un network namespace
1 $ ip netns exec <network-namespace> <command>
2
41
TFE: Enrique Fernández Sánchez 4.7. NETWORK NAMESPACE
Una de las problemáticas que supone el uso de los network namespaces, es que solo podemos
asignar una interfaz real a un namespace. Suponiendo el caso en el que el usuario root tenga
asignada la interfaz eth0 (identificador de una interfaz de red fı́sica), significarı́a que solo los
programas en el namespace de root podrán acceder a dicha interfaz. En el caso de que eth0
sea la salida a Internet de nuestro sistema, eso conllevarı́a que no podrı́amos tener conexión a
Internet en nuestros namespaces. La solución para esto reside en los veth-pair.
Como ya hemos visto anteriormente, un veth-pair funciona como si fuera un cable fı́sico, es
decir, interconecta dos dispositivos, en este caso, interfaces virtuales. Consiste en dos interfaces
virtuales, una de ellas asignada al root namespace, y la otra asignada a otro network namespace
diferente. Si a esta arquitectura le añadimos una configuración de IP válida y activamos la
opción de hacer NAT en el eth0 del host, podemos dar conectividad de Internet al network
namespace que hayamos conectado.
42
TFE: Enrique Fernández Sánchez 4.7. NETWORK NAMESPACE
Siguiendo el ejemplo propuesto, llegamos hasta el punto en el que el tráfico saliente del names-
pace ns1, será redirigido a v-eth1. Sin embargo, esto no es suficiente para tener conexión a
Internet. Tenemos que configurar el NAT en el eth0.
Ejemplo 4.14: Configuración de NAT para dar Internet a un network namespace
1 # Share internet access between host and NS
2
3 # Enable IP-forwarding
4 $ echo 1 > /proc/sys/net/ipv4/ip_forward
5
6 # Flush forward rules, policy DROP by default
7 $ iptables -P FORWARD DROP
8 $ iptables -F FORWARD
9
10 # Flush nat rules.
11 $ iptables -t nat -F
12
Si todo lo hemos configurado correctamente, ahora podrı́amos realizar un ping hacia Internet,
y este nos deberı́a resultar satisfactorio.
Aún ası́, no resulta muy cómodo el utilizar ip netns exec seguido de la aplicación a utilizar.
Es por esto por lo que es común ejecutar dicho comando para asignar el network namespace a
una shell. Esto serı́a tal que ası́:
43
TFE: Enrique Fernández Sánchez 4.8. USER ID NAMESPACE
Si por ejemplo queremos ver el UID del usuario que estamos usando en este momento, podemos
ejecutar: echo $UID, el cual nos devolverá el número asociado a nuestro usuario.
Además de diferenciar entre los IDs de usuarios (UID), también se nos permite separar entre
IDs de grupos (GID). En Linux, un grupo sirve para agrupar usuarios de modo que un grupo
puede tener asociado un privilegio que le permite usar un recurso o programas.
Por lo tanto, el namespace de UID, lo que nos permite es tener un UID y GID diferente al del
host.
Ejemplo 4.15: Ejemplo de uso UID namespace
1 $ ls -l /proc/$$/ns # espacios de nombres originales
2 $ id
3 > uid=1000(user) gid=1000(user) groups=1000(user), ...
4 $ unshare -r -u bash # Crea un namespace de tipo usuario, programa bash
5 $ id
6 > uid=0(root) gid=0(root) groups=0(root),65534(nobody)
7 $ cat /proc/$$/uid_map
8 > 0 1000 1
9 $ cat /etc/shadow # No nos deja acceder
10 > cat: /etc/shadow: Permission denied
11 $ exit
12
Como vemos en el ejemplo, el UID de usuario difiere de la máquina host. Dentro del namespace,
tenemos UID 0, sin embargo, eso no significa que podamos acceder a los archivos con UID 0 de
la máquina host, ya que en verdad lo que hace el namespace es mapear el UID 1000 al 0. Por
ejemplo, si a la vez que creamos un user namespace, también creamos un mount namespace,
este mount namespace si que tendrı́a privilegios de root [19]
44
TFE: Enrique Fernández Sánchez 4.10. CONTROL GROUPS NAMESPACE
• Monitorización. Los lı́mites establecidos para los recursos son monitorizados y son re-
portados al usuario.
• Control. Podemos controlar el estado de los procesos asociados a un grupo con un solo
comando, pudiendo elegir entre “congelado”, “parado” o “reiniciado”.
La primera versión de cgroups aparece en el Kernel en 2007, siendo ésta la versión más
estandarizada por la mayorı́a de distribuciones. Sin embargo, en 2016 aparece cgroups v2
en el Kernel, aportando mejoras en la simplificación de los arboles de jerarquı́as de la ruta
/sys/fs/cgroup, además de nuevas interfaces, aportando las bases para contenedores que
utilizan el concepto de “rootless”.
En el caso de la versión v1, los cgroups se crean en el sistema de archivos virtual en la ruta
/sys/fs/cgroup. Para crear un nuevo grupo, en nuestro caso con el objetivo de limitar un
proceso en memoria, tendrı́amos que ejecutar lo siguiente:
$ mkdir /sys/fs/cgroup/memory/<NombreGrupo>
45
TFE: Enrique Fernández Sánchez 4.10. CONTROL GROUPS NAMESPACE
Sin embargo, ahora vamos a proceder a limitar ese mismo programa en memoria. Para ello,
vamos a añadir el PID asociado a la ejecución de dicho programa al siguiente archivo (ejecutando
el script ./test [Link] &, nos aparece el PID):
Figura 4.3: Salida tras ejecutar el programa de Python, una vez limitado
46
TFE: Enrique Fernández Sánchez 4.10. CONTROL GROUPS NAMESPACE
Como podemos comprobar en las imágenes [4.2] y [4.3], la salida del programa no coincide.
Esto es debido a que como el programa Python ha superado el lı́mite establecido para su gru-
po, cgroups cerrado bruscamente dicho programa, por lo tanto no nos aparece la memoria
consumida por el programa, solo se nos notifica que el proceso con PID 62482 ha pasado a
estado “killed ”.
Por otro lado, si quisiéramos limitar el uso de CPU de un programa, tendrı́amos que escribir en
los archivos [Link] quota us o [Link] period us, dentro de la rama de cpu. Estos
parámetros representan el tiempo de ejecución asociado a un proceso. En este ejemplo, vamos
a limitar el proceso a 1 ms (1000 us), esto serı́a tal que:
Esto significa que cada 100ms de tiempo, el proceso está limitado por cgroups a utilizar solo
1ms del tiempo de CPU, dando lugar a utilizar solo el 1 % de la CPU.
Para comprobar el funcionamiento, podemos utilizar el siguiente script en background que nos
pone un núcleo de la CPU al 100 %:
$ while : ; do : ; done &
Al ejecutar dicho comando, la tarea se quedará funcionando en background. Podemos compro-
barlo si utilizamos herramientas para ver el consumo de los procesos, como pueden ser top o
htop. Una vez tenemos el PID asociado a la tarea de prueba, podemos proceder a añadirlo a
los procesos que son limitados con el cgroup creado, en este caso únicamente para limitar la
CPU.
$ echo <PID> > /sys/fs/cgroup/cpu/<NombreGrupo>/tasks
Una vez ejecutado, si volvemos a ejecutar top o htop, podemos ver como el tiempo asignado
al recurso será mucho mejor, y estrictamente igual o inferior al lı́mite que establecimos en la
creación de la regla para nuestro cgroup.
A modo de comentario, otra de las tareas importante que desempeña cgroups es la monitori-
zación del consumo de recursos de ciertas aplicaciones. Por ejemplo, para el caso del runtime
de Docker (hablaremos más en profundidad en [5.3]) para ejecutar contenedores basados en
namespaces, utiliza cgroups para asignar el consumo (CPU, RAM, uso de disco, red...) que
tiene un contenedor en concreto. De este modo, podemos discernir y aplicar limitaciones a un
contenedor en especı́fico, sin tener que afectar a otros contenedores que estén funcionando en
ese momento en el host.
Para estas tareas, limitación y monitorización de recursos en contenedores de Docker, éste nos
facilita una serie de comandos para facilitar la creación y asignación de contenedores a un
cgroup en especı́fico, facilitando en consecuencia el uso de cgroups para limitar los recursos de
los contenedores que estemos utilizando.
47
TFE: Enrique Fernández Sánchez 4.11. TIME NAMESPACE
El namespace time, permite que por cada namespace que tengamos, podamos crear desfa-
ses entre los relojes monotónicos (CLOCK MONOTONIC) y de boot (CLOCK BOOTTIME), de la
máquina host. Esto permite que dentro de los contenedores se nos permita cambiar la fecha
y la hora, sin tener que modificar la hora del sistema host. Además, supone una capa más de
seguridad, ya que no estamos vinculando directamente la hora a los relojes fı́sicos de nuestro
sistema. [33]
Un namespace de tipo time, es muy similar al namespace de tipo PID en la manera de como
lo creamos. Utilizamos el comando unshare -T, y mediante una systemcall se nos creará
un nuevo time namespace, pero no lo asocia directamente con el proceso. Tenemos que utili-
zar setns para asociar un proceso a un namespace, además todos los procesos dependientes
también tendrán asignado dicho namespace.
48
TFE: Enrique Fernández Sánchez 4.12. EJEMPLO: TOPOLOGÍA DE RED CON IP
Creamos los network namespaces, en este caso, con nombre h1 y h2. El sistema no crea directa-
mente el namespace, lo que en realidad hace es definirlos en el sistema. El network namespace
se crea cuando una aplicación se asocia a el.
$ ip netns add h1
$ ip netns add h2
Si utilizamos el comando ip netns, nos mostrará los network namespaces existentes. Como
es un sub-comando de ip, muestra los ns que el comando lsns no muestra.
Ahora, si que podemos utilizar el comando lsns. Comprobamos que si nos aparecen los ns que
hemos creado, cosa que antes de asociar una aplicación al ns, no pasaba.
El comando ip crea automáticamente un nsfs para poder colocar los archivos de configuración
del netns. Para ello, debe crearse el directorio /etc/netns/h1 y poner en el los archivos de
configuración de la red.
$ mkdir /etc/netns/h1
$ echo "nameserver [Link]" > /etc/netns/h1/[Link]
En este momento, tenemos el ns configurado con DNS. Nos quedarı́a realizar la conexión entre
el ethernet fı́sico de nuestro host y las interfaces de nuestros namespaces. Para ello, vamos a
utilizar un conmutador virtual, en este caso Open vSwitch.
$ $ ovs-vsctl add-br s1
Utilizando el comando ip, creamos las interfaces virtuales de ethernet y las asignamos a sus
namespaces.
49
TFE: Enrique Fernández Sánchez 4.12. EJEMPLO: TOPOLOGÍA DE RED CON IP
Utilizando el comando ovs-vsctl, asignamos al bridge el otro par ethernet que hemos creado
para cada namespace.
Verificamos que el controlador sea standalone, ası́ el switch se comportará como un learning-
switch.
Como la conexión es desde localhost al exterior, entendemos que es una conexión fuera de
banda.
En este momento, tenemos todos configurado a falta de habilitar las diferentes interfaces de
nuestra topologı́a.
Ahora tenemos todas las interfaces configuradas, el switch activado y el sistema interconectado,
por lo que podemos ejecutar un ping en una de las terminales de los namespaces para verificar
la topologı́a.
Si queremos revertir todas las configuraciones que hemos hecho, lo que tenemos que hacer es
ejecutar los siguientes comandos:
$ ovs-vsctl del-br s1
$ ip link delete s1-eth1
$ ip link delete s1-eth2
$ ip netns del h1
$ ip netns del h2
50
TFE: Enrique Fernández Sánchez 4.13. EJEMPLO: TOPOLOGÍA DE RED CON UNSHARE
Utilizando unshare, no podemos ponerle un nombre, pero sı́ que nos permite asociarlo a un
archivo, que montará con tipo bind. Esto nos permitirá utilizar en namespace aunque no haya
ningún proceso corriendo en el, para ello podemos utilizar el comando nsenter.
$ touch /var/net-h1
$ touch /var/uts-h1
$ unshare --net=/var/net-h1 --uts=/var/uts-h1 /bin/bash
Para destruir el namespace, lo que tendremos que hacer es desmontar los archivos asignados a
dicho namespace.
$ umount /var/net-h1
$ umount /var/uts-h1
Como será común necesitar más de un namespace, en la mayorı́a de los casos tendremos que
utilizar los comandos unshare y nsenter.
51
TFE: Enrique Fernández Sánchez
Capı́tulo 5
La principal ventaja que encontramos al utilizar namespaces, respecto a otro tipo de virtua-
lizaciones como podrı́an ser las máquinas virtuales, es el aprovechamiento de los recursos de la
máquina host. Al tener todos el mismo kernel, evitamos tener por duplicados los kernels para
cada una de las instancias a realizar, ahorrando ciclos de CPU, como espacio en RAM.
52
TFE: Enrique Fernández Sánchez 5.1. CREANDO NUESTRO PROPIO “CONTENEDOR”
Por último, es interesante comentar el concepto de chroot, ya que muchas de estas abstrac-
ciones de los namespaces hacen uso de esta técnica para su funcionamiento. Un chroot es
una operación Unix que permite cambiar la ruta aparente de un directorio para un usuario en
especı́fico. Un proceso ejecutado después de realizar un chroot solo tendrá acceso al nuevo di-
rectorio definido y a sus subdirectorios. Esta operación recibe el nombre de chroot jail, ya
que los procesos no pueden leer ni escribir fuera del nuevo directorio. Este método suele ser de
gran utilidad en virtualizaciones a nivel de kernel, es decir, virtualización ligera o contenedores.
[35]
Lo primero, es interesante comentar que aunque nos referimos a contenedores, muchas perso-
nas entienden contenedores como únicamente los Docker Containers. Sin embargo existen
otros runtimes para ejecutar contenedores en el host. En general, todos implementan las es-
pecificaciones realizadas por Linux Foundation’s Container Initiative (OCI) [36], esta fundación
recoge directivas tanto para la creación de imagenes de contenedores como de sus runtimes.
Por lo tanto, existen diferentes runtimes en función de la solución que queramos realizar,
y cada runtime trabaja a un nivel de profundidad diferente. Por ejemplo, los contenedores
LXC [37], que entraremos en detalle en el siguiente apartado, se entienden como contenedores de
bajo nivel, mientras que un ejemplo de contenedores de alto nivel podrı́a ser containerd [38].
Imágenes
Una imagen de un contenedor consiste en un sistema de archivos root, que nos aporta todas
las dependencias necesarias por la aplicación a encapsular en un contenedor. Dichas imágenes
consisten en capas, que se montan por el runtime utilizando un tipo de montaje de unión
(manera de combinar múltiples directorios en uno único y que este contenga los contenidos de
ambos directorios combinados) [39], normalmente se utiliza overlayfs [40]. Esto nos permite
dividir nuestro sistema de archivos en capas, siendo el sistema de archivos más bajo inmutable
(los directorios referidos al sistema y que no tienen nada que ver con la aplicación a encapsu-
lar), y otra capa superior en la que encontraremos todo lo referido a nuestra aplicación y que
sı́ podremos modificar.
A modo de ejemplo, comentamos el siguiente script de bash creado por el usuario Github:
Nuvalence, en donde detalla los diferentes pasos para implementar un contenedor utilizando
namespaces, y además utilizando montajes de unión.
Ejemplo 5.1: Creación de imagen para contenedor utilizando Alpine y union mounts
1 #!/bin/bash
2
53
TFE: Enrique Fernández Sánchez 5.1. CREANDO NUESTRO PROPIO “CONTENEDOR”
8 # [Link]
9 # [Link]
10 #
11 # Modified on Jan 2022 by: Enrique Ranii <[Link]
[Link]>
12
13 # Deploy a image of Alpine Linux inside a "container". Using rootfs as Union
Filesystem
14 # Installing apache2 for httpd test, and check on host that network link is
enabled
15 # Usage:
16 # - sudo ./setup-image
17 # Enable verbose output using:
18 # - sudo VERBOSE=1 ./setup-image
19
20 set -e
21
54
TFE: Enrique Fernández Sánchez 5.1. CREANDO NUESTRO PROPIO “CONTENEDOR”
3. Extraer y modificar los permisos de lo que será la capa 1 inmutable de nuestro contenedor.
4. Configurar servidor DNS de dentro del contenedor e instalar dependencias del programa
a ejecutar, en nuestro caso haremos las pruebas con el programa httpd
Runtime
Una vez tenemos la imagen creada, el siguiente paso es el runtime encargado de ejecutarla.
Para ello, creamos un “contenedor” utilizando los siguientes namespaces:
Al igual que en el caso anterior, comentamos el siguiente script de ejemplo creado por el
usuario Github: Nuvalence, en donde se nos detallan los diferentes pasos a realizar para tener
el runtime de nuestro contenedor ejecutando la imagen que previamente hemos creado.
Ejemplo 5.2: Ejecución de runtime basado en imagen de Alpine
1 #!/bin/bash
2
3 # Copyright 2020 Nuvalence <[Link]
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
8 # [Link]
9 # [Link]
10 #
11 # Modified on Jan 2022 by: Enrique Ranii <[Link]
[Link]>
12
55
TFE: Enrique Fernández Sánchez 5.1. CREANDO NUESTRO PROPIO “CONTENEDOR”
23
24 [[ -n "$VERBOSE" ]] && set -x
25
26 # Container IP
27 CONTAINER_SUBNET_CIDR=[Link]/24
28 CONTAINER_IP=[Link]
29
30 #
31 # Host network setup
32 #
33
34 echo "*** Configure Host network setup {iptables, bridge, network namespace}..."
35
36 # Enable packet forwarding
37 IP_FORWARD=$(cat /proc/sys/net/ipv4/ip_forward)
38 echo 1 > /proc/sys/net/ipv4/ip_forward
39
63 #
64 # Network namespace setup
65 #
66
67 # Create a network namespace
68 ip netns add container
69
70 # Assign the container’s virtual adapter to the namespace
71 ip link set dev veth-container netns container
72
73 # Set the container’s IP
74 ip netns exec container \
75 ip addr add dev veth-container $CONTAINER_IP/$(basename $CONTAINER_SUBNET_CIDR)
76
77 # Bring up the container’s device
78 ip netns exec container \
79 ip link set dev veth-container up
56
TFE: Enrique Fernández Sánchez 5.1. CREANDO NUESTRO PROPIO “CONTENEDOR”
80
81 # Set the container’s default route
82 ip netns exec container \
83 ip route add default via $(dirname $CONTAINER_SUBNET_CIDR)
84
85 #
86 # Container filesystem setup
87 #
88
89 echo "*** Mount container filesystem using rootfs..."
90
57
TFE: Enrique Fernández Sánchez 5.1. CREANDO NUESTRO PROPIO “CONTENEDOR”
136
137 echo "*** Starting the container and execute the command: $@"
138 # Enter namespaces and exec the provided command
139 ip netns exec container \
140 unshare --pid --ipc --uts --cgroup --mount --root=rootfs --mount-proc --fork "$@
"
141
Para comprender mejor el código, vamos a detallar las siguientes fases por las que pasa la
ejecución del runtime.
3. Creamos una pareja de ethernets virtuales, uno será asignado al host y la otra al conte-
nedor.
5. Se realiza el montaje del sistema de archivos, diferenciando en dos capas. Una capa,
llamada capa inferior, en la que se almacena todo lo necesario de la distribución a ejecutar
y que será inmutable para la aplicación a encapsular, y otra capa en la que encontramos
los directorios que si tendrá acceso dicha aplicación. En este caso, se utiliza un montaje
de tipo overlay, a modo de ejemplo de los montajes de unión.
6. Se realiza un montaje de tipo bind para todos los recursos del sistema, que se encuentran
en el directorio /sys.
7. Se define una función para revertir todos los cambios realizados al sistema.
8. Por último, se ejecuta el contenedor con el comando unshare y especificando los names-
paces que vamos a utilizar, además le ponemos como punto de entrada al unshare la
aplicación que el usuario haya especificado (por ejemplo: sh o httpd).
58
TFE: Enrique Fernández Sánchez 5.1. CREANDO NUESTRO PROPIO “CONTENEDOR”
59
TFE: Enrique Fernández Sánchez 5.1. CREANDO NUESTRO PROPIO “CONTENEDOR”
2. Una vez tenemos la imagen creada, ya podemos ejecutar el runtime con el comando que
nosotros queramos. Para ello, tendremos que ejecutar el comando ./runtime-exec
<commando>
Dado que ya estamos dentro de una shell del contenedor, podemos comprobar como solo
podemos ver los procesos del contenedor, y solo tenemos la interfaz de red asignada al
contenedor.
60
TFE: Enrique Fernández Sánchez 5.1. CREANDO NUESTRO PROPIO “CONTENEDOR”
Por lo tanto, podemos concluir que este ejemplo es válido para demostrar el funciona-
miento de un contenedor, basado en namespaces y en sistemas de montaje de capas.
61
TFE: Enrique Fernández Sánchez 5.2. CONTENEDORES LXC
• Utiliza mount namespace para conseguir la estructura de directorios propia de una dis-
tribución de Linux.
• Una vez dentro del contenedor, podemos instalar aplicaciones utilizando el gestor de
paquetes de la distribución (apt, zipper, pacman, etc...)
Una caracterı́stica muy importante de este tipo de contenedores es que permiten la posibilidad
de configurar contenedores de dos tipos: contenedores con privilegios y contenedores sin privile-
gios. Esto es importante ya que si definimos un contenedor con privilegios, hay ciertas acciones
que nos permitirı́an realizar comandos en el host. En el caso de que nuestro contenedor sea
distribuido por terceros, puede suponer un problema grave de seguridad, ya que un atacante
podrı́a tomar el control de nuestro contenedor, y por consiguiente, también podrı́a acceder a la
información del host. Es por esto por lo que surge el concepto de contenedores sin privilegios
(unprivileged containers), considerados como una técnica mucho más segura ya que disponen
de un nivel añadido de aislamiento respecto al host. La clave reside en “mapear” el UID del
usuario root de nuestro contenedor a un UID del host que no tenga permisos de administrador.
Por lo tanto, si un atacante consigue acceder a nuestro contenedor, y pudiera acceder al host,
se verı́a con que no tiene permisos para realizar ninguna acción. [37]
62
TFE: Enrique Fernández Sánchez 5.2. CONTENEDORES LXC
LXC soporta dos tipos de conexiones virtuales de red. Estas son las siguientes:
• NAT bridge. En este modo, LXC tiene su propio bridge (lxcbr0) que funciona en
conjunto con las aplicaciones dnsmasq e iptables del host, dando lugar a que se
puedan utilizar servicios de red como DNS, DHCP y NAT dentro del propio contenedor.
• Host bridge. En este modo, es necesario que el host configure su propio bridge para
dar servicio a las aplicaciones que creamos pertinentes. Esta opción nos permite mu-
cha flexibilidad a la hora de interconectar nuestros contenedores para una funcionalidad
especı́fica.
A modo de ejemplo, si quisiéramos utilizar redes tipo NAT bridge en un contenedor [37], primero
tendrı́amos que crear el archivo /etc/default/lxc-net con el siguiente contenido:
Ejemplo 5.3: Configuración interfaz NAT bridge en LXC
1 # Leave USE_LXC_BRIDGE as "true" if you want to use lxcbr0 for your
2 # containers. Set to "false" if you’ll use virbr0 or another existing
3 # bridge, or mavlan to your host’s NIC.
4 USE_LXC_BRIDGE="true"
5
24 # Uncomment the next line if you want lxcbr0’s dnsmasq to resolve the .lxc
25 # domain. You can then add "server=/lxc/[Link]’ (or your actual $LXC_ADDR)
26 # to your system dnsmasq configuration file (normally /etc/[Link],
27 # or /etc/NetworkManager/dnsmasq.d/[Link] on systems that use NetworkManager).
28 # Once these changes are made, restart the lxc-net and network-manager services.
29 # ’[Link]’ will then resolve on your host.
30 #LXC_DOMAIN="lxc"
31
63
TFE: Enrique Fernández Sánchez 5.2. CONTENEDORES LXC
Ahora, necesitamos modificar la template del contenedor LXC para que utilice la interfaz que
hemos configurado. Modificamos la plantilla con ruta /etc/lxc/[Link] tal que:
Ejemplo 5.4: Configuración contenedor LXC para usar NAT bridge
1 [Link] = veth
2 [Link] = lxcbr0
3 [Link] = up
4 [Link] = [Link]xx:xx:xx
5
Para que todos estos cambios se realicen, es necesario que tengamos activado el servicio
[Link].
> sudo systemctl enable [Link]
> sudo systemctl start [Link]
Si decidimos crear un contenedor con privilegios, tendrı́amos que seguir los siguientes pasos
[41]:
Ejemplo 5.5: Crear un contenedor con privilegios en LXC
1 sudo lxc-create --template download --name <NombreContenedor>
2
Con este comando, LXC nos preguntará de manera interactiva por un root filesystem
para el contenedor a descargar, además de la distribución elegida, la versión o la arquitectura.
Si queremos hacerlo de manera que no sea interactivo, podemos crear el contenedor tal que:
Ejemplo 5.6: Crear un contendor con privilegios en LXC, modo no interactivo
1 sudo lxc-create --template download --name <NombreContenedor> -- --dist debian
--release stretch --arch amd64
2
De esta manera, ya tendrı́amos creado nuestro contenedor. Para poder manejar dichos conte-
nedores, es conveniente conocer los comandos disponibles por LXC. Algunos de los más impor-
tantes son los siguientes [41]:
• sudo lxc-ls --fancy. Lista los contenedores disponibles por el host.
• sudo lxc-info --name <NombreContenedor>. Permite conocer la información
de un contenedor especı́fico.
• sudo lxc-attach --name <NombreContenedor>. Nos conecta directamente con
la shell de nuestro contenedor.
• sudo lxc-start --name <NombreContenedor>--daemon. Permite arrancar el
contenedor.
• sudo lxc-stop --name <NombreContendor>. Permite parar un contenedor que
esté en ejecución.
• sudo lxc-destroy --name <NombreContenedor>. Elimina un contenedor, in-
cluido su directorio root.
64
TFE: Enrique Fernández Sánchez 5.2. CONTENEDORES LXC
Una vez creamos el contenedor, es interesante modificar su configuración para asignarle una
interfaz de red. Esto lo haremos modificando el archivo config con ruta:
/var/lib/lxc/<NombreContenedor>/config
Añadiremos la siguiente configuración para que utilice el NAT bridge que definimos con ante-
rioridad, además de permitir la ejecución de aplicaciones tipo X11, utilizando xorg (aplicaciones
con interfaz gráfica) [42].
Ejemplo 5.7: Configuración interfaz NAT bridge y aplicaciones X a un contenedor LXC
1 # Network configuration (NAT bridge)
2 [Link] = veth
3 [Link] = lxcbr0
4 [Link] = up
5
6 ## for xorg
7 [Link] = /dev/dri dev/dri none bind,optional,create=dir
8 [Link] = /dev/snd dev/snd none bind,optional,create=dir
9 [Link] = /tmp/.X11-unix tmp/.X11-unix none bind,optional,create=dir,ro
10 [Link] = /dev/video0 dev/video0 none bind,optional,create=file
11
Para iniciar la interfaz gráfica, una vez tengamos iniciado el contenedor y estemos dentro de
una shell, ejecutamos el comando startx.
En el caso de optar por la opción más segura, que es la de crear un contenedor sin privilegios en
el host. Tendremos que realizar una serie de configuraciones previas [43]. La primera consistirá
en crear un usuario sin privilegios para LXC:
Ahora, necesitamos buscar los valores de grupo (subgid) e identificación (subuid) del usuario
creado, para ello ejecutamos lo siguiente:
/etc/subgid:lxc_user:100000:65536
/etc/subuid:lxc_user:100000:65536
Utilizando las mismas configuraciones de red que en el aparatado de contenedor con privilegios,
procedemos a acceder al usuario asignado a LXC. Ejecutaremos el comando id para conocer
los diferentes uid y gid asignados.
$ su lxc_user
$ id
65
TFE: Enrique Fernández Sánchez 5.2. CONTENEDORES LXC
El siguiente paso serı́a crear los directorios de configuración de LXC, y copiar la configuración
default a dichos directorios.
$ mkdir -p /home/lxc_user/.config/lxc
$ cp /etc/lxc/[Link] /home/lxc_user/.config/lxc/[Link]
Por último, tendrı́amos que modificar dicho archivo para realizar un “mapeo de permisos”. Con
el fin de asignar la ejecución del contenedor al usuario LXC que acabamos de crear. Al final del
archivo [Link] que acabamos de copiar, añadimos lo siguiente:
Ejemplo 5.8: Configuración para mapear UID y GID para un contenedor sin privilegios en LXC
1 lxc.id_map = u 0 100000 65536
2 lxc.id_map = g 0 100000 65536
3
Una vez realizados todos estos pasos, ya podrı́amos crear nuestro contenedor sin privilegios.
Lo harı́amos de manera similar que en el apartado anterior, es decir utilizando el comando
lxc-create. Además, también será importante modificar el archivo config de nuestro con-
tenedor para asignar la interfaz de red y permitir la ejecución de aplicaciones con interfaz gráfica.
A modo de ejemplo, vamos a crear un contenedor (con privilegios) paso a paso, y por último,
vamos a verificar que namespaces está utilizando dicho contenedor. Para esto, vamos a crear
un contenedor de Ubuntu 20.04.03 (Focal) utilizando el catálogo de imágenes de LXC.
1. Lo primero que tenemos que hacer es verificar que LXC está instalado correctamente en
nuestro sistema, una manera fácil de comprobarlo es con el siguiente comando:
$ sudo lxc-checkconfig
66
TFE: Enrique Fernández Sánchez 5.2. CONTENEDORES LXC
De manera interactiva, el programa nos preguntará por diferentes opciones para nuestro
contenedor. Para elegir una distribución, podemos acceder a la siguiente web: imagenes
LXC, en las que aparecen todas las imágenes disponibles. En este caso, escribimos lo
siguiente para cada una de ellas:
• Distribution: ubuntu
• Release: focal
• Architecture: amd64
67
TFE: Enrique Fernández Sánchez 5.2. CONTENEDORES LXC
5. Una vez dentro del contenedor de Ubuntu, podemos comprobar con el siguiente comando
que efectivamente se ha instalado la distribución que hemos elegido.
$ cat /etc/os-release
6. Por último, en otra terminal, procedemos a obtener el PID asociado a dicho contenedor.
Para ello, podemos hacer uso del siguiente comando:
Una vez tenemos el PID, podemos utilizar el comando lsns los namespaces que están
asociados a dicho PID. Para ello, ejecutamos lo siguiente:
68
TFE: Enrique Fernández Sánchez 5.3. CONTENEDORES DOCKER
Tal y como lo definen sus creadores [44], Docker es una plataforma de desarrollo, despliegue
y ejecución de aplicaciones. Docker permite separar entre aplicaciones de la infraestructura,
permitiendo desplegar el software mucho más rápido. Uno de los objetivos que persigue esta
plataforma es el de minimizar el tiempo entre programar y tener ejecutando la aplicación en
producción. Algunas de las ventajas que nos aporta esta plataforma son:
• Contenedores ligeros y que además, contienen todo lo necesario para ejecutar la aplicación.
Independientemente del host.
Arquitectura de Docker
69
TFE: Enrique Fernández Sánchez 5.3. CONTENEDORES DOCKER
Lo primero que tendremos que hacer para utilizar Docker, es proceder a instalar dicha herra-
mienta. Para ello, tendrı́amos que seguir los pasos que se nos especifican en [Link]
[Link]/get-docker/.
Una vez instalado, podemos comprobar que el “demonio” está funcionando si ejecutamos los
siguientes comandos desde el cliente de docker.
El comando nos devolverá información sobre el cliente de docker e intentará conectar con el
“demonio”. En el caso de que nos responda como en la imagen (5.15), tendremos que ejecutar
el comando:
70
TFE: Enrique Fernández Sánchez 5.3. CONTENEDORES DOCKER
Una vez comprobado que tenemos conexión con el “daemon”, lo siguiente será ejecutar nuestro
primer contenedor. En este caso, haremos uso de un contenedor “especial”que nos verifica la
instalación de docker en nuestros host. Para ello, ejecutamos el siguiente comando:
Una vez termina de ejecutarse el comando, la salida por consola que obtendremos serı́a equi-
valente a la siguiente figura:
Figura 5.16: Ejecución comando docker run hello-world para comprobar instalación
Como podemos ver en la figura 5.16, el contenedor nos muestra por consola los pasos que ha
seguido para completar su ejecución. Antes de comentar los pasos seguidos, es importante acla-
rar el concepto de “imagen”.
Llamamos imagen de Docker a un archivo de sistema, compuesto por diferentes capas de infor-
mación, que es utilizado para desplegar un contenedor de Docker. Entendemos estas imágenes
como la plantilla base desde la que partimos para crear nuevos contenedores para ejecutar apli-
caciones, o bien para crear una nueva imagen.
71
TFE: Enrique Fernández Sánchez 5.3. CONTENEDORES DOCKER
Una vez estamos familiarizados con el concepto de imagen, podemos proceder a comentar los
pasos realizados por Docker para mostrarnos el mensaje de la figura 5.16. Dichos pasos serı́an
los siguientes:
3. El “daemon” crea un nuevo contenedor a partir de esa imagen, que correrá un ejecutable
que producen la salida que hemos obtenido.
Por otro lado, en esa misma ejecución del contenedor “hello-world”, se nos invita a ejecutar lo
siguiente:
Si analizamos la sintaxis del comando anterior, podemos diferenciar entre cinco elementos di-
ferentes:
• -it. Opciones del argumento run. En este caso, tenemos configurado que sea de tipo
interactivo (-i), es decir que nos muestre por consola la salida del comando; y además,
hemos seleccionado que configure el terminal como un terminal dentro del contenedor que
vamos a crear (-t).
• ubuntu. Imagen que hemos elegido para nuestro contenedor. Primero la buscará local-
mente, y si no la encuentra, comprobará en el repositorio público de imágenes (Docker
Hub).
• bash. Binario a ejecutar dentro del contenedor, en este caso corresponde con una consola
shell.
Una vez ejecutamos dicho comando, automáticamente se crea un contenedor con una imagen
de ubuntu y se nos ejecuta una consola, en la que podemos navegar y trabajar de manera
aislada a nuestra máquina host. (ver Figuras 5.17 y 5.18).
72
TFE: Enrique Fernández Sánchez 5.3. CONTENEDORES DOCKER
Figura 5.17: Ejecución comando docker run -it ubuntu bash para levantar un conte-
nedor ubuntu
Para comprobar que estamos en una distribución Ubuntu, y además ver la versión que el
docker “daemon” ha descargado, ejecutamos el comando siguiente. Como podemos comprobar,
el contenedor está utilizando la última versión LTS (Long Term Support, versiones más estables
y probadas que tendrán soporte durante un largo periodo de tiempo) disponible hasta la fecha.
73
TFE: Enrique Fernández Sánchez 5.3. CONTENEDORES DOCKER
En este apartado, a modo de ejemplo, vamos a crear nuestro propio contenedor en el que
correremos una aplicación de Python muy sencilla [45]. Para ello, primero vamos a crear una
carpeta en nuestro ordenador, en ella crearemos dos archivos:
• Otro llamado Dockerfile, en el que detallaremos las instrucciones que tiene que seguir
docker para crear nuestro contenedor.
1. Primero procedemos a editar el archivo Python [Link], para ello utilizamos el siguiente
código:
Ejemplo 5.9: Codigo Python de ejemplo para crear un Dockerfile
1 #!/usr/bin/env python3
2
3 print("Saludos desde tu contendor!!")
4
Lo primero que tenemos que tener claro es lo que queremos que haga nuestro contenedor.
En este caso serı́a que ejecutase el código Python de [Link], en otro caso podrı́a ser
que quisiéramos desplegar otro tipo de aplicación. Para hacer esto, primero tenemos que
buscar una imagen que nos sirva de base para construir nuestro contenedor. En el caso de
Python, nos podrı́a servir un ubuntu, ya que viene con Python preinstalado, sin embargo,
vamos a buscar una imagen mucho más especifica. Para ello, nos dirigimos a la página
de DockerHub [Link] y buscamos Python en el buscador (ver
Figura 5.20).
74
TFE: Enrique Fernández Sánchez 5.3. CONTENEDORES DOCKER
Figura 5.20: Búsqueda en DockerHub de una imagen base para nuestro contenedor.
El primer resultado que obtenemos es el de una imagen de contenedor que nos permite
ejecutar código Python. Además, podemos ver como es muy apoyada por la comunidad
(dato importante de cara a la estabilidad y seguridad de nuestro contenedor). Por lo
tanto, usaremos la imagen python para utilizarla como base de nuestro contenedor.
Ahora, procedemos a editar el archivo Dockerfile:
Ejemplo 5.10: Contenido del archivo Dockerfile para crear contenedor con código Python
1 # Un Dockerfile siempre necesita importar una imagen como base
2 # Para ello utilizamos ’FROM’
3 # Elegimos ’python’ para la imagen y ’latest’ como version de esa imagen
4 FROM python:latest
5
6 # Para ejecutar nuestro codigo Python, lo copiamos dentro del contenedor
7 # Para ello utilizamos ’COPY’
8 # El primer parametro ’[Link]’ es la ruta origen del archivo en el host
9 # El segundo parametro ’/’ es la ruta destino del archivo dentro del
contenedor
10 # En este caso, ponemos el archivo en el root del sistema
11 COPY [Link] /
12
13 # Definimos el comando a ejecutar cuando iniciemos el contenedor
14 # Para ello utilizamos ’CMD’
15 # Para ejecutar la aplicacion utilizariamos "python ./[Link]".
16 CMD [ "python", "./[Link]" ]
17
75
TFE: Enrique Fernández Sánchez 5.3. CONTENEDORES DOCKER
3. Una vez tenemos los dos archivos, ya podemos crear la imagen de nuestro contenedor (ver
Figura 5.21). Para ello, tenemos que ejecutar el siguiente comando:
La opción -t nos permite asignar un nombre a nuestra imagen, en nuestro caso hemos
elegido python-hello world.
4. Ya tenemos nuestra imagen creada, por lo que podemos ejecutar nuestro contenedor y
comprobar que nuestro código Python se ejecuta correctamente. Para lanzar el contenedor
ejecutamos el siguiente comando:
76
TFE: Enrique Fernández Sánchez 5.3. CONTENEDORES DOCKER
A modo de comprobación de que en efecto Docker utiliza namespaces para la creación de sus
contenedores, vamos a profundizar en como obtener los namespaces asociados a un contenedor
en especı́fico. [46]
Figura 5.23: Ejecución contenedor Docker para ver sus namespaces asociados.
Si ejecutamos el comando ip netns list, podemos comprobar como nos devuelve una lista
vacı́a. De primeras, podrı́amos entender esto como que Docker no utiliza network namespaces,
sin embargo, esto pasa porque Docker “esconde” estos namespaces por defecto.
77
TFE: Enrique Fernández Sánchez 5.3. CONTENEDORES DOCKER
Si queremos exponer los namespaces de un contenedor, tenemos que hacerlo manualmente. Para
ello, utilizamos los siguientes comandos:
$ ln -s /proc/<Pid>/ns/net /var/run/netns/<Pid>
Una vez ya nos aparece el network namespace dentro del comando ip netns list, podemos
comprobar que en efecto es el namespace asociado a nuestro contenedor, para ello podemos
ejecutar los siguientes comandos:
Que tiene que dar una salida equivalente a los siguientes comandos:
Tal y como podemos ver en la figura 5.26, ambos comandos dan el mismo resultado, por lo
que quedarı́a comprobado que Docker utiliza network namespaces para dar conectividad a sus
contenedores. Además, es interesante como “esconde” los identificadores de los namespaces
que crea para dichos contenedores. Una vez conocemos el identificador, podrı́amos generar
arquitecturas de red mucho más complejas, como por ejemplo añadir un ethernet virtual para
hacer port mirroring al tráfico del contenedor, este ejemplo se ve más detallado en el siguiente
enlace Traffic mirror with OVS and Docker
78
TFE: Enrique Fernández Sánchez 5.3. CONTENEDORES DOCKER
79
TFE: Enrique Fernández Sánchez
Capı́tulo 6
En este apartado vamos a detallar los conceptos de virtualización de topologı́as de red para la
evaluación y/o simulación de situaciones concretas de nuestras redes. Para ello, podemos mo-
delar nuestros nodos de la red utilizando contenedores, siguiendo de esta manera el concepto
de “Virtual Network Function” que introdujimos en el segundo apartado de este documento. [2]
Como bien hemos visto en el apartado anterior [5], un contenedor es una abstracción de alto
nivel de un sistema aislado, utilizando “namespaces”. Por lo tanto, tenemos varias maneras
de enfocar el camino hacia el objetivo de modelar los nodos de nuestra red. Dichos caminos
radican en la metodologı́a de contenedores vamos a utilizar. Por ejemplo, algunos de los caminos
a seguir podrı́an ser los siguientes:
• Crear un shell script para desplegar los namespaces necesarios de nuestra topologı́a, uti-
lizando los comandos nsenter y unshare.
• Desplegar e interconectar contenedores LXC, en el que cada contenedor cumpla una fun-
ción dentro de nuestra topologı́a (host, switch, controller...)
80
TFE: Enrique Fernández Sánchez 6.1. MININET: SIMULADOR DE REDES
La opción de utilizar una máquina virtual preconfigurada es mucho más rápida, ya que solo
tendremos que descargar la última versión del enlace que encontramos en la web oficial del
proyecto Github: mininet releases. Una vez descargada, importamos dicha imagen en nuestro
hipervisor, por ejemplo: VirtualBox. Ejecutamos la máquina virtual recién importada y acce-
demos al sistema con el usuario: mininet/mininet.
Sin embargo, si deseamos realizar una instalación de mininet sobre nuestra distribución Linux.
Podemos instalar el programa en función de la distribución que estemos usando:
Una vez instalado, podemos comprobar la versión ejecutando el siguiente comando en una
terminal:
$ mn --version
Por otro lado, Mininet permite utilizar diferentes switches y controladores para sus topologı́as.
Para esta explicación, vamos a utilizar Open vSwitch [11] como controlador, en el modo brid-
ge/standalone. Si queremos comprobar que Open vSwitch está instalado correctamente para
mininet, podemos ejecutar el siguiente comando en una terminal:
En el caso de que el comando anterior nos de un error, tendremos que instalar Open vSwitch
para nuestra distribución y asegurarnos de que el servicio esté funcionando. Para iniciar el
servicio de Open vSwitch, tendremos que hacer lo siguiente:
81
TFE: Enrique Fernández Sánchez 6.1. MININET: SIMULADOR DE REDES
6.1.2 Ejemplos
Topologı́a simple
En este apartado, vamos a trabajar la dualidad entre implementar una topologı́a utilizando úni-
camente namespaces, a implementar esa misma topologı́a utilizando mininet. Para ello, vamos
a trabajar con una red formada por dos hosts, conectados entre sı́ mediante un conmutador de
paquetes (switch), que será manejado por un controlador.
Primero, accedemos a nuestra máquina host, en la que desplegaremos los namespaces asociados
a la topologı́a. Una vez estamos dentro de la cuenta root, procedemos a crear los network
namespaces de cada host.
Para interconectar los hosts con el conmutador, utilizaremos interfaces de red virtuales de tipo
pares de ethernet (veth pairs) (explicados en [3.5]). Por eso mismo, utilizamos los siguientes
comando para establecer los enlaces punto a punto:
82
TFE: Enrique Fernández Sánchez 6.1. MININET: SIMULADOR DE REDES
En este punto, tendremos que asociar las interfaces a los network namespaces.
Por otro lado, también será necesario que añadamos el otro extremo de las interfaces al switch.
Ahora es el momento de configurar el switch para que funcione como un conmutador. Para ello,
utilizamos el controlador Open vSwitch (tal y como pudimos ver en los ejemplos asociados a
pares ethernet [3.5]). Por lo tanto, para hacerlo funcionar en nuestra topologı́a, tendremos que
ejecutar los siguientes comandos:
Si queremos comprobar que hemos creado correctamente el switch virtual, podemos utilizar el
siguiente comando:
$ ovs-vsctl show
Una vez tenemos todos los dispositivos configurados, lo que tendremos que hacer es asignar
direcciones IP a cada host, y pondremos en funcionamiento las interfaces asociadas.
83
TFE: Enrique Fernández Sánchez 6.1. MININET: SIMULADOR DE REDES
En este punto, ya deberı́amos tener conectividad entre ambos hosts. Para ello, lo que podemos
hacer es realizar un ping entre host-1 (IP: [Link]) y host-2 (IP: [Link]).
En este caso, vamos a replicar la misma topologı́a del ejemplo anterior, pero esta vez utilizando
mininet. Dado que es una topologı́a sencilla, podemos ejecutarla con un único comando.
84
TFE: Enrique Fernández Sánchez 6.1. MININET: SIMULADOR DE REDES
Como podemos comprobar en la captura anterior, una vez ejecutamos el comando, la topologı́a
se crea automáticamente. Si quisiéramos comprobar la conectividad entre los diferentes hosts,
solo tendrı́amos que ejecutar el siguiente comando dentro de la terminal de mininet.
mininet> h1 ping h2
Mientras tenemos activa la topologı́a de mininet, podemos comprobar qué namespaces ha creado
para dicha topologı́a.
Como bien podemos ver en la imagen (6.6), mininet solo está utilizando los namespaces
mount y network para el despliegue de la topologı́a.
Si quisiéramos implementar esta topologı́a simple utilizando la API Python que está incluida
en mininet, tendrı́amos que ejecutar el script: [Link]
85
TFE: Enrique Fernández Sánchez 6.1. MININET: SIMULADOR DE REDES
En este ejemplo, vamos a profundizar sobre como aplicar limitaciones de recursos a una topo-
logı́a desplegada con mininet. Algunos de los recursos más importantes a tener en cuenta son
los siguientes:
• CPU. Nos permite asignar el máximo uso de CPU de un dispositivo. Esto nos permitirá
ajustar nuestra simulación a un entorno más real, además de permitirnos gestionar más
los recursos disponibles.
Por lo tanto, a modo de ejemplo, vamos a utilizar la topologı́a anterior, pero aplicando una
limitación de ancho de banda, latencia y CPU a uno de los enlaces que conectan un host con
el switch. El código utilizado para este ejemplo lo podemos encontrar en [Link].
Como podemos comprobar en la figura 6.8, al ejecutar la topologı́a, vemos como en consola
nos aparece un aviso de que se ha limitado un enlace a 5 Mbit de ancho de banda y 80 ms de
latencia. Por otro lado, una vez la topologı́a esta funcionando, se procede a una comprobación de
conectividad entre ambos host, seguidamente, se ejecuta la herramienta iperf para comprobar
cual es el ancho de banda máximo entre ambos hosts.
86
TFE: Enrique Fernández Sánchez 6.1. MININET: SIMULADOR DE REDES
Comparando los resultados obtenidos entre la topologı́a sin limitar (ver figura 6.7) y la topologı́a
limitada tanto en CPU como en los enlaces (ver figura 6.8), podemos comprobar que efectiva-
mente el ancho de banda que obtenemos de la herramienta iperf se ve claramente reducido,
pasando de 20 Gbits/sec a 4.58 Mbits/sec. Por lo tanto, podemos concluir que efectivamente
se están aplicando correctamente la limitación de ancho de banda.
Por otro lado, otra medida interesante a comparar es el tiempo que tarda un paquete entre que
es enviado a un host y la respuesta vuelve al host emisor. Esto lo podemos comprobar con la
herramienta ping. Dado que hemos fijado la latencia de uno de los enlaces (fijado a 80 ms),
deberı́amos obtener un valor del campo time mayor que al que obtenemos en la topologı́a
simple sin limitar. Ver figura 6.9.
Al igual que para el ancho de banda, los resultados de la topologı́a sin limitar (ver figura 6.5)
y la topologı́a limitada (ver figura 6.9), son totalmente diferentes. Estas limitaciones son muy
interesantes para modelar una topologı́a con caracterı́sticas lo más cercanas a un despliegue
real. Por lo tanto, para simular una red, deberemos configurar estos valores de ancho de banda
y latencia para cada uno de los enlaces de la topologı́a.
87
TFE: Enrique Fernández Sánchez 6.1. MININET: SIMULADOR DE REDES
En los ejemplos anteriores de mininet, hemos utilizado Open vSwitch actuando como un
conmutador learning switch. Dicho modo de funcionamiento tiene como tarea distribuir, de
manera dinámica y según ciertas reglas, la información asociada al encaminamiento de los pa-
quetes de la red, rellenando automáticamente las flow-table de los switches.
Sin embargo, mininet nos permite la utilización de otros controladores SDN externos, que
además pueden estar basados en las diferentes versiones del protocolo OpenFlow que están dis-
ponibles actualmente, permitiendo ası́ compatibilidad con más dispositivos fı́sicos disponibles
en el mercado.
Algunos de los controladores SDN que más se utilizan actualmente son los siguientes:
• Ryu [51]. Controlador SDN escrito en Python. Filosofı́a basada en componentes, por lo
que permite una fácil integración con nuevas aplicaciones. Aporta una northbound API
bien definida. Soporta OpenFlow 1.0, 1.2, 1.3, 1.4 y 1.5, además de otros protocolos como
Netconf.
• ONOS [52]. Controlador SDN muy utilizado. Desarrollado en el lenguaje Java y que tiene
una arquitectura modular.
Controlador Ryu
A modo de ejemplo, vamos a instalar y ejecutar una topologı́a en mininet utilizando el con-
trolador SDN Ryu.
Lo primero, para instalar Ryu tenemos que ejecutar el comando siguiente. Para este ejemplo,
vamos a utilizar la misma máquina virtual que estamos utilizando para mininet.
En una terminal (la llamaremos terminal A), ejecutaremos una topologı́a de mininet. Por
ejemplo, utilizando el siguiente comando:
Con dicha topologı́a, estaremos simulando una red de tipo árbol con 8 hosts, 7 switchs Open
vSwitch y un controlador remoto (escuchando en la dirección [Link]:6653).
88
TFE: Enrique Fernández Sánchez 6.1. MININET: SIMULADOR DE REDES
Una vez ejecutada la topologı́a, podemos comprobar que no tenemos conectividad entre los
host utilizando el comando pingall, esto es debido a que no tenemos ejecutando nuestro
controlador. (ver Figura 6.10)
En este caso, vamos a ejecutar nuestro controlador con una configuración de switch que so-
porta OpenFlow 1.3 (alojado en simple switch [Link]). Para ello, ejecutamos el siguiente
comando en una terminal diferente a la terminal A (la llamaremos terminal B).
Una vez ejecutado, nos aparecerá en la terminal B la información acerca de los módulos que
Ryu ha cargado para dar la funcionalidad que deseamos. Además, en dicha terminal se irán
mostrando los diferentes paquetes que conmute el switch.
89
TFE: Enrique Fernández Sánchez 6.1. MININET: SIMULADOR DE REDES
Figura 6.11: Ejecución del controlador Ryu (terminal inferior, B) y posterior pingall en
mininet (terminal superior, A)
Por otro lado, otra funcionalidad interesante de este controlador es el visualizador de topologı́as.
Nos permite ver en una página web la topologı́a de nuestra red, es decir, como se interconectan
los diferentes switches de la red. Para ello, solo tenemos que ejecutar el siguiente comando en la
terminal B (si tenemos activo el ejemplo de simple switch [Link], pararemos su ejecución
con Ctrl+C):
90
TFE: Enrique Fernández Sánchez 6.1. MININET: SIMULADOR DE REDES
Ahora, una vez ejecutado el comando anterior, tenemos que utilizar un navegador web y di-
rigirnos a la siguiente dirección: [Link] Después de acceder a dicha
página, podemos ver como se nos presenta la topologı́a de la red que estamos controlando (ver
Figura 6.12). [53]
91
TFE: Enrique Fernández Sánchez 6.2. CONTAINERNET: SIMULADOR DE REDES
Además, es un proyecto muy activo por la comunidad investigadora, ya que permite evaluar
topologı́as de cloud computing, network function virtualization (NFV) o multi-access edge com-
puting (MEC). Un ejemplo de esto es el emulador vim-emu: A NFV multi-PoP emulation
Platform, desarrollado por SONATA project, y que está basado en containernet.
Caracterı́sticas
Algunas de las caracterı́sticas más importantes que nos aporta containernet, son las si-
guientes:
92
TFE: Enrique Fernández Sánchez 6.2. CONTAINERNET: SIMULADOR DE REDES
• Nested Docker deployment. Utiliza un contenedor Docker con privilegios para desple-
gar en su interior otro entorno Docker en el que ejecutar las topologı́as de containernet.
Esta opción tiene deshabilitado la caracterı́stica de limitación de recursos.
Para nuestro entorno de pruebas, hemos elegido instalar containernet en una máquina
virtual en VirtualBox, utilizando Ubuntu 20.04 LTS, tal y como leemos en su documentación.
Para ello, seguimos las instrucciones tal que:
1. Instalación de Ansible
93
TFE: Enrique Fernández Sánchez 6.2. CONTAINERNET: SIMULADOR DE REDES
Si la instalación es satisfactoria, la salida de la consola deberı́a ser muy similar a la que vemos en
la imagen 6.15. Al final de la ejecución del “playbook” de Ansible, se nos muestra un resumen
de las tareas ejecutadas, y cuales de ellas se han realizado correctamente, o bien, cuales han
dado error al ejecutarse. En nuestro caso, se ha instalado correctamente.
94
TFE: Enrique Fernández Sánchez 6.2. CONTAINERNET: SIMULADOR DE REDES
Esta topologı́a que podemos ver en el diagrama, está ubicada en la carpeta de examples den-
tro de containernet. Con esta topologı́a podemos ver las bases del simulador, y comprobar
como puede trabajar con las caracterı́sticas de mininet, a la vez de implementar los hosts
como contenedores Docker. El código Python utilizado para ejecutar el ejemplo lo encontramos
en el siguiente enlace: containernet [Link]
La salida por consola será muy similar a la que obtendrı́amos en mininet, sin embargo,
podemos comprobar como aparece la información respectiva de nuestros contenedores Docker
que van a tener la función de host en la topologı́a. Todo esto lo podemos ver en la figura 6.17.
Una vez se termina de ejecutar la topologı́a, nos deja activa la CLI para que podamos interactuar
con la misma. En nuestro caso, vamos a comprobar cual es la configuración de red asociada
al host 1, para ver como ha gestionado las interfaces del contenedor de Docker. Para ello,
ejecutamos el siguiente comando:
containernet> d1 ifconfig
Como podemos comprobar en la figura 6.18, el host 1 tiene una interfaz (d1-eth0) que es la
que estará en uso para nuestra topologı́a de containernet, además dispone de otra interfaz
(eth0) que será la que utilice para comunicarse con el controlador de Docker. Por lo tanto,
podemos ver como containernet no manda su tráfico por el bridge que crea Docker, sino
que añade una interfaz especı́fica (un extremo de un par ethernet virtual, veth) por el que
encaminará el tráfico de la simulación.
95
TFE: Enrique Fernández Sánchez 6.2. CONTAINERNET: SIMULADOR DE REDES
Por otro lado, mientras tenemos la topologı́a ejecutándose, podemos comprobar como aparecen
los contenedores Docker en nuestro sistema. Además, también podemos acceder a una shell de
uno de los contenedores (hosts de containernet) y comprobar que efectivamente el resultado
es el mismo que si accedemos desde la CLI de containernet. Para ello, hemos comprobado
la configuración de red del mismo host que vimos en la figura 6.18, esto lo podemos ver en la
figura 6.19
96
TFE: Enrique Fernández Sánchez 6.2. CONTAINERNET: SIMULADOR DE REDES
Al igual que hacı́amos en mininet, podemos comprobar conectividad entre dos hosts utilizando
el comando ping, para ello podemos ejecutar en la CLI el siguiente comando:
Una vez ejecutado, observamos como la salida nos muestra el ping que se ha ejecutado, y como
efectivamente hay conectividad entre los dos hosts. (ver figura 6.20)
Por último, si ya no queremos seguir ejecutando la topologı́a, tendremos que ejecutar el comando
exit. Este comando nos permitirá revertir la configuración asociada a la topologı́a, además de
apagar y eliminar los contenedores Docker que estaban ejerciendo como hosts. El resultado de
la ejecución de este comando lo podemos ver en la figura 6.21.
97
TFE: Enrique Fernández Sánchez
Capı́tulo 7
En este capı́tulo vamos a comentar las conclusiones obtenidas al realizar el presente trabajo.
Por otro lado, se ha explicado las diferentes interfaces de red virtuales de Linux, ya que son
realmente importantes para las soluciones de virtualización. Para completar la explicación, se
ha seguido una metodologı́a basada en ejemplos, por lo que hemos podido comprobar la funcio-
nalidad y el objetivo de cada una de las interfaces expuestas. Hemos comprobado la importancia
de interfaces como pueden ser los pares ethernet o bien los tup/tap.
En relación a los namespaces, se han definido los namespaces presentes dentro del Kernel
de Linux. Se ha verificado el potencial que posee esta tecnologı́a, y como está muy presente
en diferentes tecnologı́as que están fuertemente arraigadas por la comunidad, por ejemplo LXC
o Docker. Gracias a los ejemplos, se ha podido comprender fácilmente el funcionamiento de
cada uno de los namespaces.
En el punto de virtualización ligera, hemos resumido las opciones disponibles para realizar
virtualización ligera. Además, hemos recreado un contenedor paso a paso, de modo que he-
mos podido comprobar la importancia de los namespaces. Por otro lado, con las definiciones
de LXC y de Docker, hemos iniciado al uso de ambas herramientas, facilitando su posterior uso.
Por último, se han propuesto ciertas herramientas que utilizan la virtualización ligera para
simular redes de comunicaciones. En este caso hemos hablado de mininet, como una im-
plementación basada totalmente en namespaces, y de containernet como un “fork” de
mininet que está extendido para trabajar con hosts de Docker. Ambas herramientas son muy
importantes ya que permiten crear entornos de desarrollo e investigación para el área de SDN.
Además, nos permite realizar pruebas con topologı́as de gran tamaño de manera sencilla y
rápida.
98
TFE: Enrique Fernández Sánchez
En resumen, podemos concluir que el presente trabajo ha cumplido todos los objetivos pro-
puestos, ya que se ha profundizado sobre la virtualización ligera, y se ha ido construyendo
una “guı́a” a base de explicaciones y ejemplos, que nos permite comprender los entresijos de
simuladores de redes basados en dicha virtualización, como puede ser mininet.
Propuestas futuras
Algunas de las lı́neas de investigación que pueden resultar interesantes para continuar este
proyecto, son las siguientes:
• Investigar sobre modificaciones del protocolo OpenFlow, como puede ser https://
[Link]/Orange-OpenSource/oko, que nos permite aplicar programas con los
que filtrar paquetes de la red utilizando BPF (Berkley Packet Filter ). Por otro lado,
serı́a realmente importante investigar la tecnologı́a DPDK, ya que nos permite acceder
directamente a la información de nuestras interfaces de red, además de mejorar conside-
rablemente el throughput en comparación de si utilizamos un Linux bridge.
99
TFE: Enrique Fernández Sánchez
Capı́tulo 8
Bibliografı́a
Enlaces y referencias
1. NFV white paper
7. IEEE 802.1ad
14. Creating tap/tun devices with IP tuntap and tunctl as detailed in Linux network tools
17. Namespaces
100
TFE: Enrique Fernández Sánchez
38. containerd
45. A beginner’s guide to Docker - how to create your first Docker application
47. Mininet: An Instant Virtual Network on your Laptop (or other PC)
101
TFE: Enrique Fernández Sánchez
Figuras
• Figura 2.1: NFV white paper
102