Skip to content

Instantly share code, notes, and snippets.

@sierky
Created November 30, 2023 15:52
Show Gist options
  • Save sierky/b4287c4c5278415e8a3281833f9fd073 to your computer and use it in GitHub Desktop.
Save sierky/b4287c4c5278415e8a3281833f9fd073 to your computer and use it in GitHub Desktop.
PHP 8.0+ ipv4 and ipv6 compare / in range checking class
<?php
namespace UpToYou;
/**
* Class to consolidate the logic of comparing ip addresses and if they are within a given range.
*/
class IpUtils {
/**
* Network ranges can be specified as:
* 1. singular ip: 192.168.2.1
* 2. Wildcard format: 1.2.3.*
* 3. CIDR format: 1.2.3/24 OR 1.2.3.4/255.255.255.0
* 4. Start-End IP format: 1.2.3.0-1.2.3.255
* 5. Start-End IP in array: [['1.3.3.0', '1.3.3.255']]
* - this must be twice nested in an array otherwise it will be processed as 2 separate ip's
*
* @param string $ip
* @param array|string $list
* @return bool
*/
public static function ipWithinHaystack(string $ip, array|string $list): bool {
if (!is_array($list)) {
$list = [$list];
}
$ipBin = inet_pton($ip);
if ($ipBin === false) { // Not a valid ip address
return false;
}
$ipv = strlen($ipBin) === 4 ? 4 : 6;
foreach ($list as $range) {
$rangeIpv = !str_contains(print_r($range, true), ':') ? 4 : 6;
if ($ipv === $rangeIpv) { // Oké, so range and ip should be of the same ip version.
if (is_array($range)) { // Assuming it is a range like: ['1.3.3.0', '1.3.3.255']
$range = implode('-', $range);
}
if (self::ipInRange($ipBin, $range, $ipv)) {
return true;
}
}
}
// Falling through
return false;
}
/**
* ipInRange
* Network ranges can be specified as:
* 1. Single IP 1.2.3.4 OR 2002::1234:abcd:ffff:c0a8:0-2002::1234
* 2. Wildcard format: 1.2.3.* OR 2002::1234:abcd:ffff:c0a8:*
* 3. CIDR format: 1.2.3.4/24 OR 1.2.3.4/255.255.255.0 OR 2002::1234:abcd:ffff:c0a8:0/112
* 4 .Start-End IP format: 1.2.3.0-1.2.3.255 OR 2002::1234:abcd:ffff:c0a8:0-2002::1234:abcd:ffff:c0a8:F135
*
* The function will return true if the supplied IP is within the range.
* Note little validation is done on the range inputs, this is done in ipWithinHaystack
*
* @param string $ipBin binary representation already converted by inet_pton()
* @param string $range
* @param int $ipVersion 4 || 6
*
* @return bool
*/
private static function ipInRange(string $ipBin, string $range, int $ipVersion): bool {
// declaring some vars for the ip version
$delimiter = $ipVersion === 4 ? '.' : ':';
$maxSegValue = $ipVersion === 4 ? '255' : 'ffff';
/** First attempt */
// If $range is just a single ip address
// When $range converted by inet_pton() !== false, it is just 1 ip.
// So, we don't have to do the other checks
$rangeBin = inet_pton($range);
if ($rangeBin !== false) {
return $rangeBin === $ipBin;
}
/** Second attempt */
// Is the range a network address with an attached subnet?
if (str_contains($range, '/')) {
// $range is in network/netmask format
list($network, $netmask) = explode('/', $range, 2);
if (str_contains($netmask, $delimiter) && $ipVersion === 4) { // subnet for ipv6 should always be as /cidr
// $netmask is a 255.255.0.0 format
$netmaskBin = inet_pton($netmask);
return ($ipBin & $netmaskBin) === (inet_pton($network) & $netmaskBin);
}
else {
// converting binary string representation '111011011011010........'
$binaryIp = '';
foreach (str_split($ipBin) as $char) {
$binaryIp .= str_pad(decbin(ord($char)), 8, '0', STR_PAD_LEFT);
}
// and the same for the netmask
$binaryNetwork = '';
foreach (str_split(inet_pton($network)) as $char) {
$binaryNetwork .= str_pad(decbin(ord($char)), 8, '0', STR_PAD_LEFT);
}
// Taking the netmask as substring length of the binary string ip's, because these should match
return substr($binaryIp,0, $netmask) === substr($binaryNetwork,0, $netmask);
}
}
/** Third attempt */
// range might be 255.255.*.* or 1.2.3.0-1.2.3.255
// Converting 255.255.*.* to 255.255.0.0-255.255.255.255 so, it is handled by the following if statement.
if (str_contains($range, '*')) { // a.b.*.* format
// Just convert to A-B format by setting * to 0 for A and 255 for B
$lowerIp = str_replace('*', '0', $range);
$upperIp = str_replace('*', $maxSegValue, $range);
$range = "$lowerIp-$upperIp"; // next if-statement will now handle it.
}
// The range check.
if (str_contains($range, '-')) { // 1.2.3.0-1.2.3.255 format
list($lowerIp, $upperIp) = explode('-', $range, 2);
$lowerBin = inet_pton($lowerIp);
$upperBin = inet_pton($upperIp);
if ($lowerBin > $upperBin) { // Who does such a thing?
return false;
}
return $ipBin >= $lowerBin && $ipBin <= $upperBin;
}
// and falling through everything., no match.
return false;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment