Nueva Herramienta de Configuración para LibreQoS
• Herbert WolversonNueva 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.confexiste. Si existe, lo cargamos y mostramos la configuración actual. Si no existe, creamos una configuración predeterminada. - Verificamos que
network.jsonexista. - Verificamos que
ShapedDevices.csvexista.
- Verificamos si
- 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.confynetwork.json. - Ofrecemos reiniciar
lqosdsi 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!
