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
2026-02-18 22:01:54 +01:00
2026-02-18 22:01:54 +01:00
2026-02-18 22:01:54 +01:00

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

  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:
    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)

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

  1. MSYS2 von https://www.msys2.org herunterladen und installieren
  2. In der MSYS2-Shell:
    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.

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_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

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.

S
Description
No description provided
Readme
30 MiB
Languages
Go 86%
JavaScript 7.9%
CSS 2.6%
HTML 2.6%
Shell 0.4%
Other 0.5%