183 lines
7.6 KiB
PHP
183 lines
7.6 KiB
PHP
<?php
|
|
|
|
use Symfony\Component\Process\Process;
|
|
|
|
/**
|
|
* Die Konfiguration des Tunnel-Gateways, wirklich ausgeführt.
|
|
*
|
|
* Nach dem Muster von HostStepTest: das Skript wird als Skript laufen gelassen,
|
|
* denn worauf es ankommt, ist was es AUSLÄSST — und ein Auslassen sieht man nur
|
|
* an der erzeugten Datei, nicht an der Absicht.
|
|
*
|
|
* Der Ausfall, um den es geht: Caddy startet nicht, wenn eine in `tls`
|
|
* genannte Datei fehlt. Ein noch nicht ausgestelltes Zertifikat für www. würde
|
|
* damit den Tunnel-Zugang zur KONSOLE mitnehmen — und das ist der Weg, auf dem
|
|
* sich ein ausgesperrter Betreiber zurückholt.
|
|
*/
|
|
function renderVpnConfig(array $env, array $certNames): string
|
|
{
|
|
$dir = sys_get_temp_dir().'/clupilot-vpn-'.bin2hex(random_bytes(6));
|
|
mkdir($dir.'/certs/certificates/acme', 0755, true);
|
|
|
|
foreach ($certNames as $name) {
|
|
file_put_contents($dir."/certs/certificates/acme/{$name}.crt", 'cert');
|
|
file_put_contents($dir."/certs/certificates/acme/{$name}.key", 'key');
|
|
}
|
|
|
|
$out = $dir.'/rendered.Caddyfile';
|
|
|
|
$env = array_merge([
|
|
'VPN_HUB_ADDRESS' => '10.66.0.1',
|
|
'VPN_HEALTH_PORT' => '8081',
|
|
'VPN_CERT_DIR' => $dir.'/certs',
|
|
'VPN_CONFIG_OUT' => $out,
|
|
'VPN_RENDER_ONLY' => '1',
|
|
], $env);
|
|
|
|
$process = Process::fromShellCommandline(
|
|
'bash '.escapeshellarg(base_path('docker/caddy/vpn-entrypoint.sh'))
|
|
);
|
|
$process->setEnv($env);
|
|
$process->run();
|
|
|
|
$rendered = is_file($out) ? file_get_contents($out) : '';
|
|
|
|
exec('rm -rf '.escapeshellarg($dir));
|
|
|
|
expect($process->getExitCode())->toBe(0, $process->getErrorOutput());
|
|
|
|
return $rendered;
|
|
}
|
|
|
|
it('bedient jeden Namen, für den ein Zertifikat da ist', function () {
|
|
$config = renderVpnConfig([
|
|
'VPN_INTERNAL_HOST' => 'admin.clupilot.test',
|
|
'VPN_TUNNEL_HOSTS' => 'app.clupilot.test,www.clupilot.test',
|
|
], ['admin.clupilot.test', 'app.clupilot.test', 'www.clupilot.test']);
|
|
|
|
expect($config)->toContain('https://admin.clupilot.test:443')
|
|
->and($config)->toContain('https://app.clupilot.test:443')
|
|
->and($config)->toContain('https://www.clupilot.test:443');
|
|
});
|
|
|
|
it('lässt einen Namen ohne Zertifikat aus, statt gar nicht zu starten', function () {
|
|
// Der ganze Grund für dieses Skript. Stünde www. mit einem `tls`-Pfad in
|
|
// der Konfiguration, den es nicht gibt, startete Caddy überhaupt nicht —
|
|
// und die Konsole wäre im Tunnel weg.
|
|
$config = renderVpnConfig([
|
|
'VPN_INTERNAL_HOST' => 'admin.clupilot.test',
|
|
'VPN_TUNNEL_HOSTS' => 'app.clupilot.test,www.clupilot.test',
|
|
], ['admin.clupilot.test', 'app.clupilot.test']);
|
|
|
|
expect($config)->toContain('https://admin.clupilot.test:443')
|
|
->and($config)->toContain('https://app.clupilot.test:443')
|
|
->and($config)->not->toContain('www.clupilot.test');
|
|
});
|
|
|
|
it('behält die Konsole, auch wenn sonst nichts ein Zertifikat hat', function () {
|
|
$config = renderVpnConfig([
|
|
'VPN_INTERNAL_HOST' => 'admin.clupilot.test',
|
|
'VPN_TUNNEL_HOSTS' => 'app.clupilot.test,www.clupilot.test',
|
|
], ['admin.clupilot.test']);
|
|
|
|
expect($config)->toContain('https://admin.clupilot.test:443')
|
|
->and($config)->not->toContain('app.clupilot.test')
|
|
->and($config)->not->toContain('www.clupilot.test');
|
|
});
|
|
|
|
it('schreibt den Gesundheits-Port als bereit, wenn die Konsole ein Zertifikat hat', function () {
|
|
// Genau das Signal, an dem VPN_READY hängt: 204 heißt, ein Client darf den
|
|
// ausgegebenen Resolver benutzen, weil die Konsole auf 443 wirklich bedient
|
|
// wird — nicht bloß, dass irgendein Caddy-Prozess läuft.
|
|
$config = renderVpnConfig([
|
|
'VPN_INTERNAL_HOST' => 'admin.clupilot.test',
|
|
'VPN_TUNNEL_HOSTS' => '',
|
|
], ['admin.clupilot.test']);
|
|
|
|
expect($config)->toContain('http://10.66.0.1:8081')
|
|
->and($config)->toContain('respond /healthz 204');
|
|
});
|
|
|
|
it('schreibt den Gesundheits-Port als NICHT bereit, wenn die Konsole kein Zertifikat hat', function () {
|
|
// Ohne Zertifikat für die Konsole bedient dieser Gateway auf 443 gar
|
|
// nichts — ein 204 hier wäre eine Falschmeldung: VPN_READY würde wahr,
|
|
// und ein Client bekäme einen Resolver genannt, der ihn auf eine Adresse
|
|
// schickt, die die Verbindung ablehnt. Andere Zertifikate (hier: app.)
|
|
// dürfen daran nichts ändern — es geht um die KONSOLE, nicht um "irgendein
|
|
// Zertifikat", das war genau die falsche Annahme im alten Test.
|
|
$config = renderVpnConfig([
|
|
'VPN_INTERNAL_HOST' => 'admin.clupilot.test',
|
|
'VPN_TUNNEL_HOSTS' => 'app.clupilot.test',
|
|
], ['app.clupilot.test']);
|
|
|
|
expect($config)->toContain('http://10.66.0.1:8081')
|
|
->and($config)->toContain('respond /healthz 503')
|
|
->and($config)->not->toContain('respond /healthz 204');
|
|
});
|
|
|
|
it('nimmt keinen Namen auf, den niemand konfiguriert hat', function () {
|
|
$config = renderVpnConfig([
|
|
'VPN_INTERNAL_HOST' => 'admin.clupilot.test',
|
|
'VPN_TUNNEL_HOSTS' => '',
|
|
], ['admin.clupilot.test', 'files.clupilot.test']);
|
|
|
|
// Beide Hälften: ohne die positive Zusage bestünde dieser Test auch dann,
|
|
// wenn das Skript überhaupt nichts renderte — und genau das ist der Fehler,
|
|
// den er fangen soll.
|
|
expect($config)->toContain('https://admin.clupilot.test:443')
|
|
->and($config)->not->toContain('files.clupilot.test');
|
|
});
|
|
|
|
it('schreibt die geladenen Zertifikate mit, damit die Erneuerung greift', function () {
|
|
// Der Update-Agent startet den Gateway neu, wenn sich eines der geladenen
|
|
// Zertifikate aendert — Caddys `tls` liest die Datei nur beim Start. Bisher
|
|
// ueberwachte er GENAU EINEN Pfad aus der .env; mit mehreren Namen liefe
|
|
// der Tunnel nach einer Erneuerung von www. mit einem abgelaufenen
|
|
// Zertifikat weiter. Die Liste ist, woran er sie erkennt.
|
|
$dir = sys_get_temp_dir().'/clupilot-vpn-'.bin2hex(random_bytes(6));
|
|
mkdir($dir.'/certs/certificates/acme', 0755, true);
|
|
|
|
foreach (['admin.clupilot.test', 'app.clupilot.test'] as $name) {
|
|
file_put_contents($dir."/certs/certificates/acme/{$name}.crt", 'cert');
|
|
file_put_contents($dir."/certs/certificates/acme/{$name}.key", 'key');
|
|
}
|
|
|
|
$list = $dir.'/certs.list';
|
|
|
|
$process = Process::fromShellCommandline(
|
|
'bash '.escapeshellarg(base_path('docker/caddy/vpn-entrypoint.sh'))
|
|
);
|
|
$process->setEnv([
|
|
'VPN_INTERNAL_HOST' => 'admin.clupilot.test',
|
|
// www. hat kein Zertifikat und darf deshalb auch nicht in der Liste stehen.
|
|
'VPN_TUNNEL_HOSTS' => 'app.clupilot.test,www.clupilot.test',
|
|
'VPN_HUB_ADDRESS' => '10.66.0.1',
|
|
'VPN_HEALTH_PORT' => '8081',
|
|
'VPN_CERT_DIR' => $dir.'/certs',
|
|
'VPN_CONFIG_OUT' => $dir.'/rendered.Caddyfile',
|
|
'VPN_CERT_LIST' => $list,
|
|
'VPN_RENDER_ONLY' => '1',
|
|
]);
|
|
$process->run();
|
|
|
|
$written = is_file($list) ? file_get_contents($list) : '';
|
|
|
|
exec('rm -rf '.escapeshellarg($dir));
|
|
|
|
expect($process->getExitCode())->toBe(0, $process->getErrorOutput())
|
|
->and($written)->toContain('admin.clupilot.test.crt')
|
|
->and($written)->toContain('app.clupilot.test.crt')
|
|
->and($written)->not->toContain('www.clupilot.test');
|
|
});
|
|
|
|
it('gibt jedem Block die Weiterleitung mit der echten Quelladresse', function () {
|
|
// Ohne X-Forwarded-For sähe die Anwendung den Gateway statt des Anrufers,
|
|
// und die Freigabeliste prüfte die falsche Adresse.
|
|
$config = renderVpnConfig([
|
|
'VPN_INTERNAL_HOST' => 'admin.clupilot.test',
|
|
'VPN_TUNNEL_HOSTS' => 'app.clupilot.test',
|
|
], ['admin.clupilot.test', 'app.clupilot.test']);
|
|
|
|
expect(substr_count($config, 'header_up X-Forwarded-For {remote_host}'))->toBe(2);
|
|
});
|