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).
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
In this example, the package used is phping
(When sending a ping, the package will be the entire header and the data to send.)
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
Adding a, b, and c gives an integer larger than 16-bit:
a + b + c: 1 0100 1111 0011 1000
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
One's complement is the same as a bitwise NOT operation.
~ 0100 1111 0011 1001: 1011 0000 1100 0110
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.
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
// )
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.
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
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.