Die eigenen Namen loesen jetzt auch im Tunnel auf
parent
a317145468
commit
22dd6df61f
|
|
@ -0,0 +1,68 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Services\Dns\HostDnsDirectory;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
/**
|
||||
* Macht Portal, Website und Statusseite im Management-Tunnel auflösbar.
|
||||
*
|
||||
* Ohne das trägt der Tunnel nur `10.66.0.0/24`, und diese Namen zeigen auf die
|
||||
* öffentliche Adresse des Servers — der Verkehr geht also am Tunnel vorbei, und
|
||||
* die Anwendung sieht die öffentliche Adresse des Anrufers statt seiner
|
||||
* `10.66.0.x`. Genau daran scheiterte „über VPN die verborgene Seite ansehen":
|
||||
* die Konsole ging, weil ihr Name als einziger umgebogen war.
|
||||
*
|
||||
* `files.` steht bewusst NICHT in der Liste. Dort holt ein Server im
|
||||
* Rettungssystem sein Bootstrap-Archiv, und der ist per Definition nicht im
|
||||
* Tunnel — bögen wir den Namen um, holte er es nie.
|
||||
*
|
||||
* Die Datei landet in demselben Verzeichnis, das dnsmasq mit `--hostsdir`
|
||||
* überwacht, und in dem RegisterHostDns schon die Host-Namen ablegt. Kein
|
||||
* Neustart, kein Reload: das Erscheinen der Datei IST die Veröffentlichung.
|
||||
*/
|
||||
class PublishTunnelNames extends Command
|
||||
{
|
||||
/** Eine Datei für die ganze Gruppe — siehe HostDnsDirectory::writeMany(). */
|
||||
private const KEY = 'platform';
|
||||
|
||||
protected $signature = 'clupilot:publish-tunnel-names';
|
||||
|
||||
protected $description = 'Macht Portal, Website und Statusseite im Management-Tunnel auflösbar';
|
||||
|
||||
public function handle(HostDnsDirectory $dns): int
|
||||
{
|
||||
// Ohne Tunnel gibt es keinen Gateway, der diese Namen bedienen könnte.
|
||||
// Eine hosts-Datei für einen Resolver, den niemand startet, ist nur
|
||||
// etwas, das beim späteren Einschalten falsch sein kann.
|
||||
if ((string) config('admin_access.vpn_internal_host') === '') {
|
||||
$dns->writeMany(self::KEY, [], '');
|
||||
$this->info('Kein Tunnel konfiguriert — nichts veröffentlicht.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$names = array_values(array_unique(array_filter(array_merge(
|
||||
[(string) config('admin_access.app_host')],
|
||||
array_map('strval', (array) config('admin_access.site_hosts', [])),
|
||||
[(string) config('admin_access.status_host')],
|
||||
))));
|
||||
|
||||
$hub = (string) config('provisioning.wireguard.hub_address');
|
||||
|
||||
$dns->writeMany(self::KEY, $names, $hub);
|
||||
|
||||
if ($names === []) {
|
||||
$this->info('Keine Hostnamen konfiguriert — nichts veröffentlicht.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
foreach ($names as $name) {
|
||||
$this->line(" {$hub} {$name}");
|
||||
}
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
|
@ -16,6 +16,9 @@ class FakeHostDnsDirectory implements HostDnsDirectory
|
|||
|
||||
public bool $failRemove = false;
|
||||
|
||||
/** @var array<string, array<int, string>> key => fqdns */
|
||||
public array $groups = [];
|
||||
|
||||
public function write(string $name, string $fqdn, string $ip): void
|
||||
{
|
||||
if ($this->failWrite) {
|
||||
|
|
@ -34,4 +37,20 @@ class FakeHostDnsDirectory implements HostDnsDirectory
|
|||
|
||||
unset($this->ips[$name], $this->fqdns[$name]);
|
||||
}
|
||||
|
||||
public function writeMany(string $key, array $fqdns, string $ip): void
|
||||
{
|
||||
if ($this->failWrite) {
|
||||
throw new RuntimeException('dns-hosts volume unavailable');
|
||||
}
|
||||
|
||||
if ($fqdns === []) {
|
||||
unset($this->groups[$key], $this->ips[$key]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->groups[$key] = array_values($fqdns);
|
||||
$this->ips[$key] = $ip;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,6 +42,30 @@ class FileHostDnsDirectory implements HostDnsDirectory
|
|||
}
|
||||
}
|
||||
|
||||
public function writeMany(string $key, array $fqdns, string $ip): void
|
||||
{
|
||||
if ($fqdns === []) {
|
||||
$this->remove($key);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->ensureDir();
|
||||
|
||||
$path = $this->path($key);
|
||||
$body = '';
|
||||
|
||||
foreach ($fqdns as $fqdn) {
|
||||
$body .= "{$ip} {$fqdn}\n";
|
||||
}
|
||||
|
||||
if (@file_put_contents($path, $body) === false) {
|
||||
throw new RuntimeException("Could not write DNS hosts entry: {$path}");
|
||||
}
|
||||
|
||||
@chmod($path, 0664);
|
||||
}
|
||||
|
||||
private function ensureDir(): void
|
||||
{
|
||||
$dir = $this->dir();
|
||||
|
|
|
|||
|
|
@ -14,4 +14,16 @@ interface HostDnsDirectory
|
|||
|
||||
/** Removes the host's entry, if one exists. */
|
||||
public function remove(string $name): void;
|
||||
|
||||
/**
|
||||
* Schreibt EINE Datei mit einer Zeile je FQDN. Eine leere Liste entfernt sie.
|
||||
*
|
||||
* Eine Datei statt einer je Name, weil diese Gruppe sich ändert: fällt ein
|
||||
* Website-Name weg, verschwindet er dadurch mit — bei einer Datei je Name
|
||||
* bliebe die verwaiste Zeile stehen und der alte Name zeigte weiter in den
|
||||
* Tunnel.
|
||||
*
|
||||
* @param array<int, string> $fqdns
|
||||
*/
|
||||
public function writeMany(string $key, array $fqdns, string $ip): void;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,96 @@
|
|||
<?php
|
||||
|
||||
use App\Services\Dns\FileHostDnsDirectory;
|
||||
use App\Services\Dns\HostDnsDirectory;
|
||||
|
||||
/**
|
||||
* Die eigenen Namen im Tunnel-Resolver.
|
||||
*
|
||||
* Über eine hosts-Datei im schon vorhandenen dns-hosts-Volume, das dnsmasq mit
|
||||
* --hostsdir überwacht — dieselbe Stelle, an der die Host-Namen landen. Kein
|
||||
* zweites `--address` im Compose-Befehl: das fängt zusätzlich jede Unterdomain
|
||||
* mit ab, und über eine variable Namensliste kann eine command-Zeile nicht
|
||||
* schleifen.
|
||||
*/
|
||||
beforeEach(function () {
|
||||
$this->dir = sys_get_temp_dir().'/clupilot-hosts-'.bin2hex(random_bytes(6));
|
||||
config()->set('provisioning.dns.hosts_dir', $this->dir);
|
||||
config()->set('provisioning.wireguard.hub_address', '10.66.0.1');
|
||||
config()->set('admin_access.vpn_internal_host', 'admin.clupilot.test');
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
foreach (glob($this->dir.'/*') ?: [] as $file) {
|
||||
@unlink($file);
|
||||
}
|
||||
@rmdir($this->dir);
|
||||
});
|
||||
|
||||
it('veröffentlicht Portal, Website und Statusseite auf der Hub-Adresse', function () {
|
||||
config()->set('admin_access.app_host', 'app.clupilot.test');
|
||||
config()->set('admin_access.site_hosts', ['www.clupilot.test', 'clupilot.test']);
|
||||
config()->set('admin_access.status_host', 'status.clupilot.test');
|
||||
|
||||
$this->artisan('clupilot:publish-tunnel-names')->assertSuccessful();
|
||||
|
||||
$written = file_get_contents($this->dir.'/platform.hosts');
|
||||
|
||||
expect($written)->toContain('10.66.0.1 app.clupilot.test')
|
||||
->and($written)->toContain('10.66.0.1 www.clupilot.test')
|
||||
->and($written)->toContain('10.66.0.1 clupilot.test')
|
||||
->and($written)->toContain('10.66.0.1 status.clupilot.test');
|
||||
});
|
||||
|
||||
it('lässt den Dateihost draußen', function () {
|
||||
// Ein Server im Rettungssystem holt dort sein Archiv — der ist per
|
||||
// Definition nicht im Tunnel. Bögen wir den Namen um, holte er es nie.
|
||||
config()->set('admin_access.app_host', 'app.clupilot.test');
|
||||
config()->set('admin_access.files_host', 'files.clupilot.test');
|
||||
|
||||
$this->artisan('clupilot:publish-tunnel-names')->assertSuccessful();
|
||||
|
||||
expect(file_get_contents($this->dir.'/platform.hosts'))
|
||||
->not->toContain('files.clupilot.test');
|
||||
});
|
||||
|
||||
it('räumt die Datei weg, wenn kein Name mehr konfiguriert ist', function () {
|
||||
config()->set('admin_access.app_host', 'app.clupilot.test');
|
||||
$this->artisan('clupilot:publish-tunnel-names')->assertSuccessful();
|
||||
expect(file_exists($this->dir.'/platform.hosts'))->toBeTrue();
|
||||
|
||||
// Eine Datei, nicht eine je Name: sonst bliebe beim Wechsel von zwei
|
||||
// Website-Namen auf einen die verwaiste Zeile im Resolver stehen, und der
|
||||
// alte Name zeigte weiter in den Tunnel.
|
||||
config()->set('admin_access.app_host', '');
|
||||
$this->artisan('clupilot:publish-tunnel-names')->assertSuccessful();
|
||||
|
||||
expect(file_exists($this->dir.'/platform.hosts'))->toBeFalse();
|
||||
});
|
||||
|
||||
it('tut nichts, solange kein Tunnel konfiguriert ist', function () {
|
||||
// Ohne VPN_INTERNAL_HOST gibt es den Gateway nicht, und eine hosts-Datei
|
||||
// für einen Resolver, den niemand startet, ist nur eine Datei, die beim
|
||||
// nächsten Einschalten falsch sein kann.
|
||||
config()->set('admin_access.vpn_internal_host', '');
|
||||
config()->set('admin_access.app_host', 'app.clupilot.test');
|
||||
|
||||
$this->artisan('clupilot:publish-tunnel-names')->assertSuccessful();
|
||||
|
||||
expect(file_exists($this->dir.'/platform.hosts'))->toBeFalse();
|
||||
});
|
||||
|
||||
it('schreibt genau eine Zeile je Name, im hosts-Format', function () {
|
||||
config()->set('admin_access.app_host', 'app.clupilot.test');
|
||||
config()->set('admin_access.site_hosts', ['www.clupilot.test']);
|
||||
|
||||
app(HostDnsDirectory::class)->writeMany('probe', ['a.test', 'b.test'], '10.66.0.1');
|
||||
|
||||
expect(file_get_contents($this->dir.'/probe.hosts'))
|
||||
->toBe("10.66.0.1 a.test\n10.66.0.1 b.test\n");
|
||||
});
|
||||
|
||||
it('ist dieselbe Datei-Umsetzung wie für die Host-Namen', function () {
|
||||
// Nicht Fake gegen Fake geprüft: die Rechte und das Anlegen des
|
||||
// Verzeichnisses sind der Teil, der in Produktion schiefgeht.
|
||||
expect(app(HostDnsDirectory::class))->toBeInstanceOf(FileHostDnsDirectory::class);
|
||||
})->skip(fn () => ! app()->environment('testing'), 'nur im Testlauf');
|
||||
Loading…
Reference in New Issue