Skip to content
Merged
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
9 changes: 8 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:

- uses: actions/setup-go@v5
with:
go-version: '1.23'
go-version-file: agent/go.mod
cache-dependency-path: agent/go.sum

- name: Regenerate the API client
Expand Down Expand Up @@ -54,3 +54,10 @@ jobs:
- name: Test
working-directory: agent
run: go test -race ./...

- name: Shell syntax
working-directory: agent
run: |
for f in setup.sh uninstall.sh packaging/*.sh packaging/lib/*.sh; do
bash -n "$f"
done
97 changes: 97 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
name: Release

# Push a tag to publish a release: `git tag 0.3.0 && git push origin 0.3.0`.
# The tag is the version (a leading "v" is dropped), so it must be
# MAJOR.MINOR.PATCH: agent packages refuse anything else.
#
# Needs, in the repository settings:
# - variable MB_RELEASE_PUBLIC_KEYS: comma-separated base64 release public
# keys (from `mbpack keygen`), compiled into the agent and mbpack
# - secret MB_RELEASE_SIGNING_KEY: the base64 seed of one of those keys,
# used to sign the agent packages
# docs/architecture.md sections 4 and 5; README.md, "Release signing".

on:
push:
tags: ['*']

permissions:
contents: write

jobs:
release:
runs-on: ubuntu-latest
env:
RELEASE_KEYS: ${{ vars.MB_RELEASE_PUBLIC_KEYS }}
MB_RELEASE_SIGNING_KEY: ${{ secrets.MB_RELEASE_SIGNING_KEY }}
steps:
- uses: actions/checkout@v4

- uses: actions/setup-go@v5
with:
go-version-file: go.mod

- name: Version from the tag
run: |
tag="${GITHUB_REF_NAME}"
version="${tag#v}"
if ! [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error::Tag '$tag' is not MAJOR.MINOR.PATCH (optionally with a leading v)."
exit 1
fi
echo "VERSION=$version" >> "$GITHUB_ENV"
echo "RELEASE_TAG=$tag" >> "$GITHUB_ENV"

# A release without a compiled-in key could never update itself, and an
# unsigned package is refused by every board. Fail before building.
- name: Check signing configuration
run: |
if [[ -z "$RELEASE_KEYS" ]]; then
echo "::error::Repository variable MB_RELEASE_PUBLIC_KEYS is not set."
exit 1
fi
if [[ -z "$MB_RELEASE_SIGNING_KEY" ]]; then
echo "::error::Repository secret MB_RELEASE_SIGNING_KEY is not set."
exit 1
fi

- name: Test
run: go test ./...

- name: Build release tarballs
run: |
make package
make package-amd64

- name: Build and sign agent packages
run: |
make package-mbu
make package-mbu-amd64

# mbpack was built with RELEASE_KEYS, so verify checks the packages
# against exactly the keys the new agent trusts.
- name: Verify agent packages
run: |
for f in build/musallahboard-agent-"$VERSION"-*.mbu; do
build/mbpack verify "$f"
done

- name: Build mbpush
run: make mbpush

- name: Publish
uses: softprops/action-gh-release@v2
with:
fail_on_unmatched_files: true
files: |
build/musallahboard-agent-${{ env.VERSION }}-arm64.tar.gz
build/musallahboard-agent-${{ env.VERSION }}-amd64.tar.gz
build/musallahboard-agent-${{ env.VERSION }}-arm64.mbu
build/musallahboard-agent-${{ env.VERSION }}-amd64.mbu
build/agent-channel-arm64.json
build/agent-channel-amd64.json
build/musallahboard-agent-arm64
build/musallahboard-agent-amd64
build/mbpush-windows-amd64.exe
build/mbpush-darwin-arm64
build/mbpush-linux-amd64
67 changes: 58 additions & 9 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
VERSION ?= 0.1.0
# The GitHub release tag the packages are published under. Usually VERSION;
# CI sets it separately when the tag has a prefix.
RELEASE_TAG ?= $(VERSION)
# Release public keys compiled into the agent (docs/architecture.md 5):
# comma-separated base64, from `mbpack keygen`. Empty is a dev build, which
# accepts only release keys listed in the board's trust.json. CI passes the
# repository variable MB_RELEASE_PUBLIC_KEYS.
RELEASE_KEYS ?=
# Must match the module path in go.mod exactly, case included: the linker
# silently ignores -X for a symbol it cannot resolve, so a wrong-case path
# here ships a binary that reports "dev" forever.
# here ships a binary that reports "dev" forever (or trusts no release key).
PKG := github.com/LensBridge/agent
LDFLAGS := -s -w -X $(PKG)/internal/version.Version=$(VERSION)
LDFLAGS := -s -w -X $(PKG)/internal/version.Version=$(VERSION) \
-X $(PKG)/internal/trust.BuiltinReleaseKeys=$(RELEASE_KEYS)
# Where the release channel points boards at (docs/architecture.md 9.4).
RELEASE_DOWNLOAD := https://github.com/LensBridge/agent/releases/download/$(RELEASE_TAG)

.PHONY: build build-arm64 build-amd64 tidy test clean \
deploy install-remote install-remote-x86 package package-amd64 \
mbpush mbpush-windows mbpush-macos mbpush-linux
mbpush mbpush-windows mbpush-macos mbpush-linux \
mbpack package-mbu package-mbu-amd64

build:
CGO_ENABLED=0 go build -trimpath -ldflags "$(LDFLAGS)" -o build/musallahboard-agent ./cmd/agent
Expand All @@ -20,8 +32,9 @@ build-amd64:
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -trimpath -ldflags "$(LDFLAGS)" -o build/musallahboard-agent-amd64 ./cmd/agent

# mbpush: the admin-laptop side of offline mode (docs/offline.md). Pure Go,
# shells out to the system ssh/scp, so a plain cross-compile is enough.
# mbpush: the laptop tool for the service port (docs/architecture.md 9.5).
# Pure Go (it only shells out to the system ssh for --ssh), so a plain
# cross-compile is enough.
mbpush: mbpush-windows mbpush-macos mbpush-linux

mbpush-windows:
Expand All @@ -36,6 +49,35 @@ mbpush-linux:
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -trimpath -ldflags "$(LDFLAGS)" -o build/mbpush-linux-amd64 ./cmd/mbpush

# mbpack: builds, signs, verifies and inspects .mbu packages (section 4.4).
# Host build: it runs where the packages are made, not on a board. `verify`
# also trusts the RELEASE_KEYS compiled in here.
mbpack:
CGO_ENABLED=0 go build -trimpath -ldflags "$(LDFLAGS)" -o build/mbpack ./cmd/mbpack

# Signed agent packages plus the channel file boards poll (sections 4 and
# 9.4). The signing seed comes from the environment, never the command line:
# MB_RELEASE_SIGNING_KEY=<base64 seed> make package-mbu VERSION=0.3.0 RELEASE_KEYS=<b64>
# $(1) is the architecture.
define agent_mbu
@test -n "$$MB_RELEASE_SIGNING_KEY" || { echo "MB_RELEASE_SIGNING_KEY is not set (base64 Ed25519 seed from mbpack keygen)"; exit 1; }
build/mbpack agent --version $(VERSION) --arch $(1) --key-env MB_RELEASE_SIGNING_KEY \
build/musallahboard-agent-$(1) -o build/musallahboard-agent-$(VERSION)-$(1).mbu
@f=build/musallahboard-agent-$(VERSION)-$(1).mbu; \
sha=$$(sha256sum "$$f" | cut -d' ' -f1); \
bytes=$$(wc -c < "$$f" | tr -d ' '); \
printf '{"version":"%s","url":"%s","sha256":"%s","bytes":%s}\n' \
"$(VERSION)" "$(RELEASE_DOWNLOAD)/musallahboard-agent-$(VERSION)-$(1).mbu" "$$sha" "$$bytes" \
> build/agent-channel-$(1).json; \
echo "Wrote $$f and build/agent-channel-$(1).json"
endef

package-mbu: build-arm64 mbpack
$(call agent_mbu,arm64)

package-mbu-amd64: build-amd64 mbpack
$(call agent_mbu,amd64)

tidy:
go mod tidy

Expand Down Expand Up @@ -72,16 +114,20 @@ install-remote-x86: package-amd64

# Build a self-contained release tarball ready to ship to a Pi:
# musallahboard-agent-$(VERSION)-arm64.tar.gz
# containing the binary, install/uninstall/update scripts, the systemd unit,
# and the sudoers allow-list. On the Pi:
# containing the binary, install/uninstall/update scripts, the systemd units
# (agent, kiosk, self-updater, USB import), the udev rules and the sudoers
# allow-list. On the Pi:
# tar xzf musallahboard-agent-*.tar.gz
# sudo bash musallahboard-agent-*/packaging/install.sh --token=X --backend=Y
package: build-arm64
@rm -rf build/pkg && mkdir -p build/pkg/musallahboard-agent-$(VERSION)-arm64
@cp build/musallahboard-agent-arm64 \
build/pkg/musallahboard-agent-$(VERSION)-arm64/musallahboard-agent
@cp -r packaging build/pkg/musallahboard-agent-$(VERSION)-arm64/
@sed -i 's/\r//' build/pkg/musallahboard-agent-$(VERSION)-arm64/packaging/*.sh
@sed -i 's/\r//' build/pkg/musallahboard-agent-$(VERSION)-arm64/packaging/*.sh \
build/pkg/musallahboard-agent-$(VERSION)-arm64/packaging/*.service \
build/pkg/musallahboard-agent-$(VERSION)-arm64/packaging/*.path \
build/pkg/musallahboard-agent-$(VERSION)-arm64/packaging/udev/*.rules
@chmod +x build/pkg/musallahboard-agent-$(VERSION)-arm64/packaging/*.sh
# setup.sh is the whole point of the arm64 tarball: the one-line installer
# fetches this archive and runs it. Not shipped in the amd64 one — it is a
Expand All @@ -103,7 +149,10 @@ package-amd64: build-amd64
@cp build/musallahboard-agent-amd64 \
build/pkg/musallahboard-agent-$(VERSION)-amd64/musallahboard-agent
@cp -r packaging build/pkg/musallahboard-agent-$(VERSION)-amd64/
@sed -i 's/\r//' build/pkg/musallahboard-agent-$(VERSION)-amd64/packaging/*.sh
@sed -i 's/\r//' build/pkg/musallahboard-agent-$(VERSION)-amd64/packaging/*.sh \
build/pkg/musallahboard-agent-$(VERSION)-amd64/packaging/*.service \
build/pkg/musallahboard-agent-$(VERSION)-amd64/packaging/*.path \
build/pkg/musallahboard-agent-$(VERSION)-amd64/packaging/udev/*.rules
@chmod +x build/pkg/musallahboard-agent-$(VERSION)-amd64/packaging/*.sh
@cp README.md build/pkg/musallahboard-agent-$(VERSION)-amd64/ 2>/dev/null || true
@tar czf build/musallahboard-agent-$(VERSION)-amd64.tar.gz \
Expand Down
66 changes: 53 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ sudo musallahboard-agent enroll \
--token <one-time-token-from-admin-ui
```

The device will then be enrolled and the MusallahBoard frontend will load automatically. The device is now ready to be used as a kiosk!
The device will then be enrolled and the board will load automatically once its first content and board app packages have synced. The device is now ready to be used as a kiosk!

## Building

Expand All @@ -67,28 +67,68 @@ make build

# Cross-compile for Raspberry Pi (arm64)
make build-arm64

# Laptop tool (Windows, macOS, Linux) and the package signer
make mbpush
make mbpack
```

Requires Go 1.23+. Binaries land in `build/`.
Requires Go 1.24+. Binaries land in `build/`. A plain build is a dev build: it trusts no release key of its own (see Release signing below).

## How a board gets updates (v2)

Every board renders from its own disk: the kiosk always loads the agent's local server at `http://127.0.0.1:8080/`. Content (payloads and posters), the board app and the agent itself all arrive as signed `.mbu` packages, which the board checks against keys it has pinned before installing anything. The network only changes how fresh the board is. [docs/architecture.md](docs/architecture.md) is the full design and the contract with the backend, the frontend and the Android app.

### Online boards

Nothing to do. An enrolled board syncs its content from LensBridge every 30 minutes (and straight after an admin changes something), and follows the release channels on GitHub for new board app and agent versions. A new agent is installed by a small root updater that checks it again and rolls it back automatically if it does not start. If the internet drops, the board keeps showing the 7 days of content it already has.

## Offline mode
### Boards without internet

A board with no internet connection can run in offline mode: the agent serves the board app and a content bundle (14 days of payloads and posters, exported from LensBridge) to the kiosk from `127.0.0.1:8080`, and makes no network calls. An admin refreshes the content every week or two by plugging a laptop into the board's ethernet port and running `mbpush`, which also corrects the board's clock.
Set the board up and enroll it online as usual, then, while it still has internet:

```bash
# On the board, once, after a normal setup and enrollment, while still online:
bash setup.sh --offline --board-dist=<frontend dist dir or .tar.gz> [--rtc]
bash setup.sh --service-port [--rtc] # --rtc if a DS3231 clock module is fitted
```

That turns the ethernet port into a service port: a laptop or phone plugged straight into it gets an address from the board. Turn it off before plugging the board into a real network: `sudo musallahboard-agent service-port off`.

To update the board, get the packages first, while you have internet:

- content: LensBridge admin, Devices, the board, Download offline bundle (`musallahboard-content-<id>-<date>.mbu`, 14 days)
- board app and agent: `mbpush fetch` (saves the latest `musallahboard-app-*.mbu` and `musallahboard-agent-*-arm64.mbu`)

Then use any of:

# On the laptop (build with: make mbpush):
mbpush musallahboard-3f2a1b4c-2026-09-24.zip # set clock, install bundle, show status
mbpush status # check only
1. **USB stick.** Copy the `.mbu` files to the stick (its root, or a `MusallahBoard/` folder) and plug it into the board. The screen shows progress; remove the stick when it says "Update complete". One stick can carry content for several boards: each board takes only its own.
2. **Laptop.** Plug into the board's ethernet port and open `http://10.77.0.1/` in a browser, or run `mbpush <file.mbu>...`. `mbpush status` shows what is installed and how far the board's clock is off; an upload also sets the clock from the laptop's. For a board reachable only over SSH: `mbpush --ssh admin@host <file.mbu>...`.
3. **Android app.** Downloads the packages while it has signal, then uploads them over a USB ethernet adapter.

# On the board:
sudo musallahboard-agent status
sudo musallahboard-agent mode online # switch back; before joining a real network
On the board itself: `sudo musallahboard-agent import <file.mbu>...` and `sudo musallahboard-agent status`.

### Release signing

Software (board app and agent) is signed with a release key that the backend never holds; content is signed by the backend's own key. To set up release signing once:

```bash
make mbpack && build/mbpack keygen # prints a private seed, a public key and its key id
```

[docs/offline.md](docs/offline.md) is the full design and the contract between the backend exporter, this agent and the frontend, including how to convert an existing online board.
- The **private seed** goes in the repository secret `MB_RELEASE_SIGNING_KEY` (and the frontend repo's, for app packages). Keep an offline copy; never commit it.
- The **public key** goes in the repository variable `MB_RELEASE_PUBLIC_KEYS` (comma-separated, to allow a second key during a rotation). CI compiles it into the agent (`RELEASE_KEYS`), so every board trusts it without any configuration.

Pushing a tag (`0.3.0`) runs `.github/workflows/release.yml`: tests, builds, signs `musallahboard-agent-<version>-<arch>.mbu`, and publishes them with `agent-channel-<arch>.json`, the release tarballs and the `mbpush` binaries. Locally: `MB_RELEASE_SIGNING_KEY=<seed> make package-mbu VERSION=0.3.0 RELEASE_KEYS=<public key>`.

### Trust management

Keys live in `/etc/musallahboard/trust.json` (root-owned). Content keys are pinned at enrollment; `sudo musallahboard-agent trust fetch` re-reads them from the backend (for example after a key rotation). Release keys are compiled in and can be extended there.

```bash
sudo musallahboard-agent trust show
sudo musallahboard-agent trust add content|release <base64 public key>
sudo musallahboard-agent trust remove <key id>
sudo musallahboard-agent trust fetch
```

## Roadmap

Expand Down
Loading
Loading