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:
- Algumas placas de rede possuem algumas regras totalmente arbitrárias para envio e recebimento de mensagens. Uma exigência encontrada é por exemplo que o tamanho mínimo de um pacote enviado seja de 14 bytes. Menos que isso, a API
send
retornava erro. Enviar bytes a mais nesse caso não é problemático, desde que do outro lado, eles sejam ignorados. - Não é garantido que um pacote seja enviado inteiro, isto é, que ele não seja enviado em vários pedaços, de forma fragmentada. Também não é garantido que ele seja recebido inteiro. Geralmente isso acontece porque enviamos pacotes de tamanho não muito grande, cabendo inteiro no buffer da placa de rede, mas não é garantido. A solução é possibilitar o recebimento de pacotes parciais.
- Placas de rede podem e vão comer pacotes de rede caso elas identifiquem que podem. Um exemplo clássico é o fato das placas de redes comerem pacotes do protocolo VLAN. Geralmente isso é feito como forma de aliviar o trabalho do sistema operacional, deixando certas tarefas a cargo da própria placa de rede (offloading), e o que a sua placa pode fazer geralmente será listado pela ferramenta
ethtool
. Uma forma de evitar o problema do VLAN é colocar um byte0xff
após bytes que identificam o protocolo VLAN (0x88
e0x81
) e removê-los do outro lado do fio. - O sistema operacional quando detecta uma conexão a cabo, tenta realizar configurações automáticas de rede. Quando se conecta dois computadores e não se configura uma rede, essa configuração vai falhar. Porém, como a configuração é feita através de pacotes enviados na rede, estes pacotes vão aparecer como lixos ocasionais nas chamadas
recv
.
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 socket dê timeout.