mirror of
https://github.com/jmcorgan/fips.git
synced 2026-07-30 19:46:15 +00:00
Router-to-router radio backhaul over an open 802.11s mesh interface, with FIPS providing all encryption (Noise IK), authentication, and routing on top of bare L2 neighbor links. The mesh runs OPEN with mesh_fwding 0 — SAE would duplicate the Noise layer and force ath10k raw mode, and FIPS is the routing layer — so the Noise handshake is the real auth/encryption boundary and FIPS's spanning tree does the routing. fips-mesh-setup: an opt-in UCI helper that creates a per-radio mesh-point interface (radio0 -> fips-mesh0, radio1 -> fips-mesh1; trailing-digit derivation with a free-index fallback and a collision guard). Radio setup stays opt-in — a package must not commandeer radios on install. 'remove' takes an optional radio and otherwise removes all instances. Dual-band routers get one instance per radio; FIPS treats the two backhaul paths as failover, not multipath: it keeps one active link per peer (cross-connection resolution picks a single winner), and the second band stands by, re-establishing the peer after keepalive timeout — traffic never uses both bands at once. fips.yaml ships the mesh0/mesh1 Ethernet-transport entries commented out, so a stock install that never creates fips-mesh* logs no per-boot "interface missing" bind warning. fips-mesh-setup uncomments the matching meshN block when it creates the interface and re-comments it on remove, so the flash-and-drop-in flow needs no manual config edit. The file is rewritten 0600-first (it may hold an inline nsec) via an atomic replace. Two field-found silent non-peering causes are surfaced by the helper and the guide: - Same channel: mesh points only peer on a shared channel, and 'auto' lets each radio pick its own. The helper prints the radio's band/channel and warns loudly on 'auto' with the exact uci command to pin one; the how-to gains an ordered no-peers triage (channel mismatch, on-air scan check, DFS CAC wait, regdomain). - STA channel capture: a client (sta) interface drags the whole radio to its upstream AP's channel, so a mesh pinned elsewhere never joins and does not recover until the STA disconnects. The helper warns when the target radio carries a STA; the guide documents the incompatibility of a roaming uplink with a fixed-channel mesh on the same radio. Both the create and remove paths run 'wifi reload', which briefly drops every client AP on all radios; the how-to sets that expectation. Regression test: the shipped OpenWrt fips.yaml must parse via the real Config deserializer in both states — as shipped (mesh inactive) and after the uncomment the helper performs. Packaging: the helper is installed across the ipk/apk/buildroot paths (three synced copies), with the CI structural checks and shellcheck targets extended to cover it. Full guide in docs/how-to/set-up-80211s-mesh-backhaul.md.
260 lines
11 KiB
Bash
Executable File
260 lines
11 KiB
Bash
Executable File
#!/bin/sh
|
|
# fips-mesh-setup — configure open 802.11s mesh interfaces for FIPS backhaul.
|
|
#
|
|
# Usage:
|
|
# fips-mesh-setup <radio> [mesh-id] e.g. fips-mesh-setup radio1
|
|
# fips-mesh-setup remove [radio] no radio: remove all instances
|
|
#
|
|
# Creates a mesh-point interface on the given radio and leaves everything
|
|
# above L2 to FIPS. Run once per radio: dual-band routers can mesh on both
|
|
# bands at once (2.4 GHz reaches further, 5 GHz carries more). Note this is
|
|
# failover, not multipath — FIPS keeps one active link per peer; the other
|
|
# band stands by and reconnects the peer if the active link dies.
|
|
#
|
|
# - encryption 'none' — the mesh is OPEN on purpose. FIPS's Noise IK
|
|
# handshake authenticates and encrypts every peer link, so SAE would
|
|
# only duplicate that (and on ath10k it forces the slower raw Tx/Rx
|
|
# firmware mode). A stranger can form an 802.11s peering but cannot
|
|
# pass the FIPS handshake.
|
|
# - mesh_fwding '0' — disables 802.11s HWMP forwarding so each mesh
|
|
# link is a plain L2 neighbor link. FIPS is the routing layer; two
|
|
# routing layers would fight.
|
|
#
|
|
# Interfaces are named per radio index (radio0 -> fips-mesh0, radio1 ->
|
|
# 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 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
|
|
echo " fips-mesh-setup remove [radio]" >&2
|
|
echo "Radios on this device:" >&2
|
|
uci show wireless 2>/dev/null | sed -n "s/^wireless\.\([^.]*\)=wifi-device$/ \1/p" >&2
|
|
exit 1
|
|
}
|
|
|
|
# List the UCI section names of fips-managed mesh wifi-ifaces.
|
|
mesh_sections() {
|
|
uci show wireless 2>/dev/null | sed -n "s/^wireless\.\(fips_mesh[^.=]*\)=wifi-iface$/\1/p"
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# remove [radio] — delete the wireless and network sections created below
|
|
# ---------------------------------------------------------------------------
|
|
|
|
if [ "$1" = "remove" ]; then
|
|
if [ -n "$2" ]; then
|
|
SECTIONS="fips_mesh_$(printf '%s' "$2" | tr -c 'a-zA-Z0-9_' '_')"
|
|
else
|
|
SECTIONS="$(mesh_sections)"
|
|
fi
|
|
[ -n "$SECTIONS" ] || {
|
|
echo "No fips mesh instances configured."
|
|
exit 0
|
|
}
|
|
for section in $SECTIONS; do
|
|
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"
|
|
exit 0
|
|
fi
|
|
|
|
RADIO="$1"
|
|
MESH_ID="${2:-$DEFAULT_MESH_ID}"
|
|
|
|
[ -n "$RADIO" ] || usage
|
|
|
|
if [ "$(uci -q get "wireless.$RADIO")" != "wifi-device" ]; then
|
|
echo "Error: '$RADIO' is not a wifi-device in /etc/config/wireless." >&2
|
|
usage
|
|
fi
|
|
|
|
# One instance per radio: section fips_mesh_<radio>, netdev fips-mesh<N>
|
|
# where N is the radio's trailing index (radio0 -> fips-mesh0). For radios
|
|
# named without a trailing number, fall back to the first free index.
|
|
SECTION="fips_mesh_$(printf '%s' "$RADIO" | tr -c 'a-zA-Z0-9_' '_')"
|
|
IDX="$(printf '%s' "$RADIO" | sed -n 's/.*[^0-9]\([0-9]\{1,\}\)$/\1/p')"
|
|
[ -n "$IDX" ] || IDX="$(printf '%s' "$RADIO" | sed -n 's/^\([0-9]\{1,\}\)$/\1/p')"
|
|
if [ -z "$IDX" ]; then
|
|
IDX=0
|
|
while uci show wireless 2>/dev/null | grep -q "\.ifname='fips-mesh$IDX'"; do
|
|
IDX=$((IDX + 1))
|
|
done
|
|
fi
|
|
MESH_IFNAME="fips-mesh$IDX"
|
|
|
|
# Refuse a name collision from another radio's instance (e.g. two radios
|
|
# whose names end in the same digit) rather than silently hijacking it.
|
|
OWNER="$(uci show wireless 2>/dev/null \
|
|
| sed -n "s/^wireless\.\(fips_mesh[^.=]*\)\.ifname='$MESH_IFNAME'$/\1/p")"
|
|
if [ -n "$OWNER" ] && [ "$OWNER" != "$SECTION" ]; then
|
|
echo "Error: $MESH_IFNAME is already used by section '$OWNER'." >&2
|
|
echo "Remove it first: fips-mesh-setup remove" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Driver capability check (advisory — config below is harmless either way)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
if command -v iw >/dev/null 2>&1; then
|
|
if ! iw list 2>/dev/null | grep -q "\* mesh point"; then
|
|
echo "Warning: no radio on this device advertises 'mesh point' support" >&2
|
|
echo "(iw list | grep 'mesh point'). The interface may fail to come up." >&2
|
|
fi
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Wireless: open 802.11s mesh point, HWMP forwarding off
|
|
# ---------------------------------------------------------------------------
|
|
|
|
uci -q delete "wireless.$SECTION"
|
|
uci set "wireless.$SECTION=wifi-iface"
|
|
uci set "wireless.$SECTION.device=$RADIO"
|
|
uci set "wireless.$SECTION.mode=mesh"
|
|
uci set "wireless.$SECTION.mesh_id=$MESH_ID"
|
|
uci set "wireless.$SECTION.encryption=none"
|
|
uci set "wireless.$SECTION.mesh_fwding=0"
|
|
uci set "wireless.$SECTION.ifname=$MESH_IFNAME"
|
|
uci set "wireless.$SECTION.network=$SECTION"
|
|
|
|
# Radios ship disabled on fresh OpenWrt installs; a disabled radio would
|
|
# leave the mesh interface down with no error anywhere visible.
|
|
if [ "$(uci -q get "wireless.$RADIO.disabled")" = "1" ]; then
|
|
echo "Note: enabling $RADIO (was disabled)."
|
|
uci -q delete "wireless.$RADIO.disabled"
|
|
fi
|
|
|
|
# The mesh inherits the radio's channel, and mesh points only peer on the
|
|
# same channel. 'auto' lets each router pick its own — the classic silent
|
|
# non-peering cause — so surface the setting loudly.
|
|
CHANNEL="$(uci -q get "wireless.$RADIO.channel")"
|
|
BAND="$(uci -q get "wireless.$RADIO.band")"
|
|
if [ -z "$CHANNEL" ] || [ "$CHANNEL" = "auto" ]; then
|
|
echo "Warning: $RADIO channel is '${CHANNEL:-unset}' — each router may" >&2
|
|
echo "auto-select a different channel and mesh points only peer on the" >&2
|
|
echo "same one. Pin the same channel on every backhaul router, e.g.:" >&2
|
|
echo " uci set wireless.$RADIO.channel='36' && uci commit wireless && wifi reload" >&2
|
|
fi
|
|
|
|
# A client (sta) interface on the same radio follows its upstream AP's
|
|
# channel and drags every other interface with it — a mesh pinned to a
|
|
# different channel silently never joins, and does not recover when the
|
|
# STA disconnects.
|
|
for s in $(uci show wireless 2>/dev/null | sed -n "s/^wireless\.\([^.]*\)\.mode='sta'$/\1/p"); do
|
|
if [ "$(uci -q get "wireless.$s.device")" = "$RADIO" ]; then
|
|
echo "Warning: $RADIO also carries client interface '$s' (mode 'sta')." >&2
|
|
echo "The whole radio follows that STA's upstream channel — a mesh" >&2
|
|
echo "pinned to a different channel stays down silently. Align the" >&2
|
|
echo "mesh channel with the upstream AP, or put the mesh on a radio" >&2
|
|
echo "without a STA (a roaming uplink is incompatible with a" >&2
|
|
echo "fixed-channel mesh on the same radio)." >&2
|
|
fi
|
|
done
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Network: unmanaged interface so netifd brings the netdev up. No IP config —
|
|
# the FIPS Ethernet transport speaks raw frames on it.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
uci -q delete "network.$SECTION"
|
|
uci set "network.$SECTION=interface"
|
|
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}).
|
|
|
|
ALL routers in this backhaul must share this mesh ID AND channel
|
|
(per band). On a dual-band router, run fips-mesh-setup for the other
|
|
radio too — second band is a standby path (failover, not multipath).
|
|
|
|
Next steps:
|
|
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
|
|
and the FIPS link on top of it:
|
|
fipsctl show peers
|
|
|
|
Run 'fips-mesh-setup remove' to undo all instances, or
|
|
'fips-mesh-setup remove $RADIO' for just this one.
|
|
EOF
|