Check whether or not a CIDR subnet contains an IP address
Asked Answered
S

16

64

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

Synchroscope answered 27/2, 2009 at 9:36 Comment(1)
For handling IPv6 addresses, see stackoverflow.com/questions/7951061Quartis
F
101

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;
}
Flagwaving answered 27/2, 2009 at 9:48 Comment(9)
perhaps if you posted your C# version we might be able to figure out why?Flagwaving
@Guillaume - are you sure about that? the important part is that the correct least significant bits are zero. The extra high bits you'd get on a 64-bit machine would be ignored.Flagwaving
@Flagwaving - sorry, you're absolutely correct. I rolled out my own function inspired by your submission and it was necessary in mine for some reason. My apologies for the noise.Endmost
This will not work on 32-bit systems: match(1.2.3.4, 0.0.0.0/0) returns false, should return trueDiligence
I don't see why you use split, it's only a char, so you don't need the regex engine, also split is deprecated from php 5.3Insulting
@JakaJančar are you sure your test is valid? maybe you misunderstood how the function worksInsulting
NOTE that if $range does not contain a /, this function will always return true (since $bits will be NULL). In other words, this function needs something like if($bits === null) return false; Also, ideally, this function would check that $subnet is at least a valid IP address.Kick
@Kick a better fix would probably be to set $bits = 32 if there was no /.Flagwaving
With the edits over the years, this seems to work fine for me. Good solution for a quick fix.Champlin
H
65

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

Healey answered 14/1, 2015 at 13:13 Comment(5)
Thanks! Somehow Symfony solution works correctly in my case, while accepted one - doesn't.Scullion
Thats because PHP, composer, and Symphony were very different back when the question was asked in 2009Synchroscope
This is the best answer IMO.Jaeger
Thanks for letting us know about this -rather obscured but useful- utility method. There's one flaw in your code, though, the second argument for the IpUtils::checkIp method is a hard-coded string, the iterated ips are not being used.-Steffens
this is the way if you're using a symphony based framework like laravelWelby
L
47

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.

Lupercalia answered 12/2, 2013 at 21:6 Comment(3)
cidr_match("1.2.3.4", "0.0.0.0/0") returns false on my machine (PHP 5.5.13, Windows x64).Ovariectomy
@Samuel Parkinson, just so you know there's a library built on your answer: github.com/tholu/php-cidr-match. The author has given the credit.Trisomic
NOTE that if $cidr does not contain a /, this function will always return true (since $mask will be NULL). In other words, this function needs something like if($mask === null) return false; Also, $subnet should be verified to at least be a valid IP address. Due to both of these issues, something silly like cidr_match("8.8.8.8", "blah"); will return true.Kick
I
5

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;
}
Insulting answered 17/12, 2012 at 8:32 Comment(1)
Here's an added implementation to check against CloudFlare's IP for example: [code] $ip = $_SERVER["REMOTE_ADDR"]; $c1 = cidr_match($ip, '204.93.240.0/24'); $c2 = cidr_match($ip, '204.93.177.0/24'); $c3 = etc.. $cTotal = round($c1+$c2+$c3+...); if($cTotal < 1) die(); [/code]Roof
T
4

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…

Tso answered 16/2, 2017 at 13:58 Comment(1)
Very comprehensive solution. Works without a hitch. Thanks a million!Hearse
A
3

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 ;
}
Anestassia answered 26/1, 2013 at 10:0 Comment(0)
A
2

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 <[email protected]>
 * 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/)

Aroid answered 30/1, 2014 at 16:52 Comment(0)
A
2

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:

Test Results IP Checks

Anarch answered 16/11, 2018 at 15:53 Comment(0)
M
1
function cidr_match($ipStr, $cidrStr) {
  $ip = ip2long($ipStr);
  $cidrArr = split('/',$cidrStr);
  $maskIP = ip2long($cidrArr[0]);
  $maskBits = 32 - $cidrArr[1];
  return (($ip>>$maskBits) == ($maskIP>>$maskBits));
}
Maximalist answered 27/2, 2009 at 9:54 Comment(1)
This will not work on 32-bit systems: match(1.2.3.4, 0.0.0.0/0) returns false, should return trueDiligence
M
1

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;
        }
    }
Monahan answered 27/3, 2013 at 11:39 Comment(0)
A
1

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);
}
Alcantar answered 16/8, 2013 at 7:42 Comment(0)
L
1

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
Load answered 14/12, 2016 at 16:26 Comment(0)
K
1

I ended up using @A.R.NasirQureshi's solution with bit to bit matching subnet and mask, which I expanded to deal with IPv6 as well since no other solution mentioned inet_pton that effectively takes care of some odd cases like "2006:BCAA:64FF::104.121.140.212" , instead of splitting by "." or ":" which may be complicated !

function cidr_match($ip, $range){
  list($subnet, $bits) = explode('/', $range);

  //add 128-32=96 for IPv4 range bits
  if(filter_var($subnet, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)){ $bits += 96; }

  // compare bits of the 2 strings of 128 0 & 1 like 11000010101100
  $subnet = substr( IP2Bin($subnet), 0, $bits);
  $ip = substr( IP2Bin($ip), 0, $bits);

  return $ip == $subnet;
}
function IP2Bin($ip){
  $IPbin = ''; $bin = inet_pton($ip);
  if($bin===false){return str_repeat('0',128);} //in case of invalid IP

  //split into 4-char hexadecimal parts (just to avoid issues with hexdec for larger hex, but you can choose more)
  $arr = str_split( current( unpack('H*', $bin) ) , 4 );

  //each char is 4 bits so 4 chars == 16 bits
  foreach($arr as $p){ $IPbin .= sprintf("%016b", hexdec($p) ); }

  //always get 128 chars string of 1s & 0s
  return str_pad( $IPbin, 128, '0', STR_PAD_LEFT );
}

Because the comparison always takes place with "128 bits" (aka 128 1s & 0s), this works whether both the IP and Range are the same type (IPv4 or IPv6), and even when their type is different (IPv4 vs IPv6 or vice-versa).

Kiloton answered 8/3, 2023 at 19:39 Comment(0)
C
0

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:

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));
}
Condillac answered 3/6, 2009 at 23:15 Comment(0)
C
0

I recently needed to match IP addresses to a network list and found this question. Below is a variant integrating all the ideas posted earlier plus IPv6 support and validation of address, subnet and mask. The function returns false if an incorrect CIDR format is submitted or when attempt to match IPv4 address to IPv6 network (or vice versa) is made.

/*
  Checks if $ip belongs to $cidr.
  cidr_match('1.2.3.4', '1.2.3.5/24') == true
  cidr_match('::1.2.3.4', '::1.2.3.0/125') == true
  cidr_match('::1.2.3.4', '::1.2.3.0/126') == false
*/
function cidr_match($ip, $cidr)
{
    if (!filter_var($ip, FILTER_VALIDATE_IP)) return false;
    $p = unpack('N*', inet_pton($ip)); $ps = count($p);

    list ($subnet, $bits) = [$cidr, '128'];
    if (strstr($cidr, '/')) list ($subnet, $bits) = explode('/', $cidr, 2);
    if (!filter_var($subnet, FILTER_VALIDATE_IP)) return false;
    if (!preg_match('/^[1-9][0-9]*$/', $bits)) return false;
    $s = unpack('N*', inet_pton($subnet));
    if (count($s) != $ps) return false;
    $bits = intval($bits);

    $m = [];
    for ($i = 1; $i <= $ps; $i++)
        $m[$i] = ($i*32 - $bits) < 0 ? -1 : -1 << ($i*32 - $bits);

    for ($i = 1; $i <= $ps; $i++)
        if (($p[$i] & $m[$i]) != ($s[$i] & $m[$i]))
            return false;

    return true;
}

/*
  Returns first matching CIDR in $netlist or null if none match.
  net_match('1.2.3.4', ['1.2.3.5/24']) == '1.2.3.5/24'
  net_match('::1.2.3.4', ['::1.2.3.0/126', '::1.2.3.0/125']) ==  '::1.2.3.0/125'
*/
function net_match($ip, $netlist)
{
    foreach ($netlist as $cidr)
        if (cidr_match($ip, $cidr))
            return $cidr;
    return null;
}
Cotinga answered 16/5, 2023 at 12:56 Comment(0)
B
0

rewriting samuel parkinsons code with input validation checks and php8

    /**
     * @param string $ip
     * @param string $cidr
     * @return bool
     * @link https://mcmap.net/q/299748/-check-whether-or-not-a-cidr-subnet-contains-an-ip-address
     */
    private static function cidr_match(string $ip, string $cidr):bool
    {
        if (!filter_var($ip, FILTER_VALIDATE_IP))
        {
            throw new \InvalidArgumentException("The ip must be valid!");
        }
        if (!str_contains($cidr, '/'))
        {
            throw new \InvalidArgumentException("The cidr must contain a / ");
        }
        [$subnet, $mask] = explode('/', $cidr);
        $mask = (int)$mask;
        if (!filter_var($subnet, FILTER_VALIDATE_IP))
        {
            throw new \InvalidArgumentException("The subnet must be a valid IP!");
        }
        if ($mask < 0 || $mask > 32)
        {
            throw new \InvalidArgumentException("The cidr mask must be between 0 and 32!");
        }
        return (ip2long($ip) & ~((1 << (32 - $mask)) - 1)) == ip2long($subnet);
    }
Brandybrandyn answered 10/10, 2023 at 23:16 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.