mirror of
https://github.com/jmcorgan/fips.git
synced 2026-10-05 19:18:25 +00:00
Merge the maintenance line up
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
# Keep the build context small.
|
||||
#
|
||||
# Several test harnesses build images with the repository root as the context
|
||||
# (testing/deb-install/test.sh and testing/dns-resolver/test.sh among them), and
|
||||
# without this every one of them uploads the whole Cargo target directory to the
|
||||
# daemon before running a build that does not use a single file from it. On a
|
||||
# developer's machine that directory reaches double-digit gigabytes.
|
||||
#
|
||||
# Deliberately narrow. Nothing here excludes testing/**/.cache, which
|
||||
# testing/deb-install/test.sh copies a package out of, and no Dockerfile in the
|
||||
# tree copies from target/ on the host: examples/k8s-sidecar/Dockerfile builds
|
||||
# its own inside the image with a multi-stage COPY --from.
|
||||
target/
|
||||
.git/
|
||||
deploy/
|
||||
+112
-54
@@ -27,7 +27,8 @@ env:
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# CI parity invariant
|
||||
#
|
||||
# This GitHub integration matrix and the local default suite set
|
||||
# This workflow's integration matrices — the `integration:` job and the
|
||||
# `deb-install:` job — and the local default suite set
|
||||
# (testing/ci-local.sh) MUST run the same integration suites, EXCEPT for the
|
||||
# deliberate local-only entries below. Adding a suite to one runner without
|
||||
# the other means "local green" and "GitHub green" stop being equivalent.
|
||||
@@ -504,29 +505,6 @@ jobs:
|
||||
# recovers from delay, and never panics.
|
||||
- suite: stun-faults
|
||||
type: stun-faults
|
||||
# ── Real-deb install across target distros ─────────────────────
|
||||
# Boots a privileged systemd container per distro, runs
|
||||
# `apt install ./fips_*.deb` with the locally-built package,
|
||||
# then asserts end-to-end `.fips` resolution + the
|
||||
# gateway/daemon default-pairing. The most thorough single
|
||||
# test surface — exercises packaging, maintainer scripts,
|
||||
# systemd unit ordering, real TUN, and the DNS responder
|
||||
# filter on a per-distro resolver backend.
|
||||
- suite: deb-install-debian12
|
||||
type: deb-install
|
||||
scenario: debian12
|
||||
- suite: deb-install-debian13
|
||||
type: deb-install
|
||||
scenario: debian13
|
||||
- suite: deb-install-ubuntu22
|
||||
type: deb-install
|
||||
scenario: ubuntu22
|
||||
- suite: deb-install-ubuntu24
|
||||
type: deb-install
|
||||
scenario: ubuntu24
|
||||
- suite: deb-install-ubuntu26
|
||||
type: deb-install
|
||||
scenario: ubuntu26
|
||||
# ── DNS resolver multi-backend coverage ────────────────────────
|
||||
# Exercises every fips-dns-setup backend (resolved, dnsmasq,
|
||||
# NM+dnsmasq, dns-delegate, no-resolver) across five distros,
|
||||
@@ -734,36 +712,6 @@ jobs:
|
||||
docker compose -f testing/static/docker-compose.yml \
|
||||
--profile gateway down --volumes --remove-orphans
|
||||
|
||||
# ── Real-deb install integration ────────────────────────────────────
|
||||
# The deb-install harness builds its own .deb from source in a
|
||||
# cargo-deb builder image; the pre-built Linux binary from the
|
||||
# build job is intentionally not used here so the test exercises
|
||||
# the full packaging pipeline. ~5-7 min cold-cache on a fresh
|
||||
# runner (.deb build dominates), ~1-2 min warm-cache.
|
||||
- name: Run deb-install scenario
|
||||
if: matrix.type == 'deb-install'
|
||||
timeout-minutes: 25
|
||||
run: bash testing/deb-install/test.sh ${{ matrix.scenario }}
|
||||
|
||||
- name: Collect logs on failure (deb-install)
|
||||
if: matrix.type == 'deb-install' && failure()
|
||||
run: |
|
||||
docker ps -a --filter "name=fips-deb-test-${{ matrix.scenario }}" --format '{{.Names}}' | while read -r c; do
|
||||
echo "--- ${c} fips.service ---"
|
||||
docker exec "$c" journalctl -u fips.service --no-pager 2>&1 | tail -100 || true
|
||||
echo "--- ${c} fips-dns.service ---"
|
||||
docker exec "$c" journalctl -u fips-dns.service --no-pager 2>&1 | tail -100 || true
|
||||
echo "--- ${c} fips-gateway.service ---"
|
||||
docker exec "$c" journalctl -u fips-gateway.service --no-pager 2>&1 | tail -100 || true
|
||||
done
|
||||
|
||||
- name: Stop containers (deb-install)
|
||||
if: matrix.type == 'deb-install' && always()
|
||||
run: |
|
||||
docker ps -a --filter "name=fips-deb-test-${{ matrix.scenario }}" --format '{{.Names}}' | while read -r c; do
|
||||
docker rm -f "$c" >/dev/null 2>&1 || true
|
||||
done
|
||||
|
||||
# ── Native datagram API ─────────────────────────────────────────────
|
||||
# Reads FIPS_TEST_IMAGE rather than defaulting to a name, so it runs
|
||||
# against the image this workflow built. The two-node check creates and
|
||||
@@ -817,3 +765,113 @@ jobs:
|
||||
docker ps -a --filter "name=fips-dns-test-" --format '{{.Names}}' | while read -r c; do
|
||||
docker rm -f "$c" >/dev/null 2>&1 || true
|
||||
done
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Job 4 – The .deb the install suite installs
|
||||
#
|
||||
# Built once, here, by the same script the release workflow and a local run
|
||||
# call, so the package the suite installs is built the way the shipped one is.
|
||||
# That was not true before: each install leg built its own package on a fresh
|
||||
# runner with no cache, so one run performed five complete Rust release builds
|
||||
# and four were waste — and none of them was built the way the release is, so
|
||||
# the suite could not exhibit a defect that only the release environment
|
||||
# produced.
|
||||
#
|
||||
# The script builds in the pinned container from packaging/build-floor.env and
|
||||
# runs testing/check-glibc-floor.sh on the result, so this job is also where a
|
||||
# floor violation stops the run.
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
deb-package:
|
||||
name: Build .deb
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build, test]
|
||||
if: ${{ !inputs.skip_integration }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
|
||||
|
||||
- name: Build the .deb in the pinned build container
|
||||
timeout-minutes: 30
|
||||
run: bash packaging/debian/build-deb-container.sh --output-dir deploy
|
||||
|
||||
- name: Upload the .deb
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: fips-deb
|
||||
path: deploy/fips_*.deb
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Job 5 – Real-deb install across target distros
|
||||
#
|
||||
# Boots a privileged systemd container per distro, runs `apt install
|
||||
# ./fips_*.deb` with the package job 4 built, then asserts end-to-end `.fips`
|
||||
# resolution + the gateway/daemon default-pairing. The most thorough single
|
||||
# test surface — exercises packaging, maintainer scripts, systemd unit
|
||||
# ordering, real TUN, and the DNS responder filter on a per-distro resolver
|
||||
# backend.
|
||||
#
|
||||
# A job of its own rather than legs of the integration matrix: the install legs
|
||||
# are the only ones that need the package, and as integration legs every other
|
||||
# integration suite would wait on the package build.
|
||||
#
|
||||
# The legs keep `type: deb-install` and `scenario:` because
|
||||
# testing/check-ci-parity.sh reads those to match this matrix against the local
|
||||
# suite's distro list; the steps below use `scenario:` only.
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
deb-install:
|
||||
name: Deb install (${{ matrix.scenario }})
|
||||
runs-on: ubuntu-latest
|
||||
needs: [deb-package]
|
||||
if: ${{ !inputs.skip_integration }}
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- type: deb-install
|
||||
scenario: debian12
|
||||
- type: deb-install
|
||||
scenario: debian13
|
||||
- type: deb-install
|
||||
scenario: ubuntu22
|
||||
- type: deb-install
|
||||
scenario: ubuntu24
|
||||
- type: deb-install
|
||||
scenario: ubuntu26
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
|
||||
|
||||
- name: Download the .deb
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||
with:
|
||||
name: fips-deb
|
||||
path: _deb
|
||||
|
||||
- name: Run deb-install scenario
|
||||
timeout-minutes: 25
|
||||
run: |
|
||||
deb=$(find _deb -maxdepth 1 -type f -name 'fips_*.deb' | sort | head -1)
|
||||
[ -n "$deb" ] || { echo "no .deb in the downloaded artifact" >&2; exit 1; }
|
||||
bash testing/deb-install/test.sh --deb "$deb" ${{ matrix.scenario }}
|
||||
|
||||
- name: Collect logs on failure
|
||||
if: failure()
|
||||
run: |
|
||||
docker ps -a --filter "name=fips-deb-test-${{ matrix.scenario }}" --format '{{.Names}}' | while read -r c; do
|
||||
echo "--- ${c} fips.service ---"
|
||||
docker exec "$c" journalctl -u fips.service --no-pager 2>&1 | tail -100 || true
|
||||
echo "--- ${c} fips-dns.service ---"
|
||||
docker exec "$c" journalctl -u fips-dns.service --no-pager 2>&1 | tail -100 || true
|
||||
echo "--- ${c} fips-gateway.service ---"
|
||||
docker exec "$c" journalctl -u fips-gateway.service --no-pager 2>&1 | tail -100 || true
|
||||
done
|
||||
|
||||
- name: Stop containers
|
||||
if: always()
|
||||
run: |
|
||||
docker ps -a --filter "name=fips-deb-test-${{ matrix.scenario }}" --format '{{.Names}}' | while read -r c; do
|
||||
docker rm -f "$c" >/dev/null 2>&1 || true
|
||||
done
|
||||
|
||||
@@ -49,6 +49,11 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
needs: determine-versioning
|
||||
|
||||
# Both legs build in the same pinned container. Nothing passes --platform,
|
||||
# so the arm runner resolves the arm64 variant of the base image and builds
|
||||
# natively; the floor check runs on that package too, so an aarch64 build
|
||||
# above the floor fails the leg rather than shipping. What the runner
|
||||
# supplies is Docker and the checkout -- neither leg compiles on the host.
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -68,32 +73,76 @@ jobs:
|
||||
- name: Set SOURCE_DATE_EPOCH from git
|
||||
run: echo "SOURCE_DATE_EPOCH=$(git log -1 --format=%ct)" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Install system dependencies
|
||||
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends libdbus-1-dev llvm
|
||||
# The host no longer compiles anything: the container carries the
|
||||
# toolchain and the build dependencies. llvm is here only for llvm-strip,
|
||||
# which build-tarball.sh uses on the binaries recovered from the package.
|
||||
- name: Install host packaging tools
|
||||
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends llvm
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1
|
||||
with:
|
||||
cache: false
|
||||
rustflags: ''
|
||||
# Build in the pinned container rather than on the runner. The runner's
|
||||
# glibc is what put a GLIBC_2.39 requirement into every Linux artifact
|
||||
# from v0.3.0 onward, so the package installed cleanly and then could not
|
||||
# load on Debian 12 or Ubuntu 22.04. packaging/build-floor.env declares
|
||||
# the base image and the floor; the script builds there and runs
|
||||
# testing/check-glibc-floor.sh on the package it produced, so a build that
|
||||
# would ship an unloadable binary fails here instead of at the user.
|
||||
#
|
||||
# This is the same script ci.yml and a local run call, so the package that
|
||||
# passes the five-distro suite is built the way this one is.
|
||||
- name: Build Debian package in the pinned container
|
||||
id: deb
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
: ${GITHUB_OUTPUT:=/tmp/github_output}
|
||||
|
||||
- name: Cache Cargo registry + build
|
||||
if: ${{ env.ACT != 'true' }}
|
||||
uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
key: linux-release-${{ runner.os }}-${{ matrix.artifact_arch }}-${{ hashFiles('**/Cargo.lock') }}
|
||||
restore-keys: |
|
||||
linux-release-${{ runner.os }}-${{ matrix.artifact_arch }}-
|
||||
packaging/debian/build-deb-container.sh \
|
||||
--version "${{ needs.determine-versioning.outputs.linux_package_version }}" \
|
||||
--output-dir deploy \
|
||||
| tee /tmp/build-deb-container.log
|
||||
|
||||
- name: Install cargo-deb
|
||||
run: cargo install cargo-deb --version 3.6.3 --locked
|
||||
# The script prints the package path as its last line of stdout.
|
||||
# Only stdout is captured; its diagnostics go to stderr and straight
|
||||
# to the job log, so nothing can land after the path.
|
||||
DEB_FILE=$(tail -n 1 /tmp/build-deb-container.log)
|
||||
if [[ ! -f "$DEB_FILE" ]]; then
|
||||
echo "build-deb-container.sh did not name a package: '$DEB_FILE'" >&2
|
||||
exit 1
|
||||
fi
|
||||
case "$DEB_FILE" in
|
||||
*_${{ matrix.deb_arch }}.deb) ;;
|
||||
*)
|
||||
echo "Package $DEB_FILE is not ${{ matrix.deb_arch }}" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
- name: Build release binaries
|
||||
run: cargo build --release
|
||||
# Record it relative to the checkout: upload-artifact derives the
|
||||
# archive layout from the common ancestor of its paths, and an
|
||||
# absolute path here would nest the package under directories the
|
||||
# release job's dist/*.deb glob does not look in.
|
||||
echo "deb=${DEB_FILE#"$PWD"/}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# The container writes its target directory to a Docker volume, so the
|
||||
# runner's target/release is empty. Recover the four binaries from the
|
||||
# package instead: they are the container-built ones, so the tarball ships
|
||||
# what the package ships rather than a second, runner-built set that the
|
||||
# floor check never saw and that no package manager would refuse.
|
||||
- name: Stage container-built binaries for the tarball
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
UNPACK=$(mktemp -d)
|
||||
dpkg-deb -x "${{ steps.deb.outputs.deb }}" "$UNPACK"
|
||||
mkdir -p target/release
|
||||
for bin in fips fipsctl fipstop fips-gateway; do
|
||||
if [[ ! -f "$UNPACK/usr/bin/$bin" ]]; then
|
||||
echo "Package is missing usr/bin/$bin" >&2
|
||||
exit 1
|
||||
fi
|
||||
install -m 0755 "$UNPACK/usr/bin/$bin" "target/release/$bin"
|
||||
done
|
||||
rm -rf "$UNPACK"
|
||||
|
||||
- name: Build systemd tarball
|
||||
env:
|
||||
@@ -104,11 +153,23 @@ jobs:
|
||||
--arch "${{ matrix.artifact_arch }}" \
|
||||
--no-build
|
||||
|
||||
- name: Build Debian package
|
||||
# The tarball has no package manager to refuse it, so nothing at install
|
||||
# time would notice a bad floor. Check the binaries out of the finished
|
||||
# tarball, after the strip, rather than trusting that they are the same
|
||||
# objects the package check already passed.
|
||||
- name: Check the tarball against the declared glibc floor
|
||||
shell: bash
|
||||
run: |
|
||||
packaging/debian/build-deb.sh \
|
||||
--version "${{ needs.determine-versioning.outputs.linux_package_version }}" \
|
||||
--no-build
|
||||
set -euo pipefail
|
||||
TARBALL="deploy/fips-${{ needs.determine-versioning.outputs.linux_package_version }}-linux-${{ matrix.artifact_arch }}.tar.gz"
|
||||
UNPACK=$(mktemp -d)
|
||||
tar -xzf "$TARBALL" -C "$UNPACK"
|
||||
testing/check-glibc-floor.sh \
|
||||
"$UNPACK"/*/fips \
|
||||
"$UNPACK"/*/fipsctl \
|
||||
"$UNPACK"/*/fipstop \
|
||||
"$UNPACK"/*/fips-gateway
|
||||
rm -rf "$UNPACK"
|
||||
|
||||
- name: Resolve Linux asset paths
|
||||
id: linux-assets
|
||||
@@ -122,14 +183,8 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
DEB_FILE=$(find deploy -maxdepth 1 -type f -name "fips_*_${{ matrix.deb_arch }}.deb" | sort | head -n 1)
|
||||
if [[ -z "$DEB_FILE" ]]; then
|
||||
echo "Missing Debian package for ${{ matrix.deb_arch }}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "tarball=$TARBALL" >> "$GITHUB_OUTPUT"
|
||||
echo "deb=$DEB_FILE" >> "$GITHUB_OUTPUT"
|
||||
echo "deb=${{ steps.deb.outputs.deb }}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: SHA-256 hashes
|
||||
run: |
|
||||
|
||||
@@ -40,6 +40,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
`fipstop` as "Own Loopback". `req_duplicate` returns to meaning only what it
|
||||
says.
|
||||
|
||||
#### Packaging
|
||||
|
||||
- The Linux `.deb` and the systemd tarball now install and run on Debian 12 and
|
||||
Ubuntu 22.04. Every Linux artifact from v0.3.0 through v0.5.0 was built on the
|
||||
newest available runner, whose C library made the standard library's `pidfd`
|
||||
references a hard `GLIBC_2.39` version requirement instead of the weak,
|
||||
runtime-checked ones it is meant to compile to. The loader refuses an image on
|
||||
that entry alone, so `fips`, `fipstop` and `fips-gateway` could not start;
|
||||
`fipsctl` was unaffected, which is why an install that was checked by running
|
||||
it looked healthy while the daemon was dead. No source code caused this and
|
||||
none was changed. The Linux artifacts are now built in a container pinned to
|
||||
the oldest supported distribution, declared with the floor in
|
||||
`packaging/build-floor.env`, and every producer runs
|
||||
`testing/check-glibc-floor.sh` on what it made, so a package or a tarball that
|
||||
would not load fails the build rather than reaching a user. The declared
|
||||
dependency is derived from the binaries instead of hand-written, so it states
|
||||
the floor it was built against.
|
||||
|
||||
## [0.5.0] - 2026-08-30
|
||||
|
||||
### Added
|
||||
|
||||
+9
-1
@@ -76,7 +76,15 @@ copyright = "2026 Johnathan Corgan"
|
||||
license-file = ["LICENSE", "0"]
|
||||
section = "net"
|
||||
priority = "optional"
|
||||
depends = "libc6, systemd, libdbus-1-3"
|
||||
# "$auto" runs dpkg-shlibdeps over each packaged binary and derives the real
|
||||
# dependencies, including the libc6 version floor. Written by hand this field
|
||||
# said only "libc6", which no glibc fails to satisfy, so a package whose
|
||||
# binaries needed 2.39 installed happily on a system with 2.35 and the daemon
|
||||
# then could not start. Deriving it also picks up a libdbus floor the hand
|
||||
# written list lacked, and re-derives per architecture, which matters because
|
||||
# arm64 links libdbus in all four binaries where amd64 links it in one.
|
||||
# systemd stays by hand: nothing links it, so nothing can derive it.
|
||||
depends = "$auto, systemd"
|
||||
recommends = "bluez"
|
||||
extended-description = """\
|
||||
FIPS is a distributed, decentralized network routing protocol for mesh \
|
||||
|
||||
+15
-2
@@ -4,7 +4,8 @@
|
||||
# All outputs are placed in deploy/ at the project root.
|
||||
#
|
||||
# Usage:
|
||||
# make deb Build a Debian/Ubuntu .deb package
|
||||
# make deb Build a Debian/Ubuntu .deb package in the pinned container
|
||||
# make deb-host Build a .deb with the host toolchain (see below)
|
||||
# make tarball Build a systemd install tarball
|
||||
# make ipk Build an OpenWrt .ipk package (opkg, OpenWrt 24.x and earlier)
|
||||
# make apk Build an OpenWrt .apk package (apk-tools, mandatory on OpenWrt 25+)
|
||||
@@ -19,11 +20,23 @@ SHELL := /bin/bash
|
||||
PACKAGING_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
|
||||
PROJECT_ROOT := $(abspath $(PACKAGING_DIR)/..)
|
||||
|
||||
.PHONY: all deb tarball ipk apk aur pkg freebsd zip clean
|
||||
.PHONY: all deb deb-host tarball ipk apk aur pkg freebsd zip clean
|
||||
|
||||
all: deb tarball
|
||||
|
||||
# `deb` builds in the pinned container so the package carries the declared glibc
|
||||
# floor and can install on every supported distribution. It also checks that
|
||||
# floor before it hands the package back.
|
||||
deb:
|
||||
@bash $(PACKAGING_DIR)/debian/build-deb-container.sh
|
||||
|
||||
# `deb-host` builds with whatever toolchain and C library the host has. It is
|
||||
# for iterating locally and NOT for anything anyone else installs: on a modern
|
||||
# host it produces a package that installs cleanly and then cannot start on
|
||||
# Debian 12 or Ubuntu 22.04, which is the defect that made the container build
|
||||
# necessary. Nothing checks its floor, deliberately, so the check stays
|
||||
# attached to the artifact that ships.
|
||||
deb-host:
|
||||
@bash $(PACKAGING_DIR)/debian/build-deb.sh
|
||||
|
||||
tarball:
|
||||
|
||||
+22
-1
@@ -7,7 +7,7 @@ and `make apk` write to `dist/` instead.
|
||||
## Quick Start
|
||||
|
||||
```sh
|
||||
make deb # Debian/Ubuntu .deb
|
||||
make deb # Debian/Ubuntu .deb (built in the pinned container)
|
||||
make tarball # systemd install tarball
|
||||
make ipk # OpenWrt .ipk (opkg, OpenWrt 24.x and earlier)
|
||||
make apk # OpenWrt .apk (apk-tools, mandatory on OpenWrt 25+)
|
||||
@@ -18,8 +18,29 @@ make zip # Windows .zip package
|
||||
make all # deb + tarball (default)
|
||||
```
|
||||
|
||||
## The two Debian build paths
|
||||
|
||||
`make deb` builds in a container pinned to the oldest supported
|
||||
distribution, named with the glibc floor in
|
||||
[build-floor.env](build-floor.env), and checks the package it produced
|
||||
against that floor before handing it back. Its only host prerequisite is
|
||||
docker: the toolchain and the build dependencies live in the image. This
|
||||
is the path the release workflow, the integration suite and the internal
|
||||
builder all take, so a package that passes locally is built the way the
|
||||
shipped one is.
|
||||
|
||||
`make deb-host` is the old path. It builds on the host, at whatever glibc
|
||||
the host has, and it is checked against nothing. Use it for local
|
||||
iteration only. A package built on a current distribution records a
|
||||
version dependency that the loader refuses on Debian 12 and Ubuntu 22.04,
|
||||
which is what shipped in every Linux artifact from v0.3.0 through v0.5.0,
|
||||
so it must not produce anything anyone else installs.
|
||||
|
||||
## Build Prerequisites
|
||||
|
||||
The prerequisites below apply to the host-build targets. `make deb` needs
|
||||
docker and nothing else.
|
||||
|
||||
These targets build FIPS from source, so the host needs a build
|
||||
environment in addition to a Rust toolchain (the version pinned in
|
||||
`rust-toolchain.toml` is auto-installed by rustup).
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
# The glibc floor for the Linux release artifacts, and the image that produces it.
|
||||
#
|
||||
# Sourced by packaging/debian/build-deb-container.sh and by
|
||||
# testing/check-glibc-floor.sh. It exists so the floor is a decision written
|
||||
# down in one place rather than a side effect of whichever build host ran last.
|
||||
#
|
||||
# The rule it encodes: FIPS installs on every version of a supported operating
|
||||
# system that its distributor still supports for free. As of 2026-09-05 that is
|
||||
#
|
||||
# Ubuntu 22.04 glibc 2.35 free support ends April 2027
|
||||
# Debian 12 glibc 2.36 LTS ends 2028-06-30
|
||||
# Ubuntu 24.04 glibc 2.39
|
||||
# Debian 13 glibc 2.41 LTS ends 2030-06-30
|
||||
# Ubuntu 26.04 glibc 2.43
|
||||
#
|
||||
# so the lowest is Ubuntu 22.04 and the floor is its 2.35. Debian 11 left the
|
||||
# set on 2026-08-31 and is deliberately not counted.
|
||||
#
|
||||
# Deliberately NOT a GitHub runner label. Runner availability follows GitHub's
|
||||
# rule of supporting the newest two images; the floor follows distributors'
|
||||
# support windows. Those are different clocks, and ubuntu-26.04 being in preview
|
||||
# means the ubuntu-22.04 runner will very likely retire before Canonical's date.
|
||||
# A container base outlives the runner label and builds the same way on a
|
||||
# developer's machine, which is what lets local and GitHub CI do identical work.
|
||||
#
|
||||
# Changing FIPS_GLIBC_FLOOR drops support for every distribution below it. Check
|
||||
# the table above first, and expect check-glibc-floor.sh to hold you to it.
|
||||
|
||||
# Base image for the build. Pinned to the oldest supported distribution.
|
||||
FIPS_BUILD_IMAGE="ubuntu:22.04"
|
||||
|
||||
# Highest glibc symbol version any shipped binary may require. Building on
|
||||
# FIPS_BUILD_IMAGE currently yields 2.34, one step below this, so there is a
|
||||
# little headroom: the check is an upper bound, not an equality.
|
||||
FIPS_GLIBC_FLOOR="2.35"
|
||||
@@ -0,0 +1,49 @@
|
||||
# Build image for the Linux release artifacts.
|
||||
#
|
||||
# Pinned to the oldest distribution FIPS supports, because the glibc a binary is
|
||||
# linked against decides the glibc it will run on. See packaging/build-floor.env
|
||||
# for the floor and the reasoning; BASE is passed from there, not written here,
|
||||
# so there is one place to change it.
|
||||
#
|
||||
# This image carries the toolchain and the build dependencies only. It never
|
||||
# carries the source: the source is mounted at run time, so editing a file does
|
||||
# not invalidate the image and a warm rebuild costs seconds rather than minutes.
|
||||
ARG BASE=ubuntu:22.04
|
||||
FROM ${BASE}
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# dpkg-dev is not in a bare ubuntu:22.04 and is what provides dpkg-shlibdeps,
|
||||
# which cargo-deb's "$auto" dependency resolution shells out to. Without it the
|
||||
# declared dependencies would silently lose their versions again.
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
build-essential \
|
||||
pkg-config \
|
||||
libdbus-1-dev \
|
||||
libclang-dev \
|
||||
clang \
|
||||
binutils \
|
||||
dpkg-dev \
|
||||
curl \
|
||||
ca-certificates \
|
||||
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# The toolchain version is passed in, read from rust-toolchain.toml by the
|
||||
# calling script, and the image tag carries it -- so the image cannot drift from
|
||||
# the compiler the rest of CI uses, and bumping the pin rebuilds the image. The
|
||||
# builder this replaces installed `stable` and never copied rust-toolchain.toml,
|
||||
# so it compiled with a different compiler from the release and nothing said so.
|
||||
ARG RUST_TOOLCHAIN
|
||||
ENV RUSTUP_HOME=/usr/local/rustup \
|
||||
CARGO_HOME=/usr/local/cargo \
|
||||
PATH=/usr/local/cargo/bin:$PATH
|
||||
RUN test -n "${RUST_TOOLCHAIN}" \
|
||||
&& curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
|
||||
| sh -s -- -y --profile minimal --default-toolchain "${RUST_TOOLCHAIN}" \
|
||||
&& chmod -R a+w "$RUSTUP_HOME" "$CARGO_HOME"
|
||||
|
||||
ARG CARGO_DEB_VERSION=3.6.3
|
||||
RUN cargo install cargo-deb --version "${CARGO_DEB_VERSION}" --locked \
|
||||
&& chmod -R a+w "$CARGO_HOME"
|
||||
|
||||
WORKDIR /src
|
||||
Executable
+135
@@ -0,0 +1,135 @@
|
||||
#!/bin/bash
|
||||
# Build the Debian package in the pinned build container, then check its floor.
|
||||
#
|
||||
# This is the one place the Linux artifacts are produced. The release workflow,
|
||||
# the CI integration job and a local run all call it, so all three build the
|
||||
# same way and a package that passes locally is the package that ships. That was
|
||||
# not true before: the test suite built its own package inside a Debian 12 image
|
||||
# while the release built on the newest GitHub runner, so the suite could not
|
||||
# exhibit a defect that only the release environment produced -- and for five
|
||||
# releases it did not.
|
||||
#
|
||||
# Usage: build-deb-container.sh [--output-dir DIR] [--version V] [--features LIST]
|
||||
# [--rebuild-image]
|
||||
#
|
||||
# Requires docker. The image is cached between runs and rebuilt only when the
|
||||
# Dockerfile or the floor changes; the source is mounted rather than copied, so
|
||||
# editing code does not invalidate it.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
|
||||
# shellcheck source=../build-floor.env
|
||||
. "$REPO_ROOT/packaging/build-floor.env"
|
||||
|
||||
DEST_DIR="$REPO_ROOT/deploy"
|
||||
VERSION=""
|
||||
FEATURES=""
|
||||
REBUILD_IMAGE=0
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--output-dir) DEST_DIR="${2:?missing value for --output-dir}"; shift 2 ;;
|
||||
--version) VERSION="${2:?missing value for --version}"; shift 2 ;;
|
||||
--features) FEATURES="${2:?missing value for --features}"; shift 2 ;;
|
||||
--rebuild-image) REBUILD_IMAGE=1; shift ;;
|
||||
-h|--help) sed -n '2,17p' "$0"; exit 0 ;;
|
||||
*) echo "Unknown option: $1" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
command -v docker >/dev/null 2>&1 || {
|
||||
echo "build-deb-container: docker is required and was not found." >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
# Read the toolchain from the pin rather than choosing one here, and put it in
|
||||
# the tag so a bump rebuilds the image instead of silently reusing a stale one.
|
||||
RUST_TOOLCHAIN=$(awk -F'"' '/^channel *=/{print $2; exit}' "$REPO_ROOT/rust-toolchain.toml")
|
||||
[ -n "$RUST_TOOLCHAIN" ] || {
|
||||
echo "build-deb-container: could not read channel from rust-toolchain.toml" >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
IMAGE_TAG="fips-deb-builder:${FIPS_BUILD_IMAGE//[:\/]/-}-rust${RUST_TOOLCHAIN}"
|
||||
|
||||
if [ "$REBUILD_IMAGE" -eq 1 ] || ! docker image inspect "$IMAGE_TAG" >/dev/null 2>&1; then
|
||||
echo "=== Building $IMAGE_TAG from $FIPS_BUILD_IMAGE with Rust $RUST_TOOLCHAIN ===" >&2
|
||||
docker build \
|
||||
--build-arg "BASE=$FIPS_BUILD_IMAGE" \
|
||||
--build-arg "RUST_TOOLCHAIN=$RUST_TOOLCHAIN" \
|
||||
-t "$IMAGE_TAG" \
|
||||
-f "$SCRIPT_DIR/Dockerfile.build" \
|
||||
"$SCRIPT_DIR"
|
||||
else
|
||||
echo "=== Using cached $IMAGE_TAG ===" >&2
|
||||
fi
|
||||
|
||||
# Derive the version and the timestamp on the host, where git works, and pass
|
||||
# both in. The container then never runs git, which matters for two reasons: a
|
||||
# worktree's .git is a file pointing outside the mount and would not resolve,
|
||||
# and a bind-mounted repository trips git's dubious-ownership check.
|
||||
if [ -z "$VERSION" ]; then
|
||||
CRATE_VERSION=$(awk -F'"' '/^version = /{print $2; exit}' "$REPO_ROOT/Cargo.toml")
|
||||
if [[ "$CRATE_VERSION" == *-dev ]]; then
|
||||
GIT_DATE=$(git -C "$REPO_ROOT" log -1 --format=%cs | tr -d '-')
|
||||
GIT_SHA=$(git -C "$REPO_ROOT" rev-parse --short HEAD)
|
||||
DIRTY=""
|
||||
[ -n "$(git -C "$REPO_ROOT" status --porcelain 2>/dev/null)" ] && DIRTY=".dirty"
|
||||
VERSION="${CRATE_VERSION%-dev}~dev+git${GIT_DATE}.${GIT_SHA}${DIRTY}-1"
|
||||
else
|
||||
VERSION="$CRATE_VERSION"
|
||||
fi
|
||||
fi
|
||||
SOURCE_DATE_EPOCH="${SOURCE_DATE_EPOCH:-$(git -C "$REPO_ROOT" log -1 --format=%ct)}"
|
||||
|
||||
mkdir -p "$DEST_DIR"
|
||||
DEST_ABS="$(cd "$DEST_DIR" && pwd)"
|
||||
|
||||
echo "=== Building fips $VERSION in $IMAGE_TAG ===" >&2
|
||||
|
||||
# A feature build hands the whole job to build-deb.sh rather than pre-building:
|
||||
# --features has to reach cargo, and build-deb.sh is also what marks the version
|
||||
# so a feature package is distinguishable from the default build of the same
|
||||
# commit. It refuses --features with --no-build for exactly that reason, so the
|
||||
# two cases cannot share one command.
|
||||
if [ -n "$FEATURES" ]; then
|
||||
# build-deb.sh does the whole job here: --features has to reach cargo, and
|
||||
# it is also what marks the version so a feature package is distinguishable
|
||||
# from the default build of the same commit. It refuses --features with
|
||||
# --no-build for that reason, so the two cases cannot share one command.
|
||||
# The version still comes from the host, because the image has no git.
|
||||
BUILD_CMD="packaging/debian/build-deb.sh --features '$FEATURES' --version '$VERSION' --output-dir /out"
|
||||
else
|
||||
BUILD_CMD="cargo build --release --locked
|
||||
packaging/debian/build-deb.sh --no-build --version '$VERSION' --output-dir /out"
|
||||
fi
|
||||
|
||||
# The source is mounted read-only so a build cannot leave artifacts in the tree.
|
||||
# CARGO_TARGET_DIR and the registry live in named volumes, which is what makes a
|
||||
# second run fast; they are per-base-image so a floor change does not reuse
|
||||
# objects linked against the wrong C library.
|
||||
VOL_SUFFIX="${FIPS_BUILD_IMAGE//[:\/]/-}"
|
||||
docker run --rm \
|
||||
-v "$REPO_ROOT":/src:ro \
|
||||
-v "$DEST_ABS":/out \
|
||||
-v "fips-deb-target-${VOL_SUFFIX}":/target \
|
||||
-v "fips-deb-registry-${VOL_SUFFIX}":/usr/local/cargo/registry \
|
||||
-e CARGO_TARGET_DIR=/target \
|
||||
-e SOURCE_DATE_EPOCH="$SOURCE_DATE_EPOCH" \
|
||||
-w /src \
|
||||
"$IMAGE_TAG" \
|
||||
bash -euo pipefail -c "$BUILD_CMD" >&2
|
||||
|
||||
DEB=$(find "$DEST_ABS" -maxdepth 1 -name "fips_*_*.deb" -newermt '-10 minutes' -print | sort | tail -1)
|
||||
[ -n "$DEB" ] || { echo "build-deb-container: no .deb was produced." >&2; exit 1; }
|
||||
|
||||
# Check the artifact here rather than in one workflow, so every producer is
|
||||
# gated: the release, the CI job, a local run and packaging/Makefile all reach
|
||||
# the check through this script.
|
||||
"$REPO_ROOT/testing/check-glibc-floor.sh" "$DEB" >&2
|
||||
|
||||
echo "=== Built $DEB ===" >&2
|
||||
printf '%s\n' "$DEB"
|
||||
@@ -23,6 +23,9 @@ Options:
|
||||
--features <list> Cargo features to build with (comma-separated). Marks the
|
||||
auto-derived Version so the package is distinguishable
|
||||
from a default build of the same commit.
|
||||
--output-dir <dir> Where to put the finished .deb. Defaults to deploy/ under
|
||||
the project root. Exists so the container build can write
|
||||
to a mount and leave the source tree read-only.
|
||||
-h, --help Show this help
|
||||
EOF
|
||||
}
|
||||
@@ -31,6 +34,7 @@ TARGET_TRIPLE=""
|
||||
VERSION_OVERRIDE=""
|
||||
NO_BUILD=0
|
||||
FEATURES=""
|
||||
DEST_DIR=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
@@ -50,6 +54,10 @@ while [[ $# -gt 0 ]]; do
|
||||
FEATURES="${2:?missing value for --features}"
|
||||
shift 2
|
||||
;;
|
||||
--output-dir)
|
||||
DEST_DIR="${2:?missing value for --output-dir}"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
@@ -124,9 +132,20 @@ if [[ -z "${VERSION_OVERRIDE}" ]]; then
|
||||
echo "Auto-derived dev Version: ${VERSION_OVERRIDE}"
|
||||
fi
|
||||
elif [[ -n "${FEATURES}" ]]; then
|
||||
echo "Warning: --version was given with --features, so the Version carries no" >&2
|
||||
echo "feature marker and this package is indistinguishable from a default" >&2
|
||||
echo "build of the same commit. Mark it yourself if that matters." >&2
|
||||
# An explicit version needs the same marker for the same reason, and it is
|
||||
# the only way a caller that cannot derive the version here can get one.
|
||||
# The container build is that caller: it derives the version on the host
|
||||
# because the image has no git, and the source is mounted read-only from a
|
||||
# worktree whose .git is a file pointing outside the mount.
|
||||
if [[ "${VERSION_OVERRIDE}" == *"+$(printf '%s' "${FEATURES}" | tr -c 'a-zA-Z0-9.' '.')"* ]]; then
|
||||
: # already marked by the caller
|
||||
elif [[ "${VERSION_OVERRIDE}" == *-* ]]; then
|
||||
# Split off the Debian revision so the marker lands on the upstream part.
|
||||
VERSION_OVERRIDE="${VERSION_OVERRIDE%-*}+$(printf '%s' "${FEATURES}" | tr -c 'a-zA-Z0-9.' '.')-${VERSION_OVERRIDE##*-}"
|
||||
else
|
||||
VERSION_OVERRIDE="${VERSION_OVERRIDE}+$(printf '%s' "${FEATURES}" | tr -c 'a-zA-Z0-9.' '.')"
|
||||
fi
|
||||
echo "Feature-marked Version: ${VERSION_OVERRIDE}"
|
||||
fi
|
||||
|
||||
# Build the .deb package
|
||||
@@ -149,8 +168,11 @@ if [[ -n "${FEATURES}" ]]; then
|
||||
fi
|
||||
cargo "${cargo_args[@]}"
|
||||
|
||||
# Move output to deploy/
|
||||
mkdir -p deploy
|
||||
# Move output to the requested directory, or deploy/ by default. Note the
|
||||
# distinction from OUTPUT_DIR above, which is cargo-deb's temporary staging
|
||||
# directory and is removed by the EXIT trap.
|
||||
: "${DEST_DIR:=deploy}"
|
||||
mkdir -p "${DEST_DIR}"
|
||||
DEB_FILE=$(find "${OUTPUT_DIR}" -maxdepth 1 -name '*.deb' -printf '%T@ %p\n' | sort -rn | head -1 | cut -d' ' -f2)
|
||||
|
||||
if [ -z "${DEB_FILE}" ]; then
|
||||
@@ -158,10 +180,10 @@ if [ -z "${DEB_FILE}" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cp "${DEB_FILE}" deploy/
|
||||
cp "${DEB_FILE}" "${DEST_DIR}/"
|
||||
BASENAME=$(basename "${DEB_FILE}")
|
||||
echo "Package built: deploy/${BASENAME}"
|
||||
echo "Package built: ${DEST_DIR}/${BASENAME}"
|
||||
echo ""
|
||||
echo "Install with: sudo dpkg -i deploy/${BASENAME}"
|
||||
echo "Install with: sudo dpkg -i ${DEST_DIR}/${BASENAME}"
|
||||
echo "Remove with: sudo dpkg -r fips"
|
||||
echo "Purge with: sudo dpkg -P fips (removes config and identity keys)"
|
||||
|
||||
+3
-2
@@ -125,11 +125,12 @@ machines. Not a Docker harness.
|
||||
|
||||
[`ci-local.sh`](ci-local.sh) runs the full local CI pipeline — build,
|
||||
clippy, unit tests, and the integration suites (including the chaos
|
||||
scenarios) — mirroring the GitHub `ci.yml` integration matrix. Run
|
||||
scenarios) — mirroring the GitHub `ci.yml` integration matrices. Run
|
||||
`./ci-local.sh --help` for the full option list and `--list` for the
|
||||
available suites. Every run starts with a parity check that verifies the
|
||||
local suite set covers the same work as the GitHub matrix, per scenario for
|
||||
chaos and per distro for deb-install; a divergence fails the run. GitHub
|
||||
chaos and per distro for deb-install, across every job that carries a
|
||||
matrix; a divergence fails the run. GitHub
|
||||
runs the same check as its own `ci-parity` job. `--check-parity` runs it
|
||||
alone (see [check-ci-parity.sh](check-ci-parity.sh)).
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
#!/bin/bash
|
||||
# ── CI parity invariant guard ───────────────────────────────────────────────
|
||||
# The GitHub integration matrix (.github/workflows/ci.yml) and the local
|
||||
# default suite set (ci-local.sh) MUST run the same integration suites,
|
||||
# EXCEPT for the deliberate local-only entries listed below. Adding a suite
|
||||
# to one runner without the other means "local green" and "GitHub green" stop
|
||||
# being equivalent claims.
|
||||
# The GitHub integration matrices (.github/workflows/ci.yml, swept across every
|
||||
# job) and the local default suite set (ci-local.sh) MUST run the same
|
||||
# integration suites, EXCEPT for the deliberate local-only entries listed below.
|
||||
# Adding a suite to one runner without the other means "local green" and
|
||||
# "GitHub green" stop being equivalent claims.
|
||||
#
|
||||
# Deliberate local-only (NOT on the GitHub gate), with reason:
|
||||
# tor-socks5 — requires live Tor network; opt-in via --with-tor,
|
||||
@@ -19,8 +19,9 @@
|
||||
# names differ cosmetically between runners and are ignored
|
||||
# — `scenario:` is the identity.
|
||||
# deb-install — per distro. GitHub splits into per-distro legs carrying
|
||||
# `scenario:`; local runs the same distro set in one suite,
|
||||
# enumerated by ALL_SCENARIOS in deb-install/test.sh.
|
||||
# `scenario:`, in a job of their own; local runs the same
|
||||
# distro set in one suite, enumerated by ALL_SCENARIOS in
|
||||
# deb-install/test.sh.
|
||||
# everything else — per suite name.
|
||||
#
|
||||
# dns-resolver is the one leg still compared at leg granularity rather than
|
||||
@@ -134,7 +135,21 @@ for name, entries in arrays.items():
|
||||
with open(ci_yml_path, encoding="utf-8") as fh:
|
||||
doc = yaml.safe_load(fh)
|
||||
|
||||
include = doc["jobs"]["integration"]["strategy"]["matrix"]["include"]
|
||||
# Sweep every job's matrix rather than one named job: the install legs live in
|
||||
# a job of their own so the rest of the integration matrix does not wait on the
|
||||
# package build, and a guard keyed to a job name reports them as local-only the
|
||||
# moment they move. Identity comes from the leg's own fields, so where a leg
|
||||
# lives does not matter; a leg deleted from every job still shows up as
|
||||
# local-only, because the local side is the reference.
|
||||
include = []
|
||||
for job in (doc.get("jobs") or {}).values():
|
||||
legs = (((job.get("strategy") or {}).get("matrix") or {}).get("include") or [])
|
||||
if isinstance(legs, list):
|
||||
include += [leg for leg in legs if isinstance(leg, dict)]
|
||||
if not include:
|
||||
print("check-ci-parity: no matrix include: found in any job of "
|
||||
f"{ci_yml_path}; cannot verify CI parity", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
github_chaos, github_deb, github = {}, set(), set()
|
||||
malformed = []
|
||||
for leg in include:
|
||||
|
||||
Executable
+147
@@ -0,0 +1,147 @@
|
||||
#!/bin/bash
|
||||
# Fail when a shipped binary needs a newer glibc than the declared floor.
|
||||
#
|
||||
# The defect this exists to catch installs cleanly and then cannot run. Rust's
|
||||
# standard library references pidfd_spawnp and pidfd_getpid as weak undefined
|
||||
# symbols guarded by a runtime check, so a binary is meant to fall back where
|
||||
# the C library lacks them. Linking against a glibc that HAS them records a
|
||||
# version dependency instead, and the loader refuses the whole image on that
|
||||
# entry alone regardless of the symbols being weak. Every Linux artifact from
|
||||
# v0.3.0 to v0.5.0 shipped that way and could not start on Debian 12 or Ubuntu
|
||||
# 22.04, while apt reported success and fipsctl -- which never spawns a process
|
||||
# and so never referenced those symbols -- ran perfectly.
|
||||
#
|
||||
# Usage: check-glibc-floor.sh <artifact|binary>...
|
||||
# A .deb is unpacked and every executable under usr/bin is checked.
|
||||
# Anything else is treated as a single ELF binary.
|
||||
#
|
||||
# Reads the floor from packaging/build-floor.env unless FIPS_GLIBC_FLOOR is set.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
|
||||
if [ -z "${FIPS_GLIBC_FLOOR:-}" ]; then
|
||||
# shellcheck source=../packaging/build-floor.env
|
||||
. "$REPO_ROOT/packaging/build-floor.env"
|
||||
fi
|
||||
FLOOR="${FIPS_GLIBC_FLOOR:?no floor declared}"
|
||||
|
||||
for tool in readelf dpkg dpkg-deb; do
|
||||
command -v "$tool" >/dev/null 2>&1 || {
|
||||
echo "check-glibc-floor: $tool is not installed; cannot check anything." >&2
|
||||
echo " Refusing to report a pass I did not establish." >&2
|
||||
exit 2
|
||||
}
|
||||
done
|
||||
|
||||
# The maximum glibc version a binary requires.
|
||||
#
|
||||
# Reads the `Version needs` section, which is the table the dynamic loader
|
||||
# enforces and the one that carries the fatal entry. Do NOT compute this from
|
||||
# `objdump -T | grep GLIBC_ | sort -V | tail -1`: that sorts whole lines, the
|
||||
# version is not the leading field, and a data symbol at GLIBC_2.2.5 therefore
|
||||
# sorts last. Measured against the shipped v0.5.0 fips binary, that pipeline
|
||||
# reports 2.2.5 for a binary whose real floor is 2.39 -- it would have passed
|
||||
# every affected release.
|
||||
#
|
||||
# Prints nothing and returns 1 when it finds no GLIBC requirement at all, so an
|
||||
# unreadable or non-dynamic input cannot be scored as a pass.
|
||||
max_glibc_need() {
|
||||
local bin="$1" found
|
||||
found=$(readelf -VW "$bin" 2>/dev/null \
|
||||
| awk '/Version needs section/,0' \
|
||||
| grep -oE 'GLIBC_[0-9.]+' \
|
||||
| sed 's/GLIBC_//' \
|
||||
| sort -V \
|
||||
| tail -1) || true
|
||||
[ -n "$found" ] || return 1
|
||||
printf '%s\n' "$found"
|
||||
# Explicit: the caller tests this status to tell "no requirement" from a
|
||||
# value, so it must not be whatever printf happened to return.
|
||||
return 0
|
||||
}
|
||||
|
||||
FAILED=0
|
||||
CHECKED=0
|
||||
|
||||
is_elf() { readelf -hW "$1" >/dev/null 2>&1; }
|
||||
|
||||
check_binary() {
|
||||
local bin="$1" label="$2" need
|
||||
if ! need=$(max_glibc_need "$bin"); then
|
||||
# A static binary is a legitimate no-requirement case, so distinguish
|
||||
# it from a file that could not be read rather than passing both.
|
||||
if readelf -hW "$bin" >/dev/null 2>&1; then
|
||||
echo " ok $label (no glibc version requirement)"
|
||||
CHECKED=$((CHECKED + 1))
|
||||
return
|
||||
fi
|
||||
echo " ERROR $label is not a readable ELF object" >&2
|
||||
FAILED=$((FAILED + 1))
|
||||
return
|
||||
fi
|
||||
CHECKED=$((CHECKED + 1))
|
||||
if dpkg --compare-versions "$need" gt "$FLOOR"; then
|
||||
echo " FAIL $label needs glibc $need, above the declared floor $FLOOR" >&2
|
||||
FAILED=$((FAILED + 1))
|
||||
else
|
||||
echo " ok $label needs glibc $need"
|
||||
fi
|
||||
}
|
||||
|
||||
check_deb() {
|
||||
local deb="$1" tmp
|
||||
tmp=$(mktemp -d)
|
||||
# shellcheck disable=SC2064
|
||||
trap "rm -rf '$tmp'" RETURN
|
||||
dpkg-deb -x "$deb" "$tmp"
|
||||
# A package legitimately ships executable shell scripts alongside its
|
||||
# binaries -- fips-dns-setup and its teardown are two -- so filter to ELF
|
||||
# objects rather than treating a script as an unreadable binary. A .deb
|
||||
# with no ELF object at all is still an error: it means the glob or the
|
||||
# layout moved and this check examined nothing.
|
||||
local found=0 f
|
||||
while IFS= read -r -d '' f; do
|
||||
is_elf "$f" || continue
|
||||
found=1
|
||||
check_binary "$f" "$(basename "$deb"):$(basename "$f")"
|
||||
done < <(find "$tmp" -type f -perm -u+x -print0)
|
||||
if [ "$found" -eq 0 ]; then
|
||||
echo " ERROR $(basename "$deb") contains no ELF executables" >&2
|
||||
FAILED=$((FAILED + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
[ $# -gt 0 ] || {
|
||||
echo "usage: check-glibc-floor.sh <artifact|binary>..." >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
echo "=== glibc floor check (declared floor: $FLOOR) ==="
|
||||
for arg in "$@"; do
|
||||
[ -e "$arg" ] || { echo " ERROR $arg does not exist" >&2; FAILED=$((FAILED + 1)); continue; }
|
||||
case "$arg" in
|
||||
*.deb) check_deb "$arg" ;;
|
||||
*) check_binary "$arg" "$(basename "$arg")" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Nothing examined is a failure, not a pass. An argument list that matched no
|
||||
# binary means the caller's glob went stale, and reporting that as green is how
|
||||
# a guard quietly stops guarding.
|
||||
if [ "$CHECKED" -eq 0 ] && [ "$FAILED" -eq 0 ]; then
|
||||
echo "check-glibc-floor: examined no binaries; refusing to report a pass." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if [ "$FAILED" -ne 0 ]; then
|
||||
echo "check-glibc-floor: $FAILED problem(s) across $CHECKED binaries." >&2
|
||||
echo " A binary above the floor installs cleanly and then fails to start." >&2
|
||||
echo " Build through packaging/debian/build-deb-container.sh, which pins" >&2
|
||||
echo " the build image to the oldest supported distribution." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=== glibc floor check passed ($CHECKED binaries, all at or below $FLOOR) ==="
|
||||
+55
-5
@@ -999,13 +999,63 @@ run_dns_resolver() {
|
||||
}
|
||||
|
||||
# Run deb-install harness (multi-distro real-package install)
|
||||
#
|
||||
# Bounded because this worker is what gates artifact publication: a suite that
|
||||
# hangs stops every branch publishing, which is worse than a red. The harness
|
||||
# bounds its own service starts now, so this is the backstop for anything else
|
||||
# that wedges -- a docker daemon that stops answering, a container that never
|
||||
# boots. 40 minutes is well above the observed cold-cache cost of a full
|
||||
# five-distro run and is not a performance budget.
|
||||
#
|
||||
# It bounds the container build and the five installs separately rather than
|
||||
# both together: the budget was sized for the installs, and a cold build that
|
||||
# ate into it would shrink theirs. Neither phase is left unbounded, which is
|
||||
# the property this exists for.
|
||||
DEB_INSTALL_TIMEOUT=${DEB_INSTALL_TIMEOUT:-2400}
|
||||
run_deb_install() {
|
||||
info "[deb-install] Running multi-distro test (slow — builds .deb + per-distro install)"
|
||||
if bash testing/deb-install/test.sh 2>&1; then
|
||||
record "deb-install" 0
|
||||
else
|
||||
record "deb-install" 1
|
||||
# Build once through the shared container script, then install that one
|
||||
# artifact into every distro. The harness would build its own package if
|
||||
# handed none, and that fallback goes through the same script -- but the
|
||||
# GitHub job builds explicitly and passes `--deb`, so doing it explicitly
|
||||
# here too makes the two systems read as the same work rather than leaving
|
||||
# a reader to discover that a fallback happens to match.
|
||||
info "[deb-install] Building the package in the pinned container"
|
||||
local build_log deb rc=0
|
||||
build_log=$(mktemp "/tmp/ci-deb-install-build.XXXXXX")
|
||||
# stdout carries the package path on its last line, so it is captured;
|
||||
# stderr stays on the console so build progress is still visible live.
|
||||
timeout "$DEB_INSTALL_TIMEOUT" \
|
||||
bash packaging/debian/build-deb-container.sh | tee "$build_log" || rc=$?
|
||||
if [[ $rc -ne 0 ]]; then
|
||||
if [[ $rc -eq 124 ]]; then
|
||||
echo " ERROR: the container build exceeded ${DEB_INSTALL_TIMEOUT}s and was killed;" >&2
|
||||
echo " no package was produced, so this is not an assertion failure." >&2
|
||||
else
|
||||
echo " ERROR: the container build failed (exit $rc); no package to install." >&2
|
||||
fi
|
||||
rm -f "$build_log"
|
||||
record "deb-install" "$rc"
|
||||
return
|
||||
fi
|
||||
deb=$(tail -n 1 "$build_log")
|
||||
rm -f "$build_log"
|
||||
if [[ -z "$deb" || ! -f "$deb" ]]; then
|
||||
echo " ERROR: the container build reported success but its last line of stdout" >&2
|
||||
echo " was not a package path: '${deb}'" >&2
|
||||
record "deb-install" 1
|
||||
return
|
||||
fi
|
||||
|
||||
info "[deb-install] Running multi-distro install test against $deb"
|
||||
rc=0
|
||||
timeout "$DEB_INSTALL_TIMEOUT" bash testing/deb-install/test.sh --deb "$deb" 2>&1 || rc=$?
|
||||
if [[ $rc -eq 124 ]]; then
|
||||
# Say so explicitly. A bare red here reads as a failed assertion, and
|
||||
# the difference matters: a timeout means no assertion was reached.
|
||||
echo " ERROR: deb-install exceeded ${DEB_INSTALL_TIMEOUT}s and was killed;" >&2
|
||||
echo " no verdict was reached, so this is not an assertion failure." >&2
|
||||
fi
|
||||
record "deb-install" "$rc"
|
||||
}
|
||||
|
||||
# Run Tor SOCKS5 outbound test (live Tor network)
|
||||
|
||||
+129
-40
@@ -36,11 +36,26 @@ DEB_CACHE_DIR="$CACHE_DIR/deb"
|
||||
BOOT_TIMEOUT=30
|
||||
SERVICE_TIMEOUT=20
|
||||
DAEMON_TIMEOUT=15
|
||||
# Bounds on a single `systemctl start`, which is not a wait loop and needs its
|
||||
# own limit. See start_unit() for why an unbounded one can never return.
|
||||
UNIT_START_TIMEOUT=30
|
||||
# fips-gateway.service's ExecStartPre waits up to 30s for fips0 to appear, by
|
||||
# design, so its start legitimately takes longer than any other.
|
||||
GATEWAY_START_TIMEOUT=60
|
||||
# The gateway-enable block restarts fips.service inside the container. Above
|
||||
# systemd's default TimeoutStopSec of 90s, so a wedged stop trips this rather
|
||||
# than this cutting a healthy stop short.
|
||||
CONFIG_RESTART_TIMEOUT=120
|
||||
|
||||
PASS=0
|
||||
FAIL=0
|
||||
SKIP=0
|
||||
|
||||
# Set by --deb. DEB_PREPARED keeps the copy-into-cache to a single operation
|
||||
# however many scenarios run in one process.
|
||||
SUPPLIED_DEB=""
|
||||
DEB_PREPARED=0
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
# Helpers
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
@@ -97,6 +112,41 @@ wait_for_systemd() {
|
||||
return 1
|
||||
}
|
||||
|
||||
# Start a unit without waiting for its start job to finish.
|
||||
#
|
||||
# Start a unit and wait for its start job, under a bound.
|
||||
#
|
||||
# Blocking is the right default and the call returning is what synchronises the
|
||||
# checks after it: `fips-gateway.service` in particular has an ExecStartPre that
|
||||
# waits up to 30s for fips0, so a caller that does not wait races it. What the
|
||||
# old code lacked was the bound, not the wait.
|
||||
start_unit() {
|
||||
local name="$1" unit="$2" limit="${3:-$UNIT_START_TIMEOUT}"
|
||||
timeout "$limit" docker exec "$name" systemctl start "$unit" 2>&1
|
||||
}
|
||||
|
||||
# Queue a unit's start job and return without waiting for it.
|
||||
#
|
||||
# For `fips-dns.service` only, and the reason is specific rather than general.
|
||||
# It is Type=oneshot with Requires=fips.service, so its start job waits on a
|
||||
# dependency that a broken daemon never satisfies: fips.service restarts every
|
||||
# 5s for ever and the oneshot's job is never dispatched. `systemctl start` then
|
||||
# never returns. That is the whole class of fault this suite exists to find, and
|
||||
# the suite answered it by hanging -- no FAIL, no Results line, no exit status,
|
||||
# observed at 21 minutes against a package whose binaries could not load.
|
||||
#
|
||||
# Queueing moves the verdict onto the wait_for_service_active call that follows,
|
||||
# which carries a timeout and dumps the journal when it fails. RemainAfterExit=yes
|
||||
# on that unit makes `is-active` a correct readiness test for a oneshot.
|
||||
#
|
||||
# This bounds these call sites, not every `docker exec` in the file. The backstop
|
||||
# for the rest is the caller's own limit: ci-local.sh bounds the whole suite, and
|
||||
# the GitHub leg carries timeout-minutes.
|
||||
start_unit_queued() {
|
||||
local name="$1" unit="$2"
|
||||
timeout "$UNIT_START_TIMEOUT" docker exec "$name" systemctl start --no-block "$unit" 2>&1
|
||||
}
|
||||
|
||||
wait_for_service_active() {
|
||||
local name="$1" service="$2" timeout="${3:-$SERVICE_TIMEOUT}"
|
||||
for _i in $(seq 1 "$timeout"); do
|
||||
@@ -124,6 +174,25 @@ container_systemd_version() {
|
||||
build_deb() {
|
||||
mkdir -p "$DEB_CACHE_DIR"
|
||||
|
||||
# A supplied package wins outright and bypasses the staleness check below.
|
||||
# That check compares mtimes, so a cached package could otherwise beat the
|
||||
# artifact the caller explicitly handed over, which is exactly the silent
|
||||
# substitution this suite exists to stop making.
|
||||
if [ -n "$SUPPLIED_DEB" ]; then
|
||||
if [ "$DEB_PREPARED" -eq 1 ]; then
|
||||
return 0
|
||||
fi
|
||||
if [ ! -f "$SUPPLIED_DEB" ]; then
|
||||
echo " ERROR: --deb $SUPPLIED_DEB does not exist" >&2
|
||||
return 1
|
||||
fi
|
||||
rm -f "$DEB_CACHE_DIR"/*.deb
|
||||
cp "$SUPPLIED_DEB" "$DEB_CACHE_DIR/"
|
||||
DEB_PREPARED=1
|
||||
log "Installing the supplied package $(basename "$SUPPLIED_DEB")"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local cached_deb
|
||||
cached_deb=$(ls "$DEB_CACHE_DIR"/fips_*_amd64.deb 2>/dev/null | head -1)
|
||||
|
||||
@@ -143,45 +212,25 @@ build_deb() {
|
||||
log "No cached .deb, building"
|
||||
fi
|
||||
|
||||
local builder_tag="fips-deb-test:builder"
|
||||
log "Building Debian 12 cargo-deb builder image (slow on first run)"
|
||||
docker build -t "$builder_tag" -f - "$REPO_ROOT" <<'DOCKERFILE' >/dev/null
|
||||
FROM debian:12
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
build-essential pkg-config libdbus-1-dev curl ca-certificates \
|
||||
libclang-dev clang && \
|
||||
apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | \
|
||||
sh -s -- -y --default-toolchain stable --profile minimal
|
||||
ENV PATH="/root/.cargo/bin:${PATH}"
|
||||
RUN cargo install cargo-deb --version 3.6.3 --locked
|
||||
WORKDIR /src
|
||||
COPY Cargo.toml Cargo.lock build.rs LICENSE README.md ./
|
||||
COPY src ./src
|
||||
COPY packaging ./packaging
|
||||
COPY docs ./docs
|
||||
RUN cargo build --release && cargo deb --no-build
|
||||
DOCKERFILE
|
||||
|
||||
if [ ! "$(docker images -q "$builder_tag" 2>/dev/null)" ]; then
|
||||
echo " ERROR: builder image build failed"
|
||||
# Build through the shared container script rather than a builder defined
|
||||
# here. This suite used to compile its own package inside a Debian 12 image
|
||||
# while the release compiled on the newest GitHub runner, so the package it
|
||||
# tested had a lower glibc floor than the package users received and could
|
||||
# not exhibit a defect that only the release environment produced. It stayed
|
||||
# green through five releases that could not start on two of the five
|
||||
# distributions in its own matrix.
|
||||
log "Building the .deb in the pinned build container (slow on first run)"
|
||||
if ! bash "$REPO_ROOT/packaging/debian/build-deb-container.sh" \
|
||||
--output-dir "$DEB_CACHE_DIR" >&2; then
|
||||
echo " ERROR: container build failed" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
log "Extracting .deb from builder image"
|
||||
rm -f "$DEB_CACHE_DIR"/*.deb
|
||||
local cid
|
||||
cid=$(docker create "$builder_tag")
|
||||
docker cp "$cid:/src/target/debian/." "$DEB_CACHE_DIR/" >/dev/null 2>&1
|
||||
docker rm "$cid" >/dev/null
|
||||
# Cargo-deb leaves intermediate artifacts; keep just the .deb.
|
||||
find "$DEB_CACHE_DIR" -mindepth 1 -not -name 'fips_*_amd64.deb' -delete 2>/dev/null || true
|
||||
cached_deb=$(ls "$DEB_CACHE_DIR"/fips_*_amd64.deb 2>/dev/null | head -1)
|
||||
if [ -n "$cached_deb" ]; then
|
||||
log "Cached at $cached_deb ($(stat -c %s "$cached_deb") bytes)"
|
||||
else
|
||||
echo " ERROR: no .deb produced by cargo-deb"
|
||||
echo " ERROR: no .deb produced by the container build" >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
@@ -349,8 +398,8 @@ DOCKERFILE
|
||||
|
||||
# Start the services as a simulated boot. (On a real system,
|
||||
# they'd come up on next reboot.)
|
||||
docker exec "$name" systemctl start fips.service 2>&1 || true
|
||||
docker exec "$name" systemctl start fips-dns.service 2>&1 || true
|
||||
start_unit "$name" fips.service || true
|
||||
start_unit_queued "$name" fips-dns.service || true
|
||||
|
||||
if wait_for_service_active "$name" fips.service; then
|
||||
pass "fips.service active after explicit start"
|
||||
@@ -381,10 +430,21 @@ DOCKERFILE
|
||||
return
|
||||
fi
|
||||
|
||||
# Wait for fips-dns.service. This should have run fips-dns-setup
|
||||
# which configures the resolver backend and writes
|
||||
# /run/fips/dns-backend.
|
||||
sleep 2
|
||||
# Wait for fips-dns.service to finish. Its start job is queued rather than
|
||||
# waited on above, so this is what makes /run/fips/dns-backend safe to read:
|
||||
# fips-dns-setup waits up to 30s for fips0 and restarts systemd-resolved
|
||||
# before it writes that file, so reading it on a timer races the setup.
|
||||
# RemainAfterExit=yes makes is-active correct for this oneshot.
|
||||
if wait_for_service_active "$name" fips-dns.service; then
|
||||
pass "fips-dns.service completed"
|
||||
else
|
||||
fail "fips-dns.service did not complete in ${SERVICE_TIMEOUT}s"
|
||||
echo " --- fips-dns.service journal ---"
|
||||
docker exec "$name" journalctl -u fips-dns.service --no-pager 2>&1 | tail -20
|
||||
cleanup_container "$name"
|
||||
return
|
||||
fi
|
||||
|
||||
local backend
|
||||
backend=$(docker exec "$name" cat /run/fips/dns-backend 2>/dev/null || echo "(missing)")
|
||||
local ver
|
||||
@@ -441,7 +501,7 @@ DOCKERFILE
|
||||
# default preset) and ipv6 forwarding (gateway checks before
|
||||
# the DNS upstream check).
|
||||
docker exec "$name" sysctl -w net.ipv6.conf.all.forwarding=1 >/dev/null 2>&1 || true
|
||||
docker exec "$name" bash -c '
|
||||
timeout "$CONFIG_RESTART_TIMEOUT" docker exec "$name" bash -c '
|
||||
systemctl unmask fips-gateway.service 2>/dev/null
|
||||
# Patch in a minimal gateway config since the shipped fips.yaml
|
||||
# has gateway disabled by default.
|
||||
@@ -482,7 +542,7 @@ EOF
|
||||
fail "non-root fips group member cannot reach control socket after restart"
|
||||
fi
|
||||
|
||||
docker exec "$name" systemctl start fips-gateway.service >/dev/null 2>&1 || true
|
||||
start_unit "$name" fips-gateway.service "$GATEWAY_START_TIMEOUT" >/dev/null 2>&1 || true
|
||||
sleep 3
|
||||
if docker exec "$name" journalctl -u fips-gateway.service --no-pager 2>/dev/null \
|
||||
| grep -q "DNS upstream is reachable"; then
|
||||
@@ -509,6 +569,35 @@ test_ubuntu26() { _run_deb_install_scenario ubuntu26 ubuntu:26.04; }
|
||||
|
||||
ALL_SCENARIOS="debian12 debian13 ubuntu22 ubuntu24 ubuntu26"
|
||||
|
||||
# `--deb PATH` installs a package the caller already built, which is how one
|
||||
# build serves all five distributions and how GitHub CI and a local run come to
|
||||
# do the same work: both build once through the container script and hand the
|
||||
# result here. Keep ALL_SCENARIOS above on its own line at column zero;
|
||||
# check-ci-parity.sh reads it to compare this matrix against the GitHub one.
|
||||
_args=()
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--deb)
|
||||
SUPPLIED_DEB="${2:?--deb requires a path}"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
echo "usage: test.sh [--deb PATH] [scenario ...]"
|
||||
echo "scenarios: $ALL_SCENARIOS"
|
||||
exit 0
|
||||
;;
|
||||
-*)
|
||||
echo "Unknown option: $1" >&2
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
_args+=("$1")
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
set -- ${_args[@]+"${_args[@]}"}
|
||||
|
||||
if [ $# -eq 0 ]; then
|
||||
scenarios="$ALL_SCENARIOS"
|
||||
else
|
||||
|
||||
Reference in New Issue
Block a user