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:
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, ðd, 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.