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 entreguesmac
– tem o valor do endereço mac de quem está enviando o frameeth_type
– é o tipo do frame ethernet0x0800
– diz que o frame transporta um pacote IPV40x86DD
– diz que o frame transporta um pacote IPV60x0806
– 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
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.
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 Netpro
- Representa se o endereço IP é IPV4 ou IPV6hln
- Tamanho do campohrd
, se for MAChln
tem o valor 6 bytespln
- Tamanho do campopro
, se for IPV4pro
tem o valor 4 bytesop
- Representa o tipo do pacote ARP0x0001
- Para a resolução de um endereço IP0x0002
- 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 ARPsip
- Endereço IP de quem está enviando o pacote ARPdmac
- 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