Files
fips/packaging/openwrt-ipk/README.md
Johnathan Corgan ade9690278 Set basic rates on the OpenWrt 802.11s mesh so 24.10 and 25.12 routers peer
802.11s mesh points peer only with neighbours whose basic rates match
their own. fips-mesh-setup set no basic_rate on the mesh interfaces it
created, and OpenWrt 24.10 and 25.12 then join with different ones:
24.10, joining through iw, takes the radio's, by default 6, 12 and 24
Mb/s on 2.4 GHz, while 25.12 passes none to the kernel, which uses 1, 2,
5.5 and 11 Mb/s. A 24.10 router and a 25.12 router heard each other's
beacons and never peered, while each still peered with routers on its
own release.

fips-mesh-setup now sets basic_rate '6000 12000 24000' on every mesh
interface it creates, which matches what 24.10 already used and costs
less airtime than the 802.11b rates. A basic_rate already on the
interface is the operator's and is kept, including when the helper is
run again for the same radio.

A new rates subcommand gives the same value to each mesh interface
fips-mesh-setup made that has none, keeps and names one the operator
set, and leaves every other wifi-iface alone. 90-fips-setup runs it on
install and upgrade, after the mesh zone step, so meshes made by older
packages get the rates too. It saves the wireless configuration with the
same check the firewall save uses, and keeps a copy for a save a full
overlay leaves damaged. It does not reload the radios itself, but the
rates apply at the next reboot or network reload of any kind, including
the one the gateway makes when an upgrade from 0.5.2 starts it, so the
upgrade can drop a router's peerings. The helper's line, the package
README and the mesh how-to say so, and have routers reached over the
mesh upgraded and reloaded one at a time, farthest first. A failed save
ends the setup with status 1, so it runs again at the next boot, and a
later run saves and names rates an earlier run left staged. When a
firewall save earlier in the setup failed, which can mean the overlay is
full, the setup leaves the wireless configuration alone, since a
wireless file cut short takes every radio down at the next reload, and
sets the rates at a later boot.

OpenWrt 22.03 and 23.05 pass no basic rates to a mesh join, so on 2.4
GHz they keep joining with 1, 2, 5.5 and 11 Mb/s whatever is set, and
stop peering with 24.10 and later routers once those are upgraded and
reloaded. The helper's closing message, the package README and the mesh
how-to say so, and give the rates to set on the newer routers to keep
such a router in a 2.4 GHz mesh. A router left on the old package also
stops peering with upgraded ones until it is upgraded and reloaded. The
README and the how-to say which rates the mesh uses, that a basic_rate
on the interface overrides one on the radio, what the upgrade does, and
how to apply the rates by hand after a reinstall.

The firewall scenarios check that an add sets the rates and its closing
message names the 22.03 and 23.05 limit, that an upgrade sets them on
the 0.5.2 helper's meshes without a wifi reload, leaves other
wifi-ifaces alone and keeps an operator's value, that a re-add keeps an
operator's value, that the line says any network reload applies the
rates and gives the upgrade order, that a failed wireless save keeps the
setup and the next boot saves the rates and says so, that a wireless
save a full overlay damages is reported and its copy restores the file,
and that a damaged firewall save leaves the wireless configuration
untouched.
2026-10-15 00:28:52 +00:00

25 KiB

FIPS OpenWrt Package

This directory is an OpenWrt feed package that builds and installs FIPS on any OpenWrt 22.03+ router via the standard opkg package system.

Package contents

Installed path Purpose
/usr/bin/fips Mesh daemon
/usr/bin/fipsctl CLI control tool (fipsctl show peers, fipsctl show links, …)
/usr/bin/fipstop Live TUI dashboard
/usr/bin/fips-gateway Outbound LAN gateway service (not started by default)
/usr/bin/fips-mesh-setup Opt-in helper — creates an open 802.11s mesh interface for router↔router backhaul, in the fips_mesh firewall zone, with basic rates 6, 12 and 24 Mb/s
/usr/bin/fips-ap-setup Opt-in helper — creates the open !FIPS access SSID for client devices
/etc/init.d/fips procd service for the daemon (auto-start, crash respawn)
/etc/init.d/fips-gateway procd service for the gateway (disabled by default)
/etc/fips/fips.yaml Node configuration (edit before first start)
/etc/sysctl.d/fips-bridge.conf Turns off br_netfilter's firewall call hooks
/etc/sysctl.d/fips-gateway.conf proxy_ndp and IPv6 forwarding for the gateway
/usr/share/nftables.d/chain-post/{input,forward}_fips/10-fips-port-forwards.nft Lets fips-gateway's port forwards through the fips zone
/usr/share/nftables.d/chain-pre/forward_wan/10-fips-wan-icmpv6.nft Refuses ICMPv6 from the wan zone into the mesh
/etc/uci-defaults/90-fips-setup First-boot kernel modules, the fips firewall zone and dnsmasq .fips forwarding
/lib/upgrade/keep.d/fips Preserves /etc/fips/ across sysupgrade

Requirements

Build host

Requirement Notes
OpenWrt SDK 22.03+ Older versions lack fw4 / nftables support
Rust host toolchain Enable in make menuconfig → Advanced → Rust, or install rustup
Rust target for your router Added automatically by the Makefile via rustup target add

Router

Requirement Notes
kmod-tun Required for fips0 TUN interface
kmod-br-netfilter Loaded, hooks off (fips-bridge.conf); see notes below

Both kernel modules are listed as package dependencies (DEPENDS) and will be installed automatically by opkg.

Target architectures

The Makefile maps the OpenWrt ARCH variable to the correct Rust musl target:

OpenWrt ARCH Rust target
aarch64 aarch64-unknown-linux-musl
x86_64 x86_64-unknown-linux-musl
mipsel mipsel-unknown-linux-musl
mips mips-unknown-linux-musl
arm arm-unknown-linux-musleabihf

To add a missing architecture, add an ifeq block in Makefile mapping the OpenWrt ARCH value to the Rust target triple.

Building with the OpenWrt SDK

1. Obtain the SDK

Download the SDK for your router's target from downloads.openwrt.org and extract it.

2. Add this package

Copy or symlink this directory into the SDK's package/ tree:

# From inside the SDK root:
ln -s /path/to/fips/packaging/openwrt-ipk package/fips

Or add the FIPS repository as a feed in feeds.conf:

src-git-full fips https://github.com/jmcorgan/fips.git

Then update and install feeds:

./scripts/feeds update fips
./scripts/feeds install -a -p fips

3. Build

make package/fips/compile V=s

The resulting .ipk is placed in bin/packages/<arch>/.

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

For reproducible production builds, replace PKG_SOURCE_VERSION:=master in Makefile with a specific commit SHA and set PKG_MIRROR_HASH to the correct hash (or keep skip for development):

PKG_SOURCE_VERSION:=bf117dfabc123...  # full 40-char SHA
PKG_MIRROR_HASH:=skip

Installing on the router

scp bin/packages/<arch>/fips_0.1.0-1_<arch>.ipk root@192.168.1.1:/tmp/
ssh root@192.168.1.1 opkg install /tmp/fips_0.1.0-1_<arch>.ipk

First-time configuration

Edit /etc/fips/fips.yaml on the router before starting the daemon:

ssh root@192.168.1.1
vi /etc/fips/fips.yaml

The default config enables:

  • An ephemeral identity, generated on each start. Uncomment node.identity.persistent: true to keep one; the key is then saved next to the config, as /etc/fips/fips.key.
  • TUN interface fips0
  • DNS responder on [::1]:5354
  • UDP transport on [::]:2121
  • TCP transport on 0.0.0.0:8443
  • Ethernet transport, including the wan, wwan and lan entries

For Ethernet transport, edit the interface names in the ethernet: section to match your router. For the LAN, bind the LAN bridge (br-lan), never one of its member ports (on DSA boards lan1..lanN; on others whichever ethN the bridge holds; bridge link lists them). A socket on a bridge member port sends frames but never forms a link, because the bridge takes the frames that arrive on its members; loading br_netfilter does not change that. A lab test with a two-member Linux bridge found this with br_netfilter unloaded, loaded with its call hooks off, and loaded with them on, while a socket on br-lan worked both with br_netfilter unloaded and with it loaded as shipped. Ports outside any bridge bind by their own name. The shipped default WAN port is eth0 (OpenWrt 24); on OpenWrt 25 (DSA) boards the WAN port is named wan, and the .apk package ships that default. Run ip link show to confirm the names on your board.

The lab used software bridges only. A DSA switch with hardware bridge offload has not been checked, so confirm the LAN entry forms links on such a router. kmod-br-netfilter, fips-bridge.conf and the module load in 90-fips-setup are kept until their removal has been checked on a router.

Firewall

The package puts fips0 in a firewall zone of its own, fips, created at install:

config zone 'fips'
        option name 'fips'
        list device 'fips0'
        option input 'REJECT'
        option output 'ACCEPT'
        option forward 'REJECT'
        option auto_helper '0'

config forwarding 'fips_lan'
        option src 'lan'
        option dest 'fips'

config rule 'fips_ping'
        option name 'Allow-FIPS-Ping'
        option src 'fips'
        option proto 'icmp'
        list icmp_type 'echo-request'
        option family 'ipv6'
        option target 'ACCEPT'

FIPS peers reach the router only with replies to its own connections, ping, and fips-gateway's port forwards. The router reaches the mesh freely, and LAN hosts reach it through the lan to fips forwarding. Router services such as LuCI, SSH and DNS are not reachable from FIPS peers until you open them.

OpenWrt's stock Allow-ICMPv6-Forward rule accepts ICMPv6 from wan into every zone, which would let a host on the WAN side ping mesh addresses and send them ICMPv6 errors from the router. The package therefore installs a second fw4 include, under /usr/share/nftables.d/chain-pre/forward_wan/, that refuses ICMPv6 from wan into fips0 ahead of that rule. ICMPv6 that belongs to a tracked connection, such as an error for a LAN host's flow into the mesh, is accepted before it. It applies to a zone named wan; a WAN zone under another name needs a copy of the file under chain-pre/forward_<zone>/.

To open a port to FIPS peers, add a rule with src fips, for example SSH:

uci add firewall rule
uci set firewall.@rule[-1].name=FIPS-SSH
uci set firewall.@rule[-1].src=fips
uci set firewall.@rule[-1].proto=tcp
uci set firewall.@rule[-1].dest_port=22
uci set firewall.@rule[-1].target=ACCEPT
# optional: only this peer
uci set firewall.@rule[-1].src_ip=<peer mesh address>
uci commit firewall
/etc/init.d/firewall reload

Port forwards need no rule while fw4's auto_includes is on (the default): the packaged include under /usr/share/nftables.d/chain-post/ accepts them in the zone's input and forward chains, after the zone's own rules. To restrict them, add a src fips rule (with dest lan for a target on the LAN) that matches the target's port, since filter rules see the packet after the gateway rewrites its destination.

If fips-gateway serves a network whose lan_interface is in a zone other than lan, add a forwarding from that zone, or its clients lose the mesh:

uci add firewall forwarding
uci set firewall.@forwarding[-1].src=<that zone>
uci set firewall.@forwarding[-1].dest=fips
uci commit firewall
/etc/init.d/firewall reload

A forwarding from any other zone does not give its hosts the mesh, because the gateway serves only lan_interface.

If fips0 is in a zone of your own instead of fips, the package leaves that zone alone, and the packaged port-forward accept does not apply to it: it is installed for the fips zone's chains only. Port forwards then need the same ct status dnat accept for your zone: copy the packaged file into /usr/share/nftables.d/chain-post/input_<zone>/ and /usr/share/nftables.d/chain-post/forward_<zone>/ (and list both in /etc/sysupgrade.conf to keep them across a sysupgrade), or move fips0 into the fips zone. The gateway's LAN path also needs a forwarding from the LAN interface's zone into yours.

When fips0 is in a zone whose input policy is ACCEPT, in lan, or in no zone while the default input policy is ACCEPT (as on stock OpenWrt 22.03), an install prints a line saying router services are reachable from FIPS peers. An upgrade prints it only while there is no fips zone or the old include is still present, so it does not report a fips zone whose input you opened yourself, or fips0 added back to lan beside the fips zone.

The zone holds the TUN by its name, fips0. If you rename the TUN with tun.name in /etc/fips/fips.yaml, add the new name to the zone (uci add_list firewall.fips.device=<name>, then commit and reload). Otherwise the TUN is in no zone, which on a release whose default input policy is ACCEPT leaves router services reachable from FIPS peers, and no line says so.

To change the posture, edit the fips zone rather than deleting it; an upgrade recreates a missing zone. On fw3 builds, which the package does not support, the zone is created but port forwards are not let through.

Removing the package leaves the zone. To delete it:

uci delete firewall.fips
uci delete firewall.fips_lan
uci delete firewall.fips_ping
uci commit firewall
/etc/init.d/firewall reload

The 802.11s mesh zone

fips-mesh-setup puts each mesh interface it creates in a firewall zone of its own, fips_mesh, before it brings the radio up:

config zone 'fips_mesh'
        option name 'fips_mesh'
        option input 'REJECT'
        option output 'ACCEPT'
        option forward 'REJECT'
        option auto_helper '0'
        list device 'fips-mesh0'

The mesh is open at the radio layer, so anyone in radio range can join it. An interface in no zone meets the default input policy, which on stock OpenWrt 22.03 is ACCEPT, and would let them reach router services such as LuCI, SSH and DNS at the interface's link-local address. The zone rejects all IP input and forwarding on the mesh, including FIPS UDP (port 2121) and mDNS, so FIPS peers over the mesh use the Ethernet transport, which is not IP and does not pass the firewall.

The zone holds each interface by its name. If you put a mesh interface in a zone of your own before running fips-mesh-setup, it is left there, and the helper prints a warning that names the zone, saying router services are reachable over the mesh when that zone's input policy is ACCEPT. The zone is created once and is yours from then on: the helper adds and removes interfaces but does not rewrite its options, and warns when its input policy is ACCEPT or the zone is disabled. If you rename a mesh interface by hand, add the new name to the zone (uci add_list firewall.fips_mesh.device=<name>, then commit and reload).

fips-mesh-setup remove takes each interface out of the zone, and deletes the zone with the last one. An upgrade adds mesh interfaces made by older packages, which put them in no zone, to the zone. A reinstall of the same version with a plain opkg install does not run the package's setup, so run the helper's zone subcommand yourself:

fips-mesh-setup zone

As with the fips zone, the helper and the upgrade check the loaded ruleset after the firewall reload. If the reload failed, IP traffic arriving on the mesh interfaces is dropped until the firewall reloads cleanly, and a warning says so. If the firewall configuration cannot be saved, fips-mesh-setup does not create the mesh, and an upgrade drops IP traffic arriving on the mesh interfaces until the firewall next reloads (fips-mesh-setup close does the same by hand). When the save left the file damaged, the warning names the copy to restore before any reload or reboot:

uci import firewall < /tmp/fips-mesh-firewall.uci
/etc/init.d/firewall reload

Removing the package leaves the zone, because the mesh interfaces stay configured. Once you have removed the mesh, delete the zone with:

uci delete firewall.fips_mesh
uci commit firewall
/etc/init.d/firewall reload

Service management

/etc/init.d/fips start
/etc/init.d/fips stop
/etc/init.d/fips restart
/etc/init.d/fips enable    # start at boot (already enabled by opkg postinstall)
/etc/init.d/fips disable

Outbound LAN gateway (optional)

The fips-gateway service is installed but disabled by default. It turns the router into an outbound gateway that bridges LAN clients onto the FIPS mesh. Enable only after configuring a gateway: section in /etc/fips/fips.yaml:

/etc/init.d/fips-gateway enable
/etc/init.d/fips-gateway start

The first start gives br-lan the address 2001:2:f1b5::1/64 (network.lan.ip6addr), replacing any address you set there: Android and Chrome send no AAAA queries on a LAN that has only ULA addresses. The address lies in an unassigned part of IANA's 2001::/23 block and is not routed on the internet. It stays across stops and restarts while the service is enabled, because changing it reloads the network, which on OpenWrt 25.12 recreates br-lan and drops the LAN for a few seconds. Turning the gateway off deletes it: disable of a stopped gateway, stop of a disabled one (so stop and disable in either order), a start with gateway.enabled not true in fips.yaml, and removing the package. Disabling a running gateway and then rebooting deletes nothing: disable keeps the address while the gateway runs, and a disabled service is neither stopped at shutdown nor started at boot. Run /etc/init.d/fips-gateway stop once afterwards; it deletes the address and withdraws the gateway's RA settings.

On a router with upstream IPv6, while the address is present, LAN clients may use it as the source for connections to destinations in 2001::/16, which share a longer prefix with it than with a real global address, and those connections then fail or fall back slowly.

A sysupgrade that keeps settings but drops the package runs none of the steps above. Delete the address by hand:

uci delete network.lan.ip6addr
uci commit network
/etc/init.d/network reload

See docs/tutorials/deploy-fips-gateway.md in the source tree for the full walkthrough.

Inspection and logs

# Node-level status overview
fipsctl show status

# Peer table
fipsctl show peers

# Transport links
fipsctl show links

# Active end-to-end sessions
fipsctl show sessions

# Live TUI dashboard
fipstop

# Daemon logs (OpenWrt syslog)
logread | grep fips

See docs/reference/cli-fipsctl.md for the full subcommand list.

Upgrading

OpenWrt 25 and later have no opkg. Upgrade there with the .apk package, using the same command that installs it:

apk add --allow-untrusted /tmp/fips_<new-version>_<arch>.apk

On a router with no internet access, add --no-network --repositories-file /dev/null so apk does not try the package feeds.

The .apk package's upgrade scripts stop fips and fips-gateway, start fips again, and start fips-gateway only if it was enabled; see ../openwrt-apk/README.md.

On OpenWrt 24.10 and earlier, install the new .ipk with a plain opkg install:

opkg install /tmp/fips_<new-version>_<arch>.ipk

opkg runs this as an upgrade. The installed package's prerm stops fips and fips-gateway without disabling them, and the new package's postinst starts fips and starts fips-gateway again if it was enabled.

An upgrade from 0.5.1 or earlier is the exception. The prerm in those packages disables fips-gateway and records nothing about whether it was enabled, so the new postinst enables it again. If you had the gateway disabled, disable it again after that first upgrade:

/etc/init.d/fips-gateway stop
/etc/init.d/fips-gateway disable

If opkg refuses because the new file's version sorts lower than the installed one, as it can between development builds, add --force-downgrade. opkg then takes the same upgrade path.

Do not use --force-reinstall. opkg runs it as a removal followed by a fresh install, so fips-gateway ends up disabled. To turn it back on:

/etc/init.d/fips-gateway enable
/etc/init.d/fips-gateway start

The config in /etc/fips/fips.yaml and the identity key /etc/fips/fips.key (when persistent identity is on) are preserved by opkg (the yaml is installed as a conffile; the key is not a package file). Both survive sysupgrade via /lib/upgrade/keep.d/fips.

Upgrading from 0.5.2 or earlier: the firewall

Those packages put fips0 in the lan zone and accepted all of its traffic through /etc/fips/firewall.sh. The upgrade moves fips0 into the fips zone (see Firewall), removes the old include, script and hotplug hook, and reloads the firewall. Router services are then no longer reachable from FIPS peers unless you open them. If you manage the router over FIPS, add the SSH rule from the Firewall section before upgrading: fw4 skips a rule that names a missing zone with a warning, and the rule takes effect at the upgrade's reload.

If the reload fails, or the upgrade cannot tell whether it loaded, or the firewall configuration cannot be saved, the upgrade prints a warning and blocks new connections from FIPS peers until the firewall next reloads. The warning names the remedy: fw4 check to see why a reload failed, or freeing overlay space and then committing or rebooting when the configuration could not be saved. In that last case the setup runs again at each boot until it can save.

On a full overlay, uci commit can report success after writing only part of /etc/config/firewall, or none of it. The upgrade compares the saved file with what it meant to save, and when they differ it warns that the file was left damaged. The next firewall reload or reboot loads the damaged file, which can cut off access from the LAN. The configuration the upgrade meant to save is in /tmp/fips-firewall.uci, which a reboot clears, so free space on the overlay and restore it first:

uci import firewall < /tmp/fips-firewall.uci
/etc/init.d/firewall reload

While the block is in place the old ruleset is still loaded, with fips0 in the lan zone. The block stops traffic from FIPS peers, but not traffic that the old ruleset forwards into the mesh from other zones: ICMPv6, ESP and IKE from wan through OpenWrt's stock rules (the wan include refuses the ICMPv6 only once the new ruleset loads), and whatever a forwarding or rule into lan allows. That lasts until the firewall reloads cleanly.

Firewall changes staged with uci and not yet committed are committed by the upgrade and loaded by its reload.

The upgrade does not cut connections FIPS peers opened to the router before it: the firewall accepts traffic on a connection it already tracks, and a peer can keep a UDP flow, such as one to DNS on port 53, open for as long as it keeps sending. If router services were reachable from FIPS peers before the upgrade, reboot the router afterwards to end those connections.

If fips0 is in a zone of your own, the upgrade leaves it there but removes the old include's accepts, so gateway port forwards and the gateway's LAN path need the rules described in the Firewall section; the upgrade prints a line saying so.

Installing an older package puts fips0 back into lan until the next install of a package with this zone.

A configuration backup taken before the upgrade carries the old firewall setup, so take a new backup afterwards. After restoring an older one, redo the migration by hand. Check the indexes first:

uci show firewall | grep -e "name='lan'" -e fips/firewall.sh

then, with those indexes:

uci del_list firewall.@zone[<lan index>].device=fips0
uci delete firewall.@include[<index of the fips include>]
rm -f /etc/fips/firewall.sh
uci set firewall.fips=zone
uci set firewall.fips.name=fips
uci add_list firewall.fips.device=fips0
uci set firewall.fips.input=REJECT
uci set firewall.fips.output=ACCEPT
uci set firewall.fips.forward=REJECT
uci set firewall.fips.auto_helper=0
uci set firewall.fips_lan=forwarding
uci set firewall.fips_lan.src=lan
uci set firewall.fips_lan.dest=fips
uci set firewall.fips_ping=rule
uci set firewall.fips_ping.name=Allow-FIPS-Ping
uci set firewall.fips_ping.src=fips
uci set firewall.fips_ping.proto=icmp
uci add_list firewall.fips_ping.icmp_type=echo-request
uci set firewall.fips_ping.family=ipv6
uci set firewall.fips_ping.target=ACCEPT
uci commit firewall
/etc/init.d/firewall reload

Upgrading from 0.5.2 or earlier: the 802.11s mesh's basic rates

802.11s mesh points peer only when they join with the same basic rates. The fips-mesh-setup in those packages set none, and OpenWrt releases then differ: 24.10 joins through iw with the radio's basic rates, by default 6, 12 and 24 Mb/s on 2.4 GHz, while 25.12 joins with the kernel's, 1, 2, 5.5 and 11 Mb/s on 2.4 GHz. A 24.10 router and a 25.12 router each hear the other's beacons and never peer. fips-mesh-setup now sets basic_rate '6000 12000 24000' on every mesh interface it creates, which 24.10 and 25.12 both honour. On those releases a basic_rate on the interface overrides one set on the radio, so a rate choice you made on the radio (basic_rate, legacy_rates or cell_density) no longer applies to the mesh; set it on the mesh interface instead.

The upgrade sets that value on each mesh interface fips-mesh-setup made that has no basic_rate, and prints a line naming them. It leaves wireless interfaces the helper did not make alone, and keeps a basic_rate you set on a fips mesh interface yourself, printing a line that names it. Running fips-mesh-setup <radio> again also keeps one. The upgrade saves the wireless configuration and does not run wifi reload itself, but the rates apply at the next reboot or network reload of any kind: wifi reload, /etc/init.d/network reload, a LuCI apply, or the fips gateway starting or being turned off. On a router with the gateway enabled, an upgrade from 0.5.2 or earlier starts the gateway, which reloads the network to set the LAN prefix, so the rates apply during the upgrade itself.

From then on the router peers only with mesh points that join with the same rates. A router still on the old package, or upgraded but not yet reloaded, stops peering with it if it joins with other rates: on 2.4 GHz that is a 25.12 router, or any 22.03 or 23.05 router (see below), while a 24.10 router on default settings already uses these rates. On 5 GHz both releases are expected to use these rates already; that has not been checked on a router.

When you reach routers over the mesh, upgrade them one at a time, farthest first, and run wifi reload on each right after its upgrade. A router that loses its peerings then is at the edge of what you reach, and the routers you reach it through keep theirs until their own turn, when the link to it comes back. Upgrading a near router first can cut off every router reached through it, which then needs another path, such as its LAN, to finish the upgrade.

OpenWrt 22.03 and 23.05 pass no basic rates to a mesh join, whatever is set on the interface or the radio, so on 2.4 GHz they join with 1, 2, 5.5 and 11 Mb/s. Upgrading the package does not change that, and such a router stops peering with 24.10 and later routers once those are upgraded and reloaded. To keep a 22.03 or 23.05 router in a 2.4 GHz mesh, set the rates it uses on the 2.4 GHz mesh interface of every 24.10 or later router in that mesh (here the one on radio1); the upgrade keeps them:

uci set wireless.fips_mesh_radio1.basic_rate='1000 2000 5500 11000'
uci commit wireless
wifi reload

A reinstall of the same version with a plain opkg install does not run the package's setup, so run the helper's rates subcommand yourself:

fips-mesh-setup rates
wifi reload

If the wireless configuration cannot be saved, the upgrade says so, and its setup runs again at each boot until it can save. When the upgrade could not save the firewall configuration, which can mean the overlay is full, it leaves the wireless configuration alone and sets the rates at a later boot. When the save left /etc/config/wireless damaged, the warning says so; free space on the overlay and restore it before the router reboots:

uci import wireless < /tmp/fips-mesh-wireless.uci