diff --git a/luci-app-fwlive/Makefile b/luci-app-fwlive/Makefile
index a46433936..9fa57c796 100644
--- a/luci-app-fwlive/Makefile
+++ b/luci-app-fwlive/Makefile
@@ -12,7 +12,7 @@ LUCI_DEPENDS:=+luci-base +logd +jsonfilter
LUCI_PKGARCH:=all
PKG_VERSION:=0.1.44
-PKG_RELEASE:=25
+PKG_RELEASE:=26
# Reproducible build: honor SOURCE_DATE_EPOCH from the build environment (set by docker-sdk.sh / CI).
ifdef SOURCE_DATE_EPOCH
diff --git a/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js b/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js
index c349dec8d..e4a6c2d38 100644
--- a/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js
+++ b/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js
@@ -33,7 +33,7 @@ const CONSENT_STORAGE_KEY = 'fwlive-logging-consent-v1';
function consentDismissedPermanent() {
try {
return localStorage.getItem(CONSENT_STORAGE_KEY) === '1';
- } catch (e) {
+ } catch (_e) {
return false;
}
}
@@ -41,7 +41,7 @@ function consentDismissedPermanent() {
function persistConsentDismissed() {
try {
localStorage.setItem(CONSENT_STORAGE_KEY, '1');
- } catch (e) {
+ } catch (_e) {
/* private mode / no storage */
}
}
@@ -72,6 +72,21 @@ function blockerCode(state) {
return '';
}
+function wanZoneCandidateNames(st) {
+ return Array.isArray(st && st.wan_zone_candidates)
+ ? st.wan_zone_candidates.filter(function (name) {
+ return typeof name === 'string';
+ })
+ : [];
+}
+
+function labelWithZoneCandidates(label, st) {
+ const children = [label];
+ const candidates = wanZoneCandidateNames(st);
+ if (candidates.length) children.push(': ', E('code', {}, [candidates.join(', ')]));
+ return children;
+}
+
function renderToolbar(host, state, callbacks) {
host.innerHTML = '';
const st = state.loggingStatus;
@@ -85,9 +100,11 @@ function renderToolbar(host, state, callbacks) {
if (blocker === 'no_wan_zone') {
host.appendChild(
- E('span', { 'class': 'fwlive-logging-status' }, [
- _('WAN logging unavailable: no WAN zone')
- ])
+ E(
+ 'span',
+ { 'class': 'fwlive-logging-status' },
+ labelWithZoneCandidates(_('WAN logging unavailable: no WAN zone'), st)
+ )
);
host.appendChild(links.firewallZonesLink());
return;
@@ -218,7 +235,13 @@ function buildEmptyStateNodes(state, callbacks) {
}
if (blocker === 'no_wan_zone') {
- nodes.push(E('p', { 'class': 'fwlive-empty-title' }, [_('No WAN zone found')]));
+ nodes.push(
+ E(
+ 'p',
+ { 'class': 'fwlive-empty-title' },
+ labelWithZoneCandidates(_('No WAN zone found'), st)
+ )
+ );
nodes.push(
E('p', {}, [
_('No WAN firewall zone found in /etc/config/firewall. Configure zones under '),
diff --git a/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js b/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js
index 26669f37c..533a8a90f 100644
--- a/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js
+++ b/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js
@@ -48,11 +48,13 @@ const callFwliveLoggingStatus = rpc.declare({
expect: {
'': {
wan_zone: null,
+ wan_zone_candidates: [],
wan_log: false,
wan_log_limit: null,
nf_log_ipv4: false,
nf_log_ipv6: false,
ready: false,
+ weak_device: false,
blockers: [],
warnings: []
}
@@ -62,20 +64,20 @@ const callFwliveLoggingStatus = rpc.declare({
const callFwliveEnableLogging = rpc.declare({
object: 'fwlive',
method: 'enable_wan_logging',
- expect: { '': { ok: false, changed: false, wan_zone: null } }
+ expect: { '': { ok: false, changed: false, wan_zone: null, wan_zone_candidates: [] } }
});
const callFwliveDisableLogging = rpc.declare({
object: 'fwlive',
method: 'disable_wan_logging',
- expect: { '': { ok: false, changed: false, wan_zone: null } }
+ expect: { '': { ok: false, changed: false, wan_zone: null, wan_zone_candidates: [] } }
});
function storedValue(key, fallback) {
try {
const v = localStorage.getItem(key);
return v === null ? fallback : v;
- } catch (e) {
+ } catch (_e) {
return fallback;
}
}
@@ -83,7 +85,7 @@ function storedValue(key, fallback) {
function storeValue(key, value) {
try {
localStorage.setItem(key, value);
- } catch (e) {
+ } catch (_e) {
/* private mode / no storage */
}
}
@@ -214,7 +216,7 @@ return view.extend({
try {
key = decodeURIComponent(kv[0]);
val = decodeURIComponent(kv[1]);
- } catch (e) {
+ } catch (_e) {
continue;
}
result.push({ key: key, val: val });
@@ -623,7 +625,7 @@ return view.extend({
/* Bounds / mktemp failures are reply.error — same idea as poll. */
this.lastRulesError = (res && res.error) || null;
if (this.lastRulesError) console.warn('fwlive rules map error:', this.lastRulesError);
- } catch (e) {
+ } catch (_e) {
if (this.viewDisposed) return;
this.rulesMap = {};
this.firewallBackend = 'nft';
@@ -676,7 +678,7 @@ return view.extend({
if (this.viewDisposed) return;
this.loggingStatus = status;
this.weakDevice = !!(this.loggingStatus && this.loggingStatus.weak_device === true);
- } catch (e) {
+ } catch (_e) {
if (this.viewDisposed) return;
this.loggingStatus = null;
}
@@ -705,7 +707,7 @@ return view.extend({
this.loggingNotice = opts.successNotice(res);
if (opts.onSuccess) opts.onSuccess(res);
await this.loadLoggingStatus();
- } catch (e) {
+ } catch (_e) {
this.loggingNotice = opts.catchNotice();
await this.loadLoggingStatus();
} finally {
@@ -791,10 +793,13 @@ return view.extend({
const st = this.loggingStatus;
/* Sort blockers so unstable backend order does not force a rebuild. */
const blockers = st && st.blockers ? st.blockers.slice().sort().join(',') : '';
+ const candidates =
+ st && Array.isArray(st.wan_zone_candidates) ? st.wan_zone_candidates.join(',') : '';
return [
st ? (st.wan_log ? '1' : '0') : 'x',
st ? String(st.wan_log_limit || '') : '',
blockers,
+ candidates,
this.loggingBusy ? '1' : '0',
this.loggingNotice || '',
this.shouldShowLoggingConsent() ? 'c1' : 'c0'
@@ -876,7 +881,7 @@ return view.extend({
reply = await callFwlivePoll({
addresses: [String(fetchLines)]
});
- } catch (e) {
+ } catch (_e) {
reply = null;
}
return {
@@ -1678,7 +1683,7 @@ return view.extend({
this.updateAdaptiveBanner();
if (updated) this.scheduleRenderRows(true);
- } catch (e) {
+ } catch (_e) {
/* resolve unavailable — show IPs */
} finally {
if (gen === this.resolveGeneration) this.resolveInFlight = false;
@@ -1964,7 +1969,7 @@ return view.extend({
try {
try {
await this.fetchEntries();
- } catch (e) {
+ } catch (_e) {
/* fetchEntries already accounts the poll RTT for every rpc
* outcome; a throw here is a local normalize/buffer bug, not
* network slowness, so count nothing further. */
@@ -1982,10 +1987,10 @@ return view.extend({
try {
await this.resolveHostnamesForEntries(this.filteredRows());
- } catch (e) {
+ } catch (_e) {
/* resolve unavailable — show IPs */
}
- } catch (e) {
+ } catch (_e) {
/* Keep the coordinator promise settling so a queued refresh cannot
* be stranded by an unexpected local rendering failure. */
if (epoch === this.currentPollEpoch()) this.lastPollError = true;
diff --git a/luci-app-fwlive/root/usr/libexec/fwlive-log-filter.sh b/luci-app-fwlive/root/usr/libexec/fwlive-log-filter.sh
index 2addba3f5..aa82ca830 100755
--- a/luci-app-fwlive/root/usr/libexec/fwlive-log-filter.sh
+++ b/luci-app-fwlive/root/usr/libexec/fwlive-log-filter.sh
@@ -21,6 +21,8 @@ if ! command -v jsonfilter >/dev/null 2>&1; then
fi
FILTER_DIR="$(cd "$(dirname "$0")" && pwd)"
+FILTER_TMP_DIR='/tmp'
+[ "$#" -gt 0 ] && FILTER_TMP_DIR="$1"
# shellcheck disable=SC1091 # classifier is a sibling file next to this script
. "$FILTER_DIR/fwlive-is-firewall-event.sh"
@@ -40,14 +42,15 @@ fi
# jsonfilter output in a secure temporary file before classification so a
# partial pipeline cannot produce malformed JSON on failure.
# This script runs from rpcd as root and reopens the path for classification.
-# Match _fwlive_mktemp: only use the verified, sticky system /tmp; honoring
-# TMPDIR would permit a non-sticky attacker-controlled directory here.
+# Match _fwlive_mktemp: only use a verified, sticky dump directory. The
+# production rpcd caller passes /tmp; the explicit argument lets host tests
+# exercise the directory guard without changing the host's /tmp.
# shellcheck disable=SC3065 # OpenWrt BusyBox test supports -k; match rpcd helper.
-if [ ! -d /tmp ] || [ -L /tmp ] || ! [ -k /tmp ]; then
+if [ ! -d "$FILTER_TMP_DIR" ] || [ -L "$FILTER_TMP_DIR" ] || ! [ -k "$FILTER_TMP_DIR" ]; then
printf '%s' '{"log":[],"error":"filter_tempfile_failed"}'
exit 1
fi
-_filter_tmp=$(mktemp /tmp/fwlive-filter.XXXXXX) || {
+_filter_tmp=$(mktemp "$FILTER_TMP_DIR/fwlive-filter.XXXXXX") || {
printf '%s' '{"log":[],"error":"filter_tempfile_failed"}'
exit 1
}
diff --git a/luci-app-fwlive/root/usr/libexec/fwlive-logging.sh b/luci-app-fwlive/root/usr/libexec/fwlive-logging.sh
index f8d5bd2ba..bccbb3eb5 100755
--- a/luci-app-fwlive/root/usr/libexec/fwlive-logging.sh
+++ b/luci-app-fwlive/root/usr/libexec/fwlive-logging.sh
@@ -9,8 +9,19 @@
# expect soft failures. The rpcd entry point enables strict mode; critical
# paths use explicit `|| return 1` / `|| true`.
-NF_LOG_IPV4='/proc/sys/net/netfilter/nf_log/2'
-NF_LOG_IPV6='/proc/sys/net/netfilter/nf_log/10'
+NF_LOG_IPV4="${FWLIVE_NF_LOG_IPV4_PATH:-/proc/sys/net/netfilter/nf_log/2}"
+NF_LOG_IPV6="${FWLIVE_NF_LOG_IPV6_PATH:-/proc/sys/net/netfilter/nf_log/10}"
+# /proc/sys/net/netfilter/nf_log/10 is a backend selector, not an IPv6
+# availability probe. A missing or empty /proc/net/if_inet6 means the IPv6
+# stack is absent (compiled out or ipv6.disable=1), so an IPv6 backend is
+# not required. Any content, including loopback ::1, means the IPv6 stack
+# is present and the IPv6 logger is required. This is not a WAN-address
+# probe: stock OpenWrt with CONFIG_IPV6 still has lo ::1 on an IPv4-only WAN.
+NF_LOG_IPV6_AVAILABLE_PATH="${FWLIVE_IPV6_AVAILABLE_PATH:-/proc/net/if_inet6}"
+
+NF_LOG_STATE_COMPUTED=0
+NF_LOG_IPV4_READY=false
+NF_LOG_IPV6_READY=false
# Serialize the WAN logging read->compute->set->commit window across
# concurrent ubus write-ACL callers: each toggle re-reads the current
@@ -26,6 +37,8 @@ NF_LOG_IPV6='/proc/sys/net/netfilter/nf_log/10'
# Overridable for tests/containers (default is root-only /etc/fwlive).
WAN_LOG_LOCK_FILE="${FWLIVE_WAN_LOG_LOCK_FILE:-/etc/fwlive/logging.lock}"
WAN_LOG_BASELINE_FILE="${FWLIVE_WAN_LOG_BASELINE_FILE:-/etc/fwlive/wan-log-baseline}"
+WAN_ZONE_DIAGNOSTIC_JSON=''
+WAN_ZONE_FOUND=''
weak_device_detected() {
# Path overrides are test hooks; production defaults stay in procfs.
@@ -170,26 +183,78 @@ release_wan_log_lock() {
exec 9>&-
}
-find_wan_zone_section() {
- # Match anonymous (@zone[N]) and named (e.g. wan) sections whose
- # name option is 'wan'. Prefer the first section whose type is
- # zone; skip non-zone sections that happen to share name='wan'.
+find_wan_zone_section_state() {
+ # Match the first zone whose name is 'wan' or whose effective network list
+ # contains 'wan'/'wan6'. UCI defaults an omitted network option to the
+ # section name, so a renamed zone such as internet with network=wan is
+ # supported while a fully renamed zone remains an explicit no-WAN result.
+ # The diagnostic list is reset on every lookup and records zone names for
+ # no_wan_zone callers; it is JSON-escaped before it reaches a reply.
# uci missing / no wan zone is empty, not fatal. pipefail + set -e
# cannot apply to this pipeline.
- _zones=$(uci -q show firewall 2>/dev/null \
- | sed -n "s/^firewall\.\([^.]*\)\.name='wan'$/\1/p") || true
+ WAN_ZONE_DIAGNOSTIC_JSON=''
+ WAN_ZONE_FOUND=''
+ _firewall_show=$(uci -q show firewall 2>/dev/null || true)
+ _zones=$(printf '%s\n' "$_firewall_show" \
+ | sed -n 's/^firewall\.\([^.=]*\)=zone$/\1/p')
for zone in $_zones; do
[ -n "$zone" ] || continue
+ _name=$(uci -q get "firewall.${zone}.name" 2>/dev/null || true)
+ if [ -z "$_name" ]; then
+ _name=$(printf '%s\n' "$_firewall_show" \
+ | awk -v key="firewall.${zone}.name=" \
+ 'index($0, key) == 1 {
+ value = substr($0, length(key) + 1)
+ sub(/^'\''/, "", value)
+ sub(/'\''$/, "", value)
+ print value
+ exit
+ }')
+ fi
+ # UCI permits a zone section without an explicit name. In that case
+ # the section id is the effective zone name for omitted network lists.
+ [ -n "$_name" ] || _name="$zone"
+ [ -n "$_name" ] && _zone_label="$_name" || _zone_label="$zone"
+ _esc=$(printf '%s' "$_zone_label" | json_escape)
+ if [ -n "$WAN_ZONE_DIAGNOSTIC_JSON" ]; then
+ WAN_ZONE_DIAGNOSTIC_JSON="${WAN_ZONE_DIAGNOSTIC_JSON},"
+ fi
+ WAN_ZONE_DIAGNOSTIC_JSON="${WAN_ZONE_DIAGNOSTIC_JSON}\"${_esc}\""
+
# uci -q get exits 1 on a missing section. Capture with || true so
# set -e cannot abort inside "$(…)" before || continue.
_type=$(uci -q get "firewall.${zone}" 2>/dev/null || true)
[ "$_type" = "zone" ] || continue
- printf '%s' "$zone"
- return 0
+ _network=$(uci -q get "firewall.${zone}.network" 2>/dev/null || true)
+ [ -n "$_network" ] || _network="$_name"
+ _is_wan=0
+ [ "$_name" = wan ] && _is_wan=1
+ case " ${_network} " in
+ *' wan '*|*' wan6 '*) _is_wan=1 ;;
+ esac
+ if [ "$_is_wan" = 1 ]; then
+ WAN_ZONE_FOUND="$zone"
+ return 0
+ fi
done
return 0
}
+find_wan_zone_section() {
+ find_wan_zone_section_state
+ printf '%s' "$WAN_ZONE_FOUND"
+}
+
+wan_zone_diagnostic_json() {
+ printf '[%s]' "${WAN_ZONE_DIAGNOSTIC_JSON:-}"
+}
+
+no_wan_zone_error_json() {
+ _candidates=$(wan_zone_diagnostic_json)
+ printf '{"ok":false,"changed":false,"wan_zone":null,"error":"no_wan_zone","wan_zone_candidates":%s}' \
+ "$_candidates"
+}
+
firewall_changes_pending() {
# uci miss is "no pending changes". set -e cannot apply.
pending="$(uci -q changes firewall 2>/dev/null || true)"
@@ -346,7 +411,8 @@ restore_wan_log_baseline() {
[ -f "$path" ] || return 0
# Empty file is a valid "option was unset" baseline.
baseline=$(cat "$path" 2>/dev/null || true)
- zone=$(find_wan_zone_section)
+ find_wan_zone_section_state
+ zone=$WAN_ZONE_FOUND
if [ -z "$zone" ]; then
logger -t fwlive "WAN log baseline restore skipped: no WAN zone" 2>/dev/null || true
return 1
@@ -425,7 +491,40 @@ read_nf_log_backend() {
path="$1"
[ -f "$path" ] || return 1
val=$(cat "$path" 2>/dev/null) || return 1
- [ -n "$val" ] && [ "$val" != 'none' ]
+ # Sysctl values are single-line, but trim surrounding whitespace before
+ # interpreting the no-backend sentinel. This keeps a malformed `NONE `
+ # value from becoming a fail-open logger.
+ while case "$val" in [[:space:]]*) true ;; *) false ;; esac; do
+ val=${val#?}
+ done
+ while case "$val" in *[[:space:]]) true ;; *) false ;; esac; do
+ val=${val%?}
+ done
+ [ -n "$val" ] || return 1
+ case "$val" in
+ [Nn][Oo][Nn][Ee]) return 1 ;;
+ esac
+ return 0
+}
+
+nf_log_family_available() {
+ family="$1"
+ case "$family" in
+ ipv4)
+ # Supported fwlive deployments require IPv4 for the WAN path.
+ return 0
+ ;;
+ ipv6)
+ # Content probe: missing/empty => stack absent; any row
+ # (including lo ::1) => stack present. Not a WAN check.
+ path="${FWLIVE_IPV6_AVAILABLE_PATH:-$NF_LOG_IPV6_AVAILABLE_PATH}"
+ [ -r "$path" ] || return 1
+ grep -q '[^[:space:]]' "$path" 2>/dev/null
+ ;;
+ *)
+ return 1
+ ;;
+ esac
}
check_nf_log_ipv4() {
@@ -433,9 +532,20 @@ check_nf_log_ipv4() {
}
check_nf_log_ipv6() {
+ # Effective readiness: an absent IPv6 stack is not a blocker. When
+ # if_inet6 has any address, including lo, the IPv6 logger is required.
+ nf_log_family_available ipv6 || return 0
read_nf_log_backend "$NF_LOG_IPV6"
}
+compute_nf_log_state() {
+ NF_LOG_IPV4_READY=false
+ check_nf_log_ipv4 && NF_LOG_IPV4_READY=true
+ NF_LOG_IPV6_READY=false
+ check_nf_log_ipv6 && NF_LOG_IPV6_READY=true
+ NF_LOG_STATE_COMPUTED=1
+}
+
logging_blockers_append() {
blocker="$1"
[ -n "$blocker" ] || return 0
@@ -459,16 +569,28 @@ logging_warnings_append() {
collect_logging_blockers() {
zone="$1"
LOGGING_BLOCKERS=''
+ [ "$NF_LOG_STATE_COMPUTED" = 1 ] || compute_nf_log_state
[ -n "$zone" ] || logging_blockers_append 'no_wan_zone'
- check_nf_log_ipv4 || logging_blockers_append 'nf_log_ipv4_missing'
- check_nf_log_ipv6 || logging_blockers_append 'nf_log_ipv6_missing'
+ [ "$NF_LOG_IPV4_READY" = true ] || logging_blockers_append 'nf_log_ipv4_missing'
+ [ "$NF_LOG_IPV6_READY" = true ] || logging_blockers_append 'nf_log_ipv6_missing'
# Report via LOGGING_BLOCKERS, not exit status: return 1 would abort
# build_logging_status_json under set -e.
return 0
}
+legacy_iptables_active() {
+ for _path in \
+ "${FWLIVE_IP_TABLES_NAMES_PATH:-/proc/net/ip_tables_names}" \
+ "${FWLIVE_IP6_TABLES_NAMES_PATH:-/proc/net/ip6_tables_names}"; do
+ if [ -r "$_path" ] && grep -q '[^[:space:]]' "$_path" 2>/dev/null; then
+ return 0
+ fi
+ done
+ return 1
+}
+
collect_logging_warnings() {
LOGGING_WARNINGS=''
@@ -476,6 +598,12 @@ collect_logging_warnings() {
# Warnings are diagnostics only — do not gate the enable-logging CTA.
command -v timeout >/dev/null 2>&1 || logging_warnings_append 'timeout_missing'
+ # Diagnostic only: supported releases use nftables, but a registered legacy
+ # iptables table can still exist in the network namespace visible to rpcd.
+ # Keep this ubus-only for now; it must not affect readiness or the enable
+ # gate, and the UI has no remediation flow for legacy tables yet.
+ legacy_iptables_active && logging_warnings_append 'legacy_iptables_detected'
+
# Report via LOGGING_WARNINGS, not exit status.
return 0
}
@@ -491,7 +619,11 @@ json_null_or_string() {
}
build_logging_status_json() {
- zone=$(find_wan_zone_section)
+ find_wan_zone_section_state
+ zone=$WAN_ZONE_FOUND
+ candidates=$(wan_zone_diagnostic_json)
+ NF_LOG_STATE_COMPUTED=0
+ compute_nf_log_state
# Empty zone / unset log bit are valid; set -e cannot apply.
log_val=$(wan_zone_log_value "$zone") || log_val=
# Unset log_limit is a valid empty value; uci -q get exits 1.
@@ -504,10 +636,8 @@ build_logging_status_json() {
wan_log=true
fi
- nf4=false
- check_nf_log_ipv4 && nf4=true
- nf6=false
- check_nf_log_ipv6 && nf6=true
+ nf4=$NF_LOG_IPV4_READY
+ nf6=$NF_LOG_IPV6_READY
collect_logging_blockers "$zone"
blockers="[${LOGGING_BLOCKERS:-}]"
@@ -524,8 +654,8 @@ build_logging_status_json() {
zone_json=$(json_null_or_string "$zone")
limit_json=$(json_null_or_string "$limit_val")
- printf '{"wan_zone":%s,"wan_log":%s,"wan_log_limit":%s,"nf_log_ipv4":%s,"nf_log_ipv6":%s,"ready":%s,"weak_device":%s,"blockers":%s,"warnings":%s}' \
- "$zone_json" "$wan_log" "$limit_json" "$nf4" "$nf6" "$ready" "$weak_device" "$blockers" "$warnings"
+ printf '{"wan_zone":%s,"wan_zone_candidates":%s,"wan_log":%s,"wan_log_limit":%s,"nf_log_ipv4":%s,"nf_log_ipv6":%s,"ready":%s,"weak_device":%s,"blockers":%s,"warnings":%s}' \
+ "$zone_json" "$candidates" "$wan_log" "$limit_json" "$nf4" "$nf6" "$ready" "$weak_device" "$blockers" "$warnings"
}
reload_firewall() {
@@ -719,15 +849,18 @@ reload_and_report_wan_log() {
}
enable_wan_logging() {
- zone=$(find_wan_zone_section)
+ find_wan_zone_section_state
+ zone=$WAN_ZONE_FOUND
if [ -z "$zone" ]; then
- printf '{"ok":false,"changed":false,"wan_zone":null,"error":"no_wan_zone"}'
+ no_wan_zone_error_json
return 0
fi
zone_json=$(json_null_or_string "$zone")
- if ! check_nf_log_ipv4 || ! check_nf_log_ipv6; then
+ NF_LOG_STATE_COMPUTED=0
+ compute_nf_log_state
+ if [ "$NF_LOG_IPV4_READY" != true ] || [ "$NF_LOG_IPV6_READY" != true ]; then
printf '{"ok":false,"changed":false,"wan_zone":%s,"error":"nf_log_missing"}' "$zone_json"
return 0
fi
@@ -778,9 +911,10 @@ enable_wan_logging() {
}
disable_wan_logging() {
- zone=$(find_wan_zone_section)
+ find_wan_zone_section_state
+ zone=$WAN_ZONE_FOUND
if [ -z "$zone" ]; then
- printf '{"ok":false,"changed":false,"wan_zone":null,"error":"no_wan_zone"}'
+ no_wan_zone_error_json
return 0
fi
diff --git a/luci-app-fwlive/root/usr/libexec/rpcd/fwlive b/luci-app-fwlive/root/usr/libexec/rpcd/fwlive
index 5a7d5bf54..484e3382d 100755
--- a/luci-app-fwlive/root/usr/libexec/rpcd/fwlive
+++ b/luci-app-fwlive/root/usr/libexec/rpcd/fwlive
@@ -482,7 +482,7 @@ fetch_firewall_logs() {
# Empty success is not valid poll JSON — the shipped filter always
# prints {"log":[...]}, but a stub/regression must not emit a blank body.
_filter_ok=1
- if ! out=$(printf '%s' "$raw" | "$FILTER_SH"); then
+ if ! out=$(printf '%s' "$raw" | "$FILTER_SH" /tmp); then
_filter_ok=0
if [ -z "$out" ]; then
out='{"log":[],"error":"filter_failed"}'
@@ -862,6 +862,16 @@ Address: 127.0.0.1:53
Address 1: 8.8.8.8 dns.google.")
check_eq parse_nslookup_name "$got" "dns.google" "busybox Address N expected dns.google got '$got'" || return 1
+ got=$(parse_nslookup_name "Server: 2001:db8::53
+Address: 2001:db8::53#53
+
+2001:db8::1.ip6.arpa name = v6.example.")
+ check_eq parse_nslookup_name "$got" "v6.example" "IPv6 bind-style expected v6.example got '$got'" || return 1
+ got=$(parse_nslookup_name "Server: 2001:db8::53
+Address: 2001:db8::53#53
+
+Address 1: 2001:db8::1 v6.example.")
+ check_eq parse_nslookup_name "$got" "v6.example" "IPv6 busybox Address N expected v6.example got '$got'" || return 1
got=$(parse_nslookup_name "Server: 192.168.1.1
Address 1: 192.168.1.1 router.lan
diff --git a/luci-app-passwall/Makefile b/luci-app-passwall/Makefile
index be1051427..eeea08bf2 100644
--- a/luci-app-passwall/Makefile
+++ b/luci-app-passwall/Makefile
@@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall
PKG_VERSION:=26.9.16
-PKG_RELEASE:=282
+PKG_RELEASE:=283
PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \
diff --git a/luci-app-passwall/luasrc/model/cbi/passwall/server/type/1_sing-box.lua b/luci-app-passwall/luasrc/model/cbi/passwall/server/type/1_sing-box.lua
index 65e69f993..3cafc9a39 100644
--- a/luci-app-passwall/luasrc/model/cbi/passwall/server/type/1_sing-box.lua
+++ b/luci-app-passwall/luasrc/model/cbi/passwall/server/type/1_sing-box.lua
@@ -83,6 +83,21 @@ o = s:option(DynamicList, "users", translate("User"))
for i, v in ipairs(user_list) do
o:value(v[".name"], v.username)
end
+o.validate = function(self, value, t)
+ if type(value) == "string" then value = {value} end
+ if type(value) == "table" then
+ for _, u in ipairs(value) do
+ local found = false
+ for _, v in ipairs(user_list) do
+ if u == v[".name"] then found = true; break end
+ end
+ if not found then
+ return nil, translate("Please configure user info on the User Management page.")
+ end
+ end
+ end
+ return value
+end
o:depends({ protocol = "mixed" })
o:depends({ protocol = "socks" })
o:depends({ protocol = "http" })
diff --git a/luci-app-passwall/luasrc/model/cbi/passwall/server/type/2_xray.lua b/luci-app-passwall/luasrc/model/cbi/passwall/server/type/2_xray.lua
index b126f0211..3374b1af4 100644
--- a/luci-app-passwall/luasrc/model/cbi/passwall/server/type/2_xray.lua
+++ b/luci-app-passwall/luasrc/model/cbi/passwall/server/type/2_xray.lua
@@ -76,6 +76,21 @@ o = s:option(DynamicList, "users", translate("User"))
for i, v in ipairs(user_list) do
o:value(v[".name"], v.username)
end
+o.validate = function(self, value, t)
+ if type(value) == "string" then value = {value} end
+ if type(value) == "table" then
+ for _, u in ipairs(value) do
+ local found = false
+ for _, v in ipairs(user_list) do
+ if u == v[".name"] then found = true; break end
+ end
+ if not found then
+ return nil, translate("Please configure user info on the User Management page.")
+ end
+ end
+ end
+ return value
+end
o:depends({ protocol = "http" })
o:depends({ protocol = "socks" })
o:depends({ protocol = "shadowsocks" })
diff --git a/luci-app-passwall/luasrc/model/cbi/passwall/server/type/5_hysteria2.lua b/luci-app-passwall/luasrc/model/cbi/passwall/server/type/5_hysteria2.lua
index b526498be..474415a05 100644
--- a/luci-app-passwall/luasrc/model/cbi/passwall/server/type/5_hysteria2.lua
+++ b/luci-app-passwall/luasrc/model/cbi/passwall/server/type/5_hysteria2.lua
@@ -50,6 +50,21 @@ for i, v in ipairs(user_list) do
o:value(v[".name"], v.username)
end
o:depends({ custom = false })
+o.validate = function(self, value, t)
+ if type(value) == "string" then value = {value} end
+ if type(value) == "table" then
+ for _, u in ipairs(value) do
+ local found = false
+ for _, v in ipairs(user_list) do
+ if u == v[".name"] then found = true; break end
+ end
+ if not found then
+ return nil, translate("Please configure user info on the User Management page.")
+ end
+ end
+ end
+ return value
+end
o = s:option(Flag, "realms", translate("Realms"))
o.default = "0"
diff --git a/luci-app-passwall/luasrc/passwall/util_xray.lua b/luci-app-passwall/luasrc/passwall/util_xray.lua
index 0c1c94e44..d722a6c0f 100644
--- a/luci-app-passwall/luasrc/passwall/util_xray.lua
+++ b/luci-app-passwall/luasrc/passwall/util_xray.lua
@@ -217,8 +217,8 @@ function gen_outbound(flag, node, tag, proxy_table)
tti = 50,
uplinkCapacity = 12,
downlinkCapacity = 100,
- CwndMultiplier = 1,
- MaxSendingWindow = 2 * 1024 * 1024
+ cwndMultiplier = 1,
+ maxSendingWindow = 2 * 1024 * 1024
} or nil,
wsSettings = (node.transport == "ws") and {
path = node.ws_path or "/",
@@ -727,8 +727,8 @@ function gen_config_server(node)
tti = 50,
uplinkCapacity = 12,
downlinkCapacity = 100,
- CwndMultiplier = 1,
- MaxSendingWindow = 2 * 1024 * 1024
+ cwndMultiplier = 1,
+ maxSendingWindow = 2 * 1024 * 1024
} or nil,
wsSettings = (node.transport == "ws") and {
host = node.ws_host or nil,
diff --git a/luci-app-passwall/luasrc/view/passwall/server/config_header.htm b/luci-app-passwall/luasrc/view/passwall/server/config_header.htm
index 742b71514..97be8a2c4 100644
--- a/luci-app-passwall/luasrc/view/passwall/server/config_header.htm
+++ b/luci-app-passwall/luasrc/view/passwall/server/config_header.htm
@@ -3,6 +3,13 @@ local map = self.map
local api = map.api
local section = self.section
-%>
+