domain

package
v1.0.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 18, 2026 License: Apache-2.0 Imports: 13 Imported by: 0

Documentation

Overview

Package domain defines the pure value objects, entities, events, and sentinel errors shared by the usbip-go library. No I/O, no goroutines, no third-party imports — only stdlib.

Index

Constants

View Source
const (
	DefaultPort     uint16 = 3240
	DefaultEndpoint        = "0.0.0.0:3240"

	// ProtocolVersion is the USBIP protocol version (matches kernel
	// include/uapi/linux/usbip.h). USBIP 1.1.1.
	ProtocolVersion uint16 = 0x0111

	// BusIDSize is the wire-format busid field length.
	BusIDSize = 32
	// SysPathSize is the wire-format sysfs-path field length.
	SysPathSize = 256

	// DefaultDialTimeout is the recommended dial budget for callers that want
	// an explicit bound. The transport zero value deliberately uses net.Dialer's
	// platform default and does not apply this constant automatically.
	DefaultDialTimeout = 10 * time.Second
	// DefaultHandshakeTimeout is the legacy recommended handshake budget.
	//
	// Deprecated: use DefaultExporterHandshakeTimeout for the runtime default or
	// configure an explicit timeout for the relevant operation.
	DefaultHandshakeTimeout = 5 * time.Second
	// DefaultExporterHandshakeTimeout is the Exporter's USB/IP OP handshake bound.
	DefaultExporterHandshakeTimeout = 10 * time.Second
	// DefaultShutdownTimeout is the CLI daemon's default graceful-shutdown budget.
	// Library callers control shutdown with their context or explicit options.
	DefaultShutdownTimeout = 30 * time.Second
)

DefaultPort is the conventional USB/IP port. IANA registered TCP/3240 for "Trio Motion Control Port" in 2002; USB/IP's use of the same port is a de-facto convention predating most implementations, and changing it would break interop with upstream usbip-utils, usbipd-win, and every other implementation.

Variables

View Source
var (
	// ErrDeviceNotFound indicates the requested busid is not present.
	ErrDeviceNotFound = errors.New("device not found")
	// ErrDeviceAlreadyBound indicates the device is already bound to
	// usbip-host and cannot be bound again.
	ErrDeviceAlreadyBound = errors.New("device already bound")
	// ErrDeviceNotBound indicates the device is not currently bound
	// to usbip-host.
	ErrDeviceNotBound = errors.New("device not bound")
	// ErrPortInUse indicates a local vhci port is already attached.
	ErrPortInUse = errors.New("port in use")
	// ErrNoFreePort indicates no vhci port is available for attach.
	ErrNoFreePort = errors.New("no free vhci port")
	// ErrProtocolMismatch indicates the peer reported a different
	// USBIP protocol version.
	ErrProtocolMismatch = errors.New("usbip protocol version mismatch")
	// ErrProtocolError indicates the peer replied with a protocol
	// error status code (OP_REP_*.status != 0 on a handshake frame).
	ErrProtocolError = errors.New("usbip protocol error reported by peer")
	// ErrAlreadyRunning indicates another exporter instance is holding
	// the shared PID lock.
	ErrAlreadyRunning = errors.New("another instance is already running")
	// ErrAlreadyShutdown indicates the exporter has been shut down and
	// cannot accept further requests.
	ErrAlreadyShutdown = errors.New("exporter already shut down")
	// ErrBusIDInvalid indicates a bus id failed validation.
	ErrBusIDInvalid = errors.New("invalid bus id")
	// ErrPermission indicates the caller lacks the privileges needed
	// (typically CAP_SYS_ADMIN or root).
	ErrPermission = errors.New("operation requires elevated privileges")
	// ErrKernelModuleMissing indicates a required kernel module
	// (usbip-core/usbip-host/vhci-hcd) is not loaded.
	ErrKernelModuleMissing = errors.New("required kernel module not loaded")
	// ErrAttachInProgress indicates Attach is already running for the
	// same (remote, busid) pair. Concurrent Attach calls race the
	// fd-passing handoff and the handle-map insert, so the deduper
	// rejects the second caller with this sentinel (importer-lifecycle OpenSpec).
	// Hosted on pkg/domain so the public facade can re-export it
	// alongside the other spec-listed sentinels.
	ErrAttachInProgress = errors.New("attach already in progress for this endpoint")
	// ErrUnsupportedDevice indicates the device cannot be exported via
	// usbip — typically a USB hub (bDeviceClass=0x09) or the vhci-hcd
	// loopback device. Distinct from ErrDeviceNotFound (device exists)
	// and ErrBusIDInvalid (id format is valid). Surfaced BEFORE any
	// destructive sysfs write so detaching a hub's drivers does not
	// cascade-disconnect downstream devices.
	ErrUnsupportedDevice = errors.New("device not supported for usbip export")
	// ErrDeviceUnavailable indicates the remote daemon reports the
	// device exists but is currently unusable for import (e.g.
	// stub-side internal error, ST_DEV_ERR=3 from upstream
	// usbip_common.h). Distinct from ErrDeviceAlreadyBound (busy/
	// already attached, ST_DEV_BUSY=2) and ErrDeviceNotFound
	// (ST_NODEV=4).
	ErrDeviceUnavailable = errors.New("remote device unavailable")
)

Sentinel errors returned by pkg/domain and by internal packages that surface domain-level conditions. Consumers match via errors.Is.

Message text is a stable part of the public API and MUST NOT be changed without a breaking-change bump. List ordering matches spec domain-model OpenSpec followed by the three additions in wire-protocol OpenSpec.

Functions

This section is empty.

Types

type BusID

type BusID string

BusID is the stable USB topology identifier (e.g. "1-1.2").

On-wire max size is 32 bytes (BusIDSize); we enforce length < BusIDSize to leave room for a trailing NUL byte when serialized.

func ParseBusID

func ParseBusID(s string) (BusID, error)

ParseBusID validates s and returns a BusID.

Rejects: empty strings, all-whitespace strings, strings with length >= BusIDSize, strings containing NUL bytes, strings with leading or trailing whitespace, and strings that do not match the Linux USB topology pattern ^[0-9]+-[0-9]+(\.[0-9]+)*$.

func ValidateWireBusID

func ValidateWireBusID(s string) (BusID, error)

ValidateWireBusID applies the wire-receive acceptance check: the field must be non-empty, shorter than the 32-byte field, and every byte must fall inside the sysfs-basename charset [A-Za-z0-9._-]. The charset is narrower than "printable ASCII" on purpose — peer busids flow directly into path.Join when the exporter opens the per-device sysfs attribute files, so any byte that could escape a basename (notably '/', but also whitespace and control bytes) is a traversal hazard. The charset still covers every real-world shape, including the vudc name "usbip-vudc.0"; user-facing entry points continue to route through ParseBusID's stricter topology check.

func (BusID) IsValid

func (b BusID) IsValid() bool

IsValid reports whether b is a non-zero, well-formed busid. Uses the same acceptance rules as ParseBusID. This makes BusID values constructed directly (bypassing ParseBusID) detectable via IsValid.

func (BusID) String

func (b BusID) String() string

String returns the busid as a string.

type Device

type Device struct {
	// Path is the sysfs path, meaningful for local devices only. It is
	// the empty string for Devices decoded from a remote OP_REP_DEVLIST.
	Path string

	BusID  BusID
	BusNum uint16
	DevNum uint16
	Speed  Speed

	// VendorID is the USB spec idVendor.
	VendorID uint16
	// ProductID is the USB spec idProduct.
	ProductID uint16
	// BcdDevice is the USB spec bcdDevice release number. The USB-spec
	// name is retained because it refers to a specific BCD-encoded field
	// on the wire and is not a mere prefix convention.
	BcdDevice uint16

	Class         USBClass
	Subclass      USBSubclass
	Protocol      USBProtocol
	ConfigValue   uint8
	NumConfigs    uint8
	NumInterfaces uint8

	// Manufacturer is the device-supplied iManufacturer string descriptor
	// resolved from the kernel via /sys/bus/usb/devices/<busid>/manufacturer.
	// Empty when the device descriptor's iManufacturer index is 0 or the
	// device does not implement string descriptors. Meaningful for local
	// devices only — OP_REP_DEVLIST does not carry these strings on the wire.
	Manufacturer string

	// Product is the device-supplied iProduct string descriptor (the human
	// product name, e.g. "AX88179 Gigabit Ethernet"). Distinct from
	// ProductID which is the numeric idProduct. Empty when not populated.
	// Meaningful for local devices only.
	Product string

	// Interfaces is an owned, read-only slice. Consumers MUST NOT mutate
	// the returned slice; future versions may copy-on-return if mutation
	// causes bugs.
	Interfaces []Interface
}

Device represents a USB device, either local (discovered via sysfs) or remote (decoded from an OP_REP_DEVLIST response). Fields follow Go naming conventions (noun-adjective) rather than USB spec names where the spec uses C-style prefixes.

type DeviceBoundEvent

type DeviceBoundEvent struct {
	At     time.Time
	Device Device
}

DeviceBoundEvent is emitted when a local device becomes exportable (bound to usbip-host).

func (DeviceBoundEvent) EventKind

func (DeviceBoundEvent) EventKind() EventKind

EventKind implements Event.

type DeviceID

type DeviceID uint32

DeviceID encodes (busnum << 16) | devnum, used in wire OP_REP_DEVLIST.

func (DeviceID) BusNum

func (d DeviceID) BusNum() uint16

BusNum returns the bus number encoded in the high 16 bits.

func (DeviceID) DevNum

func (d DeviceID) DevNum() uint16

DevNum returns the device number encoded in the low 16 bits.

type DeviceUnboundEvent

type DeviceUnboundEvent struct {
	At     time.Time
	Device Device
}

DeviceUnboundEvent is emitted when a local device is unbound from usbip-host and returned to its original driver.

func (DeviceUnboundEvent) EventKind

func (DeviceUnboundEvent) EventKind() EventKind

EventKind implements Event.

type Event

type Event interface {
	EventKind() EventKind
}

Event is the closed polymorphic union of domain events. Each concrete event type has a fixed EventKind() returning its discriminator.

type EventKind

type EventKind uint8

EventKind tags the concrete type of a domain event. Values are used as the JSON "kind" discriminator in json-contracts OpenSpec event streams.

const (
	EventPortAttached EventKind = iota
	EventPortDetached
	EventPortErrored
	EventPortReconnectExhausted
	EventDeviceBound
	EventDeviceUnbound
	EventSessionStarted
	EventSessionEnded
)

Event kinds. Canonical string forms are returned by EventKind.String and used as JSON discriminators.

func (EventKind) String

func (k EventKind) String() string

String returns the canonical snake_case kind discriminator. Unknown values return "event(N)" with N in decimal.

type Interface

type Interface struct {
	Class    USBClass
	Subclass USBSubclass
	Protocol USBProtocol

	// Alt is the bAlternateSetting. It is meaningful ONLY for Devices
	// constructed from local sysfs (e.g. ExporterKernel.ListLocalDevices).
	//
	// OP_REP_DEVLIST encodes each interface as 4 bytes — class, subclass,
	// protocol, padding — with no bAlternateSetting on the wire. For
	// remotely-listed Devices, Alt is always zero and MUST NOT be
	// interpreted as meaningful.
	Alt uint8
}

Interface describes a USB interface (bInterfaceClass/bInterfaceSubClass /bInterfaceProtocol triple).

Consumers MUST NOT mutate Device.Interfaces; treat it as read-only.

type Port

type Port struct {
	// ID is the numeric vhci port identifier.
	ID PortID
	// Status is the port state (available/used/error/...).
	Status Status
	// Speed is the negotiated USB speed.
	Speed Speed
	// DeviceID encodes (busnum << 16) | devnum of the remote device.
	DeviceID DeviceID
	// Remote is the peer address serving this port. It is zero when the
	// attachment is known only from kernel state because VHCI does not retain
	// exporter endpoint metadata.
	Remote RemoteEndpoint
	// BusID is the remote busid as reported by the exporter. It is empty when
	// the attachment is known only from kernel state.
	BusID BusID
	// LocalBusID is the local representation of the busid if one exists;
	// empty for ports without a local sysfs entry.
	LocalBusID BusID
}

Port describes one vhci kernel status row. StatusNull and StatusAvailable rows represent free capacity. StatusNotAssigned represents a claimed vdev that has not received its local USB address yet.

type PortAttachedEvent

type PortAttachedEvent struct {
	At   time.Time
	Port Port
}

PortAttachedEvent is emitted when a remote device is successfully attached.

func (PortAttachedEvent) EventKind

func (PortAttachedEvent) EventKind() EventKind

EventKind implements Event.

type PortDetachedEvent

type PortDetachedEvent struct {
	At     time.Time
	Port   Port
	Reason string
}

PortDetachedEvent is emitted when a previously-attached port is released. Reason is a free-form human-readable explanation.

func (PortDetachedEvent) EventKind

func (PortDetachedEvent) EventKind() EventKind

EventKind implements Event.

type PortErroredEvent

type PortErroredEvent struct {
	At   time.Time
	Port Port
	Err  string
}

PortErroredEvent is emitted when the vhci port transitions to the error state. Err is the error message captured at the transition.

func (PortErroredEvent) EventKind

func (PortErroredEvent) EventKind() EventKind

EventKind implements Event.

type PortID

type PortID uint32

PortID identifies a vhci port.

type PortReconnectExhaustedEvent

type PortReconnectExhaustedEvent struct {
	At        time.Time
	Port      Port
	Attempts  int
	LastError string
}

PortReconnectExhaustedEvent is emitted by the importer's reconnect watcher when MaxAttempts has been reached without a successful reattach. Port is a snapshot of the last successful Attach (the kernel slot is already gone at emission time, so this captures what was true while the port was viable). Attempts is the number of reconnect attempts actually made (not MaxAttempts). LastError is the stringified final attempt error; the domain layer does not carry Go error values across the JSON boundary.

LastError is diagnostic free-form text. Wrapping by the importer and kernel adapter typically embeds the peer endpoint, the BusID, and absolute sysfs paths. Downstream JSON consumers MUST treat this field as untrusted display text — do not parse it for control flow, and consider scrubbing it before forwarding to a less-privileged audience that should not see operator-level diagnostic strings.

func (PortReconnectExhaustedEvent) EventKind

EventKind implements Event.

type RemoteEndpoint

type RemoteEndpoint struct {
	Host string
	Port uint16 // zero means DefaultPort
}

RemoteEndpoint identifies a USB/IP peer by host and port.

Port zero is a sentinel meaning "use DefaultPort". The library never attempts to dial port 0; callers either construct a RemoteEndpoint with an explicit port or leave Port at its zero value to accept the default. Any library method that consumes a RemoteEndpoint calls NormalizePort internally, so the literal RemoteEndpoint{Host: "h"} behaves as "h:3240".

func ParseRemote

func ParseRemote(s string) (RemoteEndpoint, error)

ParseRemote accepts "host", "host:port", "[v6]:port", bare "v6" (multi-colon), or bracketed "[v6]" forms. Port defaults to DefaultPort when omitted. Port 0 is rejected as a sentinel value. Host strings are validated: no whitespace, no control characters, multi-colon forms must parse as a valid IP literal, and hostnames must satisfy RFC 1034 + RFC 1123 label rules.

func (RemoteEndpoint) NormalizePort

func (r RemoteEndpoint) NormalizePort() RemoteEndpoint

NormalizePort returns a copy of r with Port set to DefaultPort when the original Port was zero. The input is unchanged.

func (RemoteEndpoint) String

func (r RemoteEndpoint) String() string

String returns "host:port" using DefaultPort when r.Port is zero. IPv6 hosts are bracketed via net.JoinHostPort.

func (RemoteEndpoint) Validate

func (r RemoteEndpoint) Validate() error

Validate applies the same host-acceptance rules as ParseRemote to a programmatically constructed endpoint. Callers that accept a RemoteEndpoint from outside their own code (public API boundaries, RPC payloads, config deserialisation) invoke Validate before dialing; this keeps the dialer path pure and makes empty-Host input a first-class error instead of an accidental loopback dial.

type Session

type Session struct {
	// ID is the unique session identifier (UUIDv7).
	ID SessionID
	// RemoteAddr is the peer address+port as observed by the daemon.
	RemoteAddr netip.AddrPort
	// BusID is the exported device's busid for this session.
	BusID BusID
	// StartedAt is the wall-clock time at which the session handshake completed.
	StartedAt time.Time
	// BytesIn counts bytes received from the peer.
	BytesIn uint64
	// BytesOut counts bytes transmitted to the peer.
	BytesOut uint64
}

Session describes a single client connection from the daemon's view.

type SessionEndedEvent

type SessionEndedEvent struct {
	At      time.Time
	Session Session
	Reason  string
}

SessionEndedEvent is emitted when a Session closes, for any reason.

func (SessionEndedEvent) EventKind

func (SessionEndedEvent) EventKind() EventKind

EventKind implements Event.

type SessionID

type SessionID [16]byte

SessionID is a 16-byte UUIDv7 identifying a single client connection. The UUIDv7 layout (RFC 9562 §5.7) embeds a millisecond Unix timestamp in the high 48 bits, giving sessions a natural chronological ordering.

func NewSessionID

func NewSessionID() (SessionID, error)

NewSessionID returns a freshly-generated UUIDv7. It returns an error when the underlying crypto/rand source fails.

Layout per RFC 9562 §5.7:

bytes 0..5  unix_ts_ms (48 bits, big-endian)
byte  6     version 7 in high nibble + rand_a[0..3] in low nibble
byte  7     rand_a[4..11]
byte  8     variant `10` in top 2 bits + rand_b[0..5]
bytes 9..15 rand_b[6..69]

Implemented inline against crypto/rand instead of a third-party UUID package so pkg/domain stays a pure-stdlib value-object surface (the invariant the Bazel-backed lint suite protects).

func (SessionID) String

func (id SessionID) String() string

String returns the canonical 36-character hyphenated UUID form (8-4-4-4-12 lowercase hex), matching the output of every RFC 9562-conformant UUID library. The five hex segments are emitted then the four hyphens overwrite the segment boundaries in a separate pass — wsl_v5 dislikes the alternating call / assign / call shape that the inline form would produce.

type SessionStartedEvent

type SessionStartedEvent struct {
	At      time.Time
	Session Session
}

SessionStartedEvent is emitted when a client completes the USBIP handshake and is assigned a Session.

func (SessionStartedEvent) EventKind

func (SessionStartedEvent) EventKind() EventKind

EventKind implements Event.

type Speed

type Speed uint32

Speed is a USB speed enum matching kernel enum usb_device_speed values. Sysfs reports Mbps strings ("5000"), not these integers; the kernel adapter translates via ReadSpeedAttr before populating this field.

const (
	SpeedUnknown   Speed = 0
	SpeedLow       Speed = 1 // 1.5 Mbit/s
	SpeedFull      Speed = 2 // 12 Mbit/s
	SpeedHigh      Speed = 3 // 480 Mbit/s
	SpeedWireless  Speed = 4
	SpeedSuper     Speed = 5 // 5 Gbit/s
	SpeedSuperPlus Speed = 6 // 10 or 20 Gbit/s (USB 3.1 Gen 2, USB 3.2 Gen 2x2)
)

USB device speeds, numeric values match the kernel's usb_device_speed.

func (Speed) IsKnown

func (s Speed) IsKnown() bool

IsKnown reports whether s falls inside the finite enum declared above. Wire decoders call IsKnown after reading the 4-byte field so a peer emitting a value the kernel does not define cannot round-trip as a mystery domain.Speed that downstream consumers (structured logs, CLI rendering, event delivery) would silently carry.

func (Speed) String

func (s Speed) String() string

String returns a human-readable description of the speed. Unknown values return "speed(N)" with N in decimal.

type Status

type Status uint32

Status is a vhci port state matching kernel vdev_status values.

const (
	StatusNull        Status = 0
	StatusNotAssigned Status = 1
	StatusAvailable   Status = 2
	StatusUsed        Status = 3
	StatusError       Status = 4
)

Port statuses (kernel vdev_status order).

func (Status) String

func (s Status) String() string

String returns a human-readable kebab-case label for s. Unknown values return "status(N)" with N in decimal.

type USBClass

type USBClass uint8

USBClass is a USB device/interface class code (bDeviceClass/bInterfaceClass).

func (USBClass) String

func (c USBClass) String() string

String returns the canonical name from the committed usb.ids subset (see class_data.go). Unknown values return "class(0xNN)".

type USBProtocol

type USBProtocol uint8

USBProtocol is a USB device/interface protocol code. Protocol semantics depend on the owning USBClass + USBSubclass.

func (USBProtocol) String

func (p USBProtocol) String() string

String returns a hex-formatted protocol code (Protocol meaning is only defined within its owning class+subclass).

type USBSubclass

type USBSubclass uint8

USBSubclass is a USB device/interface subclass code. Subclass semantics depend on the owning USBClass; see usb.org class specifications.

func (USBSubclass) String

func (s USBSubclass) String() string

String returns a hex-formatted subclass code (Subclass meaning is only defined within its owning class; the committed data is class-indexed).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL