====== Sockets Crus ======
Uma API para comunicação em rede disponível em sistemas operacionais são os //[[http://wiki.inf.ufpr.br/maziero/doku.php?id=pua:comunicacao_em_rede&s[]=socket|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 [[https://antixlinux.com/|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
#include
#include
#include
#include
#include
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 [[https://en.wikipedia.org/wiki/IEEE_802.1ad|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 byte ''0xff'' após bytes que identificam o protocolo VLAN (''0x88'' e ''0x81'') 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//.