Merge remote-tracking branch 'origin/next' into audit-policies

This commit is contained in:
Andras Bacsai
2026-03-03 19:41:05 +01:00
67 changed files with 3715 additions and 973 deletions
+1
View File
@@ -1006,6 +1006,7 @@ function convertDockerRunToCompose(?string $custom_docker_run_options = null)
'--ulimit' => 'ulimits',
'--privileged' => 'privileged',
'--ip' => 'ip',
'--ip6' => 'ip6',
'--shm-size' => 'shm_size',
'--gpus' => 'gpus',
'--hostname' => 'hostname',
+48 -8
View File
@@ -127,10 +127,44 @@ function connectProxyToNetworks(Server $server)
return $commands->flatten();
}
/**
* Generate shell commands to fix a Docker network that has an IPv6 gateway with CIDR notation.
*
* Docker 25+ may store IPv6 gateways with CIDR (e.g. fd7d:f7d2:7e77::1/64), which causes
* ParseAddr errors in Docker Compose. This detects the issue and recreates the network.
*
* @see https://github.com/coollabsio/coolify/issues/8649
*
* @param string $network Network name to check and fix
* @return array Shell commands to execute on the remote server
*/
function fixNetworkIpv6CidrGateway(string $network): array
{
return [
"if docker network inspect {$network} >/dev/null 2>&1; then",
" IPV6_GW=\$(docker network inspect {$network} --format '{{range .IPAM.Config}}{{.Gateway}} {{end}}' 2>/dev/null | tr ' ' '\n' | grep '/' || true)",
' if [ -n "$IPV6_GW" ]; then',
" echo \"Fixing network {$network}: IPv6 gateway has CIDR notation (\$IPV6_GW)\"",
" CONTAINERS=\$(docker network inspect {$network} --format '{{range .Containers}}{{.Name}} {{end}}' 2>/dev/null)",
' for c in $CONTAINERS; do',
" [ -n \"\$c\" ] && docker network disconnect {$network} \"\$c\" 2>/dev/null || true",
' done',
" docker network rm {$network} 2>/dev/null || true",
" docker network create --attachable {$network} 2>/dev/null || true",
' for c in $CONTAINERS; do',
" [ -n \"\$c\" ] && [ \"\$c\" != \"coolify-proxy\" ] && docker network connect {$network} \"\$c\" 2>/dev/null || true",
' done',
' fi',
'fi',
];
}
/**
* Ensures all required networks exist before docker compose up.
* This must be called BEFORE docker compose up since the compose file declares networks as external.
*
* Also detects and fixes networks with IPv6 CIDR gateway notation that causes ParseAddr errors.
*
* @param Server $server The server to ensure networks on
* @return \Illuminate\Support\Collection Commands to create networks if they don't exist
*/
@@ -140,17 +174,23 @@ function ensureProxyNetworksExist(Server $server)
if ($server->isSwarm()) {
$commands = $networks->map(function ($network) {
return [
"echo 'Ensuring network $network exists...'",
"docker network ls --format '{{.Name}}' | grep -q '^{$network}$' || docker network create --driver overlay --attachable $network",
];
return array_merge(
fixNetworkIpv6CidrGateway($network),
[
"echo 'Ensuring network $network exists...'",
"docker network ls --format '{{.Name}}' | grep -q '^{$network}$' || docker network create --driver overlay --attachable $network",
]
);
});
} else {
$commands = $networks->map(function ($network) {
return [
"echo 'Ensuring network $network exists...'",
"docker network ls --format '{{.Name}}' | grep -q '^{$network}$' || docker network create --attachable $network",
];
return array_merge(
fixNetworkIpv6CidrGateway($network),
[
"echo 'Ensuring network $network exists...'",
"docker network ls --format '{{.Name}}' | grep -q '^{$network}$' || docker network create --attachable $network",
]
);
});
}
+96 -11
View File
@@ -1424,24 +1424,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
@@ -1459,6 +1483,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 {