From 97501642b8efafcf900c67cfefa3ef3b97314efc Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Mon, 28 Feb 2022 09:42:09 +0100 Subject: [PATCH] new: [tools:CidrTool] Ported CidrTool from MISP --- src/Lib/Tools/CidrTool.php | 147 +++++++++++++++++++++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 src/Lib/Tools/CidrTool.php diff --git a/src/Lib/Tools/CidrTool.php b/src/Lib/Tools/CidrTool.php new file mode 100644 index 0000000..dda7f87 --- /dev/null +++ b/src/Lib/Tools/CidrTool.php @@ -0,0 +1,147 @@ +filterInputList($list); + } + + /** + * @param string $value IPv4 or IPv6 address or range + * @return false|string + */ + public function contains($value) + { + $valueMask = null; + if (strpos($value, '/') !== false) { + list($value, $valueMask) = explode('/', $value); + } + + $match = false; + if (filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { + // This code converts IP address to all possible CIDRs that can contains given IP address + // and then check if given hash table contains that CIDR. + $ip = ip2long($value); + // Start from 1, because doesn't make sense to check 0.0.0.0/0 match + for ($bits = $this->minimumIpv4Mask; $bits <= 32; $bits++) { + $mask = -1 << (32 - $bits); + $needle = long2ip($ip & $mask) . "/$bits"; + if (isset($this->ipv4[$needle])) { + $match = $needle; + break; + } + } + } elseif (filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { + $value = unpack('n*', inet_pton($value)); + foreach ($this->ipv6 as $netmask => $lv) { + foreach ($lv as $l) { + if ($this->ipv6InCidr($value, $l, $netmask)) { + $match = inet_ntop($l) . "/$netmask"; + break; + } + } + } + } + + if ($match && $valueMask) { + $matchMask = explode('/', $match)[1]; + if ($valueMask < $matchMask) { + return false; + } + } + + return $match; + } + + /** + * @param string $cidr + * @return bool + */ + public static function validate($cidr) + { + $parts = explode('/', $cidr, 2); + $ipBytes = inet_pton($parts[0]); + if ($ipBytes === false) { + return false; + } + + $maximumNetmask = strlen($ipBytes) === 4 ? 32 : 128; + if (isset($parts[1]) && ($parts[1] > $maximumNetmask || $parts[1] < 0)) { + return false; // Netmask part of CIDR is invalid + } + + return true; + } + + /** + * Using solution from https://github.com/symfony/symfony/blob/master/src/Symfony/Component/HttpFoundation/IpUtils.php + * + * @param array $ip + * @param string $cidr + * @param int $netmask + * @return bool + */ + private function ipv6InCidr($ip, $cidr, $netmask) + { + $bytesAddr = unpack('n*', $cidr); + for ($i = 1, $ceil = ceil($netmask / 16); $i <= $ceil; ++$i) { + $left = $netmask - 16 * ($i - 1); + $left = ($left <= 16) ? $left : 16; + $mask = ~(0xffff >> $left) & 0xffff; + if (($bytesAddr[$i] & $mask) != ($ip[$i] & $mask)) { + return false; + } + } + + return true; + } + + /** + * Filter out invalid IPv4 or IPv4 CIDR and append maximum netmask if no netmask is given. + * @param array $list + */ + private function filterInputList(array $list) + { + foreach ($list as $v) { + $parts = explode('/', $v, 2); + $ipBytes = inet_pton($parts[0]); + if ($ipBytes === false) { + continue; // IP address part of CIDR is invalid + } + $maximumNetmask = strlen($ipBytes) === 4 ? 32 : 128; + + if (isset($parts[1]) && ($parts[1] > $maximumNetmask || $parts[1] < 0)) { + // Netmask part of CIDR is invalid + continue; + } + + $mask = isset($parts[1]) ? $parts[1] : $maximumNetmask; + if ($maximumNetmask === 32) { + if ($mask < $this->minimumIpv4Mask) { + $this->minimumIpv4Mask = (int)$mask; + } + if (!isset($parts[1])) { + $v = "$v/$maximumNetmask"; // If CIDR doesnt contains '/', we will consider CIDR as /32 + } + $this->ipv4[$v] = true; + } else { + $this->ipv6[$mask][] = $ipBytes; + } + } + } +}