Az Interneten leggyakrabban használt protokollok a TCP ás az UDP. Ezek a leghasznosabbak számunkra, segítségükkel érjük el a létező összes szolgáltatást (ftp, http, irc, stb...), feladatuk kommunikációs csatornát biztosítani. De vajon hogyan értesülünk a hibákról, azok típusáról, illetve hogyan tudunk egy hálózatot diagnosztizálni? Erre való az ICMP, amely bár messze nem annyira ismert és emlegetett, mint a fent említett két protokoll, mégis, legalább annyira hasznos és nélkülözhetetlen része minden TCP/IP megvalósításnak.

Az ICMP (Internet Control Message Protocol) az IP-t (Internet Protokoll) használja feslőbb szintű, azaz 'burok' protokollként, ahogy egyébként minden más internetes protokoll is teszi. Az IP fejléc után, az adatrészben van az ICMP fejléce, és a hozzá tartozó adatok. Ennek a felépítésére nem térek ki, illetve csak olyan mélységekig, ameddig szükséges. Pontosan definiálva van az RFC792-es szabványban. Megtalálható sok egyéb hasznos dologgal együtt, pl. a Connected: An Internet Encyclopedia címen.
(Illetve mellékelve, angolul.)

ICMP üzenetek számos esetben rohangásznak a hálózaton. Lehet, hogy rossz címre próbáltunk kapcsolódni, és az üzenet tudatja velünk, hogy a cél elérhetetlen. Elképzelhető, hogy arról kapunk értesítést, hogy egy csomag elveszett, de lehet, hogy egyszerűen csak egy visszhangot kérnek tőlünk (echo request), valamilyen diagnosztikai céllal. Az ICMP-t sok hasznos program használja, például a ping és a traceroute (tracert). A továbbiakban az ilyen programok legalapvetőbb műveleteit próbálom bemutatni. Úgy, mint a socket létrehozása, ICMP csomag létrehozása, elküldése, beolvasása.

Egy ICMP csomagnak van típusa. A típus attól függ, hogy az üzenet mire vonatkozik.

Az alapvető típusok:
 

Szám Név Mire való
0
echo-reply Az echo-request -re válasz. Ping is használja.
3
destination-unreachable 'A cél elérhetelen' hibaüzenet. Bármilyen TCP/UDP forgalomhoz kell, enélkül a végtelenségig várnánk a kapcsolatra.
5
redirect útválasztás, ha nem fut útválasztó démon
8
echo-request Visszhang kérés. Ilyet küld a ping.
11
time-exceeded Jelzi, hogy lejárt a csomag ideje. Traceroute használja.

Minden egyes típusnak különböző alkódjai vannak, ami az üzenet típusával kapcsolatban további részleteket árul el. Ld. a szabvány.

ICMP socket létrehozása

s = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
setuid(getuid()); //root jogok eldobása FONTOS!

Ahhoz, hogy ICMP csomagot tudjunk küldeni, SOCK_RAW típusú socketet kell nyitni. Socket megnyitása ld. első rész.
A SOCK_RAW socket azt jelenti, hogy közvetlenül a program állítja elő az elküldendő csomagot. Ez veszélyeket rejt magában, ezért csak root jogokkal futó program nyithat ilyen socket-et. Általában úgy oldják meg, hogy root suid-os. Mivel nem jó ötlet, hogy végig így működjön, ezért rögtön a socket létrehozása után ejteni kell a root jogokat. Ez úgy történik, hogy a getuid függvénnyel lekérdezi a felhasználó azonosítóját (aki indította), majd a setuid függvénnyel a program erre állítja a saját magára vonatkozó azonosítót. Így a program a socket létrehozása után az őt indító felhasználó jogaival fog tovább futni. Ez a lépés nem kötelező, hiszen nélküle is működne a program, de erősen ajánlott.

A következő lépés ezután az, hogy elő kell állítani egy ICMP csomagot, amit el fogunk küldeni.

Az ICMP csomag felépítése

Az ICMP csomag, ahogy említettem, az IP csomag adatrészében van. Magának az ICMP csomagnak a felépítése attól függ, hogy milyen típusú. Ezek egyenként részletesen le vannak írva a szabványban, én csak az ECHO_REQUEST, és REPLY csomagokra térek ki, ezt fogom használni a példában.

Az ICMP csomag kezelésére léteznek struktúrák. Ezek az icmphdr, és az icmp nevet viselik, és a /usr/include/netinet/ip_icmp.h fejléc állományban vannak definiálva. A fájl tanulmányozását ajánlom mindenkinek, aki behatóbban szeretne foglalkozni a témával. Az egyes típusokra, és kódokra vonatkozó konstansok szintén itt vannak.

Az ICMP csomag általános felépítése
 

0...7 (bit) 8...15 (bit) 16...23 (bit) 24...31 (bit)
Típus
Kód
Checksum
Típus függő adatok

Amint látszik, az első byte a típust határozza meg, a második a kódot a típushoz, a harmadik és negyedik byte pedig word-ként az ellenőrző összeget adja. Minden sor 4 byte-ot takar. Ha ilyen egyszerű, akkor miért bitekben adtam meg? A hálózati bit sorrend egyáltalán nem biztos, hogy ugyanaz, mint a számítógépünk számábrázolásában. A táblázatról látszik, hogy a legalacsonyabb bittel kell kezdeni. Mivel a PC-k is így csinálják, (nekem meg olyan van) én egyszerűen mondhatom, hogy az első, második stb. byte. Később sem kell megfordítani a biteket.

Az ECHO_REQUEST, és ECHO_REPLY csomag felépítése
 

0...7 (bit) 8...15 (bit) 16...23 (bit) 24...31 (bit)
Típus
Kód
Checksum
Azonosító
Sequence number
Adat... A hossza nem kötött, bárhol végződhet

Az ECHO_REQUEST egy kérés, hogy akinek küldjük, küldje vissza pontosan ugyanazt, amit kapott. Az ECHO_REPLY, pedig erre a válasz. Ezzel lehet megnézni, hogy van-e, és ha igen, akkor milyen gyors hálózati kapcsolat két gép között.

Az egyes mezők értékei:

  • Tipus:

  • kérés esetén 8, válasz esetén 0
     
  • Kód:

  • mindig nulla
     
  • Checksum:

  • Egy ellenőrző összeg. Az a célja, hogy a vevő biztos lehessen, hogy nem károsodott az üzenet. Kiszámítása később.
     
  • Azonosító:

  • Egy azonosító, hogy tudjuk, a válasz melyik kérésre jött. Lehet nulla is.
     
  • Sequence number:

  • Egy sorszám, hogy tudjuk, melyik kérésre érkezett a válasz. Lehet nulla is.

A válasz csomag ugyanazt az azonosítót, sorszámot, és adatot kell, hogy tartalmazza, mint amit a kérés. A Típus természetesen változni fog, ezért az ellenőrző összeget újra kell számolni.

Az ellenőrző összeg (checksum) kiszámolása

/*
 * in_cksum --
 * Checksum routine for Internet Protocol family headers (C Version)
 */
u_short in_cksum(addr, len)
  u_short *addr;
  int len;
{
  register int nleft = len;
  register u_short *w = addr;
  register int sum = 0;
  union {
    u_short us;
    u_char uc[2];
  } last;
  u_short answer;

  /*
   * Our algorithm is simple, using a 32 bit accumulator (sum), we add
   * sequential 16 bit words to it, and at the end, fold back all the
   * carry bits from the top 16 bits into the lower 16 bits.
   */
  while (nleft > 1)  {
    sum += *w++;
    nleft -= 2;
   }

  /* mop up an odd byte, if necessary */
  if (nleft == 1) {
    last.uc[0] = *(u_char *)w;
    last.uc[1] = 0;
    sum += last.us;
   }

  /* add back carry outs from top 16 bits to low 16 bits */
  sum = (sum >> 16) + (sum & 0xffff); /* add hi 16 to low 16 */
  sum += (sum >> 16);   /* add carry */
  answer = ~sum;    /* truncate to 16 bits */
  return(answer);
}

Hogy miért pont így? Csak.
A kódot a ping forráskódjából szedtem. Valószínűleg máshogy is elő lehetne állítani, de minek, ha egyszer nálam jóval okosabbak már megcsinálták…

ICMP csomag készítése

Az ip_icmp.h -ban definiált icmp struktúrát fogjuk használni.

#include <netinet/ip_icmp.h>

char packet[2048]; //a csomag. Ennyi biztos elég
char data[]="hello";
int num;
...

struct icmp *icp; //icmp csomag

icp=&packet[0];
icp->icmp_type=ICMP_ECHO; //echo request
icp->icmp_code=0;
icp->icmp_chksum=0;//az ellenőrző összeget úgy kell előállítani, hogy ez még nulla
icp->icmp_id=getpid();//azonosítónak megadjuk a process ID-t. Nem lenne kötelező
icp->icmp_seq=num; //hanyadik csomag

//Bemásoljuk az adatot. Az adat a nyolcadik byte-on kezdődik. ld. csomag felépítése.
strcpy(&packet[8],data);
//Mondjuk 64 byte-nyit küldünk a csomagból
icp->icmp_chksum=in_chksum(packet, 64);

Ezzel a csomag meglenne. El kell küldeni a címzettnek. A már ismert sendto függvényt használjuk. (Lásd előző rész.) A függvénynek meg kell adni a cél címet.

A cél cím megadása

struct hostent *hp;
struct sockaddr_in whereto;

hp=gethostbyname(targethost); //a cél ip cimét megkapjuk
bzero(&whereto, sizeof(whereto));
bcopy((char *)&hp->addr, (char *)&whereto.sin_addr, hp->h_length);
whereto.sin_family=AF_INET;

Gondolom a figyelmes olvasónak feltűnt, hogy a cél címét tartalmazó struktúrát hiányosan töltöttük ki. (sockaddr_in struktúra ld. első rész)
Megadtuk az IP címet, és hogy internet osztályú (AF_INET). De Mi van a port-tal? Nem kell megadni, sőt, nincs semmit szerepe. A port csak az UDP, és TCP esetén számít. Az ICMP nem használ port-okat!

A csomag elküldése

Egyszerű sendto hívás.

/* 64 byte-ot küldünk, ennyivel számoltuk a checksumot*/
sendto(s, packet, 64, 0, (struct sockaddr *)&whereto, sizeof(struct sockaddr));

Az IP fejlécet a kernel fogja hozzáadni. Ezzel a csomag elvándorolt a hálózat rögös útjain. Ha minden jól megy, akkor válasz is fog rá érkezni, amit illene beolvasnunk.

A válasz beolvasása

A választ a recfrom, recv, illetve recvmsg függvényekkel olvashatjuk be. Mivel semmi garancia nincs rá, hogy valaha is fogunk választ kapni a csomagra, nem szabad egyszerűen meghívni az említett függvények bármelyikét. Ekkor ugyanis örök időkig fogja várni a választ. Gondoskodnunk kell időkorlátról. Erre megfelel például a select függvény (ld. 3. rész).

fd_set rset;
struct timeval timeout;
int s;
...
timeout.tv_usec=0;
timeout.tv_sec=3; //Három másodpercig várunk maximum
FD_ZERO(&rset);
FD_SET(s, &rset); //socketet berakjuk a set-be
if (select(s+1, &rset, NULL, NULL, &timeout)>0)
{
  /*valasz beolvasasa*/
  recv(s, &packet[0],  MAXPACKET, 0);
  ...
}
else printf("timeout\n");

Ha sikerül beolvasni a választ, még akkor sem biztos, hogy nekünk szól. Minden ICMP socket megkapja az összes beérkező ICMP üzenetet. Le kell ellenőrizni, hogy tényleg a mienk. Ha a csomag típusa ICMP_ECHOREPLY, és az id-je az amit mi küldünk, és a sequence is, akkor már biztos, hogy a mienk. A címet is meg lehetne nézni, hogy onnan jött-e, ahova küldtük. Tehát nincs más dolgunk, mint az említett két három mezőt megvizsgálni.
De vigyázat!
A csomag elé, mikor elküldik, a kernel (illetve maga a program) hozzárakja az IP header-t. Mikor beolvassuk a választ, az egész csomagot olvassuk be az IP headerrel együtt. Ez mögött van az ICMP csomag. Az IP header felépítése a netinet/ip.h állományban az iphdr struktúrában van. Most csak a hosszára lesz szükség, hogy átléphessük.

  //A valasz elso resze az IP header, ezt atugorjuk.
  icp=(struct icmp *) &packet[sizeof(struct iphdr)];
  //Ha nem nekunk jott valasz, akkor oke
  if (icp->icmp_id==getpid() && icp->icmp_type==ICMP_ECHOREPLY)
    printf("Visszajott! Asszem mukodik.\n");

Természetesen az icmp adat részét sem árt megnézni, hogy az általunk elküldött adatot tartalmazza-e. Mert, ha nem, akkor baj van.

A programot lefordítás után a root tulajdonába kell helyezni, és suid-ossá tenni. Root jogok nélkül a program segmentation fault-al fog elszállni.

>gcc -o simpleping simpleping.c

majd rootként:
>chown root.root simpleping
>chmod u+s simpleping

Mára ennyi elég is volt. Legközelebb majd próbálunk mélyebbre nyúlni a témában, és magát az IP headert is megbolygatni.
A mellékelt forráskód.