Files
sing-box/service/usbip/session_userspace.go
T
世界 6a4ae5263e usbip: extract URBEngine + userspaceURBSession from darwin backend
Both the existing darwin backend and the upcoming windows backend
drive USB devices from user space (IOUSBHost CGO calls vs. VBoxUSB
IOCTLs). Refactor the per-attachment URB loop out of host_darwin.go
into a platform-agnostic userspaceURBSession that talks to a URBEngine
interface; the darwin-specific dispatch becomes a 30-line
darwinIOUSBHostEngine. Linux's kernelHandoffSession is untouched.

Move hex8 into shared.go and add usbipStatusEIO so the shared session
does not depend on golang.org/x/sys/unix (Windows has no equivalent).
2026-06-09 10:42:35 +08:00

471 lines
11 KiB
Go

//go:build linux || (darwin && cgo)
package usbip
import (
"context"
"errors"
"fmt"
"io"
"net"
"slices"
"sync"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
)
var _ DataSession = (*userspaceURBSession)(nil)
// userspaceURBSession drives one USBIP attachment when the per-URB I/O
// happens in user space (Darwin IOUSBHost, Windows VBoxUSB). It reads
// CMD_SUBMIT/CMD_UNLINK from conn, serializes per endpoint, dispatches
// to a URBEngine, frames RET_SUBMIT/RET_UNLINK back over conn. Linux
// kernel-handoff sessions use kernelHandoffSession instead.
type userspaceURBSession struct {
ctx context.Context
logger log.ContextLogger
conn net.Conn
engine URBEngine
writeAccess sync.Mutex
access sync.Mutex
pending map[uint32]userspaceSubmitState
endpoints map[uint8]*userspaceEndpointState
wg sync.WaitGroup
done chan struct{}
doneOnce sync.Once
runErr error
stateAccess sync.Mutex
started bool
closed bool
closeOnce sync.Once
closeErr error
}
type userspaceSubmitState struct {
command SubmitCommand
endpoint uint8
started bool
unlinked bool
drained chan struct{}
}
type userspaceEndpointState struct {
active uint32
queued []uint32
}
type userspaceNextSubmit struct {
sequence uint32
command SubmitCommand
}
func newUserspaceURBSession(ctx context.Context, logger log.ContextLogger, conn net.Conn, engine URBEngine) *userspaceURBSession {
return &userspaceURBSession{
ctx: ctx,
logger: logger,
conn: conn,
engine: engine,
pending: make(map[uint32]userspaceSubmitState),
endpoints: make(map[uint8]*userspaceEndpointState),
done: make(chan struct{}),
}
}
func (s *userspaceURBSession) Done() <-chan struct{} {
return s.done
}
func (s *userspaceURBSession) Err() error {
return s.runErr
}
func (s *userspaceURBSession) Start() error {
s.stateAccess.Lock()
defer s.stateAccess.Unlock()
if s.started || s.closed {
return nil
}
s.started = true
go s.run()
return nil
}
func (s *userspaceURBSession) Close() error {
s.closeOnce.Do(func() {
s.closeErr = common.Close(s.conn)
})
s.stateAccess.Lock()
started := s.started
s.closed = true
s.stateAccess.Unlock()
if started {
<-s.done
} else {
s.markDone(nil)
}
_ = s.engine.Close()
return s.closeErr
}
func (s *userspaceURBSession) markDone(err error) {
s.doneOnce.Do(func() {
s.runErr = err
close(s.done)
})
}
func (s *userspaceURBSession) run() {
err := s.serve()
if err != nil && (errors.Is(err, io.EOF) || E.IsClosedOrCanceled(err)) {
err = nil
}
s.markDone(err)
}
func (s *userspaceURBSession) serve() error {
stopCloseOnCancel := closeConnOnContextDone(s.ctx, s.conn)
defer stopCloseOnCancel()
defer func() {
s.abortPendingSubmits()
s.wg.Wait()
}()
for {
header, err := ReadDataHeader(s.conn)
if err != nil {
if errors.Is(err, io.EOF) {
return nil
}
return err
}
switch header.Command {
case CmdSubmit:
command, err := ReadSubmitCommandBody(s.conn, header)
if err != nil {
return err
}
next, shouldStart := s.enqueueSubmit(command)
if shouldStart {
s.startSubmit(next)
}
case CmdUnlink:
command, err := ReadUnlinkCommandBody(s.conn, header)
if err != nil {
return err
}
status := int32(0)
endpoint, drained, shouldAbort, found := s.unlinkSubmit(command.SeqNum)
if found {
if shouldAbort {
abortErr := s.engine.AbortEndpoint(endpoint)
if abortErr != nil {
s.logger.Debug("abort endpoint 0x", hex8(endpoint), ": ", abortErr)
}
}
<-drained
status = usbipStatusECONNRESET
}
s.writeAccess.Lock()
err = WriteUnlinkResponse(s.conn, UnlinkResponse{
Header: DataHeader{Command: RetUnlink, SeqNum: header.SeqNum, DevID: header.DevID, Direction: header.Direction, Endpoint: header.Endpoint},
Status: status,
})
s.writeAccess.Unlock()
if err != nil {
return err
}
default:
return E.New("unexpected USB/IP command ", fmt.Sprintf("0x%08x", header.Command))
}
}
}
func (s *userspaceURBSession) enqueueSubmit(command SubmitCommand) (userspaceNextSubmit, bool) {
endpoint := submitScheduleEndpoint(command)
sequence := command.Header.SeqNum
s.access.Lock()
defer s.access.Unlock()
state := userspaceSubmitState{
command: command,
endpoint: endpoint,
}
endpointState, found := s.endpoints[endpoint]
if !found {
endpointState = &userspaceEndpointState{}
s.endpoints[endpoint] = endpointState
}
if endpointState.active == 0 {
state.started = true
s.pending[sequence] = state
endpointState.active = sequence
return userspaceNextSubmit{
sequence: sequence,
command: command,
}, true
}
s.pending[sequence] = state
endpointState.queued = append(endpointState.queued, sequence)
return userspaceNextSubmit{}, false
}
func (s *userspaceURBSession) startSubmit(next userspaceNextSubmit) {
s.wg.Add(1)
go func() {
defer s.wg.Done()
response := s.handleSubmit(next.command)
shouldSend, followUp, hasFollowUp := s.finishSubmit(next.sequence)
if shouldSend {
s.writeAccess.Lock()
err := WriteSubmitResponse(s.conn, response)
s.writeAccess.Unlock()
if err != nil {
_ = s.conn.Close()
}
}
if hasFollowUp {
s.startSubmit(followUp)
}
}()
}
func (s *userspaceURBSession) handleSubmit(command SubmitCommand) SubmitResponse {
response := SubmitResponse{
Header: DataHeader{
Command: RetSubmit,
SeqNum: command.Header.SeqNum,
DevID: command.Header.DevID,
Direction: command.Header.Direction,
Endpoint: command.Header.Endpoint,
},
StartFrame: command.StartFrame,
NumberOfPackets: command.NumberOfPackets,
IsoPackets: slices.Clone(command.IsoPackets),
}
buffer := command.Buffer
if command.Header.Direction == USBIPDirIn && command.TransferBufferLength > 0 {
buffer = make([]byte, int(command.TransferBufferLength))
}
endpoint := commandEndpoint(command)
result := s.engine.Submit(URBRequest{
Command: command,
Endpoint: endpoint,
Buffer: buffer,
IsoPackets: response.IsoPackets,
})
if result.Error != nil {
s.logger.Debug("submit seq ", command.Header.SeqNum, " endpoint 0x", hex8(endpoint), ": ", result.Error)
response.Status = usbipStatusEIO
return response
}
response.Status = result.Status
if result.IsoPackets != nil {
response.IsoPackets = result.IsoPackets
}
actual := result.ActualLength
if actual < 0 {
actual = 0
}
response.ActualLength = actual
if command.Header.Direction == USBIPDirIn && actual > 0 {
if command.NumberOfPackets > 0 {
response.Buffer = packIsoInResponseBuffer(result.Buffer, response.IsoPackets)
response.ActualLength = int32(len(response.Buffer))
} else {
response.Buffer = result.Buffer[:min(int(actual), len(result.Buffer))]
}
}
return response
}
func packIsoInResponseBuffer(buffer []byte, packets []IsoPacketDescriptor) []byte {
var total int
for i := range packets {
length := int(packets[i].ActualLength)
if length <= 0 {
packets[i].ActualLength = 0
continue
}
offset := int(packets[i].Offset)
if offset < 0 || offset >= len(buffer) {
packets[i].ActualLength = 0
continue
}
if offset+length > len(buffer) {
length = len(buffer) - offset
packets[i].ActualLength = int32(length)
}
total += length
}
if total == 0 {
return nil
}
packed := make([]byte, 0, total)
for i := range packets {
length := int(packets[i].ActualLength)
if length <= 0 {
continue
}
offset := int(packets[i].Offset)
packed = append(packed, buffer[offset:offset+length]...)
}
return packed
}
func (s *userspaceURBSession) unlinkSubmit(seq uint32) (uint8, <-chan struct{}, bool, bool) {
var drained chan struct{}
s.access.Lock()
pending, found := s.pending[seq]
if !found {
s.access.Unlock()
return 0, nil, false, false
}
if pending.drained == nil {
pending.drained = make(chan struct{})
}
drained = pending.drained
if !pending.started {
endpointState := s.endpoints[pending.endpoint]
if endpointState != nil {
endpointState.queued = removeQueuedSequence(endpointState.queued, seq)
if endpointState.active == 0 && len(endpointState.queued) == 0 {
delete(s.endpoints, pending.endpoint)
}
}
delete(s.pending, seq)
s.access.Unlock()
close(drained)
return pending.endpoint, drained, false, true
}
shouldAbort := !pending.unlinked
pending.unlinked = true
s.pending[seq] = pending
s.access.Unlock()
return pending.endpoint, drained, shouldAbort, true
}
func (s *userspaceURBSession) finishSubmit(seq uint32) (bool, userspaceNextSubmit, bool) {
var drained chan struct{}
var followUp userspaceNextSubmit
var hasFollowUp bool
s.access.Lock()
pending, found := s.pending[seq]
if !found {
s.access.Unlock()
return true, userspaceNextSubmit{}, false
}
endpointState := s.endpoints[pending.endpoint]
if endpointState != nil && endpointState.active == seq {
endpointState.active = 0
}
delete(s.pending, seq)
if endpointState != nil {
for len(endpointState.queued) > 0 {
nextSequence := endpointState.queued[0]
endpointState.queued = endpointState.queued[1:]
nextPending, nextFound := s.pending[nextSequence]
if !nextFound {
continue
}
nextPending.started = true
s.pending[nextSequence] = nextPending
endpointState.active = nextSequence
followUp = userspaceNextSubmit{
sequence: nextSequence,
command: nextPending.command,
}
hasFollowUp = true
break
}
if endpointState.active == 0 && len(endpointState.queued) == 0 {
delete(s.endpoints, pending.endpoint)
}
}
drained = pending.drained
unlinked := pending.unlinked
s.access.Unlock()
if drained != nil {
close(drained)
}
return !unlinked, followUp, hasFollowUp
}
func (s *userspaceURBSession) abortPendingSubmits() {
var (
activeEndpoints []uint8
drained []chan struct{}
)
s.access.Lock()
seen := make(map[uint8]struct{})
for seq, pending := range s.pending {
if !pending.started {
delete(s.pending, seq)
if pending.drained != nil {
drained = append(drained, pending.drained)
}
continue
}
if !pending.unlinked {
seen[pending.endpoint] = struct{}{}
}
pending.unlinked = true
s.pending[seq] = pending
}
for endpoint := range s.endpoints {
endpointState := s.endpoints[endpoint]
if endpointState != nil {
endpointState.queued = nil
}
}
s.access.Unlock()
for _, drainedChannel := range drained {
close(drainedChannel)
}
activeEndpoints = make([]uint8, 0, len(seen))
for endpoint := range seen {
activeEndpoints = append(activeEndpoints, endpoint)
}
slices.Sort(activeEndpoints)
for _, endpoint := range activeEndpoints {
err := s.engine.AbortEndpoint(endpoint)
if err != nil {
s.logger.Debug("abort endpoint 0x", hex8(endpoint), ": ", err)
}
}
}
func removeQueuedSequence(queue []uint32, sequence uint32) []uint32 {
for index, current := range queue {
if current != sequence {
continue
}
return append(queue[:index], queue[index+1:]...)
}
return queue
}
func submitScheduleEndpoint(command SubmitCommand) uint8 {
if command.Header.Endpoint == 0 {
return 0
}
return commandEndpoint(command)
}
func commandEndpoint(command SubmitCommand) uint8 {
endpoint := uint8(command.Header.Endpoint & 0x0f)
if command.Header.Direction == USBIPDirIn {
endpoint |= 0x80
}
return endpoint
}