1d358ab698
Reconcile snapshots the export table, works on the copy unlocked, and commits it wholesale; FinishImport ran outside reconcileAccess and its table updates could be overwritten by the stale commit. On darwin that leaked the re-captured IOUSBHostDevice handle (device unusable until process exit) and left an export whose handle was closed. Both call sites now take reconcileAccess.
368 lines
9.5 KiB
Go
368 lines
9.5 KiB
Go
//go:build linux || (darwin && cgo) || windows
|
|
|
|
package usbip
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/sagernet/sing-box/adapter"
|
|
boxService "github.com/sagernet/sing-box/adapter/service"
|
|
"github.com/sagernet/sing-box/common/listener"
|
|
C "github.com/sagernet/sing-box/constant"
|
|
"github.com/sagernet/sing-box/log"
|
|
"github.com/sagernet/sing-box/option"
|
|
"github.com/sagernet/sing/common"
|
|
E "github.com/sagernet/sing/common/exceptions"
|
|
N "github.com/sagernet/sing/common/network"
|
|
)
|
|
|
|
type ServerService struct {
|
|
boxService.Adapter
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
logger log.ContextLogger
|
|
listener *listener.Listener
|
|
matches []option.USBIPDeviceMatch
|
|
host ExportHost
|
|
ledger *exportLedger
|
|
|
|
reconcileAccess sync.Mutex
|
|
|
|
sessionsAccess sync.Mutex
|
|
sessions map[DataSession]struct{}
|
|
sessionsClosed bool
|
|
}
|
|
|
|
func NewServerService(ctx context.Context, logger log.ContextLogger, tag string, options option.USBIPServerServiceOptions) (adapter.Service, error) {
|
|
if len(options.Devices) == 0 {
|
|
return nil, E.New("devices: at least one match is required")
|
|
}
|
|
for i, m := range options.Devices {
|
|
if m.IsZero() {
|
|
return nil, E.New("devices[", i, "]: at least one of busid/vendor_id/product_id/serial is required")
|
|
}
|
|
}
|
|
if options.ListenPort == 0 {
|
|
options.ListenPort = DefaultPort
|
|
}
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
host, err := newPlatformExportHost(ctx, logger, options.Devices)
|
|
if err != nil {
|
|
cancel()
|
|
return nil, err
|
|
}
|
|
return &ServerService{
|
|
Adapter: boxService.NewAdapter(C.TypeUSBIPServer, tag),
|
|
ctx: ctx,
|
|
cancel: cancel,
|
|
logger: logger,
|
|
matches: options.Devices,
|
|
host: host,
|
|
ledger: newExportLedger(logger, time.Now),
|
|
sessions: make(map[DataSession]struct{}),
|
|
listener: listener.New(listener.Options{
|
|
Context: ctx,
|
|
Logger: logger,
|
|
Network: []string{N.NetworkTCP},
|
|
Listen: options.ListenOptions,
|
|
}),
|
|
}, nil
|
|
}
|
|
|
|
func (s *ServerService) Start(stage adapter.StartStage) (err error) {
|
|
if stage != adapter.StartStateStart {
|
|
return nil
|
|
}
|
|
defer func() {
|
|
if err != nil {
|
|
s.cancel()
|
|
_ = s.host.Close()
|
|
}
|
|
}()
|
|
err = s.host.Start()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
events, err := s.host.Events()
|
|
if err != nil {
|
|
return E.Cause(err, "subscribe topology events")
|
|
}
|
|
err = s.reconcileAndBroadcast(false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tcpListener, err := s.listener.ListenTCP()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
go s.acceptLoop(tcpListener)
|
|
go s.eventLoop(events)
|
|
return nil
|
|
}
|
|
|
|
func (s *ServerService) Close() error {
|
|
if s.cancel != nil {
|
|
s.cancel()
|
|
}
|
|
|
|
s.sessionsAccess.Lock()
|
|
s.sessionsClosed = true
|
|
sessions := make([]DataSession, 0, len(s.sessions))
|
|
for session := range s.sessions {
|
|
sessions = append(sessions, session)
|
|
}
|
|
s.sessionsAccess.Unlock()
|
|
|
|
for _, conn := range s.ledger.CloseAllSubscribers() {
|
|
_ = conn.Close()
|
|
}
|
|
err := common.Close(common.PtrOrNil(s.listener))
|
|
|
|
for _, session := range sessions {
|
|
_ = session.Close()
|
|
}
|
|
|
|
s.reconcileAccess.Lock()
|
|
defer s.reconcileAccess.Unlock()
|
|
_ = s.host.Close()
|
|
s.ledger.ResetForClose()
|
|
return err
|
|
}
|
|
|
|
func (s *ServerService) eventLoop(events <-chan struct{}) {
|
|
for {
|
|
select {
|
|
case <-s.ctx.Done():
|
|
return
|
|
case _, ok := <-events:
|
|
if !ok {
|
|
return
|
|
}
|
|
}
|
|
err := s.reconcileAndBroadcast(true)
|
|
if err != nil {
|
|
s.logger.Warn("reconcile exports: ", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *ServerService) tearDownPreparedSession(busid string, session DataSession) {
|
|
_ = session.Close()
|
|
<-session.Done()
|
|
released := s.finishImport(busid)
|
|
if released {
|
|
err := s.reconcileAndBroadcast(true)
|
|
if err != nil {
|
|
s.logger.Debug("reconcile after ", busid, ": ", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// finishImport must hold reconcileAccess: FinishImport mutates the
|
|
// host's export table, and Reconcile snapshots that table, works on
|
|
// the copy unlocked, then commits it wholesale — an unserialized
|
|
// FinishImport in that window is overwritten by the stale commit
|
|
// (leaking the re-captured device handle on darwin).
|
|
func (s *ServerService) finishImport(busid string) bool {
|
|
s.reconcileAccess.Lock()
|
|
defer s.reconcileAccess.Unlock()
|
|
released, err := s.host.FinishImport(busid)
|
|
if err != nil {
|
|
s.logger.Debug("finish import ", busid, ": ", err)
|
|
}
|
|
s.ledger.ReleaseImport(busid, released)
|
|
return released
|
|
}
|
|
|
|
func (s *ServerService) reconcileAndBroadcast(notify bool) error {
|
|
s.reconcileAccess.Lock()
|
|
defer s.reconcileAccess.Unlock()
|
|
if s.ctx.Err() != nil {
|
|
return nil
|
|
}
|
|
snapshot, released, err := s.host.Reconcile(s.ledger.IsReserved)
|
|
s.ledger.ApplyHostSnapshot(snapshot, released)
|
|
if notify {
|
|
s.ledger.BroadcastIfChanged()
|
|
} else {
|
|
s.ledger.SeedBroadcastState()
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (s *ServerService) handleStandardConn(conn net.Conn, header OpHeader) {
|
|
closeConn := true
|
|
defer func() {
|
|
if closeConn {
|
|
_ = conn.Close()
|
|
}
|
|
}()
|
|
switch header.Code {
|
|
case OpReqDevList:
|
|
entries := s.buildDevListEntries()
|
|
err := WriteOpRepDevList(conn, entries)
|
|
if err != nil {
|
|
s.logger.Debug("write devlist: ", err)
|
|
}
|
|
case OpReqImport:
|
|
busid, err := ReadOpReqImportBody(conn)
|
|
if err != nil {
|
|
s.logger.Debug("read import body: ", err)
|
|
break
|
|
}
|
|
// The connection becomes a data session below; drop the handshake
|
|
// read deadline so URB traffic is not bounded by it.
|
|
_ = conn.SetReadDeadline(time.Time{})
|
|
closeConn = !s.handleImportBusID(conn, busid)
|
|
default:
|
|
s.logger.Debug(fmt.Sprintf("unknown opcode 0x%04x", header.Code))
|
|
}
|
|
}
|
|
|
|
func (s *ServerService) handleControlConn(conn net.Conn) {
|
|
defer conn.Close()
|
|
var cr controlReader
|
|
helloMessage, err := cr.read(conn)
|
|
if err != nil {
|
|
s.logger.Debug("read control hello: ", err)
|
|
return
|
|
}
|
|
hello := helloMessage.Frame
|
|
if hello.Type != controlFrameHello {
|
|
s.logger.Debug("unexpected control frame ", hello.Type, " before hello")
|
|
return
|
|
}
|
|
if hello.Version != controlProtocolVersion {
|
|
s.logger.Debug("unsupported control version ", hello.Version)
|
|
return
|
|
}
|
|
// The handshake read deadline from dispatchConn has served its purpose;
|
|
// readControlConn installs its own per-iteration idle deadline.
|
|
_ = conn.SetReadDeadline(time.Time{})
|
|
sub := s.ledger.Subscribe(conn)
|
|
defer s.ledger.Unsubscribe(sub)
|
|
_ = conn.SetWriteDeadline(time.Now().Add(controlWriteTimeout))
|
|
err = writeControlMessage(conn, controlFrame{
|
|
Type: controlFrameAck,
|
|
Version: controlProtocolVersion,
|
|
}, nil)
|
|
_ = conn.SetWriteDeadline(time.Time{})
|
|
if err != nil {
|
|
s.logger.Debug("write control ack: ", err)
|
|
return
|
|
}
|
|
readDone := make(chan struct{})
|
|
go s.readControlConn(sub, readDone)
|
|
for {
|
|
select {
|
|
case <-s.ctx.Done():
|
|
return
|
|
case <-readDone:
|
|
return
|
|
case message := <-sub.send:
|
|
_ = conn.SetWriteDeadline(time.Now().Add(controlWriteTimeout))
|
|
err = writeControlMessage(conn, message.Frame, message.Payload)
|
|
_ = conn.SetWriteDeadline(time.Time{})
|
|
if err != nil {
|
|
s.logger.Debug("write control frame: ", err)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *ServerService) buildDevListEntries() []DeviceEntry {
|
|
exports := s.ledger.AvailableExports()
|
|
if len(exports) == 0 {
|
|
return nil
|
|
}
|
|
entries := make([]DeviceEntry, 0, len(exports))
|
|
for _, export := range exports {
|
|
snapshot := export.Snapshot(false)
|
|
if snapshot.State != deviceStateAvailable {
|
|
continue
|
|
}
|
|
entries = append(entries, snapshot.Entry)
|
|
}
|
|
return entries
|
|
}
|
|
|
|
func (s *ServerService) handleImportBusID(conn net.Conn, busid string) bool {
|
|
// Serialize the reservation against an in-flight Reconcile pass: both take
|
|
// reconcileAccess before inventoryAccess, so a reserve cannot interleave a
|
|
// pass that would otherwise release and close a just-reserved device.
|
|
s.reconcileAccess.Lock()
|
|
export, ok, reason := s.ledger.TryReserveForImport(busid)
|
|
s.reconcileAccess.Unlock()
|
|
if !ok {
|
|
s.logger.Info("import rejected (", busid, ": ", reason, ")")
|
|
_ = WriteOpRepImport(conn, OpRepImport, OpStatusError, nil)
|
|
return false
|
|
}
|
|
return s.handleImportReserved(conn, busid, export)
|
|
}
|
|
|
|
func (s *ServerService) handleImportReserved(conn net.Conn, busid string, export Export) bool {
|
|
info, err := export.DeviceInfo()
|
|
if err != nil {
|
|
s.ledger.ReleaseImport(busid, false)
|
|
s.logger.Warn("refresh ", busid, ": ", err)
|
|
_ = WriteOpRepImport(conn, OpRepImport, OpStatusError, nil)
|
|
return false
|
|
}
|
|
session, err := export.NewServerDataSession(s.ctx, conn)
|
|
if err != nil {
|
|
s.ledger.ReleaseImport(busid, false)
|
|
s.logger.Warn("open data session ", busid, ": ", err)
|
|
_ = WriteOpRepImport(conn, OpRepImport, OpStatusError, nil)
|
|
return false
|
|
}
|
|
s.ledger.BroadcastIfChanged()
|
|
err = WriteOpRepImport(conn, OpRepImport, OpStatusOK, &info)
|
|
if err != nil {
|
|
s.logger.Warn("reply import ", busid, ": ", err)
|
|
s.tearDownPreparedSession(busid, session)
|
|
return false
|
|
}
|
|
|
|
s.sessionsAccess.Lock()
|
|
if s.sessionsClosed {
|
|
s.sessionsAccess.Unlock()
|
|
s.tearDownPreparedSession(busid, session)
|
|
return false
|
|
}
|
|
// Close may observe a prepared session before Start runs, so
|
|
// DataSession implementations must treat Close-before-Start as valid.
|
|
s.sessions[session] = struct{}{}
|
|
s.sessionsAccess.Unlock()
|
|
|
|
err = session.Start()
|
|
if err != nil {
|
|
s.sessionsAccess.Lock()
|
|
delete(s.sessions, session)
|
|
s.sessionsAccess.Unlock()
|
|
s.logger.Warn("start data session ", busid, ": ", err)
|
|
s.tearDownPreparedSession(busid, session)
|
|
return false
|
|
}
|
|
s.logger.Info("attached ", busid, " to remote ", conn.RemoteAddr())
|
|
go func() {
|
|
<-session.Done()
|
|
s.sessionsAccess.Lock()
|
|
delete(s.sessions, session)
|
|
s.sessionsAccess.Unlock()
|
|
released := s.finishImport(busid)
|
|
if released {
|
|
err := s.reconcileAndBroadcast(true)
|
|
if err != nil {
|
|
s.logger.Debug("reconcile after ", busid, ": ", err)
|
|
}
|
|
}
|
|
}()
|
|
return true
|
|
}
|