Created
November 30, 2023 15:52
-
-
Save sierky/b4287c4c5278415e8a3281833f9fd073 to your computer and use it in GitHub Desktop.
PHP 8.0+ ipv4 and ipv6 compare / in range checking class
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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