Documentation
¶
Overview ¶
Package app holds the USB/IP use-case services (importer, exporter, session lifecycle, reconnect loop) and the adapter interfaces they depend on. It is transport-free and kernel-free: concrete infrastructure lives in `internal/adapter/*` and is injected at construction time.
Index ¶
- Variables
- func ValidateAcceptRateLimit(rps float64) error
- func ValidateExporterACL(cidrs []string) error
- func ValidateTransportOptions(opts TransportOptions) error
- type AttachOptions
- type AttachOutcome
- type BackoffStrategy
- type BindOutcome
- type Clock
- type DetachOutcome
- type Dialer
- type DisconnectReason
- type ExponentialBackoff
- type ExponentialBackoffConfig
- type Exporter
- func (e *Exporter) Bind(ctx context.Context, busID domain.BusID) error
- func (e *Exporter) ListAvailable(ctx context.Context) ([]domain.Device, error)
- func (e *Exporter) ListExported(ctx context.Context) ([]domain.Device, error)
- func (e *Exporter) Serve(ctx context.Context, listener net.Listener) error
- func (e *Exporter) ServeWithListenerFactory(ctx context.Context, factory func(context.Context) (net.Listener, error)) error
- func (e *Exporter) Sessions(_ context.Context) []domain.Session
- func (e *Exporter) Shutdown(ctx context.Context) error
- func (e *Exporter) Unbind(ctx context.Context, busID domain.BusID) error
- func (e *Exporter) WatchSessions(ctx context.Context) iter.Seq[domain.Event]
- type ExporterKernel
- type ExporterOption
- func WithExporterACL(cidrs ...string) ExporterOption
- func WithExporterAcceptRateLimit(rps float64, burst int) ExporterOption
- func WithExporterClock(clk Clock) ExporterOption
- func WithExporterCodec(p ProtocolCodec) ExporterOption
- func WithExporterEvents(e KernelEvents) ExporterOption
- func WithExporterHandshakeTimeout(d time.Duration) ExporterOption
- func WithExporterKernel(k ExporterKernel) ExporterOption
- func WithExporterLogger(l *slog.Logger) ExporterOption
- func WithExporterMaxHandshakeBytes(n int) ExporterOption
- func WithExporterMaxSessions(n int) ExporterOption
- func WithExporterMaxSessionsPerPeer(n int) ExporterOption
- func WithExporterShutdownTimeout(d time.Duration) ExporterOption
- func WithExporterStatusPollInterval(d time.Duration) ExporterOption
- type FixedBackoff
- type HandshakeOp
- type Importer
- func (i *Importer) Attach(ctx context.Context, endpoint domain.RemoteEndpoint, busID domain.BusID, ...) (domain.Port, error)
- func (i *Importer) Close() error
- func (i *Importer) Detach(ctx context.Context, id domain.PortID) error
- func (i *Importer) ListPorts(ctx context.Context) ([]domain.Port, error)
- func (i *Importer) ListRemote(ctx context.Context, endpoint domain.RemoteEndpoint) ([]domain.Device, error)
- func (i *Importer) Watch(ctx context.Context) iter.Seq[domain.Event]
- func (i *Importer) WatchWithErrors(ctx context.Context) iter.Seq2[domain.Event, error]
- type ImporterKernel
- type ImporterOption
- func WithImporterClock(clk Clock) ImporterOption
- func WithImporterCodec(p ProtocolCodec) ImporterOption
- func WithImporterEvents(e KernelEvents) ImporterOption
- func WithImporterKernel(k ImporterKernel) ImporterOption
- func WithImporterLogger(l *slog.Logger) ImporterOption
- func WithImporterTransport(t Transport) ImporterOption
- func WithImporterTransportOptions(opts TransportOptions) ImporterOption
- type KernelEvents
- type KernelModule
- type ProtocolCodec
- type RealClock
- type ReconnectOutcome
- type RemoteDeviceSpec
- type SessionOutcome
- type Transport
- type TransportOptions
- type UnbindOutcome
Constants ¶
This section is empty.
Variables ¶
var ( // ErrAttachInProgress indicates Attach is already running on the // same busid/remote pair. Concurrent Attach calls would race the // fd-passing handoff and corrupt the handle map. Aliased to // pkg/domain so the public facade re-exports the same identity. ErrAttachInProgress = domain.ErrAttachInProgress // ErrHandleNotFound indicates Detach or ListPorts was called with // a PortID that is not tracked by the Importer. Typical causes: // the port was already detached, or the Importer never attached // it in the first place. ErrHandleNotFound = errors.New("port handle not found") // ErrImporterClosed indicates a method was called after Close. // Close is idempotent (a second Close returns nil), but other // operations on a closed Importer return this sentinel. ErrImporterClosed = errors.New("importer closed") // ErrEventStreamClosed indicates an established KernelEvents source // closed while both the caller context and Importer remained live. // Normal caller cancellation and Importer.Close are not errors. ErrEventStreamClosed = errors.New("event stream closed unexpectedly") // ErrPortDetached is the seed error handed to the reconnect // watcher's OnReconnect callback on the FIRST attempt: the attach // succeeded but the port was subsequently torn down. Wrapped by // the watcher with the port id and detection source context. ErrPortDetached = errors.New("port detached") // ErrAlreadyShutdown indicates Serve was called after Shutdown // completed. Shutdown is terminal: callers must construct a new // Exporter to serve again. ErrAlreadyShutdown = errors.New("exporter already shut down") // ErrServeAlreadyRunning indicates Serve was called while another // Serve is still active on the same Exporter. Overlapping accept // loops would fight over the shared session bookkeeping, so the // second call is rejected. ErrServeAlreadyRunning = errors.New("exporter: Serve already running") // ErrMaxSessionsExceeded indicates the global session cap is full // (security-release-quality OpenSpec). Returned from the per-session handler before // ExportOnConn so the kernel is never asked to attach past the cap. ErrMaxSessionsExceeded = errors.New("max sessions exceeded") // ErrPerPeerLimitExceeded indicates the per-source-IP session cap // is full (security-release-quality OpenSpec). ErrPerPeerLimitExceeded = errors.New("per-peer session limit exceeded") // ErrRateLimited indicates the accept-rate token bucket had no // tokens available (security-release-quality OpenSpec). The connection is closed without // invoking the kernel adapter. ErrRateLimited = errors.New("accept rate limit exceeded") // ErrAcceptRateLimitInvalid indicates the configured accepts-per-second // value is not finite. Finite zero and negative values intentionally remain // the documented disabled form. ErrAcceptRateLimitInvalid = errors.New("accept rate limit invalid") // ErrHandshakeTooLarge indicates the client sent more than // MaxHandshakeBytes before completing the OP request (security-release-quality OpenSpec). ErrHandshakeTooLarge = errors.New("handshake payload exceeds max bytes") // ErrHandshakeTimeout indicates the client failed to complete its // OP request within HandshakeTimeout (security-release-quality OpenSpec). ErrHandshakeTimeout = errors.New("handshake timed out") // ErrACLRejected indicates the accepted peer's remote IP is not // covered by any configured CIDR in the allow-list (security-release-quality OpenSpec). ErrACLRejected = errors.New("peer rejected by ACL") // ErrACLInvalid indicates one of the WithExporterACL CIDR strings // failed to parse. Surfaced at NewExporterWithError time — a Serve- // time failure would be a silent security regression. ErrACLInvalid = errors.New("invalid ACL CIDR") // ErrAttachOptionsInvalid indicates an AttachOptions field carried // a value the reconnect machinery cannot interpret (e.g. a // negative MaxAttempts, which the loop gate would silently treat // as "never retry"). Surfaced from Attach before the kernel is // involved so the caller sees a classifiable error at the entry // point rather than a misleading "giving up after max attempts" // log later. ErrAttachOptionsInvalid = errors.New("attach options invalid") )
Sentinel errors returned by internal/app services. Consumers in the public facade (pkg/usbip) re-export a subset; the rest are internal signalling between the service layer and its callers.
var ErrTransportOptionsInvalid = netopts.ErrTransportOptionsInvalid
ErrTransportOptionsInvalid is the sentinel for TransportOptions validation failures. Re-exported from internal/netopts so callers can match either app.ErrTransportOptionsInvalid (the surface they reach via WithImporterTransportOptions) or netopts.ErrTransportOptionsInvalid (the layer that owns the type) — errors.Is collapses them.
Functions ¶
func ValidateAcceptRateLimit ¶
ValidateAcceptRateLimit rejects values a token bucket cannot interpret safely. Omission is tracked separately by exporterConfig; every finite value is valid, with zero and negatives selecting the documented disabled form.
func ValidateExporterACL ¶
ValidateExporterACL checks public Exporter allow-list configuration without exposing the internal aclChecker representation. The public facade calls this before platform-specific adapter construction so invalid CIDRs take precedence over unsupported-platform errors.
func ValidateTransportOptions ¶
func ValidateTransportOptions(opts TransportOptions) error
ValidateTransportOptions delegates to netopts.Validate. Re-exported so callers in this package and its tests can use the app-level surface without an additional import. The returned error already wraps netopts.ErrTransportOptionsInvalid (which app re-exports as ErrTransportOptionsInvalid via type alias), so further wrapping would only obscure the field name without adding context — the wrapcheck exclusion in .golangci.yml documents this.
Types ¶
type AttachOptions ¶
type AttachOptions struct {
// AutoReconnect enables the reconnect watcher. When true, Attach
// spawns a watcher goroutine that re-establishes the attach after
// uevent- or poll-detected detach.
AutoReconnect bool
// Backoff computes delays between reconnect attempts. When nil
// and AutoReconnect is true, the watcher defaults to
// ExponentialBackoff{Min:1s, Max:60s, Jitter:0.2}.
Backoff BackoffStrategy
// BackoffFactory is the facade-owned construction seam for custom
// importer defaults. Attach invokes it at most once for a top-level logical
// reconnect lineage, then threads the returned strategy through every
// replacement generation. Callers outside the parent module cannot import
// internal/app, so this does not expand the public v1 API.
BackoffFactory func() BackoffStrategy
// MaxAttempts caps the number of reconnect retries. Zero means
// infinite.
MaxAttempts int
// OnReconnect receives retry notifications with the 1-indexed
// attempt number and the error that triggered the retry. nil disables
// the callback.
//
// A single separate goroutine invokes callbacks, so they never overlap
// and a slow callback cannot stall the retry cadence. If attempts
// outpace the callback, pending notifications are coalesced to the
// latest attempt. The callback may run concurrently with other Importer
// operations (Detach, Close, or an in-flight reconnect). Panics are
// recovered and logged via the Importer's logger but are not propagated
// to the caller or watcher goroutine.
OnReconnect func(attempt int, err error)
// StatusPollInterval controls the backstop poll period. Defaults
// to 5 seconds when zero; a negative value disables the poll
// entirely.
StatusPollInterval time.Duration
// ShutdownTimeout bounds how long Detach and Close are willing to
// wait for the watcher goroutine (and any in-flight Detach-driven
// sysfs write) to drain before proceeding anyway. Zero means use
// the importer-lifecycle OpenSpec default of 5 seconds; a negative value disables the
// bound (wait indefinitely).
ShutdownTimeout time.Duration
// contains filtered or unexported fields
}
AttachOptions configures a single Importer.Attach call. All fields are optional; zero values produce the documented defaults.
type AttachOutcome ¶
type AttachOutcome string
AttachOutcome classifies how an Importer.Attach resolved.
const ( AttachOutcomeOK AttachOutcome = "ok" AttachOutcomePermission AttachOutcome = "permission" AttachOutcomeNoFreePort AttachOutcome = "no_free_port" AttachOutcomeProtocolMismatch AttachOutcome = "protocol_mismatch" AttachOutcomeDialFailed AttachOutcome = "dial_failed" AttachOutcomeKernelError AttachOutcome = "kernel_error" )
AttachOutcome values.
type BackoffStrategy ¶
type BackoffStrategy interface {
// Next returns the delay to sleep before attempt number `attempt`
// (0-indexed — attempt 0 is the first retry after the initial
// failure). Implementations MAY ignore the attempt number (FixedBackoff)
// or use it as an exponent (ExponentialBackoff).
Next(attempt int) time.Duration
// Reset returns the strategy to its initial state. Called after
// a successful reconnect so the next failure starts from the
// smallest delay again.
Reset()
}
BackoffStrategy computes the sleep interval between reconnect attempts. Concrete implementations are FixedBackoff and ExponentialBackoff. See importer-lifecycle OpenSpec.
type BindOutcome ¶
type BindOutcome string
BindOutcome classifies how an Exporter.Bind resolved.
const ( BindOutcomeOK BindOutcome = "ok" BindOutcomeAlreadyBound BindOutcome = "already_bound" BindOutcomeNotFound BindOutcome = "not_found" BindOutcomePermission BindOutcome = "permission" BindOutcomeError BindOutcome = "error" )
BindOutcome values.
type Clock ¶
type Clock interface {
// Now returns the current time.
Now() time.Time
// Sleep pauses the calling goroutine for at least d.
Sleep(d time.Duration)
// After returns a channel that delivers the time after d has elapsed.
After(d time.Duration) <-chan time.Time
}
Clock abstracts the stdlib time functions the app layer depends on. Concrete code uses RealClock; tests inject a deterministic fake from internal/testutil.
architecture-layering OpenSpec: retry/backoff and attach timeouts must be driven by an injected clock so tests do not slow down wall time.
type DetachOutcome ¶
type DetachOutcome string
DetachOutcome classifies how an Importer.Detach resolved.
const ( DetachOutcomeOK DetachOutcome = "ok" DetachOutcomeNotFound DetachOutcome = "not_found" DetachOutcomeError DetachOutcome = "error" )
DetachOutcome values.
type Dialer ¶
type Dialer interface {
Dial(ctx context.Context, endpoint domain.RemoteEndpoint, opts TransportOptions) (net.Conn, error)
}
Dialer abstracts importer-side TCP connection establishment. Production uses internal/adapter/transport; tests inject a fake.
type DisconnectReason ¶
type DisconnectReason string
DisconnectReason classifies why a Session ended.
const ( DisconnectReasonClientGone DisconnectReason = "client_gone" DisconnectReasonKernelError DisconnectReason = "kernel_error" DisconnectReasonShutdown DisconnectReason = "shutdown" DisconnectReasonProtocolError DisconnectReason = "protocol_error" )
DisconnectReason values. ClientGone covers the graceful path — the kernel publishes PortDetached / DeviceUnbound when the importer peer detaches its port or the operator runs `usbip-go unbind` (equivalently upstream `usbip unbind`). There is no separate "graceful" reason: a session that ends without an error always ends because the kernel signalled one of those two events.
type ExponentialBackoff ¶
type ExponentialBackoff struct {
// contains filtered or unexported fields
}
ExponentialBackoff doubles the base delay every attempt, clamps at Max, and optionally jitters by ±Jitter. The structure is safe for concurrent Next calls: the internal PRNG access is serialised by a mutex because *rand.Rand is not safe for concurrent use.
func NewExponentialBackoff ¶
func NewExponentialBackoff(cfg ExponentialBackoffConfig) *ExponentialBackoff
NewExponentialBackoff constructs a pointer-receiver ExponentialBackoff from cfg. When cfg.Rand is nil and cfg.Jitter > 0, a fresh PRNG is seeded from the global rand source so production callers don't have to wire one in.
func (*ExponentialBackoff) Next ¶
func (b *ExponentialBackoff) Next(attempt int) time.Duration
Next returns the delay for the given attempt. The delay is Min * 2^attempt, clamped to Max, then optionally jittered. Negative attempts are treated as zero so the first retry always sleeps at least Min (before jitter).
func (*ExponentialBackoff) Reset ¶
func (*ExponentialBackoff) Reset()
Reset clears attempt-dependent state. ExponentialBackoff stores no per-attempt state (Next is a pure function of its input), so Reset is a no-op.
type ExponentialBackoffConfig ¶
type ExponentialBackoffConfig struct {
// Min is the delay for the first attempt (attempt 0). Zero intentionally
// collapses every delay to zero.
Min time.Duration
// Max caps every returned delay. Zero is coherent with a zero Min.
Max time.Duration
// Jitter is the fractional range applied multiplicatively to the
// base delay: the returned delay is sampled uniformly from
// [base*(1-Jitter), base*(1+Jitter)]. Must be in [0, 1). A zero
// Jitter disables jitter entirely and Next returns the deterministic
// geometric value.
Jitter float64
// Rand is the PRNG source for jitter sampling. Tests inject a
// seeded *rand.Rand for deterministic output; production code
// leaves it nil and the constructor supplies a fresh random
// source. Ignored when Jitter == 0.
Rand *rand.Rand
}
ExponentialBackoffConfig configures an ExponentialBackoff at construction time. Public validation permits non-negative bounds for v1 compatibility.
type Exporter ¶
type Exporter struct {
// contains filtered or unexported fields
}
Exporter is the use-case service that exports local USB devices via the usbip_host kernel surface. One Exporter is sufficient for a whole daemon process; construct via NewExporter and release via Shutdown. The zero value is not usable — NewExporter initialises required state.
Every handle and counter is mutex-guarded so the accept loop, the per-session handler goroutines, Sessions(), and Shutdown() can all interleave safely under the race detector.
func NewExporter ¶
func NewExporter(opts ...ExporterOption) *Exporter
NewExporter constructs an Exporter from functional options. Required dependencies missing from opts cause a panic because a missing dependency is a programming error, not a runtime condition worth propagating up the call stack. Option-driven validation failures (e.g. malformed ACL CIDR strings) also panic here; use NewExporterWithError when the caller needs to surface such errors.
func NewExporterWithError ¶
func NewExporterWithError(opts ...ExporterOption) (*Exporter, error)
NewExporterWithError is the fallible constructor variant. It returns the same Exporter NewExporter would, plus any option-validation error (ErrACLInvalid or ErrAcceptRateLimitInvalid). Missing-dependency errors still panic — those are programming bugs, not runtime conditions.
func (*Exporter) Bind ¶
Bind delegates to kernel.Bind per exporter-daemon OpenSpec. Errors are wrapped with the busid so callers that bind many devices can identify which failed. Every terminal branch logs a structured slog record with an outcome field so journald queries can filter by outcome without parsing free-form messages.
The boundary guard rejects malformed BusID values BEFORE any kernel-adapter call. Callers that construct a BusID by raw string conversion (bypassing ParseBusID) cannot smuggle a path-traversal sequence into the sysfs writes the kernel adapter performs. The CLI re-validates earlier, but library embedders enter through this surface and must hit the same gate.
func (*Exporter) ListAvailable ¶
ListAvailable forwards to kernel.ListLocalDevices per exporter-daemon OpenSpec. The kernel adapter is the authoritative source; the Exporter does not maintain a cache of its own.
func (*Exporter) ListExported ¶
ListExported returns devices currently bound to usbip-host that are not actively claimed by an importer (SDEV_ST_USED excluded). This is the semantic answer to "what devices does this daemon have BOUND right now" — distinct from ListAvailable which dumps every local USB device regardless of bind state. Consumed by the status-socket BoundDevices endpoint and by any caller that needs to mirror the wire-side OP_REP_DEVLIST view.
func (*Exporter) Serve ¶
Serve runs the accept loop per exporter-daemon OpenSpec. Each accepted connection is dispatched to a per-connection goroutine via handleConn; the accept loop returns when ctx is cancelled, the listener returns a permanent error, or Shutdown is called. Returns ErrAlreadyShutdown if invoked after Shutdown. Concurrent calls are safe, but at most one may own the accept-loop reservation; an overlapping call receives ErrServeAlreadyRunning.
func (*Exporter) ServeWithListenerFactory ¶
func (e *Exporter) ServeWithListenerFactory( ctx context.Context, factory func(context.Context) (net.Listener, error), ) error
ServeWithListenerFactory reserves the Exporter's terminal/overlap state before invoking factory. The public facade uses this internal-module seam so ListenAndServe cannot bind a socket and only then discover that Shutdown or another Serve already won. factory MUST honor the supplied context.
func (*Exporter) Sessions ¶
Sessions returns a snapshot of the currently-accepted sessions, sorted by start time so callers paginating the list see a stable order between calls. ctx is accepted for interface symmetry with other list methods; it is not currently used — the snapshot is an in-memory copy under the Exporter's read lock. Equal StartedAt values tiebreak by SessionID string form: UUIDv7 is lexical-time- ordered so the secondary key stays meaningful without additional state.
func (*Exporter) Shutdown ¶
Shutdown stops accepting new connections and drains in-flight session handlers. Per importer-lifecycle and exporter-daemon OpenSpec documents: new accepts are refused, existing handlers are signalled to exit, and the call waits for them bounded by the provided ctx deadline. Returns nil when the drain completes before the deadline; a ctx.Err-wrapped error when it does not. Repeated calls observe the retained per-session cleanup results without issuing another Disconnect.
When the caller's ctx carries no deadline and the Exporter was built with WithExporterShutdownTimeout(d>0), the option's value is applied as an internal backstop deadline so a wedged ExportOnConn cannot block Shutdown forever despite a configured timeout.
func (*Exporter) Unbind ¶
Unbind delegates to kernel.Unbind per exporter-daemon OpenSpec. Errors are wrapped with the busid. Outcome is logged structurally per Bind's contract. The same BusID validity gate as Bind applies — see Bind for the boundary rationale.
func (*Exporter) WatchSessions ¶
WatchSessions returns an iter.Seq that yields every future SessionStartedEvent and SessionEndedEvent while ctx is live. Subscriber registration is deferred until the consumer ranges over the returned iter so a caller that constructs the iter and drops it does not occupy a fanout slot until Shutdown. On the first iteration the subscriber is registered; subsequent ctx cancel, sub.done from closeAllSubscribers, or yield-false removes it. Post-Shutdown the iter terminates immediately with no events — matches Importer.Watch's post-Close semantics per importer-lifecycle and exporter-daemon OpenSpec documents.
type ExporterKernel ¶
type ExporterKernel interface {
// ListLocalDevices returns every USB device on the host regardless
// of bind state — the CLI's `list` view shows the whole bus.
ListLocalDevices(ctx context.Context) ([]domain.Device, error)
// ListExportedDevices returns only devices currently bound to
// usbip-host AND not actively claimed by an importer (SDEV_ST_USED
// excluded). This is the wire-facing OP_REP_DEVLIST view: peers
// only see what they could actually attach.
ListExportedDevices(ctx context.Context) ([]domain.Device, error)
Bind(ctx context.Context, busID domain.BusID) error
Unbind(ctx context.Context, busID domain.BusID) error
ExportOnConn(ctx context.Context, conn net.Conn, busID domain.BusID) error
// Disconnect writes -1 to usbip_sockfd to drop an active session.
Disconnect(ctx context.Context, busID domain.BusID) error
// ModulesAvailable probes usbip_host + usbip_core.
ModulesAvailable(ctx context.Context) error
}
ExporterKernel wraps the usbip_host module (exporter-side kernel surface). A process that only exports does NOT need vhci_hcd loaded. See architecture-layering OpenSpec.
type ExporterOption ¶
type ExporterOption func(*exporterConfig)
ExporterOption configures an Exporter at construction time. Apply options by passing them to NewExporter; options mutate an internal config struct in declaration order so the last option wins for any field. The split from ImporterOption (not a unified Option type) is deliberate per public-library-api OpenSpec: a unified type would let WithMaxSessions compile against an Importer, which is a typed programming error.
func WithExporterACL ¶
func WithExporterACL(cidrs ...string) ExporterOption
WithExporterACL appends CIDR strings to the accept-path allow-list (security-release-quality OpenSpec). Multiple calls accumulate. An empty list means "allow every peer" to match upstream usbip-utils behaviour; at least one CIDR opts the exporter into fail-closed ACL enforcement. Invalid CIDR strings surface as NewExporterWithError constructor errors (ErrACLInvalid) rather than deferred Serve-time failures.
func WithExporterAcceptRateLimit ¶
func WithExporterAcceptRateLimit(rps float64, burst int) ExporterOption
WithExporterAcceptRateLimit caps new accepts at rps tokens per second via a token bucket with the given burst size (security-release-quality OpenSpec). Finite rps <= 0 disables rate limiting entirely; burst <= 0 picks up a sane default. Non-finite rps is rejected by NewExporterWithError.
func WithExporterClock ¶
func WithExporterClock(clk Clock) ExporterOption
WithExporterClock injects the Clock used for handshake timeouts and drain deadlines. Defaults to RealClock when unspecified.
func WithExporterCodec ¶
func WithExporterCodec(p ProtocolCodec) ExporterOption
WithExporterCodec injects the USBIP wire codec. Required.
func WithExporterEvents ¶
func WithExporterEvents(e KernelEvents) ExporterOption
WithExporterEvents injects the shared uevent source. Required.
func WithExporterHandshakeTimeout ¶
func WithExporterHandshakeTimeout(d time.Duration) ExporterOption
WithExporterHandshakeTimeout bounds how long the exporter will wait for a client to complete its OP request (security-release-quality OpenSpec). Zero picks up the default; a negative value disables the timeout.
func WithExporterKernel ¶
func WithExporterKernel(k ExporterKernel) ExporterOption
WithExporterKernel injects the kernel-side adapter (usbip_host surface). Required — NewExporter fails fast if left nil.
func WithExporterLogger ¶
func WithExporterLogger(l *slog.Logger) ExporterOption
WithExporterLogger injects the structured logger. Defaults to slog.Default() when unspecified.
func WithExporterMaxHandshakeBytes ¶
func WithExporterMaxHandshakeBytes(n int) ExporterOption
WithExporterMaxHandshakeBytes caps bytes read during the handshake phase (security-release-quality OpenSpec). Zero picks up the default.
func WithExporterMaxSessions ¶
func WithExporterMaxSessions(n int) ExporterOption
WithExporterMaxSessions caps the total concurrent accepted sessions (security-release-quality OpenSpec). Zero picks up the default; a negative value disables the cap entirely. Each accepted connection that would push the count past the cap is closed by the handler before ExportOnConn runs, so the kernel is never asked to attach past the cap.
func WithExporterMaxSessionsPerPeer ¶
func WithExporterMaxSessionsPerPeer(n int) ExporterOption
WithExporterMaxSessionsPerPeer caps the concurrent sessions per source IP (security-release-quality OpenSpec). Zero picks up the default; a negative value disables the per-peer cap entirely.
func WithExporterShutdownTimeout ¶
func WithExporterShutdownTimeout(d time.Duration) ExporterOption
WithExporterShutdownTimeout is the backstop deadline applied by Exporter.Shutdown when the caller's ctx carries no deadline. A zero value disables the backstop; a positive value caps the drain regardless of the caller's ctx. Always overridden by a tighter caller-supplied deadline.
func WithExporterStatusPollInterval ¶
func WithExporterStatusPollInterval(d time.Duration) ExporterOption
WithExporterStatusPollInterval overrides the internal usbip_status backstop cadence. It is an internal-app test/integration seam, not a public pkg/usbip option. Zero selects the default; negative disables.
type FixedBackoff ¶
FixedBackoff returns the same Delay for every attempt. Reset is a no-op because FixedBackoff carries no attempt state.
type HandshakeOp ¶
type HandshakeOp string
HandshakeOp identifies which exporter handshake opcode was served.
const ( HandshakeOpDevlist HandshakeOp = "devlist" HandshakeOpImport HandshakeOp = "import" )
HandshakeOp values.
type Importer ¶
type Importer struct {
// contains filtered or unexported fields
}
Importer is the use-case service that imports remote USB devices via the vhci_hcd kernel surface. One Importer is sufficient for a whole process; construct via NewImporter and release via Close. The zero value is not usable — NewImporter initialises required state.
The handle map tracks every successfully-attached port along with a per-handle cancel signal and a monotonically increasing generation. The reconnect watcher (importer-lifecycle OpenSpec) reads the generation to filter stale kernel events whose port id was replaced by a successful reattach.
func NewImporter ¶
func NewImporter(opts ...ImporterOption) *Importer
NewImporter constructs an Importer from functional options. The returned *Importer is safe for concurrent use. Required dependencies (kernel, events, transport, codec) must be supplied via their With... option funcs; NewImporter panics if any are nil because a missing dependency is a programming error, not a runtime condition worth propagating up the call stack.
func (*Importer) Attach ¶
func (i *Importer) Attach( ctx context.Context, endpoint domain.RemoteEndpoint, busID domain.BusID, opts AttachOptions, ) (domain.Port, error)
Attach runs the full USB/IP import sequence per importer-lifecycle OpenSpec:
- kernel.ModulesAvailable probes vhci_hcd + usbip_core.
- transport.Dial establishes the TCP connection to endpoint.
- codec.EncodeOpReqImport(conn, busID) writes the request.
- codec.DecodeOpRepImport(conn) reads back the device body.
- kernel.AttachRemote(ctx, conn, spec) hands the fd to the kernel.
Step 5 is the fd-passing handoff defined in the kernel-adapter and importer-lifecycle OpenSpec documents. Until AttachRemote returns success, Attach owns the conn and MUST close it on any error path. After success, the kernel owns the fd and Attach MUST NOT touch it — closing it there would tear down the just-opened vhci port. The local `handedOff` flag implements that split: the deferred cleanup is a no-op once handedOff flips to true.
When AttachOptions.AutoReconnect is set, the successful-return path also spawns a reconnect watcher goroutine bound to the fresh handle (importer-lifecycle OpenSpec). The watcher is enrolled in i.wg so Close drains it.
func (*Importer) Close ¶
Close cancels every registered handle's context, waits for any background goroutines to drain, and marks the Importer closed. Subsequent Close calls are no-ops via sync.Once.
Close's wait is bounded by the longest shutdownTimeout across the registered handles (see AttachOptions.ShutdownTimeout): on timer fire, Close returns even if the waitgroup has not drained. This is a WALL-CLOCK bound only. Any in-flight wg-tracked goroutines — reconnect watchers stuck inside kernel.AttachRemote, blocking OnReconnect callbacks, or detach goroutines waiting on kernel.DetachPort — may continue running past Close's return and will be cleaned up when they naturally unwind. Callers who require synchronous shutdown must either (a) use a negative shutdownTimeout to request an unbounded wait, or (b) ensure their workloads honour ctx cancellation.
waitGroupBounded observes the Importer's lifecycle wait group through a drain channel, so a timeout-bounded Close does not allocate an extra goroutine that can linger behind the Close call.
The handle map is NOT nilled here: a concurrent Attach may be parked past AttachRemote but before registerHandle, and nilling the map under it would panic on the unconditional write. Instead, registerHandle itself rejects writes once closed is true. The map becomes garbage when the *Importer is collected.
func (*Importer) Detach ¶
Detach tears down a kernel-owned vhci port by id. When this Importer owns a matching handle, it cancels the handle's context BEFORE issuing the sysfs-backed detach per importer-lifecycle OpenSpec so any auto-reconnect watcher sees cancel ahead of the status transition and does not race a reattempt, and blocks on the watcher goroutine's done channel before touching the kernel so an in-flight reconnect attempt cannot overlap with the sysfs write. When the kernel rejects the detach for a reason other than already-free, the handle is left registered so callers can retry. An authoritative already-free result removes only that exact stale handle so a later Port generation can reuse the ID.
Detach sets handle.detaching BEFORE cancel so a reconnect watcher wedged inside kernel.AttachRemote past the bounded wait cannot silently register a fresh handle after Detach returns. The watcher observes the flag on its post-Attach check and rolls back the kernel handoff instead of taking ownership of the replacement port.
A fresh Importer has no process-local handle for ports inherited from an earlier process. In that case Detach delegates directly to the kernel and coordinates overlapping callers in untrackedDetaches. This keeps one-shot CLI attach and detach invocations interoperable without a racy ListPorts preflight; the serialized kernel mutation decides whether the Port is live.
func (*Importer) ListPorts ¶
ListPorts forwards to the kernel's view of attached vhci ports. The Importer's local handle map is internal bookkeeping; the kernel's sysfs-derived list is the authoritative source, especially after a daemon restart where our handles are empty but the kernel still owns imported sockets and tracks live ports.
func (*Importer) ListRemote ¶
func (i *Importer) ListRemote(ctx context.Context, endpoint domain.RemoteEndpoint) ([]domain.Device, error)
ListRemote dials endpoint, requests the remote device list via OP_REQ_DEVLIST, and returns the decoded []domain.Device. The TCP connection is owned for the entire call: it is always closed before ListRemote returns (success or failure). OP_REP_DEVLIST does not involve fd-passing, so the kernel-adapter and importer-lifecycle OpenSpec documents handoff contract does not apply here — the connection is a short-lived query channel.
Returned errors are wrapped with the peer endpoint so callers can distinguish which remote produced the failure when a consumer lists across multiple peers.
func (*Importer) Watch ¶
Watch returns the v1 event-only iterator. It is a compatibility wrapper around WatchWithErrors: ordinary events are forwarded, while the first terminal stream error ends iteration without changing the historical method signature.
func (*Importer) WatchWithErrors ¶
WatchWithErrors returns an iter.Seq2 that yields domain events from the shared KernelEvents source together with a terminal error channel. Subscription failures retain their wrapped cause. An established source closing while both ctx and the Importer remain live yields ErrEventStreamClosed. Caller cancellation and Importer.Close end cleanly.
Post-Close WatchWithErrors returns an iter that yields nothing and terminates immediately — the handle map is already torn down and there is no upstream to bind to.
Subscriber registration and the upstream KernelEvents.Subscribe call are deferred until the consumer ranges over the returned iter so a caller that constructs the iter and then drops it does not leak a kernel subscription handle or a fanout slot. The closed-Importer fast path stays eager because there is no resource to defer in that case.
type ImporterKernel ¶
type ImporterKernel interface {
// AttachRemote must invoke spec.ReserveLocalPort, when non-nil, after
// choosing the returned port and before starting the kernel mutation.
// A reservation error aborts the handoff without making the port live.
AttachRemote(ctx context.Context, conn net.Conn, spec RemoteDeviceSpec) (domain.PortID, error)
DetachPort(ctx context.Context, id domain.PortID) error
ListPorts(ctx context.Context) ([]domain.Port, error)
// ModulesAvailable probes vhci_hcd + usbip_core.
ModulesAvailable(ctx context.Context) error
}
ImporterKernel wraps the vhci_hcd module (importer-side kernel surface). A process that only imports does NOT need usbip_host loaded. See architecture-layering OpenSpec.
type ImporterOption ¶
type ImporterOption func(*importerConfig)
ImporterOption configures an Importer at construction time. Apply options by passing them to NewImporter; options mutate an internal config struct in declaration order so the last option wins for any field.
func WithImporterClock ¶
func WithImporterClock(clk Clock) ImporterOption
WithImporterClock injects the Clock used for backoff / timers. Defaults to RealClock when unspecified.
func WithImporterCodec ¶
func WithImporterCodec(p ProtocolCodec) ImporterOption
WithImporterCodec injects the USBIP wire codec. Required.
func WithImporterEvents ¶
func WithImporterEvents(e KernelEvents) ImporterOption
WithImporterEvents injects the shared uevent source. Required.
func WithImporterKernel ¶
func WithImporterKernel(k ImporterKernel) ImporterOption
WithImporterKernel injects the kernel-side adapter (vhci_hcd surface). Required — NewImporter fails fast if left nil.
func WithImporterLogger ¶
func WithImporterLogger(l *slog.Logger) ImporterOption
WithImporterLogger injects the structured logger. Defaults to slog.Default() when unspecified.
func WithImporterTransport ¶
func WithImporterTransport(t Transport) ImporterOption
WithImporterTransport injects the TCP transport. Required.
func WithImporterTransportOptions ¶
func WithImporterTransportOptions(opts TransportOptions) ImporterOption
WithImporterTransportOptions stores TCP-level tuning that the Importer's Dial calls hand to the Transport adapter. Zero-valued fields preserve v1.0.0 behavior; non-zero fields are applied by the adapter. NewImporter validates the struct and panics on negative values so a misconfigured caller surfaces the error at construction time rather than as a confusing socket error later.
type KernelEvents ¶
type KernelEvents interface {
Subscribe(ctx context.Context) (events <-chan domain.Event, cancel func(), err error)
}
KernelEvents is the shared uevent source consumed by the importer reconnect watcher, the public Watch/WatchSessions streams, and tests. One netlink socket, many consumers via an internal fan-out.
Subscribe semantics (architecture-layering OpenSpec):
- each call returns a fresh buffered channel
- slow subscribers drop events (logged) rather than back-pressuring fan-out
- the returned cancel func unsubscribes and closes the channel
- first Subscribe starts the netlink listener; last Unsubscribe stops it
type KernelModule ¶
type KernelModule string
KernelModule names the kernel modules whose load state the daemon reports through readiness checks. Closed set.
const ( ModuleUsbipCore KernelModule = "usbip_core" ModuleVhciHcd KernelModule = "vhci_hcd" ModuleUsbipHost KernelModule = "usbip_host" ModuleUsbipVudc KernelModule = "usbip_vudc" )
KernelModule values.
type ProtocolCodec ¶
type ProtocolCodec interface {
EncodeOpReqDevlist() []byte
EncodeOpReqImport(w io.Writer, busID domain.BusID) error
EncodeOpRepDevlist(w io.Writer, devs []domain.Device) error
EncodeOpRepImport(w io.Writer, d domain.Device) error
EncodeOpRepImportError(w io.Writer, status uint32) error
DecodeHeader(r io.Reader) (version uint16, op protocol.OpCode, status uint32, err error)
DecodeOpRepDevlist(r io.Reader) ([]domain.Device, error)
DecodeOpReqImport(r io.Reader) (domain.BusID, error)
DecodeOpReqImportBody(r io.Reader) (domain.BusID, error)
DecodeOpRepImport(r io.Reader) (domain.Device, error)
}
ProtocolCodec is the wire-level USBIP codec surface the app layer depends on. It mirrors the method set on internal/adapter/wire.Codec. architecture-layering OpenSpec declares the interface; the wire package's Codec struct provides the implementation.
type RealClock ¶
type RealClock struct{}
RealClock is the production Clock implementation: every method forwards to the stdlib time package. It carries no state and is trivially zero-value constructible.
type ReconnectOutcome ¶
type ReconnectOutcome string
ReconnectOutcome classifies how a single reconnect-watcher attempt resolved.
const ( ReconnectOutcomeOK ReconnectOutcome = "ok" ReconnectOutcomeBackoff ReconnectOutcome = "backoff" ReconnectOutcomeExhausted ReconnectOutcome = "exhausted" ReconnectOutcomeCanceled ReconnectOutcome = "canceled" )
ReconnectOutcome values.
type RemoteDeviceSpec ¶
type RemoteDeviceSpec struct {
// Device is the 312-byte device body decoded from OP_REP_IMPORT,
// populated via the wire codec.
Device domain.Device
// DevID is the (busnum<<16)|devnum identifier as it appears on the
// wire. The kernel adapter writes this value into the vhci
// usbip_sockfd attach payload.
DevID domain.DeviceID
// Speed is the negotiated USB speed reported by the remote
// exporter; also required by the vhci attach payload.
Speed domain.Speed
// Remote is the peer that surfaced the spec. Carried through for
// structured logging and telemetry only.
Remote domain.RemoteEndpoint
// ReserveLocalPort publishes the selected local port to the importer
// before the kernel mutation starts. ImporterKernel implementations
// MUST invoke a non-nil hook synchronously after port selection and
// before making the attachment live. Specs constructed outside the
// importer may leave it nil.
ReserveLocalPort func(domain.PortID) error
}
RemoteDeviceSpec is what OP_REP_IMPORT decodes into. Passed to ImporterKernel.AttachRemote by the importer service so the kernel adapter can synthesise the usbip_vhci attach payload. The importer also supplies ReserveLocalPort as a narrow transaction hook: the production adapter invokes it after selecting a free port and before starting the kernel handoff, closing the interval in which a concurrent Detach could otherwise miss a live-but-unpublished port.
The type lives in internal/app rather than pkg/domain because it is an adapter-interface contract, not consumer data. Public callers never observe a RemoteDeviceSpec directly — they pass a domain.RemoteEndpoint and the importer decodes a spec off the wire.
type SessionOutcome ¶
type SessionOutcome string
SessionOutcome classifies how an inbound exporter handshake resolved.
const ( OutcomeHandshakeOK SessionOutcome = "handshake_ok" OutcomeRejectedACL SessionOutcome = "rejected_acl" OutcomeRejectedRate SessionOutcome = "rejected_rate" OutcomeRejectedCap SessionOutcome = "rejected_cap" OutcomeHandshakeFailed SessionOutcome = "handshake_failed" )
SessionOutcome values.
type Transport ¶
type Transport = Dialer
Transport preserves the internal name used by existing option wiring while exposing only the capability the importer actually consumes.
type TransportOptions ¶
type TransportOptions = netopts.TransportOptions
TransportOptions is an alias for netopts.TransportOptions. The underlying type lives in internal/netopts to break the import cycle between internal/app (which declares the consumer-defined Transport interface) and internal/adapter/transport (which implements it). The DDD layering rule forbids internal/app from importing internal/adapter/transport, so the value type sits in a leaf package both can reference.
type UnbindOutcome ¶
type UnbindOutcome string
UnbindOutcome classifies how an Exporter.Unbind resolved.
const ( UnbindOutcomeOK UnbindOutcome = "ok" UnbindOutcomeNotBound UnbindOutcome = "not_bound" UnbindOutcomePermission UnbindOutcome = "permission" UnbindOutcomeError UnbindOutcome = "error" )
UnbindOutcome values.