Files
sing-box/service/usbip/host_linux.go
T
世界 37065daccf usbip: modernize loops and drop unused test helpers
Address `golangci-lint` modernize and unused warnings:
- replace map copy loops with `maps.Copy`
- collapse `if x > y { x = y }` clamps to `min`/`max`
- switch `for i := 0; i < n; i++` to `for i := range n`
- use `t.Context()` in the relay handoff test
- delete the unused `removeOnce` field and `openBinaryDevice` helper
2026-06-09 10:42:29 +08:00

604 lines
16 KiB
Go

//go:build linux
package usbip
import (
"context"
"errors"
"fmt"
"maps"
"net"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"time"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"golang.org/x/sys/unix"
)
func newPlatformExportHost(logger log.ContextLogger, matches []option.USBIPDeviceMatch) (ExportHost, error) {
return newLinuxExportHost(logger, matches), nil
}
func newPlatformImportHost(logger log.ContextLogger) (ImportHost, error) {
return &linuxImportHost{
logger: logger,
ports: make(map[vhciPortKey]struct{}),
}, nil
}
func isMissingUSBDeviceError(err error) bool {
return errors.Is(err, unix.ENOENT) || errors.Is(err, unix.ENODEV)
}
func linuxUSBIPStatusReason(status int) string {
switch status {
case usbipStatusAvailable:
return "available"
case usbipStatusUsed:
return "used"
case usbipStatusError:
return "error"
default:
return fmt.Sprintf("status=0x%08x", uint32(status))
}
}
type linuxExportHost struct {
logger log.ContextLogger
matches []option.USBIPDeviceMatch
access sync.Mutex
exports map[string]*linuxExport
}
func newLinuxExportHost(logger log.ContextLogger, matches []option.USBIPDeviceMatch) *linuxExportHost {
return &linuxExportHost{
logger: logger,
matches: matches,
exports: make(map[string]*linuxExport),
}
}
func (h *linuxExportHost) Start(ctx context.Context) error {
return ensureKernelPath(sysUsbipHostDriver, "usbip-host", "usbip-host driver")
}
func (h *linuxExportHost) Close() error {
h.access.Lock()
exports := h.exports
h.exports = make(map[string]*linuxExport)
h.access.Unlock()
for _, exp := range exports {
_, statErr := os.Stat(filepath.Join(sysBusUSBDevices, exp.busid))
restore := statErr == nil
releaseErr := h.releaseExport(exp, restore)
if releaseErr != nil {
h.logger.Warn("rollback ", exp.busid, ": ", releaseErr)
}
}
return nil
}
func (h *linuxExportHost) Events(ctx context.Context) (<-chan struct{}, error) {
ch := make(chan struct{}, 1)
go h.ueventLoop(ctx, ch)
return ch, nil
}
func (h *linuxExportHost) ueventLoop(ctx context.Context, ch chan<- struct{}) {
defer close(ch)
signal := func() {
select {
case ch <- struct{}{}:
default:
}
}
backoff := ueventListenerBackoffInitial
for {
listener, err := newUEventListener()
if err != nil {
if ctx.Err() != nil {
return
}
h.logger.Warn("open uevent listener: ", err)
if !sleepCtx(ctx, backoff) {
return
}
backoff *= 2
if backoff > ueventListenerBackoffMax {
backoff = ueventListenerBackoffMax
}
continue
}
backoff = ueventListenerBackoffInitial
listenerDone := make(chan struct{})
go func() {
select {
case <-ctx.Done():
_ = listener.Close()
case <-listenerDone:
}
}()
signal()
for {
err = listener.WaitUSBEvent()
if err != nil {
close(listenerDone)
_ = listener.Close()
if ctx.Err() != nil {
return
}
h.logger.Warn("read uevent: ", err)
if !sleepCtx(ctx, backoff) {
return
}
backoff *= 2
if backoff > ueventListenerBackoffMax {
backoff = ueventListenerBackoffMax
}
break
}
signal()
}
}
}
const (
ueventListenerBackoffInitial = time.Second
ueventListenerBackoffMax = 30 * time.Second
)
func (h *linuxExportHost) Reconcile(ctx context.Context, isBusy func(busid string) bool) (map[string]Export, []string, error) {
devices, err := listUSBDevices()
if err != nil {
return h.snapshotSelf(), nil, E.Cause(err, "enumerate usb devices")
}
desired := make(map[string]sysfsDevice)
present := make(map[string]struct{}, len(devices))
for i := range devices {
present[devices[i].BusID] = struct{}{}
}
for _, m := range h.matches {
for i := range devices {
deviceKey := DeviceKey{
BusID: devices[i].BusID,
VendorID: devices[i].VendorID,
ProductID: devices[i].ProductID,
Serial: devices[i].Serial,
}
if !matches(m, deviceKey) {
continue
}
path := devices[i].Path
isVHCIImport := strings.Contains(path, "vhci_hcd")
if !isVHCIImport {
realPath, err := filepath.EvalSymlinks(path)
if err == nil {
isVHCIImport = strings.Contains(realPath, "vhci_hcd")
}
}
if isVHCIImport {
h.logger.Debug("skip vhci-imported device ", devices[i].BusID, " matched by ", describeMatch(m))
continue
}
if devices[i].DeviceClass == 0x09 {
h.logger.Warn("skip hub device ", devices[i].BusID, " matched by ", describeMatch(m))
continue
}
desired[devices[i].BusID] = devices[i]
}
}
h.access.Lock()
current := make(map[string]*linuxExport, len(h.exports))
maps.Copy(current, h.exports)
h.access.Unlock()
for busid, device := range desired {
if _, ok := current[busid]; ok {
continue
}
exp, bindErr := h.bindOne(&device)
if bindErr != nil {
return h.snapshotSelf(), nil, E.Cause(bindErr, "bind ", busid)
}
h.access.Lock()
h.exports[busid] = exp
h.access.Unlock()
}
var released []string
for busid, exp := range current {
if _, ok := desired[busid]; ok {
continue
}
_, restore := present[busid]
err := h.releaseExport(exp, restore)
if err != nil {
h.logger.Warn("release ", busid, ": ", err)
}
h.access.Lock()
delete(h.exports, busid)
h.access.Unlock()
released = append(released, busid)
}
return h.snapshotSelf(), released, nil
}
func (h *linuxExportHost) FinishImport(ctx context.Context, busid string) (bool, error) {
err := writeSysfs(filepath.Join(sysBusUSBDevices, busid, "usbip_sockfd"), "-1")
if err != nil && !os.IsNotExist(err) && !isMissingUSBDeviceError(err) {
h.logger.Debug("release ", busid, " from usbip-host: ", err)
}
waitForUsbipStatusCleared(ctx, busid)
return false, nil
}
func (h *linuxExportHost) snapshotSelf() map[string]Export {
h.access.Lock()
defer h.access.Unlock()
out := make(map[string]Export, len(h.exports))
for busid, exp := range h.exports {
out[busid] = exp
}
return out
}
func (h *linuxExportHost) bindOne(d *sysfsDevice) (*linuxExport, error) {
var (
exp *linuxExport
err error
)
for attempt := range 2 {
exp, err = h.bindOneOnce(d)
if err == nil {
return exp, nil
}
if attempt > 0 || !errors.Is(err, unix.ENODEV) {
break
}
h.logger.Warn("reset usbip-host after bind failure on ", d.BusID, ": ", err)
h.access.Lock()
active := len(h.exports) > 0
h.access.Unlock()
if active {
return nil, E.Cause(E.New("active usbip-host exports are present"), "reset usbip-host after bind failure")
}
resetErr := reloadHostDriver()
if resetErr != nil {
return nil, E.Cause(resetErr, "reset usbip-host after bind failure")
}
}
return nil, err
}
func (h *linuxExportHost) bindOneOnce(d *sysfsDevice) (*linuxExport, error) {
driver, err := currentDriver(d.BusID)
if err != nil {
return nil, err
}
if driver == "usbip-host" {
h.logger.Info("device ", d.BusID, " already bound to usbip-host; co-opting")
return h.newExport(*d, false, ""), nil
}
if driver != "" {
err = writeSysfs(filepath.Join("/sys/bus/usb/drivers", driver, "unbind"), d.BusID)
if err != nil {
return nil, E.Cause(err, "unbind from ", driver)
}
}
matchBusIDPath := filepath.Join(sysUsbipHostDriver, "match_busid")
err = writeSysfs(matchBusIDPath, "add "+d.BusID)
if err != nil {
if driver != "" {
_ = writeSysfs(filepath.Join("/sys/bus/usb/drivers", driver, "bind"), d.BusID)
}
return nil, E.Cause(err, "match_busid add")
}
err = writeSysfs(filepath.Join(sysUsbipHostDriver, "bind"), d.BusID)
if err != nil {
_ = writeSysfs(matchBusIDPath, "del "+d.BusID)
if driver != "" {
_ = writeSysfs(filepath.Join("/sys/bus/usb/drivers", driver, "bind"), d.BusID)
}
return nil, E.Cause(err, "bind to usbip-host")
}
previousDriver := driver
if previousDriver == "" {
previousDriver = "(no driver)"
}
h.logger.Info("exported ", d.BusID, " (previously on ", previousDriver, ")")
return h.newExport(*d, true, driver), nil
}
func (h *linuxExportHost) releaseExport(exp *linuxExport, restore bool) error {
if !exp.managed {
h.logger.Info("stopped tracking ", exp.busid, " on usbip-host")
return nil
}
status, statusErr := readUsbipStatus(exp.busid)
if statusErr != nil && !os.IsNotExist(statusErr) && !isMissingUSBDeviceError(statusErr) {
return statusErr
}
if statusErr == nil && status == usbipStatusUsed {
err := writeSysfs(filepath.Join(sysBusUSBDevices, exp.busid, "usbip_sockfd"), "-1")
if err != nil && !os.IsNotExist(err) {
return err
}
}
err := writeSysfs(filepath.Join(sysUsbipHostDriver, "unbind"), exp.busid)
if err != nil && !os.IsNotExist(err) && !(isMissingUSBDeviceError(err) && !restore) {
return err
}
err = writeSysfs(filepath.Join(sysUsbipHostDriver, "match_busid"), "del "+exp.busid)
if err != nil {
return err
}
if !restore {
h.logger.Info("removed export state for disappeared device ", exp.busid)
return nil
}
if exp.originalDriver == "" {
h.logger.Info("released ", exp.busid, " from usbip-host")
return nil
}
err = writeSysfs(filepath.Join("/sys/bus/usb/drivers", exp.originalDriver, "bind"), exp.busid)
if err != nil {
return err
}
h.logger.Info("restored ", exp.busid, " to ", exp.originalDriver)
return nil
}
func (h *linuxExportHost) newExport(descriptor sysfsDevice, managed bool, originalDriver string) *linuxExport {
return &linuxExport{
busid: descriptor.BusID,
descriptor: descriptor,
managed: managed,
originalDriver: originalDriver,
logger: h.logger,
}
}
// linuxExport caches the bind-time descriptor because it is immutable
// post-enumeration; Snapshot only re-reads usbip_status.
type linuxExport struct {
busid string
descriptor sysfsDevice
managed bool
originalDriver string
logger log.ContextLogger
}
func (e *linuxExport) BusID() string {
return e.busid
}
func (e *linuxExport) Snapshot(ctx context.Context, busy bool) ExportSnapshot {
stableID := "linux-busid:" + e.descriptor.BusID
if e.descriptor.Serial != "" {
stableID = fmt.Sprintf("usb:%04x:%04x:%s", e.descriptor.VendorID, e.descriptor.ProductID, e.descriptor.Serial)
}
status, statusErr := readUsbipStatus(e.busid)
var state, reason string
switch {
case statusErr != nil:
state = deviceStateUnavailable
reason = statusErr.Error()
case busy:
status = usbipStatusUsed
state = deviceStateBusy
reason = linuxUSBIPStatusReason(status)
case status == usbipStatusAvailable:
state = deviceStateAvailable
reason = linuxUSBIPStatusReason(status)
case status == usbipStatusUsed:
state = deviceStateBusy
reason = linuxUSBIPStatusReason(status)
default:
state = deviceStateUnavailable
reason = linuxUSBIPStatusReason(status)
}
return ExportSnapshot{
Entry: DeviceEntry{
Info: e.descriptor.toProtocol(),
Interfaces: e.descriptor.Interfaces,
Serial: e.descriptor.Serial,
},
Backend: backendIDLinuxSysfs,
StableID: stableID,
State: state,
StatusReason: reason,
RawStatus: status,
}
}
func (e *linuxExport) LeaseCheck(ctx context.Context) (bool, string) {
status, err := readUsbipStatus(e.busid)
if err != nil {
return false, err.Error()
}
if status != usbipStatusAvailable {
return false, linuxUSBIPStatusReason(status)
}
return true, ""
}
func (e *linuxExport) DeviceInfo(ctx context.Context) (DeviceInfoTruncated, error) {
return e.descriptor.toProtocol(), nil
}
func (e *linuxExport) NewServerDataSession(ctx context.Context, conn net.Conn) (DataSession, error) {
handoff, err := newKernelHandoffSession(ctx, conn, e.logger, "server", e.busid)
if err != nil {
return nil, E.Cause(err, "prepare handoff")
}
mode := "direct"
if handoff.relayConn != nil {
mode = "relay"
}
e.logger.Debug("usbip server handoff ", e.busid, ": ", mode)
err = writeSysfs(filepath.Join(sysBusUSBDevices, e.busid, "usbip_sockfd"), strconv.Itoa(int(handoff.file.Fd())))
if err != nil {
_ = handoff.Close()
return nil, E.Cause(err, "hand off ", e.busid, " to kernel")
}
closeErr := handoff.closeKernelFD()
if closeErr != nil {
e.logger.Debug("close kernel fd ", e.busid, ": ", closeErr)
}
return handoff, nil
}
type linuxImportHost struct {
logger log.ContextLogger
controllers []vhciController
portsAccess sync.Mutex
ports map[vhciPortKey]struct{}
}
func (h *linuxImportHost) Start(ctx context.Context) error {
controllers, err := discoverVHCIControllers()
if err != nil {
return E.Cause(err, "discover vhci controllers")
}
if len(controllers) == 0 {
err = ensureKernelPath(sysVHCIControllerV0, "vhci-hcd", "vhci_hcd.0")
if err != nil {
return err
}
controllers, err = discoverVHCIControllers()
if err != nil {
return E.Cause(err, "discover vhci controllers")
}
if len(controllers) == 0 {
return E.New("no vhci controllers present after loading vhci-hcd")
}
}
h.controllers = controllers
return nil
}
func (h *linuxImportHost) Close() error {
return nil
}
func (h *linuxImportHost) Attach(ctx context.Context, info DeviceInfoTruncated, conn net.Conn) (AttachedSession, error) {
handoff, err := newKernelHandoffSession(ctx, conn, h.logger, "client", info.BusIDString())
if err != nil {
return nil, E.Cause(err, "prepare handoff")
}
mode := "direct"
if handoff.relayConn != nil {
mode = "relay"
}
h.logger.Debug("usbip client handoff ", info.BusIDString(), ": ", mode)
ctrl, port, attachErr := h.attachOnce(ctx, info, handoff)
if attachErr != nil {
_ = handoff.Close()
return nil, attachErr
}
_ = handoff.Start()
return &linuxClientSession{
handoff: handoff,
host: h,
controller: ctrl,
port: port,
}, nil
}
func (h *linuxImportHost) attachOnce(ctx context.Context, info DeviceInfoTruncated, handoff *kernelHandoffSession) (vhciController, int, error) {
triedPorts := make(map[vhciPortKey]struct{})
for {
ctrl, port, err := vhciPickFreePort(h.controllers, info.Speed, triedPorts)
if err != nil {
return "", -1, err
}
key := vhciPortKey{controller: ctrl, port: port}
if !h.reservePort(ctrl, port) {
triedPorts[key] = struct{}{}
continue
}
attachLine := fmt.Sprintf("%d %d %d %d", port, int(handoff.file.Fd()), info.DevID(), info.Speed)
err = writeSysfs(filepath.Join(string(ctrl), "attach"), attachLine)
if err != nil {
h.releasePort(ctrl, port)
if errors.Is(err, unix.EBUSY) {
triedPorts[key] = struct{}{}
continue
}
return "", -1, E.Cause(err, "vhci attach")
}
err = handoff.closeKernelFD()
if err != nil {
h.logger.Debug("close kernel fd ", info.BusIDString(), ": ", err)
}
return ctrl, port, nil
}
}
func (h *linuxImportHost) reservePort(ctrl vhciController, port int) bool {
key := vhciPortKey{controller: ctrl, port: port}
h.portsAccess.Lock()
defer h.portsAccess.Unlock()
if _, exists := h.ports[key]; exists {
h.logger.Debug(ctrl.name(), " port ", port, " already reserved locally")
return false
}
h.logger.Debug("reserve ", ctrl.name(), " port ", port)
h.ports[key] = struct{}{}
return true
}
func (h *linuxImportHost) releasePort(ctrl vhciController, port int) {
h.portsAccess.Lock()
defer h.portsAccess.Unlock()
h.logger.Debug("release ", ctrl.name(), " port ", port)
delete(h.ports, vhciPortKey{controller: ctrl, port: port})
}
type linuxClientSession struct {
handoff *kernelHandoffSession
host *linuxImportHost
controller vhciController
port int
closeOnce sync.Once
closeErr error
}
func (s *linuxClientSession) Done() <-chan struct{} {
return s.handoff.Done()
}
func (s *linuxClientSession) Err() error {
return s.handoff.Err()
}
func (s *linuxClientSession) Start() error {
return s.handoff.Start()
}
func (s *linuxClientSession) Close() error {
s.closeOnce.Do(func() {
detachErr := writeSysfs(filepath.Join(string(s.controller), "detach"), strconv.Itoa(s.port))
closeErr := s.handoff.Close()
s.host.releasePort(s.controller, s.port)
s.closeErr = E.Errors(detachErr, closeErr)
})
return s.closeErr
}
func (s *linuxClientSession) Description() string {
return fmt.Sprintf("%s port %d", s.controller.name(), s.port)
}