65535) { return null; } } $normalizedHost = filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) ? '['.$host.']' : $host; $normalized = 'https://'.$normalizedHost; if (isset($parts['port'])) { $normalized .= ':'.(string) (int) $parts['port']; } return $normalized; } public static function isPublicIp(string $ip): bool { if (filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE, ) === false) { return false; } if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { $v = ip2long($ip); if ($v === false) { return false; } // PHP 在 macOS 上 ip2long 可能为有符号,强转为 int64 统一处理 $v = (int) $v; return ! self::ipInRangesV4((int) $v, [ // 0.0.0.0/8 ['base' => ip2long('0.0.0.0'), 'mask' => 0xFF000000], // 10.0.0.0/8 ['base' => ip2long('10.0.0.0'), 'mask' => 0xFF000000], // 127.0.0.0/8 ['base' => ip2long('127.0.0.0'), 'mask' => 0xFF000000], // 169.254.0.0/16 ['base' => ip2long('169.254.0.0'), 'mask' => 0xFFFF0000], // 172.16.0.0/12 ['base' => ip2long('172.16.0.0'), 'mask' => 0xFFF00000], // 192.168.0.0/16 ['base' => ip2long('192.168.0.0'), 'mask' => 0xFFFF0000], // 100.64.0.0/10 (CGNAT) ['base' => ip2long('100.64.0.0'), 'mask' => 0xFFC00000], // 192.0.0.0/24 (IETF Protocol Assignments) ['base' => ip2long('192.0.0.0'), 'mask' => 0xFFFFFF00], // Documentation and deprecated relay ranges ['base' => ip2long('192.0.2.0'), 'mask' => 0xFFFFFF00], ['base' => ip2long('192.88.99.0'), 'mask' => 0xFFFFFF00], ['base' => ip2long('198.51.100.0'), 'mask' => 0xFFFFFF00], ['base' => ip2long('203.0.113.0'), 'mask' => 0xFFFFFF00], // 198.18.0.0/15 (benchmarking) ['base' => ip2long('198.18.0.0'), 'mask' => 0xFFFE0000], // 224.0.0.0/4 (multicast) ['base' => ip2long('224.0.0.0'), 'mask' => 0xF0000000], // 240.0.0.0/4 (reserved for future use) ['base' => ip2long('240.0.0.0'), 'mask' => 0xF0000000], ]); } // IPv6:仅做关键保守段拦截(避免复杂数值比较引入 bug) if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { $bin = inet_pton($ip); if ($bin === false) { return false; } // IPv6 ::1 (loopback) if (substr($bin, 0, 15) === str_repeat("\0", 15) && $bin[15] === "\1") { return false; } // IPv6 unspecified :: if ($bin === str_repeat("\0", 16)) { return false; } $b0 = ord($bin[0]); $b1 = ord($bin[1]); // ff00::/8 multicast if ($b0 === 0xFF) { return false; } // fc00::/7 unique local => fc or fd if ($b0 === 0xFC || $b0 === 0xFD) { return false; } // fe80::/10 link-local => fe + (second byte & 0xC0) == 0x80 if ($b0 === 0xFE && (($b1 & 0xC0) === 0x80)) { return false; } // IPv4-mapped ::ffff:0:0/96 => 检查最后 4 字节映射的 IPv4 是否为私网 if (substr($bin, 0, 10) === str_repeat("\0", 10) && substr($bin, 10, 2) === "\xFF\xFF") { $v4bin = substr($bin, 12, 4); $v4 = inet_ntop($v4bin); // inet_ntop 对 v4bin 有时返回 false,这里保守拒绝 if ($v4 === false) { return false; } return self::isPublicIp($v4); } // IPv4 translation prefixes can otherwise tunnel a private IPv4 target. $isIpv4Translation = substr($bin, 0, 12) === "\x00\x64\xFF\x9B\x00\x00\x00\x00\x00\x00\x00\x00" || substr($bin, 0, 6) === "\x00\x64\xFF\x9B\x00\x01"; // 100::/64 discard-only, 2001::/23 protocol assignments, // 2001:db8::/32 and 3fff::/20 documentation, 2002::/16 deprecated 6to4. if ($isIpv4Translation || substr($bin, 0, 8) === "\x00\x64\x00\x00\x00\x00\x00\x00" || ($b0 === 0x20 && $b1 === 0x01 && (ord($bin[2]) & 0xFE) === 0) || substr($bin, 0, 4) === "\x20\x01\x0D\xB8" || ($b0 === 0x3F && ($b1 & 0xF0) === 0xF0) || ($b0 === 0x20 && $b1 === 0x02) ) { return false; } return true; } // 非法 IP:保守拒绝 return false; } private static function ipInRangesV4(int $v, array $ranges): bool { foreach ($ranges as $r) { $base = (int) $r['base']; $mask = (int) $r['mask']; if (($v & $mask) === ($base & $mask)) { return true; } } return false; } }