# Native PHP ping

To make a ping you'll need to know about the [PHP Socket functions](https://www.php.net/manual/en/ref.sockets.php), [ICMP protocol](https://www.ietf.org/rfc/rfc792.txt) and [Computing the Internet Checksum](https://www.ietf.org/rfc/rfc1071.txt) (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.

```nocolor
╔═══════════════╦═══════════════╦══════════════════════════════╗
║    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](https://www.iana.org/assignments/icmp-parameters/icmp-parameters.xhtml#icmp-parameters-codes), 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](https://en.wikipedia.org/wiki/Ones'_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](https://gist.github.com/BirkAndMe/d5d1e069d94dd060ebccc9a866aa6bb8#file-ping-c-L270), 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](https://gist.github.com/BirkAndMe/d5d1e069d94dd060ebccc9a866aa6bb8#file-ping-c-L271).
- **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](https://gist.github.com/BirkAndMe/d5d1e069d94dd060ebccc9a866aa6bb8#file-ping-c-L249) to [compute the round-trip](https://gist.github.com/BirkAndMe/d5d1e069d94dd060ebccc9a866aa6bb8#file-ping-c-L381).

## 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](https://www.ietf.org/rfc/rfc1071.txt) for implementing this (and some examples).
This is just one way of doing it.

```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);
}
```

Check `phping` gives the expected checksum:

```php
$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](https://gist.github.com/BirkAndMe/d5d1e069d94dd060ebccc9a866aa6bb8#file-ping-c-L416).

Another PHP implementation, that resembles the original ping implementation (and C implementation in the RFC) can be found in the [socket_create() comments](https://www.php.net/manual/en/function.socket-create.php#80775).

## Preparing the package

The package is set up following the ICMP header schema:

```php
// 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:

```php
// 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](https://www.php.net/manual/en/book.sockets.php).

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

```php
// 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:

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

## Conclusion

First the script, ready for copy paste:

```php
<?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:

```nocolor
$ 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`.
