Sokety a C/C++: MTU a IP fragmentace (dokončení)
Seriál Sokety a C/C++
- Sokety a C/C++: program traceroute
- Sokety a C/C++: MTU a IP fragmentace
- Sokety a C/C++: MTU a IP fragmentace (dokončení)
- Sokety a C/C++: volitelné položky IP záhlaví
- Sokety a C/C++: zaznamenávání směrovačů
V minulém dílu jsme nakousli problematiku MTU. Dnes si vytvoříme příklad, který bude měřit MTU mezi naším a vybraným počítačem.
Příklad
Formality
Vložíme hlavičkové soubory, deklarujeme makra, začneme main, deklarujeme lokální proměnné v main a zkontrolujeme parametry příkazového řádku.
#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> #include <errno.h> #define MIN 28 #define MAX 65536 using namespace std; extern int errno; unsigned short checksum(unsigned char *addr, int count); bool isMTUProblem(char *buffer, unsigned short int bufferLenght, unsigned short int id, unsigned short int sequence); int main(int argc, char *argv[]) { size_t size; hostent *host; icmphdr *icmp, *icmpRecv; iphdr *ipRecv, *ipSend; int sock, lenght, tr = 1; unsigned int minBuffer = MIN, maxBuffer = MAX; unsigned int lenghtBuffer = (MIN + MAX) / 2; sockaddr_in sendSockAddr, receiveSockAddr; char buffer[MAX]; fd_set mySet; timeval tv; char *addrString; unsigned short int pid = getpid(); short int sequence = 1; 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 počítače, které získáme jako parametr funkce na IP adresu.
if ((host = gethostbyname(argv[1])) == NULL) { cerr << "Špatná cílová adresa" >> endl; return -1; }
Vytvoříme soket
Soket je typu SOCK_RAW, použitý protokl je IP_ICMP.
if ((sock = socket(PF_INET, SOCK_RAW, IPPROTO_ICMP)) == -1) { cerr << "Nelze vytvořit soket" >> endl; return -1; }
Vlastnost IP_HDRINCL
Protože chceme nastavit přímo zadávat atributy IP hlavičky, nastavíme vlastnost soketu IP_HDRINCL. Volba IP_HDRINCL je v úrovni voleb SOL_IP.
setsockopt(sock, SOL_IP, IP_HDRINCL, (char *)&tr, 4);
Struktura sockaddr_in
Vyplníme instanci struktury sockaddr_in, která se jmenuje sendSockAddr. Určíme cílovou adresu našeho IP paketu. Číslo portu dáme 0.
sendSockAddr.sin_family = AF_INET; sendSockAddr.sin_port = 0; x memcpy(&(sendSockAddr.sin_addr), host->h_addr, host->h_length); do { cout << "Zkousím odeslat " <<lenghtBuffer >> " bytu." << endl;
Alokace paměti a nastavení ukazatelů
Velikost odesílaného bufferu je vždy v proměnné lenghtBuffer. Nejprve alokujeme paměť odpovídající velikosti a poté nastavíme ukazatel icmp na začátek ICMP hlavičky, která následuje ihned za hlavičkou IP. Celý buffer (blok dat) zaplníme nulami.
ipSend = (iphdr *)malloc(lenghtBuffer); icmp = (icmphdr *)((char *) ipSend + sizeof(iphdr)); memset((void *) ipSend, 0, lenghtBuffer);
Zaplnění atributů IP hlavičky
Postupně zaplníme atributy IP hlavičky. Důležitý je atribut frag_off, kde nastavíme příznak DF na 1. Hodnotu TTL nastavíme na nejvyšší možnou (255). U identifikátoru IP paketu (atribut ip) zdrojové adresy (atribut saddr) a kontrolního součtu (atribut check) využijeme vlastnosti Linuxu, který si sám doplní požadované hodnoty, jestliže zadáme 0.
ipSend->version = 4; ipSend->ihl = 5; ipSend->tos = 0; ipSend->tot_len = htons(lenghtBuffer); ipSend->id = 0; // Doplní jádro OS ipSend->frag_off = htons(16384); //0100000000000000 bin ipSend->ttl = 255; ipSend->protocol = IPPROTO_ICMP; ipSend->check = 0; // Doplní jadro OS OS ipSend->saddr = 0; // Doplní jadro OS ipSend->daddr = *((unsigned long int*)host->h_addr);
Zaplnění atributů ICMP hlavičky
Tak, jak již umíme z předchozích článků, vyplníme atributy ICMP hlavičky a tím vytvoříme ECHO žádost. Kontrolní součet se posílá z celého ICMP paketu, nikoliv jen z hlavičky (jako u IP protokolu).
icmp->type = ICMP_ECHO; icmp->code = 0; icmp->un.echo.id = pid; icmp->checksum = 0; icmp->un.echo.sequence = sequence++; icmp->checksum = checksum((unsigned char *)icmp, lenghtBuffer - sizeof(iphdr));
Odeslání dat
Pomocí funkce sendto odešleme data. Jestliže funkce sendto selže a hodnota proměnné errno je rovna makru EMSGSIZE, odesílaný IP paket je příliš velký na odeslání. MTU ihned „za počítačem“ je menší. Změníme hodnotu maxBuffer, vypočteme novou velikost bufferu a znovu zopakujeme nastavení atributů nového IP paketu a jeho odeslání. V MS Windows by měla funkce WSAGetLastError vrátit hodnotu makraWSAEMSGSIZE. Ale nevrací. Funkce sendto neselže ani v případě, že data neopustí počítač.
if (((lenght = sendto(sock, (char *)ipSend, lenghtBuffer, 0, (sockaddr *)&sendSockAddr, sizeof(sockaddr))) == -1) && (errno == EMSGSIZE)) { cout << "Nejde odeslat takové množství dat." << endl; maxBuffer = lenghtBuffer; lenghtBuffer = (minBuffer + maxBuffer) / 2; free(ipSend); continue; } else { if (lenght == -1) { cerr << "Jiný problém" << endl; close(sock); free(ipSend); return -1; } }
Čekání na odpověď
Zavoláme funkci select s 5minutovým čekáním. Hlídáme příchozí data na soketu.
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říjem dat
Přijmeme data a nastavíme ukazatele na IP a ICMP hlavičku. ICMP hlavička následuje ihned za IP hlavičkou.
if (FD_ISSET(sock, &mySet)) { size = sizeof(sockaddr_in); if ((lenght = recvfrom(sock, buffer, MAX, 0, (sockaddr *)&receiveSockAddr, &size)) == -1) { cerr << "Problem při přijimáni dat" << endl; } // Přišel blok dat: IP hlavička + ICMP hlavička. ipRecv = (iphdr *) buffer; icmpRecv = (icmphdr *) (buffer + ipRecv->ihl * 4);
Zpracování ECHO odpovědi
Jestliže příchozí paket je ECHO odpověď, vypíšeme informace o paketu a nastavíme minimální velikost bufferu na hodnotu aktuální velikosti bufferu. Přijde-li ECHO odpověď, která je odpovědí na naši otázku, znamená to, že nejmenší MTU na cestě je větší než posílaný IP paket. Můžeme odesílaný IP paket zvětšit.
if ((icmpRecv->type == ICMP_ECHOREPLY) && (icmpRecv->un.echo.id == pid) && (icmpRecv->un.echo.sequence==sequence-1)) { addrString = strdup(inet_ntoa(receiveSockAddr.sin_addr)); host = gethostbyaddr ((const char *)&receiveSockAddr.sin_addr, 4, AF_INET); cout << lenght << " bytů z " << (host == NULL? "?" : host->h_name) << " (" << addrString << "): icmp_seq=" << icmpRecv->un.echo.sequence << endl; free(addrString); minBuffer = lenghtBuffer; }
Zpracování ICMP paketu typu 3, kódu 4
Funkce isMTUProblem vrací true v případě, že ICMP paket, na který se odkazuje první parametr, je typu 3, kódu 4 a informuje o zahození námi odeslané ECHO žádosti (se zadaným identifikátorem a pořadovým číslem). Jestliže opravdu paket splňuje tyto podmínky, vypíšeme informace o něm a změníme maximální hodnotu bufferu na velikost aktuálního bufferu. Příchod paketu typu 3, kódu 4 signalizuje, že paket je příliš velký. Musíme příště poslat menší paket.
if (isMTUProblem((char *)icmpRecv, lenght - ipRecv->ihl * 4, pid, sequence - 1)) { addrString = strdup(inet_ntoa(receiveSockAddr.sin_addr)); host = gethostbyaddr ((const char *)&receiveSockAddr.sin_addr, 4, AF_INET); cout << "IP paket byl zahozen počítačem " << (host == NULL? "?" : host->h_name) << " (" << addrString << ")" << ", protože paket nelze fragmentovat, ale fragmentace je potřeba" << endl; }
Nepřišel pro nás ICMP paket
Jestliže 5 sekund nepřijde ICMP paket, který je určen pro nás, znamená to, že se ECHO žádost ztratila, ale my jsme o tom nebyli informováni žádným ICMP paketem. V takovém případě opět zmenšujeme odesílaný buffer. Maximální hodnotu bufferu nastavujeme na hodnotu velikosti aktuálního bufferu.
} else { cout << "Čas vypršel. Žádost asi nedorazila." << endl; maxBuffer = lenghtBuffer; break; }
Ukončení main
ICMP pakety přijímáme, dokud některý z nich není určen pro nás nebo dokud nevyprší časový limit. V tom případě bude cyklus opuštěn díky příkazu break. V opačném případě vše kontrolujeme v podmíncewhile. Pokud je ICMP paket určen pro nás nebo vypršel časový limit, uvolníme paměť pro aktuální buffer a vypočítáme velikost nového bufferu.
} while (!((icmpRecv->type == ICMP_ECHOREPLY) && (icmpRecv->un.echo.id == pid) && (icmpRecv->un.echo.sequence == sequence - 1))); free(ipSend); lenghtBuffer = (minBuffer + maxBuffer) / 2; } while (minBuffer < maxBuffer - 1); cout << "Výsledek: MTU = " << lenghtBuffer << (lenghtBuffer == 28? " nebo méně" : "" ) << endl; close(sock); return 0; }
Funkce isMTUProblem
Funkce je velice podobná funkci isLost z dílu Sokety a C/C++ – program traceroute. Jako parametry jsou funkci předány:
- Ukazatel na začátek ICMP paketu
- Velikost ICMP paketu
- Identifikátor ECHO žádosti – chceme vědět, zda ICMP paket, na který se odkazuje první parametr, informuje o zahození ECHO žádosti s daným identifikátorem.
- Sekvenční číslo naposledy odeslané ECHO žádosti.
Funkce vrací true v případě, že ICMP paket určený prvním parametrem je typu 3, kódu 4 a informuje o zahození naposledy odeslané ICMP ECHO žádosti. V opačném případě vrací false. Ve funkci také kontrolujeme velikost ICMP paketu, aby nedošlo ke čtení dat z nealokované paměti.
bool isMTUProblem(char *buffer, unsigned short int bufferLenght, unsigned short int id, unsigned short int sequence) { if (bufferLenght < 2 * sizeof(icmphdr) + sizeof(iphdr)) { return false; } icmphdr *icmpRecv = (icmphdr *)buffer; iphdr *ipSend = (iphdr *)(buffer + sizeof(icmphdr)); icmphdr *icmpSend = (icmphdr*) ((char *)ipSend + ipSend->ihl * 4); if (bufferLenght < 2 * sizeof(icmphdr) + ipSend->ihl * 4) { return false; } if ((icmpRecv->type == ICMP_DEST_UNREACH) && (icmpRecv->code == ICMP_FRAG_NEEDED) && (icmpSend->type == ICMP_ECHO) && (icmpSend->code == 0) && (icmpSend->un.echo.id == id) && (icmpSend->un.echo.sequence == sequence)) { return true; } return false; }
Už několikrát jsem ve svém seriálu zdůrazňoval, že IP protokol nemusí mít vždy IP hlavičku velikosti 20 bytů. IP hlavička může obsahovat volitelné položky. Příště se podíváme na volitelné položky hlavičky IP podrobněji.
Školení: Pokročilejší kurz jazyka Java
- práce s řetězi, regulární výrazy
- vlákna, synchronizace, polymorfismus
- práce s databázemi v Java (JDBC)
- grafické rozhranís AWT a Swing
Detailnější informace o kurzu...
Seriál Sokety a C/C++
- Sokety a C/C++: program traceroute
- Sokety a C/C++: MTU a IP fragmentace
- Sokety a C/C++: MTU a IP fragmentace (dokončení)
- Sokety a C/C++: volitelné položky IP záhlaví
- Sokety a C/C++: zaznamenávání směrovačů
Přehled názorů
Tento text je již více než dva měsíce starý. Chcete-li na něj reagovat v diskusi, pravděpodobně vám již nikdo neodpoví. Pro řešení aktuálních problémů doporučujeme využít naše diskusní fórum.