//go:build with_usbip && (linux || (darwin && cgo) || windows) package main import ( "context" "errors" "io" "maps" "os" "os/signal" "runtime" "slices" "strings" "sync" "syscall" "time" "github.com/sagernet/sing-box/daemon" "github.com/sagernet/sing-usbip" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" "google.golang.org/protobuf/types/known/emptypb" ) const ( usbipShareDrainTimeout = 2 * time.Second usbipShareQueueDepth = 64 usbipShareMaxDeviceIDTry = 9 usbipShareStatusEIO = -5 ) func runAPIUsbipShare(args []string) error { if commandAPIUsbipShareFlagAll == (len(args) > 0) { return E.New("either --all or one or more device selectors are required") } clientConn, client, err := createAPIClient() if err != nil { return err } defer clientConn.Close() sessionCtx, cancelSession := context.WithCancel(globalCtx) defer cancelSession() statusCtx, cancelStatus := context.WithCancel(sessionCtx) defer cancelStatus() watchCtx, cancelWatch := context.WithCancel(sessionCtx) defer cancelWatch() statusStream, err := client.SubscribeUSBIPServerStatus(statusCtx, &emptypb.Empty{}) if err != nil { return err } statusUpdate, err := statusStream.Recv() if err != nil { return err } server, err := resolveUsbipServer(statusUpdate.GetServers(), commandAPIUsbipShareFlagService) if err != nil { return err } localDevices, err := listLocalUSBDevices() if err != nil { return err } selected, err := selectSharedUSBDevices(localDevices, args) if err != nil { return err } stream, err := client.ProvideUSBDevices(sessionCtx) if err != nil { return err } session := &usbipShareSession{ ctx: sessionCtx, serverTag: server.GetServerTag(), capture: commandAPIUsbipShareFlagCapture, shareAll: commandAPIUsbipShareFlagAll, stream: stream, devices: make(map[string]*usbipSharedDevice), states: make(map[string]daemon.USBDeviceState), failed: make(map[string]struct{}), } signalChan := make(chan os.Signal, 2) signal.Notify(signalChan, os.Interrupt, syscall.SIGTERM) defer signal.Stop(signalChan) receiveDone := make(chan struct{}) go func() { defer close(receiveDone) session.receive() }() go session.readStatus(statusStream) for _, device := range selected { shareErr := session.share(device, "") if shareErr != nil { cancelStatus() session.closeAll() return shareErr } } // The darwin watcher fires once at registration, so it is registered only after // the initial devices are shared. err = usbip.WatchLocalDevices(watchCtx, session.onLocalDevicesChanged) if err != nil { cancelStatus() session.closeAll() return E.Cause(err, "watch local USB devices") } select { case <-signalChan: cancelWatch() cancelStatus() session.teardown(signalChan, receiveDone) return nil case <-receiveDone: cancelWatch() cancelStatus() return session.abort() } } func selectSharedUSBDevices(devices []usbip.LocalDeviceInfo, selectors []string) ([]usbip.LocalDeviceInfo, error) { if len(selectors) == 0 { return devices, nil } selected := make([]usbip.LocalDeviceInfo, 0, len(selectors)) for _, selector := range selectors { device, err := resolveLocalUSBDevice(devices, selector) if err != nil { return nil, err } busID := device.Entry.Info.BusIDString() if slices.ContainsFunc(selected, func(it usbip.LocalDeviceInfo) bool { return it.Entry.Info.BusIDString() == busID }) { continue } selected = append(selected, device) } return selected, nil } type usbipDeviceIdentity struct { vendorID uint16 productID uint16 serial string product string } func usbipIdentityOf(device usbip.LocalDeviceInfo) usbipDeviceIdentity { return usbipDeviceIdentity{ vendorID: device.Entry.Info.IDVendor, productID: device.Entry.Info.IDProduct, serial: device.Entry.Serial, product: device.Entry.Product, } } type usbipReconnectIntent struct { identity usbipDeviceIdentity deviceID string } type usbipShareSession struct { ctx context.Context serverTag string capture bool shareAll bool stream daemon.StartedService_ProvideUSBDevicesClient sendAccess sync.Mutex printAccess sync.Mutex access sync.Mutex closed bool devices map[string]*usbipSharedDevice intents []usbipReconnectIntent states map[string]daemon.USBDeviceState failed map[string]struct{} terminalError error terminalReason string } type usbipSharedDevice struct { session *usbipShareSession info usbip.LocalDeviceInfo local usbip.LocalDevice localBusID string identity usbipDeviceIdentity deviceID string attempt int seenInStatus bool queueAccess sync.Mutex queues map[uint8]chan *daemon.USBURBRequest closed bool closeOnce sync.Once closeFinished chan struct{} } func (s *usbipShareSession) share(info usbip.LocalDeviceInfo, preferredDeviceID string) error { busID := info.Entry.Info.BusIDString() local, err := usbip.OpenLocalDevice(busID, s.capture) if err != nil { s.access.Lock() s.failed[busID] = struct{}{} s.access.Unlock() s.printEvent("error", busID, err.Error()) s.writeOpenHint(err) if usbipShareFatalOpenError(err) { return err } return nil } deviceID := preferredDeviceID if deviceID == "" { deviceID = busID } device := &usbipSharedDevice{ session: s, info: info, local: local, localBusID: busID, identity: usbipIdentityOf(info), deviceID: deviceID, attempt: 1, queues: make(map[uint8]chan *daemon.USBURBRequest), closeFinished: make(chan struct{}), } s.access.Lock() if s.closed { s.access.Unlock() _ = local.Close() return nil } _, taken := s.devices[deviceID] if taken { s.access.Unlock() device.close() s.printEvent("error", deviceID, "device id already in use by this session") return nil } s.devices[deviceID] = device s.access.Unlock() sendErr := s.send(usbipAttachMessage(s.serverTag, deviceID, info)) if sendErr != nil { s.removeDevice(deviceID) device.close() s.printEvent("error", deviceID, sendErr.Error()) } return nil } func (s *usbipShareSession) send(message *daemon.USBProviderMessage) error { s.sendAccess.Lock() defer s.sendAccess.Unlock() return s.stream.Send(message) } func (s *usbipShareSession) detach(deviceID string) { _ = s.send(&daemon.USBProviderMessage{Message: &daemon.USBProviderMessage_Detach{ Detach: &daemon.USBDeviceDetach{DeviceId: deviceID}, }}) } func (s *usbipShareSession) device(deviceID string) *usbipSharedDevice { s.access.Lock() defer s.access.Unlock() return s.devices[deviceID] } func (s *usbipShareSession) removeDevice(deviceID string) *usbipSharedDevice { s.access.Lock() defer s.access.Unlock() device := s.devices[deviceID] if device != nil { delete(s.devices, deviceID) delete(s.states, deviceID) } return device } func (s *usbipShareSession) receive() { for { message, err := s.stream.Recv() if err != nil { if s.ctx.Err() == nil { if err == io.EOF { s.terminalError = E.New("provider stream closed by the API service") } else { s.terminalError = err } s.terminalReason = "stream-closed" } return } switch body := message.GetMessage().(type) { case *daemon.USBServerMessage_Ready: s.onReady(body.Ready) case *daemon.USBServerMessage_UrbRequest: device := s.device(body.UrbRequest.GetDeviceId()) if device != nil { device.submit(body.UrbRequest) } case *daemon.USBServerMessage_Abort: s.onAbort(body.Abort) case *daemon.USBServerMessage_Error: if s.onError(body.Error) { return } } } } func (s *usbipShareSession) onReady(ready *daemon.USBDeviceReady) { device := s.device(ready.GetDeviceId()) if device == nil { return } busID := ready.GetDeviceId() if busID != device.localBusID { busID = F.ToString(busID, " (local ", device.localBusID, ")") } s.printEvent("shared", busID, usbipVendorProduct(device.identity.vendorID, device.identity.productID), device.identity.product, ) } func (s *usbipShareSession) onAbort(abort *daemon.USBEndpointAbort) { device := s.device(abort.GetDeviceId()) if device == nil { return } err := device.local.AbortEndpoint(uint8(abort.GetEndpoint())) if err != nil { s.printEvent("error", abort.GetDeviceId(), err.Error()) } } func (s *usbipShareSession) onError(failure *daemon.USBError) bool { deviceID := failure.GetDeviceId() message := failure.GetMessage() if deviceID == "" || strings.Contains(message, "usbip-server not found:") || strings.Contains(message, "is not a dynamic usbip-server") { s.terminalError = E.New(message) s.terminalReason = "server-error" return true } if strings.Contains(message, "dynamic device already provided:") { s.retryDeviceID(deviceID, message) return false } device := s.removeDevice(deviceID) if device != nil { device.close() } s.printEvent("error", deviceID, message) return false } func (s *usbipShareSession) retryDeviceID(deviceID string, message string) { s.access.Lock() device := s.devices[deviceID] if device == nil || device.attempt >= usbipShareMaxDeviceIDTry { s.access.Unlock() if device != nil { s.removeDevice(deviceID) device.close() s.printEvent("error", deviceID, message) } return } delete(s.devices, deviceID) delete(s.states, deviceID) device.attempt++ device.deviceID = F.ToString(device.localBusID, "#", device.attempt) s.devices[device.deviceID] = device retryID := device.deviceID s.access.Unlock() sendErr := s.send(usbipAttachMessage(s.serverTag, retryID, device.info)) if sendErr != nil { s.removeDevice(retryID) device.close() s.printEvent("error", retryID, sendErr.Error()) } } func (s *usbipShareSession) readStatus(stream daemon.StartedService_SubscribeUSBIPServerStatusClient) { for { update, err := stream.Recv() if err != nil { return } index := slices.IndexFunc(update.GetServers(), func(it *daemon.USBIPServerStatus) bool { return it.GetServerTag() == s.serverTag }) if index == -1 { continue } snapshot := make(map[string]daemon.USBDeviceState) for _, device := range update.GetServers()[index].GetDevices() { snapshot[device.GetBusId()] = device.GetState() } s.applyStatusSnapshot(snapshot) } } func (s *usbipShareSession) applyStatusSnapshot(snapshot map[string]daemon.USBDeviceState) { type transition struct { deviceID string verb string } var ( transitions []transition lost []*usbipSharedDevice ) s.access.Lock() for deviceID, device := range s.devices { state, present := snapshot[deviceID] if !present { if device.seenInStatus { device.seenInStatus = false delete(s.states, deviceID) lost = append(lost, device) } continue } previous, tracked := s.states[deviceID] s.states[deviceID] = state if !device.seenInStatus { device.seenInStatus = true continue } if !tracked || previous == state { continue } switch { case state == daemon.USBDeviceState_USB_DEVICE_STATE_ATTACHED: transitions = append(transitions, transition{deviceID: deviceID, verb: "attached"}) case previous == daemon.USBDeviceState_USB_DEVICE_STATE_ATTACHED: transitions = append(transitions, transition{deviceID: deviceID, verb: "released"}) } } s.access.Unlock() for _, item := range transitions { s.printEvent(item.verb, item.deviceID) } for _, device := range lost { s.printEvent("detached", device.deviceID, "server-error") sendErr := s.send(usbipAttachMessage(s.serverTag, device.deviceID, device.info)) if sendErr != nil { s.removeDevice(device.deviceID) device.close() s.printEvent("error", device.deviceID, sendErr.Error()) } } } func (s *usbipShareSession) onLocalDevicesChanged() { devices, err := listLocalUSBDevices() if err != nil { return } present := make(map[string]struct{}, len(devices)) for _, device := range devices { present[device.Entry.Info.BusIDString()] = struct{}{} } var vanished []*usbipSharedDevice s.access.Lock() if s.closed { s.access.Unlock() return } // Capturing a device on windows rewrites its reported vendor and product id, so // presence is decided by bus id alone; the identity tuple is only used to match a // replugged device back to the export it replaces. for deviceID, device := range s.devices { _, found := present[device.localBusID] if found { continue } delete(s.devices, deviceID) delete(s.states, deviceID) vanished = append(vanished, device) } for _, device := range vanished { s.intents = append(s.intents, usbipReconnectIntent{identity: device.identity, deviceID: device.deviceID}) } for busID := range s.failed { _, found := present[busID] if !found { delete(s.failed, busID) } } s.access.Unlock() for _, device := range vanished { s.detach(device.deviceID) device.close() s.printEvent("detached", device.deviceID, "unplugged") } s.reshare(devices) } func (s *usbipShareSession) reshare(devices []usbip.LocalDeviceInfo) { s.access.Lock() if s.closed { s.access.Unlock() return } shared := make(map[string]struct{}, len(s.devices)) for _, device := range s.devices { shared[device.localBusID] = struct{}{} } intents := slices.Clone(s.intents) failed := maps.Clone(s.failed) s.access.Unlock() available := common.Filter(devices, func(it usbip.LocalDeviceInfo) bool { _, found := shared[it.Entry.Info.BusIDString()] return !found }) claimed := make(map[string]struct{}) var pending []usbipReconnectIntent for _, intent := range intents { matches := common.Filter(available, func(it usbip.LocalDeviceInfo) bool { _, found := claimed[it.Entry.Info.BusIDString()] return !found && usbipIdentityOf(it) == intent.identity }) if len(matches) != 1 { pending = append(pending, intent) continue } claimed[matches[0].Entry.Info.BusIDString()] = struct{}{} s.consumeIntent(intent) _ = s.share(matches[0], intent.deviceID) } if !s.shareAll { return } for _, device := range available { busID := device.Entry.Info.BusIDString() _, claimedHere := claimed[busID] _, failedBefore := failed[busID] if claimedHere || failedBefore { continue } if slices.ContainsFunc(pending, func(it usbipReconnectIntent) bool { return it.identity == usbipIdentityOf(device) }) { continue } _ = s.share(device, "") } } func (s *usbipShareSession) consumeIntent(intent usbipReconnectIntent) { s.access.Lock() defer s.access.Unlock() index := slices.Index(s.intents, intent) if index != -1 { s.intents = slices.Delete(s.intents, index, index+1) } } func (s *usbipShareSession) takeDevices() []*usbipSharedDevice { s.access.Lock() defer s.access.Unlock() s.closed = true s.intents = nil devices := slices.Collect(maps.Values(s.devices)) s.devices = make(map[string]*usbipSharedDevice) clear(s.states) slices.SortFunc(devices, func(a *usbipSharedDevice, b *usbipSharedDevice) int { return compareBusID(a.deviceID, b.deviceID) }) return devices } func (s *usbipShareSession) teardown(signalChan <-chan os.Signal, receiveDone <-chan struct{}) { done := make(chan struct{}) go func() { defer close(done) devices := s.takeDevices() for _, device := range devices { s.detach(device.deviceID) } _ = s.stream.CloseSend() timer := time.NewTimer(usbipShareDrainTimeout) defer timer.Stop() select { case <-receiveDone: case <-timer.C: } for _, device := range devices { device.close() } for _, device := range devices { s.printEvent("detached", device.deviceID, "signal") } }() select { case <-done: case <-signalChan: if runtime.GOOS == "windows" { os.Stderr.WriteString("interrupted while stopping, USB devices may remain captured\n") } os.Exit(130) } } func (s *usbipShareSession) abort() error { reason := s.terminalReason if reason == "" { reason = "stream-closed" } for _, device := range s.takeDevices() { device.close() s.printEvent("detached", device.deviceID, reason) } if s.terminalError == nil { return E.New("provider stream closed") } return s.terminalError } func (s *usbipShareSession) closeAll() { for _, device := range s.takeDevices() { device.close() } } func (s *usbipShareSession) printEvent(verb string, busID string, fields ...string) { var builder strings.Builder builder.WriteString(padUSBIPCell(verb, 9)) builder.WriteString(" ") builder.WriteString(padUSBIPCell(busID, 10)) for _, field := range fields { builder.WriteString(" ") if field == "" { field = "-" } builder.WriteString(field) } line := strings.TrimRight(builder.String(), " ") + "\n" s.printAccess.Lock() defer s.printAccess.Unlock() os.Stdout.WriteString(line) } func (s *usbipShareSession) writeOpenHint(err error) { if errors.Is(err, os.ErrPermission) { os.Stderr.WriteString("hint: run sing-box with elevated privileges to access this device\n") return } if !s.capture { os.Stderr.WriteString("hint: retry with --capture to take the device from the driver using it (requires root or Administrator)\n") } } // The VBoxUSB driver installation runs before any device is touched, so these // failures apply to every device in the session. func usbipShareFatalOpenError(err error) bool { message := err.Error() return strings.Contains(message, "enable SeLoadDriverPrivilege") || strings.Contains(message, "install VBoxUSB drivers") } func (d *usbipSharedDevice) submit(request *daemon.USBURBRequest) { endpoint := uint8(request.GetEndpoint()) d.queueAccess.Lock() if d.closed { d.queueAccess.Unlock() d.sendResponse(usbipURBErrorResponse(request)) return } queue := d.queues[endpoint] if queue == nil { queue = make(chan *daemon.USBURBRequest, usbipShareQueueDepth) d.queues[endpoint] = queue go d.runQueue(queue) } select { case queue <- request: d.queueAccess.Unlock() default: d.queueAccess.Unlock() d.sendResponse(usbipURBErrorResponse(request)) } } func (d *usbipSharedDevice) runQueue(queue <-chan *daemon.USBURBRequest) { for request := range queue { result := d.local.Submit(usbipURBRequestFromProto(request)) d.sendResponse(usbipURBResponseToProto(request, result)) } } func (d *usbipSharedDevice) sendResponse(response *daemon.USBURBResponse) { _ = d.session.send(&daemon.USBProviderMessage{Message: &daemon.USBProviderMessage_UrbResponse{UrbResponse: response}}) } func (d *usbipSharedDevice) close() { d.closeOnce.Do(func() { defer close(d.closeFinished) d.queueAccess.Lock() d.closed = true for endpoint, queue := range d.queues { close(queue) delete(d.queues, endpoint) } d.queueAccess.Unlock() _ = d.local.Close() }) <-d.closeFinished } func usbipAttachMessage(serverTag string, deviceID string, info usbip.LocalDeviceInfo) *daemon.USBProviderMessage { entry := info.Entry return &daemon.USBProviderMessage{Message: &daemon.USBProviderMessage_Attach{Attach: &daemon.USBDeviceAttach{ ServerTag: serverTag, Descriptor_: &daemon.USBDeviceDescriptor{ DeviceId: deviceID, BusNum: entry.Info.BusNum, DevNum: entry.Info.DevNum, Speed: entry.Info.Speed, VendorId: uint32(entry.Info.IDVendor), ProductId: uint32(entry.Info.IDProduct), BcdDevice: uint32(entry.Info.BCDDevice), DeviceClass: uint32(entry.Info.BDeviceClass), DeviceSubClass: uint32(entry.Info.BDeviceSubClass), DeviceProtocol: uint32(entry.Info.BDeviceProtocol), ConfigurationValue: uint32(entry.Info.BConfigurationValue), NumConfigurations: uint32(entry.Info.BNumConfigurations), Interfaces: common.Map(entry.Interfaces, func(it usbip.DeviceInterface) *daemon.USBInterface { return &daemon.USBInterface{ InterfaceClass: uint32(it.BInterfaceClass), InterfaceSubClass: uint32(it.BInterfaceSubClass), InterfaceProtocol: uint32(it.BInterfaceProtocol), } }), Serial: entry.Serial, Product: entry.Product, }, }}} } func usbipURBRequestFromProto(request *daemon.USBURBRequest) usbip.URBRequest { endpoint := uint8(request.GetEndpoint()) direction := usbip.USBIPDirOut buffer := request.GetOutData() if request.GetDirectionIn() { direction = usbip.USBIPDirIn buffer = make([]byte, request.GetTransferBufferLength()) } var setup [8]byte copy(setup[:], request.GetSetup()) isoPackets := common.Map(request.GetIsoPackets(), func(it *daemon.USBIsoPacket) usbip.IsoPacketDescriptor { return usbip.IsoPacketDescriptor{ Offset: it.GetOffset(), Length: it.GetLength(), ActualLength: it.GetActualLength(), Status: it.GetStatus(), } }) return usbip.URBRequest{ Command: usbip.SubmitCommand{ Header: usbip.DataHeader{ Command: usbip.CmdSubmit, SeqNum: uint32(request.GetSeq()), Direction: direction, Endpoint: uint32(endpoint & 0x0f), }, TransferFlags: int32(request.GetTransferFlags()), TransferBufferLength: int32(request.GetTransferBufferLength()), StartFrame: request.GetStartFrame(), NumberOfPackets: request.GetNumberOfPackets(), Interval: request.GetInterval(), Setup: setup, Buffer: buffer, IsoPackets: isoPackets, }, Endpoint: endpoint, Buffer: buffer, IsoPackets: isoPackets, } } func usbipURBResponseToProto(request *daemon.USBURBRequest, result usbip.URBResponse) *daemon.USBURBResponse { if result.Error != nil { return usbipURBErrorResponse(request) } response := &daemon.USBURBResponse{ DeviceId: request.GetDeviceId(), Seq: request.GetSeq(), Status: result.Status, ActualLength: result.ActualLength, IsoPackets: common.Map(result.IsoPackets, func(it usbip.IsoPacketDescriptor) *daemon.USBIsoPacket { return &daemon.USBIsoPacket{ Offset: it.Offset, Length: it.Length, ActualLength: it.ActualLength, Status: it.Status, } }), } if request.GetDirectionIn() && len(result.Buffer) > 0 { if request.GetNumberOfPackets() > 0 { response.InData = result.Buffer } else { response.InData = result.Buffer[:min(max(int(result.ActualLength), 0), len(result.Buffer))] } } return response } func usbipURBErrorResponse(request *daemon.USBURBRequest) *daemon.USBURBResponse { return &daemon.USBURBResponse{ DeviceId: request.GetDeviceId(), Seq: request.GetSeq(), Status: usbipShareStatusEIO, } }