diff --git a/service/usbip/client_control.go b/service/usbip/client_control.go new file mode 100644 index 000000000..a7cd97056 --- /dev/null +++ b/service/usbip/client_control.go @@ -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() +} diff --git a/service/usbip/client_darwin.go b/service/usbip/client_darwin.go index 8ec8ef538..209943b13 100644 --- a/service/usbip/client_darwin.go +++ b/service/usbip/client_darwin.go @@ -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 { diff --git a/service/usbip/client_linux.go b/service/usbip/client_linux.go index 607bb4776..5fb7f7174 100644 --- a/service/usbip/client_linux.go +++ b/service/usbip/client_linux.go @@ -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 { diff --git a/service/usbip/control_protocol.go b/service/usbip/control_protocol.go index d83d41fe1..e30e0b1b8 100644 --- a/service/usbip/control_protocol.go +++ b/service/usbip/control_protocol.go @@ -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 +} diff --git a/service/usbip/darwin_integration_test.go b/service/usbip/darwin_integration_test.go index 07e3aa0bc..20de12593 100644 --- a/service/usbip/darwin_integration_test.go +++ b/service/usbip/darwin_integration_test.go @@ -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) diff --git a/service/usbip/linux_test.go b/service/usbip/linux_test.go index 2509eedf7..2de834e80 100644 --- a/service/usbip/linux_test.go +++ b/service/usbip/linux_test.go @@ -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() diff --git a/service/usbip/protocol.go b/service/usbip/protocol.go index 36c5eed4c..a7a4121a1 100644 --- a/service/usbip/protocol.go +++ b/service/usbip/protocol.go @@ -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 { diff --git a/service/usbip/protocol_test.go b/service/usbip/protocol_test.go index 87ca90a8d..875d8f7fd 100644 --- a/service/usbip/protocol_test.go +++ b/service/usbip/protocol_test.go @@ -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() diff --git a/service/usbip/server_darwin.go b/service/usbip/server_darwin.go index 77115adda..83e6d5aab 100644 --- a/service/usbip/server_darwin.go +++ b/service/usbip/server_darwin.go @@ -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 diff --git a/service/usbip/server_linux.go b/service/usbip/server_linux.go index 469e3c00b..9ff636c52 100644 --- a/service/usbip/server_linux.go +++ b/service/usbip/server_linux.go @@ -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 {