//go:build windows package diag import ( "fmt" "os/exec" "strings" "github.com/duffy/usb-server/internal/usb" "golang.org/x/sys/windows" "golang.org/x/sys/windows/registry" ) const usbShareServiceName = "usbshare" func isPrivileged() bool { // An elevated process has the administrators group enabled in its token. var sid *windows.SID err := windows.AllocateAndInitializeSid( &windows.SECURITY_NT_AUTHORITY, 2, windows.SECURITY_BUILTIN_DOMAIN_RID, windows.DOMAIN_ALIAS_RID_ADMINS, 0, 0, 0, 0, 0, 0, &sid, ) if err != nil { return false } defer windows.FreeSid(sid) token := windows.Token(0) // the process token member, err := token.IsMember(sid) return err == nil && member } func collectPlatform(r *Report) { r.System.OSVersion = windowsVersion() checkTestSigning(r) checkDriverService(r) checkDriverInterface(r) collectWindowsDevices(r) assessWindowsCapabilities(r) } // windowsVersion reads the build information from the registry, which does // not lie about the version the way GetVersionEx does for unmanifested // processes. func windowsVersion() string { key, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion`, registry.QUERY_VALUE) if err != nil { return "" } defer key.Close() productName, _, _ := key.GetStringValue("ProductName") displayVersion, _, _ := key.GetStringValue("DisplayVersion") build, _, _ := key.GetStringValue("CurrentBuildNumber") ubr, _, _ := key.GetIntegerValue("UBR") parts := []string{productName} if displayVersion != "" { parts = append(parts, displayVersion) } if build != "" { if ubr > 0 { parts = append(parts, fmt.Sprintf("build %s.%d", build, ubr)) } else { parts = append(parts, "build "+build) } } return strings.Join(parts, " ") } // checkTestSigning reports whether unsigned drivers may load. // // This is the single most common reason a freshly built driver does nothing: // it is installed, the INF looks fine, and Windows silently refuses to load // it because it is not signed by Microsoft. func checkTestSigning(r *Report) { out, err := exec.Command("bcdedit", "/enum", "{current}").Output() if err != nil { r.addCheck("test signing", false, "could not read the boot configuration: "+err.Error(), "run this from an elevated command prompt") return } text := strings.ToLower(string(out)) testSigning := strings.Contains(text, "testsigning") && strings.Contains(text, "yes") if testSigning { r.addCheck("test signing", true, "enabled — unsigned drivers may load", "") } else { r.addCheck("test signing", false, "disabled — Windows will refuse to load an unsigned driver, usually without any visible error", "bcdedit /set testsigning on (then reboot; only do this on a test machine)") } } // checkDriverService reports whether the filter driver is registered and // running. func checkDriverService(r *Report) { manager, err := windows.OpenSCManager(nil, nil, windows.SC_MANAGER_CONNECT) if err != nil { r.addCheck("usbshare driver service", false, "could not open the service manager: "+err.Error(), "") return } defer windows.CloseServiceHandle(manager) namePtr, _ := windows.UTF16PtrFromString(usbShareServiceName) service, err := windows.OpenService(manager, namePtr, windows.SERVICE_QUERY_STATUS) if err != nil { r.addCheck("usbshare driver service", false, "not registered — the driver has not been installed", "right-click driver/windows/usbshare.inf and choose Install, then attach it "+ "to a device in Device Manager") return } defer windows.CloseServiceHandle(service) var status windows.SERVICE_STATUS if err := windows.QueryServiceStatus(service, &status); err != nil { r.addCheck("usbshare driver service", false, "registered, but its status could not be read: "+err.Error(), "") return } switch status.CurrentState { case windows.SERVICE_RUNNING: r.addCheck("usbshare driver service", true, "registered and running", "") case windows.SERVICE_STOPPED: // A filter driver only starts when it is attached to a device, so // stopped is expected until then rather than an error in itself. r.addCheck("usbshare driver service", false, "registered but not running — normal until the filter is attached to a device", "attach the filter to a device in Device Manager, then replug it") default: r.addCheck("usbshare driver service", false, fmt.Sprintf("registered, service state %d", status.CurrentState), "") } } // checkDriverInterface reports whether any device exposes the filter's // interface, which is what user mode actually needs. func checkDriverInterface(r *Report) { devices, err := usb.Enumerate() if err != nil { r.addCheck("usbshare device interface", false, "no device exposes the interface: "+err.Error(), "the driver must be attached to a specific device, not just installed") return } if len(devices) == 0 { r.addCheck("usbshare device interface", false, "the driver is present but no device is attached to it", "in Device Manager, update the driver for the device you want to share") return } r.addCheck("usbshare device interface", true, fmt.Sprintf("%d device(s) reachable through the filter", len(devices)), "") } func collectWindowsDevices(r *Report) { devices, err := usb.Enumerate() if err != nil { r.note("device enumeration failed: %v", err) collectWindowsDevicesFallback(r) return } for _, dev := range devices { info := DeviceInfo{ BusID: dev.BusID, VendorID: fmt.Sprintf("%04x", dev.VendorID), ProductID: fmt.Sprintf("%04x", dev.ProductID), Name: dev.DisplayName(), Class: fmt.Sprintf("%02x", dev.DeviceClass), Speed: speedName(dev.Speed), Shareable: true, } for _, ep := range dev.Endpoints { info.Endpoints = append(info.Endpoints, endpointInfo(ep)) } r.Devices = append(r.Devices, info) } // Everything the filter cannot see is still worth listing: it explains // why an expected device is absent. collectWindowsDevicesFallback(r) } // collectWindowsDevicesFallback lists all USB devices via PowerShell, whether // or not the filter is attached. // // Shelling out is deliberate: reproducing this through SetupAPI would be a // few hundred lines of syscall code for something that only ever runs when a // human is already reading the output. func collectWindowsDevicesFallback(r *Report) { cmd := exec.Command("powershell", "-NoProfile", "-Command", `Get-PnpDevice -Class USB -ErrorAction SilentlyContinue | `+ `Select-Object -Property InstanceId,FriendlyName,Status,Service | `+ `ForEach-Object { "$($_.InstanceId)|$($_.FriendlyName)|$($_.Status)|$($_.Service)" }`) out, err := cmd.Output() if err != nil { r.note("could not list USB devices via PowerShell: %v", err) return } seen := make(map[string]bool) for _, d := range r.Devices { seen[strings.ToLower(d.VendorID+":"+d.ProductID)] = true } for _, line := range strings.Split(string(out), "\n") { line = strings.TrimSpace(line) if line == "" { continue } parts := strings.Split(line, "|") if len(parts) < 4 { continue } instanceID, friendly, status, service := parts[0], parts[1], parts[2], parts[3] vid, pid := parseVidPid(instanceID) if vid == "" { continue } if seen[strings.ToLower(vid+":"+pid)] { continue // already listed through the filter } blocker := "usbshare filter not attached" if !strings.EqualFold(status, "OK") { blocker = "device status: " + status } r.Devices = append(r.Devices, DeviceInfo{ VendorID: vid, ProductID: pid, Name: friendly, Driver: service, Shareable: false, Blocker: blocker, }) } } // parseVidPid pulls the IDs out of an instance ID such as // USB\VID_046D&PID_C52B\5&1a2b3c4d&0&2. func parseVidPid(instanceID string) (vid, pid string) { upper := strings.ToUpper(instanceID) if i := strings.Index(upper, "VID_"); i >= 0 && len(upper) >= i+8 { vid = strings.ToLower(upper[i+4 : i+8]) } if i := strings.Index(upper, "PID_"); i >= 0 && len(upper) >= i+8 { pid = strings.ToLower(upper[i+4 : i+8]) } return vid, pid } func assessWindowsCapabilities(r *Report) { shareable := 0 for _, d := range r.Devices { if d.Shareable { shareable++ } } if shareable > 0 { r.Sharing = Capability{ Available: true, Mechanism: "usbshare filter driver", } } else { r.Sharing = Capability{ Available: false, Reason: "no device is attached to the usbshare filter driver", Mechanism: "usbshare filter driver", } } // The use side needs usbip-win2's VHCI driver, which is a separate // product with its own installer. if _, err := exec.LookPath("usbip"); err == nil { r.Using = Capability{Available: true, Mechanism: "usbip-win2 VHCI"} } else { r.Using = Capability{ Available: false, Reason: "usbip.exe not found", Mechanism: "usbip-win2 VHCI", } r.addCheck("usbip-win2", false, "not installed — receiving remote devices needs its VHCI driver", "install from https://github.com/vadimgrn/usbip-win2/releases") } }