Tworzenie pakietów ICMPPHP

Od tygo­dnia na poważ­nie wzią­łem się za rozpra­co­wy­wa­nie proto­ko­łów siecio­wych. Dzisiaj scho­dzimy na poziom najniż­szy jaki się da w progra­mo­wa­niu, czyli zer i jedy­nek. Ja jako osoba za 2 miesiące rozpo­czy­na­jąca zawo­dowo przy­godę z progra­mo­wa­niem (ojej, wydało się ;-) ) muszę mieć dosyć dobre poję­cie jak to się dzieje, że wpisuję adres www w pasku adresu prze­glą­darki, naci­skam enter i poka­zuje się Fejs­buk. Oczy­wi­ście zwykli użyt­kow­nicy inter­netu nie muszą wiedzieć jak to dokład­nie działa w myśl zasady „nie muszę być mecha­ni­kiem samo­cho­do­wym żeby jeździć autem”.

Wobec tego na warsz­tat wzią­łem na począ­tek rzecz dosyć prostą: ICMP, a nawet tylko jego wyci­nek pod nazwą Echo request/Echo reply. Komu­ni­katy ICMP stano­wią podstawę dzia­ła­nia inter­netu. Każda maszyna podłą­czona do sieci potrafi zada­wać pyta­nia i otrzy­my­wać odpo­wie­dzi w postaci ICMP. Tak naprawdę zdubluję funk­cjo­nal­ność wbudo­waną w każdy system opera­cyjny pod powszech­nie znaną nazwą ping.

Ręczne tworze­nie pakie­tów w PHP nie ma oczy­wi­ście więk­szego sensu poza eduka­cyj­nym (no chyba, że tworzymy coś niety­po­wego lub dosto­so­wu­jemy się do już istnie­ją­cego proto­kołu). Jeśli potrze­bu­jemy coś „pingnąć”, wywo­łu­jemy pole­ce­nie ping shella z poziomu skryptu PHP i tyle.

W Gist zamie­ści­łem klasę gotową do użytku. Na Linuk­sie będzie problem z jej odpa­le­niem, gdyż bez roota nie wywo­łamy funk­cji socket_create(). Ten temat jest na tyle ciekawy, że zosta­wię sobie na następny wpis.

Sama struk­tura proto­kołu jest dosyć prosta. Mamy 6 elementów:

  • 8 bitów typ żąda­nia (0x08 w przy­padku echo requ­est, 0x00 w przy­padku echo reply),
  • 8 bitów kod żąda­nia (0x00),
  • 16 bitów suma kontro­lna (tzw. inter­net check­sum, o którym trochę niżej),
  • 16 bitów iden­ty­fi­ka­tor (losowa liczba z zakresu 0x000 - 0xFFFF),
  • 16 bitów nr sekwen­cyjny (0x00),
  • 8– bitów dane (znaki ASCII).

Operu­jąc nisko­po­zio­mowo musimy zaprzy­jaź­nić się z funk­cjami pack()unpack() w celu stwo­rze­nia i odczytu repre­zen­ta­cji bito­wej ciągu. W przy­padku 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 odzwier­cie­dlić w postaci typu unsi­gned char, a 2 bajty w postaci unsi­gned short.

Teraz może nasu­nąć się pyta­nie jak wypeł­nić pole check­sum, skoro jest gdzieś w środku? Odpo­wiedź jest prosta: w pole wpisu­jemy tymcza­sowo 0x00, obli­czamy sumę kontro­lną dla cało­ści, a następ­nie zastę­pu­jemy wyni­kiem. Jest to dokład­nie 3 i 4 bajt. Jak obli­czyć sumę kontro­lną? Jest do tego RFC z 1988 r. pod nazwą Compu­ting the Inter­net Check­sum. Jednak zanim znie­chę­ci­cie się suchym żargo­nem infor­ma­tycz­nym wytłu­ma­czę po ludzku:

  1. Cały pakiet ćwiar­tu­jemy na kawałki po 16 bitów,
  2. sumu­jemy wszystko,
  3. jeśli w wyniku powstaje nam jakaś nadwyżka w postaci 17 i więcej bitu, odci­namy ją, a następ­nie doda­jemy do pozo­sta­łych 16tu,
  4. ponow­nie spraw­dzamy czy nie powstała nam nadwyżka,
  5. odwra­camy 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 miej­sce? Mała sztuczka:

$packet[2] = $checkSum[0]; // 3 bajt pakietu
$packet[3] = $checkSum[1]; // 4 bajt pakietu

Następ­nie możemy sobie stwo­rzyć gniazdo i usta­wić 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 odbie­ramy 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. Propo­nuję dokład­nie prze­ana­li­zo­wać sobie całą klasę z linka poda­nego powyżej.

Tak jak obie­ca­łem, w następ­nym wpisie opiszę jak to wywo­ły­wać w możli­wie bezpiecz­nym środowisku.

Podobne wpisy:

  1. Writing custom stream filters
  2. (W miarę) bezpieczne urucha­mia­nie skryp­tów PHP poprzez shell_exec()
  3. MySQL i auto­ma­tyczne tworze­nie histo­rii rekordu w bazie [cz. 1/2]
  4. MySQL i auto­ma­tyczne tworze­nie histo­rii rekordu w bazie [cz. 2/2]

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany.

Możesz użyć następujących tagów oraz atrybutów HTML-a: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <p> <pre lang="" line="" escaped=""> <q cite=""> <strike> <strong>