Hlavní navigace

Sokety a C/C++: rodina protokolů PF_PACKET

13. 10. 2003
Doba čtení: 9 minut

Sdílet

Dnes si napíšeme jednoduchý program na příjem všech IP paketů, které jsou doručeny počítači. Při té příležitosti se budeme muset velmi povrchně seznámit s rodinou protokolů PF_PACKET.

Dnes se velice okrajově podíváme na rodinu protokolů PF_PACKET. V úvodu seriálu jsme pracovali se sokety na úrovni protokolu TCP (později UDP). Pohybovali jsme se na úrovni transportní vrstvy komunikačního modelu. Později jsme začali používat „syrové“ (RAW) sokety, čímž jsme klesli o jednu vrstvu níže. S pomocí RAW soketů jsme se dostali na vrstvu síťovou. Na této vrstvě jsme odesílali a přijímali hlavně ICMP pakety.

Pomocí rodiny protokolů PF_PACKET se můžeme dostat ještě o jednu vrstvu níže. Pomocí soketu z domény PF_PACKET lze pracovat s rámci linkové vrstvy. Soketem lze přijímat i odesílat linkové rámce. My se v seriálu linkovou vrstvou příliš zabývat nebudeme. Jen si ukážeme, jak v programu přijmout všechny IP pakety, které jsou na počítač doručeny. Něco takového se nám pomocí rodiny protokolů PF_INET nemohlo podařit. Vždy jsme přijímali jen IP pakety využívané určitým protokolem.

Typy soketů

V rodině protokolů PF_PACKET můžeme vytvořit jen dva druhy soketů. Jedná se o SOCK_DGRAM a SOCK_RAW. V tomto seriálu budeme používat pouze typ SOCK_DGRAM. Typ soketu SOCK_RAW pracuje i se záhlavím a zápatím linkových rámců. Naopak při práci s typem SOCK_DGRAM se staráme pouze o data, která jsou do linkových rámců (mezi záhlaví a zápatí) vkládány. Při odesílání dat nemusíme záhlaví, případně zápatí, vyplňovat a vkládat jej do odesílaného bufferu. Stejně tak buffer s přijatými daty nebude obsahovat záhlaví ani zápatí linkového rámce.

Protokol

Existuje celá škála protokolů, které můžeme použít. My chceme v ukázkovém příkladu přijímat všechny IP pakety, které jsou doručeny na počítač. Použijeme protokol ETH_P_IP . Zde je nutné upozornit, že makra pro identifikaci protokolů používaná v rodině protokolů PF_PACKET jsou dvoubytová čísla, proto musíme použít funkci htons. Makra označující všechny protokoly jsou v hlavičkovém souboru linux/if_ether.h.

Typ adresy

Každá rodina protokolů má svou specifickou adresu. Ukazatel na tuto adresu předáváme v mnoha funkcích soketového API a vždy ukazatel nakonec přetypujeme na ukazatel na sockaddr. V rodině protokolů PF_INET byla adresa ve tvaru struktury sockaddr_in. V rodině protokolů PF_UNIX byla adresa ve tvaru struktury sockaddr_un. V rodině protokolů PF_PACKET je adresa ve tvaru struktury sockaddr_ll.

Struktura sockaddr_ll

V našem případě (pro příjem paketů) nás zajímají atributy:

  • unsigned short int sll_family – jedná se o první atribut struktury. Udává rodinu protokolů, pro kterou je adresa určena. Bude vždy nabývat hodnot makra AF_PACKET. Při této příležitosti je dobré si všimnout, že všechny struktury pro adresu v dané rodině protokolů mají první dva byte určující rodinu protokolů. Tímto způsobem můžeme používat stejné API funkce pro naprosto rozdílné způsoby komunikace. Soket je obecný komunikační nástroj.
  • unsigned short int sll_protocol – číslo linkového protokolu
  • int sll_ifindex – číslo síťového rozhraní
  • unsigned char ssl_pktype – typ paketu
  • unsigned char sll_halen – velikost linkové adresy v bytech
  • unsigned char sll_addr[8] – linková adresa odesílatele paketu

O atributech sll_ifindex a ssl_pktype si povíme v budoucnu, kdy jejich význam blíže vysvětlím. Dnes se jimi nebudeme zabývat.

Nejedná se o všechny atributy struktury a uvedené atributy nejsou v tomto pořadí ve struktuře za sebou. O ostatních atributech se lze dočíst v manuálových stránkách.

Příklad

Příklad bude velice jednoduchý. Otevřeme soket domény PF_PACKET a typu SOCK_DGRAM. Pomocí funkcí select a recvfrom přijmeme data. Funkce recv from nám vyplní obsah instance struktury sockaddr_ll. Z ní vyčteme nějaká data o linkové vrstvě. Další data vyčteme z příchozích dat. Bude se jednat o IP pakety. Informace budeme průběžně vypisovat.

Fragmentace

Na rozdíl od soketu z domény PF_INET, který přijímal IP paket vždy jako celek (i v případě, že byl fragmentován), se zde již nemůžeme spoléhat na OS, že nám fragmenty IP paketu poskládá. Fragment IP paketu je samostatný IP paket. A nám může být doručen právě takový fragment. Na tento fakt nesmíme nikdy zapomínat.

Odesílatel linkového rámce

V programu budu používat pojmy jako „odesílatel linkového rámce“ a později „IP adresa odesílatele“. Je zřejmé, že ne vždy musí „IP adresa odesílatele“ korespondovat s „linkovou adresou odesílatele“. Jestliže není odesílatel IP paketu na stejné lokální síti jako příjemce, je jako „odesílatel linkového rámce“ uvedena linková adresa směrovače, kterým IP paket přišel do lokální sítě. Jedná se o adresy různých vrstev komunikačního modelu.

Formality

Vložíme hlavičkové soubory, začneme funkci main, deklarujeme proměnné a zpracujeme parametry příkazové řádky.

#include <sys/socket.h>
#include <sys/types.h>
#include <linux/if_ether.h>
#include <netpacket/packet.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#define MAX 65536

int main(int argc, char *argv[])
{
  int sock, lenght, count;
  char buffer[MAX];
  sockaddr_ll addr;
  size_t size;
  iphdr *ip;
  char *addrString;
  register char mf;
  register short int offset;
  fd_set mySet;

  if (argc != 2)
  {
    fprintf(stderr, "Syntaxe:\n\t%s N\n\t\tN je počet
        paketů, které má program přijmout\n", argv[0]);
    return -1;
  }
  sscanf(argv[1], "%d", &count);

Vytvoření soketu

Soket bude z domény rodiny protokolů PF_PACKET. Bude se jednat o datagramový soket (typ je SOCK_DGRAM). Hodnota používaného protokolu bude dána makrem ETH_P_IP. Všechny možnosti jsou uvedeny v hlavičkovém souboru linux/if_eth.h. Číslo udávající protokol musíme (jak jsem již uvedl) převést do „síťového tvaru“.

  if ((sock = socket(PF_PACKET, SOCK_DGRAM,
        htons(ETH_P_IP))) == -1)
  {
    perror("Selhal socket");
    return -1;
  }

Příjem dat

Tak, jak již mnohokrát, pomocí funkcí select a recvfrom přijmeme data. Tentokrát předáme funkci recvfrom ukazatel na sockaddr_ll, který přetypujeme na ukazatel na sockaddr.

  for(int p = 0; p < count; p++)
  {
    FD_ZERO(&mySet);
    FD_SET(sock, &mySet);
    if (select(sock + 1, &mySet, NULL, NULL, NULL) == -1)
    {
      perror("Selhal select");
      close(sock);
      return -1;
    }
    size = sizeof(addr);
    if ((lenght = recvfrom(sock, buffer,
    MAX, 0, (sockaddr *)&addr, &size)) == -1)
    {
      perror("Selhal recvfrom");
      close(sock);
      return -1;
    }

Vypíšeme informace o linkovém rámci

Postupně vypíšeme informace z instance struktury sockaddr_ll, kterou nám zaplnila funkce recvfrom. Význam jednotlivých položek je uveden v článku.

    printf("Přijato: %d bytů\n", lenght);
    printf("Protokol (linkový): %d\n", addr.sll_protocol);
    printf("Index síťového rozhraní: %d\n",
        addr.sll_ifindex);
    printf("Velikost (linkové) adresy odesílatele: %d\n",
        addr.sll_halen);
    if (addr.sll_halen != 0)
    {
      printf("Adresa (linková) odesílatele: ");
      for(int i = 0; i < addr.sll_halen - 1; i++)
      {
        printf("%x:", addr.sll_addr[i]);
      }
      printf("%x\n", addr.sll_addr[addr.sll_halen - 1]);
    }

Informace z hlavičky IP paketu

Buffer s přijatými daty začíná hlavičkou IP paketu. Za hlavičkou následuje tělo IP paketu. Pro práci s hlavičkou IP paketu použijeme nám již známou strukturu iphdr. Musíme počítat s tím, že IP paket může ve skutečnosti být pouze fragmentem většího IP paketu. Budeme se snažit zjistit, zda se jedná o fragment, nebo „původní“ IP paket.

Vše záleží na atributu frag_off z IP hlavičky. Atribut frag_off v sobě obsahuje dva příznaky DF a MF (viz článek Sokety a C/C++ – struktura IP a UDP. Zbytek atributu udává posunutí. Jestliže má IP paket ve své hlavičce nastaven příznak MF, jedná se o fragment a následují další fragmenty. V případě fragmentace mají všechny díly (fragmenty), na které je původní IP paket rozdělen, kromě posledního, nastaven příznak MF. Poslední fragment má MF hodnotu nulovou.

Každý fragment má nastaveno své posunutí od počátku původního IP paketu. Znamená to, že není-li IP paket fragmentem většího celku, má MF nastaveno na 0 (tedy nemá žádného následovníka) a zároveň je jeho posunutí 0. Mám-li hodnotu atributu frag_off, nesmím ji v první řadě zapomenout převést ze „síťového tvaru“. Potom pomocí binárního AND aplikovaného na atribut a binární masku, která má nastaveno 1 na pozicích, kde se nacházejí příznaky, postupně zjistím hodnoty MF a DF. Ještě je nutné onu zbylou jedničku (je-li daný příznak nastaven) posunout o odpovídající počet bitů doprava. Možná bylo lepší místo bitového posunu jen otestovat, zda je výsledek po AND nulový, nebo není. Mnou vybraná možnost má kratší zápis. Posunutí získáme z atributu frag_off opět binárním ANDem aplikovaným na atribut frag_off a binární masku, kde jsou první tři bity nastaveny na 0 (první bit je nulový a další dva jsou příznaky).

Po spuštění programu si můžete všimnout, že opravdu jednotlivé fragmenty stejného IP paketu, jak jsem říkal v dřívějších dílech, mají stejný identifikátor paketu.

Obvykle jsem v předchozích dílech vždy přeložil zobrazovanou IP adresu na doménové jméno. Nyní ale situace není tak jednoduchá. Musíme si uvědomit, že překlad doménového jména vyžaduje určitou „aktivitu“ na síti, kdy odešleme UDP datagram a další UDP datagram (který je vložen do IP paketu) přijmeme. Představme si, že nám přijde nějaký IP paket. My se pokusíme přeložit IP adresu odesílatele, případně i příjemce, na doménové jméno. Tím pádem nám za chvíli bude doručena odpověď na žádost o DNS překlad. Ta přijde v IP paketu. A já se budu snažit přeložit IP adresu odesílatele (DNS serveru) na doménové jméno. Odpověď zase přijde v IP paketu. Stačil by tedy jediný doručený IP paket a náš program by pořád dokolečka překládal IP adresu DNS serveru na doménové jméno, protože každý překlad by znamenal doručení IP paketu od DNS serveru s odpovědí.

root_podpora

    ip = (iphdr*)buffer;
    printf("Verze IP protokolu: %d\n", ip->version);
    printf("Velikost IP hlavičky: %d\n", ip->ihl * 4);
    printf("Typ služby: %d\n", ip->tos);
    printf("Velikost IP paketu: %d\n",
        ntohs(ip->tot_len));
    printf("Identifikátor paketu: %d\n",
        ntohs(ip->id));
    printf("TTL:  %d\n", ip->ttl);
    frag_off = ntohs(ip->frag_off);
    printf("Příznak DF: %d\n",
        (frag_off & 0x4000) >> 14);
    mf = (frag_off & 0x2000) >> 13;
    printf("Příznak MF: %d\n", mf);
    offset = (frag_off & 0x1FFF);
    if (!mf && offset == 0)
    {
      printf("IP paket není fragmentován\n");
    }
    else
    {
      printf("Posunutí fragmentu od začátku: %d\n",
        offset);
    }
    printf("Protokol: ");
    switch (ip->protocol)
    {
      case IPPROTO_UDP: printf("UDP\n"); break;
      case IPPROTO_TCP: printf("TCP\n"); break;
      case IPPROTO_ICMP: printf("ICMP\n"); break;
      default: printf("%d\n", ip->protocol); break;
    }
    addrString=strdup(inet_ntoa(*(in_addr*)&ip->saddr));
    printf("IP adresa odesílatele: %s\n", addrString);
    free(addrString);
    addrString=strdup(inet_ntoa(*(in_addr*)&ip->daddr));
    printf("IP adresa prijemce: %s\n",  addrString);
    free(addrString);
    printf("********************************\n");
  }

Ukončení programu

  close(sock);
  return 0;
}

Příklad ke stažení

Tabulka č. 503
Soubor OS
lin32.tgz Linux

V příkladu jsme uvedl pojem „číslo síťového rozhraní“, aniž bych o něm něco blíže řekl. V příštím dílu to napravím.

Byl pro vás článek přínosný?