Dnes se podrobněji podíváme na další z nepovinných položek IP hlavičky, na položku „časové razítko“. IP paket putující po síti si nechává od každého směrovače, jímž projde, zapsat čas, ve kterém směrovačem prochází. Tímto způsobem lze získat informace o rychlosti, jakou putují IP pakety po jednotlivých linkách mezi směrovači.
Oproti položce „zaznamenávej směrovače“ přibyl v hlavičce jeden byte s příznaky. Jedná se o čtvrtý byte v nepovinné části IP hlavičky (viz formát nepovinné části IP hlavičky typu „časové razítko“). Tento byte příznaků je rozdělen na dvě poloviny (po 4 bitech).
První 4 bity indikují přetečení (nedostatek buněk). Při odesílání nastavíme na 0. Paket bude putovat sítí, na každém směrovači obdrží časový údaj a jde dále. Jestliže v IP hlavičce již není místo pro další záznam, dojde k přetečení a směrovač zvýší příznak o 1. Tím pádem v cíli má tento příznak hodnotu udávající počet směrovačů, které nemohly zapsat časový údaj, protože nebylo kam.
Druhé 4 bity udávají způsob, jakým směrovače budou zapisovat a co budou zapisovat. Možné hodnoty a názvy maker, které reprezentují dané hodnoty, jsem uvedl v článku Sokety a C/C++ – volitelné položky IP záhlaví. My si v dnešním příkladu ukážeme možnosti „pouze časové razítko“ a „časové razítko a IP adresy“.
Pouze časové razítko
Každá buňka bude mít 4 byty – použijeme datový typ unsigned int. Bude obsahovat časový údaj – počet milisekund od poslední půlnoci světového času. Jestliže směrovač nezapíše časový údaj ve standardním tvaru (tedy číslo neudává počet milisekund od poslední půlnoci), je nastaven nejvyšší bit na 1. Při zpracovávání tohoto údaje nesmíme zapomenout, že číslo udávající milisekundy přijde v „síťovém“ tvaru. Chceme-li s údajem pracovat, musíme jej nejprve pomocí funkce ntohl převést do „normálního“ tvaru.
Časové razítko a IP adresy
Každá buňka bude mít 8 bytů. První 4 byty udávají IP adresu směrovače, který vkládá razítko. Druhé 4 byty buňky udávají časové razítko, pro které platí vše co v případě, kdy zaznamenáváme pouze časová razítka.
Vybrané směrovače
Existuje také možnost vybrat směrovače, jejichž časové razítko nás zajímá. V takovém případě bude mít opět buňka 8 bytů a první 4 byty budou obsahovat IP adresu směrovače, který má zapsat časová razítko. Druhé 4 byty budou nastaveny na 0. Projde-li IP paket směrovačem, jehož IP adresa je uvedena v nějaké buňce, zapíše do druhé poloviny své časové razítko. Není-li v IP hlavičce buňka s jeho IP adresou, nezapíše nic.
Příklad
Vytvoříme si jednoduchý příklad, který bude využívat možnosti „pouze časová razítka“ a „časová razítka a adresy“. Program opět odešle ICMP žádost o ECHO a počká na ICMP ECHO odpověď. ICMP paket bude v IP paketu s IP hlavičkou rozšířenou o nepovinnou položku „časové razítko“ (time stamp).
Formality
Jako vždy musíme vložit hlavičkové soubory, deklarovat proměnné a podobně. Abych se v každém článku neopakoval, zahrnu do „formalit“ také přeložení doménového jména, vytvoření soketu, zaplnění instance struktury sockaddr_in. Vše se v každém článku opakuje. Jen chci upozornit, že program může mít nepovinný druhý parametr -ipaddr, po jehož zadání se bude na směrovačích zapisovat nejen čas, ale také IP adresa. V případě, že druhý parametr programu není zadán nebo má jinou hodnotu, zapisují se pouze časy.
Za povšimnutí stojí jen proměnná fieldLength udávající velikost buňky. Implicitně má hodnotu 4, jestliže je druhým parametrem -ipaddr, zvýšíme ji na 8. Později budeme s proměnnou počítat.
#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/udp.h> #include <netinet/ip_icmp.h> #include <unistd.h> #include <string.h> #include <errno.h> #define MAX 65536 using namespace std; unsigned short checksum(unsigned char *addr, int count); void getTime(unsigned int ms, unsigned int &h, unsigned int &m, unsigned int &s); int main(int argc, char *argv[]) { size_t size; hostent *host; icmphdr icmp, *icmpRecv; iphdr *ipRecv; int sock, lenght; sockaddr_in sendSockAddr; fd_set mySet; timeval tv; char *addrString; unsigned short int pid = getpid(); char buffer[MAX]; char ipOptions[MAX_IPOPTLEN]; unsigned char *ipOptionsRecv; bool recv = false, onlyTime = true; unsigned int *time, ms, hour = 0, min = 0, sec = 0 unsigned int fieldLength = 4; if ((argc < 2) || (argc > 3)) { cerr << "Syntaxe:\n\t" << argv[0] << "adresa -ipaddr" << endl; return -1; } if ((argc == 3) && (strncmp(argv[2], "-ipaddr", 8) == 0)) { onlyTime = false; fieldLength = 8; } // Zjistíme informace o vzdáleném počítači if ((host = gethostbyname(argv[1])) == NULL) { cerr << "Špatná cílová adresa" << endl; return -1; } // Vytvoříme soket if ((sock = socket(PF_INET, SOCK_RAW, IPPROTO_ICMP)) == -1) { cerr << "Nelze vytvořit soket" << endl; return -1; } // Připravime strukturu pro sendto sendSockAddr.sin_family = AF_INET; sendSockAddr.sin_port = 0; memcpy(&(sendSockAddr.sin_addr), host-> h_addr, host->h_length);
ICMP hlavička
Vyplníme ICMP hlavičku ECHO žádosti.
icmp.type = ICMP_ECHO; icmp.code = 0; icmp.un.echo.id = pid; icmp.checksum = 0; icmp.un.echo.sequence = 1; icmp.checksum = checksum((unsigned char *)&icmp, sizeof(icmphdr));
Volitelné položky IP hlavičky
Zaplníme pole ipOptions volitelnou položkou IP záhlaví. Nejprve vynulujeme pole a poté začneme zapisovat údaje.
memset(ipOptions, 0, MAX_IPOPTLEN);
Typ položky
Typ položky je 1 byte pole (prvek s indexem 0). Jeho hodnota musí být 63 (makro IPOPT_TS).
ipOptions[IPOPT_OPTVAL] = IPOPT_TS;
Velikost položky
Velikost budeme mít maximální možnou – 40 bytů – hodnota makra MAX_IPOPTLEN. Velikost položky se zapisuje do druhého byte.
ipOptions[IPOPT_OLEN] = MAX_IPOPTLEN;
Offset první buňky
První buňka bude posunutá od začátku 5 bytů. Zde je malý rozdíl od minula, kdy bylo posunutí 4 byty. Nyní totiž přibyl jeden byte s příznaky. Makro IPOPT_MINOFF je definováno jako 4, což je nejmenší možné posunutí. Posunutí se zapisuje na 3. byte od začátku.
ipOptions[IPOPT_OFFSET] = IPOPT_MINOFF + 1;
Příznaky OF a FL
První 4 bity obsahují hodnotu OF – příznak přetečení. Při odesílání nastavujeme na 0. Druhé 4 bity obsahují příznak FL. Příznak určuje, zda se mají zaznamenávat jen časy, nebo i IP adresy, nebo jen časy na vybraných IP adresách. My nastavíme hodnotu makra IPOPT_TS_TSONLY nebo IPOPT_TS_TSANDADDR podle toho, zda bylo z příkazové řádky zadáno -ipaddr.
ipOptions[IPOPT_OFFSET + 1] = (onlyTime? IPOPT_TS_TSONLY : IPOPT_TS_TSANDADDR);
Nastavení volitelných položek IP záhlaví
Pomocí setsockopt nastavíme naše pole jako rozšířenou část IP hlavičky, kterou budou obsahovat pakety posílané naším soketem.
if (setsockopt(sock, SOL_IP, IP_OPTIONS, ipOptions, MAX_IPOPTLEN) == -1) { cout << "Nelze nastavit \"Time stamp\"" << endl; close(sock); return -1; }
Odeslání dat
if ((sendto(sock, (char *)&icmp, sizeof(icmphdr), 0, (sockaddr *)&sendSockAddr,sizeof(sockaddr))==-1)) { cerr << "Problém s odesláním dat" << endl; close(sock); return -1; }
Přijetí dat
Tak, jak jsme si ukazovali již mnohokrát, přijmeme pomocí select a následného recvfrom data.
do { FD_ZERO(&mySet); FD_SET(sock, &mySet); tv.tv_sec = 5; tv.tv_usec = 0; if (select(sock + 1, &mySet, NULL, NULL, &tv) < 0) { cerr << "Selhal select" << endl; break; } if (FD_ISSET(sock, &mySet)) { size = sizeof(sendSockAddr); if ((lenght = recvfrom(sock, buffer, MAX, 0, (sockaddr *)&sendSockAddr, &size)) == -1) { close(sock); return -1; } cout << "Přijato " << lenght << " bytů" << endl;
Nastavení ukazatelů
Za povinnou částí IP hlavičky (možná) následuje nepovinná část IP hlavičky. Za celou IP hlavičkou následuje ICMP hlavička. Poloha začátku ICMP hlavičky v bufferu je dána čtyřnásobkem hodnoty atributu ihl povinné části IP hlavičky.
ipRecv = (iphdr *)buffer; icmpRecv = (icmphdr *) (buffer + ipRecv->ihl * 4);
Kontrola příchozích dat
Jestliže jsou příchozí data menší, než by měly být (délka IP hlavičky + délka ICMP hlavičky) nebo se nejedná o ECHO odpověď na naši žádost, paket nás nezajímá.
if (ipRecv->ihl * 4 + sizeof(icmphdr) > (unsigned int)lenght) { continue; } if ((icmpRecv->type == ICMP_ECHOREPLY) && (icmpRecv->code == 0) && (icmpRecv->un.echo.id == pid) && (icmpRecv->un.echo.sequence == 1)) { cout << "Jedná se o odpověď na mou žádost" << endl;
Kontrola zpracování volitelné části IP hlavičky
Zkontrolujeme, jestli je volitelná část IP hlavičky skutečně typu „časové razítko“. Poté zjistíme, kolik přišlo vyplněných buněk. Jestliže jsme zaznamenávali kromě časů také IP adresy, je velikost jedné buňky 8 bytů, jinak 4 byty. Prvek s indexem IPOPT_OFFSET nyní udává posunutí na první prázdnou buňku. Odečteme-li od posunutí 4 (jsou to 4 byty údajů na začátku volby), získáme velikost všech buněk v bytech. Vydělíme-li tuto velikost velikostí jedné buňky (tu máme v proměnné fieldLength), získáme počet zaplněných buněk.
První 4 bity z čtvrtého bytu udávájí počet směrovačů, které nemohly zapsat své údaje, protože nebyl dostatek záznamů.
Pro lepší práci s bufferem přetypujeme ukazatel směřující na začátek první buňky na typ unsigned int *. Tím pádem budeme mít jedno nebo dvě čísla int na jednu buňku.
if (ipOptions[IPOPT_OPTVAL] != IPOPT_TS) { cout << "Volitelná položka není \"TS\" (Time Stamp)" << endl; break; } cout << "Přišly " << (int) (ipOptionsRecv[IPOPT_OFFSET] - 4)/fieldLength << " vyplněné buňky." << endl; cout << "Buňky scházely pro " << (((int) ipOptionsRecv[IPOPT_OFFSET + 1]) >> 4) << " záznamů." << endl; time=(unsigned int *)&ipOptionsRecv[IPOPT_MINOFF];
Posutpně projdeme všechny buňky
for (int i = IPOPT_MINOFF; i < ipOptionsRecv[IPOPT_OFFSET] - 1; i += fieldLength) { cout << endl;
Pouze časy
Každý prvek v poli time je jeden čtyřbyte. Zaznamenáváme-li pouze časy, má jedna buňka 4 byte. Proměnná i obsahuje počet bytů od začátku nepovinné volby (od začátku pole ipOptionsRecv), po odečtení úvodních 4 bytů získáme počet čtyřbytových buněk. Po vydělení čtyřmi máme index do poletime.
if (onlyTime) { ms = ntohl(time[(i - 4) / 4]); }
Časy a IP adresy
Způsobem, který známe z minulých dílů, získáme IP adresu a přeložíme ji na doménové jméno.
else { unsigned long int ip =*(unsigned long int*)&ipOptionsRecv[i]; addrString=strdup(inet_ntoa(*(in_addr*)&ip)); host=gethostbyaddr((char *)&ip, 4, AF_INET); cout << addrString << " (" << (host == NULL? "?" : host->h_name) << ")" << endl; free(addrString); ms = ntohl(time[(i - 4) / 4 + 1]); }
Zpracování časového razítka
První bit časového razítka udává, zda je časové razítko ve standardním tvaru, nebo není. Počet milisekund je obsažen v posledních 31 bitech. Proto zobrazuji hodnotu proměnné ms pomocí masky 0×7fffffff. Je-li první byte 1 (tedy posunu-li číslo o 31 bitů doprava a získám-li jedničku), není ve standardním tvaru. Nemá smysl se pokoušet jej převést na hodiny, minuty a sekundy. Jestliže je první byte 0, můžeme převádět čas pomocí funkce, kterou jsme vytvořili.
cout << "Přišlo " << (ms & 0x7fffffff); if (ms >> 31) { cout << " - Není ve standardním tvaru." << endl; } else { getTime(ms, hour, min, sec); cout << " - " << hour << ":" << min << ":" << sec << endl; }
Konec programu
} recv = true; } } else { cerr << "Nic nepřišlo" << endl; break; } } while (!recv); close(sock); return 0; }
Funkce pro výpočet času
V programu používáme jednoduchou funkci, kterou postupným dělením z počtu milisekund získáme počet hodin, minut a sekund.
void getTime(unsigned int ms, unsigned int &h, unsigned int &m, unsigned int &s) { ms /= 1000; s = ms % 60; ms /= 60; m = ms % 60; ms /= 60; h = ms; }
Příklady ke stažení
Oprava chyb
V minulém dílu se mi stala nepříjemná chyba. Když jsem vytvářel příklady, měl jsem několik verzí, které jsem postupně upravoval a došel tak k funkčnímu příkladu, který jsem později chtěl popsat v článku. Bohužel jsem ale nakonec nepracoval s úplně poslední funkční verzí. Výsledek je takový, že v článku je malá (ale poměrně zásadní) chyba a ukázkový příklad v MS Windows Ž nefunguje. V Linuxu funguje jen někdy. Za chybu se omlouvám.
Chyba spočívá v tom, že velikost volitelné části IP hlavičky v minulém dílu není 40 bytů, ale 39 bytů. Tři uvodní + 9 * 4 bytů. Je potřeba opravit dva řádky. Jednak zaplňování velikosti nepovinné části (řádek má vypadat ipOptions[IPOPT_OLEN] = MAX_IPOPTLEN – 1;) a poté nastavování volby soketu. Řádek má vypadat if (setsockopt(sock, SOL_IP, IP_OPTIONS, ipOptions, MAX_IPOPTLEN – 1) == –1) . Dávám ke stažení příklady znovu, tentokrát opravené.
Chyby jsem si všimnul až při psaní tohoto dílu, kdy jsem se hlouběji zanořil do podivně zmatené adresářové struktury na svém disku, ve které (jak se ukázalo při psaní minulého dílu) se bohužel nevyznám už ani já.