diff --git a/packaging/openwrt-ipk/Makefile b/packaging/openwrt-ipk/Makefile index cabd0380..a3fdbfa7 100644 --- a/packaging/openwrt-ipk/Makefile +++ b/packaging/openwrt-ipk/Makefile @@ -38,7 +38,10 @@ else ifeq ($(ARCH),arm) # OpenWrt ARM targets predominantly use hardfloat ABI. # Override RUST_TARGET in your build if your target uses softfloat. RUST_TARGET:=arm-unknown-linux-musleabihf -else +else ifeq ($(DUMP),) + # Not while OpenWrt scans package metadata (DUMP=1): the scan does not read + # the target config, so ARCH is empty then, and stopping here would leave the + # package out of the build entirely. $(error Unsupported architecture: $(ARCH). Add a RUST_TARGET mapping in packaging/openwrt-ipk/Makefile.) endif @@ -107,7 +110,8 @@ define Package/fips/install $(INSTALL_BIN) $(CURDIR)/files/etc/init.d/fips $(1)/etc/init.d/fips $(INSTALL_BIN) $(CURDIR)/files/etc/init.d/fips-gateway $(1)/etc/init.d/fips-gateway - # Default config — installed as CONF so opkg will not overwrite it on upgrade + # Default config, mode 0600. Package/fips/conffiles below is what keeps an + # edited copy across an upgrade. $(INSTALL_DIR) $(1)/etc/fips $(INSTALL_CONF) $(CURDIR)/files/etc/fips/fips.yaml $(1)/etc/fips/fips.yaml @@ -132,4 +136,26 @@ define Package/fips/install $(INSTALL_DATA) $(CURDIR)/files/lib/upgrade/keep.d/fips $(1)/lib/upgrade/keep.d/fips endef +# A user's edits to the config survive an upgrade. +define Package/fips/conffiles +/etc/fips/fips.yaml +endef + +# Maintainer scripts, read from scripts/ so this package runs the same +# postinst and prerm bodies that build-ipk.sh and build-apk.sh install. OpenWrt +# wraps those two in its generated scripts, around default_postinst and +# default_prerm. The preinst is used only here: it keeps fips-gateway disabled +# and stopped through default_postinst's enable-and-start loop. +define Package/fips/preinst +$(file < $(CURDIR)/scripts/preinst) +endef + +define Package/fips/postinst +$(file < $(CURDIR)/scripts/postinst) +endef + +define Package/fips/prerm +$(file < $(CURDIR)/scripts/prerm) +endef + $(eval $(call BuildPackage,fips)) diff --git a/packaging/openwrt-ipk/README.md b/packaging/openwrt-ipk/README.md index 30f84c22..fac25610 100644 --- a/packaging/openwrt-ipk/README.md +++ b/packaging/openwrt-ipk/README.md @@ -95,11 +95,12 @@ make package/fips/compile V=s The resulting `.ipk` is placed in `bin/packages//`. -A package built from this `Makefile` carries none of the maintainer scripts in -`scripts/`. Those scripts enable and start `fips` on install and implement the -gateway-enablement and upgrade behavior described below, so that description -does not cover a package built this way. Released packages are built by -`build-ipk.sh` (and `../openwrt-apk/build-apk.sh`), which install those scripts. +Installed on a router, the package enables and starts `fips` and leaves +`fips-gateway` disabled. Built into a firmware image, it is different: the +maintainer scripts do nothing at image build time, and the image build enables +every init script a package ships, `fips-gateway` included. To build an image +with the gateway off, pass `DISABLED_SERVICES="fips-gateway"` to the image +builder's `make image`. ### 4. Pin the source version diff --git a/packaging/openwrt-ipk/files/etc/init.d/fips-gateway b/packaging/openwrt-ipk/files/etc/init.d/fips-gateway index e1a0835d..bc897b79 100755 --- a/packaging/openwrt-ipk/files/etc/init.d/fips-gateway +++ b/packaging/openwrt-ipk/files/etc/init.d/fips-gateway @@ -34,6 +34,8 @@ GW_PIDFILE=/var/run/fips-gateway.pid # Seconds a stopping gateway gets after SIGTERM before it is killed; under # procd's own 5 s, after which procd kills only the supervise shell. GW_KILL_AFTER=3 +# Left by the package's preinst when the gateway is not enabled; see below. +INSTALL_HOLD=/var/run/fips-gateway-install-hold # Global-scope IPv6 prefix assigned to br-lan so Android/Chrome clients # believe they have full IPv6 and actually send AAAA queries. @@ -41,6 +43,18 @@ GW_KILL_AFTER=3 GLOBAL_PREFIX="2001:2:f1b5::1/64" start_service() { + # OpenWrt's generic postinst enables and starts every init script a + # package ships. A package built from the SDK feed Makefile runs it, and + # its preinst leaves this hold unless the gateway was enabled, so that + # installing or upgrading the package does not switch the gateway on. + # Honoured once. + if [ -e "$INSTALL_HOLD" ]; then + rm -f "$INSTALL_HOLD" + disable + logger -t fips-gateway "left disabled and stopped by the package installation" + return 0 + fi + # The gateway daemon exits when gateway.enabled is not true, so without # this check starting a disabled gateway would still take dnsmasq's .fips # upstream away from the daemon and point it at a port nothing listens on. diff --git a/packaging/openwrt-ipk/scripts/postinst b/packaging/openwrt-ipk/scripts/postinst index 18361729..68f4ab83 100755 --- a/packaging/openwrt-ipk/scripts/postinst +++ b/packaging/openwrt-ipk/scripts/postinst @@ -23,8 +23,24 @@ # Under apk, a fresh install runs this as post-install and the gateway stays # off. An upgrade runs it as post-upgrade, after the .apk pre-upgrade script # (the prerm body) has stopped the services and left the marker. +# +# A package built from the SDK feed Makefile runs this body from OpenWrt's +# generated postinst, beside a generic loop that enables and starts every init +# script; the preinst there leaves a hold that keeps the gateway out of that +# loop. When this body decides the gateway is to be enabled, it removes the +# hold first. See scripts/preinst. +# +# The upgrade marker stays in /tmp: an installed 0.5.2 package's prerm writes +# it there, and this script must find it to leave a disabled gateway disabled. + +# An image build (OpenWrt's image builder or buildroot) runs this on the build +# host with IPKG_INSTROOT naming the image's root. Nothing here may touch the +# host, so it does nothing there; see the README for the gateway in images. + +[ -n "${IPKG_INSTROOT:-}" ] && exit 0 UPGRADE_MARKER=/tmp/fips-prerm-upgrade +INSTALL_HOLD=/var/run/fips-gateway-install-hold # Run first-boot UCI setup (the script deletes itself when done). if [ -x /etc/uci-defaults/90-fips-setup ]; then @@ -38,6 +54,7 @@ if [ "${PKG_UPGRADE:-0}" = "1" ]; then if [ -e "$UPGRADE_MARKER" ]; then rm -f "$UPGRADE_MARKER" else + rm -f "$INSTALL_HOLD" /etc/init.d/fips-gateway enable fi diff --git a/packaging/openwrt-ipk/scripts/preinst b/packaging/openwrt-ipk/scripts/preinst new file mode 100755 index 00000000..96b11c20 --- /dev/null +++ b/packaging/openwrt-ipk/scripts/preinst @@ -0,0 +1,43 @@ +#!/bin/sh +# Maintainer script run before the FIPS package's files are unpacked. +# +# Used only by the OpenWrt SDK feed Makefile, as the package's preinst under +# opkg and its pre-install and pre-upgrade scripts under apk. build-ipk.sh and +# build-apk.sh ship a postinst of their own and do not need it. +# +# A package built in the SDK gets OpenWrt's generated postinst, whose +# default_postinst enables every init script the package installs on a fresh +# install and starts every one on every install and upgrade. The package's own +# postinst body runs before that loop under opkg and after it under apk, so the +# body cannot keep the gateway off by itself. Instead, unless the gateway is +# enabled now, this leaves a hold that init.d/fips-gateway honours once: its +# start_service removes the hold, disables the service and starts nothing. A +# fresh install therefore leaves the gateway disabled and stopped, and an +# upgrade does not start a gateway the operator disabled. +# +# An apk upgrade runs no script of the outgoing package, so nothing else stops +# the services or tells the postinst body that enablement survived the upgrade. +# This does what the outgoing package's prerm does on an opkg upgrade. + +# An image build (OpenWrt's image builder or buildroot) runs this on the build +# host with IPKG_INSTROOT naming the image's root. Nothing here may touch the +# host, so it does nothing there; see the README for the gateway in images. + +[ -n "${IPKG_INSTROOT:-}" ] && exit 0 + +INSTALL_HOLD=/var/run/fips-gateway-install-hold +UPGRADE_MARKER=/tmp/fips-prerm-upgrade + +# opkg passes "install" or "upgrade "; apk passes versions only. +if [ "${PKG_UPGRADE:-0}" = "1" ] && [ "${1:-}" != "upgrade" ]; then + : > "$UPGRADE_MARKER" 2>/dev/null || true + /etc/init.d/fips-gateway stop 2>/dev/null || true + /etc/init.d/fips stop 2>/dev/null || true +fi + +if ! /etc/init.d/fips-gateway enabled 2>/dev/null; then + mkdir -p "${INSTALL_HOLD%/*}" 2>/dev/null + : > "$INSTALL_HOLD" 2>/dev/null || true +fi + +exit 0 diff --git a/packaging/openwrt-ipk/scripts/prerm b/packaging/openwrt-ipk/scripts/prerm index 09dcf365..c3911d7e 100755 --- a/packaging/openwrt-ipk/scripts/prerm +++ b/packaging/openwrt-ipk/scripts/prerm @@ -10,10 +10,21 @@ # because nothing records it anywhere else, so an upgrade only stops them and # leaves a marker telling the incoming postinst that enablement survived. # A real removal stops and disables both, as before. +# +# A package built from the SDK feed Makefile runs this body from OpenWrt's +# generated prerm, which passes the script's own path first, so "upgrade" is +# then the second argument. opkg exports PKG_UPGRADE=1 for an upgrade either +# way, and apk runs this only on a removal. + +# An image build (OpenWrt's image builder or buildroot) runs this on the build +# host with IPKG_INSTROOT naming the image's root. Nothing here may touch the +# host, so it does nothing there; see the README for the gateway in images. + +[ -n "${IPKG_INSTROOT:-}" ] && exit 0 UPGRADE_MARKER=/tmp/fips-prerm-upgrade -if [ "$1" = "upgrade" ]; then +if [ "${1:-}" = "upgrade" ] || [ "${PKG_UPGRADE:-0}" = "1" ]; then : > "$UPGRADE_MARKER" 2>/dev/null || true /etc/init.d/fips-gateway stop 2>/dev/null || true /etc/init.d/fips stop 2>/dev/null || true diff --git a/testing/openwrt/maintainer-scripts-test.sh b/testing/openwrt/maintainer-scripts-test.sh index 8d73dc4b..a2cf9643 100755 --- a/testing/openwrt/maintainer-scripts-test.sh +++ b/testing/openwrt/maintainer-scripts-test.sh @@ -57,6 +57,7 @@ docker run --rm --network none \ -e APK_SCRIPTS=/apk \ -e "POSTINST=${POSTINST:-}" \ -e "PRERM=${PRERM:-}" \ + -e "PREINST=${PREINST:-}" \ -e "INIT_GATEWAY=${INIT_GATEWAY:-}" \ "$IMAGE" sh /src/testing/openwrt/scenarios.sh rc=$? diff --git a/testing/openwrt/package-test.sh b/testing/openwrt/package-test.sh index e6f8056c..b71f38cd 100755 --- a/testing/openwrt/package-test.sh +++ b/testing/openwrt/package-test.sh @@ -281,7 +281,8 @@ for path in ./postinst ./prerm; do done # ── P4. No source still names the drop-in ─────────────────────────────────── -# This is the only check on the SDK feed Makefile, which nothing here builds. +# With P5, one of the two checks on the SDK feed Makefile, which nothing here +# builds. # grep exits 1 when nothing matches and 2 when it could not read a path; only # the first is a pass. (cd "$PROJECT_ROOT" && grep -rlF 'dnsmasq.d/fips.conf' \ @@ -301,6 +302,31 @@ else ok "P4 the drop-in source file is gone" fi +# ── P5. The SDK feed Makefile ships the same maintainer scripts ───────────── +# Read, not built: building it needs the OpenWrt SDK. Each script must be the +# file in scripts/ that the scenarios run, and fips.yaml must be a conffile. +MAKEFILE="$PROJECT_ROOT/packaging/openwrt-ipk/Makefile" +[[ -r "$MAKEFILE" ]] || harness_fail "cannot read $MAKEFILE" +for script in preinst postinst prerm; do + if awk -v want="define Package/fips/$script" -v body="\$(file < \$(CURDIR)/scripts/$script)" ' + $0 == want { inside = 1; next } + inside && $0 == body { found = 1 } + inside && $0 == "endef" { inside = 0 } + END { exit !found }' "$MAKEFILE"; then + ok "P5 the SDK Makefile's $script is scripts/$script" + else + bad "P5 the SDK Makefile does not define Package/fips/$script as scripts/$script" + fi +done +if awk '$0 == "define Package/fips/conffiles" { inside = 1; next } + inside && $0 == "/etc/fips/fips.yaml" { found = 1 } + inside && $0 == "endef" { inside = 0 } + END { exit !found }' "$MAKEFILE"; then + ok "P5 the SDK Makefile lists /etc/fips/fips.yaml as a conffile" +else + bad "P5 the SDK Makefile does not list /etc/fips/fips.yaml as a conffile" +fi + # ── Hand the scripts to the ash scenarios ─────────────────────────────────── if [[ -n "$KEEP" ]]; then for phase in $WANT_PHASES; do diff --git a/testing/openwrt/scenarios.sh b/testing/openwrt/scenarios.sh index d0818f22..bb91d279 100755 --- a/testing/openwrt/scenarios.sh +++ b/testing/openwrt/scenarios.sh @@ -8,7 +8,7 @@ # exercised is the scripts' behaviour given that contract, not opkg itself. # A real `opkg upgrade` on a router image stays uncovered. # -# POSTINST, PRERM and INIT_GATEWAY may be pointed at other files. That is the +# PREINST, POSTINST, PRERM and INIT_GATEWAY may be pointed at other files. That is the # seam used to see a scenario red against the previously released scripts, and # to re-break the fixed ones during a break-check. # @@ -23,6 +23,7 @@ set -u REPO="${REPO:-/src}" POSTINST="${POSTINST:-$REPO/packaging/openwrt-ipk/scripts/postinst}" +PREINST="${PREINST:-$REPO/packaging/openwrt-ipk/scripts/preinst}" PRERM="${PRERM:-$REPO/packaging/openwrt-ipk/scripts/prerm}" RELEASED_PRERM="$REPO/testing/openwrt/fixtures/released-prerm" INIT_GATEWAY="${INIT_GATEWAY:-$REPO/packaging/openwrt-ipk/files/etc/init.d/fips-gateway}" @@ -1140,9 +1141,204 @@ scenario_supervise() { return 0 } +# ── 16. A package built from the SDK feed Makefile ───────────────────────── +# OpenWrt generates that package's postinst and prerm around default_postinst +# and default_prerm (package/base-files/files/lib/functions.sh, read at OpenWrt +# main and openwrt-24.10). After its own steps, default_postinst runs a loop +# over the package's init scripts: "enable" unless PKG_UPGRADE is 1, then +# "start". Under opkg the package's postinst body is sourced before that loop; +# under apk it is appended to the generated script and runs after it. +# default_prerm sources the prerm body with the generated script's own path as +# $1, then disables (on a removal) and stops each init script. The preinst is +# the package's own, run as opkg runs it ("install" or "upgrade ") or as +# apk runs it (versions only, PKG_UPGRADE=1 exported on an upgrade). +# +# /etc/init.d/fips is the recording stub. /etc/init.d/fips-gateway is the real +# script, run through a stand-in rc.common that keeps enablement as the +# /etc/rc.d links OpenWrt's does and records whether start_service opened a +# procd instance, which is what starting the gateway means. + +install_sdk_env() { + install_uci_stub + printf '#!/bin/sh\nexit 0\n' > "$STUB_BIN/logger" + chmod 0755 "$STUB_BIN/logger" + rm -rf /etc/rc.d + mkdir -p /etc/rc.d /etc/fips /var/run + cp "$SHIPPED_YAML" /etc/fips/fips.yaml + cp "$INIT_GATEWAY" /etc/init.d/fips-gateway + chmod 0755 /etc/init.d/fips-gateway + rm -f /var/run/fips-gateway-install-hold + cat > /etc/rc.common <<'SHIM' +#!/bin/sh +# Stand-in for OpenWrt's rc.common: enablement as /etc/rc.d links, and start +# runs start_service with procd's calls recorded instead of made. +initscript="$1" +action="$2" +shift 2 +name="${initscript##*/}" +enable() { ln -sf "../init.d/$name" "/etc/rc.d/S${START}$name"; ln -sf "../init.d/$name" "/etc/rc.d/K${STOP}$name"; } +disable() { rm -f /etc/rc.d/S??"$name" /etc/rc.d/K??"$name"; } +enabled() { [ -L "/etc/rc.d/S${START}$name" ]; } +procd_open_instance() { echo "$name instance" >> "$CALLS"; } +procd_set_param() { :; } +procd_close_instance() { :; } +stop_service() { :; } +. "$initscript" +sysctl() { :; } +modprobe() { :; } +gateway_add_global_prefix() { :; } +gateway_add_ra_route() { :; } +stop_service() { :; } +echo "$name $action" >> "$CALLS" +case "$action" in +start) start_service ;; +stop) stop_service ;; +*) "$action" "$@" ;; +esac +SHIM + chmod 0755 /etc/rc.common + return 0 +} + +remove_sdk_env() { + rm -rf /etc/rc.d /etc/fips "$STUB_BIN" + rm -f /etc/rc.common /var/run/fips-gateway-install-hold + return 0 +} + +sdk_loop() { + # The init-script loop of default_postinst. + for i in /etc/init.d/fips /etc/init.d/fips-gateway; do + if [ "${PKG_UPGRADE:-0}" != "1" ]; then + "$i" enable + fi + "$i" start + done + return 0 +} + +sdk_postinst() { + # sdk_postinst ipk|apk + if [ "$1" = "ipk" ]; then + ( set -- /usr/lib/opkg/info/fips.postinst configure; . "$POSTINST" ) >/dev/null 2>&1 + sdk_loop >/dev/null 2>&1 + else + sdk_loop >/dev/null 2>&1 + sh "$POSTINST" 2.0-r1 >/dev/null 2>&1 + fi + return 0 +} + +sdk_prerm_upgrade_ipk() { + # default_prerm for the outgoing SDK-built package on an opkg upgrade. + ( set -- /usr/lib/opkg/info/fips.prerm upgrade 2.0-r1; . "$PRERM" ) >/dev/null 2>&1 + for i in /etc/init.d/fips /etc/init.d/fips-gateway; do + "$i" stop >/dev/null 2>&1 + done + return 0 +} + +sdk_preinst() { + # sdk_preinst , run as a script, as opkg and apk run it. + if [ ! -f "$PREINST" ]; then + bad "there is no preinst at $PREINST" + return 0 + fi + sh "$PREINST" "$@" >/dev/null 2>&1 + return 0 +} + +assert_gateway() { + # assert_gateway + if [ -L /etc/rc.d/S96fips-gateway ]; then got=enabled; else got=disabled; fi + assert_equals "$got" "$1" "$3: fips-gateway ends $1" + if grep -qxF "fips-gateway instance" "$CALLS"; then got=started; else got=stopped; fi + assert_equals "$got" "$2" "$3: fips-gateway is $2" + assert_absent /var/run/fips-gateway-install-hold "$3: no install hold is left behind" + return 0 +} + +scenario_sdk_package() { + note "scenario 16: SDK feed package, default_postinst and default_prerm" + saved_path="$PATH" + PATH="$STUB_BIN:$PATH" + export PATH + + for fmt in ipk apk; do + reset_state + install_sdk_env + if [ "$fmt" = "ipk" ]; then + PKG_UPGRADE=0 sdk_preinst install + PKG_UPGRADE=0 sdk_postinst ipk + else + sdk_preinst 2.0-r1 + sdk_postinst apk + fi + assert_called "fips start" "$fmt fresh install: the daemon is started" + assert_file_is "$FIPS_STATE" "1" "$fmt fresh install: the daemon is enabled" + assert_gateway disabled stopped "$fmt fresh install" + remove_sdk_env + + for gw in enabled disabled; do + reset_state + install_sdk_env + echo 1 > "$FIPS_STATE" + [ "$gw" = "enabled" ] && /etc/init.d/fips-gateway enable >/dev/null 2>&1 + : > "$CALLS" + if [ "$fmt" = "ipk" ]; then + PKG_UPGRADE=1 sdk_prerm_upgrade_ipk + PKG_UPGRADE=1 sdk_preinst upgrade 1.0-r1 + PKG_UPGRADE=1 sdk_postinst ipk + else + PKG_UPGRADE=1 sdk_preinst 2.0-r1 1.0-r1 + PKG_UPGRADE=1 sdk_postinst apk + fi + if [ "$gw" = "enabled" ]; then + assert_gateway enabled started "$fmt upgrade, gateway enabled" + else + assert_gateway disabled stopped "$fmt upgrade, gateway disabled" + fi + assert_absent "$UPGRADE_MARKER" "$fmt upgrade, gateway $gw: the upgrade marker is removed" + remove_sdk_env + done + done + + # An opkg upgrade from a released package, whose prerm disabled the + # gateway and left no marker: the postinst body re-enables and starts it, + # as with build-ipk.sh, which needs it to clear the hold first. + reset_state + install_sdk_env + echo 1 > "$FIPS_STATE" + /etc/init.d/fips-gateway enable >/dev/null 2>&1 + : > "$CALLS" + PKG_UPGRADE=1 sh "$RELEASED_PRERM" upgrade 2.0-r1 >/dev/null 2>&1 + PKG_UPGRADE=1 sdk_preinst upgrade 0.5.1 + PKG_UPGRADE=1 sdk_postinst ipk + assert_gateway enabled started "ipk upgrade from a released package" + remove_sdk_env + + # An image build runs the scripts on the build host, with IPKG_INSTROOT + # naming the image root: they must not touch the host at all. + reset_state + rm -f /var/run/fips-gateway-install-hold "$UPGRADE_MARKER" + for pkg_upgrade in 0 1; do + IPKG_INSTROOT=/tmp/fips-image-root PKG_UPGRADE=$pkg_upgrade sh "$PREINST" install >/dev/null 2>&1 + IPKG_INSTROOT=/tmp/fips-image-root PKG_UPGRADE=$pkg_upgrade sh "$POSTINST" configure >/dev/null 2>&1 + IPKG_INSTROOT=/tmp/fips-image-root PKG_UPGRADE=$pkg_upgrade sh "$PRERM" upgrade 2.0-r1 >/dev/null 2>&1 + IPKG_INSTROOT=/tmp/fips-image-root PKG_UPGRADE=$pkg_upgrade sh "$PRERM" remove >/dev/null 2>&1 + done + assert_equals "$(calls_oneline)" "" "image build: no script touches the build host's services" + assert_absent /var/run/fips-gateway-install-hold "image build: the preinst leaves no hold on the build host" + assert_absent "$UPGRADE_MARKER" "image build: the prerm leaves no marker on the build host" + + PATH="$saved_path" + return 0 +} + echo "OpenWrt maintainer-script scenarios (shell: $(readlink -f /proc/$$/exe 2>/dev/null || echo sh))" echo " postinst: $POSTINST" echo " prerm: $PRERM" +echo " preinst: $PREINST" echo " apk: ${APK_SCRIPTS:-(not set)}" scenario_fresh_install @@ -1160,6 +1356,7 @@ scenario_default_port_parity scenario_swap_cleanup scenario_listen_migration scenario_supervise +scenario_sdk_package echo "" if [ "$FAILURES" -eq 0 ]; then