Making a Ping in PHP: A Quick Guide

Written by birkandme | Published 2023/08/29
Tech Story Tags: php | networking | bitwise-operations | programming | coding | making-a-ping-in-php | ping-in-php | php-ping

TLDRvia the TL;DR App

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-bit 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.

    See the code here: https://gist.github.com/BirkAndMe/d5d1e069d94dd060ebccc9a866aa6bb8#file-ping-c-L270

  • Sequence number: Like the identifier, this is used to match the reply left at 0. The original ping increments this on every ping.

    See the code here:

    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. The original ping sends a timeval struct to compute the round-trip.

    See the code here:

    https://gist.github.com/BirkAndMe/d5d1e069d94dd060ebccc9a866aa6bb8#file-ping-c-L249

    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 gives 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 two 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. 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.

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 documentation, and binary string handling in PHP.

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


Also published here.


Written by birkandme | Coding with myself
Published by HackerNoon on 2023/08/29