Files
sing-box/service/usbip/protocol.go
T
世界 9c8147f234 usbip: enforce broadcast invariant and clone-on-stale to close recurring races
- Route every reserved-state mutation through withInventoryWrite; the
  lease insert path now broadcasts so extended subscribers see the busy
  transition immediately instead of waiting for the next mutation. Folds
  cleanupExpiredLocked changes into the broadcast decision on every
  IssueLease early-reject path. Rename the field to inventory and force
  read/write/write-quiet sites through dedicated accessors so future
  callers cannot bypass the invariant.
- Mirror the linux clone-then-swap pattern in darwinExportHost.Reconcile
  via cloneDarwinExport, so the ledger's unlocked Snapshot / LeaseCheck
  reads never observe a half-mutated stale flag. Documented as
  docs/adr/0001-export-pointer-immutability.md and on the Export
  interface; both hosts now share the applyStaleClones helper.
- Rewrite 27 if (_, )?err := …; err != nil sites to assign-then-check
  per .claude/rules/go-syntax.md.
- Delete parse / builder / tautological tests forbidden by
  .claude/rules/code-test.md (option/usbip_test.go,
  iso_scheduler_test.go, usbhost_darwin_status_test.go).
- Tag nine more usbip files with linux || (darwin && cgo); fixes
  pre-existing windows / android lint failures because the protocol
  types were only consumed by tagged code.
2026-06-09 10:42:32 +08:00

317 lines
7.0 KiB
Go

//go:build linux || (darwin && cgo)
package usbip
import (
"encoding/binary"
"io"
"strings"
E "github.com/sagernet/sing/common/exceptions"
)
const (
DefaultPort = 3240
ProtocolVersion uint16 = 0x0111
OpReqDevList uint16 = 0x8005
OpRepDevList uint16 = 0x0005
OpReqImport uint16 = 0x8003
OpRepImport uint16 = 0x0003
OpReqImportExt uint16 = 0x8f03
OpRepImportExt uint16 = 0x0f03
OpStatusOK uint32 = 0
OpStatusError uint32 = 1
maxOpRepDevListEntries = 4096
maxOpRepDevListBodyBytes = 8 << 20
deviceInfoWireSize = 312
deviceInterfaceWireSize = 4
importExtBodyWireSize = 56
)
const (
SpeedUnknown uint32 = 0
SpeedLow uint32 = 1
SpeedFull uint32 = 2
SpeedHigh uint32 = 3
SpeedWireless uint32 = 4
SpeedSuper uint32 = 5
SpeedSuperPlus uint32 = 6
)
type OpHeader struct {
Version uint16
Code uint16
Status uint32
}
type DeviceInfoTruncated struct {
Path [256]byte
BusID [32]byte
BusNum uint32
DevNum uint32
Speed uint32
IDVendor uint16
IDProduct uint16
BCDDevice uint16
BDeviceClass uint8
BDeviceSubClass uint8
BDeviceProtocol uint8
BConfigurationValue uint8
BNumConfigurations uint8
BNumInterfaces uint8
}
type DeviceInterface struct {
BInterfaceClass uint8
BInterfaceSubClass uint8
BInterfaceProtocol uint8
Padding uint8
}
type DeviceEntry struct {
Info DeviceInfoTruncated
Interfaces []DeviceInterface
Serial string
}
type ImportExtRequest struct {
BusID string
LeaseID uint64
ClientNonce uint64
Flags uint32
}
func WriteOpHeader(w io.Writer, code uint16, status uint32) error {
return binary.Write(w, binary.BigEndian, OpHeader{
Version: ProtocolVersion,
Code: code,
Status: status,
})
}
func ReadOpHeader(r io.Reader) (OpHeader, error) {
var h OpHeader
err := binary.Read(r, binary.BigEndian, &h)
if err != nil {
return h, err
}
return h, nil
}
func ParseOpHeader(raw []byte) OpHeader {
return OpHeader{
Version: binary.BigEndian.Uint16(raw[:2]),
Code: binary.BigEndian.Uint16(raw[2:4]),
Status: binary.BigEndian.Uint32(raw[4:8]),
}
}
func WriteOpReqImport(w io.Writer, busid string) error {
err := WriteOpHeader(w, OpReqImport, OpStatusOK)
if err != nil {
return err
}
var field [32]byte
if len(busid) >= len(field) {
return E.New("busid too long: ", busid)
}
copy(field[:], busid)
return binary.Write(w, binary.BigEndian, field)
}
func WriteOpReqImportExt(w io.Writer, request ImportExtRequest) error {
err := WriteOpHeader(w, OpReqImportExt, OpStatusOK)
if err != nil {
return err
}
var raw [importExtBodyWireSize]byte
if len(request.BusID) >= 32 {
return E.New("busid too long: ", request.BusID)
}
copy(raw[:32], request.BusID)
binary.BigEndian.PutUint64(raw[32:40], request.LeaseID)
binary.BigEndian.PutUint64(raw[40:48], request.ClientNonce)
binary.BigEndian.PutUint32(raw[48:52], request.Flags)
_, err = w.Write(raw[:])
return err
}
func ReadOpReqImportBody(r io.Reader) (string, error) {
var field [32]byte
_, err := io.ReadFull(r, field[:])
if err != nil {
return "", err
}
return cstring(field[:]), nil
}
func ReadOpReqImportExtBody(r io.Reader) (ImportExtRequest, error) {
var raw [importExtBodyWireSize]byte
_, err := io.ReadFull(r, raw[:])
if err != nil {
return ImportExtRequest{}, err
}
return ImportExtRequest{
BusID: cstring(raw[:32]),
LeaseID: binary.BigEndian.Uint64(raw[32:40]),
ClientNonce: binary.BigEndian.Uint64(raw[40:48]),
Flags: binary.BigEndian.Uint32(raw[48:52]),
}, nil
}
func WriteOpRepImport(w io.Writer, code uint16, status uint32, info *DeviceInfoTruncated) error {
err := WriteOpHeader(w, code, status)
if err != nil {
return err
}
if status != OpStatusOK {
return nil
}
if info == nil {
return E.New("OP_REP_IMPORT success without device info")
}
return binary.Write(w, binary.BigEndian, info)
}
func ReadOpRepImportBody(r io.Reader) (DeviceInfoTruncated, error) {
var info DeviceInfoTruncated
err := binary.Read(r, binary.BigEndian, &info)
if err != nil {
return info, err
}
return info, nil
}
func WriteOpRepDevList(w io.Writer, entries []DeviceEntry) error {
err := WriteOpHeader(w, OpRepDevList, OpStatusOK)
if err != nil {
return err
}
err = binary.Write(w, binary.BigEndian, uint32(len(entries)))
if err != nil {
return err
}
for i := range entries {
err = binary.Write(w, binary.BigEndian, &entries[i].Info)
if err != nil {
return err
}
for j := range entries[i].Interfaces {
err = binary.Write(w, binary.BigEndian, &entries[i].Interfaces[j])
if err != nil {
return err
}
}
}
return nil
}
func ReadOpRepDevListBody(r io.Reader) ([]DeviceEntry, error) {
var count uint32
err := binary.Read(r, binary.BigEndian, &count)
if err != nil {
return nil, err
}
if count > maxOpRepDevListEntries {
return nil, E.New("OP_REP_DEVLIST device count too large: ", count)
}
bodyBytes := uint64(4)
entries := make([]DeviceEntry, int(count))
for i := range entries {
err = binary.Read(r, binary.BigEndian, &entries[i].Info)
if err != nil {
return nil, err
}
entries[i].Serial = entries[i].Info.SerialString()
bodyBytes += deviceInfoWireSize
if bodyBytes > maxOpRepDevListBodyBytes {
return nil, E.New("OP_REP_DEVLIST body too large")
}
n := int(entries[i].Info.BNumInterfaces)
if n > 0 {
bodyBytes += uint64(n) * deviceInterfaceWireSize
if bodyBytes > maxOpRepDevListBodyBytes {
return nil, E.New("OP_REP_DEVLIST interface data too large")
}
entries[i].Interfaces = make([]DeviceInterface, n)
for j := range entries[i].Interfaces {
err = binary.Read(r, binary.BigEndian, &entries[i].Interfaces[j])
if err != nil {
return nil, err
}
}
}
}
return entries, nil
}
func (d *DeviceInfoTruncated) BusIDString() string {
return cstring(d.BusID[:])
}
func (d *DeviceInfoTruncated) SerialString() string {
meta := trailingCString(d.Path[:])
serial, ok := strings.CutPrefix(meta, "serial=")
if !ok {
return ""
}
return serial
}
func (d *DeviceInfoTruncated) DevID() uint32 {
return (d.BusNum << 16) | (d.DevNum & 0xffff)
}
func encodePathField(dst *[256]byte, path string, serial string) {
*dst = [256]byte{}
pathLen := copy(dst[:len(dst)-1], path)
if serial == "" {
return
}
trailer := "serial=" + serial
trailerStart := pathLen + 1
if trailerStart+len(trailer)+1 > len(dst) {
return
}
copy(dst[trailerStart:], trailer)
}
func cstring(b []byte) string {
for i, c := range b {
if c == 0 {
return string(b[:i])
}
}
return string(b)
}
func trailingCString(b []byte) string {
for i, c := range b {
if c != 0 {
continue
}
if i+1 >= len(b) {
return ""
}
return cstring(b[i+1:])
}
return ""
}
func entryDeviceKey(entry DeviceEntry) DeviceKey {
serial := entry.Serial
if serial == "" {
serial = entry.Info.SerialString()
}
return DeviceKey{
BusID: entry.Info.BusIDString(),
VendorID: entry.Info.IDVendor,
ProductID: entry.Info.IDProduct,
Serial: serial,
}
}