diff --git a/app/Livewire/Admin/Vpn.php b/app/Livewire/Admin/Vpn.php index 802e5e3..ee37b99 100644 --- a/app/Livewire/Admin/Vpn.php +++ b/app/Livewire/Admin/Vpn.php @@ -318,6 +318,32 @@ class Vpn extends Component } } + /** + * What the client sends through the tunnel: the management subnet, and only + * that. + * + * It is tempting to add the server's own public address here, because the + * console lives at a public hostname and without that route the phone sends + * the request out over the mobile network instead — it arrives from a + * carrier address, the proxy refuses it, and the operator sees a blank page + * while the VPN app says "connected". + * + * That fix cannot work. The WireGuard endpoint is the SAME address. Routing + * it into the tunnel routes the handshake packets into the tunnel too, and + * a tunnel cannot carry the packets that establish it — the result is a + * loop and a connection that never comes up at all. Strictly worse: the + * same symptom, now with no way in. + * + * The console has to be reachable at an address that is INSIDE the subnet + * for the VPN to be the way in. That is a deployment change (the proxy + * answering on the hub address, and a name that resolves to it), not + * something this config line can express. + */ + private static function allowedIps(string $endpoint): string + { + return (string) config('provisioning.wireguard.subnet', '10.66.0.0/24'); + } + private function clientConfig(Keypair $keypair, string $ip): string { $hub = app(WireguardHub::class); @@ -330,7 +356,7 @@ class Vpn extends Component '[Peer]', 'PublicKey = '.$hub->publicKey(), 'Endpoint = '.$hub->endpoint(), - 'AllowedIPs = '.config('provisioning.wireguard.subnet', '10.66.0.0/24'), + 'AllowedIPs = '.self::allowedIps($hub->endpoint()), 'PersistentKeepalive = 25', '', ]); diff --git a/lang/de/coming_soon.php b/lang/de/coming_soon.php index de64057..7bc21c1 100644 --- a/lang/de/coming_soon.php +++ b/lang/de/coming_soon.php @@ -3,5 +3,7 @@ return [ 'title' => 'Wir bauen gerade.', 'body' => 'CluPilot ist noch nicht öffentlich. Managed Nextcloud aus deutschen und finnischen Rechenzentren — betreut, gesichert, aktuell gehalten.', - 'contact' => 'Fragen? info@clupilot.com', + 'contact' => 'Fragen beantworten wir gern:', + 'label' => 'Betriebszustand', + 'state' => 'In Vorbereitung', ]; diff --git a/lang/en/coming_soon.php b/lang/en/coming_soon.php index 05a1b43..ca10487 100644 --- a/lang/en/coming_soon.php +++ b/lang/en/coming_soon.php @@ -3,5 +3,7 @@ return [ 'title' => 'We are still building.', 'body' => 'CluPilot is not public yet. Managed Nextcloud from German and Finnish datacentres — operated, backed up, kept current.', - 'contact' => 'Questions? info@clupilot.com', + 'contact' => 'Questions are welcome:', + 'label' => 'Service status', + 'state' => 'Being prepared', ]; diff --git a/resources/views/coming-soon.blade.php b/resources/views/coming-soon.blade.php index 9ac0f6a..5bc5924 100644 --- a/resources/views/coming-soon.blade.php +++ b/resources/views/coming-soon.blade.php @@ -1,42 +1,91 @@ - - - {{-- Belt and braces with the X-Robots-Tag header: some crawlers read only one. --}} - - {{ config('app.name') }} - - {{-- Self-contained on purpose: this page is shown while the application is - mid-deploy or freshly installed, which is exactly when the Vite - manifest may not exist. Pulling it in through @vite would throw an - exception instead of rendering the reassurance the visitor came for. --}} - + + +{{-- Belt and braces with the X-Robots-Tag header: some crawlers read only one. --}} + +CluPilot Cloud + + +{{-- + Self-contained on purpose: this is shown while the application is mid-deploy + or freshly installed, which is exactly when the Vite manifest may not exist. + Pulling it in through @vite would throw an exception instead of rendering + the reassurance the visitor came for. + + The fonts are the site's own static files. If they are missing the page + degrades to a system serif and still reads correctly. +--}} +@verbatim + +@endverbatim -
-
-

{{ __('coming_soon.title') }}

-

{{ __('coming_soon.body') }}

-

{{ __('coming_soon.contact') }}

-
+
+
+
+ {{ __('coming_soon.label') }} + {{ __('coming_soon.state') }} +
+
+ CluPilot Cloud +

{{ __('coming_soon.title') }}

+

{{ __('coming_soon.body') }}

+

{{ __('coming_soon.contact') }} + office@clupilot.com +

+
+
+
diff --git a/resources/views/errors/layout.blade.php b/resources/views/errors/layout.blade.php index ca24079..3b9426a 100644 --- a/resources/views/errors/layout.blade.php +++ b/resources/views/errors/layout.blade.php @@ -29,8 +29,12 @@ --accent:#f97316; --accent-text:#b8500a; } @media (prefers-color-scheme: dark){ + /* The accent has to lighten too: #b8500a is chosen for contrast against + paper, and on the ink plate it is the small text that stops being + readable first. */ :root{--paper:#17140f; --card:#211d16; --ink:#fffefb; --ink-soft:#ded7c9; - --muted:#93897a; --rule:#39332a; --rule-mid:#4a4238;} + --muted:#93897a; --rule:#39332a; --rule-mid:#4a4238; + --accent-text:#f0a06a;} } *{margin:0;padding:0;box-sizing:border-box} body{ diff --git a/tests/Feature/Admin/VpnTest.php b/tests/Feature/Admin/VpnTest.php index 42b5a31..2807661 100644 --- a/tests/Feature/Admin/VpnTest.php +++ b/tests/Feature/Admin/VpnTest.php @@ -879,3 +879,19 @@ it('re-issues even when the config vault is unavailable', function () { expect($peer->fresh()->config_secret)->toBeNull(); // shown once instead }); + +it('does not route the WireGuard endpoint through its own tunnel', function () { + // The obvious fix for "the VPN does not reach the console" is to add the + // server's public address to AllowedIPs. It cannot work: the endpoint is + // that same address, so the handshake packets would be routed into the + // tunnel they are trying to establish. Same blank page, now with no way in + // at all. + config()->set('provisioning.wireguard.subnet', '10.66.0.0/24'); + + $method = new ReflectionMethod(App\Livewire\Admin\Vpn::class, 'allowedIps'); + $method->setAccessible(true); + + foreach (['203.0.113.10:51820', '[2001:db8::10]:51820', 'vpn.example.test:51820'] as $endpoint) { + expect($method->invoke(null, $endpoint))->toBe('10.66.0.0/24', $endpoint); + } +});