Skip to content

Latest commit

 

History

History
99 lines (82 loc) · 5.75 KB

systemd_doc.md

File metadata and controls

99 lines (82 loc) · 5.75 KB

USB/IP Wrapper Automation Documentation

TL;DR

The TL;DR for NixOS users is:

  • Client requests a USB device
  • Hoster only starts up the server and uses resources if a request is made over a secure channel
  • Hoster will shut down after a pre-configured time and free up the resources, guaranteeing that the USB device will become locally available again.

Details

In the following, we have two devices that communicate with each other, an USB/IP client and a host. The host makes a locally attached USB device available for a USB/IP client to remotely mount. These terms may conflict with other definitions when we are trying to unlock an encrypted device on a server. Here, a Laptop or other PC is usually used to host a USB device and the server wants to mount the USB device remotely. So in this scenario, the Laptop becomes the USB/IP host (as it is hosting a USB device) and the server becomes the USB/IP client as it wants to mount a remote USB device. I know it is confusing, but just try to keep track of who hosts the USB stick :)

These two devices share a few variables:

  • port: The port over which the client communicates with the hoster
  • usb-ids: A list of USB IDs that a hoster may provide and a client may want to mount
    • The lists may only contain a single entry and may be asymmetric as one host may provide different devices for various clients.

Finally, the auto-mount process that is provided for NixOS users looks as follows:

  • The USB/IP client requests to mount a remote USB device with a given USB ID over a secure channel
  • The USB/IP host has an active systemd-socket that listens for an incoming TCP connection on the pre-defined port. If data is received over this port:
    • The actual USB/IP server application is started on the hoster with a safe-loader
    • The safe-loader ensures that the next steps are delayed until the server application completely finished its start-up process1
      • For the interested: This is done via a hacky systemd unit that is spawned before the server application starts and watches the journalctl output from this point in time and waits until the server application announces that it finished initializing and is ready for incoming connections.
    • Start the global time-to-live for the server application to guarantee the USB device is unmounted from the remote client and becomes available again for the hoster
  • Reply to the request of the USB/IP client and mount the requested USB device if available
  • If the time-to-live timer runs up, stop the USB/IP server application and free up all resources

Security

As one can imagine, this setup does raise some security concerns, especially if a USB stick is shared that contains a key file or is a USB hardware key that doesn't require any interaction. There are quite a few uncommon parts one has to be aware of when thinking about sharing USB devices over a network.

For one, the communication is not done via HTTP(s), which means that the service cannot be routed through a classic HTTP(s) reverse proxy like Caddy/NGINX/Traefik. Also, the USB hoster is usually a Laptop that is also connected to public Wi-Fi or any other untrusted network. This means that opening the firewall/ports shouldn't be done without thinking2. Finally, if this should be done in an automated and transparent fashion one has to be very sure that one doesn't accidentally allow any random client to mount your hardware key without you noticing!

The solution I came up with, is one of my favorite solutions when it comes to security. Acknowledging that this is too risky and that I should build on work from other specialists that know what they are doing. So instead of trying to come up with custom firewall rules, complicated authentication schemes, or encryption strategies, I simply require that a secure channel has to be used from the start.

The secure channel

Here, we define a secure channel as an interface that only allows devices to communicate with each other that have been authenticated. Practically speaking, this should be a network interface that is managed via WireGuard, tailscale, or similar. This may seem like a trivial solution but it does allow the project to stay simple and keep the sensitive part configurable.

I use tailscale and only have to open up the port on the tailscale interface. As tailscale already takes care of the authentication, I can be sure that only authenticated devices can communicate with each other. With the ACL tags one can even configure which exact machines are allowed to communicate over the pre-defined port.

Final notes

Yeah, I know this was a lot... Sorry for that. I wanted to write a blog post about it, but creating a blog is too much work... And this document will help me remember what past me was thinking. But hey, maybe you also learned something along the way. Feel free to create an issue if something was unclear or, if you are as obsessive as I am, ask for more details

Either way, farewell traveler 👋.

Footnotes

  1. If this is skipped, the USB/IP server application will respond during the start-up phase that it isn't ready for incoming connections and will fail to process the request of the client.

  2. Yes, I know, you should never open ports without thinking. Just saying you should maybe think thrice not twice before doing so :p