I have been running a hybrid Linux/Windows box for some time now, using Xen. Xen is a bare-metal hypervisor with many powerful features, including the ability to map physical hardware (say, a graphics card) to guest VMs. I have struggled with getting USB to work seamlessly, however. My hardware prevents me from mapping a physical USB device to the guest, so I needed a different solution.
Xen allows emulated USB controllers to be mapped to the guest, and this works well. One important feature is missing, however. It is not possible to automatically connect a device to the guest when it is plugged in.
A lot of research and some diving into the code brought me to a solution, however. Utilizing the udev interface in linux and the low-level hypervisor interfaces, I can hot-plug devices at run-time. This post describes how I got there.
The source code discussed in this article can be found, in full, at https://github.com/stephen-czetty/xen-auto-usb.
The problem
It is possible to hot-plug a USB device into the guest via the command-line, but it is rife with issues. For the guest to see the device, you have to plug it in and manually run a command to connect it. Devices are also addressed by the physical port they are plugged into. For one-offs this isn’t so bad, since you can connect it once and forget it. For hot-plugging, this quickly gets tedious.
If you unplug the device, the guest doesn’t notice, and this can cause errors, even crashes. If you plug the device back into a different USB port, it won’t be reconnected, because it is addressed by physical location. I wanted a solution where I could plug in any USB device and have it immediately be seen in the guest, and when it was removed, it would go away.
Researching the solution
So I googled, but didn’t find many viable solutions. Then I looked at the source code for the tool set to see how it implemented the hot plug functionality. I discovered that there is a protocol (QMP) that is used via a UNIX-domain socket on the host. By connecting to that, I could hand-craft commands that instructed the guest to add or remove a device from its emulated USB controller.
Documentation on QMP is thin, and figuring this out required a lot of experimentation and reverse engineering. I crashed my guest on several occasions during this process, so this is not for the faint of heart.
QMP boils down to a JSON-based protocol. When initially connecting, you must send it a handshake message:
{
"execute": "qmp_capabilities"
}
After this handshake, you can now send commands. Important ones for the purposes of this application are device_add
, device_del
, qom_get
, and qom_list
. The first two add and remove devices, as is evident. The last two are lower-level commands that get details about the run-time configuration of the VM.
Examples
Add a device
{
"execute": "device_add",
"arguments": {
"id": "xenusb-0-1",
"driver": "usb-host",
"bus": "xenusb-0.0",
"port": 1,
"hostbus": 1
"hostaddr": 1
}
}
This command tells the host to attach the first device on bus 1 to the VM. When a device is attached to the host, dmesg
will report the device as (1−1). These are used for hostbus
and hostaddr
. It is connected to virtual controller 1 ("bus": "xenusb-1.0"
), on port 1. The id
parameter can be anything, but I chose values that match what the command-line tools expect.
Remove a device
{
"execute": "device_del",
"arguments": {
"id": "xenusb-0-1"
}
}
This command removes a device from the guest. All that is required is the id.
Create a controller
{
"execute": "device_add",
"arguments": {
"id": "xenusb-0",
"driver": "nec-usb-xhci",
"p2": "15",
"p3": "15"
}
}
This creates an emulated controller on the guest. The equivalent device_del
will remove it, but I have found that it crashes Windows when I tried it. p2
and p3
specify the number of ports, 15 is the maximum supported.
The driver nec-usb-xhci
creates a USB 3.0 controller. It is also possible to create 1.1 or 2.0 controllers by using piix3-usb-uhci
or usb-ehci
, respectively, leaving off the p2
and p3
arguments. (Xen uses a default port count for these controllers.)
List existing devices
{
"execute": "qom-list",
"arguments": {
"path": "xenusb-0.0"
}
}
This uses the relatively low-level command qom-list
to return all of the devices attached to controller 0. The return looks like:
{
"return": [{
"type": "link<usb-host>",
"name": "child[0]"
},
{
"type": "link<usb-host>",
"name": "child[1]"
}]
}
Nice, right? child[0]
is so helpful. You need to get the specifics of the device in order to work with it, as detailed below.
Get device specifics
{
"execute": "qom-get",
"arguments": {
"path": "xenusb-0.0",
"property": "child[0]"
}
}
This gets the details of the device. It returns:
{
"return": "/machine/peripheral/xenusb-2-3"
}
We then need to do further calls to qom-get
using the returned path to retrieve the port
, hostbus
, and hostaddr
values:
{
"execute": "qom-get",
"arguments": {
"path": "/machine/peripheral/xenusb-2-3",
"property": "port"
}
}
Etc.
As you can see from the examples above, QMP is not exactly simple to use, nor is it entirely self-consistent. (Note the underscores in device_add
and device_del
and the dashes in qom-list
and qom-get
.) It is easy to parse and generate, but there are a lot of steps to run through to get the necessary data.
Monitoring the device
At this point, I had a manual way to do what the tools already did. The next step was to figure out how to automate it. I reasoned that if I had a root device that was dedicated to the guest VM, I could listen for events on that device and automatically route all connections to the guest. So I grabbed an extra PCIe USB card I had lying around, threw it into the machine and got to work.
After some research, I found a python library called pyudev. It is not truly asynchronous, so I programmed it to poll with no timeout, and added an asyncio.sleep(1.0)
call to only poll for events once a second. This means that there is a possible delay of a second after plugging in a device, but I’m pretty sure it takes Windows much longer than that to initialize it anyway.
async def monitor_devices(self) -> None:
monitor = pyudev.Monitor.from_netlink(self.__context)
monitor.filter_by('usb')
while True:
device = monitor.poll(0)
if device is None:
await asyncio.sleep(1.0)
continue
device = Device(device)
self.__options.print_very_verbose('{0.action} on {0.device_path}'.format(device))
if device.action == "add":
if self.__is_a_device_we_care_about(device):
await self.device_added.fire(device)
elif device.action == "remove":
await self.device_removed.fire(device)
When an interesting device is added, the device_added
event is fired. The code always fires the device_removed
event, to cover devices that the script may have missed.
There is a lot of plumbing and methods for registering hubs and specific devices to watch for, but this loop is the meat. Combined with QMP commands, I was able to hot-plug and hot-unplug USB devices in a guest operating system.
Additional Features
I’ve added a lot of functionality to the script since I first conceived it. I’m not going into the details here, but in it’s current incarnation, it can:
- Hot-plug and unplug USB devices connected to specific (configurable) hubs
- Hot-plug and unplug USB devices specified by their Vendor:Device ids (in any hub)
- Create a new virtual controller if the first one fills up
- Watch for VM reboots and reconnect when it’s back up
- Read from a YAML config file
- Wrapper in C to run setuid root (instead of sudo)
On the Roadmap
- Run as a daemon
- Refactoring/cleanup
- Watch optical drives
- Rotating logs
- Limit number of controllers created
- External control (Web-API)
- Multiple VM support
Hi,
I wanted to thank you for your work in this code. It has been of great help to us for a very “particular” problem that we had.
If you want, I can tell you more details.
Thank you, Regards