Sokety a C/C++: Raw soket

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

Sdílet

Dnes se podíváme na "raw" sokety. Pomocí "raw" soketu vytvoříme a odešleme IP paket obsahující UDP datagram. V článku je podrobně popsán zdrojový text příkladu. Také si povíme něco o volbě IP_HDRINCL.

Chceme-li posílat data pomocí raw soketu, musíme vyplnit odpovídající hlavičky protokolů. V minulém dílu jsme si vysvětlili jednotlivé položky v hlavičce IP protokolu a v hlavičce UDP protokolu. Nyní se konečně dostaneme k příkladu.

Volba IP_HDRINCL

Existuje volba soketu (viz článek

volby soketů), která se jmenuje IP_HDRINCL (IP Header Include). Je definována v úrovni voleb IPPROTO_IP. Je-li volba zapnutá (její hodnota je nastavena na true), musí být součástí dat, která odesíláme, také IP hlavička. Vložením IP hlavičky do odesílaného bufferu získáme možnost ovlivňovat atributy hlavičky IP protokolu. Druhá možnost by byla mít nastavenou volbu IP_HDRINCL na hodnotu false a ovlivňovat parametry IP protokolu pomocí funkce setsockopt

(úroveň voleb IPPROTO_IP). Tuto možnost si ukážeme v příštích dílech.

Důležité upozornění

Nyní jsme se dostali do oblastí, ve kterých se jednotlivé operační systémy od sebe dost liší. Nejprve je nutné upozornit na důležitý rozdíl mezi WinSoc a „klasickým“ socket API. Vytváříme-li soket typu SOCK_RAW s použitím pseudoprotokolu IPPROTO_RAW, je v Linuxu nastavena volba soketu IP_HDRINCL na hodnotu true. Naopak v MS WindowsŽ je nastavena na hodnotu false. Chcete-li pracovat v MS WindowsŽ se soketem tak, jak je uvedeno v článku, musíte hodnotu IP_HDRINCLnas­tavit na true. Na to NESMÍTE NIKDY zapomenout. Jinak se budete dost divit. (Viz příklady ke stažení.)

Ve skutečnosti nemůžu tvrdit, že by hlavička odchozího IP paketu byla stejná, jako je hlavička obsažená na začátku odesílacího bufferu (viz příklad). Operační systém může při odesílání bufferu nějakým způsobem do hlavičky zasáhnout. Kdybychom například dali jako identifikátor IP paketu číslo 0, jak MS WindowsŽ, tak i Linux by dodali správný identifikátor. Obecně zde ale nemohu moc říci. Liší se zde systém od systému (Linux, MS WindowsŽ, AIX atd…) a někdy se také liší i jednotlivé verze stejného systému. Chcete-li znát více podrobností ke konkrétnímu OS, musíte si prostudovat dokumentaci k podpoře IP protokolu daného OS.

V MS WindowsŽ jsme doposud používali knihovnu WinSock 1.1. Chceme-li pracovat se soketem typu SOCK_RAW s použitím pseudoprotokolu IPPROTO_RAW, musíme použít knihovnu WinSock 2.*. Liší se parametr funkce WSAStartup.

Pro vytvoření raw soketu musí program běžet s administrátor­skými právy.

Příklad – UDP na Raw soketu

Formality

Nejprve provedeme potřebné formality. Vložíme hlavičkové soubory, začneme funkci main, deklarujeme proměnné a podobně. Při programování v MS Windows Ž nesmíme zapomenout na funkci WSAStartup.

#include <sys/socket.h>
#include <sys/types.h> #include <sys/ipc.h>
#include <netinet/in.h>
#include <netdb.h>
#include <netinet/ip.h>
#include <netinet/udp.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <unistd.h>
#include <string.h>
#include <iostream>
#include <stdlib.h>
#include <iostream>

using namespace std;

int main(int argc, char *argv[])
{
hostent *host;
  iphdr *ip;
  udphdr *udp;
  char *body = "Toto posílám přes RAW soket";
  char *buffer, name[255];
  int sock;
  int total;
  sockaddr_in serverSock;
  short int port;
  unsigned long int localIP;

  gethostname(name, 255);
  if (argc != 3)
  {
    cerr << "Syntaxe:\n\t" << argv[0]
    << " " << "adresa port" << endl;
    return -1;
  }
  // Zjistíme info o lokálním počítači
  if ((host = gethostbyname(name)) == NULL)
  {
    cerr << "Špatná adresa lokálního počítače" << endl;
    return -1;
  }
  localIP = *((unsigned long int*)host->h_addr);
  port = atoi(argv[2]);
  // Zjistíme info o vzdáleném počítači
  if ((host = gethostbyname(argv[1])) == NULL)
  {
    cerr << "Špatná adresa" << endl;
    return -1;
  }

Vytvoření soketu

Vytvoříme soket typu SOCK_RAW a použijeme pseudoprotokol IPPROTO_RAW.

 // Vytvořím soket
 if ((sock = socket(PF_INET, SOCK_RAW, IPPROTO_RAW)) == -1)
 {
   cerr << "Nelze vytvořit soket" << endl;
   return -1;
 }

Alokujeme potřebnou paměť

Kromě samotných dat budeme také posílat záhlaví IP a UDP. Musíme spočítat jejich celkovou velikost a alokovat potřebný buffer. Také pro pohodlnější práci nastavíme a přetypujeme ukazatele na jednotlivé hlavičky.

 total = sizeof(iphdr) + sizeof(udphdr) + strlen(body);
 buffer = (char *)malloc(total);
 ip = (iphdr *)buffer;
 udp = (udphdr *)(buffer + sizeof(iphdr));

Zaplnění položek IP hlavičky

Postupně zaplníme položky IP hlavičky.

Verze IP

Používáme verzi číslo 4 protokolu IP.

ip->version = 4;

Velikost hlavičky

Nastavím velikost hlavičky vydělenou 4. Velikost hlavičky je 20 bytů.

ip->ihl = 5;

Typ služby

Typ služby se nepoužívá.

ip->tos = 0;

Velikost IP paketu

Nastavíme celkovou velikost paketu v bytech. Číslo převedeme do správného formátu pomocí funkce htons.

ip->tot_len = htons(total);

Identifikátor IP paketu

Identifikátor IP paketu je číslo, které jednoznačně identifikuje paket. Přiděluje jej OS. Já si zde číslo vymyslím. Tím se ale nejspíše chovám trochu nekorektně. Kdyby shodou náhod OS zrovna odesílal IP paket se stejným ID ke stejnému příjemci, mohl by nastat problém. Hlavně v případě, že by pakety byly po cestě fragmentovány. Fragmentované pakety se u příjemce skládají dohromady právě na základě identifikátoru IP. Pro jistotu dále zakážeme fragmentaci IP paketu nastavením bitu DF v příznacích.

ip->id = 12345;

Příznaky a offset fragmentu

První bit je roven 0. Druhý bit DF nastavíme na 1. Tím zakážeme fragmentaci. Snad by to nemělo vadit. Paket je dost malý. Třetí bit MF je roven 0. Hodnota 0 oznamuje, že paket je posledním fragmentem. Zbylých 13 bitů udává posunutí fragmentu od počátku originálního paketu. Zadáme 0. Je-li MF rovno 0 a offset roven 0, znamená to, že paket není fragmentován. Zadáme tedy hodnotu 16384, což je 0100000000000­000 binárně. Opět použijeme funkci htons.

ip->frag_off = htons(16384); //0100000000000000 binárně

Doba životnosti paketu

Zde nastavíme počet počítačů (směrovačů), kterými může paket projít. Poté bude zahozen.

ip->ttl = 64;

Protokol vyšší vrstvy

Nastavíme protokol vyšší vrstvy. My budeme posílat UDP datagram. Zadáme hodnotu makra IPPROTO_UDP.

ip->protocol = IPPROTO_UDP;

Kontrolní součet – přednastavení

Kontrolní součet bude počítán z hlavičky IP datagramu, až budeme mít vyplněnou celou IP hlavičku. Jenomže kontrolní součet je součástí IP hlavičky. Musíme jej proto počítat z hlavičky, která má položku kontrolní součet nastavenou na 0.

ip->check = 0;

IP adresa odesílatele

Nastavíme IP adresu odesílatele, tedy našeho počítače.

ip->saddr = localIP;

IP adresa příjemce

Nastavíme IP adresu příjemce.

ip->daddr = *((unsigned long int*)host->h_addr);

Kontrolní součet – výpočet

Nyní pomocí funkce, kterou máme z RFC1071 dokumentu, vypočítáme kontrolní součet IP hlavičky. Prvním parametrem funkce je ukazatel na začátek IP hlavičky, druhým parametrem je velikost IP hlavičky. Mělo by se jednat o hodnotu atributu ihl * 4.

ip->check = htons(checksum((unsigned char *)ip, sizeof(iphdr)));

Zaplnění položek UDP hlavičky

Postupně zaplníme položky UDP hlavičky.

Číslo zdrojového portu

Nastavíme číslo UDP portu odesílatele. Pro převod do správného formátu použijeme funkci htons.

udp->source = htons(4999);

Číslo cílového UDP portu

Nastavíme číslo UDP portu příjemce. Pro převod do správného formátu použijeme funkci htons.

udp->dest = htons(port);

Velikost UDP datagramu

Nastavíme velikost UDP datagramu. Velikost UDP datagramu je součet velikostí UDP hlavičky a dat, která UDP datagram přenáší ve svém těle. Máme-li celkovou velikost IP paketu, můžeme také velikost UDP datagramu spočítat jako rozdíl velikosti IP paketu a IP hlavičky.

udp->len = htons(total - sizeof(iphdr));

Kontrolní součet UDP

Nyní bychom měli vypočítat kontrolní součet UDP. Nejprve bychom nastavili kontrolní součet na 0 a poté spočítali kontrolní součet z UDP datagramu. Kontrolní součet v UDP hlavičce se (na rozdíl od IP kontrolního součtu) počítá z celého UDP datagramu (hlavička + data). Podle mě ale na kontrolní součet UDP nikdo moc nehledí a nic se nestane, odešleme-li UDP datagram s položkou kontrolního součtu rovnou 0.

udp->check = 0;

Odeslání dat

Nejprve zaplníme strukturu sockaddr_in. Číslo portu ve struktuře není rozhodující. Důležité je číslo portu uvedené v UDP hlavičce. Dále vložíme přenášená data za UDP hlavičku a poté pomocí funkce sendto odešleme celý IP paket, který jsme vytvořili.

Sice by hodnota atributu sin_port neměla mít na nic vliv, ale ve WinSock přece jen vliv má. Musí být správně nastavená. Takže v programu pro MS WindowsŽ bude řádek serverSock.sin_port = htons(port);

 serverSock.sin_family = AF_INET;
 serverSock.sin_port = 0;
 memcpy(&(serverSock.sin_addr), host->h_addr, host->h_length);
 memcpy(((char *)udp) + sizeof(udphdr), body, strlen(body));
 sendto(sock, buffer, total, 0, (sockaddr *)&serverSock, sizeof(sockaddr));

Konec

Pomocí typu soketu SOCK_RAW s použitím pseudoprotokolu IPPROTO_RAW nelze přijímat data. IP paket je na cestě a snad dojde i k příjemci. Nám nezbývá než skončit.

  free(buffer);
  close(sock);
  return 0;
}

Funkčnost příkladu si můžete ověřit v kombinaci s UDP serverem z článku Protokol UDP 1. část. Podobným způsobem by šlo pracovat i s TCP protokolem. TCP protokol je ale trochu náročnější. Jednak má větší hlavičku, jednak bychom museli nejprve navázat spojení, posílat data, při tom pracovat s potvrzováním a nakonec spojení ukončit. Popsat veškeré techniky potvrzování a okna by zabralo mnoho článků a navíc bych si netroufal takové články psát.

Příklady

Popsaný program je k dispozici ke stažení pro Linux a MS Windows Ž.

bitcoin_skoleni

Tabulka č. 448
Soubor OS
win18.zip MS WindowsŽ
lin18.tgz Linux

Příklad pro MS WindowsŽ obsahuje hlavičkové soubory z minulého dílu.

Podívá-li se někdo na tento příklad a příklad UDP klienta z článku Protokol UDP 1. část, asi mu dnešní příklad připadá tak trochu jako úlet. Je to pravda. Pravděpodobně pro programování „normálních“ aplikací nebude nikdo posílat UDP datagramy pomocí syrového soketu. Tímto příkladem jsem chtěl pouze osvětlit celou problematiku internetového a transportního protokolu. Není ale pravda, že by syrové sokety neměly využití. Chceme-li pracovat s ICMP protokolem, nezbývá nám nic jiného než syrové sokety použít. ICMP protokolu se budeme věnovat v příštím článku.