├── .gitignore ├── composer.json ├── phpunit.xml.dist ├── LICENSE ├── README.md ├── tests └── Lifo │ └── IP │ └── IPTest.php └── src ├── IP.php ├── BC.php └── CIDR.php /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lifo/ip", 3 | "description": "IP address helper PHP library for working with IPv4 and IPv6 addresses", 4 | "keywords": ["ip address", "ip", "ipv4", "ipv6"], 5 | "type": "library", 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Jason Morriss", 10 | "email": "lifo2013@gmail.com" 11 | } 12 | ], 13 | "require": { 14 | "php": ">=5.3.0", 15 | "ext-bcmath": "*" 16 | }, 17 | "autoload": { 18 | "psr-4": { "Lifo\\IP\\": "src/" } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | tests 12 | 13 | 14 | 15 | 16 | 17 | ./src 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Jason Morriss 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## IP Address Helper Library 2 | 3 | IP address helper PHP library for working with IPv4 and IPv6 addresses. Convert any IP address into decimal, hex or binary and back again. 4 | 5 | ### Notes 6 | 7 | This library is not complete and is missing certain CIDR, Subnet and other miscellaneous IP features. 8 | Most of the IP conversion routines use `BCMATH` to do calculations which means this library is not the fastest it 9 | could be. Once the library is in a more stable state I may start attempting to optimize certain bits. 10 | 11 | Feel free to send pull requests with missing functionality. 12 | 13 | ### Examples 14 | 15 | The translation routines are IP agnostic, meaning they don't care if you pass in an IPv4 or IPv6 address. 16 | All IP calculations are done in `Decimal` which is perfect for storing in databases. 17 | 18 | ```php 19 | use Lifo\IP\IP; 20 | use Lifo\IP\CIDR: 21 | 22 | // IPv4 23 | echo '127.0.0.1 = ', IP::inet_ptod('127.0.0.1'), "\n"; 24 | echo IP::inet_dtop('2130706433'), " = 2130706433\n"; 25 | echo '127.0.0.1 = ', IP::inet_ptoh('127.0.0.1'), " (hex)\n"; 26 | 27 | // IPv6 28 | echo '2001:4056::1 = ', IP::inet_ptod('2001:4056::1'), "\n"; 29 | echo IP::inet_dtop('42541793049812452694190522094162280449'), " = 42541793049812452694190522094162280449\n"; 30 | echo '2001:4056::1 = ', IP::inet_ptoh('2001:4056::1'), " (hex)\n"; 31 | 32 | // CIDR 33 | 34 | // note: the true CIDR block is calculated from the prefix (the ::1 is ignored) 35 | $ip = new CIDR('2001:4056::1/96'); 36 | 37 | echo "$ip\n", implode(' - ', $ip->getRange()), " (" . number_format($ip->getTotal()) . " hosts)\n"; 38 | ``` 39 | 40 | ``` 41 | // expected output: 42 | 127.0.0.1 = 2130706433 43 | 127.0.0.1 = 2130706433 44 | 127.0.0.1 = 7f000001 (hex) 45 | 46 | 2001:4056::1 = 42541793049812452694190522094162280449 47 | 2001:4056::1 = 42541793049812452694190522094162280449 48 | 2001:4056::1 = 20014056000000000000000000000001 (hex) 49 | 50 | 2001:4056::1/96 51 | 2001:4056:: - 2001:4056::ffff:ffff (4,294,967,296 hosts) 52 | ``` 53 | -------------------------------------------------------------------------------- /tests/Lifo/IP/IPTest.php: -------------------------------------------------------------------------------- 1 | assertTrue(IP::isIPv4('10.0.0.1')); 12 | $this->assertFalse(IP::isIPv4('2007::1')); 13 | $this->assertEquals(IP::inet_ptod('10.0.0.1'), '167772161', 'IPv4 inet_ptod (decimal)'); 14 | $this->assertEquals(IP::inet_ptod('10.0.0.1/24'), '167772161', 'IPv4 inet_ptod with cidr (decimal)'); 15 | $this->assertEquals(IP::inet_ptoh('10.0.0.1'), '0a000001', 'IPv4 inet_ptoh (hex)'); 16 | $this->assertEquals(IP::inet_ptob('10.0.0.1'), '00001010000000000000000000000001', 'IPv4 inet_ptob (binary)'); 17 | $this->assertEquals(IP::inet_dtop('167772161'), '10.0.0.1', 'IPv4 inet_dtop (presentational)'); 18 | $this->assertEquals(IP::inet_htop('0a000001'), '10.0.0.1', 'IPv4 inet_htop (hex)'); 19 | $this->assertEquals(IP::inet_btop('00001010000000000000000000000001'), '10.0.0.1', 'IPv4 inet_btop (binary)'); 20 | $this->assertEquals(IP::inet_expand('10.0.0.1'), '10.0.0.1', 'IPv4 expand'); 21 | $this->assertEquals(IP::inet_expand('10.0.0.1/24'), '10.0.0.1', 'IPv4 expand with cidr'); 22 | $this->assertEquals(IP::to_ipv6('10.0.0.1'), 'a00:1', 'IPv6 to_ipv6'); 23 | $this->assertEquals(IP::to_ipv6('10.0.0.1', true), '0:0:0:0:0:ffff:a00:1', 'IPv6 to_ipv6 mapped'); 24 | 25 | // IPv6 26 | $this->assertTrue(IP::isIPv6('2007:1234:5678::1')); 27 | $this->assertFalse(IP::isIPv6('10.0.0.1')); 28 | $this->assertEquals(IP::inet_ptod('2007:1234:5678::1'), '42572011173125150141124156729380044801', 'IPv6 inet_ptod (decimal)'); 29 | $this->assertEquals(IP::inet_ptod('2007:1234:5678::1/64'), '42572011173125150141124156729380044801', 'IPv6 inet_ptod with cidr (decimal)'); 30 | $this->assertEquals(IP::inet_ptoh('2007:1234:5678::1'), '20071234567800000000000000000001', 'IPv6 inet_ptoh (hex)'); 31 | $this->assertEquals(IP::inet_ptob('2007:1234:5678::1'), '00100000000001110001001000110100010101100111100000000000000000000000000000000000000000000000000000000000000000000000000000000001', 'IPv6 inet_ptob (binary)'); 32 | $this->assertEquals(IP::inet_dtop('42572011173125150141124156729380044801'), '2007:1234:5678::1', 'IPv6 inet_dtop (presentational)'); 33 | $this->assertEquals(IP::inet_htop('20071234567800000000000000000001'), '2007:1234:5678::1', 'IPv6 inet_htop (hex)'); 34 | $this->assertEquals(IP::inet_btop('00100000000001110001001000110100010101100111100000000000000000000000000000000000000000000000000000000000000000000000000000000001'), '2007:1234:5678::1', 'IPv6 inet_btop (binary)'); 35 | $this->assertEquals(IP::inet_expand('2007:1234:5678::1'), '2007:1234:5678:0000:0000:0000:0000:0001', 'IPv6 expand'); 36 | $this->assertEquals(IP::inet_expand('2007:1234:5678::1/64'), '2007:1234:5678:0000:0000:0000:0000:0001', 'IPv6 expand with cidr'); 37 | 38 | // Misc tests for coverage 39 | $this->assertEquals(IP::inet_dtop('167772161'), '10.0.0.1', 'IPv6 inet_dtop low-end no version (decimal)'); 40 | $this->assertEquals(IP::inet_dtop('167772161', 6), '::10.0.0.1', 'IPv6 inet_dtop low-end forced v6 (decimal)'); 41 | $this->assertEquals(IP::inet_dtop('167772161', 4), '10.0.0.1', 'IPv6 inet_dtop low-end forced v4 (decimal)'); 42 | } 43 | 44 | public function testIPv4cmp() 45 | { 46 | $lo = '10.0.0.1'; 47 | $hi = '192.168.1.2'; 48 | 49 | $this->assertTrue(IP::cmp($lo, $lo) == 0); 50 | $this->assertTrue(IP::cmp($lo, $hi) == -1); 51 | $this->assertTrue(IP::cmp($hi, $lo) == 1); 52 | } 53 | 54 | public function testIPv6cmp() 55 | { 56 | $lo = '2001::1'; 57 | $hi = '9504::2'; 58 | 59 | $this->assertTrue(IP::cmp($lo, $lo) == 0); 60 | $this->assertTrue(IP::cmp($lo, $hi) == -1); 61 | $this->assertTrue(IP::cmp($hi, $lo) == 1); 62 | } 63 | 64 | /** 65 | * @expectedException InvalidArgumentException 66 | */ 67 | public function testIPException() 68 | { 69 | // param 1 must be an IPv4 address 70 | IP::to_ipv6('2007::1'); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/IP.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | namespace Lifo\IP; 11 | 12 | /** 13 | * IP Address helper class. 14 | * 15 | * Provides routines to translate IPv4 and IPv6 addresses between human readable 16 | * strings, decimal, hexidecimal and binary. 17 | * 18 | * Requires BCmath extension and IPv6 PHP support 19 | */ 20 | abstract class IP 21 | { 22 | /** 23 | * Convert a human readable (presentational) IP address string into a decimal string. 24 | */ 25 | public static function inet_ptod($ip) 26 | { 27 | // shortcut for IPv4 addresses 28 | if (strpos($ip, ':') === false && strpos($ip, '.') !== false) { 29 | // remove any cidr block notation 30 | if (($o = strpos($ip, '/')) !== false) { 31 | $ip = substr($ip, 0, $o); 32 | } 33 | return sprintf('%u', ip2long($ip)); 34 | } 35 | 36 | // remove any cidr block notation 37 | if (($o = strpos($ip, '/')) !== false) { 38 | $ip = substr($ip, 0, $o); 39 | } 40 | 41 | // unpack into 4 32bit integers 42 | $parts = unpack('N*', inet_pton($ip)); 43 | foreach ($parts as &$part) { 44 | if ($part < 0) { 45 | // convert signed int into unsigned 46 | $part = sprintf('%u', $part); 47 | //$part = bcadd($part, '4294967296'); 48 | } 49 | } 50 | 51 | // add each 32bit integer to the proper bit location in our big decimal 52 | $decimal = $parts[4]; // << 0 53 | $decimal = bcadd($decimal, bcmul($parts[3], '4294967296')); // << 32 54 | $decimal = bcadd($decimal, bcmul($parts[2], '18446744073709551616')); // << 64 55 | $decimal = bcadd($decimal, bcmul($parts[1], '79228162514264337593543950336')); // << 96 56 | 57 | return $decimal; 58 | } 59 | 60 | /** 61 | * Convert a decimal string into a human readable IP address. 62 | * 63 | * @param string $decimal Decimal number to convert into presentational IP string. 64 | * @param integer $version Force IP version to 4 or 6. Leave null for automatic. 65 | */ 66 | public static function inet_dtop($decimal, $version = null) 67 | { 68 | $parts = array(); 69 | $parts[1] = bcdiv($decimal, '79228162514264337593543950336', 0); // >> 96 70 | $decimal = bcsub($decimal, bcmul($parts[1], '79228162514264337593543950336')); 71 | $parts[2] = bcdiv($decimal, '18446744073709551616', 0); // >> 64 72 | $decimal = bcsub($decimal, bcmul($parts[2], '18446744073709551616')); 73 | $parts[3] = bcdiv($decimal, '4294967296', 0); // >> 32 74 | $decimal = bcsub($decimal, bcmul($parts[3], '4294967296')); 75 | $parts[4] = $decimal; // >> 0 76 | 77 | foreach ($parts as &$part) { 78 | if (bccomp($part, '2147483647') == 1) { 79 | $part = bcsub($part, '4294967296'); 80 | } 81 | $part = (int) $part; 82 | } 83 | 84 | if (!$version) { 85 | // if the first 96bits is all zeros then we can safely assume we 86 | // actually have an IPv4 address. Even though it's technically possible 87 | // you're not really ever going to see an IPv6 address in the range: 88 | // ::0 - ::ffff 89 | // It's feasible to see an IPv6 address of "::", in which case the 90 | // caller is going to have to account for that on their own (or 91 | // pass $version to this function). 92 | if (($parts[1] | $parts[2] | $parts[3]) == 0) { 93 | $version = 4; 94 | } 95 | } 96 | 97 | if ($version == 4) { 98 | $ip = long2ip($parts[4]); 99 | } else { 100 | $packed = pack('N4', $parts[1], $parts[2], $parts[3], $parts[4]); 101 | $ip = inet_ntop($packed); 102 | } 103 | 104 | return $ip; 105 | } 106 | 107 | /** 108 | * Convert a human readable (presentational) IP address into a HEX string. 109 | */ 110 | public static function inet_ptoh($ip) 111 | { 112 | return bin2hex(inet_pton($ip)); 113 | //return BC::bcdechex(self::inet_ptod($ip)); 114 | } 115 | 116 | /** 117 | * Convert a human readable (presentational) IP address into a BINARY string. 118 | */ 119 | public static function inet_ptob($ip, $bits = null) 120 | { 121 | if ($bits === null) { 122 | $bits = self::isIPv4($ip) ? 32 : 128; 123 | } 124 | return BC::bcdecbin(self::inet_ptod($ip), $bits); 125 | } 126 | 127 | /** 128 | * Convert a binary string into an IP address (presentational) string. 129 | */ 130 | public static function inet_btop($bin) 131 | { 132 | return self::inet_dtop(BC::bcbindec($bin)); 133 | } 134 | 135 | /** 136 | * Convert a HEX string into a human readable (presentational) IP address 137 | */ 138 | public static function inet_htop($hex) 139 | { 140 | return self::inet_dtop(BC::bchexdec($hex)); 141 | } 142 | 143 | /** 144 | * Expand an IP address. IPv4 addresses are returned as-is. 145 | * 146 | * Example: 147 | * 2001::1 expands to 2001:0000:0000:0000:0000:0000:0000:0001 148 | * ::127.0.0.1 expands to 0000:0000:0000:0000:0000:0000:7f00:0001 149 | * 127.0.0.1 expands to 127.0.0.1 150 | */ 151 | public static function inet_expand($ip) 152 | { 153 | // strip possible cidr notation off 154 | if (($pos = strpos($ip, '/')) !== false) { 155 | $ip = substr($ip, 0, $pos); 156 | } 157 | $bytes = unpack('n*', inet_pton($ip)); 158 | if (count($bytes) > 2) { 159 | return implode(':', array_map(function ($b) { 160 | return sprintf("%04x", $b); 161 | }, $bytes)); 162 | } 163 | return $ip; 164 | } 165 | 166 | /** 167 | * Convert an IPv4 address into an IPv6 address. 168 | * 169 | * One use-case for this is IP 6to4 tunnels used in networking. 170 | * 171 | * @example 172 | * to_ipv6("10.10.10.10") == a0a:a0a 173 | * 174 | * @param string $ip IPv4 address. 175 | * @param bool $mapped If true a Full IPv6 address is returned within the 176 | * official ipv4to6 mapped space "0:0:0:0:0:ffff:x:x" 177 | */ 178 | public static function to_ipv6($ip, $mapped = false) 179 | { 180 | if (!self::isIPv4($ip)) { 181 | throw new \InvalidArgumentException("Invalid IPv4 address \"$ip\""); 182 | } 183 | 184 | $num = IP::inet_ptod($ip); 185 | $o1 = dechex($num >> 16); 186 | $o2 = dechex($num & 0x0000FFFF); 187 | 188 | return $mapped ? "0:0:0:0:0:ffff:$o1:$o2" : "$o1:$o2"; 189 | } 190 | 191 | /** 192 | * Returns true if the IP address is a valid IPv4 address 193 | */ 194 | public static function isIPv4($ip) 195 | { 196 | return $ip === filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4); 197 | } 198 | 199 | /** 200 | * Returns true if the IP address is a valid IPv6 address 201 | */ 202 | public static function isIPv6($ip) 203 | { 204 | return $ip === filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6); 205 | } 206 | 207 | /** 208 | * Compare two IP's (v4 or v6) and return -1, 0, 1 if the first is < = > 209 | * the second. 210 | * 211 | * @param string $ip1 IP address 212 | * @param string $ip2 IP address to compare against 213 | * @return integer Return -1,0,1 depending if $ip1 is <=> $ip2 214 | */ 215 | public static function cmp($ip1, $ip2) 216 | { 217 | return bccomp(self::inet_ptod($ip1), self::inet_ptod($ip2), 0); 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /src/BC.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Lifo\IP; 12 | 13 | /** 14 | * BCMath helper class. 15 | * 16 | * Provides a handful of BCMath routines that are not included in the native 17 | * PHP library. 18 | * 19 | * Note: The Bitwise functions operate on fixed byte boundaries. For example, 20 | * comparing the following numbers uses X number of bits: 21 | * 0xFFFF and 0xFF will result in comparison of 16 bits. 22 | * 0xFFFFFFFF and 0xF will result in comparison of 32 bits. 23 | * etc... 24 | * 25 | */ 26 | abstract class BC 27 | { 28 | // Some common (maybe useless) constants 29 | const MAX_INT_32 = '2147483647'; // 7FFFFFFF 30 | const MAX_UINT_32 = '4294967295'; // FFFFFFFF 31 | const MAX_INT_64 = '9223372036854775807'; // 7FFFFFFFFFFFFFFF 32 | const MAX_UINT_64 = '18446744073709551615'; // FFFFFFFFFFFFFFFF 33 | const MAX_INT_96 = '39614081257132168796771975167'; // 7FFFFFFFFFFFFFFFFFFFFFFF 34 | const MAX_UINT_96 = '79228162514264337593543950335'; // FFFFFFFFFFFFFFFFFFFFFFFF 35 | const MAX_INT_128 = '170141183460469231731687303715884105727'; // 7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF 36 | const MAX_UINT_128 = '340282366920938463463374607431768211455'; // FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF 37 | 38 | /** 39 | * BC Math function to convert a HEX string into a DECIMAL 40 | * 41 | * @param string $hex 42 | * @return float|int|string 43 | */ 44 | public static function bchexdec($hex) 45 | { 46 | if (strlen($hex) == 1) { 47 | return hexdec($hex); 48 | } 49 | 50 | $remain = substr($hex, 0, -1); 51 | $last = substr($hex, -1); 52 | return bcadd(bcmul(16, self::bchexdec($remain), 0), hexdec($last), 0); 53 | } 54 | 55 | /** 56 | * BC Math function to convert a DECIMAL string into a BINARY string 57 | * 58 | * @param string $dec 59 | * @param int $pad 60 | * @return string 61 | */ 62 | public static function bcdecbin($dec, $pad = null) 63 | { 64 | $bin = ''; 65 | while ($dec) { 66 | $m = bcmod($dec, 2); 67 | $dec = bcdiv($dec, 2, 0); 68 | $bin = abs($m) . $bin; 69 | } 70 | return $pad ? sprintf("%0{$pad}s", $bin) : $bin; 71 | } 72 | 73 | /** 74 | * BC Math function to convert a BINARY string into a DECIMAL string 75 | * 76 | * @param string $bin 77 | * @return string 78 | */ 79 | public static function bcbindec($bin) 80 | { 81 | $dec = '0'; 82 | for ($i = 0, $j = strlen($bin); $i < $j; $i++) { 83 | $dec = bcmul($dec, '2', 0); 84 | $dec = bcadd($dec, $bin[$i], 0); 85 | } 86 | return $dec; 87 | } 88 | 89 | /** 90 | * BC Math function to convert a BINARY string into a HEX string 91 | * 92 | * @param string $bin 93 | * @return string 94 | */ 95 | public static function bcbinhex($bin) 96 | { 97 | return self::bcdechex(self::bcbindec($bin)); 98 | } 99 | 100 | /** 101 | * BC Math function to convert a DECIMAL into a HEX string 102 | * 103 | * @param string $dec 104 | * @return string 105 | */ 106 | public static function bcdechex($dec) 107 | { 108 | $last = bcmod($dec, 16); 109 | $remain = bcdiv(bcsub($dec, $last, 0), 16, 0); 110 | return $remain == 0 ? dechex($last) : self::bcdechex($remain) . dechex($last); 111 | } 112 | 113 | /** 114 | * Bitwise AND two arbitrarily large numbers together. 115 | * 116 | * @param string $left 117 | * @param string $right 118 | * @return string 119 | */ 120 | public static function bcand($left, $right) 121 | { 122 | $len = self::_bitwise($left, $right); 123 | 124 | $value = ''; 125 | for ($i = 0; $i < $len; $i++) { 126 | $value .= (($left[$i] + 0) & ($right[$i] + 0)) ? '1' : '0'; 127 | } 128 | return self::bcbindec($value != '' ? $value : '0'); 129 | } 130 | 131 | /** 132 | * Bitwise OR two arbitrarily large numbers together. 133 | * 134 | * @param string $left 135 | * @param string $right 136 | * @return string 137 | */ 138 | public static function bcor($left, $right) 139 | { 140 | $len = self::_bitwise($left, $right); 141 | 142 | $value = ''; 143 | for ($i = 0; $i < $len; $i++) { 144 | $value .= (($left[$i] + 0) | ($right[$i] + 0)) ? '1' : '0'; 145 | } 146 | return self::bcbindec($value != '' ? $value : '0'); 147 | } 148 | 149 | /** 150 | * Bitwise XOR two arbitrarily large numbers together. 151 | * 152 | * @param string $left 153 | * @param string $right 154 | * @return string 155 | */ 156 | public static function bcxor($left, $right) 157 | { 158 | $len = self::_bitwise($left, $right); 159 | 160 | $value = ''; 161 | for ($i = 0; $i < $len; $i++) { 162 | $value .= (($left[$i] + 0) ^ ($right[$i] + 0)) ? '1' : '0'; 163 | } 164 | return self::bcbindec($value != '' ? $value : '0'); 165 | } 166 | 167 | /** 168 | * Bitwise NOT two arbitrarily large numbers together. 169 | * 170 | * @param string $left 171 | * @param int $bits 172 | * @return string 173 | */ 174 | public static function bcnot($left, $bits = null) 175 | { 176 | $right = 0; 177 | $len = self::_bitwise($left, $right, $bits); 178 | $value = ''; 179 | for ($i = 0; $i < $len; $i++) { 180 | $value .= $left[$i] == '1' ? '0' : '1'; 181 | } 182 | return self::bcbindec($value); 183 | } 184 | 185 | /** 186 | * Shift number to the left 187 | * 188 | * @param string $num 189 | * @param integer $bits Total bits to shift 190 | * @return string 191 | */ 192 | public static function bcleft($num, $bits) 193 | { 194 | return bcmul($num, bcpow('2', $bits)); 195 | } 196 | 197 | /** 198 | * Shift number to the right 199 | * 200 | * @param string $num 201 | * @param integer $bits Total bits to shift 202 | * @return string|null 203 | */ 204 | public static function bcright($num, $bits) 205 | { 206 | return bcdiv($num, bcpow('2', $bits)); 207 | } 208 | 209 | /** 210 | * Determine how many bits are needed to store the number rounded to the 211 | * nearest bit boundary. 212 | * 213 | * @param string $num 214 | * @param int $boundary 215 | * @return float|int 216 | */ 217 | public static function bits_needed($num, $boundary = 4) 218 | { 219 | $bits = 0; 220 | while ($num > 0) { 221 | $num = bcdiv($num, '2', 0); 222 | $bits++; 223 | } 224 | // round to nearest boundrary 225 | return $boundary ? ceil($bits / $boundary) * $boundary : $bits; 226 | } 227 | 228 | /** 229 | * BC Math function to return an arbitrarily large random number. 230 | * 231 | * @param string $min 232 | * @param string $max 233 | * @return string 234 | */ 235 | public static function bcrand($min, $max = null) 236 | { 237 | if ($max === null) { 238 | $max = $min; 239 | $min = 0; 240 | } 241 | 242 | // swap values if $min > $max 243 | if (bccomp($min, $max) == 1) { 244 | list($min, $max) = array($max, $min); 245 | } 246 | 247 | return bcadd( 248 | bcmul( 249 | bcdiv( 250 | mt_rand(0, mt_getrandmax()), 251 | mt_getrandmax(), 252 | strlen($max) 253 | ), 254 | bcsub( 255 | bcadd($max, '1'), 256 | $min 257 | ) 258 | ), 259 | $min 260 | ); 261 | } 262 | 263 | /** 264 | * Computes the natural logarithm using a series. 265 | * 266 | * @param string $num 267 | * @param int $iter 268 | * @param int $scale 269 | * @return string 270 | * @author Thomas Oldbury. 271 | * @license Public domain. 272 | */ 273 | public static function bclog($num, $iter = 10, $scale = 100) 274 | { 275 | $log = "0.0"; 276 | for ($i = 0; $i < $iter; $i++) { 277 | $pow = 1 + (2 * $i); 278 | $mul = bcdiv("1.0", $pow, $scale); 279 | $fraction = bcmul($mul, bcpow(bcsub($num, "1.0", $scale) / bcadd($num, "1.0", $scale), $pow, $scale), $scale); 280 | $log = bcadd($fraction, $log, $scale); 281 | } 282 | return bcmul("2.0", $log, $scale); 283 | } 284 | 285 | /** 286 | * Computes the base2 log using baseN log. 287 | * 288 | * @param string $num 289 | * @param int $iter 290 | * @param int $scale 291 | * @return string|null 292 | */ 293 | public static function bclog2($num, $iter = 10, $scale = 100) 294 | { 295 | return bcdiv(self::bclog($num, $iter, $scale), self::bclog("2", $iter, $scale), $scale); 296 | } 297 | 298 | /** 299 | * Rounds fractions down 300 | * 301 | * @param string $num 302 | * @return string 303 | */ 304 | public static function bcfloor($num) 305 | { 306 | if (substr($num, 0, 1) == '-') { 307 | return bcsub($num, 1, 0); 308 | } 309 | return bcadd($num, 0, 0); 310 | } 311 | 312 | /** 313 | * Rounds fractions up 314 | * 315 | * @param string $num 316 | * @return string 317 | */ 318 | public static function bcceil($num) 319 | { 320 | if (substr($num, 0, 1) == '-') { 321 | return bcsub($num, 0, 0); 322 | } 323 | return bcadd($num, 1, 0); 324 | } 325 | 326 | /** 327 | * Compare two numbers and return -1, 0, 1 depending if the LEFT number is 328 | * < = > the RIGHT. 329 | * 330 | * @param string|integer $left Left side operand 331 | * @param string|integer $right Right side operand 332 | * @return integer Return -1,0,1 for <=> comparison 333 | */ 334 | public static function cmp($left, $right) 335 | { 336 | // @todo could an optimization be done to determine if a normal 32bit 337 | // comparison could be done instead of using bccomp? But would 338 | // the number verification cause too much overhead to be useful? 339 | return bccomp($left, $right, 0); 340 | } 341 | 342 | /** 343 | * Internal function to prepare for bitwise operations 344 | * 345 | * @param string $left 346 | * @param string $right 347 | * @param int $bits 348 | * @return mixed 349 | */ 350 | private static function _bitwise(&$left, &$right, $bits = null) 351 | { 352 | if ($bits === null) { 353 | $bits = max(self::bits_needed($left), self::bits_needed($right)); 354 | } 355 | 356 | $left = self::bcdecbin($left); 357 | $right = self::bcdecbin($right); 358 | 359 | $len = max(strlen($left), strlen($right), (int)$bits); 360 | 361 | $left = sprintf("%0{$len}s", $left); 362 | $right = sprintf("%0{$len}s", $right); 363 | 364 | return $len; 365 | } 366 | 367 | } 368 | -------------------------------------------------------------------------------- /src/CIDR.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | namespace Lifo\IP; 11 | 12 | /** 13 | * CIDR Block helper class. 14 | * 15 | * Most routines can be used statically or by instantiating an object and 16 | * calling its methods. 17 | * 18 | * Provides routines to do various calculations on IP addresses and ranges. 19 | * Convert to/from CIDR to ranges, etc. 20 | */ 21 | class CIDR 22 | { 23 | const INTERSECT_NO = 0; 24 | const INTERSECT_YES = 1; 25 | const INTERSECT_LOW = 2; 26 | const INTERSECT_HIGH = 3; 27 | 28 | protected $start; 29 | protected $end; 30 | protected $prefix; 31 | protected $version; 32 | protected $istart; 33 | protected $iend; 34 | 35 | private $cache; 36 | 37 | /** 38 | * Create a new CIDR object. 39 | * 40 | * The IP range can be arbitrary and does not have to fall on a valid CIDR 41 | * range. Some methods will return different values depending if you ignore 42 | * the prefix or not. By default all prefix sensitive methods will assume 43 | * the prefix is used. 44 | * 45 | * @param string $cidr An IP address (1.2.3.4), CIDR block (1.2.3.4/24), 46 | * or range "1.2.3.4-1.2.3.10" 47 | * @param string $end Ending IP in range if no cidr/prefix is given 48 | */ 49 | public function __construct($cidr, $end = null) 50 | { 51 | if ($end !== null) { 52 | $this->setRange($cidr, $end); 53 | } else { 54 | $this->setCidr($cidr); 55 | } 56 | } 57 | 58 | /** 59 | * Returns the string representation of the CIDR block. 60 | */ 61 | public function __toString() 62 | { 63 | // do not include the prefix if its a single IP 64 | try { 65 | if ($this->isTrueCidr() && ( 66 | ($this->version == 4 and $this->prefix != 32) || 67 | ($this->version == 6 and $this->prefix != 128) 68 | ) 69 | ) { 70 | return $this->start . '/' . $this->prefix; 71 | } 72 | } catch (\Exception $e) { 73 | // isTrueCidr() calls getRange which can throw an exception 74 | } 75 | if (strcmp($this->start, $this->end) == 0) { 76 | return $this->start; 77 | } 78 | return $this->start . ' - ' . $this->end; 79 | } 80 | 81 | public function __clone() 82 | { 83 | // do not clone the cache. No real reason why. I just want to keep the 84 | // memory foot print as low as possible, even though this is trivial. 85 | $this->cache = array(); 86 | } 87 | 88 | /** 89 | * Set an arbitrary IP range. 90 | * The closest matching prefix will be calculated but the actual range 91 | * stored in the object can be arbitrary. 92 | * @param string $start Starting IP or combination "start-end" string. 93 | * @param string $end Ending IP or null. 94 | */ 95 | public function setRange($ip, $end = null) 96 | { 97 | if (strpos($ip, '-') !== false) { 98 | list($ip, $end) = array_map('trim', explode('-', $ip, 2)); 99 | } 100 | 101 | if (false === filter_var($ip, FILTER_VALIDATE_IP) || 102 | false === filter_var($end, FILTER_VALIDATE_IP)) { 103 | throw new \InvalidArgumentException("Invalid IP range \"$ip-$end\""); 104 | } 105 | 106 | // determine version (4 or 6) 107 | $this->version = (false === filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) ? 6 : 4; 108 | 109 | $this->istart = IP::inet_ptod($ip); 110 | $this->iend = IP::inet_ptod($end); 111 | 112 | // fix order 113 | if (bccomp($this->istart, $this->iend) == 1) { 114 | list($this->istart, $this->iend) = array($this->iend, $this->istart); 115 | list($ip, $end) = array($end, $ip); 116 | } 117 | 118 | $this->start = $ip; 119 | $this->end = $end; 120 | 121 | // calculate real prefix 122 | $len = $this->version == 4 ? 32 : 128; 123 | $this->prefix = $len - strlen(BC::bcdecbin(BC::bcxor($this->istart, $this->iend))); 124 | } 125 | 126 | /** 127 | * Returns true if the current IP is a true cidr block 128 | */ 129 | public function isTrueCidr() 130 | { 131 | return $this->start == $this->getNetwork() && $this->end == $this->getBroadcast(); 132 | } 133 | 134 | /** 135 | * Set the CIDR block. 136 | * 137 | * The prefix length is optional and will default to 32 ot 128 depending on 138 | * the version detected. 139 | * 140 | * @param string $cidr CIDR block string, eg: "192.168.0.0/24" or "2001::1/64" 141 | * @throws \InvalidArgumentException If the CIDR block is invalid 142 | */ 143 | public function setCidr($cidr) 144 | { 145 | if (strpos($cidr, '-') !== false) { 146 | return $this->setRange($cidr); 147 | } 148 | 149 | list($ip, $bits) = array_pad(array_map('trim', explode('/', $cidr, 2)), 2, null); 150 | if (false === filter_var($ip, FILTER_VALIDATE_IP)) { 151 | throw new \InvalidArgumentException("Invalid IP address \"$cidr\""); 152 | } 153 | 154 | // determine version (4 or 6) 155 | $this->version = (false === filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) ? 6 : 4; 156 | 157 | $this->start = $ip; 158 | $this->istart = IP::inet_ptod($ip); 159 | 160 | if ($bits !== null and $bits !== '') { 161 | $this->prefix = $bits; 162 | } else { 163 | $this->prefix = $this->version == 4 ? 32 : 128; 164 | } 165 | 166 | if (($this->prefix < 0) 167 | || ($this->prefix > 32 and $this->version == 4) 168 | || ($this->prefix > 128 and $this->version == 6)) { 169 | throw new \InvalidArgumentException("Invalid IP address \"$cidr\""); 170 | } 171 | 172 | $this->end = $this->getBroadcast(); 173 | $this->iend = IP::inet_ptod($this->end); 174 | 175 | $this->cache = array(); 176 | } 177 | 178 | /** 179 | * Get the IP version. 4 or 6. 180 | * 181 | * @return integer 182 | */ 183 | public function getVersion() 184 | { 185 | return $this->version; 186 | } 187 | 188 | /** 189 | * Get the prefix. 190 | * 191 | * Always returns the "proper" prefix, even if the IP range is arbitrary. 192 | * 193 | * @return integer 194 | */ 195 | public function getPrefix() 196 | { 197 | return $this->prefix; 198 | } 199 | 200 | /** 201 | * Return the starting presentational IP or Decimal value. 202 | * 203 | * Ignores prefix 204 | */ 205 | public function getStart($decimal = false) 206 | { 207 | return $decimal ? $this->istart : $this->start; 208 | } 209 | 210 | /** 211 | * Return the ending presentational IP or Decimal value. 212 | * 213 | * Ignores prefix 214 | */ 215 | public function getEnd($decimal = false) 216 | { 217 | return $decimal ? $this->iend : $this->end; 218 | } 219 | 220 | /** 221 | * Return the next presentational IP or Decimal value (following the 222 | * broadcast address of the current CIDR block). 223 | */ 224 | public function getNext($decimal = false) 225 | { 226 | $next = bcadd($this->getEnd(true), '1'); 227 | return $decimal ? $next : new self(IP::inet_dtop($next)); 228 | } 229 | 230 | /** 231 | * Returns true if the IP is an IPv4 232 | * 233 | * @return bool 234 | */ 235 | public function isIPv4() 236 | { 237 | return $this->version == 4; 238 | } 239 | 240 | /** 241 | * Returns true if the IP is an IPv6 242 | * 243 | * @return bool 244 | */ 245 | public function isIPv6() 246 | { 247 | return $this->version == 6; 248 | } 249 | 250 | /** 251 | * Get the cidr notation for the subnet block. 252 | * 253 | * This is useful for when you want a string representation of the IP/prefix 254 | * and the starting IP is not on a valid network boundrary (eg: Displaying 255 | * an IP from an interface). 256 | * 257 | * Note: The CIDR block returned is NOT always bit aligned. 258 | * 259 | * @return string IP in CIDR notation "start_ip/prefix" 260 | */ 261 | public function getCidr() 262 | { 263 | return $this->start . '/' . $this->prefix; 264 | } 265 | 266 | /** 267 | * Get the TRUE cidr notation for the subnet block. 268 | * 269 | * This is useful for when you want a string representation of the IP/prefix 270 | * and the starting IP is not on a valid network boundrary (eg: Displaying 271 | * an IP from an interface). 272 | * 273 | * Note: The CIDR block returned is ALWAYS bit aligned. 274 | * 275 | * @return string IP in CIDR notation "network/prefix" 276 | */ 277 | public function getTrueCidr() 278 | { 279 | return $this->getNetwork() . '/' . $this->prefix; 280 | } 281 | 282 | /** 283 | * Get the [low,high] range of the CIDR block 284 | * 285 | * Prefix sensitive. 286 | * 287 | * @param bool $ignorePrefix If true the arbitrary start-end range is 288 | * returned. default=false. 289 | */ 290 | public function getRange($ignorePrefix = false) 291 | { 292 | $range = $ignorePrefix 293 | ? array($this->start, $this->end) 294 | : self::cidr_to_range($this->start, $this->prefix); 295 | // watch out for IP '0' being converted to IPv6 '::' 296 | if ($range[0] == '::' and strpos($range[1], ':') == false) { 297 | $range[0] = '0.0.0.0'; 298 | } 299 | return $range; 300 | } 301 | 302 | /** 303 | * Return the IP in its fully expanded form. 304 | * 305 | * For example: 2001::1 == 2007:0000:0000:0000:0000:0000:0000:0001 306 | * 307 | * @see IP::inet_expand 308 | */ 309 | public function getExpanded() 310 | { 311 | return IP::inet_expand($this->start); 312 | } 313 | 314 | /** 315 | * Get network IP of the CIDR block 316 | * 317 | * Prefix sensitive. 318 | * 319 | * @param bool $ignorePrefix If true the arbitrary start-end range is 320 | * returned. default=false. 321 | */ 322 | public function getNetwork($ignorePrefix = false) 323 | { 324 | // micro-optimization to prevent calling getRange repeatedly 325 | $k = $ignorePrefix ? 1 : 0; 326 | if (!isset($this->cache['range'][$k])) { 327 | $this->cache['range'][$k] = $this->getRange($ignorePrefix); 328 | } 329 | return $this->cache['range'][$k][0]; 330 | } 331 | 332 | /** 333 | * Get broadcast IP of the CIDR block 334 | * 335 | * Prefix sensitive. 336 | * 337 | * @param bool $ignorePrefix If true the arbitrary start-end range is 338 | * returned. default=false. 339 | */ 340 | public function getBroadcast($ignorePrefix = false) 341 | { 342 | // micro-optimization to prevent calling getRange repeatedly 343 | $k = $ignorePrefix ? 1 : 0; 344 | if (!isset($this->cache['range'][$k])) { 345 | $this->cache['range'][$k] = $this->getRange($ignorePrefix); 346 | } 347 | return $this->cache['range'][$k][1]; 348 | } 349 | 350 | /** 351 | * Get the network mask based on the prefix. 352 | * 353 | */ 354 | public function getMask() 355 | { 356 | return self::prefix_to_mask($this->prefix, $this->version); 357 | } 358 | 359 | /** 360 | * Get total hosts within CIDR range 361 | * 362 | * Prefix sensitive. 363 | * 364 | * @param bool $ignorePrefix If true the arbitrary start-end range is 365 | * returned. default=false. 366 | */ 367 | public function getTotal($ignorePrefix = false) 368 | { 369 | // micro-optimization to prevent calling getRange repeatedly 370 | $k = $ignorePrefix ? 1 : 0; 371 | if (!isset($this->cache['range'][$k])) { 372 | $this->cache['range'][$k] = $this->getRange($ignorePrefix); 373 | } 374 | return bcadd(bcsub(IP::inet_ptod($this->cache['range'][$k][1]), 375 | IP::inet_ptod($this->cache['range'][$k][0])), '1'); 376 | } 377 | 378 | public function intersects($cidr) 379 | { 380 | return self::cidr_intersect((string)$this, $cidr); 381 | } 382 | 383 | /** 384 | * Determines the intersection between an IP (with optional prefix) and a 385 | * CIDR block. 386 | * 387 | * The IP will be checked against the CIDR block given and will either be 388 | * inside or outside the CIDR completely, or partially. 389 | * 390 | * NOTE: The caller should explicitly check against the INTERSECT_* 391 | * constants because this method will return a value > 1 even for partial 392 | * matches. 393 | * 394 | * @param mixed $ip The IP/cidr to match 395 | * @param mixed $cidr The CIDR block to match within 396 | * @return integer Returns an INTERSECT_* constant 397 | * @throws \InvalidArgumentException if either $ip or $cidr is invalid 398 | */ 399 | public static function cidr_intersect($ip, $cidr) 400 | { 401 | // use fixed length HEX strings so we can easily do STRING comparisons 402 | // instead of using slower bccomp() math. 403 | $map = function($v){ return sprintf("%032s", IP::inet_ptoh($v)); }; 404 | list($lo,$hi) = array_map($map, CIDR::cidr_to_range($ip)); 405 | list($min,$max) = array_map($map, CIDR::cidr_to_range($cidr)); 406 | 407 | /** visualization of logic used below 408 | lo-hi = $ip to check 409 | min-max = $cidr block being checked against 410 | --- --- --- lo --- --- hi --- --- --- --- --- IP/prefix to check 411 | --- min --- --- max --- --- --- --- --- --- --- Partial "LOW" match 412 | --- --- --- --- --- min --- --- max --- --- --- Partial "HIGH" match 413 | --- --- --- --- min max --- --- --- --- --- --- No match "NO" 414 | --- --- --- --- --- --- --- --- min --- max --- No match "NO" 415 | min --- max --- --- --- --- --- --- --- --- --- No match "NO" 416 | --- --- min --- --- --- --- max --- --- --- --- Full match "YES" 417 | */ 418 | 419 | // IP is exact match or completely inside the CIDR block 420 | if ($lo >= $min and $hi <= $max) { 421 | return self::INTERSECT_YES; 422 | } 423 | 424 | // IP is completely outside the CIDR block 425 | if ($max < $lo or $min > $hi) { 426 | return self::INTERSECT_NO; 427 | } 428 | 429 | // @todo is it useful to return LOW/HIGH partial matches? 430 | 431 | // IP matches the lower end 432 | if ($max <= $hi and $min <= $lo) { 433 | return self::INTERSECT_LOW; 434 | } 435 | 436 | // IP matches the higher end 437 | if ($min >= $lo and $max >= $hi) { 438 | return self::INTERSECT_HIGH; 439 | } 440 | 441 | return self::INTERSECT_NO; 442 | } 443 | 444 | /** 445 | * Converts an IPv4 or IPv6 CIDR block into its range. 446 | * 447 | * @todo May not be the fastest way to do this. 448 | * 449 | * @static 450 | * @param string $cidr CIDR block or IP address string. 451 | * @param integer|null $bits If /bits is not specified on string they can be 452 | * passed via this parameter instead. 453 | * @return array A 2 element array with the low, high range 454 | */ 455 | public static function cidr_to_range($cidr, $bits = null) 456 | { 457 | if (strpos($cidr, '/') !== false) { 458 | list($ip, $_bits) = array_pad(explode('/', $cidr, 2), 2, null); 459 | } else { 460 | $ip = $cidr; 461 | $_bits = $bits; 462 | } 463 | 464 | if (false === filter_var($ip, FILTER_VALIDATE_IP)) { 465 | throw new \InvalidArgumentException("IP address \"$cidr\" is invalid"); 466 | } 467 | 468 | // force bit length to 32 or 128 depending on type of IP 469 | $version = IP::isIPv4($ip) ? 4 : 6; 470 | $bitlen = $version == 4 ? 32 : 128; 471 | 472 | if ($bits === null) { 473 | // if no prefix is given use the length of the binary string which 474 | // will give us 32 or 128 and result in a single IP being returned. 475 | $bits = $_bits !== null ? $_bits : $bitlen; 476 | } 477 | 478 | if ($bits > $bitlen) { 479 | throw new \InvalidArgumentException("IP address \"$cidr\" is invalid"); 480 | } 481 | 482 | $ipdec = IP::inet_ptod($ip); 483 | $ipbin = BC::bcdecbin($ipdec, $bitlen); 484 | 485 | // calculate network 486 | $netmask = BC::bcbindec(str_pad(str_repeat('1',$bits), $bitlen, '0')); 487 | $ip1 = BC::bcand($ipdec, $netmask); 488 | 489 | // calculate "broadcast" (not technically a broadcast in IPv6) 490 | $ip2 = BC::bcor($ip1, BC::bcnot($netmask)); 491 | 492 | return array(IP::inet_dtop($ip1, $version), IP::inet_dtop($ip2, $version)); 493 | } 494 | 495 | /** 496 | * Return the CIDR string from the range given 497 | */ 498 | public static function range_to_cidr($start, $end) 499 | { 500 | $cidr = new CIDR($start, $end); 501 | return (string)$cidr; 502 | } 503 | 504 | /** 505 | * Return the maximum prefix length that would fit the IP address given. 506 | * 507 | * This is useful to determine how my bit would be needed to store the IP 508 | * address when you don't already have a prefix for the IP. 509 | * 510 | * @example 216.240.32.0 would return 27 511 | * 512 | * @param string $ip IP address without prefix 513 | * @param integer $bits Maximum bits to check; defaults to 32 for IPv4 and 128 for IPv6 514 | */ 515 | public static function max_prefix($ip, $bits = null) 516 | { 517 | static $mask = array(); 518 | 519 | $ver = (false === filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) ? 6 : 4; 520 | $max = $ver == 6 ? 128 : 32; 521 | if ($bits === null) { 522 | $bits = $max; 523 | 524 | } 525 | 526 | $int = IP::inet_ptod($ip); 527 | while ($bits > 0) { 528 | // micro-optimization; calculate mask once ... 529 | if (!isset($mask[$ver][$bits-1])) { 530 | // 2^$max - 2^($max - $bits); 531 | if ($ver == 4) { 532 | $mask[$ver][$bits-1] = pow(2, $max) - pow(2, $max - ($bits-1)); 533 | } else { 534 | $mask[$ver][$bits-1] = bcsub(bcpow(2, $max), bcpow(2, $max - ($bits-1))); 535 | } 536 | } 537 | 538 | $m = $mask[$ver][$bits-1]; 539 | //printf("%s/%d: %s & %s == %s\n", $ip, $bits-1, BC::bcdecbin($m, 32), BC::bcdecbin($int, 32), BC::bcdecbin(BC::bcand($int, $m))); 540 | //echo "$ip/", $bits-1, ": ", IP::inet_dtop($m), " ($m) & $int == ", BC::bcand($int, $m), "\n"; 541 | if (bccomp(BC::bcand($int, $m), $int) != 0) { 542 | return $bits; 543 | } 544 | $bits--; 545 | } 546 | return $bits; 547 | } 548 | 549 | /** 550 | * Return a contiguous list of true CIDR blocks that span the range given. 551 | * 552 | * Note: It's not a good idea to call this with IPv6 addresses. While it may 553 | * work for certain ranges this can be very slow. Also an IPv6 list won't be 554 | * as accurate as an IPv4 list. 555 | * 556 | * @example 557 | * range_to_cidrlist(192.168.0.0, 192.168.0.15) == 558 | * 192.168.0.0/28 559 | * range_to_cidrlist(192.168.0.0, 192.168.0.20) == 560 | * 192.168.0.0/28 561 | * 192.168.0.16/30 562 | * 192.168.0.20/32 563 | */ 564 | public static function range_to_cidrlist($start, $end) 565 | { 566 | $ver = (false === filter_var($start, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) ? 6 : 4; 567 | $start = IP::inet_ptod($start); 568 | $end = IP::inet_ptod($end); 569 | 570 | $len = $ver == 4 ? 32 : 128; 571 | $log2 = $ver == 4 ? log(2) : BC::bclog(2); 572 | 573 | $list = array(); 574 | while (BC::cmp($end, $start) >= 0) { // $end >= $start 575 | $prefix = self::max_prefix(IP::inet_dtop($start), $len); 576 | if ($ver == 4) { 577 | $diff = $len - floor( log($end - $start + 1) / $log2 ); 578 | } else { 579 | // this is not as accurate due to the bclog function 580 | $diff = bcsub($len, BC::bcfloor(bcdiv(BC::bclog(bcadd(bcsub($end, $start), '1')), $log2))); 581 | } 582 | 583 | if ($prefix < $diff) { 584 | $prefix = $diff; 585 | } 586 | 587 | $list[] = IP::inet_dtop($start) . "/" . $prefix; 588 | 589 | if ($ver == 4) { 590 | $start += pow(2, $len - $prefix); 591 | } else { 592 | $start = bcadd($start, bcpow(2, $len - $prefix)); 593 | } 594 | } 595 | return $list; 596 | } 597 | 598 | /** 599 | * Return an list of optimized CIDR blocks by collapsing adjacent CIDR 600 | * blocks into larger blocks. 601 | * 602 | * @param array $cidrs List of CIDR block strings or objects 603 | * @param integer $maxPrefix Maximum prefix to allow 604 | * @return array Optimized list of CIDR objects 605 | */ 606 | public static function optimize_cidrlist($cidrs, $maxPrefix = 32) 607 | { 608 | // all indexes must be a CIDR object 609 | $cidrs = array_map(function($o){ return $o instanceof CIDR ? $o : new CIDR($o); }, $cidrs); 610 | // sort CIDR blocks in proper order so we can easily loop over them 611 | $cidrs = self::cidr_sort($cidrs); 612 | 613 | $list = array(); 614 | while ($cidrs) { 615 | $c = array_shift($cidrs); 616 | $start = $c->getStart(); 617 | 618 | $max = bcadd($c->getStart(true), $c->getTotal()); 619 | 620 | // loop through each cidr block until its ending range is more than 621 | // the current maximum. 622 | while (!empty($cidrs) and $cidrs[0]->getStart(true) <= $max) { 623 | $b = array_shift($cidrs); 624 | $newmax = bcadd($b->getStart(true), $b->getTotal()); 625 | if ($newmax > $max) { 626 | $max = $newmax; 627 | } 628 | } 629 | 630 | // add the new cidr range to the optimized list 631 | $list = array_merge($list, self::range_to_cidrlist($start, IP::inet_dtop(bcsub($max, '1')))); 632 | } 633 | 634 | return $list; 635 | } 636 | 637 | /** 638 | * Sort the list of CIDR blocks, optionally with a custom callback function. 639 | * 640 | * @param array $cidrs A list of CIDR blocks (strings or objects) 641 | * @param Closure $callback Optional callback to perform the sorting. 642 | * See PHP usort documentation for more details. 643 | */ 644 | public static function cidr_sort($cidrs, $callback = null) 645 | { 646 | // all indexes must be a CIDR object 647 | $cidrs = array_map(function($o){ return $o instanceof CIDR ? $o : new CIDR($o); }, $cidrs); 648 | 649 | if ($callback === null) { 650 | $callback = function($a, $b) { 651 | if (0 != ($o = BC::cmp($a->getStart(true), $b->getStart(true)))) { 652 | return $o; // < or > 653 | } 654 | if ($a->getPrefix() == $b->getPrefix()) { 655 | return 0; 656 | } 657 | return $a->getPrefix() < $b->getPrefix() ? -1 : 1; 658 | }; 659 | } elseif (!($callback instanceof \Closure) or !is_callable($callback)) { 660 | throw new \InvalidArgumentException("Invalid callback in CIDR::cidr_sort, expected Closure, got " . gettype($callback)); 661 | } 662 | 663 | usort($cidrs, $callback); 664 | return $cidrs; 665 | } 666 | 667 | /** 668 | * Return the Prefix bits from the IPv4 mask given. 669 | * 670 | * This is only valid for IPv4 addresses since IPv6 addressing does not 671 | * have a concept of network masks. 672 | * 673 | * Example: 255.255.255.0 == 24 674 | * 675 | * @param string $mask IPv4 network mask. 676 | */ 677 | public static function mask_to_prefix($mask) 678 | { 679 | if (false === filter_var($mask, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { 680 | throw new \InvalidArgumentException("Invalid IP netmask \"$mask\""); 681 | } 682 | return strrpos(IP::inet_ptob($mask, 32), '1') + 1; 683 | } 684 | 685 | /** 686 | * Return the network mask for the prefix given. 687 | * 688 | * Normally this is only useful for IPv4 addresses but you can generate a 689 | * mask for IPv6 addresses as well, only because its mathematically 690 | * possible. 691 | * 692 | * @param integer $prefix CIDR prefix bits (0-128) 693 | * @param integer $version IP version. If null the version will be detected 694 | * based on the prefix length given. 695 | */ 696 | public static function prefix_to_mask($prefix, $version = null) 697 | { 698 | if ($version === null) { 699 | $version = $prefix > 32 ? 6 : 4; 700 | } 701 | if ($prefix < 0 or $prefix > 128) { 702 | throw new \InvalidArgumentException("Invalid prefix length \"$prefix\""); 703 | } 704 | if ($version != 4 and $version != 6) { 705 | throw new \InvalidArgumentException("Invalid version \"$version\". Must be 4 or 6"); 706 | } 707 | 708 | if ($version == 4) { 709 | return long2ip($prefix == 0 ? 0 : (0xFFFFFFFF >> (32 - $prefix)) << (32 - $prefix)); 710 | } else { 711 | return IP::inet_dtop($prefix == 0 ? 0 : BC::bcleft(BC::bcright(BC::MAX_UINT_128, 128-$prefix), 128-$prefix)); 712 | } 713 | } 714 | 715 | /** 716 | * Return true if the $ip given is a true CIDR block. 717 | * 718 | * A true CIDR block is one where the $ip given is the actual Network 719 | * address and broadcast matches the prefix appropriately. 720 | */ 721 | public static function cidr_is_true($ip) 722 | { 723 | $ip = new CIDR($ip); 724 | return $ip->isTrueCidr(); 725 | } 726 | } 727 | --------------------------------------------------------------------------------