Dnes konečně odešleme první ICMP paket. Uděláme si velice jednoduchou implementaci programu ping. Program ping slouží k testování dostupnosti počítače v síti.
Princip programu ping
Program ping odešle na požadovaný počítač několikrát požadavek ECHO. Poté čeká na ECHO odpověď, kterou zmiňovaný počítač odpoví. Program ping si po odeslání ICMP paketu musí zapamatovat jeho identifikátor a pořadové číslo žádosti. Poté čeká a čte všechny příchozí ICMP pakety. Zajímá jej pouze ECHO odpověď se stejným identifikátorem a stejným pořadovým číslem. Jestliže přijde ECHO odpověď ve stanoveném čase, program vypíše, že počítač je dostupný. Také ping obvykle vypíše další informace včetně času, který uplynul od odeslání ICMP paketu – žádost o ECHO – k přijetí ICMP paketu – žádost o odpověď. Jestliže paket nepřijde v požadovaném čase, je daný počítač považován za nedostupný z našeho počítače.
Co naprogramujeme?
Napíšeme si velice jednoduchou (několikařádkovou) implementaci programu ping, který toho na rozdíl od jiných variant programu ping nebude mnoho umět. Nebude umět vlastně nic jiného než 5× odeslat ICMP paket a čekat na požadovaný ICMP paket. Podíváte-li se na program ping, který máte určitě nainstalován (ať už používáte Linux, MS Windows Ž, nebo cokoliv jiného), zjistíte, že je možné nastavovat mnoho užitečných věcí, které náš program umět nebude.
Příklad
Formality
Tak jako vždy, i nyní musím vložit hlavičkové soubory, začít funkci main a definovat proměnné. V příkladu musí být také funkce checksum, která je opsána z dokumentu RFC 1071. V MS WindowsŽ musíme také zavolat funkci WSAStartup.
#include <iostream> #include <netinet/in.h> #include <sys/socket.h> #include <sys/types.h> #include <netdb.h> #include <arpa/inet.h> #include <stdlib.h> #include <netinet/ip.h> #include <netinet/ip_icmp.h> #include <unistd.h> #include <string.h> #define BUFSIZE 1024 using namespace std; int main(int argc, char *argv[]) { size_t size; hostent *host; icmphdr *icmp, *icmpRecv; iphdr *ip; int sock, total, lenght; unsigned int ttl; sockaddr_in sendSockAddr, receiveSockAddr; char buffer[BUFSIZE]; fd_set mySet; timeval tv; char *addrString; in_addr addr; unsigned short int pid = getpid(), p; if (argc != 2) { cerr << "Syntaxe:\n\t" << argv[0] << " " << "adresa" << endl; return -1; }
Překlad doménového jména
Přeložíme doménové jméno, které je parametrem programu, na IP adresu.
if ((host = gethostbyname(argv[1])) == NULL) { cerr << "Špatná adresa" << endl; return -1; }
Vytvoření soketu
Vytvoříme soket typu SOCK_RAW a použijeme protokol IPPROTO_ICMP.
if ((sock = socket(PF_INET, SOCK_RAW, IPPROTO_ICMP)) == -1) { cerr << "Nelze vytvořit soket" << endl; return -1; }
Nastavení atributu TTL hlavičky IP
Nastavíme co největší položku TTL u odchozích paketů. Hodnotu nastavíme na 255. Použijeme nastavení voleb soketů. Volba se jmenuje IP_TTL, je v úrovni SOL_IP.
ttl = 255; setsockopt(sock, IPPROTO_IP, IP_TTL, (const char *)&ttl, sizeof(ttl));
Vytvoříme ICMP paket – ECHO žádost
Postupně vyplníme všechny atributy ICMP paketu typu 8 kódu 0 (ECHO žádost). Nejprve alokujeme potřebnou paměť, potom vyplníme typ a kód ICMP paketu. Identifikátor ECHO žádosti by měl být vyplněn nějakým jednoznačným číslem. Stejný identifikátor bude mít ECHO odpověď, čímž zajistíme, že odpověď je určena pro nás. proměnná pid má v Linuxu hodnotu, kterou vrátila funkce getpid, v MS Windows Ž má hodnotu, kterou vrátila funkce GetCurrentProcessId. Zbývá ještě vyplnit kontrolní součet a pořadové číslo žádosti. Protože budeme posílat pět ECHO žádostí, které odešleme v cyklu, budeme kontrolní součet a pořadové číslo vyplňovat v cyklu před odesláním. Budou pro každý odeslaný ICMP paket ECHO žádost různé.
icmp = (icmphdr *)malloc(sizeof(icmphdr)); icmp->type = ICMP_ECHO; icmp->code = 0; icmp->un.echo.id = pid;
Příprava odeslání dat
Zaplníme strukturu sockaddr_in, kterou použijeme jako parametr funkce sendto. Číslo portu je nyní nedůležité. Zadáme 0. Adresáta ICMP paketu zaplníme tak, jak jsme zvyklí.
sendSockAddr.sin_family = AF_INET; sendSockAddr.sin_port = 0; memcpy(&sendSockAddr.sin_addr, host->h_addr, host->h_length);
Doplnění dalších položek ICMP hlavičky
V cyklu budeme vyplňovat ty části ICMP hlavičky, které budou v každém ICMP paketu odlišné. Pořadové číslo ECHO žádosti společně s identifikátorem ECHO žádosti jednoznačně identifikuje ECHO žádost. ECHO odpověď na žádost má stejný identifikátor i stejné pořadové číslo. Zatímco identifikátor určuje de facto aplikaci, kterou byla ICMP žádost odeslána, pořadové číslo rozlišuje jednotlivé žádosti ECHO. Jestliže jedna aplikace odešle více žádostí o ECHO, měla by je rozlišit (číslovat) pomocí pořadových čísel. Vyplnění identifikátoru a pořadového čísla podle zmíněných pravidel není dáno žádnou normou nebo předpisem. Jedná se jen o doporučení.
Kontrolní součet nejprve nastavíme na 0, poté spočítáme z celého ICMP paketu pomocí již známé funkce. ICMP žádost ECHO může mít i tělo s libovolným obsahem. Stejná data, která odejdou v ECHO žádosti, se vrátí v ECHO odpovědi. Tělo by následovalo za hlavičkou. Kontrolní součet by se počítal z hlavičky i těla. My budeme posílat pouze hlavičku (takže tělo bude mít nulovou délku).
for (p = 1; p <= 5; p++) { icmp->checksum = 0; icmp->un.echo.sequence = p; icmp->checksum = checksum((unsigned char *)icmp, sizeof(icmphdr));
Odešleme data
Pomocí sendto odešleme vyplněnou ICMP hlavičku ECHO žádosti.
sendto(sock, (char *)icmp, sizeof(icmphdr), 0, (sockaddr *)&sendSockAddr, sizeof(sockaddr));
Počkáme maximálně 5 sekund
Pomocí funkce select počkáme maximálně 5 sekund, jestli nepřijde ECHO odpověď. Jestliže ne, považujeme ECHO žádost za ztracenou. Musíme počítat se situací, že přijde ICMP paket, který není určený pro nás (není ECHO odpověď nebo je ECHO odpověď s jiným identifikátorem nebo s jiným pořadovým číslem, než čekáme). Náš soket přijímá všechny ICMP pakety, které jsou dopraveny do počítače.
Takto napsaný program bude s jistotou fungovat jen v Linuxu. Je tady totiž menší problém, co udělat v případě, že přijde ICMP paket, který není pro nás. Funkce select se ukončí, my následně zjistíme, že není pro nás a zavoláme funkci select znova. Jaký ji ale dát poslední parametr? Podíváme-li se na manuálovou stránku funkce select, zjistíme, že v případě události na soketu bude ukončeno volání select a časový údaj (předávaný jako poslední parametr) bude změněn. Bude od původní hodnoty snížen o čas, který funkce select čekala. Toho já využívám. Problém je v tom, že se jedná o speciální vlastnost Linuxu. Rozhodně nefunguje v MS WindowsŽ. V jiných Unix-like systémech (kromě Linuxu) možná taky ne. V tom případě je nutné čas odečítat tak, jak to dělám v příkladu ke stažení pro MS WindowsŽ.
tv.tv_sec = 5; tv.tv_usec = 0; do { FD_ZERO(&mySet); FD_SET(sock, &mySet); if (select(sock + 1, &mySet, NULL, NULL, &tv) < 0) { cerr << "Selhal select" << endl; break; }
Přijmeme data
Jestliže nastala událost na soketu, přijmeme data pomocí recvfrom.
if (FD_ISSET(sock, &mySet)) { size = sizeof(sockaddr_in); if ((lenght = recvfrom(sock, buffer, BUFSIZE, 0, (sockaddr *)&receiveSockAddr, &size)) == -1) { cerr << "Problém při přijímáni dat" << endl; }
Analýza dat
S přijímáním ICMP paketů jsme se setkali již v článku Sokety a C/C++ – přijímání ICMP paketů. V přijímaném bufferu je nejprve hlavička IP protokolu, až poté hlavička ICMP.
Přišel-li ICMP paket, musíme nejprve zjistit, jestli se jedná o ECHO odpověď. Jestliže ano, musíme zjistit, zda se jedná o ECHO odpověď se stejným identifikátorem, jaký měla naše žádost, a zda má stejné pořadové číslo jako naše žádost. Jestliže ano, vypíšeme informace o přijetí odpovědi. Jestliže ne, čekáme dál nebo vypíšeme informaci o nepřijetí. ICMP pakety přijímáme, dokud nepřijde požadovaná odpověď nebo dokud nevyprší čas 5 sekund.
ip = (iphdr *) buffer; icmpRecv = (icmphdr *) (buffer + ip->ihl * 4); if ((icmpRecv->un.echo.id == pid) && (icmpRecv->type == ICMP_ECHOREPLY) && (icmpRecv->un.echo.sequence == p)) { addrString = strdup(inet_ntoa(receiveSockAddr.sin_addr)); host = gethostbyaddr(&receiveSockAddr.sin_addr, 4, AF_INET); cout << lenght << " bytů z " << (host == NULL? "?" : host->h_name) << " (" << addrString << "): icmp_seq=" << icmpRecv->un.echo.sequence << " ttl=" << (int)ip->ttl << " čas=nevím, neměřil jsem:-)" << endl; free(addrString); } } else { cout << "Čas vypršel" << endl; break; } } while (!((icmpRecv->un.echo.id == pid) && (icmpRecv->type == ICMP_ECHOREPLY) && (icmpRecv->un.echo.sequence == p)));
Konec
Ukončíme závorky, uvolníme paměť, uzavřeme soket, v MS Windows Ž zavoláme funkci WSACleanup a ukončíme funkci main.
} close(sock); free(icmp); return 0; }
Další možnosti
ECHO žádost a ECHO odpověď je v podstatě přímo dělaná na testování dostupnosti počítače v síti. Existuje ale i jiná (horší) možnost, jak testovat dostupnost počítače. V minulém díle jsme dělali experimenty, ve kterých jsme odeslali UDP paket na nějaký počítač na nesmyslný port. Přišel nám zpátky ICMP paket oznamující nedostupnost daného portu. Co to ale znamená? Znamená to, že počítač existuje (musí existovat, když nám poslal ICMP paket), pouze na tom počítači nečeká žádný proces na námi zvoleném UDP portu. Tímto způsobem lze také testovat dostupnost počítače. Prostě odešleme UDP datagram na zvolený počítač a nesmyslný port. Poté čekáme na ICMP paket oznamující nedoručení. Viz příklad v minulém článku.
Příště se podíváme na program traceroute (tracert v MS WindowsŽ). Vysvětlíme si princip a pokusíme se o jednoduchou implementaci.