← All articles

· 3 min read

Making an undocumented device work on Linux

A USB controller with no official support, a packet capture tool and a Rust workspace. How you attack a protocol nobody published.

RustUSBLinux

I bought a button controller meant for streaming. It worked on Windows with the manufacturer’s software. On Linux, nothing: the system saw it as a generic HID device and the buttons did nothing useful at all.

There was no protocol documentation. No SDK. Manufacturers do not answer those questions.

I ended up writing the driver. I am telling it because the method works for any integration where the other end is a black box, and that happens far more often than it sounds: an old phone exchange, a card reader, an industrial scale, an ERP without an API.

Look first, write later

The temptation is to open the editor. That is the mistake.

The first step is capturing real traffic. On Linux, usbmon exposes everything going in and out of a USB port, and Wireshark reads it directly. With the device running in a Windows virtual machine, you press buttons and watch which packets come out.

What you are looking for in that mountain of bytes are patterns:

That last one is the kind of finding that changes the whole design of the code. And you do not deduce it by reading: you deduce it by pressing buttons and taking notes.

Write down everything, including what you do not understand

In my capture there was a byte that changed without me touching anything. I lost a couple of hours trying to make sense of it until I realised it was a sequence counter: it climbed on its own, wrapped at 255, and meant nothing else.

I left that written in the repository, in plain text, next to the code. Not because it is elegant, but because in six months I will not remember, and because someone else will want to know why that byte is ignored.

A reconstructed protocol with no notes is a protocol that has to be reconstructed again.

Why Rust, and why three crates

I chose Rust for a concrete reason: in a driver, the bugs are about bytes and timing, and the compiler takes a whole family of memory bugs off your plate while you fight the other two.

I split it into three parts:

  1. The protocol, with no input or output at all. It takes bytes, returns events. It can be tested without the device plugged in.
  2. The transport, which talks to the real USB.
  3. The application, which turns events into system actions.

That separation is not ceremony. It is what makes real tests possible: 90% of the logic is tested with bytes saved in a file, no hardware. If it were all one piece, testing anything would require having the device plugged in, and the tests would never get written.

Test it where you did not write it

The driver runs in CI against Fedora, Debian, Ubuntu and Arch. That is not excessive caution: USB access permissions and udev rules differ between distributions, and it is exactly the kind of thing that works on your machine and fails on the machine of whoever installs it.

If you are publishing something that touches the system, test it on more than one system. It is cheap and saves you half the issues.

What you take home

When a vendor tells you “it has no API”, what they almost always mean is “it has no documented API”. If there is a client that works, there is a protocol, and a protocol can be observed.

It is not always worth doing: it takes time, and if the manufacturer changes the firmware you start over. But when the alternative is replacing your whole fleet of devices, a couple of weeks of reverse engineering comes out considerably cheaper.

The code is published under the MIT licence, in case the starting point is useful to anyone.

Sounds familiar? Tell me about your case

Arrows to move, Enter to choose, Esc to close