serialdev

package
v0.8.2 Latest Latest
Warning

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

Go to latest
Published: Oct 7, 2026 License: MIT Imports: 17 Imported by: 0

Documentation

Overview

Package serialdev enumerates host serial devices and records which ones a run has been approved to use.

Approval is trust-on-first-use: the first run to use a device name records that device's identity (see Pin), and later runs must present the same device. Enforcement lives here rather than in the kernel because the devices cgroup can only filter on major:minor, which cannot distinguish two boards of the same model. See docs/plans/2026-08-15-serial-devices-design.md.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrNoMatch means no attached device matched the config's vid/pid.
	ErrNoMatch = errors.New("no matching serial device")
	// ErrAmbiguous means several devices matched. Match still returns the
	// candidates so callers can name them in the error shown to the user.
	ErrAmbiguous = errors.New("multiple matching serial devices")
)
View Source
var ErrPinMismatch = errors.New("serial device does not match its pin")

ErrPinMismatch means the attached device is not the one previously approved under this name. It is always fatal rather than a warning: a swapped device is the exact case pinning exists to catch.

View Source
var ErrPinNameTaken = errors.New("a device is already pinned under that name")

ErrPinNameTaken is returned by Rename when the new name already holds a pin.

View Source
var ErrPinNotFound = errors.New("no device is pinned under that name")

ErrPinNotFound is returned when an operation names a pin that does not exist.

Functions

func DefaultPinPath

func DefaultPinPath() string

DefaultPinPath is where pins live under the moat home directory. MOAT_HOME overrides the location the same way it does for every other moat state.

Types

type Device

type Device struct {
	Path        string // host device node, e.g. /dev/ttyUSB0 or /dev/cu.usbserial-14200
	VID         string // 4-hex-digit USB vendor ID, lowercase
	PID         string // 4-hex-digit USB product ID, lowercase
	Serial      string // USB iSerial, empty on devices that ship without one
	PortPath    string // physical topology: sysfs port path (Linux) or IOKit locationID (macOS)
	Description string // human label for `moat device list`

	// Interface distinguishes the UARTs of a multi-interface bridge (FT2232H,
	// CP2105, ESP-Prog): one USB device, several ttys, everything else about
	// them identical. It is the interface's number within the device — "0",
	// "1" — and empty when the platform cannot see it.
	Interface string
	// contains filtered or unexported fields
}

Device is a serial port attached to the host.

func Match

func Match(devs []Device, m Matcher) ([]Device, error)

Match returns devices matching m. Hex IDs compare case-insensitively so a config written as "303A" matches a device enumerated as "303a".

On ambiguity it returns every candidate alongside ErrAmbiguous — the caller needs them to tell the user which devices collided.

func (Device) Identity

func (d Device) Identity() Identity

Identity returns the device's approval identity.

type Enumerator

type Enumerator interface {
	List(ctx context.Context) ([]Device, error)
}

Enumerator lists serial devices attached to the host.

It is an interface so tests can supply a fixed device list: every automated test in this area runs without hardware.

func NewEnumerator

func NewEnumerator() Enumerator

NewEnumerator returns the host's serial device enumerator.

type EnumeratorFunc

type EnumeratorFunc func(ctx context.Context) ([]Device, error)

EnumeratorFunc adapts a function to the Enumerator interface.

func (EnumeratorFunc) List

func (f EnumeratorFunc) List(ctx context.Context) ([]Device, error)

List implements Enumerator.

type Identity

type Identity struct {
	VID       string
	PID       string
	Serial    string
	PortPath  string
	Interface string
}

Identity is the subset of a Device used for approval decisions.

type Matcher

type Matcher struct {
	VID string
	PID string
	// Interface narrows a match to one UART of a multi-interface bridge, where
	// several ttys share a USB ID. Empty matches any interface, which keeps
	// single-UART devices as they were.
	Interface string
}

Matcher selects devices by USB vendor and product ID.

type Pin

type Pin struct {
	Name     string `json:"name"`
	VID      string `json:"vid"`
	PID      string `json:"pid"`
	Serial   string `json:"serial,omitempty"`
	PortPath string `json:"port_path,omitempty"`
	// Interface is the UART index of a multi-interface bridge (FT2232H,
	// CP2105). Empty on pins written before the field existed, which skips
	// the interface check rather than demanding "0": CDC modems expose their
	// single tty on interface 1.
	Interface string    `json:"interface,omitempty"`
	FirstSeen time.Time `json:"first_seen"`
}

Pin records the identity a device name was first approved with.

func PinFor

func PinFor(name string, d Device) Pin

PinFor builds the pin recorded on first approval of d under name.

func (Pin) PinnedByPortPath

func (p Pin) PinnedByPortPath() bool

PinnedByPortPath reports whether this pin identifies its device only by physical port, which happens when the device ships without a serial number (common on cheap CH340 and CP2102 clones).

Callers must surface this in the consent prompt: such a pin approves whatever is plugged into that port, not that particular device.

func (Pin) Verify

func (p Pin) Verify(d Device) error

Verify checks that d is the device this pin was created for.

type PinStore

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

PinStore persists pins as JSON. It is safe for concurrent use within a process, and across processes via an advisory lock on a sidecar lock file: two moat processes approving devices concurrently would otherwise race — each reads the file at open, each writes its own map back, and the second write drops the first's pins, silently re-arming trust-on-first-use for every device the loser had pinned.

func OpenPinStore

func OpenPinStore(path string) (*PinStore, error)

OpenPinStore loads the store at path, creating an empty one if absent.

A malformed file is an error rather than an empty store: silently discarding pins would silently disable the approval check.

The store keeps the file's lock sidecar open so mutations can flock it; a store whose lock file cannot be opened still works single-process (lf is nil, lockFile is skipped) rather than failing every run.

func (*PinStore) Close

func (s *PinStore) Close() error

Close releases the lock file. The store is unusable after Close.

func (*PinStore) Forget

func (s *PinStore) Forget(name string) error

Forget removes a pin so the next run re-approves the device. Forgetting an unknown name is not an error.

func (*PinStore) Get

func (s *PinStore) Get(name string) (Pin, bool, error)

Get returns the pin recorded for name, if any.

func (*PinStore) List

func (s *PinStore) List() ([]Pin, error)

List returns all pins, for `moat device list`.

func (*PinStore) Put

func (s *PinStore) Put(p Pin) error

Put records a pin and flushes the store.

func (*PinStore) Rename

func (s *PinStore) Rename(from, to string) error

Rename moves a pin from one name to another under a single lock.

Doing this as Put+Forget from the caller would leave a window where the hardware is pinned twice, and a crash inside it would make that permanent — the state this exists to avoid, since a device pinned under two names is invisible in `moat device list` (it reports the first pin that verifies). Renaming onto an existing name fails rather than overwriting: that pin belongs to some other hardware, and silently replacing it would un-approve a device the user never mentioned.

type USBEnumerator

type USBEnumerator interface {
	ListUSB(ctx context.Context) ([]Device, error)
}

USBEnumerator lists USB devices the serial broker cannot serve: ones with no serial interface at all (SDR dongles, keyboards, storage). It exists so `moat device list` can show a plugged-in-but-not-serial device instead of implying nothing is attached. Its devices carry a USB identity (VID/PID, serial, port path) but Path is always empty.

type USBEnumeratorFunc

type USBEnumeratorFunc func(ctx context.Context) ([]Device, error)

USBEnumeratorFunc adapts a function to the USBEnumerator interface.

func (USBEnumeratorFunc) ListUSB

func (f USBEnumeratorFunc) ListUSB(ctx context.Context) ([]Device, error)

ListUSB implements USBEnumerator.

Jump to

Keyboard shortcuts

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