Files
fips/packaging/openwrt-ipk/build-ipk.sh
Johnathan Corgan 49ed35af28 Find the OpenWrt package builders' binaries where cargo put them
build-ipk.sh and build-apk.sh, run without --bin-dir, compiled with
cargo zigbuild and then looked for the binaries under target/ in the
checkout. A build with CARGO_TARGET_DIR, CARGO_BUILD_TARGET_DIR or a
build.target-dir setting in any cargo config puts them elsewhere, so the
builders failed after compiling, at `du: .../target/<triple>/release/fips:
No such file or directory`. Both now ask `cargo metadata` for the target
directory, read with sed so a build host needs no jq or Python, and stop
with a clear error when it cannot be read rather than fall back to
target/.

The apk README describes packaging in an alpine:3.23 container, whose
own apk-tools 3.0.8 builds packages OpenWrt 25.12 installs, instead of
building apk-tools from source. It passes PKG_VERSION and APK_VERSION
from the host, since without git in the container both fall back
silently to versions that sort below an installed development build.
Both READMEs give the flags that install or upgrade on a router with no
internet access (`--no-network --repositories-file /dev/null`).

package-test.sh builds both packages without --bin-dir against a cargo
stub, once with each environment variable, and checks the packages hold
the binaries cargo wrote there.
2026-10-15 00:28:52 +00:00

303 lines
11 KiB
Bash
Executable File

#!/bin/bash
# Build a FIPS .ipk package for OpenWrt without the OpenWrt SDK.
#
# Uses cargo-zigbuild for cross-compilation and assembles the .ipk directly.
# An .ipk is just an ar archive containing two tarballs — no SDK required.
#
# Usage:
# ./packaging/openwrt-ipk/build-ipk.sh [--arch <name>]
#
# Architectures (--arch):
# aarch64 GL.iNet MT3000/MT6000, RPi 3/4/5, most modern routers [default]
# mipsel Older MIPS routers (TP-Link, Netgear, GL.iNet AR750)
# mips MIPS big-endian routers (ath79)
# arm 32-bit ARM routers (Cortex-A7)
# x86_64 x86 routers / VMs
#
# Output: dist/fips_<PKG_VERSION>_<openwrt-arch>.ipk
#
# Environment:
# PKG_VERSION label for the file name (default: git describe)
# IPK_VERSION control file Version (default: PKG_VERSION). CI passes a
# release candidate as vX.Y.Z~rcN, which opkg sorts below the
# release, while PKG_VERSION keeps the tag's vX.Y.Z-rcN.
#
# Prerequisites:
# cargo install cargo-zigbuild
# rustup target add <rust-triple> (added automatically if missing)
set -euo pipefail
# ---------------------------------------------------------------------------
# Arguments
# ---------------------------------------------------------------------------
ARCH="aarch64"
BIN_DIR="" # if set, use prebuilt binaries from here instead of compiling
while [[ $# -gt 0 ]]; do
case "$1" in
--arch) ARCH="$2"; shift 2 ;;
--arch=*) ARCH="${1#*=}"; shift ;;
--bin-dir) BIN_DIR="$2"; shift 2 ;;
--bin-dir=*) BIN_DIR="${1#*=}"; shift ;;
*) echo "Unknown argument: $1" >&2; exit 1 ;;
esac
done
# ---------------------------------------------------------------------------
# Architecture mapping
#
# RUST_TARGET — passed to cargo --target
# OPENWRT_ARCH — goes in the .ipk control file and filename
# ---------------------------------------------------------------------------
case "$ARCH" in
aarch64)
RUST_TARGET="aarch64-unknown-linux-musl"
OPENWRT_ARCH="aarch64_cortex-a53"
;;
mipsel)
RUST_TARGET="mipsel-unknown-linux-musl"
OPENWRT_ARCH="mipsel_24kc"
;;
mips)
RUST_TARGET="mips-unknown-linux-musl"
OPENWRT_ARCH="mips_24kc"
;;
arm)
RUST_TARGET="arm-unknown-linux-musleabihf"
OPENWRT_ARCH="arm_cortex-a7"
;;
x86_64)
RUST_TARGET="x86_64-unknown-linux-musl"
OPENWRT_ARCH="x86_64"
;;
*)
echo "Unknown arch: $ARCH" >&2
echo "Valid: aarch64, mipsel, mips, arm, x86_64" >&2
exit 1
;;
esac
# ---------------------------------------------------------------------------
# Paths
# ---------------------------------------------------------------------------
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
FILES_DIR="$SCRIPT_DIR/files"
SCRIPTS_SRC="$SCRIPT_DIR/scripts" # maintainer scripts (metadata, not payload)
DIST_DIR="$PROJECT_ROOT/dist"
PKG_NAME="fips"
PKG_VERSION="${PKG_VERSION:-$(cd "$PROJECT_ROOT" && git describe --tags --always --dirty 2>/dev/null || echo "0.1.0")}"
IPK_VERSION="${IPK_VERSION:-$PKG_VERSION}"
if [ "$IPK_VERSION" = "$PKG_VERSION" ]; then
echo "==> Building $PKG_NAME $PKG_VERSION for $OPENWRT_ARCH ($RUST_TARGET)"
else
echo "==> Building $PKG_NAME $PKG_VERSION (control Version $IPK_VERSION) for $OPENWRT_ARCH ($RUST_TARGET)"
fi
# ---------------------------------------------------------------------------
# 1. Obtain binaries
#
# Either use a directory of prebuilt binaries (--bin-dir; CI cross-compiles
# once in a shared job and hands them to both the .ipk and .apk packagers), or
# compile from source here for a self-contained local build.
# ---------------------------------------------------------------------------
if [ -n "$BIN_DIR" ]; then
RELEASE_DIR="$BIN_DIR"
echo "==> Using prebuilt binaries from $RELEASE_DIR"
for bin in fips fipsctl fipstop fips-gateway; do
[ -f "$RELEASE_DIR/$bin" ] || {
echo "Error: prebuilt binary not found: $RELEASE_DIR/$bin" >&2
exit 1
}
done
else
if ! command -v cargo-zigbuild &>/dev/null; then
echo "Error: cargo-zigbuild not found." >&2
echo " Install: cargo install cargo-zigbuild" >&2
exit 1
fi
if ! rustup target list --installed | grep -q "^$RUST_TARGET$"; then
echo "==> Adding Rust target $RUST_TARGET..."
rustup target add "$RUST_TARGET"
fi
echo "==> Compiling..."
cd "$PROJECT_ROOT"
cargo zigbuild \
--release \
--target "$RUST_TARGET" \
--bin fips \
--bin fipsctl \
--bin fipstop \
--bin fips-gateway
# cargo writes under its target directory, which CARGO_TARGET_DIR,
# CARGO_BUILD_TARGET_DIR or build.target-dir in any config.toml can move
# away from target/, so ask cargo where it is. Read with sed, so a build
# host needs no jq or Python. Kept in sync with ../openwrt-apk/build-apk.sh.
TARGET_DIR="$(cargo metadata --format-version 1 --no-deps 2>/dev/null \
| sed -n 's/.*"target_directory":"\([^"]*\)".*/\1/p')" || TARGET_DIR=""
if [ -z "$TARGET_DIR" ] || [ ! -d "$TARGET_DIR" ]; then
echo "Error: could not read cargo's target directory from 'cargo metadata'" \
"(got '$TARGET_DIR')." >&2
exit 1
fi
RELEASE_DIR="$TARGET_DIR/$RUST_TARGET/release"
echo "==> Stripping binaries..."
STRIP="${LLVM_STRIP:-strip}"
for bin in fips fipsctl fipstop fips-gateway; do
"$STRIP" "$RELEASE_DIR/$bin" 2>/dev/null || true
done
fi
SIZE=$(du -sh "$RELEASE_DIR/fips" | cut -f1)
echo " fips: $SIZE"
# ---------------------------------------------------------------------------
# 2. Assemble .ipk
# ---------------------------------------------------------------------------
# An .ipk is an ar archive with three members:
# debian-binary — format version ("2.0\n")
# control.tar.gz — package metadata, conffiles, pre/post scripts
# data.tar.gz — the actual filesystem tree
WORK_DIR="$(mktemp -d)"
trap 'rm -rf "$WORK_DIR"' EXIT
CONTROL_DIR="$WORK_DIR/control"
DATA_DIR="$WORK_DIR/data"
mkdir -p "$CONTROL_DIR" "$DATA_DIR"
# ---- data tree ----
install -d "$DATA_DIR/usr/bin"
install -m 0755 "$RELEASE_DIR/fips" "$DATA_DIR/usr/bin/fips"
install -m 0755 "$RELEASE_DIR/fipsctl" "$DATA_DIR/usr/bin/fipsctl"
install -m 0755 "$RELEASE_DIR/fipstop" "$DATA_DIR/usr/bin/fipstop"
install -m 0755 "$RELEASE_DIR/fips-gateway" "$DATA_DIR/usr/bin/fips-gateway"
install -m 0755 "$FILES_DIR/usr/bin/fips-mesh-setup" "$DATA_DIR/usr/bin/fips-mesh-setup"
install -m 0755 "$FILES_DIR/usr/bin/fips-ap-setup" "$DATA_DIR/usr/bin/fips-ap-setup"
install -d "$DATA_DIR/etc/init.d"
install -m 0755 "$FILES_DIR/etc/init.d/fips" "$DATA_DIR/etc/init.d/fips"
install -m 0755 "$FILES_DIR/etc/init.d/fips-gateway" "$DATA_DIR/etc/init.d/fips-gateway"
install -d "$DATA_DIR/etc/fips"
install -m 0600 "$FILES_DIR/etc/fips/fips.yaml" "$DATA_DIR/etc/fips/fips.yaml"
install -d "$DATA_DIR/etc/sysctl.d"
install -m 0644 "$FILES_DIR/etc/sysctl.d/fips-bridge.conf" "$DATA_DIR/etc/sysctl.d/fips-bridge.conf"
install -m 0644 "$FILES_DIR/etc/sysctl.d/fips-gateway.conf" "$DATA_DIR/etc/sysctl.d/fips-gateway.conf"
# fw4 include: lets fips-gateway's port forwards through the fips zone. One
# source, installed for the zone's input and forward chains.
for chain in input_fips forward_fips; do
install -d "$DATA_DIR/usr/share/nftables.d/chain-post/$chain"
install -m 0644 "$FILES_DIR/usr/share/nftables.d/10-fips-port-forwards.nft" \
"$DATA_DIR/usr/share/nftables.d/chain-post/$chain/10-fips-port-forwards.nft"
done
# fw4 include: refuses ICMPv6 from wan into the mesh, ahead of the stock rule
# that accepts it into every zone.
install -d "$DATA_DIR/usr/share/nftables.d/chain-pre/forward_wan"
install -m 0644 "$FILES_DIR/usr/share/nftables.d/10-fips-wan-icmpv6.nft" \
"$DATA_DIR/usr/share/nftables.d/chain-pre/forward_wan/10-fips-wan-icmpv6.nft"
install -d "$DATA_DIR/etc/uci-defaults"
install -m 0755 "$FILES_DIR/etc/uci-defaults/90-fips-setup" "$DATA_DIR/etc/uci-defaults/90-fips-setup"
install -d "$DATA_DIR/lib/upgrade/keep.d"
install -m 0644 "$FILES_DIR/lib/upgrade/keep.d/fips" "$DATA_DIR/lib/upgrade/keep.d/fips"
# ---- control files ----
PKG_SIZE=$(du -sk "$DATA_DIR" | cut -f1)
cat > "$CONTROL_DIR/control" <<EOF
Package: $PKG_NAME
Version: $IPK_VERSION
Architecture: $OPENWRT_ARCH
Maintainer: FIPS Network
Section: net
Priority: optional
Depends: kmod-tun, kmod-br-netfilter, kmod-nft-nat, kmod-nf-conntrack, ip-full
Description: FIPS Mesh Network Daemon
Distributed, decentralized mesh networking over UDP, TCP, and raw Ethernet.
Provides a TUN interface (fips0) with ULA IPv6 addressing and a DNS
responder for .fips name resolution.
Installed-Size: $PKG_SIZE
EOF
# Mark fips.yaml as a conffile so opkg won't overwrite user edits on upgrade.
cat > "$CONTROL_DIR/conffiles" <<EOF
/etc/fips/fips.yaml
EOF
# The maintainer scripts live in files of their own rather than in heredocs
# here, so testing/openwrt/ can run the same bodies the package ships.
install -m 0755 "$SCRIPTS_SRC/postinst" "$CONTROL_DIR/postinst"
install -m 0755 "$SCRIPTS_SRC/prerm" "$CONTROL_DIR/prerm"
# ---- pack ----
PKG_FILENAME="${PKG_NAME}_${PKG_VERSION}_${OPENWRT_ARCH}.ipk"
IPK_WORK="$WORK_DIR/ipk"
mkdir -p "$IPK_WORK"
echo "2.0" > "$IPK_WORK/debian-binary"
# Detect a tar that supports --format=gnu.
# On macOS, Homebrew's GNU tar is installed as 'gtar'; the system tar is BSD.
# Our filenames are short so BSD tar (ustar) works too, but gnu is preferred
# to match ipkg-build exactly and to embed numeric UID/GID.
# COPYFILE_DISABLE=1 suppresses macOS resource-fork (._*) files; no-op on Linux.
if command -v gtar &>/dev/null; then
# Homebrew GNU tar on macOS
TAR_CMD="gtar"
TAR_EXTRA_FLAGS="--format=gnu --numeric-owner"
elif tar --version 2>/dev/null | grep -q 'GNU tar'; then
# System tar is GNU tar (Linux)
TAR_CMD="tar"
TAR_EXTRA_FLAGS="--format=gnu --numeric-owner"
else
# macOS BSD tar (libarchive). Its default format is PAX (typeflag 0x78),
# which OpenWrt's busybox tar cannot handle. Force ustar explicitly.
TAR_CMD="tar"
TAR_EXTRA_FLAGS="--format=ustar"
fi
ipk_tar() {
# ipk_tar <output.tar.gz> <source-dir> [paths...]
local out="$1" src="$2"; shift 2
local mtime_flags=""
if [ -n "${SOURCE_DATE_EPOCH:-}" ]; then
mtime_flags="--mtime=@$SOURCE_DATE_EPOCH"
fi
COPYFILE_DISABLE=1 "$TAR_CMD" $TAR_EXTRA_FLAGS $mtime_flags -czf "$out" -C "$src" "$@"
}
ipk_tar "$IPK_WORK/control.tar.gz" "$CONTROL_DIR" .
ipk_tar "$IPK_WORK/data.tar.gz" "$DATA_DIR" .
# The outer .ipk container is a gzip-compressed tar — NOT an ar archive.
# (Debian .deb uses ar; OpenWrt .ipk uses tar.gz.)
# Entries must be named with ./ prefix, as ipkg-build produces.
mkdir -p "$DIST_DIR"
ipk_tar "$DIST_DIR/$PKG_FILENAME" "$IPK_WORK" ./debian-binary ./control.tar.gz ./data.tar.gz
echo ""
echo "==> Done: dist/$PKG_FILENAME"
echo " $(du -sh "$DIST_DIR/$PKG_FILENAME" | cut -f1)"
echo ""
echo "Install on router:"
echo " scp -O dist/$PKG_FILENAME root@192.168.1.1:/tmp/"
echo " ssh root@192.168.1.1 opkg install /tmp/$PKG_FILENAME"