This page describes how developers can implement an interface that supports USB hotplugging,via snapd’s built-in hotplug support.
For a general user overview, including enabling this functionality and hotplug interface management, see Hotplug support.
ⓘ Hotplug support is currently under active development, and will become widely available with the release of snapd 2.39.
Adding hotplug interfaces
The code for a snapd interface has be modified to support hotplug, and subsequently create slots on the system snap, by implementing the following new function:
HotplugDeviceDetected(deviceInfo *hotplug.HotplugDeviceInfo) (*hotplug.ProposedSlot, error)
When defined, HotplugDeviceDetected
is executed whenever a any hotplug device is connected to the system. The purpose of the function is to decide whether a freshly connected device is relevant for any given interface by returning one of the following:
- if the connected device is not relevant, the function should return
nil, nil
- if the connected device is relevant, a definition of the proposed hotplug slot that needs to be created should be returned (see below)
- if the connected device is relevant, but it’s impossible to create a slot definition for it, an error should be returned
It is important to note that this function may be called multiple times as the device connected because, in many cases, the kernel creates various pseudo- and virtual- devices for given physical device. The function should filter out irrelevant calls and only create a single slot definition for the actual device.
The returned slot is a proposed slot because snapd’s hotplug subsystem mediates the creation of the final slot. It may simply update the slot if:
- the device is being reconnected
- the slot previously existed and had connections
Attribute access methods
The above function receives the hotplug.HotplugDeviceInfo
structure which contains all the attributes provided by the udev event for the device, and offers the following methods to access them:
-
Subsystem() string
Returns the name of the kernel subsystem of the device, for example
tty
orsound
. -
DeviceName() string
Corresponds to udev’s
DEVNAME
attribute and provides the actual device path, e.g. /dev/ttyUSB0. This is the path that the interface is generally interested in, as far as confinement rules are concerned. -
DevicePath() string
Corresponds to udev’s
DEVPATH
attribute and provides device path under the sysfs filesystem, e.g. /sys/devices/pci0000:00/0000:00:14.0/usb1/1-2. -
DeviceType() string
Corresponds to udev’s
DEVTYPE
attribute, e.g. disk. -
Major() string
andMinor() string
Correspond to udev’s
MAJOR
andMINOR
attributes which provide major/minor device numbers (if applicable). -
Attribute(name string) (string, bool)
A generic method to query any udev attribute. It returns false if the attribute is not present in udev event data.
The hotplug.ProposedSlot
structure is created by HotplugDeviceDetected
in response to a udev event, and may define a name and label for the slot.
However, they are both optional and, in most cases, should be empty. Snapd’s hotplug subsystem will automatically generate them by probing and sanitising some of the well-defined udev attributes, such as model and vendor names. This ensures the resulting name, whether provided by interface code or left empty for snapd to figure out, is unique.
However, the hotplug.ProposedSlot
must have some attributes set, and in typical cases, should at least include an attribute that carries the path of the device.
The path attribute can then be read by the following methods of the interface (and others, as applicable), to create respective security profiles for the snap and the given device:
AppArmorConnectedSlot
SecCompConnectedPlug
SecCompConnectedSlot
Example implementation
The following is one potentially complete implementation of HotplugDeviceDetected
:
func (iface *myInterface) HotplugDeviceDetected(di *hotplug.HotplugDeviceInfo) (*hotplug.ProposedSlot, error) {
bus, _ := di.Attribute("ID_BUS”)
// some arbitrary criteria to filter irrelevant devices out
if di.Subsystem() != "tty" || bus != "usb” || di.Major() != „123” ) {
return nil, nil
}
slot := hotplug.ProposedSlot{
Attrs: map[string]interface{}{
"path": di.DeviceName(),
},
}
return slot, nil
}
When creating filtering logic for the above function, it is useful to inspect udev attributes of the respective device. One way of doing this with the is with the udevadm command. This can be used to report all currently existing devices (eg. udevadm info -e
) or run in monitor mode (udevadm monitor -p
) to continuously report all udev event, along with their attributed, as devices connected and unplugged.
As an example, the following is an example of output from udevadm for a USB serial port adapter:
P: /devices/pci0000:00/0000:00:11.0/0000:02:00.0/usb2/2-2/2-2.1/2-2.1:1.0/ttyUSB0/tty/ttyUSB0
N: ttyUSB0
S: serial/by-id/usb-FTDI_FT232R_USB_UART_AH06W0EQ-if00-port0
S: serial/by-path/pci-0000:02:00.0-usb-0:2.1:1.0-port0
E: DEVLINKS=/dev/serial/by-id/usb-FTDI_FT232R_USB_UART_AH06W0EQ-if00-port0 /dev/serial/by-path/pci-0000:02:00.0-usb-0:2.1:1.0-port0
E: DEVNAME=/dev/ttyUSB0
E: DEVPATH=/devices/pci0000:00/0000:00:11.0/0000:02:00.0/usb2/2-2/2-2.1/2-2.1:1.0/ttyUSB0/tty/ttyUSB0
E: ID_BUS=usb
E: ID_MM_CANDIDATE=1
E: ID_MODEL=FT232R_USB_UART
E: ID_MODEL_ENC=FT232R\x20USB\x20UART
E: ID_MODEL_FROM_DATABASE=FT232 Serial (UART) IC
E: ID_MODEL_ID=6001
E: ID_PATH=pci-0000:02:00.0-usb-0:2.1:1.0
E: ID_PATH_TAG=pci-0000_02_00_0-usb-0_2_1_1_0
E: ID_PCI_CLASS_FROM_DATABASE=Serial bus controller
E: ID_PCI_INTERFACE_FROM_DATABASE=UHCI
E: ID_PCI_SUBCLASS_FROM_DATABASE=USB controller
E: ID_REVISION=0600
E: ID_SERIAL=FTDI_FT232R_USB_UART_AH06W0EQ
E: ID_SERIAL_SHORT=AH06W0EQ
E: ID_TYPE=generic
E: ID_USB_DRIVER=ftdi_sio
E: ID_USB_INTERFACES=:ffffff:
E: ID_USB_INTERFACE_NUM=00
E: ID_VENDOR=FTDI
E: ID_VENDOR_ENC=FTDI
E: ID_VENDOR_FROM_DATABASE=Future Technology Devices International, Ltd
E: ID_VENDOR_ID=0403
E: MAJOR=188
E: MINOR=0
E: SUBSYSTEM=tty
E: TAGS=:systemd:
E: USEC_INITIALIZED=415122796440
Snapd hardware interfaces, whose slots may appear as part of their gadget.yaml definition, for example, allowing gadget
in their allow-installation
section of the base declaration, must additionally implement the following function:
HandledByGadget(deviceInfo *HotplugDeviceInfo, slot *snap.SlotInfo) bool
The above function acts as a predicate that should return true if the device described by deviceInfo
is the same as the one represented by the given slot. It’s called by the hotplug subsystem whenever a device is connected, and it receives slot(s) of the given interface defined statically in gadget.yaml.
In the typical cases where gadget slots are defined by means of device paths, the implementation of this method becomes a simple comparison of device path and the path attribute of the slot, for example:
func (iface *myInterface) HandledByGadget(di *hotplug.HotplugDeviceInfo, slot *snap.SlotInfo) bool {
var path string
if err := slot.Attr("path", &path); err != nil {
return false
}
return di.DeviceName() == path
}
Hotplug and interface hooks
When a supported device is connected to the system, snapd creates a hotplug slot for its respective interface. If the slot is then connected to a plug, either manually by the user, or via the auto-connect mechanism, the following interface hooks are executed if they exist:
prepare-plug-<plugname>
connect-plug-<plugnname>
(for the snap on the plug side)
Similarly, when the device is disconnected and its slot had been connected to a plug, its disconnect
interface hooks get executed, eg:
disconnect-plug-<plugname>
This mechanism can be used by snaps to react to devices appearing/disappearing from their connected plugs. Please refer to the documentation on Interface Hooks for more details on this functionality.