How to create an OS Image with OpenThread Border Router¶
This tutorial walks you through creating an OS image that is pre-loaded with OpenThread Border Router (OTBR). We use Ubuntu Core as the Linux distribution because it is optimized for IoT and is secure by design. We configure the image and bundle the snapped version of OTBR. After the deployment, the snaps will continue to receive updates for the latest security and bug fixes.
Before starting, it is recommended to read the documentation on Ubuntu Core components and get familiar with various useful concepts.
Requirements:
An amd64 Ubuntu development environment
An amd64 machine as target for installing the new OS
A Thread Radio Co-processor (RCP), connected to the target machine.
Used in this tutorial:
Desktop computer running Ubuntu 23.10
Intel NUC11TNH with 8GB RAM and 250GB NAND flash storage
Nordic Semiconductor nRF52840 Dongle, connected to Intel NUC
We need the following tools on the development environment:
snapcraft to manage keys in the store and build snaps
yq to validate YAML files and convert them to JSON
ubuntu-image v2 to build the Ubuntu Core image
Install them using the following commands:
sudo snap install snapcraft --classic
sudo snap install yq
sudo snap install ubuntu-image --classic
Create a custom gadget¶
Overriding the snap configurations upon installation is possible with a gadget snap.
The pc gadget is available as a pre-built snap in the store, however, in this chapter, we need to build our own to include custom configurations and interface connections.
We will create our custom gadget by staging the latest stable core22 gadget, and making the necessary modifications.
We need to create two files:
snapcraft.yaml
: the definition of our custom Gadget snap. We need this custom gadget in order to ship snap configurations on our image.gadget.yaml
: the volumes layout for the image, list of snap default configuration, and interface connections.
Create the snapcraft.yaml
file with the following content:
name: otbr-gadget
type: gadget
base: core22
version: test
summary: OpenThread Border Router Gadget
description: Custom gadget to configure the OpenThread Border Router snap
architectures:
- build-on: [amd64]
grade: stable
confinement: strict
parts:
gadget:
plugin: nil
stage-snaps:
- pc/22
Then, create the gadget.yaml
file :
# Default and unchanged volume definitions taken from
# https://github.com/snapcore/pc-gadget/blob/22/gadget/gadget-amd64.yaml
volumes:
pc:
schema: gpt
# bootloader configuration is shipped and managed by snapd
bootloader: grub
structure:
- name: mbr
type: mbr
size: 440
update:
edition: 1
content:
- image: mbr.img
# This one should be removed in core24
# or if we find a way to allow updates without keeping
# all partitions
- name: BIOS Boot
type: 21686148-6449-6E6F-744E-656564454649
size: 1M
offset: 1M
update:
edition: 2
- name: ubuntu-seed
role: system-seed
filesystem: vfat
# UEFI will boot the ESP partition by default first
type: C12A7328-F81F-11D2-BA4B-00A0C93EC93B
size: 1200M
update:
edition: 2
content:
- source: grubx64.efi
target: EFI/boot/grubx64.efi
- source: shim.efi.signed
target: EFI/boot/bootx64.efi
- name: ubuntu-boot
role: system-boot
filesystem: ext4
type: 0FC63DAF-8483-4772-8E79-3D69D8477DE4
# whats the appropriate size?
size: 750M
update:
edition: 1
content:
- source: grubx64.efi
target: EFI/boot/grubx64.efi
- source: shim.efi.signed
target: EFI/boot/bootx64.efi
- name: ubuntu-save
role: system-save
filesystem: ext4
type: 0FC63DAF-8483-4772-8E79-3D69D8477DE4
size: 32M
- name: ubuntu-data
role: system-data
filesystem: ext4
type: 0FC63DAF-8483-4772-8E79-3D69D8477DE4
size: 1G
# Custom snap configurations
defaults:
# openthread-border-router
AmezHbALZOOhReOPtKyluS5TJmySg15e:
# Set to enable and start services
autostart: true
# For QEMU the networking interface should be enp0s2
# For Intel NUC 11: enp88s0, enp89s0, or wlo1
infra-if: enp88s0
thread-if: wpan0
radio-url: "spinel+hdlc+uart:///dev/ttyACM0"
# Custom interface connections
connections:
# openthread-border-router -> system
- plug: AmezHbALZOOhReOPtKyluS5TJmySg15e:firewall-control
- plug: AmezHbALZOOhReOPtKyluS5TJmySg15e:raw-usb
- plug: AmezHbALZOOhReOPtKyluS5TJmySg15e:network-control
- plug: AmezHbALZOOhReOPtKyluS5TJmySg15e:bluetooth-control
# openthread-border-router -> avahi
- plug: AmezHbALZOOhReOPtKyluS5TJmySg15e:avahi-control
slot: dVK2PZeOLKA7vf1WPCap9F8luxTk9Oll:avahi-control
# openthread-border-router -> bluez
- plug: AmezHbALZOOhReOPtKyluS5TJmySg15e:bluez
slot: JmzJi9kQvHUWddZ32PDJpBRXUpGRxvNS:service
Build the gadget snap:
snapcraft --verbose
This results in creating a snap named otbr-gadget_test_amd64.snap
.
Note
You need to rebuild the snap every time you change the gadget.yaml
file.
Create the model assertion¶
The model assertion is a digitally signed document that describes the content of the Ubuntu Core image.
Below is an example model assertion in YAML, describing a core22
Ubuntu Core
image:
type: model
series: '16'
model: ubuntu-core-22-amd64
architecture: amd64
base: core22
# Setting grade to dangerous to allow use of an unsigned gadget snap
grade: dangerous
# Since this is a custom model assertion, set the following to your developer ID
authority-id: <developer-id>
brand-id: <developer-id>
# Timestamp should be within your signature's validity period, in RFC3339 format
timestamp: '<timestamp>'
snaps:
- # This is our custom, dev gadget snap
# It has no channel and id, because it isn't in the store.
# We're going to build it locally and pass it to the image builder.
name: otbr-gadget
type: gadget
# default-channel:
# id:
- name: pc-kernel
type: kernel
default-channel: 22/stable
id: pYVQrBcKmBa0mZ4CCN7ExT6jH8rY1hza
- name: snapd
type: snapd
default-channel: latest/stable
id: PMrrV4ml8uWuEUDBT8dSGnKUYbevVhc4
- name: core22
type: base
default-channel: latest/stable
id: amcUKQILKXHHTlmSa7NMdnXSx02dNeeT
# Apps
- name: avahi
type: app
default-channel: 22/stable
id: dVK2PZeOLKA7vf1WPCap9F8luxTk9Oll
- name: bluez
type: app
default-channel: 22/stable
id: JmzJi9kQvHUWddZ32PDJpBRXUpGRxvNS
- name: openthread-border-router
type: app
default-channel: latest/edge
id: AmezHbALZOOhReOPtKyluS5TJmySg15e
Refer to the model assertion documentation and inline comments for details.
Create a model.yaml
with the above content, replacing authority-id
,
brand-id
, and timestamp
.
Note
Unlike the official documentation which uses JSON, we use YAML serialization for the model. This is for consistency with all the other serialization formats in this tutorial. Moreover, it allows us to comment out some parts for testing or add comments to describe the details inline.
To find you developer ID, use the Snapcraft CLI:
$ snapcraft whoami
...
developer-id: <developer-id>
or get it from the Snapcraft Dashboard.
Follow these instructions to create a developer account, if you don’t already have one.
Next, we need to sign the model assertion. Refer to this article for details on how to sign the model assertion. Here are the needed steps:
Create and register a key
snap login
snap keys
# Continue if you have no existing keys.
# You'll be asked to set a passphrase which is needed before signing
snap create-key otbr-uc-tutorial
snapcraft register-key otbr-uc-tutorial
We now have a registered key named otbr-uc-tutorial
which we’ll use later.
Sign the model assertion
We sign the model using the otbr-uc-tutorial
key created and registered earlier.
The snap sign
command takes JSON as input and produces YAML as output!
We use the YQ app to convert our model assertion to JSON before passing it in
for signing.
yq eval model.yaml -o=json | snap sign -k otbr-uc-tutorial > model.signed.yaml
This will produce a signed model named model.signed.yaml
.
Note
You need to repeat the signing every time you change the input model, because the signature is calculated based on the model.
Build the Ubuntu Core image¶
We use ubuntu-image
and set the path to:
The signed model assertion YAML file.
The locally built gadget snap.
ubuntu-image snap model.signed.yaml --verbose --validation=enforce \
--snap otbr-gadget_test_amd64.snap
This downloads all the snaps specified in the model assertion and builds
an image file called pc.img
.
✅ The image file is now ready to be flashed on a medium to create a bootable drive with the Ubuntu Core installer!
Install the Ubuntu Core image¶
The installation instructions are device specific. You may refer to Ubuntu Core section in this page. For example:
Intel NUC - applicable to most computers with a secondary storage
A precondition to continue with some of the instructions is to compress pc.img
.
This speeds up the transfer and makes the input file similar to official images,
improving compatibility with the official instructions.
To compress with the lowest compression rate of zero:
xz -vk -0 pc.img
A higher compression rate significantly increases the processing time and needed resources, with very little gain.
Now, follow the device specific instructions.
✅ Continue to perform the OS initialization steps appearing by default.
Once the installation is complete, you will see the interface of the
console-conf
program.
It will walk you through the networking and user account setup.
You’ll need to enter the email address of your Ubuntu account to create an OS
user account with your registered username and have your SSH public keys
deployed as authorized SSH keys for that user.
If you haven’t done so in the past, refer to the
Creating your developer account documentation
to add your SSH keys before doing this setup.
Read about system user assertion to know how the manual account setup looks like and how it can be automated.
✅ Congratulations. The Ubuntu Core installation is complete and the device is ready for use. The OTBR services should be running and functional.
Sanity check¶
Now, let’s verify that everything is in place and functional.
Connect to the machine over SSH:
$ ssh <ubuntu-one-username>@<device-ip>
Welcome to Ubuntu 22.04.3 LTS (GNU/Linux 5.15.0-91-generic x86_64)
The programs included with the Ubuntu system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.
Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
applicable law.
* Ubuntu Core: https://www.ubuntu.com/core
* Community: https://forum.snapcraft.io
* Snaps: https://snapcraft.io
This Ubuntu Core 22 machine is a tiny, transactional edition of Ubuntu,
designed for appliances, firmware and fixed-function VMs.
If all the software you care about is available as snaps, you are in
the right place. If not, you will be more comfortable with classic
deb-based Ubuntu Server or Desktop, where you can mix snaps with
traditional debs. It's a brave new world here in Ubuntu Core!
Please see 'snap --help' for app installation and updates.
List the installed snaps:
<user>@ubuntu:~$ snap list
Name Version Rev Tracking Publisher Notes
avahi 0.8 327 22/stable ondra -
bluez 5.64-4 356 22/stable canonical✓ -
core22 20231123 1033 latest/stable canonical✓ base
openthread-border-router thread-reference-20230119+snap 37 latest/edge canonical-iot-labs -
otbr-gadget test x1 - - gadget
pc-kernel 5.15.0-91.101.1 1540 22/stable canonical✓ kernel
snapd 2.60.4 20290 latest/stable canonical✓ snapd
✅ Avahi, BlueZ, and openthread-border-router are installed.
Check the running snap services:
<user>@ubuntu:~$ snap services
Service Startup Current Notes
avahi.daemon enabled active -
bluez.bluez enabled active -
openthread-border-router.otbr-agent enabled active -
openthread-border-router.otbr-setup enabled inactive -
openthread-border-router.otbr-web enabled active -
✅ Avahi and BlueZ’s services are enabled and active.
✅ The OTBR agent and web server are enabled and active.
✅ The OTBR setup oneshot service is enabled, but inactive.
It is enabled because it needs to run on every boot to setup the firewall and network.
It is inactive because it has completed its work and exited.
Check the snap connections:
<user>@ubuntu:~$ snap connections openthread-border-router
Interface Plug Slot Notes
avahi-control openthread-border-router:avahi-control avahi:avahi-control gadget
bluetooth-control openthread-border-router:bluetooth-control :bluetooth-control gadget
bluez openthread-border-router:bluez bluez:service gadget
dbus - openthread-border-router:dbus-wpan0 -
firewall-control openthread-border-router:firewall-control :firewall-control gadget
network openthread-border-router:network :network -
network-bind openthread-border-router:network-bind :network-bind -
network-control openthread-border-router:network-control :network-control gadget
raw-usb openthread-border-router:raw-usb :raw-usb gadget
✅ The connections with gadget
in the Note match those defined as
connections
in our gadget.
Finally, check the snap configurations:
<user>@ubuntu:~$ snap get openthread-border-router
Key Value
autostart true
infra-if enp88s0
radio-url spinel+hdlc+uart:///dev/ttyACM0
thread-if wpan0
✅ The values are according to the defaults
set in our gadget.
You may further continue by checking the logs, for example with snap logs -n 100 -f openthread-border-router
.