Hlavní navigace

Sokety a C/C++: program traceroute

Radim Dostál 11. 8. 2003

Dnes napíšeme jednoduchou implementaci programu traceroute. Vysvětlíme si princip programu traceroute a seznámíme se blíže s ICMP paketem typu 11 kódu 0. V článku je podrobně okomentovaný zdrojový text.

Program traceroute slouží k zjištění počítačů, které se nacházejí mezi našim a zadaným počítačem. Data putující po síti (na úrovni internetové vrstvy protokolu TCP/IP – tedy vrstvy zajišťující komunikaci počítačů, které nemusejí být bezprostředně propojeny) procházejí různými počítači (směrovači), které se nalézají na cestě mezi dvěma komunikujícími počítači. Program traceroute slouží k zjištění IP adres počítačů (přesněji IP adres síťových karek, kterými data do počítače vstupují), které jsou mezi našim a zadaným vzdáleným počítačem.

Program traceroute je k dispozici snad v každém OS, který podporuje protokol TCP/IP. V MS WindowsŽ se program jmenuje tracert.

Princip traceroute

Princip programu je opět (tak jako u programu ping) založen na odesílání požadavku ECHO. Program traceroute je už ale trochu složitější než ping. Traceroute odesílá postupně ECHO žádosti na počítač. První ECHO žádost odešle s atributem TTL nastaveným na 1. Znamená to, že odeslaný IP paket může projít maximálně přes jeden počítač. Pak bude zahozen. V případě, že dojde k zahození IP paketu, kvůli snížení jeho TTL na 0, bude odesílatel IP paketu upozorněn na zahození odeslaného IP paketu ICMP paketem typu 11, kódu 0. Program přijme signalizační ICMP paket typu 11 kódu 0. Poznačí si (vypíše) jeho odesílatele. Zvýší TTL o jedna a opět odešle ECHO žádost. Takhle pokračuje dokud neobdrží ICMP paket ECHO odpověď.

Popis algoritmu

Počítač, ke kterému chceme zjistit cestu nazveme POČÍTAČ.

  1. Nastav hodnotu TTL na 1.
  2. Odešli ECHO žádost na POČÍTAČ.
  3. Přečti příchozí ICMP paket.
  4. Jestliže se jedná o ECHO odpověď k naší žádosti (shodný identifikátor i sekvenční číslo), potom konec. Nalezli jsme cestu.
  5. Jestliže se nejedná o ICMP paket typu 11, kódu 0, který oznamuje ztrátu námi odeslané žádosti, jdi na bod 3.
  6. Vypiš (poznamenej) adresu odesílatele ICMP paketu typu 11, kódu 0.
  7. Zvyš hodnotu TTL o 1.
  8. Jestliže hodnota TTL nepřekročila maximální možnou hodnotu TTL (256 a vyšší), pak jdi na bod 2.
  9. Konec – cestu jsme nenalezli

Popis ICMP paketu typu 11, kódu 0 (Čas vypršel – položka TTL klesla na 0)

ICMP paket signalizuje zahození IP paketu z důvodu snížení hodnoty TTL na 0. Je odeslán počítačem, který IP paket zahodil, počítači, který IP paket odeslal. Tím se odesílatel zahozeného IP paketu dozví, že došlo k zahození paketu, který odesílal. Také se dozví, kdo jej zahodil. Při popisu algoritmu 5 jsem napsal „který oznamuje ztrátu námi odeslané žádosti“. Měli bysme opravdu kontrolovat, jestli příchozí ICMP paket typu 11 a kódu 0 signalizuje zahození právě našeho paketu. Neměl by nám stačit fakt, že přišel ICMP paket typu 11 a kódu 0. Měli bysme si zjistit, který IP paket byl zahozen. Víme už totiž, že soket typu SOCK_RAW používající protokol IPPROTO_ICMP přijímá všechny pakety přicházející na počítač. Ne všechny ICMP pakety typu 11 a kódu 0, které soketem obdržíme, musejí informovat o zahození námi odeslaných IP paketů. Například program traceroute může v jeden okamžik běžet na počítači vícekrát.

ICMP paket typu 11 a kódu 0 má za svou hlavičkou tělo. Tělo ICMP paketu obsahuje IP hlavičku zahozeného IP paketu (typicky 20 bytů, ale nemusí tomu tak být vždy). Za IP hlavičkou následuje prvních 64 bytů těla zahozeného IP paketu. V našem případě byla zahozená ICMP žádost o ECHO. Proto za IP hlavičkou bude následovat ICMP hlavička ECHO žádosti. Právě z hlavičky ECHO žádosti, pomocí identifikátoru žádosti a sekvenčního čísla, zjistíme, zda se opravdu jedná o naši žádost. ICMP paket typu 11, kódu 3 je svou strukturou velmi podobný paketu „Nedosažitelný UDP port – typ 3, kód 3“. Přijatý buffer bude vypadat asi takto:

Tabulka č. 462
Typicky 20 bytů IP hlavička přijatého IP paketu. Paket v sobě nesl ICMP paket typu 11, kódu 0.
8 bytů ICMP hlavička typu 11 kódu 0.
Typicky 20 bytů IP hlavička zahozeného IP paketu. Paket v sobě nesl ICMP žádost o ECHO. Odesílatelem paketu jsme byli my.
Maximálně 64 bytů Tělo zahozeného IP paketu. V našem případě IP paket ve svém těle nesl ICMP paket ECHO žádost. ICMP paket měl pouze hlavičku (ICMP tělo v naších příkladech neposíláme). Proto místo 64 bytů zde bude pouze 8:
  • 8 bytů Hlavička ECHO žádosti, kterou jsme odeslali. (Obsahuje identifikátor žádosti a pořadové číslo žádosti.)
  • 0 bytů Tělo ECHO žádosti – my žádné tělo neposíláme.

Příklad

Nyní si jednoduchou implementaci programu traceroute naprogramujeme. Opět, tak jako u našeho pingu, bude náš traceroute za ostatními trochu zaostávat. Program traceroute má mnoho možností nastavení. Náš program nebude mít žádnou.

V programu postupně odesíláme ECHO žádosti a zvyšuje jim hodnotu TTL. Každá žádost má jiné sekvenční číslo. Je jistě pohodlné a přehledné mít hodnotu TTL a sekvenčního čísla stejnou. Já používám proměnnou ttl pro uchování hodnoty TTL poslední žádosti, ale také pro uchování hodnoty posledního sekvenčního čísla. Tím možná trochu matu pojmy a čtenáře, ale také ušetřím jednu proměnnou.

Formality

Vložíme potřebné hlavičkové soubory, deklarujeme funkce, definujeme makra, začneme funkci main a deklarujeme lokální proměnné ve funkcimain. Nakonec zkontrolujeme počet parametrů z příkazového řád­ku.

#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;

unsigned short checksum(unsigned char *addr, int count);
bool isLost(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 *ip;
  int sock, lenght;
  unsigned int ttl;
  sockaddr_in sendSockAddr, receiveSockAddr;
  char buffer[BUFSIZE];
  fd_set mySet;
  timeval tv;
  char *addrString;
  unsigned short int pid = getpid();

  if (argc != 2)
  {
    cerr << "Syntaxe:\n\t" << argv[0]
     << " " << "adresa" << endl;
    return -1;
  }

Překlad doménového jména

Z parametru zadaného z příkazové řádky získáme strukturu hostent.

  if ((host = gethostbyname(argv[1])) == NULL)
  {
    cerr << "Špatná adresa" << endl;
    return -1;
  }

Vytvoříme soket

Vytvářený soket bude typu SOCK_RAW s použitým protokolem IPPROTO_ICMP.

  if ((sock = socket(PF_INET, SOCK_RAW, IPPROTO_ICMP)) == -1)
  {
    cerr << "Nelze vytvořit soket" << endl;
    return -1;
  }

Částečně vyplníme hlavičku ICMP paketu

Vyplníme tu část ICMP hlavičky, kterou budou mít všechny žádosti stejné. Typ paketu bude žádost o ECHO. Podtyp 0 a identifikátor bude mít hodnotu identifikátoru procesu.

  icmp = (icmphdr *)malloc(sizeof(icmphdr));
  icmp->type = ICMP_ECHO;
  icmp->code = 0;
  icmp->un.echo.id = pid;

Zaplnění struktury sockaddr_in

  sendSockAddr.sin_family = AF_INET;
  sendSockAddr.sin_port = 0;
  memcpy(&(sendSockAddr.sin_addr), host->h_addr, host->h_length);

Nastavení proměnné TTL na 1

Nastavíme proměnnou, která udává velikost TTL naposledy odeslané žádosti.

  ttl = 1;

Doplnění atributů ICMP hlavičky

V cyklu budeme zaplňovat ty atributy ICMP hlavičky, které se budou pro jednotlivé žádosti lišit. V každém průchodu cyklem odešleme jednu žádost.

  do
  {
    icmp->checksum = 0;
    icmp->un.echo.sequence = ttl;
    icmp->checksum =
        checksum((unsigned char *)icmp, sizeof(icmphdr));

Nastavení vlastnosti TTL u soketu

Nastavíme vlastnost IP_TTL úrovně SOL_IP.

    setsockopt(sock, SOL_IP, IP_TTL,
        (const char *)&ttl, sizeof(ttl));

Odešleme data

Odešleme ICMP žádost ECHO.

    sendto(sock, (char *)icmp, sizeof(icmphdr),
        0, (sockaddr *)&sendSockAddr, sizeof(sockaddr));

Přijímání ICMP paketů

Budeme přijímat všechny ICMP pakety, které přijdou. Zajímat nás budou ale jen některé.

    do
    {
      FD_ZERO(&mySet);
      FD_SET(sock, &mySet);
      if (select(sock + 1, &mySet, NULL, NULL, &tv) < 0)
      {
    cerr << "Selhal select" << endl;
    break;
      }
      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 příchozího ICMP paketu

Z IP hlavičky příchozího paketu zjistíme velikost hlavičky. Za IP hlavičkou se nachází ICMP hlavička (bude na ní ukazovat ukazatel icmpRecv). Zajímají nás pouze ECHO odpovědi nebo ICMP pakety typu 11, kódu 0. Funkce isLost je popsána níže. Vrací nám true v případě, že ICMP paket opravdu signalizuje ztrátu naší ECHO žádosti. Jedná-li se o ICMP pakety, které nás zajímají, vypíšeme o nich informace.

        ip = (iphdr *) buffer;
    icmpRecv = (icmphdr *) (buffer + ip->ihl * 4);
    if (isLost((char *)icmpRecv,
        lenght - ip->ihl * 4, pid, ttl))
    {
      addrString=strdup(inet_ntoa(receiveSockAddr.sin_addr));
      host = gethostbyaddr(&receiveSockAddr.sin_addr,
        4, AF_INET);
      cout << ttl << "\t" << lenght << " bytů z "
               << (host == NULL? "?" : host->h_name) << " ("
               << addrString << ") " << endl;
      free(addrString);
    }
        if ((icmpRecv->type == ICMP_ECHOREPLY)
            && (icmpRecv->code == 0)
            && (icmpRecv->un.echo.id == pid)
            && (icmpRecv->un.echo.sequence == ttl))
        {
          addrString =
              strdup(inet_ntoa(receiveSockAddr.sin_addr));
          host = gethostbyaddr(&receiveSockAddr.sin_addr,
                              4, AF_INET);
          cout << ttl << "\t" << lenght << " bytů z "
                   << (host == NULL? "?" : host->h_name) << " ("
                   << addrString << ") " << endl;
          free(addrString);
        }
      }

Čas vypršel

Jestliže nepřišel požadovaný ICMP paket za 5 sekund, vypíšeme hlášku.

      else
      {
    cout << "Čas vypršel" << endl;
    break;
      }

Podmínka pro přijímání ICMP paketu

Přijímáme ICMP pakety, dokud neobdržíme ECHO odpověď na naší otázku, nebo ICMP paket typu 11, kódu 3, který je určen pro nás.

    }while (!(isLost((char *)icmpRecv,lenght-ip->ihl*4, pid, ttl)
        || ((icmpRecv->type == ICMP_ECHOREPLY)
        && (icmpRecv->code == 0)
        && (icmpRecv->un.echo.id == pid)
        && (icmpRecv->un.echo.sequence == ttl))));

Zvýšíme TTL o 1

    ttl++;

Podmínka pro odesílání ICMP paketů

Odesílání ICMP paketů má smysl, dokud nepřijde ICMP odpověď na ECHO nebo dokud položka TTL nepřekročí maximální hodnotu.

  } while ((ttl != 256)
    && (!((icmpRecv->type == ICMP_ECHOREPLY)
    && (icmpRecv->code == 0)
    && (icmpRecv->un.echo.id == pid)
    && (icmpRecv->un.echo.sequence == ttl - 1))));

Ukončení programu

Je-li hodnota TTL o 1 více než maximální možná, nebyl hledaný počítač nalezen. V takovém případě vypíšeme informaci o nedosažení počítače. Dále uvolníme paměť, uzavřeme soket a ukončíme main.

  if (ttl == 256)
  {
        cout << "Počítač nedosažen" << endl;
  }
  close(sock);
  free(icmp);
  return 0;
}

Funkce isLost

V programu používám funkci isLost, která vrací true v případě, že ICMP paket signalizuje zahození zadané ECHO žádosti. Prvním parametrem je ukazatel za IP hlavičku dat, která jsme obdrželi. Druhým parametrem je velikost dat, která jsou ještě za místem, na které se odkazuje první ukazatel, alokována. Další parametry jsou identifikátor a pořadové číslo naší žádosti.

bool isLost(char *buffer, unsigned short int bufferLenght,
    unsigned short int id, unsigned short int sequence)
{

Kontrola délky bufferu

Je-li buffer kratší než minimální možný, vrátíme false. Díky této podmínce se nemůže stát, že bysme četli data z nealokované paměťi. IP hlavička může být ale větší, proto zkontrolujeme velikost ještě jednou.

   if (bufferLenght < 2 * sizeof(icmphdr) + sizeof(iphdr))
   {
     return false;
   }

Nastavení ukazatelů

Nasměrujeme do bufferu ukazatele přesně tak, jak jdou data za sebou.

   icmphdr *icmpRecv = (icmphdr *)buffer;
   iphdr *ipSend = (iphdr *)(buffer + sizeof(icmphdr));
   icmphdr *icmpSend =
    (icmphdr*) ((char *)ipSend + ipSend->ihl * 4);

Kontrola velikosti bufferu

Nyní můžeme konečně s jistotou zkontrolovat velikost bufferu.

   if (bufferLenght <2 * sizeof(icmphdr) + ipSend->ihl * 4)
   {
     return false;
   }

Kontrola obsahu ICMP paketu

Vrátíme true v případě, že se jedná o ICMP paket typu 11 (makro ICMP_TIME_EXCE­EDED), kódu 0 (makro ICMP_EXC_TTL) a ve svém těle nese za IP hlavičkou námi odeslanou ECHO žádost. Jinak vrátíme false.

   if ((icmpRecv->type == ICMP_TIME_EXCEEDED)
    && (icmpRecv->code == ICMP_EXC_TTL)
    && (icmpSend->type == ICMP_ECHO)
    && (icmpSend->code == 0)
    && (icmpSend->un.echo.id == id)
    && (icmpSend->un.echo.sequence == sequence))
   {
        return true;
   }
   return false;
}

Příklady ke stažení

Tabulka č. 463
Soubor Operační systém
lin23.tgz Linux
win23.zip MS WindowsŽ

Příště se podíváme na hodnotu MTU. Vysvětlíme si pojem MTU a povíme si, jak změřit MTU mezi našim a vybraným počítačem.

Našli jste v článku chybu?

13. 8. 2003 21:02

Tomas (neregistrovaný)

tak na toto tam mam niekolko assertov, vsetko poctivo zatvaram (close())a jobov mam relativne malo. ono to (ne)funguje tak, ze sa to rozbehne, bezi, potom to cca. 30 x spadne na tej chybe pri volani connect() a potom bezi dalej ...

13. 8. 2003 15:01

PaJaSoft (neregistrovaný)

Mozna je to jen pruvodni jev pri vycerpanych file deskriptorech... - kouknete na ulimit.

Podnikatel.cz: K EET. Štamgast už peníze na stole nenechá

K EET. Štamgast už peníze na stole nenechá

DigiZone.cz: Česká televize mění schéma ČT :D

Česká televize mění schéma ČT :D

Podnikatel.cz: Přehledná titulka, průvodci, responzivita

Přehledná titulka, průvodci, responzivita

Root.cz: Vypadl Google a rozbilo se toho hodně

Vypadl Google a rozbilo se toho hodně

Vitalia.cz: Říká amoleta - a myslí palačinka

Říká amoleta - a myslí palačinka

Lupa.cz: Avast po spojení s AVG propustí 700 lidí

Avast po spojení s AVG propustí 700 lidí

Podnikatel.cz: Prodává přes internet. Kdy platí zdravotko?

Prodává přes internet. Kdy platí zdravotko?

DigiZone.cz: Sony KD-55XD8005 s Android 6.0

Sony KD-55XD8005 s Android 6.0

Měšec.cz: U levneELEKTRO.cz už reklamaci nevyřídíte

U levneELEKTRO.cz už reklamaci nevyřídíte

Lupa.cz: Babiš: E-shopů se EET možná nebude týkat

Babiš: E-shopů se EET možná nebude týkat

Vitalia.cz: Paštiky plné masa ho zatím neuživí

Paštiky plné masa ho zatím neuživí

Lupa.cz: UX přestává pro firmy být magie

UX přestává pro firmy být magie

Vitalia.cz: To není kašel! Správná diagnóza zachrání život

To není kašel! Správná diagnóza zachrání život

Lupa.cz: Co se dá měřit přes Internet věcí

Co se dá měřit přes Internet věcí

120na80.cz: Rakovina oka. Jak ji poznáte?

Rakovina oka. Jak ji poznáte?

Lupa.cz: Není sleva jako sleva. Jak obchodům nenaletět?

Není sleva jako sleva. Jak obchodům nenaletět?

Vitalia.cz: Baletky propagují zdravotní superpostel

Baletky propagují zdravotní superpostel

Podnikatel.cz: 1. den EET? Problémy s pokladnami

1. den EET? Problémy s pokladnami

Podnikatel.cz: EET: Totálně nezvládli metodologii projektu

EET: Totálně nezvládli metodologii projektu

Podnikatel.cz: Na poslední chvíli šokuje vyjímkami v EET

Na poslední chvíli šokuje vyjímkami v EET