%PDF- %PDF-
Direktori : /backups/router/usr/local/opnsense/mvc/app/library/OPNsense/Firewall/ |
Current File : //backups/router/usr/local/opnsense/mvc/app/library/OPNsense/Firewall/Util.php |
<?php /* * Copyright (C) 2017-2023 Deciso B.V. * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE * AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ namespace OPNsense\Firewall; use OPNsense\Core\Config; use OPNsense\Firewall\Alias; /** * Class Util, common static firewall support functions * @package OPNsense\Firewall */ class Util { /** * @var null|Alias reference to alias object */ private static $aliasObject = null; /** * @var null|array cached alias descriptions */ private static $aliasDescriptions = []; /** * @var array cached getservbyname results */ private static $servbynames = []; /** * is provided address an ip address. * @param string $address network address * @return boolean */ public static function isIpAddress($address) { return !empty(filter_var($address, FILTER_VALIDATE_IP)); } public static function isIpv4Address($address) { return !empty(filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)); } public static function isIpv6Address($address) { return !empty(filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)); } /** * is provided address an link-locak IPv6 address. * @param string $address network address * @return boolean */ public static function isLinkLocal($address) { return !!preg_match('/^fe[89ab][0-9a-f]:/i', $address); } /** * is provided address a mac address. * @param string $address network address * @return boolean */ public static function isMACAddress($address) { return !empty(filter_var($address, FILTER_VALIDATE_MAC)); } /** * is provided network valid * @param string $network network * @return boolean */ public static function isSubnet($network) { $tmp = explode('/', $network); if (count($tmp) == 2) { if (self::isIpAddress($tmp[0]) && ctype_digit($tmp[1]) && abs($tmp[1]) == $tmp[1]) { if (strpos($tmp[0], ':') !== false && $tmp[1] <= 128) { // subnet v6 return true; } elseif ($tmp[1] <= 32) { // subnet v4 return true; } } } return false; } /** * is provided network strict (host bits not set) * @param string $network network * @return boolean */ public static function isSubnetStrict($network) { if (self::isSubnet($network)) { list($net, $mask) = explode('/', $network); $ip_net = inet_pton($net); $bits = (strpos($net, ":") !== false && $mask <= 128) ? 128 : 32; $ip_mask = ""; $significant_bits = $mask; for ($i = 0; $i < $bits / 8; $i++) { if ($significant_bits >= 8) { $ip_mask .= chr(0xFF); $significant_bits -= 8; } else { $ip_mask .= chr(~(0xFF >> $significant_bits)); $significant_bits = 0; } } return $ip_net == ($ip_net & $ip_mask); } return false; } /** * is provided network a valid wildcard (https://en.wikipedia.org/wiki/Wildcard_mask) * @param string $network network * @return boolean */ public static function isWildcard($network) { $tmp = explode('/', $network); if (count($tmp) == 2) { if (self::isIpAddress($tmp[0]) && self::isIpAddress($tmp[1])) { if (strpos($tmp[0], ':') !== false && strpos($tmp[1], ':') !== false) { return true; } elseif (strpos($tmp[0], ':') === false && strpos($tmp[1], ':') === false) { return true; } } } return false; } /** * use provided alias object instead of creating one. When modifying multiple aliases referencing each other * we need to use the same object for validations. * @param Alias $alias object to link */ public static function attachAliasObject($alias) { self::$aliasObject = $alias; if ($alias != null) { $alias->flushCache(); } } /** * check if name exists in alias config section * @param string $name name * @param boolean $valid check if the alias can safely be used * @return boolean * @throws \OPNsense\Base\ModelException */ public static function isAlias($name, $valid = false) { if (self::$aliasObject == null) { // Cache the alias object to avoid object creation overhead. self::$aliasObject = new Alias(); self::$aliasObject->flushCache(); } if (!empty($name)) { $alias = self::$aliasObject->getByName($name); if ($alias != null) { if ($valid) { // check validity for port type aliases if (preg_match("/port/i", (string)$alias->type) && empty((string)$alias->content)) { return false; } } return true; } } return false; } /** * return alias descriptions * @param string $name name * @return string */ public static function aliasDescription($name) { if (empty(self::$aliasDescriptions)) { // read all aliases at once, and cache descriptions. foreach ((new Alias())->aliasIterator() as $alias) { if (empty(self::$aliasDescriptions[$alias['name']])) { if (!empty($alias['description'])) { self::$aliasDescriptions[$alias['name']] = '<strong>' . $alias['description'] . '</strong><br/>'; } else { self::$aliasDescriptions[$alias['name']] = ""; } if (!empty($alias['content'])) { $tmp = array_slice($alias['content'], 0, 10); asort($tmp, SORT_NATURAL | SORT_FLAG_CASE); if (count($alias['content']) > 10) { $tmp[] = '[...]'; } self::$aliasDescriptions[$alias['name']] .= implode("<br/>", $tmp); } } } } if (!empty(self::$aliasDescriptions[$name])) { return self::$aliasDescriptions[$name]; } else { return null; } } /** * Fetch port alias contents, other alias types are handled using tables so there usually no need * to know the contents within any of the scripts. * @param string $name name * @param array $aliases aliases already parsed (prevent deadlock) * @return array containing all ports or addresses * @throws \OPNsense\Base\ModelException when unable to create alias model */ public static function getPortAlias($name, $aliases = array()) { if (self::$aliasObject == null) { // Cache the alias object to avoid object creation overhead. self::$aliasObject = new Alias(); } $result = array(); foreach (self::$aliasObject->aliasIterator() as $node) { if (!empty($name) && (string)$node['name'] == $name && $node['type'] == 'port') { $aliases[] = $name; foreach ($node['content'] as $address) { if (Util::isAlias($address)) { if (!in_array($address, $aliases)) { foreach (Util::getPortAlias($address, $aliases) as $port) { if (!in_array($port, $result)) { $result[] = $port; } } } } elseif (!in_array($address, $result)) { $result[] = $address; } } } } return $result; } /** * cached version of getservbyname() existence check * @param string $service service name * @return boolean */ public static function getservbyname($service) { if (empty(self::$servbynames)) { foreach (explode("\n", file_get_contents('/etc/services')) as $line) { if (strlen($line) > 1 && $line[0] != '#') { foreach (preg_split('/\s+/', $line) as $idx => $tmp) { if ($tmp[0] == '#') { break; } elseif ($idx != 1) { self::$servbynames[$tmp] = true; } } } } } return isset(self::$servbynames[$service]); } /** * check if name exists in alias config section * @param string $number port number or range * @param boolean $allow_range ranges allowed * @return boolean */ public static function isPort($number, $allow_range = true) { $tmp = $number !== null ? explode(':', $number) : []; foreach ($tmp as $port) { if ( (filter_var($port, FILTER_VALIDATE_INT, array( "options" => array("min_range" => 1, "max_range" => 65535))) === false || !ctype_digit($port)) && !self::getservbyname($port) ) { return false; } } if (($allow_range && count($tmp) <= 2) || count($tmp) == 1) { return true; } return false; } /** * Check if provided string is a valid domain name * @param string $domain * @return false|int */ public static function isDomain($domain) { $pattern = '/^(?:(?:[a-z\pL0-9]|[a-z\pL0-9][a-z\pL0-9\-]*[a-z\pL0-9])\.)*(?:[a-z\pL0-9]|[a-z\pL0-9][a-z\pL0-9\-]*[a-z\pL0-9])$/iu'; $parts = explode(".", $domain); if (ctype_digit($parts[0]) && ctype_digit($parts[count($parts) - 1])) { // according to rfc1123 2.1 // a valid host name can never have the dotted-decimal form #.#.#.#, since at least the highest-level // component label will be alphabetic. return false; } elseif (preg_match($pattern, $domain)) { return true; } return false; } /** * calculate rule hash value * @param array $rule * @return string */ public static function calcRuleHash($rule) { // remove irrelevant fields foreach (array('updated', 'created', 'descr') as $key) { unset($rule[$key]); } ksort($rule); foreach ($rule as &$value) { if (is_array($value)) { ksort($value); } } return md5(json_encode($rule)); } private static function isIPv4InCIDR($ip, $cidr) { list ($subnet, $bits) = explode('/', $cidr); if ($bits === null) { $bits = 32; } $ip = ip2long($ip); $subnet = ip2long($subnet); $mask = -1 << (32 - $bits); $subnet &= $mask; return ($ip & $mask) == $subnet; } private static function isIPv6InCIDR($ip, $cidr) { $inet_to_bits = function ($ip) { $split = str_split($ip); $bin_ip = ''; foreach ($split as $char) { $bin_ip .= str_pad(decbin(ord($char)), 8, '0', STR_PAD_LEFT); } return $bin_ip; }; $in_addr = inet_pton($ip); $bin_ip = $inet_to_bits($in_addr); list ($net, $maskbits) = explode('/', $cidr); $net = inet_pton($net); $bin_net = $inet_to_bits($net); $ip_net_bits = substr($bin_ip, 0, $maskbits); $net_bits = substr($bin_net, 0, $maskbits); return $ip_net_bits === $net_bits; } /** * returns whether a given IP (v4 or v6) is in a CIDR block */ public static function isIPInCIDR($ip, $cidr) { if (!self::isIpAddress($ip) || !self::isSubnet($cidr)) { return false; } elseif (str_contains($ip, ':') && str_contains($cidr, ':')) { return self::isIPv6InCIDR($ip, $cidr); } elseif (!str_contains($ip, ':') && !str_contains($cidr, ':')) { return self::isIPv4InCIDR($ip, $cidr); } return false; } /** * convert ipv4 cidr to netmask e.g. 24 --> 255.255.255.0 * @param int $bits ipv4 bits * @return string netmask */ public static function CIDRToMask($bits) { return long2ip(0xFFFFFFFF << (32 - $bits)); } /** * Find the smallest possible subnet mask for given IP range * @param array $ips (start, end) * @param string $family inet6 or inet * @return int smallest mask */ public static function smallestCIDR($ips, $family = 'inet') { if ($family == 'inet6') { foreach ($ips as $id => $ip) { $ips[$id] = unpack('N*', inet_pton($ip)); } for ($bits = 0; $bits <= 128; $bits += 1) { $mask1 = (0xffffffff << max($bits - 96, 0)) & 0xffffffff; $mask2 = (0xffffffff << max($bits - 64, 0)) & 0xffffffff; $mask3 = (0xffffffff << max($bits - 32, 0)) & 0xffffffff; $mask4 = (0xffffffff << $bits) & 0xffffffff; $test = []; foreach ($ips as $ip) { $test[sprintf('%032b%032b%032b%032b', $ip[1] & $mask1, $ip[2] & $mask2, $ip[3] & $mask3, $ip[4] & $mask4)] = true; } if (count($test) == 1) { /* one element means CIDR size matches all */ break; } } return 128 - $bits; } else { foreach ($ips as $id => $ip) { $ips[$id] = ip2long($ip); } for ($bits = 0; $bits <= 32; $bits += 1) { $mask = (0xffffffff << $bits) & 0xffffffff; $test = []; foreach ($ips as $ip) { $test[$ip & $mask] = true; } if (count($test) == 1) { /* one element means CIDR size matches all */ break; } } return 32 - $bits; } } /** * convert cidr to array containing a start and end address * @param array $cidr ipv4/6 cidr range definition * @return array|bool address range or false when not a valid cidr */ public static function cidrToRange($cidr) { if (!self::isSubnet($cidr)) { return false; } list ($ipaddr, $bits) = explode('/', $cidr); $inet_ip = inet_pton($ipaddr); if (str_contains($ipaddr, ':')) { /* IPv6 */ $size = 128 - (int)$bits; $mask = [ 1 => (0xffffffff << max($size - 96, 0)) & 0xffffffff, 2 => (0xffffffff << max($size - 64, 0)) & 0xffffffff, 3 => (0xffffffff << max($size - 32, 0)) & 0xffffffff, 4 => (0xffffffff << $size) & 0xffffffff, ]; $netmask_parts = []; for ($pos = 1; $pos <= 4; $pos += 1) { $netmask_parts = array_merge($netmask_parts, str_split(sprintf('%08x', $mask[$pos]), 4)); } $inet_mask = inet_pton(implode(':', $netmask_parts)); } else { /* IPv4 */ $size = 32 - (int)$bits; $inet_mask = inet_pton(long2ip((0xffffffff << $size) & 0xffffffff)); } $inet_start = $inet_ip & $inet_mask; $inet_end = $inet_ip | ~$inet_mask; return [inet_ntop($inet_start), inet_ntop($inet_end)]; } /** * @param array $cidr ipv4/6 cidr range definition * @return Generator yielding usable addresses in cidr range (max size for ipv6 is 32 bits) */ public static function cidrRangeIterator($cidr) { $range = self::cidrToRange($cidr); if ($range) { $bits = explode('/', $cidr)[1]; $inet_start = inet_pton($range[0]); if (str_contains($range[0], ':')) { for ($i = 0; $i < pow(2, min(128 - (int)$bits, 32)); ++$i) { yield inet_ntop($inet_start | inet_pton('0000::' . dechex($i))); } } else { /** * For ipv4, skip network and broadcast addresses, * unless size is >= 31 in which case these may be omitted acording to RFC3021 */ $range_start = (int)$bits >= 31 ? 0 : 1; $range_stop = (int)$bits >= 31 ? pow(2, 32 - (int)$bits) : pow(2, 32 - (int)$bits) - 1; for ($i = $range_start; $i < $range_stop; ++$i) { yield inet_ntop($inet_start | inet_pton(long2ip($i))); } } } } }