next up previous contents
Next: Zaawansowane programowanie gniazd Up: Gniazda sieciowe - podstawy Previous: PF_UNIX   Contents

SOCK_RAW i PF_PACKET

Zaczniemy od omówienia gniazda typu SOCK_RAW. Typ ten udostępnia nam dostęp do warstw leżących poniżej warstwy aplikacyjnej, a więc do warstwy sieciowej (IP) i warstwy transportowej (TCP,UDP,ICMP). Dzięki takiemu gniazdu możemy zaimplementować własne protokoły bez potrzeby ingerencji w kod jądra systemu. Najbardziej powszechnym przykładem wykorzystania SOCK_RAW jest program ping. Ani system operacyjny ani standardowa biblioteka C nie udostępniają żadnego wywołania przeznaczonego specjalnie do "pingowania". Konieczne jest "ręczne" tworzenie pakietów ICMP Echo i ich wysyłanie w sieć.

Za chwilę zajmiemy się gniazdami SOCK_RAW w domenie PF_INET. Potem przyjdzie czas na specjalną domenę PF_PACKET i zakres użycia SOCK_RAW w tejże domenie. Ważna uwaga: zarówno gniazda typu SOCK_RAW, jak i gniazda domeny PF_PACKET mogą być otwierane tylko przez procesy z EUID=0 (lub posiadające włączony atrybut CAP_NET_RAW).

Poniżej znajdziemy namiastkę prawdziwego pinga. Programowi dużo brakuje aby można było go używać w praktyce - ma on na celu zaprezentowanie ogólnych zasad obsługi omawianego typu gniazda.

  #include <sys/types.h>
  #include <sys/socket.h>         /* socket()        */
  #include <linux/in.h>
  #include <linux/icmp.h>         /* struct icmphdr  */
  #include <linux/ip.h>           /* struct iphdr    */
  #include <netdb.h>              /* gethostbyname() */
  #include <stdio.h>
  #include <unistd.h>             /* alarm()         */
  #include <signal.h>             /* signal()        */
  
  #define TIMEO   2          /* limit czasu na odpowiedź */
  
  int timeout;               /* zmienna przyjmie wartość 1 jeśli zdalny host */
                             /* nie odpowie w ciągu TIMEO sekund             */
  
  main (int argc, char *argv[])
  {
    int                   sd, /* deskryptor gniazda                  */
                         len, /* długość adresu gniazda              */
                           n; /* ilość wysłanych pingów              */ 
    struct sockaddr_in haddr, /* adres pingowanego hosta             */
                       raddr; /* adres odbieranych pakietów          */
    struct icmphdr    icmphd, /* nagłówek ICMP                       */
                     *icmphp; /* wskaźnik do nagłówka ICMP           */
    struct iphdr       *iphp; /* wskaźnik do nagłówka IP             */
    struct hostent     *hent; /* struktura hostent pingowanego hosta */
    char       pktbuf[65536]; /* bufor do odbierania pakietów IP     */
  
    if (argc != 2)
      {
        printf ("Uzycie:\n%s adres_hosta\n", argv[0]);
  exit (1);
      }
  
  Programu używamy podobnie jak standardowego pinga.
  
    sd = socket (PF_INET, SOCK_RAW, IPPROTO_ICMP);
    if (sd < 0)
      {
        perror ("socket()");
        exit (1);
      }
  
Otwieramy gniazdo typu SOCK_RAW. W tym wypadku w miejsce protokołu (trzeci parametr) nie możemy wpisać po prostu 0. Konieczne jest poinformowanie systemu, jakie pakiety mamy zamiar obsługiwać. Do wyboru mamy m.in.: IPPROTO_ICMP, IPPROTO_TCP oraz IPPROTO_UDP (kompletny spis można znaleźć w linux/in.h). Kopie wszystkich pakietów IP zawierających nagłówki podanych protokołów będą dostarczane do naszego gniazda. Ważne jest, że będą to tylko kopie, a więc system sam zadba o odpowiednie zareagowanie na otrzymane pakiety (tzn. np. sam będzie wysyłał potwierdzenia odbioru w przypadku protokołu TCP)- my nie musimy zawracać sobie tym głowy. Każde wywołanie recvfrom() (nie używa się recv() dla gniazd SOCK_RAW) dostarczy nam cały otrzymany pakiet od nagłówka IP "w górę". Natomiast wywołanie sendto() oczekuje, że w podanym buforze do wysłania znajdują się dane od warstwy transportowej (TCP, UDP, ICMP ...) "w górę". System sam utworzy nagłówek IP. Jeśli chcielibyśmy mieć możliwość ręcznego tworzenia całego pakietu IP (począwszy od nagłówka IP) musimy użyć IPPROTO_RAW jako protokołu lub skorzystać z domeny PF_PACKET (o tym później).
    hent = gethostbyname (argv[1]);
    if (!hent)
      {
        herror ("gethostbyname()");
        exit (1);
      }
  
    haddr.sin_family = PF_INET;
    haddr.sin_port = 0;
    bcopy (hent->h_addr, (char *) &haddr.sin_addr, hent->h_length);
  
Zamieniliśmy nazwę domenową pingowanego hosta na adres IP i wypełniliśmy odpowiednią strukturę, której niedługo użyjemy jako argumentu dla sendto().
    signal (SIGALRM, &alrm_handle);
  
Ustawiamy funkcję obsługującą sygnał SIGALRM. Po co to nam potrzebne dowiemy się później, a tymczasem nie zaszkodzi popatrzyć, jak wygląda ta funkcja:
  void
  alrm_handle (int signum)
  {
    timeout = 1;
  }
  
  // Chyba obędzie się bez komentarzy :P 
  
    for (n = 1; n < 4; n++)
      {
        int resp = 0;
  
        memset (&icmphd, 0, sizeof (icmphd));
        icmphd.type = ICMP_ECHO;
        icmphd.code = 0;
        icmphd.un.echo.id = htons (666);
        icmphd.un.echo.sequence = htons (n);
        icmphd.checksum = in_cksum ((u_short *) & icmphd, sizeof (icmphd));
        bcopy (&icmphd, pktbuf, sizeof (icmphd));
  
Rozpoczęliśmy główną pętlę, która wyśle trzy (n<4) pakiety ICMP Echo Request w kierunku badanego hosta. Zmienna w dalszej części programu przyjmie wartość 1 jeśli otrzymaliśmy odpowiedź na Echo. W kolejnych liniach wypełniamy strukturę opisującą nagłówek ICMP. Wygląda on tak (linux/icmp.h):
  struct icmphdr {
    __u8          type;             /* Typ pakietu ICMP                      */
    __u8          code;             /* Kod (podtyp)                          */ 
    __u16         checksum;         /* Suma kontrolna                        */
    union {
          struct {
                  __u16   id;       /* Dwa pola pomocnicze używane           */  
                  __u16   sequence; /* dla pakietów ICMP_ECHO/ICMP_ECHOREPLY */ 
          } echo;
          __u32   gateway;          /* Kolejne pola wykorzystywane są przez  */
          struct {                  /* inne typy ICMP                        */
                  __u16   __unused;
                  __u16   mtu;
          } frag;
    } un;
  };
  
Jak widać w kodzie źródłowym typ pakietu ustawiliśmy na ICMP_ECHO (type = 8) natomiast kod może być dowolny (nie jest on używany przez ICMP_ECHO). W pole id z kolei wstawiliśmy pewną charakterystyczną liczbę ;), dzięki czemu potem będziemy mogli odróżnić pakiety przychodzące w odpowiedzi na nasze zapytanie od całej reszty. Pole sequence będzie oznaczało numer kolejnego ICMP_ECHO wysłanego przez nas. Warto wspomnieć, że pola id oraz sequence nie mają żadnego szczególnego znaczenia dla samego protokołu ICMP i właściwie tylko od nas zależy, jak chcemy je wykorzystać. Ostatnim krokiem jest obliczenie sumy kontrolnej pakietu ICMP i wstawienie jej w pole checksum. Do liczenia sumy kontrolnej użyliśmy osobnej funkcji in_cksum() realizujacej algorytm bardzo często wykorzystywany także w innych protokołach rodziny TCP/IP. Jak dokładnie wygląda taka funkcja możemy zobaczyć w źródle analizowanego programu na płycie. Wróćmy do omawianego fragmentu kodu. Pozostało jeszcze skopiowanie całego stworzonego właśnie nagłówka ICMP do bufora pktbuf, który będzie nam służył zarówno do wysyłania pakietów, jak i do ich odbierania.
        printf ("Wysylanie ICMP Echo Request nr. %i ... ", n);
  fflush (stdout);
  
        sendto (sd, pktbuf, sizeof (struct icmphdr), 0,
                (struct sockaddr *) &haddr, sizeof (haddr));
  
W tym momencie przekazujemy nasze ICMP_ECHO warstwie IP w jądrze. System sam zajmie się stworzeniem nagłówka IP i wysłaniem wszystkiego pod adres zawarty w haddr.
        len = sizeof (raddr);
        alarm (TIMEO);
        timeout = 0;
  
Dajemy zdalnemu systemowi TIMEO sekund na odpowiedź. Funkcja alarm mówi systemowi aby dostarczył nam sygnał SIGALRM po upłynięciu czasu podanego, jako parametr. Wcześniej wskazaliśmy systemowi, jaką funkcję chcemy wywołać w momencie otrzymania SIGALRM. Jedyne, co robi ta funkcja to nadaje zmiennej timeout wartość 1. Ze zmiennej tej skorzystamy w poniższej pętli:
        while (recvfrom
               (sd, pktbuf, sizeof (pktbuf), 0, (struct sockaddr *) &raddr,
                &len) > 0)
          {
            iphp = (struct iphdr *) pktbuf;
            icmphp = (struct icmphdr *) ((char *) iphp + 4 * iphp->ihl);
  
            if (ntohs (icmphp->un.echo.id) == 666
                && ntohs (icmphp->un.echo.sequence) == n
                && raddr.sin_addr.s_addr == haddr.sin_addr.s_addr
                && icmphp->type == ICMP_ECHOREPLY)
              {
                printf ("Odpowiedz otrzymana.\n");
                resp = 1;
                alarm (0);
                break;
              }
  
Pętla ta odczytuje wszystkie pakiety, które nadejdą do naszego gniazda od momentu wysłania ICMP_ECHO do chwili otrzymania odpowiedzi. Musimy jakoś stwierdzić, czy któryś z tych pakietów nie jest odpowiedzią na ICMP_ECHO przez nas (pamiętajmy, że na to gniazdo dostajemy WSZYSTKIE pakiety ICMP, które przychodzą na adres lokalnego hosta). Po pierwsze wskaźnikowi iphp (wskaźnik do nagłówka IP) przypisujemy adres pierwszego bajtu w otrzymanym buforze. Następnie próbujemy zlokalizować początek nagłówka ICMP. Znajduje się on zaraz za nagłówkiem IP. Długość nagłówka IP równa jest z kolei polu ihl pomnożonemu przez 4 (pole ihl podaje długość nagłówka wyrażoną w 32-bitowych słowach). Kiedy mamy już nagłówek ICMP przechodzimy do warunku if, które dokonuje sprawdzenia: Jeśli wszystkie warunki są spełnione to znaczy, że rzeczywiście otrzymaliśmy odpowiedź na nasze zapytanie i że możemy przejść do wysłania następnego ICMP_ECHO (break) wyłączając uprzednio zamówiony alarm.

Prześledźmy teraz, do czego były nam potrzebne wywołania signal() oraz alarm(). Po wysłaniu ICMP_ECHO wywoływana jest blokująca funkcja recvfrom(). Jeśli zdalny host nie dawałby znaku życia to nasz program zablokowałby się na dobre. My jednak pomyśleliśmy o tym wcześniej i nakazaliśmy systemowi obudzenie nas jeśli w określonym przedziale czasu nie uzyskamy żadnej odpowiedzi. Tak więc jeśli po upływie TIMEO sekund nie ma żadnych nowych pakietów funkcja alrm_handle() nadaje zmiennej timeout wartość 1 i zwraca sterowanie do pętli, w której oczekiwaliśmy na datagramy. Funkcje blokujące są przerywane po wykonaniu przez proces procedury obsługi sygnału. Tak więc po przekroczeniu limitu czasowego recvfrom() odblokowuje się i program wykonuje kolejne instrukcje pętli do momentu aż natrafi na warunek:

            if (timeout)
              break;
          }
  
Sprawdza on, czy upłynął limit czasowy. Jeśli tak było istotnie to przestajemy oczekiwać na pakiety i wychodzimy z pętli:
        if (!resp)
          printf ("Uplynal limit czasu.\n");
      }
  }
  
Wyświetlamy odpowiedni komunikat i wracamy do początku pętli for aby wysłać pozostałe pakiety ICMP_ECHO.

Przyszła kolej na domenę PF_PACKET. Pamiętamy, że przy użyciu SOCK_RAW najniższą warstwą, do jakiej mieliśmy dostęp była warstwa sieciowa. Dzięki domenie, którą zaraz poznamy zejdziemy jeszcze niżej - aż do warstwy fizycznej (mówiąc ściślej : do warstwy łącza danych). Domena ta obsługuje dwa typy gniazd: SOCK_RAW i SOCK_DGRAM. Kiedy korzystamy z pierwszego typu otrzymujemy tzw. ramki (tak nazywa się samodzielne porcje danych w warstwie fizycznej) i wysyłamy także kompletne ramki. Musimy więc zatroszczyć się o samodzielne stworzenie wszystkich potrzebnych nagłówków począwszy od nagłówka ramki ethernetowej (zajmiemy się tylko Ethernetem). Drugi typ gniazda - SOCK_DGRAM operuje na poziomie o szczebel wyższym, czyli kiedy oczytujemy dane system usuwa nagłówek ethernetowy i udostępnia nam wszystko, co znajduje się "powyżej" niego. Podobna sytuacja zachodzi podczas wysyłania danych, z tym że teraz system sam zajmie się stworzeniem odpowiedniego nagłówka ethernetowego.

W ramach ćwiczeń praktycznych przyjrzymy się programikowi, który służy do odnajdywania adresu MAC (fizycznego) dla podanego adresu IP. Do tego celu służy protokół ARP (ang. Address Resolution Protocol - protokół odnajdywania adresu). Pracuje on pomiędzy warstwą fizyczną a sieciową i dlatego aby z niego skorzystać musimy zejść aż do najniższej warstwy. Przejdźmy do rzeczy:

  #include <sys/types.h>
  #include <sys/socket.h>         /* socket()           */
  #include <sys/time.h>           /* struct timeval     */
  #include <linux/if_ether.h>     /* struct ethhdr      */
  #include <linux/if_packet.h>    /* struct sockaddr_ll */
  #include <linux/if_arp.h>       /* struct arphdr      */
  
  #define TIMEOUT 2                             /* czas, przez jaki będziemy    */
                                                /* czekać na odpowiedź          */
  #define SRCETHADDR "\x00\x80\x48\xc9\x4e\xd8" /* adres MAC naszego interfejsu */
                                                /* /sbin/ifconfig               */
  #define DSTETHADDR "\xff\xff\xff\xff\xff\xff" /* fizyczny adres broadcast     */
  
Adres MAC naszego interfejsu podajemy w makrze SRCETHADDR. Nie jest to z pewnością najwygodniejsze rozwiązanie. Na razie nie potrafimy uzyskać tego adresu z wnętrza programu. API do obsługi interfejsów sieciowych poznamy w swoim czasie.
  char *
  eth_ntoa (unsigned char n[ETH_ALEN])
  {
    static char buf[64];
  
    sprintf (buf, "%.2x:%.2x:%.2x:%.2x:%.2x:%.2x", n[0], n[1], n[2], n[3], n[4],
  n[5]);
  
    return buf;
  }
  
Prosta funkcja, która zamienia adres MAC podany w postaci 6 (ETH_ALEN) wartości typu char na ciąg liczb szesnastkowych w postaci xx:xx:xx:xx:xx:xx .
  int main (int argc, char *argv[])
  {
    int                   sd, /* deskryptor gniazda              */ 
                         ret; /* pomocnicza                      */
    struct ethhdr       ethd, /* nagłówek ethernetowy            */
                      *ethdp; /* wskaźnik nagłówka ethernetowego */
    struct arphdr      arphd, /* nagłówek ARP                    */
                     *arphdp; /* wskaźnik do nagłówka ARP        */
    char  ethsaddr[ETH_ALEN], /* nasz adres MAC                  */ 
          ethdaddr[ETH_ALEN]; /* adres MAC zdalnego komputera    */
    unsigned long     ipaddr; /* adres IP zdalnego komputera     */
    struct sockaddr_ll haddr; /* adres gniazda PF_PACKET         */
    char pktbuf[1024];        /* bufor na pakiety (ramki)        */
    fd_set fds;               /* zbiór deskryptorów              */
    struct timeval tv;        /* limit czasowy                   */
  
    if (argc != 2)
      {
        printf ("Uzycie:\n%s IP\n", argv[0]);
  exit (1);
      }
  
    if ((ipaddr = inet_addr (argv[1])) < 0)
      {
        perror ("inet_addr()");
        exit (1);
      }
  
Zamieniany adres IP z linii komend podany w postaci xxx.xxx.xxx.xxx na adres w postaci liczby 32-bitowej. Przyda się potem.
    bcopy (SRCETHADDR, ethsaddr, ETH_ALEN);
    bcopy (DSTETHADDR, ethdaddr, ETH_ALEN);
  
Kopiujemy makra do odpowiednich zmiennych.
    sd = socket (PF_PACKET, SOCK_RAW, htons (ETH_P_ARP));
    if (sd < 0)
      {
        perror ("socket()");
        exit (1);
      }
  
Tworzymy gniazdo PF_PACKET typu SOCK_RAW, czyli deklarujemy chęć własnoręcznej obsługi całych ramek. W miejsce protokołu wstawiliśmy ETH_P_ARP aby do gniazda były dostarczane tylko ramki zawierające dane protokołu ARP. Konieczna jest zamiana numeru protokołu na format sieciowy (htons()). Innymi często wykorzystywanymi protokołami są ETH_P_IP (IP) i ETH_P_ALL (wszystkie ramki niezależnie od przenoszonego protokołu wyższej warstwy). Wszystkie dostępne wartośći znajdują się w pliku linux/if_ether.h .
    memset (pktbuf, 0, sizeof (pktbuf));
    memcpy (ethd.h_dest, ethdaddr, ETH_ALEN);
    memcpy (ethd.h_source, ethsaddr, ETH_ALEN);
    ethd.h_proto = htons (ETH_P_ARP);
    memcpy (pktbuf, &ethd, sizeof (ethd));
  
Przystępujemy do wypełniania nagłówków. Zaczynamy od samego dołu - od nagłówka ethernetowego (linux/if_ether.h):
  struct ethhdr
  {
          unsigned char   h_dest[ETH_ALEN];       /* docelowy adres MAC       */
          unsigned char   h_source[ETH_ALEN];     /* źródłowy adres MAC       */
          unsigned short  h_proto;                /* protokól wyższej warstwy */
  };
  
Nigdy nie zaszkodzi na początek wyczyścić całą wypełnianą strukturę. Następnie w pole h_dest wstawiamy adres broadcast, a w pole h_source adres lokalnego interfejsu sieciowego. Dlaczego adres źródłowy jest taki a nie inny to powinno być jasne. Wątpliwości mogą się rodzić w przypadku adresu docelowego. Musimy wiedzieć, w jaki sposób działa w ogóle protokół ARP. Otóż zapytania ARP wysyłane są pod ethernetowy adres broadcast równy ff:ff:ff:ff:ff:ff (czyli do wszystkich komputerów w sieci) ponieważ w chwili ich wysyłania nie znamy adresu MAC komputera docelowego - dopiero po otrzymaniu odpowiedzi na zapytanie ARP poznamy ten adres. Trzecie pole nagłówka ethernetowego zawiera numer przenoszonego protokołu wyższej warstwy - tutaj jest to ARP. Po stworzeniu nagłówka wstawiamy go na początek bufora przeznaczonego na całą ramkę (pktbuf). Możeby teraz zająć się stworzeniem następnego nagłówka (ARP):
    memset (&arphd, 0, sizeof (arphd));
    arphd.ar_hrd = htons (ARPHRD_ETHER);
    arphd.ar_pro = htons (ETH_P_IP);
    arphd.ar_hln = ETH_ALEN;               /* 6 */
    arphd.ar_pln = 4;
    arphd.ar_op = htons (ARPOP_REQUEST);
    memcpy ((char *) &arphd + sizeof (arphd), ethsaddr, ETH_ALEN);
    memset ((char *) &arphd + sizeof (arphd) + ETH_ALEN, 0, 4);
    memset ((char *) &arphd + sizeof (arphd) + ETH_ALEN + 4, 0, ETH_ALEN);
    memcpy ((char *) &arphd + sizeof (arphd) + ETH_ALEN + 4 + ETH_ALEN, &ipaddr,4);
    memcpy (pktbuf + 14, &arphd, sizeof (arphd) + 2 * 4 + 2 * ETH_ALEN);
  
Struktura nagłówka ARP jest zdefiniowana tak (linux/if_arp.h):
  struct arphdr
  {
          unsigned short  ar_hrd;         /* format adresu fizycznego    */
          unsigned short  ar_pro;         /* format adresu sieciowego    */
          unsigned char   ar_hln;         /* długość adresu fizycznego   */
          unsigned char   ar_pln;         /* długość adresu sieciowego   */
          unsigned short  ar_op;          /* kod operacji ARP            */
  
  #if 0
          /*   Tak będzie wyglądała dalsza część w przypadku konwersji adresów  */
          /*   IP->MAC                                                          */
          unsigned char ar_sha[ETH_ALEN];       /* źródłowy adres MAC           */
          unsigned char ar_sip[4];              /* źródłowy adres IP            */
          unsigned char ar_tha[ETH_ALEN];       /* docelowy adres MAC           */
          unsigned char ar_tip[4];              /* docelowy adres IP            */
  #endif
  };
  
Patrząc na kod programu widzimy, że pole ar_hrd wypełniamy wartością ARPHRD_ETHER (linux/if_arp.h) - oznacza to, że będziemy dokonywać konwersji NA adres ethernetowy. Kolejna linijka ustawia ar_pro na ETH_P_IP - będziemy konwertować Z adresu IP. Dwa kolejne pola (ar_hln, ar_pln) oznaczają kolejno długość adresu fizycznego (MAC) i sieciowego (IP). Następnie polu ar_op nadajemy wartość ARPOP_REQUEST. Istnieje kilka różnych operacji ARP (linux/if_arp.h). My zamierzamy zamienić IP na MAC, a do tego używa się ARPOP_REQUEST. Pozostały jeszcze cztery pola. Źródłowy adres MAC to nasz adres. Źródłowy adres IP nie ma tu znaczenia dlatego go zerujemy. Docelowego adresu MAC nie znamy - to pole zostanie uzupełnione przez komputer, który odpowie na zapytanie ARP. Na koniec docelowy adres IP, czyli adres, o którego konwersję zabiegamy. Pozostało już tylko dokleić nagłówek ARP zaraz za nagłówkiem ethernetowym (w buforze pktbuf).

Poświęćmy jeszcze chwilę na omówienie samego protokołu ARP. W sieć zostanie wysłana ramka zaadresowana do wszystkich komputerów. Każdy z komputerów odbiera tą ramkę i patrzy, co jest w środku. Znajduje tam zapytanie ARP (ARPOP_REQUEST). Sprawdza więc, czy docelowy adres IP (adr_tip) jest jego własnym adresem. Jeśli stwierdzi, że tak jest to zamienia miejscami pola: ar_sha<->ar_tha oraz ar_sip<->ar_tip a ar_op ustawia na ARPOP_REPLY (odpowiedź ARP). Następnie tak zmieniony paket opakowuje w ramkę ethernetową i wysyła pod adres ar_tha. Kiedy komputer inicjujący całą tą operację otrzyma odpowiedź ARP, zagląda do pola ar_sha (bądź do adresu źródłowego w nagłówku ethernetowym) gdzie znajduje się adres, o który prosił.

Możemy teraz wysłać ramkę w sieć:

    memset (&haddr, 0, sizeof (haddr));
    haddr.sll_family = AF_PACKET;
    haddr.sll_protocol = htons (ETH_P_ARP);
    haddr.sll_ifindex = 2;
  
    if (sendto
        (sd, pktbuf, sizeof (ethd) + sizeof (arphd) + 2 * 4 + 2 * ETH_ALEN, 0,
         (struct sockaddr *) &haddr, sizeof (haddr)) < 0)
      {
        perror ("sendto()");
        exit (1);
      }
  
Jak zawsze przed użyciem sendto() musimy wypełnić strukturę opisującą adres zdalnego komputera. W przypadku PF_PACKET struktura ta wygląda tak (linux/if_packet.h):
  struct sockaddr_ll
  {
          unsigned short  sll_family;   /* PF_PACKET                        */
          unsigned short  sll_protocol; /* protokół wyższej warstwy         */
          int             sll_ifindex;  /* numer interfejsu używanego do    */
                                        /* wysłania ramki                   */  
          unsigned short  sll_hatype;
          unsigned char   sll_pkttype;
          unsigned char   sll_halen;    /* długość adresu docelowego        */
          unsigned char   sll_addr[8];  /* adres docelowy                   */
  };
  
Wystarczy, że wypełnimy tylko trzy pola: sll_family, sll_protocol i sll_ifindex. Reszta informacji potrzebna do wysłania ramki znajduje się w buforze pktbuf. Należy pamiętać o odpowiednim dobraniu trzeciego parametru dla funkcji sendto() (długość bufora). Jest on równy długości nagłówka ethernetowego + długość nagłówka ARP + długość samego zapytania ARP.
    FD_ZERO (&fds);
    FD_SET (sd, &fds);
  
    tv.tv_sec = TIMEOUT;
    tv.tv_usec = 0;
  
    ret = select (sd + 1, &fds, NULL, NULL, &tv);
    if (ret < 0)
      {
        perror ("select()");
        exit (1);
      }
  
Używamy znanej już funkcji select() aby poczekać na odpowiedź ARP. Czekamy maksymalnie TIMEOUT sekund.
    else if (ret == 0)
      printf ("Uplynal limit czasu.\n");
    else
  
Jeśli select() zwróci 0 to znaczy, że upłynąl limit czasowy.
      {
        recv (sd, pktbuf, sizeof (pktbuf), 0);
  
        ethdp = (struct ethhdr *) pktbuf;
        arphdp = (struct arphdr *) (pktbuf + sizeof (struct ethhdr));
  
W przeciwnym przypadku odbieramy przybyłą właśnie ramkę. Zwróćmy uwagę na zastosowanie recv() zamiast recvfrom() - w tym przypadku wystarczy nam recv() ponieważ dane adresowe zdalnego komputera i tak odczytamy z nagłówka ethernetowego. Następnie ustawiamy wskaźniki do nagłówka ethernetowego (początek bufora pktbuf) i do nagłówka arp (zaraz za nagłówkiem ethernetowym).
        if (ntohs (arphdp->ar_op) == ARPOP_REPLY
            && !memcmp ((char *) arphdp + sizeof (struct arphdr) + ETH_ALEN,
                        &ipaddr, 4))
          {
            printf ("IP\t: %s\n", argv[1]);
  printf ("MAC\t: %s\n", eth_ntoa (ethdp->h_source));
  }
      }
    exit (0);
  }
  
Pozostało już tylko sprawdzić, czy otrzymaliśmy odpowiedź od komputera, do którego wysyłaliśmy zapytanie. Jeśli tak to znaczy, że cała operacja się powiodła i możemy wyświetlić wyniki poszukiwań.

Nie ma co ukrywać, że program ten ma bardzo ograniczoną przydatność. Ciekawym pomysłem na jego rozbudowę jest przystosowanie go do odnajdywania wszystkich urządzeń podłączonych do lokalnej sieci (posiadających stos TCP/IP). Wystarczą niewielkie zmiany: musimy po prostu wysłać po jednym zapytaniu ARP do wszystkich możliwych adresów, tzn. jeśli jesteśmy podłączeni do sieci 192.168.1.0 z maską 255.255.255.0 to wysyłamy zapytania pod adresy 192.168.1.1-254. Potem oczekujemy już tylko na odpowiedzi ARP.

Na tym na razie poprzestaniemy. Opanowaliśmy wszyskie podstawowe umiejętności potrzebne do zaaranżowania komunikacji przy wykorzystaniu mechanizmu gniazd. Jak widzieliśmy na przykładzie analizowanych programow nasza obecna wiedza na ten temat wystarcza w zupełności do realizacji nawet całkiem złożonych zadań. Jednak aby nie mieć żadnych problemów z tworzeniem najbardziej wyrafinowanych programów sieciowych pozostało jeszcze do przyswojenia kilka bardziej zaawansowanych technik. Jeśli wszystko pójdzie dobrze już niedługo nauczymy się sprawnego posługiwania tymi technikami.


next up previous contents
Next: Zaawansowane programowanie gniazd Up: Gniazda sieciowe - podstawy Previous: PF_UNIX   Contents
Paweł Niewiadomski
2000-10-17