Egyre mélyebbre ásunk a hálózat működésében. Az előző részben már közvetlenül a programunk gyártotta az elküldendő csomagot, igaz még nem az egészet. Csak az ICMP részét. Mint már említettem, az ICMP rész valójában az IP csomag adat részében található. Itt az ideje, hogy belelessünk az IP fejlécébe is. Itt található a csomag célja, forrása, a rá jellemző többi fontos adattal együtt. Ezek módosítására általában nincs szükségünk, hiszen a kernel helyesen tölti ki nélkülünk is. Gyakorlati jelentősége ritkán van a fejléc átírásának, de mindenképpen érdekes lehetőségeket rejt.

A legkülső, burok protokoll minden Interneten használt protokollnál az IP. Ez azt jelenti, hogy az eddig tárgyalt többi protokoll is (TCP, UDP) mind az IP csomag adat részében utazik. Röviden az IP az Internet alapja. Valószínűleg innen is a név, ami: Internet Protocol. :)

Az IP-nek jelenleg két verziója van. A jelenleg használt IPv4, amely 32 bites címeket használ, és az újabb, még nem széles körben használt IPv6, mely 128 bites címeket használ. A 32 bites címtartomány lassan betelik, így csak idő kérdése, mikor tér át mindenki az IPv6-ra. Az itt leírtak az IPv4-re vonatkoznak, habár nagy része az IPv6-ra is igaz.

Az Internet Protokoll (IP)

Az IP történetéről nem írok, úgyis mindenki tudja, hogy a TCP/IP az USA-ban indult a védelmi minisztérium fejlesztéséből, ez volt az ún. ARPANET hálózat. A TCP/IP-ről itt egy jó leírás Scheidler Balázs-tól: ip.html

Itt mindenki megértheti, hogy mit jelent a TCP/IP rövidítés, és nagyon jó képet kap egy hálózat működéséről, IP címekről, netmask-ról, a Linux hálózati beállításairól stb...

Szóval érdemes elolvasni.

Az IP feladata, hogy adat blokkokat (amiket datagram-nak hívunk) juttasson el forrástól célig. A forrás és a cél állomás (host) címei meghatározott hosszúságúak. Szintén az IP feladata, hogy darabokra törje, és összerakja a datagram-ot, ha a fizikai vonalon csak kisebb csomagok tudnak átmenni. Az IP állapotmentes protokoll, ez annyit jelent, hogy a fizikai eszköz mindent megtesz a csomag célbajuttatása érdekében, de semmit nem garantál. Az egyes csomagok egymástól teljesen függetlenül vándorolnak, így a megérkezési sorrend is megjósolhatatlan. Ha egy csomag elveszik, arról nem kér ismétlést, ha pedig meghibásodik, egyszerűen eldobja. Tehát eleve arra lett tervezve, hogy a felsőbb szintű protokollok törődjenek a hibákkal (lásd TCP).

Itt jegyezném meg, hogy eddig pontatlanul használtam a 'csomag' (packet) kifejezést. A csomag a datagram fizikai megjelenése. Jó esetben egy datagram egy csomagban utazik a dróton. Azonban előfordulhat, hogy a datagram túl nagy az átvitelhez, így több csomagban megy át a fizikai közegen. Ekkor egy datagram nem egy csomag.

Az IP protokollt az RFC791-es szabvány írja le. A pontos specifikáció nem ezen cikk tárgya, így a szabványt mellékeltem. Angol nyelvű.

Nézzük a programozás szemszögéből. Ha IP fejlécet akarunk átírni, az a legfontosabb, hogy tisztában legyünk az IP fejléc felépítésével.

Az IP fejléc felépítése
0 ... 3 4 ... 7 8 ... 15 16..18 19 ... 31
Version
IHL
Type of Service
Total Length
Identification
Flags
Fragment Offset
Time to Live
Protocol
Header Checksum
Source Address
Destination Address
Options
Padding

A fejlécben a számok biteket jelentenek. Egy sor 32 bit, azaz 4 byte. Mivel az utolsó sor opcionális (nem kötelező), egy mezei IP header hossza 5x4, azaz 20 byte. Az egyes mezőkben a legelső bit az msb, azaz legnagyobb helyiértékű bit, az utolsó pedig az lsb, azaz a legkisebb helyiértékű bit. Mivel az Intel Architektúra számábrázolása pont fordított, az IP header kitöltésénél mindig használnunk kell a helyes bitsorrendet szolgáltató függvényeket, pl. ntohs.

Az egyes mezők jelentése
Version 4 bit
Ez mutatja meg, hogy milyen verziójú IP fejléccel van dolgunk. Az ábra a 4-es verzióra vonatkozik.
IHL 4 bit
Az IP fejléc hossza 4 bájtos word-ben. Tehát, egy szokványos 20 byte-os fejléc esetén itt 5-öt fogunk találni (4*5=20).
Type of Service 8 bit
Arról ad információt, hogy az IP csomag milyen típusú szolgáltatásban vesz részt. Erre azért van szükség, mert pl. hálózati túlterhelés esetén a legfontosabb csomagokat fogják a hálózati eszközök hamarabb továbbítani. Ezen kívül meg lehet adni, hogy mi a legfontosabb a csomag átvitelekor. A biztonság, a teljesítmény, vagy az idő. Bővebben ld. szabvány.
Total Length 16 bit
A csomag hossza bájtban. Az adatrész, és a fejléc hossza együtt. Mivel 16 bit, egy csomag maximális mérete 65535 byte. Ez nem valami praktikus, egy csomag ajánlott mérete 576 byte. Ebbe belefér 512 byte adat, az IP fejléc, és egy fejléc a magasabb szintű protokollnak.
Identification 16 bit
A küldő által meghatározott azonosító érték. Ez fog segíteni a széttördelt csomagok összerakásakor. Az azonos ID-jű darabok eredetileg egy csomagot alkottak.
Flags 3 bit
A legelső bit nem használt.

A második 1, ha a csomag tördelése tiltott (don't fragment bit), 0, ha engedélyezett.
A harmadik bit akkor egy, ha a csomagot további töredékek követik, egyébként nulla.

Fragment Offset 13bit
Ez az adat mondja meg, hogy a töredék az eredeti datagram melyik része. Ez nélkül nem lehetne összerakni a csomagot.
Time To Live 8bit
Ez határozza meg a maximális időt, amíg a csomag létezhet. Minden gép, ahol áthalad, levon ebből az értékből egyet. Ha már nulla, akkor eldobja a csomagot. Így biztos, hogy egy csomag sem fog örökké bolyongani a hálózaton.
Protocol 8bit
Ez határozza meg az adatrészben utazó, felsőbb szintű protokoll fajtáját.
Header Checksum 16 bit
A fejléc ellenőrző összege. Ha nem stimmel, a csomag eldobandó. Az összeget az előző részben is használt függvénnyel számolhatjuk ki.
Source Address 32bit
A küldő IP címe.
Destination Address 32bit
A címzett IP címe.
Options
Ez nem kötelező, így nem szólunk róla. Ld. szabvány.
Az IP headert leíró struktúra a netinet/ip.h fejlécállományban van. Kicsit leegyszerűsítve Intel számábrázolásra:
struct iphdr {   unsigned int ihl:4;   unsigned int version:4;   u_int8_t tos;   u_int16_t tot_len;   u_int16_t id;   //A fragment offset össze van vonva a flag-el !!! u_int16_t frag_off;   u_int8_t ttl;   u_int8_t protocol;   u_int16_t check;   u_int32_t saddr;   u_int32_t daddr;   /*The options start here. */ };

Ami feltűnik, az az, hogy a flag mező kimaradt. Pontosabban a frag_off alsó három bitjeként tudjuk kezelni.
Ezen kívül még használható ugyanebből a header-ből a struct ip is, ami majdnem ugyanígy néz ki.

Egy ICMP csomag létrehozása az IP fejléccel együtt

    #define IPHDRSIZE sizeof(struct iphdr) #define ICMPHDRSIZE sizeof(struct icmphdr) #define PACKETSIZE (IPHDRSIZE+64) struct sockaddr_in destaddr; //cel cim ... char *packet; //A csomag struct iphdr *iph; //IP fejlec struct icmphdr *icmph; //ICMP fejlec packet=(char *)malloc(PACKETSIZE); bzero(packet,PACKETSIZE); iph=(struct iphdr*)packet; //az icmp fejlec kozvetlenul az ip fejlec utan jon icmph=(struct icmphdr*)&packet[IPHDRSIZE]; /*IP fejlec*/ iph->version = 4; //ipv4 iph->ihl = 5; //5*4, azaz 20 byte a fejlec iph->ttl = 128 //time to live //ICMP a hordozot protokoll iph->protocol = IPPROTO_ICMP; //egesz csomag merete fejleccel egyutt iph->tot_len = htons(PACKETSIZE); //cel cim bemasolasa bcopy((char *)&destaddr.sin_addr, &iph->daddr, sizeof(iph->daddr)); //forras cimnek a sajat cimet adjuk meg iph->saddr=INADDR_ANY; //checksum kitoltese iph->check=in_cksum((u_short *)iph,IPHDRSIZE); /*ICMP fejlec*/ icmph->type = ICMP_ECHO; //echo request icmph->un.echo.sequence = 1; icmph->un.echo.id = getpid(); icmph->checksum = in_cksum((u_short*)icmph,64);
     
A részlet egy IP csomagot állít össze, ami egy 64 byte-os ICMP részt tartalmaz az adatrészében. Az IP fejlécbe a saját címünket írjuk be. Pontosabban INADDR_ANY-t, ami igazából '0.0.0.0'. A kernel írja be a valódi címet, mivel látja, hogy üres. Ha bármi más, nullától különböző címet adunk meg, akkor nem nyúl hozzá.

Akkor van poén a dologban, ha nem a saját címünket írjuk be az IP header-be, hanem valaki másét. Ez persze nem szép dolog, és nem biztos, hogy működni fog. Itt ismerni kell a hálózat felépítését. Elképzelhető, hogy az első szembejövő router, vagy gateway nem engedi tovább a csomagot, mert nem tetszik neki a forrás cím. Persze ez elvi lehetőség, többnyire megy. A technikát úgy hívják, hogy IP spoofing. A használata egyáltalán nem mondható általánosnak. Használható DoS támadáskor, hogy ne látsszon a forrás cím. De hogy ne csak a 'gonosz' célokról essen szó, maga a kernel is újrafejlécezi a csomagot, ha masquerading-ot használunk.

Az IP csomag elküldése
Először socketet kell nyitni:
s = socket(AF_INET, SOCK_RAW, IPPROTO_RAW);
setuid(getuid()); //root jogok eldobasa FONTOS!

A socket SOC_RAW típusú, és IPPROTO_RAW a megadandó protokoll. SOCK_RAW típusú socket-et csak root jogokkal futó program nyithat, ezért az ilyenek általában root SUID-osak. A socket megnyitása után azonnal dobjuk a root jogokat.

Az IPPROTO_RAW megadásával jeleztük, hogy az IP csomagot teljes egészében a program állítja elő. Így a kernel nem fog, még egy IP fejlécet elérakni. Az ilyen típusú socket csak írható! Tegyük fel, hogy a saját címünket adtuk meg a fejlécben. Ekkor, mivel az ICMP részben ismétlést kértünk, válasz fog jönni. Amit azonban ezen a socket-en NEM fogunk tudni olvasni. Így ne is akarjuk.

A csomag elküldése:

sendto(s, packet, PACKETSIZE, 0, (struct sockaddr*)&destaddr, sizeof(struct sockaddr));

A példaprogram küld 3 darab 64 bájtos echo request-et a megadott célcímre, a megadott forráscímmel. Lásd: spoofping.c. Lefordítás után root SUID-ossá kell tenni, vagy rootként futtatható.
A mellékelt forráskód.