Files
usb-server/README.md
T
duffyduckandClaude Opus 5 9ed473a965 Fix HID transfers, harden the tunnel, add E2E crypto and direct peers
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>
2026-08-11 22:02:04 +02:00

523 lines
18 KiB
Markdown

# 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
```bash
# Mit Docker
docker compose up -d
# Oder direkt
make relay
./bin/usb-relay --port 8443
```
### 2. Tokens generieren
Auf dem ersten Client:
```bash
./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:
```bash
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:
```bash
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)
```bash
# 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
1. Installer herunterladen: https://go.dev/dl/ (Datei `go1.25.0.windows-amd64.msi`)
2. MSI-Installer ausfuehren - installiert Go nach `C:\Program Files\Go`
3. Der Installer richtet den PATH automatisch ein
4. Neue Eingabeaufforderung oeffnen und pruefen:
```cmd
go version
```
Falls der PATH nicht automatisch gesetzt wurde:
1. Windows-Taste > "Umgebungsvariablen" suchen > "Umgebungsvariablen fuer dieses Konto bearbeiten"
2. Variable `Path` bearbeiten > Neu > `C:\Program Files\Go\bin` hinzufuegen
3. Eingabeaufforderung neu oeffnen
### Make installieren
#### Linux (Debian/Ubuntu)
```bash
sudo apt update
sudo apt install make
```
#### Linux (Fedora/RHEL)
```bash
sudo dnf install make
```
#### Windows
**Option A: Chocolatey** (empfohlen)
```powershell
# 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**
1. MSYS2 von https://www.msys2.org herunterladen und installieren
2. In der MSYS2-Shell:
```bash
pacman -S make
```
3. `C:\msys64\usr\bin` zum 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](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](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](https://github.com/vadimgrn/usbip-win2/releases) 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`:
```bash
sudo modprobe vhci-hcd
```
- Fuer automatisches Laden bei Boot:
```bash
echo "vhci-hcd" | sudo tee /etc/modules-load.d/vhci-hcd.conf
```
### Bauen
#### Mit Make
```bash
# 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)
```bash
# 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):
```cmd
set CGO_ENABLED=0
go build -ldflags="-s -w" -o bin\usb-client.exe ./cmd/usb-client/
```
Unter Windows (PowerShell):
```powershell
$env:CGO_ENABLED=0
go build -ldflags="-s -w" -o bin\usb-client.exe ./cmd/usb-client/
```
### Docker (Relay-Server)
```bash
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_port` in der Config setzen und diesen Port
in Firewall/Router weiterleiten. Dann finden auch Peers hinter NAT hierher.
- **Gar keine Direktverbindung gewuenscht:** `disable_direct: true` setzt 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
```bash
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:
```bash
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:
```json
{
"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
```bash
# 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:
```nginx
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](LICENSE) fuer Details.
### Third-Party Lizenzen
Diese Software verwendet Open-Source-Bibliotheken. Siehe [NOTICE](NOTICE) fuer Details.