package usbgadget import ( "bufio" "fmt" "os" "os/exec" "path" "strings" "time" ) type gadgetConfigItem struct { order uint device string path []string attrs gadgetAttributes configAttrs gadgetAttributes configPath []string reportDesc []byte } type gadgetAttributes map[string]string type gadgetConfigItemWithKey struct { key string item gadgetConfigItem } type orderedGadgetConfigItems []gadgetConfigItemWithKey var defaultGadgetConfig = map[string]gadgetConfigItem{ "base": { order: 0, 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 "bmAttributes": "0xa0", // 0x80 = bus-powered, 0xa0 = bus-powered + remote wakeup }, }, "base_info": { order: 1, path: []string{"strings", "0x409"}, configPath: []string{"strings", "0x409"}, attrs: gadgetAttributes{ "serialnumber": "", "manufacturer": "KVM", "product": "KVM USB Emulation Device", }, configAttrs: gadgetAttributes{ "configuration": "Config 1: HID", }, }, // mtp "mtp": mtpConfig, // keyboard HID "keyboard": keyboardConfig, // mouse HID "absolute_mouse": absoluteMouseConfig, // relative mouse HID "relative_mouse": relativeMouseConfig, // mass storage "mass_storage_base": massStorageBaseConfig, "mass_storage_lun0": massStorageLun0Config, // audio "audio": { order: 4000, device: "uac1.usb0", path: []string{"functions", "uac1.usb0"}, configPath: []string{"uac1.usb0"}, attrs: gadgetAttributes{ "p_chmask": "3", "p_srate": "48000", "p_ssize": "2", "p_volume_present": "0", "c_chmask": "3", "c_srate": "48000", "c_ssize": "2", "c_volume_present": "0", }, }, } func (u *UsbGadget) isGadgetConfigItemEnabled(itemKey string) bool { switch itemKey { case "absolute_mouse": return u.enabledDevices.AbsoluteMouse case "relative_mouse": return u.enabledDevices.RelativeMouse case "keyboard": return u.enabledDevices.Keyboard case "mass_storage_base": return u.enabledDevices.MassStorage case "mass_storage_lun0": return u.enabledDevices.MassStorage case "mtp": return u.enabledDevices.Mtp case "audio": return u.enabledDevices.Audio default: return true } } func (u *UsbGadget) loadGadgetConfig() { if u.customConfig.isEmpty { u.log.Trace().Msg("using default gadget config") return } u.configMap["base"].attrs["idVendor"] = u.customConfig.VendorId u.configMap["base"].attrs["idProduct"] = u.customConfig.ProductId u.configMap["base_info"].attrs["serialnumber"] = u.customConfig.SerialNumber u.configMap["base_info"].attrs["manufacturer"] = u.customConfig.Manufacturer u.configMap["base_info"].attrs["product"] = u.customConfig.Product } func (u *UsbGadget) SetGadgetConfig(config *Config) { u.configLock.Lock() defer u.configLock.Unlock() if config == nil { return // nothing to do } u.customConfig = *config u.loadGadgetConfig() } func (u *UsbGadget) SetGadgetDevices(devices *Devices) { u.configLock.Lock() defer u.configLock.Unlock() if devices == nil { return // nothing to do } u.enabledDevices = *devices } // GetConfigPath returns the path to the config item. func (u *UsbGadget) GetConfigPath(itemKey string) (string, error) { item, ok := u.configMap[itemKey] if !ok { return "", fmt.Errorf("config item %s not found", itemKey) } return joinPath(u.kvmGadgetPath, item.configPath), nil } // GetPath returns the path to the item. func (u *UsbGadget) GetPath(itemKey string) (string, error) { item, ok := u.configMap[itemKey] if !ok { return "", fmt.Errorf("config item %s not found", itemKey) } return joinPath(u.kvmGadgetPath, item.path), nil } // OverrideGadgetConfig overrides the gadget config for the given item and attribute. // It returns an error if the item is not found or the attribute is not found. // It returns true if the attribute is overridden, false otherwise. func (u *UsbGadget) OverrideGadgetConfig(itemKey string, itemAttr string, value string) (error, bool) { u.configLock.Lock() defer u.configLock.Unlock() // get it as a pointer _, ok := u.configMap[itemKey] if !ok { return fmt.Errorf("config item %s not found", itemKey), false } if u.configMap[itemKey].attrs[itemAttr] == value { return nil, false } u.configMap[itemKey].attrs[itemAttr] = value u.log.Info().Str("itemKey", itemKey).Str("itemAttr", itemAttr).Str("value", value).Msg("overriding gadget config") return nil, true } func mountConfigFS(path string) error { err := exec.Command("mount", "-t", "configfs", "none", path).Run() if err != nil { return fmt.Errorf("failed to mount configfs: %w", err) } return nil } func mountFunctionFS(path string) error { err := os.MkdirAll("/dev/ffs-mtp", 0755) if err != nil { return fmt.Errorf("failed to create mtp dev dir: %w", err) } mounted := false if f, err := os.Open("/proc/mounts"); err == nil { scanner := bufio.NewScanner(f) for scanner.Scan() { if strings.Contains(scanner.Text(), functionFSPath) { mounted = true break } } f.Close() } if !mounted { err := exec.Command("mount", "-t", "functionfs", "mtp", path).Run() if err != nil { return fmt.Errorf("failed to mount functionfs: %w", err) } } umtprdRunning := false if out, err := exec.Command("pgrep", "-x", "umtprd").Output(); err == nil && len(out) > 0 { umtprdRunning = true } if !umtprdRunning { cmd := exec.Command("umtprd") if err := cmd.Start(); err != nil { return fmt.Errorf("failed to exec binary: %w", err) } } return nil } func (u *UsbGadget) Init() error { u.configLock.Lock() defer u.configLock.Unlock() u.loadGadgetConfig() udcs := getUdcs() if len(udcs) < 1 { u.log.Warn().Msg("no UDC found, skipping USB stack init") return u.logWarn("no udc found, skipping USB stack init", nil) } u.udc = udcs[0] if err := u.ensureGadgetUnbound(); err != nil { u.log.Warn().Err(err).Msg("failed to ensure gadget is unbound, will continue") } else { u.log.Info().Msg("gadget unbind check completed") } err := u.configureUsbGadget(false) if err != nil { u.log.Error().Err(err). Str("udc", u.udc). Interface("enabled_devices", u.enabledDevices). Msg("USB gadget initialization FAILED") return u.logError("unable to initialize USB stack", err) } return nil } func (u *UsbGadget) ensureGadgetUnbound() error { udcPath := path.Join(u.kvmGadgetPath, "UDC") if _, err := os.Stat(u.kvmGadgetPath); os.IsNotExist(err) { return nil } udcContent, err := os.ReadFile(udcPath) if err != nil { if os.IsNotExist(err) { return nil } return fmt.Errorf("failed to read UDC file: %w", err) } currentUDC := strings.TrimSpace(string(udcContent)) if currentUDC == "" || currentUDC == "none" { return nil } u.log.Info(). Str("current_udc", currentUDC). Str("target_udc", u.udc). Msg("unbinding existing UDC before reconfiguration") if err := u.UnbindUDC(); err != nil { u.log.Warn().Err(err).Msg("failed to unbind via UDC file, trying DWC3") if err := u.UnbindUDCToDWC3(); err != nil { return fmt.Errorf("failed to unbind UDC: %w", err) } } time.Sleep(200 * time.Millisecond) if content, err := os.ReadFile(udcPath); err == nil { if strings.TrimSpace(string(content)) != "none" { u.log.Warn().Msg("UDC still bound after unbind attempt") } } return nil } func (u *UsbGadget) UpdateGadgetConfig() error { u.configLock.Lock() defer u.configLock.Unlock() u.loadGadgetConfig() err := u.configureUsbGadget(true) if err != nil { return u.logError("unable to update gadget config", err) } return nil } func (u *UsbGadget) configureUsbGadget(resetUsb bool) error { u.log.Info(). Bool("reset_usb", resetUsb). Msg("configuring USB gadget via transaction") return u.WithTransaction(func() error { u.log.Info().Msg("Transaction: Mounting configfs") u.tx.MountConfigFS() u.log.Info().Msg("Transaction: Creating config path") u.tx.CreateConfigPath() u.log.Info().Msg("Transaction: Writing gadget configuration") u.tx.WriteGadgetConfig() if resetUsb { u.log.Info().Msg("Transaction: Rebinding USB") u.tx.RebindUsb(true) } return nil }) } func (u *UsbGadget) VerifyMassStorage() error { if !u.enabledDevices.MassStorage { return nil } massStoragePath := path.Join(u.kvmGadgetPath, "functions/mass_storage.usb0") if _, err := os.Stat(massStoragePath); err != nil { return fmt.Errorf("mass_storage function not found: %w", err) } lunPath := path.Join(massStoragePath, "lun.0") if _, err := os.Stat(lunPath); err != nil { return fmt.Errorf("mass_storage LUN not found: %w", err) } configLink := path.Join(u.configC1Path, "mass_storage.usb0") if _, err := os.Lstat(configLink); err != nil { return fmt.Errorf("mass_storage symlink not found: %w", err) } u.log.Info().Msg("mass storage verified") return nil }