diff --git a/subdomains/README.md b/subdomains/README.md index e149fddd..e0d04d27 100644 --- a/subdomains/README.md +++ b/subdomains/README.md @@ -9,17 +9,32 @@ The token needs to have read permissions for `Zone.Zone` and write for `Zone.Dns By default every server has a subdomain limit of 0. You can change this limit by editing the server in the admin area. -Note: You can't create subdomains for servers with `0.0.0.0` or `::` as allocation! - -## Configuring domains +### Domains Each domain is composed of a name and an optional prefix. The name must be a valid Cloudflare Zone, while the prefix can be used to specify a subdomain on which the server subdomains will be created. For example: when creating a subdomain `server1` on a domain with name `example.com` and prefix `abc`, the created record will be `server1.abc.example.com`. -## SRV Records +## Configuration + +Subdomains support several different DNS Record types. Each type has different requirements before it can be created. + +If a DNS Record type is not available, check whether all of it's requirements have been met. + +### Valid primary allocation addresses + +A and AAAA Subdomains point to the IP address of the server's primary allocation, so they require that IP address to be valid. + +The only invalid values are `0.0.0.0` and `::`. They should be changed to proper IP addresses on which your servers can be reached. + +### Subdomain targets + +CNAME and SRV Subdomains must point to a specific Subdomain target. These can be configured for every node individually in the admin area. + +Note: According to [RFC2782](https://www.rfc-editor.org/info/rfc2782/), SRV records must always point to either an A or AAAA record. While some applications may handle SRV records pointing to CNAME records correctly, this can lead to undefined behavior. + +### SRV service types -In order to create SRV records instead of A/AAAA you need to do the following: +SRV Subdomains require an SRV service type. This must be configured in the egg features section. The format is `srv-` and then the service name, e.g. `srv-minecraft` or `srv-rust`. -1. Set a `SRV target` for the node -2. Add a [SRV service type](https://github.com/pelican/plugins/blob/main/subdomains/src/Enums/SRVServiceType.php#L10-L15) to the features of the egg. The format is `srv-` and then the service name, e.g. `srv-minecraft` or `srv-rust`. +You can find the list of currently supported SRV service types [here](https://github.com/pelican/plugins/blob/main/subdomains/src/Enums/SRVServiceType.php#L10-L15). diff --git a/subdomains/database/migrations/006_rename_srv_target_to_subdomain_target.php b/subdomains/database/migrations/006_rename_srv_target_to_subdomain_target.php new file mode 100644 index 00000000..87d2bd36 --- /dev/null +++ b/subdomains/database/migrations/006_rename_srv_target_to_subdomain_target.php @@ -0,0 +1,22 @@ +renameColumn('srv_target', 'subdomain_target'); + }); + } + + public function down(): void + { + Schema::table('nodes', function (Blueprint $table) { + $table->renameColumn('subdomain_target', 'srv_target'); + }); + } +}; diff --git a/subdomains/lang/de/strings.php b/subdomains/lang/de/strings.php index 7c32c2ad..a3751340 100644 --- a/subdomains/lang/de/strings.php +++ b/subdomains/lang/de/strings.php @@ -16,8 +16,8 @@ 'prefix' => 'Präfix', 'record_type' => 'Record Typ', 'is_synced' => 'Ist synchronisiert?', - 'srv_target' => 'SRV Ziel', - 'no_srv_target' => 'Kein SRV Ziel', + 'subdomain_target' => 'Subdomain Ziel', + 'no_subdomain_target' => 'Kein Subdomain Ziel', 'sync' => 'Synchronisieren', diff --git a/subdomains/lang/en/strings.php b/subdomains/lang/en/strings.php index 7ce18016..e8956032 100644 --- a/subdomains/lang/en/strings.php +++ b/subdomains/lang/en/strings.php @@ -16,8 +16,8 @@ 'prefix' => 'Prefix', 'record_type' => 'Record type', 'is_synced' => 'Is Synced?', - 'srv_target' => 'SRV target', - 'no_srv_target' => 'No SRV target', + 'subdomain_target' => 'Subdomain target', + 'no_subdomain_target' => 'No Subdomain target', 'sync' => 'Sync', diff --git a/subdomains/src/Enums/RecordType.php b/subdomains/src/Enums/RecordType.php new file mode 100644 index 00000000..91452936 --- /dev/null +++ b/subdomains/src/Enums/RecordType.php @@ -0,0 +1,47 @@ +name; + } + + /** + * @return array + */ + public static function availableRecordTypes(Server $server): array + { + if (!$server->allocation) { + return []; + } + + $types = []; + + if (!in_array($server->allocation->ip, ['0.0.0.0', '::'])) { + if (is_ipv6($server->allocation->ip)) { + $types[self::AAAA->name] = self::AAAA->value; + } else { + $types[self::A->name] = self::A->value; + } + } + + // @phpstan-ignore property.notFound + if ($server->node->subdomain_target) { + $types[self::CNAME->name] = self::CNAME->value; + $types[self::SRV->name] = self::SRV->value; + } + + return $types; + } +} diff --git a/subdomains/src/Filament/Admin/Resources/Servers/RelationManagers/SubdomainRelationManager.php b/subdomains/src/Filament/Admin/Resources/Servers/RelationManagers/SubdomainRelationManager.php index 721128b4..c2ac85ec 100644 --- a/subdomains/src/Filament/Admin/Resources/Servers/RelationManagers/SubdomainRelationManager.php +++ b/subdomains/src/Filament/Admin/Resources/Servers/RelationManagers/SubdomainRelationManager.php @@ -3,6 +3,7 @@ namespace Boy132\Subdomains\Filament\Admin\Resources\Servers\RelationManagers; use App\Models\Server; +use Boy132\Subdomains\Enums\RecordType; use Boy132\Subdomains\Models\CloudflareDomain; use Boy132\Subdomains\Models\Subdomain; use Boy132\Subdomains\Rules\NotOnBlacklist; @@ -84,7 +85,7 @@ public function table(Table $table): Table }), CreateAction::make() ->visible(fn () => CloudflareDomain::count() > 0) - ->disabled(fn () => !$this->getOwnerRecord()->allocation || in_array($this->getOwnerRecord()->allocation->ip, ['0.0.0.0', '::'])) + ->disabled(fn () => count(RecordType::availableRecordTypes($this->getOwnerRecord())) <= 0) ->createAnother(false) ->action(function (array $data, SubdomainService $service) { try { @@ -132,21 +133,11 @@ public function form(Schema $schema): Schema Select::make('record_type') ->label(trans('subdomains::strings.record_type')) ->disabledOn('edit') - ->hidden(fn () => is_null($this->getOwnerRecord()->node->srv_target)) // @phpstan-ignore property.notFound - ->dehydratedWhenHidden() + ->disabled(fn () => count(RecordType::availableRecordTypes($this->getOwnerRecord())) <= 1) ->required() ->selectablePlaceholder(false) - ->options(function () { - $types = is_ipv6($this->getOwnerRecord()->allocation->ip) ? ['AAAA' => 'AAAA'] : ['A' => 'A']; - - // @phpstan-ignore property.notFound - if (!is_null($this->getOwnerRecord()->node->srv_target)) { - $types['SRV'] = 'SRV'; - } - - return $types; - }) - ->default(fn () => is_ipv6($this->getOwnerRecord()->allocation->ip) ? 'AAAA' : 'A'), + ->options(RecordType::availableRecordTypes($this->getOwnerRecord())) + ->default(array_first(RecordType::availableRecordTypes($this->getOwnerRecord()))), ]); } } diff --git a/subdomains/src/Filament/Admin/Resources/SrvTargets/Pages/ManageSrvTargets.php b/subdomains/src/Filament/Admin/Resources/SrvTargets/Pages/ManageSrvTargets.php deleted file mode 100644 index 5699c79d..00000000 --- a/subdomains/src/Filament/Admin/Resources/SrvTargets/Pages/ManageSrvTargets.php +++ /dev/null @@ -1,11 +0,0 @@ -url(fn (Node $node) => user()?->can('update', $node) ? EditNode::getUrl(['record' => $node]) : null), TextColumn::make('fqdn') ->label(trans('admin/node.table.address')), - TextInputColumn::make('srv_target') - ->label(trans('subdomains::strings.srv_target')) - ->placeholder(trans('subdomains::strings.no_srv_target')) + TextInputColumn::make('subdomain_target') + ->label(trans('subdomains::strings.subdomain_target')) + ->placeholder(trans('subdomains::strings.no_subdomain_target')) ->updateStateUsing(function (Node $node, $state) { $node->forceFill([ - 'srv_target' => $state, + 'subdomain_target' => $state, ])->save(); }), ]) @@ -54,7 +54,7 @@ public static function table(Table $table): Table public static function getPages(): array { return [ - 'index' => ManageSrvTargets::route('/'), + 'index' => ManageSubdomainTargets::route('/'), ]; } } diff --git a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php index fa724741..2cd73478 100644 --- a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php +++ b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php @@ -5,6 +5,7 @@ use App\Models\Server; use App\Traits\Filament\BlockAccessInConflict; use App\Traits\Filament\HasLimitBadge; +use Boy132\Subdomains\Enums\RecordType; use Boy132\Subdomains\Filament\Server\Resources\Subdomains\Pages\ListSubdomains; use Boy132\Subdomains\Models\CloudflareDomain; use Boy132\Subdomains\Models\Subdomain; @@ -42,7 +43,7 @@ public static function canAccess(): bool /** @var Server $server */ $server = Filament::getTenant(); - return parent::canAccess() && $server->allocation && !in_array($server->allocation->ip, ['0.0.0.0', '::']) && CloudflareDomain::count() > 0; + return parent::canAccess() && CloudflareDomain::count() > 0 && count(RecordType::availableRecordTypes($server)) > 0; } public static function getNavigationLabel(): string @@ -138,6 +139,9 @@ public static function table(Table $table): Table public static function form(Schema $schema): Schema { + /** @var Server $server */ + $server = Filament::getTenant(); + return $schema ->components([ TextInput::make('name') @@ -163,35 +167,11 @@ public static function form(Schema $schema): Schema Select::make('record_type') ->label(trans('subdomains::strings.record_type')) ->disabledOn('edit') - ->hidden(function () { - /** @var Server $server */ - $server = Filament::getTenant(); - - // @phpstan-ignore property.notFound - return is_null($server->node->srv_target); - }) - ->dehydratedWhenHidden() + ->disabled(fn () => count(RecordType::availableRecordTypes($server)) <= 1) ->required() ->selectablePlaceholder(false) - ->options(function () { - /** @var Server $server */ - $server = Filament::getTenant(); - - $types = is_ipv6($server->allocation->ip) ? ['AAAA' => 'AAAA'] : ['A' => 'A']; - - // @phpstan-ignore property.notFound - if (!is_null($server->node->srv_target)) { - $types['SRV'] = 'SRV'; - } - - return $types; - }) - ->default(function () { - /** @var Server $server */ - $server = Filament::getTenant(); - - return is_ipv6($server->allocation->ip) ? 'AAAA' : 'A'; - }), + ->options(RecordType::availableRecordTypes($server)) + ->default(array_first(RecordType::availableRecordTypes($server))), ]); } diff --git a/subdomains/src/Models/Subdomain.php b/subdomains/src/Models/Subdomain.php index 4c834c4f..12ab4469 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -62,47 +62,71 @@ public function upsertOnCloudflare(): void throw new Exception('Server has no allocation'); } - if ($this->record_type === 'SRV') { - $srvTarget = $this->server->node->srv_target; // @phpstan-ignore property.notFound + $subdomainTarget = $this->server->node->subdomain_target; // @phpstan-ignore property.notFound - if (!$srvTarget) { - throw new Exception('Node has no SRV target'); - } + switch ($this->record_type) { + case 'SRV': + if (!$subdomainTarget) { + throw new Exception('Node has no Subdomain target'); + } - $srvServiceType = SRVServiceType::fromServer($this->server); + $srvServiceType = SRVServiceType::fromServer($this->server); - if (!$srvServiceType) { - throw new Exception('Server has no SRV type'); - } + if (!$srvServiceType) { + throw new Exception('Server has no SRV type'); + } - $searchName = $this->domain->prependPrefix("$srvServiceType->value.$this->name"); - - $payload = [ - 'name' => $searchName, - 'type' => $this->record_type, - 'comment' => 'Created by Pelican Subdomains plugin', - 'data' => [ - 'port' => $this->server->allocation->port, - 'priority' => 0, - 'target' => $srvTarget, - 'weight' => 0, - ], - 'proxied' => false, - ]; - } else { - if (in_array($this->server->allocation->ip, ['0.0.0.0', '::'])) { - throw new Exception('Server has invalid allocation ip (0.0.0.0 or ::)'); - } + $searchName = $this->domain->prependPrefix("$srvServiceType->value.$this->name"); + + $payload = [ + 'name' => $searchName, + 'type' => $this->record_type, + 'comment' => 'Created by Pelican Subdomains plugin', + 'data' => [ + 'port' => $this->server->allocation->port, + 'priority' => 0, + 'target' => $subdomainTarget, + 'weight' => 0, + ], + 'proxied' => false, + ]; + break; + + case 'CNAME': + if (!$subdomainTarget) { + throw new Exception('Node has no Subdomain target'); + } + + $searchName = $this->domain->prependPrefix($this->name); + + $payload = [ + 'name' => $searchName, + 'type' => $this->record_type, + 'comment' => 'Created by Pelican Subdomains plugin', + 'content' => $subdomainTarget, + 'proxied' => false, + ]; + break; + + case 'A': + case 'AAAA': + if (in_array($this->server->allocation->ip, ['0.0.0.0', '::'])) { + throw new Exception('Server has invalid allocation ip (0.0.0.0 or ::)'); + } + + $searchName = $this->domain->prependPrefix($this->name); - $searchName = $this->domain->prependPrefix($this->name); + $payload = [ + 'name' => $searchName, + 'type' => $this->record_type, + 'comment' => 'Created by Pelican Subdomains plugin', + 'content' => $this->server->allocation->ip, + 'proxied' => false, + ]; + break; - $payload = [ - 'name' => $searchName, - 'type' => $this->record_type, - 'comment' => 'Created by Pelican Subdomains plugin', - 'content' => $this->server->allocation->ip, - 'proxied' => false, - ]; + default: + throw new Exception('Requested subdomain type is unsupported'); } // @phpstan-ignore staticMethod.notFound