Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
9cafbba
feat(boot): the media foundations — ISO9660, probing, catalogue, stanzas
wamxx Aug 27, 2026
6de435a
feat(boot): the media listener, its own socket, and the media commands
wamxx Aug 27, 2026
44409a2
feat(boot): a `boot` feature, so the smallest build stays the smallest
wamxx Aug 27, 2026
df2a6b9
docs: record what Phase 1 taught, and re-measure what had gone stale
wamxx Aug 27, 2026
fb06170
docs(guide): serving boot media, in both languages
wamxx Aug 27, 2026
7a7fe04
feat(boot): TFTP, the loader table, and dropping privileges after bin…
wamxx Aug 27, 2026
15c74d5
feat(boot): the menu, the bootstrap, and their DHCP server's two lines
wamxx Aug 27, 2026
16fa55c
build(ipxe): the branded loader build, its pin, and the CI job that r…
wamxx Aug 27, 2026
10aaddd
docs(guide): netbooting a machine, in both languages
wamxx Aug 27, 2026
0055aae
test(boot-rig): the rig, and the loader build it verified on the way in
wamxx Aug 27, 2026
368ab0b
feat(boot): adding a file to an ISO without writing 1.5 GB
wamxx Aug 27, 2026
5a02b5d
docs: preparing an image, and the traps phases 2 and 4 paid for
wamxx Aug 27, 2026
6430eb6
feat(boot): the server fetches its own images, and the directory is t…
wamxx Aug 27, 2026
268b27d
fix(boot-rig): build the client image, and install the tool it needs
wamxx Aug 27, 2026
e7214cf
fix(boot-rig): bake dnsmasq in, and notice when a service has died
wamxx Aug 27, 2026
c3d39c9
test(boot-rig): the chain runs end to end, and has been watched failing
wamxx Aug 27, 2026
c6e9a97
ci: run the boot chain on every push, and require it to go red
wamxx Aug 27, 2026
849faa6
feat(dsm): package the boot media, and say plainly why TFTP is not in it
wamxx Aug 27, 2026
791a1dc
test(dsm): the package runs on a real DSM 7.2.2 machine
wamxx Aug 27, 2026
aef7b2b
fix(boot): RESCRIPTUM_TFTP_ADDR=off, and name the folders the package…
wamxx Aug 27, 2026
9d9f6ce
feat(config): show the address the server derived, and what else the …
wamxx Aug 27, 2026
95ce6af
test(dsm): assert on the machine that the panel shows a real address
wamxx Aug 27, 2026
6d46223
docs(traps): a runtime default has to reach settings() as well
wamxx Aug 27, 2026
e3d0079
feat(boot): a TFTP port that cannot be bound is not a dead server
wamxx Aug 27, 2026
cb55ab7
fix(dsm): the package is the TFTP server again
wamxx Aug 27, 2026
d076595
docs: TFTP is ours on DSM too, and the four measurements that settle it
wamxx Aug 27, 2026
d788e31
build(release): ship the loaders, so a release is a boot server
wamxx Aug 27, 2026
0a23baa
test(boot): the probe's healthy path, which nothing reached
wamxx Aug 27, 2026
79be077
test(dsm): the machine answers the two questions the reversal rested on
wamxx Aug 27, 2026
cb106d0
docs(testing): the count was 333 and the boot suites were not in it
wamxx Aug 27, 2026
04b177e
feat(dsm): ship the loaders, so the package actually serves iPXE
wamxx Aug 27, 2026
52f61c8
docs(traps): root on DSM is gated on the signature, and DSM says so
wamxx Aug 27, 2026
193854c
docs(traps): Synology answers it — root needs *their* signature
wamxx Aug 27, 2026
1a48f89
docs(dsm): the procedure for testing the package on a real NAS
wamxx Aug 28, 2026
5f4d1c9
fix(dsm): a setting this version adds must reach a file that predates it
wamxx Aug 28, 2026
a8b6702
docs: the DS416j binds port 69, measured on the machine
wamxx Aug 28, 2026
7f3f868
feat(boot): offer the usual installer images, from the vendors' own i…
wamxx Aug 28, 2026
55f4be5
feat(dsm): manage images from the panel, catalogue included
wamxx Aug 28, 2026
ecd022c
feat(dsm): the Prepare button, which the tab was missing
wamxx Aug 28, 2026
8105eb7
feat(boot): let a deployment invert what an answer file means
wamxx Aug 28, 2026
b298b20
chore: a gitignored tmp/ for a deployment's own answer documents
wamxx Aug 28, 2026
70022e6
feat(installed): the machine says it is done, and stops being claimed
wamxx Aug 28, 2026
e83085b
feat(installed): every family can report in, not only Proxmox
wamxx Aug 28, 2026
0d878db
feat(dsm): generate the install-finished token, so nobody has to inve…
wamxx Aug 28, 2026
957129a
fix(tftp): report what a transfer did, not what it was about to do
wamxx Aug 28, 2026
4f78989
fix(tftp): the health probe must not log a failure of its own
wamxx Aug 28, 2026
dfe71fe
fix(tftp): never let a request arrive in silence
wamxx Aug 28, 2026
b0ec148
fix(tftp): do not blame a cap that did nothing
wamxx Aug 28, 2026
6432252
feat(tftp): pin the ports a transfer answers from, so a firewall can …
wamxx Aug 28, 2026
d033beb
fix(tftp): a connected data socket hides the packets you need to see
wamxx Aug 28, 2026
89bc913
fix(tftp): decline windowsize instead of agreeing to it and ignoring it
wamxx Aug 28, 2026
1323e15
fix(tftp): say which options the refused reply offered
wamxx Aug 28, 2026
601c926
fix(stanza): the real initrd must not be named, or it is not the init…
wamxx Aug 28, 2026
9a082e6
fix(patch): the mode file's keys are snake case, and a hyphen rejects it
wamxx Aug 28, 2026
85cbdb4
docs: a real machine installed itself, and the traps that stood in th…
wamxx Aug 28, 2026
bda8e91
docs: bring the counts and the record up to what today established
wamxx Aug 28, 2026
19e4475
feat(store)!: a directory per identity, not a file per answer
wamxx Aug 29, 2026
7a45f55
fix(build): clear clippy 1.98 and repair the no-default-features build
wamxx Aug 29, 2026
ff374a1
test(cli): stop `boot check` tests depending on who can bind port 69
wamxx Aug 29, 2026
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
17 changes: 17 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# What a Docker build context must not carry.
#
# Without this the boot rig's build context was **6 GB** — `target/` alone is most of
# it — and every `docker compose up` spent two minutes transferring build artefacts the
# image then rebuilds from scratch anyway. A rig nobody waits for is a rig nobody runs.
target/
_site/
node_modules/
.git/
plans/
packaging/dsm/vm/storage/
packaging/boot-rig/results/
packaging/boot-rig/.work/
packaging/ipxe/.work/
packaging/ipxe/out/
**/*.iso
**/*.spk
107 changes: 106 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,102 @@ jobs:
run: cargo test --all-features

# The smallest build has to keep working, or the NAS target rots unnoticed.
- name: Build without SQLite
# Both features off, and then each on its own: the combination that ships is not
# the only one that has to compile.
- name: Build without SQLite or boot media
run: cargo build --release --no-default-features

- name: Build with boot media but no SQLite
run: cargo build --release --no-default-features --features boot

loaders:
name: Branded iPXE loaders
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

# A C toolchain and the ARM64 cross binutils. iPXE's EFI targets need the
# architecture's own `ld` and `objcopy`; the BIOS ones build with the host's.
- name: Toolchain
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
build-essential liblzma-dev mtools xorriso isolinux gcc-aarch64-linux-gnu
aarch64-linux-gnu-ld --version | head -1

- name: Build the loaders
run: packaging/ipxe/build.sh --out "$GITHUB_WORKSPACE/loaders"

# **The chain of custody.** What the rig boots and what a release publishes have to
# be the same bytes, never a rebuild of them — a loader rebuilt after the run that
# proved it is a loader nobody has booted.
- name: What was built
run: |
ls -la "$GITHUB_WORKSPACE/loaders"
cat "$GITHUB_WORKSPACE/loaders/SHA256SUMS"

# Ask the server whether the directory satisfies the table it serves from. A
# snippet naming a loader that is not here fails silently at the ROM, and this is
# the only thing that catches it.
# `RESCRIPTUM_TFTP_ADDR=off` because the question here is whether the *directory*
# satisfies the table, not whether a listener is up: `boot check` also probes the
# TFTP port, and on a runner where port 69 is neither bound nor bindable that is a
# problem it would rightly report and wrongly fail this job over. The rig is what
# proves a loader actually gets handed over.
- name: Does the server agree the set is complete?
run: |
cargo build --release
RESCRIPTUM_BOOT_DIR="$GITHUB_WORKSPACE/loaders" \
RESCRIPTUM_TFTP_ADDR=off \
RESCRIPTUM_PUBLIC_HOST=192.0.2.10 \
./target/release/rescriptum boot check

- uses: actions/upload-artifact@v4
with:
name: ipxe-loaders
path: ${{ github.workspace }}/loaders
if-no-files-found: error

rig:
name: The boot chain, end to end
runs-on: ubuntu-latest
# **The tripwire, not the contract.** The dev rig is what decides; this re-proves the
# BIOS chain on every push without a single real machine. GitHub's runners are
# somebody else's machines with somebody else's limits, so it is sized so it cannot
# fail for capacity reasons — BIOS only, TCG, one claimed and one unclaimed client,
# no OS image, a few minutes. If it ever needs hardware virtualisation to pass, it
# has grown too big.
#
# Runners are x86_64, so this is native rather than the emulation a Mac does — and
# there is still no /dev/kvm, which is exactly the constraint the rig is built for.
timeout-minutes: 30
steps:
- uses: actions/checkout@v4

- name: Build the rig
run: docker build -f packaging/boot-rig/Dockerfile.rig -t rescriptum-rig:one .

# Four markers: the DHCP handoff answered from our own generated snippet, a loader
# fetched over TFTP, an unclaimed machine on its own disk, and a claimed machine at
# its own answer.
- name: Boot a claimed and an unclaimed machine
run: |
docker run --rm --cap-add NET_ADMIN --device /dev/net/tun \
-e UNCLAIMED_SECONDS=180 -e CLAIMED_SECONDS=180 \
rescriptum-rig:one bios

# **Watched failing, in CI as well as by hand.** A green rig that has never been
# red proves nothing, and the cheapest way to keep that true is to break one link
# on every run and require the break to show.
- name: And it must go red when a loader is missing
run: |
if docker run --rm --cap-add NET_ADMIN --device /dev/net/tun \
--entrypoint sh rescriptum-rig:one \
-c 'rm /srv/boot/ipxe-undionly.kpxe && exec /rig/rig-in-one.sh bios'; then
echo "the rig passed with a loader missing — it is not testing what it claims" >&2
exit 1
fi

docs:
name: Documentation
runs-on: ubuntu-latest
Expand Down Expand Up @@ -112,6 +205,9 @@ jobs:
cross:
name: Cross-compile for the NAS, and package it
runs-on: ubuntu-latest
# The packages carry the loaders now, and they come from the job that built them —
# so this waits rather than rebuilding iPXE a second time.
needs: loaders
# This is where a C dependency breaks first: SQLite is compiled from source, and
# armv7-musl is the least forgiving target we ship. Catching it here beats catching
# it while cutting a release. The DSM packages are assembled on the same job for the
Expand Down Expand Up @@ -175,9 +271,18 @@ jobs:
# Packaging breaks on the PR that breaks it, rather than at tag time. This is the
# cheap half of "does this package work"; the other half is installing it, which
# only a DSM machine can answer.
# The loaders the package ships, from the job that already built and verified them
# — never a rebuild here. What `check-spk.sh` and the lifecycle harness look at has
# to be the same bytes the `loaders` job proved satisfy the table.
- uses: actions/download-artifact@v4
with:
name: ipxe-loaders
path: packaging/ipxe/out

- name: Assemble the DSM packages
run: |
set -euo pipefail
ls packaging/ipxe/out
packaging/dsm/make-spk.sh armv7
packaging/dsm/make-spk.sh x86_64

Expand Down
78 changes: 76 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -130,9 +130,75 @@ jobs:
path: dist/*.tar.gz*
retention-days: 7

loaders:
name: Branded iPXE loaders
needs: verify
runs-on: ubuntu-latest
# **Without this the release is incomplete, and quietly so.** A deployment that
# installs the package gets a TFTP server with nothing to hand out: `boot check`
# reports three MISSING loaders and every machine the generated DHCP snippet sends
# here asks for a file, gets nothing, and stops. Until this job existed, "one binary
# boots an arbitrary machine" was true only for whoever built iPXE themselves.
#
# They are a separate download rather than part of any binary archive or `.spk`
# because they are iPXE and iPXE is GPLv2: separate files served alongside is mere
# aggregation, and packaging/ipxe/ is the written offer that goes with them.
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.tag || github.ref }}

# iPXE's EFI targets need the architecture's own `ld` and `objcopy`; the BIOS ones
# build with the host's, and they are 32-bit x86 — which is why this job is amd64
# and not arm.
- name: Toolchain
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
build-essential liblzma-dev mtools xorriso isolinux gcc-aarch64-linux-gnu
aarch64-linux-gnu-ld --version | head -1

- name: Build them
run: |
set -euo pipefail
VERSION="${{ needs.verify.outputs.version }}"
NAME="rescriptum-boot-assets-$VERSION"
packaging/ipxe/build.sh --out "$GITHUB_WORKSPACE/dist/$NAME"
cat "$GITHUB_WORKSPACE/dist/$NAME/SHA256SUMS"

# The same question CI asks of the same script: does the server agree the directory
# satisfies the table it serves from. A snippet naming a loader that is not here
# fails silently at the ROM, and this is the only thing that catches it before a
# release goes out. TFTP off because the question is the file set, not a listener.
- name: Does the server agree the set is complete?
run: |
set -euo pipefail
VERSION="${{ needs.verify.outputs.version }}"
cargo build --release
RESCRIPTUM_BOOT_DIR="$GITHUB_WORKSPACE/dist/rescriptum-boot-assets-$VERSION" \
RESCRIPTUM_TFTP_ADDR=off \
RESCRIPTUM_PUBLIC_HOST=192.0.2.10 \
./target/release/rescriptum boot check

- name: Pack
run: |
set -euo pipefail
VERSION="${{ needs.verify.outputs.version }}"
NAME="rescriptum-boot-assets-$VERSION"
tar -C dist -czf "dist/${NAME}.tar.gz" "$NAME"
( cd dist && shasum -a 256 "${NAME}.tar.gz" > "${NAME}.tar.gz.sha256" )
ls -l dist

- uses: actions/upload-artifact@v4
with:
name: boot-assets
path: dist/*.tar.gz*
if-no-files-found: error
retention-days: 7

package-dsm:
name: Synology packages
needs: [verify, build]
needs: [verify, build, loaders]
runs-on: ubuntu-latest
# Nothing is compiled here: the binaries are already built and statically linked, and
# an .spk is a release format — the same artifact, wrapped for one platform's package
Expand All @@ -156,6 +222,14 @@ jobs:
VERSION="${{ needs.verify.outputs.version }}"
SPK_BUILD="${{ inputs.spk_build || '1' }}"
mkdir -p bins
# **The loaders go inside the package.** A TFTP server with nothing to hand out
# boots nothing, and telling a NAS owner to find a second download is how a
# working appliance becomes a support thread. They are iPXE, GPLv2, separate
# files never linked into our binary — mere aggregation, and the NOTICE beside
# them is the written offer that travels with them.
tar -xzf "artifacts/boot-assets/rescriptum-boot-assets-$VERSION.tar.gz" -C .
export RESCRIPTUM_LOADERS="$PWD/rescriptum-boot-assets-$VERSION"
ls "$RESCRIPTUM_LOADERS"
# One .spk per *build*; the arch line inside each covers the platforms that
# build serves. aarch64 (arch="armv8") joins this list once the binary has been
# run on one of its platforms — make-spk.sh already knows the mapping.
Expand Down Expand Up @@ -184,7 +258,7 @@ jobs:

publish:
name: Publish the release
needs: [verify, build, package-dsm]
needs: [verify, build, loaders, package-dsm]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand Down
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ node_modules/

# ── Build output / generated ─────────────────────────────────
dist/
# Scratch for a deployment's own files — answer documents carry credentials and must
# never reach a public repository.
tmp/
# The built loaders. **No binaries in git, ever** — packaging/ipxe/ is the written
# offer, and build.sh reproduces them from the pinned commit.
packaging/ipxe/out/
packaging/ipxe/.work/
build/
.astro/
.output/
Expand Down
Loading
Loading