Codificando uma pilha TCP/IP 1: Ethernet e ARP (2016)
(saminiir.com)- Como primeiro passo de uma série que ensina camadas de rede construindo diretamente uma pilha TCP/IP em espaço de usuário no Linux, a implementação começa pelo tratamento de quadros Ethernet da camada 2 e pelas respostas ARP
- O tráfego de rede de baixo nível do kernel é recebido por meio de um dispositivo TAP, e o descritor de arquivo retornado permite fazer
read/writeno buffer Ethernet do dispositivo virtual - O cabeçalho Ethernet é tratado com
dmac,smac,ethertypeepayload, e quando o valor deethertypeé 1536 ou maior ele indica o tipo do payload; quando é menor, indica o comprimento do payload - O ARP mapeia dinamicamente endereços de protocolo como endereços IPv4 para endereços MAC de 48 bits, e a implementação segue o fluxo de atualizar a tabela de conversão ao responder a requisições
- No teste com
arping, a resposta ARP da pilha personalizada é reconhecida pelo kernel Linux, adicionando a entrada10.0.0.4ao cache ARP da interfacetap0
Ponto de partida de uma pilha TCP/IP em espaço de usuário
- O objetivo é implementar uma pilha TCP/IP mínima em espaço de usuário no Linux para entender mais profundamente redes e programação de sistemas
- O TCP acumulou várias especificações ao longo de mais de 30 anos e é complexo, mas seus elementos centrais de implementação podem ser reduzidos a parsing do cabeçalho TCP, máquina de estados, controle de congestionamento e cálculo de timeout de retransmissão
- Ethernet e IP têm complexidade menor que TCP, então a série começa a implementação pela camada 2
Recebendo tráfego Ethernet com um dispositivo TAP
- Para interceptar o tráfego de rede de baixo nível do kernel Linux, é usado um dispositivo TAP do Linux
- Dispositivos TUN/TAP são frequentemente usados quando aplicações de rede em espaço de usuário manipulam tráfego L3/L2, respectivamente
- Tunelamento é a técnica de encapsular um pacote dentro do payload de outro pacote
- Programas como o OpenVPN também usam dispositivos TUN/TAP
- Como a pilha de rede é construída a partir da camada 2, é necessário um dispositivo TAP, não TUN
- O dispositivo TAP é criado abrindo
/dev/net/tape usandoioctl(fd, TUNSETIFF, ...)IFF_TAPseleciona um dispositivo TAPIFF_NO_PIevita que informações extras de pacote sejam anexadas antes do quadro Ethernet
- Depois da criação, o descritor de arquivo retornado
fdé usado para ler e gravar no buffer Ethernet do dispositivo virtual
Formato do quadro Ethernet
- Ethernet é a tecnologia básica de conexão de computadores em LANs, e o primeiro padrão Ethernet foi publicado em 1980 por Digital Equipment Corporation, Intel e Xerox
- A primeira versão usava velocidade de cerca de 10Mb/s e comunicação half-duplex, por isso precisava de um protocolo MAC para coordenar o fluxo de dados
- Em interfaces Ethernet half-duplex, o CSMA/CD era necessário como método MAC
- O 100BASE-T usa cabeamento twisted-pair para permitir comunicação full-duplex e maior throughput
- Com a ampla adoção de switches Ethernet, a necessidade de CSMA/CD em grande parte diminuiu
- O padrão Ethernet é mantido pelo grupo de trabalho IEEE 802.3
O cabeçalho Ethernet usado na implementação
- A implementação inclui o
if_ether.hdo Linux para usar o mapeamento entreethertypee valores hexadecimais - O cabeçalho Ethernet é representado em uma struct C com os seguintes campos
dmac: endereço MAC de destinosmac: endereço MAC de origemethertype: comprimento ou tipo do payloadpayload: ponteiro para o payload que contém um pacote ARP ou IPv4
ethertypeé um campo de 2 octetos e seu significado muda conforme o valor- Se o valor for 1536 ou maior, ele indica o tipo do payload, como IPv4 ou ARP
- Se o valor for menor, ele indica o comprimento do payload
- Quadros Ethernet podem ter tags que indicam VLAN ou QoS, mas esta implementação exclui tags de quadro
- Se o comprimento do payload for menor que o mínimo exigido de 48 bytes sem tags, bytes de padding são adicionados ao final
- No fim do formato do quadro Ethernet há o campo Frame Check Sequence, que verifica integridade com CRC, mas ele não é tratado nesta implementação
Como os quadros Ethernet são parseados
- O atributo
packedna declaração da struct impede que o compilador GNU C otimize o layout de memória da struct com bytes de padding para alinhamento de dados - O parsing da implementação faz type casting do buffer para a struct de protocolo apropriada
- Exemplo:
struct eth_hdr *hdr = (struct eth_hdr *) buf;
- Exemplo:
- Uma abordagem mais portável seria serializar manualmente os dados do protocolo
- Nesse caso, o compilador pode adicionar bytes de padding para atender aos requisitos de alinhamento de dados específicos do processador
- O processamento de quadros Ethernet recebidos segue um fluxo simples
- Ler um buffer do dispositivo TAP
- Inicializar o cabeçalho Ethernet com
init_eth_hdr(buf) handle_frame(&netdev, hdr)decide a próxima ação com base no valor deethertype
Estrutura e papel dos pacotes ARP
- ARP (Address Resolution Protocol) mapeia dinamicamente endereços de protocolo, como endereços IPv4, para endereços MAC, que são endereços Ethernet de 48 bits
- O ARP não é limitado ao IPv4 e pode ser usado com vários protocolos L3
- Como exemplo, CHAOS define um endereço de protocolo de 16 bits
- Em comunicações LAN comuns, mesmo conhecendo o endereço IP do serviço, ainda é necessário um endereço MAC para a transmissão real
- O ARP envia uma consulta em broadcast pela rede para que o dono daquele endereço IP informe seu endereço de hardware
Cabeçalho ARP e payload para IPv4
- O cabeçalho ARP é composto pelos seguintes campos
hwtype: campo de 2 octetos que indica o tipo da camada de enlace; para Ethernet, o valor é0x0001protype: campo de 2 octetos que indica o tipo de protocolo; para IPv4, o valor é0x0800hwsize: campo de 1 octeto que indica o tamanho do endereço de hardware; um endereço MAC tem 6 bytesprosize: campo de 1 octeto que indica o tamanho do endereço de protocolo; um endereço IPv4 tem 4 bytesopcode: campo de 2 octetos que indica o tipo da mensagem ARP
- Os valores de
opcodese dividem em quatro tipos- ARP request:
1 - ARP reply:
2 - RARP request:
3 - RARP reply:
4
- ARP request:
- Os dados ARP para IPv4 são tratados pela struct
arp_ipv4smac: endereço MAC do remetentesip: endereço IP do remetentedmac: endereço MAC do destinatáriodip: endereço IP do destinatário
Algoritmo de resolução de endereços e cache
- O algoritmo de resolução de endereços da RFC 826 segue o fluxo de verificar o tipo de hardware e o tipo de protocolo, atualizar a tabela de conversão e, se o endereço de destino for o próprio host, gerar uma resposta
- A translation table armazena os resultados do ARP para que o host possa consultar entradas já conhecidas no cache
- Esse cache reduz o preenchimento desnecessário da rede com requisições ARP duplicadas
- O código da implementação está em
arp.c
Teste da resposta ARP e próximos passos
- O teste final da implementação de ARP é verificar se ela responde corretamente às requisições
- Ao executar
arping -I tap0 10.0.0.4, volta uma resposta unicast de10.0.0.4com o endereço MAC00:0C:29:6D:50:25 - Depois disso, na saída de
arp, aparece a entrada10.0.0.4 ether 00:0c:29:6d:50:25 tap0no cache ARP do kernel Linux - Mesmo com apenas o tratamento mínimo de quadros Ethernet e a implementação de ARP, já é possível confirmar que o dispositivo Ethernet personalizado preenche o cache ARP do host Linux
- O código-fonte do projeto está no GitHub, e o próximo passo é implementar o parsing de pacotes IPv4 e o ping, que corresponde a ICMP echo/reply
1 comentários
Comentários do Hacker News
Alguns anos atrás, criei uma pilha de rede em espaço de usuário em C, processei pacotes brutos por meio de uma interface TUN e consegui fazê-la funcionar até certo ponto
Hoje ela inclui um shell simples que permite configurar endereços IP, rotas etc., e os pacotes de rede são colocados em uma estrutura híbrida que parece uma mistura de mbuf com sk_buf
No entanto, depois de terminar a implementação de UDP, não tive tempo nem motivação para implementar TCP, e o código está aqui: https://github.com/cakturk/unet
Naturalmente, era provavelmente uma das coisas mais lentas e frágeis da história, mas funcionava de verdade e foi bem divertido. Espero que esse código ainda esteja em algum lugar
O “porte POSIX” do lwip também obtém bytes Ethernet brutos de um dispositivo TUN/TAP da mesma forma
https://github.com/lwip-tcpip/lwip/blob/master/contrib/ports...
Se você compilar um kernel Linux mínimo sem pilha TCP/IP, ele fica com 400 KB; com a pilha TCP/IP, fica com 800 KB
Em um projeto que só precisava enviar a temperatura, enviamos o valor dentro de uma mensagem UDP feita à mão por um pequeno programa em C no espaço de usuário, e isso reduziu bastante o espaço e a complexidade
Se você desativar o ARP, pode configurar o mesmo IP em vários servidores na mesma rede
Se um servidor atuando como frontend de roteamento conseguir encaminhar pacotes para a interface de rede de um servidor backend com base no endereço MAC, esse backend vai reconhecer que ele é o destino e poderá responder diretamente ao cliente, trocando os IPs de origem/destino. Nesse caso, a resposta não passa de novo pelo frontend de roteamento
Ou, sem desligar o ARP, dá para obter o mesmo efeito adicionando o endereço IP comum como um alias da interface loopback, permitindo que o backend reconheça a si mesmo como destino e evite conflitos de ARP. Era um truque usado pelo balanceador de carga por software IBM WebSphere nos anos 90 e 2000
A vantagem em relação ao balanceamento de carga L3 mais comum é que não é necessário reescrever o cabeçalho do pacote IP
Portanto, se for usar essa abordagem, é melhor criar uma VLAN dedicada
Fiz algo parecido em Python: https://github.com/georgek/notebooks/blob/master/internet.ip...
Provavelmente a qualidade do código é pior e, para ser sincero, eu simplesmente inventei o algoritmo de resolução de endereços. Consegui chegar ao ponto de enviar ping para hosts na internet com ICMP
Gosto do fato de caber inteiro em um notebook curto. O texto original omite no corpo muitos detalhes que estão no código-fonte maior referenciado
Não vi esse artigo; fiz só olhando a Wikipedia. Mas a partir do TCP a complexidade aumenta bastante, e perdi um pouco o interesse. Como a parte 3 trata disso, talvez um dia eu leia e termine. Se você tem interesse em redes, acho que é uma tarefa que vale a pena e é gratificante para programadores de qualquer nível
Alguns anos atrás, trabalhei com instrumentação de usinas nucleares. O desenvolvimento do lado cliente era feito em estações de trabalho Sun, e fui contratado justamente por causa da minha experiência com TCP/IP, que eu tinha adquirido na disciplina de “sistemas operacionais” da CMU
Já o computador da usina era um minicomputador sem pilha TCP/IP, então aquela equipe teve de criar a própria pilha
Logo no primeiro minuto do texto ele diz que “dmac e smac são campos bastante autoexplicativos”, mas leitores que não sabem o que isso significa podem desistir ali mesmo
Eles acabam pensando: “este artigo é para pessoas para quem esses campos são óbvios. Não é para mim, então é melhor parar de ler”
Além disso, em um artigo sobre criar uma pilha de rede, é seguro presumir que o leitor saiba pelo menos um pouco sobre redes
Posts relacionados:
Let’s code a TCP/IP stack (2016) - https://news.ycombinator.com/item?id=27654182 - junho de 2021, 49 comentários
Let’s code a TCP/IP stack, 1: Ethernet & ARP (2016) - https://news.ycombinator.com/item?id=17316487 - junho de 2018, 47 comentários
Let’s Code a TCP/IP Stack: TCP Retransmission - https://news.ycombinator.com/item?id=14701199 - julho de 2017, 30 comentários
Let’s code a TCP/IP stack, 1: Ethernet and ARP - https://news.ycombinator.com/item?id=11234229 - março de 2016, 49 comentários
Não sei de onde o autor tirou o endereço IP 10.0.0.4 que usa no teste de análise de ARP
É o endereço de quê? É um dispositivo falso acessível a partir do dispositivo Ethernet falso criado aqui, ou é um dispositivo que realmente existe na rede do autor?
Um dispositivo TAP é como um link Ethernet emulado por software. Quando você envia pacotes para ele, eles são entregues diretamente a um programa em nível de usuário, e esse programa decide qual endereço IP ele terá e como responderá a ARP
Normalmente, o sistema operacional trata disso, e é preciso ter privilégios de root para adicionar um endereço IP à interface. O mesmo vale para abrir um dispositivo TAP. Redes em geral funcionam de forma cooperativa, e um agente malicioso com privilégios de root na rede pode fazer coisas ruins
Pelo que me lembro, ARP só funciona no segmento local. O roteador preenche o próprio endereço e encaminha o pacote
Também existe rarp, que é uma das formas de perguntar à “rede” qual é o seu próprio endereço IP. Não sei se o rarp ainda funciona em ambientes reais hoje em dia