usbip: add extended control protocol

This commit is contained in:
世界
2026-04-24 07:12:56 +08:00
parent e94a99d5b9
commit 0135eaef7c
10 changed files with 1705 additions and 101 deletions
+208
View File
@@ -0,0 +1,208 @@
//go:build linux || (darwin && cgo)
package usbip
import (
"context"
"errors"
"net"
"sync"
"time"
E "github.com/sagernet/sing/common/exceptions"
)
var errClientControlSessionClosed = errors.New("usbip control session closed")
type clientImportLease struct {
Valid bool
ID uint64
ClientNonce uint64
}
type clientControlSession struct {
conn net.Conn
capabilities uint32
writeMu sync.Mutex
mu sync.Mutex
nextNonce uint64
pending map[uint64]chan clientLeaseResult
closed bool
}
type clientLeaseResult struct {
response controlLeaseResponse
err error
}
func newClientControlSession(conn net.Conn, capabilities uint32) *clientControlSession {
return &clientControlSession{
conn: conn,
capabilities: capabilities,
pending: make(map[uint64]chan clientLeaseResult),
}
}
func (s *clientControlSession) supportsImportLease() bool {
return supportsControlExtensions(s.capabilities)
}
func (s *clientControlSession) writeControl(frame controlFrame, payload any) error {
s.writeMu.Lock()
defer s.writeMu.Unlock()
_ = s.conn.SetWriteDeadline(time.Now().Add(controlWriteTimeout))
err := writeControlMessage(s.conn, frame, payload)
_ = s.conn.SetWriteDeadline(time.Time{})
return err
}
func (s *clientControlSession) requestLease(ctx context.Context, busid string) (controlLeaseResponse, error) {
s.mu.Lock()
if s.closed {
s.mu.Unlock()
return controlLeaseResponse{}, errClientControlSessionClosed
}
s.nextNonce++
nonce := s.nextNonce
waiter := make(chan clientLeaseResult, 1)
s.pending[nonce] = waiter
s.mu.Unlock()
request := controlLeaseRequest{
BusID: busid,
ClientNonce: nonce,
}
if err := s.writeControl(controlFrame{
Type: controlFrameLeaseRequest,
Version: controlProtocolVersion,
}, request); err != nil {
s.removeLeaseWaiter(nonce)
return controlLeaseResponse{}, err
}
select {
case result := <-waiter:
return result.response, result.err
case <-ctx.Done():
s.removeLeaseWaiter(nonce)
return controlLeaseResponse{}, ctx.Err()
}
}
func (s *clientControlSession) deliverLeaseResponse(response controlLeaseResponse) bool {
s.mu.Lock()
waiter, ok := s.pending[response.ClientNonce]
if ok {
delete(s.pending, response.ClientNonce)
}
s.mu.Unlock()
if !ok {
return false
}
waiter <- clientLeaseResult{response: response}
close(waiter)
return true
}
func (s *clientControlSession) removeLeaseWaiter(nonce uint64) {
s.mu.Lock()
delete(s.pending, nonce)
s.mu.Unlock()
}
func (s *clientControlSession) closeWithError(err error) {
s.mu.Lock()
if s.closed {
s.mu.Unlock()
return
}
s.closed = true
pending := s.pending
s.pending = make(map[uint64]chan clientLeaseResult)
s.mu.Unlock()
for _, waiter := range pending {
waiter <- clientLeaseResult{err: err}
close(waiter)
}
}
func (c *ClientService) setControlSession(session *clientControlSession) {
c.controlMu.Lock()
c.controlSession = session
c.controlMu.Unlock()
}
func (c *ClientService) clearControlSession(session *clientControlSession, err error) {
c.controlMu.Lock()
if c.controlSession == session {
c.controlSession = nil
}
c.controlMu.Unlock()
session.closeWithError(err)
}
func (c *ClientService) currentControlSession() *clientControlSession {
c.controlMu.Lock()
defer c.controlMu.Unlock()
return c.controlSession
}
func (c *ClientService) requestImportLease(ctx context.Context, busid string) (clientImportLease, error) {
session := c.currentControlSession()
if session == nil || !session.supportsImportLease() {
return clientImportLease{}, nil
}
response, err := session.requestLease(ctx, busid)
if err != nil {
return clientImportLease{}, E.Cause(err, "request import lease")
}
if response.ErrorCode != "" {
return clientImportLease{}, E.New("remote rejected import lease (", response.ErrorCode, ": ", response.ErrorMessage, ")")
}
return clientImportLease{
Valid: true,
ID: response.LeaseID,
ClientNonce: response.ClientNonce,
}, nil
}
func (c *ClientService) applyControlSnapshot(snapshot controlDeviceSnapshot) {
devices := deviceInfoV2Map(snapshot.Devices)
values := sortedDeviceInfoV2Values(devices)
c.remoteMu.Lock()
c.remoteDevicesV2 = devices
c.remoteMu.Unlock()
c.applyRemoteEntries(deviceInfoV2ToEntries(values, true))
}
func (c *ClientService) applyControlDelta(delta controlDeviceDelta) {
c.remoteMu.Lock()
if c.remoteDevicesV2 == nil {
c.remoteDevicesV2 = make(map[string]DeviceInfoV2)
}
for _, busid := range delta.Removed {
delete(c.remoteDevicesV2, busid)
}
for _, device := range delta.Added {
if device.BusID == "" {
continue
}
c.remoteDevicesV2[device.BusID] = device
}
for _, device := range delta.Updated {
if device.BusID == "" {
continue
}
c.remoteDevicesV2[device.BusID] = device
}
values := sortedDeviceInfoV2Values(c.remoteDevicesV2)
c.remoteMu.Unlock()
c.applyRemoteEntries(deviceInfoV2ToEntries(values, true))
}
func (c *ClientService) clearControlDeviceState() {
c.remoteMu.Lock()
c.remoteDevicesV2 = nil
c.remoteMu.Unlock()
}
+76 -12
View File
@@ -68,6 +68,12 @@ type ClientService struct {
activeMu sync.Mutex
activeBusIDs map[string]struct{}
controlMu sync.Mutex
controlSession *clientControlSession
remoteMu sync.Mutex
remoteDevicesV2 map[string]DeviceInfoV2
}
func NewClientService(ctx context.Context, logger log.ContextLogger, tag string, options option.USBIPClientServiceOptions) (adapter.Service, error) {
@@ -198,18 +204,23 @@ func (c *ClientService) runControlSession() error {
if err != nil {
return E.Cause(errControlUnsupported, "read control ack: ", err)
}
if ack.Type != controlFrameAck || ack.Version != controlProtocolVersion || ack.Capabilities&controlCapabilities != controlCapabilities {
if ack.Type != controlFrameAck || ack.Version != controlProtocolVersion || ack.Capabilities&controlRequiredCapabilities != controlRequiredCapabilities {
return E.Cause(errControlUnsupported, "invalid control ack")
}
_ = conn.SetWriteDeadline(time.Time{})
_ = conn.SetReadDeadline(time.Time{})
if err := c.syncRemoteState(); err != nil {
session := newClientControlSession(conn, ack.Capabilities)
extended := supportsControlExtensions(ack.Capabilities)
if extended {
c.setControlSession(session)
defer c.clearControlSession(session, errClientControlSessionClosed)
} else if err := c.syncRemoteState(); err != nil {
return E.Cause(err, "initial devlist sync")
}
pingDone := make(chan struct{})
go c.controlPingLoop(conn, pingDone)
go c.controlPingLoop(session, pingDone)
defer close(pingDone)
lastSeq := ack.Sequence
@@ -217,19 +228,57 @@ func (c *ClientService) runControlSession() error {
if err := conn.SetReadDeadline(time.Now().Add(controlReadTimeout)); err != nil {
return err
}
frame, err := ReadControlFrame(conn)
message, err := readControlMessage(conn)
if err != nil {
return E.Cause(errImmediateReconnect, controlSessionIdleHint, ": ", err)
}
frame := message.Frame
switch frame.Type {
case controlFrameChanged:
if frame.Sequence != lastSeq+1 {
if frame.Sequence != lastSeq && frame.Sequence != lastSeq+1 {
return E.Cause(errImmediateReconnect, "control sequence jumped from ", lastSeq, " to ", frame.Sequence)
}
lastSeq = frame.Sequence
if err := c.syncRemoteState(); err != nil {
return E.Cause(errImmediateReconnect, "devlist sync after change ", frame.Sequence, ": ", err)
}
case controlFrameDeviceSnapshot:
if !extended {
return E.Cause(errImmediateReconnect, "unexpected control frame ", frame.Type)
}
var snapshot controlDeviceSnapshot
if err := unmarshalControlPayload(message.Payload, &snapshot); err != nil {
return E.Cause(errImmediateReconnect, "read device snapshot: ", err)
}
lastSeq = frame.Sequence
c.applyControlSnapshot(snapshot)
case controlFrameDeviceDelta:
if !extended {
return E.Cause(errImmediateReconnect, "unexpected control frame ", frame.Type)
}
if frame.Sequence != lastSeq+1 {
if err := c.syncRemoteState(); err != nil {
return E.Cause(errImmediateReconnect, "devlist sync after sequence jump ", frame.Sequence, ": ", err)
}
c.clearControlDeviceState()
lastSeq = frame.Sequence
continue
}
var delta controlDeviceDelta
if err := unmarshalControlPayload(message.Payload, &delta); err != nil {
return E.Cause(errImmediateReconnect, "read device delta: ", err)
}
lastSeq = frame.Sequence
c.applyControlDelta(delta)
case controlFrameLeaseResponse:
if !extended {
return E.Cause(errImmediateReconnect, "unexpected control frame ", frame.Type)
}
var response controlLeaseResponse
if err := unmarshalControlPayload(message.Payload, &response); err != nil {
return E.Cause(errImmediateReconnect, "read lease response: ", err)
}
session.deliverLeaseResponse(response)
case controlFramePong:
default:
return E.Cause(errImmediateReconnect, "unexpected control frame ", frame.Type)
@@ -245,7 +294,7 @@ func (c *ClientService) runStandardSession() error {
return nil
}
func (c *ClientService) controlPingLoop(conn net.Conn, done <-chan struct{}) {
func (c *ClientService) controlPingLoop(session *clientControlSession, done <-chan struct{}) {
ticker := time.NewTicker(controlPingInterval)
defer ticker.Stop()
for {
@@ -255,12 +304,13 @@ func (c *ClientService) controlPingLoop(conn net.Conn, done <-chan struct{}) {
case <-done:
return
case <-ticker.C:
_ = conn.SetWriteDeadline(time.Now().Add(controlWriteTimeout))
if err := WriteControlPing(conn); err != nil {
_ = conn.Close()
if err := session.writeControl(controlFrame{
Type: controlFramePing,
Version: controlProtocolVersion,
}, nil); err != nil {
_ = session.conn.Close()
return
}
_ = conn.SetWriteDeadline(time.Time{})
}
}
}
@@ -520,7 +570,21 @@ func (c *ClientService) attemptAttach(ctx context.Context, busid string) (*darwi
}()
stopCloseOnCancel := closeConnOnContextDone(ctx, conn)
defer stopCloseOnCancel()
if err := WriteOpReqImport(conn, busid); err != nil {
lease, err := c.requestImportLease(ctx, busid)
if err != nil {
return nil, err
}
expectedReply := OpRepImport
if lease.Valid {
expectedReply = OpRepImportExt
if err := WriteOpReqImportExt(conn, ImportExtRequest{
BusID: busid,
LeaseID: lease.ID,
ClientNonce: lease.ClientNonce,
}); err != nil {
return nil, E.Cause(err, "write OP_REQ_IMPORT_EXT")
}
} else if err := WriteOpReqImport(conn, busid); err != nil {
return nil, E.Cause(err, "write OP_REQ_IMPORT")
}
header, err := ReadOpHeader(conn)
@@ -530,7 +594,7 @@ func (c *ClientService) attemptAttach(ctx context.Context, busid string) (*darwi
if header.Version != ProtocolVersion {
return nil, E.New("unexpected reply version 0x", hex16(header.Version))
}
if header.Code != OpRepImport {
if header.Code != expectedReply {
return nil, E.New("unexpected reply code 0x", hex16(header.Code))
}
if header.Status != OpStatusOK {
+76 -12
View File
@@ -80,6 +80,12 @@ type ClientService struct {
activeMu sync.Mutex
activeBusIDs map[string]struct{}
controlMu sync.Mutex
controlSession *clientControlSession
remoteMu sync.Mutex
remoteDevicesV2 map[string]DeviceInfoV2
}
func NewClientService(ctx context.Context, logger log.ContextLogger, tag string, options option.USBIPClientServiceOptions) (adapter.Service, error) {
@@ -221,18 +227,23 @@ func (c *ClientService) runControlSession() error {
if ack.Version != controlProtocolVersion {
return E.Cause(errControlUnsupported, "unsupported control version ", ack.Version)
}
if ack.Capabilities&controlCapabilities != controlCapabilities {
if ack.Capabilities&controlRequiredCapabilities != controlRequiredCapabilities {
return E.Cause(errControlUnsupported, "missing control capabilities 0x", ack.Capabilities)
}
_ = conn.SetWriteDeadline(time.Time{})
_ = conn.SetReadDeadline(time.Time{})
if err := c.syncRemoteState(); err != nil {
session := newClientControlSession(conn, ack.Capabilities)
extended := supportsControlExtensions(ack.Capabilities)
if extended {
c.setControlSession(session)
defer c.clearControlSession(session, errClientControlSessionClosed)
} else if err := c.syncRemoteState(); err != nil {
return E.Cause(err, "initial devlist sync")
}
pingDone := make(chan struct{})
go c.controlPingLoop(conn, pingDone)
go c.controlPingLoop(session, pingDone)
defer close(pingDone)
lastSeq := ack.Sequence
@@ -240,19 +251,57 @@ func (c *ClientService) runControlSession() error {
if err := conn.SetReadDeadline(time.Now().Add(controlReadTimeout)); err != nil {
return err
}
frame, err := ReadControlFrame(conn)
message, err := readControlMessage(conn)
if err != nil {
return E.Cause(errImmediateReconnect, controlSessionIdleHint, ": ", err)
}
frame := message.Frame
switch frame.Type {
case controlFrameChanged:
if frame.Sequence != lastSeq+1 {
if frame.Sequence != lastSeq && frame.Sequence != lastSeq+1 {
return E.Cause(errImmediateReconnect, "control sequence jumped from ", lastSeq, " to ", frame.Sequence)
}
lastSeq = frame.Sequence
if err := c.syncRemoteState(); err != nil {
return E.Cause(errImmediateReconnect, "devlist sync after change ", frame.Sequence, ": ", err)
}
case controlFrameDeviceSnapshot:
if !extended {
return E.Cause(errImmediateReconnect, "unexpected control frame ", frame.Type)
}
var snapshot controlDeviceSnapshot
if err := unmarshalControlPayload(message.Payload, &snapshot); err != nil {
return E.Cause(errImmediateReconnect, "read device snapshot: ", err)
}
lastSeq = frame.Sequence
c.applyControlSnapshot(snapshot)
case controlFrameDeviceDelta:
if !extended {
return E.Cause(errImmediateReconnect, "unexpected control frame ", frame.Type)
}
if frame.Sequence != lastSeq+1 {
if err := c.syncRemoteState(); err != nil {
return E.Cause(errImmediateReconnect, "devlist sync after sequence jump ", frame.Sequence, ": ", err)
}
c.clearControlDeviceState()
lastSeq = frame.Sequence
continue
}
var delta controlDeviceDelta
if err := unmarshalControlPayload(message.Payload, &delta); err != nil {
return E.Cause(errImmediateReconnect, "read device delta: ", err)
}
lastSeq = frame.Sequence
c.applyControlDelta(delta)
case controlFrameLeaseResponse:
if !extended {
return E.Cause(errImmediateReconnect, "unexpected control frame ", frame.Type)
}
var response controlLeaseResponse
if err := unmarshalControlPayload(message.Payload, &response); err != nil {
return E.Cause(errImmediateReconnect, "read lease response: ", err)
}
session.deliverLeaseResponse(response)
case controlFramePong:
default:
return E.Cause(errImmediateReconnect, "unexpected control frame ", frame.Type)
@@ -268,7 +317,7 @@ func (c *ClientService) runStandardSession() error {
return nil
}
func (c *ClientService) controlPingLoop(conn net.Conn, done <-chan struct{}) {
func (c *ClientService) controlPingLoop(session *clientControlSession, done <-chan struct{}) {
ticker := time.NewTicker(controlPingInterval)
defer ticker.Stop()
for {
@@ -278,12 +327,13 @@ func (c *ClientService) controlPingLoop(conn net.Conn, done <-chan struct{}) {
case <-done:
return
case <-ticker.C:
_ = conn.SetWriteDeadline(time.Now().Add(controlWriteTimeout))
if err := WriteControlPing(conn); err != nil {
_ = conn.Close()
if err := session.writeControl(controlFrame{
Type: controlFramePing,
Version: controlProtocolVersion,
}, nil); err != nil {
_ = session.conn.Close()
return
}
_ = conn.SetWriteDeadline(time.Time{})
}
}
}
@@ -548,7 +598,21 @@ func (c *ClientService) attemptAttach(ctx context.Context, busid string) (int, e
}()
stopCloseOnCancel := closeConnOnContextDone(ctx, conn)
defer stopCloseOnCancel()
if err := WriteOpReqImport(conn, busid); err != nil {
lease, err := c.requestImportLease(ctx, busid)
if err != nil {
return -1, err
}
expectedReply := OpRepImport
if lease.Valid {
expectedReply = OpRepImportExt
if err := WriteOpReqImportExt(conn, ImportExtRequest{
BusID: busid,
LeaseID: lease.ID,
ClientNonce: lease.ClientNonce,
}); err != nil {
return -1, E.Cause(err, "write OP_REQ_IMPORT_EXT")
}
} else if err := WriteOpReqImport(conn, busid); err != nil {
return -1, E.Cause(err, "write OP_REQ_IMPORT")
}
header, err := ReadOpHeader(conn)
@@ -558,7 +622,7 @@ func (c *ClientService) attemptAttach(ctx context.Context, busid string) (int, e
if header.Version != ProtocolVersion {
return -1, E.New("unexpected reply version 0x", hex16(header.Version))
}
if header.Code != OpRepImport {
if header.Code != expectedReply {
return -1, E.New("unexpected reply code 0x", hex16(header.Code))
}
if header.Status != OpStatusOK {
+321 -25
View File
@@ -2,34 +2,130 @@ package usbip
import (
"encoding/binary"
"encoding/json"
"io"
"reflect"
"sort"
"time"
E "github.com/sagernet/sing/common/exceptions"
)
const (
controlProtocolVersion uint8 = 1
controlFrameHello uint8 = 1
controlFrameAck uint8 = 2
controlFrameChanged uint8 = 3
controlFramePing uint8 = 4
controlFramePong uint8 = 5
controlFrameHello uint8 = 1
controlFrameAck uint8 = 2
controlFrameChanged uint8 = 3
controlFramePing uint8 = 4
controlFramePong uint8 = 5
controlFrameDeviceSnapshot uint8 = 6
controlFrameDeviceDelta uint8 = 7
controlFrameLeaseRequest uint8 = 8
controlFrameLeaseResponse uint8 = 9
controlCapabilityChanged uint32 = 1 << 0
controlCapabilityPingPong uint32 = 1 << 1
controlCapabilities = controlCapabilityChanged | controlCapabilityPingPong
controlCapabilityChanged uint32 = 1 << 0
controlCapabilityPingPong uint32 = 1 << 1
controlCapabilityPayloadFrames uint32 = 1 << 2
controlCapabilityDeviceStateV2 uint32 = 1 << 3
controlCapabilityImportLease uint32 = 1 << 4
controlRequiredCapabilities = controlCapabilityChanged | controlCapabilityPingPong
controlExtensionCapabilities = controlCapabilityPayloadFrames | controlCapabilityDeviceStateV2 | controlCapabilityImportLease
controlCapabilities = controlRequiredCapabilities | controlExtensionCapabilities
controlPrefaceSize = 8
controlFrameSize = 16
controlPrefaceSize = 8
controlFrameSize = 16
maxControlPayloadLength = 64<<10 - 1
deviceStateAvailable = "available"
deviceStateBusy = "busy"
deviceStateUnavailable = "unavailable"
importLeaseTTL = 10 * time.Second
)
var controlPreface = [controlPrefaceSize]byte{'S', 'B', 'U', 'S', 'B', 'I', 'P', '1'}
type controlFrame struct {
Type uint8
Version uint8
_ uint16
Capabilities uint32
Sequence uint64
Type uint8
Version uint8
PayloadLength uint16
Capabilities uint32
Sequence uint64
}
type controlMessage struct {
Frame controlFrame
Payload []byte
}
type controlOutboundMessage struct {
Frame controlFrame
Payload []byte
}
type DeviceInterfaceV2 struct {
Class uint8 `json:"class"`
SubClass uint8 `json:"subclass"`
Protocol uint8 `json:"protocol"`
}
type DeviceInfoV2 struct {
BusID string `json:"busid"`
StableID string `json:"stable_id,omitempty"`
Backend string `json:"backend,omitempty"`
Path string `json:"path,omitempty"`
Serial string `json:"serial,omitempty"`
VendorID uint16 `json:"vendor_id"`
ProductID uint16 `json:"product_id"`
BCDDevice uint16 `json:"bcd_device,omitempty"`
Speed uint32 `json:"speed"`
DeviceClass uint8 `json:"device_class"`
DeviceSubClass uint8 `json:"device_subclass"`
DeviceProtocol uint8 `json:"device_protocol"`
ConfigurationValue uint8 `json:"configuration_value"`
NumConfigurations uint8 `json:"num_configurations"`
NumInterfaces uint8 `json:"num_interfaces"`
Interfaces []DeviceInterfaceV2 `json:"interfaces,omitempty"`
State string `json:"state"`
StatusCode int `json:"status_code,omitempty"`
StatusReason string `json:"status_reason,omitempty"`
}
type controlDeviceSnapshot struct {
Sequence uint64 `json:"sequence"`
Devices []DeviceInfoV2 `json:"devices"`
}
type controlDeviceDelta struct {
Sequence uint64 `json:"sequence"`
Added []DeviceInfoV2 `json:"added,omitempty"`
Updated []DeviceInfoV2 `json:"updated,omitempty"`
Removed []string `json:"removed,omitempty"`
}
type controlLeaseRequest struct {
BusID string `json:"busid"`
ClientNonce uint64 `json:"client_nonce"`
}
type controlLeaseResponse struct {
BusID string `json:"busid"`
LeaseID uint64 `json:"lease_id,omitempty"`
ClientNonce uint64 `json:"client_nonce"`
Generation uint64 `json:"generation,omitempty"`
TTLMillis int64 `json:"ttl_millis,omitempty"`
ErrorCode string `json:"error_code,omitempty"`
ErrorMessage string `json:"error_message,omitempty"`
}
type serverImportLease struct {
ID uint64
SubscriberID uint64
BusID string
ClientNonce uint64
Generation uint64
Expires time.Time
}
func WriteControlPreface(w io.Writer) error {
@@ -58,10 +154,14 @@ func WriteControlHello(w io.Writer) error {
}
func WriteControlAck(w io.Writer, sequence uint64) error {
return writeControlAckWithCapabilities(w, sequence, controlCapabilities)
}
func writeControlAckWithCapabilities(w io.Writer, sequence uint64, capabilities uint32) error {
return writeControlFrame(w, controlFrame{
Type: controlFrameAck,
Version: controlProtocolVersion,
Capabilities: controlCapabilities,
Capabilities: capabilities,
Sequence: sequence,
})
}
@@ -89,24 +189,220 @@ func WriteControlPong(w io.Writer) error {
}
func ReadControlFrame(r io.Reader) (controlFrame, error) {
var raw [controlFrameSize]byte
if _, err := io.ReadFull(r, raw[:]); err != nil {
message, err := readControlMessage(r)
if err != nil {
return controlFrame{}, err
}
return controlFrame{
Type: raw[0],
Version: raw[1],
Capabilities: binary.BigEndian.Uint32(raw[4:8]),
Sequence: binary.BigEndian.Uint64(raw[8:16]),
}, nil
if len(message.Payload) > 0 {
return controlFrame{}, E.New("unexpected control payload length ", len(message.Payload))
}
return message.Frame, nil
}
func readControlMessage(r io.Reader) (controlMessage, error) {
var raw [controlFrameSize]byte
if _, err := io.ReadFull(r, raw[:]); err != nil {
return controlMessage{}, err
}
frame := controlFrame{
Type: raw[0],
Version: raw[1],
PayloadLength: binary.BigEndian.Uint16(raw[2:4]),
Capabilities: binary.BigEndian.Uint32(raw[4:8]),
Sequence: binary.BigEndian.Uint64(raw[8:16]),
}
var payload []byte
if frame.PayloadLength > 0 {
payload = make([]byte, frame.PayloadLength)
if _, err := io.ReadFull(r, payload); err != nil {
return controlMessage{}, err
}
}
return controlMessage{Frame: frame, Payload: payload}, nil
}
func writeControlFrame(w io.Writer, frame controlFrame) error {
return writeControlMessage(w, frame, nil)
}
func writeControlMessage(w io.Writer, frame controlFrame, payload any) error {
rawPayload, err := marshalControlPayload(payload)
if err != nil {
return err
}
if len(rawPayload) > maxControlPayloadLength {
return E.New("control payload too large: ", len(rawPayload))
}
frame.PayloadLength = uint16(len(rawPayload))
var raw [controlFrameSize]byte
raw[0] = frame.Type
raw[1] = frame.Version
binary.BigEndian.PutUint16(raw[2:4], frame.PayloadLength)
binary.BigEndian.PutUint32(raw[4:8], frame.Capabilities)
binary.BigEndian.PutUint64(raw[8:16], frame.Sequence)
_, err := w.Write(raw[:])
if _, err := w.Write(raw[:]); err != nil {
return err
}
if len(rawPayload) == 0 {
return nil
}
_, err = w.Write(rawPayload)
return err
}
func marshalControlPayload(payload any) ([]byte, error) {
switch value := payload.(type) {
case nil:
return nil, nil
case []byte:
return value, nil
default:
return json.Marshal(value)
}
}
func unmarshalControlPayload(payload []byte, value any) error {
if len(payload) == 0 {
return E.New("missing control payload")
}
return json.Unmarshal(payload, value)
}
func negotiatedControlCapabilities(peer uint32) uint32 {
return peer & controlCapabilities
}
func supportsControlExtensions(capabilities uint32) bool {
return capabilities&controlExtensionCapabilities == controlExtensionCapabilities
}
func deviceInfoV2FromEntry(entry DeviceEntry, backend string, stableID string, state string, statusCode int, statusReason string) DeviceInfoV2 {
interfaces := make([]DeviceInterfaceV2, len(entry.Interfaces))
for i := range entry.Interfaces {
interfaces[i] = DeviceInterfaceV2{
Class: entry.Interfaces[i].BInterfaceClass,
SubClass: entry.Interfaces[i].BInterfaceSubClass,
Protocol: entry.Interfaces[i].BInterfaceProtocol,
}
}
if state == "" {
state = deviceStateAvailable
}
return DeviceInfoV2{
BusID: entry.Info.BusIDString(),
StableID: stableID,
Backend: backend,
Path: entry.Info.PathString(),
Serial: entry.Info.SerialString(),
VendorID: entry.Info.IDVendor,
ProductID: entry.Info.IDProduct,
BCDDevice: entry.Info.BCDDevice,
Speed: entry.Info.Speed,
DeviceClass: entry.Info.BDeviceClass,
DeviceSubClass: entry.Info.BDeviceSubClass,
DeviceProtocol: entry.Info.BDeviceProtocol,
ConfigurationValue: entry.Info.BConfigurationValue,
NumConfigurations: entry.Info.BNumConfigurations,
NumInterfaces: entry.Info.BNumInterfaces,
Interfaces: interfaces,
State: state,
StatusCode: statusCode,
StatusReason: statusReason,
}
}
func (d DeviceInfoV2) toDeviceEntry() DeviceEntry {
var info DeviceInfoTruncated
encodePathField(&info.Path, d.Path, d.Serial)
copy(info.BusID[:], d.BusID)
info.Speed = d.Speed
info.IDVendor = d.VendorID
info.IDProduct = d.ProductID
info.BCDDevice = d.BCDDevice
info.BDeviceClass = d.DeviceClass
info.BDeviceSubClass = d.DeviceSubClass
info.BDeviceProtocol = d.DeviceProtocol
info.BConfigurationValue = d.ConfigurationValue
info.BNumConfigurations = d.NumConfigurations
info.BNumInterfaces = d.NumInterfaces
interfaces := make([]DeviceInterface, len(d.Interfaces))
for i := range d.Interfaces {
interfaces[i] = DeviceInterface{
BInterfaceClass: d.Interfaces[i].Class,
BInterfaceSubClass: d.Interfaces[i].SubClass,
BInterfaceProtocol: d.Interfaces[i].Protocol,
}
}
return DeviceEntry{Info: info, Interfaces: interfaces}
}
func (d DeviceInfoV2) key() DeviceKey {
return DeviceKey{
BusID: d.BusID,
VendorID: d.VendorID,
ProductID: d.ProductID,
Serial: d.Serial,
}
}
func (d DeviceInfoV2) available() bool {
return d.State == "" || d.State == deviceStateAvailable
}
func deviceInfoV2Map(devices []DeviceInfoV2) map[string]DeviceInfoV2 {
out := make(map[string]DeviceInfoV2, len(devices))
for _, device := range devices {
if device.BusID == "" {
continue
}
out[device.BusID] = device
}
return out
}
func sortedDeviceInfoV2Values(devices map[string]DeviceInfoV2) []DeviceInfoV2 {
busids := make([]string, 0, len(devices))
for busid := range devices {
busids = append(busids, busid)
}
sort.Strings(busids)
out := make([]DeviceInfoV2, 0, len(busids))
for _, busid := range busids {
out = append(out, devices[busid])
}
return out
}
func deviceInfoV2ToEntries(devices []DeviceInfoV2, availableOnly bool) []DeviceEntry {
entries := make([]DeviceEntry, 0, len(devices))
for _, device := range devices {
if availableOnly && !device.available() {
continue
}
entries = append(entries, device.toDeviceEntry())
}
return entries
}
func buildControlDeviceDelta(sequence uint64, previous map[string]DeviceInfoV2, current map[string]DeviceInfoV2) controlDeviceDelta {
delta := controlDeviceDelta{Sequence: sequence}
for busid, device := range current {
prev, ok := previous[busid]
if !ok {
delta.Added = append(delta.Added, device)
continue
}
if !reflect.DeepEqual(prev, device) {
delta.Updated = append(delta.Updated, device)
}
}
for busid := range previous {
if _, ok := current[busid]; !ok {
delta.Removed = append(delta.Removed, busid)
}
}
sort.Slice(delta.Added, func(i, j int) bool { return delta.Added[i].BusID < delta.Added[j].BusID })
sort.Slice(delta.Updated, func(i, j int) bool { return delta.Updated[i].BusID < delta.Updated[j].BusID })
sort.Strings(delta.Removed)
return delta
}
+48 -3
View File
@@ -377,26 +377,55 @@ func (s *darwinFakeUSBIPServer) handleConn(conn net.Conn) {
_ = WriteOpRepDevList(conn, []DeviceEntry{s.entry})
case OpReqImport:
s.handleImport(conn)
case OpReqImportExt:
s.handleImportExt(conn)
}
}
func (s *darwinFakeUSBIPServer) handleControlConn(conn net.Conn) {
hello, err := ReadControlFrame(conn)
helloMessage, err := readControlMessage(conn)
if err != nil {
return
}
hello := helloMessage.Frame
if hello.Type != controlFrameHello || hello.Version != controlProtocolVersion {
return
}
if err := WriteControlAck(conn, 0); err != nil {
capabilities := negotiatedControlCapabilities(hello.Capabilities)
if err := writeControlAckWithCapabilities(conn, 0, capabilities); err != nil {
return
}
if supportsControlExtensions(capabilities) {
_ = writeControlMessage(conn, controlFrame{
Type: controlFrameDeviceSnapshot,
Version: controlProtocolVersion,
}, controlDeviceSnapshot{
Devices: []DeviceInfoV2{deviceInfoV2FromEntry(s.entry, "darwin-fake", "darwin-fake:"+s.entry.Info.BusIDString(), deviceStateAvailable, 0, "available")},
})
}
for {
frame, err := ReadControlFrame(conn)
message, err := readControlMessage(conn)
if err != nil {
return
}
frame := message.Frame
if frame.Type != controlFramePing {
if frame.Type == controlFrameLeaseRequest && supportsControlExtensions(capabilities) {
var request controlLeaseRequest
if unmarshalControlPayload(message.Payload, &request) != nil {
return
}
_ = writeControlMessage(conn, controlFrame{
Type: controlFrameLeaseResponse,
Version: controlProtocolVersion,
}, controlLeaseResponse{
BusID: request.BusID,
LeaseID: 1,
ClientNonce: request.ClientNonce,
TTLMillis: int64(importLeaseTTL / time.Millisecond),
})
continue
}
return
}
if err := WriteControlPong(conn); err != nil {
@@ -421,6 +450,22 @@ func (s *darwinFakeUSBIPServer) handleImport(conn net.Conn) {
s.handleDataSession(conn)
}
func (s *darwinFakeUSBIPServer) handleImportExt(conn net.Conn) {
request, err := ReadOpReqImportExtBody(conn)
if err != nil {
return
}
if request.BusID != s.entry.Info.BusIDString() || request.LeaseID == 0 {
_ = WriteOpRepImportExt(conn, OpStatusError, nil)
return
}
info := s.entry.Info
if err := WriteOpRepImportExt(conn, OpStatusOK, &info); err != nil {
return
}
s.handleDataSession(conn)
}
func (s *darwinFakeUSBIPServer) handleDataSession(conn net.Conn) {
for {
header, err := ReadDataHeader(conn)
+237 -3
View File
@@ -1226,6 +1226,13 @@ func TestServerDispatchConnHandlesControlPingAndChanged(t *testing.T) {
require.Equal(t, controlCapabilities, ack.Capabilities)
require.Zero(t, ack.Sequence)
snapshotMessage, err := readControlMessage(conn)
require.NoError(t, err)
require.Equal(t, controlFrameDeviceSnapshot, snapshotMessage.Frame.Type)
var snapshot controlDeviceSnapshot
require.NoError(t, unmarshalControlPayload(snapshotMessage.Payload, &snapshot))
require.Empty(t, snapshot.Devices)
require.NoError(t, WriteControlPing(conn))
pong, err := ReadControlFrame(conn)
require.NoError(t, err)
@@ -1233,10 +1240,108 @@ func TestServerDispatchConnHandlesControlPingAndChanged(t *testing.T) {
require.Equal(t, controlProtocolVersion, pong.Version)
server.broadcastChanged()
changed, err := ReadControlFrame(conn)
changed, err := readControlMessage(conn)
require.NoError(t, err)
require.Equal(t, controlFrameChanged, changed.Type)
require.Equal(t, uint64(1), changed.Sequence)
require.Equal(t, controlFrameDeviceDelta, changed.Frame.Type)
require.Equal(t, uint64(1), changed.Frame.Sequence)
var delta controlDeviceDelta
require.NoError(t, unmarshalControlPayload(changed.Payload, &delta))
require.Equal(t, uint64(1), delta.Sequence)
}
func TestServerControlLeaseEnablesImportExt(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
device := newTestDevice("1-1", 0x1d6b, 0x0002, "serial-1", SpeedHigh)
store := newTestDeviceStore(device)
store.setStatus("1-1", usbipStatusAvailable)
serverOps := newTestUSBIPOps(t)
serverOps.readUsbipStatus = store.readUsbipStatus
serverOps.readSysfsDevice = store.readSysfsDevice
serverOps.writeUsbipSockfd = store.writeUsbipSockfd
server := &ServerService{
ctx: ctx,
cancel: cancel,
logger: newTestLogger(),
exports: map[string]serverExport{"1-1": {busid: "1-1"}},
controlSubs: make(map[uint64]*serverControlConn),
controlState: make(map[string]DeviceInfoV2),
leases: make(map[uint64]serverImportLease),
leaseByBusID: make(map[string]uint64),
ops: serverOps,
}
server.refreshControlState()
serverAddr, closeServer := startDispatchServer(t, server)
defer closeServer()
controlConn, err := net.Dial("tcp", serverAddr.String())
require.NoError(t, err)
defer controlConn.Close()
require.NoError(t, WriteControlPreface(controlConn))
require.NoError(t, WriteControlHello(controlConn))
ack, err := ReadControlFrame(controlConn)
require.NoError(t, err)
require.Equal(t, controlCapabilities, ack.Capabilities)
_, err = readControlMessage(controlConn)
require.NoError(t, err)
require.NoError(t, writeControlMessage(controlConn, controlFrame{
Type: controlFrameLeaseRequest,
Version: controlProtocolVersion,
}, controlLeaseRequest{BusID: "1-1", ClientNonce: 42}))
leaseMessage, err := readControlMessage(controlConn)
require.NoError(t, err)
require.Equal(t, controlFrameLeaseResponse, leaseMessage.Frame.Type)
var lease controlLeaseResponse
require.NoError(t, unmarshalControlPayload(leaseMessage.Payload, &lease))
require.Empty(t, lease.ErrorCode)
require.Equal(t, uint64(42), lease.ClientNonce)
require.NotZero(t, lease.LeaseID)
require.NoError(t, writeControlMessage(controlConn, controlFrame{
Type: controlFrameLeaseRequest,
Version: controlProtocolVersion,
}, controlLeaseRequest{BusID: "1-1", ClientNonce: 43}))
busyMessage, err := readControlMessage(controlConn)
require.NoError(t, err)
var busy controlLeaseResponse
require.NoError(t, unmarshalControlPayload(busyMessage.Payload, &busy))
require.Equal(t, "busy", busy.ErrorCode)
importConn, err := net.Dial("tcp", serverAddr.String())
require.NoError(t, err)
require.NoError(t, WriteOpReqImportExt(importConn, ImportExtRequest{
BusID: "1-1",
LeaseID: lease.LeaseID,
ClientNonce: lease.ClientNonce,
}))
header, err := ReadOpHeader(importConn)
require.NoError(t, err)
require.Equal(t, OpRepImportExt, header.Code)
require.Equal(t, OpStatusOK, header.Status)
info, err := ReadOpRepImportBody(importConn)
require.NoError(t, err)
require.Equal(t, "1-1", info.BusIDString())
require.NoError(t, importConn.Close())
require.Positive(t, store.lastSockfd("1-1"))
reuseConn, err := net.Dial("tcp", serverAddr.String())
require.NoError(t, err)
defer reuseConn.Close()
require.NoError(t, WriteOpReqImportExt(reuseConn, ImportExtRequest{
BusID: "1-1",
LeaseID: lease.LeaseID,
ClientNonce: lease.ClientNonce,
}))
header, err = ReadOpHeader(reuseConn)
require.NoError(t, err)
require.Equal(t, OpRepImportExt, header.Code)
require.Equal(t, OpStatusError, header.Status)
}
func TestClientAttemptAttachUsesImportReplyAndVHCIAttach(t *testing.T) {
@@ -1301,6 +1406,135 @@ func TestClientAttemptAttachUsesImportReplyAndVHCIAttach(t *testing.T) {
require.Positive(t, store.lastSockfd("1-1"))
}
func TestClientAttemptAttachUsesImportExtLease(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
controlClient, controlServer := net.Pipe()
defer controlClient.Close()
defer controlServer.Close()
controlSession := newClientControlSession(controlClient, controlCapabilities)
controlErrCh := make(chan error, 1)
go func() {
message, err := readControlMessage(controlServer)
if err != nil {
controlErrCh <- err
return
}
if message.Frame.Type != controlFrameLeaseRequest {
controlErrCh <- fmt.Errorf("unexpected control frame %d", message.Frame.Type)
return
}
var request controlLeaseRequest
if err := unmarshalControlPayload(message.Payload, &request); err != nil {
controlErrCh <- err
return
}
if request.BusID != "1-1" {
controlErrCh <- fmt.Errorf("unexpected lease busid %s", request.BusID)
return
}
controlErrCh <- writeControlMessage(controlServer, controlFrame{
Type: controlFrameLeaseResponse,
Version: controlProtocolVersion,
}, controlLeaseResponse{
BusID: request.BusID,
LeaseID: 55,
ClientNonce: request.ClientNonce,
Generation: 2,
TTLMillis: int64(importLeaseTTL / time.Millisecond),
})
}()
deliverErrCh := make(chan error, 1)
go func() {
message, err := readControlMessage(controlClient)
if err != nil {
deliverErrCh <- err
return
}
if message.Frame.Type != controlFrameLeaseResponse {
deliverErrCh <- fmt.Errorf("unexpected control response %d", message.Frame.Type)
return
}
var response controlLeaseResponse
if err := unmarshalControlPayload(message.Payload, &response); err != nil {
deliverErrCh <- err
return
}
controlSession.deliverLeaseResponse(response)
deliverErrCh <- nil
}()
listener, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
defer listener.Close()
device := newTestDevice("1-1", 0x1d6b, 0x0002, "serial-1", SpeedHigh)
serverErrCh := make(chan error, 1)
go func() {
conn, acceptErr := listener.Accept()
if acceptErr != nil {
serverErrCh <- acceptErr
return
}
defer conn.Close()
header, readErr := ReadOpHeader(conn)
if readErr != nil {
serverErrCh <- readErr
return
}
if header.Code != OpReqImportExt {
serverErrCh <- fmt.Errorf("unexpected request code 0x%s", hex16(header.Code))
return
}
request, readErr := ReadOpReqImportExtBody(conn)
if readErr != nil {
serverErrCh <- readErr
return
}
if request.BusID != "1-1" || request.LeaseID != 55 || request.ClientNonce != 1 {
serverErrCh <- fmt.Errorf("unexpected import-ext request %+v", request)
return
}
info := device.toProtocol()
serverErrCh <- WriteOpRepImportExt(conn, OpStatusOK, &info)
}()
ops := newTestUSBIPOps(t)
ops.vhciPickFreePort = func(speed uint32) (int, error) {
require.Equal(t, SpeedHigh, speed)
return 4, nil
}
ops.vhciAttach = func(port int, _ uintptr, devid uint32, speed uint32) error {
require.Equal(t, 4, port)
info := device.toProtocol()
require.Equal(t, info.DevID(), devid)
require.Equal(t, SpeedHigh, speed)
return nil
}
client := &ClientService{
ctx: ctx,
cancel: cancel,
logger: newTestLogger(),
dialer: testDialer{},
serverAddr: M.SocksaddrFromNet(listener.Addr()),
ops: ops,
}
client.setControlSession(controlSession)
defer client.clearControlSession(controlSession, errClientControlSessionClosed)
port, err := client.attemptAttach(ctx, "1-1")
require.NoError(t, err)
require.Equal(t, 4, port)
require.NoError(t, <-controlErrCh)
require.NoError(t, <-deliverErrCh)
require.NoError(t, <-serverErrCh)
}
func TestClientAttemptAttachWithOpaqueConnRelay(t *testing.T) {
t.Parallel()
+52 -5
View File
@@ -14,10 +14,12 @@ const (
ProtocolVersion uint16 = 0x0111
OpReqDevList uint16 = 0x8005
OpRepDevList uint16 = 0x0005
OpReqImport uint16 = 0x8003
OpRepImport uint16 = 0x0003
OpReqDevList uint16 = 0x8005
OpRepDevList uint16 = 0x0005
OpReqImport uint16 = 0x8003
OpRepImport uint16 = 0x0003
OpReqImportExt uint16 = 0x8f03
OpRepImportExt uint16 = 0x0f03
OpStatusOK uint32 = 0
OpStatusError uint32 = 1
@@ -26,6 +28,7 @@ const (
maxOpRepDevListBodyBytes = 8 << 20
deviceInfoWireSize = 312
deviceInterfaceWireSize = 4
importExtBodyWireSize = 56
)
// USB speeds (enum usb_device_speed).
@@ -79,6 +82,13 @@ type DeviceEntry struct {
Interfaces []DeviceInterface
}
type ImportExtRequest struct {
BusID string
LeaseID uint64
ClientNonce uint64
Flags uint32
}
// WriteOpHeader emits the 8-byte OP header.
func WriteOpHeader(w io.Writer, code uint16, status uint32) error {
return binary.Write(w, binary.BigEndian, OpHeader{
@@ -118,6 +128,22 @@ func WriteOpReqImport(w io.Writer, busid string) error {
return binary.Write(w, binary.BigEndian, field)
}
func WriteOpReqImportExt(w io.Writer, request ImportExtRequest) error {
if err := WriteOpHeader(w, OpReqImportExt, OpStatusOK); 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
}
// ReadOpReqImportBody reads the 32-byte busid that follows the OP header.
func ReadOpReqImportBody(r io.Reader) (string, error) {
var field [32]byte
@@ -127,9 +153,30 @@ func ReadOpReqImportBody(r io.Reader) (string, error) {
return cstring(field[:]), nil
}
func ReadOpReqImportExtBody(r io.Reader) (ImportExtRequest, error) {
var raw [importExtBodyWireSize]byte
if _, err := io.ReadFull(r, raw[:]); 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
}
// WriteOpRepImport sends OP_REP_IMPORT. If status != OpStatusOK, info is omitted.
func WriteOpRepImport(w io.Writer, status uint32, info *DeviceInfoTruncated) error {
if err := WriteOpHeader(w, OpRepImport, status); err != nil {
return writeOpRepImport(w, OpRepImport, status, info)
}
func WriteOpRepImportExt(w io.Writer, status uint32, info *DeviceInfoTruncated) error {
return writeOpRepImport(w, OpRepImportExt, status, info)
}
func writeOpRepImport(w io.Writer, code uint16, status uint32, info *DeviceInfoTruncated) error {
if err := WriteOpHeader(w, code, status); err != nil {
return err
}
if status != OpStatusOK {
+111
View File
@@ -89,6 +89,57 @@ func TestControlPrefaceAndFrames(t *testing.T) {
}
}
func TestControlMessagePayloadRoundTrip(t *testing.T) {
t.Parallel()
payload := controlDeviceSnapshot{
Sequence: 3,
Devices: []DeviceInfoV2{{
BusID: "1-2",
StableID: "usb:1d6b:0002:serial-1",
Backend: "linux-sysfs",
VendorID: 0x1d6b,
ProductID: 0x0002,
Speed: SpeedHigh,
State: deviceStateAvailable,
}},
}
var buffer bytes.Buffer
require.NoError(t, writeControlMessage(&buffer, controlFrame{
Type: controlFrameDeviceSnapshot,
Version: controlProtocolVersion,
Sequence: 3,
}, payload))
message, err := readControlMessage(&buffer)
require.NoError(t, err)
require.Equal(t, controlFrameDeviceSnapshot, message.Frame.Type)
require.Equal(t, uint64(3), message.Frame.Sequence)
require.Positive(t, message.Frame.PayloadLength)
var decoded controlDeviceSnapshot
require.NoError(t, unmarshalControlPayload(message.Payload, &decoded))
require.Equal(t, payload, decoded)
}
func TestControlMessagePayloadSizeGuard(t *testing.T) {
t.Parallel()
var buffer bytes.Buffer
err := writeControlMessage(&buffer, controlFrame{Type: controlFrameDeviceSnapshot}, bytes.Repeat([]byte{'x'}, maxControlPayloadLength+1))
require.ErrorContains(t, err, "control payload too large")
}
func TestReadControlFrameRejectsPayload(t *testing.T) {
t.Parallel()
var buffer bytes.Buffer
require.NoError(t, writeControlMessage(&buffer, controlFrame{Type: controlFrameDeviceSnapshot}, []byte(`{}`)))
_, err := ReadControlFrame(&buffer)
require.ErrorContains(t, err, "unexpected control payload length")
}
func TestOpHeaderRoundTrip(t *testing.T) {
t.Parallel()
@@ -114,6 +165,28 @@ func TestOpHeaderRoundTrip(t *testing.T) {
}, ParseOpHeader(raw[:]))
}
func TestOpReqImportExtRoundTrip(t *testing.T) {
t.Parallel()
request := ImportExtRequest{
BusID: "1-2",
LeaseID: 9,
ClientNonce: 7,
Flags: 1,
}
var buffer bytes.Buffer
require.NoError(t, WriteOpReqImportExt(&buffer, request))
header, err := ReadOpHeader(&buffer)
require.NoError(t, err)
require.Equal(t, OpReqImportExt, header.Code)
require.Equal(t, OpStatusOK, header.Status)
parsed, err := ReadOpReqImportExtBody(&buffer)
require.NoError(t, err)
require.Equal(t, request, parsed)
}
func TestOpReqImportRoundTrip(t *testing.T) {
t.Parallel()
@@ -231,6 +304,44 @@ func TestDeviceInfoHelpers(t *testing.T) {
require.Equal(t, uint32(0x00030009), info.DevID())
}
func TestDeviceInfoV2RoundTrip(t *testing.T) {
t.Parallel()
var path [256]byte
encodePathField(&path, "/sys/bus/usb/devices/1-2", "serial-1")
entry := DeviceEntry{
Info: DeviceInfoTruncated{
Path: path,
BusNum: 1,
DevNum: 2,
Speed: SpeedSuper,
IDVendor: 0x1d6b,
IDProduct: 0x0002,
BCDDevice: 0x0100,
BDeviceClass: 0xff,
BDeviceSubClass: 1,
BDeviceProtocol: 2,
BConfigurationValue: 1,
BNumConfigurations: 1,
BNumInterfaces: 1,
},
Interfaces: []DeviceInterface{{BInterfaceClass: 0xff, BInterfaceSubClass: 1, BInterfaceProtocol: 2}},
}
copy(entry.Info.BusID[:], "1-2")
info := deviceInfoV2FromEntry(entry, "linux-sysfs", "usb:1d6b:0002:serial-1", deviceStateAvailable, 1, "available")
require.Equal(t, "1-2", info.BusID)
require.Equal(t, "serial-1", info.Serial)
require.True(t, info.available())
require.Equal(t, DeviceKey{BusID: "1-2", VendorID: 0x1d6b, ProductID: 0x0002, Serial: "serial-1"}, info.key())
roundTrip := info.toDeviceEntry()
require.Equal(t, "1-2", roundTrip.Info.BusIDString())
require.Equal(t, "serial-1", roundTrip.Info.SerialString())
require.Equal(t, uint16(0x1d6b), roundTrip.Info.IDVendor)
require.Equal(t, uint16(0x0002), roundTrip.Info.IDProduct)
require.Equal(t, entry.Interfaces, roundTrip.Interfaces)
}
func TestEncodePathFieldSkipsSerialWithoutRoom(t *testing.T) {
t.Parallel()
+260 -18
View File
@@ -33,9 +33,10 @@ type serverExport struct {
}
type serverControlConn struct {
id uint64
conn net.Conn
send chan controlFrame
id uint64
capabilities uint32
conn net.Conn
send chan controlOutboundMessage
}
type ServerService struct {
@@ -54,6 +55,10 @@ type ServerService struct {
controlSeq uint64
controlNextID uint64
controlSubs map[uint64]*serverControlConn
controlState map[string]DeviceInfoV2
leaseNextID uint64
leases map[uint64]serverImportLease
leaseByBusID map[string]uint64
reconcileMu sync.Mutex
}
@@ -81,7 +86,10 @@ func NewServerService(ctx context.Context, logger log.ContextLogger, tag string,
Network: []string{N.NetworkTCP},
Listen: options.ListenOptions,
}),
controlSubs: make(map[uint64]*serverControlConn),
controlSubs: make(map[uint64]*serverControlConn),
controlState: make(map[string]DeviceInfoV2),
leases: make(map[uint64]serverImportLease),
leaseByBusID: make(map[string]uint64),
}, nil
}
@@ -187,6 +195,8 @@ func (s *ServerService) reconcileAndBroadcast(notify bool) error {
}
if notify && changed {
s.broadcastChanged()
} else {
s.refreshControlState()
}
return nil
}
@@ -307,6 +317,8 @@ func (s *ServerService) handleStandardConn(conn net.Conn, header OpHeader) {
s.handleDevList(conn)
case OpReqImport:
s.handleImport(conn)
case OpReqImportExt:
s.handleImportExt(conn)
default:
s.logger.Debug("unknown opcode 0x", hex16(header.Code))
}
@@ -314,21 +326,26 @@ func (s *ServerService) handleStandardConn(conn net.Conn, header OpHeader) {
func (s *ServerService) handleControlConn(conn net.Conn) {
defer conn.Close()
hello, err := ReadControlFrame(conn)
helloMessage, err := readControlMessage(conn)
if err != nil {
s.logger.Debug("read control hello: ", err)
return
}
if hello.Type != controlFrameHello || hello.Version != controlProtocolVersion || hello.Capabilities&controlCapabilities != controlCapabilities {
hello := helloMessage.Frame
if hello.Type != controlFrameHello || hello.Version != controlProtocolVersion || hello.Capabilities&controlRequiredCapabilities != controlRequiredCapabilities {
s.logger.Debug("invalid control hello")
return
}
sub, seq := s.registerControlConn(conn)
capabilities := negotiatedControlCapabilities(hello.Capabilities)
sub, seq := s.registerControlConn(conn, capabilities)
defer s.unregisterControlConn(sub.id)
if err := WriteControlAck(conn, seq); err != nil {
if err := writeControlAckWithCapabilities(conn, seq, capabilities); err != nil {
s.logger.Debug("write control ack: ", err)
return
}
if supportsControlExtensions(capabilities) {
s.enqueueControlSnapshot(sub, seq)
}
readDone := make(chan struct{})
go s.readControlConn(sub, readDone)
for {
@@ -337,8 +354,8 @@ func (s *ServerService) handleControlConn(conn net.Conn) {
return
case <-readDone:
return
case frame := <-sub.send:
if err := writeControlFrame(conn, frame); err != nil {
case message := <-sub.send:
if err := writeControlMessage(conn, message.Frame, message.Payload); err != nil {
s.logger.Debug("write control frame: ", err)
return
}
@@ -349,13 +366,20 @@ func (s *ServerService) handleControlConn(conn net.Conn) {
func (s *ServerService) readControlConn(sub *serverControlConn, done chan<- struct{}) {
defer close(done)
for {
frame, err := ReadControlFrame(sub.conn)
message, err := readControlMessage(sub.conn)
if err != nil {
return
}
frame := message.Frame
switch frame.Type {
case controlFramePing:
s.enqueueControlFrame(sub, controlFrame{Type: controlFramePong, Version: controlProtocolVersion})
case controlFrameLeaseRequest:
if supportsControlExtensions(sub.capabilities) {
s.handleControlLeaseRequest(sub, message.Payload)
continue
}
return
default:
return
}
@@ -379,10 +403,32 @@ func (s *ServerService) handleImport(conn net.Conn) {
s.logger.Debug("read import body: ", err)
return
}
s.handleImportBusID(conn, busid, false)
}
func (s *ServerService) handleImportExt(conn net.Conn) {
request, err := ReadOpReqImportExtBody(conn)
if err != nil {
s.logger.Debug("read import-ext body: ", err)
return
}
if !s.consumeImportLease(request) {
s.logger.Info("import-ext rejected (invalid lease): ", request.BusID)
_ = WriteOpRepImportExt(conn, OpStatusError, nil)
return
}
s.handleImportBusID(conn, request.BusID, true)
}
func (s *ServerService) handleImportBusID(conn net.Conn, busid string, extended bool) {
writeReply := WriteOpRepImport
if extended {
writeReply = WriteOpRepImportExt
}
export, ok := s.claimExport(busid)
if !ok {
s.logger.Info("import rejected (unknown or busy busid): ", busid)
_ = WriteOpRepImport(conn, OpStatusError, nil)
_ = writeReply(conn, OpStatusError, nil)
return
}
releaseClaim := true
@@ -392,7 +438,7 @@ func (s *ServerService) handleImport(conn net.Conn) {
}
}()
info := export.entry.Info
if err := WriteOpRepImport(conn, OpStatusOK, &info); err != nil {
if err := writeReply(conn, OpStatusOK, &info); err != nil {
s.logger.Warn("reply import ", busid, ": ", err)
return
}
@@ -421,14 +467,15 @@ func (s *ServerService) reconcileLoop() {
}
}
func (s *ServerService) registerControlConn(conn net.Conn) (*serverControlConn, uint64) {
func (s *ServerService) registerControlConn(conn net.Conn, capabilities uint32) (*serverControlConn, uint64) {
s.controlMu.Lock()
defer s.controlMu.Unlock()
s.controlNextID++
sub := &serverControlConn{
id: s.controlNextID,
conn: conn,
send: make(chan controlFrame, 16),
id: s.controlNextID,
capabilities: capabilities,
conn: conn,
send: make(chan controlOutboundMessage, 16),
}
s.controlSubs[sub.id] = sub
return sub, s.controlSeq
@@ -438,6 +485,7 @@ func (s *ServerService) unregisterControlConn(id uint64) {
s.controlMu.Lock()
defer s.controlMu.Unlock()
delete(s.controlSubs, id)
s.deleteImportLeasesForSubscriberLocked(id)
}
func (s *ServerService) closeControlSubscribers() {
@@ -454,9 +502,14 @@ func (s *ServerService) closeControlSubscribers() {
}
func (s *ServerService) broadcastChanged() {
devices := s.buildDeviceStateV2()
nextState := deviceInfoV2Map(devices)
s.controlMu.Lock()
s.controlSeq++
sequence := s.controlSeq
delta := buildControlDeviceDelta(sequence, s.controlState, nextState)
s.controlState = nextState
subs := make([]*serverControlConn, 0, len(s.controlSubs))
for _, sub := range s.controlSubs {
subs = append(subs, sub)
@@ -464,19 +517,208 @@ func (s *ServerService) broadcastChanged() {
s.controlMu.Unlock()
frame := controlFrame{Type: controlFrameChanged, Version: controlProtocolVersion, Sequence: sequence}
for _, sub := range subs {
if supportsControlExtensions(sub.capabilities) {
s.enqueueControlPayload(sub, controlFrame{
Type: controlFrameDeviceDelta,
Version: controlProtocolVersion,
Sequence: sequence,
}, delta, frame)
continue
}
s.enqueueControlFrame(sub, frame)
}
}
func (s *ServerService) enqueueControlFrame(sub *serverControlConn, frame controlFrame) {
s.enqueueControlMessage(sub, controlOutboundMessage{Frame: frame})
}
func (s *ServerService) enqueueControlPayload(sub *serverControlConn, frame controlFrame, payload any, fallback controlFrame) {
rawPayload, err := marshalControlPayload(payload)
if err != nil || len(rawPayload) > maxControlPayloadLength {
s.enqueueControlFrame(sub, fallback)
return
}
s.enqueueControlMessage(sub, controlOutboundMessage{Frame: frame, Payload: rawPayload})
}
func (s *ServerService) enqueueControlSnapshot(sub *serverControlConn, sequence uint64) {
devices := s.buildDeviceStateV2()
s.controlMu.Lock()
s.controlState = deviceInfoV2Map(devices)
s.controlMu.Unlock()
s.enqueueControlPayload(sub, controlFrame{
Type: controlFrameDeviceSnapshot,
Version: controlProtocolVersion,
Sequence: sequence,
}, controlDeviceSnapshot{Sequence: sequence, Devices: devices}, controlFrame{
Type: controlFrameChanged,
Version: controlProtocolVersion,
Sequence: sequence,
})
}
func (s *ServerService) enqueueControlMessage(sub *serverControlConn, message controlOutboundMessage) {
select {
case sub.send <- frame:
case sub.send <- message:
default:
s.logger.Debug("control subscriber ", sub.id, " lagged behind")
_ = sub.conn.Close()
}
}
func (s *ServerService) refreshControlState() {
devices := s.buildDeviceStateV2()
s.controlMu.Lock()
s.controlState = deviceInfoV2Map(devices)
s.controlMu.Unlock()
}
func (s *ServerService) buildDeviceStateV2() []DeviceInfoV2 {
exports := s.currentExports()
if len(exports) == 0 {
return nil
}
devices := make([]DeviceInfoV2, 0, len(exports))
for _, export := range exports {
devices = append(devices, deviceInfoV2FromEntry(export.entry, "darwin-iokit", darwinStableID(export.registryID), deviceStateAvailable, 0, "available"))
}
return devices
}
func (s *ServerService) handleControlLeaseRequest(sub *serverControlConn, payload []byte) {
var request controlLeaseRequest
if err := unmarshalControlPayload(payload, &request); err != nil {
s.enqueueControlPayload(sub, controlFrame{
Type: controlFrameLeaseResponse,
Version: controlProtocolVersion,
}, controlLeaseResponse{
ErrorCode: "bad_request",
ErrorMessage: err.Error(),
}, controlFrame{Type: controlFrameChanged, Version: controlProtocolVersion, Sequence: s.currentControlSequence()})
return
}
response := s.createControlLeaseResponse(sub.id, request, s.darwinLeaseAvailable)
s.enqueueControlPayload(sub, controlFrame{
Type: controlFrameLeaseResponse,
Version: controlProtocolVersion,
}, response, controlFrame{Type: controlFrameChanged, Version: controlProtocolVersion, Sequence: s.currentControlSequence()})
}
func (s *ServerService) darwinLeaseAvailable(busid string) (bool, string) {
s.mu.Lock()
defer s.mu.Unlock()
export, ok := s.exports[busid]
if !ok {
return false, "unknown busid"
}
if export.busy {
return false, "busy"
}
return true, ""
}
func (s *ServerService) createControlLeaseResponse(subID uint64, request controlLeaseRequest, available func(string) (bool, string)) controlLeaseResponse {
response := controlLeaseResponse{
BusID: request.BusID,
ClientNonce: request.ClientNonce,
}
if request.BusID == "" {
response.ErrorCode = "bad_request"
response.ErrorMessage = "missing busid"
return response
}
if ok, reason := available(request.BusID); !ok {
response.ErrorCode = "unavailable"
response.ErrorMessage = reason
return response
}
now := time.Now()
expires := now.Add(importLeaseTTL)
s.controlMu.Lock()
defer s.controlMu.Unlock()
s.cleanupExpiredImportLeasesLocked(now)
if _, exists := s.leaseByBusID[request.BusID]; exists {
response.ErrorCode = "busy"
response.ErrorMessage = "lease already active"
return response
}
s.leaseNextID++
lease := serverImportLease{
ID: s.leaseNextID,
SubscriberID: subID,
BusID: request.BusID,
ClientNonce: request.ClientNonce,
Generation: s.controlSeq,
Expires: expires,
}
if s.leases == nil {
s.leases = make(map[uint64]serverImportLease)
}
if s.leaseByBusID == nil {
s.leaseByBusID = make(map[string]uint64)
}
s.leases[lease.ID] = lease
s.leaseByBusID[lease.BusID] = lease.ID
response.LeaseID = lease.ID
response.Generation = lease.Generation
response.TTLMillis = int64(importLeaseTTL / time.Millisecond)
return response
}
func (s *ServerService) consumeImportLease(request ImportExtRequest) bool {
now := time.Now()
s.controlMu.Lock()
defer s.controlMu.Unlock()
s.cleanupExpiredImportLeasesLocked(now)
lease, ok := s.leases[request.LeaseID]
if !ok {
return false
}
if lease.BusID != request.BusID || lease.ClientNonce != request.ClientNonce {
return false
}
delete(s.leases, request.LeaseID)
delete(s.leaseByBusID, request.BusID)
return now.Before(lease.Expires)
}
func (s *ServerService) cleanupExpiredImportLeasesLocked(now time.Time) {
if s.leases == nil {
s.leases = make(map[uint64]serverImportLease)
}
if s.leaseByBusID == nil {
s.leaseByBusID = make(map[string]uint64)
}
for id, lease := range s.leases {
if now.Before(lease.Expires) {
continue
}
delete(s.leases, id)
delete(s.leaseByBusID, lease.BusID)
}
}
func (s *ServerService) deleteImportLeasesForSubscriberLocked(subID uint64) {
for id, lease := range s.leases {
if lease.SubscriberID != subID {
continue
}
delete(s.leases, id)
delete(s.leaseByBusID, lease.BusID)
}
}
func (s *ServerService) currentControlSequence() uint64 {
s.controlMu.Lock()
defer s.controlMu.Unlock()
return s.controlSeq
}
func darwinStableID(registryID uint64) string {
return "darwin-registry:" + hex32(uint32(registryID>>32)) + hex32(uint32(registryID))
}
type darwinServerDataSession struct {
ctx context.Context
logger log.ContextLogger
+316 -23
View File
@@ -34,9 +34,10 @@ type serverExport struct {
}
type serverControlConn struct {
id uint64
conn net.Conn
send chan controlFrame
id uint64
capabilities uint32
conn net.Conn
send chan controlOutboundMessage
}
type ServerService struct {
@@ -56,6 +57,10 @@ type ServerService struct {
controlSeq uint64
controlNextID uint64
controlSubs map[uint64]*serverControlConn
controlState map[string]DeviceInfoV2
leaseNextID uint64
leases map[uint64]serverImportLease
leaseByBusID map[string]uint64
reconcileMu sync.Mutex
}
@@ -83,8 +88,11 @@ func NewServerService(ctx context.Context, logger log.ContextLogger, tag string,
Network: []string{N.NetworkTCP},
Listen: options.ListenOptions,
}),
controlSubs: make(map[uint64]*serverControlConn),
ops: systemUSBIPOps,
controlSubs: make(map[uint64]*serverControlConn),
controlState: make(map[string]DeviceInfoV2),
leases: make(map[uint64]serverImportLease),
leaseByBusID: make(map[string]uint64),
ops: systemUSBIPOps,
}
return s, nil
}
@@ -267,6 +275,8 @@ func (s *ServerService) reconcileAndBroadcast(notify bool) error {
}
if notify && changed {
s.broadcastChanged()
} else {
s.refreshControlState()
}
return nil
}
@@ -357,6 +367,8 @@ func (s *ServerService) handleStandardConn(conn net.Conn, header OpHeader) {
s.handleDevList(conn)
case OpReqImport:
closeConn = !s.handleImport(conn)
case OpReqImportExt:
closeConn = !s.handleImportExt(conn)
default:
s.logger.Debug("unknown opcode 0x", hex16(header.Code))
}
@@ -365,11 +377,12 @@ func (s *ServerService) handleStandardConn(conn net.Conn, header OpHeader) {
func (s *ServerService) handleControlConn(conn net.Conn) {
defer conn.Close()
hello, err := ReadControlFrame(conn)
helloMessage, err := readControlMessage(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
@@ -378,18 +391,22 @@ func (s *ServerService) handleControlConn(conn net.Conn) {
s.logger.Debug("unsupported control version ", hello.Version)
return
}
if hello.Capabilities&controlCapabilities != controlCapabilities {
if hello.Capabilities&controlRequiredCapabilities != controlRequiredCapabilities {
s.logger.Debug("missing control capabilities 0x", hello.Capabilities)
return
}
capabilities := negotiatedControlCapabilities(hello.Capabilities)
sub, seq := s.registerControlConn(conn)
sub, seq := s.registerControlConn(conn, capabilities)
defer s.unregisterControlConn(sub.id)
if err := WriteControlAck(conn, seq); err != nil {
if err := writeControlAckWithCapabilities(conn, seq, capabilities); err != nil {
s.logger.Debug("write control ack: ", err)
return
}
if supportsControlExtensions(capabilities) {
s.enqueueControlSnapshot(sub, seq)
}
readDone := make(chan struct{})
go s.readControlConn(sub, readDone)
@@ -399,8 +416,8 @@ func (s *ServerService) handleControlConn(conn net.Conn) {
return
case <-readDone:
return
case frame := <-sub.send:
if err := writeControlFrame(conn, frame); err != nil {
case message := <-sub.send:
if err := writeControlMessage(conn, message.Frame, message.Payload); err != nil {
s.logger.Debug("write control frame: ", err)
return
}
@@ -411,16 +428,23 @@ func (s *ServerService) handleControlConn(conn net.Conn) {
func (s *ServerService) readControlConn(sub *serverControlConn, done chan<- struct{}) {
defer close(done)
for {
frame, err := ReadControlFrame(sub.conn)
message, err := readControlMessage(sub.conn)
if err != nil {
return
}
frame := message.Frame
switch frame.Type {
case controlFramePing:
s.enqueueControlFrame(sub, controlFrame{
Type: controlFramePong,
Version: controlProtocolVersion,
})
case controlFrameLeaseRequest:
if supportsControlExtensions(sub.capabilities) {
s.handleControlLeaseRequest(sub, message.Payload)
continue
}
return
default:
return
}
@@ -468,41 +492,63 @@ func (s *ServerService) handleImport(conn net.Conn) bool {
s.logger.Debug("read import body: ", err)
return false
}
return s.handleImportBusID(conn, busid, false)
}
func (s *ServerService) handleImportExt(conn net.Conn) bool {
request, err := ReadOpReqImportExtBody(conn)
if err != nil {
s.logger.Debug("read import-ext body: ", err)
return false
}
if !s.consumeImportLease(request) {
s.logger.Info("import-ext rejected (invalid lease): ", request.BusID)
_ = WriteOpRepImportExt(conn, OpStatusError, nil)
return false
}
return s.handleImportBusID(conn, request.BusID, true)
}
func (s *ServerService) handleImportBusID(conn net.Conn, busid string, extended bool) bool {
writeReply := WriteOpRepImport
if extended {
writeReply = WriteOpRepImportExt
}
if !s.isExported(busid) {
s.logger.Info("import rejected (unknown busid): ", busid)
_ = WriteOpRepImport(conn, OpStatusError, nil)
_ = writeReply(conn, OpStatusError, nil)
return false
}
status, err := s.ops.readUsbipStatus(busid)
if err != nil || status != usbipStatusAvailable {
s.logger.Info("import rejected (busid ", busid, " status=", status, " err=", err, ")")
_ = WriteOpRepImport(conn, OpStatusError, nil)
_ = writeReply(conn, OpStatusError, nil)
return false
}
dev, err := s.ops.readSysfsDevice(busid, sysBusDevicePath(busid))
if err != nil {
s.logger.Warn("refresh ", busid, ": ", err)
_ = WriteOpRepImport(conn, OpStatusError, nil)
_ = writeReply(conn, OpStatusError, nil)
return false
}
handoff, err := newUSBIPConnHandoff(conn)
if err != nil {
s.logger.Warn("prepare handoff ", busid, ": ", err)
_ = WriteOpRepImport(conn, OpStatusError, nil)
_ = writeReply(conn, OpStatusError, nil)
return false
}
defer handoff.Close()
s.logger.Debug("usbip server handoff ", busid, ": ", handoff.mode())
if err := s.ops.writeUsbipSockfd(busid, int(handoff.kernelFD())); err != nil {
s.logger.Warn("hand off ", busid, " to kernel: ", err)
_ = WriteOpRepImport(conn, OpStatusError, nil)
_ = writeReply(conn, OpStatusError, nil)
return false
}
if err := handoff.closeKernelFD(); err != nil {
s.logger.Debug("close kernel fd ", busid, ": ", err)
}
info := dev.toProtocol()
if err := WriteOpRepImport(conn, OpStatusOK, &info); err != nil {
if err := writeReply(conn, OpStatusOK, &info); err != nil {
s.logger.Warn("reply import ", busid, ": ", err)
_ = s.ops.writeUsbipSockfd(busid, -1)
return false
@@ -577,14 +623,15 @@ func (s *ServerService) reconcileLoop() {
}
}
func (s *ServerService) registerControlConn(conn net.Conn) (*serverControlConn, uint64) {
func (s *ServerService) registerControlConn(conn net.Conn, capabilities uint32) (*serverControlConn, uint64) {
s.controlMu.Lock()
defer s.controlMu.Unlock()
s.controlNextID++
sub := &serverControlConn{
id: s.controlNextID,
conn: conn,
send: make(chan controlFrame, 16),
id: s.controlNextID,
capabilities: capabilities,
conn: conn,
send: make(chan controlOutboundMessage, 16),
}
s.controlSubs[sub.id] = sub
return sub, s.controlSeq
@@ -594,6 +641,7 @@ func (s *ServerService) unregisterControlConn(id uint64) {
s.controlMu.Lock()
defer s.controlMu.Unlock()
delete(s.controlSubs, id)
s.deleteImportLeasesForSubscriberLocked(id)
}
func (s *ServerService) closeControlSubscribers() {
@@ -610,9 +658,14 @@ func (s *ServerService) closeControlSubscribers() {
}
func (s *ServerService) broadcastChanged() {
devices := s.buildDeviceStateV2()
nextState := deviceInfoV2Map(devices)
s.controlMu.Lock()
s.controlSeq++
sequence := s.controlSeq
delta := buildControlDeviceDelta(sequence, s.controlState, nextState)
s.controlState = nextState
subs := make([]*serverControlConn, 0, len(s.controlSubs))
for _, sub := range s.controlSubs {
subs = append(subs, sub)
@@ -625,19 +678,255 @@ func (s *ServerService) broadcastChanged() {
Sequence: sequence,
}
for _, sub := range subs {
if supportsControlExtensions(sub.capabilities) {
s.enqueueControlPayload(sub, controlFrame{
Type: controlFrameDeviceDelta,
Version: controlProtocolVersion,
Sequence: sequence,
}, delta, frame)
continue
}
s.enqueueControlFrame(sub, frame)
}
}
func (s *ServerService) enqueueControlFrame(sub *serverControlConn, frame controlFrame) {
s.enqueueControlMessage(sub, controlOutboundMessage{Frame: frame})
}
func (s *ServerService) enqueueControlPayload(sub *serverControlConn, frame controlFrame, payload any, fallback controlFrame) {
rawPayload, err := marshalControlPayload(payload)
if err != nil || len(rawPayload) > maxControlPayloadLength {
s.enqueueControlFrame(sub, fallback)
return
}
s.enqueueControlMessage(sub, controlOutboundMessage{Frame: frame, Payload: rawPayload})
}
func (s *ServerService) enqueueControlSnapshot(sub *serverControlConn, sequence uint64) {
devices := s.buildDeviceStateV2()
s.controlMu.Lock()
s.controlState = deviceInfoV2Map(devices)
s.controlMu.Unlock()
s.enqueueControlPayload(sub, controlFrame{
Type: controlFrameDeviceSnapshot,
Version: controlProtocolVersion,
Sequence: sequence,
}, controlDeviceSnapshot{Sequence: sequence, Devices: devices}, controlFrame{
Type: controlFrameChanged,
Version: controlProtocolVersion,
Sequence: sequence,
})
}
func (s *ServerService) enqueueControlMessage(sub *serverControlConn, message controlOutboundMessage) {
select {
case sub.send <- frame:
case sub.send <- message:
default:
s.logger.Debug("control subscriber ", sub.id, " lagged behind")
_ = sub.conn.Close()
}
}
func (s *ServerService) refreshControlState() {
devices := s.buildDeviceStateV2()
s.controlMu.Lock()
s.controlState = deviceInfoV2Map(devices)
s.controlMu.Unlock()
}
func (s *ServerService) buildDeviceStateV2() []DeviceInfoV2 {
busids := s.currentExports()
if len(busids) == 0 {
return nil
}
devices := make([]DeviceInfoV2, 0, len(busids))
for _, busid := range busids {
status, statusErr := s.ops.readUsbipStatus(busid)
dev, devErr := s.ops.readSysfsDevice(busid, sysBusDevicePath(busid))
if devErr != nil {
devices = append(devices, DeviceInfoV2{
BusID: busid,
Backend: "linux-sysfs",
StableID: "linux-busid:" + busid,
State: deviceStateUnavailable,
StatusReason: devErr.Error(),
})
continue
}
state := linuxUSBIPStatusState(status)
reason := linuxUSBIPStatusReason(status)
if statusErr != nil {
state = deviceStateUnavailable
reason = statusErr.Error()
}
entry := DeviceEntry{Info: dev.toProtocol(), Interfaces: dev.Interfaces}
devices = append(devices, deviceInfoV2FromEntry(entry, "linux-sysfs", linuxStableID(dev), state, status, reason))
}
return devices
}
func (s *ServerService) handleControlLeaseRequest(sub *serverControlConn, payload []byte) {
var request controlLeaseRequest
if err := unmarshalControlPayload(payload, &request); err != nil {
s.enqueueControlPayload(sub, controlFrame{
Type: controlFrameLeaseResponse,
Version: controlProtocolVersion,
}, controlLeaseResponse{
ErrorCode: "bad_request",
ErrorMessage: err.Error(),
}, controlFrame{Type: controlFrameChanged, Version: controlProtocolVersion, Sequence: s.currentControlSequence()})
return
}
response := s.createControlLeaseResponse(sub.id, request, s.linuxLeaseAvailable)
s.enqueueControlPayload(sub, controlFrame{
Type: controlFrameLeaseResponse,
Version: controlProtocolVersion,
}, response, controlFrame{Type: controlFrameChanged, Version: controlProtocolVersion, Sequence: s.currentControlSequence()})
}
func (s *ServerService) linuxLeaseAvailable(busid string) (bool, string) {
if !s.isExported(busid) {
return false, "unknown busid"
}
status, err := s.ops.readUsbipStatus(busid)
if err != nil {
return false, err.Error()
}
if status != usbipStatusAvailable {
return false, linuxUSBIPStatusReason(status)
}
return true, ""
}
func (s *ServerService) createControlLeaseResponse(subID uint64, request controlLeaseRequest, available func(string) (bool, string)) controlLeaseResponse {
response := controlLeaseResponse{
BusID: request.BusID,
ClientNonce: request.ClientNonce,
}
if request.BusID == "" {
response.ErrorCode = "bad_request"
response.ErrorMessage = "missing busid"
return response
}
if ok, reason := available(request.BusID); !ok {
response.ErrorCode = "unavailable"
response.ErrorMessage = reason
return response
}
now := time.Now()
expires := now.Add(importLeaseTTL)
s.controlMu.Lock()
defer s.controlMu.Unlock()
s.cleanupExpiredImportLeasesLocked(now)
if _, exists := s.leaseByBusID[request.BusID]; exists {
response.ErrorCode = "busy"
response.ErrorMessage = "lease already active"
return response
}
s.leaseNextID++
lease := serverImportLease{
ID: s.leaseNextID,
SubscriberID: subID,
BusID: request.BusID,
ClientNonce: request.ClientNonce,
Generation: s.controlSeq,
Expires: expires,
}
if s.leases == nil {
s.leases = make(map[uint64]serverImportLease)
}
if s.leaseByBusID == nil {
s.leaseByBusID = make(map[string]uint64)
}
s.leases[lease.ID] = lease
s.leaseByBusID[lease.BusID] = lease.ID
response.LeaseID = lease.ID
response.Generation = lease.Generation
response.TTLMillis = int64(importLeaseTTL / time.Millisecond)
return response
}
func (s *ServerService) consumeImportLease(request ImportExtRequest) bool {
now := time.Now()
s.controlMu.Lock()
defer s.controlMu.Unlock()
s.cleanupExpiredImportLeasesLocked(now)
lease, ok := s.leases[request.LeaseID]
if !ok {
return false
}
if lease.BusID != request.BusID || lease.ClientNonce != request.ClientNonce {
return false
}
delete(s.leases, request.LeaseID)
delete(s.leaseByBusID, request.BusID)
return now.Before(lease.Expires)
}
func (s *ServerService) cleanupExpiredImportLeasesLocked(now time.Time) {
if s.leases == nil {
s.leases = make(map[uint64]serverImportLease)
}
if s.leaseByBusID == nil {
s.leaseByBusID = make(map[string]uint64)
}
for id, lease := range s.leases {
if now.Before(lease.Expires) {
continue
}
delete(s.leases, id)
delete(s.leaseByBusID, lease.BusID)
}
}
func (s *ServerService) deleteImportLeasesForSubscriberLocked(subID uint64) {
for id, lease := range s.leases {
if lease.SubscriberID != subID {
continue
}
delete(s.leases, id)
delete(s.leaseByBusID, lease.BusID)
}
}
func (s *ServerService) currentControlSequence() uint64 {
s.controlMu.Lock()
defer s.controlMu.Unlock()
return s.controlSeq
}
func linuxStableID(d sysfsDevice) string {
if d.Serial != "" {
return "usb:" + hex16(d.VendorID) + ":" + hex16(d.ProductID) + ":" + d.Serial
}
return "linux-busid:" + d.BusID
}
func linuxUSBIPStatusState(status int) string {
switch status {
case usbipStatusAvailable:
return deviceStateAvailable
case usbipStatusUsed:
return deviceStateBusy
default:
return deviceStateUnavailable
}
}
func linuxUSBIPStatusReason(status int) string {
switch status {
case usbipStatusAvailable:
return "available"
case usbipStatusUsed:
return "used"
case usbipStatusError:
return "error"
default:
return "status=" + hex32(uint32(status))
}
}
func sysBusDevicePath(busid string) string {
return sysBusUSBDevices + "/" + busid
}
@@ -691,6 +980,10 @@ func hex16(v uint16) string {
})
}
func hex32(v uint32) string {
return hex16(uint16(v>>16)) + hex16(uint16(v))
}
func joinComma(parts []string) string {
out := ""
for i, p := range parts {