mirror of
https://github.com/jmcorgan/fips.git
synced 2026-10-30 07:37:36 +00:00
fips-mesh-setup creates an 802.11s mesh interface that is open at the radio layer, unmanaged, and in no firewall zone. Its input therefore met the router's default input policy, which is ACCEPT on OpenWrt 22.03, so any station in radio range could join the mesh and reach router services such as LuCI, dropbear and dnsmasq at the interface's link-local address. Each mesh interface now goes into a zone of its own, fips_mesh, with input and forward REJECT, output ACCEPT and auto_helper 0. The zone holds the interface by device name, so it is in the ruleset from the moment the firewall loads and does not wait for netifd. FIPS UDP and mDNS on the mesh are rejected with everything else; FIPS peers over the mesh use the Ethernet transport, which is not IP and does not pass the firewall. On add, the firewall is configured, saved and reloaded before the radio comes up. The save is checked by exporting the configuration before and after the commit, since a full overlay can make uci commit succeed with a truncated or empty file; on a failed save the mesh is not created, and a damaged file is reported with the copy to restore. The reload's exit status says nothing about whether the ruleset loaded, so the loaded input and forward chains are read instead; when the zone is not loaded, IP traffic arriving on the interface is dropped at the head of both chains until the firewall reloads cleanly. Changes left staged by an earlier failed save count as something to save, and a run with nothing to save still checks the loaded ruleset and reloads only when that check fails. An interface already held by another zone, by device, wildcard or network, is left there with a warning naming that zone, as the package leaves fips0 in a zone the operator chose. The zone is created once and its options are the operator's from then on; an ACCEPT input or a disabled zone is reported. Remove takes each interface out of the zone once it is gone, and deletes the zone with the last one, so add followed by remove leaves the firewall configuration as it was. A new zone subcommand adds existing mesh interfaces to the zone, and the package setup runs it at upgrade and at the first boot after a sysupgrade, so routers that ran an earlier package are closed too. It runs only when the setup's own firewall save succeeded; otherwise the new close subcommand drops IP traffic arriving on the mesh interfaces in the loaded ruleset without touching the configuration, and the setup is kept to run again at the next boot. The OpenWrt firewall scenarios gain cases for the add, remove, upgrade, failed reload, failed save and other-zone paths, run against the real uci, fw4 and nft in the four pinned OpenWrt images.
262 lines
11 KiB
Bash
262 lines
11 KiB
Bash
#!/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 AND a FIPS
|
|
# peer link — the Noise handshake authenticates each link (no
|
|
# impersonation of another identity, no MITM), it does not gate who
|
|
# may peer. Admission is open up to the daemon's max-peers cap.
|
|
# - 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
|