0

I need to check if visitor IPv6 is in given prefix (I have some whitelisted ipv6 prefixes).

I adjusted the below function using two different functions from StackOverflow (one was converting an ipv6 prefix to first and last address and another answer was testing if a given IP is in given first+last ip range), but looking at the code, there are many functions I am unfamiliar with, plus the ipv6 confuse me.

Can someone tell me if this code looks correct ? From my tests it validates the IPs I send to it:

<?php 
if(!function_exists('is_ipv6_in_prefix')){
    function is_ipv6_in_prefix($ipv6_ip_to_check, $ipv6_prefix_to_check){ 
            
        $ipv6_ip_to_check = inet_pton($ipv6_ip_to_check);

        // Split in address and prefix length
        list($firstaddrstr, $prefixlen) = explode('/', $ipv6_prefix_to_check);
        
        // Parse the address into a binary string
        $firstaddrbin = inet_pton($firstaddrstr);
        
        // Convert the binary string to a string with hexadecimal characters
        
        if(function_exists('bin2hex')){
            $firstaddrhex = bin2hex($firstaddrbin);
        } else {
            // older php versions can use pack/unpack
            $firstaddrhex = reset(unpack('H*', $firstaddrbin));
        }
        
        
        // Overwriting first address string to make sure notation is optimal
        $firstaddrstr = inet_ntop($firstaddrbin);
        
        // Calculate the number of 'flexible' bits
        $flexbits = 128 - $prefixlen;
        
        // Build the hexadecimal string of the last address
        $lastaddrhex = $firstaddrhex;
        
        // We start at the end of the string (which is always 32 characters long)
        $pos = 31;
        while ($flexbits > 0) {
          // Get the character at this position
          $orig = substr($lastaddrhex, $pos, 1);
        
          // Convert it to an integer
          $origval = hexdec($orig);
        
          // OR it with (2^flexbits)-1, with flexbits limited to 4 at a time
          $newval = $origval | (pow(2, min(4, $flexbits)) - 1);
        
          // Convert it back to a hexadecimal character
          $new = dechex($newval);
        
          // And put that character back in the string
          $lastaddrhex = substr_replace($lastaddrhex, $new, $pos, 1);
        
          // We processed one nibble, move to previous position
          $flexbits -= 4;
          $pos -= 1;
        }
        
        // Convert the hexadecimal string to a binary string

        if(function_exists('hex2bin')){
            $lastaddrbin = hex2bin($lastaddrhex);
        } else {
            // older PHP versions can use pack/unpack
            $lastaddrbin = pack('H*', $lastaddrhex);
        }
        
        // And create an IPv6 address from the binary string
        $lastaddrstr = inet_ntop($lastaddrbin);
        
        // print string values for user
        // echo "\nPrefix: $ipv6_prefix_to_check";
        // echo "\nFirst: $firstaddrstr";
        // echo "\nLast: $lastaddrstr";
        
        
        if ((strlen($ipv6_ip_to_check) == strlen($firstaddrbin)) &&  ($ipv6_ip_to_check >= $firstaddrbin && $ipv6_ip_to_check <= $lastaddrbin)) {
            // In range
            return true;
        } else {
            // Not in range
            return false;
        }
        

    }
}

if(is_ipv6_in_prefix('2a03:2880:f800:4::', '2a03:2880:f800::/48')){
    echo "\nTRUE";
} else {
    echo "\nFALSE";
}

?>

I am unsure because another answer there comes with a correction and says:

This is a fix to the accepted answer, which incorrectly assumes the "first address" should be identical to the inputted string. Rather, it needs to have its value modified via an AND operator against its mask.

To demonstrate the problem, consider this example input: 2001:db8:abc:1403::/54

Expected result:

First: 2001:db8:abc:1400:: Actual result:

First: 2001:db8:abc:1403::

[...] full code for the fix [...]

When I tried to apply the code in the "correction" above, it failed to match IP 2a03:2880:f800:4:: inside this prefix: 2a03:2880:f800::/48. Maybe I applied the fix wrong, but still, does this correction mentioned at the end apply to my final code above ?

0

1 Answer 1

1

Your entire procedure is backwards. The check can be done without computing the first & last addresses of the prefix, without even the 'flexible bit' calculation – all that you need to do is compare the initial X bits of the given address and prefix (i.e. the not-'flexible' bits).

(The binary-to-hex conversion is also generally pointless – if you need to hex2bin() each nibble to operate on it, then you might as well keep the entire string binary in the first place, as it makes no difference whether you work on 4-bit or 8-bit units, or even 32-bit units for that matter.)

It is useful to remember how old-fashioned netmasks such as 255.255.128.0 worked – the term 'mask' implies that it would be directly bitwise-ANDed to the address, and the result compared; that is, (address & mask) == net. This does not change with CIDR prefixes; all that changes is the syntax for writing down the same netmask. (Indeed some systems internally pre-expand the "/48" into a full mask such as "ffff:ffff:ffff::" for faster comparison.)

Example:

function ip_cidr($host, $mask) {
    list ($net, $len) = explode("/", $mask, 2);

    $host = inet_pton($host);
    $net = inet_pton($net);
    if ($host === false || $net === false || (strlen($len) && !is_numeric($len)))
        throw new \InvalidArgumentException();
    elseif (strlen($host) !== strlen($net))
        return false; /* mismatching address families not an error */

    $nbits = strlen($host) * 8;
    $len = strlen($len) ? intval($len) : $nbits;
    if ($len < 0 || $len > $nbits)
        throw new \InvalidArgumentException();

    $host = unpack("C*", $host);
    $net = unpack("C*", $net);
    /* loop over each byte and compare them */
    for ($i = 1; $i <= count($net) && $len > 0; $i++) {
        $bits = min($len, 8);
        $len -= 8;
        /* if less than 8 bits left to check, generate an 'AND' mask for them */
        $bmask = (0xFF00 >> $bits) & 0xFF;

        //printf("host[%d] = %02x, net[%d] = %02x, bits = %d, bmask = %02x\n",
        //       $i, $host[$i], $i, $net[$i], $bits, $bmask);

        if (($host[$i] & $bmask) !== ($net[$i] & $bmask))
            return false;
    }
    return true;
}

As you can see, most of the function is just pre-checks, with a bitwise-AND comparison as the actual check (which could be made if (($host[$i] ^ $net[$i]) & $bmask) if you want one fewer operation).

The same function works for IPv4 and IPv6.


Bonus: range start-end

The first or last address of a prefix can be calculated with net & mask and net | ~mask (the latter being bitwise NOT operation), if needed for some other purpose.

This is almost the same procedure as in the original "range calculator" you found, only looping forwards instead of backwards (IMO, the initial 'useless' iterations are well worth the reduced complexity), as well as using bitwise operations instead of 2bits-1 to calculate the AND/OR mask:

function ip_range($mask) {
    @list ($net, $len) = explode("/", $mask, 2);
    $net = inet_pton($net);
    if ($net === false || !is_numeric("0$len"))
        throw new \InvalidArgumentException();

    $nbits = strlen($net) * 8;
    $len = strlen($len) ? intval($len) : $nbits;
    if ($len < 0 || $len > $nbits)
        throw new \InvalidArgumentException();

    $net = unpack("C*", $net);
    $first = [];
    $last = [];
    for ($i = 1; $i <= count($net); $i++) {
        $bits = min($len, 8);
        $len = max($len - 8, 0);
        $bmask = (0xFF00 >> $bits) & 0xFF;

        $first[$i] = $net[$i] & $bmask;
        $last[$i] = $net[$i] | ~$bmask & 0xFF;
    }

    $first = inet_ntop(pack("C*", ...$first));
    $last = inet_ntop(pack("C*", ...$last));
    return [$first, $last];
}

(I wanted to make it $len -= $bits, which works equally well in both functions, but my sleep-deprived brain refuses to trust it for some reason. I'm sure it's correct, but still the max() seems slightly more readable.)


About the original code

When I tried to apply the code in the "correction" above, it failed to match IP 2a03:2880:f800:4:: inside this prefix: 2a03:2880:f800::/48. Maybe I applied the fix wrong, but still, does this correction mentioned at the end apply to my final code above ?

It applies to inputs where the prefix has 'host' bits set, such as 2a03:2880:f800::/32 – which are a valid way of expressing a network interface configuration (host address + netmask), but not strictly valid as a prefix. (The prefix in this example would be 2a03:2880::/32.)

So if you can ensure that your list of "trusted prefixes" is already in the canonical form (without any 'host' bits set), then the function can assume the prefix to be equal to the first address of that prefix, and there's no real need to apply the correction.

However, the function ought to return identical results either way (assuming canonical input), so in your case I'd say you must have applied the correction incorrectly. You haven't shown your updated code, but the original post by Allen Ellis seems to be correct – basically the entire computation of 'last address' has to be duplicated and inverted.

(In my example ip_range() function, $first[i] = $net[i] & $bmask is where the same AND operation is done.)

If you want to try debugging the code, I would at minimum keep the it split into a separate "calculate first-last" function so that you could test that alone (make sure it always calculates the correct first address, etc.) and only after you get that working right, wrap a separate "check if within a-b range" function around it.

But I wouldn't really spend any time on trying to improve the current code. As mentioned at the start, implementing the prefix check in terms of first <= addr <= last is really a bit backwards, and there is a much more direct way of doing it which inherently does not have any problems with non-canonical prefixes.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.