From edc710cbd2c21379a09020b35289e58195a9633c Mon Sep 17 00:00:00 2001 From: Sunil Thakare Date: Wed, 2 Sep 2026 15:19:19 +0530 Subject: [PATCH] Release v1.1.0 --- CHANGELOG.md | 23 + Cargo.lock | 14 +- Cargo.toml | 2 +- README.md | 43 +- crates/nx9-wg-api/src/diagnostics.rs | 7 +- crates/nx9-wg-api/src/reconciliation.rs | 80 +- crates/nx9-wg-api/src/routes/app_client_js.js | 637 +++++++- crates/nx9-wg-api/src/routes/app_index.html | 3 + crates/nx9-wg-api/src/routes/cli.rs | 1278 +++++++++++++++++ crates/nx9-wg-api/src/routes/interfaces.rs | 250 +++- crates/nx9-wg-api/src/routes/mod.rs | 15 + .../nx9-wg-api/tests/test_client_profiles.rs | 7 +- .../test_interface_lifecycle_hardening.rs | 501 +++++++ .../tests/test_reconciliation_drift.rs | 19 +- .../tests/test_reconciliation_subsystem.rs | 5 +- crates/nx9-wg-api/tests/test_rest_api.rs | 56 +- .../tests/test_road_warrior_dataplane.rs | 15 +- .../tests/test_upstream_lifecycle.rs | 896 ++++++++++++ .../tests/test_wiregui_capabilities.rs | 12 +- crates/nx9-wg-core/src/crypto.rs | 29 + crates/nx9-wg-core/src/types/wireguard.rs | 74 +- crates/nx9-wg-db/README.md | 9 +- .../migrations/0004_interface_roles.sql | 6 + .../migrations/0005_optional_listen_port.sql | 34 + crates/nx9-wg-db/src/interfaces.rs | 27 +- .../tests/test_network_repositories.rs | 5 +- .../tests/test_wireguard_repositories.rs | 50 +- crates/nx9-wireguard/src/config_builder.rs | 14 +- crates/nx9-wireguard/src/engine.rs | 4 +- crates/nx9-wireguard/src/lib.rs | 2 + crates/nx9-wireguard/src/native_linux.rs | 9 +- crates/nx9-wireguard/src/upstream_parser.rs | 464 ++++++ .../tests/test_upstream_parser.rs | 274 ++++ .../tests/test_wireguard_engine.rs | 10 +- docs/API.md | 14 +- docs/ARCHITECTURE.md | 55 +- docs/CLI.md | 15 +- docs/INSTALLATION.md | 4 +- docs/NATIVE-WIREGUARD.md | 17 +- docs/RECONCILIATION.md | 14 +- docs/RELEASE.md | 10 +- docs/SECURITY.md | 5 +- docs/TESTING.md | 12 +- docs/UI.md | 16 +- src/main.rs | 311 +++- tests/test_cli_commands.rs | 167 ++- 46 files changed, 5324 insertions(+), 190 deletions(-) create mode 100644 crates/nx9-wg-api/src/routes/cli.rs create mode 100644 crates/nx9-wg-api/tests/test_interface_lifecycle_hardening.rs create mode 100644 crates/nx9-wg-api/tests/test_upstream_lifecycle.rs create mode 100644 crates/nx9-wg-db/migrations/0004_interface_roles.sql create mode 100644 crates/nx9-wg-db/migrations/0005_optional_listen_port.sql create mode 100644 crates/nx9-wireguard/src/upstream_parser.rs create mode 100644 crates/nx9-wireguard/tests/test_upstream_parser.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 4613d0b..aa19bf7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,29 @@ All notable changes to **NX9-WG (`nx9-wg`)** are documented here. +## [1.1.0] — 2026-09-02 + +### Added +- **Interface Roles**: Explicit `InterfaceRole` discriminator (`Overlay` vs `Upstream`). Primary interface `wg0` is protected from deletion and disabling. +- **Optional Third-Party Upstream Interfaces**: In-process parser and validator for standard third-party WireGuard `.conf` files (validated against ProtonVPN), creating managed `Upstream` interfaces (e.g. `proton0`) with exactly one provider peer. +- **REST API Endpoints**: Added `POST /api/v1/interfaces/upstreams/preview` (dry-run configuration validation with secret redaction), `POST /api/v1/interfaces/upstreams/import` (atomic SQLite persistence and reconciliation), and `POST /api/v1/interfaces/{id}/restart` (link teardown and re-synchronization). +- **Native CLI Commands**: Added `nx9-wg interface upstream` command suite (`list`, `show`, `import`, `status`, `enable`, `disable`, `restart`, `delete`) and `nx9-wg interface restart`. +- **Read-Only SPA CLI Console**: Embedded web-based CLI runner enforcing a strict read-only command allowlist and output secret scrubbing. +- **Reconciliation Hardening**: Added orphan kernel interface detection and removal during `apply()`, backed by empty-desired-state safety guards preventing destructive cleanup on database read failures. +- **Provider AllowedIPs Preservation**: Upstream provider peers retain full-tunnel AllowedIPs (`0.0.0.0/0, ::/0`) in WireGuard Cryptokey Routing without modifying or hijacking host Linux FIB default routes. + +### Changed +- **Optional Local Listen Ports**: Changed `Interface.listen_port` to `Option` across domain models, Netlink device configuration, REST API, and SQLite database (`0005_optional_listen_port.sql`). +- **Dynamic Port Web UI**: Unspecified listen ports are rendered as `Auto (Dynamic)` rather than a fabricated `51820`. + +### Fixed +- **Local Listen Port Collision (errno=-98 / EADDRINUSE)**: Fixed upstream interfaces defaulting omitted `ListenPort` to `51820`, which collided with `wg0`. Omitted listen ports now remain `None`, allowing Linux WireGuard to bind an ephemeral dynamic UDP port. +- **Reconciliation Dynamic Port Drift**: Suppressed false listen-port drift when desired `listen_port` is `None` and the kernel reports a dynamic port. +- **Provider Endpoint Port Independence**: Ensured remote destination `[Peer] Endpoint` port (e.g. `37.19.199.155:51820`) is strictly preserved and never assigned as the local interface listen port. + +### Interoperability Status +- **ProtonVPN**: ProtonVPN WireGuard `.conf` files import and synchronize cleanly into Linux kernel devices (`proton0`) with dynamic local listen ports. Upstream connectivity status is classified as `interop_pending_external_validation` (pending external provider session/endpoint resolution, not an NX9-WG implementation defect). + ## [1.0.0] — 2026-08-18 NX9-WG 1.0.0 is the first production release of the native Linux WireGuard + network control plane. diff --git a/Cargo.lock b/Cargo.lock index 907ac3c..aa5570d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1785,7 +1785,7 @@ dependencies = [ [[package]] name = "nx9-wg" -version = "1.0.0" +version = "1.1.0" dependencies = [ "axum", "base64", @@ -1807,7 +1807,7 @@ dependencies = [ [[package]] name = "nx9-wg-api" -version = "1.0.0" +version = "1.1.0" dependencies = [ "axum", "chrono", @@ -1832,7 +1832,7 @@ dependencies = [ [[package]] name = "nx9-wg-core" -version = "1.0.0" +version = "1.1.0" dependencies = [ "argon2", "base64", @@ -1852,7 +1852,7 @@ dependencies = [ [[package]] name = "nx9-wg-db" -version = "1.0.0" +version = "1.1.0" dependencies = [ "chrono", "ipnet", @@ -1869,7 +1869,7 @@ dependencies = [ [[package]] name = "nx9-wg-network" -version = "1.0.0" +version = "1.1.0" dependencies = [ "async-trait", "chrono", @@ -1890,7 +1890,7 @@ dependencies = [ [[package]] name = "nx9-wg-ui" -version = "1.0.0" +version = "1.1.0" dependencies = [ "chrono", "nx9-wg-core", @@ -1901,7 +1901,7 @@ dependencies = [ [[package]] name = "nx9-wireguard" -version = "1.0.0" +version = "1.1.0" dependencies = [ "async-trait", "base64", diff --git a/Cargo.toml b/Cargo.toml index 00e82d7..6eefaff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ members = [ [workspace.package] license = "MIT OR Apache-2.0" -version = "1.0.0" +version = "1.1.0" edition = "2024" authors = ["NX9 Authors "] repository = "https://github.com/thakares/nx9-wg" diff --git a/README.md b/README.md index 7981964..015fb24 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ![SQLite](https://img.shields.io/badge/SQLite-Embedded-blue) ![Platform](https://img.shields.io/badge/Platform-Linux-lightgrey) ![License](https://img.shields.io/badge/License-MIT%20OR%20Apache--2.0-green) -![Version](https://img.shields.io/badge/Version-v1.0.0-purple) +![Version](https://img.shields.io/badge/Version-v1.1.0-purple) > **Sovereign, self-hosted, Linux-native VPN and network control plane built directly around the kernel's WireGuard implementation.** @@ -57,21 +57,22 @@ Rather than functioning as a user interface wrapper that shells out to external --- -## 2. Key Capabilities - -- **WireGuard Interface & Peer Lifecycle**: Direct RTNETLINK link management (`RTM_NEWLINK`/`RTM_DELLINK`) and WireGuard Generic Netlink (`WG_CMD_SET_DEVICE`/`WG_CMD_GET_DEVICE`) with cryptokey routing. +- **Explicit Interface Roles (Overlay vs Upstream)**: Formal separation of the primary protected overlay interface (`wg0`) from optional third-party WireGuard VPN upstream interfaces (e.g. `proton0`). +- **Third-Party WireGuard .conf Import**: In-process parser and validator for standard `.conf` files (supporting single `[Interface]` and single `[Peer]`), with live configuration preview before atomic database persistence. +- **Optional Local Listen Ports**: Strict modeling of `Interface.listen_port` as `Option`, allowing Linux WireGuard to select ephemeral dynamic UDP ports when `ListenPort` is omitted from imported configurations, preventing local port collisions with `wg0` (51820). +- **WireGuard Interface & Peer Lifecycle**: Direct RTNETLINK link management (`RTM_NEWLINK`/`RTM_DELLINK`) and WireGuard Generic Netlink (`WG_CMD_SET_DEVICE`/`WG_CMD_GET_DEVICE`) with role-aware cryptokey routing. - **Persistent Server Endpoint Settings**: Authoritative configuration of public client-reachable endpoint (`wireguard.server_host`, `wireguard.server_port`, `wireguard.server_endpoint_enabled`) automatically embedded into client exports and QR codes. -- **Strict AllowedIPs Semantic Separation**: Correctly derives server-side cryptokey routing AllowedIPs (`/32` and `/128`) from assigned tunnel addresses, distinct from client full-tunnel (`0.0.0.0/0, ::/0`) routing policies. +- **Strict AllowedIPs Semantic Separation**: Correctly derives server-side cryptokey routing AllowedIPs (`/32` and `/128`) from assigned tunnel addresses for Overlay peers, while preserving full-tunnel provider AllowedIPs (`0.0.0.0/0, ::/0`) for Upstream peers without mutating the host default routing table. - **IPv4/IPv6 Address Management**: In-process `RTM_NEWADDR` and `RTM_DELADDR` Netlink execution without invoking `ip addr`. - **Protected Route Management**: In-process routing table reconciliation protecting host default routes from accidental disruption. - **In-Process nftables Firewall & NAT**: Transactional rule compilation via `libnftables.so.1` strictly scoped to `table inet nx9_wg`. - **Scoped Outbound NAT Masquerade**: Automated masquerading scoped to managed WireGuard client subnets and non-WireGuard egress interfaces. - **Atomic IP Packet Forwarding**: Direct `/proc/sys/net/ipv4/ip_forward` and IPv6 forwarding control. - **Live Kernel Telemetry**: Live handshake timestamps, authenticated roaming endpoints, and 64-bit RX/TX byte counters merged into API and WebUI responses. -- **Closed-Loop Reconciliation**: Continuous drift detection, dry-run deterministic planning, and serialized convergence. +- **Closed-Loop Reconciliation**: Continuous drift detection, dry-run deterministic planning, orphan interface removal, and serialized convergence with empty-desired-state safety guards. - **Cold-Boot Restart Recovery**: Deterministic reconstruction of live kernel networking from authoritative SQLite state upon boot. - **Single Administrator Identity**: Database-level `CHECK (id = 1)` constraint, Argon2id password hashing, and SHA-256 API token digests. -- **Zero-Dependency Single Page Application (SPA)**: Embedded HTML5/CSS/JS frontend with dark/light themes, live WebSocket telemetry, and responsive mobile-first UI. +- **Zero-Dependency Single Page Application (SPA)**: Embedded HTML5/CSS/JS frontend with dark/light themes, live WebSocket telemetry, responsive mobile-first UI, Upstream import modal with live preview, and read-only CLI console. - **Pure Rust Client Configuration & QR**: In-process generation of standard `.conf` text and SVG, PNG, and terminal ASCII QR codes. - **Automated Health Diagnostics**: Deep inspection across 11 subsystems with actionable remediation hints. - **Atomic SQLite Online Backups**: Non-blocking `VACUUM INTO` snapshots with SHA-256 integrity manifests and pre-restore safety snapshots. @@ -139,8 +140,8 @@ When generating client configuration files (`.conf`) and QR codes, `nx9-wg` auto ```bash # Extract release archive: -tar -xzf nx9-wg-v1.0.0-linux-x86_64.tar.gz -cd nx9-wg-v1.0.0-linux-x86_64 +tar -xzf nx9-wg-v1.1.0-linux-x86_64.tar.gz +cd nx9-wg-v1.1.0-linux-x86_64 # Run production installer as root: sudo bash install.sh @@ -213,7 +214,7 @@ max_count = 5 Access the Web UI at `http://:8080/`. The interface is a zero-dependency SPA embedded inside the binary: - **Dashboard (`#dashboard`)**: System status, uptime, interface/peer counts, diagnostics health summary, and live reconciliation status. -- **Interfaces (`#interfaces`)**: Interface list, "+ Create Interface" modal, interface **Edit** action (preserves private/public key identity), enable/disable toggle, and delete action. +- **Interfaces (`#interfaces`)**: Interface list with explicit **Role** badges (`Overlay` vs `Upstream`), "+ Create Interface" modal with tabbed **Standard Overlay** vs **Import Upstream VPN** (`.conf` parser & live preview), interface **Edit** action (preserves private/public key identity), **Restart** action (link teardown + re-sync), enable/disable toggle, and delete action (protected against `wg0`), plus an embedded read-only CLI console. - **Peers (`#peers`)**: Enrolled peer table with real-time handshakes, status filters, "+ Add Peer" modal with MTU profile resolution, client configuration export modal, and live SVG QR rendering. - **Networks (`#networks`)**: Subnet network definitions, CIDR blocks, available unallocated IP inspection, and "+ Create Network" modal. - **Routes (`#routes`)**: Kernel routing table entries, gateway assignments, and "+ Create Route" modal. @@ -240,11 +241,19 @@ nx9-wg system settings set wireguard.server_host vpn.thakares.com nx9-wg system settings set wireguard.server_port 51820 nx9-wg system settings set wireguard.server_endpoint_enabled true -# 2. Interface Creation & Editing +# 2. Interface Creation, Editing & Restart nx9-wg interface create wg0 --address-v4 10.100.0.1/24 --port 51820 --mtu 1420 nx9-wg interface update wg0 --mtu 1420 +nx9-wg interface restart wg0 -# 3. Peer Enrollment & Client Config Export +# 3. Third-Party Upstream Management (e.g. ProtonVPN) +nx9-wg interface upstream import proton0 --file /path/to/protonvpn.conf +nx9-wg interface upstream list +nx9-wg interface upstream show proton0 +nx9-wg interface upstream status proton0 +nx9-wg interface upstream restart proton0 + +# 4. Peer Enrollment & Client Config Export nx9-wg peer create --interface wg0 --name alice-phone --profile full_tunnel --mtu 1280 # Export client configuration (uses persistent server endpoint): @@ -259,16 +268,16 @@ nx9-wg peer qr # Render QR code as SVG: nx9-wg peer qr --qr-format svg -# 4. Reconciliation +# 5. Reconciliation nx9-wg reconcile plan nx9-wg reconcile apply nx9-wg reconcile verify -# 5. Live Telemetry & Diagnostics +# 6. Live Telemetry & Diagnostics nx9-wg live peer wg0 nx9-wg diagnostics all -# 6. Database Backups +# 7. Database Backups nx9-wg backup create --description "Pre-maintenance snapshot" nx9-wg backup list nx9-wg backup verify /var/lib/nx9-wg/backups/snapshot.db @@ -326,8 +335,8 @@ All release quality gates have been executed and verified on Debian Linux: | **Formatting** | `cargo fmt --all -- --check` | **PASS** (0 errors) | | **Compilation** | `cargo check --workspace --all-targets` | **PASS** (0 errors) | | **Clippy Linting** | `cargo clippy --workspace --all-targets --all-features -- -D warnings` | **PASS** (0 warnings) | -| **Workspace Test Suite** | `cargo test --workspace --all-targets` | **PASS** (All 162 tests passing) | -| **CLI Test Suite** | `cargo test --test test_cli_commands` | **PASS** (11 tests passing) | +| **Workspace Test Suite** | `cargo test --workspace --all-targets` | **PASS** (All 195 tests passing) | +| **CLI Test Suite** | `cargo test --test test_cli_commands` | **PASS** (12 tests passing) | | **Release Compilation** | `cargo build --release --workspace` | **PASS** (Optimized release binary) | | **Production Server Acceptance** | Physical Android WireGuard client connection | **VERIFIED** (Live handshake and RX/TX telemetry confirmed) | diff --git a/crates/nx9-wg-api/src/diagnostics.rs b/crates/nx9-wg-api/src/diagnostics.rs index 4a9b12d..b8fb196 100644 --- a/crates/nx9-wg-api/src/diagnostics.rs +++ b/crates/nx9-wg-api/src/diagnostics.rs @@ -325,7 +325,12 @@ impl DiagnosticsService { stats.listen_port, stats.peers.len() ), - expected_value: Some(format!("port {}", iface.listen_port)), + expected_value: Some( + iface + .listen_port + .map(|p| format!("port {p}")) + .unwrap_or_else(|| "port auto".to_string()), + ), diagnostic_message: format!( "Interface '{}' is running and responsive", iface.name diff --git a/crates/nx9-wg-api/src/reconciliation.rs b/crates/nx9-wg-api/src/reconciliation.rs index d87ec68..b30dae8 100644 --- a/crates/nx9-wg-api/src/reconciliation.rs +++ b/crates/nx9-wg-api/src/reconciliation.rs @@ -5,7 +5,7 @@ use crate::state::{AppState, SystemEvent}; use chrono::Utc; use ipnet::IpNet; use nx9_wg_core::types::audit::AuditEventType; -use nx9_wg_core::types::wireguard::PeerState; +use nx9_wg_core::types::wireguard::{InterfaceRole, PeerState}; use nx9_wg_db::Store; use nx9_wg_network::NetworkEngine; use nx9_wireguard::WireGuardEngine; @@ -28,16 +28,12 @@ fn matches_ipnet(live_addrs: &[String], desired: &IpNet) -> bool { fn matches_allowed_ips(live_allowed_ips: &[String], desired_str: &str) -> bool { let desired_nets: std::collections::BTreeSet = desired_str .split(',') - .map(|s| s.trim()) - .filter(|s| !s.is_empty()) - .filter_map(|s| s.parse::().ok()) + .filter_map(|s| s.trim().parse::().ok()) .collect(); let live_nets: std::collections::BTreeSet = live_allowed_ips .iter() - .map(|s| s.trim()) - .filter(|s| !s.is_empty()) - .filter_map(|s| s.parse::().ok()) + .filter_map(|s| s.trim().parse::().ok()) .collect(); desired_nets == live_nets @@ -45,15 +41,13 @@ fn matches_allowed_ips(live_allowed_ips: &[String], desired_str: &str) -> bool { /// Collect Interface CIDRs plus enabled Subnet Network CIDRs for NAT/forwarding. /// -/// Interface addresses remain the WireGuard transport identity. Enabled Network -/// CIDRs are the peer allocation domains and must be masqueraded so selected- -/// Network peers receive the same full-tunnel Internet path as Interface-CIDR -/// peers. `network_id = null` peers still match the Interface CIDR. +/// Only `InterfaceRole::Overlay` interfaces and peer-allocation Networks are collected +/// for client WAN NAT. Upstream interface addresses are not included. pub async fn collect_managed_wg_subnets(store: &Store) -> ApiResult> { let mut subnets = Vec::new(); for iface in store.list_interfaces().await? { - if !iface.enabled { + if !iface.enabled || iface.role != InterfaceRole::Overlay { continue; } subnets.push(iface.address_v4); @@ -205,8 +199,13 @@ impl ReconciliationEngine { { drift_reasons.push("public key mismatch".to_string()); } - if stats.listen_port != 0 && stats.listen_port != iface.listen_port { - drift_reasons.push("listen port mismatch".to_string()); + if let Some(desired_port) = iface.listen_port { + if desired_port != 0 + && stats.listen_port != 0 + && stats.listen_port != desired_port + { + drift_reasons.push("listen port mismatch".to_string()); + } } if !matches_ipnet(&stats.addresses, &iface.address_v4) { drift_reasons @@ -278,7 +277,8 @@ impl ReconciliationEngine { for p in &active_desired_peers { let pub_key_str = p.public_key.as_str(); - let desired_server_allowed = p.server_wireguard_allowed_ips(); + let desired_server_allowed = + p.server_wireguard_allowed_ips_for_role(iface.role); if let Some(live_p) = live_peers_map.get(pub_key_str) { // Peer is present in live kernel interface. Verify semantic drift: @@ -384,6 +384,24 @@ impl ReconciliationEngine { } } + // Detect orphan kernel interfaces not in desired state + let desired_names: std::collections::HashSet<_> = + desired_interfaces.iter().map(|i| i.name.as_str()).collect(); + for live_name in &live_interfaces { + if !desired_names.contains(live_name.as_str()) { + plan.actions.push(ReconciliationAction { + subsystem: "wireguard".to_string(), + resource_id: live_name.clone(), + action_type: "delete_orphan_interface".to_string(), + description: format!( + "Orphan WireGuard interface '{}' exists in kernel but not in desired state; remove", + live_name + ), + }); + plan.interface_changes += 1; + } + } + // 2. Routes (SQLite Routes table only; peer-allocation Networks are not routes) let desired_routes = self.state.store.list_routes().await?; let enabled_routes: Vec<_> = desired_routes.iter().filter(|r| r.enabled).collect(); @@ -502,6 +520,18 @@ impl ReconciliationEngine { } let desired_interfaces = self.state.store.list_interfaces().await?; + // Safety: refuse to orphan-cleanup if desired state appears empty + // while live kernel interfaces exist. + if desired_interfaces.is_empty() { + let live_check = self.wg_engine.list_interfaces().await.unwrap_or_default(); + if !live_check.is_empty() { + return Err(ApiError::Internal( + "Reconciliation aborted: desired state is empty but live kernel interfaces \ + exist. This may indicate a database read failure." + .to_string(), + )); + } + } let mut details = Vec::new(); // 1. Sync all active WireGuard interfaces and their peers @@ -531,6 +561,26 @@ impl ReconciliationEngine { } } + // Remove orphan kernel WireGuard interfaces absent from desired state + let desired_names: std::collections::HashSet<_> = + desired_interfaces.iter().map(|i| i.name.as_str()).collect(); + let live_interfaces = self.wg_engine.list_interfaces().await.unwrap_or_default(); + for live_name in &live_interfaces { + if !desired_names.contains(live_name.as_str()) { + match self.wg_engine.delete_interface(live_name).await { + Ok(()) => { + details.push(format!("Removed orphan kernel interface '{}'", live_name)); + } + Err(e) => { + details.push(format!( + "Failed to remove orphan kernel interface '{}': {e}", + live_name + )); + } + } + } + } + let wg_subnets = collect_managed_wg_subnets(&self.state.store).await?; // 2. Sync Routes (SQLite Routes table only; peer-allocation Networks are not routes) diff --git a/crates/nx9-wg-api/src/routes/app_client_js.js b/crates/nx9-wg-api/src/routes/app_client_js.js index 75daffc..6e7124d 100644 --- a/crates/nx9-wg-api/src/routes/app_client_js.js +++ b/crates/nx9-wg-api/src/routes/app_client_js.js @@ -323,6 +323,9 @@ case 'live-state': await renderLiveStatePage(container); break; + case 'cli-console': + await renderCliConsolePage(container); + break; case 'settings': await renderSettingsPage(container); break; @@ -942,7 +945,7 @@