Search code examples
phpipv6

How to check if an IPv6 address is in a specific network (for ACL implementation) in PHP


I have an ACL implementation in PHP that takes an array of IPv4 networks and checks if an address matches one of them, works like this:

$acl = array("192.168.0.0/24", "10.6.0.0/16");
if (address_in_range($_SERVER['REMOTE_ADDR'], $acl) {
  // Allow the action...
}

The address_in_range() function I made works for IPv4, I did it like this:

  list($subnet, $mask) = explode('/', $cidr);
  if ((ip2long($ip) & ~((1 << (32 - $mask)) - 1) ) == ip2long($subnet)) {
    return true;
  }

However, I also need it to work with IPv6 addresses, so I can supply an array of mixed IPv4 and IPv6 ranges to my function, like this:

$acl = array("192.168.0.0/24", "10.6.0.0/16", "2a01:1098:15::/48");

And the checked IP can be either IPv4 or IPv6.


Solution

  • I ended up writing this function to do the job:

    /**
     * Check if an IP address matches an entry in an access control list (ACL)
     * Returns true if match, false otherwise (including if $ip is not a valid IP
     * address). Works with both IPv4 and IPv6 addresses.
     *
     * Example: check_acl("10.6.1.16", array("10.6.0.0/16","2a01:fe8:95::/48"));
     * @param string $ip   IP address to check
     * @param array  $acl  Array of CIDR-notation IP addresses
     * @return boolean
     */
    function check_acl($ip, $acl) {
        $ipb = inet_pton($ip);
        $iplen = strlen($ipb);
        if (strlen($ipb) < 4) {
            // Invalid IP address
            return false;
        }
        foreach ($acl as $cidr) {
            $ar = explode('/',$cidr);
            $ip1 = $ar[0];
            $ip1b = inet_pton($ip1);
            $ip1len = strlen($ip1b);
            if ($ip1len != $iplen) {
                // Different type
                continue;
            }
            if (count($ar)>1) {
                $bits=(int)($ar[1]);
            } else {
                $bits = $iplen * 8;
            }
            for ($c=0; $bits>0; $c++) {
                $bytemask = ($bits < 8) ? 0xff ^ ((1 << (8-$bits))-1) : 0xff;
                if (((ord($ipb[$c]) ^ ord($ip1b[$c])) & $bytemask) != 0)
                    continue 2;
                $bits-=8;
            }
            return true;
        }
        return false;
    }