Reutilización del mismo puerto en el lado del cliente

La entrada anterior está escrita desde el punto de vista de los servidores, aunque en el lado del cliente, también podemos reutilizar puertos. ¿Cómo? Pues añadiendo, antes de la función connect de, por ejemplo, este código de cliente, las siguientes órdenes:

sock.setsockopt(socket.SOL_SOCKET, SO_REUSEPORT, 1)
sock.bind((“0.0.0.0”,3000))

la primera orden es la misma que hemos comentado ya y es la que nos activa el flag que permite reutilizar el puerto; la segunda es necesaria para “forzar” el puerto y que no sea uno aleatorio. Si lanzamos 2 conexiones, nos queda:

Screenshot from 2015-03-23 10:59:20

¡Espero que os sea útil!


Reutilización de puertos en GNU/Linux

Para todos los sistemas operativos, una conexión (comunicación) entre dos equipos está definida por cualquier combinación de la tupla formada por la dirección IP origen y destino, el puerto de origen y destino y el protocolo de transporte (TCP o UDP).  Así, en cualquier nodo de nuestra red, podremos utilizar la función bind para “anclar” nuestro servidor a cualquier puerto libre. Si el puerto ya está en uso, y si nuestro nodo dispone de más de una dirección IP, podremos unir nuestro proceso utilizando otra IP que no tenga “ocupado” el puerto que nos interesa. Si el puerto y la dirección están en uso, siempre podremos usar el otro protocolo de transporte, suponiendo, nuevamente, que esté libre (y nos interese, claro).

En definitiva, esta tupla (recordemos: ip origen, puerto origen, ip destino, puerto destino, protocolo de transporte) le permiten al sistema operativo identificar unívocamente, el proceso que debe encargarse de los datos de aplicación.

Así que, ¿a qué viene el título de la entrada? ¿Qué es reutilizar puertos?

Pues para responder, debemos ver las opciones SO_REUSEADDR y, sobre todo, SO_REUSEPORT cuyo comportamiento es distinto en función del sistema operativo donde las utilicemos (ver [1]).

La opción SO_USEADDR ya la mencioné en esta entrada en la que comentaba los timeouts de TCP; en ella indicaba que, en general, interviene en el tiempo de espera…

antes de poner disponible, de nuevo, un puerto tras su cierre y el objetivo es dejar tiempo suficiente para que los segmentos de esta conexión saliente desaparezcan del sistema

aunque hay más y es lo que voy a tratar en el resto de la entrada junto con la opción SO_REUSEPORT. Ambas son importantes para la programación de Sistemas Distribuidos; y podemos usarlas cuando nos convenga, pero sin olvidarnos que debemos disponer de una versión del núcleo igual o superior a la 3.9.

SO_REUSEADDR, en GNU/Linux (así como en BSD y otros sistemas operativos) introduce, tal y como podemos comprobar en la página man de la función socket [2], la posibilidad de reutilizar la misma combinación IP-Puerto en un servidor, siempre y cuando el puerto no esté en escucha activa (es decir, se haya ejecutado la función listen para ese puerto). Básicamente, nos permite reutilizar puertos UDP, teniendo un comportamiento diferente en otros sistemas como por ejemplo BSD (ver [1]).

SO_REUSEPORT, en GNU/Linux, permite asociar un número arbitrario de sockets a la misma pareja IP-puerto, tanto para TCP como para UDP. Esto solo tiene una limitación (además de que el flag debe estar activo, antes de ejecutar bind, en todos los procesos): todos los sockets deben de pertenecer a procesos con el mismo EUID (identificador de usuario efectivo) para evitar el secuestro de puertos (port hijacking).

¿Qué ventaja aporta? Si consultamos la página man de socket:

this option allows accept load distribution in a multi-threaded 
server to be improved by using a distinct listener socket for each 
thread.  This provides improved load distribution as compared to 
traditional techniques such using a single accepting thread 
that distributes connections, or having multiple threads that 
compete to accept from the same socket.
UDP sockets, the use of this option can provide better distribution
of incoming datagrams to multiple processes (or threads) las compared
to the traditional technique of having multiple processes compete 
to receive datagrams on the same socket.

¿Cómo lo implementamos? Veamos un ejemplo. Si tomamos el código del servidor sencillo descrito en esta entrada y le añadimos la siguiente orden, justo antes de invocar la función bind*:

s.setsockopt(socket.SOL_SOCKET, SO_REUSEPORT, 1)

tras varias ejecuciones del programa, tendremos el resultado de la imagen 1 :

Screenshot from 2015-03-20 10:53:54

Y ¿qué proceso recibe los datos de aplicación? ¿Todos? Pues no. Tanto en TCP como en UDP, el núcleo de GNU/Linux trata de repartir equitativamente el trabajo entre los procesos: las conexiones entrantes para TCP (las reparte equitativamente entre los procesos que están con la ejecución de la función accept en ese puerto) y los datagramas para UDP. En el caso específico de multicast, SO_REUSEADDR tiene el mismo comportamiento que SO_REUSEPORT en conexiones unicast.

Para el ejemplo visto, tras 2 conexiones (una desde el propio equipo y otra desde otro nodo de la red) tenemos que estas se asignan a 2 procesos diferentes (ver imagen 2)

Screenshot from 2015-03-20 10:56:55

¡Espero que os sea útil!

*Solo nos falta definir: SO_REUSEPORT=15

Referencias

1.- Muy completa la respuesta dada en: http://stackoverflow.com/questions/14388706/socket-options-so-reuseaddr-and-so-reuseport-how-do-they-differ-do-they-mean-t

2.- man socket

3.- man setsockopt


Fortificando el acceso con SSH: 2FA con Google Authenticator

Como comentaba al final de la entrada anterior, quien disponga de un hosting propio al que se conecte con sesiones de trabajo también debería estar interesado en fortificar este acceso utilizando un mecanismo 2FA en el servicio SSHGoogle Authenticator es una buena opción, entre otros argumentos, nos centraliza en la misma herramienta el acceso a múltiples servicios: los de Google, Redes Sociales,…

Para el caso que nos ocupa, debemos, para CentOS:

  • Instalar en el servidor el software necesario para la autenticación.
yum install pam-devel make gcc-c++ wget 
wget http://google-authenticator.googlecode.com/files/libpam-google-authenticator-1.0-source.tar.bz2
tar xvf libpam-google-authenticator-1.0-source.tar.bz2 && cd libpam-google-authenticator-1.0
make
sudo make install
  • Este sistema está basado en el tiempo y, por tanto, debemos asegurarnos que la fecha y hora de nuestros sistemas estén sincronizadas. Es un paso MUY IMPORTANTE y, si no lo hacemos, NO FUNCIONARÁ. Para ello, debemos instalar el servidor ntp y configurarlo para que se sincronice con algún servicio accesible desde nuestra red:
yum install ntp
En el fichero /etc/ntp.conf añadimos la información de nuestro servidor de tiempos: server servidor_ntp
service ntpd start
  • Ejecutamos google-authenticator con el usuario que queremos proteger (no tienen que ser todos) Nos generará un enlace al sitio de Google donde encontraremos el código QR con el que debemos, en nuestro móvil y con la app Google Authenticator, crear una cuenta para nuestro acceso SSH igual que comentamos en la entrada anterior para WordPress. Además, nos proporcionará los códigos de un solo uso que debemos usar cuando no tengamos disponible dicha app.
  • Además de la ejecución, deberemos responder a 4 preguntas: mejor ‘Y’ a todas ;).
    • Con la primera, le estamos indicando que queremos que los tokens estén basados en el tiempo y que actualice el fichero HOME/.google_authenticator que es donde almacenará información importante como el enlace y los código mencionados en el punto anterior.
    • En la segunda nos pregunta si queremos deshabilitar el uso del mismo token por diferentes usuarios
    • En la tercera nos indica el tiempo por defecto (30″) para compensar posibles diferencias temporales entre cliente y servidor
    • Y en la última, limita el número de intentos a 3 cada 30″ para evitar ataques de fuerza bruta
  • En el siguiente paso, debemos configurar el módulo PAM del servicio SSH. En el fichero /etc/pam.d/sshd debemos añadir
auth   required  pam_google_authenticator nullok secret=/home/${USER}/.google_authenticator

nullok permite que los usuarios para los que no tengamos configurado el segundo factor de autenticación, puedan entrar al sistema sin solicitarles el código. Con secret le indicamos dónde está el fichero con la información de autenticación.

  • En el fichero /etc/ssh/sshd_config debemos activar (escribir  YES) la opción ChallengeResponseAuthentication.
  • También debemos asegurarnos, en el mismo fichero, que esté UsePAM yes y PubKeyAuthentication no
  • Y finalmente, reiniciar el servicio ejecutando:
service sshd restart

Existen más opciones que nos pueden interesar para afinar el sistema 2FA. Por ejemplo una idea es que cuando accedamos desde determinados sitios (nuestra red local), no nos solicite el segundo factor. Esto se consigue con la instalación (si no está ya) del módulo pam_access y configurando las PAM:

auth [success=1 default=ignore] pam_access.so accessfile=/etc/security/access-local.conf
auth       required     pam_google_authenticator.so nullok secret=/home/${USER}/.google_authenticator

Además, en el fichero /etc/security/access-local.conf debemos escribir algo parecido a esto (donde 10.0.0.0/24 es la red local):

# only allow from local IP range
+: ALL :10.0.0.0/24
+: ALL : LOCAL
-: ALL : ALL

Si queremos que el 2FA NO se aplique a los usuarios de un grupo determinado:

auth [success=1 default=ignore] pam_succeed_if.so user ingroup sudo
auth       required     pam_google_authenticator.so nullok secret=/home/${USER}/.google_authenticator

Si quisiéramos que se aplicara solo a ese grupo, basta con sustituir “user ingroup” por “user notingroup

La información de esta entrada sirve también para proteger otros servicios de sesión de usuario como login, su, sudo, lightdm

¡Espero que os sea útil!

Referencias

  1. https://code.google.com/p/google-authenticator/wiki/PamModuleInstructions

 


Mecanismos de comunicación entre procesos (y V)

En esta última entrada dedicada a los mecanismos IPC veremos las tuberías con nombre o FIFO (First in, First out). El mecanismo se basa en abrir y utilizar el fichero nombrado igual que haríamos con cualquier otro fichero ordinario, donde unos procesos lo abren en modo lectura y otros en modo escritura.

Un ejemplo de proceso lector:

#include <fcntl.h>
#include <stdio.h>
#include <sys/stat.h>
#include <unistd.h>
#define MAX_BUF 1024
int main(){
 int fd;
 char *procPipe = "/tmp/fifo";
 char msj[MAX_BUF];
 /* Open the file*/ 
 fd = open(procPipe,O_RDONLY);
 /* We read the message*/
 read(fd,msj,MAX_BUF);
 /*We show the read message*/ 
 printf("Received message: %sn",msj);
 /* We close the named pipe*/
 close(fd);
return 0;
}

Un ejemplo del escritor:

#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
int main(){
 int fd;
 char *procPipe = "/tmp/fifo";
/*we make de FIFO file*/
 mkfifo(procPipe,0666);
 /*We open the file */
 fd = open(procPipe,O_WRONLY);
 /*We write the message*/ 
 write(fd,"Writer process messagen",sizeof("Writer process messagen"));
 /* We close the named pipe*/
 close(fd);
 /*We delete pipe*/
 unlink(procPipe);
 return 0;
}

Referencias:

  1. Advanced Linux Programming, de Mark Mitchell, Jeffrey Oldham y Alex Samuel

 

 


Mecanismos de comunicación entre procesos (IV)

Pipes

El tercer mecanismo IPC que vamos a tratar en esta saga son las tuberías (Pipes) y cuya principal característica es que son unidireccionales. Tal y como hace el shell en una orden del estilo ls | grep txt, el proceso a la izquierda de ‘|’ escribe en la tubería y el proceso de la derecha, lee.

Para hacer un uso correcto de las tuberías, debemos saber y comprender el funcionamiento  de los descriptores de archivo en el mundo Unix y, en particular, Linux (haremos uso de las funciones pipe y close).

Un ejemplo clásico de uso en programación de este tipo de IPC es cuando tenemos que desarrollar una aplicación que necesita comunicar a un padre con sus procesos hijos (o threads diferentes):

#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
 
#define SIZE 1024
 
int main( int argc, char **argv )
{
    pid_t pid;
    int p[2], readbytes;
    char msg[SIZE];
    int state, i=0;
    pipe(p);
    if ( (pid=fork()) == 0 ){ 
      /* Child process*/
      /* First: We close pipe write side*/
      close(p[1]);
      while((readbytes=read(p[0], msg, SIZE)) > 0)
       /* We write it in stdout*/
          write(1, msg, readbytes);
      close(p[0]);
    }
    else {
        /* Father proccess*/
        /* Firts: We close pipe read side*/
        close(p[0]); 
        while (i<10) {
            sprintf(msg, "Message number %dn", i);
            write(p[1], msg, strlen(msg));
            i++;
        }
        close(p[1]);
        wait(&state);
    }
    exit(0);
}

Las funciones importantes del ejemplo son:

  • pipe: Crea la tubería. Necesita un vector de 2 elementos de enteros. El primero (posición 0) será la salida de la tubería (lectura) y el segundo, la entrada (escritura)
  • close: Cierra los descriptores que no vamos a usar. La lectura en el proceso que escriba y la escritura en el que lea. Además, cuando los procesos terminan, cierran los descriptores abiertos.
  • read y write: usadas para leer y escribir tomando como argumentos los descriptores de archivo apropiados (exactamente igual que haríamos con cualquier fichero)

 Referencias:

  1. Unix Programación avanzada, Fco. Manuel Márquez García

Mecanismos de comunicación entre procesos (III)

En esta entrada vamos a ver en qué consiste el siguiente IPC listado en la primera entrada de esta saga: mapped memory. Con este mecanismo podemos lograr comunicar procesos que se ejecutan en un mismo sistema operativo via un fichero compartido (usualmente este mecanismo se estudia como un modo de acceder de una forma rápida y fácil al contenido de ese fichero). Conceptualmente, es muy parecido a la memoria compartida. La función que se utiliza para mapear un fichero en memoria es mmap(), con el flag MAP_SHARED para que pueda ser compartida por varios procesos, y munmap() para el proceso inverso. El uso de estas funciones se puede consultar en la página man del sistema o en http://man7.org/linux/man-pages/man2/mmap.2.html

Ejemplo de uso

#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#define FILE_LENGTH 0x1000
int main (int argc, char* const argv[])
{
 int fd;
 void* file_memory;
 char msg[10]="";
 int state,pid;
if ((pid=fork()) != -1) {
 if (pid == 0) {
 /* Open the file. */
 fd = open (argv[1], O_RDWR, S_IRUSR | S_IWUSR);
 write (fd, "", 10); /* The file can not be empty*/
 lseek (fd, 0, SEEK_SET);
 /* Create the memory mapping. */
 file_memory = mmap(0, FILE_LENGTH, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
 close (fd);
 printf("Writingn");
 sprintf ((char*)file_memory, "%sn", "Message 1n");
 munmap (file_memory, FILE_LENGTH);
 }
 else {
 /* Open the file. */
 printf("Opening %sn", argv[1]);
 fd = open (argv[1], O_RDWR, S_IRUSR | S_IWUSR);
 lseek (fd, 0, SEEK_SET);
 /* Read the integer, print it out, and double it. */
 file_memory = mmap(0, FILE_LENGTH, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
 if (file_memory!= NULL) {
 close (fd);
 printf("Wating child endn");
 /*wait(&state);*/
 sleep(1);
 printf("Readingn");
 sscanf(file_memory, "%9s", msg);
 printf("I've read: %sn", msg);
 sprintf ((char*)file_memory, "%sn", "Reaaadn");
 /* Release the memory */
 munmap (file_memory, FILE_LENGTH);
 wait(&state);
 }
 else {
 printf("mmap error");
 }
 }
 }
 else {
 printf("Error making child");
 }
exit(0);
}

Al ejecutar este código, podemos ver como la variable msg toma, en el código ejecutado por el padre,  el valor escrito en la memoria por el hijo (“Message“).

Referencias:

  1. Advanced Linux Programming, de Mark Mitchell, Jeffrey Oldham y Alex Samuel

Mecanismos de comunicación entre procesos (II)

Los mecanismos IPC del Unix System V (semáforos para sincronizar, memoria compartida y colas de mensajes) están implementados como una unidad y, por tanto, comparten características comunes de tal forma que, cada mecanismo dispone de una función:

  • get para crear o recuperar uno de estos elementos de comunicación (por ejemplo, para C, shmget, semget, msgget)
  • de control que permite leer y modificar el estado del elemento.

Existen más características que se pueden consultar en la documentación referenciada. Existen, además, 2 utilidades que nos permiten, desde el shell, obtener información y controlar los mecanismos usados en un sistema GNU/Linux concreto: ipcs e ipcrm. El primero sirve para saber los IPCs asignados junto con cierta información. El segundo permite borrarlos. Para saber más sobre ellos: man ipcs o man ipcrm.

Shared Memory

Quizás sea la forma más rápida de comunicar 2 procesos ya que para ello, solo debemos escribir en la zona de memoria compartida por ambos. Para poder utilizar este mecanismo, tenemos las funciones (en C):

  • shmget para crear (o habilitar si ya lo está) una zona de memoria
  • shmctl para acceder y modificar la información administrativa y de control asociada a la memoria
  • shmat para asociar una zona de memoria a un proceso
  • shmdt para desasociar la memoria del proceso

Para sincronizar el acceso a la memoria compartida entre distintos procesos debemos usar un mecanismo de sincronización: semáforos.

Ejemplo de uso

 int segment_id;
 char* shared_memory;
 struct shmid_ds shmbuffer;
 int segment_size;
 const int shared_segment_size = 0x6400;
 
 /* Allocate a shared memory segment. */
 segment_id = shmget (IPC_PRIVATE, shared_segment_size, IPC_CREAT | IPC_EXCL | S_IRUSR | S_IWUSR);
 /* Attach the shared memory segment. */
 shared_memory = (char*) shmat (segment_id, 0, 0);
 printf ("shared memory attached at address %pn"€, shared_memory);
 /* Determine the segment'™s size. */
 shmctl (segment_id, IPC_STAT, &shmbuffer);
 segment_size = shmbuffer.shm_segsz;
 printf ("€œsegment size: %dn"€, segment_size);
 /* Write a string to the shared memory segment. */
 sprintf (shared_memory, "€œHello, world"€);
 /* Detach the shared memory segment. */
 shmdt (shared_memory);
 /* Reattach the shared memory segment, at a different address. */
 shared_memory = (char*) shmat (segment_id, (void*) 0x5000000, 0);
 printf ("shared memory reattached at address %pn"€, shared_memory);
 /* Print out the string from shared memory. */
 printf ("€œ%sn"€, shared_memory);
 /* Detach the shared memory segment. */
 shmdt (shared_memory);
 /* Deallocate the shared memory segment.
 shmctl (segment_id, IPC_RMID, 0);

Referencias:

  1. Advanced Linux Programming, de Mark Mitchell, Jeffrey Oldham y Alex Samuel
  2. Unix Programación avanzada, Fco. Manuel Márquez García
  3. Para Python: https://docs.python.org/2/library/multiprocessing.html

Mecanismos de Comunicación entre procesos (I)

En un estado normal de un sistema operativo, pueden cohabitar diferentes procesos en ejecución y que necesiten comunicarse. Por ejemplo, en esta entrada vimos que para la implementación de un sencillo servidor concurrente, podíamos crear un proceso hijo encargado de atender la petición del cliente. Dependiendo del servicio, podríamos necesitar la intercomunicación del proceso padre con los hijos* o, incluso, entre hijos.

En la próxima serie de entradas, comentaré los distintos tipos de IPC** que existen, excepto uno: sockets, porque de éste, ya hemos hablado aquí, aquí y aquí):

  • Shared Memory: Memoria compartida entre procesos que les permite comunicarse leyendo y escribiendo en una zona específica de memoria. Quizás sea el mecanismo IPC más simple de comunicación.
  • Mapped Memory: similar al anterior pero asociado a un fichero.
  • Pipes: canal secuencial entre procesos (es la ‘barra’ de esta orden: ‘ls | grep *.exe’)
  • FIFO: similar a las tuberías excepto que los procesos no tienen que estar relacionados debido a que la tubería se “construye” sobre un nombre en el sistema de archivos.

Si nos fijamos, la principal diferencia entre los 5 tipo de IPC (los 4 anteriores más los sockets) está en (1) función de si los procesos están o no relacionados (o si están en el mismo nodo de red), (2) si pueden escribir, leer o ambos y (3) el número de procesos que pueden comunicar.

Referencias:

  1. Advanced Linux Programming, de Mark Mitchell, Jeffrey Oldham y Alex Samuel

* En este caso no me refiero a, simplemente, saber el estado de finalización de los hijos que, en si mismo, podríamos aceptar como comunicación entre procesos aunque muy simple.

** Hay quien considera las señales como un mecanismo IPC. En mi opinión, éstas están destinadas a proporcionar una forma de control sobre un proceso, no como un mecanismo IPC. Además, debemos tener en cuanta que no contienen datos a excepción de ella misma (la señal que es) y, si lanzamos una gran cantidad de ellas, el sistema se verá interrumpido constantemente por algo que, encima, necesita de un tratamiento especial.

 


Timeouts de TCP en GNU/Linux y opciones para “afinar” un servidor

Los tiempos de espera de TCP vistos en la entrada anterior se pueden consultar y modificar en GNU/Linux a través de los ficheros apropiados del /proc y, para afinar un servidor y mejorar sus prestaciones en base a estos timeout, podremos modificar sus valores en este sistema de archivos sin necesidad de recompilar el núcleo. Pero, antes de nada, si lo que queremos es afinar nuestro sistema, lo que debemos es de plantearnos es esta pregunta: ¿cuántos puertos concurrentes podemos tener abiertos? Para ello:

Leer más


Timeouts de TCP

En TCP(1) existen 4 tipos de timeouts (tiempos de espera) y cada uno de ellos se aplica en diferentes momentos y con diferente duración. Algunos, incluso, podemos evitarlos si es lo que deseamos…

Conocer estos tiempos es importante tanto para administradores de sistemas como para programadores de aplicaciones distribuidas (¡¡¡pocas no lo son ya!!!)

Leer más


Las cookies nos permiten ofrecer nuestros servicios. Al utilizar nuestros servicios, aceptas el uso que hacemos de las cookies. Más información.