Native PHP ping

Native PHP ping

To make a ping you'll need to know about the PHP Socket functions, ICMP protocol and Computing the Internet Checksum (don't let the RFCs scare you, the basics will be covered here).

Echo message

The ICMP header starts after the regular IP header (which is handled by socket_connect). It's a pretty simple header of 8 bytes, followed by the message being sent.

╔═══════════════╦═══════════════╦══════════════════════════════╗
║    Type 8b    ║    Code 8b    ║         Checksum 16b         ║
╠═══════════════╩═══════════════╬══════════════════════════════╣
║         Identifier 16b        ║      Sequence number 16b     ║
╠═══════════════════════════════╩══════════════════════════════╣
║                     data (variable length)                   ║
╚══════════════════════════════════════════════════════════════╝
  • Type: The type of control message to send. The echo message is 8, but there are a lot of other types, for other commands.
  • Code: An extra setting for the type, depending on the type. The echo message doesn't have any extra settings, so it's left at 0.
  • Checksum: This is calculated after the package is assembled, to start with it's simply set to 0. The package is paired into 16-bits integers, and one's complement of the sum(explained later on).
  • Identifier: The request identifier can be anything, it's used to match the reply. In the original ping, this was the UNIX process ID, but we'll leave it 0.
  • Sequence number: Like the identifier, this is used to match the reply, left at 0. The original ping increments this on every ping.
  • Data: The message. Like with the Identifier and Sequence number, the destination needs to reply with the same values. Original ping sends a timeval struct to compute the round-trip.

Calculating the checksum

In this example, the package used is phping (when sending a ping the package will be the entire header and the data to send)

1) Divide the package into 16-bit pairs.

The package phping is the following in binary:

p: 0111 0000
h: 0110 1000
p: 0111 0000
i: 0110 1001
n: 0110 1110
g: 0110 0111

This is paired into 16-bit integers:

a: 0111 0000 0110 1000
b: 0111 0000 0110 1001
c: 0110 1110 0110 0111

2) Add all the pairs to get the sum.

Adding a, b and c give an integer larger than 16 bit:

a + b + c: 1 0100 1111 0011 1000

3) Sum 16-bit pairs of the sum again.

Reiterate the first 2 steps, until the sum is a single 16-bit integer.

Divide it:

a: 0000 0000 0000 0001
b: 0100 1111 0011 1000

Sum it:

a + b: 0100 1111 0011 1001

4) End with one's complement, to invert the integer.

One's complement is the same as a bitwise NOT operation.

~ 0100 1111 0011 1001: 1011 0000 1100 0110

PHP checksum implementation

There are a lot of tips and tricks, in the previously mentioned RFC 1071 for implementing this (and some examples). This is just one way of doing it.

function computeInternetChecksum($in) {
  // Add an empty char (8-bit).
  // This trick leverages the way unpack() works.
  $in .= "\x0";

  // The n* format splits up the data string into 16-bit pairs.
  // It will unpack the string from the beginning, and only split
  // whole pairs. So it will automatically leave out (or include) the
  // odd byte added above.
  $pairs = unpack('n*', $in);

  // Sum the pairs.
  $sum = array_sum($pairs);

  // Add the hi 16 to the low 16 bits, ending in a single 16-bit int.
  while ($sum >> 16)
    $sum = ($sum >> 16) + ($sum & 0xffff);

  // End with one's complement, to invert the integer.
  // Note the ~ operator before packing the sum into a string again.
  return pack('n', ~$sum);
}

Check phping gives the expected checksum:

$checksum = computeInternetChecksum('phping');

// Note that unpack() returns an array starting 1 (not 0).
echo decbin(unpack('n*', $checksum)[1]);

// Output (the same as manually calculated):
// 1011000011000110

If you're interested check out the original ping checksum implementation.

Another PHP implementation, that resembles the original ping implementation (and C implementation in the RFC) can be found in the socket_create() comments.

Preparing the package

The package is set up following the ICMP header schema:

// Prepare the package.
$package = [
  'type' => "\x08",
  'code' => "\x00",
  'checksum' => "\x00\x00",
  'identifier' => "\x00\x00",
  'seqNumber' => "\x00\x00",
  'data' => 'phping',
];

// Compute the checksum, so it's ready to send.
$package['checksum'] = computeInternetChecksum(implode('', $package));
$rawPackage = implode('', $package);

Check the package is as expected:

// Unpack the package into an associated array.
$icmpHeaderFormat = 'Ctype/Ccode/nchecksum/nidentifier/nsequence';
// And show the binary values of each header part.
print_r(array_map('decbin', unpack($icmpHeaderFormat, $rawPackage)));

// Output:
// Array
// (
//     [type] => 1000
//     [code] => 0
//     [checksum] => 1010100011000110
//     [identifier] => 0
//     [sequence] => 0
// )

Moving on to sockets

Sending the package is done by using the PHP socket functions.

ICMP requests need raw network access, this causes permission issues, more on this later.

// Create the socket.
// AF_INIT is the IPv4 protocol.
// SOCK_RAW is needed to perform ICMP requests.
$socket = socket_create(AF_INET, SOCK_RAW, getprotobyname('icmp'));

// Open up the connection to a host.
socket_connect($socket, 'google.com', null);

// Used to calculate the response time.
$time = microtime(true);

// Send the package to the target host.
socket_send($socket, $rawPackage, strlen($rawPackage), 0);

// Read the response.
if ($in = socket_read($socket, 1)) {
  // Print the response time.
  echo microtime(true) - $time . " seconds\n";
}

// Close the socket.
socket_close($socket);

This doesn't check the reply, and simply assumes any reply is valid. This is also why only 1 byte is read in the socket_read.

To check the reply, you would need to parse the input (including the IPv4 header) and verify the checksum.

The SOCK_RAW issue.

Because of security risks, root / administrator access is needed to use SOCK_RAW (or the PHP executable needs CAP_NET_RAW capability).

So when testing the script you'll need to sudo the command (I believe the Windows equivalent would be run as administrator).

PHP will trigger the following warning (this may vary depending on OS), if it's not run with sufficient permissions:

PHP Warning:  socket_create(): Unable to create socket [1]: Operation not permitted

Conclusion

First the script, ready for copy paste:

<?php

function computeInternetChecksum($in) {
  // Add an empty char (8-bit).
  // This trick leverages the way unpack() works.
  $in .= "\x0";

  // The n* format splits up the data string into 16-bit pairs.
  // It will unpack the string from the beginning, and only split
  // whole pairs. So it will automatically leave out (or include) the
  // odd byte added above.
  $pairs = unpack('n*', $in);

  // Sum the pairs.
  $sum = array_sum($pairs);

  // Add the hi 16 to the low 16 bits, ending in a single 16-bit int.
  while ($sum >> 16)
    $sum = ($sum >> 16) + ($sum & 0xffff);

  // End with one's complement, to invert the integer.
  // Note the ~ operator before packing the sum into a string again.
  return pack('n', ~$sum);
}

// Prepare the package.
$package = [
  'type' => "\x08",
  'code' => "\x00",
  'checksum' => "\x00\x00",
  'identifier' => "\x00\x00",
  'seqNumber' => "\x00\x00",
  'data' => 'phping',
];

// Compute the checksum, so it's ready to send.
$package['checksum'] = computeInternetChecksum(implode('', $package));
$rawPackage = implode('', $package);

// Create the socket.
// AF_INIT is the IPv4 protocol.
// SOCK_RAW is needed to perform ICMP requests.
$socket = socket_create(AF_INET, SOCK_RAW, getprotobyname('icmp'));

// Open up the connection to a host.
socket_connect($socket, 'google.com', null);

// Used to calculate the response time.
$time = microtime(true);

// Send the package to the target host.
socket_send($socket, $rawPackage, strlen($rawPackage), 0);

// Read the response.
if ($in = socket_read($socket, 1)) {
  // Print the response time.
  echo microtime(true) - $time . " seconds\n";
}

// Close the socket.
socket_close($socket);

And the result:

$ sudo php ping.php
0.015023946762085 seconds

Because of the SOCK_RAW limitation, it's mostly an exercise in working with sockets, RFC documentations and binary string handling in PHP.

It might have its merits in a PHP CLI script, or if the PHP executable called by the webserver can get the CAP_NET_RAW using setcap.

Did you find this article valuable?

Support Philip Birk-Jensen by becoming a sponsor. Any amount is appreciated!