Fix DNS zone glue for in-zone nameservers
This commit is contained in:
@@ -543,6 +543,16 @@ class DnsZones extends Page implements HasActions, HasForms, HasTable
|
||||
$defaultRecords[] = ['name' => 'mail', 'type' => 'AAAA', 'content' => $serverIpv6, 'ttl' => 3600, 'priority' => null];
|
||||
}
|
||||
|
||||
$defaultRecords = $this->appendNameserverRecords(
|
||||
$defaultRecords,
|
||||
$domain->domain,
|
||||
$ns1,
|
||||
$ns2,
|
||||
$serverIp,
|
||||
$serverIpv6,
|
||||
3600
|
||||
);
|
||||
|
||||
foreach ($defaultRecords as $record) {
|
||||
DnsRecord::create(array_merge(['domain_id' => $this->selectedDomainId], $record));
|
||||
}
|
||||
@@ -879,6 +889,7 @@ class DnsZones extends Page implements HasActions, HasForms, HasTable
|
||||
{
|
||||
$records = DnsRecord::whereHas('domain', fn ($q) => $q->where('domain', $domain))->get()->toArray();
|
||||
$settings = DnsSetting::getAll();
|
||||
$defaultIp = $settings['default_ip'] ?? trim(shell_exec("hostname -I | awk '{print $1}'") ?? '') ?: '127.0.0.1';
|
||||
|
||||
$this->getAgent()->send('dns.sync_zone', [
|
||||
'domain' => $domain,
|
||||
@@ -886,7 +897,82 @@ class DnsZones extends Page implements HasActions, HasForms, HasTable
|
||||
'ns1' => $settings['ns1'] ?? 'ns1.example.com',
|
||||
'ns2' => $settings['ns2'] ?? 'ns2.example.com',
|
||||
'admin_email' => $settings['admin_email'] ?? 'admin.example.com',
|
||||
'default_ip' => $defaultIp,
|
||||
'default_ipv6' => $settings['default_ipv6'] ?? null,
|
||||
'default_ttl' => $settings['default_ttl'] ?? 3600,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array<string, mixed>> $records
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
protected function appendNameserverRecords(
|
||||
array $records,
|
||||
string $domain,
|
||||
string $ns1,
|
||||
string $ns2,
|
||||
string $ipv4,
|
||||
?string $ipv6,
|
||||
int $ttl
|
||||
): array {
|
||||
$labels = $this->getNameserverLabels($domain, [$ns1, $ns2]);
|
||||
|
||||
if ($labels === []) {
|
||||
return $records;
|
||||
}
|
||||
|
||||
$existingA = array_map(
|
||||
fn (array $record): string => $record['name'] ?? '',
|
||||
array_filter($records, fn (array $record): bool => ($record['type'] ?? '') === 'A')
|
||||
);
|
||||
$existingAAAA = array_map(
|
||||
fn (array $record): string => $record['name'] ?? '',
|
||||
array_filter($records, fn (array $record): bool => ($record['type'] ?? '') === 'AAAA')
|
||||
);
|
||||
|
||||
foreach ($labels as $label) {
|
||||
if (! in_array($label, $existingA, true)) {
|
||||
$records[] = ['name' => $label, 'type' => 'A', 'content' => $ipv4, 'ttl' => $ttl, 'priority' => null];
|
||||
}
|
||||
|
||||
if ($ipv6 && ! in_array($label, $existingAAAA, true)) {
|
||||
$records[] = ['name' => $label, 'type' => 'AAAA', 'content' => $ipv6, 'ttl' => $ttl, 'priority' => null];
|
||||
}
|
||||
}
|
||||
|
||||
return $records;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, string> $nameservers
|
||||
* @return array<int, string>
|
||||
*/
|
||||
protected function getNameserverLabels(string $domain, array $nameservers): array
|
||||
{
|
||||
$domain = rtrim($domain, '.');
|
||||
$labels = [];
|
||||
|
||||
foreach ($nameservers as $nameserver) {
|
||||
$nameserver = rtrim($nameserver ?? '', '.');
|
||||
|
||||
if ($nameserver === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($nameserver === $domain) {
|
||||
$label = '@';
|
||||
} elseif (str_ends_with($nameserver, '.'.$domain)) {
|
||||
$label = substr($nameserver, 0, -strlen('.'.$domain));
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($label !== '@') {
|
||||
$labels[] = $label;
|
||||
}
|
||||
}
|
||||
|
||||
return array_values(array_unique($labels));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -290,6 +290,7 @@ class DomainIpAssignmentsTable extends Component implements HasActions, HasSchem
|
||||
$settings = DnsSetting::getAll();
|
||||
$hostname = gethostname() ?: 'localhost';
|
||||
$serverIp = trim(shell_exec("hostname -I | awk '{print $1}'") ?? '') ?: '127.0.0.1';
|
||||
$serverIpv6 = $settings['default_ipv6'] ?? null;
|
||||
|
||||
try {
|
||||
$records = $domain->dnsRecords()->get()->toArray();
|
||||
@@ -300,6 +301,7 @@ class DomainIpAssignmentsTable extends Component implements HasActions, HasSchem
|
||||
'ns2' => $settings['ns2'] ?? "ns2.{$hostname}",
|
||||
'admin_email' => $settings['admin_email'] ?? "admin.{$hostname}",
|
||||
'default_ip' => $settings['default_ip'] ?? $serverIp,
|
||||
'default_ipv6' => $serverIpv6,
|
||||
'default_ttl' => (int) ($settings['default_ttl'] ?? 3600),
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
|
||||
@@ -588,6 +588,16 @@ class DnsRecords extends Page implements HasActions, HasForms, HasTable
|
||||
$defaultRecords[] = ['name' => 'mail', 'type' => 'AAAA', 'content' => $serverIpv6, 'ttl' => 3600, 'priority' => null];
|
||||
}
|
||||
|
||||
$defaultRecords = $this->appendNameserverRecords(
|
||||
$defaultRecords,
|
||||
$domain->domain,
|
||||
$ns1,
|
||||
$ns2,
|
||||
$serverIp,
|
||||
$serverIpv6,
|
||||
3600
|
||||
);
|
||||
|
||||
foreach ($defaultRecords as $record) {
|
||||
DnsRecord::create(array_merge(['domain_id' => $this->selectedDomainId], $record));
|
||||
}
|
||||
@@ -773,12 +783,15 @@ class DnsRecords extends Page implements HasActions, HasForms, HasTable
|
||||
try {
|
||||
$records = DnsRecord::whereHas('domain', fn ($q) => $q->where('domain', $domain))->get();
|
||||
$settings = DnsSetting::getAll();
|
||||
$defaultIp = $settings['default_ip'] ?? trim(shell_exec("hostname -I | awk '{print $1}'") ?? '') ?: '127.0.0.1';
|
||||
$this->getAgent()->send('dns.sync_zone', [
|
||||
'domain' => $domain,
|
||||
'records' => $records->toArray(),
|
||||
'ns1' => $settings['ns1'] ?? 'ns1.example.com',
|
||||
'ns2' => $settings['ns2'] ?? 'ns2.example.com',
|
||||
'admin_email' => $settings['admin_email'] ?? 'admin.example.com',
|
||||
'default_ip' => $defaultIp,
|
||||
'default_ipv6' => $settings['default_ipv6'] ?? null,
|
||||
'default_ttl' => $settings['default_ttl'] ?? 3600,
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
@@ -796,4 +809,77 @@ class DnsRecords extends Page implements HasActions, HasForms, HasTable
|
||||
->visible(fn () => $this->selectedDomainId !== null),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array<string, mixed>> $records
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
protected function appendNameserverRecords(
|
||||
array $records,
|
||||
string $domain,
|
||||
string $ns1,
|
||||
string $ns2,
|
||||
string $ipv4,
|
||||
?string $ipv6,
|
||||
int $ttl
|
||||
): array {
|
||||
$labels = $this->getNameserverLabels($domain, [$ns1, $ns2]);
|
||||
|
||||
if ($labels === []) {
|
||||
return $records;
|
||||
}
|
||||
|
||||
$existingA = array_map(
|
||||
fn (array $record): string => $record['name'] ?? '',
|
||||
array_filter($records, fn (array $record): bool => ($record['type'] ?? '') === 'A')
|
||||
);
|
||||
$existingAAAA = array_map(
|
||||
fn (array $record): string => $record['name'] ?? '',
|
||||
array_filter($records, fn (array $record): bool => ($record['type'] ?? '') === 'AAAA')
|
||||
);
|
||||
|
||||
foreach ($labels as $label) {
|
||||
if (! in_array($label, $existingA, true)) {
|
||||
$records[] = ['name' => $label, 'type' => 'A', 'content' => $ipv4, 'ttl' => $ttl, 'priority' => null];
|
||||
}
|
||||
|
||||
if ($ipv6 && ! in_array($label, $existingAAAA, true)) {
|
||||
$records[] = ['name' => $label, 'type' => 'AAAA', 'content' => $ipv6, 'ttl' => $ttl, 'priority' => null];
|
||||
}
|
||||
}
|
||||
|
||||
return $records;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, string> $nameservers
|
||||
* @return array<int, string>
|
||||
*/
|
||||
protected function getNameserverLabels(string $domain, array $nameservers): array
|
||||
{
|
||||
$domain = rtrim($domain, '.');
|
||||
$labels = [];
|
||||
|
||||
foreach ($nameservers as $nameserver) {
|
||||
$nameserver = rtrim($nameserver ?? '', '.');
|
||||
|
||||
if ($nameserver === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($nameserver === $domain) {
|
||||
$label = '@';
|
||||
} elseif (str_ends_with($nameserver, '.'.$domain)) {
|
||||
$label = substr($nameserver, 0, -strlen('.'.$domain));
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($label !== '@') {
|
||||
$labels[] = $label;
|
||||
}
|
||||
}
|
||||
|
||||
return array_values(array_unique($labels));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Filament\Jabali\Pages;
|
||||
|
||||
use App\Filament\Concerns\HasPageTour;
|
||||
use App\Models\Autoresponder;
|
||||
use App\Models\DnsRecord;
|
||||
use App\Models\Domain;
|
||||
@@ -11,6 +12,8 @@ use App\Models\EmailDomain;
|
||||
use App\Models\EmailForwarder;
|
||||
use App\Models\Mailbox;
|
||||
use App\Services\Agent\AgentClient;
|
||||
use BackedEnum;
|
||||
use Exception;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Actions\Concerns\InteractsWithActions;
|
||||
use Filament\Actions\Contracts\HasActions;
|
||||
@@ -22,10 +25,10 @@ use Filament\Forms\Components\Toggle;
|
||||
use Filament\Forms\Concerns\InteractsWithForms;
|
||||
use Filament\Forms\Contracts\HasForms;
|
||||
use Filament\Infolists\Components\TextEntry;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Schemas\Components\View;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Pages\Page;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Schemas\Components\View;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Concerns\InteractsWithTable;
|
||||
@@ -35,19 +38,17 @@ use Illuminate\Contracts\Support\Htmlable;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use App\Filament\Concerns\HasPageTour;
|
||||
use BackedEnum;
|
||||
use Exception;
|
||||
use Livewire\Attributes\Url;
|
||||
|
||||
class Email extends Page implements HasForms, HasActions, HasTable
|
||||
class Email extends Page implements HasActions, HasForms, HasTable
|
||||
{
|
||||
use InteractsWithForms;
|
||||
use InteractsWithActions;
|
||||
use InteractsWithTable;
|
||||
use HasPageTour;
|
||||
use InteractsWithActions;
|
||||
use InteractsWithForms;
|
||||
use InteractsWithTable;
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-envelope';
|
||||
|
||||
protected static ?int $navigationSort = 3;
|
||||
|
||||
public static function getNavigationLabel(): string
|
||||
@@ -61,6 +62,7 @@ class Email extends Page implements HasForms, HasActions, HasTable
|
||||
public ?string $activeTab = 'mailboxes';
|
||||
|
||||
public string $credEmail = '';
|
||||
|
||||
public string $credPassword = '';
|
||||
|
||||
protected ?AgentClient $agent = null;
|
||||
@@ -134,8 +136,9 @@ class Email extends Page implements HasForms, HasActions, HasTable
|
||||
public function getAgent(): AgentClient
|
||||
{
|
||||
if ($this->agent === null) {
|
||||
$this->agent = new AgentClient();
|
||||
$this->agent = new AgentClient;
|
||||
}
|
||||
|
||||
return $this->agent;
|
||||
}
|
||||
|
||||
@@ -482,6 +485,7 @@ class Email extends Page implements HasForms, HasActions, HasTable
|
||||
if ($record->start_date && now()->lt($record->start_date)) {
|
||||
return __('Scheduled');
|
||||
}
|
||||
|
||||
return __('Expired');
|
||||
})
|
||||
->color(function (Autoresponder $record): string {
|
||||
@@ -494,6 +498,7 @@ class Email extends Page implements HasForms, HasActions, HasTable
|
||||
if ($record->start_date && now()->lt($record->start_date)) {
|
||||
return 'warning';
|
||||
}
|
||||
|
||||
return 'danger';
|
||||
}),
|
||||
TextColumn::make('start_date')
|
||||
@@ -616,7 +621,7 @@ class Email extends Page implements HasForms, HasActions, HasTable
|
||||
return Mailbox::where('email_domain_id', $record->id)
|
||||
->pluck('local_part')
|
||||
->mapWithKeys(fn ($local) => [
|
||||
$local . '@' . $record->domain->domain => $local . '@' . $record->domain->domain
|
||||
$local.'@'.$record->domain->domain => $local.'@'.$record->domain->domain,
|
||||
])
|
||||
->toArray();
|
||||
})
|
||||
@@ -832,6 +837,7 @@ class Email extends Page implements HasForms, HasActions, HasTable
|
||||
$settings = \App\Models\DnsSetting::getAll();
|
||||
$hostname = gethostname() ?: 'localhost';
|
||||
$serverIp = trim(shell_exec("hostname -I | awk '{print \$1}'") ?? '') ?: '127.0.0.1';
|
||||
$serverIpv6 = $settings['default_ipv6'] ?? null;
|
||||
|
||||
$this->getAgent()->send('dns.sync_zone', [
|
||||
'domain' => $domain->domain,
|
||||
@@ -840,6 +846,7 @@ class Email extends Page implements HasForms, HasActions, HasTable
|
||||
'ns2' => $settings['ns2'] ?? "ns2.{$hostname}",
|
||||
'admin_email' => $settings['admin_email'] ?? "admin.{$hostname}",
|
||||
'default_ip' => $settings['default_ip'] ?? $serverIp,
|
||||
'default_ipv6' => $serverIpv6,
|
||||
'default_ttl' => $settings['default_ttl'] ?? 3600,
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
@@ -920,6 +927,7 @@ class Email extends Page implements HasForms, HasActions, HasTable
|
||||
$domain = Domain::where('user_id', Auth::id())->find($data['domain_id']);
|
||||
if (! $domain) {
|
||||
Notification::make()->title(__('Domain not found'))->danger()->send();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -932,6 +940,7 @@ class Email extends Page implements HasForms, HasActions, HasTable
|
||||
|
||||
if (Mailbox::where('email_domain_id', $emailDomain->id)->where('local_part', $data['local_part'])->exists()) {
|
||||
Notification::make()->title(__('Mailbox already exists'))->danger()->send();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -998,6 +1007,7 @@ class Email extends Page implements HasForms, HasActions, HasTable
|
||||
$mailbox = Mailbox::with('emailDomain.domain')->find($mailboxId);
|
||||
if (! $mailbox) {
|
||||
Notification::make()->title(__('Mailbox not found'))->danger()->send();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1067,6 +1077,7 @@ class Email extends Page implements HasForms, HasActions, HasTable
|
||||
$domain = Domain::where('user_id', Auth::id())->find($data['domain_id']);
|
||||
if (! $domain) {
|
||||
Notification::make()->title(__('Domain not found'))->danger()->send();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1075,6 +1086,7 @@ class Email extends Page implements HasForms, HasActions, HasTable
|
||||
|
||||
if (empty($destinations)) {
|
||||
Notification::make()->title(__('Invalid destination emails'))->danger()->send();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1086,11 +1098,13 @@ class Email extends Page implements HasForms, HasActions, HasTable
|
||||
|
||||
if (EmailForwarder::where('email_domain_id', $emailDomain->id)->where('local_part', $data['local_part'])->exists()) {
|
||||
Notification::make()->title(__('Forwarder already exists'))->danger()->send();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (Mailbox::where('email_domain_id', $emailDomain->id)->where('local_part', $data['local_part'])->exists()) {
|
||||
Notification::make()->title(__('A mailbox with this address already exists'))->danger()->send();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1122,6 +1136,7 @@ class Email extends Page implements HasForms, HasActions, HasTable
|
||||
|
||||
if (empty($destinations)) {
|
||||
Notification::make()->title(__('Invalid destination emails'))->danger()->send();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1145,6 +1160,7 @@ class Email extends Page implements HasForms, HasActions, HasTable
|
||||
$forwarder = EmailForwarder::with('emailDomain.domain')->find($forwarderId);
|
||||
if (! $forwarder) {
|
||||
Notification::make()->title(__('Forwarder not found'))->danger()->send();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1229,6 +1245,7 @@ class Email extends Page implements HasForms, HasActions, HasTable
|
||||
|
||||
if (! $mailbox) {
|
||||
Notification::make()->title(__('Mailbox not found'))->danger()->send();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1239,6 +1256,7 @@ class Email extends Page implements HasForms, HasActions, HasTable
|
||||
->body(__('Edit the existing autoresponder instead.'))
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1369,6 +1387,7 @@ class Email extends Page implements HasForms, HasActions, HasTable
|
||||
->body(__('Please select a mailbox to receive catch-all emails'))
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1439,9 +1458,16 @@ class Email extends Page implements HasForms, HasActions, HasTable
|
||||
|
||||
protected function formatBytes(int $bytes): string
|
||||
{
|
||||
if ($bytes < 1024) return $bytes . ' B';
|
||||
if ($bytes < 1048576) return round($bytes / 1024, 1) . ' KB';
|
||||
if ($bytes < 1073741824) return round($bytes / 1048576, 1) . ' MB';
|
||||
if ($bytes < 1024) {
|
||||
return $bytes.' B';
|
||||
}
|
||||
if ($bytes < 1048576) {
|
||||
return round($bytes / 1024, 1).' KB';
|
||||
}
|
||||
if ($bytes < 1073741824) {
|
||||
return round($bytes / 1048576, 1).' MB';
|
||||
}
|
||||
|
||||
return round($bytes / 1073741824, 1).' GB';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,6 +71,16 @@ class DomainObserver
|
||||
$defaultRecords[] = ['name' => 'mail', 'type' => 'AAAA', 'content' => $defaultIpv6, 'ttl' => $defaultTtl];
|
||||
}
|
||||
|
||||
$defaultRecords = $this->appendNameserverRecords(
|
||||
$defaultRecords,
|
||||
$domain->domain,
|
||||
$ns1,
|
||||
$ns2,
|
||||
$defaultIp,
|
||||
$defaultIpv6,
|
||||
$defaultTtl
|
||||
);
|
||||
|
||||
foreach ($defaultRecords as $record) {
|
||||
DnsRecord::create(array_merge(['domain_id' => $domain->id], $record));
|
||||
}
|
||||
@@ -83,6 +93,7 @@ class DomainObserver
|
||||
$records = DnsRecord::where('domain_id', $domain->id)->get()->toArray();
|
||||
$hostname = $this->getServerHostname();
|
||||
$serverIp = $this->getServerIp();
|
||||
$serverIpv6 = $settings['default_ipv6'] ?? null;
|
||||
|
||||
$agent = new AgentClient;
|
||||
$agent->send('dns.sync_zone', [
|
||||
@@ -92,6 +103,7 @@ class DomainObserver
|
||||
'ns2' => $settings['ns2'] ?? "ns2.{$hostname}",
|
||||
'admin_email' => $settings['admin_email'] ?? "admin.{$hostname}",
|
||||
'default_ip' => $settings['default_ip'] ?? $serverIp,
|
||||
'default_ipv6' => $serverIpv6,
|
||||
'default_ttl' => $settings['default_ttl'] ?? 3600,
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
@@ -110,4 +122,77 @@ class DomainObserver
|
||||
|
||||
return $ip ?: '127.0.0.1';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array<string, mixed>> $records
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
protected function appendNameserverRecords(
|
||||
array $records,
|
||||
string $domain,
|
||||
string $ns1,
|
||||
string $ns2,
|
||||
string $ipv4,
|
||||
?string $ipv6,
|
||||
int $ttl
|
||||
): array {
|
||||
$labels = $this->getNameserverLabels($domain, [$ns1, $ns2]);
|
||||
|
||||
if ($labels === []) {
|
||||
return $records;
|
||||
}
|
||||
|
||||
$existingA = array_map(
|
||||
fn (array $record): string => $record['name'] ?? '',
|
||||
array_filter($records, fn (array $record): bool => ($record['type'] ?? '') === 'A')
|
||||
);
|
||||
$existingAAAA = array_map(
|
||||
fn (array $record): string => $record['name'] ?? '',
|
||||
array_filter($records, fn (array $record): bool => ($record['type'] ?? '') === 'AAAA')
|
||||
);
|
||||
|
||||
foreach ($labels as $label) {
|
||||
if (! in_array($label, $existingA, true)) {
|
||||
$records[] = ['name' => $label, 'type' => 'A', 'content' => $ipv4, 'ttl' => $ttl];
|
||||
}
|
||||
|
||||
if ($ipv6 && ! in_array($label, $existingAAAA, true)) {
|
||||
$records[] = ['name' => $label, 'type' => 'AAAA', 'content' => $ipv6, 'ttl' => $ttl];
|
||||
}
|
||||
}
|
||||
|
||||
return $records;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, string> $nameservers
|
||||
* @return array<int, string>
|
||||
*/
|
||||
protected function getNameserverLabels(string $domain, array $nameservers): array
|
||||
{
|
||||
$domain = rtrim($domain, '.');
|
||||
$labels = [];
|
||||
|
||||
foreach ($nameservers as $nameserver) {
|
||||
$nameserver = rtrim($nameserver ?? '', '.');
|
||||
|
||||
if ($nameserver === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($nameserver === $domain) {
|
||||
$label = '@';
|
||||
} elseif (str_ends_with($nameserver, '.'.$domain)) {
|
||||
$label = substr($nameserver, 0, -strlen('.'.$domain));
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($label !== '@') {
|
||||
$labels[] = $label;
|
||||
}
|
||||
}
|
||||
|
||||
return array_values(array_unique($labels));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ class MigrationDnsSyncService
|
||||
$settings = DnsSetting::getAll();
|
||||
$hostname = $this->getServerHostname();
|
||||
$serverIp = $this->getServerIp();
|
||||
$serverIpv6 = $settings['default_ipv6'] ?? null;
|
||||
|
||||
$this->agent->send('dns.sync_zone', [
|
||||
'domain' => $domain->domain,
|
||||
@@ -61,6 +62,7 @@ class MigrationDnsSyncService
|
||||
'ns2' => $settings['ns2'] ?? "ns2.{$hostname}",
|
||||
'admin_email' => $settings['admin_email'] ?? "admin.{$hostname}",
|
||||
'default_ip' => $settings['default_ip'] ?? $serverIp,
|
||||
'default_ipv6' => $serverIpv6,
|
||||
'default_ttl' => (int) ($settings['default_ttl'] ?? 3600),
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
@@ -117,6 +119,16 @@ class MigrationDnsSyncService
|
||||
$records[] = ['name' => 'mail', 'type' => 'AAAA', 'content' => $defaultIpv6, 'ttl' => $defaultTtl];
|
||||
}
|
||||
|
||||
$records = $this->appendNameserverRecords(
|
||||
$records,
|
||||
$domain->domain,
|
||||
$ns1,
|
||||
$ns2,
|
||||
$defaultIp,
|
||||
$defaultIpv6,
|
||||
$defaultTtl
|
||||
);
|
||||
|
||||
return $records;
|
||||
}
|
||||
|
||||
@@ -150,6 +162,79 @@ class MigrationDnsSyncService
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array<string, mixed>> $records
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
protected function appendNameserverRecords(
|
||||
array $records,
|
||||
string $domain,
|
||||
string $ns1,
|
||||
string $ns2,
|
||||
string $ipv4,
|
||||
?string $ipv6,
|
||||
int $ttl
|
||||
): array {
|
||||
$labels = $this->getNameserverLabels($domain, [$ns1, $ns2]);
|
||||
|
||||
if ($labels === []) {
|
||||
return $records;
|
||||
}
|
||||
|
||||
$existingA = array_map(
|
||||
fn (array $record): string => $record['name'] ?? '',
|
||||
array_filter($records, fn (array $record): bool => ($record['type'] ?? '') === 'A')
|
||||
);
|
||||
$existingAAAA = array_map(
|
||||
fn (array $record): string => $record['name'] ?? '',
|
||||
array_filter($records, fn (array $record): bool => ($record['type'] ?? '') === 'AAAA')
|
||||
);
|
||||
|
||||
foreach ($labels as $label) {
|
||||
if (! in_array($label, $existingA, true)) {
|
||||
$records[] = ['name' => $label, 'type' => 'A', 'content' => $ipv4, 'ttl' => $ttl];
|
||||
}
|
||||
|
||||
if ($ipv6 && ! in_array($label, $existingAAAA, true)) {
|
||||
$records[] = ['name' => $label, 'type' => 'AAAA', 'content' => $ipv6, 'ttl' => $ttl];
|
||||
}
|
||||
}
|
||||
|
||||
return $records;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, string> $nameservers
|
||||
* @return array<int, string>
|
||||
*/
|
||||
protected function getNameserverLabels(string $domain, array $nameservers): array
|
||||
{
|
||||
$domain = rtrim($domain, '.');
|
||||
$labels = [];
|
||||
|
||||
foreach ($nameservers as $nameserver) {
|
||||
$nameserver = rtrim($nameserver ?? '', '.');
|
||||
|
||||
if ($nameserver === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($nameserver === $domain) {
|
||||
$label = '@';
|
||||
} elseif (str_ends_with($nameserver, '.'.$domain)) {
|
||||
$label = substr($nameserver, 0, -strlen('.'.$domain));
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($label !== '@') {
|
||||
$labels[] = $label;
|
||||
}
|
||||
}
|
||||
|
||||
return array_values(array_unique($labels));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Collection<int, DnsRecord> $records
|
||||
* @return array<int, array<string, mixed>>
|
||||
|
||||
@@ -7251,12 +7251,42 @@ function dnsSyncZone(array $params): array {
|
||||
$ns2 = $params['ns2'] ?? 'ns2.example.com';
|
||||
$adminEmail = $params['admin_email'] ?? 'admin.example.com';
|
||||
$defaultTtl = $params['default_ttl'] ?? 3600;
|
||||
$defaultIpv4 = $params['default_ip'] ?? null;
|
||||
$defaultIpv6 = $params['default_ipv6'] ?? null;
|
||||
|
||||
if (empty($domain)) return ['success' => false, 'error' => 'Domain required'];
|
||||
|
||||
$zonesDir = '/etc/bind/zones';
|
||||
$zoneFile = "{$zonesDir}/db.{$domain}";
|
||||
$isNew = !file_exists($zoneFile);
|
||||
$domainName = rtrim($domain, '.');
|
||||
$ns1 = rtrim($ns1, '.');
|
||||
$ns2 = rtrim($ns2, '.');
|
||||
|
||||
$recordMap = ['A' => [], 'AAAA' => []];
|
||||
|
||||
foreach ($records as $r) {
|
||||
$type = strtoupper($r['type'] ?? 'A');
|
||||
if (!in_array($type, ['A', 'AAAA'], true)) continue;
|
||||
$name = trim($r['name'] ?? '@');
|
||||
if ($name === '' || $name === '@') {
|
||||
$fqdn = $domainName;
|
||||
} else {
|
||||
$fqdn = rtrim($name, '.');
|
||||
if (substr($fqdn, -strlen(".{$domainName}")) !== ".{$domainName}") {
|
||||
$fqdn .= ".{$domainName}";
|
||||
}
|
||||
}
|
||||
$recordMap[$type][$fqdn] = $r['content'] ?? '';
|
||||
}
|
||||
|
||||
if (empty($defaultIpv4)) {
|
||||
$defaultIpv4 = $recordMap['A'][$domainName] ?? (!empty($recordMap['A']) ? reset($recordMap['A']) : null);
|
||||
}
|
||||
|
||||
if (empty($defaultIpv6)) {
|
||||
$defaultIpv6 = $recordMap['AAAA'][$domainName] ?? (!empty($recordMap['AAAA']) ? reset($recordMap['AAAA']) : null);
|
||||
}
|
||||
|
||||
// Ensure zones directory exists
|
||||
if (!is_dir($zonesDir)) {
|
||||
@@ -7267,6 +7297,33 @@ function dnsSyncZone(array $params): array {
|
||||
$zoneContent = "\$TTL {$defaultTtl}\n@ IN SOA {$ns1}. {$adminEmail}. ({$serial} 3600 1800 604800 86400)\n";
|
||||
$zoneContent .= "@ IN NS {$ns1}.\n@ IN NS {$ns2}.\n";
|
||||
|
||||
$glueNames = [];
|
||||
foreach (array_filter([$ns1, $ns2]) as $ns) {
|
||||
if ($ns === $domainName) {
|
||||
$label = '@';
|
||||
} elseif (substr($ns, -strlen(".{$domainName}")) === ".{$domainName}") {
|
||||
$label = substr($ns, 0, -strlen(".{$domainName}"));
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
$glueNames[$label] = $ns;
|
||||
}
|
||||
|
||||
foreach ($glueNames as $label => $fqdn) {
|
||||
if ($label === '@') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($recordMap['A'][$fqdn]) && !empty($defaultIpv4)) {
|
||||
$zoneContent .= "{$label}\tIN\tA\t{$defaultIpv4}\n";
|
||||
}
|
||||
|
||||
if (!isset($recordMap['AAAA'][$fqdn]) && !empty($defaultIpv6)) {
|
||||
$zoneContent .= "{$label}\tIN\tAAAA\t{$defaultIpv6}\n";
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($records as $r) {
|
||||
$name = $r['name'] ?? '@';
|
||||
$type = $r['type'] ?? 'A';
|
||||
|
||||
74
tests/Unit/MigrationDnsSyncServiceTest.php
Normal file
74
tests/Unit/MigrationDnsSyncServiceTest.php
Normal file
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use App\Models\DnsSetting;
|
||||
use App\Models\Domain;
|
||||
use App\Services\Agent\AgentClient;
|
||||
use App\Services\Migration\MigrationDnsSyncService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use ReflectionMethod;
|
||||
use Tests\TestCase;
|
||||
|
||||
class MigrationDnsSyncServiceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_default_records_include_nameserver_glue_records(): void
|
||||
{
|
||||
DnsSetting::set('ns1', 'ns1.jabali-panel.com');
|
||||
DnsSetting::set('ns2', 'ns2.jabali-panel.com');
|
||||
DnsSetting::set('default_ip', '182.54.236.100');
|
||||
DnsSetting::set('default_ipv6', '2001:db8::1');
|
||||
DnsSetting::clearCache();
|
||||
|
||||
$domain = null;
|
||||
|
||||
Domain::withoutEvents(function () use (&$domain): void {
|
||||
$domain = Domain::factory()->create([
|
||||
'domain' => 'jabali-panel.com',
|
||||
'ip_address' => null,
|
||||
'ipv6_address' => null,
|
||||
]);
|
||||
});
|
||||
|
||||
$this->assertNotNull($domain);
|
||||
|
||||
$service = new MigrationDnsSyncService($this->createMock(AgentClient::class));
|
||||
$method = new ReflectionMethod($service, 'getDefaultRecords');
|
||||
$method->setAccessible(true);
|
||||
|
||||
$records = $method->invoke($service, $domain);
|
||||
|
||||
$this->assertTrue($this->recordExists($records, 'ns1', 'A', '182.54.236.100'));
|
||||
$this->assertTrue($this->recordExists($records, 'ns2', 'A', '182.54.236.100'));
|
||||
$this->assertTrue($this->recordExists($records, 'ns1', 'AAAA', '2001:db8::1'));
|
||||
$this->assertTrue($this->recordExists($records, 'ns2', 'AAAA', '2001:db8::1'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array<string, mixed>> $records
|
||||
*/
|
||||
private function recordExists(array $records, string $name, string $type, string $content): bool
|
||||
{
|
||||
foreach ($records as $record) {
|
||||
if (($record['name'] ?? null) !== $name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (($record['type'] ?? null) !== $type) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (($record['content'] ?? null) !== $content) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user