The HID failure came down to the endpoint type map being indexed by endpoint number without the direction bit. A composite device can have endpoint 1 as both interrupt IN (0x81) and bulk OUT (0x01); the last one read won, so interrupt URBs were submitted as bulk and the kernel rejected them. The device attached and stayed silent. Endpoint data now comes from the raw descriptors read from /dev/bus/usb rather than sysfs, which only ever exposes the active alternate setting — a webcam's isochronous endpoints are invisible there because they only exist after SET_INTERFACE. Two sysfs parsing bugs fell out of that too: the numeric endpoint attributes are hex without a prefix (wMaxPacketSize "0040" was read as 40, not 64), and bInterval was never read at all. Reliability: three places could freeze the whole process. The share path fed io.Pipe from the WebSocket read loop, so one slow USB transfer stalled every tunnel and the keepalives with them. The relay wrote to client sockets while holding the hub lock, so one peer that stopped reading blocked routing and registration for everyone. Control transfers ran inline in the protocol loop behind a 5s timeout. Also fixed: a use-after- free where a discarded URB's memory could be collected while the kernel still owned it, a reap loop that spun at 100% CPU on ioctl errors, a missing attach timeout, a double close(done) panic, and Hash[:8] in the relay's log line, which let a client with a short hash take the server down. Adds mode "both", so one client can offer and consume devices at once. The tunnel and client-left callbacks became multicast for it: as plain fields the second manager to register silently unhooked the first. Tunnel traffic is now AES-256-GCM end to end, on the relay path as well as directly. The key is derived from the three tokens, not from the group hash — the relay is told the hash, so a key derived from it would protect nothing from the one party in the middle. Group IDs are unchanged, so existing setups keep working; only clients configured without the tokens drop to unencrypted, relay-only operation. Peers now try to connect directly, with the relay supplying the public address neither side can determine for itself. Candidates are raced because an unreachable address hangs until timeout rather than refusing. Falling back to the relay is not an error. Platform reach: cross-compiled targets for ARM, MIPS and RISC-V (the Linux client needed no code changes — usbdevfs is not architecture specific), multi-arch Docker images, an Android bridge that accepts devices over SCM_RIGHTS because apps cannot open /dev/bus/usb, and macOS builds via system_profiler enumeration. Adds a Windows KMDF filter driver under driver/windows with its Go side. UNTESTED: it has never been compiled or run, needs the WDK to build and an EV certificate to distribute. Treat it as a starting point. Adds "usb-client diag": says per machine whether sharing and using are possible, what stands in the way, and what fixes it. Reports can be uploaded to a relay to get them off machines that are awkward to copy from. 96 tests, all green under -race. Builds for linux, windows and darwin on amd64 and arm64. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
18 KiB
USB Server
USB-Sharing ueber Netzwerk mit Relay-Server fuer NAT-Traversal.
Architektur
┌──────────────────┐ ┌──────────────┐ ┌──────────────────┐
│ Client (both) │──ws/wss─│ Relay Server │─ws/wss──│ Client (both) │
│ gibt Geraete │ │ (Docker) │ │ gibt Geraete │
│ frei UND │ │ gruppiert │ │ frei UND │
│ empfaengt sie │ │ nach Hash │ │ empfaengt sie │
│ Web-UI :8080 │ │ │ │ Web-UI :8080 │
└────────┬─────────┘ └──────────────┘ └────────┬─────────┘
│ │
Physische USB-Geraete Virtuelle USB-Geraete
(usbdevfs) (vhci-hcd)
Relay-Server: WebSocket-Vermittler. Braucht keine Konfiguration - verbindet alle Clients die den gleichen Hash haben. Als Docker-Container deploybar. Er vermittelt die Verbindungen und reicht Tunneldaten weiter, ohne das USB/IP-Protokoll zu interpretieren.
Verbindungswege: Nach der Vermittlung durch den Relay versuchen zwei Clients, sich direkt zu verbinden. Klappt das - im gleichen LAN, ueber VPN, bei oeffentlicher IP oder Portfreigabe - laeuft der USB-Verkehr an der Relay vorbei. Das spart eine komplette Netzwerkstrecke, was bei USB/IP spuerbar ist: jeder Transfer ist ein eigener Round-Trip, und die Latenz addiert sich auf. Scheitert der Versuch (beide hinter NAT ohne Portfreigabe), laeuft alles unveraendert weiter ueber den Relay.
Verschluesselung: Tunneldaten sind Ende-zu-Ende mit AES-256-GCM verschluesselt, auf beiden Wegen. Der Schluessel wird aus den drei Tokens abgeleitet, nicht aus dem Hash - der Relay bekommt nur den Hash und kann daraus die Tokens nicht zurueckrechnen. Er sieht also weder USB-Daten noch kann er sich als Peer ausgeben.
Was der Relay trotzdem sieht und kann: Metadaten - wer in welcher Gruppe ist, welche Geraete angeboten werden, wann welches angefordert wird, und wie viel Verkehr fließt. Der Hash gruppiert, er authentifiziert nicht: wer ihn kennt, kann der Gruppe beitreten, Geraetelisten sehen und Geraete anfordern. Die Tunneldaten kann so jemand ohne die Tokens aber nicht lesen.
Clients ohne Tokens: Wer nur den Hash konfiguriert hat, kann keine Schluessel ableiten. Solche Clients funktionieren weiter, aber unverschluesselt und ohne Direktverbindung - und ein Peer, der verschluesselt, lehnt sie ab. Die Web-UI zeigt im Kopf an, welcher Fall vorliegt. Abhilfe: die drei Tokens auf alle Clients kopieren (Token-Tab). Die Gruppen-ID aendert sich dadurch nicht, bestehende Setups bleiben also verbunden.
Client: Kann in drei Modi betrieben werden:
- both: Gibt lokale Geraete frei und verbindet sich mit fremden. Das ist der uebliche Fall in einer Gruppe, in der jede Maschine Hardware verleiht und ausleiht.
- share: Gibt nur lokale USB-Geraete frei. Geraete werden erst dann vom System getrennt wenn ein Use-Client sie anfordert.
- use: Empfaengt nur. Zeigt verfuegbare Geraete aller Share-Clients an, einzeln verbindbar/trennbar.
Gruppierung: 3 zufaellige Tokens werden zu einem SHA256-Hash kombiniert. Alle Clients mit dem gleichen Hash gehoeren zusammen.
Quick Start
1. Relay-Server starten
# Mit Docker
docker compose up -d
# Oder direkt
make relay
./bin/usb-relay --port 8443
2. Tokens generieren
Auf dem ersten Client:
./bin/usb-client generate-token
Ausgabe:
Token 1: aB3dE...
Token 2: xY7zW...
Token 3: mN4pQ...
Hash: a1b2c3d4e5...
Die 3 Tokens auf alle weiteren Clients kopieren.
3. Clients starten
Auf jeder Maschine, die Geraete freigeben und empfangen soll:
sudo ./bin/usb-client both --relay ws://relay-server:8443
Root wird auf der Share-Seite gebraucht: das Oeffnen der Geraete unter
/dev/bus/usb/, das Loesen der Kernel-Treiber und das spaetere Zurueckbinden
laufen ueber privilegierte Schnittstellen.
Wer die Rollen trennen will, startet stattdessen nur eine Seite:
sudo ./bin/usb-client share --relay ws://relay-server:8443 # nur freigeben
sudo ./bin/usb-client use --relay ws://relay-server:8443 # nur empfangen
Web-UI oeffnen: http://localhost:8080
Installation
Go installieren
Linux (Debian/Ubuntu)
# Aktuelle Version herunterladen (go1.25.0 oder neuer)
wget https://go.dev/dl/go1.25.0.linux-amd64.tar.gz
# Altes Go entfernen und neues installieren
sudo rm -rf /usr/local/go
sudo tar -C /usr/local -xzf go1.25.0.linux-amd64.tar.gz
# PATH einrichten - ans Ende von ~/.bashrc oder ~/.profile anfuegen:
echo 'export PATH=$PATH:/usr/local/go/bin' >> ~/.bashrc
source ~/.bashrc
# Pruefen
go version
Windows
- Installer herunterladen: https://go.dev/dl/ (Datei
go1.25.0.windows-amd64.msi) - MSI-Installer ausfuehren - installiert Go nach
C:\Program Files\Go - Der Installer richtet den PATH automatisch ein
- Neue Eingabeaufforderung oeffnen und pruefen:
go version
Falls der PATH nicht automatisch gesetzt wurde:
- Windows-Taste > "Umgebungsvariablen" suchen > "Umgebungsvariablen fuer dieses Konto bearbeiten"
- Variable
Pathbearbeiten > Neu >C:\Program Files\Go\binhinzufuegen - Eingabeaufforderung neu oeffnen
Make installieren
Linux (Debian/Ubuntu)
sudo apt update
sudo apt install make
Linux (Fedora/RHEL)
sudo dnf install make
Windows
Option A: Chocolatey (empfohlen)
# Chocolatey installieren (Admin-PowerShell):
Set-ExecutionPolicy Bypass -Scope Process -Force
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072
iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))
# Make installieren:
choco install make
Option B: MSYS2
- MSYS2 von https://www.msys2.org herunterladen und installieren
- In der MSYS2-Shell:
pacman -S make C:\msys64\usr\binzum Windows PATH hinzufuegen
Option C: Ohne Make - Go-Befehle direkt ausfuehren (siehe unten)
Plattform-Unterstuetzung
| Funktion | Linux | Windows | Android | macOS |
|---|---|---|---|---|
| Share-Modus | Ja | Ungetestet (eigener Treiber noetig, siehe unten) | Ja, ueber App-Bridge | Nein (braucht IOKit) |
| Use-Modus | Ja (vhci-hcd) | Ja (usbip-win2) | Nein (kein vhci-hcd) | Nein (braucht Treiber) |
Kombinierter Modus (both) |
Ja | Nein | Nein | Nein |
| Relay-Server | Ja | Ja | - | Ja |
| Web-UI / Config | Ja | Ja | - | Ja |
Windows Share: Der Code ist vorhanden (driver/windows/ plus die
Go-Anbindung), aber der Filtertreiber wurde nie gebaut oder getestet. Er
braucht das WDK zum Bauen und ein EV-Zertifikat zum Verteilen. Details und
ehrliche Einordnung in driver/windows/README.md.
Android Share: Das Go-Binary laeuft dort unveraendert, aber eine App darf
/dev/bus/usb nicht oeffnen. Eine kleine App holt deshalb die Berechtigung
ueber das Framework und reicht den Dateideskriptor an den Client durch — siehe
android/README.md. Die Go-Seite dieser Bruecke ist fertig
und getestet, die App-Seite ist eine ungetestete Referenzimplementierung.
Docker und USB: Auf einem Linux-Host koennen Geraete in den Container
durchgereicht werden (/dev/bus/usb mounten, privileged: true). Auf macOS
und Windows geht das nicht: Docker laeuft dort in einer Linux-VM, die die
USB-Hardware des Hosts nie zu sehen bekommt. Container teilen sich den Kernel
des Hosts — auf Linux ist das derselbe Kernel, der die Geraete verwaltet, auf
den anderen Plattformen nicht.
Architekturen
Der Linux-Client laeuft unveraendert auf jeder Architektur - usbdevfs und
vhci-hcd sind nicht architekturspezifisch. make release baut fuer alle:
| Ziel | Typische Geraete |
|---|---|
| linux/amd64 | normale PCs, Intel-basierte Synology |
| linux/arm64 | Raspberry Pi 3/4/5 (64-Bit-OS), ARM-Synology |
| linux/arm | Raspberry Pi mit 32-Bit-OS, aeltere ARM-Boards |
| linux/386 | alte 32-Bit-x86-Rechner |
| linux/mips64le, linux/mipsle | diverse NAS- und Router-Plattformen |
| linux/riscv64 | RISC-V-Boards |
| windows/amd64, windows/arm64 | Windows (nur Use-Modus) |
Voraussetzung bleibt in allen Faellen: Share braucht Zugriff auf
/dev/bus/usb (Root), Use braucht das Kernel-Modul vhci-hcd. Auf
NAS-Systemen mit eigenem Kernel ist vhci-hcd haeufig nicht vorhanden -
solche Geraete taugen dann als Share-Client, nicht als Use-Client.
Windows Use-Modus: Benoetigt den usbip-win2 VHCI-Treiber (WHKL-zertifiziert, Microsoft-signiert). Der Client erkennt automatisch ob usbip-win2 installiert ist.
Windows Share-Modus: Nicht verfuegbar - erfordert Linux-spezifische Kernel-Schnittstelle (usbdevfs).
Voraussetzungen (Laufzeit)
Linux (Share-Modus):
- Root-Rechte (fuer USB-Geraetezugriff)
Linux (Use-Modus):
- Kernel-Modul
vhci-hcd:sudo modprobe vhci-hcd - Fuer automatisches Laden bei Boot:
echo "vhci-hcd" | sudo tee /etc/modules-load.d/vhci-hcd.conf
Bauen
Mit Make
# Alles bauen (Relay + Client, Linux)
make all
# Nur Relay-Server
make relay
# Nur Client (Linux)
make client
# Client fuer Windows (Cross-Compilation von Linux)
make client-windows
Ohne Make (Go direkt)
# Linux Relay-Server
CGO_ENABLED=0 go build -ldflags="-s -w" -o bin/usb-relay ./cmd/usb-relay/
# Linux Client
CGO_ENABLED=0 go build -ldflags="-s -w" -o bin/usb-client ./cmd/usb-client/
# Windows Client (Cross-Compilation von Linux)
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags="-s -w" -o bin/usb-client.exe ./cmd/usb-client/
Unter Windows (cmd.exe):
set CGO_ENABLED=0
go build -ldflags="-s -w" -o bin\usb-client.exe ./cmd/usb-client/
Unter Windows (PowerShell):
$env:CGO_ENABLED=0
go build -ldflags="-s -w" -o bin\usb-client.exe ./cmd/usb-client/
Docker (Relay-Server)
docker compose up -d
Der Relay lauscht auf Port 8443.
CLI Befehle
usb-client generate-token # 3 Tokens + Hash generieren
usb-client both [optionen] # Freigeben und Empfangen gleichzeitig
usb-client share [optionen] # Nur freigeben
usb-client use [optionen] # Nur empfangen
usb-client list # Lokale USB-Geraete auflisten
usb-client list -v # Mit Interfaces und Endpunkten (Diagnose)
usb-client gui # Nur Web-UI starten
usb-client config # Konfiguration anzeigen
usb-client config set [optionen] # Konfiguration aendern
usb-client install-service # Als systemd-Service installieren
usb-client uninstall-service # Service deinstallieren
Optionen
--config <pfad> Config-Datei (Standard: ~/.usb-server/config.json)
--relay <adresse> Relay-Server (z.B. ws://localhost:8443)
--hash <hash> Gruppen-Hash
--name <name> Client-Name
--web-port <port> Web-UI Port (Standard: 8080)
--no-gui Web-UI deaktivieren
Netzwerk
Fuer Direktverbindungen oeffnet jeder Share-faehige Client einen TCP-Port (standardmaessig zufaellig). Zwei Faelle brauchen Handarbeit:
- Feste Portfreigabe:
direct_portin der Config setzen und diesen Port in Firewall/Router weiterleiten. Dann finden auch Peers hinter NAT hierher. - Gar keine Direktverbindung gewuenscht:
disable_direct: truesetzt alles zurueck auf den reinen Relay-Betrieb.
Ob eine Verbindung direkt zustande kam, steht im Log des Use-Clients
(direct connection to ... established gegen using the relay).
Umgebungsvariablen
USBSRV_DEBUG=1 Protokolliert jeden einzelnen USB-Transfer (URB).
Nur zur Fehlersuche - ein aktives Video- oder Audiogeraet
erzeugt tausende URBs pro Sekunde, und das Protokollieren
kostet dann mehr Zeit als das Weiterleiten.
Fehlersuche
Erste Anlaufstelle
usb-client diag
Sagt fuer die jeweilige Maschine, ob Freigeben und Empfangen moeglich sind,
was im Weg steht und was dagegen hilft. Laeuft auf Linux, Windows und macOS
und prueft plattformspezifisch das Richtige: Rechte auf /dev/bus/usb und das
vhci-hcd-Modul unter Linux, Treiberstatus und Testsignierung unter Windows,
die IOKit-Lage unter macOS. Fuer jedes Geraet steht dabei, ob es freigegeben
werden kann - und wenn nicht, warum.
Zum Weitergeben:
usb-client diag -json # maschinenlesbar
usb-client diag -out report.txt # zusaetzlich in eine Datei
usb-client diag -id meinreport # zum konfigurierten Relay hochladen
Der Upload legt den Report unter <relay>/diag/<id> ab, wo er 24 Stunden
liegt. Das erspart es, von einer schwer erreichbaren Maschine - headless NAS,
Windows-Rechner mitten im Treiber-Debugging - tausende Zeilen von Hand zu
kopieren.
Der Report enthaelt Hostname, Geraeteliste und OS-Version. Beim Hochladen auf einen Relay kann jeder mit der ID ihn lesen. Auf einem oeffentlichen Relay also eine schwer zu erratende ID waehlen, oder den Report lokal speichern und selbst weitergeben.
Haeufige Faelle
Geraet verbindet sich, liefert aber keine Daten.
usb-client list -v auf der Share-Seite zeigt, wie jeder Endpunkt eingestuft
wird. Der Transfertyp dort entscheidet, wie die Transfers zum Geraet
geschickt werden - steht bei einem Interrupt-Endpunkt bulk, weist der
Kernel die Uebertragungen ab. Ohne Root-Rechte koennen die vollstaendigen
Deskriptoren nicht gelesen werden; dann erscheint ein Hinweis statt der
Endpunktliste.
Geraet bleibt nach einem Absturz auf "in Benutzung".
Beim Verbindungsverlust zum Relay geben beide Seiten ihre Geraete
automatisch wieder frei. Bleibt trotzdem eines haengen, loest ein Neustart
des Share-Clients die Bindung; die Kernel-Treiber werden dabei ueber
sysfs authorized neu gebunden.
Verbindung bricht regelmaessig ab. Client und Relay senden alle 20 Sekunden WebSocket-Pings und trennen nach 60 Sekunden Stille. Liegt ein Proxy dazwischen, der WebSockets frueher schliesst, muss dessen Timeout hoeher liegen als 60 Sekunden.
Web-UI
Die Web-UI ist unter http://localhost:8080 erreichbar und bietet:
- Geraete-Tab: Liste aller verfuegbaren/verbundenen USB-Geraete
- Einstellungen: Relay-Adresse, Modus, Name konfigurieren
- Token: Tokens generieren oder eintragen
- Service: Als Systemdienst installieren/deinstallieren
Konfiguration
Die Konfiguration wird in ~/.usb-server/config.json gespeichert:
{
"relay_addr": "ws://localhost:8443",
"hash": "a1b2c3d4...",
"mode": "use",
"name": "Mein-PC",
"web_port": 8080,
"token1": "...",
"token2": "...",
"token3": "...",
"auto_connect": [
{"vendor_id": "1234", "product_id": "5678"},
{"bus_id": "1-1.4"}
]
}
Als Service installieren
# Service installieren (braucht root)
sudo ./bin/usb-client install-service
# Service Status pruefen
systemctl status usb-client
# Service deinstallieren
sudo ./bin/usb-client uninstall-service
Sicherheit
- Tunnel-Verschluesselung: USB-Daten werden Ende-zu-Ende mit AES-256-GCM verschluesselt - sowohl ueber den Relay als auch bei Direktverbindung. Der Schluessel wird per HKDF aus den 3 Tokens abgeleitet, pro Tunnel neu. Jeder Frame ist authentifiziert; manipulierte oder wiederholte Frames beenden den Tunnel.
- Gruppierung: Die 3 Tokens sind das gemeinsame Geheimnis. Der Relay bekommt nur ihren SHA256-Hash und kann daraus die Tokens nicht zurueckrechnen - er kann also weder mitlesen noch sich als Peer ausgeben.
- Direktverbindungen werden mit einem aus den Tokens abgeleiteten Token authentifiziert, das an die Tunnel-ID gebunden ist. Wer nur den Port erreicht, kommt nicht hinein.
- Transport-Verschluesselung: Trotzdem
wss://verwenden, wenn der Relay ueber das Internet laeuft. Das schuetzt die Steuernachrichten und Metadaten, die nicht Teil der Tunnel-Verschluesselung sind.
Grenzen: Der Hash gruppiert, er authentifiziert nicht - wer ihn kennt, kann der Gruppe beitreten, Geraetelisten sehen und Geraete anfordern (ohne die Tokens aber keine Tunneldaten lesen). Der Relay sieht Metadaten: wer verbunden ist, welche Geraete angeboten und wann sie angefordert werden. Ein Client mit den Tokens hat vollen Zugriff auf alle freigegebenen Geraete der Gruppe; eine Rechteverwaltung pro Geraet oder Client gibt es nicht.
Fuer TLS am Relay-Server empfiehlt sich ein Reverse-Proxy (nginx/traefik) mit Let's Encrypt:
server {
listen 443 ssl;
server_name relay.example.com;
ssl_certificate /etc/letsencrypt/live/relay.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/relay.example.com/privkey.pem;
location /ws {
proxy_pass http://localhost:8443;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
Technische Details
- Sprache: Go (single binary, keine Runtime-Abhaengigkeiten)
- USB/IP: Natives USB/IP-Protokoll (kein externes
usbip-Paket noetig) - USB-Zugriff: Direkte usbdevfs-ioctls (kein CGO/libusb)
- VHCI: Direkte sysfs-Schnittstelle zum vhci-hcd Kernel-Modul
- Relay: WebSocket mit JSON-Kontrollnachrichten und Binary-Tunneldaten
- Tunnel: USB/IP-Protokolldaten werden durch WebSocket-Binary-Frames getunnelt
Verzeichnisstruktur
cmd/usb-relay/ - Relay-Server Binary
cmd/usb-client/ - Client Binary (share + use Modi)
internal/relay/ - WebSocket Relay & Hub
internal/protocol/ - Nachrichtentypen
internal/client/ - Client Core, Share & Use Manager
internal/usbip/ - USB/IP Protokoll, Server, VHCI
internal/usb/ - USB Enumeration & usbdevfs
internal/token/ - Token-Generierung
internal/config/ - Konfiguration
internal/web/ - Web-UI (embedded)
internal/service/ - Systemd Service
Lizenz
Proprietaere Software. Alle Rechte vorbehalten. Siehe LICENSE fuer Details.
Third-Party Lizenzen
Diese Software verwendet Open-Source-Bibliotheken. Siehe NOTICE fuer Details.