Sockets Crus

Uma API para comunicação em rede disponível em sistemas operacionais são os sockets. Eles geralmente abstraem protocolos de rede como o protocolo TCP, criando uma comunicação bidirecional permitindo a comunicação através de funções send e recv. Nesta disciplina, estamos interessados em criar o nosso próprio protocolo de rede, evitando abstrações ao máximo possível. Para tal, podemos utilizar sockets raw, que são “crus”, que nos permitem interagir com a placa de rede quase que diretamente, lendo e escrevendo bytes na rede.

Essa interação mais direta com a placa de rede traz riscos a segurança: Qualquer programa pode escrever bytes arbitrários na placa de rede, passando por cima de quaisquer filtros e firewalls presentes no sistema operacional. Além disso, é possível também ler todos os pacotes que estão passando pelo fio desta maneira, o que traz riscos para o usuário que usa aplicações que enviam pacotes descriptografados. Assim, o uso de sockets crus é limitado ao usuário root (ou com capability CAP_NET_RAW) em sistemas Linux. Esse é o motivo, por exemplo, do programa ping rodar como root via setuid, pois utiliza sockets crus para mandar pacotes ICMP.

O resultado é que para rodar aplicações utilizando sockets crus, você vai precisar de um computador com acesso a root. Nos laboratórios do departamento, a maneira mais fácil de fazer isso é utilizando uma distribuição Linux em um pen drive. Qualquer uma pode ser utilizada, sendo uma opção a distribuição antiX Linux, que permite que o sistema operacional inteiro rode em memória, permitindo que o pen drive seja retirado depois da inicialização (o que permite utilizar o mesmo pen drive para fazer setup de dois ou mais computadores!).

Um exemplo de utilização de sockets crus em C segue. A função cria_raw_socket recebe como parâmetro o nome da interface de rede e retorna um descritor de arquivo do socket cru. O nome da interface de rede pode ser descoberto através do comando ip addr que lista todas as interfaces de rede de um sistema Linux. Interfaces de rede de cabo geralmente tem nome da forma enp3s0 ou eth0, e a interface de rede de loopback, que sempre retorna as mensagens enviadas para si mesma geralmente é chamada de lo. As interfaces de rede wireless também podem ser utilizadas, porém o comportamento não é previsível como as de cabo.

#include <arpa/inet.h>
#include <net/ethernet.h>
#include <linux/if_packet.h>
#include <net/if.h>
#include <stdlib.h>
#include <stdio.h>
 
int cria_raw_socket(char* nome_interface_rede) {
    // Cria arquivo para o socket sem qualquer protocolo
    int soquete = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
    if (soquete == -1) {
        fprintf(stderr, "Erro ao criar socket: Verifique se você é root!\n");
        exit(-1);
    }
 
    int ifindex = if_nametoindex(nome_interface_rede);
 
    struct sockaddr_ll endereco = {0};
    endereco.sll_family = AF_PACKET;
    endereco.sll_protocol = htons(ETH_P_ALL);
    endereco.sll_ifindex = ifindex;
    // Inicializa socket
    if (bind(soquete, (struct sockaddr*) &endereco, sizeof(endereco)) == -1) {
        fprintf(stderr, "Erro ao fazer bind no socket\n");
        exit(-1);
    }
 
    struct packet_mreq mr = {0};
    mr.mr_ifindex = ifindex;
    mr.mr_type = PACKET_MR_PROMISC;
    // Não joga fora o que identifica como lixo: Modo promíscuo
    if (setsockopt(soquete, SOL_PACKET, PACKET_ADD_MEMBERSHIP, &mr, sizeof(mr)) == -1) {
        fprintf(stderr, "Erro ao fazer setsockopt: "
            "Verifique se a interface de rede foi especificada corretamente.\n");
        exit(-1);
    }
 
    return soquete;
}

Neste código, utilizamos também o modo promíscuo, que faz com que protocolos não identificados pelo sistema operacional também sejam lidos pelo socket que criamos. Sem essa opção, o sistema operacional inspeciona cada pacote, procurando apenas protocolos válidos cujo destino é a própria máquina. Como o intuito é definir nosso próprio protocolo que o sistema operacional desconhece, precisamos deste modo para que o nosso código funcione.

Algumas ressalvas importantes sobre os sockets crus:

Timeouts

Assim como sockets normais, sockets crus também podem ter timeouts. Timeouts são maneiras de evitar que uma chamada ao socket, geralmente a recv, bloqueie o sistema por tempo indeterminado. Para tal, é apenas necessário que o socket seja configurado para que depois de um determinado intervalo de tempo, um recv simplesmente falhe. Isso pode ser feito através do setsockopt e a opção SO_RCVTIMEO. Uma estrutura de tempo deve ser passada informando qual o timeout desejado. Lembre-se que o segundo campo da estrutura é em microsegundos, e que os segundos devem ser corretamente especificados no campo de segundos.

const int timeoutMillis = 300; // 300 milisegundos de timeout por exemplo
struct timeval timeout = { .tv_sec = timeoutMillis / 1000, .tv_usec = (timeoutMilis % 1000) * 1000 };
setsockopt(soquete, SOL_SOCKET, SO_RCVTIMEO, (char*) &timeout, sizeof(timeout));

Sempre verifique a saída do recv, que irá retornar -1 caso dê o sockettimeout.