En los títulos y los textos vais a encontrar unas cuantas citaciones cinematográficas (y si, soy un cinéfilo). Si no os interesan podéis fingir no verlas, ya que no son fundamentales para la comprensión de los post...

Este blog es la versión en Español de mi blog en Italiano L'arte della programmazione in C. Espero que mis traducciones sean comprensibles...

sábado, 17 de febrero de 2018

The Test Connection
cómo testear la conectividad en C

La Test Connection de este post es un poco menos peligrosa que la French Connection de la cual habla la obra maestra de W.Friedkin. Pero sigue siendo una actividad muy importante en algunos tipos de aplicaciones, entonces ni hablar de subestimarla.
...caras de conectividad avanzada...
Cuando utilizamos un computer interactivamente y queremos verificar el estado de la red, tenemos muchas formas sencillas de hacerlo: por ejemplo, con un buen ping a google.com sabemos si estamos conectados a Internet y, si no funciona, podemos utilizar algunas utilidades del sistema para averiguar lo que esta mal. Supongamos, sin embargo, tener que desarrollar una aplicación para un sistema embedded (¿somos o no somos programadores C?) y esta aplicación está conectada a la red y, en el caso de que no haya comunicación, debe informar de alguna manera (yo que se, encender un led error, ¡ciertamente no enviando un mensaje en la red!). Ok, y... ¿cómo lo hacemos? Veamos, ¡vamos con el código!
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <ifaddrs.h>
#include <net/if.h>
#include <sys/ioctl.h>
#include <linux/ethtool.h>
#include <linux/sockios.h>
#include <arpa/inet.h>

int main(int argc, char *argv[])
{
    int sock;

    // test interfaces
    //

    // get de todos los network devices configurados
    struct ifaddrs *addrs;
    getifaddrs(&addrs);

    // loop sobre los network devices configurados
    int i_alr = 0;
    struct ifaddrs *tmpaddrs = addrs;
    while (tmpaddrs) {
        // test interface
        if (tmpaddrs->ifa_addr && tmpaddrs->ifa_addr->sa_family == AF_PACKET) {
            // abre un socket para el test
            if ((sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)) < 0) {
                // enseña error y continua
                printf("test interfaces: error socket para la interface %s: %s\n",
                       tmpaddrs->ifa_name, strerror(errno));
                continue;
            }

            // prepara los dati para ioctl()
            struct ethtool_value edata;
            edata.cmd = ETHTOOL_GLINK;
            struct ifreq ifr;
            strncpy(ifr.ifr_name, tmpaddrs->ifa_name, sizeof(ifr.ifr_name) - 1);
            ifr.ifr_data = (char *)&edata;

            // esegue ioctl()
            if (ioctl(sock, SIOCETHTOOL, &ifr) == -1) {
                // error ioctl: cierra el socket y continua
                printf("test interfaces: error ioctl para la interface %s: %s\n",
                       tmpaddrs->ifa_name, strerror(errno));
                close(sock);
                continue;
            }

            // enseña los resultados y cierra el socket
            printf("test interfaces: interface %s: %s\n",
                   tmpaddrs->ifa_name, edata.data ? "OK" : "NOK");
            close(sock);
        }

        // pasa al proximo device
        tmpaddrs = tmpaddrs->ifa_next;
    }

    // libera la devices list
    freeifaddrs(addrs);

    // test conectividad
    //

    // abre un socket para el test
    if ((sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) {
        // error socket
        printf("test de conectividad: socket error: %s\n", strerror(errno));
        return EXIT_FAILURE;
    }

    // set de un timeout para send() (en este caso en realidad lo usa connect()) para evitar
    // un bloqueo sobre error de conexion)
    struct timeval tv;
    tv.tv_sec  = 1;     // set timeout en segundos
    tv.tv_usec = 0;     // set timeout en usegundos
    if (setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, (char*)&tv, sizeof(tv)) < 0) {
        // error ioctl
        printf("test de conectividad: setsockopt error: %s\n", strerror(errno));
        close(sock);
        return EXIT_FAILURE;
    }

    // NOTE: alternativa (non portable) a el uso de SO_SNDTIMEO:
    //int syn_retries = 1;     // send de un total de 1 SYN packets => timeout ~2s
    //if (setsockopt(sock, IPPROTO_TCP, TCP_SYNCNT, &syn_retries, sizeof(syn_retries)) < 0) {
    //  ...

    // prepara la estructura sockaddr_in para el server remoto
    struct sockaddr_in server;                  // server (remoto) socket info
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;                // set familia direcciones
    server.sin_addr.s_addr = inet_addr("216.58.214.174");   // set direccion server
    server.sin_port = htons(80);                // set numero port server

    // conexion al server remoto
    if (connect(sock, (struct sockaddr *)&server, sizeof(server)) < 0) {
        // error connect
        printf("test de conectividad: error connect: %s\n", strerror(errno));
        close(sock);
        return EXIT_FAILURE;
    }

    // enseña los resultados ed esce
    printf("test de conectividad: conectividad OK\n");
    close(sock);
    return EXIT_SUCCESS;
}
una premisa: este código está destinado a aplicaciones Linux Embedded, por lo que todo lo que sigue se refiere a un entorno Linux (bueno, como siempre, y es inútil explicar otra vez el tema...). El código es ampliamente comentado, así que no tengo que escribir demasiadas explicaciones. En este ejemplo, el código de test se escribe directamente en el main() pero, en un proyecto real, debería transformarse en una función que se puede llamar periódicamente en la posición más adecuada, tal vez directamente en el main() de la aplicación. Dado el tipo de prueba que se realiza, se debe llamar ocasionalmente, en función de cuanto de inmediata debe ser la señal de alarma que necesitamos. Normalmente, el uso es de baja frecuencia (hablamos de segundos, no de milisegundos), de lo contrario, nuestra función consumiría mucho tiempo de CPU solo para verificar la conectividad (y este no parece ser el caso).

el test se lleva a cabo en dos fases: test de las interfaces de red y test de conectividad. La primera comprueba eventuales problemas, digamos, Hardware: si tengo una conexión WiFi y una Ethernet en la misma máquina podría tener una conexión a Internet incluso si, por ejemplo, el cable de red está desconectado, y me interesa informar de esta situación, por lo que un único test de conexión no sería suficiente. La segunda fase verifica la conectividad real y, en caso de que falte, podemos saber si no hay conectividad a pesar de que las interfaces de red están bien conectadas, así de aislar mejor las posibles causas (en resumen, es un test bastante completo, pero se puede sofisticar aun más).

La fase de test de las interfaces se realiza con una función (relativamente nueva) de la siempre indispensable (en casos como este) ioctl(). Gracias a la función SIOCETHTOOL podemos verificar el funcionamiento de bajo nivel de cada interfaz, ya que nuestro código incluye un loop con el que analizamos todas las interfaces que, normalmente, son dos (loopback y Ethernet) y, a veces, más (WiFi, otra tarjeta de red, etc.). La interfaz de loopback está siempre presente y, si no queremos probarla, se puede omitir haciendo un simple test sobre el nombre (que siempre es "lo", pero en cualquier caso, el nombre puede verificarse con antelación, para evitar malentendidos, en el file /etc/network/interfaces).

El test de conectividad se basa, en vez, en una simple conexión de tipo Client a una dirección "segura": En el ejemplo he usado IP y Port de google.com, pero se puede usar la que parece más apropiada, por ejemplo, para un dispositivo conectado solo a la red local, se puede usar la conexión a un servidor en la red, o bien se puede conectar a la dirección del gateway local, etc. También depende de si lo que tenemos que probar es conectividad a Internet o una simple conectividad local. Tener en cuenta que todo funciona usando la system call connect(), que implementa el envío de datos y la recepción de una respuesta, por lo que es un test más que suficiente (sin recurrir a un mucho más complicado ping). Ya que connect() se bloquea durante mucho tiempo cuando no recibe una respuesta inmediata, he añadido un oportuno timeout (leer el comentario en el código) utilizando setcsockopt() + SO_SNDTIMEO, pero (leer el otro comentario) también se puede usar el flag TCP_SYNCNT, que es, pero, una solución menos ortodoxa y menos portátil.

En un normal PC (en lugar de un sistema embedded) con una conexión a Internet a través de Ethernet, el resultado en condiciones normales es el siguiente:
aldo@mylinux:~/blogtest$ ./conntest 
test interfaces: interface lo: OK
test interfaces: interface enp3s0: OK
test de conectividad: conectividad OK
y, si forzamos la desconexión del Software (utilizando, por ejemplo, el NetworkManager de Linux) el resultado es:
aldo@mylinux:~/blogtest$ ./conntest 
test interfaces: interface lo: OK
test interfaces: interface enp3s0: OK
test de conectividad: error connect: Network is unreachable
mientras que si forzamos la desconexión Hardware (desconectando el cable de red) el resultado es:
aldo@mylinux:~/blogtest$ ./conntest 
test interfaces: interface lo: OK
test interfaces: interface enp3s0: NOK
test de conectividad: error connect: Network is unreachable
No está mal, ¿verdad? Una función simple simple pero muy útil. ¡Y por hoy ya està, misión cumplida!

¡Hasta el próximo post!