Dumping this here so I can refer to this when needed and in case anyone is looking for this.
function inet_prefixlen_start(string $inet, int $version, int $prefixlen): ?string { switch ($version) { case 4: if (filter_var($inet, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) !== false) { $pack = 'c*'; $size = 8; } break; case 6: if (filter_var($inet, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false) { $pack = 'n*'; $size = 16; } break; } if (!isset($pack)) { return null; } $ipArray = unpack($pack, inet_pton($inet)); $groupMask = (1 << $size) - 1; $fullSize = $size * count($ipArray); $groupCount = count($ipArray); for ($i = 0; $i < $groupCount; $i++) { $mask = $groupMask; $indexStart = $i * $size; for ($j = 0; $j < $size; $j++) { $fullIndex = $indexStart + $j; if ($fullIndex < $prefixlen) { continue; } // shifting goes from the end, one less of size: // (size 8) // i = 0 -> shift = 7 (1000 0000) // i = 7 -> shift = 0 (0000 0001) $index = $size - $j - 1; $mask ^= 1 << $index; } // ipArray is 1-indexed because that's how unpack works $group = $i + 1; $ipArray[$group] = $ipArray[$group] & $mask; } return inet_ntop(pack($pack, ...$ipArray)); } /** some sample data ['1.2.3.4', 4, 24, '1.2.3.0'], ['1.2.3.5', 4, 31, '1.2.3.4'], ['::10.0.0.3', 4, 32, null], ['1:2:3:4::', 4, 24, null], ['invalid', 4, 32, null], ['1:2:3:4::1', 6, 64, '1:2:3:4::'], ['1:2:3::1', 6, 64, '1:2:3::'], ['1:2:3:4:5:6:7:8', 6, 64, '1:2:3:4::'], ['1::4:5:6:7:8', 6, 64, '1:0:0:4::'], ['1:2:3:4::10.0.0.1', 6, 64, '1:2:3:4::'], ['1:2:3:ffff::1', 6, 56, '1:2:3:ff00::'], ['::3', 6, 127, '::2'], ['1.2.3.4', 6, 128, null], ['invalid', 6, 128, null], */
Or just use one of those IP handling libraries.