CluPilotCloud/app/Support/HostTakeoverCommand.php

104 lines
3.8 KiB
PHP

<?php
namespace App\Support;
use App\Models\Host;
/**
* Die eine Zeile, die der Betreiber im Rettungssystem einfügt.
*
* An einer Stelle gebaut, weil sie an zwei Stellen gezeigt wird (beim Anlegen
* und beim Neuausstellen eines Codes) und weil jeder ihrer sieben Werte aus
* einer anderen Ecke kommt. Zwei Fassungen davon würden auseinanderlaufen, und
* der Unterschied fiele erst auf einem Server auf, der schon bestellt ist.
*
* Sie trägt ALLES, was das Skript vor dem Tunnel braucht (Spec §5): es holt
* nichts ab, es gibt nichts abzurufen. Deshalb steht der private
* WireGuard-Schlüssel darin — mit einer Lebensdauer von Minuten, denn Task 9
* des Skripts tauscht ihn gegen einen auf der Maschine erzeugten.
*/
final class HostTakeoverCommand
{
/** Wohin das Archiv ausgepackt wird. Das Skript sucht sich dort selbst. */
public const INSTALL_DIR = '/opt/clupilot';
/**
* @param array{code: string, private_key: string, public_key: string, wg_ip: string} $enrolment
*/
public static function for(Host $host, array $enrolment): string
{
return implode(' ', [
'mkdir -p '.self::INSTALL_DIR,
'&& curl -fsSL '.self::archiveUrl(),
'| tar xz -C '.self::INSTALL_DIR,
'&& sh '.self::INSTALL_DIR.'/bootstrap/clupilot-bootstrap.sh',
'--code '.$enrolment['code'],
// Einfache Anführungszeichen: WireGuard-Schlüssel sind base64 und
// enthalten +, / und =. Ohne sie zerlegt die Shell die Zeile an
// einer Stelle, die der Betreiber nicht sieht.
"--wg-private '".$enrolment['private_key']."'",
'--wg-ip '.$enrolment['wg_ip'].'/'.self::subnetPrefix(),
"--hub-pubkey '".config('provisioning.wireguard.hub_public_key')."'",
'--hub-endpoint '.config('provisioning.wireguard.endpoint'),
'--api http://'.config('provisioning.wireguard.hub_address'),
]);
}
/**
* Immer über den ÖFFENTLICHEN Hostnamen.
*
* Die Konsole läuft unter `admin.…`, aber diese Zeile wird auf einer
* Maschine ausgeführt, die den Adminbereich nicht erreichen darf und soll —
* er ist genau dafür abgeriegelt. `route()` von der Konsole aus lieferte den
* Konsolen-Hostnamen, und die Zeile liefe in eine 404 auf einem Server, an
* den man dann nur noch über die Anbieterkonsole kommt.
*/
public static function archiveUrl(): string
{
$appHost = (string) config('admin_access.app_host');
if ($appHost === '') {
return url('/bootstrap.tar.gz');
}
return 'https://'.$appHost.'/bootstrap.tar.gz';
}
/** Die Präfixlänge des Tunnel-Subnetzes, damit `--wg-ip` vollständig ist. */
private static function subnetPrefix(): string
{
$subnet = (string) config('provisioning.wireguard.subnet', '10.66.0.0/24');
return str_contains($subnet, '/') ? explode('/', $subnet)[1] : '24';
}
/**
* Was in der Zeile fehlt, damit sie überhaupt funktionieren kann.
*
* Ein leerer Hub-Schlüssel oder ein leerer Endpunkt ergibt eine Zeile, die
* sauber aussieht, kopiert wird, läuft — und in einem Tunnel endet, der nie
* einen Handshake hat. Das fiele erst auf der Maschine auf, nach der
* Installation. Lieber hier sagen, was fehlt.
*
* @return list<string>
*/
public static function missingSettings(): array
{
$missing = [];
if (blank(config('provisioning.wireguard.hub_public_key'))) {
$missing[] = 'CLUPILOT_WG_HUB_PUBKEY';
}
if (blank(config('provisioning.wireguard.endpoint'))) {
$missing[] = 'CLUPILOT_WG_ENDPOINT';
}
if (blank(config('provisioning.wireguard.hub_address'))) {
$missing[] = 'CLUPILOT_WG_HUB_ADDRESS';
}
return $missing;
}
}