1 pontos por GN⁺ 2025-03-05 | 1 comentários | Compartilhar no WhatsApp
  • 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/write no buffer Ethernet do dispositivo virtual
  • O cabeçalho Ethernet é tratado com dmac, smac, ethertype e payload, e quando o valor de ethertype é 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 entrada 10.0.0.4 ao cache ARP da interface tap0

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/tap e usando ioctl(fd, TUNSETIFF, ...)
    • IFF_TAP seleciona um dispositivo TAP
    • IFF_NO_PI evita 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.h do Linux para usar o mapeamento entre ethertype e valores hexadecimais
  • O cabeçalho Ethernet é representado em uma struct C com os seguintes campos
    • dmac: endereço MAC de destino
    • smac: endereço MAC de origem
    • ethertype: comprimento ou tipo do payload
    • payload: 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 packed na 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;
  • 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 de ethertype

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 é 0x0001
    • protype: campo de 2 octetos que indica o tipo de protocolo; para IPv4, o valor é 0x0800
    • hwsize: campo de 1 octeto que indica o tamanho do endereço de hardware; um endereço MAC tem 6 bytes
    • prosize: campo de 1 octeto que indica o tamanho do endereço de protocolo; um endereço IPv4 tem 4 bytes
    • opcode: campo de 2 octetos que indica o tipo da mensagem ARP
  • Os valores de opcode se dividem em quatro tipos
    • ARP request: 1
    • ARP reply: 2
    • RARP request: 3
    • RARP reply: 4
  • Os dados ARP para IPv4 são tratados pela struct arp_ipv4
    • smac: endereço MAC do remetente
    • sip: endereço IP do remetente
    • dmac: endereço MAC do destinatário
    • dip: 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 de 10.0.0.4 com o endereço MAC 00:0C:29:6D:50:25
  • Depois disso, na saída de arp, aparece a entrada 10.0.0.4 ether 00:0c:29:6d:50:25 tap0 no 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

 
GN⁺ 2025-03-05
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

    • Muito tempo atrás, escrevi um parser de pcap/tcpdump em bash puro, porque na época essa era a única ferramenta com que eu sabia escrever um “programa”
      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
    • Muitos dispositivos embarcados usam lwip como implementação de TCP/IP
      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

    • Para quem não sabe nada sobre isso, é bem surpreendente; mas imagino que isso não queira dizer que a parte de TCP/IP seja metade de todo o código-fonte do kernel, certo?
    • Fico curioso para saber por que a pilha IP é tão grande. Um binário de 400 KB já é bastante código; será que é porque ela é altamente otimizada para uso em grandes servidores?
  • 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

    • O Cisco IOS SLB também pode funcionar de modo parecido. A ideia é adicionar o IP virtual como alias no loopback de cada servidor do farm
      A vantagem em relação ao balanceamento de carga L3 mais comum é que não é necessário reescrever o cabeçalho do pacote IP
    • Isso também é conhecido como DSR (Direct Server Return): https://www.haproxy.com/blog/layer-4-load-balancing-direct-s...
    • Se você desativar o ARP e configurar o mesmo IP em vários servidores na mesma rede, o switch/bridge não conseguirá aprender os endereços MAC e continuará fazendo flooding/broadcast dos pacotes para todas as portas daquele segmento
      Portanto, se for usar essa abordagem, é melhor criar uma VLAN dedicada
    • O F5 tem uma configuração de proxy ARP, então não é preciso fazer isso. A desvantagem é que isso frequentemente quebra o DHCP
    • Para esse tipo de brincadeira de baixo nível, também dá para mexer com DPDK. O ARP vem desativado por padrão
  • 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”

    • A frase completa é “dmac e smac são campos bastante autoexplicativos. Eles contêm os endereços MAC das partes da comunicação (destino e origem, respectivamente)”, então na verdade ele explica
      Além disso, em um artigo sobre criar uma pilha de rede, é seguro presumir que o leitor saiba pelo menos um pouco sobre redes
    • A menos que isso tenha acabado de ser atualizado, a frase seguinte já explica: “Eles contêm os endereços MAC das partes da comunicação (destino e origem, respectivamente)”
  • 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?

    • Não aparece no texto, mas é um valor que o autor deixou hardcoded na inicialização da interface: https://github.com/saminiir/level-ip/blob/e9ceb08f01a5499b85...
      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