Od tygodnia na poważnie wziąłem się za rozpracowywanie protokołów sieciowych. Dzisiaj schodzimy na poziom najniższy jaki się da w programowaniu, czyli zer i jedynek. Ja jako osoba za 2 miesiące rozpoczynająca zawodowo przygodę z programowaniem (ojej, wydało się ;-) ) muszę mieć dosyć dobre pojęcie jak to się dzieje, że wpisuję adres www w pasku adresu przeglądarki, naciskam enter i pokazuje się Fejsbuk. Oczywiście zwykli użytkownicy internetu nie muszą wiedzieć jak to dokładnie działa w myśl zasady „nie muszę być mechanikiem samochodowym żeby jeździć autem”.
Wobec tego na warsztat wziąłem na początek rzecz dosyć prostą: ICMP, a nawet tylko jego wycinek pod nazwą Echo request/Echo reply. Komunikaty ICMP stanowią podstawę działania internetu. Każda maszyna podłączona do sieci potrafi zadawać pytania i otrzymywać odpowiedzi w postaci ICMP. Tak naprawdę zdubluję funkcjonalność wbudowaną w każdy system operacyjny pod powszechnie znaną nazwą ping.
Ręczne tworzenie pakietów w PHP nie ma oczywiście większego sensu poza edukacyjnym (no chyba, że tworzymy coś nietypowego lub dostosowujemy się do już istniejącego protokołu). Jeśli potrzebujemy coś „pingnąć”, wywołujemy polecenie ping shella z poziomu skryptu PHP i tyle.
W Gist zamieściłem klasę gotową do użytku. Na Linuksie będzie problem z jej odpaleniem, gdyż bez roota nie wywołamy funkcji socket_create(). Ten temat jest na tyle ciekawy, że zostawię sobie na następny wpis.
Sama struktura protokołu jest dosyć prosta. Mamy 6 elementów:
- 8 bitów typ żądania (
0x08w przypadku echo request,0x00w przypadku echo reply), - 8 bitów kod żądania (
0x00), - 16 bitów suma kontrolna (tzw. internet checksum, o którym trochę niżej),
- 16 bitów identyfikator (losowa liczba z zakresu
0x000 - 0xFFFF), - 16 bitów nr sekwencyjny (
0x00), - 8– bitów dane (znaki ASCII).
Operując niskopoziomowo musimy zaprzyjaźnić się z funkcjami pack() i unpack() w celu stworzenia i odczytu reprezentacji bitowej ciągu. W przypadku ICMP będzie to wyglądało tak:
const PACKET_REQUEST_TEMPLATE = 'CCnnnA*'; const PACKET_RESPOND_TEMPLATE = 'Ctype/Ccode/nchecksum/nuid/nseq/A*message';
Widać, że 1 bajt możemy odzwierciedlić w postaci typu unsigned char, a 2 bajty w postaci unsigned short.
Teraz może nasunąć się pytanie jak wypełnić pole checksum, skoro jest gdzieś w środku? Odpowiedź jest prosta: w pole wpisujemy tymczasowo 0x00, obliczamy sumę kontrolną dla całości, a następnie zastępujemy wynikiem. Jest to dokładnie 3 i 4 bajt. Jak obliczyć sumę kontrolną? Jest do tego RFC z 1988 r. pod nazwą Computing the Internet Checksum. Jednak zanim zniechęcicie się suchym żargonem informatycznym wytłumaczę po ludzku:
- Cały pakiet ćwiartujemy na kawałki po 16 bitów,
- sumujemy wszystko,
- jeśli w wyniku powstaje nam jakaś nadwyżka w postaci 17 i więcej bitu, odcinamy ją, a następnie dodajemy do pozostałych 16tu,
- ponownie sprawdzamy czy nie powstała nam nadwyżka,
- odwracamy wynik.
Zapis w PHP:
protected function computeChecksum($packet) { // treat the whole packet as 16 bits unsigned short integers $seqPer16bits = unpack('n*', $packet); $sum = array_sum($seqPer16bits); // if there is a carry above 16 bit, add it at the beginning $sum = ($sum >> 16) + ($sum & 0xFFFF); // double check if there is no new carry after previous addition $sum += ($sum >> 16); // return 16 bits negate return pack('n', ~$sum); }
Jak „wkleić” wynik w dobre miejsce? Mała sztuczka:
$packet[2] = $checkSum[0]; // 3 bajt pakietu $packet[3] = $checkSum[1]; // 4 bajt pakietu
Następnie możemy sobie stworzyć gniazdo i ustawić jakiś timeout:
$this->_socket = socket_create( AF_INET, SOCK_RAW, getprotobyname('icmp')); socket_set_option( $this->_socket, SOL_SOCKET, SO_RCVTIMEO, array( 'sec' => 1, 'usec' => 0));
Jesteśmy gotowi do wysyłki:
public function sendPacket($destination, $message = self::DEFAULT_MSG, $port = 0) { $packet = $this->getNewPacket($message); socket_sendto( $this->_socket, $packet, strlen($packet), 0, $destination, $port); $respond = 0; socket_recvfrom($this->_socket, $respond, 255, 0, $destination, $port); // strip IP header return substr($respond, 20); }
Jeśli odbieramy pakiet, warto poddać go analizie:
public function analyzeRespond($responsePacket) { $unpackedRespond = unpack(self::PACKET_RESPOND_TEMPLATE, $responsePacket); if ($unpackedRespond['type'] !== self::TYPE_RESPONSE) { throw new Exception('Bad response type'); } if ($unpackedRespond['uid'] !== $this->_uid) { throw new Exception('Bad unique id'); } // set 3rd and 4th checksum byte to 0x00 // in order to calculate correct checksum $responsePacket[2] = pack('C', self::INITIAL_CHECKSUM); $responsePacket[3] = pack('C', self::INITIAL_CHECKSUM); if (pack('n*', $unpackedRespond['checksum']) !== $this->computeChecksum($responsePacket)) { throw new Exception('Bad checksum'); } return $unpackedRespond['message']; }
Zdaję sobie sprawę, że opis jest niepełny. Chciałem zwrócić uwagę na ciekawsze/trudniejsze momenty. Proponuję dokładnie przeanalizować sobie całą klasę z linka podanego powyżej.
Tak jak obiecałem, w następnym wpisie opiszę jak to wywoływać w możliwie bezpiecznym środowisku.

O autorze