deviceforward

package
v0.0.6 Latest Latest
Warning

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

Go to latest
Published: Oct 16, 2026 License: MIT Imports: 28 Imported by: 0

Documentation

Index

Constants

View Source
const DefaultEnrollTTL = 15 * time.Minute

DefaultEnrollTTL bounds an unredeemed enrollment token.

View Source
const DefaultRevocationRecheck = 5 * time.Second

DefaultRevocationRecheck bounds how long a revoked forwarder's live session can outlast its revocation when nothing told the holding process at once.

View Source
const DefaultVHCIRoot = "/sys/devices/platform/vhci_hcd.0"

DefaultVHCIRoot is the platform-device directory of the first controller.

View Source
const MaxEnrollTTL = 24 * time.Hour

MaxEnrollTTL caps --ttl, so a single-use token cannot be turned into a standing credential.

View Source
const MaxFramePayload = 1 << 20 // 1 MiB

MaxFramePayload bounds one frame's payload. A Data chunk larger than this is a bug or an attack, not a legitimate stream write; USB/IP URBs and Ethernet frames are far smaller, and the relay chunks its reads well under it.

View Source
const MinProtocolVersion = 1

MinProtocolVersion is the oldest a connecting forwarder may speak.

View Source
const ProtocolVersion = 1

ProtocolVersion is the newest forwarder protocol this build speaks.

View Source
const TokenFileMode os.FileMode = 0o600

TokenFileMode is the permission an enrollment file must carry.

Variables

View Source
var (
	// ErrInvalidSpec reports a device descriptor or configuration that is not
	// admissible. It is the forwarder's ErrInvalidSpec, kept separate from
	// executor.ErrInvalidSpec because the two packages do not import each
	// other's error here and a caller branches on the one it was handed.
	ErrInvalidSpec = errors.New("deviceforward: invalid specification")

	// ErrTokenInvalid reports a malformed enrollment token or credential, or
	// one whose MAC does not verify against this control plane.
	ErrTokenInvalid = errors.New("deviceforward: token is invalid")
	// ErrTokenExpired reports an enrollment token past its TTL.
	ErrTokenExpired = errors.New("deviceforward: enrollment token expired")
	// ErrTokenAlreadyUsed reports an enrollment token redeemed before.
	ErrTokenAlreadyUsed = errors.New("deviceforward: enrollment token already redeemed")
	// ErrRevoked reports a token or credential that was explicitly revoked.
	ErrRevoked = errors.New("deviceforward: credential revoked")
	// ErrCredentialInvalid reports a long-lived forwarder credential that does
	// not authenticate — reported distinctly from a token so the device log
	// says "re-enroll" rather than "bad token".
	ErrCredentialInvalid = errors.New("deviceforward: credential is invalid")
	// ErrForwarderNotFound reports no enrollment token or forwarder with an ID.
	ErrForwarderNotFound = errors.New("deviceforward: no such forwarder")

	// ErrForwarderOffline reports that the named forwarder has no live session,
	// so the device it carries cannot be reached right now.
	ErrForwarderOffline = errors.New("deviceforward: forwarder is offline")
	// ErrChannelRefused reports that the forwarder declined to open a channel
	// to its device — the device is unplugged, or the request named something
	// other than the one device the forwarder is configured for.
	ErrChannelRefused = errors.New("deviceforward: forwarder refused to open the device")
	// ErrProtocol reports a wire-format violation on the forwarder connection.
	ErrProtocol = errors.New("deviceforward: protocol error")
)

Sentinel errors for the forwarder feature. They are errors.Is targets so a caller — the CLI, the hub endpoint, a test — can branch on what went wrong without matching on message text.

Functions

func Revoke

func Revoke(store Store, id string, now time.Time) (kind string, err error)

Revoke invalidates an enrollment token or a forwarder credential by ID. An enrollment ID additionally revokes the forwarder it produced, so a token that leaked and was redeemed by an attacker does not leave a live credential.

func SaveCredential

func SaveCredential(path string, c Credential) error

SaveCredential writes the credential atomically at 0600.

Types

type Bundle

type Bundle struct {
	Server       string          `json:"server"`
	Token        string          `json:"token"`
	Pin          string          `json:"pin,omitempty"`
	EnrollmentID string          `json:"enrollment_id"`
	Name         string          `json:"name"`
	Device       ForwardedDevice `json:"device"`
	ExpiresAt    time.Time       `json:"expires_at"`
}

Bundle is the enrollment payload handed to a forwarder.

func DecodeBundle

func DecodeBundle(s string) (Bundle, error)

DecodeBundle parses the output of Encode.

func ReadTokenFile

func ReadTokenFile(path string) (b Bundle, warning string, err error)

ReadTokenFile loads enrollment material from a file, accepting either a bundle or a bare token, exactly as the agent's loader does.

func (Bundle) Encode

func (b Bundle) Encode() (string, error)

Encode renders the bundle as a single copy-pasteable token.

func (Bundle) Validate

func (b Bundle) Validate() error

Validate reports whether the bundle has the fields a forwarder needs.

type ByePayload

type ByePayload struct {
	Reason    string `json:"reason,omitempty"`
	Reconnect bool   `json:"reconnect"`
}

ByePayload ends a session.

type Credential

type Credential struct {
	Server      string          `json:"server"`
	ForwarderID string          `json:"forwarder_id"`
	Credential  string          `json:"credential"`
	Name        string          `json:"name,omitempty"`
	Device      ForwardedDevice `json:"device"`
	Pin         string          `json:"pin,omitempty"`
}

Credential is what a forwarder saves after enrolling.

func LoadCredential

func LoadCredential(path string) (Credential, bool, error)

LoadCredential reads a saved credential. A missing file is reported as (zero, false, nil): the caller then needs an enrollment token.

type DeviceKind

type DeviceKind string

DeviceKind is what sort of device a forwarder carries.

const (
	// KindUSB forwards a USB device over USB/IP: the forwarder exposes the
	// on-prem kernel's usbip server for the one device, and the consumer
	// attaches it with vhci as if it were plugged in locally.
	KindUSB DeviceKind = "usb"
	// KindL2 forwards a layer-2 network interface: the forwarder streams the
	// interface's Ethernet frames, and the consumer replays them onto a TAP,
	// so a sandbox in the cloud shares the on-prem segment at the link layer.
	KindL2 DeviceKind = "l2"
)

func (DeviceKind) Valid

func (k DeviceKind) Valid() bool

Valid reports whether k is a known kind.

type DeviceStream

type DeviceStream struct {
	io.ReadWriteCloser
	ForwarderID string
	Device      ForwardedDevice
	Exported    ExportedDevice
}

DeviceStream is an open stream to a forwarder's device, with what is known about the device: the enrolled descriptor, which is authoritative, and what the forwarder reported opening, which for USB names the bus id a consumer's import must carry.

type DialFunc

type DialFunc func(ctx context.Context, server, token string) (frameConn, error)

DialFunc opens a frame connection to a server, for tests to substitute an in-memory transport. token is the credential or enrollment token to present.

type EnrollmentRecord

type EnrollmentRecord struct {
	ID         string          `json:"id"`
	Name       string          `json:"name"`
	SecretHash string          `json:"secret_hash"`
	Device     ForwardedDevice `json:"device"`
	CreatedAt  time.Time       `json:"created_at"`
	ExpiresAt  time.Time       `json:"expires_at"`
	CreatedBy  string          `json:"created_by,omitempty"`
	// Owner scopes the forwarder to one identity on a multi-user hub, so a
	// forwarder is listed and revocable by its owner. Empty on a single-user
	// hub.
	Owner           string    `json:"owner,omitempty"`
	RedeemedAt      time.Time `json:"redeemed_at,omitempty"`
	RedeemedAgentID string    `json:"redeemed_agent_id,omitempty"`
	RevokedAt       time.Time `json:"revoked_at,omitempty"`
}

EnrollmentRecord is the hub's durable view of one minted forwarder token. The secret itself is never stored — only SecretHash.

func Mint

func Mint(store Store, opts MintOptions) (token string, rec EnrollmentRecord, err error)

Mint creates a single-use enrollment token, persists its hash, and returns the token string. The token is the only time the secret exists outside the operator's terminal.

func MintBundle

func MintBundle(store Store, opts MintOptions) (encoded string, rec EnrollmentRecord, err error)

MintBundle mints a token and wraps it, the server URL and the pin into a bundle in one step, for the enroll command and the dashboard.

func (EnrollmentRecord) Expired

func (r EnrollmentRecord) Expired(now time.Time) bool

Expired reports whether the token's TTL has elapsed as of now.

func (EnrollmentRecord) Redeemed

func (r EnrollmentRecord) Redeemed() bool

Redeemed reports whether the token has already been used.

func (EnrollmentRecord) Revoked

func (r EnrollmentRecord) Revoked() bool

Revoked reports whether the token was explicitly revoked.

type ErrorPayload

type ErrorPayload struct {
	Message string `json:"message"`
}

ErrorPayload carries a non-fatal error.

type ExportedDevice

type ExportedDevice struct {
	Kind      DeviceKind `json:"kind"`
	BusID     string     `json:"bus_id,omitempty"`
	VendorID  string     `json:"vendor_id,omitempty"`
	ProductID string     `json:"product_id,omitempty"`
	// Speed is the kernel's usb_device_speed enum value.
	Speed     uint32 `json:"speed,omitempty"`
	Interface string `json:"interface,omitempty"`
}

ExportedDevice is what a forwarder reports, in its open acknowledgement, about the device it just opened — the facts a consumer needs to import it. For USB, BusID is the on-prem kernel's bus id, which OP_REQ_IMPORT must name; the IDs are informational.

type ForwardedDevice

type ForwardedDevice struct {
	// Kind selects which of the fields below is meaningful.
	Kind DeviceKind `json:"kind"`
	// USB identifies the forwarded USB device by vendor, product and
	// optionally serial — the identity that survives re-enumeration. Set when
	// Kind is KindUSB.
	USB *executor.USBMatch `json:"usb,omitempty"`
	// Interface is the kernel name of the forwarded network interface on the
	// on-prem host ("eth1", "can0"). Set when Kind is KindL2.
	Interface string `json:"interface,omitempty"`
	// Label is an operator-facing description shown in the dashboard and in
	// audit rows. Optional; a default is derived from the identity when empty.
	Label string `json:"label,omitempty"`
}

ForwardedDevice is the one device a forwarder is configured to carry.

Exactly one of USB and Interface is set, selected by Kind.

func (ForwardedDevice) DefaultLabel

func (d ForwardedDevice) DefaultLabel() string

DefaultLabel returns Label, or a label derived from the identity when none was set, so a forwarder always has something an operator recognises.

func (ForwardedDevice) Describe

func (d ForwardedDevice) Describe() string

Describe renders the device for a card, a log line or an audit row.

func (ForwardedDevice) Normalize

func (d ForwardedDevice) Normalize() (ForwardedDevice, error)

Normalize canonicalises and validates the descriptor.

func (ForwardedDevice) Validate

func (d ForwardedDevice) Validate() error

Validate reports whether the descriptor is admissible.

type Forwarder

type Forwarder struct {
	// contains filtered or unexported fields
}

Forwarder is a running edge forwarder.

func NewForwarder

func NewForwarder(cfg ForwarderConfig) (*Forwarder, error)

NewForwarder validates the configuration and builds the transport. Every failure here is an operator-fixable misconfiguration, so they are reported now rather than discovered inside a reconnect loop that would retry forever.

func (*Forwarder) Run

func (f *Forwarder) Run(ctx context.Context) error

Run dials the control plane and serves until ctx is cancelled or the credential is refused permanently.

func (*Forwarder) TransportSummary

func (f *Forwarder) TransportSummary() string

TransportSummary describes the link for startup output.

type ForwarderConfig

type ForwarderConfig struct {
	// Server is the full wss://host/api/forwarders/connect URL.
	Server string
	// Token is a bare enrollment token, used only until a credential is saved.
	Token string
	// Bundle, if set, supplies Server/Token/Pin together (explicit fields win).
	Bundle string
	// CredentialPath is where the long-lived credential is saved and loaded.
	CredentialPath string
	// Pin is the hub's SPKI fingerprint; CAFile an extra root; Insecure allows
	// plaintext to a non-loopback host (refused by default).
	Pin      string
	CAFile   string
	Insecure bool
	// Source is the on-prem device endpoint. Required.
	Source LocalSource
	// AgentVersion is the build string reported in the hello.
	AgentVersion string
	// Logf receives operational messages. Nil discards them.
	Logf func(format string, args ...any)
	// Dial overrides the transport, for tests.
	Dial DialFunc
}

ForwarderConfig configures a forwarder.

type ForwarderRecord

type ForwarderRecord struct {
	ForwarderID  string          `json:"forwarder_id"`
	Name         string          `json:"name"`
	SecretHash   string          `json:"secret_hash"`
	Device       ForwardedDevice `json:"device"`
	Owner        string          `json:"owner,omitempty"`
	CreatedAt    time.Time       `json:"created_at"`
	LastSeen     time.Time       `json:"last_seen,omitempty"`
	RevokedAt    time.Time       `json:"revoked_at,omitempty"`
	EnrollmentID string          `json:"enrollment_id,omitempty"`
}

ForwarderRecord is the hub's durable view of one enrolled forwarder.

func Authenticate

func Authenticate(store Store, credential string, now time.Time) (ForwarderRecord, error)

Authenticate verifies a long-lived forwarder credential on every reconnect.

func Redeem

func Redeem(store Store, token string, now time.Time) (credential string, fwd ForwarderRecord, err error)

Redeem exchanges an enrollment token for a long-lived forwarder credential. The order of checks matches the agent's: format and MAC, existence, revocation, replay, expiry, secret, then the atomic single-use claim.

func (ForwarderRecord) Revoked

func (r ForwarderRecord) Revoked() bool

Revoked reports whether the forwarder's credential was revoked.

type Frame

type Frame struct {
	Type    FrameType
	Channel uint32
	Payload []byte
}

Frame is one unit on the wire.

func (Frame) Marshal

func (f Frame) Marshal() ([]byte, error)

Marshal renders the frame to its wire bytes.

type FrameType

type FrameType byte

FrameType is the one-byte discriminator at the head of every frame.

const (
	// TypeHello (forwarder→hub, channel 0) opens the session and advertises
	// the one device the forwarder carries.
	TypeHello FrameType = 1
	// TypeWelcome (hub→forwarder, channel 0) accepts the session and names the
	// negotiated version.
	TypeWelcome FrameType = 2
	// TypeOpen (hub→forwarder) asks the forwarder to open a new stream to its
	// device on the named channel.
	TypeOpen FrameType = 3
	// TypeOpenAck (forwarder→hub) reports whether the open succeeded.
	TypeOpenAck FrameType = 4
	// TypeData (both directions) carries one chunk of a stream's bytes on its
	// channel. The payload is opaque — it is the device's own protocol.
	TypeData FrameType = 5
	// TypeClose (both directions) ends a stream's channel. After it, the
	// channel number must not be reused.
	TypeClose FrameType = 6
	// TypePing / TypePong (channel 0) are the heartbeat.
	TypePing FrameType = 7
	TypePong FrameType = 8
	// TypeBye (hub→forwarder, channel 0) ends the session and says whether to
	// reconnect.
	TypeBye FrameType = 9
	// TypeError (both, any channel) reports a non-fatal problem. On channel 0
	// it is about the session; on a positive channel it is about that stream.
	TypeError FrameType = 10
)

type HelloPayload

type HelloPayload struct {
	Version int             `json:"version"`
	Device  ForwardedDevice `json:"device"`
	// AgentVersion is the forwarder build string, for the fleet view.
	AgentVersion string `json:"agent_version,omitempty"`
	// Available reports whether the forwarder can currently reach its device
	// (the USB device is plugged in, the interface exists). A forwarder that
	// connects with its device missing is still useful to see in the UI, so
	// this is advisory, not a refusal.
	Available bool `json:"available"`
	// Detail explains a false Available, for the operator.
	Detail string `json:"detail,omitempty"`
	// Resolved, for a USB device, is what the forwarder found the configured
	// identity resolving to on its host right now — the concrete device, with
	// its current bus/port, so the dashboard can show the real hardware.
	Resolved *executor.USBDevice `json:"resolved,omitempty"`
}

HelloPayload is the forwarder's opening frame.

type Hub

type Hub struct {
	// contains filtered or unexported fields
}

Hub accepts forwarder connections and tracks the sessions they back.

func NewHub

func NewHub(opts HubOptions) (*Hub, error)

NewHub builds a hub.

func (*Hub) Connected

func (h *Hub) Connected() []string

Connected reports the IDs of forwarders with a live session.

func (*Hub) DropSession

func (h *Hub) DropSession(forwarderID, reason string)

DropSession ends a forwarder's live session without touching storage.

It exists for the caller that persists a revocation against the control-plane database directly — the source of truth a cluster shares — and then wants the live connection this member happens to hold cut, rather than routing the whole revocation through this hub's own store. It is a no-op when this member holds no session for the forwarder.

func (*Hub) OpenDevice

func (h *Hub) OpenDevice(ctx context.Context, forwarderID, purpose string) (*DeviceStream, error)

OpenDevice opens a fresh stream to a connected forwarder's device (Task 20430). It is the one way a consumer reaches a forwarded device: the hub hands the stream to an executor agent over the agent's own tunnel, or attaches it in-process for a sandbox on its own host. There is no listener.

func (*Hub) OpenStream

func (h *Hub) OpenStream(ctx context.Context, forwarderID, purpose string) (io.ReadWriteCloser, error)

OpenStream opens a fresh stream to a connected forwarder's device. The caller splices it to a consumer and closes it when done.

func (*Hub) Resolved

func (h *Hub) Resolved(forwarderID string) *HelloPayload

Resolved returns what a connected forwarder reported its USB identity resolving to, for the dashboard. Nil when offline or not a USB device.

func (*Hub) Revoke

func (h *Hub) Revoke(forwarderID string) error

Revoke revokes a forwarder's credential and drops its live session.

func (*Hub) ServeHTTP

func (h *Hub) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP upgrades a forwarder connection and runs its session.

func (*Hub) Session

func (h *Hub) Session(forwarderID string) (*session, bool)

Session returns the live session for a forwarder, if connected.

type HubOptions

type HubOptions struct {
	// Store is the enrollment/credential persistence. Required.
	Store Store
	// OnConnect is called after every successful handshake, with the record
	// and what the forwarder advertised, so the caller can refresh a row.
	OnConnect func(rec ForwarderRecord, hello HelloPayload)
	// OnEnroll is called once, when a token is first redeemed.
	OnEnroll func(rec ForwarderRecord)
	// OnStatusChange mirrors connectivity into durable storage.
	OnStatusChange func(forwarderID, status string, at time.Time)
	// CheckOrigin decides whether a browser Origin may open the socket. Nil
	// accepts same-origin and no-Origin (an agent sets no Origin). The caller
	// passes the hub's own origin policy so the two endpoints judge alike.
	CheckOrigin func(*http.Request) (bool, string)
	// RevocationRecheck is how often a live session re-reads its forwarder's
	// record and ends itself if the credential was revoked meanwhile (Task
	// 20430). It is the backstop under the immediate cut a revocation makes on
	// the member holding the session: a revocation recorded by a process that
	// could not reach this one — the CLI, another hub member — still ends the
	// session within this interval. Zero means DefaultRevocationRecheck.
	RevocationRecheck time.Duration
	// Logf receives operational messages. Now overrides the clock.
	Logf func(format string, args ...any)
	Now  func() time.Time
}

HubOptions configures a Hub.

type ImportOptions

type ImportOptions struct {
	// VHCI is the controller to attach to. Zero means the host's first.
	VHCI VHCI
	// SysfsRoot and DevRoot locate /sys and /dev, for resolving the device
	// once it has enumerated. Empty means the host's.
	SysfsRoot string
	DevRoot   string
	// Ledger, when set, records the attachment so a successor process can
	// sweep it if this one dies holding it.
	Ledger *Ledger
	// EnumerateTimeout bounds the wait for the kernel to enumerate the device
	// after the attach. Zero means 15 seconds.
	EnumerateTimeout time.Duration
	// Logf receives operational messages.
	Logf func(string, ...any)
}

ImportOptions configures an import, with seams for tests.

type L2Source

type L2Source struct {
	// contains filtered or unexported fields
}

L2Source forwards one network interface's frames.

func NewL2Source

func NewL2Source(iface string, opts L2SourceOptions) (*L2Source, error)

NewL2Source builds an L2 source for an interface name.

func (*L2Source) Close

func (s *L2Source) Close() error

Close is a no-op; a raw socket is owned by its stream.

func (*L2Source) Device

func (s *L2Source) Device() ForwardedDevice

Device reports the descriptor this source serves.

func (*L2Source) Open

func (s *L2Source) Open(ctx context.Context) (io.ReadWriteCloser, error)

Open opens a promiscuous raw socket on the interface, framed for the tunnel.

func (*L2Source) Probe

func (s *L2Source) Probe(ctx context.Context) (bool, string, *executor.USBDevice)

Probe reports whether the interface exists and is up.

type L2SourceOptions

type L2SourceOptions struct {
	Logf func(string, ...any)
}

L2SourceOptions configures an L2 source.

type Ledger

type Ledger struct {
	// contains filtered or unexported fields
}

Ledger is this process's record of its live attachments.

func OpenLedger

func OpenLedger(dir string, logf func(string, ...any)) (*Ledger, error)

OpenLedger opens this process's ledger in dir, creating dir 0700.

func (*Ledger) Add

func (l *Ledger) Add(e LedgerEntry) string

Add records an attachment and returns its ledger id.

func (*Ledger) Entries

func (l *Ledger) Entries() []LedgerEntry

Entries returns the live entries, for diagnostics and tests.

func (*Ledger) Remove

func (l *Ledger) Remove(id string)

Remove forgets an attachment that was undone.

type LedgerEntry

type LedgerEntry struct {
	ID       string     `json:"id"`
	Kind     LedgerKind `json:"kind"`
	VHCIRoot string     `json:"vhci_root,omitempty"`
	VHCIPort int        `json:"vhci_port,omitempty"`
	DevID    uint32     `json:"dev_id,omitempty"`
	TAP      string     `json:"tap,omitempty"`
	At       time.Time  `json:"at"`
}

LedgerEntry is one attachment.

type LedgerKind

type LedgerKind string

LedgerKind is what an entry records.

const (
	// LedgerVHCI is a vhci_hcd port this process attached a device to.
	LedgerVHCI LedgerKind = "vhci"
	// LedgerTAP is a TAP interface this process created.
	LedgerTAP LedgerKind = "tap"
)

type LocalSource

type LocalSource interface {
	// Device is the descriptor this source serves, for the hello.
	Device() ForwardedDevice
	// Probe reports whether the device is reachable right now. resolved, for a
	// USB source, is the concrete device the configured identity matched, so
	// the dashboard can show the real hardware. A false ok is advisory: the
	// forwarder still connects so the operator can see it, and refuses Opens
	// until the device is present.
	Probe(ctx context.Context) (ok bool, detail string, resolved *executor.USBDevice)
	// Open opens a fresh byte stream to the device — a connection to the local
	// usbip server for a USB device, a raw interface handle for L2. The caller
	// closes it when the stream ends.
	Open(ctx context.Context) (io.ReadWriteCloser, error)
	// Close releases anything held across the forwarder's whole life, such as
	// a spawned usbip daemon. Called once at shutdown.
	Close() error
}

LocalSource is the on-prem endpoint of a forwarded device.

type MintOptions

type MintOptions struct {
	Name   string
	Device ForwardedDevice
	TTL    time.Duration
	Owner  string
	// CreatedBy identifies the minting operator (OIDC subject, or "" local).
	CreatedBy string
	// Server and Pin go into the bundle the operator carries to the device;
	// they are not persisted.
	Server string
	Pin    string
	Now    func() time.Time
}

MintOptions parameterises token creation.

type OpenAckPayload

type OpenAckPayload struct {
	OK    bool   `json:"ok"`
	Error string `json:"error,omitempty"`
	// Device describes what the forwarder opened (Task 20430): for USB the
	// on-prem bus id a consumer's OP_REQ_IMPORT must name. Additive, so an
	// older forwarder that omits it still interoperates; the hub then falls
	// back to the bus id the forwarder's hello resolved.
	Device *ExportedDevice `json:"device,omitempty"`
}

OpenAckPayload reports the outcome of an Open.

type OpenPayload

type OpenPayload struct {
	// Purpose is a short free-form note for the forwarder's log and the audit
	// trail: "attach", "devlist". Advisory only.
	Purpose string `json:"purpose,omitempty"`
}

OpenPayload asks the forwarder to open a stream to its device.

type ProbeResult

type ProbeResult struct {
	BusID      string `json:"bus_id"`
	VendorID   string `json:"vendor_id"`
	ProductID  string `json:"product_id"`
	Speed      uint32 `json:"speed"`
	Interfaces int    `json:"interfaces"`
}

ProbeResult is what a USB probe read through a forwarder's stream.

func ProbeUSB

func ProbeUSB(ctx context.Context, stream io.ReadWriteCloser) (ProbeResult, error)

ProbeUSB asks the forwarder behind stream for its device list and closes the stream (Task 20430). It proves the whole path — hub, tunnel, forwarder, device — answers, without binding the device or handing it to any kernel.

type Store

type Store interface {
	PutEnrollment(rec EnrollmentRecord) error
	GetEnrollment(id string) (EnrollmentRecord, error)
	RedeemEnrollment(id, forwarderID string, at time.Time) error
	RevokeEnrollment(id string, at time.Time) error
	ListEnrollments() ([]EnrollmentRecord, error)

	PutForwarder(rec ForwarderRecord) error
	GetForwarder(forwarderID string) (ForwarderRecord, error)
	RevokeForwarder(forwarderID string, at time.Time) error
	TouchForwarder(forwarderID string, at time.Time) error
	ListForwarders() ([]ForwarderRecord, error)
}

Store is the persistence the enrollment flow needs. It is an interface so the security-relevant logic can be tested against an in-memory fake, including the concurrent-redemption race. RedeemEnrollment must perform the "still unredeemed?" check and the write atomically.

type SweepReport

type SweepReport struct {
	// Detached are the vhci ports detached, as "port N".
	Detached []string
	// Removed are the TAP interfaces removed.
	Removed []string
	// Errors are the entries that could not be undone.
	Errors []string
}

SweepReport says what a sweep found and undid.

func SweepLedgers

func SweepLedgers(dir string, logf func(string, ...any)) SweepReport

SweepLedgers undoes the attachments recorded in dir by processes that are no longer running, and removes their ledgers. A live process's ledger — this one's, or another member's on the same host — is left alone.

type TAPBridge

type TAPBridge struct {
	// contains filtered or unexported fields
}

TAPBridge is a running L2 bridge: a TAP interface fed by a forwarder stream.

func BridgeTAP

func BridgeTAP(stream io.ReadWriteCloser, tapName string) (*TAPBridge, error)

BridgeTAP creates a TAP interface and bridges it to an open stream to an L2 forwarder's device. The TAP is created down and unaddressed; the caller brings it up — or hands it to a sandbox, whose passthrough does. The bridge ends when the stream does, or when the caller closes it, which removes the TAP.

func (*TAPBridge) Close

func (b *TAPBridge) Close() error

Close tears down the bridge and removes the TAP.

func (*TAPBridge) Done

func (b *TAPBridge) Done() <-chan struct{}

Done is closed once the bridge has ended, for any reason.

func (*TAPBridge) Name

func (b *TAPBridge) Name() string

Name is the TAP interface name on this host.

type USBImport

type USBImport struct {

	// Port is the vhci port the device is attached to, and DevID the remote
	// device id the kernel recorded for it.
	Port  int
	DevID uint32
	// Remote is what the forwarder exported.
	Remote usbipDevice
	// Device is the device as it enumerated on this host.
	Device executor.USBDevice
	// contains filtered or unexported fields
}

USBImport is a forwarded USB device attached to this host's vhci_hcd.

func ImportUSB

func ImportUSB(ctx context.Context, stream io.ReadWriteCloser, busid string, opts ImportOptions) (_ *USBImport, err error)

ImportUSB imports the device behind stream onto this host. busid is the forwarder's bus id for it, which OP_REQ_IMPORT must name. On success the device has enumerated on this host and u.Device describes it; the stream is owned by the import from then on and closed with it. On failure the stream is closed too.

func (*USBImport) Close

func (u *USBImport) Close() error

Close ends the attachment: the relay stops, the port is detached if it still holds this device, and the stream is closed. Idempotent; safe to call from any goroutine.

func (*USBImport) Done

func (u *USBImport) Done() <-chan struct{}

Done is closed once the attachment has ended, for any reason.

func (*USBImport) HostDevices

func (u *USBImport) HostDevices(name string) []executor.HostDevice

HostDevices are the device nodes a sandbox is given for this device: the usbfs node and its interface nodes, named after name.

type USBSource

type USBSource struct {
	// contains filtered or unexported fields
}

USBSource forwards one USB device.

func NewUSBSource

func NewUSBSource(match executor.USBMatch, opts USBSourceOptions) (*USBSource, error)

NewUSBSource builds a USB source for a device identity.

func (*USBSource) Close

func (s *USBSource) Close() error

Close ensures the device is no longer bound to usbip-host.

func (*USBSource) Device

func (s *USBSource) Device() ForwardedDevice

Device reports the descriptor this source serves.

func (*USBSource) Open

func (s *USBSource) Open(ctx context.Context) (io.ReadWriteCloser, error)

Open resolves the device and returns a stream that answers the consumer's opening USB/IP request. Nothing is bound yet: a device list is answered from sysfs and binds nothing, and only an accepted import binds the device to usbip-host and hands the connection to the kernel (Task 20430). A probe therefore never takes the device away from the host it is plugged into.

func (*USBSource) Probe

func (s *USBSource) Probe(ctx context.Context) (bool, string, *executor.USBDevice)

Probe resolves the identity and reports whether the device is attached.

type USBSourceOptions

type USBSourceOptions struct {
	// SysfsRoot defaults to /sys.
	SysfsRoot string
	// UsbipBinary is the usbip(8) tool used to bind/unbind the device to the
	// usbip-host driver. Defaults to "usbip" resolved on PATH.
	UsbipBinary string
	// Logf receives operational messages.
	Logf func(string, ...any)
}

USBSourceOptions configures a USB source, with seams for tests.

type VHCI

type VHCI struct {
	// Root is the platform-device directory holding attach, detach and
	// status. Empty means DefaultVHCIRoot.
	Root string
}

VHCI is one host's virtual host controller.

func (VHCI) Available

func (v VHCI) Available() error

Available reports whether vhci_hcd is loaded and this process can attach.

type WelcomePayload

type WelcomePayload struct {
	Version int `json:"version"`
	// ForwarderID is the hub's durable ID for this forwarder, echoed so the
	// device can log which identity it is serving as.
	ForwarderID string `json:"forwarder_id"`
	// Credential is the long-lived forwarder credential, non-empty only on the
	// connection that redeemed an enrollment token. The forwarder persists it
	// and reconnects with it; it is shown once and never again.
	Credential string `json:"credential,omitempty"`
}

WelcomePayload is the hub's acceptance.

Jump to

Keyboard shortcuts

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