From 7ef7fee59ab661b20a4600bbe39b5543e0b48ee3 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 1 Oct 2026 07:16:26 +0200 Subject: [PATCH] fix(proxy): accept Docker Compose merge tags in proxy configurations Proxy port validation parsed the configuration with plain Symfony YAML, which rejects Docker Compose tags such as !reset and !override. Proxy start, restart and save then failed for these valid configurations, and automatic start after a reboot stopped silently. v4.3.23 did not block on such configurations. The parser now reads custom tags and validates the values inside them, so malformed ports are still rejected. CheckProxy refreshes the status of a running proxy before it reads the configuration, as in v4.3.23, so an unreadable configuration no longer hides a running proxy. --- app/Actions/Proxy/CheckProxy.php | 22 +++++++++---------- app/Services/ProxyPortParser.php | 18 ++++++++++++++- tests/Feature/Proxy/ProxyPortSecurityTest.php | 15 ++++++++++++- tests/Unit/ProxyPortValidationTest.php | 14 ++++++++++++ 4 files changed, 56 insertions(+), 13 deletions(-) diff --git a/app/Actions/Proxy/CheckProxy.php b/app/Actions/Proxy/CheckProxy.php index 47bc002d68..6ac4487082 100644 --- a/app/Actions/Proxy/CheckProxy.php +++ b/app/Actions/Proxy/CheckProxy.php @@ -53,6 +53,17 @@ class CheckProxy return true; } else { + $status = getContainerStatus($server, $proxyContainerName); + if ($status === 'running') { + $server->proxy->set('status', 'running'); + $server->save(); + + return false; + } + if ($server->settings->is_cloudflare_tunnel) { + return false; + } + $portsToCheck = []; try { @@ -65,17 +76,6 @@ class CheckProxy return false; } - - $status = getContainerStatus($server, $proxyContainerName); - if ($status === 'running') { - $server->proxy->set('status', 'running'); - $server->save(); - - return false; - } - if ($server->settings->is_cloudflare_tunnel) { - return false; - } if (count($portsToCheck) === 0) { return false; } diff --git a/app/Services/ProxyPortParser.php b/app/Services/ProxyPortParser.php index 5d118ed278..efc5a551f5 100644 --- a/app/Services/ProxyPortParser.php +++ b/app/Services/ProxyPortParser.php @@ -3,6 +3,7 @@ namespace App\Services; use Symfony\Component\Yaml\Exception\ParseException; +use Symfony\Component\Yaml\Tag\TaggedValue; use Symfony\Component\Yaml\Yaml; class ProxyPortParser @@ -31,12 +32,14 @@ class ProxyPortParser * a port invalid: a variable with a default is checked as its default, and a port that still * depends on a variable (or whose default is not a valid port) is accepted but not returned. * + * Docker Compose merge tags such as `!reset` and `!override` are accepted; their values are validated. + * * @return list */ public static function fromConfiguration(string $configuration): array { try { - $parsed = Yaml::parse($configuration); + $parsed = self::withoutTags(Yaml::parse($configuration, Yaml::PARSE_CUSTOM_TAGS)); } catch (ParseException $exception) { throw new \InvalidArgumentException('The proxy configuration must contain valid YAML.', previous: $exception); } @@ -70,6 +73,19 @@ class ProxyPortParser return array_values(array_unique($ports)); } + private static function withoutTags(mixed $value): mixed + { + if ($value instanceof TaggedValue) { + return self::withoutTags($value->getValue()); + } + + if (is_array($value)) { + return array_map(self::withoutTags(...), $value); + } + + return $value; + } + private static function publishedPort(mixed $configuredPort): ?int { if (is_string($configuredPort) && self::containsVariable($configuredPort)) { diff --git a/tests/Feature/Proxy/ProxyPortSecurityTest.php b/tests/Feature/Proxy/ProxyPortSecurityTest.php index 88ec25dc9d..4d4c77f6f3 100644 --- a/tests/Feature/Proxy/ProxyPortSecurityTest.php +++ b/tests/Feature/Proxy/ProxyPortSecurityTest.php @@ -4,6 +4,7 @@ use App\Actions\Proxy\CheckProxy; use App\Actions\Proxy\SaveProxyConfiguration; use App\Enums\ProxyTypes; use App\Models\InstanceSettings; +use App\Models\PrivateKey; use App\Models\Server; use App\Models\Team; use Illuminate\Foundation\Testing\RefreshDatabase; @@ -44,7 +45,19 @@ it('does not execute remote commands for a malformed legacy stored port', functi $this->server->save(); expect(CheckProxy::run($this->server))->toBeFalse(); - Process::assertNothingRan(); + Process::assertDidntRun(fn ($process): bool => str_contains((string) $process->command, '$(id)')); +}); + +it('refreshes the status of a running proxy even when its stored configuration is invalid', function () { + Process::fake([ + '*' => Process::result(output: json_encode(['State' => ['Status' => 'running']])), + ]); + $this->server->private_key_id = PrivateKey::factory()->create(['team_id' => $this->server->team_id])->id; + $this->server->proxy->last_saved_proxy_configuration = "services:\n traefik:\n image: traefik:v3.7\n ports:\n - '$(id):80'\n"; + $this->server->save(); + + expect(CheckProxy::run($this->server->fresh()))->toBeFalse() + ->and($this->server->fresh()->proxy->status)->toBe('running'); }); it('builds port checks only from normalized integer ports', function () { diff --git a/tests/Unit/ProxyPortValidationTest.php b/tests/Unit/ProxyPortValidationTest.php index 2e8b13a4a9..ef2edcadaf 100644 --- a/tests/Unit/ProxyPortValidationTest.php +++ b/tests/Unit/ProxyPortValidationTest.php @@ -113,3 +113,17 @@ it('rejects invalid proxy port collection shapes', function (mixed $ports) { 'null' => [null], 'boolean' => [true], ]); + +it('accepts Docker Compose merge tags in proxy configurations', function (string $configuration, array $expected) { + expect(ProxyPortParser::fromConfiguration($configuration))->toBe($expected); +})->with([ + 'override ports' => ["services:\n traefik:\n ports: !override\n - '80:80'\n - '443:443'\n", [80, 443]], + 'reset ports' => ["services:\n traefik:\n ports: !reset []\n", []], + 'reset labels next to ports' => ["services:\n caddy:\n labels: !reset []\n ports: ['8080:80']\n", [8080]], + 'override a long syntax port' => ["services:\n traefik:\n ports: !override\n - target: 80\n published: !!str 8080\n", [8080]], +]); + +it('still rejects malformed ports inside Docker Compose merge tags', function () { + expect(fn () => ProxyPortParser::fromConfiguration("services:\n traefik:\n ports: !override\n - '\$(id):80'\n")) + ->toThrow(InvalidArgumentException::class); +});