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:
send
retornava erro. Enviar bytes a mais nesse caso não é problemático, desde que do outro lado, eles sejam ignorados.ethtool
. Uma forma de evitar o problema do VLAN é colocar um byte 0xff
após bytes que identificam o protocolo VLAN (0x88
e 0x81
) e removê-los do outro lado do fio.recv
.
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.