sergio/takana
> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…
⎇ 5b761c17554ac412dde80ab2c2f77657fd1a90fe
crates/uevent-relay/src/main.rs
Historia
Quién tocó qué
crudo
feat(uevent-relay): que un accesorio enchufado DESPUES de arrancar aparezca
1//! `uevent-relay` — que un accesorio enchufado DESPUÉS de arrancar aparezca.
2//!
3//! ══ EL DEFECTO, MEDIDO EN METAL EL 2026-10-15 ═════════════════════════════════════════════════
4//! El operador, con el escritorio abierto: *«los accesorios no aparecen cuando se conectan después
5//! de iniciar»*. No es el compositor y no es el kernel. Es una pieza que falta, y la ausencia es
6//! silenciosa en las tres capas:
7//!
8//! · El kernel crea el nodo (`CONFIG_DEVTMPFS_MOUNT=y`) y emite su uevent al **grupo netlink 1**.
9//! · `libudev-zero` —el libudev de la imagen, el único que sintetiza `ID_INPUT_*` sin `udevd`—
10//! abre el monitor `"udev"` en el **grupo 0x4**, que es donde `udevd` reenvía lo ya procesado.
11//! · `libinput` pide exactamente ese monitor (`udev_monitor_new_from_netlink(udev, "udev")`).
12//!
13//! Sin `udevd`, **nadie manda nunca nada al grupo 0x4**: el monitor no recibe un solo evento en
14//! toda la sesión. Y no es un bug de libudev-zero, es su contrato, escrito en su README:
15//!
16//! «If you're developing your own device manager, you need to rebroadcast kernel uevents to the
17//! `0x4` netlink group of NETLINK_KOBJECT_UEVENT. This is required because libudev-zero can't
18//! simply listen to kernel uevents due to potential race conditions.»
19//! «Hotplugging support is fully optional. You can skip this step…»
20//!
21//! takana se salteó ese paso. El candidato natural era `mdev`, y se **retiró** del defconfig de
22//! busybox el 2026-09-22 con este argumento: «`CONFIG_DEVTMPFS_MOUNT=y` hace que el kernel cree y
23//! monte `/dev`». Es cierto para los NODOS y no cubre el REENVÍO — y ahí se perdió el hotplug sin
24//! que ninguna prueba lo notara, porque todo lo que está enchufado al arrancar funciona igual.
25//!
26//! ══ POR QUÉ UN REENVIADOR Y NO UN PARCHE DE UNA LÍNEA ═════════════════════════════════════════
27//! Hacer que libudev-zero escuche el grupo 1 es una línea, y fue lo primero que pensé. El autor
28//! explica por qué no: **carrera**. El uevent del kernel puede llegar antes de que el nodo de `/dev`
29//! o los atributos de `/sys` estén listos, y entonces `libinput` abre un dispositivo a medio nacer y
30//! lo descarta — un hotplug que funciona nueve de cada diez veces es peor que ninguno, porque nadie
31//! lo puede diagnosticar. Este proceso **espera a que el dispositivo exista** antes de reenviarlo,
32//! que es lo que cierra esa carrera, y deja libudev-zero sin tocar (su hash no se mueve).
33//!
34//! ══ LO QUE NO HACE ════════════════════════════════════════════════════════════════════════════
35//! No crea nodos (lo hace devtmpfs), no aplica reglas, no monta nada. Un pendrive va a APARECER
36//! como `/dev/sdb1` y que alguien lo monte es otra conversación: esto es la campana, no la mano.
37
38use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
39use std::process::ExitCode;
40
41/// La familia netlink de los uevents. No está en `libc` para todos los targets, y es ABI estable.
42const NETLINK_KOBJECT_UEVENT: libc::c_int = 15;
43/// Donde emite el KERNEL. Un proceso de usuario puede escucharlo, pero no puede escribir ahí.
44const GRUPO_KERNEL: u32 = 1;
45/// Donde escucha `libudev-zero` el monitor `"udev"` (`UDEV_MONITOR_NLGRP` en su fuente).
46const GRUPO_UDEV: u32 = 4;
47/// Un `recvbuf` chico PIERDE eventos en ráfaga —enchufar un hub USB son veinte de golpe— y lo que
48/// se pierde no se reintenta nunca. udev pide 128 MiB; 8 cubre cualquier ráfaga de un escritorio.
49const RECVBUF: libc::c_int = 8 * 1024 * 1024;
50/// Cuánto se espera a que el dispositivo exista antes de reenviar (la carrera del README).
51const ESPERA_MAX_MS: u64 = 300;
52const ESPERA_PASO_MS: u64 = 10;
53
54fn main() -> ExitCode {
55 let verboso = std::env::var_os("UEVENT_RELAY_DEBUG").is_some();
56
57 // ⚠ Sin privilegio no se puede hacer multicast a un grupo netlink: el `sendmsg` sale con
58 // EPERM. Se dice ACÁ y con la causa, porque el síntoma de no decirlo es «el hotplug no anda»
59 // otra vez, con un proceso vivo que parece estar trabajando.
60 if unsafe { libc::geteuid() } != 0 {
61 eprintln!(
62 "uevent-relay: hace falta ser root (o CAP_NET_ADMIN) para emitir al grupo netlink {GRUPO_UDEV}; \
63 sin eso los accesorios enchufados en caliente no aparecen"
64 );
65 return ExitCode::from(1);
66 }
67
68 let rx = match abrir_socket(GRUPO_KERNEL) {
69 Ok(s) => s,
70 Err(e) => {
71 eprintln!("uevent-relay: no pude escuchar los uevents del kernel: {e}");
72 return ExitCode::from(1);
73 }
74 };
75 // El socket de salida se liga al grupo 0 —no escucha nada— sólo para que el kernel le asigne
76 // un `nl_pid`. Emitir por el MISMO socket con el que se escucha haría que cada reenvío volviera
77 // a entrar por el `recv`: un bucle que se multiplica solo. Medido en el diseño, no en producción.
78 let tx = match abrir_socket(0) {
79 Ok(s) => s,
80 Err(e) => {
81 eprintln!("uevent-relay: no pude abrir el socket de salida: {e}");
82 return ExitCode::from(1);
83 }
84 };
85
86 println!(
87 "uevent-relay: escuchando el grupo {GRUPO_KERNEL} (kernel) y reenviando al {GRUPO_UDEV} (el que mira libudev-zero)"
88 );
89
90 let mut buf = vec![0u8; 16 * 1024];
91 let mut reenviados: u64 = 0;
92 loop {
93 let n = unsafe {
94 libc::recv(rx.as_raw_fd(), buf.as_mut_ptr() as *mut libc::c_void, buf.len(), 0)
95 };
96 if n <= 0 {
97 // EINTR no es un error: una señal no tiene por qué matar el hotplug de la máquina.
98 let err = std::io::Error::last_os_error();
99 if err.kind() == std::io::ErrorKind::Interrupted {
100 continue;
101 }
102 eprintln!("uevent-relay: recv falló ({err}); salgo para que arje me reinicie");
103 return ExitCode::from(1);
104 }
105 let n = n as usize;
106 let ev = Uevent::parsear(&buf[..n]);
107
108 // `libudev-zero` descarta todo mensaje cuyo contenido no traiga las cuatro claves que
109 // espera; reenviar lo que no es un uevent sería ruido en el socket de todos.
110 if !ev.util() {
111 continue;
112 }
113 if ev.accion.as_deref() == Some("add") {
114 esperar_a_que_exista(&ev, verboso);
115 }
116
117 let mut sa: libc::sockaddr_nl = unsafe { std::mem::zeroed() };
118 sa.nl_family = libc::AF_NETLINK as u16;
119 sa.nl_groups = GRUPO_UDEV;
120 let enviados = unsafe {
121 libc::sendto(
122 tx.as_raw_fd(),
123 buf.as_ptr() as *const libc::c_void,
124 n,
125 0,
126 &sa as *const libc::sockaddr_nl as *const libc::sockaddr,
127 std::mem::size_of::<libc::sockaddr_nl>() as libc::socklen_t,
128 )
129 };
130 if enviados < 0 {
131 eprintln!(
132 "uevent-relay: no pude reenviar el evento de {}: {}",
133 ev.devpath.as_deref().unwrap_or("?"),
134 std::io::Error::last_os_error()
135 );
136 continue;
137 }
138 reenviados += 1;
139 if verboso {
140 eprintln!(
141 "uevent-relay: #{reenviados} {} {} {}",
142 ev.accion.as_deref().unwrap_or("?"),
143 ev.subsistema.as_deref().unwrap_or("?"),
144 ev.devpath.as_deref().unwrap_or("?")
145 );
146 }
147 }
148}
149
150fn abrir_socket(grupo: u32) -> std::io::Result<OwnedFd> {
151 let fd = unsafe {
152 libc::socket(
153 libc::AF_NETLINK,
154 libc::SOCK_DGRAM | libc::SOCK_CLOEXEC,
155 NETLINK_KOBJECT_UEVENT,
156 )
157 };
158 if fd < 0 {
159 return Err(std::io::Error::last_os_error());
160 }
161 let fd = unsafe { OwnedFd::from_raw_fd(fd) };
162 unsafe {
163 libc::setsockopt(
164 fd.as_raw_fd(),
165 libc::SOL_SOCKET,
166 libc::SO_RCVBUF,
167 &RECVBUF as *const libc::c_int as *const libc::c_void,
168 std::mem::size_of::<libc::c_int>() as libc::socklen_t,
169 );
170 }
171 let mut sa: libc::sockaddr_nl = unsafe { std::mem::zeroed() };
172 sa.nl_family = libc::AF_NETLINK as u16;
173 sa.nl_groups = grupo;
174 let r = unsafe {
175 libc::bind(
176 fd.as_raw_fd(),
177 &sa as *const libc::sockaddr_nl as *const libc::sockaddr,
178 std::mem::size_of::<libc::sockaddr_nl>() as libc::socklen_t,
179 )
180 };
181 if r < 0 {
182 return Err(std::io::Error::last_os_error());
183 }
184 Ok(fd)
185}
186
187/// Lo que hace falta leer de un uevent para decidir si se reenvía y a qué hay que esperar.
188#[derive(Default, Debug, PartialEq)]
189struct Uevent {
190 accion: Option<String>,
191 devpath: Option<String>,
192 subsistema: Option<String>,
193 devname: Option<String>,
194 seqnum: bool,
195}
196
197impl Uevent {
198 /// El formato del alambre: `ACTION=add\0DEVPATH=/devices/...\0SUBSYSTEM=input\0...`, con un
199 /// primer campo `add@/devices/...` que no es un par y se salta.
200 fn parsear(buf: &[u8]) -> Uevent {
201 let mut ev = Uevent::default();
202 for campo in buf.split(|&b| b == 0) {
203 if campo.is_empty() {
204 continue;
205 }
206 let Ok(s) = std::str::from_utf8(campo) else { continue };
207 let Some((k, v)) = s.split_once('=') else { continue };
208 match k {
209 "ACTION" => ev.accion = Some(v.to_string()),
210 "DEVPATH" => ev.devpath = Some(v.to_string()),
211 "SUBSYSTEM" => ev.subsistema = Some(v.to_string()),
212 "DEVNAME" => ev.devname = Some(v.to_string()),
213 "SEQNUM" => ev.seqnum = true,
214 _ => {}
215 }
216 }
217 ev
218 }
219
220 /// Las cuatro claves que `libudev-zero` exige (`cnt != 4` ⇒ descarta el dispositivo). Reenviar
221 /// un mensaje que el otro lado va a tirar es gastar el socket de todos.
222 fn util(&self) -> bool {
223 self.accion.is_some() && self.devpath.is_some() && self.subsistema.is_some() && self.seqnum
224 }
225}
226
227/// La carrera que el README de libudev-zero nombra, cerrada: no se reenvía un `add` hasta que el
228/// dispositivo del que habla **existe**. Con devtmpfs el nodo suele estar antes que el evento, así
229/// que lo normal es que esto no espere ni un paso; cuando espera, es justo el caso que fallaba.
230fn esperar_a_que_exista(ev: &Uevent, verboso: bool) {
231 let mut esperado = 0;
232 while esperado < ESPERA_MAX_MS {
233 let sys_ok = ev
234 .devpath
235 .as_ref()
236 .map(|p| std::path::Path::new(&format!("/sys{p}")).exists())
237 .unwrap_or(true);
238 let dev_ok = ev
239 .devname
240 .as_ref()
241 .map(|n| std::path::Path::new(&format!("/dev/{n}")).exists())
242 .unwrap_or(true);
243 if sys_ok && dev_ok {
244 if esperado > 0 && verboso {
245 eprintln!("uevent-relay: esperé {esperado} ms a que naciera {}", ev.devpath.as_deref().unwrap_or("?"));
246 }
247 return;
248 }
249 std::thread::sleep(std::time::Duration::from_millis(ESPERA_PASO_MS));
250 esperado += ESPERA_PASO_MS;
251 }
252 // No se descarta: un dispositivo que tarda más que esto igual puede servirle a libinput, y
253 // tirarlo sería volver al síntoma. Se reenvía y se dice.
254 if verboso {
255 eprintln!(
256 "uevent-relay: {} no apareció en {ESPERA_MAX_MS} ms; lo reenvío igual",
257 ev.devpath.as_deref().unwrap_or("?")
258 );
259 }
260}
261
262#[cfg(test)]
263mod tests {
264 use super::*;
265
266 /// Un uevent de verdad, copiado de la forma que emite el kernel al enchufar un ratón USB.
267 fn crudo(partes: &[&str]) -> Vec<u8> {
268 let mut v = Vec::new();
269 for p in partes {
270 v.extend_from_slice(p.as_bytes());
271 v.push(0);
272 }
273 v
274 }
275
276 #[test]
277 fn parsea_un_uevent_de_raton() {
278 let buf = crudo(&[
279 "add@/devices/pci0000:00/0000:00:14.0/usb1/1-2/1-2:1.0/0003:046D:C077.0003/input/input21/event5",
280 "ACTION=add",
281 "DEVPATH=/devices/pci0000:00/0000:00:14.0/usb1/1-2/1-2:1.0/0003:046D:C077.0003/input/input21/event5",
282 "SUBSYSTEM=input",
283 "DEVNAME=input/event5",
284 "SEQNUM=4821",
285 ]);
286 let ev = Uevent::parsear(&buf);
287 assert_eq!(ev.accion.as_deref(), Some("add"));
288 assert_eq!(ev.subsistema.as_deref(), Some("input"));
289 assert_eq!(ev.devname.as_deref(), Some("input/event5"));
290 assert!(ev.devpath.unwrap().ends_with("event5"));
291 assert!(ev.seqnum);
292 }
293
294 /// El brazo de control: lo que `libudev-zero` va a tirar no se reenvía. Sin esta prueba, el
295 /// relay podría estar «funcionando» y reenviando sólo basura.
296 #[test]
297 fn lo_que_el_otro_lado_descarta_no_se_reenvia() {
298 // Sin SEQNUM: libudev-zero cuenta 3 de 4 claves y devuelve NULL.
299 let sin_seq = Uevent::parsear(&crudo(&["ACTION=add", "DEVPATH=/devices/x", "SUBSYSTEM=input"]));
300 assert!(!sin_seq.util(), "sin SEQNUM el otro lado lo tira");
301 // Sin SUBSYSTEM, idem.
302 let sin_sub = Uevent::parsear(&crudo(&["ACTION=add", "DEVPATH=/devices/x", "SEQNUM=1"]));
303 assert!(!sin_sub.util());
304 // Un mensaje que no es un uevent (los hay en ese socket) no pasa.
305 assert!(!Uevent::parsear(b"basura sin pares").util());
306 // Y el completo sí.
307 let ok = Uevent::parsear(&crudo(&[
308 "ACTION=remove",
309 "DEVPATH=/devices/x",
310 "SUBSYSTEM=usb",
311 "SEQNUM=9",
312 ]));
313 assert!(ok.util());
314 }
315
316 /// Un `remove` no espera a que nada exista —el dispositivo justamente ya no está— y ésa es la
317 /// razón de que la espera cuelgue sólo del `add`.
318 #[test]
319 fn el_remove_no_espera() {
320 let ev = Uevent::parsear(&crudo(&[
321 "ACTION=remove",
322 "DEVPATH=/devices/no-existe-jamas",
323 "SUBSYSTEM=input",
324 "DEVNAME=input/event99",
325 "SEQNUM=12",
326 ]));
327 assert_eq!(ev.accion.as_deref(), Some("remove"));
328 let t0 = std::time::Instant::now();
329 // La función se llama sólo para `add` en el bucle; acá se comprueba que si se llamara con
330 // un syspath inexistente, el techo es el declarado y no un cuelgue.
331 esperar_a_que_exista(&ev, false);
332 assert!(
333 t0.elapsed().as_millis() <= (ESPERA_MAX_MS as u128) + 200,
334 "la espera no tiene techo: {} ms",
335 t0.elapsed().as_millis()
336 );
337 }
338}