Como estou escrevendo uma pilha tcp/ip no espaço de usuário com Linux e linguagem C

O objetivo do projeto é entender um pouco mais sobre desenvolvimento de sistemas e como o protocolo TCP/IP funciona.

E pretendo documentar os principais pontos e como está sendo essa jornada.

Passo 0: Entendendo TCP/IP

Primeiro precisava entender como o protocolo TCP/IP funcionava, além do básico. Então fui em busca da rfc do tcp/ip. E encontrei a rfc 1180, com uma imagem muito interessante:

                     ----------------------------
                     |    network applications  |
                     |                          |
                     |...  \ | /  ..  \ | /  ...|
                     |     -----      -----     |
                     |     |TCP|      |UDP|     |
                     |     -----      -----     |
                     |         \      /         |
                     |         --------         |
                     |         |  IP  |         |
                     |  -----  -*------         |
                     |  |ARP|   |               |
                     |  -----   |               |
                     |      \   |               |
                     |      ------              |
                     |      |ENET|              |
                     |      ---@--              |
                     ----------|-----------------
                               |
         ----------------------o---------
             Ethernet Cable

São esses os principais protocolos usados para enviar pacotes pela rede. Precisava então conhece-los mais de perto. Depois disso poderia dar início a codificação \0/

Então começaram a surgir questões importantes: Como ler frames ethernet da placa de rede? E como escrever os freme ethernet?

Depois de pesquisar um pouco na internet descobri que é possível ler “dados brutos” usando sockets, mas a principio busquei outra maneira de resolver meu problema. Depois de alguns minutos pesquisando descobrir os TUN/TAP devices ou dispositivos de rede virtual. Wow!!!

Passo 1: Ler Frames Ethernet com o TAP device

Nesse ponto fui buscar mais informações na internet a respeito dos TUN/TAP

  • TUN: Atua na camada de Rede, possibilitando ler pacotes IP.
  • TAP: Atua na camada de Enlace, possibilitando ler frames Ethernet.

Eureca!! Usar um “disposto TAP” iria resolver meu problema. Voltando à internet, novamente, é fácil encontrar o código para iniciar um destes dispositivos. Primeiro criar um Nó de dispositivo, é fácil com o comando:

sudo mknod /dev/net/tun c 10 200

Depois precisava interagir com o dispositivo através da linguagem C, esse código inicia um TAP device e “linka” com o Nó criando.

/**
 *    https://www.kernel.org/doc/Documentation/networking/tuntap.txt
 */
static void alloc_tap_device(char * dev, int * fd)
{
    struct ifreq ifr;
    int err;

    if ( (*fd = open("/dev/net/tap", O_RDWR)) < 0 )
    {
        printf("alloc_tap(): Error init tap device.\n");
        exit(EXIT_FAILURE);
    }

    memset(&ifr, 0, sizeof(ifr));

    /**
     *        IFF_TAP:   Packet with ethernet headers
     *        IFF_TUN:   Packet without ethernet headers
     *      IFF_NO_PI:   Do not provide packet information
     */
    ifr.ifr_flags = IFF_TAP | IFF_NO_PI;

    if ( *dev )
        strncpy(ifr.ifr_name, dev, IFNAMSIZ);

    if ( (err = ioctl(*fd, TUNSETIFF, (void *) &ifr)) < 0 )
    {
        printf("alloc_tap(): ioctl error.\n");
        exit(EXIT_FAILURE);
    }

    #ifdef DEBUG
    printf("TAP (%s): started\n", dev);
    #endif
}

esse codigo foi retirado da documentação do kernel linux e alterado de acordo com minha necessidade.

logo em seguida pecisava configurar esse dispositivo com os comandos:

// tap0 foi o nome dado ao dispositivo

system("ip link set dev tap0 up") // faz o interface "tap0" ficar ativa
system("ip route add 10.0.0.0/24 dev tap0") // diz ao SO para direcionar todo
                                            // os pacotes destinados ao ip
                                            // 10.0.0.0/24 seja encaminhado
                                            // para a interface tap0

Depois disso é possivel usar a função read() para ler frames ethernet.

Passo 2: Ler frames ethernet

O próximo passo depois que já era possível encaminhar os pacotes para a interface tap0, era lê-los, analisa-los e gerar uma resposta. O código necessário para ler os frames ethernet se parece com isso:

struct eth_frame * eth_read(int fd, char * buf)
{
    ssize_t count = read(fd, buf, BUFFER_SIZE);

    if ( count < -1 ) 
    {
        printf("eth_read(): Error read frame\n");
        return NULL;
    }

    struct eth_frame * frame = (struct eth_frame *) buf;
    return frame;
}

OK! mas qual é o formato dos frames? Aqui esta a resposta! Os frames tem essa estrutura:

struct eth_frame {
    unsigned char dmac[6];
    unsigned char smac[6];
    unsigned short eth_type;
    unsigned char payload[];
} __attribute__((packed));
  • dmac – tem o valor do endereço mac de destino, onde o frame deve ser entregue
  • smac – tem o valor do endereço mac de quem está enviando o frame
  • eth_type – é o tipo do frame ethernet
    • 0x0800 – diz que o frame transporta um pacote IPV4
    • 0x86DD – diz que o frame transporta um pacote IPV6
    • 0x0806 – diz que o frame transporta um pacote ARP

Passo 3: Protocolo ARP

O protocolo ARP (Address resolution protocol) é responsável em traduzir endereços IP em endereços MAC. Para ser mais específico antes de enviarmos um pacote IP, precisamos descobrir qual é o endereço MAC de destino, uma vez que o endereço IP não tem relação com o endereço MAC não podemos usar uma função que traduz o endereço IP para MAC. Então o ARP entra em ação, primeiro o ARP checa a tabela de tradução, que se parece com:

                  ------------------------------------
                  |IP address       Ethernet address |
                  ------------------------------------
                  |223.1.2.1        08-00-39-00-2F-C3|
                  |223.1.2.3        08-00-5A-21-A7-22|
                  |223.1.2.4        08-00-10-99-AC-54|
                  ------------------------------------
                      TABLE 1.  Example ARP Table

rfc 1180 - TCP/IP

Se o endereço IP estiver na tabela, o endereço MAC associado é usado para preencher o campodmac do frame ethernet, caso contrario uma abordagem diferente é usada. O protocolo ARP manda um broadcast (uma mensagem para todas as maquinas na rede) solicitando a resolução de um endereço IP, os computadores ou host conectados na rede recebe o pacote e executa um algoritmo:

?Do I have the hardware type in ar$hrd?
Yes: (almost definitely)
  [optionally check the hardware length ar$hln]
  ?Do I speak the protocol in ar$pro?
  Yes:
    [optionally check the protocol length ar$pln]
    Merge_flag := false
    If the pair <protocol type, sender protocol address> is
        already in my translation table, update the sender
        hardware address field of the entry with the new
        information in the packet and set Merge_flag to true.
    ?Am I the target protocol address?
    Yes:
      If Merge_flag is false, add the triplet <protocol type,
          sender protocol address, sender hardware address> to
          the translation table.
      ?Is the opcode ares_op$REQUEST?  (NOW look at the opcode!!)
      Yes:
        Swap hardware and protocol fields, putting the local
            hardware and protocol addresses in the sender fields.
        Set the ar$op field to ares_op$REPLY
        Send the packet to the (new) target hardware address on
            the same hardware on which the request was received.

rfc 826 - ARP

Ao receber a resposta o ARP pode determinar o endereço MAC de destino. A seguinte estrutura descreve o pacote ARP:

struct arp_packet {
    unsigned short hrd;
    unsigned short pro;
    unsigned char hln;
    unsigned char pln;
    unsigned short op;
    unsigned char payload[];
} __attribute__((packed));
  • hrd - Representa o tipo do endereço se é ethernet, Packet Radio Net
  • pro - Representa se o endereço IP é IPV4 ou IPV6
  • hln - Tamanho do campo hrd, se for MAC hln tem o valor 6 bytes
  • pln - Tamanho do campo pro, se for IPV4 pro tem o valor 4 bytes
  • op - Representa o tipo do pacote ARP
    • 0x0001 - Para a resolução de um endereço IP
    • 0x0002 - Para a resposta de uma resolução de um endereço IP

Quando o op contém o valor 0x0001 e o campo pro tem o valor 0x0800, isso nos diz que temos a solicitação de resolução de um endereço IP e um pacote ARP_IPV4 está no payload.

struct arp_ipv4 {
    unsigned char smac[6];
    unsigned int sip;

    unsigned char dmac[6];
    unsigned int dip;
} __attribute__((packed));
  • smac - Endereço MAC de quem está enviando o pacote ARP
  • sip - Endereço IP de quem está enviando o pacote ARP
  • dmac - Endereço MAC do destinatário (o que estamos tentando determinar)
  • dip - Endereço IP do destinatário

O pacote é então processado e produzido uma resposta. De acordo com o algoritmo comentado antes.

Por fim os campos dmac e dip são alterados para as informações dos campos smac e sip, isso é feito para produzir o pacote de resposta. Esse novo pacote é colocado dentro de um pacote ARP, com o op = 0x002, que sinaliza uma resposta. O novo pacote ARP é então colocado dentro de um frame enthernet e está pronto para ser enviado pela rede.

Esses foram meus primeiros passos para construção desse projeto, próximo passo: programar o comportamento do protocolo IP. Até lá! O codigo está no GitHub

Gostei! as vezes é bom saber como as coisas funcionam por debaixo dos panos, bem legal isso ai! Parabéns!

Wow! ótimo post, por mais posts assim no tabnews :3 São posts assim que não morrem e trazem relevancia ao site com o passar dos anos.