TCP/IP - part I

Znalazłem ostatnio w I-necie pseudo art o czytaniu logów ze snifferów. Umiejętność czytania logów to bardziej znajomość całej otoczki, która transportuje dane w I-necie. Postanowiłem, że zamiast odstraszać i emanować próżnością można zrobić coś ciekawszego.

W tym arcie i po nim będziesz w stanie zupełnie samodzielnie ;) sprawdzić z dużą szansą powodzenia dostępność zdalnego hosta. Jeśli zamiast zwykłej ciekawości przyszedł Ci do głowy genialny pomysł skorzystania odrazu z programu ping - proszę o opuszczenie tego serwera. Jeśli wstępnie opanujesz praktykę z zakresu protokołów sieciowych wtedy czytanie logów będzie dużo łatwiejsze - w drugą stronę to nie działa tak samo. Np - z typowego artykułu dowiesz się, że istnieje tak zwany bufor na dane w nagłówku IP - Window. Szkoda, że właściwie niczego więcej się nie dowiesz. Jeśli podejdziesz do tego od strony programowej - będziesz wiedział, że możesz np sam ustalić wielkość takiego bufora a wtedy będziesz się zastanawiać jak duży może być żeby nie przesadzić i zaczniesz szukać informacji. Poświęcisz na to więcej czasu ale zapamiętasz to do emerytury a nie do następnej imprezy.

Jeśli nie wiesz za wiele na temat programowania aplikacji sieciowych to zanim przejdziesz dalej - odwiedz inne strony związane z tym tematem i wtedy wróć. Jest to ważne a sam nie zamierzam tutaj przepisywać Internetu i produkować kolejnego tutoriala o gniazdach. Nie wyważa się już otwartych drzwi.

Okey, zakładam że masz już podstawową wiedzę na temat gniazd - tym samym stwierdzić mogę, że wiesz w zasadzie nic. Wiesz mniej więcej jak przesłać przez sieć komunikat "I love Lady Gaga" i w zasadzie powinno Ci się wydawać, że to już coś. Gdybym teraz zapytał się Ciebie na jakiej dokładnie zasadzie przesłałeś ten komunikat - odpowiesz, że w pakiecie. Okey - ale czym jest ten pakiet, jak on wygląda z czego się składa i w końcu gdzie dokładnie w tym pakiecie jest Twój komunikat? - Tutaj pojawia się problem z odpowiedzią. Wszystko to będziesz wiedzieć jeśli podejmiesz się tutaj próby realizacji programu imitującego PING.

Od czego zaczniesz? - proponuję ustalić najpierw czy Twój system jest w stanie wyprodukować tak zwany 'surowy pakiet'. Na różnych tutorialach dowiedziałeś się pewnie i wiesz co oznacza taka linijka:

int sockfd = socket( AF_INET, SOCK_RAW, IPPROTO_TCP );

Ten fragment informuje jądro systemu, że przygotowujemy się do zbudowania pakietu sieciowego z danymi. Zrobimy to sami - szok - ale tak można. Można zbudować własną strukturę i przesłać ją w Internet! Szok - ale spokojnie, tą wysyłką rządzą pewne reguły o których zaraz się dowiesz. XP sp2 nie wspiera surowych gniazd na bazie protokołu TCP ale UDP i ICMP już TAK. Z tego też powodu swoje poczynania przeniosłem na system Linux. Vista i 7 już wspierają surowce a jeśli bardzo upierasz się na XP to możesz na podobnej zasadzie zbudować imitację PING'a korzystając z biblioteki WinPcap.

Formalności mamy już za sobą. Kolejna sprawa - fajnie, żebyś wiedział trochę o budowie nagłówków protokołów - zwłaszcza IP, TCP, UDP i np ICMP.

03table02

To jest nagłowek protokołu IP - na tym obrazku wygląda mało praktycznie a bardziej teoretycznie. Spróbuj spojrzeć na niego z trochę inne rzutu:

  1. typedef struct ip_hdr
  2. {
  3. unsigned char ip_header_len:4;
  4. unsigned char ip_version :4;
  5. unsigned char ip_tos;
  6. unsigned short ip_total_length;
  7. unsigned short ip_id;
  8. unsigned char ip_frag_offset :5;
  9. unsigned char ip_more_fragment :1;
  10. unsigned char ip_dont_fragment :1;
  11. unsigned char ip_reserved_zero :1;
  12. unsigned char ip_frag_offset1;
  13. unsigned char ip_ttl;
  14. unsigned char ip_protocol;
  15. unsigned short ip_checksum;
  16. unsigned int ip_srcaddr;
  17. unsigned int ip_destaddr;
  18. } IPV4_HDR;

Z programistycznego punktu widzenia ma to dla Ciebie trochę większy sens prawda? Prawda, a zatem spróbuj odrazu spojrzeć na pozostałe nagłówki wymienionych protokołów w taki sposób - TCP:

 

  1. typedef struct tcp_header
  2. {
  3. unsigned short source_port;
  4. unsigned short dest_port;
  5. unsigned int sequence;
  6. unsigned int acknowledge;
  7. unsigned char ns :1;
  8. unsigned char reserved_part1:3;
  9. unsigned char data_offset:4;
  10. unsigned char fin :1;
  11. unsigned char syn :1;
  12. unsigned char rst :1;
  13. unsigned char psh :1;
  14. unsigned char ack :1;
  15. unsigned char urg :1;
  16. unsigned char ecn :1;
  17. unsigned char cwr :1;
  18. unsigned short window;
  19. unsigned short checksum;
  20. unsigned short urgent_pointer;
  21. } TCP_HDR;

UDP Header

  1. typedef struct udp_hdr
  2. {
  3. unsigned short source_port;
  4. unsigned short dest_port;
  5. unsigned short length;
  6. unsigned short checksum;
  7. } UDP_HDR;

ICMP Header

  1. typedef struct icmp_hdr
  2. {
  3. unsigned char icmp_type;
  4. unsigned char icmp_code;
  5. unsigned short icmp_checksum;
  6. unsigned short icmp_id;
  7. unsigned short icmp_seq;
  8. } ICMP_HDR;

Okey - są to struktury, które zadeklarujesz w swoim pliku programu. Skąd wiadomo, że są takie? - Wynika to ze schematycznych rysunków oraz opisów w dokumentach RFC. Narazie nie musisz ich czytać - wszystko znajdziesz tutaj lub odeślę Cie kiedy będzie trzeba gdzie trzeba. Jedziemy dalej - musisz wiedzieć, że PAKIET SIECIOWY składa się z warstw różnych. Strumień bajtów wpływający do Twojej sieci w pierwszej kolejności interpetowany jest przez urządzenia sieciowe jako RAMKA z DANYMI. Ramka ta równiez posiada swój nagłówek:

frame

Nie jest to coś co w tym arcie Ciebie interesuje. Ale Pierwszą warstwą czyli fundamentem pakietu sieciowego jest RAMKA a potem mamy DANE i te DANE już Ciebie bardziej interesują.

ethernet network packet holding an ip packet

Interesuje nas teraz część pakietu związana z IP - protokołem internetowym - adresowym, znajdującym się w warstwie sieciowej modelu OSI. Jeśli założyliśmy, że budujemy pakiet prawie od podstaw - prawie bo nie zajmujemy się nagłówkiem ethernetowym - to pora stworzyć zmienną będącą naszym pakietem i w coś ją - ten pakiet w końcu ubrać.

char packet[128];

To będzie nasz pakiet! - dobre nie? Tablica znaków? Ale jak to? - tak to - nasz pakiet będzie stanowić ciąg bajtów o wielkości powiedzmy 128 bajtów. Czemu 128 bajtów? - akurat tyle mi się zachciało to sobie zadeklarowałem. Ale musisz pamiętać, że pakiet ma swoje minimalne wymagania wielkościowe tzn - nagłowek Ethernet 14 bajtów - nagłowek IP 20 bajtów no i dalej już zależy to jakiego protokołu chcemy użyć w warstwie transportowej. Czyli musisz uwzględnić na pewno wielkości wszystkich nagłówków protokołów z których korzystasz. Do Pinga będzie Ci potrzebny Ethernet + IP + ICMP + jakieś tam dane. Ustawiłem sobie 128 - na pewno nie za mało - lepiej za dużo niż za mało a jeśli chodzi o DANE to też zauważysz że w zasadzie są zbędne w przypadku PING'a. W danych np TCP patrz obrazek - umieściłeś swój słynny napis 'I love Lady Gaga'. Eureka! - zapamiętaj jeszcze taką rzecz - DANE dla Ethernetu to całość IP + TCP. DANE dla IP to całość TCP z danymi. DANE dla TCP to poprostu DANE czyli np napis, który przesłałeś do procesu klienta przez sieć.

Okey mamy pole do zarobienia - patrz packet (tablica bajtowa) - pora stworzyć nagłówek IP.

  1. char packet[128];
  2. memset( packet, 0, 128 );
  3. IPV4_HDR * ipv4H = (IPV4_HDR*)&packet;
  4. ICMP_HDR * icmpH = (ICMP_HDR*)&packet[sizeof(IPV4_HDR)];
  5.  
  6. ipv4H->ip_header_len = 5;
  7. ipv4H->ip_version = 4;
  8. ipv4H->ip_tos = 0;
  9. ipv4H->ip_id = htons(2); // na screenie id wynosi 0x0200, blad braku uzycia htons przed screenem
  10. ipv4H->ip_ttl = 32;
  11. ipv4H->ip_protocol = 1; // ICMP
  12. ipv4H->ip_checksum = 0; // uzupelniane przez kernel
  13. ipv4H->ip_srcaddr = (unsigned int)inet_addr( "192.168.1.103" );
  14. ipv4H->ip_destaddr = (unsigned int)inet_addr( "212.77.100.101" );
  15.  
  16. icmpH->icmp_type = 8; // echo request
  17. icmpH->icmp_code = htons(0);
  18. icmpH->icmp_id = htons(getpid());
  19. icmpH->icmp_seq = htons(1);
  20. icmpH->icmp_checksum = suma_kontrolna( (unsigned short*)icmpH, 128 - sizeof(IPV4_HDR) );

Popatrz na ten fragment kodu i na schemat obrazka i sprawdz czy ma to dla Ciebie jakiś sens. W ten oto sposób zorganizowałeś swój pierwszy nagłowek IP jakiego użyjesz przy wystrzale pakietu w kosmos Internetu.

Co to jest htons() lub inet_addr() - odsyłam do innych tutoriali - nie na to tutaj jest miejsce. Jedziemy dalej - PING działa na bazie protokołu diagnostycznego ICMP - jeśli wątpliwości to odsyłam np na wiki. Małe wyjaśnienie co do pola icmp_id - w dokumencie RFC przeczytasz, że jest to identyfikator procesu związany z wysyłaniem pakietu. Chodzi tu o to, żeby kiedy różne pakiety ICMP docierają do Twojego systemu wiadomo było, z którym procesem są związane. Suma kontrola to specjalny algorytm opisany przez Panów Braden, Borman i Partridge w artykule "Computing the Internet Checksum". Nie jest to narazie również coś, co powinno Cie akurat zająć najbardziej. Dalej - zauważysz adresy IP serwera Wirtualnej Polski i mój lokalny - są to zupełnie przykładowe dane i możesz wpisać tam co chcesz.

Tak więc mamy już właściwie zbudowany nasz pakiet - polegać on będzie na zapytaniu o dostępność zdalnego hosta - poprosimy go o ECHO REPLY czyli odpowiedz na nasze ECHO REQUEST. Teraz ważna bardzo rzecz - trzeba poinformować kernel, że sami odpowiadamy za zbudowanie nagłówka IP:

  1. int optval = 1;
  2. if( setsockopt( sockfd, IPPROTO_IP, IP_HDRINCL, (char*)&optval, sizeof(optval) ) == -1 ) {
  3. fprintf( stderr, "setsockopt failed.\n" );
  4. return(-1);
  5. }
  6. fprintf( stdout, "setsockopt success.\n" );

Funkcja setsockopt umożliwia nam budowanie nagłówków protokołów i informuje kernel, że te nagłówki będą znajdować się w buforze przygotowanym przez nas. Dokladnie takie wywołanie opcji w symbiozie z ustaleniem wcześniej, że korzystamy z surowego gniazda pozwala nam samym budować pakiet sieciowy. Opcji, które pozwalają dostroić nasze gniazdo jest całe mnóstwo ale innych narazie nie będziemy potrzebować.

Przychodzi w końcu moment na wysłanie naszego pakietu:

  1. if( sendto( sockfd, packet, (size_t)128, 0, (sockaddr*)&destAddr, sizeof(struct sockaddr_in) ) == -1 ) {
  2. fprintf( stderr, "sendto failed.\n" );
  3. return(-1);
  4. }
  5. fprintf( stdout, "sendto success.\n" );
  6.  
  7. struct sockaddr_in local;
  8. char answer[512];
  9. int len = sizeof( struct sockaddr_in );
  10. if( recvfrom( sockfd, answer, 512, 0, (struct sockaddr*)&local, (socklen_t*)&len ) == -1 ) {
  11. fprintf( stderr, "recfrom failed.\n" );
  12. return(-1);
  13. }
  14. fprintf( stdout, "revcfrom success.\n" );
  15.  
  16. ICMP_HDR * icmp_answer = (ICMP_HDR*)&answer[sizeof(IPV4_HDR)];
  17. unsigned short icmpType = ntohs(icmp_answer->icmp_code);
  18. if( icmpType == 0 ) {
  19. fprintf( stdout, "ICMP type returned: %d, ECHO REPLY.\n", icmpType );
  20. } else if ( icmpType == 3 ) {
  21. fprintf( stdout, "ICMP type returned: %d, DESTINATION UNREACHABLE.\n", icmpType );
  22. } else
  23. fprintf( stdout, "ICMP type returned: %d, check this code by yourself.\n", icmpType );

Do wysłania pakietu posłuży nam funkcja sendto - do przeczytania tego co nam serwer odesłał funkcja recvfrom. Przypatrz się dokładnie na ten kod - zastanów się dlaczego tak a nie inaczej, dlaczego te wszystkie rzutowania i dziwne indexy tablic, dlaczego icmpType == 3 albo 0 - why, why - bez sensu tutaj wszystko opisywać bo nie wiele się z tego potem wyniesie jak będzie podane cacy na tacy.

echo reply

Napiszę tak, można czytać tutoriale o socketach w nieskończoność ale i tak nie znajdzie się tam praktycznych informacji na temat wykorzystania tej wiedzy. Bez wstępnego obeznania się z protokołami sieciowymi i znajomością abstrakcji modelu OSI czytanie o programowaniu sieciowym z użyciem gniazd to strata czasu. Jak słusznie zauważyłeś np nie wykorzystaliśmy tutaj funkcji connect albo bind albo send czy read - no ale jak to, przecież żeby zestawić połączenie ze zdalnym hostem trzeba wykonać ten 3-stopniowy uściśk dłoni właśnie poprzez funkcję connect! - ale czy jest to właściwie model połączeniowy czy bezpołączeniowy? (itd.) Cytat z Wiki:

"W warstwie internetowej datagramy dostarczane są w sposób bezpołączeniowy, na zasadzie ?najlepiej, jak się da?. Protokół ICMP jest zestawem komunikatów, przesyłanych w datagramach IP i zdolnych do zgłaszania błędów w dostarczaniu innych datagramów IP." - ten fragment zaczyna nabierać znaczenia dopiero po przeczytaniu tego arta i zadaniu sobie kilku pytań. Datagram? To przypomina listonosza - on tylko dostarcza, a to czy paczka jest uszkodzona czy dobra - cały chuj go obchodzi. Tymi tematami zajmuje się TCP i to on steruje i bada transmisję. Będziesz o tym wiedział kiedy będziesz się zastanawiać czy jako parametr do funkcji socket() użyć SOCK_STREAM czy może SOCK_DGRAM. Kolejny cytat z Wiki: "Ponieważ w szybko ewoluującym środowisku może wystąpić zalew komunikatów, niedostarczenie komunikatu ICMP nie powoduje wysłania komunikatu ICMP o błędzie. Szczególnie, gdy komunikat ICMP o niedostępności hosta docelowego nie dotrze do hosta źródłowego, ten nie wysyła kolejnego komunikatu ICMP." - a zatem modeł bezpołączeniowy - teraz wiesz. Nauka TCP/IP jedynie z czytania logów albo czytanie logów bez wiedzy na temat TCP/IP (praktycznej) - dla mnie bezsens.

Aha i jeszcze jedna rzecz - adres który chcesz sprawdzić zdobędziesz wypełniając strukturę hostent dzięki funkcji gethostbyname() przekazując do niej argument z wiersza poleceń. Gratuluje - moim zdaniem po przeczytaniu wszystkich tutoriali o socketach i tego tutaj wiesz na czym rzecz polega. Na pewno wiesz więcej a to jeszcze nie koniec. W następnym arcie zrobisz swój pierwszy podstawowy skaner portów sieciowych - o zgrozo to już brzmi poważnie.

  1. if( argc < 2 ) {
  2. fprintf( stderr, "Usage: sudo ./ping <host> \n" );
  3. return(-1);
  4. }
  5.  
  6. struct hostent * hostZdalny;
  7. if( (hostZdalny = gethostbyname( argv[1] )) == NULL ) {
  8. fprintf( stderr, "Nie mozna uzyskac informacji o adresie hosta.\n" );
  9. return(-1);
  10. }
  11. struct in_addr *hostZdalnyAscii = (in_addr*)(*hostZdalny->h_addr_list);
  12. fprintf( stdout, "Adres hosta: %s\n", inet_ntoa(*hostZdalnyAscii) );

To jest kod początku programu - spróbuj skleić sam do końca ten program a jak będziesz potrzebować pomocy w sprawie sumy kontrolnej ICMP i nie znajdziesz nic - wtedy napisz. Na konsoli wynik prezentuje się w ten sposób:

  1. flash@flash-laptop:~/Ubuntu/socket/ping$ sudo ./ping www.onet.pl
  2. Adres hosta: 213.180.146.27
  3. socket success.
  4. setsockopt success.
  5. sendto success.
  6. revcfrom success.
  7. ICMP type returned: 0, ECHO REPLY.