fix(openwrt): ship mesh transports commented, uncomment on setup

The default-on mesh0/mesh1 transport entries logged a per-boot
"interface missing" bind warning on installs that never create
fips-mesh* (PR #123 review). Ship them commented out instead, and have
fips-mesh-setup uncomment the matching meshN block in fips.yaml when it
creates the interface (re-commenting it on remove) — a stock install
stays quiet while the flash-and-drop-in flow still needs no manual
config edit.

Also note in the how-to that both the create and remove paths run
`wifi reload`, which briefly drops client APs on all radios.

The shipped-config regression test now checks both states parse: as
shipped (mesh inactive) and after the uncomment the helper performs.
This commit is contained in:
Arjen
2026-07-13 11:29:02 +02:00
parent cdf384d730
commit 3798ba853c
4 changed files with 155 additions and 40 deletions

View File

@@ -70,9 +70,16 @@ fips-mesh-setup radio1
This creates an open 802.11s interface with mesh ID `fips-mesh` and
HWMP forwarding off, attaches it to an unmanaged netifd interface (no
IP configuration — none is needed), and reloads the radio. Interfaces
are named by radio index: `radio0` → `fips-mesh0`, `radio1` →
`fips-mesh1`. Pass a second argument to use a different mesh ID.
IP configuration — none is needed), uncomments the matching `meshN`
transport entry in `/etc/fips/fips.yaml` (see Step 2), and reloads the
radio. Interfaces are named by radio index: `radio0` → `fips-mesh0`,
`radio1` → `fips-mesh1`. Pass a second argument to use a different
mesh ID.
Note: the helper runs `wifi reload`, which re-applies the whole
wireless config and so briefly drops every client AP on all radios for
a few seconds. `fips-mesh-setup remove` reloads the same way. Expect
the blip if clients are connected.
On dual-band routers, meshing **both** bands is worth it: 2.4 GHz
reaches further at lower rates, 5 GHz carries more over shorter
@@ -123,10 +130,13 @@ wifi reload
## Step 2 — check the FIPS transport binding
The `fips.yaml` shipped in the OpenWrt package already carries one
transport entry per radio, enabled by default — each is inert until
its interface exists. If you maintain your own config, make sure they
are present:
The `fips.yaml` shipped in the OpenWrt package carries one transport
entry per radio, but **commented out** — so a stock install that never
runs this helper logs no per-boot "interface missing" warning.
`fips-mesh-setup` uncommented the matching `meshN` entry in Step 1, so
there is normally nothing to do here. If you maintain your own config
(or ran the manual UCI above instead of the helper), make sure the
entries are present and uncommented:
```yaml
transports:

View File

@@ -100,28 +100,31 @@ transports:
auto_connect: true
accept_connections: true
# 802.11s mesh backhaul between FIPS routers. Each entry is inert until
# its radio interface exists — create them with 'fips-mesh-setup <radio>'
# (once per radio; radio0 -> fips-mesh0, radio1 -> fips-mesh1), then
# restart fips (a transport whose interface is missing at startup is
# skipped, not retried). Dual-band routers can mesh on both bands at
# once — failover, not multipath: FIPS keeps one active link per peer,
# the other band stands by. The mesh runs OPEN (no SAE) with 802.11s
# 802.11s mesh backhaul between FIPS routers. These entries ship
# commented out so a stock install that never creates fips-mesh*
# logs no per-boot "interface missing" bind warning. Running
# 'fips-mesh-setup <radio>' creates the interface AND uncomments the
# matching block here (once per radio; radio0 -> fips-mesh0, radio1 ->
# fips-mesh1); 'fips-mesh-setup remove' re-comments it. Restart fips
# after — a transport whose interface is missing at startup is skipped,
# not retried. Dual-band routers can mesh on both bands at once —
# failover, not multipath: FIPS keeps one active link per peer, the
# other band stands by. The mesh runs OPEN (no SAE) with 802.11s
# forwarding off: FIPS's Noise handshake is the encryption and
# authentication, and FIPS is the routing layer. See
# docs/how-to/set-up-80211s-mesh-backhaul.md.
mesh0:
interface: "fips-mesh0"
discovery: true
announce: true
auto_connect: true
accept_connections: true
mesh1:
interface: "fips-mesh1"
discovery: true
announce: true
auto_connect: true
accept_connections: true
# mesh0:
# interface: "fips-mesh0"
# discovery: true
# announce: true
# auto_connect: true
# accept_connections: true
# mesh1:
# interface: "fips-mesh1"
# discovery: true
# announce: true
# auto_connect: true
# accept_connections: true
# Bluetooth Low Energy transport — requires BlueZ and the 'ble' feature.
# ble:

View File

@@ -24,12 +24,51 @@
# fips-mesh1) and are intentionally NOT bridged into br-lan: the FIPS
# Ethernet transport binds each directly and runs discovery beacons over it.
#
# The shipped /etc/fips/fips.yaml already carries 'mesh0' and 'mesh1'
# entries under 'transports.ethernet' bound to these names — each is inert
# until its interface exists. After an interface is up, restart fips.
# The shipped /etc/fips/fips.yaml carries 'mesh0' and 'mesh1' entries under
# 'transports.ethernet' bound to these names, but commented out — a stock
# install that never creates fips-mesh* then logs no bind warning. This
# helper uncomments the matching entry when it creates an interface and
# re-comments it on remove, so the daemon binds the transport without a
# manual config edit. After an interface is up, restart fips.
# See docs/how-to/set-up-80211s-mesh-backhaul.md for the full guide.
DEFAULT_MESH_ID="fips-mesh"
CONFIG="/etc/fips/fips.yaml"
# Replace $CONFIG with the rewritten $CONFIG.tmp. Force mode 0600 first: the
# package installs fips.yaml 0600 (it may hold an inline 'nsec' private key),
# and a fresh tmp file would otherwise land world-readable after the move.
mesh_config_write() {
chmod 600 "$CONFIG.tmp" && mv "$CONFIG.tmp" "$CONFIG"
}
# Uncomment the 'mesh<idx>' transports.ethernet block in $CONFIG (created by
# 'fips-mesh-setup'). Reversible with mesh_config_disable. Returns:
# 0 enabled (or already active) 1 no config file 2 no such block
mesh_config_enable() {
idx="$1"
[ -f "$CONFIG" ] || return 1
grep -q "^ mesh$idx:" "$CONFIG" && return 0
grep -q "^ # mesh$idx:" "$CONFIG" || return 2
awk -v idx="$idx" '
$0 ~ ("^ # mesh" idx ":[ \t]*$") { blk = 1; sub(/^ # /, " "); print; next }
blk && /^ # / { sub(/^ # /, " "); print; next }
{ blk = 0; print }
' "$CONFIG" > "$CONFIG.tmp" && mesh_config_write
}
# Re-comment the 'mesh<idx>' block so the daemon stops binding it (and stops
# warning about the now-missing interface). Inverse of mesh_config_enable.
mesh_config_disable() {
idx="$1"
[ -f "$CONFIG" ] || return 1
grep -q "^ mesh$idx:" "$CONFIG" || return 0
awk -v idx="$idx" '
$0 ~ ("^ mesh" idx ":[ \t]*$") { blk = 1; sub(/^ /, " # "); print; next }
blk && /^ / { sub(/^ /, " # "); print; next }
{ blk = 0; print }
' "$CONFIG" > "$CONFIG.tmp" && mesh_config_write
}
usage() {
echo "Usage: fips-mesh-setup <radio> [mesh-id]" >&2
@@ -62,15 +101,18 @@ if [ "$1" = "remove" ]; then
ifname="$(uci -q get "wireless.$section.ifname")"
uci -q delete "wireless.$section"
uci -q delete "network.$section"
# Re-comment the matching mesh<N> transport in fips.yaml so the
# daemon stops warning about the interface we just removed.
idx="$(printf '%s' "$ifname" | sed -n 's/.*[^0-9]\([0-9]\{1,\}\)$/\1/p')"
[ -n "$idx" ] && mesh_config_disable "$idx"
echo "Removed ${ifname:-$section}."
done
uci commit wireless
uci commit network
# 'wifi reload' re-applies the whole wireless config, so it briefly drops
# every client AP on all radios (a few seconds) — expected on remove.
wifi reload
echo "Restart fips: /etc/init.d/fips restart"
echo "(The 'mesh*' entries in /etc/fips/fips.yaml are harmless without"
echo "their interfaces, but you may comment them out to silence the"
echo "startup warnings.)"
exit 0
fi
@@ -178,8 +220,22 @@ uci set "network.$SECTION.proto=none"
uci commit wireless
uci commit network
# 'wifi reload' re-applies the whole wireless config, so it briefly drops
# every client AP on all radios (a few seconds) — expected when adding a mesh.
wifi reload
# Enable the matching mesh<N> transport in the shipped fips.yaml (it ships
# commented out). Tailor the restart hint to what we could do.
mesh_config_enable "$IDX"
case $? in
0) TRANSPORT_NOTE="The mesh$IDX transport in $CONFIG that binds '$MESH_IFNAME' is
now uncommented and enabled." ;;
1) TRANSPORT_NOTE="No $CONFIG found — add a transports.ethernet entry binding
interface '$MESH_IFNAME' by hand." ;;
*) TRANSPORT_NOTE="No 'mesh$IDX' entry in $CONFIG — add a transports.ethernet
entry binding interface '$MESH_IFNAME' by hand (copy the mesh0 block)." ;;
esac
cat <<EOF
Created open 802.11s mesh '$MESH_ID' as $MESH_IFNAME on $RADIO \
(band ${BAND:-?}, channel ${CHANNEL:-auto}).
@@ -189,10 +245,9 @@ ALL routers in this backhaul must share this mesh ID AND channel
radio too — second band is a standby path (failover, not multipath).
Next steps:
1. Restart the daemon (the shipped /etc/fips/fips.yaml already binds
mesh transports to fips-mesh0/fips-mesh1). Restart AFTER the
interface is up — a transport whose interface is missing at
startup is skipped, not retried:
1. $TRANSPORT_NOTE
Restart the daemon AFTER the interface is up — a transport whose
interface is missing at startup is skipped, not retried:
/etc/init.d/fips restart
2. Verify L2 peering with a second FIPS router in range:
iw dev $MESH_IFNAME station dump

View File

@@ -771,13 +771,35 @@ node:
}
/// The fips.yaml shipped in the OpenWrt package must keep parsing as the
/// config schema evolves, including the default-enabled 802.11s mesh
/// backhaul transport entries — one per radio, so dual-band routers can
/// mesh on both bands (docs/how-to/set-up-80211s-mesh-backhaul.md).
/// config schema evolves. The 802.11s mesh backhaul entries (one per
/// radio, so dual-band routers can mesh on both bands) ship commented
/// out — a stock install that never creates fips-mesh* logs no per-boot
/// bind warning; `fips-mesh-setup` uncomments the matching block when it
/// creates the interface (docs/how-to/set-up-80211s-mesh-backhaul.md).
/// Verify both states parse: as shipped (mesh inactive), and after the
/// uncomment `fips-mesh-setup` performs.
#[test]
fn shipped_openwrt_config_parses() {
let yaml = include_str!("../../packaging/openwrt-ipk/files/etc/fips/fips.yaml");
// As shipped: parses, and the mesh entries are commented out (a
// running daemon binds no fips-mesh* transport, so no bind warning).
let config: Config = serde_yaml::from_str(yaml).expect("shipped OpenWrt fips.yaml");
for name in ["mesh0", "mesh1"] {
assert!(
!config
.transports
.ethernet
.iter()
.any(|(n, _)| n == Some(name)),
"{name} must ship commented out, not active, in fips.yaml"
);
}
// What `fips-mesh-setup` produces: uncomment each mesh block, which
// must still parse into a transport entry bound to the right netdev.
let config: Config = serde_yaml::from_str(&uncomment_mesh_blocks(yaml))
.expect("fips.yaml with mesh transports uncommented");
for (name, interface) in [("mesh0", "fips-mesh0"), ("mesh1", "fips-mesh1")] {
assert!(
config
@@ -785,11 +807,36 @@ node:
.ethernet
.iter()
.any(|(n, eth)| n == Some(name) && eth.interface == interface),
"{name} backhaul entry missing from shipped OpenWrt fips.yaml"
"{name} backhaul entry missing after uncommenting shipped fips.yaml"
);
}
}
/// Mirror `fips-mesh-setup`'s block uncomment: strip the ` # ` prefix
/// from each `# mesh<N>:` header and its ` # ` continuation lines,
/// leaving every other comment untouched.
fn uncomment_mesh_blocks(yaml: &str) -> String {
let mut out = String::new();
let mut in_block = false;
for line in yaml.lines() {
let is_header = line
.strip_prefix(" # mesh")
.and_then(|r| r.strip_suffix(':'))
.is_some_and(|n| !n.is_empty() && n.bytes().all(|b| b.is_ascii_digit()));
if is_header {
in_block = true;
out.push_str(&line.replacen(" # ", " ", 1));
} else if in_block && line.starts_with(" # ") {
out.push_str(&line.replacen(" # ", " ", 1));
} else {
in_block = false;
out.push_str(line);
}
out.push('\n');
}
out
}
#[test]
fn test_parse_yaml_with_hex() {
let yaml = r#"