diff --git a/docs-es/v2.0/bridge-es.md b/docs-es/v2.0/bridge-es.md index 20a00eed..59364ef2 100644 --- a/docs-es/v2.0/bridge-es.md +++ b/docs-es/v2.0/bridge-es.md @@ -15,6 +15,10 @@ A continuación, se encuentran las instrucciones para configurar Netplan, ya sea La página Network Mode en la interfaz web de LibreQoS ahora inspecciona los archivos actuales de Netplan, ofrece en menús desplegables las interfaces elegibles que no forman parte de la ruta de gestión, prepara cambios administrados para `libreqos.yaml` en los modos puente de Linux y de interfaz única, los aplica con una ventana temporizada de reversión manejada por LibreQoS y le permite confirmar o revertir el cambio pendiente. También puede restaurar desde esa página la copia de seguridad administrada anterior. El modo puente XDP sigue siendo un flujo manual de Netplan. ``` +```{note} +La configuración inicial actual solo ofrece Puente Linux e Interfaz Única para instalaciones nuevas. Si LibreQoS detecta un despliegue XDP existente, la configuración preserva ese modo XDP heredado y le avisa antes de migrar fuera de él. +``` + ```{note} Si un cambio temporizado de Netplan interrumpe brevemente la sesión del navegador, vuelva a la página Network Mode cuando regrese la conectividad. LibreQoS retomará automáticamente desde esa página el flujo pendiente de confirmar o revertir. ``` diff --git a/docs/v2.0/bridge.md b/docs/v2.0/bridge.md index 1127a711..7f232bff 100644 --- a/docs/v2.0/bridge.md +++ b/docs/v2.0/bridge.md @@ -15,6 +15,10 @@ Below are the instructions to configure Netplan, whether using the Linux Bridge The Network Mode page in the LibreQoS web UI now inspects the current Netplan files, offers eligible non-management interfaces in dropdowns, stages managed `libreqos.yaml` changes for Linux bridge and single-interface modes, applies them with a timed LibreQoS rollback window, and lets you confirm or revert the pending change. You can also restore the previous managed backup from that page. XDP bridge mode remains a manual Netplan workflow. ``` +```{note} +Current first-run setup only offers Linux Bridge and Single Interface for new installs. If LibreQoS detects an existing XDP deployment, setup preserves that legacy XDP mode and warns before you migrate away from it. +``` + ```{note} If a timed Netplan change briefly interrupts your browser session, return to the Network Mode page after connectivity comes back. LibreQoS will resume the pending confirm or revert flow from that page automatically. ``` diff --git a/src/build_dpkg.sh b/src/build_dpkg.sh index 7c3fd0e7..66388a1d 100755 --- a/src/build_dpkg.sh +++ b/src/build_dpkg.sh @@ -140,16 +140,6 @@ else fi } -should_activate_runtime_on_upgrade() { -local previous_version="${2:-}" - -# Debian passes the previous package version as the second postinst argument on -# upgrade. If an existing LibreQoS config is already present, keep runtime -# services in charge of the main WebUI and let lqosd handle any follow-up -# onboarding instead of launching the dedicated setup web service on port 9123. -[ -n "$previous_version" ] && [ -f /etc/lqos.conf ] -} - # Install Python Dependencies pushd /opt/libreqos > /dev/null # - Setup Python dependencies as a post-install task. Use --ignore-installed so @@ -175,14 +165,32 @@ install -m 0644 /opt/libreqos/src/bin/lqos_setup.service.example /etc/systemd/sy /bin/systemctl disable lqos_node_manager || true # In case it's running from a previous release /bin/systemctl stop lqos_netplan_helper || true /bin/systemctl disable lqos_netplan_helper || true -if should_activate_runtime_on_upgrade "$@"; then -/opt/libreqos/src/bin/lqos_setup activate-runtime -elif /opt/libreqos/src/bin/lqos_setup is-ready; then -/opt/libreqos/src/bin/lqos_setup activate-runtime -else -/opt/libreqos/src/bin/lqos_setup activate-setup -/opt/libreqos/src/bin/lqos_setup print-link || true -fi +case "$(/opt/libreqos/src/bin/lqos_setup postinst-action)" in +activate_runtime) + /opt/libreqos/src/bin/lqos_setup activate-runtime + ;; +activate_setup) + /opt/libreqos/src/bin/lqos_setup activate-setup + /opt/libreqos/src/bin/lqos_setup print-link || true + ;; +block_for_hotfix) + /opt/libreqos/src/bin/lqos_setup hotfix-status || true + cat <<'HOTFIX' + +Install the Noble systemd hotfix before package configuration can start LibreQoS services. + +Run: + sudo /opt/libreqos/src/systemd_hotfix.sh install + +Then re-run the LibreQoS package installation. +HOTFIX + exit 1 + ;; +*) + echo "Unknown LibreQoS post-install action." >&2 + exit 1 + ;; +esac popd > /dev/null EOF diff --git a/src/rust/lqos_setup/src/bootstrap.rs b/src/rust/lqos_setup/src/bootstrap.rs index 0978222f..6e76f339 100644 --- a/src/rust/lqos_setup/src/bootstrap.rs +++ b/src/rust/lqos_setup/src/bootstrap.rs @@ -92,6 +92,28 @@ pub struct StatusSnapshot { pub hotfix_detail: String, } +/// Service mode the package postinst should activate after installation. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum PostinstAction { + /// Stop before starting services because the Noble systemd hotfix is still required. + BlockForHotfix, + /// Start the runtime services and keep setup inside `lqosd`. + ActivateRuntime, + /// Start the dedicated setup service for a true first-run install. + ActivateSetup, +} + +impl PostinstAction { + /// Stable token printed for package postinst shell logic. + pub fn as_str(self) -> &'static str { + match self { + Self::BlockForHotfix => "block_for_hotfix", + Self::ActivateRuntime => "activate_runtime", + Self::ActivateSetup => "activate_setup", + } + } +} + #[derive(Default, Deserialize)] struct AuthUsersFile { #[serde(default)] @@ -189,6 +211,24 @@ fn display_status(setup_complete: bool) -> SetupStatus { } } +fn legacy_runtime_install_detected() -> bool { + Path::new("/etc/lqos.conf").exists() +} + +fn classify_postinst_action_from_inputs( + hotfix_required: bool, + runtime_ready: bool, + legacy_runtime_install: bool, +) -> PostinstAction { + if hotfix_required { + PostinstAction::BlockForHotfix + } else if runtime_ready || legacy_runtime_install { + PostinstAction::ActivateRuntime + } else { + PostinstAction::ActivateSetup + } +} + fn detect_host_hints() -> Vec { let mut hints = BTreeSet::new(); @@ -443,6 +483,18 @@ pub fn runtime_services_should_start() -> Result { && !hotfix_status.required) } +/// Classifies which service mode package postinst should activate. +pub fn classify_postinst_action() -> Result { + let hotfix_required = hotfix::status()?.required; + let runtime_ready = runtime_services_should_start()?; + let legacy_runtime_install = legacy_runtime_install_detected(); + Ok(classify_postinst_action_from_inputs( + hotfix_required, + runtime_ready, + legacy_runtime_install, + )) +} + /// Returns true when first-run setup has not yet been completed. pub fn setup_is_incomplete() -> Result { let live = collect_live_inputs(); @@ -487,7 +539,10 @@ fn yes_no(value: bool) -> &'static str { #[cfg(test)] mod tests { - use super::{TOKEN_TTL, current_unix_seconds, new_bootstrap_token}; + use super::{ + PostinstAction, TOKEN_TTL, classify_postinst_action_from_inputs, current_unix_seconds, + new_bootstrap_token, + }; #[test] fn bootstrap_tokens_expire_in_future() { @@ -496,4 +551,28 @@ mod tests { assert!(!token.token.is_empty()); assert!(token.expires_at_unix >= now + TOKEN_TTL.as_secs() - 1); } + + #[test] + fn postinst_action_blocks_for_hotfix_before_services_restart() { + assert_eq!( + classify_postinst_action_from_inputs(true, true, true), + PostinstAction::BlockForHotfix + ); + } + + #[test] + fn postinst_action_keeps_existing_runtime_installs_on_runtime_services() { + assert_eq!( + classify_postinst_action_from_inputs(false, false, true), + PostinstAction::ActivateRuntime + ); + } + + #[test] + fn postinst_action_only_activates_setup_for_true_first_run() { + assert_eq!( + classify_postinst_action_from_inputs(false, false, false), + PostinstAction::ActivateSetup + ); + } } diff --git a/src/rust/lqos_setup/src/bridge_mode.rs b/src/rust/lqos_setup/src/bridge_mode.rs index e09d0998..d65bf8d9 100644 --- a/src/rust/lqos_setup/src/bridge_mode.rs +++ b/src/rust/lqos_setup/src/bridge_mode.rs @@ -4,10 +4,11 @@ use cursive::{ views::{Dialog, LinearLayout, RadioGroup, TextView}, }; -use crate::config_builder::{BridgeMode, CURRENT_CONFIG}; +use crate::config_builder::{BridgeMode, CURRENT_CONFIG, existing_config_uses_xdp}; pub fn bridge_mode(s: &mut Cursive) { - let single_selected = CURRENT_CONFIG.lock().bridge_mode == BridgeMode::Single; + let current_mode = CURRENT_CONFIG.lock().bridge_mode; + let show_legacy_xdp = existing_config_uses_xdp() || current_mode == BridgeMode::XDP; // create the group and buttons let mut group = RadioGroup::new().on_change(|_s, mode| { @@ -19,20 +20,40 @@ pub fn bridge_mode(s: &mut Cursive) { BridgeMode::Linux, "Linux Bridge (2 interfaces) - LibreQoS will inspect and stage the managed Netplan change", ); + let mut xdp_btn = show_legacy_xdp.then(|| { + group.button( + BridgeMode::XDP, + "Legacy XDP Bridge (existing installs only - keep only if you already run it)", + ) + }); let mut single_btn = group.button(BridgeMode::Single, "Single Interface (1 interface)"); // mark the one we want as selected - if single_selected { - single_btn.select(); - } else { - linux_btn.select(); + match current_mode { + BridgeMode::Single => { + single_btn.select(); + } + BridgeMode::XDP if show_legacy_xdp => { + if let Some(button) = xdp_btn.as_mut() { + button.select(); + } + } + _ => { + linux_btn.select(); + } } // now add them (in any order) to your layout - let layout = LinearLayout::vertical() + let mut layout = LinearLayout::vertical() .child(TextView::new("Select the bridge mode you want to use:")) - .child(linux_btn) - .child(single_btn); + .child(linux_btn); + if let Some(button) = xdp_btn { + layout.add_child(TextView::new( + "Legacy XDP mode was detected on this install. New installs should use Linux Bridge; leave XDP selected only if you intend to keep the existing XDP deployment.", + )); + layout.add_child(button); + } + layout.add_child(single_btn); s.add_layer( Dialog::around(layout) diff --git a/src/rust/lqos_setup/src/config_builder.rs b/src/rust/lqos_setup/src/config_builder.rs index 53801b61..7d15eab4 100644 --- a/src/rust/lqos_setup/src/config_builder.rs +++ b/src/rust/lqos_setup/src/config_builder.rs @@ -83,3 +83,10 @@ impl ConfigBuilder { } } } + +pub fn existing_config_uses_xdp() -> bool { + lqos_config::load_config() + .ok() + .and_then(|config| config.bridge.as_ref().map(|bridge| bridge.use_xdp_bridge)) + .unwrap_or(false) +} diff --git a/src/rust/lqos_setup/src/lib.rs b/src/rust/lqos_setup/src/lib.rs index 9c89ed69..1aa39990 100644 --- a/src/rust/lqos_setup/src/lib.rs +++ b/src/rust/lqos_setup/src/lib.rs @@ -2,3 +2,5 @@ pub mod bootstrap; pub mod hotfix; + +pub use bootstrap::PostinstAction; diff --git a/src/rust/lqos_setup/src/main.rs b/src/rust/lqos_setup/src/main.rs index ab9a0d47..dbbca920 100644 --- a/src/rust/lqos_setup/src/main.rs +++ b/src/rust/lqos_setup/src/main.rs @@ -46,6 +46,8 @@ enum Command { InstallHotfix, /// Exit 0 when runtime services should start, or 1 when setup is still required IsReady, + /// Internal helper: print the package postinst action token + PostinstAction, /// Internal helper: stop setup and activate runtime services now ActivateRuntime, /// Internal helper: stop runtime and activate the first-run setup service now @@ -213,6 +215,13 @@ fn main() { std::process::exit(1); } }, + Some(Command::PostinstAction) => match bootstrap::classify_postinst_action() { + Ok(action) => println!("{}", action.as_str()), + Err(err) => { + eprintln!("Unable to determine package postinst action: {err:#}"); + std::process::exit(1); + } + }, Some(Command::ActivateRuntime) => match service_handoff::activate_runtime_services() { Ok(message) => println!("{message}"), Err(err) => { diff --git a/src/rust/lqos_setup/src/web.rs b/src/rust/lqos_setup/src/web.rs index ffca8c86..eea8cf4a 100644 --- a/src/rust/lqos_setup/src/web.rs +++ b/src/rust/lqos_setup/src/web.rs @@ -1,7 +1,7 @@ //! Minimal setup-only web server for first-run LibreQoS configuration. use crate::{ - config_builder::{BridgeMode, CURRENT_CONFIG}, + config_builder::{BridgeMode, CURRENT_CONFIG, existing_config_uses_xdp}, interfaces, service_handoff, setup_actions::{self, CommitOutcome}, }; @@ -26,11 +26,6 @@ struct SetupQuery { notice: Option, } -#[derive(Debug, Deserialize)] -struct CompletedQuery { - auto: Option, -} - #[derive(Debug, Deserialize)] struct AdminForm { token: String, @@ -99,7 +94,7 @@ pub(crate) fn run() -> Result<()> { async fn root() -> Redirect { let target = match bootstrap::status_snapshot() { - Ok(snapshot) if snapshot.state.setup_complete => "/setup/completed?auto=1", + Ok(snapshot) if snapshot.state.setup_complete => "/setup/completed", _ => "/setup", }; Redirect::temporary(target) @@ -112,9 +107,8 @@ async fn setup_page(Query(query): Query) -> Response { } } -async fn completed_page(Query(query): Query) -> Response { - let automatic = query.auto.as_deref() == Some("1"); - match render_completed_page(automatic) { +async fn completed_page() -> Response { + match render_completed_page() { Ok(html) => Html(html).into_response(), Err(err) => error_response(StatusCode::INTERNAL_SERVER_ERROR, &err.to_string()), } @@ -248,8 +242,10 @@ fn render_setup_page( let config = CURRENT_CONFIG.lock().clone(); let mode_label = match config.bridge_mode { BridgeMode::Single => "single", - _ => "linux", + BridgeMode::XDP => "xdp", + BridgeMode::Linux => "linux", }; + let persisted_xdp = existing_config_uses_xdp(); let mut html = page_shell("LibreQoS Setup"); html.push_str("
"); @@ -285,6 +281,16 @@ fn render_setup_page( )); html.push_str(""); + if persisted_xdp { + html.push_str("

Legacy XDP Mode Detected

"); + if config.bridge_mode == BridgeMode::XDP { + html.push_str("

This system already uses legacy XDP bridge mode. LibreQoS no longer recommends XDP for new installs, but this page will preserve it if you leave the mode unchanged.

"); + } else { + html.push_str("

This system previously used legacy XDP bridge mode. Saving with Linux Bridge or Single Interface will migrate the node away from XDP.

"); + } + html.push_str("

Use Linux Bridge for long-term maintenance unless you intentionally need to keep the existing XDP deployment.

"); + } + if snapshot.hotfix_required { html.push_str("

Ubuntu 24.04 Hotfix Required

"); html.push_str(&format!( @@ -328,13 +334,19 @@ fn render_setup_page( "", selected(mode_label == "linux") )); + if persisted_xdp { + html.push_str(&format!( + "", + selected(mode_label == "xdp") + )); + } html.push_str(&format!( "", selected(mode_label == "single") )); html.push_str(""); - html.push_str("
"); - html.push_str("

Recommended for most installs. Select the WAN-facing and subscriber-facing interfaces.

"); + html.push_str("
"); + html.push_str("

Linux Bridge is recommended for most installs. Select the WAN-facing and subscriber-facing interfaces below. Existing XDP installs use the same interface pair if you choose to preserve XDP.

"); html.push_str(""); html.push_str(""); html.push_str("
"); - html.push_str("
"); + html.push_str("
"); html.push_str("

Use this when a single interface carries both internet and subscriber traffic with optional VLAN tags.

"); html.push_str("
diff --git a/src/rust/lqosd/src/node_manager/template.rs b/src/rust/lqosd/src/node_manager/template.rs index 027dd3bc..aeb2c8be 100644 --- a/src/rust/lqosd/src/node_manager/template.rs +++ b/src/rust/lqosd/src/node_manager/template.rs @@ -269,11 +269,11 @@ mod tests { use std::time::{SystemTime, UNIX_EPOCH}; const VALID_PNG: &[u8] = &[ - 0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, b'I', b'H', - b'D', b'R', 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00, 0x00, - 0x00, 0x1F, 0x15, 0xC4, 0x89, 0x00, 0x00, 0x00, 0x0A, b'I', b'D', b'A', b'T', 0x78, - 0x9C, 0x63, 0x00, 0x01, 0x00, 0x00, 0x05, 0x00, 0x01, 0x0D, 0x0A, 0x2D, 0xB4, 0x00, - 0x00, 0x00, 0x00, b'I', b'E', b'N', b'D', 0xAE, 0x42, 0x60, 0x82, + 0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, b'I', b'H', b'D', + b'R', 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00, 0x00, 0x00, 0x1F, + 0x15, 0xC4, 0x89, 0x00, 0x00, 0x00, 0x0A, b'I', b'D', b'A', b'T', 0x78, 0x9C, 0x63, 0x00, + 0x01, 0x00, 0x00, 0x05, 0x00, 0x01, 0x0D, 0x0A, 0x2D, 0xB4, 0x00, 0x00, 0x00, 0x00, b'I', + b'E', b'N', b'D', 0xAE, 0x42, 0x60, 0x82, ]; fn temp_runtime_dir() -> std::path::PathBuf {