mirror of
https://github.com/jmcorgan/fips.git
synced 2026-10-29 23:27:37 +00:00
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.
2662 lines
115 KiB
Bash
2662 lines
115 KiB
Bash
#!/bin/sh
|
|
# OpenWrt firewall scenarios for 90-fips-setup and fips-mesh-setup, run inside
|
|
# an openwrt/rootfs image so the real uci, fw4 and nft are exercised under ash.
|
|
#
|
|
# Driven by testing/openwrt/maintainer-scripts-test.sh, once per pinned image,
|
|
# with the container started with --cap-add NET_ADMIN so fw4 can load its
|
|
# table into the container's own network namespace, and with a small tmpfs at
|
|
# /fw-small that the cases saving onto a full overlay put /etc/config on. Each case resets the
|
|
# container to the image's stock configuration, starts fw4, builds a starting
|
|
# state (often the one the 0.5.2 package left), runs the setup under test and
|
|
# asserts on uci and on the loaded nftables ruleset.
|
|
#
|
|
# Seams:
|
|
# FW_SETUP the setup script under test; default the one built into the
|
|
# .ipk, as package-test.sh --keep extracts it under /apk.
|
|
# FW_SNIPPETS a directory laid out like /usr/share/nftables.d, copied there
|
|
# before each case; default the .ipk's. "none" installs none.
|
|
# FW_MESH_SETUP the mesh helper under test; default the one built into the
|
|
# .ipk.
|
|
# FW_CASES case function names to run, separated by spaces; default all.
|
|
#
|
|
# The mesh cases have no radio: the zone holds each interface by device name,
|
|
# which fw4 renders without netifd, and wifi is a stub that logs its call to
|
|
# the same file as the firewall reloads, so the log gives their order.
|
|
#
|
|
# Assertions do not stop a case. A failed precondition records a failure and
|
|
# the case skips to its end. Every case that reaches its end is counted, and a
|
|
# run that counts fewer cases than it selected exits 2.
|
|
#
|
|
# Exit 0 = every case passed. Exit 1 = at least one failed. Exit 2 = the
|
|
# harness could not run; never treated as a pass.
|
|
|
|
set -u
|
|
|
|
REPO="${REPO:-/src}"
|
|
FW_SETUP="${FW_SETUP:-/apk/ipk-root/etc/uci-defaults/90-fips-setup}"
|
|
FW_SNIPPETS="${FW_SNIPPETS:-/apk/ipk-root/usr/share/nftables.d}"
|
|
FW_MESH_SETUP="${FW_MESH_SETUP:-/apk/ipk-root/usr/bin/fips-mesh-setup}"
|
|
FW_CASES="${FW_CASES:-}"
|
|
FIXTURES="$REPO/testing/openwrt/fixtures"
|
|
|
|
WORK=/tmp/fw-work
|
|
STOCK=/tmp/fw-stock
|
|
WRAP=/tmp/fw-wrap
|
|
RELOADLOG=/tmp/fw-reload.log
|
|
FLUSHONLY=/tmp/fw-flush-only
|
|
FLUSHLIST=/tmp/fw-flush-listing
|
|
SMALL=/fw-small
|
|
COPY=/tmp/fips-firewall.uci
|
|
MCOPY=/tmp/fips-mesh-firewall.uci
|
|
WCOPY=/tmp/fips-mesh-wireless.uci
|
|
STUB=/tmp/fw-wrap/stub
|
|
OUT="$WORK/out"
|
|
SYSPATH=/usr/sbin:/usr/bin:/sbin:/bin
|
|
|
|
# The number of cases in the full run, counted by hand from the list at the
|
|
# end of this file. A run that completes fewer exits 2.
|
|
ALL_CASES=80
|
|
|
|
FAILURES=0
|
|
CASES=0
|
|
RAN=0
|
|
CUR=""
|
|
LIST=""
|
|
RC=0
|
|
|
|
note() { echo " $*"; }
|
|
|
|
ok() {
|
|
CASES=$((CASES + 1))
|
|
echo " ok $CUR: $*"
|
|
return 0
|
|
}
|
|
|
|
bad() {
|
|
CASES=$((CASES + 1))
|
|
FAILURES=$((FAILURES + 1))
|
|
echo " FAIL $CUR: $*"
|
|
return 0
|
|
}
|
|
|
|
harness_fail() {
|
|
echo "firewall-scenarios: $*" >&2
|
|
exit 2
|
|
}
|
|
|
|
# check <description> <command...>: passes when the command succeeds.
|
|
check() {
|
|
local d="$1"
|
|
shift
|
|
if "$@"; then ok "$d"; else bad "$d"; fi
|
|
return 0
|
|
}
|
|
|
|
# refute <description> <command...>: passes when the command fails.
|
|
refute() {
|
|
local d="$1"
|
|
shift
|
|
if "$@"; then bad "$d"; else ok "$d"; fi
|
|
return 0
|
|
}
|
|
|
|
# precond <description> <command...>: like check, but returns the verdict so
|
|
# the case can skip to its end.
|
|
precond() {
|
|
local d="$1"
|
|
shift
|
|
if "$@"; then
|
|
ok "precondition: $d"
|
|
return 0
|
|
fi
|
|
bad "precondition: $d"
|
|
return 1
|
|
}
|
|
|
|
same() { [ "$1" = "$2" ]; }
|
|
|
|
# ── The loaded ruleset ──────────────────────────────────────────────────────
|
|
# Every predicate fails when the listing itself fails, so an absent table can
|
|
# never read as an absent rule.
|
|
|
|
listing() { LIST="$(nft list chain inet fw4 "$1" 2>/dev/null)"; }
|
|
|
|
has() { listing "$1" && printf '%s\n' "$LIST" | grep -qF -- "$2"; }
|
|
|
|
lacks() { listing "$1" && ! printf '%s\n' "$LIST" | grep -qF -- "$2"; }
|
|
|
|
table_lacks() {
|
|
local t
|
|
t="$(nft list table inet fw4 2>/dev/null)" || return 1
|
|
! printf '%s\n' "$t" | grep -qF -- "$1"
|
|
}
|
|
|
|
table_has() {
|
|
local t
|
|
t="$(nft list table inet fw4 2>/dev/null)" || return 1
|
|
case "$t" in
|
|
*"$1"*) return 0 ;;
|
|
esac
|
|
return 1
|
|
}
|
|
|
|
# The rules of a chain that name fips0, in order, without indentation.
|
|
naming() {
|
|
listing "$1" || return 1
|
|
printf '%s\n' "$LIST" | grep -F '"fips0"' | sed 's/^[[:space:]]*//'
|
|
}
|
|
|
|
ACCEPTPAIR='iifname "fips0" ct state established,related accept comment "fips-reload-failed"'
|
|
DROPPAIR='iifname "fips0" drop comment "fips-reload-failed"'
|
|
|
|
# In input, forward and output, the only rule naming fips0 is fw4's jump into
|
|
# the fips zone's chain.
|
|
only_zone_jumps() {
|
|
local c dir lines
|
|
for c in input forward output; do
|
|
dir=iifname
|
|
[ "$c" = output ] && dir=oifname
|
|
lines="$(naming "$c")" || return 1
|
|
[ "$(printf '%s\n' "$lines" | grep -c .)" = 1 ] || return 1
|
|
case "$lines" in
|
|
"$dir \"fips0\" jump ${c}_fips "*) ;;
|
|
*) return 1 ;;
|
|
esac
|
|
done
|
|
return 0
|
|
}
|
|
|
|
# The failsafe pair heads the rules naming fips0 in <chain>, and a later rule
|
|
# naming fips0 jumps to <target>.
|
|
pair_heads() {
|
|
local lines
|
|
lines="$(naming "$1")" || return 1
|
|
[ "$(printf '%s\n' "$lines" | sed -n 1p)" = "$ACCEPTPAIR" ] || return 1
|
|
[ "$(printf '%s\n' "$lines" | sed -n 2p)" = "$DROPPAIR" ] || return 1
|
|
printf '%s\n' "$lines" | sed -n '3,$p' | grep -qF "jump $2 "
|
|
}
|
|
|
|
# The failsafe pair, once each, is everything naming fips0 in <chain>.
|
|
pair_only() {
|
|
local lines
|
|
lines="$(naming "$1")" || return 1
|
|
[ "$lines" = "$(printf '%s\n%s' "$ACCEPTPAIR" "$DROPPAIR")" ]
|
|
}
|
|
|
|
# <chain> holds exactly one rule of each half of the failsafe pair.
|
|
pair_once() {
|
|
listing "$1" || return 1
|
|
[ "$(printf '%s\n' "$LIST" | grep -cF "$ACCEPTPAIR")" = 1 ] &&
|
|
[ "$(printf '%s\n' "$LIST" | grep -cF "$DROPPAIR")" = 1 ]
|
|
}
|
|
|
|
# A rule in <chain> jumps to <target> and names fips0 (the lan set jump).
|
|
lan_jump() {
|
|
naming "$1" | grep -qF "jump $2 "
|
|
}
|
|
|
|
# In <chain>, the first line containing <a> comes before the first containing
|
|
# <b>, and both are present.
|
|
before() {
|
|
local la lb
|
|
listing "$1" || return 1
|
|
la="$(printf '%s\n' "$LIST" | grep -nF -- "$2" | sed -n '1s/:.*//p')"
|
|
lb="$(printf '%s\n' "$LIST" | grep -nF -- "$3" | sed -n '1s/:.*//p')"
|
|
[ -n "$la" ] && [ -n "$lb" ] && [ "$la" -lt "$lb" ]
|
|
}
|
|
|
|
# The rules of <chain> in order, without indentation, counters or the table and
|
|
# chain lines.
|
|
rules_of() {
|
|
listing "$1" || return 1
|
|
printf '%s\n' "$LIST" | sed -e 's/^[[:space:]]*//' -e '/^table /d' -e '/^chain /d' -e '/^}$/d' -e '/^$/d' \
|
|
-e 's/counter packets [0-9]* bytes [0-9]* //'
|
|
}
|
|
|
|
# Everything the fips zone accepts from FIPS peers, as fw4 renders the zone the
|
|
# setup creates with the packaged include: ping and port forwards to the router,
|
|
# port forwards through it, and nothing else.
|
|
INPUT_FIPS='icmpv6 type echo-request accept comment "!fw4: Allow-FIPS-Ping"
|
|
ct status dnat accept comment "fips: accept fips-gateway port forwards"
|
|
jump reject_from_fips'
|
|
FORWARD_FIPS='ct status dnat accept comment "fips: accept fips-gateway port forwards"
|
|
jump reject_to_fips'
|
|
|
|
zone_rules_exact() {
|
|
[ "$(rules_of input_fips)" = "$INPUT_FIPS" ] && [ "$(rules_of forward_fips)" = "$FORWARD_FIPS" ]
|
|
}
|
|
|
|
# The packaged wan include is the first rule of forward_wan, so it refuses
|
|
# ICMPv6 into fips0 before OpenWrt's stock Allow-ICMPv6-Forward accepts it.
|
|
WAN_ICMPV6='oifname "fips0" meta l4proto ipv6-icmp reject comment "fips: no ICMPv6 from wan into the mesh"'
|
|
|
|
wan_icmpv6_first() {
|
|
[ "$(rules_of forward_wan | sed -n 1p)" = "$WAN_ICMPV6" ] &&
|
|
before forward_wan 'comment "fips: no ICMPv6 from wan into the mesh"' 'Allow-ICMPv6-Forward'
|
|
}
|
|
|
|
no_reload_rules() {
|
|
table_lacks 'fips-reload-failed' && table_lacks 'fips-reload-probe'
|
|
}
|
|
|
|
no_old_accepts() { table_lacks 'comment "fips"'; }
|
|
|
|
# ── uci ─────────────────────────────────────────────────────────────────────
|
|
|
|
uget() { uci -q get "$1"; }
|
|
|
|
# A section's options, without its name: uci names an anonymous section by an
|
|
# id that changes when the sections before it change.
|
|
dump() { uci -q show "$1" | sed 's/^[^.]*\.[^.=]*//'; }
|
|
|
|
# The section of the first zone named <name>, as @zone[N].
|
|
zone_of() {
|
|
local z=0
|
|
while uci -q get "firewall.@zone[$z]" >/dev/null; do
|
|
if [ "$(uci -q get "firewall.@zone[$z].name")" = "$1" ]; then
|
|
echo "@zone[$z]"
|
|
return 0
|
|
fi
|
|
z=$((z + 1))
|
|
done
|
|
return 1
|
|
}
|
|
|
|
lan_device() {
|
|
local z d
|
|
z="$(zone_of lan)" || return 1
|
|
for d in $(uci -q get "firewall.$z.device"); do
|
|
[ "$d" = "$1" ] && return 0
|
|
done
|
|
return 1
|
|
}
|
|
|
|
# lan is found and lists br-lan, so a missing fips0 entry is not a missing zone.
|
|
lan_lacks_fips0() { lan_device br-lan && ! lan_device fips0; }
|
|
|
|
old_include() { uci -q show firewall | grep -qF "path='/etc/fips/firewall.sh'"; }
|
|
|
|
no_old_include() {
|
|
local s
|
|
s="$(uci -q show firewall)" || return 1
|
|
printf '%s\n' "$s" | grep -qF "=defaults" || return 1
|
|
! printf '%s\n' "$s" | grep -qF "path='/etc/fips/firewall.sh'"
|
|
}
|
|
|
|
zone_made() {
|
|
[ "$(uget firewall.fips)" = zone ] &&
|
|
[ "$(uget firewall.fips.name)" = fips ] &&
|
|
[ "$(uget firewall.fips.device)" = fips0 ] &&
|
|
[ "$(uget firewall.fips.input)" = REJECT ] &&
|
|
[ "$(uget firewall.fips.output)" = ACCEPT ] &&
|
|
[ "$(uget firewall.fips.forward)" = REJECT ] &&
|
|
[ "$(uget firewall.fips.auto_helper)" = 0 ]
|
|
}
|
|
|
|
forwarding_made() {
|
|
[ "$(uget firewall.fips_lan)" = forwarding ] &&
|
|
[ "$(uget firewall.fips_lan.src)" = lan ] &&
|
|
[ "$(uget firewall.fips_lan.dest)" = fips ]
|
|
}
|
|
|
|
ping_made() {
|
|
[ "$(uget firewall.fips_ping)" = rule ] &&
|
|
[ "$(uget firewall.fips_ping.src)" = fips ] &&
|
|
[ "$(uget firewall.fips_ping.proto)" = icmp ] &&
|
|
[ "$(uget firewall.fips_ping.icmp_type)" = echo-request ] &&
|
|
[ "$(uget firewall.fips_ping.family)" = ipv6 ] &&
|
|
[ "$(uget firewall.fips_ping.target)" = ACCEPT ]
|
|
}
|
|
|
|
# ── Output and reloads ──────────────────────────────────────────────────────
|
|
|
|
said() { grep -qF -- "$1" "$OUT"; }
|
|
|
|
said_once() { [ "$(grep -cF -- "$1" "$OUT")" = 1 ]; }
|
|
|
|
reloads() { grep -cx reload "$RELOADLOG" 2>/dev/null; }
|
|
|
|
one_reload() { [ "$(reloads)" = 1 ]; }
|
|
|
|
no_reload() { [ -f "$RELOADLOG" ] && [ "$(reloads)" = 0 ]; }
|
|
|
|
# ── Case scaffolding ────────────────────────────────────────────────────────
|
|
|
|
install_snippets() {
|
|
[ "$FW_SNIPPETS" = none ] && return 0
|
|
cp -R "$FW_SNIPPETS/." /usr/share/nftables.d/ ||
|
|
harness_fail "cannot copy the snippets from $FW_SNIPPETS"
|
|
}
|
|
|
|
# The container as the image ships it, with fw4 not running.
|
|
fw_clean() {
|
|
if [ -e /usr/sbin/nft.hidden ]; then
|
|
mv /usr/sbin/nft.hidden /usr/sbin/nft || harness_fail "cannot restore /usr/sbin/nft"
|
|
fi
|
|
fw4 -q flush >/dev/null 2>&1
|
|
rm -f /var/run/fw4.state /var/run/fw3.state
|
|
rm -rf /tmp/.uci "$WRAP" "$WORK"
|
|
rm -f /etc/nftables.d/zz-*.nft "$FLUSHONLY" "$FLUSHLIST" "$RELOADLOG"
|
|
rm -f /etc/fips/firewall.sh /etc/hotplug.d/net/99-fips /etc/uci-defaults/90-fips-setup
|
|
rm -f "$COPY" "$COPY.staged" "$COPY.now" "$MCOPY" "$MCOPY.staged" "$MCOPY.now"
|
|
rm -f "$WCOPY" "$WCOPY.staged" "$WCOPY.now"
|
|
rm -f /usr/bin/fips-mesh-setup
|
|
for l in /sys/class/net/fips-mesh*; do
|
|
[ -e "$l" ] && ip link del "${l##*/}" >/dev/null 2>&1
|
|
done
|
|
# A symbolic link when the case before put /etc/config on the small tmpfs.
|
|
rm -rf /etc/config /usr/share/nftables.d
|
|
[ -d "$SMALL" ] && rm -rf "$SMALL/config" "$SMALL/fill"
|
|
cp -a "$STOCK/config" /etc/config || harness_fail "cannot restore /etc/config"
|
|
cp -a "$STOCK/nftables.d" /usr/share/nftables.d || harness_fail "cannot restore /usr/share/nftables.d"
|
|
install_snippets
|
|
mkdir -p /var/run /var/lock /etc/fips /etc/hotplug.d/net /etc/uci-defaults "$WORK" "$WRAP"
|
|
# No netifd here, so lan gets a device of its own to render with, as a
|
|
# router's br-lan does.
|
|
uci add_list "firewall.$(zone_of lan).device=br-lan" && uci commit firewall ||
|
|
harness_fail "cannot add br-lan to the lan zone"
|
|
: > "$RELOADLOG"
|
|
}
|
|
|
|
# fw_begin <name> <what it shows> [nostart]: reset and start fw4.
|
|
fw_begin() {
|
|
CUR="$1"
|
|
note "$1: $2"
|
|
fw_clean
|
|
[ "${3:-}" = nostart ] && return 0
|
|
fw4 -q start >/dev/null 2>&1 || harness_fail "$CUR: fw4 -q start failed after the reset"
|
|
nft list table inet fw4 >/dev/null 2>&1 || harness_fail "$CUR: fw4 started but inet fw4 is not loaded"
|
|
return 0
|
|
}
|
|
|
|
fw_end() {
|
|
RAN=$((RAN + 1))
|
|
echo " done $CUR"
|
|
return 0
|
|
}
|
|
|
|
# The state the 0.5.2 package leaves: its setup, the firewall reload that runs
|
|
# its include, and its hotplug hook for fips0.
|
|
released_state() {
|
|
cp "$FIXTURES/released-firewall.sh" /etc/fips/firewall.sh &&
|
|
cp "$FIXTURES/released-99-fips" /etc/hotplug.d/net/99-fips &&
|
|
chmod 0755 /etc/fips/firewall.sh /etc/hotplug.d/net/99-fips ||
|
|
harness_fail "cannot install the released firewall files"
|
|
sh "$FIXTURES/released-90-fips-setup" >/dev/null 2>&1
|
|
/etc/init.d/firewall reload >/dev/null 2>&1
|
|
INTERFACE=fips0 ACTION=add sh /etc/hotplug.d/net/99-fips >/dev/null 2>&1
|
|
return 0
|
|
}
|
|
|
|
released_reached() {
|
|
has input 'iifname "fips0" accept comment "fips"' &&
|
|
has forward 'iifname "fips0" accept comment "fips"' &&
|
|
has forward 'oifname "fips0" accept comment "fips"' &&
|
|
has output 'oifname "fips0" accept comment "fips"' &&
|
|
lan_jump input input_lan &&
|
|
lan_device fips0 && old_include
|
|
}
|
|
|
|
# What opkg and apk do with the previous package's files that the new one no
|
|
# longer carries.
|
|
drop_released_files() {
|
|
rm -f /etc/fips/firewall.sh /etc/hotplug.d/net/99-fips
|
|
}
|
|
|
|
# run_setup [how]: run the setup under test, keeping its output in $OUT and
|
|
# its status in RC. how: exec (default), apk, sourced, path=<PATH>, wrap.
|
|
run_setup() {
|
|
: > "$RELOADLOG"
|
|
case "${1:-exec}" in
|
|
exec) sh "$FW_SETUP" > "$OUT" 2>&1 ;;
|
|
apk) env -i PATH="$SYSPATH" sh "$FW_SETUP" > "$OUT" 2>&1 ;;
|
|
sourced) (. "$FW_SETUP") > "$OUT" 2>&1 ;;
|
|
path=*) PATH="${1#path=}" sh "$FW_SETUP" > "$OUT" 2>&1 ;;
|
|
wrap) PATH="$WRAP:$PATH" sh "$FW_SETUP" > "$OUT" 2>&1 ;;
|
|
esac
|
|
RC=$?
|
|
sed 's/^/ | /' "$OUT"
|
|
return 0
|
|
}
|
|
|
|
# hook_path [wrap]: the PATH the setup runs with. The wifi stub comes first,
|
|
# so a wifi reload the mesh hook makes is logged; outside the mesh cases the
|
|
# stub does not exist. wrap puts the uci and nft wrappers next.
|
|
hook_path() {
|
|
if [ "${1:-}" = wrap ]; then
|
|
printf '%s' "$STUB:$WRAP:$PATH"
|
|
else
|
|
printf '%s' "$STUB:$PATH"
|
|
fi
|
|
}
|
|
|
|
# run_postinst [wrap]: install the setup in /etc/uci-defaults and run it as the
|
|
# package's postinst does, deleting it only when it ends with status 0.
|
|
run_postinst() {
|
|
local p
|
|
p="$(hook_path "${1:-}")"
|
|
cp "$FW_SETUP" /etc/uci-defaults/90-fips-setup && chmod 0755 /etc/uci-defaults/90-fips-setup ||
|
|
harness_fail "cannot install the setup in /etc/uci-defaults"
|
|
: > "$RELOADLOG"
|
|
PATH="$p" sh -c '/etc/uci-defaults/90-fips-setup && rm -f /etc/uci-defaults/90-fips-setup' > "$OUT" 2>&1
|
|
RC=$?
|
|
sed 's/^/ | /' "$OUT"
|
|
return 0
|
|
}
|
|
|
|
# Run the kept setup as the uci-defaults loop at boot does.
|
|
run_boot() {
|
|
: > "$RELOADLOG"
|
|
PATH="$(hook_path)" sh -c '( cd /etc/uci-defaults && . ./90-fips-setup ) && rm -f /etc/uci-defaults/90-fips-setup' > "$OUT" 2>&1
|
|
RC=$?
|
|
sed 's/^/ | /' "$OUT"
|
|
return 0
|
|
}
|
|
|
|
# Put /etc/config on the small tmpfs, so a save can run out of space as it
|
|
# does on a full overlay.
|
|
small_config() {
|
|
local size
|
|
size="$(df -k "$SMALL" 2>/dev/null | awk 'NR == 2 { print $2 }')"
|
|
[ -n "$size" ] && [ "$size" -le 256 ] ||
|
|
harness_fail "$SMALL is not a small tmpfs; the driver mounts one with --tmpfs"
|
|
mkdir "$SMALL/config" && cp -a /etc/config/. "$SMALL/config/" &&
|
|
rm -rf /etc/config && ln -s "$SMALL/config" /etc/config ||
|
|
harness_fail "cannot move /etc/config onto $SMALL"
|
|
}
|
|
|
|
# small_free <pages>: fill the small tmpfs until only <pages> 4 KiB pages are
|
|
# free.
|
|
small_free() {
|
|
local avail
|
|
avail="$(df -k "$SMALL" | awk 'NR == 2 { print $4 }')"
|
|
dd if=/dev/zero of="$SMALL/fill" bs=4096 count=$((avail / 4 - $1)) 2>/dev/null
|
|
[ "$(df -k "$SMALL" | awk 'NR == 2 { print $4 }')" = $(($1 * 4)) ]
|
|
}
|
|
|
|
wrap_uci_commit_fails() {
|
|
cat > "$WRAP/uci" <<'EOF'
|
|
#!/bin/sh
|
|
echo "$*" >> /tmp/fw-wrap/uci.log
|
|
[ "$*" = "commit firewall" ] && exit 1
|
|
exec /sbin/uci "$@"
|
|
EOF
|
|
chmod 0755 "$WRAP/uci"
|
|
}
|
|
|
|
# wrap_nft_fails <exact argument string to fail>
|
|
wrap_nft_fails() {
|
|
{
|
|
echo '#!/bin/sh'
|
|
echo 'echo "$*" >> /tmp/fw-wrap/nft.log'
|
|
printf '[ "$*" = %s ] && exit 1\n' "'$1'"
|
|
echo 'exec /usr/sbin/nft "$@"'
|
|
} > "$WRAP/nft"
|
|
chmod 0755 "$WRAP/nft"
|
|
}
|
|
|
|
wrap_called() { grep -qxF -- "$2" "$WRAP/$1.log" 2>/dev/null; }
|
|
|
|
# ── Successful upgrades ─────────────────────────────────────────────────────
|
|
|
|
# The assertions of an upgrade from 0.5.2 that loads cleanly.
|
|
upgrade_ok() {
|
|
check "the setup exits 0" same "$RC" 0
|
|
check "exactly one firewall reload" one_reload
|
|
check "fips0 is no longer a lan device" lan_lacks_fips0
|
|
check "no include names /etc/fips/firewall.sh" no_old_include
|
|
check "the fips zone is REJECT/ACCEPT/REJECT on fips0 with auto_helper 0" zone_made
|
|
check "the lan to fips forwarding exists" forwarding_made
|
|
check "the fips ping rule exists" ping_made
|
|
if table_has 'jump helper_lan'; then
|
|
check "no helper chain for the fips zone" table_lacks helper_fips
|
|
else
|
|
note "helper assertion not exercised on this host"
|
|
fi
|
|
check "base chains name fips0 only in the fips zone jumps" only_zone_jumps
|
|
check "input_fips accepts echo-request" has input_fips 'Allow-FIPS-Ping'
|
|
check "input_fips accepts port forwards" has input_fips 'ct status dnat accept'
|
|
check "input_fips ends in the reject jump" before input_fips 'ct status dnat accept' 'jump reject_from_fips'
|
|
check "forward_fips accepts port forwards" has forward_fips 'ct status dnat accept'
|
|
check "forward_lan jumps to accept_to_fips" has forward_lan 'jump accept_to_fips'
|
|
check "input_fips and forward_fips hold exactly the ping, port-forward and reject rules" zone_rules_exact
|
|
check "forward_wan refuses ICMPv6 into fips0 before the stock Allow-ICMPv6-Forward" wan_icmpv6_first
|
|
refute "no reload-failure warning" said 'firewall reload failed'
|
|
refute "no not-loaded warning" said 'firewall is not loaded'
|
|
refute "no could-not-be-checked warning" said 'could not be checked'
|
|
check "no failsafe or probe rule" no_reload_rules
|
|
check "no copy of the staged configuration is left in /tmp" eval '[ ! -e "$COPY" ]'
|
|
return 0
|
|
}
|
|
|
|
fw_upgrade_moves_fips0_into_its_own_zone() {
|
|
fw_begin fw_upgrade_moves_fips0_into_its_own_zone "an upgrade from 0.5.2 moves fips0 out of lan into the fips zone"
|
|
released_state
|
|
if precond "the 0.5.2 accepts, lan jump and include are live" released_reached; then
|
|
drop_released_files
|
|
run_setup
|
|
upgrade_ok
|
|
fi
|
|
fw_end
|
|
}
|
|
|
|
fw_upgrade_under_apk_environment_moves_fips0() {
|
|
fw_begin fw_upgrade_under_apk_environment_moves_fips0 "the same upgrade with apk-tools' PATH-only environment"
|
|
released_state
|
|
if precond "the 0.5.2 accepts, lan jump and include are live" released_reached; then
|
|
drop_released_files
|
|
run_setup apk
|
|
upgrade_ok
|
|
fi
|
|
fw_end
|
|
}
|
|
|
|
fw_path_without_sbin_still_migrates() {
|
|
fw_begin fw_path_without_sbin_still_migrates "a caller's PATH without /usr/sbin still loads the migration"
|
|
released_state
|
|
if precond "the 0.5.2 accepts, lan jump and include are live" released_reached; then
|
|
drop_released_files
|
|
run_setup path=/usr/bin:/sbin:/bin
|
|
upgrade_ok
|
|
fi
|
|
fw_end
|
|
}
|
|
|
|
fw_second_run_changes_nothing() {
|
|
fw_begin fw_second_run_changes_nothing "a later upgrade changes nothing and does not reload"
|
|
released_state
|
|
if precond "the 0.5.2 accepts, lan jump and include are live" released_reached; then
|
|
drop_released_files
|
|
run_setup
|
|
uci export firewall > "$WORK/first.uci"
|
|
run_setup
|
|
uci export firewall > "$WORK/second.uci"
|
|
check "the second run exits 0" same "$RC" 0
|
|
check "uci export firewall is byte-identical" cmp -s "$WORK/first.uci" "$WORK/second.uci"
|
|
check "the second run does not reload" no_reload
|
|
fi
|
|
fw_end
|
|
}
|
|
|
|
# operator_zone <input policy> [device]: the 0.5.2 state plus a zone of the
|
|
# operator's own holding fips0 by device, fips0 unless given.
|
|
operator_zone() {
|
|
released_state
|
|
uci add firewall zone >/dev/null &&
|
|
uci set firewall.@zone[-1].name=mesh &&
|
|
uci add_list "firewall.@zone[-1].device=${2:-fips0}" &&
|
|
uci set "firewall.@zone[-1].input=$1" &&
|
|
uci set firewall.@zone[-1].output=ACCEPT &&
|
|
uci set firewall.@zone[-1].forward=REJECT &&
|
|
uci commit firewall || harness_fail "cannot add the operator's zone"
|
|
dump "firewall.$(zone_of mesh)" > "$WORK/mesh.before"
|
|
}
|
|
|
|
operator_zone_ok() {
|
|
check "the setup exits 0" same "$RC" 0
|
|
check "no fips zone is created" same "$(uget firewall.fips)" ""
|
|
check "fips0 is no longer a lan device" lan_lacks_fips0
|
|
check "no include names /etc/fips/firewall.sh" no_old_include
|
|
dump "firewall.$(zone_of mesh)" > "$WORK/mesh.after"
|
|
check "the operator's zone is unchanged" cmp -s "$WORK/mesh.before" "$WORK/mesh.after"
|
|
check "exactly one firewall reload" one_reload
|
|
check "input jumps fips0 into the operator's zone" has input 'iifname "fips0" jump input_mesh'
|
|
refute "no reload-failure warning" said 'firewall reload failed'
|
|
refute "no could-not-be-checked warning" said 'could not be checked'
|
|
check "no failsafe or probe rule" no_reload_rules
|
|
check "the operator's zone is named as needing its own rules" \
|
|
said 'fips0 is in firewall zone mesh, not in the fips zone; fips-gateway port forwards into it'
|
|
check "forward_mesh has no port-forward accept" lacks forward_mesh 'ct status dnat accept'
|
|
}
|
|
|
|
fw_operator_zone_is_kept_and_named() {
|
|
fw_begin fw_operator_zone_is_kept_and_named "an operator's own zone holding fips0 is kept, and the upgrade says it needs rules"
|
|
operator_zone REJECT
|
|
if precond "the 0.5.2 accepts, lan jump and include are live" released_reached; then
|
|
drop_released_files
|
|
run_setup
|
|
operator_zone_ok
|
|
refute "no exposure line for a REJECT zone" said 'router services are reachable'
|
|
fi
|
|
fw_end
|
|
}
|
|
|
|
fw_operator_zone_accepting_input_is_reported() {
|
|
local v
|
|
# fw4 reads a policy in any case and from any leading part of its name.
|
|
for v in ACCEPT accept acc; do
|
|
fw_begin fw_operator_zone_accepting_input_is_reported "an operator's zone with input $v is reported as exposing router services"
|
|
operator_zone "$v"
|
|
if precond "input $v: the 0.5.2 accepts, lan jump and include are live" released_reached; then
|
|
drop_released_files
|
|
run_setup
|
|
operator_zone_ok
|
|
check "input $v: the exposure is reported" \
|
|
said 'fips0 is in firewall zone mesh, whose input policy is ACCEPT, so router services are reachable from FIPS peers'
|
|
fi
|
|
done
|
|
fw_end
|
|
}
|
|
|
|
fw_operator_wildcard_zone_holds_fips0() {
|
|
fw_begin fw_operator_wildcard_zone_holds_fips0 "an operator's zone holding fips0 by the device wildcard fips+ is kept, and no fips zone is made"
|
|
operator_zone REJECT fips+
|
|
if precond "the 0.5.2 accepts, lan jump and include are live" released_reached; then
|
|
drop_released_files
|
|
run_setup
|
|
check "the setup exits 0" same "$RC" 0
|
|
check "no fips zone is created" same "$(uget firewall.fips)" ""
|
|
check "no zone named fips" eval '! uci -q show firewall | grep -q "\.name=.fips.$"'
|
|
check "fips0 is no longer a lan device" lan_lacks_fips0
|
|
check "no include names /etc/fips/firewall.sh" no_old_include
|
|
dump "firewall.$(zone_of mesh)" > "$WORK/mesh.after"
|
|
check "the operator's zone is unchanged" cmp -s "$WORK/mesh.before" "$WORK/mesh.after"
|
|
check "exactly one firewall reload" one_reload
|
|
check "input jumps the fips wildcard into the operator's zone" has input 'iifname "fips*" jump input_mesh'
|
|
check "no failsafe or probe rule" no_reload_rules
|
|
check "the operator's zone is named as needing its own rules" \
|
|
said 'fips0 is in firewall zone mesh, not in the fips zone; fips-gateway port forwards into it'
|
|
fi
|
|
fw_end
|
|
}
|
|
|
|
# ── Fresh installs and an operator's configuration ──────────────────────────
|
|
|
|
fresh_ok() {
|
|
check "the setup exits 0" same "$RC" 0
|
|
check "the fips zone is REJECT/ACCEPT/REJECT on fips0 with auto_helper 0" zone_made
|
|
check "the lan to fips forwarding exists" forwarding_made
|
|
check "the fips ping rule exists" ping_made
|
|
check "exactly one firewall reload" one_reload
|
|
check "base chains name fips0 only in the fips zone jumps" only_zone_jumps
|
|
check "input_fips accepts echo-request" has input_fips 'Allow-FIPS-Ping'
|
|
check "input_fips accepts port forwards" has input_fips 'ct status dnat accept'
|
|
check "forward_fips accepts port forwards" has forward_fips 'ct status dnat accept'
|
|
check "forward_lan jumps to accept_to_fips" has forward_lan 'jump accept_to_fips'
|
|
check "input_fips and forward_fips hold exactly the ping, port-forward and reject rules" zone_rules_exact
|
|
check "forward_wan refuses ICMPv6 into fips0 before the stock Allow-ICMPv6-Forward" wan_icmpv6_first
|
|
check "no failsafe or probe rule" no_reload_rules
|
|
}
|
|
|
|
fw_fresh_install_creates_live_zone() {
|
|
fw_begin fw_fresh_install_creates_live_zone "a fresh install creates the zone, forwarding and rule, live"
|
|
run_setup
|
|
fresh_ok
|
|
fw_end
|
|
}
|
|
|
|
fw_sourced_setup_does_not_leak() {
|
|
fw_begin fw_sourced_setup_does_not_leak "sourced in a subshell, the setup works and leaks neither PATH nor its device variable"
|
|
: > "$RELOADLOG"
|
|
(
|
|
tundev=caller
|
|
before="$PATH"
|
|
trap 'printf "%s\n%s\n%s\n" "$before" "$PATH" "$tundev" > /tmp/fw-work/leak' EXIT
|
|
. "$FW_SETUP"
|
|
) > "$OUT" 2>&1
|
|
RC=$?
|
|
sed 's/^/ | /' "$OUT"
|
|
fresh_ok
|
|
if precond "the subshell wrote its PATH record" eval '[ -n "$(sed -n 1p "$WORK/leak" 2>/dev/null)" ]'; then
|
|
check "PATH after sourcing equals PATH before" same "$(sed -n 1p "$WORK/leak")" "$(sed -n 2p "$WORK/leak")"
|
|
check "the caller's tundev is unchanged" same "$(sed -n 3p "$WORK/leak")" caller
|
|
fi
|
|
fw_end
|
|
}
|
|
|
|
fw_operator_edits_survive_rerun() {
|
|
fw_begin fw_operator_edits_survive_rerun "an operator's edits to the zone survive a later run byte for byte"
|
|
run_setup
|
|
if precond "the first run created the fips zone" eval '[ "$(uget firewall.fips)" = zone ]'; then
|
|
uci set firewall.fips.input=ACCEPT &&
|
|
uci set firewall.op_ssh=rule &&
|
|
uci set firewall.op_ssh.name=Op-SSH &&
|
|
uci set firewall.op_ssh.src=fips &&
|
|
uci set firewall.op_ssh.proto=tcp &&
|
|
uci set firewall.op_ssh.dest_port=22 &&
|
|
uci set firewall.op_ssh.target=ACCEPT &&
|
|
uci commit firewall || harness_fail "cannot edit the zone"
|
|
uci export firewall > "$WORK/edited.uci"
|
|
run_setup
|
|
uci export firewall > "$WORK/after.uci"
|
|
check "the setup exits 0" same "$RC" 0
|
|
check "the edited configuration is byte-identical" cmp -s "$WORK/edited.uci" "$WORK/after.uci"
|
|
fi
|
|
fw_end
|
|
}
|
|
|
|
fw_lan_through_network_is_reported() {
|
|
local v opt
|
|
# The interface names fips0 by device, or by ifname as before OpenWrt 21.02;
|
|
# and by device again with lan's input hardened to REJECT, which is not an
|
|
# ACCEPT exposure but still gives FIPS peers the access of LAN hosts.
|
|
for v in device ifname reject; do
|
|
opt=$v
|
|
[ "$v" = reject ] && opt=device
|
|
fw_begin fw_lan_through_network_is_reported "fips0 in lan through a network interface's $opt option is left alone and reported ($v)"
|
|
# The image has no network configuration of its own.
|
|
touch /etc/config/network
|
|
uci set network.fipsnet=interface &&
|
|
uci set "network.fipsnet.$opt=fips0" &&
|
|
uci set network.fipsnet.proto=none &&
|
|
uci commit network &&
|
|
uci add_list "firewall.$(zone_of lan).network=fipsnet" &&
|
|
uci commit firewall || harness_fail "cannot add fipsnet to lan"
|
|
if [ "$v" = reject ]; then
|
|
uci set "firewall.$(zone_of lan).input=REJECT" && uci commit firewall ||
|
|
harness_fail "cannot harden the lan zone's input"
|
|
fi
|
|
dump "firewall.$(zone_of lan)" > "$WORK/lan.before"
|
|
run_setup
|
|
dump "firewall.$(zone_of lan)" > "$WORK/lan.after"
|
|
check "$v: the setup exits 0" same "$RC" 0
|
|
check "$v: no fips zone is created" same "$(uget firewall.fips)" ""
|
|
check "$v: the lan zone is untouched" cmp -s "$WORK/lan.before" "$WORK/lan.after"
|
|
if [ "$v" = reject ]; then
|
|
check "reject: the lan membership is reported" \
|
|
said_once 'fips0 is in the lan firewall zone, so FIPS peers have the access of LAN hosts'
|
|
refute "reject: no ACCEPT exposure line" said 'whose input policy is ACCEPT'
|
|
else
|
|
check "$v: the exposure is reported" \
|
|
said 'fips0 is in firewall zone lan, whose input policy is ACCEPT, so router services are reachable from FIPS peers'
|
|
fi
|
|
done
|
|
fw_end
|
|
}
|
|
|
|
fw_old_include_and_files_removed() {
|
|
fw_begin fw_old_include_and_files_removed "with fips0 already out of lan by hand, the include, its accepts and the old files go"
|
|
released_state
|
|
uci del_list "firewall.$(zone_of lan).device=fips0" && uci commit firewall ||
|
|
harness_fail "cannot remove fips0 from lan"
|
|
if precond "the inserted input accept is live and both old files are present" \
|
|
eval 'has input "iifname \"fips0\" accept comment \"fips\"" && [ -f /etc/fips/firewall.sh ] && [ -f /etc/hotplug.d/net/99-fips ]'; then
|
|
run_setup
|
|
check "the setup exits 0" same "$RC" 0
|
|
check "no include names /etc/fips/firewall.sh" no_old_include
|
|
check "base chains name fips0 only in the fips zone jumps" only_zone_jumps
|
|
check "/etc/fips/firewall.sh is gone" eval '[ ! -e /etc/fips/firewall.sh ]'
|
|
check "/etc/hotplug.d/net/99-fips is gone" eval '[ ! -e /etc/hotplug.d/net/99-fips ]'
|
|
fi
|
|
fw_end
|
|
}
|
|
|
|
# A second copy of the old include follows the first, so a walk that steps past
|
|
# the index uci has just renumbered leaves one behind.
|
|
fw_unrelated_include_survives() {
|
|
fw_begin fw_unrelated_include_survives "an unrelated include survives and every copy of the old one goes"
|
|
printf '#!/bin/sh\n' > /etc/fw-unrelated.sh
|
|
chmod 0755 /etc/fw-unrelated.sh
|
|
uci add firewall include >/dev/null &&
|
|
uci set firewall.@include[-1].path=/etc/fw-unrelated.sh &&
|
|
uci set firewall.@include[-1].type=script &&
|
|
uci commit firewall || harness_fail "cannot add the unrelated include"
|
|
released_state
|
|
uci add firewall include >/dev/null &&
|
|
uci set firewall.@include[-1].path=/etc/fips/firewall.sh &&
|
|
uci set firewall.@include[-1].reload=1 &&
|
|
uci commit firewall || harness_fail "cannot add the second old include"
|
|
if precond "the unrelated include precedes two copies of the old one" \
|
|
eval '[ "$(uget firewall.@include[0].path)" = /etc/fw-unrelated.sh ] && [ "$(uget firewall.@include[1].path)" = /etc/fips/firewall.sh ] && [ "$(uget firewall.@include[2].path)" = /etc/fips/firewall.sh ]'; then
|
|
drop_released_files
|
|
run_setup
|
|
check "the setup exits 0" same "$RC" 0
|
|
check "the unrelated include survives" same "$(uget firewall.@include[0].path)" /etc/fw-unrelated.sh
|
|
check "no include names /etc/fips/firewall.sh" no_old_include
|
|
check "only one include is left" eval '! uci -q get firewall.@include[1] >/dev/null'
|
|
fi
|
|
rm -f /etc/fw-unrelated.sh
|
|
fw_end
|
|
}
|
|
|
|
fw_no_lan_zone_creates_zone_without_forwarding() {
|
|
local how
|
|
for how in fresh upgrade; do
|
|
fw_begin fw_no_lan_zone_creates_zone_without_forwarding "with no lan zone ($how), the zone and rule are created and the missing forwarding is reported once"
|
|
uci delete "firewall.$(zone_of lan)" && uci commit firewall || harness_fail "cannot delete the lan zone"
|
|
if [ "$how" = upgrade ]; then
|
|
released_state
|
|
precond "upgrade: the 0.5.2 include is present" old_include || continue
|
|
drop_released_files
|
|
fi
|
|
run_setup
|
|
check "$how: the setup exits 0" same "$RC" 0
|
|
check "$how: the fips zone is created" zone_made
|
|
check "$how: the fips ping rule exists" ping_made
|
|
check "$how: no forwarding is created" same "$(uget firewall.fips_lan)" ""
|
|
check "$how: one line reports the missing lan zone" said_once 'no lan firewall zone'
|
|
refute "$how: no second line about the forwarding" said 'has no forwarding from lan'
|
|
done
|
|
fw_end
|
|
}
|
|
|
|
fw_operator_scoping_renders_before_port_forwards() {
|
|
fw_begin fw_operator_scoping_renders_before_port_forwards "an operator's src fips rules render ahead of the port-forward accept"
|
|
run_setup
|
|
uci set firewall.op_in=rule &&
|
|
uci set firewall.op_in.name=Op-Scope-Input &&
|
|
uci set firewall.op_in.src=fips &&
|
|
uci set firewall.op_in.proto=tcp &&
|
|
uci set firewall.op_in.dest_port=2222 &&
|
|
uci set firewall.op_in.target=DROP &&
|
|
uci set firewall.op_fwd=rule &&
|
|
uci set firewall.op_fwd.name=Op-Scope-Forward &&
|
|
uci set firewall.op_fwd.src=fips &&
|
|
uci set firewall.op_fwd.dest=lan &&
|
|
uci set firewall.op_fwd.proto=tcp &&
|
|
uci set firewall.op_fwd.dest_port=2222 &&
|
|
uci set firewall.op_fwd.target=DROP &&
|
|
uci commit firewall || harness_fail "cannot add the scoping rules"
|
|
/etc/init.d/firewall reload >/dev/null 2>&1
|
|
check "input_fips: operator rule before the port-forward accept" before input_fips 'Op-Scope-Input' 'ct status dnat accept'
|
|
check "input_fips: port-forward accept before the policy jump" before input_fips 'ct status dnat accept' 'jump reject_from_fips'
|
|
check "forward_fips: operator rule before the port-forward accept" before forward_fips 'Op-Scope-Forward' 'ct status dnat accept'
|
|
check "forward_fips: port-forward accept before the policy jump" before forward_fips 'ct status dnat accept' 'jump reject_to_fips'
|
|
fw_end
|
|
}
|
|
|
|
# ── Sections that are not ours ──────────────────────────────────────────────
|
|
|
|
fw_bare_plus_device_does_not_hold_fips0() {
|
|
fw_begin fw_bare_plus_device_does_not_hold_fips0 "a bare + device does not hold fips0; a foreign fips_ping section is kept"
|
|
uci add firewall zone >/dev/null &&
|
|
uci set firewall.@zone[-1].name=plus &&
|
|
uci add_list firewall.@zone[-1].device=+ &&
|
|
uci set firewall.@zone[-1].input=REJECT &&
|
|
uci set firewall.fips_ping=forwarding &&
|
|
uci set firewall.fips_ping.src=lan &&
|
|
uci set firewall.fips_ping.dest=wan &&
|
|
uci commit firewall || harness_fail "cannot add the sections"
|
|
uci show firewall.fips_ping > "$WORK/ping.before"
|
|
run_setup
|
|
uci show firewall.fips_ping > "$WORK/ping.after"
|
|
check "the setup exits 0" same "$RC" 0
|
|
check "the fips zone is created" zone_made
|
|
check "fips_ping keeps its type and options" cmp -s "$WORK/ping.before" "$WORK/ping.after"
|
|
check "one line names fips_ping" said_once 'firewall section fips_ping exists'
|
|
fw_end
|
|
}
|
|
|
|
fw_operator_fips_ping_rule_kept() {
|
|
fw_begin fw_operator_fips_ping_rule_kept "an operator's rule named fips_ping is kept byte for byte"
|
|
uci set firewall.fips_ping=rule &&
|
|
uci set firewall.fips_ping.name=My-Ping &&
|
|
uci set firewall.fips_ping.src=wan &&
|
|
uci set firewall.fips_ping.proto=icmp &&
|
|
uci add_list firewall.fips_ping.icmp_type=echo-reply &&
|
|
uci set firewall.fips_ping.family=ipv6 &&
|
|
uci set firewall.fips_ping.target=ACCEPT &&
|
|
uci commit firewall || harness_fail "cannot add the operator's fips_ping"
|
|
uci show firewall.fips_ping > "$WORK/ping.before"
|
|
run_setup
|
|
uci show firewall.fips_ping > "$WORK/ping.after"
|
|
check "the setup exits 0" same "$RC" 0
|
|
check "the fips zone is created" zone_made
|
|
check "fips_ping keeps its type and options" cmp -s "$WORK/ping.before" "$WORK/ping.after"
|
|
check "one line names fips_ping" said_once 'firewall section fips_ping exists'
|
|
fw_end
|
|
}
|
|
|
|
# foreign_fips_zone <default input>: an anonymous zone named fips that does not
|
|
# hold fips0.
|
|
foreign_fips_zone() {
|
|
uci set "firewall.@defaults[0].input=$1" &&
|
|
uci add firewall zone >/dev/null &&
|
|
uci set firewall.@zone[-1].name=fips &&
|
|
uci set firewall.@zone[-1].input=REJECT &&
|
|
uci commit firewall || harness_fail "cannot add the anonymous fips zone"
|
|
}
|
|
|
|
foreign_zone_ok() {
|
|
check "the setup exits 0" same "$RC" 0
|
|
check "no firewall.fips section" same "$(uget firewall.fips)" ""
|
|
check "no fips_lan section" same "$(uget firewall.fips_lan)" ""
|
|
check "no fips_ping section" same "$(uget firewall.fips_ping)" ""
|
|
check "one line names the existing zone" said_once 'a firewall zone named fips already exists'
|
|
}
|
|
|
|
fw_foreign_fips_zone_not_doubled() {
|
|
fw_begin fw_foreign_fips_zone_not_doubled "a zone named fips in another section stops the zone being created"
|
|
foreign_fips_zone REJECT
|
|
run_setup
|
|
foreign_zone_ok
|
|
refute "no exposure line under a REJECT default" said 'router services are reachable'
|
|
fw_end
|
|
}
|
|
|
|
fw_foreign_fips_zone_under_accept_default_reported() {
|
|
local v
|
|
# fw4 reads a policy in any case and from any leading part of its name.
|
|
for v in ACCEPT accept acc; do
|
|
fw_begin fw_foreign_fips_zone_under_accept_default_reported "with no zone for fips0 and a default input of $v, the exposure is reported"
|
|
foreign_fips_zone "$v"
|
|
run_setup
|
|
foreign_zone_ok
|
|
check "default input $v: the exposure is reported" \
|
|
said 'fips0 is in no firewall zone and the default input policy is ACCEPT, so router services are reachable from FIPS peers'
|
|
done
|
|
fw_end
|
|
}
|
|
|
|
# An operator's anonymous zone named fips holds fips0 at an upgrade from 0.5.2.
|
|
# It gets the packaged port-forward include, which fw4 places by chain name,
|
|
# but no forwarding from lan is created for it, so the upgrade names that.
|
|
fw_operator_zone_named_fips_without_lan_forwarding_is_named() {
|
|
local fwd
|
|
for fwd in none lan disabled; do
|
|
fw_begin fw_operator_zone_named_fips_without_lan_forwarding_is_named "an operator's zone named fips holding fips0, forwarding from lan: $fwd"
|
|
released_state
|
|
uci add firewall zone >/dev/null &&
|
|
uci set firewall.@zone[-1].name=fips &&
|
|
uci add_list firewall.@zone[-1].device=fips0 &&
|
|
uci set firewall.@zone[-1].input=REJECT &&
|
|
uci set firewall.@zone[-1].output=ACCEPT &&
|
|
uci set firewall.@zone[-1].forward=REJECT &&
|
|
uci commit firewall || harness_fail "cannot add the anonymous fips zone"
|
|
if [ "$fwd" != none ]; then
|
|
uci add firewall forwarding >/dev/null &&
|
|
uci set firewall.@forwarding[-1].src=lan &&
|
|
uci set firewall.@forwarding[-1].dest=fips &&
|
|
uci commit firewall || harness_fail "cannot add the lan to fips forwarding"
|
|
fi
|
|
if [ "$fwd" = disabled ]; then
|
|
uci set firewall.@forwarding[-1].enabled=0 && uci commit firewall ||
|
|
harness_fail "cannot disable the lan to fips forwarding"
|
|
fi
|
|
if precond "$fwd: the 0.5.2 accepts, lan jump and include are live" released_reached; then
|
|
drop_released_files
|
|
run_setup
|
|
check "$fwd: the setup exits 0" same "$RC" 0
|
|
check "$fwd: no firewall.fips section" same "$(uget firewall.fips)" ""
|
|
check "$fwd: no fips_lan section" same "$(uget firewall.fips_lan)" ""
|
|
check "$fwd: fips0 is no longer a lan device" lan_lacks_fips0
|
|
check "$fwd: input jumps fips0 into the zone" has input 'iifname "fips0" jump input_fips'
|
|
refute "$fwd: no line calls it not the fips zone" said 'not in the fips zone'
|
|
if [ "$fwd" != lan ]; then
|
|
check "$fwd: the missing lan forwarding is named" \
|
|
said_once 'fips0 is in a firewall zone named fips that has no forwarding from lan'
|
|
else
|
|
refute "lan: no line about a missing lan forwarding" said 'has no forwarding from lan'
|
|
fi
|
|
fi
|
|
done
|
|
fw_end
|
|
}
|
|
|
|
fw_disabled_zone_does_not_hold_fips0() {
|
|
local v
|
|
# fw4's false values for a boolean option.
|
|
for v in 0 off false no; do
|
|
fw_begin fw_disabled_zone_does_not_hold_fips0 "a zone holding fips0 with enabled $v does not count"
|
|
uci add firewall zone >/dev/null &&
|
|
uci set firewall.@zone[-1].name=off &&
|
|
uci add_list firewall.@zone[-1].device=fips0 &&
|
|
uci set "firewall.@zone[-1].enabled=$v" &&
|
|
uci commit firewall || harness_fail "cannot add the disabled zone"
|
|
run_setup
|
|
check "enabled $v: the setup exits 0" same "$RC" 0
|
|
check "enabled $v: the fips zone is created" zone_made
|
|
check "enabled $v: input jumps fips0 into the fips zone" has input 'iifname "fips0" jump input_fips'
|
|
done
|
|
fw_end
|
|
}
|
|
|
|
fw_disabled_fips_zone_under_accept_default_reported() {
|
|
fw_begin fw_disabled_fips_zone_under_accept_default_reported "an upgrade onto a disabled fips zone under an ACCEPT default migrates and reports the exposure"
|
|
released_state
|
|
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.enabled=0 &&
|
|
uci set firewall.@defaults[0].input=ACCEPT &&
|
|
uci commit firewall || harness_fail "cannot add the disabled fips zone"
|
|
uci show firewall.fips > "$WORK/fips.before"
|
|
if precond "the 0.5.2 accepts, lan jump and include are live" released_reached; then
|
|
drop_released_files
|
|
run_setup
|
|
uci show firewall.fips > "$WORK/fips.after"
|
|
check "the setup exits 0" same "$RC" 0
|
|
check "no include names /etc/fips/firewall.sh" no_old_include
|
|
check "fips0 is no longer a lan device" lan_lacks_fips0
|
|
check "firewall.fips is unchanged" cmp -s "$WORK/fips.before" "$WORK/fips.after"
|
|
check "no new zone named fips" eval '[ "$(uci -q show firewall | grep -c "\.name=.fips.$")" = 1 ]'
|
|
check "the exposure is reported" \
|
|
said 'fips0 is in no firewall zone and the default input policy is ACCEPT, so router services are reachable from FIPS peers'
|
|
fi
|
|
fw_end
|
|
}
|
|
|
|
fw_downgrade_then_upgrade_migrates_again() {
|
|
fw_begin fw_downgrade_then_upgrade_migrates_again "after a downgrade put fips0 back in lan, an upgrade migrates again"
|
|
run_setup
|
|
uci show firewall.fips > "$WORK/sections.before"
|
|
uci show firewall.fips_lan >> "$WORK/sections.before"
|
|
uci show firewall.fips_ping >> "$WORK/sections.before"
|
|
released_state
|
|
if precond "fips0 is a lan device again, the include is back, and the lan jump precedes the fips jump" \
|
|
eval 'lan_device fips0 && old_include && before input "jump input_lan" "jump input_fips" && lan_jump input input_lan'; then
|
|
drop_released_files
|
|
run_setup
|
|
uci show firewall.fips > "$WORK/sections.after"
|
|
uci show firewall.fips_lan >> "$WORK/sections.after"
|
|
uci show firewall.fips_ping >> "$WORK/sections.after"
|
|
check "the setup exits 0" same "$RC" 0
|
|
check "fips0 is no longer a lan device" lan_lacks_fips0
|
|
check "no include names /etc/fips/firewall.sh" no_old_include
|
|
check "the zone, forwarding and rule are as before the downgrade" cmp -s "$WORK/sections.before" "$WORK/sections.after"
|
|
check "base chains name fips0 only in the fips zone jumps" only_zone_jumps
|
|
check "exactly one firewall reload" one_reload
|
|
refute "no line calls the fips zone another zone" said 'not in the fips zone'
|
|
refute "no line about a missing lan forwarding" said 'has no forwarding from lan'
|
|
fi
|
|
fw_end
|
|
}
|
|
|
|
# The operator renamed the fips zone, which fw4 then renders as input_mesh0,
|
|
# and a downgrade put fips0 back in lan: the upgrade checks the load by the
|
|
# zone's own name.
|
|
fw_renamed_fips_zone_is_checked_by_its_name() {
|
|
fw_begin fw_renamed_fips_zone_is_checked_by_its_name "an upgrade onto the fips zone renamed mesh0 loads cleanly and is taken as loaded"
|
|
run_setup
|
|
# The setup under test creates the zone, so a missing one is a red result.
|
|
if precond "the first run created the fips zone" eval '[ "$(uget firewall.fips)" = zone ]'; then
|
|
uci set firewall.fips.name=mesh0 && uci commit firewall || harness_fail "cannot rename the fips zone"
|
|
/etc/init.d/firewall reload >/dev/null 2>&1
|
|
released_state
|
|
fi
|
|
if [ "$(uget firewall.fips.name)" = mesh0 ] &&
|
|
precond "fips0 is a lan device again, the include is back, and input jumps to input_mesh0" \
|
|
eval 'lan_device fips0 && old_include && has input "jump input_mesh0 "'; then
|
|
drop_released_files
|
|
run_setup
|
|
check "the setup exits 0" same "$RC" 0
|
|
check "exactly one firewall reload" one_reload
|
|
check "fips0 is no longer a lan device" lan_lacks_fips0
|
|
check "input jumps fips0 into the renamed zone" has input 'iifname "fips0" jump input_mesh0 '
|
|
refute "no could-not-be-checked warning" said 'could not be checked'
|
|
refute "no reload-failure warning" said 'firewall reload failed'
|
|
check "no failsafe or probe rule" no_reload_rules
|
|
fi
|
|
fw_end
|
|
}
|
|
|
|
# ── No firewall running ─────────────────────────────────────────────────────
|
|
|
|
migrated() {
|
|
check "fips0 is no longer a lan device" lan_lacks_fips0
|
|
check "no include names /etc/fips/firewall.sh" no_old_include
|
|
check "the fips zone is created" zone_made
|
|
}
|
|
|
|
fw_no_firewall_migrates_without_reload() {
|
|
fw_begin fw_no_firewall_migrates_without_reload "with neither state file nor table, as at boot, the setup migrates and does not reload"
|
|
released_state
|
|
fw4 -q flush >/dev/null 2>&1
|
|
if precond "fips0 in lan, the include present, and neither state file nor table" \
|
|
eval 'lan_device fips0 && old_include && [ ! -e /var/run/fw4.state ] && ! nft list table inet fw4 >/dev/null 2>&1'; then
|
|
run_setup
|
|
check "the setup exits 0" same "$RC" 0
|
|
migrated
|
|
check "no reload is attempted" no_reload
|
|
fi
|
|
fw_end
|
|
}
|
|
|
|
fw_sysupgrade_first_boot_migrates() {
|
|
fw_begin fw_sysupgrade_first_boot_migrates "the first boot after a sysupgrade migrates and removes the kept firewall.sh"
|
|
released_state
|
|
rm -f /etc/hotplug.d/net/99-fips
|
|
fw4 -q flush >/dev/null 2>&1
|
|
if precond "firewall.sh kept, 99-fips absent, no state file, fips0 in lan" \
|
|
eval '[ -f /etc/fips/firewall.sh ] && [ ! -e /etc/hotplug.d/net/99-fips ] && [ ! -e /var/run/fw4.state ] && lan_device fips0'; then
|
|
run_setup sourced
|
|
check "the setup exits 0" same "$RC" 0
|
|
migrated
|
|
check "no reload is attempted" no_reload
|
|
check "/etc/fips/firewall.sh is gone" eval '[ ! -e /etc/fips/firewall.sh ]'
|
|
fi
|
|
fw_end
|
|
}
|
|
|
|
fw_missing_state_file_blocks_fips0() {
|
|
fw_begin fw_missing_state_file_blocks_fips0 "a loaded table without fw4's state file fails the reload, so fips0 is closed"
|
|
released_state
|
|
rm -f /var/run/fw4.state
|
|
if precond "the 0.5.2 state is live, the table lists and the state file is absent" \
|
|
eval 'released_reached && [ ! -e /var/run/fw4.state ]'; then
|
|
drop_released_files
|
|
run_setup
|
|
check "the setup exits 0" same "$RC" 0
|
|
check "exactly one firewall reload" one_reload
|
|
check "the missing state file is reported" said "the firewall reload failed because fw4's state file is missing"
|
|
check "input: the failsafe pair precedes the lan jump naming fips0" pair_heads input input_lan
|
|
check "forward: the failsafe pair precedes the lan jump naming fips0" pair_heads forward forward_lan
|
|
check "no old accept in any chain" no_old_accepts
|
|
fi
|
|
fw_end
|
|
}
|
|
|
|
# fw4 can load the new configuration, but no table was loaded to put the probe
|
|
# in, so the load is judged by the jump into the fips zone alone.
|
|
fw_reload_without_prior_table_is_judged_by_the_zone_jump() {
|
|
fw_begin fw_reload_without_prior_table_is_judged_by_the_zone_jump "with the state file present and no table loaded, a clean reload is taken as loaded"
|
|
released_state
|
|
if precond "the 0.5.2 accepts, lan jump and include are live" released_reached; then
|
|
nft delete table inet fw4 >/dev/null 2>&1
|
|
if precond "the state file exists and inet fw4 is not loaded" \
|
|
eval '[ -e /var/run/fw4.state ] && ! nft list table inet fw4 >/dev/null 2>&1'; then
|
|
drop_released_files
|
|
run_setup
|
|
upgrade_ok
|
|
fi
|
|
fi
|
|
fw_end
|
|
}
|
|
|
|
fw_unloaded_firewall_is_reported() {
|
|
fw_begin fw_unloaded_firewall_is_reported "a state file with no table loaded is reported" nostart
|
|
echo 'this is not nft;' > /etc/nftables.d/zz-invalid.nft
|
|
fw4 -q start >/dev/null 2>&1
|
|
if precond "the state file exists and inet fw4 is not loaded" \
|
|
eval '[ -e /var/run/fw4.state ] && ! nft list table inet fw4 >/dev/null 2>&1'; then
|
|
run_setup
|
|
check "the setup exits 0" same "$RC" 0
|
|
check "the fips zone is created" zone_made
|
|
check "the unloaded firewall is reported" said 'fw4 firewall is not loaded, so its zones do not filter'
|
|
fi
|
|
fw_end
|
|
}
|
|
|
|
# ── A reload that fails ─────────────────────────────────────────────────────
|
|
|
|
failed_reload_ok() {
|
|
check "the setup exits 0" same "$RC" 0
|
|
migrated
|
|
check "the failed reload is reported" \
|
|
said 'the firewall reload failed, so new connections from FIPS peers to this router and all fips-gateway port forwards are blocked'
|
|
check "input: the failsafe pair precedes the lan jump naming fips0" pair_heads input input_lan
|
|
check "forward: the failsafe pair precedes the lan jump naming fips0" pair_heads forward forward_lan
|
|
check "no old accept in any chain" no_old_accepts
|
|
check "no probe rule" table_lacks fips-reload-probe
|
|
}
|
|
|
|
failed_reload_start() {
|
|
released_state
|
|
precond "the 0.5.2 accepts, lan jump and include are live" released_reached || return 1
|
|
drop_released_files
|
|
echo 'this is not nft;' > /etc/nftables.d/zz-invalid.nft
|
|
precond "fw4 check fails on the invalid include" eval '! fw4 check >/dev/null 2>&1'
|
|
}
|
|
|
|
fw_failed_reload_blocks_new_fips0_connections() {
|
|
fw_begin fw_failed_reload_blocks_new_fips0_connections "a reload that fails to load closes fips0 in the old ruleset"
|
|
if failed_reload_start; then
|
|
run_setup
|
|
failed_reload_ok
|
|
fi
|
|
fw_end
|
|
}
|
|
|
|
fw_failed_reload_under_apk_environment_blocks_fips0() {
|
|
fw_begin fw_failed_reload_under_apk_environment_blocks_fips0 "the same failed reload with apk-tools' PATH-only environment"
|
|
if failed_reload_start; then
|
|
run_setup apk
|
|
failed_reload_ok
|
|
fi
|
|
fw_end
|
|
}
|
|
|
|
# Continues from fw_failed_reload_blocks_new_fips0_connections, without a reset.
|
|
fw_recovery_after_failed_reload_clears_block() {
|
|
CUR=fw_recovery_after_failed_reload_clears_block
|
|
note "$CUR: once the invalid include is gone, a reload clears the block"
|
|
if precond "the failed reload left one failsafe pair in input and in forward" \
|
|
eval 'pair_once input && pair_once forward'; then
|
|
rm -f /etc/nftables.d/zz-invalid.nft
|
|
/etc/init.d/firewall reload >/dev/null 2>&1
|
|
check "no failsafe rule" table_lacks fips-reload-failed
|
|
check "base chains name fips0 only in the fips zone jumps" only_zone_jumps
|
|
fi
|
|
fw_end
|
|
}
|
|
|
|
# ── A save that fails ───────────────────────────────────────────────────────
|
|
|
|
# commit_fails_start [drop]: the 0.5.2 state, then the setup run as the
|
|
# postinst runs it, with every commit firewall failing. drop removes the old
|
|
# files first, as a package upgrade does.
|
|
commit_fails_start() {
|
|
released_state
|
|
precond "the 0.5.2 accepts, lan jump and include are live" released_reached || return 1
|
|
[ "${1:-}" = drop ] && drop_released_files
|
|
cp /etc/config/firewall "$WORK/firewall.before"
|
|
wrap_uci_commit_fails
|
|
run_postinst wrap
|
|
precond "the uci wrapper failed a commit firewall" wrap_called uci 'commit firewall'
|
|
}
|
|
|
|
commit_fails_ok() {
|
|
check "the setup exits 1" same "$RC" 1
|
|
check "the setup is kept in /etc/uci-defaults" eval '[ -f /etc/uci-defaults/90-fips-setup ]'
|
|
check "/etc/config/firewall is unchanged" cmp -s "$WORK/firewall.before" /etc/config/firewall
|
|
check "no reload is attempted" no_reload
|
|
check "the failed save is reported" \
|
|
said 'the firewall configuration could not be saved, so fips0 may still be in the lan zone; new connections'
|
|
check "input: the failsafe pair precedes the lan jump naming fips0" pair_heads input input_lan
|
|
check "forward: the failsafe pair precedes the lan jump naming fips0" pair_heads forward forward_lan
|
|
check "no old accept in any chain" no_old_accepts
|
|
}
|
|
|
|
fw_failed_commit_blocks_and_keeps_setup() {
|
|
fw_begin fw_failed_commit_blocks_and_keeps_setup "a failed save closes fips0 and keeps the setup for the next boot"
|
|
if commit_fails_start drop; then
|
|
commit_fails_ok
|
|
fi
|
|
fw_end
|
|
}
|
|
|
|
# Continues from fw_failed_commit_blocks_and_keeps_setup: the second run an
|
|
# SDK-feed install makes, with the save still failing.
|
|
fw_second_sdk_run_after_failed_commit_keeps_one_block() {
|
|
CUR=fw_second_sdk_run_after_failed_commit_keeps_one_block
|
|
note "$CUR: a second run in the same install retries the save and stacks no second failsafe"
|
|
if precond "the first run's changes are still staged" eval '[ -n "$(uci -q changes firewall)" ]'; then
|
|
: > "$RELOADLOG"
|
|
PATH="$WRAP:$PATH" sh -c '( cd /etc/uci-defaults && . ./90-fips-setup ) && rm -f /etc/uci-defaults/90-fips-setup' > "$OUT" 2>&1
|
|
RC=$?
|
|
sed 's/^/ | /' "$OUT"
|
|
check "the run ends non-zero" eval '[ "$RC" != 0 ]'
|
|
check "the setup is still kept" eval '[ -f /etc/uci-defaults/90-fips-setup ]'
|
|
check "the save was tried again" eval '[ "$(grep -cx "commit firewall" /tmp/fw-wrap/uci.log)" = 2 ]'
|
|
check "the failed save is reported" said 'the firewall configuration could not be saved'
|
|
check "input holds one failsafe pair" pair_once input
|
|
check "forward holds one failsafe pair" pair_once forward
|
|
check "input: the failsafe pair precedes the lan jump naming fips0" pair_heads input input_lan
|
|
check "forward: the failsafe pair precedes the lan jump naming fips0" pair_heads forward forward_lan
|
|
fi
|
|
fw_end
|
|
}
|
|
|
|
# Continues from fw_second_sdk_run_after_failed_commit_keeps_one_block: the
|
|
# next boot, when the save works.
|
|
fw_boot_retry_after_failed_commit_migrates() {
|
|
CUR=fw_boot_retry_after_failed_commit_migrates
|
|
note "$CUR: at the next boot the kept setup migrates, then the firewall starts clean"
|
|
rm -rf "$WRAP"
|
|
mkdir -p "$WRAP"
|
|
fw4 -q flush >/dev/null 2>&1
|
|
rm -rf /tmp/.uci
|
|
run_boot
|
|
check "the run exits 0" same "$RC" 0
|
|
check "the setup is removed" eval '[ ! -e /etc/uci-defaults/90-fips-setup ]'
|
|
check "no copy of the staged configuration is left in /tmp" eval '[ ! -e "$COPY" ]'
|
|
migrated
|
|
check "no reload is attempted" no_reload
|
|
fw4 -q start >/dev/null 2>&1
|
|
check "after the start, base chains name fips0 only in the fips zone jumps" only_zone_jumps
|
|
check "no failsafe rule" table_lacks fips-reload-failed
|
|
fw_end
|
|
}
|
|
|
|
fw_failed_commit_removes_leftover_files() {
|
|
fw_begin fw_failed_commit_removes_leftover_files "a failed save still removes the old files, so no hotplug re-runs firewall.sh"
|
|
if commit_fails_start; then
|
|
commit_fails_ok
|
|
check "/etc/fips/firewall.sh is gone" eval '[ ! -e /etc/fips/firewall.sh ]'
|
|
check "/etc/hotplug.d/net/99-fips is gone" eval '[ ! -e /etc/hotplug.d/net/99-fips ]'
|
|
fi
|
|
fw_end
|
|
}
|
|
|
|
# The first run's failed save leaves input with only the drop of the failsafe
|
|
# pair, because the accept could not be inserted; the second run an SDK-feed
|
|
# install makes must complete the pair, or replies to the router's own
|
|
# connections over fips0 stay dropped.
|
|
fw_second_run_completes_a_half_failsafe_pair() {
|
|
fw_begin fw_second_run_completes_a_half_failsafe_pair "a second run after a failed save completes a failsafe pair the first run left half inserted"
|
|
wrap_nft_fails 'insert rule inet fw4 input iifname fips0 ct state established,related accept comment "fips-reload-failed"'
|
|
if commit_fails_start drop &&
|
|
precond "the nft wrapper failed the input accept" \
|
|
wrap_called nft 'insert rule inet fw4 input iifname fips0 ct state established,related accept comment "fips-reload-failed"' &&
|
|
precond "input holds the drop and not the accept" eval 'has input "$DROPPAIR" && lacks input "$ACCEPTPAIR"'; then
|
|
rm -f "$WRAP/nft"
|
|
: > "$RELOADLOG"
|
|
PATH="$WRAP:$PATH" sh -c '( cd /etc/uci-defaults && . ./90-fips-setup ) && rm -f /etc/uci-defaults/90-fips-setup' > "$OUT" 2>&1
|
|
RC=$?
|
|
sed 's/^/ | /' "$OUT"
|
|
check "the second run ends non-zero" eval '[ "$RC" != 0 ]'
|
|
check "the block is reported" \
|
|
said 'the firewall configuration could not be saved, so fips0 may still be in the lan zone; new connections'
|
|
check "input holds one failsafe pair" pair_once input
|
|
check "forward holds one failsafe pair" pair_once forward
|
|
check "input: the failsafe pair precedes the lan jump naming fips0" pair_heads input input_lan
|
|
check "forward: the failsafe pair precedes the lan jump naming fips0" pair_heads forward forward_lan
|
|
fi
|
|
fw_end
|
|
}
|
|
|
|
# ── A save onto a full overlay ──────────────────────────────────────────────
|
|
# uci commit exits 0 there, after replacing the file with as much of the new
|
|
# configuration as fitted and dropping the staged changes.
|
|
|
|
DAMAGED='the firewall configuration could not be saved and /etc/config/firewall was left damaged, which the next firewall reload or reboot loads; new connections'
|
|
|
|
# full_save_start <free pages> [pad]: the 0.5.2 state with /etc/config on the
|
|
# small tmpfs, pad adding rules until the configuration spans several pages,
|
|
# then the setup run as the postinst runs it with only <free pages> free.
|
|
full_save_start() {
|
|
local i=0 max=$(($1 * 4096))
|
|
small_config
|
|
if [ "${2:-}" = pad ]; then
|
|
while [ "$i" -lt 40 ]; do
|
|
uci add firewall rule >/dev/null &&
|
|
uci set "firewall.@rule[-1].name=Pad-$i-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" &&
|
|
uci set firewall.@rule[-1].src=wan &&
|
|
uci set firewall.@rule[-1].target=DROP &&
|
|
uci set firewall.@rule[-1].enabled=0 || harness_fail "cannot add the padding rules"
|
|
i=$((i + 1))
|
|
done
|
|
uci commit firewall || harness_fail "cannot commit the padding rules"
|
|
fi
|
|
released_state
|
|
precond "the 0.5.2 accepts, lan jump and include are live" released_reached || return 1
|
|
drop_released_files
|
|
precond "the small tmpfs has $1 page(s) free" small_free "$1" || return 1
|
|
run_postinst
|
|
precond "the save left /etc/config/firewall cut short" \
|
|
eval '[ "$(wc -c < /etc/config/firewall)" -le "$max" ] && [ -z "$(uci -q changes firewall)" ]'
|
|
}
|
|
|
|
full_save_ok() {
|
|
check "the setup exits 1" same "$RC" 1
|
|
check "the setup is kept in /etc/uci-defaults" eval '[ -f /etc/uci-defaults/90-fips-setup ]'
|
|
check "no reload is attempted" no_reload
|
|
check "the damaged save is reported" said "$DAMAGED"
|
|
check "the warning names the copy to import" said "uci import firewall < $COPY"
|
|
check "input: the failsafe pair precedes the lan jump naming fips0" pair_heads input input_lan
|
|
check "forward: the failsafe pair precedes the lan jump naming fips0" pair_heads forward forward_lan
|
|
check "no old accept in any chain" no_old_accepts
|
|
check "the copy holds the fips zone" grep -qF "config zone 'fips'" "$COPY"
|
|
}
|
|
|
|
fw_full_overlay_save_blocks_and_keeps_setup() {
|
|
fw_begin fw_full_overlay_save_blocks_and_keeps_setup "a save onto a full overlay that empties the file closes fips0, keeps the setup, and the copy restores it"
|
|
if full_save_start 0; then
|
|
full_save_ok
|
|
rm -f "$SMALL/fill"
|
|
uci import firewall < "$COPY" && /etc/init.d/firewall reload >/dev/null 2>&1
|
|
check "after the import and a reload: the fips zone is live" zone_made
|
|
check "after the import and a reload: fips0 is no longer a lan device" lan_lacks_fips0
|
|
check "after the import and a reload: base chains name fips0 only in the fips zone jumps" only_zone_jumps
|
|
check "after the import and a reload: no failsafe rule" table_lacks fips-reload-failed
|
|
run_boot
|
|
check "the boot run exits 0" same "$RC" 0
|
|
check "the boot run removes the setup" eval '[ ! -e /etc/uci-defaults/90-fips-setup ]'
|
|
fi
|
|
fw_end
|
|
}
|
|
|
|
fw_partial_save_blocks_and_keeps_setup() {
|
|
fw_begin fw_partial_save_blocks_and_keeps_setup "a save onto an overlay with one page free cuts the file short; fips0 is closed and the setup kept"
|
|
if full_save_start 1 pad; then
|
|
full_save_ok
|
|
fi
|
|
fw_end
|
|
}
|
|
|
|
# ── nft unavailable or failing ──────────────────────────────────────────────
|
|
|
|
fw_missing_nft_is_reported() {
|
|
fw_begin fw_missing_nft_is_reported "with nft missing, the setup says the configuration cannot have loaded"
|
|
released_state
|
|
if precond "the 0.5.2 accepts, lan jump and include are live" released_reached; then
|
|
drop_released_files
|
|
mv /usr/sbin/nft /usr/sbin/nft.hidden || harness_fail "cannot hide nft"
|
|
run_setup
|
|
check "the setup exits 0" same "$RC" 0
|
|
check "exactly one firewall reload" one_reload
|
|
check "the missing nft is reported" \
|
|
said 'nft was not found, so the new configuration cannot have loaded and fips0 could not be closed'
|
|
check "the old accepts are still loaded" eval '/usr/sbin/nft.hidden list chain inet fw4 input 2>/dev/null | grep -qF "iifname \"fips0\" accept comment \"fips\""'
|
|
check "the lan jump naming fips0 is still loaded" eval '/usr/sbin/nft.hidden list chain inet fw4 input 2>/dev/null | grep -F "\"fips0\"" | grep -qF "jump input_lan "'
|
|
fi
|
|
fw_end
|
|
}
|
|
|
|
# unchecked_ok: the reload loaded, but could not be confirmed.
|
|
unchecked_ok() {
|
|
check "the setup exits 0" same "$RC" 0
|
|
check "exactly one firewall reload" one_reload
|
|
check "the unchecked reload is reported" said 'the firewall reload could not be checked, so new connections'
|
|
check "input: the failsafe pair precedes the fips zone jump" pair_heads input input_fips
|
|
check "forward: the failsafe pair precedes the fips zone jump" pair_heads forward forward_fips
|
|
}
|
|
|
|
fw_probe_add_failure_blocks_fips0() {
|
|
fw_begin fw_probe_add_failure_blocks_fips0 "when the probe rule cannot be added, the setup fails closed"
|
|
released_state
|
|
if precond "the 0.5.2 accepts, lan jump and include are live" released_reached; then
|
|
drop_released_files
|
|
wrap_nft_fails 'add rule inet fw4 input counter comment "fips-reload-probe"'
|
|
run_setup wrap
|
|
precond "the nft wrapper failed the probe" wrap_called nft 'add rule inet fw4 input counter comment "fips-reload-probe"' &&
|
|
unchecked_ok
|
|
fi
|
|
fw_end
|
|
}
|
|
|
|
# The probe cannot be added and fw4's state file is missing, so the reload dies
|
|
# and nothing it says can be checked; only a restart recovers.
|
|
fw_unchecked_reload_without_state_file_names_restart() {
|
|
fw_begin fw_unchecked_reload_without_state_file_names_restart "an unchecked reload with fw4's state file missing names the restart"
|
|
released_state
|
|
rm -f /var/run/fw4.state
|
|
if precond "the 0.5.2 state is live and the state file is absent" \
|
|
eval 'released_reached && [ ! -e /var/run/fw4.state ]'; then
|
|
drop_released_files
|
|
wrap_nft_fails 'add rule inet fw4 input counter comment "fips-reload-probe"'
|
|
run_setup wrap
|
|
if precond "the nft wrapper failed the probe" wrap_called nft 'add rule inet fw4 input counter comment "fips-reload-probe"'; then
|
|
check "the setup exits 0" same "$RC" 0
|
|
check "exactly one firewall reload" one_reload
|
|
check "the unchecked reload and missing state file are reported" \
|
|
said "the firewall reload could not be checked and fw4's state file is missing, so new connections"
|
|
check "the remedy is a restart" said "run '/etc/init.d/firewall restart'"
|
|
refute "no reload remedy" said "run '/etc/init.d/firewall reload' to clear the block"
|
|
check "input: the failsafe pair precedes the lan jump naming fips0" pair_heads input input_lan
|
|
check "forward: the failsafe pair precedes the lan jump naming fips0" pair_heads forward forward_lan
|
|
fi
|
|
fi
|
|
fw_end
|
|
}
|
|
|
|
fw_listing_failure_blocks_fips0() {
|
|
fw_begin fw_listing_failure_blocks_fips0 "when the input chain cannot be listed after the reload, the setup fails closed"
|
|
released_state
|
|
if precond "the 0.5.2 accepts, lan jump and include are live" released_reached; then
|
|
drop_released_files
|
|
wrap_nft_fails 'list chain inet fw4 input'
|
|
run_setup wrap
|
|
precond "the nft wrapper failed the listing" wrap_called nft 'list chain inet fw4 input' &&
|
|
unchecked_ok
|
|
fi
|
|
fw_end
|
|
}
|
|
|
|
fw_flush_only_load_blocks_fips0() {
|
|
fw_begin fw_flush_only_load_blocks_fips0 "a load that only flushed the table is not taken for success"
|
|
released_state
|
|
if precond "the 0.5.2 accepts, lan jump and include are live" released_reached; then
|
|
drop_released_files
|
|
: > "$FLUSHONLY"
|
|
run_setup
|
|
if precond "the flush left the input chain listed with no rules" \
|
|
eval 'grep -qF "chain input" "$FLUSHLIST" && ! grep -qE "jump|accept comment|drop comment|counter|fips-reload-probe" "$FLUSHLIST"'; then
|
|
check "the setup exits 0" same "$RC" 0
|
|
check "exactly one firewall reload" one_reload
|
|
check "the unchecked reload is reported" said 'the firewall reload could not be checked, so new connections'
|
|
check "input holds the failsafe pair and nothing else naming fips0" pair_only input
|
|
check "forward holds the failsafe pair and nothing else naming fips0" pair_only forward
|
|
fi
|
|
fi
|
|
fw_end
|
|
}
|
|
|
|
# ── The 802.11s mesh zone ───────────────────────────────────────────────────
|
|
# fips-mesh-setup puts the mesh interfaces it creates in the fips_mesh zone,
|
|
# and 90-fips-setup adds those made by earlier packages to it.
|
|
|
|
MESHDROP='drop comment "fips-mesh-reload-failed"'
|
|
|
|
# mesh_begin <name> <what it shows> [nostart]: reset, start fw4, and give the
|
|
# container two radios, an empty network configuration and the wifi stub.
|
|
mesh_begin() {
|
|
fw_begin "$@"
|
|
touch /etc/config/network
|
|
cat > /etc/config/wireless <<'EOF'
|
|
config wifi-device 'radio0'
|
|
option type 'mac80211'
|
|
option channel '36'
|
|
option band '5g'
|
|
|
|
config wifi-device 'radio1'
|
|
option type 'mac80211'
|
|
option channel '1'
|
|
option band '2g'
|
|
EOF
|
|
mkdir -p "$STUB" && printf '#!/bin/sh\necho "wifi $*" >> %s\n' "$RELOADLOG" > "$STUB/wifi" &&
|
|
chmod 0755 "$STUB/wifi" || harness_fail "cannot install the wifi stub"
|
|
}
|
|
|
|
# run_mesh [wrap] <args>: run the mesh helper under test, keeping its output in
|
|
# $OUT and its status in RC. wrap puts the uci and nft wrappers first.
|
|
run_mesh() {
|
|
local p="$STUB:$PATH"
|
|
if [ "${1:-}" = wrap ]; then
|
|
p="$STUB:$WRAP:$PATH"
|
|
shift
|
|
fi
|
|
: > "$RELOADLOG"
|
|
PATH="$p" sh "$FW_MESH_SETUP" "$@" > "$OUT" 2>&1
|
|
RC=$?
|
|
sed 's/^/ | /' "$OUT"
|
|
return 0
|
|
}
|
|
|
|
# The helper as 0.5.2 shipped it, which put the interface in no zone.
|
|
run_released_mesh() {
|
|
PATH="$STUB:$PATH" sh "$FIXTURES/released-fips-mesh-setup" "$@" >/dev/null 2>&1
|
|
}
|
|
|
|
# Install the helper under test where 90-fips-setup looks for it.
|
|
mesh_install() {
|
|
cp "$FW_MESH_SETUP" /usr/bin/fips-mesh-setup && chmod 0755 /usr/bin/fips-mesh-setup ||
|
|
harness_fail "cannot install the mesh helper"
|
|
}
|
|
|
|
# The hooks' PATH finds the wifi stub, so a wifi reload they make is logged.
|
|
stub_first() { [ "$(PATH="$(hook_path)" sh -c 'command -v wifi')" = "$STUB/wifi" ]; }
|
|
|
|
# In input, forward and output, a rule naming <dev> jumps to the chain of
|
|
# zone <zone>, fips_mesh unless given.
|
|
mesh_jumps() {
|
|
local c dir z="${2:-fips_mesh}"
|
|
for c in input forward output; do
|
|
dir=iifname
|
|
[ "$c" = output ] && dir=oifname
|
|
listing "$c" || return 1
|
|
printf '%s\n' "$LIST" | grep -F "\"$1\"" | grep -F "$dir " | grep -qF "jump ${c}_$z " || return 1
|
|
done
|
|
return 0
|
|
}
|
|
|
|
# No rule names a mesh interface or jumps to a fips_mesh chain. The empty
|
|
# chains a reload leaves after the zone is deleted are allowed.
|
|
mesh_named_nowhere() {
|
|
local t
|
|
t="$(nft list table inet fw4 2>/dev/null)" || return 1
|
|
! printf '%s\n' "$t" | grep -q '"fips-mesh[0-9]*"' &&
|
|
! printf '%s\n' "$t" | grep -qE 'jump [A-Za-z_]*_fips_mesh( |$)'
|
|
}
|
|
|
|
# mesh_zone_made <device list>: the zone is exactly what the helper creates.
|
|
mesh_zone_made() {
|
|
local want
|
|
want="$(printf '%s\n' "=zone" ".name='fips_mesh'" ".input='REJECT'" ".output='ACCEPT'" \
|
|
".forward='REJECT'" ".auto_helper='0'" ".device=$1")"
|
|
[ "$(dump firewall.fips_mesh)" = "$want" ]
|
|
}
|
|
|
|
# The drop for <dev> is in <chain> once, and only failsafe rules precede it.
|
|
mesh_drop_heads() {
|
|
local r n
|
|
r="$(rules_of "$1" | grep -v '^type ')" || return 1
|
|
[ "$(printf '%s\n' "$r" | grep -cxF "iifname \"$2\" $MESHDROP")" = 1 ] || return 1
|
|
n="$(printf '%s\n' "$r" | grep -nxF "iifname \"$2\" $MESHDROP" | sed 's/:.*//')"
|
|
[ "$(printf '%s\n' "$r" | sed -n "1,${n}p" | grep -vc 'reload-failed"$')" = 0 ]
|
|
}
|
|
|
|
mesh_failsafe() { mesh_drop_heads input "$1" && mesh_drop_heads forward "$1"; }
|
|
|
|
no_mesh_drop() { table_lacks fips-mesh-reload-failed; }
|
|
|
|
# The basic rates the helper gives every mesh interface.
|
|
RATES='6000 12000 24000'
|
|
|
|
# mesh_wireless_made <radio> <ifname>: the sections the helper makes.
|
|
mesh_wireless_made() {
|
|
local s="fips_mesh_$1"
|
|
[ "$(uget "wireless.$s")" = wifi-iface ] &&
|
|
[ "$(uget "wireless.$s.device")" = "$1" ] &&
|
|
[ "$(uget "wireless.$s.mode")" = mesh ] &&
|
|
[ "$(uget "wireless.$s.encryption")" = none ] &&
|
|
[ "$(uget "wireless.$s.mesh_fwding")" = 0 ] &&
|
|
[ "$(uget "wireless.$s.basic_rate")" = "$RATES" ] &&
|
|
[ "$(uget "wireless.$s.ifname")" = "$2" ] &&
|
|
[ "$(uget "wireless.$s.network")" = "$s" ] &&
|
|
[ "$(uget "network.$s")" = interface ] &&
|
|
[ "$(uget "network.$s.proto")" = none ]
|
|
}
|
|
|
|
mesh_exports_save() {
|
|
uci export wireless > "$WORK/wireless.before"
|
|
uci export network > "$WORK/network.before"
|
|
}
|
|
|
|
mesh_exports_same() {
|
|
uci export wireless > "$WORK/wireless.after"
|
|
uci export network > "$WORK/network.after"
|
|
cmp -s "$WORK/wireless.before" "$WORK/wireless.after" &&
|
|
cmp -s "$WORK/network.before" "$WORK/network.after"
|
|
}
|
|
|
|
# Network is unchanged, and wireless is unchanged but for basic_rate lines
|
|
# with the helper's rates.
|
|
mesh_exports_same_but_rates() {
|
|
uci export wireless | grep -vxF " option basic_rate '$RATES'" > "$WORK/wireless.after"
|
|
uci export network > "$WORK/network.after"
|
|
cmp -s "$WORK/wireless.before" "$WORK/wireless.after" &&
|
|
cmp -s "$WORK/network.before" "$WORK/network.after"
|
|
}
|
|
|
|
log_is() { [ "$(cat "$RELOADLOG" 2>/dev/null)" = "$1" ]; }
|
|
|
|
no_wifi() { [ -f "$RELOADLOG" ] && ! grep -q '^wifi' "$RELOADLOG"; }
|
|
|
|
# The log holds a wifi reload, and a firewall reload after it.
|
|
wifi_then_reload() {
|
|
local w r
|
|
w="$(grep -nx 'wifi reload' "$RELOADLOG" | sed -n '1s/:.*//p')"
|
|
r="$(grep -nx reload "$RELOADLOG" | sed -n '$s/:.*//p')"
|
|
[ -n "$w" ] && [ -n "$r" ] && [ "$w" -lt "$r" ]
|
|
}
|
|
|
|
ZONED='is in firewall zone fips_mesh, which rejects all IP input'
|
|
MESHCLOSED='closed to IP traffic until the firewall next reloads'
|
|
|
|
mesh_add_puts_interface_in_reject_zone() {
|
|
mesh_begin mesh_add_puts_interface_in_reject_zone "adding a mesh puts its interface in the fips_mesh zone before the radio comes up"
|
|
note "defaults input $(uget firewall.@defaults[0].input); loaded input chain $(nft list chain inet fw4 input 2>/dev/null | sed -n 's/.*\(policy [a-z]*\);.*/\1/p'); rules naming fips-mesh0: $(nft list chain inet fw4 input 2>/dev/null | grep -c '"fips-mesh0"')"
|
|
run_mesh radio0
|
|
check "the helper exits 0" same "$RC" 0
|
|
check "the fips_mesh zone is REJECT/ACCEPT/REJECT on fips-mesh0 with auto_helper 0" mesh_zone_made "'fips-mesh0'"
|
|
check "input, forward and output jump fips-mesh0 into the zone" mesh_jumps fips-mesh0
|
|
check "input_fips_mesh only rejects" same "$(rules_of input_fips_mesh | grep -v '^type ')" 'jump reject_from_fips_mesh'
|
|
check "forward_fips_mesh only rejects" same "$(rules_of forward_fips_mesh | grep -v '^type ')" 'jump reject_to_fips_mesh'
|
|
if table_has 'jump helper_lan'; then
|
|
check "no helper chain for the fips_mesh zone" table_lacks helper_fips_mesh
|
|
else
|
|
note "helper assertion not exercised on this host"
|
|
fi
|
|
check "the firewall reloads, then the radio" log_is "$(printf 'reload\nwifi reload')"
|
|
check "the wireless and network sections are as before" mesh_wireless_made radio0 fips-mesh0
|
|
check "the mesh interface has basic_rate '$RATES'" same "$(uget wireless.fips_mesh_radio0.basic_rate)" "$RATES"
|
|
check "no failsafe rule" no_mesh_drop
|
|
check "the closing message names the zone" said "fips-mesh0 $ZONED"
|
|
check "the closing message says 22.03 and 23.05 ignore the rates" said 'and 23.05 ignore basic_rate on a mesh'
|
|
fw_end
|
|
}
|
|
|
|
mesh_second_add_changes_nothing() {
|
|
mesh_begin mesh_second_add_changes_nothing "adding the same mesh again changes no firewall configuration and does not reload"
|
|
run_mesh radio0
|
|
uci export firewall > "$WORK/first.uci"
|
|
run_mesh radio0
|
|
uci export firewall > "$WORK/second.uci"
|
|
check "the second add exits 0" same "$RC" 0
|
|
check "uci export firewall is byte-identical" cmp -s "$WORK/first.uci" "$WORK/second.uci"
|
|
check "the second add does not reload the firewall" no_reload
|
|
check "fips-mesh0 is in the zone" mesh_jumps fips-mesh0
|
|
check "no failsafe rule" no_mesh_drop
|
|
fw_end
|
|
}
|
|
|
|
mesh_two_radios_share_one_zone_and_remove_cleans_up() {
|
|
mesh_begin mesh_two_radios_share_one_zone_and_remove_cleans_up "two radios share the zone; remove takes each out and the last deletes it"
|
|
run_mesh radio0
|
|
run_mesh radio1
|
|
check "the zone lists both interfaces" same "$(uget firewall.fips_mesh.device)" "fips-mesh0 fips-mesh1"
|
|
check "input sends both to the zone in one rule" \
|
|
eval 'listing input && printf "%s\n" "$LIST" | grep -F "\"fips-mesh0\"" | grep -F "\"fips-mesh1\"" | grep -qF "jump input_fips_mesh "'
|
|
run_mesh remove radio1
|
|
check "remove radio1: the zone lists fips-mesh0" same "$(uget firewall.fips_mesh.device)" fips-mesh0
|
|
check "remove radio1: no rule names fips-mesh1" table_lacks '"fips-mesh1"'
|
|
check "remove radio1: fips-mesh0 is still in the zone" mesh_jumps fips-mesh0
|
|
check "remove radio1: the radio reloads before the firewall" wifi_then_reload
|
|
run_mesh remove
|
|
check "remove: the helper exits 0" same "$RC" 0
|
|
check "remove: no firewall.fips_mesh" same "$(uget firewall.fips_mesh)" ""
|
|
check "remove: nothing loaded names a mesh interface or jumps to the zone" mesh_named_nowhere
|
|
check "remove: no mesh section in wireless or network" \
|
|
eval '! uci -q show wireless | grep -q fips_mesh_ && ! uci -q show network | grep -q fips_mesh_'
|
|
fw_end
|
|
}
|
|
|
|
# mesh_veth_remove <deleted> [window]: the helper adds radio0, a veth stands in
|
|
# for its interface, and the wifi stub deletes it two seconds after the reload
|
|
# when <deleted> is 1. The stub logs the deletion just before making it, so a
|
|
# firewall reload logged after that line came after a two-second wait at
|
|
# least, and one that did not wait is logged before it. With window, the stub
|
|
# first reloads the firewall through fw4, not the logged init script, as
|
|
# OpenWrt's firewall hotplug script does when a zoned interface comes up
|
|
# during a radio reload, and writes to $WORK/window how many loaded rules then
|
|
# name fips-mesh0.
|
|
mesh_veth_remove() {
|
|
run_mesh radio0
|
|
ip link add fips-mesh0 type veth peer name fmpeer0 >/dev/null 2>&1
|
|
if precond "ip is found and fips-mesh0 exists" eval 'command -v ip >/dev/null && [ -e /sys/class/net/fips-mesh0 ]'; then
|
|
{
|
|
echo '#!/bin/sh'
|
|
echo "echo \"wifi \$*\" >> $RELOADLOG"
|
|
[ "${2:-}" = window ] &&
|
|
echo "fw4 -q reload >/dev/null 2>&1; nft list table inet fw4 | grep -c '\"fips-mesh0\"' > $WORK/window"
|
|
[ "$1" = 1 ] &&
|
|
echo "( sleep 2; echo 'gone fips-mesh0' >> $RELOADLOG; ip link del fips-mesh0 ) >/dev/null 2>&1 &"
|
|
} > "$STUB/wifi"
|
|
run_mesh remove
|
|
return 0
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
mesh_remove_waits_for_interface_before_reload() {
|
|
mesh_begin mesh_remove_waits_for_interface_before_reload "remove reloads the firewall only once the interface is gone"
|
|
if mesh_veth_remove 1; then
|
|
check "remove exits 0" same "$RC" 0
|
|
check "the radio reloads, the interface goes, then the firewall reloads" \
|
|
log_is "$(printf 'wifi reload\ngone fips-mesh0\nreload')"
|
|
refute "no still-present line" said 'is still present after the wifi reload'
|
|
fi
|
|
fw_end
|
|
}
|
|
|
|
mesh_remove_waits_for_interface_before_reload_and_reports_one_that_stays() {
|
|
mesh_begin mesh_remove_waits_for_interface_before_reload_and_reports_one_that_stays "an interface still present after the wait is named, and the firewall reloads"
|
|
if mesh_veth_remove 0; then
|
|
check "remove exits 0" same "$RC" 0
|
|
check "the line names fips-mesh0" said 'fips-mesh0 is still present after the wifi reload'
|
|
check "the firewall reloads after the radio" log_is "$(printf 'wifi reload\nreload')"
|
|
fi
|
|
fw_end
|
|
}
|
|
|
|
mesh_remove_keeps_zone_until_interface_is_gone() {
|
|
mesh_begin mesh_remove_keeps_zone_until_interface_is_gone "a firewall reload while the radio reloads leaves the interface in the zone"
|
|
if mesh_veth_remove 1 window; then
|
|
check "remove exits 0" same "$RC" 0
|
|
check "the reload during the radio reload still loads rules naming fips-mesh0" \
|
|
eval '[ "$(cat "$WORK/window" 2>/dev/null || echo 0)" -gt 0 ]'
|
|
check "remove: no firewall.fips_mesh" same "$(uget firewall.fips_mesh)" ""
|
|
check "remove: nothing loaded names a mesh interface or jumps to the zone" mesh_named_nowhere
|
|
fi
|
|
fw_end
|
|
}
|
|
|
|
mesh_add_then_remove_restores_firewall_config() {
|
|
mesh_begin mesh_add_then_remove_restores_firewall_config "add then remove leaves the firewall configuration as it was, with the fips and fips_ap zones"
|
|
run_setup
|
|
uci set firewall.fips_ap=zone &&
|
|
uci set firewall.fips_ap.name=fips_ap &&
|
|
uci set firewall.fips_ap.input=REJECT &&
|
|
uci set firewall.fips_ap.output=ACCEPT &&
|
|
uci set firewall.fips_ap.forward=REJECT &&
|
|
uci add_list firewall.fips_ap.network=fips_ap_radio1 &&
|
|
uci set firewall.fips_ap_icmpv6=rule && uci set firewall.fips_ap_icmpv6.src=fips_ap &&
|
|
uci set firewall.fips_ap_icmpv6.proto=icmp && uci set firewall.fips_ap_icmpv6.target=ACCEPT &&
|
|
uci set firewall.fips_ap_dhcpv4=rule && uci set firewall.fips_ap_dhcpv4.src=fips_ap &&
|
|
uci set firewall.fips_ap_dhcpv4.proto=udp && uci set firewall.fips_ap_dhcpv4.dest_port=67 &&
|
|
uci set firewall.fips_ap_dhcpv4.target=ACCEPT &&
|
|
uci set firewall.fips_ap_mdns=rule && uci set firewall.fips_ap_mdns.src=fips_ap &&
|
|
uci set firewall.fips_ap_mdns.proto=udp && uci set firewall.fips_ap_mdns.dest_port=5353 &&
|
|
uci set firewall.fips_ap_mdns.target=ACCEPT &&
|
|
uci set firewall.fips_ap_fips_udp=rule && uci set firewall.fips_ap_fips_udp.src=fips_ap &&
|
|
uci set firewall.fips_ap_fips_udp.proto=udp && uci set firewall.fips_ap_fips_udp.dest_port=2121 &&
|
|
uci set firewall.fips_ap_fips_udp.target=ACCEPT &&
|
|
uci set firewall.fips_ap_fips_tcp=rule && uci set firewall.fips_ap_fips_tcp.src=fips_ap &&
|
|
uci set firewall.fips_ap_fips_tcp.proto=tcp && uci set firewall.fips_ap_fips_tcp.dest_port=8443 &&
|
|
uci set firewall.fips_ap_fips_tcp.target=ACCEPT &&
|
|
uci commit firewall || harness_fail "cannot add the fips_ap zone"
|
|
/etc/init.d/firewall reload >/dev/null 2>&1
|
|
uci export firewall > "$WORK/before.uci"
|
|
run_mesh radio0
|
|
run_mesh remove
|
|
uci export firewall > "$WORK/after.uci"
|
|
check "remove exits 0" same "$RC" 0
|
|
check "uci export firewall is byte-identical to before the add" cmp -s "$WORK/before.uci" "$WORK/after.uci"
|
|
check "fips0 is still in the fips zone" has input 'iifname "fips0" jump input_fips '
|
|
fw_end
|
|
}
|
|
|
|
mesh_existing_zone_options_are_kept() {
|
|
mesh_begin mesh_existing_zone_options_are_kept "an existing fips_mesh zone keeps its options; an ACCEPT input is reported"
|
|
uci set firewall.fips_mesh=zone &&
|
|
uci set firewall.fips_mesh.name=fips_mesh &&
|
|
uci set firewall.fips_mesh.input=ACCEPT &&
|
|
uci set firewall.fips_mesh.output=ACCEPT &&
|
|
uci set firewall.fips_mesh.forward=REJECT &&
|
|
uci add_list firewall.fips_mesh.device=op-dev0 &&
|
|
uci commit firewall || harness_fail "cannot add the operator's fips_mesh zone"
|
|
/etc/init.d/firewall reload >/dev/null 2>&1
|
|
dump firewall.fips_mesh | sed "s/^\.device='op-dev0'\$/.device='op-dev0' 'fips-mesh0'/" > "$WORK/zone.want"
|
|
run_mesh radio0
|
|
dump firewall.fips_mesh > "$WORK/zone.after"
|
|
check "the helper exits 0" same "$RC" 0
|
|
check "the zone's options are unchanged but for fips-mesh0 appended" cmp -s "$WORK/zone.want" "$WORK/zone.after"
|
|
check "one line reports the ACCEPT input" said_once 'has input policy ACCEPT, so router services are reachable from anyone in radio range'
|
|
refute "the closing message does not say the zone rejects all input" said "$ZONED"
|
|
run_mesh remove
|
|
check "remove keeps the zone with the operator's device" same "$(uget firewall.fips_mesh.device)" op-dev0
|
|
fw_end
|
|
}
|
|
|
|
mesh_remove_keeps_empty_zone_it_took_nothing_from() {
|
|
mesh_begin mesh_remove_keeps_empty_zone_it_took_nothing_from "remove on a radio with no mesh leaves an empty fips_mesh zone alone"
|
|
uci set firewall.fips_mesh=zone &&
|
|
uci set firewall.fips_mesh.name=fips_mesh &&
|
|
uci set firewall.fips_mesh.input=REJECT &&
|
|
uci set firewall.fips_mesh.output=ACCEPT &&
|
|
uci set firewall.fips_mesh.forward=REJECT &&
|
|
uci set firewall.fips_mesh.log=1 &&
|
|
uci commit firewall || harness_fail "cannot add the empty fips_mesh zone"
|
|
/etc/init.d/firewall reload >/dev/null 2>&1
|
|
uci export firewall > "$WORK/before.uci"
|
|
run_mesh remove radio1
|
|
uci export firewall > "$WORK/after.uci"
|
|
check "remove exits 0" same "$RC" 0
|
|
check "the zone is still there" same "$(uget firewall.fips_mesh)" zone
|
|
check "uci export firewall is byte-identical to before" cmp -s "$WORK/before.uci" "$WORK/after.uci"
|
|
check "no firewall reload" no_reload
|
|
fw_end
|
|
}
|
|
|
|
mesh_disabled_zone_is_reported_not_failed() {
|
|
mesh_begin mesh_disabled_zone_is_reported_not_failed "a disabled fips_mesh zone is reported as such, not as a failed reload"
|
|
uci set firewall.fips_mesh=zone &&
|
|
uci set firewall.fips_mesh.name=fips_mesh &&
|
|
uci set firewall.fips_mesh.input=REJECT &&
|
|
uci set firewall.fips_mesh.enabled=0 &&
|
|
uci commit firewall || harness_fail "cannot add the disabled zone"
|
|
run_mesh radio0
|
|
check "the helper exits 0" same "$RC" 0
|
|
check "the zone is still disabled" same "$(uget firewall.fips_mesh.enabled)" 0
|
|
check "fips-mesh0 is listed" same "$(uget firewall.fips_mesh.device)" fips-mesh0
|
|
check "the disabled zone is reported" said 'the fips_mesh firewall zone is disabled, so fips-mesh0 is in no firewall zone'
|
|
refute "no reload-failure line" said 'firewall reload failed'
|
|
check "no failsafe rule" no_mesh_drop
|
|
check "the mesh is created" mesh_wireless_made radio0 fips-mesh0
|
|
fw_end
|
|
}
|
|
|
|
mesh_renamed_zone_is_checked_by_its_name() {
|
|
mesh_begin mesh_renamed_zone_is_checked_by_its_name "a fips_mesh zone named mesh is checked by that name"
|
|
uci set firewall.fips_mesh=zone &&
|
|
uci set firewall.fips_mesh.name=mesh &&
|
|
uci set firewall.fips_mesh.input=REJECT &&
|
|
uci set firewall.fips_mesh.output=ACCEPT &&
|
|
uci set firewall.fips_mesh.forward=REJECT &&
|
|
uci set firewall.fips_mesh.auto_helper=0 &&
|
|
uci commit firewall || harness_fail "cannot add the renamed zone"
|
|
/etc/init.d/firewall reload >/dev/null 2>&1
|
|
run_mesh radio0
|
|
check "the helper exits 0" same "$RC" 0
|
|
check "fips-mesh0 is listed" same "$(uget firewall.fips_mesh.device)" fips-mesh0
|
|
check "input sends fips-mesh0 to input_mesh" mesh_jumps fips-mesh0 mesh
|
|
check "no failsafe rule" no_mesh_drop
|
|
refute "no reload-failure line" said 'firewall reload failed'
|
|
refute "no stays-in-zone line" said 'stays in that zone'
|
|
run_mesh radio0
|
|
check "second add: exits 0" same "$RC" 0
|
|
check "second add: no firewall reload" no_reload
|
|
refute "second add: no stays-in-zone line" said 'stays in that zone'
|
|
fw_end
|
|
}
|
|
|
|
mesh_conflicting_firewall_section_is_refused() {
|
|
local v
|
|
for v in rule zone; do
|
|
mesh_begin mesh_conflicting_firewall_section_is_refused "a fips_mesh section that is a $v the helper cannot use stops the add"
|
|
if [ "$v" = rule ]; then
|
|
uci set firewall.fips_mesh=rule &&
|
|
uci set firewall.fips_mesh.src=lan &&
|
|
uci set firewall.fips_mesh.target=ACCEPT &&
|
|
uci commit firewall || harness_fail "cannot add the fips_mesh rule"
|
|
else
|
|
uci add firewall zone >/dev/null &&
|
|
uci set firewall.@zone[-1].name=fips_mesh &&
|
|
uci set firewall.@zone[-1].input=REJECT &&
|
|
uci commit firewall || harness_fail "cannot add the anonymous fips_mesh zone"
|
|
fi
|
|
cp /etc/config/firewall "$WORK/firewall.before"
|
|
run_mesh radio0
|
|
check "$v: the helper exits 1" same "$RC" 1
|
|
check "$v: /etc/config/firewall is unchanged" cmp -s "$WORK/firewall.before" /etc/config/firewall
|
|
check "$v: no wireless section" same "$(uget wireless.fips_mesh_radio0)" ""
|
|
check "$v: no reload of either kind" eval '[ -f "$RELOADLOG" ] && [ ! -s "$RELOADLOG" ]'
|
|
if [ "$v" = rule ]; then
|
|
check "rule: the line names the section" said 'firewall section fips_mesh exists and is not a zone'
|
|
else
|
|
check "zone: the line names the zone" said 'a firewall zone named fips_mesh already exists (firewall.cfg'
|
|
fi
|
|
done
|
|
fw_end
|
|
}
|
|
|
|
mesh_conflicting_firewall_section_is_refused_unless_it_holds_the_interface() {
|
|
mesh_begin mesh_conflicting_firewall_section_is_refused_unless_it_holds_the_interface "an anonymous zone named fips_mesh that holds the interface keeps it"
|
|
uci add firewall zone >/dev/null &&
|
|
uci set firewall.@zone[-1].name=fips_mesh &&
|
|
uci set firewall.@zone[-1].input=REJECT &&
|
|
uci set firewall.@zone[-1].output=ACCEPT &&
|
|
uci set firewall.@zone[-1].forward=REJECT &&
|
|
uci add_list firewall.@zone[-1].device=fips-mesh0 &&
|
|
uci commit firewall || harness_fail "cannot add the anonymous fips_mesh zone"
|
|
/etc/init.d/firewall reload >/dev/null 2>&1
|
|
cp /etc/config/firewall "$WORK/firewall.before"
|
|
run_mesh radio0
|
|
check "the helper exits 0" same "$RC" 0
|
|
check "no section firewall.fips_mesh" same "$(uget firewall.fips_mesh)" ""
|
|
check "/etc/config/firewall is unchanged" cmp -s "$WORK/firewall.before" /etc/config/firewall
|
|
check "no firewall reload" no_reload
|
|
check "the line names the anonymous zone" said 'fips-mesh0 is in firewall zone fips_mesh (firewall.cfg'
|
|
refute "no conflict line" said 'already exists'
|
|
check "input sends fips-mesh0 to that zone" has input 'iifname "fips-mesh0" jump input_fips_mesh '
|
|
check "the mesh is created" mesh_wireless_made radio0 fips-mesh0
|
|
fw_end
|
|
}
|
|
|
|
mesh_failed_reload_start() {
|
|
echo 'this is not nft;' > /etc/nftables.d/zz-invalid.nft
|
|
precond "fw4 check fails on the invalid include" eval '! fw4 check >/dev/null 2>&1'
|
|
}
|
|
|
|
mesh_failed_reload_drops_mesh_input() {
|
|
mesh_begin mesh_failed_reload_drops_mesh_input "a reload that fails to load drops IP traffic arriving on the mesh"
|
|
if mesh_failed_reload_start; then
|
|
run_mesh radio0
|
|
check "the helper exits 0" same "$RC" 0
|
|
check "the zone is in /etc/config/firewall" grep -qF "config zone 'fips_mesh'" /etc/config/firewall
|
|
check "the drop heads input and forward" mesh_failsafe fips-mesh0
|
|
check "the old ruleset is still loaded" lacks input 'jump input_fips_mesh'
|
|
check "the failed reload is reported" said 'the firewall reload failed, so IP traffic arriving on fips-mesh0 is dropped'
|
|
check "the mesh is created" mesh_wireless_made radio0 fips-mesh0
|
|
run_mesh radio0
|
|
check "second add: exits 0" same "$RC" 0
|
|
check "second add: one firewall reload" one_reload
|
|
check "second add: still one drop in each chain" mesh_failsafe fips-mesh0
|
|
rm -f /etc/nftables.d/zz-invalid.nft
|
|
/etc/init.d/firewall reload >/dev/null 2>&1
|
|
check "after a clean reload: no failsafe rule" no_mesh_drop
|
|
check "after a clean reload: fips-mesh0 is in the zone" mesh_jumps fips-mesh0
|
|
fi
|
|
fw_end
|
|
}
|
|
|
|
mesh_missing_state_file_names_restart() {
|
|
mesh_begin mesh_missing_state_file_names_restart "a loaded table without fw4's state file drops mesh input and names the restart"
|
|
rm -f /var/run/fw4.state
|
|
if precond "the table is loaded and the state file is absent" \
|
|
eval 'nft list table inet fw4 >/dev/null 2>&1 && [ ! -e /var/run/fw4.state ]'; then
|
|
run_mesh radio0
|
|
check "the helper exits 0" same "$RC" 0
|
|
check "the drop heads input and forward" mesh_failsafe fips-mesh0
|
|
check "the missing state file is reported" said "the firewall reload failed because fw4's state file is missing"
|
|
check "the remedy is a restart" said "run '/etc/init.d/firewall restart'"
|
|
refute "no fw4 check remedy" said "run 'fw4 check'"
|
|
check "the mesh is created" mesh_wireless_made radio0 fips-mesh0
|
|
fi
|
|
fw_end
|
|
}
|
|
|
|
mesh_unloaded_firewall_is_reported() {
|
|
mesh_begin mesh_unloaded_firewall_is_reported "with fw4 not running the zone is configured, not reloaded, and applies when it starts" nostart
|
|
if precond "neither state file nor table" \
|
|
eval '[ ! -e /var/run/fw4.state ] && ! nft list table inet fw4 >/dev/null 2>&1'; then
|
|
run_mesh radio0
|
|
check "the helper exits 0" same "$RC" 0
|
|
check "the zone is configured" mesh_zone_made "'fips-mesh0'"
|
|
check "no firewall reload" no_reload
|
|
check "the unloaded firewall is reported" said 'firewall is not loaded'
|
|
fw4 -q start >/dev/null 2>&1
|
|
check "after fw4 starts: fips-mesh0 is in the zone" mesh_jumps fips-mesh0
|
|
fi
|
|
fw_end
|
|
}
|
|
|
|
mesh_state_file_without_table_is_reported() {
|
|
mesh_begin mesh_state_file_without_table_is_reported "a state file with no table loaded is reported, and the mesh is created" nostart
|
|
echo 'this is not nft;' > /etc/nftables.d/zz-invalid.nft
|
|
fw4 -q start >/dev/null 2>&1
|
|
if precond "the state file exists and inet fw4 is not loaded" \
|
|
eval '[ -e /var/run/fw4.state ] && ! nft list table inet fw4 >/dev/null 2>&1'; then
|
|
run_mesh radio0
|
|
check "the helper exits 0" same "$RC" 0
|
|
check "the zone is configured" mesh_zone_made "'fips-mesh0'"
|
|
check "the unloaded firewall is reported" said 'firewall is not loaded'
|
|
refute "no could-not-be-closed line" said 'could not be closed'
|
|
check "the mesh is created" mesh_wireless_made radio0 fips-mesh0
|
|
check "the radio is reloaded" grep -qx 'wifi reload' "$RELOADLOG"
|
|
fi
|
|
fw_end
|
|
}
|
|
|
|
# The 0.5.2 helper's two meshes, with no zone.
|
|
mesh_released_two() {
|
|
run_released_mesh radio0
|
|
run_released_mesh radio1
|
|
mesh_exports_save
|
|
precond "no fips_mesh section, nothing loaded names a mesh interface, and the hooks find the wifi stub" \
|
|
eval '[ -z "$(uget firewall.fips_mesh)" ] && table_lacks fips-mesh && stub_first && [ "$(uget wireless.fips_mesh_radio1.ifname)" = fips-mesh1 ]'
|
|
}
|
|
|
|
mesh_upgrade_zones_existing_interfaces() {
|
|
mesh_begin mesh_upgrade_zones_existing_interfaces "an upgrade adds the interfaces the 0.5.2 helper made to the zone"
|
|
if mesh_released_two; then
|
|
mesh_install
|
|
run_postinst
|
|
check "the setup exits 0 and is deleted" eval '[ "$RC" = 0 ] && [ ! -e /etc/uci-defaults/90-fips-setup ]'
|
|
check "the zone holds both interfaces" mesh_zone_made "'fips-mesh0' 'fips-mesh1'"
|
|
check "fips-mesh0 is in the zone" mesh_jumps fips-mesh0
|
|
check "fips-mesh1 is in the zone" mesh_jumps fips-mesh1
|
|
check "no failsafe rule" no_mesh_drop
|
|
check "no wifi reload" no_wifi
|
|
check "wireless and network are unchanged but for the basic rates" mesh_exports_same_but_rates
|
|
check "the fips zone is made as in a fresh install" zone_made
|
|
fi
|
|
fw_end
|
|
}
|
|
|
|
mesh_second_upgrade_changes_nothing() {
|
|
mesh_begin mesh_second_upgrade_changes_nothing "a later upgrade changes nothing and does not reload"
|
|
if mesh_released_two; then
|
|
mesh_install
|
|
run_postinst
|
|
if precond "the first upgrade zoned both interfaces" \
|
|
eval 'mesh_zone_made "'"'fips-mesh0' 'fips-mesh1'"'" && mesh_jumps fips-mesh0 && mesh_jumps fips-mesh1'; then
|
|
uci export firewall > "$WORK/first.uci"
|
|
uci export wireless > "$WORK/first.wireless"
|
|
run_postinst
|
|
uci export firewall > "$WORK/second.uci"
|
|
uci export wireless > "$WORK/second.wireless"
|
|
check "uci export firewall is byte-identical" cmp -s "$WORK/first.uci" "$WORK/second.uci"
|
|
check "uci export wireless is byte-identical" cmp -s "$WORK/first.wireless" "$WORK/second.wireless"
|
|
check "no firewall reload" no_reload
|
|
refute "no line about basic rates" said 'basic_rate'
|
|
fi
|
|
fi
|
|
fw_end
|
|
}
|
|
|
|
mesh_first_boot_after_sysupgrade_zones_interfaces() {
|
|
mesh_begin mesh_first_boot_after_sysupgrade_zones_interfaces "the first boot after a sysupgrade zones the mesh before the firewall starts" nostart
|
|
run_released_mesh radio0
|
|
if precond "no state file, no table, and the hook finds the wifi stub" \
|
|
eval '[ ! -e /var/run/fw4.state ] && ! nft list table inet fw4 >/dev/null 2>&1 && stub_first'; then
|
|
mesh_install
|
|
cp "$FW_SETUP" /etc/uci-defaults/90-fips-setup || harness_fail "cannot install the setup"
|
|
run_boot
|
|
check "the run exits 0" same "$RC" 0
|
|
check "the setup is removed" eval '[ ! -e /etc/uci-defaults/90-fips-setup ]'
|
|
check "the zone is made" mesh_zone_made "'fips-mesh0'"
|
|
check "no firewall reload" no_reload
|
|
check "no wifi reload" no_wifi
|
|
fw4 -q start >/dev/null 2>&1
|
|
check "after fw4 starts: fips-mesh0 is in the zone" mesh_jumps fips-mesh0
|
|
fi
|
|
fw_end
|
|
}
|
|
|
|
# The 0.5.2 helper's two meshes after an upgrade, with the mesh zone deleted
|
|
# again, so only the mesh needs saving.
|
|
mesh_hook_unzoned() {
|
|
if mesh_released_two; then
|
|
mesh_install
|
|
run_postinst
|
|
# Absent when the setup under test makes no mesh zone.
|
|
uci -q delete firewall.fips_mesh
|
|
uci commit firewall || harness_fail "cannot delete the mesh zone"
|
|
/etc/init.d/firewall reload >/dev/null 2>&1
|
|
precond "the fips zone holds fips0 and no zone holds the mesh" \
|
|
eval 'zone_made && table_lacks "\"fips-mesh0\"" && [ -z "$(uci -q changes firewall)" ]'
|
|
return
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
mesh_hook_failed_save_ok() {
|
|
check "$1: the setup ends non-zero and is kept" eval '[ "$RC" != 0 ] && [ -f /etc/uci-defaults/90-fips-setup ]'
|
|
check "$1: no firewall reload" no_reload
|
|
check "$1: fips-mesh0 is dropped" mesh_failsafe fips-mesh0
|
|
check "$1: fips-mesh1 is dropped" mesh_failsafe fips-mesh1
|
|
check "$1: the failed save is reported" said 'the firewall configuration could not be saved'
|
|
check "$1: the mesh interfaces are reported closed" said "$MESHCLOSED"
|
|
}
|
|
|
|
mesh_hook_failed_save_closes_and_keeps_setup() {
|
|
local v
|
|
for v in full commit; do
|
|
mesh_begin mesh_hook_failed_save_closes_and_keeps_setup "an upgrade whose mesh save fails ($v) closes the mesh and keeps the setup"
|
|
if mesh_hook_unzoned; then
|
|
if [ "$v" = full ]; then
|
|
small_config
|
|
if precond "full: no mesh copy is left from before" eval '[ ! -e "$MCOPY" ]' &&
|
|
precond "full: the small tmpfs has no page free" small_free 0; then
|
|
run_postinst
|
|
mesh_hook_failed_save_ok full
|
|
check "full: the line names the copy to import" said "uci import firewall < $MCOPY"
|
|
check "full: the copy holds the zone" grep -qF "config zone 'fips_mesh'" "$MCOPY"
|
|
rm -f "$SMALL/fill"
|
|
uci import firewall < "$MCOPY" 2>/dev/null
|
|
/etc/init.d/firewall reload >/dev/null 2>&1
|
|
check "full: after the import and a reload, fips-mesh0 is in the zone" mesh_jumps fips-mesh0
|
|
check "full: after the import and a reload, fips-mesh1 is in the zone" mesh_jumps fips-mesh1
|
|
check "full: after the import and a reload, no failsafe rule" no_mesh_drop
|
|
fi
|
|
else
|
|
wrap_uci_commit_fails
|
|
run_postinst wrap
|
|
if precond "commit: the uci wrapper failed a commit firewall" wrap_called uci 'commit firewall'; then
|
|
mesh_hook_failed_save_ok commit
|
|
fi
|
|
fi
|
|
fi
|
|
done
|
|
fw_end
|
|
}
|
|
|
|
# Continues from mesh_hook_failed_save_closes_and_keeps_setup's last variant:
|
|
# the second run an SDK-feed install makes, with the save still failing.
|
|
mesh_second_sdk_run_after_failed_save_keeps_one_drop() {
|
|
CUR=mesh_second_sdk_run_after_failed_save_keeps_one_drop
|
|
note "$CUR: a second run retries the save and stacks no second drop"
|
|
if precond "the mesh changes are still staged" eval '[ -n "$(uci -q changes firewall)" ]'; then
|
|
: > "$RELOADLOG"
|
|
PATH="$(hook_path wrap)" sh -c '( cd /etc/uci-defaults && . ./90-fips-setup ) && rm -f /etc/uci-defaults/90-fips-setup' > "$OUT" 2>&1
|
|
RC=$?
|
|
sed 's/^/ | /' "$OUT"
|
|
check "the run ends non-zero and the setup is kept" eval '[ "$RC" != 0 ] && [ -f /etc/uci-defaults/90-fips-setup ]'
|
|
check "the save was tried again" eval '[ "$(grep -cx "commit firewall" /tmp/fw-wrap/uci.log)" = 2 ]'
|
|
check "no firewall reload" no_reload
|
|
check "fips-mesh0 has one drop in each chain" mesh_failsafe fips-mesh0
|
|
check "fips-mesh1 has one drop in each chain" mesh_failsafe fips-mesh1
|
|
check "the mesh interfaces are reported closed" said "$MESHCLOSED"
|
|
fi
|
|
fw_end
|
|
}
|
|
|
|
mesh_hook_without_mesh_does_nothing() {
|
|
mesh_begin mesh_hook_without_mesh_does_nothing "with no mesh the hook changes nothing and adds no reload"
|
|
mesh_install
|
|
run_postinst
|
|
check "the setup exits 0" same "$RC" 0
|
|
check "no fips_mesh section" same "$(uget firewall.fips_mesh)" ""
|
|
check "one firewall reload, as a fresh install makes" one_reload
|
|
refute "no line from the mesh helper" said 'fips-mesh-setup:'
|
|
fw_end
|
|
}
|
|
|
|
mesh_hook_after_damaged_save_closes_without_reload() {
|
|
local v
|
|
for v in 1 0; do
|
|
mesh_begin mesh_hook_after_damaged_save_closes_without_reload "when the fips zone's save leaves the file damaged ($v page free), the mesh is closed and nothing saved"
|
|
run_released_mesh radio0
|
|
uci export wireless > "$WORK/wireless.before"
|
|
mesh_install
|
|
if [ "$v" = 1 ]; then
|
|
full_save_start 1 pad
|
|
else
|
|
full_save_start 0
|
|
fi
|
|
if [ "$?" = 0 ]; then
|
|
check "$v: the setup exits 1 and is kept" eval '[ "$RC" = 1 ] && [ -f /etc/uci-defaults/90-fips-setup ]'
|
|
check "$v: no firewall reload" no_reload
|
|
check "$v: the mesh helper saved nothing" eval '! grep -q fips_mesh /etc/config/firewall'
|
|
check "$v: fips-mesh0 is dropped" mesh_failsafe fips-mesh0
|
|
check "$v: fips0's pair is in input" pair_once input
|
|
check "$v: fips0's pair is in forward" pair_once forward
|
|
check "$v: the damaged save is reported" said "$DAMAGED"
|
|
check "$v: the mesh interface is reported closed" said "$MESHCLOSED"
|
|
uci export wireless > "$WORK/wireless.after"
|
|
check "$v: the wireless configuration is left alone" \
|
|
eval 'cmp -s "$WORK/wireless.before" "$WORK/wireless.after" && [ -z "$(uci -q changes wireless)" ]'
|
|
refute "$v: no line about basic rates" said 'basic_rate'
|
|
fi
|
|
done
|
|
fw_end
|
|
}
|
|
|
|
mesh_interface_in_another_zone_is_left_there() {
|
|
local v
|
|
for v in device network hook; do
|
|
mesh_begin mesh_interface_in_another_zone_is_left_there "an interface lan holds by $v is left in lan and reported"
|
|
case "$v" in
|
|
device | hook)
|
|
[ "$v" = hook ] && run_released_mesh radio0
|
|
uci add_list "firewall.$(zone_of lan).device=fips-mesh0" && uci commit firewall ||
|
|
harness_fail "cannot add fips-mesh0 to lan"
|
|
;;
|
|
network)
|
|
uci add_list "firewall.$(zone_of lan).network=fips_mesh_radio0" && uci commit firewall ||
|
|
harness_fail "cannot add the mesh network to lan"
|
|
;;
|
|
esac
|
|
/etc/init.d/firewall reload >/dev/null 2>&1
|
|
if [ "$v" = hook ]; then
|
|
mesh_install
|
|
run_postinst
|
|
else
|
|
run_mesh radio0
|
|
fi
|
|
check "$v: exits 0" same "$RC" 0
|
|
check "$v: no fips_mesh section" same "$(uget firewall.fips_mesh)" ""
|
|
# The stock lan zone is an anonymous section, so the line names it too.
|
|
check "$v: the line names lan" said 'fips-mesh0 is in firewall zone lan (firewall.'
|
|
check "$v: the line names the exposure" \
|
|
said 'whose input policy is ACCEPT, so router services are reachable from anyone in radio range of the mesh; it stays in that zone'
|
|
check "$v: no failsafe rule" no_mesh_drop
|
|
if [ "$v" != hook ]; then
|
|
refute "$v: the closing message does not say fips_mesh rejects all input" said "$ZONED"
|
|
check "$v: the mesh is created" mesh_wireless_made radio0 fips-mesh0
|
|
fi
|
|
[ "$v" = device ] && check "device: no firewall reload" no_reload
|
|
if [ "$v" != network ]; then
|
|
check "$v: input sends fips-mesh0 to lan" eval 'naming_dev input fips-mesh0 | grep -qF "jump input_lan "'
|
|
check "$v: nothing jumps to input_fips_mesh" table_lacks 'jump input_fips_mesh'
|
|
fi
|
|
done
|
|
fw_end
|
|
}
|
|
|
|
# The rules of <chain> naming <dev>.
|
|
naming_dev() {
|
|
listing "$1" || return 1
|
|
printf '%s\n' "$LIST" | grep -F "\"$2\""
|
|
}
|
|
|
|
mesh_add_failed_save_ok() {
|
|
check "$1: the helper exits 1" same "$RC" 1
|
|
check "$1: no reload of either kind" eval '[ -f "$RELOADLOG" ] && [ ! -s "$RELOADLOG" ]'
|
|
check "$1: no wireless section" same "$(uget wireless.fips_mesh_radio0)" ""
|
|
check "$1: /etc/config/wireless is unchanged" cmp -s "$WORK/wireless.conf" /etc/config/wireless
|
|
check "$1: the line says the mesh was not created" said 'so the mesh was not created'
|
|
}
|
|
|
|
mesh_add_failed_save_does_not_create_mesh() {
|
|
local v
|
|
for v in full commit; do
|
|
mesh_begin mesh_add_failed_save_does_not_create_mesh "an add whose save fails ($v) does not create the mesh"
|
|
cp /etc/config/wireless "$WORK/wireless.conf"
|
|
if [ "$v" = full ]; then
|
|
small_config
|
|
if precond "full: no mesh copy is left from before" eval '[ ! -e "$MCOPY" ]' &&
|
|
precond "full: the small tmpfs has no page free" small_free 0; then
|
|
run_mesh radio0
|
|
mesh_add_failed_save_ok full
|
|
check "full: the line names the copy to import" said "uci import firewall < $MCOPY"
|
|
fi
|
|
else
|
|
wrap_uci_commit_fails
|
|
run_mesh wrap radio0
|
|
if precond "commit: the uci wrapper failed a commit firewall" wrap_called uci 'commit firewall'; then
|
|
mesh_add_failed_save_ok commit
|
|
fi
|
|
fi
|
|
done
|
|
fw_end
|
|
}
|
|
|
|
# Continues from mesh_add_failed_save_does_not_create_mesh's last variant.
|
|
mesh_readd_after_failed_save_saves_staged_zone() {
|
|
CUR=mesh_readd_after_failed_save_saves_staged_zone
|
|
note "$CUR: an add after a failed save saves the zone left staged"
|
|
rm -f "$WRAP/uci"
|
|
if precond "the zone is staged, reads back as listed, and is not in the file" \
|
|
eval '[ -n "$(uci -q changes firewall)" ] && [ "$(uget firewall.fips_mesh.device)" = fips-mesh0 ] && ! grep -q fips_mesh /etc/config/firewall'; then
|
|
run_mesh radio0
|
|
check "the helper exits 0" same "$RC" 0
|
|
check "nothing is left staged" eval '[ -z "$(uci -q changes firewall)" ]'
|
|
check "/etc/config/firewall holds the zone and fips-mesh0" \
|
|
eval 'grep -qF "config zone '"'fips_mesh'"'" /etc/config/firewall && grep -qF "fips-mesh0" /etc/config/firewall'
|
|
check "fips-mesh0 is in the zone" mesh_jumps fips-mesh0
|
|
check "the firewall reloads, then the radio" log_is "$(printf 'reload\nwifi reload')"
|
|
check "no failsafe rule" no_mesh_drop
|
|
fi
|
|
fw_end
|
|
}
|
|
|
|
mesh_add_failed_close_does_not_create_mesh() {
|
|
mesh_begin mesh_add_failed_close_does_not_create_mesh "an add that cannot close the interface after a failed reload does not create the mesh"
|
|
if mesh_failed_reload_start; then
|
|
wrap_nft_fails 'insert rule inet fw4 input iifname fips-mesh0 drop comment "fips-mesh-reload-failed"'
|
|
run_mesh wrap radio0
|
|
if precond "the nft wrapper failed the insert" \
|
|
wrap_called nft 'insert rule inet fw4 input iifname fips-mesh0 drop comment "fips-mesh-reload-failed"'; then
|
|
check "the helper exits 1" same "$RC" 1
|
|
check "no wifi reload" no_wifi
|
|
check "no wireless section" same "$(uget wireless.fips_mesh_radio0)" ""
|
|
check "the line says it could not be closed" said 'could not be closed'
|
|
fi
|
|
fi
|
|
fw_end
|
|
}
|
|
|
|
mesh_readd_unnumbered_radio_moves_zone_entry() {
|
|
mesh_begin mesh_readd_unnumbered_radio_moves_zone_entry "a re-add that renames the interface keeps the old name zoned until the rename"
|
|
uci set wireless.radiox=wifi-device &&
|
|
uci set wireless.radiox.type=mac80211 &&
|
|
uci set wireless.radiox.channel=1 &&
|
|
uci set wireless.radiox.band=2g &&
|
|
uci commit wireless || harness_fail "cannot add radiox"
|
|
run_mesh radiox
|
|
if precond "the first add listed fips-mesh0" same "$(uget firewall.fips_mesh.device)" fips-mesh0; then
|
|
run_mesh radiox
|
|
check "the second add exits 0" same "$RC" 0
|
|
check "the zone lists exactly fips-mesh1" same "$(uget firewall.fips_mesh.device)" fips-mesh1
|
|
check "the file lists fips-mesh1 and not fips-mesh0" \
|
|
eval 'grep -qF "fips-mesh1" /etc/config/firewall && ! grep -qF "fips-mesh0" /etc/config/firewall'
|
|
check "the firewall reloads once, before the radio" log_is "$(printf 'reload\nwifi reload')"
|
|
check "the loaded input still names both" \
|
|
eval 'naming_dev input fips-mesh0 >/dev/null && naming_dev input fips-mesh1 >/dev/null'
|
|
/etc/init.d/firewall reload >/dev/null 2>&1
|
|
check "after a reload: fips-mesh1 is in the zone" mesh_jumps fips-mesh1
|
|
check "after a reload: nothing names fips-mesh0" table_lacks '"fips-mesh0"'
|
|
fi
|
|
fw_end
|
|
}
|
|
|
|
mesh_hook_failed_reload_drops_mesh_input() {
|
|
mesh_begin mesh_hook_failed_reload_drops_mesh_input "an upgrade whose reload fails drops IP traffic arriving on the mesh"
|
|
if mesh_released_two && mesh_failed_reload_start; then
|
|
mesh_install
|
|
run_postinst
|
|
check "the setup exits 0 and is deleted" eval '[ "$RC" = 0 ] && [ ! -e /etc/uci-defaults/90-fips-setup ]'
|
|
check "fips-mesh0 is dropped" mesh_failsafe fips-mesh0
|
|
check "fips-mesh1 is dropped" mesh_failsafe fips-mesh1
|
|
check "fips0's pair is in input" pair_once input
|
|
check "fips0's pair is in forward" pair_once forward
|
|
check "the failed reload is reported" said 'the firewall reload failed, so IP traffic arriving on fips-mesh0 fips-mesh1 is dropped'
|
|
check "no wifi reload" no_wifi
|
|
fi
|
|
fw_end
|
|
}
|
|
|
|
# ── The mesh's basic rates ──────────────────────────────────────────────────
|
|
# fips-mesh-setup gives each mesh interface basic_rate '6000 12000 24000', and
|
|
# 90-fips-setup gives it to those made by earlier packages, which set none.
|
|
|
|
# wrap_uci_commit_fails_for <config>: a uci wrapper whose commit of <config>
|
|
# fails.
|
|
wrap_uci_commit_fails_for() {
|
|
cat > "$WRAP/uci" <<EOF
|
|
#!/bin/sh
|
|
echo "\$*" >> /tmp/fw-wrap/uci.log
|
|
[ "\$*" = "commit $1" ] && exit 1
|
|
exec /sbin/uci "\$@"
|
|
EOF
|
|
chmod 0755 "$WRAP/uci"
|
|
}
|
|
|
|
mesh_upgrade_sets_basic_rate_on_existing_interfaces() {
|
|
mesh_begin mesh_upgrade_sets_basic_rate_on_existing_interfaces "an upgrade gives the interfaces the 0.5.2 helper made the mesh's basic rates, without a wifi reload"
|
|
if mesh_released_two &&
|
|
precond "the 0.5.2 helper set no basic_rate" \
|
|
eval '[ -z "$(uget wireless.fips_mesh_radio0.basic_rate)" ] && [ -z "$(uget wireless.fips_mesh_radio1.basic_rate)" ]'; then
|
|
mesh_install
|
|
run_postinst
|
|
check "the setup exits 0 and is deleted" eval '[ "$RC" = 0 ] && [ ! -e /etc/uci-defaults/90-fips-setup ]'
|
|
check "fips_mesh_radio0 has basic_rate '$RATES'" same "$(uget wireless.fips_mesh_radio0.basic_rate)" "$RATES"
|
|
check "fips_mesh_radio1 has basic_rate '$RATES'" same "$(uget wireless.fips_mesh_radio1.basic_rate)" "$RATES"
|
|
check "the rates are saved, not only staged" \
|
|
eval '[ "$(grep -cxF " option basic_rate '"'$RATES'"'" /etc/config/wireless)" = 2 ] && [ -z "$(uci -q changes wireless)" ]'
|
|
check "nothing else in wireless or network changed" mesh_exports_same_but_rates
|
|
check "no wifi reload" no_wifi
|
|
check "the line names both interfaces and that any network reload applies the rates" \
|
|
said "set basic_rate '$RATES' on fips-mesh0 fips-mesh1; it applies at the next reboot or network reload, which 'wifi reload' and a LuCI apply make, as can the fips gateway's start or stop"
|
|
check "a line says to upgrade routers reached over the mesh farthest first" \
|
|
said 'when you reach routers over the mesh, upgrade and reload them one at a time, farthest first'
|
|
fi
|
|
fw_end
|
|
}
|
|
|
|
mesh_upgrade_leaves_other_wifi_ifaces_alone() {
|
|
mesh_begin mesh_upgrade_leaves_other_wifi_ifaces_alone "an upgrade sets no basic_rate on wifi-ifaces fips-mesh-setup did not make"
|
|
run_released_mesh radio0
|
|
uci set wireless.own_mesh=wifi-iface &&
|
|
uci set wireless.own_mesh.device=radio1 &&
|
|
uci set wireless.own_mesh.mode=mesh &&
|
|
uci set wireless.own_mesh.mesh_id=other &&
|
|
uci set wireless.own_mesh.encryption=none &&
|
|
uci set wireless.default_radio1=wifi-iface &&
|
|
uci set wireless.default_radio1.device=radio1 &&
|
|
uci set wireless.default_radio1.mode=ap &&
|
|
uci set wireless.default_radio1.ssid=OpenWrt &&
|
|
uci commit wireless || harness_fail "cannot add the operator's wifi-ifaces"
|
|
dump wireless.own_mesh > "$WORK/own.before"
|
|
dump wireless.default_radio1 > "$WORK/ap.before"
|
|
if precond "fips_mesh_radio0 exists without basic_rate" \
|
|
eval '[ "$(uget wireless.fips_mesh_radio0)" = wifi-iface ] && [ -z "$(uget wireless.fips_mesh_radio0.basic_rate)" ]'; then
|
|
mesh_install
|
|
run_postinst
|
|
dump wireless.own_mesh > "$WORK/own.after"
|
|
dump wireless.default_radio1 > "$WORK/ap.after"
|
|
check "the setup exits 0 and is deleted" eval '[ "$RC" = 0 ] && [ ! -e /etc/uci-defaults/90-fips-setup ]'
|
|
check "fips_mesh_radio0 has basic_rate '$RATES'" same "$(uget wireless.fips_mesh_radio0.basic_rate)" "$RATES"
|
|
check "the operator's mesh wifi-iface is unchanged" cmp -s "$WORK/own.before" "$WORK/own.after"
|
|
check "the operator's mesh wifi-iface has no basic_rate" same "$(uget wireless.own_mesh.basic_rate)" ""
|
|
check "the AP wifi-iface is unchanged" cmp -s "$WORK/ap.before" "$WORK/ap.after"
|
|
check "/etc/config/wireless has one basic_rate" same "$(grep -c basic_rate /etc/config/wireless)" 1
|
|
fi
|
|
fw_end
|
|
}
|
|
|
|
mesh_upgrade_keeps_operator_basic_rate() {
|
|
mesh_begin mesh_upgrade_keeps_operator_basic_rate "an upgrade keeps a basic_rate the operator set on a fips mesh interface, and names it"
|
|
if mesh_released_two; then
|
|
uci set wireless.fips_mesh_radio1.basic_rate='1000 2000 5500 11000' && uci commit wireless ||
|
|
harness_fail "cannot set the operator's basic_rate"
|
|
dump wireless.fips_mesh_radio1 > "$WORK/radio1.before"
|
|
mesh_install
|
|
run_postinst
|
|
dump wireless.fips_mesh_radio1 > "$WORK/radio1.after"
|
|
check "the setup exits 0 and is deleted" eval '[ "$RC" = 0 ] && [ ! -e /etc/uci-defaults/90-fips-setup ]'
|
|
check "fips_mesh_radio0 has basic_rate '$RATES'" same "$(uget wireless.fips_mesh_radio0.basic_rate)" "$RATES"
|
|
check "fips_mesh_radio1 keeps the operator's basic_rate" same "$(uget wireless.fips_mesh_radio1.basic_rate)" '1000 2000 5500 11000'
|
|
check "fips_mesh_radio1 is otherwise unchanged" cmp -s "$WORK/radio1.before" "$WORK/radio1.after"
|
|
check "one line names the kept rates" said_once "fips-mesh1 keeps its basic_rate '1000 2000 5500 11000'"
|
|
check "the set line names only fips-mesh0" said "set basic_rate '$RATES' on fips-mesh0;"
|
|
fi
|
|
fw_end
|
|
}
|
|
|
|
mesh_readd_keeps_operator_basic_rate() {
|
|
mesh_begin mesh_readd_keeps_operator_basic_rate "running the helper again on a radio keeps a basic_rate the operator set"
|
|
run_mesh radio0
|
|
if precond "the first add set basic_rate '$RATES'" same "$(uget wireless.fips_mesh_radio0.basic_rate)" "$RATES"; then
|
|
uci set wireless.fips_mesh_radio0.basic_rate='12000 24000' && uci commit wireless ||
|
|
harness_fail "cannot set the operator's basic_rate"
|
|
run_mesh radio0
|
|
check "the second add exits 0" same "$RC" 0
|
|
check "the operator's basic_rate is kept" same "$(uget wireless.fips_mesh_radio0.basic_rate)" '12000 24000'
|
|
check "the closing message names the kept rates" said "basic_rate '12000 24000'"
|
|
fi
|
|
fw_end
|
|
}
|
|
|
|
mesh_rates_failed_save_keeps_setup() {
|
|
mesh_begin mesh_rates_failed_save_keeps_setup "an upgrade whose wireless save fails says so and keeps the setup for the next boot"
|
|
if mesh_released_two; then
|
|
mesh_install
|
|
wrap_uci_commit_fails_for wireless
|
|
run_postinst wrap
|
|
if precond "the uci wrapper failed a commit wireless" wrap_called uci 'commit wireless'; then
|
|
check "the setup ends non-zero and is kept" eval '[ "$RC" != 0 ] && [ -f /etc/uci-defaults/90-fips-setup ]'
|
|
check "the failed save is reported" said "the wireless configuration could not be saved, so the mesh interfaces keep the basic rates they had"
|
|
check "/etc/config/wireless has no basic_rate" eval '! grep -q basic_rate /etc/config/wireless'
|
|
check "the mesh zone was still made" mesh_zone_made "'fips-mesh0' 'fips-mesh1'"
|
|
rm -f "$WRAP/uci"
|
|
run_boot
|
|
check "the next boot exits 0 and removes the setup" eval '[ "$RC" = 0 ] && [ ! -e /etc/uci-defaults/90-fips-setup ]'
|
|
check "the next boot saves the rates" same "$(grep -cxF " option basic_rate '$RATES'" /etc/config/wireless)" 2
|
|
check "the next boot says it saved the rates staged before, on both interfaces" \
|
|
said "set basic_rate '$RATES' on fips-mesh0 fips-mesh1;"
|
|
fi
|
|
fi
|
|
fw_end
|
|
}
|
|
|
|
mesh_rates_damaged_save_names_copy() {
|
|
mesh_begin mesh_rates_damaged_save_names_copy "a rates save onto a full overlay that empties the wireless file says so, and its copy restores it"
|
|
if mesh_released_two; then
|
|
small_config
|
|
if precond "no wireless copy is left from before" eval '[ ! -e "$WCOPY" ]' &&
|
|
precond "the small tmpfs has no page free" small_free 0; then
|
|
run_mesh rates
|
|
if precond "the save left /etc/config/wireless without radio0" \
|
|
eval '! grep -qF "config wifi-device '"'radio0'"'" /etc/config/wireless'; then
|
|
check "the helper exits 1" same "$RC" 1
|
|
check "the damaged save is reported" said 'the wireless configuration could not be saved and /etc/config/wireless was left damaged'
|
|
check "the line names the copy to import" said "uci import wireless < $WCOPY"
|
|
refute "no line says the rates were set" said "set basic_rate '$RATES' on"
|
|
check "the copy holds radio0 and the rates" \
|
|
eval 'grep -qF "config wifi-device '"'radio0'"'" "$WCOPY" && [ "$(grep -cxF " option basic_rate '"'$RATES'"'" "$WCOPY")" = 2 ]'
|
|
rm -f "$SMALL/fill"
|
|
uci import wireless < "$WCOPY" 2>/dev/null
|
|
check "after the import: both radios are back" \
|
|
eval '[ "$(uget wireless.radio0)" = wifi-device ] && [ "$(uget wireless.radio1)" = wifi-device ]'
|
|
check "after the import: both mesh interfaces have the rates" \
|
|
eval '[ "$(uget wireless.fips_mesh_radio0.basic_rate)" = "$RATES" ] && [ "$(uget wireless.fips_mesh_radio1.basic_rate)" = "$RATES" ]'
|
|
run_mesh rates
|
|
check "a later run exits 0 and says nothing" eval '[ "$RC" = 0 ] && [ ! -s "$OUT" ]'
|
|
fi
|
|
fi
|
|
fi
|
|
fw_end
|
|
}
|
|
|
|
# ── Harness start ───────────────────────────────────────────────────────────
|
|
|
|
[ -r "$FW_SETUP" ] || harness_fail "cannot read the setup under test, $FW_SETUP"
|
|
[ -r "$FW_MESH_SETUP" ] || harness_fail "cannot read the mesh helper under test, $FW_MESH_SETUP"
|
|
if [ "$FW_SNIPPETS" != none ] && [ ! -d "$FW_SNIPPETS" ]; then
|
|
harness_fail "FW_SNIPPETS is neither none nor a directory: $FW_SNIPPETS"
|
|
fi
|
|
for t in uci fw4 nft; do
|
|
command -v "$t" >/dev/null 2>&1 || harness_fail "$t is missing from this image"
|
|
done
|
|
[ -x /usr/sbin/nft ] || harness_fail "/usr/sbin/nft is missing"
|
|
[ -f /etc/init.d/firewall ] || harness_fail "/etc/init.d/firewall is missing"
|
|
|
|
rm -rf "$STOCK"
|
|
mkdir -p "$STOCK" && cp -a /etc/config "$STOCK/config" && cp -a /usr/share/nftables.d "$STOCK/nftables.d" ||
|
|
harness_fail "cannot save the stock configuration"
|
|
|
|
# Every firewall init-script call is logged, and then runs the real script. With
|
|
# the flush-only marker, a reload only flushes the table instead, standing in
|
|
# for a ruleset whose rendering stopped right after its flush.
|
|
mv /etc/init.d/firewall /etc/init.d/firewall.real || harness_fail "cannot move the firewall init script"
|
|
cat > /etc/init.d/firewall <<EOF
|
|
#!/bin/sh
|
|
echo "\$*" >> $RELOADLOG
|
|
if [ -e $FLUSHONLY ] && [ "\$1" = reload ]; then
|
|
nft flush table inet fw4
|
|
nft list chain inet fw4 input > $FLUSHLIST 2>&1
|
|
exit 0
|
|
fi
|
|
exec /etc/rc.common /etc/init.d/firewall.real "\$@"
|
|
EOF
|
|
chmod 0755 /etc/init.d/firewall || harness_fail "cannot install the firewall wrapper"
|
|
|
|
echo "OpenWrt firewall scenarios ($(. /etc/openwrt_release && echo "$DISTRIB_DESCRIPTION"))"
|
|
echo "setup under test: $FW_SETUP; mesh helper: $FW_MESH_SETUP; snippets: $FW_SNIPPETS"
|
|
|
|
ALL="fw_upgrade_moves_fips0_into_its_own_zone
|
|
fw_upgrade_under_apk_environment_moves_fips0
|
|
fw_second_run_changes_nothing
|
|
fw_operator_zone_is_kept_and_named
|
|
fw_operator_zone_accepting_input_is_reported
|
|
fw_operator_wildcard_zone_holds_fips0
|
|
fw_fresh_install_creates_live_zone
|
|
fw_operator_edits_survive_rerun
|
|
fw_lan_through_network_is_reported
|
|
fw_old_include_and_files_removed
|
|
fw_unrelated_include_survives
|
|
fw_no_lan_zone_creates_zone_without_forwarding
|
|
fw_sourced_setup_does_not_leak
|
|
fw_no_firewall_migrates_without_reload
|
|
fw_sysupgrade_first_boot_migrates
|
|
fw_missing_state_file_blocks_fips0
|
|
fw_operator_scoping_renders_before_port_forwards
|
|
fw_bare_plus_device_does_not_hold_fips0
|
|
fw_operator_fips_ping_rule_kept
|
|
fw_foreign_fips_zone_not_doubled
|
|
fw_disabled_zone_does_not_hold_fips0
|
|
fw_foreign_fips_zone_under_accept_default_reported
|
|
fw_operator_zone_named_fips_without_lan_forwarding_is_named
|
|
fw_disabled_fips_zone_under_accept_default_reported
|
|
fw_downgrade_then_upgrade_migrates_again
|
|
fw_renamed_fips_zone_is_checked_by_its_name
|
|
fw_failed_reload_blocks_new_fips0_connections
|
|
fw_recovery_after_failed_reload_clears_block
|
|
fw_failed_reload_under_apk_environment_blocks_fips0
|
|
fw_unloaded_firewall_is_reported
|
|
fw_reload_without_prior_table_is_judged_by_the_zone_jump
|
|
fw_failed_commit_blocks_and_keeps_setup
|
|
fw_second_sdk_run_after_failed_commit_keeps_one_block
|
|
fw_boot_retry_after_failed_commit_migrates
|
|
fw_failed_commit_removes_leftover_files
|
|
fw_second_run_completes_a_half_failsafe_pair
|
|
fw_full_overlay_save_blocks_and_keeps_setup
|
|
fw_partial_save_blocks_and_keeps_setup
|
|
fw_path_without_sbin_still_migrates
|
|
fw_missing_nft_is_reported
|
|
fw_probe_add_failure_blocks_fips0
|
|
fw_unchecked_reload_without_state_file_names_restart
|
|
fw_listing_failure_blocks_fips0
|
|
fw_flush_only_load_blocks_fips0
|
|
mesh_add_puts_interface_in_reject_zone
|
|
mesh_second_add_changes_nothing
|
|
mesh_two_radios_share_one_zone_and_remove_cleans_up
|
|
mesh_remove_waits_for_interface_before_reload
|
|
mesh_remove_waits_for_interface_before_reload_and_reports_one_that_stays
|
|
mesh_remove_keeps_zone_until_interface_is_gone
|
|
mesh_add_then_remove_restores_firewall_config
|
|
mesh_existing_zone_options_are_kept
|
|
mesh_remove_keeps_empty_zone_it_took_nothing_from
|
|
mesh_disabled_zone_is_reported_not_failed
|
|
mesh_renamed_zone_is_checked_by_its_name
|
|
mesh_conflicting_firewall_section_is_refused
|
|
mesh_conflicting_firewall_section_is_refused_unless_it_holds_the_interface
|
|
mesh_failed_reload_drops_mesh_input
|
|
mesh_missing_state_file_names_restart
|
|
mesh_unloaded_firewall_is_reported
|
|
mesh_state_file_without_table_is_reported
|
|
mesh_upgrade_zones_existing_interfaces
|
|
mesh_second_upgrade_changes_nothing
|
|
mesh_first_boot_after_sysupgrade_zones_interfaces
|
|
mesh_hook_failed_save_closes_and_keeps_setup
|
|
mesh_second_sdk_run_after_failed_save_keeps_one_drop
|
|
mesh_hook_without_mesh_does_nothing
|
|
mesh_hook_after_damaged_save_closes_without_reload
|
|
mesh_interface_in_another_zone_is_left_there
|
|
mesh_add_failed_save_does_not_create_mesh
|
|
mesh_readd_after_failed_save_saves_staged_zone
|
|
mesh_add_failed_close_does_not_create_mesh
|
|
mesh_readd_unnumbered_radio_moves_zone_entry
|
|
mesh_hook_failed_reload_drops_mesh_input
|
|
mesh_upgrade_sets_basic_rate_on_existing_interfaces
|
|
mesh_upgrade_leaves_other_wifi_ifaces_alone
|
|
mesh_upgrade_keeps_operator_basic_rate
|
|
mesh_readd_keeps_operator_basic_rate
|
|
mesh_rates_failed_save_keeps_setup
|
|
mesh_rates_damaged_save_names_copy"
|
|
|
|
# The five continuation cases run only right after the case they continue.
|
|
if [ -n "$FW_CASES" ]; then
|
|
SELECTED="$FW_CASES"
|
|
EXPECTED=$(echo "$FW_CASES" | wc -w)
|
|
for c in $FW_CASES; do
|
|
printf '%s\n' "$ALL" | grep -qxF "$c" || harness_fail "unknown case in FW_CASES: $c"
|
|
done
|
|
else
|
|
SELECTED="$ALL"
|
|
EXPECTED=$ALL_CASES
|
|
fi
|
|
|
|
for c in $SELECTED; do
|
|
"$c"
|
|
done
|
|
|
|
fw_clean
|
|
echo ""
|
|
echo "ran $RAN of $EXPECTED cases"
|
|
if [ "$RAN" -ne "$EXPECTED" ]; then
|
|
echo "firewall-scenarios: $((EXPECTED - RAN)) case(s) did not reach their end" >&2
|
|
exit 2
|
|
fi
|
|
if [ "$FAILURES" -eq 0 ]; then
|
|
echo "firewall-scenarios: all $CASES checks passed"
|
|
exit 0
|
|
fi
|
|
echo "firewall-scenarios: $FAILURES of $CASES checks failed"
|
|
exit 1
|