usb-rea 0.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. usb_rea-0.2.1/.dockerignore +8 -0
  2. usb_rea-0.2.1/.github/workflows/ci.yml +18 -0
  3. usb_rea-0.2.1/.github/workflows/publish.yml +28 -0
  4. usb_rea-0.2.1/.gitignore +11 -0
  5. usb_rea-0.2.1/CHANGELOG.md +44 -0
  6. usb_rea-0.2.1/Dockerfile +21 -0
  7. usb_rea-0.2.1/LICENSE +21 -0
  8. usb_rea-0.2.1/PKG-INFO +140 -0
  9. usb_rea-0.2.1/README.md +124 -0
  10. usb_rea-0.2.1/docs/VALIDATION.md +22 -0
  11. usb_rea-0.2.1/docs/demo.gif +0 -0
  12. usb_rea-0.2.1/docs/demo.tape +18 -0
  13. usb_rea-0.2.1/glama.json +4 -0
  14. usb_rea-0.2.1/pyproject.toml +22 -0
  15. usb_rea-0.2.1/scripts/gadget_e2e.sh +70 -0
  16. usb_rea-0.2.1/server.json +27 -0
  17. usb_rea-0.2.1/smithery.yaml +8 -0
  18. usb_rea-0.2.1/src/usb_rea/__init__.py +2 -0
  19. usb_rea-0.2.1/src/usb_rea/actions.py +73 -0
  20. usb_rea-0.2.1/src/usb_rea/capture.py +38 -0
  21. usb_rea-0.2.1/src/usb_rea/checksums.py +46 -0
  22. usb_rea-0.2.1/src/usb_rea/cli.py +180 -0
  23. usb_rea-0.2.1/src/usb_rea/dissector.py +271 -0
  24. usb_rea-0.2.1/src/usb_rea/hid.py +344 -0
  25. usb_rea-0.2.1/src/usb_rea/infer.py +434 -0
  26. usb_rea-0.2.1/src/usb_rea/mcp_server.py +190 -0
  27. usb_rea-0.2.1/src/usb_rea/model.py +87 -0
  28. usb_rea-0.2.1/src/usb_rea/pcap.py +162 -0
  29. usb_rea-0.2.1/src/usb_rea/pipeline.py +64 -0
  30. usb_rea-0.2.1/src/usb_rea/replay.py +19 -0
  31. usb_rea-0.2.1/src/usb_rea/report.py +43 -0
  32. usb_rea-0.2.1/src/usb_rea/scaffold.py +136 -0
  33. usb_rea-0.2.1/src/usb_rea/synth.py +216 -0
  34. usb_rea-0.2.1/tests/data/SOURCES.md +10 -0
  35. usb_rea-0.2.1/tests/test_checksums.py +10 -0
  36. usb_rea-0.2.1/tests/test_dissector.py +96 -0
  37. usb_rea-0.2.1/tests/test_e2e.py +97 -0
  38. usb_rea-0.2.1/tests/test_hid.py +99 -0
  39. usb_rea-0.2.1/tests/test_infer.py +64 -0
  40. usb_rea-0.2.1/tests/test_infer_advanced.py +135 -0
  41. usb_rea-0.2.1/tests/test_pcap.py +45 -0
  42. usb_rea-0.2.1/tests/test_public_pcaps.py +46 -0
  43. usb_rea-0.2.1/tests/test_segment.py +30 -0
  44. usb_rea-0.2.1/tests/test_smoke.py +8 -0
  45. usb_rea-0.2.1/tests/test_transactions.py +66 -0
  46. usb_rea-0.2.1/tests/test_validation.py +63 -0
@@ -0,0 +1,8 @@
1
+ .git
2
+ .venv
3
+ dist
4
+ drafts
5
+ **/__pycache__
6
+ .pytest_cache
7
+ *.pcap
8
+ *.pcapng
@@ -0,0 +1,18 @@
1
+ name: ci
2
+ on: [push, pull_request]
3
+ jobs:
4
+ test:
5
+ runs-on: ubuntu-latest
6
+ strategy:
7
+ matrix:
8
+ python: ["3.10", "3.12", "3.13"]
9
+ steps:
10
+ - uses: actions/checkout@v4
11
+ - uses: actions/setup-python@v5
12
+ with:
13
+ python-version: ${{ matrix.python }}
14
+ - name: tshark + lua for the generated Wireshark dissector tests
15
+ if: matrix.python == '3.12'
16
+ run: sudo apt-get update -q && sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -q tshark lua5.4
17
+ - run: pip install -e ".[dev]"
18
+ - run: pytest -q
@@ -0,0 +1,28 @@
1
+ name: publish
2
+ # Publishes to PyPI via Trusted Publishing (no token stored) when a GitHub release is published.
3
+ on:
4
+ release:
5
+ types: [published]
6
+ workflow_dispatch:
7
+ jobs:
8
+ build:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@v4
12
+ - uses: actions/setup-python@v5
13
+ with: {python-version: "3.12"}
14
+ - run: pip install build twine && python -m build && twine check dist/*
15
+ - uses: actions/upload-artifact@v4
16
+ with: {name: dist, path: dist}
17
+ pypi:
18
+ needs: build
19
+ runs-on: ubuntu-latest
20
+ environment:
21
+ name: pypi
22
+ url: https://pypi.org/p/usb-rea
23
+ permissions:
24
+ id-token: write
25
+ steps:
26
+ - uses: actions/download-artifact@v4
27
+ with: {name: dist, path: dist}
28
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,11 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .venv/
7
+ .pytest_cache/
8
+ *.pcap
9
+ *.pcapng
10
+ !tests/data/**
11
+ drafts/
@@ -0,0 +1,44 @@
1
+ # Changelog
2
+
3
+ ## Unreleased
4
+
5
+ ## v0.2.1 — 2026-10-16
6
+ - multi-packet transactions: actions that send several packets (e.g. write + apply) are inferred as one command; the driver sends all packets in order
7
+ - cross-packet checksums (a later packet carrying a checksum of an earlier one)
8
+ - fix: `mcp` is now a core dependency, so a plain `pip install usb-rea` gives a working `usb-rea mcp`
9
+
10
+ - MCP tools: richer descriptions (side effects, privileges, when-to-use) and `pydantic.Field` parameter
11
+ descriptions so Glama TDQS can score the schema; analysis tools accept the same `labels` string/file
12
+ as the CLI for marker-free captures
13
+
14
+ ## v0.2.0 — 2026-10-15
15
+ Wireshark dissector generator, HID descriptor decoding, wider inference, marker-free captures.
16
+
17
+ - `usb-rea dissector` (MCP tool `wireshark_dissector`): generate a Wireshark Lua dissector for the inferred protocol —
18
+ command names in the Info column, decoded values, counters, lookup labels, checksum verification with expert info;
19
+ tested against real tshark 4.4 in CI. `usb-rea demo` now writes `<device>.lua` too
20
+ - synthetic captures: submission and completion now share one URB id and use proper usbmon data flags,
21
+ so Wireshark pairs requests with responses (GET DESCRIPTOR responses and VID/PID are decoded)
22
+ - `--labels`: analyse plain Wireshark/USBPcap captures without markers by splitting traffic at the pauses between clicks
23
+ - inference: 32-bit LE/BE linear values and packed bitfields (contiguous bits beside non-zero const bits,
24
+ including 12-bit spans across two LE bytes); synthetic `dial` device + encode round-trip tests
25
+ - HID report descriptor decoder (`usb-rea hid`, MCP tool `hid_descriptor`): items, reports, bit-exact fields,
26
+ report IDs, Push/Pop, extended usages, payload decoding; checked against the HID 1.11 boot mouse/keyboard descriptors
27
+ - reports include a HID section and tag each inferred command with its HID report when the capture has GET_DESCRIPTOR 0x22
28
+ - Dockerfile and glama.json so the MCP server can be listed on Glama
29
+ - `usb-rea demo --device dial` (32-bit value + bitfield demo); README capability table, MCP tool list and roadmap
30
+ updated for 32-bit/bitfield inference and `hid_descriptor`
31
+ - registry metadata: `server.json` (official MCP Registry, `mcp-name` marker in README) and `smithery.yaml`
32
+ - listed on Glama (score badge in README); MCP install snippets now use the git URL so they work before the PyPI release
33
+
34
+ ## v0.1.0 — 2026-10-14
35
+ First public release.
36
+
37
+ - pcap/pcapng parser for Linux usbmon (DLT 189/220) and Windows USBPcap (DLT 249), pure Python
38
+ - action markers (`usb-rea mark`) and per-action diffing with background-traffic filtering
39
+ - inference: constants, command ids, linear 8/16-bit LE/BE values, multi-value actions, lookup tables,
40
+ rolling counters, sum/xor/CRC8/CRC16 checksums; leave-one-out guard against coincidental fits
41
+ - Markdown evidence report: every claim linked to Wireshark frame numbers
42
+ - standalone pyusb driver generator (bulk/interrupt and control/HID SET_REPORT)
43
+ - confirmed-only replay, CLI, MCP server (capture, mark, diff, report, scaffold, replay)
44
+ - validation: synthetic devices, liquidctl Kraken X, rivalcfg Rival 100, public Wireshark captures
@@ -0,0 +1,21 @@
1
+ # Stdio MCP server for Glama / local Docker use.
2
+ # Build: docker build -t usb-rea .
3
+ # Run: docker run --rm -i usb-rea (analysis of capture files)
4
+ # Live capture/replay needs host USB access: add --privileged -v /dev/bus/usb:/dev/bus/usb
5
+ FROM python:3.12-slim-bookworm
6
+
7
+ # libusb is needed by pyusb for replay/scaffolded drivers; analysis works without it
8
+ RUN apt-get update && apt-get install -y --no-install-recommends libusb-1.0-0 \
9
+ && rm -rf /var/lib/apt/lists/*
10
+
11
+ ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 PIP_DISABLE_PIP_VERSION_CHECK=1
12
+ WORKDIR /app
13
+ COPY pyproject.toml README.md LICENSE ./
14
+ COPY src ./src
15
+ RUN pip install --no-cache-dir ".[mcp]"
16
+
17
+ RUN useradd --create-home --uid 1000 app
18
+ USER app
19
+ WORKDIR /home/app
20
+
21
+ ENTRYPOINT ["usb-rea", "mcp"]
usb_rea-0.2.1/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Emir Mrkaljević
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
usb_rea-0.2.1/PKG-INFO ADDED
@@ -0,0 +1,140 @@
1
+ Metadata-Version: 2.5
2
+ Name: usb-rea
3
+ Version: 0.2.1
4
+ Summary: Reverse Engineer any USB gadget: capture, diff, infer, and generate open drivers with evidence
5
+ License-Expression: MIT
6
+ License-File: LICENSE
7
+ Requires-Python: >=3.10
8
+ Requires-Dist: mcp<3,>=1.2
9
+ Requires-Dist: pyusb>=1.2
10
+ Provides-Extra: dev
11
+ Requires-Dist: mcp<3,>=1.2; extra == 'dev'
12
+ Requires-Dist: pytest>=8; extra == 'dev'
13
+ Provides-Extra: mcp
14
+ Requires-Dist: mcp<3,>=1.2; extra == 'mcp'
15
+ Description-Content-Type: text/markdown
16
+
17
+ # usb-rea — Reverse Engineer any USB gadget
18
+
19
+ <!-- mcp-name: io.github.emirmrkaljevic/usb-rea -->
20
+
21
+ [![ci](https://github.com/emirmrkaljevic/usb-rea/actions/workflows/ci.yml/badge.svg)](https://github.com/emirmrkaljevic/usb-rea/actions/workflows/ci.yml) ![license](https://img.shields.io/badge/license-MIT-blue) [![usb-rea MCP server](https://glama.ai/mcp/servers/emirmrkaljevic/usb-rea/badges/score.svg)](https://glama.ai/mcp/servers/emirmrkaljevic/usb-rea)
22
+
23
+ ![usb-rea demo: capture labelled clicks, get the protocol and a working driver](docs/demo.gif)
24
+
25
+ Got a USB device that only ships a Windows vendor app (case LCD, RGB controller, mouse, "smart" lamp)? Click around in the vendor app while **usb-rea** captures the USB traffic (usbmon on Linux, USBPcap on Windows), diffs the packets per action, infers the protocol, and writes an open-source Python/libusb driver — with every conclusion linked to the exact packets that prove it.
26
+
27
+ > Status: alpha (v0.2.0). Validated on synthetic devices, on protocols documented by liquidctl and rivalcfg, and on public Wireshark captures — see [docs/VALIDATION.md](docs/VALIDATION.md). Not yet tested on a physical gadget: your captures are welcome.
28
+
29
+ ## Try it in 10 seconds (no hardware needed)
30
+ ```bash
31
+ pip install git+https://github.com/emirmrkaljevic/usb-rea
32
+ usb-rea demo --device fan
33
+ ```
34
+ ```
35
+ fan 100% 0x5a | counter (+1 per message) | 0x10 | u16be round(value * 1 + 0) | 0x00 | u16le crc16_modbus(bytes[0:6])
36
+ color 100% 0x5a | counter (+1 per message) | 0x20 | round(value * 1 + 0) | round(value[1] * 1 + 0) | round(value[2] * 1 + 0) | u16le crc16_modbus(bytes[0:6])
37
+ mode 100% 0x5a | counter (+1 per message) | 0x30 | lookup 1→0x11, 2→0x22, 3→0x47 | 0x00 | 0x00 | u16le crc16_modbus(bytes[0:6])
38
+ ```
39
+ It writes a synthetic capture of a fan controller, recovers the protocol, and writes `report.md` (every claim linked to Wireshark frame numbers) and a working pyusb driver that encodes values it never saw.
40
+
41
+ `--device lamp` (default) is a simple 8-bit device; `--device dial` shows 32-bit values and packed bitfields:
42
+ ```
43
+ position 100% 0xa5 | 0x01 | u32le round(value * 1 + 0) | sum8(bytes[0:6])
44
+ level 100% 0xa5 | 0x02 | bits[0:4] = round(value * 1 + 0) | 0x00 | sum8(bytes[0:4])
45
+ ```
46
+
47
+ ## What it can infer
48
+ | Role | Example |
49
+ |---|---|
50
+ | constant header / command id | `5a`, `10` vs `20` vs `30` |
51
+ | linear values, 8-, 16- or 32-bit LE/BE, several per action | `fan 2650` → `0a 5a`; `color 255 24 0`; `position 50000` → `50 c3 00 00` |
52
+ | packed bitfields (bits beside constant flag bits, incl. 12-bit spans over two bytes) | `level 7` → `b7` = flags `b0` + bits[0:4] |
53
+ | lookup tables (non-linear choices) | DPI 250→`08`, 500→`07` … |
54
+ | rolling counters | sequence byte +1 per message |
55
+ | checksums | sum8, xor8, neg/inv sum8, sum16, CRC8, CRC8/MAXIM, CRC16 CCITT-FALSE/XMODEM/MODBUS/KERMIT |
56
+
57
+ Linear fits must survive leave-one-out cross-checks; otherwise usb-rea falls back to an honest lookup table and says so.
58
+
59
+ ## Real device workflow (Linux)
60
+ ```bash
61
+ sudo modprobe usbmon
62
+ usb-rea capture start -o lamp.pcapng
63
+ usb-rea mark -m lamp.pcapng.actions.json "brightness 0%" # then click it in the vendor app
64
+ usb-rea mark -m lamp.pcapng.actions.json "brightness 50%" # ...repeat for 3+ values per setting
65
+ usb-rea capture stop
66
+ usb-rea report lamp.pcapng -o report.md
67
+ usb-rea scaffold lamp.pcapng -o lamp_driver.py
68
+ usb-rea dissector lamp.pcapng -o lamp.lua # Wireshark dissector for the same protocol
69
+ ```
70
+ Captured on Windows with USBPcap/Wireshark and no markers? Click each action with a short pause in between, then just list them in order — usb-rea splits the traffic at the pauses:
71
+ ```bash
72
+ usb-rea report lamp.pcapng --labels "brightness 0%; brightness 50%; brightness 100%; power off; power on"
73
+ usb-rea scaffold lamp.pcapng --labels labels.txt -o lamp_driver.py # one label per line
74
+ ```
75
+ (Markers, if you have them, are a JSON list of `{"label", "start", "end"}` epoch timestamps.)
76
+
77
+ ## MCP server
78
+ `pip install "usb-rea[mcp] @ git+https://github.com/emirmrkaljevic/usb-rea"` then add `usb-rea mcp` as a stdio server. Tools: `capture_start`, `capture_stop`, `mark_action`, `diff_actions`, `report`, `scaffold_driver`, `hid_descriptor` (decode a HID report descriptor / report payloads), `wireshark_dissector`, `replay_packet` (refuses unless the human confirmed). Analysis tools accept the same `labels` argument as the CLI for marker-free captures.
79
+
80
+ With [uv](https://docs.astral.sh/uv/) no install step is needed:
81
+ ```json
82
+ {"mcpServers": {"usb-rea": {"command": "uvx", "args": ["--from", "usb-rea[mcp] @ git+https://github.com/emirmrkaljevic/usb-rea", "usb-rea", "mcp"]}}}
83
+ ```
84
+ Once usb-rea is on PyPI, `--from "usb-rea[mcp]"` is enough. The server is also listed on [Glama](https://glama.ai/mcp/servers/emirmrkaljevic/usb-rea), which builds it from the repo's `Dockerfile` and checks that it starts and lists its tools.
85
+
86
+ ## How it works
87
+ 1. `usb-rea capture start` — record traffic (usbmon / USBPcap)
88
+ 2. `usb-rea mark "brightness 50%"` — label each action you take in the vendor app
89
+ 3. `usb-rea diff` — which bytes change per action, value correlation, checksums, counters
90
+ 4. `usb-rea infer` — message templates + evidence report
91
+ 5. `usb-rea scaffold` — generate a pyusb driver and replay tests
92
+ 6. `usb-rea dissector` — write a Wireshark Lua dissector for the inferred protocol
93
+ 7. `usb-rea replay` — send packets to the device (only with explicit confirmation)
94
+ 8. `usb-rea hid` — decode a HID report descriptor from a capture (GET_DESCRIPTOR 0x22) or from hex, and decode report payloads
95
+ 9. `usb-rea mcp` — the same tools as an MCP server for AI agents
96
+
97
+ ### Wireshark dissector
98
+ `usb-rea dissector capture.pcapng -o lamp.lua` turns the inferred protocol into a Wireshark Lua plugin, so you can keep
99
+ capturing and read the traffic as commands instead of hex:
100
+ ```bash
101
+ wireshark -X lua_script:lamp.lua capture.pcapng
102
+ tshark -X lua_script:lamp.lua -r capture.pcapng -Y 'usbrea.command == "brightness"'
103
+ ```
104
+ Each matching packet gets a `usbrea` tree with the command name, decoded values (`usbrea.brightness.value`), counters,
105
+ lookup labels, and a checksum check (`usbrea.checksum_ok`, with an expert warning on mismatch); the Info column reads
106
+ `brightness 50`. It works for bulk, interrupt and control/HID SET_REPORT transfers, from usbmon or USBPcap, and needs no
107
+ bit library (Wireshark 3.x and 4.x). `-n NAME` changes the filter prefix. CI runs every generated dissector through tshark.
108
+
109
+ ### HID devices
110
+ Most RGB keyboards, mice, headsets and LCD gadgets are HID with a vendor-defined page. If the capture includes the
111
+ report descriptor (start capturing, then replug the device), the report gets a section with every report's ID, size and
112
+ fields, and each inferred command is tagged with the HID report it travels in. Without a capture:
113
+
114
+ ```sh
115
+ usb-rea hid /sys/class/hidraw/hidraw0/device/report_descriptor --items
116
+ usb-rea hid capture.pcapng --decode "10ff0d1d000000" --type Output
117
+ ```
118
+
119
+ ## Roadmap
120
+ - [x] pcap/pcapng parser (usbmon + USBPcap)
121
+ - [x] action markers and per-action diffing
122
+ - [x] checksum detection (sum8, xor8, neg/inv sum8, CRC8, CRC16 variants)
123
+ - [x] evidence-backed protocol report
124
+ - [x] driver scaffolding (pyusb, bulk/interrupt + control/HID SET_REPORT)
125
+ - [x] confirmed replay
126
+ - [x] MCP server
127
+ - [x] rolling counters, multi-byte (16/32-bit LE/BE) values, lookup tables, multi-value actions
128
+ - [x] packed bitfields
129
+ - [x] validation against documented protocols (liquidctl, rivalcfg)
130
+ - [x] HID report descriptor decoding (`usb-rea hid`), with captured commands mapped to their HID reports
131
+ - [ ] emulated-device CI (dummy_hcd + configfs HID; script in `scripts/gadget_e2e.sh`)
132
+ - [ ] real-hardware case studies
133
+ - [x] multi-packet transactions (write + apply), including checksums over earlier packets
134
+ - [ ] state machines (request/response handshakes)
135
+
136
+ ## Legal
137
+ Reverse engineering for interoperability is permitted (EU Directive 2009/24/EC Art. 6, US DMCA §1201(f)). usb-rea only analyzes traffic of devices you own and never redistributes vendor code.
138
+
139
+ ## License
140
+ MIT
@@ -0,0 +1,124 @@
1
+ # usb-rea — Reverse Engineer any USB gadget
2
+
3
+ <!-- mcp-name: io.github.emirmrkaljevic/usb-rea -->
4
+
5
+ [![ci](https://github.com/emirmrkaljevic/usb-rea/actions/workflows/ci.yml/badge.svg)](https://github.com/emirmrkaljevic/usb-rea/actions/workflows/ci.yml) ![license](https://img.shields.io/badge/license-MIT-blue) [![usb-rea MCP server](https://glama.ai/mcp/servers/emirmrkaljevic/usb-rea/badges/score.svg)](https://glama.ai/mcp/servers/emirmrkaljevic/usb-rea)
6
+
7
+ ![usb-rea demo: capture labelled clicks, get the protocol and a working driver](docs/demo.gif)
8
+
9
+ Got a USB device that only ships a Windows vendor app (case LCD, RGB controller, mouse, "smart" lamp)? Click around in the vendor app while **usb-rea** captures the USB traffic (usbmon on Linux, USBPcap on Windows), diffs the packets per action, infers the protocol, and writes an open-source Python/libusb driver — with every conclusion linked to the exact packets that prove it.
10
+
11
+ > Status: alpha (v0.2.0). Validated on synthetic devices, on protocols documented by liquidctl and rivalcfg, and on public Wireshark captures — see [docs/VALIDATION.md](docs/VALIDATION.md). Not yet tested on a physical gadget: your captures are welcome.
12
+
13
+ ## Try it in 10 seconds (no hardware needed)
14
+ ```bash
15
+ pip install git+https://github.com/emirmrkaljevic/usb-rea
16
+ usb-rea demo --device fan
17
+ ```
18
+ ```
19
+ fan 100% 0x5a | counter (+1 per message) | 0x10 | u16be round(value * 1 + 0) | 0x00 | u16le crc16_modbus(bytes[0:6])
20
+ color 100% 0x5a | counter (+1 per message) | 0x20 | round(value * 1 + 0) | round(value[1] * 1 + 0) | round(value[2] * 1 + 0) | u16le crc16_modbus(bytes[0:6])
21
+ mode 100% 0x5a | counter (+1 per message) | 0x30 | lookup 1→0x11, 2→0x22, 3→0x47 | 0x00 | 0x00 | u16le crc16_modbus(bytes[0:6])
22
+ ```
23
+ It writes a synthetic capture of a fan controller, recovers the protocol, and writes `report.md` (every claim linked to Wireshark frame numbers) and a working pyusb driver that encodes values it never saw.
24
+
25
+ `--device lamp` (default) is a simple 8-bit device; `--device dial` shows 32-bit values and packed bitfields:
26
+ ```
27
+ position 100% 0xa5 | 0x01 | u32le round(value * 1 + 0) | sum8(bytes[0:6])
28
+ level 100% 0xa5 | 0x02 | bits[0:4] = round(value * 1 + 0) | 0x00 | sum8(bytes[0:4])
29
+ ```
30
+
31
+ ## What it can infer
32
+ | Role | Example |
33
+ |---|---|
34
+ | constant header / command id | `5a`, `10` vs `20` vs `30` |
35
+ | linear values, 8-, 16- or 32-bit LE/BE, several per action | `fan 2650` → `0a 5a`; `color 255 24 0`; `position 50000` → `50 c3 00 00` |
36
+ | packed bitfields (bits beside constant flag bits, incl. 12-bit spans over two bytes) | `level 7` → `b7` = flags `b0` + bits[0:4] |
37
+ | lookup tables (non-linear choices) | DPI 250→`08`, 500→`07` … |
38
+ | rolling counters | sequence byte +1 per message |
39
+ | checksums | sum8, xor8, neg/inv sum8, sum16, CRC8, CRC8/MAXIM, CRC16 CCITT-FALSE/XMODEM/MODBUS/KERMIT |
40
+
41
+ Linear fits must survive leave-one-out cross-checks; otherwise usb-rea falls back to an honest lookup table and says so.
42
+
43
+ ## Real device workflow (Linux)
44
+ ```bash
45
+ sudo modprobe usbmon
46
+ usb-rea capture start -o lamp.pcapng
47
+ usb-rea mark -m lamp.pcapng.actions.json "brightness 0%" # then click it in the vendor app
48
+ usb-rea mark -m lamp.pcapng.actions.json "brightness 50%" # ...repeat for 3+ values per setting
49
+ usb-rea capture stop
50
+ usb-rea report lamp.pcapng -o report.md
51
+ usb-rea scaffold lamp.pcapng -o lamp_driver.py
52
+ usb-rea dissector lamp.pcapng -o lamp.lua # Wireshark dissector for the same protocol
53
+ ```
54
+ Captured on Windows with USBPcap/Wireshark and no markers? Click each action with a short pause in between, then just list them in order — usb-rea splits the traffic at the pauses:
55
+ ```bash
56
+ usb-rea report lamp.pcapng --labels "brightness 0%; brightness 50%; brightness 100%; power off; power on"
57
+ usb-rea scaffold lamp.pcapng --labels labels.txt -o lamp_driver.py # one label per line
58
+ ```
59
+ (Markers, if you have them, are a JSON list of `{"label", "start", "end"}` epoch timestamps.)
60
+
61
+ ## MCP server
62
+ `pip install "usb-rea[mcp] @ git+https://github.com/emirmrkaljevic/usb-rea"` then add `usb-rea mcp` as a stdio server. Tools: `capture_start`, `capture_stop`, `mark_action`, `diff_actions`, `report`, `scaffold_driver`, `hid_descriptor` (decode a HID report descriptor / report payloads), `wireshark_dissector`, `replay_packet` (refuses unless the human confirmed). Analysis tools accept the same `labels` argument as the CLI for marker-free captures.
63
+
64
+ With [uv](https://docs.astral.sh/uv/) no install step is needed:
65
+ ```json
66
+ {"mcpServers": {"usb-rea": {"command": "uvx", "args": ["--from", "usb-rea[mcp] @ git+https://github.com/emirmrkaljevic/usb-rea", "usb-rea", "mcp"]}}}
67
+ ```
68
+ Once usb-rea is on PyPI, `--from "usb-rea[mcp]"` is enough. The server is also listed on [Glama](https://glama.ai/mcp/servers/emirmrkaljevic/usb-rea), which builds it from the repo's `Dockerfile` and checks that it starts and lists its tools.
69
+
70
+ ## How it works
71
+ 1. `usb-rea capture start` — record traffic (usbmon / USBPcap)
72
+ 2. `usb-rea mark "brightness 50%"` — label each action you take in the vendor app
73
+ 3. `usb-rea diff` — which bytes change per action, value correlation, checksums, counters
74
+ 4. `usb-rea infer` — message templates + evidence report
75
+ 5. `usb-rea scaffold` — generate a pyusb driver and replay tests
76
+ 6. `usb-rea dissector` — write a Wireshark Lua dissector for the inferred protocol
77
+ 7. `usb-rea replay` — send packets to the device (only with explicit confirmation)
78
+ 8. `usb-rea hid` — decode a HID report descriptor from a capture (GET_DESCRIPTOR 0x22) or from hex, and decode report payloads
79
+ 9. `usb-rea mcp` — the same tools as an MCP server for AI agents
80
+
81
+ ### Wireshark dissector
82
+ `usb-rea dissector capture.pcapng -o lamp.lua` turns the inferred protocol into a Wireshark Lua plugin, so you can keep
83
+ capturing and read the traffic as commands instead of hex:
84
+ ```bash
85
+ wireshark -X lua_script:lamp.lua capture.pcapng
86
+ tshark -X lua_script:lamp.lua -r capture.pcapng -Y 'usbrea.command == "brightness"'
87
+ ```
88
+ Each matching packet gets a `usbrea` tree with the command name, decoded values (`usbrea.brightness.value`), counters,
89
+ lookup labels, and a checksum check (`usbrea.checksum_ok`, with an expert warning on mismatch); the Info column reads
90
+ `brightness 50`. It works for bulk, interrupt and control/HID SET_REPORT transfers, from usbmon or USBPcap, and needs no
91
+ bit library (Wireshark 3.x and 4.x). `-n NAME` changes the filter prefix. CI runs every generated dissector through tshark.
92
+
93
+ ### HID devices
94
+ Most RGB keyboards, mice, headsets and LCD gadgets are HID with a vendor-defined page. If the capture includes the
95
+ report descriptor (start capturing, then replug the device), the report gets a section with every report's ID, size and
96
+ fields, and each inferred command is tagged with the HID report it travels in. Without a capture:
97
+
98
+ ```sh
99
+ usb-rea hid /sys/class/hidraw/hidraw0/device/report_descriptor --items
100
+ usb-rea hid capture.pcapng --decode "10ff0d1d000000" --type Output
101
+ ```
102
+
103
+ ## Roadmap
104
+ - [x] pcap/pcapng parser (usbmon + USBPcap)
105
+ - [x] action markers and per-action diffing
106
+ - [x] checksum detection (sum8, xor8, neg/inv sum8, CRC8, CRC16 variants)
107
+ - [x] evidence-backed protocol report
108
+ - [x] driver scaffolding (pyusb, bulk/interrupt + control/HID SET_REPORT)
109
+ - [x] confirmed replay
110
+ - [x] MCP server
111
+ - [x] rolling counters, multi-byte (16/32-bit LE/BE) values, lookup tables, multi-value actions
112
+ - [x] packed bitfields
113
+ - [x] validation against documented protocols (liquidctl, rivalcfg)
114
+ - [x] HID report descriptor decoding (`usb-rea hid`), with captured commands mapped to their HID reports
115
+ - [ ] emulated-device CI (dummy_hcd + configfs HID; script in `scripts/gadget_e2e.sh`)
116
+ - [ ] real-hardware case studies
117
+ - [x] multi-packet transactions (write + apply), including checksums over earlier packets
118
+ - [ ] state machines (request/response handshakes)
119
+
120
+ ## Legal
121
+ Reverse engineering for interoperability is permitted (EU Directive 2009/24/EC Art. 6, US DMCA §1201(f)). usb-rea only analyzes traffic of devices you own and never redistributes vendor code.
122
+
123
+ ## License
124
+ MIT
@@ -0,0 +1,22 @@
1
+ # Validation
2
+
3
+ usb-rea is validated without anyone's hardware in four ways. All of them run in CI (`pytest`).
4
+
5
+ | Suite | What it proves | File |
6
+ |---|---|---|
7
+ | Synthetic lamp | constant header, command byte, scaled value, sum8, over bulk and HID SET_REPORT | `tests/test_infer.py` |
8
+ | Synthetic fan controller | rolling counter, 16-bit big-endian value, 3-value RGB, lookup table, CRC16/MODBUS | `tests/test_infer_advanced.py` |
9
+ | liquidctl Kraken X (1e71:170e) | recovers `02 4d <channel> 00 <duty>` exactly as liquidctl's `kraken2.py` builds it | `tests/test_validation.py` |
10
+ | rivalcfg Rival 100 (1038:1702) | recovers non-linear DPI/polling lookup tables and RGB color as in rivalcfg's `rival100.py` | `tests/test_validation.py` |
11
+ | CRC catalogue | CRC8/CRC16 variants match the standard `123456789` check values | `tests/test_checksums.py` |
12
+ | Wireshark dissector | generated Lua plugin labels every command in tshark 4.4, values match, checksums verify, a corrupted checksum is flagged; Lua checksum helpers match the Python ones | `tests/test_dissector.py` |
13
+ | Real captures | parser handles public usbmon/USBPcap captures (downloaded on demand) | `tests/test_public_pcaps.py` |
14
+
15
+ The driver generated from each capture is also executed against a mocked pyusb device and must emit
16
+ byte-identical packets for values that were **never captured**.
17
+
18
+ ## Not yet covered
19
+ * Emulated USB gadget (dummy_hcd + raw-gadget). It needs a kernel with `CONFIG_USB_DUMMY_HCD` and
20
+ `CONFIG_USB_RAW_GADGET`; the development box runs a sandboxed kernel without them. Script planned in
21
+ `scripts/gadget_e2e.sh` for anyone with a normal Linux machine.
22
+ * Real hardware. Captures + action labels from users are very welcome (open an issue).
Binary file
@@ -0,0 +1,18 @@
1
+ # Regenerate with: vhs docs/demo.tape
2
+ Output docs/demo.gif
3
+ Set FontSize 15
4
+ Set Width 1100
5
+ Set Height 640
6
+ Set Theme "Dracula"
7
+ Set TypingSpeed 45ms
8
+ Hide
9
+ Type `export PATH=$PWD/.venv/bin:$PATH PS1='$ ' && cd $(mktemp -d) && clear` Enter
10
+ Show
11
+ Type "# a fan controller with a Windows-only app: capture + click labels -> protocol + driver" Enter
12
+ Sleep 600ms
13
+ Type "usb-rea demo --device fan -o fan" Enter
14
+ Sleep 2500ms
15
+ Type "sed -n '/^## .fan/,/^Evidence/p' fan/report.md" Enter
16
+ Sleep 3500ms
17
+ Type `python -c 'import fan.fan_driver as d; print(d.encode(d.FAN, [3100], seq=7).hex(" "))'` Enter
18
+ Sleep 3500ms
@@ -0,0 +1,4 @@
1
+ {
2
+ "$schema": "https://glama.ai/mcp/schemas/server.json",
3
+ "maintainers": ["emirmrkaljevic"]
4
+ }
@@ -0,0 +1,22 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "usb-rea"
7
+ version = "0.2.1"
8
+ description = "Reverse Engineer any USB gadget: capture, diff, infer, and generate open drivers with evidence"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.10"
12
+ dependencies = ["pyusb>=1.2", "mcp>=1.2,<3"]
13
+
14
+ [project.optional-dependencies]
15
+ dev = ["pytest>=8", "mcp>=1.2,<3"]
16
+ mcp = ["mcp>=1.2,<3"]
17
+
18
+ [project.scripts]
19
+ usb-rea = "usb_rea.cli:main"
20
+
21
+ [tool.hatch.build.targets.wheel]
22
+ packages = ["src/usb_rea"]
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env bash
2
+ # Emulated-device end-to-end test (EXPERIMENTAL, not run in CI yet).
3
+ #
4
+ # Needs a normal Linux kernel with dummy_hcd, libcomposite (usb_f_hid) and usbmon, plus root.
5
+ # Creates a virtual HID "lamp" on dummy_hcd via configfs, captures real kernel USB traffic with
6
+ # usbmon while writing lamp commands to it, runs usb-rea on the capture, and checks that the
7
+ # device side received exactly what the generated driver would send.
8
+ set -euo pipefail
9
+ [[ $EUID -eq 0 ]] || { echo "run as root"; exit 1; }
10
+ command -v dumpcap >/dev/null || { echo "need dumpcap (apt install tshark)"; exit 1; }
11
+ modprobe dummy_hcd libcomposite usbmon
12
+
13
+ G=/sys/kernel/config/usb_gadget/usbrea_lamp
14
+ cleanup() {
15
+ [[ -n "${CAP_PID:-}" ]] && kill "$CAP_PID" 2>/dev/null || true
16
+ if [[ -d $G ]]; then
17
+ echo "" > $G/UDC 2>/dev/null || true
18
+ rm -f $G/configs/c.1/hid.usb0; rmdir $G/configs/c.1/strings/0x409 $G/configs/c.1 \
19
+ $G/functions/hid.usb0 $G/strings/0x409 $G 2>/dev/null || true
20
+ fi
21
+ }
22
+ trap cleanup EXIT
23
+
24
+ mkdir -p $G && cd $G
25
+ echo 0x1234 > idVendor; echo 0xabcd > idProduct
26
+ mkdir -p strings/0x409; echo usb-rea > strings/0x409/manufacturer; echo "Fake Lamp" > strings/0x409/product
27
+ mkdir -p functions/hid.usb0
28
+ echo 0 > functions/hid.usb0/protocol; echo 0 > functions/hid.usb0/subclass; echo 5 > functions/hid.usb0/report_length
29
+ # vendor-defined report: 5 bytes in, 5 bytes out
30
+ printf '\x06\x00\xff\x09\x01\xa1\x01\x15\x00\x26\xff\x00\x75\x08\x95\x05\x09\x01\x81\x02\x95\x05\x09\x01\x91\x02\xc0' \
31
+ > functions/hid.usb0/report_desc
32
+ mkdir -p configs/c.1/strings/0x409; echo lamp > configs/c.1/strings/0x409/configuration
33
+ ln -s functions/hid.usb0 configs/c.1/
34
+ UDC=$(ls /sys/class/udc | grep dummy | head -1); echo "$UDC" > UDC
35
+ sleep 1
36
+ cd - >/dev/null
37
+
38
+ BUS=$(for d in /sys/bus/usb/devices/*; do [[ -f $d/idVendor && $(cat $d/idVendor) == 1234 ]] && cat $d/busnum; done | head -1)
39
+ HIDRAW=/dev/$(ls /sys/class/hidraw | while read h; do grep -q 1234 /sys/class/hidraw/$h/device/uevent 2>/dev/null && echo $h; done | head -1)
40
+ OUT=$(mktemp -d)
41
+ dumpcap -q -i usbmon$BUS -w $OUT/lamp.pcapng & CAP_PID=$!
42
+ sleep 1
43
+ cat /dev/hidg0 > $OUT/device_side.bin & READER=$!
44
+
45
+ python3 - "$HIDRAW" "$OUT" <<'PY'
46
+ import json, sys, time
47
+ from usb_rea.synth import encode
48
+ hidraw, out = sys.argv[1], sys.argv[2]
49
+ marks = []
50
+ with open(hidraw, "wb", buffering=0) as f:
51
+ for label in ["brightness 0%", "brightness 30%", "brightness 60%", "brightness 100%",
52
+ "hue 0", "hue 90", "hue 300"]:
53
+ name, val = label.split()[0], float(label.split()[1].rstrip("%"))
54
+ start = time.time(); time.sleep(0.2)
55
+ f.write(b"\x00" + encode(name, val)) # report id 0 + payload
56
+ time.sleep(0.3); marks.append({"label": label, "start": start, "end": time.time()})
57
+ json.dump(marks, open(f"{out}/lamp.pcapng.actions.json", "w"))
58
+ PY
59
+ sleep 1; kill $CAP_PID; CAP_PID=; kill $READER || true
60
+ usb-rea report $OUT/lamp.pcapng -o $OUT/report.md
61
+ python3 - "$OUT" <<'PY'
62
+ import sys
63
+ from usb_rea.pipeline import analyze
64
+ from usb_rea.synth import encode
65
+ p = analyze(f"{sys.argv[1]}/lamp.pcapng")
66
+ c = {c.name: c for c in p.commands}
67
+ assert c["brightness"].encode(45) == encode("brightness", 45), c["brightness"].fields
68
+ assert c["hue"].encode(200) == encode("hue", 200)
69
+ print("gadget e2e OK:", sys.argv[1])
70
+ PY
@@ -0,0 +1,27 @@
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json",
3
+ "name": "io.github.emirmrkaljevic/usb-rea",
4
+ "description": "Reverse engineer USB gadgets: capture traffic, infer the protocol, generate a pyusb driver.",
5
+ "repository": {
6
+ "url": "https://github.com/emirmrkaljevic/usb-rea",
7
+ "source": "github"
8
+ },
9
+ "version": "0.2.0",
10
+ "packages": [
11
+ {
12
+ "registryType": "pypi",
13
+ "identifier": "usb-rea",
14
+ "version": "0.2.0",
15
+ "transport": {
16
+ "type": "stdio"
17
+ },
18
+ "runtimeHint": "uvx",
19
+ "packageArguments": [
20
+ {
21
+ "type": "positional",
22
+ "value": "mcp"
23
+ }
24
+ ]
25
+ }
26
+ ]
27
+ }
@@ -0,0 +1,8 @@
1
+ # Smithery configuration: https://smithery.ai/docs/config#smitheryyaml
2
+ startCommand:
3
+ type: stdio
4
+ configSchema:
5
+ type: object
6
+ properties: {}
7
+ commandFunction: |-
8
+ (config) => ({command: "uvx", args: ["--from", "usb-rea[mcp]", "usb-rea", "mcp"]})
@@ -0,0 +1,2 @@
1
+ """usb-rea: Reverse Engineer any USB gadget."""
2
+ __version__ = "0.2.1"