mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-04 10:42:23 +00:00
fix(dns): check the closest Cloudflare wildcard for deeper domain names
Closes #12436 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d70801aaa1
commit
e20e652c00
5 files changed
+52
-14
No files matched your search
+1
-1
@@ -29,7 +29,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
- Do not copy the preview deployments of an application when you clone it. A clone of an application with a fixed preview URL template (for example `demo-mr-{{pr_id}}.example.com`) failed with a duplicate preview domain error and left a stopped, partial clone. The clone gets its own previews from new pull requests
|
||||
- Handle an existing Cloudflare DNS record with a different IP when Coolify adds the DNS record of a new domain automatically. Coolify now replaces the record when it created the record and no other resource uses it. Otherwise it opens the **Configure DNS** dialog, where you can replace the record, and the domain shows the conflict instead of "Not checked"
|
||||
- Add the Cloudflare DNS record automatically, or open the **Configure DNS** dialog, also when you edit a domain to a hostname in a connected Cloudflare zone, for example when you change a generated domain to your own domain. Before, Coolify did this only when you added a new domain. This applies to the **Domains** pages, the application and service application settings, and the service application API
|
||||
- Show "DNS mismatch" when a domain in a connected Cloudflare zone goes through Cloudflare to a different server. Before, the DNS check accepted every Cloudflare IP, so a proxied wildcard record that points to another server showed "DNS matches", and the domain then failed with Cloudflare error 526 and a Let's Encrypt error. Coolify now reads the Cloudflare record of the domain, or else its wildcard record, and compares the IP with the server. CNAME records, for example of a Cloudflare Tunnel, are still accepted ([#12436](https://github.com/coollabsio/coolify/issues/12436))
|
||||
- Show "DNS mismatch" when a domain in a connected Cloudflare zone goes through Cloudflare to a different server. Before, the DNS check accepted every Cloudflare IP, so a proxied wildcard record that points to another server showed "DNS matches", and the domain then failed with Cloudflare error 526 and a Let's Encrypt error. Coolify now reads the Cloudflare record of the domain, or else the closest wildcard record that covers it (also for deeper names, such as `app.apps.example.com` through `*.example.com`), and compares the IP with the server. CNAME records, for example of a Cloudflare Tunnel, are still accepted ([#12436](https://github.com/coollabsio/coolify/issues/12436))
|
||||
- Add the Cloudflare DNS record automatically when you clone an application or a service, and when you create an application or a service through the API, for each domain in a Cloudflare zone with automatic DNS. A clone gets records only for the domains that the source does not use, so the record of the source stays as it is ([#12436](https://github.com/coollabsio/coolify/issues/12436))
|
||||
- Save deployments started through the MCP server as MCP deployments, not as API deployments. The deployment list shows them with the **MCP** source and filter, and the API and MCP deployment data have a new `is_mcp` field. The deployments to additional servers keep the source of the main deployment, and the audit log records actions that MCP tools do through the API as MCP events
|
||||
- Save the update channel in **Settings > Updates** when you select **Preview** or **Nightly**. Before, the "Save changes" bar did not open for these channels, so the change was lost when you refreshed the page
|
||||
|
||||
@@ -119,7 +119,8 @@ class CloudflareDnsProvider
|
||||
|
||||
/**
|
||||
* Whether Cloudflare sends a proxied hostname to another server than the given one. A Cloudflare IP in DNS hides
|
||||
* the origin, so the zone's records are read: the hostname's own record, or else the wildcard record that covers it.
|
||||
* the origin, so the zone's records are read: the hostname's own record, or else the closest wildcard record that
|
||||
* covers it (a Cloudflare wildcard also covers deeper names, so `*.example.com` covers `app.apps.example.com`).
|
||||
* False when Coolify cannot tell: no zone of the server's team manages the hostname, the records cannot be read,
|
||||
* a CNAME sends the hostname on, or no record is found.
|
||||
*/
|
||||
@@ -136,10 +137,12 @@ class CloudflareDnsProvider
|
||||
}
|
||||
|
||||
$names = [$hostname];
|
||||
$parent = str($hostname)->after('.')->value();
|
||||
if ($parent === $zone->name || str_ends_with($parent, '.'.$zone->name)) {
|
||||
for ($parent = str($hostname)->after('.')->value(); str_ends_with($parent, '.'.$zone->name); $parent = str($parent)->after('.')->value()) {
|
||||
$names[] = '*.'.$parent;
|
||||
}
|
||||
if (str_ends_with($hostname, '.'.$zone->name)) {
|
||||
$names[] = '*.'.$zone->name;
|
||||
}
|
||||
|
||||
try {
|
||||
foreach ($names as $name) {
|
||||
|
||||
@@ -486,6 +486,40 @@ describe('domain DNS configuration after add', function () {
|
||||
Queue::assertNotPushed(CheckDomainDnsJob::class);
|
||||
});
|
||||
|
||||
test('service domains queues a dns record job when a generated domain is edited to a managed zone', function () {
|
||||
$service = Service::factory()->create([
|
||||
'server_id' => $this->server->id,
|
||||
'destination_id' => $this->destination->id,
|
||||
'destination_type' => $this->destination->getMorphClass(),
|
||||
'environment_id' => $this->environment->id,
|
||||
'docker_compose_raw' => "services:\n web:\n image: nginx:alpine\n",
|
||||
]);
|
||||
$webApp = ServiceApplication::create([
|
||||
'uuid' => (string) Str::uuid(),
|
||||
'service_id' => $service->id,
|
||||
'name' => 'web',
|
||||
'human_name' => 'Web',
|
||||
'image' => 'nginx:alpine',
|
||||
'fqdn' => 'https://web-generated.203.0.113.10.sslip.io',
|
||||
]);
|
||||
|
||||
Livewire::test(ServiceDomains::class, ['service' => $service->fresh(['applications', 'server'])])
|
||||
->call('startEdit', 0)
|
||||
->set('editingDomainParts.host', 'app.example.com')
|
||||
->call('updateDomain')
|
||||
->assertHasNoErrors()
|
||||
->assertDispatched('success', 'Domain updated.')
|
||||
->assertDispatched('info', 'Adding DNS record for app.example.com.');
|
||||
|
||||
expect($webApp->fresh()->fqdn)->toBe('https://app.example.com');
|
||||
Queue::assertPushed(ConfigureDnsRecordJob::class, 1);
|
||||
Queue::assertPushed(ConfigureDnsRecordJob::class, fn (ConfigureDnsRecordJob $job): bool => $job->hostname === 'app.example.com'
|
||||
&& $job->content === '203.0.113.10'
|
||||
&& $job->zoneId === $this->zone->id
|
||||
&& (string) $job->resourceId === (string) $webApp->id);
|
||||
Queue::assertNotPushed(CheckDomainDnsJob::class);
|
||||
});
|
||||
|
||||
test('private server addresses are never queued as dns records', function (string $address) {
|
||||
$this->server->update(['ip' => $address]);
|
||||
$application = Application::factory()->create([
|
||||
|
||||
@@ -312,7 +312,7 @@ it('passes bracketed IPv6 custom dns servers to the dns query', function () {
|
||||
expect($queriedServers->getArrayCopy())->toBe(['[2606:4700:4700::1111]', '[2001:db8::53]', '192.0.2.1']);
|
||||
});
|
||||
|
||||
it('checks the Cloudflare record origin when a proxied hostname is in a zone of the team', function (array $records, string $status) {
|
||||
it('checks the Cloudflare record origin when a proxied hostname is in a zone of the team', function (array $records, string $status, string $hostname = 'app.example.com') {
|
||||
InstanceSettings::unguarded(fn () => InstanceSettings::query()->updateOrCreate(
|
||||
['id' => 0],
|
||||
['is_dns_validation_enabled' => true, 'custom_dns_servers' => '192.0.2.1']
|
||||
@@ -337,12 +337,12 @@ it('checks the Cloudflare record origin when a proxied hostname is in a zone of
|
||||
$server = new Server(['ip' => '203.0.113.10']);
|
||||
$server->team_id = $team->id;
|
||||
|
||||
$result = CheckDomainDns::run(['app' => 'https://app.example.com'], $server, '203.0.113.10');
|
||||
$result = CheckDomainDns::run(['app' => "https://{$hostname}"], $server, '203.0.113.10');
|
||||
|
||||
expect($result['app']['status'])->toBe($status)
|
||||
->and(validateDNSEntry('https://app.example.com', $server))->toBe($status === 'ok');
|
||||
->and(validateDNSEntry("https://{$hostname}", $server))->toBe($status === 'ok');
|
||||
if ($status === 'failed') {
|
||||
expect($result['app']['message'])->toBe('Cloudflare sends app.example.com to another server. Required DNS record type A pointing to 203.0.113.10');
|
||||
expect($result['app']['message'])->toBe("Cloudflare sends {$hostname} to another server. Required DNS record type A pointing to 203.0.113.10");
|
||||
}
|
||||
})->with([
|
||||
'record points to the server' => [[['type' => 'A', 'name' => 'app.example.com', 'content' => '203.0.113.10']], 'ok'],
|
||||
@@ -353,6 +353,11 @@ it('checks the Cloudflare record origin when a proxied hostname is in a zone of
|
||||
['type' => 'A', 'name' => 'app.example.com', 'content' => '203.0.113.10'],
|
||||
['type' => 'A', 'name' => '*.example.com', 'content' => '198.51.100.20'],
|
||||
], 'ok'],
|
||||
'wildcard of the zone covers a deeper hostname' => [[['type' => 'A', 'name' => '*.example.com', 'content' => '198.51.100.20']], 'failed', 'app.apps.example.com'],
|
||||
'closest wildcard wins for a deeper hostname' => [[
|
||||
['type' => 'A', 'name' => '*.apps.example.com', 'content' => '203.0.113.10'],
|
||||
['type' => 'A', 'name' => '*.example.com', 'content' => '198.51.100.20'],
|
||||
], 'ok', 'app.apps.example.com'],
|
||||
'CNAME origin cannot be verified' => [[['type' => 'CNAME', 'name' => 'app.example.com', 'content' => 'tunnel.cfargotunnel.com']], 'ok'],
|
||||
'no record found' => [[], 'ok'],
|
||||
]);
|
||||
|
||||
@@ -39,7 +39,8 @@ uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->withoutVite();
|
||||
Queue::fake([CheckDomainDnsJob::class]);
|
||||
// A new hostname of an automatic DNS zone also queues a record; DnsProviderManagementTest and DnsApiTest cover that.
|
||||
Queue::fake([CheckDomainDnsJob::class, ConfigureDnsRecordJob::class]);
|
||||
config()->set('app.maintenance.store', 'array');
|
||||
config()->set('app.id', 'test-instance');
|
||||
|
||||
@@ -315,11 +316,6 @@ describe('UI service domain edit', function () {
|
||||
});
|
||||
|
||||
describe('API domain edit', function () {
|
||||
beforeEach(function () {
|
||||
// A new domain of an automatic DNS zone also queues a record; DnsApiTest covers that.
|
||||
Queue::fake([CheckDomainDnsJob::class, ConfigureDnsRecordJob::class]);
|
||||
});
|
||||
|
||||
test('an application domain update releases the removed hostname', function () {
|
||||
$cloudflare = fakeCloudflareDns();
|
||||
$application = createDnsTestApplication($this, 'https://app.example.com,https://keep.example.com');
|
||||
|
||||
Reference in new issue
Block a user