
Sniffer live a livello di sistema per trasferimenti USB in eBPF — decodifica il traffico USB inline (control SETUP, SCSI, HID) da due hook URB universali. Niente usbmon, niente sniffer hardware. Portabile CO-RE.

Un feed in tempo reale e colorizzato del traffico USB a livello di sistema — costruito sui due punti di passaggio URB universali attraverso cui passa ogni driver host-controller, quindi funziona su xHCI/EHCI/OHCI/dwc allo stesso modo, senza tracepoint per-controller e senza usbmon. Pienamente portabile CO-RE.
| fentry hook | cosa ci dice |
|---|---|
usb_submit_urb | una richiesta di trasferimento è stata accodata (device, endpoint, tipo, payload) |
usb_hcd_giveback_urb | è stato completato (stato, byte trasferiti, latenza, payload) |
Una lru_hash indicizzata dal puntatore URB unisce i due eventi: submit registra un tempo di inizio, il completamento lo rilegge per calcolare la latenza submit→complete, poi lo elimina. Questo rispecchia l'abbinamento richiesta/risposta di httpbody — SUBMIT è la "richiesta" (ciò che l'host invia), COMPLETE la "risposta" (ciò che il dispositivo restituisce).
I control transfer vedono decodificato il loro pacchetto SETUP di 8 byte nel nome standard della richiesta (GET_DESCRIPTOR, SET_CONFIGURATION, …); gli stadi di dati vengono mostrati come testo quando appaiono testuali e come hexdump altrimenti.
L'output è una riga per evento (compatto). La prima volta che appare un dispositivo, riceve una riga legenda ▸ (bus-dev, vid:pid, prodotto, velocità di link); dopo di che ogni riga riporta solo il tag breve DEV, così le colonne a sinistra restano allineate e scansionabili sotto traffico intenso. Ogni riga mostra tempo, tipo (SUBMIT/CMPLT), tipo di trasferimento, epNdir, la freccia di direzione (← device→host IN, → host→device OUT), conteggi di byte, stato, latenza e il driver kernel proprietario, poi un · e il dettaglio più utile (SETUP decodificato, comando SCSI o una breve anteprima del payload). Passa --hex per l'hexdump completo su più righe. I byte esadecimali sono colorati in base alla classe di valore (null blu, ASCII stampabile ciano, spazi bianchi verdi, altri caratteri di controllo magenta, alto/non-ASCII giallo) su TTY; l'output tramite pipe è in chiaro.
usbmon. I pacchetti SETUP e i payload vengono decodificati mentre interagisci con il dispositivo.READ(10) lba=… blocks=…, WRITE(10), CSW PASS/FAIL).--errors-only mette in evidenza stall (EPIPE), timeout, babble ed errori CRC su tutti i dispositivi contemporaneamente.INT o scritture di controllo SET_REPORT che non hai innescato.--json emette NDJSON; invialo tramite pipe a jq o a un file per confrontare i payload tra diverse esecuzioni.curl -fsSL https://yeet.cx | sh
Poi eseguilo direttamente da GitHub — yeet scarica l'esempio e lo compila per te, senza bisogno di clonare:
yeet run github:yeet-src/usbsnoop
Per compilare invece da un checkout locale:
make
Scarica il BTF del kernel in vmlinux.h (per struct urb, usb_device e il device descriptor), poi compila. Richiede clang, bpftool e 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
| flag | predefinito | significato |
|---|---|---|
--secs | forever | quanto tempo eseguire; ometti per eseguire fino a Ctrl-C (un numero ferma e stampa un riepilogo) |
--vid, --vendor-id | any | filtra per vendor id (hex 0x1d6b o decimale) |
--pid, --product-id | any | filtra per product id |
--bus | any | filtra per numero di bus |
--dev | any | filtra per indirizzo del dispositivo |
--type | all | csv di iso, int, control, bulk |
--no-data | off | non leggere i buffer di trasferimento (solo metadati) |
--max-data | 4096 | numero massimo di byte di payload mostrati per evento |
--errors-only | off | mostra solo i completamenti non-OK (salta SUBMIT e OK) |
--hex | off | hexdump completo su più righe per trasferimento (anteprima inline compatta altrimenti) |
--json | off | emette NDJSON (un oggetto per evento) invece della vista TTY |
--page-offset-base | off | indirizzo kernel page_offset_base (hex) — abilita la cattura del payload SG (x86-64) |
--vmemmap-base | off | indirizzo kernel vmemmap_base (hex) — da abbinare a --page-offset-base |
Tutto il filtraggio avviene lato kernel, quindi il traffico filtrato non raggiunge mai lo spazio utente.
Ogni riga di evento termina con il driver kernel proprietario tra parentesi quadre ([hid_irq_in], [usb_api_blocking_completion]) — urb->complete viene simbolizzato in-kernel tramite bpf_snprintf("%ps"), quindi non serve alcuna interrogazione di /proc/kallsyms. I bulk transfer di mass-storage decodificano il loro wrapper Bulk-Only Transport nel comando SCSI (CBW READ(10) lba=… blocks=… / CSW PASS). Su un'uscita a tempo (raggiungimento di --secs) vengono stampati un riepilogo per dispositivo e un istogramma delle latenze in log2; un'uscita con Ctrl-C lo salta (non c'è alcun hook di segnale visibile a JS).
Il traffico bulk (mass storage e simili) spesso passa allo stack un array struct scatterlist (urb->sg) invece di un singolo transfer_buffer lineare, quindi il payload è sparso su più pagine. usbsnoop percorre quell'array e copia i byte di ogni segmento, ma raggiungerli significa tradurre una pagina nel suo indirizzo virtuale di kernel — l'inverso di page_to_virt di x86-64, che richiede page_offset_base e vmemmap_base del kernel in esecuzione (entrambi randomizzati da KASLR).
L'isolato JS non può leggere /proc/kallsyms e il loader non ha supporto ksym, quindi passi i due indirizzi dei simboli e il lato BPF li dereferenzia: