mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-31 21:35:13 +08:00
⛅ Sync 2026-09-01 05:29:46
This commit is contained in:
1 parent
16957188b7
commit
ded477010d
16 files changed
+1624
-1211
No files matched your search
@@ -23,5 +23,8 @@ input="$(cat)"
|
||||
[ -n "$input" ] || input='{"log":[]}'
|
||||
|
||||
printf '%s' '{"log":['
|
||||
jsonfilter -s "$input" -e '@.log[*]' 2>/dev/null | _fwlive_filter_json_entries
|
||||
# Prefer stdin over -s: Linux MAX_ARG_STRLEN is 128KiB; a raised logd ring
|
||||
# (or paused FETCH_LINES_MAX poll) can exceed that and make jsonfilter fail
|
||||
# while this script still printed {"log":[]} and exited 0 (#234).
|
||||
printf '%s' "$input" | jsonfilter -e '@.log[*]' 2>/dev/null | _fwlive_filter_json_entries
|
||||
printf '%s' ']}'
|
||||
@@ -62,18 +62,20 @@ json_escape() {
|
||||
}'
|
||||
}
|
||||
|
||||
# Production lock dir must be root-owned with no group/other write (#204).
|
||||
# Production lock dir must be owned by euid with no group/other write (#204).
|
||||
# Avoid GNU/BusyBox `stat -c` — stock OpenWrt omits FEATURE_STAT_FORMAT (#232).
|
||||
wan_log_lock_dir_safe() {
|
||||
dir="$1"
|
||||
[ -n "$dir" ] || return 1
|
||||
[ -L "$dir" ] && return 1
|
||||
[ -d "$dir" ] || return 1
|
||||
u=$(stat -c '%u' "$dir" 2>/dev/null) || return 1
|
||||
[ "$u" = "0" ] || return 1
|
||||
m=$(stat -c '%a' "$dir" 2>/dev/null) || return 1
|
||||
o=$((m % 10))
|
||||
g=$(( (m / 10) % 10 ))
|
||||
[ $((o & 2)) -eq 0 ] && [ $((g & 2)) -eq 0 ]
|
||||
# POSIX -O: true when the effective uid owns the directory (rpcd → root).
|
||||
# shellcheck disable=SC3067 # BusyBox/dash implement -O; SC3067 is overly strict
|
||||
[ -O "$dir" ] || return 1
|
||||
# Fail closed if group or other write is set. find -perm is on BusyBox;
|
||||
# -prune limits the walk to this directory only.
|
||||
_writable=$(find "$dir" -prune \( -perm -020 -o -perm -002 \) -print 2>/dev/null) || return 1
|
||||
[ -z "$_writable" ]
|
||||
}
|
||||
|
||||
# Acquire the exclusive logging lock on fd 9. Blocks until free; fails closed
|
||||
@@ -97,7 +99,12 @@ acquire_wan_log_lock() {
|
||||
chmod 0600 "$WAN_LOG_LOCK_FILE" 2>/dev/null || true
|
||||
chown 0:0 "$WAN_LOG_LOCK_FILE" 2>/dev/null || true
|
||||
[ -L "$WAN_LOG_LOCK_FILE" ] && return 1
|
||||
exec 9>"$WAN_LOG_LOCK_FILE" 2>/dev/null || return 1
|
||||
# Probe in a subshell first: a failed `exec` redirection aborts a POSIX
|
||||
# non-interactive shell outright, so `|| return 1` on the real exec would
|
||||
# never run — and `2>/dev/null` on the same exec would permanently
|
||||
# silence this process's stderr on the success path (#244).
|
||||
( exec 9>>"$WAN_LOG_LOCK_FILE" ) 2>/dev/null || return 1
|
||||
exec 9>>"$WAN_LOG_LOCK_FILE"
|
||||
flock 9 2>/dev/null || {
|
||||
exec 9>&-
|
||||
return 1
|
||||
|
||||
@@ -44,6 +44,13 @@ slug_key() {
|
||||
echo "$1" | tr '[:upper:]' '[:lower:]' | tr ' _' '--'
|
||||
}
|
||||
|
||||
# Cap rules JSON size so a huge ruleset cannot unbound an rpcd reply (#229).
|
||||
RULES_MAP_MAX_KEYS=512
|
||||
RULES_MAP_MAX_BYTES=65536
|
||||
# Same wall-clock bound as nft for iptables-save / ip6tables-save (#229).
|
||||
# Overridable for host tests (e.g. FWLIVE_IPTABLES_TIMEOUT=1).
|
||||
IPTABLES_TIMEOUT="${FWLIVE_IPTABLES_TIMEOUT:-5}"
|
||||
|
||||
map_add() {
|
||||
key="$1"
|
||||
val="$2"
|
||||
@@ -54,10 +61,21 @@ map_add() {
|
||||
# Uses anchored pattern "\"<key>\":" to avoid substring false positives
|
||||
# ("foo" vs "foobar") and value false positives ("see foo").
|
||||
# Quoting "$esc_key" makes the match literal (no glob escaping needed).
|
||||
# Callers apply UCI, then labeled (!fw4:) prefixes, then unlabeled
|
||||
# cosmetics so first-wins prefers authoritative names (#230).
|
||||
case "$OUT" in
|
||||
*"\"$esc_key\":"*) return 0 ;;
|
||||
esac
|
||||
esc_val=$(printf '%s' "$val" | json_escape)
|
||||
_pair_len=$(( ${#esc_key} + ${#esc_val} + 5 ))
|
||||
_next_len=$(( ${#OUT} + _pair_len ))
|
||||
[ -n "$OUT" ] && _next_len=$((_next_len + 1))
|
||||
if [ "${_map_keys:-0}" -ge "$RULES_MAP_MAX_KEYS" ] || \
|
||||
[ "$_next_len" -gt "$RULES_MAP_MAX_BYTES" ]; then
|
||||
_map_overflow=1
|
||||
return 0
|
||||
fi
|
||||
_map_keys=$(( ${_map_keys:-0} + 1 ))
|
||||
if [ -n "$OUT" ]; then
|
||||
OUT="${OUT},"
|
||||
fi
|
||||
@@ -114,22 +132,26 @@ run_with_timeout() {
|
||||
if command -v timeout >/dev/null 2>&1; then
|
||||
timeout "$secs" "$@" 2>/dev/null
|
||||
else
|
||||
"$@" 2>/dev/null
|
||||
# Fail closed (#229): never run unbounded when timeout is absent.
|
||||
return 127
|
||||
fi
|
||||
}
|
||||
|
||||
map_log_prefix_entry() {
|
||||
prefix="$1"
|
||||
comment="$2"
|
||||
pass="${3:-all}"
|
||||
label=''
|
||||
|
||||
if [ -n "$comment" ] && is_uci_style_name "$comment"; then
|
||||
label="$comment"
|
||||
fi
|
||||
|
||||
# Single normalization site is map_prefix_with_label; idempotent so
|
||||
# double-normalization is safe but this redundant call is removed.
|
||||
[ -n "$prefix" ] || return 0
|
||||
case "$pass" in
|
||||
labeled) [ -n "$label" ] || return 0 ;;
|
||||
unlabeled) [ -z "$label" ] || return 0 ;;
|
||||
esac
|
||||
|
||||
map_prefix_with_label "$prefix" "$label"
|
||||
}
|
||||
@@ -154,7 +176,10 @@ $(uci_rule_names)
|
||||
EOF
|
||||
}
|
||||
|
||||
# pass=labeled|unlabeled|all — labeled before unlabeled so !fw4: beats
|
||||
# an earlier cosmetic for the same prefix under first-wins (#230).
|
||||
map_from_nft_stream() {
|
||||
pass="${1:-all}"
|
||||
while IFS= read -r line; do
|
||||
# Capture [^"]* plus escaped-quote units (\"...) so a log prefix or
|
||||
# comment containing an escaped quote is not truncated at the quote.
|
||||
@@ -172,14 +197,22 @@ map_from_nft_stream() {
|
||||
esac
|
||||
|
||||
if [ -n "$prefix" ]; then
|
||||
case "$pass" in
|
||||
labeled) [ -n "$label" ] || continue ;;
|
||||
unlabeled) [ -z "$label" ] || continue ;;
|
||||
esac
|
||||
map_prefix_with_label "$prefix" "$label"
|
||||
elif [ -n "$label" ]; then
|
||||
case "$pass" in
|
||||
unlabeled) continue ;;
|
||||
esac
|
||||
map_prefix_with_label "$label" "$label"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
map_from_iptables_save_stream() {
|
||||
pass="${1:-all}"
|
||||
while IFS= read -r line; do
|
||||
case "$line" in
|
||||
-*) ;;
|
||||
@@ -188,7 +221,7 @@ map_from_iptables_save_stream() {
|
||||
prefix=$(echo "$line" | sed -n 's/.*--log-prefix "\([^"\\]*\(\\.[^"\\]*\)*\)".*/\1/p')
|
||||
comment=$(echo "$line" | sed -n 's/.*--comment "\([^"\\]*\(\\.[^"\\]*\)*\)".*/\1/p')
|
||||
[ -n "$prefix" ] || continue
|
||||
map_log_prefix_entry "$prefix" "$comment"
|
||||
map_log_prefix_entry "$prefix" "$comment" "$pass"
|
||||
done
|
||||
}
|
||||
|
||||
@@ -239,65 +272,85 @@ _fwlive_mktemp() {
|
||||
|
||||
build_rules_map() {
|
||||
OUT=''
|
||||
_map_keys=0
|
||||
_map_overflow=''
|
||||
_enrich_error=''
|
||||
backend=$(detect_firewall_backend)
|
||||
|
||||
# UCI first, then labeled stream prefixes, then unlabeled cosmetics
|
||||
# (#230). First-wins keeps UCI over nft and !fw4: over cosmetic.
|
||||
map_uci_rule_names
|
||||
|
||||
case "$backend" in
|
||||
nft)
|
||||
_tmp=$(_fwlive_mktemp fwlive-nft) || _tmp=''
|
||||
if [ -n "$_tmp" ]; then
|
||||
# nft ruleset write to /tmp (tmpfs on OpenWrt, RAM) is
|
||||
# duration-bounded by NFT_TIMEOUT (5s) for nft only;
|
||||
# iptables-save/ip6tables-save below run with no timeout.
|
||||
# Size is not bounded by code; typical firewall ruleset
|
||||
# dumps are <100KB and the file is removed immediately
|
||||
# after parsing so RAM impact is negligible in practice.
|
||||
# No pipeline subshell: accumulation stays in main shell via
|
||||
# redirect for global first-wins dedup.
|
||||
if nft_list_ruleset >"$_tmp" 2>/dev/null; then
|
||||
map_from_nft_stream <"$_tmp"
|
||||
_rules_dump=$(_fwlive_mktemp fwlive-nft) || _rules_dump=''
|
||||
if [ -n "$_rules_dump" ]; then
|
||||
# Dumps land on /tmp (tmpfs). Duration bounded by
|
||||
# NFT_TIMEOUT / IPTABLES_TIMEOUT; key/byte caps in map_add.
|
||||
if nft_list_ruleset >"$_rules_dump" 2>/dev/null; then
|
||||
map_from_nft_stream labeled <"$_rules_dump"
|
||||
map_from_nft_stream unlabeled <"$_rules_dump"
|
||||
fi
|
||||
rm -f "$_tmp"
|
||||
rm -f "$_rules_dump"
|
||||
else
|
||||
_enrich_error='mktemp_failed'
|
||||
fi
|
||||
;;
|
||||
iptables)
|
||||
_tmp=$(_fwlive_mktemp fwlive-ipt) || _tmp=''
|
||||
if [ -n "$_tmp" ]; then
|
||||
if iptables-save >"$_tmp" 2>/dev/null; then
|
||||
map_from_iptables_save_stream <"$_tmp"
|
||||
_rules_dump=$(_fwlive_mktemp fwlive-ipt) || _rules_dump=''
|
||||
if [ -n "$_rules_dump" ]; then
|
||||
if run_with_timeout "$IPTABLES_TIMEOUT" iptables-save >"$_rules_dump" 2>/dev/null; then
|
||||
map_from_iptables_save_stream labeled <"$_rules_dump"
|
||||
map_from_iptables_save_stream unlabeled <"$_rules_dump"
|
||||
fi
|
||||
rm -f "$_tmp"
|
||||
rm -f "$_rules_dump"
|
||||
else
|
||||
_enrich_error='mktemp_failed'
|
||||
fi
|
||||
if command -v ip6tables-save >/dev/null 2>&1; then
|
||||
_tmp=$(_fwlive_mktemp fwlive-ip6t) || _tmp=''
|
||||
if [ -n "$_tmp" ]; then
|
||||
if ip6tables-save >"$_tmp" 2>/dev/null; then
|
||||
map_from_iptables_save_stream <"$_tmp"
|
||||
_rules_dump=$(_fwlive_mktemp fwlive-ip6t) || _rules_dump=''
|
||||
if [ -n "$_rules_dump" ]; then
|
||||
if run_with_timeout "$IPTABLES_TIMEOUT" ip6tables-save >"$_rules_dump" 2>/dev/null; then
|
||||
map_from_iptables_save_stream labeled <"$_rules_dump"
|
||||
map_from_iptables_save_stream unlabeled <"$_rules_dump"
|
||||
fi
|
||||
rm -f "$_tmp"
|
||||
rm -f "$_rules_dump"
|
||||
elif [ -z "$_enrich_error" ]; then
|
||||
_enrich_error='mktemp_failed'
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
printf '{"backend":"%s","rules":{%s}}' "$backend" "$OUT"
|
||||
_rules_json="{\"backend\":\"$backend\",\"rules\":{$OUT}"
|
||||
if [ -n "$_enrich_error" ]; then
|
||||
_rules_json="${_rules_json},\"error\":\"${_enrich_error}\""
|
||||
elif [ -n "$_map_overflow" ]; then
|
||||
_rules_json="${_rules_json},\"error\":\"rules_truncated\""
|
||||
fi
|
||||
printf '%s}' "$_rules_json"
|
||||
}
|
||||
|
||||
rulesmap_from_iptables_file() {
|
||||
file="$1"
|
||||
OUT=''
|
||||
_map_keys=0
|
||||
_map_overflow=''
|
||||
[ "$file" = "$RULESMAP_IPTABLES_FILE" ] || return 1
|
||||
[ -f "$file" ] || return 1
|
||||
map_from_iptables_save_stream < "$file"
|
||||
printf '{"backend":"iptables","rules":{%s}}' "$OUT"
|
||||
map_from_iptables_save_stream labeled < "$file"
|
||||
map_from_iptables_save_stream unlabeled < "$file"
|
||||
_rules_json="{\"backend\":\"iptables\",\"rules\":{$OUT}"
|
||||
if [ -n "$_map_overflow" ]; then
|
||||
_rules_json="${_rules_json},\"error\":\"rules_truncated\""
|
||||
fi
|
||||
printf '%s}' "$_rules_json"
|
||||
}
|
||||
|
||||
poll_clamp_lines() {
|
||||
# Contract: non-digit input returns default 50; digit strings are
|
||||
# clamped to 1..POLL_LINES_MAX with leading zeros stripped. The
|
||||
# caller (poll_lines_from_input) already filters non-digits, but this
|
||||
# helper is now directly testable so it validates itself.
|
||||
# clamped to 1..POLL_LINES_MAX with leading zeros stripped. Validates
|
||||
# itself so __selftest can call it without jshn.
|
||||
_val="$1"
|
||||
case "$_val" in
|
||||
''|*[!0-9]*)
|
||||
@@ -305,19 +358,19 @@ poll_clamp_lines() {
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
_tmp=$(printf '%s' "$_val" | sed 's/^0*//')
|
||||
if [ -z "$_tmp" ]; then
|
||||
_digits=$(printf '%s' "$_val" | sed 's/^0*//')
|
||||
if [ -z "$_digits" ]; then
|
||||
printf '%s' "50"
|
||||
return 0
|
||||
fi
|
||||
if [ "${#_tmp}" -gt "${#POLL_LINES_MAX}" ]; then
|
||||
if [ "${#_digits}" -gt "${#POLL_LINES_MAX}" ]; then
|
||||
printf '%s' "$POLL_LINES_MAX"
|
||||
return 0
|
||||
fi
|
||||
if [ "$_tmp" -gt "$POLL_LINES_MAX" ]; then
|
||||
if [ "$_digits" -gt "$POLL_LINES_MAX" ]; then
|
||||
printf '%s' "$POLL_LINES_MAX"
|
||||
else
|
||||
printf '%s' "$_tmp"
|
||||
printf '%s' "$_digits"
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user