Hlavní navigace

Sokety a C/C++: časové razítko

Radim Dostál 15. 9. 2003

Dnes se podíváme na další volitelnou položku IP záhlaví, na volbu "časové razítko". Existují tři varianty položky "časové razítko". Dvě si ukážeme v příkladu a o třetí se okrajově zmíníme.

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_TSAN­DADDR 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í

Tabulka č. 491
Soubor Operační systém
lin28.tgz Linux
win28.zip MS Windows Ž

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[IPOP­T_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é.

Tabulka č. 492
Soubor Operační systém
lin27.tgz Linux
win27.zip MS Windows Ž

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á.

Našli jste v článku chybu?

27. 9. 2003 21:55

Radek (neregistrovaný)

Diky za poucne povidani o zmrdech, v par vecech sem se tam taky poznal.

Ted bych ale potreboval poradit s prikladem TraceRootu. Nevim proc, ale stale se mi vraci pouze IP adresa 0.0.0.0. Chyba je v packetu ktery se prijme. Necite nekdo co s tim?

Radek





17. 9. 2003 10:01

neco tam napis (neregistrovaný)

http://www.dfens9.wz.cz/zmrdi/zmrdi.htm

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

Přehledná titulka, průvodci, responzivita

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

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

Vitalia.cz: „Připluly“ z Německa a možná obsahují jed

„Připluly“ z Německa a možná obsahují jed

Lupa.cz: Slevové šílenství je tu. Kde nakoupit na Black Friday?

Slevové šílenství je tu. Kde nakoupit na Black Friday?

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

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

120na80.cz: Jak oddálit Alzheimera?

Jak oddálit Alzheimera?

Lupa.cz: Proč firmy málo chrání data? Chovají se logicky

Proč firmy málo chrání data? Chovají se logicky

Lupa.cz: Google měl výpadek, nejel Gmail ani YouTube

Google měl výpadek, nejel Gmail ani YouTube

Vitalia.cz: Chtějí si léčit kvasinky. Lék je jen v Německu

Chtějí si léčit kvasinky. Lék je jen v Německu

DigiZone.cz: NG natáčí v Praze seriál o Einsteinovi

NG natáčí v Praze seriál o Einsteinovi

Měšec.cz: Zdravotní a sociální pojištění 2017: Připlatíte

Zdravotní a sociální pojištění 2017: Připlatíte

Vitalia.cz: Tesco: Chudá rodina si koupí levné polské kuře

Tesco: Chudá rodina si koupí levné polské kuře

Vitalia.cz: Jsou čajové sáčky toxické?

Jsou čajové sáčky toxické?

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

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

Podnikatel.cz: Víme první výsledky doby odezvy #EET

Víme první výsledky doby odezvy #EET

120na80.cz: Pánové, pečujte o svoje přirození a prostatu

Pánové, pečujte o svoje přirození a prostatu

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

1. den EET? Problémy s pokladnami

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

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

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

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

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

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