package kvm import ( "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 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 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 } err = writeGadgetAttrs(kvmGadgetPath, [][]string{ {"bcdUSB", "0x0200"}, //USB 2.0 {"idVendor", config.UsbConfig.VendorId}, //The Linux Foundation {"idProduct", config.UsbConfig.ProductId}, //Multifunction Composite Gadget¬ {"bcdDevice", "0100"}, }) if err != nil { return err } gadgetStringsPath := filepath.Join(kvmGadgetPath, "strings", "0x409") err = os.MkdirAll(gadgetStringsPath, 0755) if err != nil { return err } err = writeGadgetAttrs(gadgetStringsPath, [][]string{ {"serialnumber", GetDeviceID()}, {"manufacturer", config.UsbConfig.Manufacturer}, {"product", config.UsbConfig.Product}, }) if err != nil { return err } configC1StringsPath := path.Join(configC1Path, "strings", "0x409") err = os.MkdirAll(configC1StringsPath, 0755) if err != nil { return err } err = writeGadgetAttrs(configC1Path, [][]string{ {"MaxPower", "250"}, //in unit of 2mA }) if err != nil { return err } err = writeGadgetAttrs(configC1StringsPath, [][]string{ {"configuration", "Config 1: HID"}, }) if err != nil { return err } //keyboard HID hid0Path := path.Join(kvmGadgetPath, "functions", "hid.usb0") err = os.MkdirAll(hid0Path, 0755) if err != nil { return err } err = writeGadgetAttrs(hid0Path, [][]string{ {"protocol", "1"}, {"subclass", "1"}, {"report_length", "8"}, }) if err != nil { return err } err = os.WriteFile(path.Join(hid0Path, "report_desc"), KeyboardReportDesc, 0644) if err != nil { return err } //mouse HID hid1Path := path.Join(kvmGadgetPath, "functions", "hid.usb1") err = os.MkdirAll(hid1Path, 0755) if err != nil { return err } err = writeGadgetAttrs(hid1Path, [][]string{ {"protocol", "2"}, {"subclass", "1"}, {"report_length", "6"}, }) if err != nil { return err } err = os.WriteFile(path.Join(hid1Path, "report_desc"), CombinedMouseReportDesc, 0644) if err != nil { return err } //mass storage massStoragePath := path.Join(kvmGadgetPath, "functions", "mass_storage.usb0") err = os.MkdirAll(massStoragePath, 0755) if err != nil { return err } err = writeGadgetAttrs(massStoragePath, [][]string{ {"stall", "1"}, }) if err != nil { return err } lun0Path := path.Join(massStoragePath, "lun.0") err = os.MkdirAll(lun0Path, 0755) if err != nil { return err } err = writeGadgetAttrs(lun0Path, [][]string{ {"cdrom", "1"}, {"ro", "1"}, {"removable", "1"}, {"file", "\n"}, {"inquiry_string", "JetKVM Virtual Media"}, }) if err != nil { return err } err = os.Symlink(hid0Path, path.Join(configC1Path, "hid.usb0")) if err != nil { return err } err = os.Symlink(hid1Path, path.Join(configC1Path, "hid.usb1")) if err != nil { return err } err = os.Symlink(massStoragePath, path.Join(configC1Path, "mass_storage.usb0")) 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 accumulatedWheelY += float64(wheelY) / 8.0 // Only send a report if the accumulated value is significant if abs(accumulatedWheelY) >= 1.0 { scaledWheelY := int8(accumulatedWheelY) _, err := mouseHidFile.Write([]byte{ 2, // Report ID 2 byte(scaledWheelY), // Scaled Wheel Y (signed) }) // Reset the accumulator, keeping any remainder accumulatedWheelY -= float64(scaledWheelY) 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 }