
Живой общесистемный сниффер USB-передач на eBPF — декодирует USB-трафик на лету (control SETUP, SCSI, HID) из двух универсальных хуков URB. Без usbmon, без аппаратного сниффера. Переносимость CO-RE.

Поток USB-трафика в реальном времени, системного масштаба, с цветовой разметкой — построен на двух
универсальных точках прохождения URB, через которые прогоняются все драйверы хост-контроллеров, поэтому
работает на xHCI/EHCI/OHCI/dwc одинаково, без tracepoint'ов для каждого контроллера и без
usbmon. Полностью переносимый благодаря CO-RE.
| fentry hook | что он нам сообщает |
|---|---|
usb_submit_urb | передача поставлена в очередь (устройство, endpoint, тип, полезная нагрузка) |
usb_hcd_giveback_urb | она завершилась (статус, передано байт, задержка, полезная нагрузка) |
lru_hash с ключом по указателю URB связывает эти два события: submit ставит
время начала, completion считывает его для вычисления задержки submit→complete, а затем
удаляет запись. Это повторяет схему пары запрос/ответ из httpbody — SUBMIT это
«запрос» (что отправляет хост), COMPLETE — «ответ» (что возвращает
устройство).
Для управляющих передач 8-байтовый SETUP-пакет декодируется в стандартное
имя запроса (GET_DESCRIPTOR, SET_CONFIGURATION, …); стадии данных отображаются как текст, когда
выглядят текстовыми, и как hexdump в противном случае.
Вывод — одна строка на событие (компактный). При первом появлении устройства выводится
строка-легенда ▸ (bus-dev, vid:pid, продукт, скорость соединения); после этого
каждая строка содержит только короткий тег DEV, поэтому левые колонки остаются выровненными
и хорошо читаемыми при интенсивном трафике. Каждая строка показывает время, вид (SUBMIT/CMPLT),
тип передачи, epNdir, стрелку направления (← устройство→хост IN, →
хост→устройство OUT), количество байт, статус, задержку и драйвер ядра-владельца,
затем · и наиболее полезную деталь (декодированный SETUP, SCSI-команду или краткий
предпросмотр полезной нагрузки). Передайте --hex для полного многострочного hexdump вместо этого. Шестнадцатеричные
байты раскрашиваются по классу значения (null — синий, печатный ASCII — голубой, пробелы —
зелёный, прочие управляющие — пурпурный, старший/не-ASCII — жёлтый) на TTY; в конвейере вывод
обычный.
usbmon.
SETUP-пакеты и полезные нагрузки декодируются, пока вы взаимодействуете с устройством.READ(10) lba=… blocks=…, WRITE(10), CSW PASS/FAIL).--errors-only показывает стойки (EPIPE), таймауты,
babble и ошибки CRC сразу на всех устройствах.INT-отчёты или
управляющие записи SET_REPORT, которые вы не инициировали.--json выводит NDJSON; передавайте в jq или в
файл, чтобы сравнивать полезные нагрузки между запусками.curl -fsSL https://yeet.cx | sh
Затем запускайте прямо из GitHub — yeet загружает пример и собирает его за вас, клонирование не требуется:
yeet run github:yeet-src/usbsnoop
Чтобы собрать из локальной копии:
make
Сначала извлекает BTF ядра в vmlinux.h (для struct urb, usb_device и
дескриптора устройства), затем компилирует. Требуются clang, bpftool и ядро
с BTF.
yeet run . # все устройства, работает до Ctrl-C
yeet run . -- --secs 30 # остановиться через 30 c (вывести сводку)
yeet run . -- --vid 0x320f # один вендор
yeet run . -- --vendor-id 0x046d --product-id 0xc52b # одно устройство по id
yeet run . -- --bus 3 --dev 4 # одно устройство по адресу на шине
yeet run . -- --type control,int # только эти типы передач
yeet run . -- --no-data # только метаданные, без захвата полезной нагрузки
yeet run . -- --max-data 64 # ограничить отображаемую нагрузку 64 байтами
yeet run . -- --errors-only # только неудачные завершения (stall'ы, таймауты)
yeet run . -- --hex # полный многострочный hexdump на каждую передачу
yeet run . -- --json | jq . # NDJSON, один объект на событие
Вся фильтрация выполняется на стороне ядра, поэтому отфильтрованный трафик никогда не достигает пользовательского пространства.
Каждая строка события заканчивается драйвером ядра-владельцем в скобках
([hid_irq_in], [usb_api_blocking_completion]) — urb->complete символизируется
в ядре через bpf_snprintf("%ps"), так что обращение к /proc/kallsyms не требуется.
Массовые bulk-передачи декодируют свою обёртку Bulk-Only Transport в
SCSI-команду (CBW READ(10) lba=… blocks=… / CSW PASS). При завершении по таймеру
(достижении --secs) печатаются сводка по каждому устройству и гистограмма задержек в лог2-шкале;
завершение по Ctrl-C пропускает их (хука на сигнал из JS не существует).
Bulk-трафик (массовые накопители и им подобные) часто передаёт стеку массив
struct scatterlist (urb->sg) вместо единого линейного transfer_buffer, поэтому
полезная нагрузка разбросана по страницам. usbsnoop обходит этот массив и копирует
байты каждого сегмента, но для доступа к ним нужно перевести страницу в её виртуальный
адрес ядра — это операция, обратная page_to_virt на x86-64, для которой нужны
page_offset_base и vmemmap_base работающего ядра (оба рандомизированы KASLR).
Изолят JS не может читать /proc/kallsyms, а загрузчик не поддерживает ksym,
поэтому вы передаёте два адреса символов, а сторона BPF разыменовывает их:
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)
Без этих флагов SG-передачи по-прежнему показывают полные метаданные, только без байт полезной нагрузки — как и раньше. Этот путь работает только на x86-64: на других архитектурах оставляйте флаги выключенными.
actual/requested. Каждая запись кольцевого
буфера содержит полный data[16384], поэтому кольцо на 8 МиБ вмещает ~512 событий.--page-offset-base / --vmemmap-base
выше и хост на x86-64; каждый сегмент захватывается до одной страницы, и обходятся только
первые 64 сегмента передачи.| флаг | по умолч. | значение |
|---|
--secs | бесконечно | как долго работать; опустите, чтобы работать до Ctrl-C (число останавливает и печатает сводку) |
--vid, --vendor-id | любой | фильтр по id вендора (hex 0x1d6b или десятичное) |
--pid, --product-id | любой | фильтр по id продукта |
--bus | любой | фильтр по номеру шины |
--dev | любой | фильтр по адресу устройства |
--type | все | csv из iso, int, control, bulk |
--no-data | выкл | не читать буферы передач (только метаданные) |
--max-data | 4096 | максимум байт полезной нагрузки, отображаемых на событие |
--errors-only | выкл | показывать только не-OK завершения (пропускает SUBMIT и OK) |
--hex | выкл | полный многострочный hexdump на передачу (иначе компактный встроенный предпросмотр) |
--json | выкл | выводить NDJSON (один объект на событие) вместо TTY-представления |
--page-offset-base | выкл | адрес page_offset_base ядра (hex) — включает захват SG-полезной нагрузки (x86-64) |
--vmemmap-base | выкл | адрес vmemmap_base ядра (hex) — используется вместе с --page-offset-base |