Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
212 changes: 212 additions & 0 deletions bluez.sysext/create.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
#!/usr/bin/env bash
# vim: et ts=2 syn=bash
#
# The BlueZ sysext.
#
# BlueZ is the Linux Bluetooth userspace: bluetoothd plus the bluetoothctl /
# btmon / btmgmt tools. It has no upstream static or portable release, and it
# links glib, dbus and readline, so this extension bundles the binaries with
# their shared-library closure (including glibc + the dynamic loader) extracted
# from a Debian container. tools/flix.sh resolves the closure and patchelf's the
# binaries onto a private loader/rpath, isolating them from the host's libraries
# (same approach as qemu.sysext and tilde.sysext). Debian packages BlueZ, so the
# version parameter selects the Debian suite (stable/testing) that ships it.
#
# The kernel side is not part of this extension: Bluetooth modules have to match
# the running kernel exactly, so they belong in the Flatcar image rather than in
# a sysext. Flatcar images built after flatcar/scripts#4197 carry them.

RELOAD_SERVICES_ON_MERGE="true"

DEBIAN_BLUEZ_API="https://sources.debian.org/api/src/bluez/"

# A transient network hiccup should not fail a build; same retry policy the
# other recipes in this repo use (consul, haproxy, nomad, vault).
CURL_RETRY=(--retry-delay 1 --retry 60 --retry-connrefused
--retry-max-time 60 --connect-timeout 20)

# Resolve a Debian suite alias (stable/testing) to its codename from the archive
# Release file, e.g. stable -> trixie. The sources API tags versions by codename
# only, so we need this mapping. Fails if the lookup can't be made.
function _debian_codename() {
local alias="$1" codename
codename="$(curl -fsSL "${CURL_RETRY[@]}" "https://deb.debian.org/debian/dists/${alias}/Release" 2>/dev/null \
| sed -nE 's/^Codename:[[:space:]]*([^[:space:]]+).*/\1/p')" || return 1
[[ -n "${codename}" ]] || return 1
printf '%s\n' "${codename}"
}
# --

# Read a Debian sources API response on stdin and print the x.y BlueZ versions
# shipped by the given suite codenames (args), newest first.
function _bluez_versions_in() {
jq -r --args '
($ARGS.positional) as $cn
| .versions[]?
| select((.suites // []) | any(. as $s | $cn | index($s)))
| .version' "$@" \
| sed -nE 's/^([0-9]+\.[0-9]+).*/\1/p' \
| sort -Vru
}
# --

# Return 0 if the given Debian codename ships the requested x.y BlueZ version.
function _bluez_version_in() {
local api="$1" ver="$2" codename="$3"
echo "${api}" | jq -e --arg v "${ver}" --arg c "${codename}" '
.versions[]?
| select((.suites // []) | index($c))
| select(.version | test("^" + ($v | gsub("\\.";"\\.")) + "([-.+~]|$)"))
' >/dev/null
}
# --

function list_available_versions() {
local api stable testing
stable="$(_debian_codename stable)" && testing="$(_debian_codename testing)" || {
echo "ERROR: failed to resolve Debian stable/testing codenames." >&2
return 1
}
api="$(curl -fsSL "${CURL_RETRY[@]}" "${DEBIAN_BLUEZ_API}")" || {
echo "ERROR: failed to query the Debian sources API (${DEBIAN_BLUEZ_API})." >&2
return 1
}
echo "${api}" | _bluez_versions_in "${stable}" "${testing}"
}
# --

function populate_sysext_root() {
local sysextroot="$1"
local arch="$2"
local version="$3"

local img_arch
img_arch="$(arch_transform 'x86-64' 'amd64' "${arch}")"
img_arch="$(arch_transform 'arm64' 'arm64' "${img_arch}")"

# Map the stable/testing aliases to codenames (the sources API only tags by
# codename) and fetch the version list ONCE. Fail hard on any lookup/parse
# error so a transient failure can't silently change which suite (and thus
# which BlueZ version) we build against.
local stable testing api
stable="$(_debian_codename stable)" && testing="$(_debian_codename testing)" || {
echo "ERROR: failed to resolve Debian stable/testing codenames." >&2
return 1
}
api="$(curl -fsSL "${CURL_RETRY[@]}" "${DEBIAN_BLUEZ_API}")" || {
echo "ERROR: failed to query the Debian sources API (${DEBIAN_BLUEZ_API})." >&2
return 1
}
echo "${api}" | jq -e '.versions' >/dev/null 2>&1 || {
echo "ERROR: unexpected response from the Debian sources API." >&2
return 1
}

# 'latest' resolves to the newest version in stable/testing so the checks below
# have a concrete target.
if [[ "${version}" == "latest" ]]; then
version="$(echo "${api}" | _bluez_versions_in "${stable}" "${testing}" | head -1)"
[[ -n "${version}" ]] || {
echo "ERROR: could not determine the latest bluez version from Debian." >&2
return 1
}
fi

# Choose the suite that actually ships the requested version: prefer stable,
# else testing, else fail. Never fall back blindly.
local suite=""
if _bluez_version_in "${api}" "${version}" "${stable}"; then
suite="stable"
elif _bluez_version_in "${api}" "${version}" "${testing}"; then
suite="testing"
else
echo "ERROR: bluez ${version} is not in Debian stable (${stable}) or testing (${testing})." >&2
return 1
fi

announce "Building bluez ${version} (Debian ${suite}) for ${arch}"

# Build the sysext inside a Debian container of the chosen suite: install
# BlueZ, verify bluetoothd actually matches the requested version (a moved
# suite must not silently produce a different build), then hand the binaries
# and the D-Bus policy to tools/flix.sh, which resolves the library closure
# and patchelf's them onto a private loader/rpath.
docker run --rm -i \
-v "${scriptroot}/tools/":/tools \
-v "${sysextroot}":/install_root \
--platform "linux/${img_arch}" \
--pull always \
--network host \
-e WANT_VERSION="${version}" \
"docker.io/debian:${suite}-slim" bash -euc '
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
apt-get install -y -qq --no-install-recommends bluez patchelf >/dev/null

# bluetoothd moved from /usr/lib to /usr/libexec across Debian releases.
bluetoothd=""
for c in /usr/libexec/bluetooth/bluetoothd /usr/lib/bluetooth/bluetoothd; do
[ -x "${c}" ] && bluetoothd="${c}" && break
done
if [ -z "${bluetoothd}" ]; then
echo "ERROR: bluetoothd not found in the bluez package." >&2
exit 1
fi

got="$("${bluetoothd}" --version | sed -nE "s/^([0-9]+\.[0-9]+).*/\1/p" | head -1)"
if [ "${got}" != "${WANT_VERSION}" ]; then
echo "ERROR: requested bluez ${WANT_VERSION} but Debian installs ${got}." >&2
exit 1
fi

# bluetoothd plus every CLI tool this suite's bluez package ships. Take
# the list from the package rather than hardcoding it: the tool set drifts
# across Debian releases (the legacy hciconfig/hcitool/... tools are
# deprecated upstream and already gone from newer ones), and a hardcoded
# subset silently drops tools that are still shipped -- hciattach in
# particular, which is what attaches a UART controller.
paths="${bluetoothd}"
for b in $(dpkg -L bluez | sed -nE 's|^(/usr/s?bin/[^/]+)$|\1|p' | sort -u); do
[ -f "${b}" ] && [ -x "${b}" ] && paths="${paths} ${b}"
done

# bluetoothd will not take its name on the system bus without this policy.
dbus_conf=""
for c in /usr/share/dbus-1/system.d/bluetooth.conf \
/etc/dbus-1/system.d/bluetooth.conf; do
[ -f "${c}" ] && dbus_conf="${c}" && break
done
if [ -z "${dbus_conf}" ]; then
echo "ERROR: the bluez D-Bus policy file was not found." >&2
exit 1
fi
# flix.sh only accepts the "source:target" form for paths outside /usr;
# a path that is already under /usr must be passed as-is.
case "${dbus_conf}" in
/usr/*) paths="${paths} ${dbus_conf}" ;;
*) paths="${paths} ${dbus_conf}:/usr/share/dbus-1/system.d/bluetooth.conf" ;;
esac

cd /install_root
/tools/flix.sh / bluez ${paths}

# bluetoothd is looked up at a fixed path by our unit; normalise the
# location so the unit does not have to care which Debian release we used.
if [ "${bluetoothd}" != "/usr/libexec/bluetooth/bluetoothd" ]; then
mkdir -p bluez/usr/libexec/bluetooth
mv "bluez${bluetoothd}" bluez/usr/libexec/bluetooth/bluetoothd
fi

owner="$(stat -c "%u:%g" /install_root)"
if [ "${owner}" != "$(id -u):$(id -g)" ]; then
chown -R "${owner}" /install_root/bluez
fi
'

# Merge rather than move: the bakery has already copied this extension's
# static files/ tree into ${sysextroot}/usr before calling us.
mkdir -p "${sysextroot}/usr"
cp -a "${sysextroot}/bluez/usr/." "${sysextroot}/usr/"
rm -rf "${sysextroot}/bluez"
}
# --
50 changes: 50 additions & 0 deletions bluez.sysext/files/usr/lib/systemd/system/bluetooth.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
[Unit]
Description=Bluetooth service
Documentation=man:bluetoothd(8)
# Only present once a Bluetooth controller has been detected and its driver
# (btusb, hci_uart, ...) has loaded. Without an adapter the unit is skipped
# rather than failed, so the Upholds= drop-in does not retry in a loop. A
# controller that shows up later is caught by the udev rule this sysext ships,
# since a Condition= is only evaluated when the unit is queued.
ConditionPathIsDirectory=/sys/class/bluetooth
Comment thread
ananthb marked this conversation as resolved.
After=dbus.service
Requires=dbus.service

[Service]
Type=dbus
BusName=org.bluez
# The sysext drops a new D-Bus policy into /usr/share/dbus-1/system.d, which a
# running dbus-daemon has not read yet. Without this reload bluetoothd cannot
# take its name on the system bus on first merge. busctl ships with systemd, so
# it is always present on Flatcar.
ExecStartPre=-/usr/bin/busctl call org.freedesktop.DBus /org/freedesktop/DBus org.freedesktop.DBus ReloadConfig
ExecStart=/usr/libexec/bluetooth/bluetoothd
NotifyAccess=main
LimitNPROC=1
Restart=on-failure
RestartSec=2

# Adapter settings and pairing keys. Replaces a tmpfiles.d rule: systemd creates
# the directory before the daemon starts, and ProtectSystem=strict below needs
# it declared here to leave it writable.
StateDirectory=bluetooth
StateDirectoryMode=0700
Comment on lines +27 to +31

# Sandboxing, as shipped by upstream BlueZ in src/bluetooth.service.in.
CapabilityBoundingSet=CAP_NET_RAW CAP_NET_ADMIN CAP_NET_BIND_SERVICE
NoNewPrivileges=true
ProtectHome=true
ProtectSystem=strict
PrivateTmp=true
ProtectKernelTunables=true
ProtectControlGroups=true
MemoryDenyWriteExecute=true
RestrictRealtime=true

[Install]
# Upstream BlueZ uses bluetooth.target here. systemd does ship that target, but
# nothing pulls it in by default, so 'systemctl enable bluetooth.service' would
# leave the service unstarted. multi-user.target matches what the other sysexts
# in this repo do and makes enabling behave as expected.
WantedBy=multi-user.target
Alias=dbus-org.bluez.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[Unit]
Upholds=bluetooth.service
8 changes: 8 additions & 0 deletions bluez.sysext/files/usr/lib/udev/rules.d/99-bluez.rules
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# A controller can appear long after multi-user.target has been reached: a USB
# dongle gets hot-plugged, or btusb/hci_uart loads late. bluetooth.service is
# guarded by ConditionPathIsDirectory=/sys/class/bluetooth, and a Condition= is
# only evaluated when the unit is queued -- nothing re-queues it when the
# directory finally shows up, so the daemon would stay stopped indefinitely.
# Pulling the unit in from the device event closes that gap; starting an
# already-running service is a no-op, so this is safe to fire per controller.
ACTION=="add", SUBSYSTEM=="bluetooth", TAG+="systemd", ENV{SYSTEMD_WANTS}+="bluetooth.service"
Empty file added bluez.sysext/test.sh
Empty file.
105 changes: 105 additions & 0 deletions docs/bluez.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# BlueZ sysext

This extension ships [BlueZ](http://www.bluez.org/), the Linux Bluetooth userspace: the `bluetoothd` daemon plus the `bluetoothctl`, `btmon`, `btmgmt` and `btattach` tools.

BlueZ has no upstream static or portable release and it links glib, dbus and readline, so this extension bundles the binaries together with their shared-library closure (including glibc and the dynamic loader) extracted from a Debian container, the same way the `qemu` and `tilde` extensions do. Because of that the version numbers you can build are the BlueZ versions Debian stable/testing ship, not arbitrary upstream releases.

## Kernel requirements

The extension ships **userspace only**. Bluetooth kernel modules have to match the running kernel exactly, so they belong in the Flatcar image rather than in a sysext.

Flatcar images built before [flatcar/scripts#4197](https://github.com/flatcar/scripts/pull/4197) contain no Bluetooth support at all — `CONFIG_BT` was never enabled — so `bluetoothd` has nothing to talk to. On such an image the `bluetooth.service` unit shipped here is skipped rather than failed, because it is guarded by `ConditionPathIsDirectory=/sys/class/bluetooth`.

On an image that does have the modules, the driver for your controller (`btusb` for USB adapters, `hci_uart` for UART-attached ones) is autoloaded when the device is detected, `/sys/class/bluetooth` appears, and `bluetooth.service` starts.

## Usage

Download and merge the sysext at provisioning time using the below butane snippet.

The snippet includes automated updates via systemd-sysupdate.
Sysupdate will stage updates and request a reboot by creating a flag file at `/run/reboot-required`.
You can deactivate updates by changing `enabled: true` to `enabled: false` in `systemd-sysupdate.timer`.

Note that the snippet is for the x86-64 version of BlueZ 5.82. Other architectures are also available.

Check out the metadata release at https://github.com/flatcar/sysext-bakery/releases/tag/bluez for a list of all versions available in the bakery.

```yaml
variant: flatcar
version: 1.0.0

storage:
files:
- path: /opt/extensions/bluez/bluez-5.82-x86-64.raw
mode: 0644
contents:
source: https://extensions.flatcar.org/extensions/bluez-5.82-x86-64.raw
- path: /etc/sysupdate.bluez.d/bluez.conf
contents:
source: https://extensions.flatcar.org/extensions/bluez.conf
- path: /etc/sysupdate.d/noop.conf
contents:
source: https://extensions.flatcar.org/extensions/noop.conf
links:
- target: /opt/extensions/bluez/bluez-5.82-x86-64.raw
path: /etc/extensions/bluez.raw
hard: false
systemd:
units:
- name: systemd-sysupdate.timer
enabled: true
- name: systemd-sysupdate.service
dropins:
- name: bluez.conf
contents: |
[Service]
ExecStartPre=/usr/bin/sh -c "readlink --canonicalize /etc/extensions/bluez.raw > /tmp/bluez"
ExecStartPre=/usr/lib/systemd/systemd-sysupdate -C bluez update
ExecStartPost=/usr/bin/sh -c "readlink --canonicalize /etc/extensions/bluez.raw > /tmp/bluez-new"
ExecStartPost=/usr/bin/sh -c "if ! cmp --silent /tmp/bluez /tmp/bluez-new; then touch /run/reboot-required; fi"
```

## What the extension starts

`bluetooth.service` is upheld by `multi-user.target`, so it starts on merge and is restarted if it dies. It runs `bluetoothd` as a D-Bus service under the `org.bluez` name.

The extension drops a D-Bus policy file into `/usr/share/dbus-1/system.d/`, which a *running* `dbus-daemon` has not read yet. The unit therefore asks D-Bus to reload its configuration before starting `bluetoothd`; without that, `bluetoothd` cannot take its name on the system bus until the next reboot.

Adapter settings and pairing keys are stored under `/var/lib/bluetooth`, created by the unit's `StateDirectory=`.

The unit is guarded by `ConditionPathIsDirectory=/sys/class/bluetooth`, so on a host without a controller it is skipped rather than restarted in a loop. A controller that appears later — a hot-plugged USB dongle, or `btusb` loading after boot — is picked up by the udev rule the extension ships, which pulls the unit in from the device event.

## Configuration

`bluetoothd` reads `/etc/bluetooth/main.conf` if present and runs with built-in defaults otherwise. To change settings, drop your own file there via Ignition, for example to power adapters on automatically:

```yaml
storage:
files:
- path: /etc/bluetooth/main.conf
mode: 0644
contents:
inline: |
[Policy]
AutoEnable=true
```

## Checking it works

```sh
# the controller shows up once the kernel driver has bound it
bluetoothctl list

# live HCI trace, useful when a controller misbehaves
btmon
```

If `bluetoothctl list` prints nothing, check that a driver actually bound the controller:

```sh
systemctl status bluetooth
ls /sys/class/bluetooth
journalctl -u bluetooth
```

An empty `/sys/class/bluetooth` means no Bluetooth driver is loaded — either the hardware is absent, or the image predates the kernel change linked above.
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ Check out documentation on specific extensions at the navigation menu on the lef
| Extension | Availability | Versions available |
| ---------------- | ------------ | ------------- |
| `bird` | released | [bird versions](https://github.com/flatcar/sysext-bakery/releases/tag/bird) |
| `bluez` | released | [bluez versions](https://github.com/flatcar/sysext-bakery/releases/tag/bluez) |
| `chrony` | released | [chrony versions](https://github.com/flatcar/sysext-bakery/releases/tag/chrony) |
| `cilium` | released | [cilium versions](https://github.com/flatcar/sysext-bakery/releases/tag/cilium) |
| `cloud-hypervisor` | released | [cloud-hypervisor versions](https://github.com/flatcar/sysext-bakery/releases/tag/cloud-hypervisor) |
Expand Down
3 changes: 3 additions & 0 deletions release_build_versions.txt
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,9 @@ haproxy latest
bird 3.1.2
bird latest

bluez 5.82
bluez latest

btop 1.4.0
btop latest

Expand Down