Check whether or not a CIDR subnet contains an IP address - php

I'm looking for quick/simple method for matching a given IP4 dotted quad IP to a CIDR notation mask.
I have a bunch of IPs I need to see if they match a range of IPs.
example:
$ips = array('10.2.1.100', '10.2.1.101', '10.5.1.100', '1.2.3.4');
foreach ($ips as $IP) {
if (cidr_match($IP, '10.2.0.0/16') == true) {
print "you're in the 10.2 subnet\n";
}
}
What would cidr_match() look like?
It doesn't really have to be simple, but fast would be good. Anything that uses only built-in/common functions is a bonus (as I'm likely to get one person to show me something in pear that does this, but I can't depend on pear or that package being installed where my code is deployed).

If only using IPv4:
use ip2long() to convert the IPs and the subnet range into long integers
convert the /xx into a subnet mask
do a bitwise 'and' (i.e. ip & mask)' and check that that 'result = subnet'
something like this should work:
function cidr_match($ip, $range)
{
list ($subnet, $bits) = explode('/', $range);
if ($bits === null) {
$bits = 32;
}
$ip = ip2long($ip);
$subnet = ip2long($subnet);
$mask = -1 << (32 - $bits);
$subnet &= $mask; # nb: in case the supplied subnet wasn't correctly aligned
return ($ip & $mask) == $subnet;
}

In a similar situation, I ended up using symfony/http-foundation.
When using this package, your code would look like:
$ips = array('10.2.1.100', '10.2.1.101', '10.5.1.100', '1.2.3.4');
foreach($ips as $IP) {
if (\Symfony\Component\HttpFoundation\IpUtils::checkIp($IP, '10.2.0.0/16')) {
print "you're in the 10.2 subnet\n";
}
}
It also handles IPv6.
Link: https://packagist.org/packages/symfony/http-foundation

I found many of these methods breaking after PHP 5.2. However the following solution works on versions 5.2 and above:
function cidr_match($ip, $cidr)
{
list($subnet, $mask) = explode('/', $cidr);
if ((ip2long($ip) & ~((1 << (32 - $mask)) - 1) ) == ip2long($subnet))
{
return true;
}
return false;
}
Example results
cidr_match("1.2.3.4", "0.0.0.0/0"): true
cidr_match("127.0.0.1", "127.0.0.1/32"): true
cidr_match("127.0.0.1", "127.0.0.2/32"): false
Source http://www.php.net/manual/en/function.ip2long.php#82397.

Some function changed:
split with explode
function cidr_match($ip, $range)
{
list ($subnet, $bits) = explode('/', $range);
$ip = ip2long($ip);
$subnet = ip2long($subnet);
$mask = -1 << (32 - $bits);
$subnet &= $mask;
return ($ip & $mask) == $subnet;
}

Here is one fast 64bits function to do it, please comment the return line you don't need. Accepting any valid Ipv4 with or without valid CIDR Routing Prefix for example 63.161.156.0/24 or 63.161.156.0
<?php
function cidr2range($ipv4){
if ($ip=strpos($ipv4,'/'))
{$n_ip=(1<<(32-substr($ipv4,1+$ip)))-1; $ip_dec=ip2long(substr($ipv4,0,$ip)); }
else
{$n_ip=0; $ip_dec=ip2long($ipv4); }
$ip_min=$ip_dec&~$n_ip;
$ip_max=$ip_min+$n_ip;
#Array(2) of Decimal Values Range
return [$ip_min,$ip_max];
#Array(2) of Ipv4 Human Readable Range
return [long2ip($ip_min),long2ip($ip_max)];
#Array(2) of Ipv4 and Subnet Range
return [long2ip($ip_min),long2ip(~$n_ip)];
#Array(2) of Ipv4 and Wildcard Bits
return [long2ip($ip_min),long2ip($n_ip)];
#Integer Number of Ipv4 in Range
return ++$n_ip;
}
To fast check if a given ipv4 is matching a given CIDR you can do it inline like in this example
<?php
$given_cidr='55.55.55.0/24';
$given_ipv4='55.55.55.55';
if(($range=cidr2range($given_cidr)) &&
($check=ip2long($given_ipv4))!==false &&
$check>=$range[0] && $check<=$range[1])
{
echo 'Yes, '.$given_ipv4.' is included in '.$given_cidr;
}
else
{
echo 'No, '.$given_ipv4.' is not included in '.$given_cidr;
}
To get the full range as an array for a given IP (with or without CIDR Routing Prefix) you can use the following code but be carefull because for example 25.25.25.25/16 return an array with 65536 elements and you can easily run out of memory using a smaller Routing Prefix
<?php
$result=cidr2range($ipv4);
for($ip_dec=$result[0];$ip_dec<=$result[1];$ip_dec++)
$full_range[$ip_dec]=long2ip($ip_dec);
print_r($full_range);
To fast check if a given ipv4 is matching a given array of IP (with or without CIDR Routing Prefix)
<?php
#This code is checking if a given ip belongs to googlebot
$given_ipv4='74.125.61.208';
$given_cidr_array=['108.59.93.43/32','108.59.93.40/31','108.59.93.44/30','108.59.93.32/29','108.59.93.48/28','108.59.93.0/27','108.59.93.64/26','108.59.93.192/26','108.59.92.192/27','108.59.92.128/26','108.59.92.96/27','108.59.92.0/27','108.59.94.208/29','108.59.94.192/28','108.59.94.240/28','108.59.94.128/26','108.59.94.16/29','108.59.94.0/28','108.59.94.32/27','108.59.94.64/26','108.59.95.0/24','108.59.88.0/22','108.59.81.0/27','108.59.80.0/24','108.59.82.0/23','108.59.84.0/22','108.170.217.128/28','108.170.217.160/27','108.170.217.192/26','108.170.217.0/25','108.170.216.0/24','108.170.218.0/23','108.170.220.0/22','108.170.208.0/21','108.170.192.0/20','108.170.224.0/19','108.177.0.0/17','104.132.0.0/14','104.154.0.0/15','104.196.0.0/14','107.167.160.0/19','107.178.192.0/18','125.17.82.112/30','125.16.7.72/30','74.125.0.0/16','72.14.192.0/18','77.109.131.208/28','77.67.50.32/27','66.102.0.0/20','66.227.77.144/29','66.249.64.0/19','67.148.177.136/29','64.124.98.104/29','64.71.148.240/29','64.68.64.64/26','64.68.80.0/20','64.41.221.192/28','64.41.146.208/28','64.9.224.0/19','64.233.160.0/19','65.171.1.144/28','65.170.13.0/28','65.167.144.64/28','65.220.13.0/24','65.216.183.0/24','70.32.132.0/23','70.32.128.0/22','70.32.136.0/21','70.32.144.0/20','85.182.250.128/26','85.182.250.0/25','80.239.168.192/26','80.149.20.0/25','61.246.224.136/30','61.246.190.124/30','63.237.119.112/29','63.226.245.56/29','63.158.137.224/29','63.166.17.128/25','63.161.156.0/24','63.88.22.0/23','41.206.188.128/26','12.234.149.240/29','12.216.80.0/24','8.34.217.24/29','8.34.217.0/28','8.34.217.32/27','8.34.217.64/26','8.34.217.128/25','8.34.216.0/24','8.34.218.0/23','8.34.220.0/22','8.34.208.128/29','8.34.208.144/28','8.34.208.160/27','8.34.208.192/26','8.34.208.0/25','8.34.209.0/24','8.34.210.0/23','8.34.212.0/22','8.35.195.128/28','8.35.195.160/27','8.35.195.192/26','8.35.195.0/25','8.35.194.0/24','8.35.192.0/23','8.35.196.0/22','8.35.200.0/21','8.8.8.0/24','8.8.4.0/24','8.6.48.0/21','4.3.2.0/24','23.236.48.0/20','23.251.128.0/19','216.239.32.0/19','216.252.220.0/22','216.136.145.128/27','216.33.229.160/29','216.33.229.144/29','216.34.7.176/28','216.58.192.0/19','216.109.75.80/28','216.74.130.48/28','216.74.153.0/27','217.118.234.96/28','208.46.199.160/29','208.44.48.240/29','208.21.209.0/28','208.184.125.240/28','209.185.108.128/25','209.85.128.0/17','213.200.103.128/26','213.200.99.192/26','213.155.151.128/26','199.192.112.224/29','199.192.112.192/27','199.192.112.128/26','199.192.112.0/25','199.192.113.176/28','199.192.113.128/27','199.192.113.192/26','199.192.113.0/25','199.192.115.80/28','199.192.115.96/27','199.192.115.0/28','199.192.115.128/25','199.192.114.192/26','199.192.114.0/25','199.223.232.0/21','198.108.100.192/28','195.16.45.144/29','192.104.160.0/23','192.158.28.0/22','192.178.0.0/15','206.160.135.240/28','207.223.160.0/20','203.222.167.144/28','173.255.125.72/29','173.255.125.80/28','173.255.125.96/27','173.255.125.0/27','173.255.125.128/25','173.255.124.240/29','173.255.124.232/29','173.255.124.192/27','173.255.124.128/29','173.255.124.144/28','173.255.124.160/27','173.255.124.48/29','173.255.124.32/28','173.255.124.0/27','173.255.124.64/26','173.255.126.0/23','173.255.122.128/26','173.255.122.64/26','173.255.123.0/24','173.255.121.128/26','173.255.121.0/25','173.255.120.0/24','173.255.117.32/27','173.255.117.64/26','173.255.117.128/25','173.255.116.192/27','173.255.116.128/26','173.255.116.0/25','173.255.118.0/23','173.255.112.0/22','173.194.0.0/16','172.102.8.0/21','172.253.0.0/16','172.217.0.0/16','162.216.148.0/22','162.222.176.0/21','180.87.33.64/26','128.177.109.0/26','128.177.119.128/25','128.177.163.0/25','130.211.0.0/16','142.250.0.0/15','146.148.0.0/17'];
echo '<pre>';
$in_range=false;
if (($given_ipv4_dec=ip2long($given_ipv4))!==false)
{
foreach($given_cidr_array as $given_cidr){
if(($range=cidr2range($given_cidr)) &&
$given_ipv4_dec>=$range[0] && $given_ipv4_dec<=$range[1])
{
$in_range=true;
echo $given_ipv4.' matched '.$given_cidr.' ('.join(array_map('long2ip',$range),' - ').")\n";
}
}
}
echo $given_ipv4.' is probably'.($in_range?'':' not').' a Googlebot IP';
To run fast the function don't check input but formally it should be a string matching the following regex
#^(?:((?:0)|(?:2(?:(?:[0-4][0-9])|(?:5[0-5])))|(?:1?[0-9]{1,2}))\.((?:0)|(?:2(?:(?:[0-4][0-9])|(?:5[0-5])))|(?:1?[0-9]{1,2}))\.((?:0)|(?:2(?:(?:[0-4][0-9])|(?:5[0-5])))|(?:1?[0-9]{1,2}))\.((?:0)|(?:2(?:(?:[0-4][0-9])|(?:5[0-5])))|(?:1?[0-9]{1,2}))(?:/((?:(?:0)|(?:3[0-2])|(?:[1-2]?[0-9]))))?)$#
If you want to verify the input before using the function
<?php
if (is_string($ipv4) && preg_match('#^(?:((?:0)|(?:2(?:(?:[0-4][0-9])|(?:5[0-5])))|(?:1?[0-9]{1,2}))\.((?:0)|(?:2(?:(?:[0-4][0-9])|(?:5[0-5])))|(?:1?[0-9]{1,2}))\.((?:0)|(?:2(?:(?:[0-4][0-9])|(?:5[0-5])))|(?:1?[0-9]{1,2}))\.((?:0)|(?:2(?:(?:[0-4][0-9])|(?:5[0-5])))|(?:1?[0-9]{1,2}))(?:/((?:(?:0)|(?:3[0-2])|(?:[1-2]?[0-9]))))?)$#',$ipv4))
{
#This is a valid ipv4 with or without CIDR Routing Prefix
$result=cidr2range($ipv4);
print_r($result);
}
Then the formal answer to your question is the following
<?php
#Requiring cidr2range shown above function
function cidr_match($mixed_ip,$mixed_cidr){
if (!is_array($mixed_ip)){
$string_mode=true;
$mixed_ip=[$mixed_ip=>0];
}
else $mixed_ip=array_fill_keys($mixed_ip,0);
if (!is_array($mixed_cidr)) $mixed_cidr=[$mixed_cidr];
foreach($mixed_ip as $ip => &$result)
foreach($mixed_cidr as $cidr)
{
if(($range=cidr2range($cidr)) &&
($check=ip2long($ip))!==false &&
$check>=$range[0] && $check<=$range[1]){
$result=$cidr;
break;
}
}
$mixed_ip=array_filter($mixed_ip);
return $string_mode?($mixed_ip?true:false):$mixed_ip;
}
print '<pre>';
#Your example
$ips = array('10.2.1.100', '10.2.1.101', '10.5.1.100', '1.2.3.4');
foreach ($ips as $IP) {
if (cidr_match($IP, '10.2.0.0/16') == true) {
print "you're in the 10.2 subnet\n";
}
}
#Also working with IP array and/or CIDR array
#If IP array is given then return an array containing IP (keys) matching CIDR (values)
$result=cidr_match($ips,['20.2.0.0/16','10.2.0.0/15']);
foreach($result as $ip => $cidr){
print "$ip is in the $cidr subnet\n";
}
You can compile your own function using these examples, hope these few lines have helped you…

My technique uses bit to bit matching using subnet and mask.
function cidr_match($ip, $range){
list ($subnet, $bits) = explode('/', $range);
$ip = substr(IP2bin($ip),0,$bits) ;
$subnet = substr(IP2Bin($subnet),0,$bits) ;
return ($ip == $subnet) ;
}
function IP2Bin($ip){
$ipbin = '';
$ips = explode(".",$ip) ;
foreach ($ips as $iptmp){
$ipbin .= sprintf("%08b",$iptmp) ;
}
return $ipbin ;
}

I also needed to test IP's against CIDR masks. I've found a website with excellent explanation and sourcecode which works perfectly well.
The website http://pgregg.com/blog/2009/04/php-algorithms-determining-if-an-ip-is-within-a-specific-range/
Because the website can one day cease to exist, here is the code
<?php
/*
* ip_in_range.php - Function to determine if an IP is located in a
* specific range as specified via several alternative
* formats.
*
* Network ranges can be specified as:
* 1. Wildcard format: 1.2.3.*
* 2. CIDR format: 1.2.3/24 OR 1.2.3.4/255.255.255.0
* 3. Start-End IP format: 1.2.3.0-1.2.3.255
*
* Return value BOOLEAN : ip_in_range($ip, $range);
*
* Copyright 2008: Paul Gregg <pgregg#pgregg.com>
* 10 January 2008
* Version: 1.2
*
* Source website: http://www.pgregg.com/projects/php/ip_in_range/
* Version 1.2
*
* This software is Donationware - if you feel you have benefited from
* the use of this tool then please consider a donation. The value of
* which is entirely left up to your discretion.
* http://www.pgregg.com/donate/
*
* Please do not remove this header, or source attibution from this file.
*/
// decbin32
// In order to simplify working with IP addresses (in binary) and their
// netmasks, it is easier to ensure that the binary strings are padded
// with zeros out to 32 characters - IP addresses are 32 bit numbers
Function decbin32 ($dec) {
return str_pad(decbin($dec), 32, '0', STR_PAD_LEFT);
}
// ip_in_range
// This function takes 2 arguments, an IP address and a "range" in several
// different formats.
// Network ranges can be specified as:
// 1. Wildcard format: 1.2.3.*
// 2. CIDR format: 1.2.3/24 OR 1.2.3.4/255.255.255.0
// 3. Start-End IP format: 1.2.3.0-1.2.3.255
// The function will return true if the supplied IP is within the range.
// Note little validation is done on the range inputs - it expects you to
// use one of the above 3 formats.
Function ip_in_range($ip, $range) {
if (strpos($range, '/') !== false) {
// $range is in IP/NETMASK format
list($range, $netmask) = explode('/', $range, 2);
if (strpos($netmask, '.') !== false) {
// $netmask is a 255.255.0.0 format
$netmask = str_replace('*', '0', $netmask);
$netmask_dec = ip2long($netmask);
return ( (ip2long($ip) & $netmask_dec) == (ip2long($range) & $netmask_dec) );
} else {
// $netmask is a CIDR size block
// fix the range argument
$x = explode('.', $range);
while(count($x)<4) $x[] = '0';
list($a,$b,$c,$d) = $x;
$range = sprintf("%u.%u.%u.%u", empty($a)?'0':$a, empty($b)?'0':$b,empty($c)?'0':$c,empty($d)?'0':$d);
$range_dec = ip2long($range);
$ip_dec = ip2long($ip);
# Strategy 1 - Create the netmask with 'netmask' 1s and then fill it to 32 with 0s
#$netmask_dec = bindec(str_pad('', $netmask, '1') . str_pad('', 32-$netmask, '0'));
# Strategy 2 - Use math to create it
$wildcard_dec = pow(2, (32-$netmask)) - 1;
$netmask_dec = ~ $wildcard_dec;
return (($ip_dec & $netmask_dec) == ($range_dec & $netmask_dec));
}
} else {
// range might be 255.255.*.* or 1.2.3.0-1.2.3.255
if (strpos($range, '*') !==false) { // a.b.*.* format
// Just convert to A-B format by setting * to 0 for A and 255 for B
$lower = str_replace('*', '0', $range);
$upper = str_replace('*', '255', $range);
$range = "$lower-$upper";
}
if (strpos($range, '-')!==false) { // A-B format
list($lower, $upper) = explode('-', $range, 2);
$lower_dec = (float)sprintf("%u",ip2long($lower));
$upper_dec = (float)sprintf("%u",ip2long($upper));
$ip_dec = (float)sprintf("%u",ip2long($ip));
return ( ($ip_dec>=$lower_dec) && ($ip_dec<=$upper_dec) );
}
echo 'Range argument is not in 1.2.3.4/24 or 1.2.3.4/255.255.255.0 format';
return false;
}
}
?>
(I did not develop this; this is developed by Paul Gregg (http://pgregg.com/)

I recently needed to match an IP address to a CIDR mask and came across this article. Below is a slightly different approach based on the ideas above and includes a check on the CIDR input. The function returns false if an incorrect CIDR format is submitted.
I posted this solution for anyone who needs a turn-key function that has been tested.
/**
* Validates subnet specified by CIDR notation.of the form IP address followed by
* a '/' character and a decimal number specifying the length, in bits, of the subnet
* mask or routing prefix (number from 0 to 32).
*
* #param $ip - IP address to check
* #param $cidr - IP address range in CIDR notation for check
* #return bool - true match found otherwise false
*/
function cidr_match($ip, $cidr) {
$outcome = false;
$pattern = '/^(([01]?\d?\d|2[0-4]\d|25[0-5])\.){3}([01]?\d?\d|2[0-4]\d|25[0-5])\/(\d{1}|[0-2]{1}\d{1}|3[0-2])$/';
if (preg_match($pattern, $cidr)){
list($subnet, $mask) = explode('/', $cidr);
if (ip2long($ip) >> (32 - $mask) == ip2long($subnet) >> (32 - $mask)) {
$outcome = true;
}
}
return $outcome;
}
Test data is shown in the image below:

function cidr_match($ipStr, $cidrStr) {
$ip = ip2long($ipStr);
$cidrArr = split('/',$cidrStr);
$maskIP = ip2long($cidrArr[0]);
$maskBits = 32 - $cidrArr[1];
return (($ip>>$maskBits) == ($maskIP>>$maskBits));
}

Just a note, Alnitak's answer works 32/64 bit.
Here is a cooked version of it, for quick spam protection based on country IP lists that you can get everywhere. google for country ip list or country ip block (Have to give one here, really difficult to find it in that sites page navigation:Country ip block generator)
Copy-paste your cidr ip list to string $cidrs. And put this code just before page html, possibly in the header.php file.
Can also be used to filter adsense use in page templates based on country.
This is only a in-the-middle-of-the-night-urgency solution. Sometimes one needs to come up with something like this for a client quickly yesterday, so here it is.
//++++++++++++++++++++++
//COUNTRY SPAM PROTECTOR
//speed: ~5ms # 2000 cidrs
//comments start with #
//++++++++++++++++++++++
$cidrs=
'
#yourcountry
1.3.4.5/21
#mycountry
6.7.8.9/20
';
//$cidrs.="\n".'123.12.12.12/32';//test, your ip
$cidrs_ar=preg_split('/\s+/',$cidrs,-1,PREG_SPLIT_NO_EMPTY);
$ip=#$_SERVER['REMOTE_ADDR'];
$iplong=ip2long($ip);
//var_export($cidrs_ar);var_export($ip);var_export($iplong);
if($iplong)
foreach($cidrs_ar as $cidr)
{
$ar=explode ('/', $cidr);
$netiplong=ip2long($ar[0]);
if($netiplong===false) continue;
$mask=intval(#$ar[1]);
if(!$mask) continue;
$bitmask=-1 <<(32-$mask);
if(($iplong & $bitmask) == ($netiplong & $bitmask))
{
header('Location: http://www.someotherwebsite.com/',true,303);
exit;
}
}

You also can use Net_IPv4 PEAR library.
function cidr_match($ip, $net){
include_once("Net/IPv4.php");
$objIP = new Net_IPv4();
return $objIP->ipInNetwork($ip, $net);
}

Perhaps it is useful to someone.
Convert bit mask into IP mask:
// convert 12 => 255.240.0.0
// ip2long('255.255.255.255') == -1
$ip = long2ip((-1 << (32 - $bit)) & -1);
Convert IP mask into bit mask:
// convert 255.240.0.0 => 12
// is valid IP
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) === false) {
throw new \InvalidArgumentException(sprintf('Invalid IP "%s".', $ip));
}
// convert decimal to binary
$mask = '';
foreach (explode('.', $ip) as $octet) {
$mask .= str_pad(decbin($octet), 8, '0', STR_PAD_LEFT);
}
// check mask
if (strpos('01', $mask) !== false) {
// valid 11111111111111111111111100000000 -> 255.255.255.0
// invalid 11111111111111111111111100000001 -> 255.255.255.1
throw new \InvalidArgumentException(sprintf('IP mask "%s" is not valid.', $ip));
}
$bit = substr_count($mask, '1'); // bit mask

I want to have you look at my few lines. The examples that people suggested before me don't seem to work. One reason being, as far as I understand it, is that CIDR mask bits are binary numbers, so the bit shift must be done on a binary number. I have tried converting the long IP's into binaries, but ran into a max binary number limit.
OK, here my few lines ... I await your comments.
function cidr_match($ipStr, $cidrStr) {
$ipStr = explode('.', $ipStr);
foreach ($ipStr as $key => $val) {
$ipStr[$key] = str_pad(decbin($val), 8, '0', STR_PAD_LEFT);
}
$ip = '';
foreach ($ipStr as $binval) {
$ip = $ip . $binval;
}
$cidrArr = explode('/',$cidrStr);
$maskIP = explode('.', $cidrArr[0]);
foreach ($maskIP as $key => $val) {
$maskIP[$key] = str_pad(decbin($val), 8, '0', STR_PAD_LEFT);
}
$maskIP = '';
foreach ($ipStr as $binval) {
$maskIP = $maskIP . $binval;
}
$maskBits = 32 - $cidrArr[1];
return (($ip>>$maskBits) == ($maskIP>>$maskBits));
}

Related

How to check if googlebot is really Google via IP List? [duplicate]

I'm looking for quick/simple method for matching a given IP4 dotted quad IP to a CIDR notation mask.
I have a bunch of IPs I need to see if they match a range of IPs.
example:
$ips = array('10.2.1.100', '10.2.1.101', '10.5.1.100', '1.2.3.4');
foreach ($ips as $IP) {
if (cidr_match($IP, '10.2.0.0/16') == true) {
print "you're in the 10.2 subnet\n";
}
}
What would cidr_match() look like?
It doesn't really have to be simple, but fast would be good. Anything that uses only built-in/common functions is a bonus (as I'm likely to get one person to show me something in pear that does this, but I can't depend on pear or that package being installed where my code is deployed).
If only using IPv4:
use ip2long() to convert the IPs and the subnet range into long integers
convert the /xx into a subnet mask
do a bitwise 'and' (i.e. ip & mask)' and check that that 'result = subnet'
something like this should work:
function cidr_match($ip, $range)
{
list ($subnet, $bits) = explode('/', $range);
if ($bits === null) {
$bits = 32;
}
$ip = ip2long($ip);
$subnet = ip2long($subnet);
$mask = -1 << (32 - $bits);
$subnet &= $mask; # nb: in case the supplied subnet wasn't correctly aligned
return ($ip & $mask) == $subnet;
}
In a similar situation, I ended up using symfony/http-foundation.
When using this package, your code would look like:
$ips = array('10.2.1.100', '10.2.1.101', '10.5.1.100', '1.2.3.4');
foreach($ips as $IP) {
if (\Symfony\Component\HttpFoundation\IpUtils::checkIp($IP, '10.2.0.0/16')) {
print "you're in the 10.2 subnet\n";
}
}
It also handles IPv6.
Link: https://packagist.org/packages/symfony/http-foundation
I found many of these methods breaking after PHP 5.2. However the following solution works on versions 5.2 and above:
function cidr_match($ip, $cidr)
{
list($subnet, $mask) = explode('/', $cidr);
if ((ip2long($ip) & ~((1 << (32 - $mask)) - 1) ) == ip2long($subnet))
{
return true;
}
return false;
}
Example results
cidr_match("1.2.3.4", "0.0.0.0/0"): true
cidr_match("127.0.0.1", "127.0.0.1/32"): true
cidr_match("127.0.0.1", "127.0.0.2/32"): false
Source http://www.php.net/manual/en/function.ip2long.php#82397.
Some function changed:
split with explode
function cidr_match($ip, $range)
{
list ($subnet, $bits) = explode('/', $range);
$ip = ip2long($ip);
$subnet = ip2long($subnet);
$mask = -1 << (32 - $bits);
$subnet &= $mask;
return ($ip & $mask) == $subnet;
}
Here is one fast 64bits function to do it, please comment the return line you don't need. Accepting any valid Ipv4 with or without valid CIDR Routing Prefix for example 63.161.156.0/24 or 63.161.156.0
<?php
function cidr2range($ipv4){
if ($ip=strpos($ipv4,'/'))
{$n_ip=(1<<(32-substr($ipv4,1+$ip)))-1; $ip_dec=ip2long(substr($ipv4,0,$ip)); }
else
{$n_ip=0; $ip_dec=ip2long($ipv4); }
$ip_min=$ip_dec&~$n_ip;
$ip_max=$ip_min+$n_ip;
#Array(2) of Decimal Values Range
return [$ip_min,$ip_max];
#Array(2) of Ipv4 Human Readable Range
return [long2ip($ip_min),long2ip($ip_max)];
#Array(2) of Ipv4 and Subnet Range
return [long2ip($ip_min),long2ip(~$n_ip)];
#Array(2) of Ipv4 and Wildcard Bits
return [long2ip($ip_min),long2ip($n_ip)];
#Integer Number of Ipv4 in Range
return ++$n_ip;
}
To fast check if a given ipv4 is matching a given CIDR you can do it inline like in this example
<?php
$given_cidr='55.55.55.0/24';
$given_ipv4='55.55.55.55';
if(($range=cidr2range($given_cidr)) &&
($check=ip2long($given_ipv4))!==false &&
$check>=$range[0] && $check<=$range[1])
{
echo 'Yes, '.$given_ipv4.' is included in '.$given_cidr;
}
else
{
echo 'No, '.$given_ipv4.' is not included in '.$given_cidr;
}
To get the full range as an array for a given IP (with or without CIDR Routing Prefix) you can use the following code but be carefull because for example 25.25.25.25/16 return an array with 65536 elements and you can easily run out of memory using a smaller Routing Prefix
<?php
$result=cidr2range($ipv4);
for($ip_dec=$result[0];$ip_dec<=$result[1];$ip_dec++)
$full_range[$ip_dec]=long2ip($ip_dec);
print_r($full_range);
To fast check if a given ipv4 is matching a given array of IP (with or without CIDR Routing Prefix)
<?php
#This code is checking if a given ip belongs to googlebot
$given_ipv4='74.125.61.208';
$given_cidr_array=['108.59.93.43/32','108.59.93.40/31','108.59.93.44/30','108.59.93.32/29','108.59.93.48/28','108.59.93.0/27','108.59.93.64/26','108.59.93.192/26','108.59.92.192/27','108.59.92.128/26','108.59.92.96/27','108.59.92.0/27','108.59.94.208/29','108.59.94.192/28','108.59.94.240/28','108.59.94.128/26','108.59.94.16/29','108.59.94.0/28','108.59.94.32/27','108.59.94.64/26','108.59.95.0/24','108.59.88.0/22','108.59.81.0/27','108.59.80.0/24','108.59.82.0/23','108.59.84.0/22','108.170.217.128/28','108.170.217.160/27','108.170.217.192/26','108.170.217.0/25','108.170.216.0/24','108.170.218.0/23','108.170.220.0/22','108.170.208.0/21','108.170.192.0/20','108.170.224.0/19','108.177.0.0/17','104.132.0.0/14','104.154.0.0/15','104.196.0.0/14','107.167.160.0/19','107.178.192.0/18','125.17.82.112/30','125.16.7.72/30','74.125.0.0/16','72.14.192.0/18','77.109.131.208/28','77.67.50.32/27','66.102.0.0/20','66.227.77.144/29','66.249.64.0/19','67.148.177.136/29','64.124.98.104/29','64.71.148.240/29','64.68.64.64/26','64.68.80.0/20','64.41.221.192/28','64.41.146.208/28','64.9.224.0/19','64.233.160.0/19','65.171.1.144/28','65.170.13.0/28','65.167.144.64/28','65.220.13.0/24','65.216.183.0/24','70.32.132.0/23','70.32.128.0/22','70.32.136.0/21','70.32.144.0/20','85.182.250.128/26','85.182.250.0/25','80.239.168.192/26','80.149.20.0/25','61.246.224.136/30','61.246.190.124/30','63.237.119.112/29','63.226.245.56/29','63.158.137.224/29','63.166.17.128/25','63.161.156.0/24','63.88.22.0/23','41.206.188.128/26','12.234.149.240/29','12.216.80.0/24','8.34.217.24/29','8.34.217.0/28','8.34.217.32/27','8.34.217.64/26','8.34.217.128/25','8.34.216.0/24','8.34.218.0/23','8.34.220.0/22','8.34.208.128/29','8.34.208.144/28','8.34.208.160/27','8.34.208.192/26','8.34.208.0/25','8.34.209.0/24','8.34.210.0/23','8.34.212.0/22','8.35.195.128/28','8.35.195.160/27','8.35.195.192/26','8.35.195.0/25','8.35.194.0/24','8.35.192.0/23','8.35.196.0/22','8.35.200.0/21','8.8.8.0/24','8.8.4.0/24','8.6.48.0/21','4.3.2.0/24','23.236.48.0/20','23.251.128.0/19','216.239.32.0/19','216.252.220.0/22','216.136.145.128/27','216.33.229.160/29','216.33.229.144/29','216.34.7.176/28','216.58.192.0/19','216.109.75.80/28','216.74.130.48/28','216.74.153.0/27','217.118.234.96/28','208.46.199.160/29','208.44.48.240/29','208.21.209.0/28','208.184.125.240/28','209.185.108.128/25','209.85.128.0/17','213.200.103.128/26','213.200.99.192/26','213.155.151.128/26','199.192.112.224/29','199.192.112.192/27','199.192.112.128/26','199.192.112.0/25','199.192.113.176/28','199.192.113.128/27','199.192.113.192/26','199.192.113.0/25','199.192.115.80/28','199.192.115.96/27','199.192.115.0/28','199.192.115.128/25','199.192.114.192/26','199.192.114.0/25','199.223.232.0/21','198.108.100.192/28','195.16.45.144/29','192.104.160.0/23','192.158.28.0/22','192.178.0.0/15','206.160.135.240/28','207.223.160.0/20','203.222.167.144/28','173.255.125.72/29','173.255.125.80/28','173.255.125.96/27','173.255.125.0/27','173.255.125.128/25','173.255.124.240/29','173.255.124.232/29','173.255.124.192/27','173.255.124.128/29','173.255.124.144/28','173.255.124.160/27','173.255.124.48/29','173.255.124.32/28','173.255.124.0/27','173.255.124.64/26','173.255.126.0/23','173.255.122.128/26','173.255.122.64/26','173.255.123.0/24','173.255.121.128/26','173.255.121.0/25','173.255.120.0/24','173.255.117.32/27','173.255.117.64/26','173.255.117.128/25','173.255.116.192/27','173.255.116.128/26','173.255.116.0/25','173.255.118.0/23','173.255.112.0/22','173.194.0.0/16','172.102.8.0/21','172.253.0.0/16','172.217.0.0/16','162.216.148.0/22','162.222.176.0/21','180.87.33.64/26','128.177.109.0/26','128.177.119.128/25','128.177.163.0/25','130.211.0.0/16','142.250.0.0/15','146.148.0.0/17'];
echo '<pre>';
$in_range=false;
if (($given_ipv4_dec=ip2long($given_ipv4))!==false)
{
foreach($given_cidr_array as $given_cidr){
if(($range=cidr2range($given_cidr)) &&
$given_ipv4_dec>=$range[0] && $given_ipv4_dec<=$range[1])
{
$in_range=true;
echo $given_ipv4.' matched '.$given_cidr.' ('.join(array_map('long2ip',$range),' - ').")\n";
}
}
}
echo $given_ipv4.' is probably'.($in_range?'':' not').' a Googlebot IP';
To run fast the function don't check input but formally it should be a string matching the following regex
#^(?:((?:0)|(?:2(?:(?:[0-4][0-9])|(?:5[0-5])))|(?:1?[0-9]{1,2}))\.((?:0)|(?:2(?:(?:[0-4][0-9])|(?:5[0-5])))|(?:1?[0-9]{1,2}))\.((?:0)|(?:2(?:(?:[0-4][0-9])|(?:5[0-5])))|(?:1?[0-9]{1,2}))\.((?:0)|(?:2(?:(?:[0-4][0-9])|(?:5[0-5])))|(?:1?[0-9]{1,2}))(?:/((?:(?:0)|(?:3[0-2])|(?:[1-2]?[0-9]))))?)$#
If you want to verify the input before using the function
<?php
if (is_string($ipv4) && preg_match('#^(?:((?:0)|(?:2(?:(?:[0-4][0-9])|(?:5[0-5])))|(?:1?[0-9]{1,2}))\.((?:0)|(?:2(?:(?:[0-4][0-9])|(?:5[0-5])))|(?:1?[0-9]{1,2}))\.((?:0)|(?:2(?:(?:[0-4][0-9])|(?:5[0-5])))|(?:1?[0-9]{1,2}))\.((?:0)|(?:2(?:(?:[0-4][0-9])|(?:5[0-5])))|(?:1?[0-9]{1,2}))(?:/((?:(?:0)|(?:3[0-2])|(?:[1-2]?[0-9]))))?)$#',$ipv4))
{
#This is a valid ipv4 with or without CIDR Routing Prefix
$result=cidr2range($ipv4);
print_r($result);
}
Then the formal answer to your question is the following
<?php
#Requiring cidr2range shown above function
function cidr_match($mixed_ip,$mixed_cidr){
if (!is_array($mixed_ip)){
$string_mode=true;
$mixed_ip=[$mixed_ip=>0];
}
else $mixed_ip=array_fill_keys($mixed_ip,0);
if (!is_array($mixed_cidr)) $mixed_cidr=[$mixed_cidr];
foreach($mixed_ip as $ip => &$result)
foreach($mixed_cidr as $cidr)
{
if(($range=cidr2range($cidr)) &&
($check=ip2long($ip))!==false &&
$check>=$range[0] && $check<=$range[1]){
$result=$cidr;
break;
}
}
$mixed_ip=array_filter($mixed_ip);
return $string_mode?($mixed_ip?true:false):$mixed_ip;
}
print '<pre>';
#Your example
$ips = array('10.2.1.100', '10.2.1.101', '10.5.1.100', '1.2.3.4');
foreach ($ips as $IP) {
if (cidr_match($IP, '10.2.0.0/16') == true) {
print "you're in the 10.2 subnet\n";
}
}
#Also working with IP array and/or CIDR array
#If IP array is given then return an array containing IP (keys) matching CIDR (values)
$result=cidr_match($ips,['20.2.0.0/16','10.2.0.0/15']);
foreach($result as $ip => $cidr){
print "$ip is in the $cidr subnet\n";
}
You can compile your own function using these examples, hope these few lines have helped you…
My technique uses bit to bit matching using subnet and mask.
function cidr_match($ip, $range){
list ($subnet, $bits) = explode('/', $range);
$ip = substr(IP2bin($ip),0,$bits) ;
$subnet = substr(IP2Bin($subnet),0,$bits) ;
return ($ip == $subnet) ;
}
function IP2Bin($ip){
$ipbin = '';
$ips = explode(".",$ip) ;
foreach ($ips as $iptmp){
$ipbin .= sprintf("%08b",$iptmp) ;
}
return $ipbin ;
}
I also needed to test IP's against CIDR masks. I've found a website with excellent explanation and sourcecode which works perfectly well.
The website http://pgregg.com/blog/2009/04/php-algorithms-determining-if-an-ip-is-within-a-specific-range/
Because the website can one day cease to exist, here is the code
<?php
/*
* ip_in_range.php - Function to determine if an IP is located in a
* specific range as specified via several alternative
* formats.
*
* Network ranges can be specified as:
* 1. Wildcard format: 1.2.3.*
* 2. CIDR format: 1.2.3/24 OR 1.2.3.4/255.255.255.0
* 3. Start-End IP format: 1.2.3.0-1.2.3.255
*
* Return value BOOLEAN : ip_in_range($ip, $range);
*
* Copyright 2008: Paul Gregg <pgregg#pgregg.com>
* 10 January 2008
* Version: 1.2
*
* Source website: http://www.pgregg.com/projects/php/ip_in_range/
* Version 1.2
*
* This software is Donationware - if you feel you have benefited from
* the use of this tool then please consider a donation. The value of
* which is entirely left up to your discretion.
* http://www.pgregg.com/donate/
*
* Please do not remove this header, or source attibution from this file.
*/
// decbin32
// In order to simplify working with IP addresses (in binary) and their
// netmasks, it is easier to ensure that the binary strings are padded
// with zeros out to 32 characters - IP addresses are 32 bit numbers
Function decbin32 ($dec) {
return str_pad(decbin($dec), 32, '0', STR_PAD_LEFT);
}
// ip_in_range
// This function takes 2 arguments, an IP address and a "range" in several
// different formats.
// Network ranges can be specified as:
// 1. Wildcard format: 1.2.3.*
// 2. CIDR format: 1.2.3/24 OR 1.2.3.4/255.255.255.0
// 3. Start-End IP format: 1.2.3.0-1.2.3.255
// The function will return true if the supplied IP is within the range.
// Note little validation is done on the range inputs - it expects you to
// use one of the above 3 formats.
Function ip_in_range($ip, $range) {
if (strpos($range, '/') !== false) {
// $range is in IP/NETMASK format
list($range, $netmask) = explode('/', $range, 2);
if (strpos($netmask, '.') !== false) {
// $netmask is a 255.255.0.0 format
$netmask = str_replace('*', '0', $netmask);
$netmask_dec = ip2long($netmask);
return ( (ip2long($ip) & $netmask_dec) == (ip2long($range) & $netmask_dec) );
} else {
// $netmask is a CIDR size block
// fix the range argument
$x = explode('.', $range);
while(count($x)<4) $x[] = '0';
list($a,$b,$c,$d) = $x;
$range = sprintf("%u.%u.%u.%u", empty($a)?'0':$a, empty($b)?'0':$b,empty($c)?'0':$c,empty($d)?'0':$d);
$range_dec = ip2long($range);
$ip_dec = ip2long($ip);
# Strategy 1 - Create the netmask with 'netmask' 1s and then fill it to 32 with 0s
#$netmask_dec = bindec(str_pad('', $netmask, '1') . str_pad('', 32-$netmask, '0'));
# Strategy 2 - Use math to create it
$wildcard_dec = pow(2, (32-$netmask)) - 1;
$netmask_dec = ~ $wildcard_dec;
return (($ip_dec & $netmask_dec) == ($range_dec & $netmask_dec));
}
} else {
// range might be 255.255.*.* or 1.2.3.0-1.2.3.255
if (strpos($range, '*') !==false) { // a.b.*.* format
// Just convert to A-B format by setting * to 0 for A and 255 for B
$lower = str_replace('*', '0', $range);
$upper = str_replace('*', '255', $range);
$range = "$lower-$upper";
}
if (strpos($range, '-')!==false) { // A-B format
list($lower, $upper) = explode('-', $range, 2);
$lower_dec = (float)sprintf("%u",ip2long($lower));
$upper_dec = (float)sprintf("%u",ip2long($upper));
$ip_dec = (float)sprintf("%u",ip2long($ip));
return ( ($ip_dec>=$lower_dec) && ($ip_dec<=$upper_dec) );
}
echo 'Range argument is not in 1.2.3.4/24 or 1.2.3.4/255.255.255.0 format';
return false;
}
}
?>
(I did not develop this; this is developed by Paul Gregg (http://pgregg.com/)
I recently needed to match an IP address to a CIDR mask and came across this article. Below is a slightly different approach based on the ideas above and includes a check on the CIDR input. The function returns false if an incorrect CIDR format is submitted.
I posted this solution for anyone who needs a turn-key function that has been tested.
/**
* Validates subnet specified by CIDR notation.of the form IP address followed by
* a '/' character and a decimal number specifying the length, in bits, of the subnet
* mask or routing prefix (number from 0 to 32).
*
* #param $ip - IP address to check
* #param $cidr - IP address range in CIDR notation for check
* #return bool - true match found otherwise false
*/
function cidr_match($ip, $cidr) {
$outcome = false;
$pattern = '/^(([01]?\d?\d|2[0-4]\d|25[0-5])\.){3}([01]?\d?\d|2[0-4]\d|25[0-5])\/(\d{1}|[0-2]{1}\d{1}|3[0-2])$/';
if (preg_match($pattern, $cidr)){
list($subnet, $mask) = explode('/', $cidr);
if (ip2long($ip) >> (32 - $mask) == ip2long($subnet) >> (32 - $mask)) {
$outcome = true;
}
}
return $outcome;
}
Test data is shown in the image below:
function cidr_match($ipStr, $cidrStr) {
$ip = ip2long($ipStr);
$cidrArr = split('/',$cidrStr);
$maskIP = ip2long($cidrArr[0]);
$maskBits = 32 - $cidrArr[1];
return (($ip>>$maskBits) == ($maskIP>>$maskBits));
}
Just a note, Alnitak's answer works 32/64 bit.
Here is a cooked version of it, for quick spam protection based on country IP lists that you can get everywhere. google for country ip list or country ip block (Have to give one here, really difficult to find it in that sites page navigation:Country ip block generator)
Copy-paste your cidr ip list to string $cidrs. And put this code just before page html, possibly in the header.php file.
Can also be used to filter adsense use in page templates based on country.
This is only a in-the-middle-of-the-night-urgency solution. Sometimes one needs to come up with something like this for a client quickly yesterday, so here it is.
//++++++++++++++++++++++
//COUNTRY SPAM PROTECTOR
//speed: ~5ms # 2000 cidrs
//comments start with #
//++++++++++++++++++++++
$cidrs=
'
#yourcountry
1.3.4.5/21
#mycountry
6.7.8.9/20
';
//$cidrs.="\n".'123.12.12.12/32';//test, your ip
$cidrs_ar=preg_split('/\s+/',$cidrs,-1,PREG_SPLIT_NO_EMPTY);
$ip=#$_SERVER['REMOTE_ADDR'];
$iplong=ip2long($ip);
//var_export($cidrs_ar);var_export($ip);var_export($iplong);
if($iplong)
foreach($cidrs_ar as $cidr)
{
$ar=explode ('/', $cidr);
$netiplong=ip2long($ar[0]);
if($netiplong===false) continue;
$mask=intval(#$ar[1]);
if(!$mask) continue;
$bitmask=-1 <<(32-$mask);
if(($iplong & $bitmask) == ($netiplong & $bitmask))
{
header('Location: http://www.someotherwebsite.com/',true,303);
exit;
}
}
You also can use Net_IPv4 PEAR library.
function cidr_match($ip, $net){
include_once("Net/IPv4.php");
$objIP = new Net_IPv4();
return $objIP->ipInNetwork($ip, $net);
}
Perhaps it is useful to someone.
Convert bit mask into IP mask:
// convert 12 => 255.240.0.0
// ip2long('255.255.255.255') == -1
$ip = long2ip((-1 << (32 - $bit)) & -1);
Convert IP mask into bit mask:
// convert 255.240.0.0 => 12
// is valid IP
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) === false) {
throw new \InvalidArgumentException(sprintf('Invalid IP "%s".', $ip));
}
// convert decimal to binary
$mask = '';
foreach (explode('.', $ip) as $octet) {
$mask .= str_pad(decbin($octet), 8, '0', STR_PAD_LEFT);
}
// check mask
if (strpos('01', $mask) !== false) {
// valid 11111111111111111111111100000000 -> 255.255.255.0
// invalid 11111111111111111111111100000001 -> 255.255.255.1
throw new \InvalidArgumentException(sprintf('IP mask "%s" is not valid.', $ip));
}
$bit = substr_count($mask, '1'); // bit mask
I want to have you look at my few lines. The examples that people suggested before me don't seem to work. One reason being, as far as I understand it, is that CIDR mask bits are binary numbers, so the bit shift must be done on a binary number. I have tried converting the long IP's into binaries, but ran into a max binary number limit.
OK, here my few lines ... I await your comments.
function cidr_match($ipStr, $cidrStr) {
$ipStr = explode('.', $ipStr);
foreach ($ipStr as $key => $val) {
$ipStr[$key] = str_pad(decbin($val), 8, '0', STR_PAD_LEFT);
}
$ip = '';
foreach ($ipStr as $binval) {
$ip = $ip . $binval;
}
$cidrArr = explode('/',$cidrStr);
$maskIP = explode('.', $cidrArr[0]);
foreach ($maskIP as $key => $val) {
$maskIP[$key] = str_pad(decbin($val), 8, '0', STR_PAD_LEFT);
}
$maskIP = '';
foreach ($ipStr as $binval) {
$maskIP = $maskIP . $binval;
}
$maskBits = 32 - $cidrArr[1];
return (($ip>>$maskBits) == ($maskIP>>$maskBits));
}

Compare submit IP with Database IP Range [duplicate]

I have an IP address and I'm given two other IP addresses which together creates an IP range. I want to check if the first IP address is within this range. How can i find that out in PHP?
With ip2long() it's easy to convert your addresses to numbers. After this, you just have to check if the number is in range:
if ($ip <= $high_ip && $low_ip <= $ip) {
echo "in range";
}
This website offers a great guide and code to do this (which was the first result of a Google search for this question):
<?php
/*
* ip_in_range.php - Function to determine if an IP is located in a
* specific range as specified via several alternative
* formats.
*
* Network ranges can be specified as:
* 1. Wildcard format: 1.2.3.*
* 2. CIDR format: 1.2.3/24 OR 1.2.3.4/255.255.255.0
* 3. Start-End IP format: 1.2.3.0-1.2.3.255
*
* Return value BOOLEAN : ip_in_range($ip, $range);
*
* Copyright 2008: Paul Gregg <pgregg#pgregg.com>
* 10 January 2008
* Version: 1.2
*
* Source website: http://www.pgregg.com/projects/php/ip_in_range/
* Version 1.2
*
* This software is Donationware - if you feel you have benefited from
* the use of this tool then please consider a donation. The value of
* which is entirely left up to your discretion.
* http://www.pgregg.com/donate/
*
* Please do not remove this header, or source attibution from this file.
*/
// decbin32
// In order to simplify working with IP addresses (in binary) and their
// netmasks, it is easier to ensure that the binary strings are padded
// with zeros out to 32 characters - IP addresses are 32 bit numbers
Function decbin32 ($dec) {
return str_pad(decbin($dec), 32, '0', STR_PAD_LEFT);
}
// ip_in_range
// This function takes 2 arguments, an IP address and a "range" in several
// different formats.
// Network ranges can be specified as:
// 1. Wildcard format: 1.2.3.*
// 2. CIDR format: 1.2.3/24 OR 1.2.3.4/255.255.255.0
// 3. Start-End IP format: 1.2.3.0-1.2.3.255
// The function will return true if the supplied IP is within the range.
// Note little validation is done on the range inputs - it expects you to
// use one of the above 3 formats.
Function ip_in_range($ip, $range) {
if (strpos($range, '/') !== false) {
// $range is in IP/NETMASK format
list($range, $netmask) = explode('/', $range, 2);
if (strpos($netmask, '.') !== false) {
// $netmask is a 255.255.0.0 format
$netmask = str_replace('*', '0', $netmask);
$netmask_dec = ip2long($netmask);
return ( (ip2long($ip) & $netmask_dec) == (ip2long($range) & $netmask_dec) );
} else {
// $netmask is a CIDR size block
// fix the range argument
$x = explode('.', $range);
while(count($x)<4) $x[] = '0';
list($a,$b,$c,$d) = $x;
$range = sprintf("%u.%u.%u.%u", empty($a)?'0':$a, empty($b)?'0':$b,empty($c)?'0':$c,empty($d)?'0':$d);
$range_dec = ip2long($range);
$ip_dec = ip2long($ip);
# Strategy 1 - Create the netmask with 'netmask' 1s and then fill it to 32 with 0s
#$netmask_dec = bindec(str_pad('', $netmask, '1') . str_pad('', 32-$netmask, '0'));
# Strategy 2 - Use math to create it
$wildcard_dec = pow(2, (32-$netmask)) - 1;
$netmask_dec = ~ $wildcard_dec;
return (($ip_dec & $netmask_dec) == ($range_dec & $netmask_dec));
}
} else {
// range might be 255.255.*.* or 1.2.3.0-1.2.3.255
if (strpos($range, '*') !==false) { // a.b.*.* format
// Just convert to A-B format by setting * to 0 for A and 255 for B
$lower = str_replace('*', '0', $range);
$upper = str_replace('*', '255', $range);
$range = "$lower-$upper";
}
if (strpos($range, '-')!==false) { // A-B format
list($lower, $upper) = explode('-', $range, 2);
$lower_dec = (float)sprintf("%u",ip2long($lower));
$upper_dec = (float)sprintf("%u",ip2long($upper));
$ip_dec = (float)sprintf("%u",ip2long($ip));
return ( ($ip_dec>=$lower_dec) && ($ip_dec<=$upper_dec) );
}
echo 'Range argument is not in 1.2.3.4/24 or 1.2.3.4/255.255.255.0 format';
return false;
}
}
?>
I found this little gist
which has simpler/shorter solution than already mentioned here.
Second argument (range) can either be a static ip such as 127.0.0.1 or a range like 127.0.0.0/24.
/**
* Check if a given ip is in a network
* #param string $ip IP to check in IPV4 format eg. 127.0.0.1
* #param string $range IP/CIDR netmask eg. 127.0.0.0/24, also 127.0.0.1 is accepted and /32 assumed
* #return boolean true if the ip is in this range / false if not.
*/
function ip_in_range( $ip, $range ) {
if ( strpos( $range, '/' ) === false ) {
$range .= '/32';
}
// $range is in IP/CIDR format eg 127.0.0.1/24
list( $range, $netmask ) = explode( '/', $range, 2 );
$range_decimal = ip2long( $range );
$ip_decimal = ip2long( $ip );
$wildcard_decimal = pow( 2, ( 32 - $netmask ) ) - 1;
$netmask_decimal = ~ $wildcard_decimal;
return ( ( $ip_decimal & $netmask_decimal ) == ( $range_decimal & $netmask_decimal ) );
}
if(version_compare($low_ip, $ip) + version_compare($ip, $high_ip) === -2) {
echo "in range";
}
Comparing in range (Including Ipv6 support)
The following two functions were introduced in PHP 5.1.0, inet_pton and inet_pton. Their purpose is to convert human readable IP addresses into their packed in_addr representation. Since the result is not pure binary, we need to use the unpack function in order to apply bitwise operators.
Both functions support IPv6 as well as IPv4. The only difference is how you unpack the address from the results. With IPv6, you will unpack with contents with A16, and with IPv4, you will unpack with A4.
To put the previous in a perspective here is a little sample output to help clarify:
// Our Example IP's
$ip4= "10.22.99.129";
$ip6= "fe80:1:2:3:a:bad:1dea:dad";
// ip2long examples
var_dump( ip2long($ip4) ); // int(169239425)
var_dump( ip2long($ip6) ); // bool(false)
// inet_pton examples
var_dump( inet_pton( $ip4 ) ); // string(4)
var_dump( inet_pton( $ip6 ) ); // string(16)
We demonstrate above that the inet_* family supports both IPv6 and v4. Our next step will be to translate the packed result into an unpacked variable.
// Unpacking and Packing
$_u4 = current( unpack( "A4", inet_pton( $ip4 ) ) );
var_dump( inet_ntop( pack( "A4", $_u4 ) ) ); // string(12) "10.22.99.129"
$_u6 = current( unpack( "A16", inet_pton( $ip6 ) ) );
var_dump( inet_ntop( pack( "A16", $_u6 ) ) ); //string(25) "fe80:1:2:3:a:bad:1dea:dad"
Note : The current function returns the first index of an array. It is equivelant to saying $array[0].
After the unpacking and packing, we can see we achieved the same result as input. This is a simple proof of concept to ensure we are not losing any data.
Finally use,
if ($ip <= $high_ip && $low_ip <= $ip) {
echo "in range";
}
Reference: php.net
Use the excellent rlanvin/php-ip which supports IPv4 and IPv6 (via the GMP extension):
use PhpIP\IPBlock;
$block = IPBlock::create('10.0.0.0/24');
$block->contains('10.0.0.42'); // true
See their docs for more examples.
I would always suggest ip2long, but sometimes you need to check networks and etc. I've built in the past a IPv4 Networking class, which can be found here on HighOnPHP.
The nice thing about working with IP addressing is it's flexibility especially when using BITWISE operators. AND'ing, OR'ing and BitShifting will work like a charm.
This is old post but there is one good solution on the GitHub what I made.
$ip_in_range = is_ip_in_range('54.208.101.55', array(
'50.16.241.113' => '50.16.241.117',
'54.208.100.253' => '54.208.102.37'
));
This function will return matched IP or boolean false on the not match.
Here is the function:
// https://github.com/CreativForm/PHP-Solutions/blob/master/function.ip.in.range.php
function is_ip_in_range( $ip, $range ){
if(!is_array($range)) return false;
// Let's search first single one
ksort($range);
// We need numerical representation of the IP
$ip2long = ip2long($ip);
// Non IP values needs to be removed
if($ip2long !== false)
{
// Let's loop
foreach($range as $start => $end)
{
// Convert to numerical representations as well
$end = ip2long($end);
$start = ip2long($start);
$is_key = ($start === false);
// Remove bad one
if($end === false) continue;
// Here we looking for single IP does match
if(is_numeric($start) && $is_key && $end === $ip2long)
{
return $ip;
}
else
{
// And here we have check is in the range
if(!$is_key && $ip2long >= $start && $ip2long <= $end)
{
return $ip;
}
}
}
}
// Ok, it's not finded
return false;
}
Btw, in case you need to check multiple ranges at once you can add few rows to the code in order to pass array of ranges. The second argument can be an array or string:
public static function ip_in_range($ip, $range) {
if (is_array($range)) {
foreach ($range as $r) {
return self::ip_in_range($ip, $r);
}
} else {
if ($ip === $range) { // in case you have passed a static IP, not a range
return TRUE;
}
}
// The rest of the code follows here..
// .........
}
Here is my approach of the subject.
function validateIP($whitelist, $ip) {
// e.g ::1
if($whitelist == $ip) {
return true;
}
// split each part of the IP address and set it to an array
$validated1 = explode(".", $whitelist);
$validated2 = explode(".", $ip);
// check array index to avoid undefined index errors
if(count($validated1) >= 3 && count($validated2) == 4) {
// check that each value of the array is identical with our whitelisted IP,
// except from the last part which doesn't matter
if($validated1[0] == $validated2[0] && $validated1[1] == $validated2[1] && $validated1[2] == $validated2[2]) {
return true;
}
}
return false;
}
I used this for my client:
$clientIpArray = explode(".", $clientIp);
$fromArray = explode(".", $from);
$toArray = explode(".", $to);
if( ((str_pad($clientIpArray[0], 3, "0", STR_PAD_LEFT) >= str_pad($fromArray[0], 3, "0", STR_PAD_LEFT)) && (str_pad($clientIpArray[0], 3, "0", STR_PAD_LEFT) <= str_pad($toArray[0], 3, "0", STR_PAD_LEFT)))
&&((str_pad($clientIpArray[1], 3, "0", STR_PAD_LEFT) >= str_pad($fromArray[1], 3, "0", STR_PAD_LEFT)) && (str_pad($clientIpArray[1], 3, "0", STR_PAD_LEFT) <= str_pad($toArray[1], 3, "0", STR_PAD_LEFT)))
&&((str_pad($clientIpArray[2], 3, "0", STR_PAD_LEFT) >= str_pad($fromArray[2], 3, "0", STR_PAD_LEFT)) && (str_pad($clientIpArray[2], 3, "0", STR_PAD_LEFT) <= str_pad($toArray[2], 3, "0", STR_PAD_LEFT)))
&&((str_pad($clientIpArray[3], 3, "0", STR_PAD_LEFT) >= str_pad($fromArray[3], 3, "0", STR_PAD_LEFT)) && (str_pad($clientIpArray[3], 3, "0", STR_PAD_LEFT) <= str_pad($toArray[3], 3, "0", STR_PAD_LEFT)))){
echo "IP within range";
}
For example, let's take this example:
$clientIp = "120.02.3.112";
$from = "1.02.1.112";
$to = "120.02.20.112";
As you can understand this IP is within the range. If you try to compare it as it is it won't work.
My solution is to divide the IP into elements, so for example the generated arrays would be:
$clientIpArray = ["120","02","3","112"];
$fromArray = ["1","02","1","112"];
$toArray = ["120","02","20","112"];
We have 4 elements to compare, here I used str_pad function to generate a 3 characters string from each element, so instead of checking if "3" is between "1" and "20" (which is not true) we will check if "003" is between "001" and "020" (which is correct).

Find the network distance between two IPv4 addresses (not geographic distance)

Given an IPv4 address (needle) and an unsorted array of IPv4 addresses (haystack), how could I programmatically determine which single address in the given haystack is the closest (network-wise, NOT geographically) to the needle?
Since I don't have access to the netmask of every address, the solution should ignore netmasks and traceroute alike options.
All sorts of addresses are used, by which I mean: private, reserved, broadcast, lan and wan.
Any help in the form of theory, pseudo-code, python, php or perl is welcome.
The question Getting IP address list between two IP addresses is roughly similar, but it does quite cut it.
I'm still not quite sure what you're asking, but based on your comment
#PeterGibson by closest I meant, 192.168.1.101 is closer to
192.168.56.1 than to 172.30.130.66 . And 192.168.1.254 is closer to 192.168.1.240 than to 192.168.2.1
You could try the following python code for a distance function:
import socket
def dist(a, b):
def to_num(addr):
# parse the address string into integer quads
quads = map(ord, socket.inet_aton(addr))
# spread the quads out
return reduce(lambda x,y: x * 0x10000 + y, quads)
return abs(to_num(a) - to_num(b))
The numbers returned are fairly arbitrary, but should suffice for basic needs. I'm still not sure what it should do with broadcast addresses etc though.
Some examples:
>>> dist('192.168.1.254', '192.168.1.240')
14L
>>> dist('192.168.1.254', '192.168.2.1')
65283L
>>> dist('192.168.1.101', '192.168.56.1')
3604380L
>>> dist('192.168.1.101', '172.30.130.66')
5630092231245859L
I'd start by dividing array into groups, considering reserved ip-addresses (http://en.wikipedia.org/wiki/Reserved_IP_addresses#Reserved_IPv4_addresses). Then i'd separate them by regions. Maybe some tree structure would suffice: root nodes are top level regions and then, closing to leafs it goes for smaller regions.
Not the prettiest of the approaches, but since very high performance is not required, it does the job.
<?php
$ip = isset($argv[1]) ? $argv[1] : '192.168.1.254';
printf("Input => %s => %u\n", $ip, ip2long($ip));
var_dump(
net_distance(
$ip,
array(
'87.86.85.84',
'173.194.41.170',
'192.168.1.150',
'192.168.1.245',
'192.168.2.2',
'192.168.56.50',
)
)
);
// The problem assumes that:
// a) any local (127) and lan (10,172,192) IPs are closer to each other than to any wan IP.
// b) any wan IP is closer to local and lan IPs than to other wan IP.
function net_distance($src_ip, $list)
{
if (in_array($src_ip, $list)) {
return $src_ip; // exact match
}
list($a0, $b0, $c0, $d0) = explode('.', $src_ip, 4);
$triples = array();
$doubles = array();
$singles = array();
foreach($list as $ip) {
list($a1, $b1, $c1, $d1) = explode('.', $ip, 4);
if ($a0 == $a1 && $b0 == $b1 && $c0 == $c1) { // match A.B.C.x
$triples[] = $ip;
}
elseif ($a0 == $a1 && $b0 == $b1) { // match A.B.x.y
$doubles[] = $ip;
}
elseif ($a0 == $a1 || (in_array($a0, array(127, 10, 172, 192)) && in_array($a1, array(127, 10, 172, 192)))) {
$singles[] = $ip; // match A.x.y.z or both As are *likely* to be lan addresses
}
}
if (count($triples) > 0) {
$list = $triples;
}
elseif (count($doubles) > 0) {
$list = $doubles;
}
elseif (count($singles) > 0) {
$list = $singles;
}
$min = PHP_INT_MAX;
$rtn = false;
$l1 = ip2long($src_ip);
foreach($list as $ip)
{
$l2 = ip2long($ip);
$d = ($l1 > $l2) ? $l1 - $l2 : $l2 - $l1;
// echo "\t" . str_pad($ip, 15, ' ', STR_PAD_RIGHT) . " => $l2 => $d\n";
if ($min > $d) {
$rtn = $ip;
$min = $d;
}
}
return $rtn;
}

How to check an IP address is within a range of two IPs in PHP?

I have an IP address and I'm given two other IP addresses which together creates an IP range. I want to check if the first IP address is within this range. How can i find that out in PHP?
With ip2long() it's easy to convert your addresses to numbers. After this, you just have to check if the number is in range:
if ($ip <= $high_ip && $low_ip <= $ip) {
echo "in range";
}
This website offers a great guide and code to do this (which was the first result of a Google search for this question):
<?php
/*
* ip_in_range.php - Function to determine if an IP is located in a
* specific range as specified via several alternative
* formats.
*
* Network ranges can be specified as:
* 1. Wildcard format: 1.2.3.*
* 2. CIDR format: 1.2.3/24 OR 1.2.3.4/255.255.255.0
* 3. Start-End IP format: 1.2.3.0-1.2.3.255
*
* Return value BOOLEAN : ip_in_range($ip, $range);
*
* Copyright 2008: Paul Gregg <pgregg#pgregg.com>
* 10 January 2008
* Version: 1.2
*
* Source website: http://www.pgregg.com/projects/php/ip_in_range/
* Version 1.2
*
* This software is Donationware - if you feel you have benefited from
* the use of this tool then please consider a donation. The value of
* which is entirely left up to your discretion.
* http://www.pgregg.com/donate/
*
* Please do not remove this header, or source attibution from this file.
*/
// decbin32
// In order to simplify working with IP addresses (in binary) and their
// netmasks, it is easier to ensure that the binary strings are padded
// with zeros out to 32 characters - IP addresses are 32 bit numbers
Function decbin32 ($dec) {
return str_pad(decbin($dec), 32, '0', STR_PAD_LEFT);
}
// ip_in_range
// This function takes 2 arguments, an IP address and a "range" in several
// different formats.
// Network ranges can be specified as:
// 1. Wildcard format: 1.2.3.*
// 2. CIDR format: 1.2.3/24 OR 1.2.3.4/255.255.255.0
// 3. Start-End IP format: 1.2.3.0-1.2.3.255
// The function will return true if the supplied IP is within the range.
// Note little validation is done on the range inputs - it expects you to
// use one of the above 3 formats.
Function ip_in_range($ip, $range) {
if (strpos($range, '/') !== false) {
// $range is in IP/NETMASK format
list($range, $netmask) = explode('/', $range, 2);
if (strpos($netmask, '.') !== false) {
// $netmask is a 255.255.0.0 format
$netmask = str_replace('*', '0', $netmask);
$netmask_dec = ip2long($netmask);
return ( (ip2long($ip) & $netmask_dec) == (ip2long($range) & $netmask_dec) );
} else {
// $netmask is a CIDR size block
// fix the range argument
$x = explode('.', $range);
while(count($x)<4) $x[] = '0';
list($a,$b,$c,$d) = $x;
$range = sprintf("%u.%u.%u.%u", empty($a)?'0':$a, empty($b)?'0':$b,empty($c)?'0':$c,empty($d)?'0':$d);
$range_dec = ip2long($range);
$ip_dec = ip2long($ip);
# Strategy 1 - Create the netmask with 'netmask' 1s and then fill it to 32 with 0s
#$netmask_dec = bindec(str_pad('', $netmask, '1') . str_pad('', 32-$netmask, '0'));
# Strategy 2 - Use math to create it
$wildcard_dec = pow(2, (32-$netmask)) - 1;
$netmask_dec = ~ $wildcard_dec;
return (($ip_dec & $netmask_dec) == ($range_dec & $netmask_dec));
}
} else {
// range might be 255.255.*.* or 1.2.3.0-1.2.3.255
if (strpos($range, '*') !==false) { // a.b.*.* format
// Just convert to A-B format by setting * to 0 for A and 255 for B
$lower = str_replace('*', '0', $range);
$upper = str_replace('*', '255', $range);
$range = "$lower-$upper";
}
if (strpos($range, '-')!==false) { // A-B format
list($lower, $upper) = explode('-', $range, 2);
$lower_dec = (float)sprintf("%u",ip2long($lower));
$upper_dec = (float)sprintf("%u",ip2long($upper));
$ip_dec = (float)sprintf("%u",ip2long($ip));
return ( ($ip_dec>=$lower_dec) && ($ip_dec<=$upper_dec) );
}
echo 'Range argument is not in 1.2.3.4/24 or 1.2.3.4/255.255.255.0 format';
return false;
}
}
?>
I found this little gist
which has simpler/shorter solution than already mentioned here.
Second argument (range) can either be a static ip such as 127.0.0.1 or a range like 127.0.0.0/24.
/**
* Check if a given ip is in a network
* #param string $ip IP to check in IPV4 format eg. 127.0.0.1
* #param string $range IP/CIDR netmask eg. 127.0.0.0/24, also 127.0.0.1 is accepted and /32 assumed
* #return boolean true if the ip is in this range / false if not.
*/
function ip_in_range( $ip, $range ) {
if ( strpos( $range, '/' ) === false ) {
$range .= '/32';
}
// $range is in IP/CIDR format eg 127.0.0.1/24
list( $range, $netmask ) = explode( '/', $range, 2 );
$range_decimal = ip2long( $range );
$ip_decimal = ip2long( $ip );
$wildcard_decimal = pow( 2, ( 32 - $netmask ) ) - 1;
$netmask_decimal = ~ $wildcard_decimal;
return ( ( $ip_decimal & $netmask_decimal ) == ( $range_decimal & $netmask_decimal ) );
}
if(version_compare($low_ip, $ip) + version_compare($ip, $high_ip) === -2) {
echo "in range";
}
Comparing in range (Including Ipv6 support)
The following two functions were introduced in PHP 5.1.0, inet_pton and inet_pton. Their purpose is to convert human readable IP addresses into their packed in_addr representation. Since the result is not pure binary, we need to use the unpack function in order to apply bitwise operators.
Both functions support IPv6 as well as IPv4. The only difference is how you unpack the address from the results. With IPv6, you will unpack with contents with A16, and with IPv4, you will unpack with A4.
To put the previous in a perspective here is a little sample output to help clarify:
// Our Example IP's
$ip4= "10.22.99.129";
$ip6= "fe80:1:2:3:a:bad:1dea:dad";
// ip2long examples
var_dump( ip2long($ip4) ); // int(169239425)
var_dump( ip2long($ip6) ); // bool(false)
// inet_pton examples
var_dump( inet_pton( $ip4 ) ); // string(4)
var_dump( inet_pton( $ip6 ) ); // string(16)
We demonstrate above that the inet_* family supports both IPv6 and v4. Our next step will be to translate the packed result into an unpacked variable.
// Unpacking and Packing
$_u4 = current( unpack( "A4", inet_pton( $ip4 ) ) );
var_dump( inet_ntop( pack( "A4", $_u4 ) ) ); // string(12) "10.22.99.129"
$_u6 = current( unpack( "A16", inet_pton( $ip6 ) ) );
var_dump( inet_ntop( pack( "A16", $_u6 ) ) ); //string(25) "fe80:1:2:3:a:bad:1dea:dad"
Note : The current function returns the first index of an array. It is equivelant to saying $array[0].
After the unpacking and packing, we can see we achieved the same result as input. This is a simple proof of concept to ensure we are not losing any data.
Finally use,
if ($ip <= $high_ip && $low_ip <= $ip) {
echo "in range";
}
Reference: php.net
Use the excellent rlanvin/php-ip which supports IPv4 and IPv6 (via the GMP extension):
use PhpIP\IPBlock;
$block = IPBlock::create('10.0.0.0/24');
$block->contains('10.0.0.42'); // true
See their docs for more examples.
I would always suggest ip2long, but sometimes you need to check networks and etc. I've built in the past a IPv4 Networking class, which can be found here on HighOnPHP.
The nice thing about working with IP addressing is it's flexibility especially when using BITWISE operators. AND'ing, OR'ing and BitShifting will work like a charm.
This is old post but there is one good solution on the GitHub what I made.
$ip_in_range = is_ip_in_range('54.208.101.55', array(
'50.16.241.113' => '50.16.241.117',
'54.208.100.253' => '54.208.102.37'
));
This function will return matched IP or boolean false on the not match.
Here is the function:
// https://github.com/CreativForm/PHP-Solutions/blob/master/function.ip.in.range.php
function is_ip_in_range( $ip, $range ){
if(!is_array($range)) return false;
// Let's search first single one
ksort($range);
// We need numerical representation of the IP
$ip2long = ip2long($ip);
// Non IP values needs to be removed
if($ip2long !== false)
{
// Let's loop
foreach($range as $start => $end)
{
// Convert to numerical representations as well
$end = ip2long($end);
$start = ip2long($start);
$is_key = ($start === false);
// Remove bad one
if($end === false) continue;
// Here we looking for single IP does match
if(is_numeric($start) && $is_key && $end === $ip2long)
{
return $ip;
}
else
{
// And here we have check is in the range
if(!$is_key && $ip2long >= $start && $ip2long <= $end)
{
return $ip;
}
}
}
}
// Ok, it's not finded
return false;
}
Btw, in case you need to check multiple ranges at once you can add few rows to the code in order to pass array of ranges. The second argument can be an array or string:
public static function ip_in_range($ip, $range) {
if (is_array($range)) {
foreach ($range as $r) {
return self::ip_in_range($ip, $r);
}
} else {
if ($ip === $range) { // in case you have passed a static IP, not a range
return TRUE;
}
}
// The rest of the code follows here..
// .........
}
Here is my approach of the subject.
function validateIP($whitelist, $ip) {
// e.g ::1
if($whitelist == $ip) {
return true;
}
// split each part of the IP address and set it to an array
$validated1 = explode(".", $whitelist);
$validated2 = explode(".", $ip);
// check array index to avoid undefined index errors
if(count($validated1) >= 3 && count($validated2) == 4) {
// check that each value of the array is identical with our whitelisted IP,
// except from the last part which doesn't matter
if($validated1[0] == $validated2[0] && $validated1[1] == $validated2[1] && $validated1[2] == $validated2[2]) {
return true;
}
}
return false;
}
I used this for my client:
$clientIpArray = explode(".", $clientIp);
$fromArray = explode(".", $from);
$toArray = explode(".", $to);
if( ((str_pad($clientIpArray[0], 3, "0", STR_PAD_LEFT) >= str_pad($fromArray[0], 3, "0", STR_PAD_LEFT)) && (str_pad($clientIpArray[0], 3, "0", STR_PAD_LEFT) <= str_pad($toArray[0], 3, "0", STR_PAD_LEFT)))
&&((str_pad($clientIpArray[1], 3, "0", STR_PAD_LEFT) >= str_pad($fromArray[1], 3, "0", STR_PAD_LEFT)) && (str_pad($clientIpArray[1], 3, "0", STR_PAD_LEFT) <= str_pad($toArray[1], 3, "0", STR_PAD_LEFT)))
&&((str_pad($clientIpArray[2], 3, "0", STR_PAD_LEFT) >= str_pad($fromArray[2], 3, "0", STR_PAD_LEFT)) && (str_pad($clientIpArray[2], 3, "0", STR_PAD_LEFT) <= str_pad($toArray[2], 3, "0", STR_PAD_LEFT)))
&&((str_pad($clientIpArray[3], 3, "0", STR_PAD_LEFT) >= str_pad($fromArray[3], 3, "0", STR_PAD_LEFT)) && (str_pad($clientIpArray[3], 3, "0", STR_PAD_LEFT) <= str_pad($toArray[3], 3, "0", STR_PAD_LEFT)))){
echo "IP within range";
}
For example, let's take this example:
$clientIp = "120.02.3.112";
$from = "1.02.1.112";
$to = "120.02.20.112";
As you can understand this IP is within the range. If you try to compare it as it is it won't work.
My solution is to divide the IP into elements, so for example the generated arrays would be:
$clientIpArray = ["120","02","3","112"];
$fromArray = ["1","02","1","112"];
$toArray = ["120","02","20","112"];
We have 4 elements to compare, here I used str_pad function to generate a 3 characters string from each element, so instead of checking if "3" is between "1" and "20" (which is not true) we will check if "003" is between "001" and "020" (which is correct).

An optimized method to compare IP addresses with wildcards in PHP?

Anyone know of an effective and secure method to see if this input:
$_SERVER['REMOTE_ADDR']
matches against something similar to this array of inconsistent filters (note that 200.100.*.* could be expressed as just 200.100.*) with wildcards indicated by *'s:
array(
'192.168.1.*',
'192.168.2.1*',
'10.0.0.*',
'200.100.*.*',
'300.200.*',
)
Update
Thoughts?
foreach($instanceSettings['accessControl']['allowedIpV4Addresses'] as $ipV4Address) {
echo 'Now checking against '.$ipV4Address.'.';
// Compare each octet
$ipV4AddressOctets = String::explode('.', $ipV4Address);
$remoteIpV4AddressOctets = String::explode('.', $_SERVER['REMOTE_ADDR']);
$remoteIpV4AddressIsAllowed = true;
for($i = 0; $i < Arr::size($ipV4AddressOctets); $i++) {
echo 'Comparing '.$ipV4AddressOctets[$i].' against '.$remoteIpV4AddressOctets[$i].'.';
if($ipV4AddressOctets[$i] != $remoteIpV4AddressOctets[$i] && $ipV4AddressOctets[$i] != '*') {
echo 'No match.';
$remoteIpV4AddressIsAllowed = false;
break;
}
}
// Get out of the foreach if we've found a match
if($remoteIpV4AddressIsAllowed) {
break;
}
}
I haven't bench-marked this, but I would opt to use the method that networking hardware/software uses...
Replace any * with 0 and 255.
Convert the IPs to integers
So if 255.255.255.* becomes 255.255.255.0 and 255.255.255.255
Then do ip2long function on these two ips.
Then you can convert the given ip into long ip. for example 255.255.50.51 into long ip.
Then you can compare whether the long ip for this given ip is between the converted long ips in the blacklist. If it is then it is not allowed else it is.
$ips = array("ip1", "ip2");
foreach($ips as $ip){
$ip1 = str_replace("*", "0", $ip);
$ip2 = str_replace("*", "255", $ip);
$ip1 = ip2long($ip1);
$ip2 = ip2long($ip2);
$givenip = $_GET["ip"];
$givenip = ip2long($givenip);
if($givenip >= $ip1 && $ip <= $givenip){
echo "blacklist ip hit between {$ip1} and {$ip2} on {$ip}";
}
}
Remove the asterisks and just do:
$ips = array('192.168.1.', '10.0.0.');
foreach ($ips as $ip) {
if (strpos($_SERVER['REMOTE_ADDR'], $ip) === 0) {
// match
}
}
This one allows all cases in the question plus short masks with no asterisks like 123.123.
/**
* Checks given IP against array of masks like 123.123.123.123, 123.123.*.101, 123.123., 123.123.1*.*
*
* #param $ip
* #param $masks
* #return bool
*/
public static function checkIp($ip, $masks)
{
if (in_array($ip, $masks)) {
return true; // Simple match
} else {
foreach ($masks as $mask) {
if (substr($mask, -1) == '.' AND substr($ip, 0, strlen($mask)) == $mask) {
return true; // Case for 123.123. mask
}
if (strpos($mask, '*') === false) {
continue; // No simple matching and no wildcard in the mask, leaves no chance to match
}
// Breaking into triads
$maskParts = explode('.', $mask);
$ipParts = explode('.', $ip);
foreach ($maskParts as $key => $maskPart) {
if ($maskPart == '*') {
continue; // This triad is matching, continue with next triad
} elseif (strpos($maskPart, '*') !== false) {
// Case like 1*, 1*2, *1
// Let's use regexp for this
$regExp = str_replace('*', '\d{0,3}', $maskPart);
if (preg_match('/^' . $regExp . '$/', $ipParts[$key])) {
continue; // Matching, go to check next triad
} else {
continue 2; // Not matching, Go to check next mask
}
} else {
if ($maskPart != $ipParts[$key]) {
continue 2; // If triad has no wildcard and not matching, check next mask
}
// otherwise just continue
}
}
// We checked all triads and all matched, hence this mask is matching
return true;
}
// We went through all masks and none has matched.
return false;
}
}
Why not just use a regular expression?
preg_match("((192\\.168\\.1)|(10\\.0\\.0)|(127\\.0\\.0)\\.[012]\\d{0,2}|(\\:\\:1))",$_SERVER['REMOTE_ADDR'])
Just for fun, I'm going to over-engineer this. Well, unless you have a fairly long list to match against.
Assuming you're only using wildcards to mean "I don't care about this octet", then you can parse each entry in your array into four values (one per octet). Say that you use -1 to mean wildcard, 0–255 means to match that value exactly. (If you need better performance than O(n), where n is the size of the match list, then there are better data structures you can use here—a trie, for example.) Call this array L. Of course, you only need do this once—not per-request.
You can then parse the remote address the same way (except without wildcards). You can also catch REMOTE_ADDR not being in the expected format here It now becomes fairly trivial to check matches:
has_match(ip) =
for n in [0 … L.length)
if (-1 == L.n.0 || L.n.0 = ip.0) && (-1 == L.n.1 || L.n.1 == ip.1) && …
return true
return false
(That's pseudo-code, of course)
This one is based on preg_match(), you provide the pattern and the subject to test it against.
/**
* ip_match("172.30.20.*", "172.30.20.162"); // true
* ip_match("172.30.20", "172.30.20.162"); // true; works if incomplete
* ip_match("172.30.*.12", "172.30.20.12"); // true
* ip_match("172.30.*.12", "172.30.20.11"); // false
* ip_match("172.30.20.*", "172.30.20.*"); // true; wildcards in the subject will match with wildcards in the pattern
* ip_match("172.30.20.12", "172.30.*.*"); // false
*
* #param $pattern The pattern to test against as a string
* #param $subject The input string
*
* #return bool
*
*/
function ip_match($pattern, $subject) {
$pattern = explode(".", trim($pattern, "."));
$pattern = array_pad($pattern, 4, "*");
$subject = explode(".", trim($subject, "."));
$subject = array_pad($subject, 4, "1");
foreach ($pattern as $i => $octet) {
if ($octet != "*" && $subject[$i] != $octet) {
return false;
}
}
return true;
}
<?php
function ipArrCheck($allowedIPArr = [], $IP = '')
{
$IP_ARR = explode('.', $IP);
$resultArr = [];
foreach ($IP_ARR as $IPkey => $IPvalue) {
foreach ($allowedIPArr as $IPArrKey => $IPArrValue) {
$checkIPArr = explode('.', $IPArrValue);
$resultArr[$IPArrKey][$IPkey] = $checkIPArr[$IPkey] == $IP_ARR[$IPkey] || $checkIPArr[$IPkey] == '*';
}
}
foreach ($resultArr as $value) {
if (count(array_unique($value)) == 1 && current($value)) {
return TRUE;
}
}
return FALSE;
}
$MY_IP = '192.168.52.10';
$ALLOWED_IP_ARR = ['127.0.0.1', '192.168.*.*'];
var_dump(ipArrCheck($ALLOWED_IP_ARR, $MY_IP));

Categories