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 -%> +