
Sniffer de transferts USB en direct et à l'échelle du système en eBPF — décode le trafic USB en ligne (SETUP de contrôle, SCSI, HID) à partir de deux hooks URB universels. Pas d'usbmon, pas de renifleur matériel. Portable CO-RE.

Un flux en temps réel et colorisé du trafic USB à l'échelle du système — construit sur les deux points de passage universels URB que chaque pilote de contrôleur hôte traverse, il fonctionne donc aussi bien sur xHCI/EHCI/OHCI/dwc, sans tracepoints par contrôleur ni usbmon. Entièrement portable CO-RE.
| hook fentry | ce que cela nous indique |
|---|---|
usb_submit_urb | un transfert a été mis en file (périphérique, endpoint, type, charge utile) |
usb_hcd_giveback_urb | il s'est terminé (statut, octets déplacés, latence, charge utile) |
Un lru_hash indexé par le pointeur URB relie les deux : le submit horodate un début, la complétion le relit pour la latence submit→complete, puis le supprime. Cela reflète le couplage requête/réponse de httpbody — SUBMIT est la « requête » (ce que l'hôte envoie), COMPLETE la « réponse » (ce que le périphérique renvoie).
Les transferts de contrôle voient leur paquet SETUP de 8 octets décodé en nom de requête standard (GET_DESCRIPTOR, SET_CONFIGURATION, …) ; les phases de données s'affichent sous forme de texte quand elles semblent textuelles et sous forme de hexdump sinon.
La sortie est une ligne par événement (compacte). La première fois qu'un périphérique apparaît, il reçoit une ligne de légende ▸ (bus-dev, vid:pid, produit, vitesse de liaison) ; ensuite chaque ligne ne porte que la courte balise DEV, afin que les colonnes de gauche restent alignées et lisibles sous un trafic intense. Chaque ligne affiche l'heure, le type d'événement (SUBMIT/CMPLT), le type de transfert, epNdir, la flèche de direction (← périphérique→hôte IN, → hôte→périphérique OUT), les nombres d'octets, le statut, la latence et le pilote noyau propriétaire, puis un · et le détail le plus utile (SETUP décodé, commande SCSI, ou un court aperçu de la charge utile). Passez --hex pour obtenir le hexdump complet sur plusieurs lignes à la place. Les octets hexadécimaux sont colorés selon leur classe de valeur (nul bleu, ASCII imprimable cyan, espacement vert, autre contrôle magenta, haut/non-ASCII jaune) sur un TTY ; la sortie redirigée est en clair.
usbmon. Les paquets SETUP et les charges utiles sont décodés pendant que vous interagissez avec le périphérique.READ(10) lba=… blocks=…, WRITE(10), CSW PASS/FAIL).--errors-only fait remonter les stalls (EPIPE), les timeouts, le babble et les erreurs CRC sur tous les périphériques à la fois.INT ou d'écritures de contrôle SET_REPORT que vous n'avez pas déclenchées.--json émet du NDJSON ; redirigez vers ou un fichier pour différencier les charges utiles entre les exécutions.curl -fsSL https://yeet.cx | sh
Puis exécutez-le directement depuis GitHub — yeet récupère l'exemple et le compile pour vous, aucun clone nécessaire :
yeet run github:yeet-src/usbsnoop
Pour compiler à partir d'un checkout local à la place :
make
Extrait le BTF du noyau vers vmlinux.h (pour struct urb, usb_device et le descripteur de périphérique), puis compile. Nécessite clang, bpftool et un noyau avec 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
Tout le filtrage se fait côté noyau, donc le trafic filtré n'atteint jamais l'espace utilisateur.
Chaque ligne d'événement se termine par le pilote noyau propriétaire entre crochets ([hid_irq_in], [usb_api_blocking_completion]) — urb->complete est symbolisé dans le noyau via bpf_snprintf("%ps"), donc aucune recherche dans /proc/kallsyms n'est nécessaire. Les transferts bulk de stockage de masse décodent leur enveloppe Bulk-Only Transport en commande SCSI (CBW READ(10) lba=… blocks=… / CSW PASS). À une sortie temporisée (atteinte de --secs), un résumé par périphérique et un histogramme de latence log2 s'impriment ; une sortie par Ctrl-C les ignore (il n'y a pas de hook de signal visible en JS).
Le trafic bulk (stockage de masse et assimilés) confie souvent à la pile un tableau struct scatterlist (urb->sg) au lieu d'un transfer_buffer linéaire unique, la charge utile est donc répartie sur plusieurs pages. usbsnoop parcourt ce tableau et copie les octets de chaque segment, mais y accéder implique de traduire une page en son adresse virtuelle noyau — l'inverse du page_to_virt de x86-64, qui nécessite page_offset_base et vmemmap_base du noyau en cours d'exécution (tous deux randomisés par KASLR).
L'isolat JS ne peut pas lire /proc/kallsyms et le chargeur n'a pas de support ksym, vous passez donc les deux adresses de symboles et le côté BPF les déréférence :
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)
Sans ces options, les transferts SG affichent toujours les métadonnées complètes, mais aucun octet de charge utile — le comportement précédent. Ce chemin est x86-64 uniquement : sur les autres architectures, laissez ces options de côté.
actual/requested. Chaque enregistrement de l'anneau contient un data[16384] complet, donc l'anneau de 8 Mio contient ~512 événements.--page-offset-base / --vmemmap-base ci-dessus et un hôte x86-64 ; chaque segment est capturé jusqu'à une page, et seuls les 64 premiers segments d'un transfert sont parcourus.jq| option | défaut | signification |
|---|
--secs | indéfini | combien de temps exécuter ; omettez pour exécuter jusqu'à Ctrl-C (un nombre arrête et imprime un résumé) |
--vid, --vendor-id | tous | filtrer par identifiant constructeur (hex 0x1d6b ou décimal) |
--pid, --product-id | tous | filtrer par identifiant produit |
--bus | tous | filtrer par numéro de bus |
--dev | tous | filtrer par adresse de périphérique |
--type | tous | csv de iso, int, control, bulk |
--no-data | désactivé | ne pas lire les tampons de transfert (métadonnées uniquement) |
--max-data | 4096 | nombre maximal d'octets de charge utile affichés par événement |
--errors-only | désactivé | afficher uniquement les complétions non-OK (ignore SUBMIT et OK) |
--hex | désactivé | hexdump complet sur plusieurs lignes par transfert (aperçu compact en ligne sinon) |
--json | désactivé | émettre du NDJSON (un objet par événement) au lieu de la vue TTY |
--page-offset-base | désactivé | adresse noyau page_offset_base (hex) — active la capture de charge utile SG (x86-64) |
--vmemmap-base | désactivé | adresse noyau vmemmap_base (hex) — à associer avec --page-offset-base |