Nueva Herramienta de Configuración para LibreQoS

• Herbert Wolverson

Nueva Herramienta de Configuración

La antigua herramienta de configuración de LibreQoS ya tenía algunas deficiencias. En particular:

  • Aún creaba una configuración en Python adecuada para la versión 1.4, la cual luego se convertía al nuevo formato 1.5.
  • La herramienta no se parecía en nada a las herramientas típicas de configuración de Ubuntu/Debian.
  • Una navegación unidireccional no resulta ideal para una configuración compleja.
  • No ofrecía ninguna ayuda una vez que se ejecutaba por primera vez.

nlnet financió la creación de una herramienta de configuración mucho mejor.

Aspecto Visual

Deseábamos que la herramienta se asemejara a otros sistemas de configuración de Ubuntu/Debian. La consistencia es importante, y la mayor parte de la infraestructura utiliza un estilo muy similar a “Borland Turbo Vision” - el ratón funciona, la interfaz de texto se ve bien en la mayoría de los dispositivos y se adapta al tamaño de su terminal. Por ello, decidimos usar cursive, que cumple con todos estos requisitos.

El aspecto encaja perfectamente:

Funcionalidad

La herramienta incluye varias características útiles:

La herramienta de configuración le informará de inmediato si ninguna de sus tarjetas de red funcionará con LibreQoS.

Esta función resultó ser sorprendentemente necesaria. Todos los días recibimos preguntas en nuestro chat acerca de diferentes tarjetas y por qué no funcionan. Si ninguna cumple con los criterios (compatibilidad con XDP, más de 2 filas, etc), la herramienta de configuración se detendrá.

Configure lo mínimo necesario y ponga en marcha la interfaz web

Simplificamos el proceso de configuración. Ahora ofrecemos secciones de configuración que usted debe de configurar de manera imprescindible, y hemos habilitado la edición de toda la configuración restante en la interfaz web (y, por supuesto, usted todavía puede editar el archivo /etc/lqos.conf manualmente).

Estrategia de Desarrollo

Puede encontrar el código fuente en GitHub.

Queríamos aprovechar funcionalidades existentes, pero — por razones obvias — no podíamos depender de que lqosd estuviera disponible para ayudar (ya que precisamente lo estamos configurando). Por ello, comenzamos con un Cargo.toml que incorpora las partes independientes de la configuración de LibreQoS, pero que, de otro modo, funciona de manera autónoma.

[package]
name = "lqos_setup"
version = "0.1.0"
edition = "2024"
license = "GPL-2.0-only"

[dependencies]
cursive = "0.21.1"
lqos_config = { path = "../lqos_config" }
nix.workspace = true
anyhow.workspace = true
once_cell = { workspace = true}
ip_network = { workspace = true }

Validation Code

Copiamos parte del código de validación del sistema lqosd. En el futuro, podríamos dividir esto en un modulo separado — pero necesitábamos sacar esta herramienta rápidamente. Por ejemplo, obtenemos las listas de tarjetas de red desde nix:

pub fn get_interfaces() -> anyhow::Result<Vec<String>> {
    let interfaces = nix::ifaddrs::getifaddrs()?
    .filter(|iface| check_queues(&iface.interface_name).is_ok())
    .map(|iface| iface.interface_name)
        .collect::<Vec<_>>();
    Ok(interfaces)
}

Y luego validamos el número de filas utilizando el mismo mecanismo exacto que lqosd usa (tanto en C como en Rust):


fn check_queues(interface: &str) -> anyhow::Result<()> {
    let path = format!("/sys/class/net/{interface}/queues/");
    let sys_path = Path::new(&path);
    if !sys_path.exists() {
        return Err(anyhow::anyhow!(
            "/sys/class/net/{interface}/queues/ does not exist. Does this card only support one queue (not supported)?"
        ));
    }

    let mut counts = (0, 0);
    let paths = std::fs::read_dir(sys_path)?;
    for path in paths {
        if let Ok(path) = &path {
            if path.path().is_dir() {
                if let Some(filename) = path.path().file_name() {
                    if let Some(filename) = filename.to_str() {
                        if filename.starts_with("rx-") {
                            counts.0 += 1;
                        } else if filename.starts_with("tx-") {
                            counts.1 += 1;
                        }
                    }
                }
            }
        }
    }

    if counts.0 == 0 || counts.1 == 0 {
        return Err(anyhow::anyhow!(
            "Interface {} does not have both RX and TX queues.",
            interface
        ));
    }
    if counts.0 == 1 || counts.1 == 1 {
        return Err(anyhow::anyhow!(
            "Interface {} only has one RX or TX queue. This is not supported.",
            interface
        ));
    }

    Ok(())
}

Estructura

Seguimos un patrón bastante predecible:

  • Inicialización:
    • Verificamos si /etc/lqos.conf existe. Si existe, lo cargamos y mostramos la configuración actual. Si no existe, creamos una configuración predeterminada.
    • Verificamos que network.json exista.
    • Verificamos que ShapedDevices.csv exista.
  • Menú principal:
    • Mostramos el estado actual.
    • Ofrecemos botones para cambiar el modo de puente (bridge), configurar interfaces y establecer las velocidades máximas de las filas.
  • Interfaces:
    • Mostramos la configuración actual y ofrecemos una interfaz para realizar cambios.
  • Guardar:
    • Si existe una configuración actual, hacemos una copia de seguridad.
    • Guardamos la configuración en /etc/lqos.conf y network.json.
    • Ofrecemos reiniciar lqosd si se está ejecutando.

Como cursive utiliza una configuración basada en pila (stack), el nivel superior siempre es el primer elemento. Luego agregamos menús adicionales a la pila según sea necesario y los retiramos cuando el usuario ha terminado. Como solo mantenemos una única configuración, la compartimos como una variable global. ¡Las variables globales son difíciles de evitar en este patrón!

Casi todo el código restante es la definición de la interfaz de usuario en Cursive. Por ejemplo:

use cursive::{
    view::{Nameable, Resizable},
    views::{Button, Dialog, EditView, LinearLayout, SelectView, TextView},
    Cursive,
};
use ip_network::IpNetwork;
use crate::config_builder::CURRENT_CONFIG;

/// Muestra y gestiona la lista de rangos de IP permitidos.
pub fn ranges(s: &mut Cursive) {
    let initial_ranges = {
        let config = CURRENT_CONFIG.lock().unwrap();
        config.allow_subnets.clone()
    };

    let select_view = SelectView::<String>::new()
        .with_all(initial_ranges.iter().map(|range| (range.clone(), range.clone())))
        .on_submit(|_s, range: &str| {
            let mut config = CURRENT_CONFIG.lock().unwrap();
            config.allow_subnets.push(range.parse().unwrap());
        })
        .with_name("ip_ranges")
        .fixed_width(30);

    let layout = LinearLayout::horizontal()
        .child(LinearLayout::vertical()
            .child(TextView::new("Allowed IP Ranges:"))
            .child(select_view)
            .child(Button::new("Remove Selected", |s| {
                    s.call_on_name("ip_ranges", |view: &mut SelectView<String>| {
                        if let Some(selected) = view.selected_id() {
                            let mut config = CURRENT_CONFIG.lock().unwrap();
                            config.allow_subnets.remove(selected);
                            view.remove_item(selected);
                        }
                    });
                })
            )
        )
        .child(TextView::new(" ")) // Espaciador de 1 carácter entre columnas
        .child(LinearLayout::vertical()
            .child(TextView::new("Add New Range:"))
            .child(
                EditView::new()
                    .on_submit(|s, content| {
                        let parsed = content.parse::<IpNetwork>();
                        if parsed.is_ok() {
                            let range = content.to_string();
                            let mut config = CURRENT_CONFIG.lock().unwrap();
                            config.allow_subnets.push(range);
                            s.call_on_name("ip_ranges", |view: &mut SelectView<String>| {
                                view.add_item(content.to_string(), content.to_string());
                            });
                        } else {
                            s.add_layer(Dialog::info("Invalid IP range format. Use CIDR notation, e.g., 192.168.0.0/16"));
                        }
                    })
                    .fixed_width(20)
            )
            .child(TextView::new("Press Enter to add the range"))
        );

        s.add_layer(
            Dialog::around(layout)
                .title("Allowed IP Ranges")
                .button("OK", |s| { s.pop_layer(); })
                .full_screen()
        );
}

Conclusión

La nueva herramienta es mucho más fácil de usar que la anterior. Se parece a una herramienta de configuración moderna de Ubuntu/Debian y ofrece una experiencia de usuario mucho mejor. El desarrollo fue relativamente sencillo, y pudimos reutilizar gran parte del código existente de LibreQoS.

¡Gracias a NLNet!