Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions subdomains/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,14 @@ CNAME and SRV Subdomains must point to a specific Subdomain target. These can be

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.

### Use Allocation Alias

You can specify whether servers on a specific node should use the allocation Alias instead of the allocation IP when creating A and AAAA records.

This option is recommended if your node is not directly exposed to the internet.

If your allocation Alias is not a valid IPv4 / IPv6 address, the corresponding record types will not be available.

### SRV service types

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`.
Expand Down
22 changes: 22 additions & 0 deletions subdomains/database/migrations/010_add_alias_toggle_to_nodes.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
public function up(): void
{
Schema::table('nodes', function (Blueprint $table) {
$table->boolean('subdomain_use_alias')->default(false)->after('subdomain_target');
});
}

public function down(): void
{
Schema::table('nodes', function (Blueprint $table) {
$table->dropColumn('subdomain_use_alias');
});
}
};
1 change: 1 addition & 0 deletions subdomains/lang/de/strings.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
'is_synced' => 'Ist synchronisiert?',
'subdomain_target' => 'Subdomain Ziel',
'no_subdomain_target' => 'Kein Subdomain Ziel',
'use_allocation_alias' => 'Allocation Alias verwenden',

'sync' => 'Synchronisieren',

Expand Down
1 change: 1 addition & 0 deletions subdomains/lang/en/strings.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
'is_synced' => 'Is Synced?',
'subdomain_target' => 'Subdomain target',
'no_subdomain_target' => 'No Subdomain target',
'use_allocation_alias' => 'Use allocation alias',

'sync' => 'Sync',

Expand Down
74 changes: 74 additions & 0 deletions subdomains/src/Enums/RecordType.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@

namespace Boy132\Subdomains\Enums;

use App\Models\Allocation;
use App\Models\Server;
use Boy132\Subdomains\Models\CloudflareDomain;
use Filament\Support\Contracts\HasLabel;
use Illuminate\Support\Collection;

enum RecordType: string implements HasLabel
{
Expand All @@ -15,4 +19,74 @@ public function getLabel(): string
{
return $this->name;
}

/**
* Returns errors that prevent this record type from being used with the provided server and domain.
* If empty, then this record type is allowed to be used.
* Most important error is always returned first.
*
* @return Collection<string>
*/
public function canBeUsedErrors(Server $server, CloudflareDomain $domain): Collection
{
$errors = new Collection();

$allocation = $server->allocation;

$targetAddress = '';
if ($allocation) {
if (in_array($allocation->ip, ['0.0.0.0', '::'])) {
$errors->add('Allocation ip is invalid (0.0.0.0 or ::)');
}

$targetAddress = $server->node->subdomain_use_alias ? $allocation->ip_alias : $allocation->ip; // @phpstan-ignore property.notFound
}

$subdomainTarget = $server->node->subdomain_target; // @phpstan-ignore property.notFound
$srvServiceType = SRVServiceType::fromServer($server);

$node_id = $server->node->id;

// General restrictions checks

if (!($domain->nodes->isEmpty() || $domain->nodes()->where('nodes.id', $node_id)->exists())) {
$errors->add('Domain ' . $domain->nameWithPrefix() . ' is not permitted on node ' . $server->node->name);
}

if (!($domain->allowed_record_types->isEmpty() || $domain->allowed_record_types->contains($this))) {
$errors->add('Record type ' . $this->value . ' is not permitted on domain ' . $domain->nameWithPrefix());
}

// Allocation checks

if (in_array($this, [self::A, self::AAAA, self::SRV]) && !$allocation) {
$errors->add('Server has no allocation');
}

if (in_array($targetAddress, ['0.0.0.0', '::'])) {
$errors->add('Allocation target address is invalid (0.0.0.0 or ::)');
}

if ($this == self::A && !is_ipv4($targetAddress)) {
$errors->add('Allocation target address ' . $targetAddress . ' is not a valid IPv4 address');
}

if ($this == self::AAAA && !is_ipv6($targetAddress)) {
$errors->add('Allocation target address ' . $targetAddress . ' is not a valid IPv6 address');
}

// Subdomain target checks

if (in_array($this, [self::CNAME, self::SRV]) && !$subdomainTarget) {
$errors->add('Server has no Subdomain target');
}

// Other checks

if ($this == self::SRV && !$srvServiceType) {
$errors->add('Server has no SRV service type');
}

return $errors;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Filament\Resources\Resource;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Columns\TextInputColumn;
use Filament\Tables\Columns\ToggleColumn;
use Filament\Tables\Table;

class SubdomainTargetResource extends Resource
Expand Down Expand Up @@ -45,6 +46,13 @@ public static function table(Table $table): Table
'subdomain_target' => $state,
])->save();
}),
ToggleColumn::make('subdomain_use_alias')
->label(trans('subdomains::strings.use_allocation_alias'))
->updateStateUsing(function (Node $node, $state) {
$node->forceFill([
'subdomain_use_alias' => $state,
])->save();
}),
])
->emptyStateIcon('tabler-world-www')
->emptyStateDescription('')
Expand Down
39 changes: 6 additions & 33 deletions subdomains/src/Models/CloudflareDomain.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
use App\Models\Node;
use App\Models\Server;
use Boy132\Subdomains\Enums\RecordType;
use Boy132\Subdomains\Enums\SRVServiceType;
use Exception;
use Illuminate\Database\Eloquent\Casts\AsEnumCollection;
use Illuminate\Database\Eloquent\Model;
Expand Down Expand Up @@ -66,7 +65,7 @@ public function nameWithPrefix(): string
return $this->prefix == '' ? $this->name : "$this->prefix.$this->name";
}

public function prependPrefix(string $subdomain): string
public function appendPrefix(string $subdomain): string
{
return $this->prefix == '' ? $subdomain : "$subdomain.$this->prefix";
}
Expand Down Expand Up @@ -97,40 +96,14 @@ public function fetchCloudflareId(): void
}

/**
* @return Collection<string>
* @return Collection<RecordType>
*/
public function availableRecordTypes(Server $server): Collection
{
$allocation = $server->allocation;
$subdomainTarget = $server->node->subdomain_target; // @phpstan-ignore property.notFound
$allowedRecordTypes = $this->allowed_record_types;
$allowedRecordsFilterDisabled = $allowedRecordTypes->isEmpty();
$srvServiceType = SRVServiceType::fromServer($server);

$types = new Collection();

// Explicitly forbid ANY record creation when primary allocation is invalid
if ($allocation && in_array($allocation->ip, ['0.0.0.0', '::'])) {
return $types;
}

if (($allowedRecordsFilterDisabled || $allowedRecordTypes->contains(RecordType::A)) && $allocation && is_ipv4($allocation->ip)) {
$types->add(RecordType::A);
}

if (($allowedRecordsFilterDisabled || $allowedRecordTypes->contains(RecordType::AAAA)) && $allocation && is_ipv6($allocation->ip)) {
$types->add(RecordType::AAAA);
}

if (($allowedRecordsFilterDisabled || $allowedRecordTypes->contains(RecordType::CNAME)) && $subdomainTarget) {
$types->add(RecordType::CNAME);
}

if (($allowedRecordsFilterDisabled || $allowedRecordTypes->contains(RecordType::SRV)) && $allocation && $subdomainTarget && $srvServiceType) {
$types->add(RecordType::SRV);
}

return $types;
return collect(RecordType::cases())
->filter(fn ($recordType) => $recordType
->canBeUsedErrors($server, $this)
->isEmpty());
}

/**
Expand Down
55 changes: 15 additions & 40 deletions subdomains/src/Models/Subdomain.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,46 +66,33 @@ public function getLabel(): string|Htmlable|null
/** @throws Exception */
public function upsertOnCloudflare(): void
{
// Explicitly forbid ANY record creation when primary allocation is invalid
if ($this->server->allocation && in_array($this->server->allocation->ip, ['0.0.0.0', '::'])) {
throw new Exception('Server has invalid allocation ip (0.0.0.0 or ::)');
$errors = $this->record_type->canBeUsedErrors($this->server, $this->domain);
if ($errors->isNotEmpty()) {
throw new Exception($errors->first());
}

$subdomainTarget = $this->server->node->subdomain_target; // @phpstan-ignore property.notFound
$node_id = $this->server->node->id;
$allocation = $this->server->allocation;

if (!($this->domain->nodes->isEmpty() || $this->domain->nodes()->where('nodes.id', $node_id)->exists())) {
throw new Exception('Domain ' . $this->domain->nameWithPrefix() . ' is not permitted on node ' . $this->server->node->name);
$targetAddress = '';
if ($allocation) {
$targetAddress = $this->server->node->subdomain_use_alias ? $allocation->ip_alias : $allocation->ip; // @phpstan-ignore property.notFound
}

if (!($this->domain->allowed_record_types->isEmpty() || $this->domain->allowed_record_types->contains($this->record_type))) {
throw new Exception('Record type ' . $this->record_type->value . ' is not permitted on domain ' . $this->domain->nameWithPrefix());
}
$subdomainTarget = $this->server->node->subdomain_target; // @phpstan-ignore property.notFound
$srvServiceType = SRVServiceType::fromServer($this->server);

$searchName = $this->domain->appendPrefix($this->name);

switch ($this->record_type) {
case RecordType::SRV:
if (!$this->server->allocation) {
throw new Exception('Server has no allocation');
}

if (!$subdomainTarget) {
throw new Exception('Node has no Subdomain target');
}

$srvServiceType = SRVServiceType::fromServer($this->server);

if (!$srvServiceType) {
throw new Exception('Server has no SRV type');
}

$searchName = $this->domain->prependPrefix("$srvServiceType->value.$this->name");
$searchName = "$srvServiceType->value.$searchName";

$payload = [
'name' => $searchName,
'type' => $this->record_type->value,
'comment' => 'Created by Pelican Subdomains plugin',
'data' => [
'port' => $this->server->allocation->port,
'port' => $allocation->port,
'priority' => 0,
'target' => $subdomainTarget,
'weight' => 0,
Expand All @@ -115,12 +102,6 @@ public function upsertOnCloudflare(): void
break;

case RecordType::CNAME:
if (!$subdomainTarget) {
throw new Exception('Node has no Subdomain target');
}

$searchName = $this->domain->prependPrefix($this->name);

$payload = [
'name' => $searchName,
'type' => $this->record_type->value,
Expand All @@ -132,23 +113,17 @@ public function upsertOnCloudflare(): void

case RecordType::A:
case RecordType::AAAA:
if (!$this->server->allocation) {
throw new Exception('Server has no allocation');
}

$searchName = $this->domain->prependPrefix($this->name);

$payload = [
'name' => $searchName,
'type' => $this->record_type->value,
'comment' => 'Created by Pelican Subdomains plugin',
'content' => $this->server->allocation->ip,
'content' => $targetAddress,
'proxied' => false,
];
break;

default:
throw new Exception('Requested subdomain type is unsupported');
throw new Exception('Requested subdomain type '. $this->record_type . ' is unsupported');
}

// @phpstan-ignore staticMethod.notFound
Expand Down