Hlavní navigace

Sokety a C/C++: zaznamenávání směrovačů

9. 9. 2003
Doba čtení: 8 minut

Sdílet

Dnes si ukážeme volitelnou volbu IP záhlaví typu "zaznamenávej směrovače". V článku je okomentovaný příklad odeslání ECHO žádosti v IP paketu s nastavenou volitelnou položkou IP hlavičky typu "zaznamenávej směrovače".

V minulém článku jsme se seznámili z volitelnými položkami IP záhlaví. Dnes se podíváme podrobněji na jednu z nich. Volba „zaznamenávej směrovače“ umožňuje IP paketu vytvořit ve své hlavičce prostor, do kterého budou zapisovány IP adresy výstupních síťových rozhraní směrovačů, kterými IP paket projde. S formátem volby „zaznamenávej směrovače“ jsme se setkali již v minulém dílu.

Zadávání voleb rozšířené části IP paketu

Nejprve vytvoříme souvislý blok paměti (buffer), který nemůže mít více než 40 bytů. V této souvislosti je vhodné použít makro MAX_IPOPTLEN, které je definováno na hodnotu 40. S tímto blokem paměti budeme pracovat jako s polem char. Některé významné indexy jsou předdefinovány jako makra.

Tabulka č. 484
Makro Hodnota Význam
IPOPT_OPTVAL 0 Jako prvek s indexem 0 je typ volby (první byte volby). V našem případě to nyní bude IPOPT_RR.
IPOPT_OLEN 1 Jako prvek s indexem 1 je velikost volby (2. byte volby).
IPOPT_OFFSET 2 Jako prvek s indexem 2 je posunutí začátku první buňky s IP adresou od začátku volby. Jedná se o pointer, o kterém jsme se zmínili minule (3. byte volby).
IPOPT_MINOFF 4 Minimální hodnota posunutí. Již minule jsme si řekli, že atribut „ukazatel“ má na počátku hodnotu 4.

Všechna makra (o kterých jsme mluvili minule i dnes) jsou v Linuxu definována v hlavičkovém souboru iphdr.h. Pro překladač WinDev není tento soubor k dispozici. Nechtěl jsem již měnit soubor iphdr.h, který jsem vytvořil pro mé příklady. Vytvořil jsem nový hlavičkový soubor ip_opt.h, který je součástí příkladu ke stažení. Obsahuje definici maker potřebných pro kompilaci ukázkového příkladu v MS WindowsŽ pomocí WinDev. Jiné překladače mohou mít makra v nějakém hlavičkovém souboru definována.

Volba soketu

Volba soketu jménem IP_OPTIONS úrovně SOL_IP nám umožňuje nastavit volitelné položky IP záhlaví. Hodnota, kterou nastavujeme (pomocí funkce setsockopt), je námi vyplněný buffer, který zaplníme odpovídajícími hodnotami. S bufferem pracujeme jako s polem a indexujeme jej pomocí maker, o kterých jsem se již zmínil. Předpokládejme, že máme deklarován souvislý blok paměti délky 40 bytů. Například char option[MAX_IPOP­TLEN];, potom nastavíme hodnotu atributu typ (1. byte volby „zaznamenávej směrovače“) na 7. Provedeme to takto: option[IPOPT_OP­TVAL] = IPOPT_RR;.

Podobným způsobem nastavíme všechny atributy, čímž zaplníme náš buffer. Poté zavoláme funkci setsockopt, pomocí níž nařídíme přidat náš buffer do hlavičky IP paketů, které jsou odesílány daným soketem. Vše je vysvětleno v příkladu.

Příklad

Formailty

Vložíme hlavičkové soubory, deklarujeme makra, začneme funkci main a deklarujeme lokální proměnné v main. Měli bychom také zkontrolovat parametry příkazové řádky. Tak jako vždy. Za povšimnutí stojí proměnná ipOptions. Jedná se vlastně o pole velikosti 40 bytů (hodnota makra MAX_IPOPTLEN). Právě do tohoto pole vložíme ve správném tvaru volbu „zaznamenávej směrovače“.

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

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;

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

Překlad doménového jména

Zaplníme instanci struktury hostnet údaji o cílovém počítači, na který bude poslána naše žádost o ECHO.

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

Vytvoření soketu

Soket vytvoříme 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;
  }

Příprava sendto

Zaplníme strukturu sockaddr_in, kterou použijeme jako parametr funkce sendto. Rodina protokolů je AF_INET. Na portu nezáleží, protože protokol ICMP nepoužívá porty. Pojem port zavádějí protokoly TCP a UDP, které jsou o vrstvu výše. Atribut sin_port může mít jakoukoliv hodnotu. (To ale neplatí v MS WindowsŽ), tam musí být 0 – o tom jsme již v předchozích dílech mluvili.)

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

Volitelné položky IP záhlaví

Vyplníme rozšiřující položky IP záhlaví. Nastavíme buffer ipOptions do předepsaného tvaru, o kterém jsme mluvili minule. Chceme zaznamenávat směrovače, proto typ rozšířené IP volby nastavíme na IPOPT_RR (hodnota 7). První byte je prvek s indexem IPOPT_OPTVAL (hodnota 0). Takto nastavíme také druhý byte (velikost volby) na MAX_IPOPTLEN (hodnota 40) a ukazatel (offset) bude ukazovat na první buňku. Hodnota ukazatele je počet bytů od začátku volby. Tedy bude nastaven na IPOPT_MINOFF (hodnota 4).

Nastavením velikosti volby na 40 bytů jsme vlastně rozhodli, že „buněk“ (slotů) pro IP adresy bude 9. Protože 9 * 4 (velikost buňky) = 36 bytů, 36 + 3 (první 3 byte, které jsme nastavili) = 39 bytů. Poslední byte je ve volbě „zaznamenávej směrovače“ nevyužit.

  ipOptions[IPOPT_OPTVAL] = IPOPT_RR;
  ipOptions[IPOPT_OLEN] = MAX_IPOPTLEN;
  ipOptions[IPOPT_OFFSET] = IPOPT_MINOFF;

Představme si povinnou IP hlavičku (velikosti 40 bytů). Za ní bude vložen buffer, který jsme vytvořili. Přesněji, buffer nebude vložen za hlavičku, ale do hlavičky. Hlavička se zvětší. Zvýší se hodnota atributu ihl (délka hlavičky vydělená čtyřmi) povinné části IP hlavičky na 15. Za hlavičkou IP paketu bude následovat tělo IP paketu. V našem případě ICMP paket (který má svou hlavičku). IP paket bude odeslán a každý směrovač, kterým projde, zapíše do jeho hlavičky na pozici začátek volby + hodnota 3 byte (ukazatel) volby IP adresu výstupního síťového rozhraní, kterým IP paket opustí směrovač. Poté zvýší hodnotu 3. byte o 4 a paket může směrovač opustit. Na cílovém počítači bude vytvořen ICMP paket ECHO odpověď, který bude vložen do IP paketu se stejnou rozšiřující volbou IP záhlaví jako paket, ve kterém přišla ICMP žádost. Navíc směrovače, které byly zaznamenány v IP paketu nesoucím ECHO žádost, budou zkopírovány do IP paketu nesoucího ECHO odpověď. Samozřejmě bude nastaven ukazatel na hodnotu ukazující za poslední zaplněnou buňku.

ICMP žádost o ECHO

Zaplníme hlavičku ECHO žádosti. Pro nás již notoricky známé nastavení atributů ICMP hlavičky.

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

Nastavení volby soketu

Aby náš soket odeslal IP paket s požadovanou rozšiřující vlastností, musíme nastavit volbu soketu jménem IP_OPTIONS v úrovni voleb SOL_IP (IPPROTO_IP v MS WindowsŽ). Jako hodnotu předáme ukazatel na začátek našeho bufferu. Velikost hodnoty je tím pádem velikost bufferu.

  if (setsockopt(sock, SOL_IP, IP_OPTIONS,
    ipOptions, MAX_IPOPTLEN) == -1)
  {
    cout << "Nelze nastavit \"Record Route\"" << endl;
    close(sock);
    return -1;
  }

Až nyní jsme zajistili, aby náš soket odeslal IP paket v požadovaném tva­ru.

Odešleme ICMP paket

Pomocí sendto odešleme žádost o ECHO.

  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řijímání ICMP paketů

V cyklu budeme přijímat všechny ICMP pakety a kontrolovat, zda se jedná o odpověď na naši žádost o ECHO.

  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 zmínku stojí správné nastavení ukazatelů. Zatímco dříve bylo možné (ale rozhodně ne správné) považovat velikost IP hlavičky za 20 (konstanta) bytový blok (za IP hlavičkou následuje ICMP hlavička), nyní to již nejde. Musíme doopravdy začátek ICMP hlavičky hledat pomocí atributu ihl, který vynásobíme čtyřmi.

      ipRecv = (iphdr *)buffer;
      icmpRecv = (icmphdr *) (buffer + ipRecv->ihl * 4);

Kontrola ECHO odpovědi

Zkontrolujeme, jestli je ICMP paket odpovědí na naši ECHO žádost. Pokud ano, vypíšeme informace z hlaviček.

      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;

Volitelné položky IP záhlaví ECHO odpovědi

Volitelné položky IP záhlaví ECHO odpovědi začínají za povinnými položkami IP hlavičky.

root_podpora

    ipOptionsRecv = ((unsigned char *)ipRecv)
        + sizeof(iphdr);

Informace z volitelné položky IP záhlaví

Jestliže se jedná o volbu „zaznamenávej směrovače“, můžeme vypsat informace. Kdyby se jednalo o jinou volbu, můžeme samozřejmě také, ale s jinými volbami jsme se zatím v seriálu nesetkali. Navíc ECHO odpověď by stejně měla přijít v IP paketu se stejnou volitelnou položkou IP záhlaví, jakou měla žádost o ECHO. Poté postupně od čtvrtého byte (index v poli má hodnotu 3) procházíme 4 bytové buňky, které postupně převedeme na IP adresu v čitelném tvaru a přeložíme na doménové jméno. Získané informace vypíšeme.

    if (ipOptions[IPOPT_OPTVAL] != IPOPT_RR)
    {
      cout << "Volitelná položka není \"RR\"" << endl;
      break;
    }
    cout << "Přišlo "
        << (int) ipOptionsRecv[IPOPT_OFFSET] / 4 - 1
        << " adres" << endl;
    for (int i = IPOPT_MINOFF - 1;
        i < ipOptionsRecv[IPOPT_OFFSET] - 4; i += 4)
    {
      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);
    }
    recv = true;
      }
    }

Konec programu

Ukončíme závorky a zavřeme soket.

    else
    {
      cerr << "Nic nepřišlo" << endl;
      break;
    }
  } while (!recv);
  close(sock);
  return 0;
}
Tabulka č. 485
Soubor Operační systém
lin27.tgz Linux
win27.zip MS Windows Ž

Byl pro vás článek přínosný?