mirror of
https://github.com/tiennm99/coolify.git
synced 2026-06-28 19:08:36 +00:00
fix(server): handle limit edge case and IPv6 allowlist dedupe
Update server limit enforcement to re-enable force-disabled servers when the team is at or under its limit (`<= 0` condition). Improve allowlist validation and matching by: - supporting IPv6 CIDR mask ranges up to `/128` - adding IPv6-aware CIDR matching in `checkIPAgainstAllowlist` - normalizing/deduplicating redundant allowlist entries before saving Add feature tests for `ServerLimitCheckJob` covering under-limit, at-limit, over-limit, and no-op scenarios.
This commit is contained in:
@@ -1416,24 +1416,48 @@ function checkIPAgainstAllowlist($ip, $allowlist)
|
||||
}
|
||||
|
||||
$mask = (int) $mask;
|
||||
$isIpv6Subnet = filter_var($subnet, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false;
|
||||
$maxMask = $isIpv6Subnet ? 128 : 32;
|
||||
|
||||
// Validate mask
|
||||
if ($mask < 0 || $mask > 32) {
|
||||
// Validate mask for address family
|
||||
if ($mask < 0 || $mask > $maxMask) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Calculate network addresses
|
||||
$ip_long = ip2long($ip);
|
||||
$subnet_long = ip2long($subnet);
|
||||
if ($isIpv6Subnet) {
|
||||
// IPv6 CIDR matching using binary string comparison
|
||||
$ipBin = inet_pton($ip);
|
||||
$subnetBin = inet_pton($subnet);
|
||||
|
||||
if ($ip_long === false || $subnet_long === false) {
|
||||
continue;
|
||||
}
|
||||
if ($ipBin === false || $subnetBin === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$mask_long = ~((1 << (32 - $mask)) - 1);
|
||||
// Build a 128-bit mask from $mask prefix bits
|
||||
$maskBin = str_repeat("\xff", (int) ($mask / 8));
|
||||
$remainder = $mask % 8;
|
||||
if ($remainder > 0) {
|
||||
$maskBin .= chr(0xFF & (0xFF << (8 - $remainder)));
|
||||
}
|
||||
$maskBin = str_pad($maskBin, 16, "\x00");
|
||||
|
||||
if (($ip_long & $mask_long) == ($subnet_long & $mask_long)) {
|
||||
return true;
|
||||
if (($ipBin & $maskBin) === ($subnetBin & $maskBin)) {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
// IPv4 CIDR matching
|
||||
$ip_long = ip2long($ip);
|
||||
$subnet_long = ip2long($subnet);
|
||||
|
||||
if ($ip_long === false || $subnet_long === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$mask_long = ~((1 << (32 - $mask)) - 1);
|
||||
|
||||
if (($ip_long & $mask_long) == ($subnet_long & $mask_long)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Special case: 0.0.0.0 means allow all
|
||||
@@ -1451,6 +1475,67 @@ function checkIPAgainstAllowlist($ip, $allowlist)
|
||||
return false;
|
||||
}
|
||||
|
||||
function deduplicateAllowlist(array $entries): array
|
||||
{
|
||||
if (count($entries) <= 1) {
|
||||
return array_values($entries);
|
||||
}
|
||||
|
||||
// Normalize each entry into [original, ip, mask]
|
||||
$parsed = [];
|
||||
foreach ($entries as $entry) {
|
||||
$entry = trim($entry);
|
||||
if (empty($entry)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($entry === '0.0.0.0') {
|
||||
// Special case: bare 0.0.0.0 means "allow all" — treat as /0
|
||||
$parsed[] = ['original' => $entry, 'ip' => '0.0.0.0', 'mask' => 0];
|
||||
} elseif (str_contains($entry, '/')) {
|
||||
[$ip, $mask] = explode('/', $entry);
|
||||
$parsed[] = ['original' => $entry, 'ip' => $ip, 'mask' => (int) $mask];
|
||||
} else {
|
||||
$ip = $entry;
|
||||
$isIpv6 = filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false;
|
||||
$parsed[] = ['original' => $entry, 'ip' => $ip, 'mask' => $isIpv6 ? 128 : 32];
|
||||
}
|
||||
}
|
||||
|
||||
$count = count($parsed);
|
||||
$redundant = array_fill(0, $count, false);
|
||||
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
if ($redundant[$i]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for ($j = 0; $j < $count; $j++) {
|
||||
if ($i === $j || $redundant[$j]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Entry $j is redundant if its mask is narrower/equal (>=) than $i's mask
|
||||
// AND $j's network IP falls within $i's CIDR range
|
||||
if ($parsed[$j]['mask'] >= $parsed[$i]['mask']) {
|
||||
$cidr = $parsed[$i]['ip'].'/'.$parsed[$i]['mask'];
|
||||
if (checkIPAgainstAllowlist($parsed[$j]['ip'], [$cidr])) {
|
||||
$redundant[$j] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$result = [];
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
if (! $redundant[$i]) {
|
||||
$result[] = $parsed[$i]['original'];
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
function get_public_ips()
|
||||
{
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user