package kvm import ( "bytes" "errors" "fmt" "log" "os" "os/exec" "path" "path/filepath" "strings" "sync" "time" gadget "github.com/openstadia/go-usb-gadget" ) const configFSPath = "/sys/kernel/config" const gadgetPath = "/sys/kernel/config/usb_gadget" const kvmGadgetPath = "/sys/kernel/config/usb_gadget/jetkvm" const configC1Path = "/sys/kernel/config/usb_gadget/jetkvm/configs/c.1" func (u *UsbDevicesConfig) isGadgetConfigItemEnabled(itemKey string) bool { switch itemKey { case "absolute_mouse": return u.AbsoluteMouse case "relative_mouse": return u.RelativeMouse case "keyboard": return u.Keyboard case "mass_storage_base": return u.MassStorage case "mass_storage_usb0": return u.MassStorage default: return true } } type gadgetConfigItem struct { device string path []string attrs gadgetAttributes configAttrs gadgetAttributes configPath string reportDesc []byte } type gadgetAttributes map[string]string var gadgetConfig = map[string]gadgetConfigItem{ "base": { attrs: gadgetAttributes{ "bcdUSB": "0x0200", // USB 2.0 "idVendor": "0x1d6b", // The Linux Foundation "idProduct": "0104", // Multifunction Composite Gadget¬ "bcdDevice": "0100", }, configAttrs: gadgetAttributes{ "MaxPower": "250", // in unit of 2mA }, }, "base_info": { path: []string{"strings", "0x409"}, attrs: gadgetAttributes{ "serialnumber": GetDeviceID(), "manufacturer": "JetKVM", "product": "JetKVM USB Emulation Device", }, configAttrs: gadgetAttributes{ "configuration": "Config 1: HID", }, }, // keyboard HID "keyboard": { device: "hid.usb0", path: []string{"functions", "hid.usb0"}, configPath: path.Join(configC1Path, "hid.usb0"), attrs: gadgetAttributes{ "protocol": "1", "subclass": "1", "report_length": "8", }, reportDesc: KeyboardReportDesc, }, // mouse HID "absolute_mouse": { device: "hid.usb1", path: []string{"functions", "hid.usb1"}, configPath: path.Join(configC1Path, "hid.usb1"), attrs: gadgetAttributes{ "protocol": "2", "subclass": "1", "report_length": "6", }, reportDesc: CombinedAbsoluteMouseReportDesc, }, // relative mouse HID "relative_mouse": { device: "hid.usb2", path: []string{"functions", "hid.usb2"}, configPath: path.Join(configC1Path, "hid.usb2"), attrs: gadgetAttributes{ "protocol": "2", "subclass": "1", "report_length": "4", }, reportDesc: CombinedRelativeMouseReportDesc, }, // mass storage "mass_storage_base": { device: "mass_storage.usb0", path: []string{"functions", "mass_storage.usb0"}, configPath: path.Join(configC1Path, "mass_storage.usb0"), attrs: gadgetAttributes{ "stall": "1", }, }, "mass_storage_usb0": { path: []string{"functions", "mass_storage.usb0", "lun.0"}, attrs: gadgetAttributes{ "cdrom": "1", "ro": "1", "removable": "1", "file": "\n", "inquiry_string": "JetKVM Virtual Media", }, }, } func mountConfigFS() error { _, err := os.Stat(gadgetPath) if os.IsNotExist(err) { err = exec.Command("mount", "-t", "configfs", "none", configFSPath).Run() if err != nil { return fmt.Errorf("failed to mount configfs: %w", err) } } else { return fmt.Errorf("unable to access usb gadget path: %w", err) } return nil } func writeIfDifferent(filePath string, content []byte, permMode os.FileMode) error { if _, err := os.Stat(filePath); err == nil { oldContent, err := os.ReadFile(filePath) if err == nil { if bytes.Equal(oldContent, content) { logger.Tracef("skipping writing to %s as it already has the correct content", filePath) return nil } if len(oldContent) == len(content)+1 && bytes.Equal(oldContent[:len(content)], content) && oldContent[len(content)] == 10 { logger.Tracef("skipping writing to %s as it already has the correct content", filePath) return nil } logger.Tracef("writing to %s as it has different content %v %v", filePath, oldContent, content) } } return os.WriteFile(filePath, content, permMode) } func disableGadgetItemConfig(item gadgetConfigItem) error { // remove symlink if exists if item.configPath != "" { if _, err := os.Lstat(item.configPath); os.IsNotExist(err) { logger.Tracef("symlink %s does not exist", item.configPath) } else { err := os.Remove(item.configPath) if err != nil { return fmt.Errorf("failed to remove symlink %s: %w", item.configPath, err) } } } return nil } func writeGadgetItemConfig(item gadgetConfigItem) error { // create directory for the item gadgetItemPathArr := append([]string{kvmGadgetPath}, item.path...) gadgetItemPath := filepath.Join(gadgetItemPathArr...) err := os.MkdirAll(gadgetItemPath, 0755) if err != nil { return fmt.Errorf("failed to create path %s: %w", gadgetItemPath, err) } if len(item.configAttrs) > 0 { configItemPathArr := append([]string{configC1Path}, item.path...) configItemPath := filepath.Join(configItemPathArr...) err = os.MkdirAll(configItemPath, 0755) if err != nil { return fmt.Errorf("failed to create path %s: %w", config, err) } err = writeGadgetAttrs(configItemPath, item.configAttrs) if err != nil { return fmt.Errorf("failed to write config attributes for %s: %w", configItemPath, err) } } if len(item.attrs) > 0 { // write attributes for the item err = writeGadgetAttrs(gadgetItemPath, item.attrs) if err != nil { return fmt.Errorf("failed to write attributes for %s: %w", gadgetItemPath, err) } } // write report descriptor if available if item.reportDesc != nil { err = writeIfDifferent(path.Join(gadgetItemPath, "report_desc"), item.reportDesc, 0644) if err != nil { return err } } // create symlink if configPath is set if item.configPath != "" { logger.Tracef("Creating symlink from %s to %s", item.configPath, gadgetItemPath) // check if the symlink already exists, if yes, check if it points to the correct path if _, err := os.Lstat(item.configPath); err == nil { linkPath, err := os.Readlink(item.configPath) if err != nil || linkPath != gadgetItemPath { err = os.Remove(item.configPath) if err != nil { return fmt.Errorf("failed to remove existing symlink %s: %w", item.configPath, err) } } } else if !os.IsNotExist(err) { return fmt.Errorf("failed to check if symlink exists: %w", err) } err = os.Symlink(gadgetItemPath, item.configPath) if err != nil { return fmt.Errorf("failed to create symlink from %s to %s: %w", item.configPath, gadgetItemPath, err) } } return nil } func init() { ensureConfigLoaded() _ = os.MkdirAll(imagesFolder, 0755) udcs := gadget.GetUdcs() if len(udcs) < 1 { usbLogger.Error("no udc found, skipping USB stack init") return } udc = udcs[0] _, err := os.Stat(kvmGadgetPath) if err == nil { logger.Info("usb gadget already exists, skipping usb gadget initialization") return } err = mountConfigFS() if err != nil { logger.Errorf("failed to mount configfs: %v, usb stack might not function properly", err) } err = writeGadgetConfig() if err != nil { logger.Errorf("failed to start gadget: %v", err) } //TODO: read hid reports(capslock, numlock, etc) from keyboardHidFile } func UpdateGadgetConfig() error { LoadConfig() gadgetAttrs := [][]string{ {"idVendor", config.UsbConfig.VendorId}, {"idProduct", config.UsbConfig.ProductId}, } err := writeGadgetAttrs(kvmGadgetPath, gadgetAttrs) if err != nil { return err } log.Printf("Successfully updated usb gadget attributes: %v", gadgetAttrs) strAttrs := [][]string{ {"serialnumber", config.UsbConfig.SerialNumber}, {"manufacturer", config.UsbConfig.Manufacturer}, {"product", config.UsbConfig.Product}, } gadgetStringsPath := filepath.Join(kvmGadgetPath, "strings", "0x409") err = os.MkdirAll(gadgetStringsPath, 0755) if err != nil { return err } err = writeGadgetAttrs(gadgetStringsPath, strAttrs) if err != nil { return err } log.Printf("Successfully updated usb string attributes: %s", strAttrs) err = rebindUsb() if err != nil { return err } return nil } func writeGadgetAttrs(basePath string, attrs [][]string) error { for _, item := range attrs { filePath := filepath.Join(basePath, item[0]) err := os.WriteFile(filePath, []byte(item[1]), 0644) if err != nil { return fmt.Errorf("failed to write to %s: %w", filePath, err) } } return nil } func writeGadgetConfig() error { if _, err := os.Stat(gadgetPath); os.IsNotExist(err) { return fmt.Errorf("USB gadget path does not exist: %s", gadgetPath) } err := os.MkdirAll(kvmGadgetPath, 0755) if err != nil { return err } logger.Tracef("writing gadget config") for key, item := range gadgetConfig { // check if the item is enabled in the config if !config.UsbDevices.isGadgetConfigItemEnabled(key) { logger.Tracef("disabling gadget config: %s", key) err = disableGadgetItemConfig(item) if err != nil { return err } continue } logger.Tracef("writing gadget config: %s", key) err = writeGadgetItemConfig(item) if err != nil { return err } } err = os.WriteFile(path.Join(kvmGadgetPath, "UDC"), []byte(udc), 0644) if err != nil { return err } return nil } func rebindUsb() error { err := os.WriteFile("/sys/bus/platform/drivers/dwc3/unbind", []byte(udc), 0644) if err != nil { return err } err = os.WriteFile("/sys/bus/platform/drivers/dwc3/bind", []byte(udc), 0644) if err != nil { return err } return nil } var keyboardHidFile *os.File var keyboardLock = sync.Mutex{} var mouseHidFile *os.File var mouseLock = sync.Mutex{} func rpcKeyboardReport(modifier uint8, keys []uint8) error { keyboardLock.Lock() defer keyboardLock.Unlock() if keyboardHidFile == nil { var err error keyboardHidFile, err = os.OpenFile("/dev/hidg0", os.O_RDWR, 0666) if err != nil { return fmt.Errorf("failed to open hidg0: %w", err) } } if len(keys) > 6 { keys = keys[:6] } if len(keys) < 6 { keys = append(keys, make([]uint8, 6-len(keys))...) } _, err := keyboardHidFile.Write([]byte{modifier, 0, keys[0], keys[1], keys[2], keys[3], keys[4], keys[5]}) if err != nil { keyboardHidFile.Close() keyboardHidFile = nil return err } resetUserInputTime() return err } func rpcAbsMouseReport(x, y int, buttons uint8) error { mouseLock.Lock() defer mouseLock.Unlock() if mouseHidFile == nil { var err error mouseHidFile, err = os.OpenFile("/dev/hidg1", os.O_RDWR, 0666) if err != nil { return fmt.Errorf("failed to open hidg1: %w", err) } } resetUserInputTime() _, err := mouseHidFile.Write([]byte{ 1, // Report ID 1 buttons, // Buttons uint8(x), // X Low Byte uint8(x >> 8), // X High Byte uint8(y), // Y Low Byte uint8(y >> 8), // Y High Byte }) if err != nil { mouseHidFile.Close() mouseHidFile = nil return err } return nil } var accumulatedWheelY float64 = 0 func rpcWheelReport(wheelY int8) error { if mouseHidFile == nil { return errors.New("hid not initialized") } // Accumulate the wheelY value with finer granularity // Reduce divisor from 8.0 to a smaller value (e.g., 2.0 or 4.0) accumulatedWheelY += float64(wheelY) / 4.0 // Lower the threshold for sending a report (0.25 instead of 1.0) if abs(accumulatedWheelY) >= 0.25 { // Scale the wheel value appropriately for the HID report // The descriptor uses an 8-bit signed value (-127 to 127) scaledWheelY := int8(accumulatedWheelY * 0.5) // Scale down to prevent too much scrolling _, err := mouseHidFile.Write([]byte{ 2, // Report ID 2 byte(scaledWheelY), // Scaled Wheel Y (signed) }) // Reset the accumulator, keeping any remainder accumulatedWheelY -= float64(scaledWheelY) / 0.5 // Adjust based on the scaling factor resetUserInputTime() return err } return nil } // Helper function to get absolute value of float64 func abs(x float64) float64 { if x < 0 { return -x } return x } var usbState = "unknown" func rpcGetUSBState() (state string) { stateBytes, err := os.ReadFile("/sys/class/udc/ffb00000.usb/state") if err != nil { return "unknown" } return strings.TrimSpace(string(stateBytes)) } func triggerUSBStateUpdate() { go func() { if currentSession == nil { log.Println("No active RPC session, skipping update state update") return } writeJSONRPCEvent("usbState", usbState, currentSession) }() } var udc string func init() { ensureConfigLoaded() go func() { for { newState := rpcGetUSBState() if newState != usbState { log.Printf("USB state changed from %s to %s", usbState, newState) usbState = newState requestDisplayUpdate() triggerUSBStateUpdate() } time.Sleep(500 * time.Millisecond) } }() } // Source: https://www.kernel.org/doc/Documentation/usb/gadget_hid.txt var KeyboardReportDesc = []byte{ 0x05, 0x01, /* USAGE_PAGE (Generic Desktop) */ 0x09, 0x06, /* USAGE (Keyboard) */ 0xa1, 0x01, /* COLLECTION (Application) */ 0x05, 0x07, /* USAGE_PAGE (Keyboard) */ 0x19, 0xe0, /* USAGE_MINIMUM (Keyboard LeftControl) */ 0x29, 0xe7, /* USAGE_MAXIMUM (Keyboard Right GUI) */ 0x15, 0x00, /* LOGICAL_MINIMUM (0) */ 0x25, 0x01, /* LOGICAL_MAXIMUM (1) */ 0x75, 0x01, /* REPORT_SIZE (1) */ 0x95, 0x08, /* REPORT_COUNT (8) */ 0x81, 0x02, /* INPUT (Data,Var,Abs) */ 0x95, 0x01, /* REPORT_COUNT (1) */ 0x75, 0x08, /* REPORT_SIZE (8) */ 0x81, 0x03, /* INPUT (Cnst,Var,Abs) */ 0x95, 0x05, /* REPORT_COUNT (5) */ 0x75, 0x01, /* REPORT_SIZE (1) */ 0x05, 0x08, /* USAGE_PAGE (LEDs) */ 0x19, 0x01, /* USAGE_MINIMUM (Num Lock) */ 0x29, 0x05, /* USAGE_MAXIMUM (Kana) */ 0x91, 0x02, /* OUTPUT (Data,Var,Abs) */ 0x95, 0x01, /* REPORT_COUNT (1) */ 0x75, 0x03, /* REPORT_SIZE (3) */ 0x91, 0x03, /* OUTPUT (Cnst,Var,Abs) */ 0x95, 0x06, /* REPORT_COUNT (6) */ 0x75, 0x08, /* REPORT_SIZE (8) */ 0x15, 0x00, /* LOGICAL_MINIMUM (0) */ 0x25, 0x65, /* LOGICAL_MAXIMUM (101) */ 0x05, 0x07, /* USAGE_PAGE (Keyboard) */ 0x19, 0x00, /* USAGE_MINIMUM (Reserved) */ 0x29, 0x65, /* USAGE_MAXIMUM (Keyboard Application) */ 0x81, 0x00, /* INPUT (Data,Ary,Abs) */ 0xc0, /* END_COLLECTION */ } // Combined absolute and relative mouse report descriptor with report ID var CombinedMouseReportDesc = []byte{ 0x05, 0x01, // Usage Page (Generic Desktop Ctrls) 0x09, 0x02, // Usage (Mouse) 0xA1, 0x01, // Collection (Application) // Report ID 1: Absolute Mouse Movement 0x85, 0x01, // Report ID (1) 0x09, 0x01, // Usage (Pointer) 0xA1, 0x00, // Collection (Physical) 0x05, 0x09, // Usage Page (Button) 0x19, 0x01, // Usage Minimum (0x01) 0x29, 0x03, // Usage Maximum (0x03) 0x15, 0x00, // Logical Minimum (0) 0x25, 0x01, // Logical Maximum (1) 0x75, 0x01, // Report Size (1) 0x95, 0x03, // Report Count (3) 0x81, 0x02, // Input (Data, Var, Abs) 0x95, 0x01, // Report Count (1) 0x75, 0x05, // Report Size (5) 0x81, 0x03, // Input (Cnst, Var, Abs) 0x05, 0x01, // Usage Page (Generic Desktop Ctrls) 0x09, 0x30, // Usage (X) 0x09, 0x31, // Usage (Y) 0x16, 0x00, 0x00, // Logical Minimum (0) 0x26, 0xFF, 0x7F, // Logical Maximum (32767) 0x36, 0x00, 0x00, // Physical Minimum (0) 0x46, 0xFF, 0x7F, // Physical Maximum (32767) 0x75, 0x10, // Report Size (16) 0x95, 0x02, // Report Count (2) 0x81, 0x02, // Input (Data, Var, Abs) 0xC0, // End Collection // Report ID 2: Relative Wheel Movement 0x85, 0x02, // Report ID (2) 0x09, 0x38, // Usage (Wheel) 0x15, 0x81, // Logical Minimum (-127) 0x25, 0x7F, // Logical Maximum (127) 0x75, 0x08, // Report Size (8) 0x95, 0x01, // Report Count (1) 0x81, 0x06, // Input (Data, Var, Rel) 0xC0, // End Collection }