A missing match or target produced several lines of backend warnings and a parser complaint about whatever could no longer be read, handed to the user unchanged. That reads as a broken configuration rather than as a kernel without the module, and for mark, NFQUEUE and queue-bypass — which have no alternative and must refuse — naming the extension is the only help the message can offer. Every match and target the module authors is recognised, so the refusal now leads with the cause and keeps the backend's own words after it.
947 lines
36 KiB
Shell
947 lines
36 KiB
Shell
#!/system/bin/sh
|
|
# Idempotent boot-local firewall reconciler.
|
|
#
|
|
# The stable ZAPRET2_OUT/ZAPRET2_IN namespace is exclusively owned by this
|
|
# module. A complete ruleset is derived from the compiled preset on every
|
|
# start. There is deliberately no firewall WAL: iptables-restore validates the
|
|
# complete candidate and publishes it at COMMIT. Any interruption is recovered
|
|
# by repeating z2_fw_cleanup_family under the lifecycle lock.
|
|
|
|
Z2_FW_OUT_CHAIN="${Z2_FW_OUT_CHAIN:-ZAPRET2_OUT}"
|
|
Z2_FW_IN_CHAIN="${Z2_FW_IN_CHAIN:-ZAPRET2_IN}"
|
|
Z2_FW_BACKEND=""
|
|
Z2_FW_CONNBYTES=0
|
|
Z2_FW_MULTIPORT=1
|
|
Z2_FW_RULES=0
|
|
Z2_FW_CHAINS=0
|
|
Z2_FW_ANCHORS=0
|
|
Z2_FW_FAILURE_CLASS=""
|
|
Z2_FW_ERROR_DETAIL=""
|
|
Z2_FW_FALLBACK_DETAIL=""
|
|
Z2_FW_LAST_RESTORE_EXIT=0
|
|
Z2_FW_LAST_RESTORE_DETAIL=""
|
|
Z2_FW_LAST_FAILURE_CLASS=""
|
|
Z2_FW_RESTORE_WAIT_IPTABLES=unknown
|
|
Z2_FW_RESTORE_WAIT_IP6TABLES=unknown
|
|
Z2_FW_BASELINE_READY=0
|
|
Z2_FW_BASELINE_OUT_CHAIN=0
|
|
Z2_FW_BASELINE_IN_CHAIN=0
|
|
Z2_FW_BASELINE_OUT_ANCHORS=0
|
|
Z2_FW_BASELINE_IN_ANCHORS=0
|
|
Z2_FW_AUDIT_IPTABLES=""
|
|
Z2_FW_AUDIT_IP6TABLES=""
|
|
Z2_FW_VERIFY_DETAIL=""
|
|
|
|
# iptables-restore gained native xtables-lock waiting later than the oldest
|
|
# Android release supported by the module. Prefer the backend's own lock wait
|
|
# when advertised. Older/vendor backends receive the same bounded wait only
|
|
# after they explicitly report the xtables lock as busy.
|
|
Z2_FW_LOCK_WAIT_SECONDS=5
|
|
Z2_FW_DIAGNOSTIC_MAX_BYTES=384
|
|
|
|
z2_fw_restore_command() {
|
|
case "$1" in
|
|
iptables) printf '%s\n' iptables-restore ;;
|
|
ip6tables) printf '%s\n' ip6tables-restore ;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
|
|
z2_fw_restore_available() {
|
|
local restore
|
|
restore="$(z2_fw_restore_command "$1")" || return 1
|
|
command -v "$restore" >/dev/null 2>&1
|
|
}
|
|
|
|
z2_fw_restore_supports_wait() {
|
|
local restore="$1" cached
|
|
case "$restore" in
|
|
iptables-restore) cached="$Z2_FW_RESTORE_WAIT_IPTABLES" ;;
|
|
ip6tables-restore) cached="$Z2_FW_RESTORE_WAIT_IP6TABLES" ;;
|
|
*) return 1 ;;
|
|
esac
|
|
if [ "$cached" = unknown ]; then
|
|
if "$restore" --help 2>&1 | grep -Fq -- '--wait'; then
|
|
cached=1
|
|
else
|
|
cached=0
|
|
fi
|
|
case "$restore" in
|
|
iptables-restore) Z2_FW_RESTORE_WAIT_IPTABLES="$cached" ;;
|
|
ip6tables-restore) Z2_FW_RESTORE_WAIT_IP6TABLES="$cached" ;;
|
|
esac
|
|
fi
|
|
[ "$cached" = 1 ]
|
|
}
|
|
|
|
z2_fw_reset_restore_wait_capabilities() {
|
|
Z2_FW_RESTORE_WAIT_IPTABLES=unknown
|
|
Z2_FW_RESTORE_WAIT_IP6TABLES=unknown
|
|
}
|
|
|
|
z2_fw_normalize_diagnostic() {
|
|
local LC_ALL=C
|
|
printf '%s' "$1" | tr '[:cntrl:]' ' ' | cut -b "1-$Z2_FW_DIAGNOSTIC_MAX_BYTES"
|
|
}
|
|
|
|
z2_fw_read_restore_diagnostic() {
|
|
local path="$1" detail
|
|
detail="$(tail -c "$Z2_FW_DIAGNOSTIC_MAX_BYTES" "$path" 2>/dev/null)" || detail=""
|
|
z2_fw_normalize_diagnostic "$detail"
|
|
}
|
|
|
|
z2_fw_diagnostic_is_lock_busy() {
|
|
case "$1" in
|
|
*xtables*lock*|*XTABLES*lock*|*Another\ app*holding*lock*|\
|
|
*another\ app*holding*lock*|*lock*temporarily\ unavailable*|\
|
|
*lock*busy*) return 0 ;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
|
|
z2_fw_lock_retry_pause() {
|
|
sleep 1
|
|
}
|
|
|
|
z2_fw_diagnostic_is_connbytes_unsupported() {
|
|
case "$1" in
|
|
*[Cc]onnbytes*) return 0 ;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
|
|
# Two independent signals are required here, unlike connbytes. A kernel without
|
|
# xt_multiport makes iptables report the missing extension and then reject the
|
|
# port argument it can no longer parse, so the port complaint arrives together
|
|
# with the extension name. That complaint on its own is also exactly what a
|
|
# genuinely malformed port list produces, and silently rebuilding the intended
|
|
# topology because of it would hide a broken configuration instead of a missing
|
|
# kernel module.
|
|
z2_fw_diagnostic_is_multiport_unsupported() {
|
|
case "$1" in
|
|
*[Mm]ultiport*) ;;
|
|
*) return 1 ;;
|
|
esac
|
|
case "$1" in
|
|
*not\ supported*|*missing\ kernel\ module*|*no\ kernel\ module*|\
|
|
*load\ match*|*[Uu]nknown\ option*|*invalid\ port/service*) return 0 ;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
|
|
# The backend answers a missing extension with several lines of warnings and a
|
|
# parser complaint about whatever it could no longer read. Handed to the user
|
|
# unchanged that reads as a broken configuration. Every match and target the
|
|
# module authors is named here, so the one the kernel refused can be said
|
|
# plainly ahead of the backend's own words — including the three that have no
|
|
# fallback, where naming the extension is the only help there is.
|
|
z2_fw_missing_extension() {
|
|
local detail="$1" name
|
|
case "$detail" in
|
|
*not\ supported*|*missing\ kernel\ module*|*no\ kernel\ module*|\
|
|
*[Nn]o\ chain/target/match*|*[Uu]nknown\ option*|*[Cc]ouldn\'t\ load*) ;;
|
|
*) return 1 ;;
|
|
esac
|
|
for name in multiport connbytes mark NFQUEUE; do
|
|
case "$detail" in
|
|
*"$name"*) printf '%s\n' "$name"; return 0 ;;
|
|
esac
|
|
done
|
|
return 1
|
|
}
|
|
|
|
z2_fw_ensure_scratch_dir() {
|
|
# common.sh owns the authenticated implementation; the standalone fallback
|
|
# exists only for tests that source this reconciler on its own.
|
|
if command -v ensure_state_tmp_dir >/dev/null 2>&1; then
|
|
ensure_state_tmp_dir
|
|
return
|
|
fi
|
|
umask 077
|
|
if [ ! -e "$STATE_DIR/tmp" ] && [ ! -L "$STATE_DIR/tmp" ]; then
|
|
mkdir "$STATE_DIR/tmp" 2>/dev/null
|
|
fi
|
|
[ -d "$STATE_DIR/tmp" ] && [ ! -L "$STATE_DIR/tmp" ] || return 1
|
|
chmod 0700 "$STATE_DIR/tmp" 2>/dev/null || return 1
|
|
[ -d "$STATE_DIR/tmp" ] && [ ! -L "$STATE_DIR/tmp" ] || return 1
|
|
}
|
|
|
|
# Scratch names end in the creating PID, so residue from a previous boot whose
|
|
# PID the kernel handed us again would otherwise fence every transaction
|
|
# forever — and a liveness sweep cannot help, because the PID in the name is
|
|
# ours and therefore alive. Nothing else can hold this name: we own the
|
|
# lifecycle lock and have not written it yet in this process, so whatever is
|
|
# there was abandoned by a process that no longer exists.
|
|
z2_fw_claim_scratch_path() {
|
|
local path="$1"
|
|
{ [ -e "$path" ] || [ -L "$path" ]; } || return 0
|
|
rm -rf "$path" 2>/dev/null || return 1
|
|
[ ! -e "$path" ] && [ ! -L "$path" ]
|
|
}
|
|
|
|
z2_fw_run_restore() {
|
|
local restore="$1" tool="$2" phase="$3" batch="$4"
|
|
local capture wait_supported=0 attempts=0 rc=1 cleanup_rc=0 detail
|
|
capture="$STATE_DIR/tmp/firewall-restore.${tool}.$$.error"
|
|
# Reset the result fields before the first failure exit, or a failure here
|
|
# would report the exit code of the previous phase.
|
|
Z2_FW_LAST_RESTORE_EXIT=0
|
|
Z2_FW_LAST_RESTORE_DETAIL=""
|
|
Z2_FW_LAST_FAILURE_CLASS=""
|
|
z2_fw_ensure_scratch_dir || {
|
|
Z2_FW_LAST_FAILURE_CLASS=STATE_UNAVAILABLE
|
|
Z2_FW_LAST_RESTORE_DETAIL="unavailable firewall scratch directory"
|
|
return 1
|
|
}
|
|
state_path_is_managed_file "$capture" || {
|
|
Z2_FW_LAST_FAILURE_CLASS=STATE_UNAVAILABLE
|
|
Z2_FW_LAST_RESTORE_DETAIL="unsafe firewall diagnostic path"
|
|
return 1
|
|
}
|
|
z2_fw_claim_scratch_path "$capture" || {
|
|
Z2_FW_LAST_FAILURE_CLASS=STATE_UNAVAILABLE
|
|
Z2_FW_LAST_RESTORE_DETAIL="firewall diagnostic path already exists"
|
|
return 1
|
|
}
|
|
umask 077
|
|
if ! : > "$capture" || ! chmod 0600 "$capture" 2>/dev/null; then
|
|
rm -f "$capture" 2>/dev/null
|
|
Z2_FW_LAST_FAILURE_CLASS=STATE_UNAVAILABLE
|
|
Z2_FW_LAST_RESTORE_DETAIL="cannot create private firewall diagnostic capture"
|
|
return 1
|
|
fi
|
|
z2_fw_restore_supports_wait "$restore" && wait_supported=1
|
|
|
|
while :; do
|
|
: > "$capture" || {
|
|
rc=1
|
|
Z2_FW_LAST_FAILURE_CLASS=STATE_UNAVAILABLE
|
|
Z2_FW_LAST_RESTORE_DETAIL="cannot reset firewall diagnostic capture"
|
|
break
|
|
}
|
|
if [ "$wait_supported" = 1 ]; then
|
|
if [ "$phase" = test ]; then
|
|
"$restore" --wait "$Z2_FW_LOCK_WAIT_SECONDS" --test --noflush \
|
|
< "$batch" >/dev/null 2>"$capture"
|
|
else
|
|
"$restore" --wait "$Z2_FW_LOCK_WAIT_SECONDS" --noflush \
|
|
< "$batch" >/dev/null 2>"$capture"
|
|
fi
|
|
elif [ "$phase" = test ]; then
|
|
"$restore" --test --noflush < "$batch" >/dev/null 2>"$capture"
|
|
else
|
|
"$restore" --noflush < "$batch" >/dev/null 2>"$capture"
|
|
fi
|
|
rc=$?
|
|
detail="$(z2_fw_read_restore_diagnostic "$capture")"
|
|
[ "$rc" -ne 0 ] || break
|
|
if [ "$wait_supported" = 0 ] && [ "$rc" -eq 4 ] 2>/dev/null &&
|
|
z2_fw_diagnostic_is_lock_busy "$detail" &&
|
|
[ "$attempts" -lt "$Z2_FW_LOCK_WAIT_SECONDS" ] 2>/dev/null; then
|
|
attempts=$((attempts + 1))
|
|
if ! z2_fw_lock_retry_pause; then
|
|
Z2_FW_LAST_FAILURE_CLASS=STATE_UNAVAILABLE
|
|
Z2_FW_LAST_RESTORE_DETAIL="xtables lock wait could not be scheduled"
|
|
rc=1
|
|
break
|
|
fi
|
|
continue
|
|
fi
|
|
break
|
|
done
|
|
|
|
Z2_FW_LAST_RESTORE_EXIT="$rc"
|
|
if [ -z "$Z2_FW_LAST_RESTORE_DETAIL" ]; then
|
|
Z2_FW_LAST_RESTORE_DETAIL="$(z2_fw_read_restore_diagnostic "$capture")"
|
|
fi
|
|
rm -f "$capture" 2>/dev/null || cleanup_rc=1
|
|
if [ "$cleanup_rc" -ne 0 ]; then
|
|
Z2_FW_LAST_FAILURE_CLASS=STATE_UNAVAILABLE
|
|
Z2_FW_LAST_RESTORE_DETAIL="cannot remove private firewall diagnostic capture"
|
|
return 1
|
|
fi
|
|
[ "$rc" -eq 0 ] 2>/dev/null && return 0
|
|
if [ -z "$Z2_FW_LAST_FAILURE_CLASS" ]; then
|
|
if z2_fw_diagnostic_is_lock_busy "$Z2_FW_LAST_RESTORE_DETAIL"; then
|
|
Z2_FW_LAST_FAILURE_CLASS=LOCK_TIMEOUT
|
|
elif [ "$phase" = test ]; then
|
|
Z2_FW_LAST_FAILURE_CLASS=RULESET_REJECTED
|
|
else
|
|
Z2_FW_LAST_FAILURE_CLASS=PUBLICATION_FAILED
|
|
fi
|
|
fi
|
|
return "$rc"
|
|
}
|
|
|
|
z2_fw_set_restore_failure() {
|
|
local restore="$1" phase="$2" connbytes="$3" detail missing cause=""
|
|
detail="${Z2_FW_LAST_RESTORE_DETAIL:-no backend diagnostic}"
|
|
Z2_FW_FAILURE_CLASS="${Z2_FW_LAST_FAILURE_CLASS:-PUBLICATION_FAILED}"
|
|
if missing="$(z2_fw_missing_extension "$detail")"; then
|
|
cause="this kernel does not provide the $missing extension; "
|
|
fi
|
|
Z2_FW_ERROR_DETAIL="$restore $phase failed (connbytes=$connbytes, exit=$Z2_FW_LAST_RESTORE_EXIT): $cause$detail"
|
|
Z2_FW_ERROR_DETAIL="$(z2_fw_normalize_diagnostic "$Z2_FW_ERROR_DETAIL")"
|
|
}
|
|
|
|
z2_fw_tool_available() {
|
|
command -v "$1" >/dev/null 2>&1 &&
|
|
"$1" -t mangle -L OUTPUT -n >/dev/null 2>&1
|
|
}
|
|
|
|
z2_fw_capture_baseline() {
|
|
local tool="$1" listing plan
|
|
Z2_FW_BASELINE_READY=0
|
|
Z2_FW_BASELINE_OUT_CHAIN=0
|
|
Z2_FW_BASELINE_IN_CHAIN=0
|
|
Z2_FW_BASELINE_OUT_ANCHORS=0
|
|
Z2_FW_BASELINE_IN_ANCHORS=0
|
|
listing="$("$tool" -t mangle -S 2>/dev/null)" || return 1
|
|
plan="$(printf '%s\n' "$listing" |
|
|
awk -v out="$Z2_FW_OUT_CHAIN" -v inchain="$Z2_FW_IN_CHAIN" '
|
|
$1 == "-N" && $2 == out { out_chain++ }
|
|
$1 == "-N" && $2 == inchain { in_chain++ }
|
|
$1 == "-A" {
|
|
for (i = 3; i <= NF; i++) {
|
|
if ($i != "-j" && $i != "--jump" &&
|
|
$i != "-g" && $i != "--goto") continue
|
|
target = $(i + 1)
|
|
if (target == out) {
|
|
if ($0 == "-A OUTPUT -j " out) out_anchor++
|
|
else bad = 1
|
|
}
|
|
if (target == inchain) {
|
|
if ($0 == "-A INPUT -j " inchain) in_anchor++
|
|
else bad = 1
|
|
}
|
|
}
|
|
}
|
|
END {
|
|
if (bad || out_chain > 1 || in_chain > 1 ||
|
|
out_anchor > 8 || in_anchor > 8 ||
|
|
(out_anchor && !out_chain) || (in_anchor && !in_chain))
|
|
exit 1
|
|
printf "%d %d %d %d\n",
|
|
out_chain, in_chain, out_anchor, in_anchor
|
|
}
|
|
')" || return 1
|
|
# The awk producer emits exactly four decimal fields.
|
|
# shellcheck disable=SC2086
|
|
set -- $plan
|
|
[ "$#" = 4 ] || return 1
|
|
Z2_FW_BASELINE_OUT_CHAIN="$1"
|
|
Z2_FW_BASELINE_IN_CHAIN="$2"
|
|
Z2_FW_BASELINE_OUT_ANCHORS="$3"
|
|
Z2_FW_BASELINE_IN_ANCHORS="$4"
|
|
Z2_FW_BASELINE_READY=1
|
|
return 0
|
|
}
|
|
|
|
z2_fw_cleanup_is_unambiguous() {
|
|
z2_fw_capture_baseline "$1"
|
|
}
|
|
|
|
z2_fw_save_audit() {
|
|
local tool="$1" plan
|
|
[ "$Z2_FW_BASELINE_READY" = 1 ] || return 1
|
|
plan="$Z2_FW_BASELINE_OUT_CHAIN $Z2_FW_BASELINE_IN_CHAIN $Z2_FW_BASELINE_OUT_ANCHORS $Z2_FW_BASELINE_IN_ANCHORS"
|
|
case "$tool" in
|
|
iptables) Z2_FW_AUDIT_IPTABLES="$plan" ;;
|
|
ip6tables) Z2_FW_AUDIT_IP6TABLES="$plan" ;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
|
|
z2_fw_load_audit() {
|
|
local tool="$1" plan
|
|
case "$tool" in
|
|
iptables) plan="$Z2_FW_AUDIT_IPTABLES" ;;
|
|
ip6tables) plan="$Z2_FW_AUDIT_IP6TABLES" ;;
|
|
*) return 1 ;;
|
|
esac
|
|
# Saved audit plans contain exactly four decimal fields.
|
|
# shellcheck disable=SC2086
|
|
set -- $plan
|
|
[ "$#" = 4 ] || return 1
|
|
Z2_FW_BASELINE_OUT_CHAIN="$1"
|
|
Z2_FW_BASELINE_IN_CHAIN="$2"
|
|
Z2_FW_BASELINE_OUT_ANCHORS="$3"
|
|
Z2_FW_BASELINE_IN_ANCHORS="$4"
|
|
Z2_FW_BASELINE_READY=1
|
|
}
|
|
|
|
z2_fw_family_absent() {
|
|
local tool="$1"
|
|
command -v "$tool" >/dev/null 2>&1 || return 2
|
|
z2_fw_capture_baseline "$tool" || return 2
|
|
[ "$Z2_FW_BASELINE_OUT_CHAIN:$Z2_FW_BASELINE_IN_CHAIN:$Z2_FW_BASELINE_OUT_ANCHORS:$Z2_FW_BASELINE_IN_ANCHORS" = 0:0:0:0 ]
|
|
}
|
|
|
|
z2_fw_emit_baseline_cleanup() {
|
|
local n
|
|
[ "$Z2_FW_BASELINE_READY" = 1 ] || return 1
|
|
n=0
|
|
while [ "$n" -lt "$Z2_FW_BASELINE_OUT_ANCHORS" ]; do
|
|
printf '%s\n' "-D OUTPUT -j $Z2_FW_OUT_CHAIN"
|
|
n=$((n + 1))
|
|
done
|
|
n=0
|
|
while [ "$n" -lt "$Z2_FW_BASELINE_IN_ANCHORS" ]; do
|
|
printf '%s\n' "-D INPUT -j $Z2_FW_IN_CHAIN"
|
|
n=$((n + 1))
|
|
done
|
|
if [ "$Z2_FW_BASELINE_IN_CHAIN" = 1 ]; then
|
|
printf '%s\n' "-F $Z2_FW_IN_CHAIN" "-X $Z2_FW_IN_CHAIN"
|
|
fi
|
|
if [ "$Z2_FW_BASELINE_OUT_CHAIN" = 1 ]; then
|
|
printf '%s\n' "-F $Z2_FW_OUT_CHAIN" "-X $Z2_FW_OUT_CHAIN"
|
|
fi
|
|
}
|
|
|
|
# multiport accepts at most 15 values and a range spends two of them. That is a
|
|
# userspace parser limit rather than a kernel capability, so it is computed
|
|
# before the batch is written instead of being discovered from a rejection.
|
|
Z2_FW_MULTIPORT_MAX_VALUES=15
|
|
|
|
z2_fw_port_intervals() {
|
|
local rest="$1" token
|
|
while [ -n "$rest" ]; do
|
|
case "$rest" in
|
|
*,*) token="${rest%%,*}"; rest="${rest#*,}" ;;
|
|
*) token="$rest"; rest="" ;;
|
|
esac
|
|
[ -z "$token" ] || printf '%s\n' "$token"
|
|
done
|
|
}
|
|
|
|
z2_fw_port_list_weight() {
|
|
local list="$1" weight=0 token
|
|
for token in $(z2_fw_port_intervals "$list"); do
|
|
case "$token" in
|
|
*:*) weight=$((weight + 2)) ;;
|
|
*) weight=$((weight + 1)) ;;
|
|
esac
|
|
done
|
|
printf '%s\n' "$weight"
|
|
}
|
|
|
|
z2_fw_port_interval_count() {
|
|
local count=0 token
|
|
for token in $(z2_fw_port_intervals "$1"); do
|
|
count=$((count + 1))
|
|
done
|
|
printf '%s\n' "$count"
|
|
}
|
|
|
|
# Both families are authored from the same port lists, so one list over the
|
|
# limit disqualifies multiport for the whole ruleset rather than for one rule.
|
|
z2_fw_multiport_fits() {
|
|
local tcp udp
|
|
tcp="$(z2_fw_port_list_weight "$PORTS_TCP")" || return 1
|
|
udp="$(z2_fw_port_list_weight "$PORTS_UDP")" || return 1
|
|
[ "$tcp" -le "$Z2_FW_MULTIPORT_MAX_VALUES" ] &&
|
|
[ "$udp" -le "$Z2_FW_MULTIPORT_MAX_VALUES" ]
|
|
}
|
|
|
|
z2_fw_emit_rule_tail() {
|
|
local packet_count="$1" cb_dir="$2" connbytes="$3"
|
|
if [ "$connbytes" = 1 ]; then
|
|
printf '%s' " -m connbytes --connbytes 1:$packet_count --connbytes-dir $cb_dir --connbytes-mode packets"
|
|
fi
|
|
printf '%s\n' " -m mark ! --mark $DESYNC_MARK/$DESYNC_MARK -j NFQUEUE --queue-num $QNUM --queue-bypass"
|
|
}
|
|
|
|
z2_fw_emit_batch_rule() {
|
|
local chain="$1" proto="$2" direction="$3" ports="$4"
|
|
local packet_count="$5" cb_dir="$6" connbytes="$7" multiport="${8:-1}"
|
|
local token
|
|
[ -n "$ports" ] || return 0
|
|
if [ "$multiport" = 1 ]; then
|
|
printf '%s' "-A $chain -p $proto -m multiport"
|
|
if [ "$direction" = out ]; then
|
|
printf '%s' " --dports $ports"
|
|
else
|
|
printf '%s' " --sports $ports"
|
|
fi
|
|
z2_fw_emit_rule_tail "$packet_count" "$cb_dir" "$connbytes"
|
|
return 0
|
|
fi
|
|
# A port list has no single-rule form without xt_multiport, so each
|
|
# interval becomes its own rule. The protocol match provides --dport and
|
|
# --sport natively, and both accept one port or one range, so this form
|
|
# needs no extension beyond the one -p already loaded.
|
|
for token in $(z2_fw_port_intervals "$ports"); do
|
|
printf '%s' "-A $chain -p $proto"
|
|
if [ "$direction" = out ]; then
|
|
printf '%s' " --dport $token"
|
|
else
|
|
printf '%s' " --sport $token"
|
|
fi
|
|
z2_fw_emit_rule_tail "$packet_count" "$cb_dir" "$connbytes"
|
|
done
|
|
}
|
|
|
|
z2_fw_write_batch() {
|
|
local path="$1" connbytes="$2" multiport="${3:-1}"
|
|
{
|
|
printf '%s\n' '*mangle'
|
|
z2_fw_emit_baseline_cleanup
|
|
printf ':%s - [0:0]\n' "$Z2_FW_OUT_CHAIN"
|
|
[ "$connbytes" != 1 ] || printf ':%s - [0:0]\n' "$Z2_FW_IN_CHAIN"
|
|
z2_fw_emit_batch_rule "$Z2_FW_OUT_CHAIN" tcp out "$PORTS_TCP" "$TCP_PKT_OUT" original "$connbytes" "$multiport"
|
|
z2_fw_emit_batch_rule "$Z2_FW_OUT_CHAIN" udp out "$PORTS_UDP" "$UDP_PKT_OUT" original "$connbytes" "$multiport"
|
|
if [ "$connbytes" = 1 ]; then
|
|
z2_fw_emit_batch_rule "$Z2_FW_IN_CHAIN" tcp in "$PORTS_TCP" "$TCP_PKT_IN" reply 1 "$multiport"
|
|
z2_fw_emit_batch_rule "$Z2_FW_IN_CHAIN" udp in "$PORTS_UDP" "$UDP_PKT_IN" reply 1 "$multiport"
|
|
fi
|
|
printf '%s\n' "-A OUTPUT -j $Z2_FW_OUT_CHAIN"
|
|
[ "$connbytes" != 1 ] || printf '%s\n' "-A INPUT -j $Z2_FW_IN_CHAIN"
|
|
printf '%s\n' COMMIT
|
|
} > "$path"
|
|
}
|
|
|
|
z2_fw_write_cleanup_batch() {
|
|
local path="$1"
|
|
{
|
|
printf '%s\n' '*mangle'
|
|
z2_fw_emit_baseline_cleanup
|
|
printf '%s\n' COMMIT
|
|
} > "$path"
|
|
}
|
|
|
|
z2_fw_apply_restore() {
|
|
local tool="$1" connbytes="$2" multiport="${3:-1}" restore batch
|
|
Z2_FW_FAILURE_CLASS=""
|
|
Z2_FW_ERROR_DETAIL=""
|
|
restore="$(z2_fw_restore_command "$tool")" || return 2
|
|
command -v "$restore" >/dev/null 2>&1 || return 3
|
|
batch="$STATE_DIR/tmp/firewall-batch.${tool}.$$"
|
|
z2_fw_ensure_scratch_dir || {
|
|
Z2_FW_FAILURE_CLASS=STATE_UNAVAILABLE
|
|
Z2_FW_ERROR_DETAIL="unavailable firewall scratch directory"
|
|
return 1
|
|
}
|
|
state_path_is_managed_file "$batch" || {
|
|
Z2_FW_FAILURE_CLASS=STATE_UNAVAILABLE
|
|
Z2_FW_ERROR_DETAIL="unsafe firewall batch path"
|
|
return 1
|
|
}
|
|
z2_fw_claim_scratch_path "$batch" || {
|
|
Z2_FW_FAILURE_CLASS=STATE_UNAVAILABLE
|
|
Z2_FW_ERROR_DETAIL="firewall batch path already exists"
|
|
return 1
|
|
}
|
|
umask 077
|
|
z2_fw_write_batch "$batch" "$connbytes" "$multiport" || {
|
|
rm -f "$batch" 2>/dev/null
|
|
Z2_FW_FAILURE_CLASS=STATE_UNAVAILABLE
|
|
Z2_FW_ERROR_DETAIL="cannot create firewall batch"
|
|
return 1
|
|
}
|
|
chmod 0600 "$batch" 2>/dev/null || {
|
|
rm -f "$batch" 2>/dev/null
|
|
Z2_FW_FAILURE_CLASS=STATE_UNAVAILABLE
|
|
Z2_FW_ERROR_DETAIL="cannot secure firewall batch"
|
|
return 1
|
|
}
|
|
if z2_fw_run_restore "$restore" "$tool" test "$batch"; then
|
|
:
|
|
else
|
|
z2_fw_set_restore_failure "$restore" test "$connbytes"
|
|
rm -f "$batch" 2>/dev/null || {
|
|
Z2_FW_FAILURE_CLASS=STATE_UNAVAILABLE
|
|
Z2_FW_ERROR_DETAIL="cannot remove rejected firewall batch"
|
|
return 1
|
|
}
|
|
[ "$Z2_FW_FAILURE_CLASS" = RULESET_REJECTED ] && return 4
|
|
return 1
|
|
fi
|
|
if z2_fw_run_restore "$restore" "$tool" commit "$batch"; then
|
|
:
|
|
else
|
|
z2_fw_set_restore_failure "$restore" commit "$connbytes"
|
|
rm -f "$batch" 2>/dev/null || {
|
|
Z2_FW_FAILURE_CLASS=STATE_UNAVAILABLE
|
|
Z2_FW_ERROR_DETAIL="cannot remove failed firewall batch"
|
|
return 1
|
|
}
|
|
return 1
|
|
fi
|
|
rm -f "$batch" 2>/dev/null || {
|
|
Z2_FW_FAILURE_CLASS=STATE_UNAVAILABLE
|
|
Z2_FW_ERROR_DETAIL="cannot remove committed firewall batch"
|
|
return 1
|
|
}
|
|
Z2_FW_BACKEND=restore
|
|
return 0
|
|
}
|
|
|
|
z2_fw_expected_rule_count() {
|
|
local connbytes="$1" multiport="${2:-1}" per_direction=0 tcp udp
|
|
if [ "$multiport" = 1 ]; then
|
|
[ -z "$PORTS_TCP" ] || per_direction=$((per_direction + 1))
|
|
[ -z "$PORTS_UDP" ] || per_direction=$((per_direction + 1))
|
|
else
|
|
# One rule per interval, so the published count is the interval count.
|
|
tcp="$(z2_fw_port_interval_count "$PORTS_TCP")" || return 1
|
|
udp="$(z2_fw_port_interval_count "$PORTS_UDP")" || return 1
|
|
per_direction=$((tcp + udp))
|
|
fi
|
|
printf '%s\n' $((per_direction * (1 + connbytes)))
|
|
}
|
|
|
|
z2_fw_verify_family() {
|
|
local tool="$1" connbytes="$2" multiport="${3:-1}" listing verification
|
|
local out_tcp out_udp in_tcp in_udp
|
|
Z2_FW_VERIFY_DETAIL=""
|
|
listing="$("$tool" -t mangle -S 2>/dev/null)" || {
|
|
Z2_FW_VERIFY_DETAIL="$tool mangle snapshot command failed"
|
|
return 1
|
|
}
|
|
out_tcp="$(z2_fw_emit_batch_rule "$Z2_FW_OUT_CHAIN" tcp out "$PORTS_TCP" "$TCP_PKT_OUT" original "$connbytes" "$multiport")" || return 1
|
|
out_udp="$(z2_fw_emit_batch_rule "$Z2_FW_OUT_CHAIN" udp out "$PORTS_UDP" "$UDP_PKT_OUT" original "$connbytes" "$multiport")" || return 1
|
|
in_tcp=""
|
|
in_udp=""
|
|
if [ "$connbytes" = 1 ]; then
|
|
in_tcp="$(z2_fw_emit_batch_rule "$Z2_FW_IN_CHAIN" tcp in "$PORTS_TCP" "$TCP_PKT_IN" reply 1 "$multiport")" || return 1
|
|
in_udp="$(z2_fw_emit_batch_rule "$Z2_FW_IN_CHAIN" udp in "$PORTS_UDP" "$UDP_PKT_IN" reply 1 "$multiport")" || return 1
|
|
fi
|
|
# The backend re-renders published rules in its own save format: match
|
|
# option order, --dports vs --dport for a single port, and mark mask
|
|
# elision all differ between iptables builds. Verification therefore
|
|
# compares canonical rule signatures built from the closed module
|
|
# vocabulary, never the authored batch text. Anything outside that
|
|
# vocabulary inside the owned namespace is a foreign rule.
|
|
verification="$(printf '%s\n' "$listing" | awk \
|
|
-v out="$Z2_FW_OUT_CHAIN" -v inchain="$Z2_FW_IN_CHAIN" \
|
|
-v connbytes="$connbytes" \
|
|
-v out_tcp="$out_tcp" -v out_udp="$out_udp" \
|
|
-v in_tcp="$in_tcp" -v in_udp="$in_udp" '
|
|
# Without multiport a port list becomes one rule per interval, so a
|
|
# single expected signature per chain and protocol is no longer the
|
|
# shape to compare against. Expectations are loaded as a multiset of
|
|
# canonical signatures: every published rule must match one, and every
|
|
# expected one must appear exactly as often as it was authored.
|
|
function load_expected(rules, chainkey, n, lines, i, sig) {
|
|
if (rules == "") return
|
|
n = split(rules, lines, "\n")
|
|
for (i = 1; i <= n; i++) {
|
|
if (lines[i] == "") continue
|
|
sig = canon(lines[i])
|
|
if (sig == "") { expected_bad = 1; return }
|
|
exp_count[sig]++
|
|
exp_total[chainkey]++
|
|
}
|
|
}
|
|
function expectations_met(chainkey, sig) {
|
|
for (sig in exp_count)
|
|
if (index(sig, "-A " chainkey " ") == 1 &&
|
|
seen_count[sig] != exp_count[sig]) return 0
|
|
return 1
|
|
}
|
|
function canon(line, n, t, i, tok, val, chain, proto, portskey,
|
|
ports, cbrange, cbdir, cbmode, markval, markinv,
|
|
target, qnum, bypass, invert, sig) {
|
|
n = split(line, t, " ")
|
|
if (n < 4 || t[1] != "-A") return ""
|
|
chain = t[2]
|
|
invert = 0
|
|
for (i = 3; i <= n; i++) {
|
|
tok = t[i]
|
|
if (tok == "!") {
|
|
if (invert) return ""
|
|
invert = 1
|
|
continue
|
|
}
|
|
if (tok == "-m" || tok == "--match") {
|
|
i++
|
|
if (invert || i > n) return ""
|
|
if (t[i] != "multiport" && t[i] != "connbytes" &&
|
|
t[i] != "mark" && t[i] != "tcp" && t[i] != "udp")
|
|
return ""
|
|
continue
|
|
}
|
|
if (tok == "--queue-bypass") {
|
|
if (invert) return ""
|
|
bypass = 1
|
|
continue
|
|
}
|
|
i++
|
|
if (i > n) return ""
|
|
val = t[i]
|
|
if (tok == "-p" || tok == "--protocol") {
|
|
if (invert || proto != "") return ""
|
|
proto = val
|
|
} else if (tok == "--dports" || tok == "--dport") {
|
|
if (invert || ports != "") return ""
|
|
portskey = "d"; ports = val
|
|
} else if (tok == "--sports" || tok == "--sport") {
|
|
if (invert || ports != "") return ""
|
|
portskey = "s"; ports = val
|
|
} else if (tok == "--connbytes") {
|
|
if (invert || cbrange != "") return ""
|
|
cbrange = val
|
|
} else if (tok == "--connbytes-dir") {
|
|
if (invert || cbdir != "") return ""
|
|
cbdir = val
|
|
} else if (tok == "--connbytes-mode") {
|
|
if (invert || cbmode != "") return ""
|
|
cbmode = val
|
|
} else if (tok == "--mark") {
|
|
if (markval != "") return ""
|
|
markinv = invert
|
|
invert = 0
|
|
if (!index(val, "/")) val = val "/0xffffffff"
|
|
markval = val
|
|
} else if (tok == "-j" || tok == "--jump") {
|
|
if (invert || target != "") return ""
|
|
target = val
|
|
} else if (tok == "--queue-num") {
|
|
if (invert || qnum != "") return ""
|
|
qnum = val
|
|
} else {
|
|
return ""
|
|
}
|
|
}
|
|
if (invert) return ""
|
|
if ((cbrange != "" || cbdir != "" || cbmode != "") &&
|
|
(cbrange == "" || cbdir == "" || cbmode == "")) return ""
|
|
sig = "-A " chain " p=" proto " " portskey "ports=" ports
|
|
if (cbrange != "") sig = sig " cb=" cbrange "/" cbmode "/" cbdir
|
|
if (markval != "") sig = sig " mark=" (markinv ? "!" : "") markval
|
|
sig = sig " j=" target
|
|
if (qnum != "") sig = sig " qnum=" qnum
|
|
if (bypass) sig = sig " bypass"
|
|
return sig
|
|
}
|
|
BEGIN {
|
|
load_expected(out_tcp, out)
|
|
load_expected(out_udp, out)
|
|
load_expected(in_tcp, inchain)
|
|
load_expected(in_udp, inchain)
|
|
if (expected_bad) {
|
|
print "EXPECTED_RULE_UNPARSEABLE"
|
|
bail = 1
|
|
exit 1
|
|
}
|
|
}
|
|
$1 == "-N" && $2 == out { out_chain++ }
|
|
$1 == "-N" && $2 == inchain { in_chain++ }
|
|
$1 == "-A" && $2 == out {
|
|
out_rules++
|
|
sig = canon($0)
|
|
if (sig != "" && (sig in exp_count)) seen_count[sig]++
|
|
else bad=1
|
|
}
|
|
$1 == "-A" && $2 == inchain {
|
|
in_rules++
|
|
sig = canon($0)
|
|
if (sig != "" && (sig in exp_count)) seen_count[sig]++
|
|
else bad=1
|
|
}
|
|
$1 == "-A" {
|
|
for (i=3; i<=NF; i++) {
|
|
if ($i != "-j" && $i != "--jump" &&
|
|
$i != "-g" && $i != "--goto") continue
|
|
target=$(i+1)
|
|
if (target == out) {
|
|
if ($0 == "-A OUTPUT -j " out) out_anchor++
|
|
else bad=1
|
|
} else if (target == inchain) {
|
|
if ($0 == "-A INPUT -j " inchain) in_anchor++
|
|
else bad=1
|
|
}
|
|
}
|
|
}
|
|
END {
|
|
if (bail) exit 1
|
|
expected_out=exp_total[out]
|
|
expected_in=exp_total[inchain]
|
|
if (bad) reason="FOREIGN_OR_UNEXPECTED_RULE"
|
|
else if (out_chain != 1) reason="OUT_CHAIN_COUNT:" out_chain
|
|
else if (out_anchor != 1) reason="OUT_ANCHOR_COUNT:" out_anchor
|
|
else if (out_rules != expected_out) reason="OUT_RULE_COUNT:" out_rules
|
|
else if (!expectations_met(out)) reason="OUT_RULE_MISMATCH"
|
|
if (connbytes == 1) {
|
|
if (reason == "" && in_chain != 1) reason="INPUT_CHAIN_COUNT:" in_chain
|
|
else if (reason == "" && in_anchor != 1) reason="INPUT_ANCHOR_COUNT:" in_anchor
|
|
else if (reason == "" && in_rules != expected_in) reason="INPUT_RULE_COUNT:" in_rules
|
|
else if (reason == "" && !expectations_met(inchain))
|
|
reason="INPUT_RULE_MISMATCH"
|
|
} else {
|
|
if (reason == "" &&
|
|
(in_chain != 0 || in_anchor != 0 || in_rules != 0))
|
|
reason="UNEXPECTED_INPUT_TOPOLOGY"
|
|
}
|
|
if (reason != "") {
|
|
print reason
|
|
exit 1
|
|
}
|
|
}')" || {
|
|
[ -n "$verification" ] || verification=UNKNOWN_TOPOLOGY_MISMATCH
|
|
Z2_FW_VERIFY_DETAIL="$tool post-publication topology mismatch (connbytes=$connbytes, reason=$verification)"
|
|
return 1
|
|
}
|
|
Z2_FW_CONNBYTES="$connbytes"
|
|
Z2_FW_RULES="$(z2_fw_expected_rule_count "$connbytes" "$multiport")" || return 1
|
|
Z2_FW_CHAINS=$((1 + connbytes))
|
|
Z2_FW_ANCHORS=$((1 + connbytes))
|
|
return 0
|
|
}
|
|
|
|
z2_fw_apply_cleanup() {
|
|
local tool="$1" restore batch phase rc detail
|
|
[ "$Z2_FW_BASELINE_READY" = 1 ] || return 1
|
|
if [ "$Z2_FW_BASELINE_OUT_CHAIN:$Z2_FW_BASELINE_IN_CHAIN:$Z2_FW_BASELINE_OUT_ANCHORS:$Z2_FW_BASELINE_IN_ANCHORS" = 0:0:0:0 ]; then
|
|
return 0
|
|
fi
|
|
restore="$(z2_fw_restore_command "$tool")" || return 2
|
|
command -v "$restore" >/dev/null 2>&1 || return 3
|
|
batch="$STATE_DIR/tmp/firewall-cleanup.${tool}.$$"
|
|
z2_fw_ensure_scratch_dir || return 1
|
|
state_path_is_managed_file "$batch" || return 1
|
|
z2_fw_claim_scratch_path "$batch" || return 1
|
|
umask 077
|
|
if ! z2_fw_write_cleanup_batch "$batch" ||
|
|
! chmod 0600 "$batch" 2>/dev/null; then
|
|
rm -f "$batch" 2>/dev/null
|
|
return 1
|
|
fi
|
|
for phase in test commit; do
|
|
if z2_fw_run_restore "$restore" "$tool" "$phase" "$batch"; then
|
|
:
|
|
else
|
|
rc=$?
|
|
detail="${Z2_FW_LAST_RESTORE_DETAIL:-no backend diagnostic}"
|
|
Z2_FW_FAILURE_CLASS="${Z2_FW_LAST_FAILURE_CLASS:-CLEANUP_FAILED}"
|
|
Z2_FW_ERROR_DETAIL="$restore atomic cleanup $phase failed (exit=$Z2_FW_LAST_RESTORE_EXIT): $detail"
|
|
Z2_FW_ERROR_DETAIL="$(z2_fw_normalize_diagnostic "$Z2_FW_ERROR_DETAIL")"
|
|
rm -f "$batch" 2>/dev/null || true
|
|
return "$rc"
|
|
fi
|
|
done
|
|
rm -f "$batch" 2>/dev/null || return 1
|
|
if z2_fw_family_absent "$tool"; then
|
|
return 0
|
|
fi
|
|
Z2_FW_FAILURE_CLASS=POSTCONDITION_FAILED
|
|
Z2_FW_ERROR_DETAIL="$tool atomic cleanup postcondition failed"
|
|
return 1
|
|
}
|
|
|
|
z2_fw_cleanup_family() {
|
|
local tool="$1" baseline_mode="${2:-owned}"
|
|
case "$baseline_mode" in
|
|
owned)
|
|
command -v "$tool" >/dev/null 2>&1 || return 2
|
|
z2_fw_capture_baseline "$tool" || return 1
|
|
;;
|
|
audited)
|
|
command -v "$tool" >/dev/null 2>&1 || return 2
|
|
z2_fw_load_audit "$tool" || return 1
|
|
;;
|
|
*) return 2 ;;
|
|
esac
|
|
z2_fw_restore_available "$tool" || return 3
|
|
z2_fw_apply_cleanup "$tool"
|
|
}
|
|
|
|
z2_fw_reconcile_family() {
|
|
local tool="$1" baseline_mode="${2:-owned}" apply_rc candidate_detail verify_detail
|
|
local connbytes multiport
|
|
case "$baseline_mode" in owned|audited) ;; *) return 2 ;; esac
|
|
Z2_FW_BACKEND=""; Z2_FW_CONNBYTES=0; Z2_FW_MULTIPORT=1
|
|
Z2_FW_RULES=0; Z2_FW_CHAINS=0; Z2_FW_ANCHORS=0
|
|
Z2_FW_FAILURE_CLASS=""; Z2_FW_ERROR_DETAIL=""; Z2_FW_FALLBACK_DETAIL=""
|
|
command -v "$tool" >/dev/null 2>&1 || {
|
|
Z2_FW_FAILURE_CLASS=BACKEND_UNAVAILABLE
|
|
Z2_FW_ERROR_DETAIL="$tool command is unavailable"
|
|
return 2
|
|
}
|
|
z2_fw_restore_available "$tool" || {
|
|
Z2_FW_FAILURE_CLASS=BACKEND_UNAVAILABLE
|
|
Z2_FW_ERROR_DETAIL="$tool restore backend is unavailable"
|
|
return 3
|
|
}
|
|
if [ "$baseline_mode" = audited ]; then
|
|
z2_fw_load_audit "$tool" || {
|
|
Z2_FW_FAILURE_CLASS=CLEANUP_FAILED
|
|
Z2_FW_ERROR_DETAIL="$tool authenticated transition baseline is unavailable"
|
|
return 1
|
|
}
|
|
else
|
|
z2_fw_capture_baseline "$tool" || {
|
|
Z2_FW_FAILURE_CLASS=CLEANUP_FAILED
|
|
Z2_FW_ERROR_DETAIL="$tool stable namespace transition preflight failed"
|
|
return 1
|
|
}
|
|
fi
|
|
# Two optional capabilities, each with its own latch. A rejection may only
|
|
# retire the capability its own diagnostic names, and each is retired at
|
|
# most once, so at most two downgrades happen and neither can be undone by
|
|
# a later failure. Anything the backend rejects for a reason it does not
|
|
# name is a publication error: silently rebuilding a different topology
|
|
# would hide a broken configuration instead of a missing kernel module.
|
|
#
|
|
# The two fail at different phases. iptables-restore --test parses in
|
|
# userspace but asks the kernel for match revisions while doing so, which
|
|
# is where a missing xt_multiport surfaces; connbytes passes the test phase
|
|
# and is rejected only at COMMIT. Legacy restore submits the whole table in
|
|
# one atomic replace, so a rejected COMMIT leaves the pre-transaction
|
|
# state, and post-publication verification still gates every result.
|
|
connbytes=1
|
|
multiport=1
|
|
# The 15-value limit is a parser rule, not a capability, so it is settled
|
|
# before the first attempt rather than learned from a rejection.
|
|
z2_fw_multiport_fits || {
|
|
multiport=0
|
|
Z2_FW_FALLBACK_DETAIL="port list exceeds the $Z2_FW_MULTIPORT_MAX_VALUES values multiport accepts"
|
|
}
|
|
while :; do
|
|
if z2_fw_apply_restore "$tool" "$connbytes" "$multiport"; then
|
|
apply_rc=0
|
|
else
|
|
apply_rc=$?
|
|
fi
|
|
if [ "$apply_rc" = 0 ]; then
|
|
if ! z2_fw_verify_family "$tool" "$connbytes" "$multiport"; then
|
|
verify_detail="$Z2_FW_VERIFY_DETAIL"
|
|
z2_fw_cleanup_family "$tool" >/dev/null 2>&1 || true
|
|
Z2_FW_FAILURE_CLASS=POSTCONDITION_FAILED
|
|
Z2_FW_ERROR_DETAIL="$verify_detail"
|
|
return 1
|
|
fi
|
|
Z2_FW_CONNBYTES="$connbytes"
|
|
Z2_FW_MULTIPORT="$multiport"
|
|
Z2_FW_FAILURE_CLASS=""; Z2_FW_ERROR_DETAIL=""
|
|
return 0
|
|
fi
|
|
candidate_detail="$Z2_FW_ERROR_DETAIL"
|
|
# A candidate rejection at test is a capability signal by itself; any
|
|
# other failure has to be a publication failure to be one at all.
|
|
if [ "$apply_rc" != 4 ] && [ "$Z2_FW_FAILURE_CLASS" != PUBLICATION_FAILED ]; then
|
|
return 1
|
|
fi
|
|
# Match the raw backend stderr: the wrapped detail carries markers of
|
|
# its own. The named capability wins, so a multiport rejection never
|
|
# spends the connbytes latch on its way down.
|
|
if [ "$connbytes" = 1 ] &&
|
|
z2_fw_diagnostic_is_connbytes_unsupported "$Z2_FW_LAST_RESTORE_DETAIL"; then
|
|
connbytes=0
|
|
elif [ "$multiport" = 1 ] &&
|
|
z2_fw_diagnostic_is_multiport_unsupported "$Z2_FW_LAST_RESTORE_DETAIL"; then
|
|
multiport=0
|
|
elif [ "$apply_rc" = 4 ] && [ "$connbytes" = 1 ]; then
|
|
# An unnamed test rejection retires the richer topology first,
|
|
# which is the only one whose absence a kernel can survive.
|
|
connbytes=0
|
|
else
|
|
if [ -z "$Z2_FW_ERROR_DETAIL" ]; then
|
|
Z2_FW_FAILURE_CLASS=POSTCONDITION_FAILED
|
|
Z2_FW_ERROR_DETAIL="$tool post-publication verification failed (connbytes=$connbytes, multiport=$multiport)"
|
|
fi
|
|
return 1
|
|
fi
|
|
Z2_FW_FALLBACK_DETAIL="${Z2_FW_FALLBACK_DETAIL:+$Z2_FW_FALLBACK_DETAIL; }$candidate_detail"
|
|
done
|
|
}
|