Flash one image, plug in your USB devices + Ethernet, power on — and the Pi becomes a dedicated USB-over-IP appliance on your LAN. No desktop, no monitor, no login: the Pi only forwards USB.
Old hardware is the point. A dusty Pi 3B (or any older Pi with Ethernet) is exactly the right machine for this job: give it a keyboard, mouse, serial dongle or other USB device on one end, and it appears on any Linux machine on the network as if it were plugged in locally.
On the client the exported devices show up as ordinary USB devices (mice,
serial adapters, …); keyboards are injected through a low-latency uinput
channel. Everything is re-attached automatically whenever the Pi or a device
comes back.
Wireless is not used; Ethernet only. Wi-Fi/Bluetooth stay enabled but idle.
Status: hobby project, working and in daily use. See Caveats.
You need a Raspberry Pi (the Pi 3B and other older Pis are ideal — see
Caveats), a microSD card (or USB drive), Ethernet, and a Linux
machine to connect from. The client-side tooling is built for NixOS, but the
attach daemon works on any Linux with usbip and uinput available.
1. Build the image and flash it — one command. If no device is given you get a menu of attached drives:
git clone https://github.com/Corallus-Caninus/USBPI && cd USBPI
./flash.sh /dev/sdX # or just ./flash.sh to pick from a menuflash.sh builds usbip-pi.img first if it is missing or out of date. The
build downloads Raspberry Pi OS Lite once, patches it, and needs no root or
loop mounts. Missing tools? It enters the bundled nix shell automatically.
2. Boot the Pi — insert the medium, plug in the USB device(s) to export, plug in Ethernet, and power on. The green ACT LED pulses steadily once Linux is running (a handy headless "it's alive" indicator).
- First boot installs the configuration and reboots (~1–2 min).
- Second boot needs internet: it installs the
usbippackage and starts the server. After a few minutes the Pi is reachable atusbip-pi.localand exports every attached USB device on TCP 3240; keyboards use TCP 3250.
No keyboard, monitor or login is ever required for normal operation.
3. Connect from your machine — forward every exported device:
./connect.sh # keep everything attached (Ctrl-C to stop)
./connect.sh --once # attach non-keyboard devices once, then exit--once does not start the keyboard daemon (keyboards need the long-running
keyd process); use the normal mode if you need the keyboard.
It builds the C client if needed, self-elevates with sudo, finds the Pi by
usbip-pi.lan (DNS), usbip-pi.local (mDNS via avahi) or an IP, and keeps
devices attached. Keyboards are received by usbpi keyd through uinput.
flash.sh build the image (if needed) + write it to a block device
connect.sh build/launch the C client (keyboard daemon + attach loop)
build.sh the one build entry point (server/client/native/all/test)
configuration.c every tunable interval, buffer size and constant (#define)
shared/ wire-format helpers shared by the Pi and the client
client/ single C client (attach/detach/watch/keyd) + NixOS module
server/ flat unity C daemon + systemd units and configuration
scripts/ image build + drive-inspection helpers
shell.nix, flake.nix, usbip.nix build environment / client usbip tool
The C is deliberately flat: no header files and no Makefile. build.sh is the
only build system, and configuration.c holds nothing but #defines.
first boot
cmdline.txt carries
systemd.run=/boot/firmware/usbpi/firstrun.sh systemd.run_success_action=reboot
-> firstrun.sh creates the break-glass user, installs /etc/systemd/system/*
units + /usr/local/sbin scripts, sets the hostname, then reboots and
removes the kernel parameter.
second boot
usbip-provision.service (needs network)
-> apt-get install usbip
-> systemctl enable --now usbpi-hid-mirror.service usbipd.service
usbipd-device.service
steady state
usbipd listens on 0.0.0.0:3240
usbpi-hid-mirror watches kernel uevents, exports non-keyboard devices
directly through usbip-host (no helper script), and
keeps keyboards on the Pi's usbhid for the fast path
on TCP 3250
usbipd -e device mode on 3241 for legacy HID gadgets (unused
when the keyboard fast path owns keyboards)
The daemon deliberately excludes:
- the onboard Ethernet (
smsc95xx,0424:ec00) and its hub (0424:9514/0424:9512) — exporting them would kill the network; - USB hubs / root hubs — not exportable anyway;
- any mass-storage interface (USB class 08) — this protects the USB flash drive the Pi boots from;
- anything claimed by the keyboard fast path.
Everything else (serial adapters, mice, gamepads, …) is exported the plain USB/IP way.
Keyboards are not forwarded as USB devices. Instead:
- the physical keyboard stays on the Pi's own
usbhid, which polls it at its native rate no matter what the network is doing; usbpi-hid-mirrorreads/dev/input/event*(evdev, so the kernel already parsed HID), numbers every key transition with a sequence number, and sends it to the client over TCP 3250 with immediate event packets plus 20 Hz full state snapshots;- the client's
usbpi keydcreates a realuinputkeyboard (withEV_REP, so the OS handles auto-repeat normally) and applies the events; - every transition is acknowledged and retained in a 4096-event ring, so a
reconnect replays exactly what the client missed — once. If the client was
away longer than the ring, the server sends the current state with a
GAPmarker and the middle is lost by design.
Held keys are protected by a dead-man watchdog tied to the kernel's keyboard
autorepeat delay. The client reads the uinput keyboard's repeat delay (and
raises only that virtual device to at least 250 ms if the system value is
shorter), then releases after three missed snapshots (150 ms) — always before
the first autorepeat could fire. A key that is still held is re-pressed from
the next snapshot, with modifiers first so Shift+A never degrades to a
lowercase a. On a noisy link you will see held keys release and re-press, but
the OS never starts repeating a key whose heartbeat was missed.
A key-up lost on the physical USB link between keyboard and Pi cannot be detected by this (or any network-side) watchdog: the Pi never learned the key was released and keeps reporting it held. That is the physical-link limitation below.
Non-keyboard HID (mice, gamepads, tablets) stays on plain USB/IP. A composite
device uses the fast path only when its primary (lowest-numbered) HID
interface is a keyboard; then the whole device stays on usbhid and only the
keyboard is forwarded. A mouse with an auxiliary macro-keyboard interface is
therefore left on plain USB/IP, and keyboard+touchpad combos can be forced
either way with exclude = on the Pi.
Configuration lives in /etc/usbpi/hid-mirror.conf on the Pi:
enabled = 1
keyboard = 1
queue_limit = 4096
rescan_ms = 2000
# exclude = 046d:c539 1234:5678
Set keyboard = 0 to fall back to the legacy buffered HID gadget. On the
client, USBIP_KEYBOARD=0 ./connect.sh disables the keyboard daemon.
Limitations:
- the fast path needs
/dev/uinputon the client and root (or a udev rule); - the keyboard channel is LAN-bound but unauthenticated — see Security;
- only keyboards get the replay/dead-man treatment;
- the queue lives in RAM: a Pi reboot or daemon restart loses whatever was buffered at that moment;
- errors on the physical USB link between device and Pi cannot be recovered by any software.
./connect.sh # all devices, kept attached
./connect.sh --once # all devices, one pass
USBIP_VIDPID=413c:2113 ./connect.sh # only this vendor:productFind the Pi on the LAN first. Many machines have avahi but not nss-mdns, so
ping usbip-pi.local may not resolve — use avahi directly:
avahi-resolve -n usbip-pi.local # prints: usbip-pi.local 192.168.1.xx
# or check your router's DHCP leases if that failsThe C client self-elevates and resolves .local via avahi-resolve:
sudo build/usbpi attach usbip-pi.local # first exported device
sudo build/usbpi attach usbip-pi.local 413c:2113 # pick by vendor:product
sudo build/usbpi attach 192.168.1.55 # or give an IP directly
sudo build/usbpi detach 413c:2113 # detach it againusbpi watch is the resilient re-attach loop behind connect.sh — it
survives the Pi powering off/on, network drops, a changed IP, the device being
replugged, and the client rebooting.
sudo USBIP_VIDPID=413c:2113 build/usbpi watch usbip-pi.localIn /etc/nixos/configuration.nix:
{
imports = [ /path/to/USBPI/client/usbip-client.nix ];
services.usbipClient = {
enable = true;
server = "usbip-pi.local"; # default
# busid = "1-1.3"; # optional, otherwise auto-detected
# vidPid = "046d:c31c"; # optional filter
# keyboard = true; # keyboard fast path (default)
};
}sudo nixos-rebuild switch. The attach and keyboard daemons start at boot and
restart automatically. Check them with
systemctl status usbip-attach usbip-keyd /
journalctl -u usbip-attach -u usbip-keyd -f.
If you prefer nss-mdns so ordinary programs resolve .local, add
services.avahi.nssmdns = true; to your NixOS configuration.
usbipdhas no authentication. Debian builds it againstlibwrap, soserver/hosts.allowlimits connections to RFC1918 and link-local ranges; everything else is denied byserver/hosts.deny. The device-mode daemon on port 3241 is covered by the same rules. If your LAN uses a different range, editserver/hosts.allowand reflash. Rebuild withUSBIP_LOCKDOWN=0 ./flash.sh /dev/sdXto export to the whole LAN with no restriction (not recommended).- The keyboard fast path (TCP 3250) is not authenticated. It is intended
for a trusted LAN. It is not covered by
hosts.allow(that is a TCP-wrapper rule forusbipdonly), so bind it to the LAN interface and add a firewall rule, or run it inside WireGuard, if your LAN is not trusted. - Break-glass SSH is enabled (
usbip/ password inCREDENTIALS.txt, plus your~/.ssh/*.pubkey). It is only for debugging — normal operation never needs it.CREDENTIALS.txtis git-ignored; delete it if you don't want it lying around (the hash lives only on the Pi).
| Symptom | Check |
|---|---|
usbip-pi.local doesn't resolve |
If the Pi is on another subnet, mDNS won't cross it — use the router's DNS name (e.g. usbip-pi.lan) or the Pi's IP. Otherwise avahi-resolve -n usbip-pi.local. |
usbip ... -r <name> fails "Unknown Op Common Status" |
The name has an IPv6 (AAAA) record that plain usbip tries first. Use ./connect.sh or the C client (it resolves IPv4), or pass the IPv4 address. |
usbip list -r shows nothing |
Provisioning may still be running. SSH in and run journalctl -u usbip-provision -f (needs internet on first boot). |
| Device missing after replug | The daemon re-exports it on the next uevent. On the client, systemctl restart usbip-attach (module) or re-run connect.sh. |
| Keyboard repeats or lags | systemctl status usbpi-hid-mirror / journalctl -u usbpi-hid-mirror -f on the Pi; journalctl -u usbip-keyd -f on the client. Check that port 3250 is reachable. |
| Keyboard not appearing on the client | The client needs /dev/uinput; run usbpi keyd as root and check `lsmod |
| Device exported raw instead of using the fast path | It is excluded in /etc/usbpi/hid-mirror.conf, or keyboard = 0. |
| Did the Pi actually boot the drive? | ./scripts/check-drive.sh /dev/sdX (read-only) reports whether firstrun.sh ran. |
| Server logs | journalctl -u usbpi-hid-mirror -u usbipd -u usbipd-device; provisioning log in /var/log/usbip-provision.log. |
| Wrong LAN range for restrictions | edit server/hosts.allow, rebuild, reflash. |
- Target is the Raspberry Pi 3 Model B. It can only boot from USB if the one-time OTP bit is set, and even then its USB boot is unreliable; booting from the microSD card always works and is recommended.
- Isochronous USB devices (webcams, USB audio) do not work well over USB/IP; interrupt/bulk devices (mice, serial, storage) are fine.
- Keyboards are re-created as
uinputdevices, so the client sees a synthetic keyboard with the USBPI vendor/product rather than the physical device. - Composite devices whose primary HID interface is a keyboard keep the whole
device on the Pi's
usbhid; only the keyboard is forwarded. Other composite devices (and anything inexclude) use plain USB/IP. - Only one client can use an exported USB device at a time — a USB limitation. The keyboard fast path supports one client as well.
- The keyboard plugged into the Pi is not usable locally on the Pi (by design).
- The keyboard channel has no authentication; see Security.
flash.sh runs the image build for you, but you can drive it directly:
./build.sh client # native client (build/usbpi)
./build.sh server # armv7 static server
./build.sh native # host compile check
./build.sh all # server + client
nix-shell --run ./scripts/build-image.sh # produce usbip-pi.img
USBPI_IMAGE_URL=... ./scripts/build-image.sh # override the base image
./scripts/check-drive.sh /dev/sdX # read-only diagnosis of a flashed drive
rm -rf build usbip-pi.img usbip-pi.img.sha256 # force a clean rebuildAll tunables — intervals, queue sizes, ports, buffer sizes, scheduler
priorities — are #defines in configuration.c. Override the armv7 compiler
with USBIP_ARM_CC (and USBIP_ARM_STATIC_LIB for the static libc.a
directory) if you supply your own toolchain.
The image applies automatic latency defaults: the server and client daemons
request real-time scheduling, lock their memory, the server pins itself to the
last CPU and moves the USB/Ethernet IRQs to CPU 0, the CPU governor is set to
performance, and USB autosuspend is disabled. No manual tuning is required;
failures fall back silently on kernels that do not expose a knob.
WTFPL — do what you want.