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?

27. 8. 2003 17:12

xgeo (neregistrovaný)

Presne tak, bohuzel na ICMP Fragmentation Needed se neda spolehnout, a docilit toho aby to tyhle systemy posilaly je nemozne ( mnoho jich neni ve vasi sprave, je na konci sveta, atd... ).
Spise nejaka aplikace, treba by na obou koncich tohoto spoje byl linux kde by "neco" data prijimalo a dal posilalo, a na druhem konci opet skladalo.
Nejake dalsi TCP/IP s nizsim MTU nad TCP/IP...



26. 8. 2003 20:53

hkmaly (neregistrovaný)

Podle normy se vlastnosti linky mohou zmenit kdykoliv. Aplikace ktera dava DF na nejake packety musi po celou dobu behu hlidat jestli nedostala ICMP Fragmentation Needed a pripadne na ni reagovat. Pochopitelne, nemuzu tvrdit ze to ty wokeni programy udelaji. Navic jsou firewally ktere zahazuji ICMP Fragmentation Needed a diky tomu dokonce nektere webovske stranky nefunguji pokud nemate maximalni MTU ...

Lupa.cz: Teletext je „internetem hipsterů“

Teletext je „internetem hipsterů“

Podnikatel.cz: Změny v cestovních náhradách 2017

Změny v cestovních náhradách 2017

Měšec.cz: Air Bank zruší TOP3 garanci a zdražuje kurzy

Air Bank zruší TOP3 garanci a zdražuje kurzy

DigiZone.cz: Recenze Westworld: zavraždit a...

Recenze Westworld: zavraždit a...

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

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

Root.cz: Certifikáty zadarmo jsou horší než za peníze?

Certifikáty zadarmo jsou horší než za peníze?

Vitalia.cz: Pamlsková vyhláška bude platit jen na základkách

Pamlsková vyhláška bude platit jen na základkách

Měšec.cz: Jak levně odeslat balík přímo z domu?

Jak levně odeslat balík přímo z domu?

Podnikatel.cz: Snížení DPH na 15 % se netýká všech

Snížení DPH na 15 % se netýká všech

Podnikatel.cz: Udávání a účtenková loterie, hloupá komedie

Udávání a účtenková loterie, hloupá komedie

Lupa.cz: Insolvenční řízení kvůli cookies? Vítejte v ČR

Insolvenční řízení kvůli cookies? Vítejte v ČR

Vitalia.cz: Když přijdete o oko, přijdete na rok o řidičák

Když přijdete o oko, přijdete na rok o řidičák

120na80.cz: Horní cesty dýchací. Zkuste fytofarmaka

Horní cesty dýchací. Zkuste fytofarmaka

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

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

Podnikatel.cz: Udávání kvůli EET začalo

Udávání kvůli EET začalo

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

Jsou čajové sáčky toxické?

Měšec.cz: Jak vymáhat výživné zadarmo?

Jak vymáhat výživné zadarmo?

120na80.cz: Na ucho teplý, nebo studený obklad?

Na ucho teplý, nebo studený obklad?

Podnikatel.cz: Chaos u EET pokračuje. Jsou tu další návrhy

Chaos u EET pokračuje. Jsou tu další návrhy

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

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