fix(database): listen on IPv6 in public TCP proxy when network has IPv6

Docker publishes the proxy port on [::] too and forwards it to the
container's IPv6 address when the network has IPv6 enabled. nginx
listened only on IPv4, so every IPv6 connection was reset.

Add listen [::]:<port> when the destination network has IPv6 enabled.
Networks without IPv6 keep the IPv4-only listener.

Fixes #12133
This commit is contained in:
Andras Bacsai
2026-10-09 13:31:25 +02:00
parent 376e7aab24
commit 45d165e82e
2 changed files with 36 additions and 1 deletions
+24 -1
View File
@@ -2,6 +2,7 @@
namespace App\Actions\Database;
use App\Models\Server;
use App\Models\ServiceDatabase;
use App\Models\StandaloneClickhouse;
use App\Models\StandaloneDragonfly;
@@ -58,6 +59,7 @@ class StartDatabaseProxy
$configuration_dir = database_proxy_dir($database->uuid);
$host_configuration_dir = devHostDockerPath($server, $configuration_dir);
$timeoutConfig = $this->buildProxyTimeoutConfig($database->public_port_timeout);
$listenConfig = $this->buildListenConfig($database->public_port, $this->isNetworkIpv6Enabled($network, $server));
$nginxconf = <<<EOF
user nginx;
worker_processes auto;
@@ -69,7 +71,7 @@ class StartDatabaseProxy
}
stream {
server {
listen $database->public_port;
$listenConfig
proxy_pass $containerName:$internalPort;
$timeoutConfig
}
@@ -164,6 +166,27 @@ class StartDatabaseProxy
return false;
}
/**
* Docker publishes the port on [::] too and forwards it to the container's IPv6 address
* when the network has IPv6 enabled, so nginx must listen there as well.
*/
private function isNetworkIpv6Enabled(string $network, Server $server): bool
{
$safeNetwork = escapeshellarg($network);
$output = instant_remote_process(["docker network inspect {$safeNetwork} --format '{{.EnableIPv6}}'"], $server, false);
return trim((string) $output) === 'true';
}
private function buildListenConfig(int $port, bool $ipv6Enabled): string
{
if (! $ipv6Enabled) {
return "listen {$port};";
}
return "listen {$port};\n listen [::]:{$port};";
}
private function buildProxyTimeoutConfig(?int $timeout): string
{
if ($timeout === null || $timeout < 1) {
@@ -70,3 +70,15 @@ test('buildProxyTimeoutConfig normalizes invalid values to default', function (?
[-10, 'proxy_timeout 3600s;'],
[120, 'proxy_timeout 120s;'],
]);
test('buildListenConfig adds an IPv6 listener only when the network has IPv6 enabled', function (bool $ipv6Enabled, array $expectedListeners) {
$action = new StartDatabaseProxy;
$method = new ReflectionMethod($action, 'buildListenConfig');
$listeners = array_map('trim', explode("\n", $method->invoke($action, 28197, $ipv6Enabled)));
expect($listeners)->toBe($expectedListeners);
})->with([
'IPv4 only network' => [false, ['listen 28197;']],
'IPv6 enabled network' => [true, ['listen 28197;', 'listen [::]:28197;']],
]);