Hlavní navigace

Sokety a C/C++: MTU a IP fragmentace (dokončení)

Radim Dostál

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;
}
Tabulka č. 476
Soubor OS
lin25.tgz Linux
win25.zip MS WindowsŽ

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.

Našli jste v článku chybu?