'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 unabhängig von jedem Zertifikat', function () { // Daran hängt VPN_READY, und damit ob Client-Konfigurationen den Resolver // überhaupt nennen. Ein Gesundheits-Port, der ein Zertifikat braucht, wäre // genau die Attrappe, die schon einmal VPN_READY auf einer gesunden Anlage // false stehen ließ. $config = renderVpnConfig([ 'VPN_INTERNAL_HOST' => 'admin.clupilot.test', 'VPN_TUNNEL_HOSTS' => '', ], []); expect($config)->toContain('http://10.66.0.1:8081') ->and($config)->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); });