usbip: unify linux teardown and handoff lifecycle

This commit is contained in:
世界
2026-05-15 19:20:02 +08:00
parent 55b520235f
commit 0eae853c5f
4 changed files with 124 additions and 35 deletions
+33 -18
View File
@@ -33,9 +33,12 @@ type kernelHandoffSession struct {
done chan struct{}
doneOnce sync.Once
runErr error
startOnce sync.Once
closeOnce sync.Once
closeErr error
stateAccess sync.Mutex
started bool
closed bool
}
func newKernelHandoffSession(ctx context.Context, conn net.Conn, logger log.ContextLogger, side string, busid string) (*kernelHandoffSession, error) {
@@ -103,6 +106,9 @@ func (h *kernelHandoffSession) Err() error {
}
func (h *kernelHandoffSession) Close() error {
h.stateAccess.Lock()
h.closed = true
h.stateAccess.Unlock()
h.closeOnce.Do(func() {
h.closeErr = E.Errors(
h.closeKernelFD(),
@@ -126,22 +132,31 @@ func (h *kernelHandoffSession) markDone(err error) {
}
func (h *kernelHandoffSession) Start() error {
h.startOnce.Do(func() {
if h.relayConn == nil {
err := h.conn.Close()
if err != nil && !E.IsClosedOrCanceled(err) {
h.logger.Debug("close usbip ", h.side, " userspace socket ", h.busid, ": ", err)
}
h.conn = nil
monitorFile := h.monitorFile
h.monitorFile = nil
go h.runDirect(h.ctx, h.logger, h.side, h.busid, monitorFile)
return
h.stateAccess.Lock()
if h.started || h.closed {
h.stateAccess.Unlock()
return nil
}
h.started = true
conn := h.conn
relayConn := h.relayConn
monitorFile := h.monitorFile
h.stateAccess.Unlock()
if relayConn == nil {
err := common.Close(conn)
if err != nil && !E.IsClosedOrCanceled(err) {
h.logger.Debug("close usbip ", h.side, " userspace socket ", h.busid, ": ", err)
}
relayConn := h.relayConn
h.relayConn = nil
go h.runRelay(h.ctx, h.logger, h.side, h.busid, relayConn)
})
h.stateAccess.Lock()
if h.conn == conn {
h.conn = nil
}
h.stateAccess.Unlock()
go h.runDirect(h.ctx, h.logger, h.side, h.busid, monitorFile)
return nil
}
go h.runRelay(h.ctx, h.logger, h.side, h.busid, conn, relayConn)
return nil
}
@@ -182,8 +197,8 @@ func (h *kernelHandoffSession) runDirect(ctx context.Context, logger log.Context
}
}
func (h *kernelHandoffSession) runRelay(ctx context.Context, logger log.ContextLogger, side string, busid string, relayConn net.Conn) {
err := sBufio.CopyConn(ctx, h.conn, relayConn)
func (h *kernelHandoffSession) runRelay(ctx context.Context, logger log.ContextLogger, side string, busid string, conn net.Conn, relayConn net.Conn) {
err := sBufio.CopyConn(ctx, conn, relayConn)
var runErr error
switch {
case err == nil:
+21 -17
View File
@@ -168,7 +168,7 @@ func (h *linuxExportHost) Close() error {
h.exports = make(map[string]*linuxExport)
h.access.Unlock()
for _, exp := range exports {
releaseErr := h.releaseExport(exp, exp.shouldRestoreCurrentDevice())
releaseErr := h.releaseExport(exp)
if releaseErr != nil {
h.logger.Warn("rollback ", exp.busid, ": ", releaseErr)
}
@@ -338,7 +338,7 @@ func (h *linuxExportHost) Reconcile(ctx context.Context, isBusy func(busid strin
}
for _, exp := range plan.toRelease {
releaseErr := h.releaseExport(exp, false)
releaseErr := h.releaseExport(exp)
if releaseErr != nil {
h.logger.Warn("release ", exp.busid, ": ", releaseErr)
reconcileErrors = append(reconcileErrors, E.Cause(releaseErr, "release ", exp.busid))
@@ -411,7 +411,7 @@ func (h *linuxExportHost) FinishImport(ctx context.Context, busid string) (bool,
if !ok || !exp.stale {
return false, nil
}
releaseErr := h.releaseExport(exp, false)
releaseErr := h.releaseExport(exp)
h.access.Lock()
current, stillPresent := h.exports[busid]
if stillPresent && current == exp {
@@ -569,7 +569,7 @@ func (h *linuxExportHost) bindOneOnce(d *sysfsDevice) (*linuxExport, error) {
return h.newExport(*d, true, driver), nil
}
func (h *linuxExportHost) releaseExport(exp *linuxExport, restore bool) error {
func (h *linuxExportHost) releaseExport(exp *linuxExport) error {
if !exp.managed {
h.logger.Info("stopped tracking ", exp.busid, " on usbip-host")
return nil
@@ -585,14 +585,18 @@ func (h *linuxExportHost) releaseExport(exp *linuxExport, restore bool) error {
}
}
err := writeSysfs(filepath.Join(sysUsbipHostDriver, "unbind"), exp.busid)
if err != nil && !os.IsNotExist(err) && !(isMissingUSBDeviceError(err) && !restore) {
if err != nil && !os.IsNotExist(err) && !isMissingUSBDeviceError(err) {
return err
}
err = writeSysfs(filepath.Join(sysUsbipHostDriver, "match_busid"), "del "+exp.busid)
if err != nil {
return err
}
if !restore {
restoreCurrentDevice, err := h.shouldRestoreCurrentDevice(exp)
if err != nil {
return err
}
if !restoreCurrentDevice {
h.logger.Info("removed export state for ", exp.busid)
return nil
}
@@ -608,6 +612,17 @@ func (h *linuxExportHost) releaseExport(exp *linuxExport, restore bool) error {
return nil
}
func (h *linuxExportHost) shouldRestoreCurrentDevice(exp *linuxExport) (bool, error) {
descriptor, err := readSysfsDevice(exp.busid, filepath.Join(sysBusUSBDevices, exp.busid))
if err != nil {
if os.IsNotExist(err) || isMissingUSBDeviceError(err) {
return false, nil
}
return false, E.Cause(err, "read current device ", exp.busid)
}
return exp.identity.Equal(newLinuxExportIdentity(descriptor)), nil
}
func (h *linuxExportHost) newExport(descriptor sysfsDevice, managed bool, originalDriver string) *linuxExport {
return &linuxExport{
busid: descriptor.BusID,
@@ -734,17 +749,6 @@ func (e *linuxExport) NewServerDataSession(ctx context.Context, conn net.Conn) (
return handoff, nil
}
func (e *linuxExport) shouldRestoreCurrentDevice() bool {
if e.originalDriver == "" || e.stale {
return false
}
descriptor, err := readSysfsDevice(e.busid, filepath.Join(sysBusUSBDevices, e.busid))
if err != nil {
return false
}
return e.identity.Equal(newLinuxExportIdentity(descriptor))
}
type linuxImportHost struct {
logger log.ContextLogger
+68
View File
@@ -16,6 +16,7 @@ import (
"time"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/stretchr/testify/require"
"golang.org/x/sys/unix"
@@ -294,6 +295,42 @@ func TestUSBIPConnHandoffDirectTCP(t *testing.T) {
}
}
func TestUSBIPConnHandoffCloseBeforeStartIsSafe(t *testing.T) {
t.Parallel()
listener, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
defer listener.Close()
accepted := make(chan net.Conn, 1)
go func() {
conn, _ := listener.Accept()
accepted <- conn
}()
conn, err := net.Dial("tcp", listener.Addr().String())
require.NoError(t, err)
acceptedConn := <-accepted
defer acceptedConn.Close()
handoff, err := newKernelHandoffSession(context.Background(), conn, newTestLogger(t), "test", "close-before-start")
require.NoError(t, err)
require.NoError(t, handoff.Close())
require.NoError(t, handoff.Start())
select {
case <-handoff.Done():
default:
t.Fatal("expected close-before-start handoff to be done")
}
setConnDeadline(t, acceptedConn)
buffer := make([]byte, 1)
_, err = acceptedConn.Read(buffer)
require.ErrorIs(t, err, io.EOF)
}
func TestUSBIPConnHandoffRelaySocketpairCopies(t *testing.T) {
t.Parallel()
@@ -367,3 +404,34 @@ func TestUSBIPLinuxSmoke(t *testing.T) {
require.NoError(t, err)
require.Equal(t, "usb", driver)
}
func TestUSBIPLinuxReconcileReleaseRestoresOriginalDriver(t *testing.T) {
requireRoot(t)
requireUSBIPHost(t)
requireVHCI(t)
gadget := newTestUSBGadget(t)
host := newLinuxExportHost(newTestLogger(t), []option.USBIPDeviceMatch{{BusID: gadget.busid}})
require.NoError(t, host.Start(context.Background()))
snapshot, released, err := host.Reconcile(context.Background(), func(string) bool { return false })
require.NoError(t, err)
require.Empty(t, released)
_, exported := snapshot[gadget.busid]
require.True(t, exported)
driver, err := currentDriver(gadget.busid)
require.NoError(t, err)
require.Equal(t, "usbip-host", driver)
host.matches = nil
snapshot, released, err = host.Reconcile(context.Background(), func(string) bool { return false })
require.NoError(t, err)
require.Equal(t, []string{gadget.busid}, released)
require.Empty(t, snapshot)
driver, err = currentDriver(gadget.busid)
require.NoError(t, err)
require.Equal(t, "usb", driver)
}
+2
View File
@@ -331,6 +331,8 @@ func (s *ServerService) handleImportReserved(conn net.Conn, busid string, export
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.sessionsWG.Add(1)
s.sessionsAccess.Unlock()