added windows support over usbip-win2
This commit is contained in:
parent
4d33063b82
commit
e2a97fa774
|
|
@ -149,11 +149,13 @@ choco install make
|
|||
| Funktion | Linux | Windows |
|
||||
|----------|-------|---------|
|
||||
| Share-Modus (USB-Geraete freigeben) | Ja | Nein (kein usbdevfs) |
|
||||
| Use-Modus (USB-Geraete empfangen) | Ja | Nein (kein VHCI-Treiber) |
|
||||
| Use-Modus (USB-Geraete empfangen) | Ja (vhci-hcd) | Ja (usbip-win2) |
|
||||
| Relay-Server | Ja | Ja |
|
||||
| Web-UI / Config | Ja | Ja |
|
||||
|
||||
**Windows-Einschraenkung:** Der Windows-Client kann zur Zeit nur als Relay-Server, fuer die Web-UI und zur Konfiguration genutzt werden. Share- und Use-Modus erfordern Linux-spezifische Kernel-Schnittstellen (usbdevfs bzw. vhci-hcd).
|
||||
**Windows Use-Modus:** Benoetigt den [usbip-win2](https://github.com/cezanne/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)
|
||||
|
||||
|
|
|
|||
BIN
bin/usb-client
BIN
bin/usb-client
Binary file not shown.
Binary file not shown.
BIN
bin/usb-relay
BIN
bin/usb-relay
Binary file not shown.
|
|
@ -3,16 +3,58 @@
|
|||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/duffy/usb-server/internal/protocol"
|
||||
"github.com/duffy/usb-server/internal/usbip"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// createVHCIAttachment creates a VHCI attachment on Linux using socketpair + sysfs.
|
||||
// Returns the tunnel connection (our end of the socketpair), the VHCI port number, and any error.
|
||||
func createVHCIAttachment(_ context.Context, granted *protocol.DeviceGranted, _ *RemoteDevice) (net.Conn, int, error) {
|
||||
// Create a socketpair - one end for VHCI, one for our tunnel
|
||||
fds, err := createSocketPair()
|
||||
if err != nil {
|
||||
return nil, -1, fmt.Errorf("creating socketpair: %w", err)
|
||||
}
|
||||
|
||||
vhciFD := fds[0]
|
||||
tunnelFD := fds[1]
|
||||
|
||||
// Find a free VHCI port
|
||||
port, err := usbip.FindFreePort(granted.Speed)
|
||||
if err != nil {
|
||||
closeFDs(fds)
|
||||
return nil, -1, fmt.Errorf("finding free VHCI port: %w", err)
|
||||
}
|
||||
|
||||
// Attach to VHCI
|
||||
if err := usbip.AttachDevice(port, vhciFD, granted.DevID, granted.Speed); err != nil {
|
||||
closeFDs(fds)
|
||||
return nil, -1, fmt.Errorf("VHCI attach: %w", err)
|
||||
}
|
||||
|
||||
// The VHCI driver now owns vhciFD, so we don't close it
|
||||
// Create a net.Conn from the tunnel FD
|
||||
tunnelFile := fdToFile(tunnelFD, "usb-tunnel")
|
||||
tunnelConn, err := net.FileConn(tunnelFile)
|
||||
tunnelFile.Close() // FileConn dups the fd
|
||||
if err != nil {
|
||||
usbip.DetachDevice(port)
|
||||
return nil, -1, fmt.Errorf("creating tunnel conn: %w", err)
|
||||
}
|
||||
|
||||
return tunnelConn, port, nil
|
||||
}
|
||||
|
||||
// createSocketPair creates a Unix domain socket pair
|
||||
func createSocketPair() ([2]int, error) {
|
||||
fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM, 0)
|
||||
|
|
|
|||
|
|
@ -3,12 +3,194 @@
|
|||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/duffy/usb-server/internal/protocol"
|
||||
"github.com/duffy/usb-server/internal/usbip"
|
||||
)
|
||||
|
||||
// createVHCIAttachment creates a VHCI attachment on Windows using usbip-win2.
|
||||
// It starts a local TCP proxy, launches usbip.exe to connect to it,
|
||||
// handles the USB/IP management phase (OP_REQ_IMPORT) locally,
|
||||
// and returns the TCP connection for the transfer phase bridge.
|
||||
func createVHCIAttachment(ctx context.Context, granted *protocol.DeviceGranted, devInfo *RemoteDevice) (net.Conn, int, error) {
|
||||
// Find usbip.exe
|
||||
usbipExe, err := usbip.FindUsbipExe()
|
||||
if err != nil {
|
||||
return nil, -1, err
|
||||
}
|
||||
|
||||
// Start TCP listener on localhost with random port
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
return nil, -1, fmt.Errorf("starting TCP listener: %w", err)
|
||||
}
|
||||
tcpPort := listener.Addr().(*net.TCPAddr).Port
|
||||
log.Printf("[vhci-win] TCP proxy listening on 127.0.0.1:%d", tcpPort)
|
||||
|
||||
// Channel for the accepted connection after management phase
|
||||
type acceptResult struct {
|
||||
conn net.Conn
|
||||
err error
|
||||
}
|
||||
resultCh := make(chan acceptResult, 1)
|
||||
|
||||
// Accept connection and handle management phase in goroutine
|
||||
go func() {
|
||||
conn, err := listener.Accept()
|
||||
listener.Close() // only accept one connection
|
||||
if err != nil {
|
||||
resultCh <- acceptResult{nil, fmt.Errorf("accepting connection: %w", err)}
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("[vhci-win] usbip.exe connected, handling management phase")
|
||||
|
||||
// Handle OP_REQ_IMPORT from usbip.exe
|
||||
if err := handleImportRequest(conn, granted, devInfo); err != nil {
|
||||
conn.Close()
|
||||
resultCh <- acceptResult{nil, fmt.Errorf("management phase: %w", err)}
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("[vhci-win] management phase complete, entering transfer phase")
|
||||
resultCh <- acceptResult{conn, nil}
|
||||
}()
|
||||
|
||||
// Launch usbip.exe attach
|
||||
cmd := exec.CommandContext(ctx, usbipExe,
|
||||
"--tcp-port", fmt.Sprintf("%d", tcpPort),
|
||||
"attach", "-r", "127.0.0.1", "-b", granted.BusID)
|
||||
output, err := cmd.CombinedOutput()
|
||||
outputStr := strings.TrimSpace(string(output))
|
||||
if err != nil {
|
||||
// Close listener to unblock Accept goroutine
|
||||
listener.Close()
|
||||
return nil, -1, fmt.Errorf("usbip.exe attach failed: %w (output: %s)", err, outputStr)
|
||||
}
|
||||
|
||||
log.Printf("[vhci-win] usbip.exe output: %s", outputStr)
|
||||
|
||||
// Parse VHCI port from usbip.exe output (e.g. "succesfully attached to port 0")
|
||||
vhciPort := parsePortFromOutput(outputStr)
|
||||
|
||||
// Wait for management phase to complete
|
||||
result := <-resultCh
|
||||
if result.err != nil {
|
||||
return nil, -1, result.err
|
||||
}
|
||||
|
||||
log.Printf("[vhci-win] device attached on VHCI port %d", vhciPort)
|
||||
return result.conn, vhciPort, nil
|
||||
}
|
||||
|
||||
// handleImportRequest reads OP_REQ_IMPORT from the usbip.exe client
|
||||
// and responds with OP_REP_IMPORT containing the device descriptor.
|
||||
func handleImportRequest(conn net.Conn, granted *protocol.DeviceGranted, devInfo *RemoteDevice) error {
|
||||
// Read the OpHeader (8 bytes)
|
||||
hdr, err := usbip.ReadOpHeader(conn)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading op header: %w", err)
|
||||
}
|
||||
|
||||
if hdr.Command != usbip.OpReqImport {
|
||||
return fmt.Errorf("unexpected command: 0x%04x (expected OP_REQ_IMPORT 0x%04x)", hdr.Command, usbip.OpReqImport)
|
||||
}
|
||||
|
||||
// Read the 32-byte bus ID
|
||||
var busIDBuf [32]byte
|
||||
if _, err := io.ReadFull(conn, busIDBuf[:]); err != nil {
|
||||
return fmt.Errorf("reading bus ID: %w", err)
|
||||
}
|
||||
|
||||
requestedBusID := usbip.GetBusID(busIDBuf)
|
||||
log.Printf("[vhci-win] OP_REQ_IMPORT for bus ID: %s", requestedBusID)
|
||||
|
||||
// Build device descriptor from available info
|
||||
desc := buildDeviceDescriptor(granted, devInfo)
|
||||
|
||||
// Build and send OP_REP_IMPORT reply
|
||||
reply, err := usbip.BuildImportReply(0, &desc)
|
||||
if err != nil {
|
||||
return fmt.Errorf("building import reply: %w", err)
|
||||
}
|
||||
|
||||
if _, err := conn.Write(reply); err != nil {
|
||||
return fmt.Errorf("writing import reply: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// buildDeviceDescriptor creates a USB/IP DeviceDescriptor from the
|
||||
// information available in the DeviceGranted message and RemoteDevice.
|
||||
func buildDeviceDescriptor(granted *protocol.DeviceGranted, devInfo *RemoteDevice) usbip.DeviceDescriptor {
|
||||
var desc usbip.DeviceDescriptor
|
||||
|
||||
usbip.SetBusID(&desc.BusID, granted.BusID)
|
||||
usbip.SetPath(&desc.Path, "/sys/bus/usb/"+granted.BusID)
|
||||
|
||||
desc.Speed = granted.Speed
|
||||
desc.BusNum = granted.DevID >> 16
|
||||
desc.DevNum = granted.DevID & 0xFFFF
|
||||
|
||||
// Fill from RemoteDevice if available
|
||||
if devInfo != nil {
|
||||
desc.BusNum = devInfo.BusNum
|
||||
desc.DevNum = devInfo.DevNum
|
||||
|
||||
// Parse hex VendorID/ProductID
|
||||
if vid, err := strconv.ParseUint(devInfo.VendorID, 16, 16); err == nil {
|
||||
desc.IDVendor = uint16(vid)
|
||||
}
|
||||
if pid, err := strconv.ParseUint(devInfo.ProductID, 16, 16); err == nil {
|
||||
desc.IDProduct = uint16(pid)
|
||||
}
|
||||
|
||||
desc.BDeviceClass = devInfo.Class
|
||||
desc.BDeviceSubClass = devInfo.SubClass
|
||||
desc.BDeviceProtocol = devInfo.Protocol
|
||||
desc.BNumInterfaces = devInfo.NumInterfaces
|
||||
}
|
||||
|
||||
// Defaults for fields not available in the protocol
|
||||
desc.BcdDevice = 0x0100
|
||||
desc.BConfigurationValue = 1
|
||||
desc.BNumConfigurations = 1
|
||||
|
||||
if desc.BNumInterfaces == 0 {
|
||||
desc.BNumInterfaces = 1
|
||||
}
|
||||
|
||||
return desc
|
||||
}
|
||||
|
||||
// parsePortFromOutput extracts the VHCI port number from usbip.exe output.
|
||||
// Returns -1 if the port cannot be parsed.
|
||||
func parsePortFromOutput(output string) int {
|
||||
// Match patterns like "port 0", "port 1", etc.
|
||||
re := regexp.MustCompile(`(?i)port\s+(\d+)`)
|
||||
matches := re.FindStringSubmatch(output)
|
||||
if len(matches) >= 2 {
|
||||
if port, err := strconv.Atoi(matches[1]); err == nil {
|
||||
return port
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// createSocketPair is not used on Windows but required for compilation.
|
||||
func createSocketPair() ([2]int, error) {
|
||||
return [2]int{}, fmt.Errorf("socketpair not implemented on Windows")
|
||||
return [2]int{}, fmt.Errorf("socketpair not available on Windows")
|
||||
}
|
||||
|
||||
func closeFDs(fds [2]int) {}
|
||||
|
|
@ -17,6 +199,5 @@ func fdToFile(fd int, name string) *os.File {
|
|||
return nil
|
||||
}
|
||||
|
||||
func fixVHCIDevicePermissions(port int) {
|
||||
// Not applicable on Windows
|
||||
}
|
||||
// fixVHCIDevicePermissions is not needed on Windows.
|
||||
func fixVHCIDevicePermissions(port int) {}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ type AttachedDevice struct {
|
|||
RemoteDevice
|
||||
TunnelID string `json:"tunnel_id"`
|
||||
VHCIPort int `json:"vhci_port"`
|
||||
SocketFD int `json:"socket_fd"`
|
||||
}
|
||||
|
||||
// UseManager handles receiving/using remote USB devices
|
||||
|
|
@ -184,36 +183,22 @@ func (um *UseManager) DetachDevice(clientID, busID string) error {
|
|||
}
|
||||
|
||||
func (um *UseManager) setupVHCI(clientID, busID string, granted *protocol.DeviceGranted) error {
|
||||
// Create a socketpair - one end for VHCI, one for our tunnel
|
||||
fds, err := createSocketPair()
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating socketpair: %w", err)
|
||||
// Look up device info from available list (needed for Windows management phase)
|
||||
var devInfo *RemoteDevice
|
||||
um.mu.RLock()
|
||||
for _, d := range um.available[clientID] {
|
||||
if d.BusID == busID {
|
||||
cp := d
|
||||
devInfo = &cp
|
||||
break
|
||||
}
|
||||
}
|
||||
um.mu.RUnlock()
|
||||
|
||||
vhciFD := fds[0]
|
||||
tunnelFD := fds[1]
|
||||
|
||||
// Find a free VHCI port
|
||||
port, err := usbip.FindFreePort(granted.Speed)
|
||||
// Platform-specific VHCI attachment (Linux: socketpair+sysfs, Windows: TCP proxy+usbip.exe)
|
||||
tunnelConn, vhciPort, err := createVHCIAttachment(um.client.ctx, granted, devInfo)
|
||||
if err != nil {
|
||||
closeFDs(fds)
|
||||
return fmt.Errorf("finding free VHCI port: %w", err)
|
||||
}
|
||||
|
||||
// Attach to VHCI
|
||||
if err := usbip.AttachDevice(port, vhciFD, granted.DevID, granted.Speed); err != nil {
|
||||
closeFDs(fds)
|
||||
return fmt.Errorf("VHCI attach: %w", err)
|
||||
}
|
||||
|
||||
// The VHCI driver now owns vhciFD, so we don't close it
|
||||
// Create a net.Conn from the tunnel FD
|
||||
tunnelFile := fdToFile(tunnelFD, "usb-tunnel")
|
||||
tunnelConn, err := net.FileConn(tunnelFile)
|
||||
tunnelFile.Close() // FileConn dups the fd
|
||||
if err != nil {
|
||||
usbip.DetachDevice(port)
|
||||
return fmt.Errorf("creating tunnel conn: %w", err)
|
||||
return fmt.Errorf("VHCI attachment: %w", err)
|
||||
}
|
||||
|
||||
tunnel := &useTunnel{
|
||||
|
|
@ -233,19 +218,18 @@ func (um *UseManager) setupVHCI(clientID, busID string, granted *protocol.Device
|
|||
ClientID: clientID,
|
||||
},
|
||||
TunnelID: granted.TunnelID,
|
||||
VHCIPort: port,
|
||||
SocketFD: vhciFD,
|
||||
VHCIPort: vhciPort,
|
||||
}
|
||||
um.mu.Unlock()
|
||||
|
||||
// Start reading from the tunnel socket (VHCI -> relay)
|
||||
go um.tunnelReadLoop(tunnel)
|
||||
|
||||
log.Printf("[use] device %s attached on VHCI port %d", key, port)
|
||||
log.Printf("[use] device %s attached on VHCI port %d", key, vhciPort)
|
||||
|
||||
// Fix permissions on newly created device nodes (e.g. /dev/video*)
|
||||
// VHCI-created devices don't get normal udev permissions
|
||||
go fixVHCIDevicePermissions(port)
|
||||
go fixVHCIDevicePermissions(vhciPort)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,25 +2,78 @@
|
|||
|
||||
package usbip
|
||||
|
||||
import "fmt"
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// FindUsbipExe locates the usbip.exe binary from usbip-win2.
|
||||
// Checks PATH first, then the standard installation directory.
|
||||
func FindUsbipExe() (string, error) {
|
||||
// Check PATH
|
||||
if path, err := exec.LookPath("usbip"); err == nil {
|
||||
return path, nil
|
||||
}
|
||||
|
||||
// Check standard installation path
|
||||
standardPath := filepath.Join(os.Getenv("ProgramFiles"), "USBip", "usbip.exe")
|
||||
if _, err := os.Stat(standardPath); err == nil {
|
||||
return standardPath, nil
|
||||
}
|
||||
|
||||
// Check x86 program files too
|
||||
x86Path := filepath.Join(os.Getenv("ProgramFiles(x86)"), "USBip", "usbip.exe")
|
||||
if _, err := os.Stat(x86Path); err == nil {
|
||||
return x86Path, nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("usbip.exe nicht gefunden. Bitte usbip-win2 installieren: https://github.com/cezanne/usbip-win2/releases")
|
||||
}
|
||||
|
||||
// IsVHCIAvailable checks if usbip-win2 is installed
|
||||
func IsVHCIAvailable() bool {
|
||||
return false
|
||||
_, err := FindUsbipExe()
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// VHCIUnavailableError returns an error describing why VHCI is not available.
|
||||
// VHCIUnavailableError returns an error describing why VHCI is not available,
|
||||
// or nil if usbip-win2 is installed and ready.
|
||||
func VHCIUnavailableError() error {
|
||||
return fmt.Errorf("VHCI wird unter Windows nicht unterstuetzt. Der Use-Modus (USB-Geraete empfangen) ist nur unter Linux verfuegbar")
|
||||
}
|
||||
|
||||
func FindFreePort(speed uint32) (int, error) {
|
||||
return -1, fmt.Errorf("VHCI not supported on Windows")
|
||||
}
|
||||
|
||||
func AttachDevice(port int, sockfd int, devID uint32, speed uint32) error {
|
||||
return fmt.Errorf("VHCI not supported on Windows")
|
||||
if IsVHCIAvailable() {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("usbip-win2 nicht installiert. Der Use-Modus benoetigt den usbip-win2 VHCI-Treiber. Download: https://github.com/cezanne/usbip-win2/releases")
|
||||
}
|
||||
|
||||
// DetachDevice detaches a device from the VHCI driver using usbip.exe
|
||||
func DetachDevice(port int) error {
|
||||
return fmt.Errorf("VHCI not supported on Windows")
|
||||
usbipExe, err := FindUsbipExe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd := exec.Command(usbipExe, "detach", "-p", fmt.Sprintf("%d", port))
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("usbip detach failed: %w (output: %s)", err, strings.TrimSpace(string(output)))
|
||||
}
|
||||
|
||||
log.Printf("[vhci-win] detached port %d", port)
|
||||
return nil
|
||||
}
|
||||
|
||||
// FindFreePort is not used on Windows (usbip.exe selects the port automatically).
|
||||
// Signature required for cross-platform compilation.
|
||||
func FindFreePort(speed uint32) (int, error) {
|
||||
return -1, fmt.Errorf("not used on Windows - usbip.exe selects port automatically")
|
||||
}
|
||||
|
||||
// AttachDevice is not used on Windows (usbip.exe handles attachment).
|
||||
// Signature required for cross-platform compilation.
|
||||
func AttachDevice(port int, sockfd int, devID uint32, speed uint32) error {
|
||||
return fmt.Errorf("not used on Windows - use createVHCIAttachment instead")
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue