
Sniffer en vivo de transferencias USB a nivel de sistema en eBPF: decodifica el tráfico USB en línea (control SETUP, SCSI, HID) desde dos ganchos URB universales. Sin usbmon, sin sniffer de hardware. Portable con CO-RE.

Una fuente en tiempo real y coloreada del tráfico USB en todo el sistema — construida sobre los dos puntos de estrangulamiento universales de URB por los que pasa todo driver de controlador de host, por lo que funciona en xHCI/EHCI/OHCI/dwc por igual, sin tracepoints por controlador y sin usbmon. Totalmente portable con CO-RE.
| hook fentry | qué nos dice |
|---|---|
usb_submit_urb | una transferencia fue puesta en cola (dispositivo, endpoint, tipo, payload) |
usb_hcd_giveback_urb | se completó (estado, bytes movidos, latencia, payload) |
Un lru_hash con clave el puntero URB une ambos: el submit registra una marca de tiempo de inicio, la finalización la lee para obtener la latencia submit→complete y luego la elimina. Esto refleja el emparejamiento petición/respuesta de httpbody — SUBMIT es la "petición" (lo que envía el host), COMPLETE la "respuesta" (lo que devuelve el dispositivo).
Las transferencias de control decodifican su paquete SETUP de 8 bytes al nombre de petición estándar (GET_DESCRIPTOR, SET_CONFIGURATION, …); las etapas de datos se muestran como texto cuando parecen textuales y como hexdump en caso contrario.
La salida es una línea por evento (compacta). La primera vez que aparece un dispositivo recibe una línea de leyenda ▸ (bus-dev, vid:pid, producto, velocidad de enlace); después, cada fila lleva solo la etiqueta corta DEV, de modo que las columnas de la izquierda se mantienen alineadas y escaneables bajo mucho tráfico. Cada fila muestra la hora, el tipo (SUBMIT/CMPLT), el tipo de transferencia, epNdir, la flecha de dirección (← dispositivo→host IN, → host→dispositivo OUT), recuentos de bytes, estado, latencia y el driver del kernel propietario, seguido de un · y el detalle más útil (SETUP decodificado, comando SCSI o una vista previa corta del payload). Pasa --hex para obtener el hexdump multilínea completo en su lugar. Los bytes hexadecimales se colorean por clase de valor (null azul, ASCII imprimible cian, espacios en blanco verde, otros controles magenta, alto/no-ASCII amarillo) en una TTY; la salida por pipe es plana.
usbmon. Los paquetes SETUP y los payloads se decodifican mientras tocas el dispositivo.READ(10) lba=… blocks=…, WRITE(10), CSW PASS/FAIL).--errors-only saca a la superficie stalls (EPIPE), timeouts, babble y errores CRC en todos los dispositivos a la vez.INT o escrituras de control SET_REPORT que no disparaste.--json emite NDJSON; conéctalo a jq o a un archivo para comparar payloads entre ejecuciones.curl -fsSL https://yeet.cx | sh
Luego ejecútalo directamente desde GitHub — yeet descarga el ejemplo y lo compila por ti, sin necesidad de clonar:
yeet run github:yeet-src/usbsnoop
Para compilar desde un checkout local:
make
Vuelca el BTF del kernel a vmlinux.h (para struct urb, usb_device y el descriptor de dispositivo) y luego compila. Requiere clang, bpftool y un kernel con BTF.
yeet run . # all devices, runs until Ctrl-C
yeet run . -- --secs 30 # stop after 30s (prints a summary)
yeet run . -- --vid 0x320f # one vendor
yeet run . -- --vendor-id 0x046d --product-id 0xc52b # one device by id
yeet run . -- --bus 3 --dev 4 # one device by bus address
yeet run . -- --type control,int # only these transfer types
yeet run . -- --no-data # metadata only, skip payload capture
yeet run . -- --max-data 64 # cap rendered payload at 64 bytes
yeet run . -- --errors-only # only failed completions (stalls, timeouts)
yeet run . -- --hex # full multi-line hexdump per transfer
yeet run . -- --json | jq . # NDJSON, one object per event
Todo el filtrado ocurre en el lado del kernel, por lo que el tráfico filtrado nunca llega al espacio de usuario.
Cada línea de evento termina con el driver del kernel propietario entre corchetes ([hid_irq_in], [usb_api_blocking_completion]) — urb->complete se simboliza en el kernel mediante bpf_snprintf("%ps"), por lo que no se necesita consultar /proc/kallsyms. Las transferencias bulk de almacenamiento masivo decodifican su wrapper Bulk-Only Transport al comando SCSI (CBW READ(10) lba=… blocks=… / CSW PASS). En una salida temporizada (al alcanzar --secs) se imprime un resumen por dispositivo y un histograma de latencia log2; una salida con Ctrl-C lo omite (no hay hook de señal visible en JS).
El tráfico bulk (almacenamiento masivo y similares) a menudo entrega a la pila un array struct scatterlist (urb->sg) en lugar de un único transfer_buffer lineal, por lo que el payload vive disperso entre páginas. usbsnoop recorre ese array y copia los bytes de cada segmento, pero alcanzarlos significa traducir una página a su dirección virtual del kernel — la operación inversa de page_to_virt en x86-64, que necesita los page_offset_base y vmemmap_base del kernel en ejecución (ambos aleatorizados por KASLR).
El aislado JS no puede leer /proc/kallsyms y el loader no tiene soporte ksym, así que pasas las dos direcciones de los símbolos y el lado de BPF las desreferencia:
yeet run . -- \
--page-offset-base 0x$(sudo awk '$3=="page_offset_base"{print $1}' /proc/kallsyms) \
--vmemmap-base 0x$(sudo awk '$3=="vmemmap_base"{print $1}' /proc/kallsyms)
Sin esas opciones, las transferencias SG siguen mostrando metadatos completos, solo que sin bytes de payload — el comportamiento anterior. Esta vía es solo x86-64: en otras arquitecturas deja las opciones sin usar.
actual/requested. Cada registro del anillo lleva un data[16384] completo, por lo que el anillo de 8 MiB contiene ~512 eventos.--page-offset-base / --vmemmap-base anteriores y un host x86-64; cada segmento se captura hasta una página, y solo se recorren los primeros 64 segmentos de una transferencia.| opción | por defecto | significado |
|---|
--secs | para siempre | cuánto tiempo ejecutarse; omítelo para ejecutar hasta Ctrl-C (un número detiene e imprime un resumen) |
--vid, --vendor-id | cualquiera | filtrar por ID de fabricante (hex 0x1d6b o decimal) |
--pid, --product-id | cualquiera | filtrar por ID de producto |
--bus | cualquiera | filtrar por número de bus |
--dev | cualquiera | filtrar por dirección de dispositivo |
--type | todos | csv de iso, int, control, bulk |
--no-data | desactivado | no leer buffers de transferencia (solo metadatos) |
--max-data | 4096 | máx. de bytes de payload renderizados por evento |
--errors-only | desactivado | mostrar solo finalizaciones no OK (omite SUBMIT y OK) |
--hex | desactivado | hexdump multilínea completo por transferencia (vista previa compacta en línea en caso contrario) |
--json | desactivado | emitir NDJSON (un objeto por evento) en lugar de la vista TTY |
--page-offset-base | desactivado | dirección del page_offset_base del kernel (hex) — habilita la captura de payload SG (x86-64) |
--vmemmap-base | desactivado | dirección del vmemmap_base del kernel (hex) — se combina con --page-offset-base |