Sokety a C/C++: Tělo ICMP paketu

28. 7. 2003
Doba čtení: 12 minut

Sdílet

Dnes si ukážeme situaci, kdy ICMP paket přenáší data ve svém těle. Podrobněji si na příkladu rozebereme ICMP paket typu 3, kódu 3. Dnešní článek bude takové procvičení pointerové aritmetiky jazyka C.

ICMP protokol přenáší řídící informace, které jsou nutné pro provoz sítě. Zatím jsem mluvil pouze o situacích, kdy je přenášena pouze ICMP hlavička. Nebo spíše přesněji, všímali jsme si pouze hlavičky ICMP paketu. Jenomže ICMP paket nemá pouze hlavičku. Může obsahovat také tělo. I v těle ICMP paketu se přenášejí řídící data a bylo by možné přenášet i jiná data.

Existuje několik možností, v jakém formátu může být tělo ICMP paketu. Nebudeme si ukazovat všechny, pouze se zaměříme na jednu možnost, kterou si ukážeme v příkladě. Ostatní by se dělaly obdobně. V podstatě vždy jde jen o to, správně posunovat ukazatele v bufferu, který přijmeme.

ECHO

I požadavek o ECHO nebo ECHO odpověď (ozvěna) může mít své tělo. Jednoduše za ICMP hlavičkou je nějaký blok dat. Není předepsáno, jak má vypadat. Používá se v případě, že chceme ovlivnit velikost ICMP paketu u programu ping. Obvykle obsahuje náhodná data. Touto situací se nebudeme zabývat. Jen nastíním v příštím článku (kdy si napíšeme jednoduchou implementaci ping), jak přidat k ICMP žádosti o ECHO tělo paketu.

Potíže s IP paketem

ICMP protokol signalizuje v mnoha případech nějaké potíže s IP paketem. Odesílatel takového problematického IP paketu je informován ICMP paketem. Může se jednat například o ICMP paket typu 3 (Nedoručitelný IP datagram), 4 (Sniž rychlost odesílání), 5 (Změň směrování), 11 (Čas vypršel). Některá typy ICMP paketů jsem popsal v článku Sokety a C/C++ – ICMP protokol. V takovém případě za hlavičkou ICMP paketu následuje tělo, obsahující informace o původním IP paketu, který způsobil problémy.

Tělo ICMP paketu se skládá ze záhlaví problematického IP paketu a prvních 62 bytů paketu. Což je čistá teorie, ve skutečnosti je tomu trochu jinak – viz experimenty na konci článku. Ještě jen podotknu, že u typu ICMP paketu 3, 4, 11 jsou druhé čtyři byty ICMP hlavičky nevyužity a nastaveny na 0. Také nesmíme zapomenout na to, že hlavička IP paketu nemusí mít vždy 20 bytů. Může obsahovat volitelné položky. Délka hlavičky vydělená čtyřmi je obsažena v atributu ihl IP hlavičky.

Nedosažitelný UDP port – typ 3, kód 3

Tento typ ICMP paketu jsem si vybral jako příklad. Podrobněji jej zanalyzujeme. Jestliže odešleme UDP datagram na nějaký počítač na nějaký port a na tomto portu nečeká žádný proces na přijetí UDP datagramu, ICMP paket typu 3 a kódu 3 nás na to upozorní. Máme tedy otevřený soket typu SOCK_RAW a používáme protokol IPPROTO_ICMP. Z tohoto soketu přijmeme data. Máme blok dat, který analyzujeme. Co obsahuje?

  • IP hlavičku – IP hlavička paketu, který nám doručil ICMP paket s upozorněním.
  • ICMP hlavičku – Hlavička ICMP paketu typu 3, kódu 3.
  • Tělo ICMP paketu – skládá se z:
    • IP záhlaví paketu, který jsme poslali (přenášel UDP datagram). Pozor, nemusí být vždy 20 bytů!
    • Prvních 62 bytů z problematického IP paketu. IP paket přenášel UDP datagram, takže obsahoval:
      • UDP hlavičku – 8 byte UDP hlavičky
      • Začátek těla UDP datagramu – teoreticky 62 – 8 = 54 počátečních bytů z UDP datagramu. Záleží ale na OS vzdáleného počítače.
Tabulka č. 458
IP hlavička ICMP hlavička ICMP tělo
Typ Kód Kontrolní součet Volitelné položky UDP hlavička Začátek UDP těla
Typicky 20 byte – popsána je v článku Struktura IP a UDP. 1 byte 1 byte 2 byte 4 byte 8 byte – popsána je v článku Struktura IP a UDP. Prvních 54 byte dat z UDP datagramu.

Jak je asi zřejmé, přečíst data z ICMP paketu není nijak složité. Jen musí člověk znát strukturu ICMP data (RFC dokumenty, Internet, literatura) a potom se neztratit v pointerové aritmetice jazyka C.

Použité funkce

Již v minulém dílu se objevila funkce z soketového API, o které jsem se předtím nikdy nezmínil. Dnes přibude další nová funkce.

Funkce gethostbyaddr

Funkce vytvoří strukturu hostent odpovídající zadané IP adrese.

Funkce v Linuxu

  • struct hostent* gethostbyaddr(con­st char *addr, int len, int type);

Funkce v MS WindowsŽ

  • struct HOSTENT* FAR gethostbyaddr(con­st char *addr, int len, int type);

Prvním parametrem je ukazatel na adresu (na 4 byte) v síťovém tvaru. Druhý parametr udává velikost adresy v bytech (4). Posledním parametrem je typ adresy (makro AF_INET). Funkce vrací NULL v případě neúspěchu, jinak vrátí ukazatel na řádně vyplněnou strukturu s informacemi o počítači. Funkce pracuje obdobně jako gethostbyname, se kterou jsme se setkali již v prvních dílech. Rozdíl je jen v tom, že gethostbyaddr má jako parametr doménové jméno, ale gethostbyaddr má jako parametr IP adresu.

Funkce ntohs

Funkce převádí dvoubytové bezznaménkové číslo ze „síťového“ tvaru do tvaru používaného na platformě hostitelského počítače (Network To Host). Jedná se v podstatě o funkci, která dělá přesný opak nám již známé funkce htons (Host To Network). Parametrem funkce je číslo unsigned shot int. Funkce vrací unsigned shot int. Jak funkce htons, tak i ntohs slouží k vyřešení problémů s prezentací vícebytového čísla.

Funkce v Linuxu

  • unsigned short int ntohs(unsigned short int netshort);

Funkce v MS Windows

  • u_short ntohs(u_short netshort);

Příklad

Tentokrát si vytvoříme funkci, která nám zanalyzuje přijatá data a vypíše získané informace. Funkci vložíme do příkladů z minulého dílu.

Formality

Vytvoříme hlavičku funkce a deklarujeme lokální proměnné. Jako parametry funkce dostaneme ukazatel na blok dat (buffer) a jeho velikost. V bufferu jsou přímo data, která zaplnila funkce recvfrom. Velikost dat je návratová hodnota funkce recvfrom.

void analyzeBody(char *buffer, unsigned int size)
{
    char *addr;
    hostent *host;
    in_addr a;

IP hlavička

Na začátku je IP hlavička IP paketu, který nám přinesl ICMP paket.

    iphdr *ip = (iphdr *)buffer;

ICMP hlavička

Záhlaví ICMP paketu začíná hned za IP hlavičkou. Neměli bychom spoléhat na to, že IP hlavička má 20 bytů. Může totiž obsahovat volitelné položky. Velikost IP hlavičky vydělená čtyřmi je v atributu ihl.

    icmphdr *icmp = (icmphdr *)(buffer + ip->ihl * 4);

ICMP data

Ihned za ICMP hlavičkou budou následovat ICMP data. Naši funkci bychom měli volat pouze v případě, že se jedná o ICMP paket typu 3, 4, 5, nebo 11. V takovém případě má buffer takový tvar, jaký je znázorněn v tabulce. Znamená to, že ICMP data obsahují IP hlavičku problematického paketu (ten jsme odeslali my). Ukazatel na tuto hlavičku jsem nazval ipBad. Upozorňuji, že přetypování na ukazatel na char ve výrazu je nutné!

iphdr *ipBad = (iphdr *)((char *)icmp + sizeof(icmphdr));

Výpis z hlavičky problematického IP paketu

Vypíšeme informace z hlavičky problematického IP paketu. Na začátek hlavičky se odkazuje ukazatel ipBad.

 printf("IP paket, se kterým byly problémy:\n");
 printf("\tVerze IP: %d\n", (int)ipBad->version);
 printf("\tVelikost hlavičky: %d\n", (int)ipBad->ihl * 4);
 printf("\tIdentifikátor datagramu: %d\n", ntohs(ipBad->id));
 printf("\tTTL: %d\n", (int)ipBad->ttl);
 printf("\tProtokol přenášený paketem: ");
 switch (ipBad->protocol)
 {
     case IPPROTO_UDP: printf("UDP\n"); break;
     case IPPROTO_TCP: printf("TCP\n"); break;
     case IPPROTO_ICMP: printf("ICMP\n"); break;
     default: printf("Nějaký jiný - %d \n", ipBad->protocol);break;
 }

Odesílatel vadného IP paketu

Z hlavičky problémového IP paketu zjistíme adresu odesílatele (atribut saddr). Odesílatelem jsme byli my, ale zjistíme tím rozhraní, kterým IP paket opustil náš počítač. IP adresu si nejprve převedeme do (pro člověka) čitelné formy, poté se pokusíme zjistit z IP adresy doménové jméno. Informace vypíšeme.

 a.s_addr = ipBad->saddr;
 addr = strdup(inet_ntoa(a));
 host = gethostbyaddr((char *)&a, 4, AF_INET);
 printf("\tByl odeslán z rozhraní: %s (%s)\n",
        (host == NULL? "?" : host->h_name), addr);
 free(addr);

Příjemce vadného IP paketu

Z hlavičky problémového IP paketu zjistíme adresu příjemce (atributdaddr). Dále postupujeme stejně jako v případě adresy odesílatele.

 a.s_addr = ipBad->daddr;
 addr = strdup(inet_ntoa(a));
 host = gethostbyaddr((char *)&a, 4, AF_INET);
 printf("\tPaket byl poslán na: %s (%s)\n",
        (host == NULL? "?" : host->h_name), addr);

Analýza UDP

Jestliže se jedná o ICMP paket typu 3, kódu 3, pokračuje za IP hlavičkou UDP datagram, který vlastně tvoří tělo IP paketu. UDP datagram začíná svou hlavičkou. UDP datagram začíná za IP hlavičkou problémového IP paketu. Opět velikost IP hlavičky zjistíme pomocí atributu ihl. Přetypování na ukazatel na char ve výrazu je opět nutné!

 if ((icmp->type == ICMP_DEST_UNREACH)
    && (icmp->code == ICMP_PORT_UNREACH))
 {
     udphdr *udp = (udphdr *)((char *)ipBad + ipBad->ihl * 4);

Výpis informací z UDP hlavičky – konec

Vypíšeme informace z UDP hlavičky, ukončíme blok příkazů patřící větvi if, uvolníme používanou paměť a ukončíme funkci.

     printf("\tIP paket nesl UDP datagram, který měl
            špatný cílový port.\n");
     printf("\tZdrojový port %d\n", ntohs(udp->source));
     printf("\tCílový port: %d\n", ntohs(udp->dest));
     printf("\tVelikost UDP (odeslaného) datagramu: %d\n",
            ntohs(udp->len));
     printf("\tNa počítači %s (%s) není na UDP portu %d nikdo
            připraven.\n",(host == NULL? "?" : host->h_name),
        addr, ntohs(udp->dest));
  }
  free(addr);
}

Ve funkci není ošetřena situace, kdy přijde kratší blok dat, než předpokládáme, což je velmi lehkovážné. Pro naše jednoduché příklady to ale stačí.

Naši funkci vložíme do příkladů z minulého dílu. Příklad pro MS WindowsŽ jsem opravil. Oba nové příklady jsou k dispozici ke stažení na konci článku.

WinSock a minulý díl

V minulém dílu jsem v příkladu pro MS WindowsŽ trochu tápal. Výsledkem je v podstatě nefunkční příklad. Nerad měním již vydané články, proto nebudu měnit ani příklad k předchozímu článku. Dnes je stejně k dispozici opravená a rozšířená verze.

Problém byl v tom, jak upozornil v diskusi pod článkem jeden čtenář, že jsem nevolal funkci bind pro „přivázání“ soketu se síťovým rozhraním. V MS WindowsŽ je nutné na RAW soket zavolat bind. Neuděláme-li tak, nemůžeme zavolat sendto. V Linuxu tento problém nenastává, není nutné volat bind. To jsem nevěděl, a proto vznikl chybný příklad pro MS WindowsŽ.

Za chybu se omlouvám.

Experimenty s příkladem

Nyní pro lepší pochopení si s programem trochu zaexperimentujeme. Aby vše fungovalo tak, jak píšu, nesmí být mezi počítačem, na kterém budeme experimentovat, a počítači, na které z našeho počítače budeme posílat ICMP pakety, žádný firewall, který zahodí naše nebo pro nás určené ICMP pakety.

1) Monitoring ICMP

Spusťte program na počítači, který je připojen k síti. Program bude vypisovat všechny příchozí ICMP pakety. Máte-li pocit, že je na síti „nuda“, tedy že k vám nechodí ICMP pakety nebo jich je málo, spusťte program ping (testuje dostupnost počítače v síti). Dejte mu jako parametr třeba www.root.cz. Uvidíte, že na počítač začnou přicházet ECHO odpovědi.

Program ping posílá žádost o ECHO (ozvěnu) a čeká na ECHO odpověď. Ta k vašemu počítači přijde a je k dispozici všem procesům na počítači. Jednak ji přečte ping a jednak náš program.

2) Více příjemců ICMP paketů

Už to možná vyplynulo z předchozího experimentu. Každý program, který otevře soket typu SOCK_RAW a používá protokol IPPROTO_ICMP, má k dispozici všechny příchozí ICMP pakety. Nejlépe se o tom přesvědčíte, jestliže spustíte náš program na jednom počítači vícekrát (tak, aby běžel současně) a zopakujete první nebo kterýkoliv jiný experiment.

3) Více typů ICMP paketů

Jestliže vám za celou dobu experimentování s 1. a 2. experimentem nedorazil jiný ICMP paket než ECHO odpověď (a je vám to strašně líto), můžete spustit program ping s parametrem 127.0.0.1 (zpětná smyčka). Ping nyní bude odesílat žádosti o ECHO na náš počítač, což náš program zaregistruje. Počítač bude také pingu odpovídat ECHO odpovědí, kterou rovněž náš program zaregistruje. Náš program bude přijímat ECHO žádosti i odpovědi.

Takhle by to mělo fungovat, jenomže v MS WindowsŽ nemůžu získat žádost o ECHO. V Linuxu ano. Že by stále byla někde chyba v příkladu pro MS WindowsŽ? Nebo jádro MS WindowsŽ nedá žádost o ECHO aplikacím používající raw sokety? Zkušenosti čtenářů uvítám v diskusi pod článkem.

4) Různé ECHO žádosti a odpovědi

Nyní na počítači, kde běží náš program, spusťte 2× program ping, tak, aby běžel současně. Jednou mu dejte parametr například www.root.cz a podruhé třeba www.seznam.cz. Uvidíte, že příchozí ECHO odpovědi se kromě odesílatele liší také v identifikátoru ECHO odpovědi a v pořadovém čísle ECHO odpovědi. Pokud na počítači, kde běží náš program, spustíte 2× program ping se stejnou adresou (stejným parametrem), uvidíte, že příchozí ECHO odpovědi se liší pouze v identifikátoru a v pořadovém čísle ECHO odpovědi. Tyto dva atributy jsou jediné atributy, kterými lze od sebe odlišit ECHO odpovědi. O programu ping si toho řekneme příště více.

5) TTL sníženo na nulu

Nyní zkuste na počítači, na kterém běží náš program, spustit program traceroute (tracert v MS WindowsŽ) například s adresou www.root.cz. Můžete si všimnout, že začnou přicházet ICMP pakety oznamující nám, že z našeho počítače byl odeslán IP paket, který byl po cestě zahozen, protože jeho TTL kleslo na 0. Jedná se o ICMP paket typu 11 kódu 0. Souvisí to s principem programu traceroute. Až příště probereme ping, v dalším dílu se budeme věnovat traceroute, kde si jeho princip vysvětlíme podrobněji.

6) Nedosažitelný UDP port

V článku Sokety a C/C++ – Raw soket jsme vytvořili UDP datagram, který jsme odeslali na zadanou adresu a zadaný port. Na dané adrese a portu měl čekat na příchozí UDP datagram server z článku Protokol UDP 1. část. Klient odesílající UDP datagram vyplnil jako číslo UDP portu odesílatele hodnotu 4999. Tento UDP port ale není nikým obsazen. Ze serveru přijde odpověď – UDP datagram, který má jako číslo cílového portu 4999. Protože UDP port 4999 není obsazen, počítač zareaguje odesláním ICMP paketu typu 3 kódu 3. A my se o tom pomocí našeho programu přesvědčíme.

Spusťte na jednom počítači náš program z tohoto dílu a UDP server z článku Protokol UDP 1. část. Poté na jiném počítači (nemáte-li jinou možnost, můžete i na stejném) spusťte program z článku Sokety a C/C++ – Raw soket. Náš program by měl zaregistrovat příchozí ICMP paket typu 3 kódu 3.

Jednodušší možností je pustit klienta z článku Protokol UDP 1. část a na stejném počítači příklad z tohoto dílu. Nikde nebudete muset spouštět server. Vyberte si nějakou adresu nějakého počítače a jako číslo UDP portu zadejte číslo portu, na kterém určitě žádný proces nezpracovává UDP. Třeba dejte jako parametr localhost a port 5000 (ale ať tam nic neběží). Bohužel UDP klient je velmi jednoduchý a po odeslání UDP datagramu zůstane „navždy“ čekat na příchozí UDP datagram, kterého se nedočká. Pro nás je důležité, že UDP datagram odešle.

bitcoin_skoleni

V tomto experimentu si všimněte, že příchozí ICMP paket typu 3, kódu 3 nemá někdy správnou velikost. V úvodu jsem psal, že problémový IP paket, který je dopraven v těle ICMP paketu, by měl obsahovat svou hlavičku a prvních 62 bytů. Tedy UDP hlavičku (8 bytů) a prvních 54 bytů UDP datagramu. Nyní záleží hodně na tom, jaký OS je na vzdáleném počítači, kam posíláme UDP datagram. Je-li tam Linux, opravdu přijde tolik dat, kolik má. Je-li tam MS WindowsŽ nebo také AIX, chodí méně bytů. Ať sčítám, jak sčítám, chodí v těle ICMP paketu pouze IP a UDP hlavička. Za UDP hlavičkou již není nic.

Příklady

Tabulka č. 459
Soubor Operační systém
lin21.tgz Linux
win21.zip MS WindowsŽ

V příštím dílu konečně vytvoříme sami ICMP paket, který odešleme. Vytvoříme jednoduchou implementaci programu ping. Ukážeme si, jak vyplnit a odeslat žádost o ECHO, a také, jak z příchozích ICMP paketů vybrat ten, který je určen pro nás jako odpověď zrovna na naši žádost o ECHO.