diff --git a/luci-app-daede/Makefile b/luci-app-daede/Makefile index 8f917070a..1797e661c 100644 --- a/luci-app-daede/Makefile +++ b/luci-app-daede/Makefile @@ -6,7 +6,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-daede PKG_VERSION:=1.15 -PKG_RELEASE:=43 +PKG_RELEASE:=44 PKG_MAINTAINER:=kenzok8 PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME) diff --git a/luci-app-daede/htdocs/luci-static/resources/view/daede/dae.js b/luci-app-daede/htdocs/luci-static/resources/view/daede/dae.js index c32f42ca7..9d22520df 100644 --- a/luci-app-daede/htdocs/luci-static/resources/view/daede/dae.js +++ b/luci-app-daede/htdocs/luci-static/resources/view/daede/dae.js @@ -336,6 +336,15 @@ function renderDaeForms(ctx) { s = m.section(form.NamedSection, 'config', 'dae', _('Logging')); s.addremove = false; + o = s.option(form.ListValue, 'log_level', _('Log level'), + _('Debug and trace show per-connection routing details, but increase CPU usage and log volume. Enable them only while troubleshooting.')); + o.value('error', 'ERROR'); + o.value('warn', 'WARN'); + o.value('info', 'INFO'); + o.value('debug', 'DEBUG'); + o.value('trace', 'TRACE'); + o.default = 'info'; + o.rmempty = false; o = s.option(form.Value, 'log_maxsize', _('Max Log Size (MB)'), _('Rotate the log file once it grows past this many megabytes.')); o.datatype = 'uinteger'; diff --git a/luci-app-daede/htdocs/luci-static/resources/view/daede/log.js b/luci-app-daede/htdocs/luci-static/resources/view/daede/log.js index 961539ebd..c39cd0b46 100644 --- a/luci-app-daede/htdocs/luci-static/resources/view/daede/log.js +++ b/luci-app-daede/htdocs/luci-static/resources/view/daede/log.js @@ -37,6 +37,7 @@ const CSS = [ '.dd-log-pane .dd-line.dd-warn{color:#e8b95a}', '.dd-log-pane .dd-line.dd-error{color:#ea7878;background:rgba(234,120,120,.06)}', '.dd-log-pane .dd-line.dd-debug{color:#7a8290;opacity:.7}', + '.dd-log-pane .dd-line.dd-trace{color:#68717d;opacity:.62}', '.dd-log-pane .dd-line.dd-hidden{display:none}', '.dd-log-pane .dd-empty{opacity:.5;font-style:italic}', /* 简化后的字段视觉 */ @@ -46,6 +47,7 @@ const CSS = [ '.dd-log-pane .dd-lvl-warn{color:#e8b95a;background:rgba(232,185,90,.10)}', '.dd-log-pane .dd-lvl-error{color:#ea7878;background:rgba(234,120,120,.10)}', '.dd-log-pane .dd-lvl-debug{color:#7a8290;background:rgba(122,130,144,.10)}', + '.dd-log-pane .dd-lvl-trace{color:#68717d;background:rgba(104,113,125,.10)}', '.dd-log-pane .dd-msg{color:inherit}', '.dd-log-pane .dd-kv{margin-left:6px;color:#6b7480;font-size:11px;opacity:.85}', 'body.dark .dd-log-card,html[data-theme="dark"] .dd-log-card,html[data-bs-theme="dark"] .dd-log-card{border-color:rgba(255,255,255,.08);background:rgba(255,255,255,.02)}', @@ -54,20 +56,39 @@ const CSS = [ /* 拆字段:time="May 25 07:04:59" level=info msg="..." key=val key="val with space" ... */ const RE_LINE = /^time="([^"]*)"\s+level=(\w+)\s+msg=(?:"((?:[^"\\]|\\.)*)"|(\S+))\s*(.*)$/; -const RE_PREFIXED_LINE = /^\[([^\]]+)\]\s+(DEBUG|INFO|WARN(?:ING)?|ERROR|FATAL|PANIC)\s*(.*)$/i; -const RE_PLAIN_LEVEL_LINE = /^\s*(DEBUG|INFO|WARN(?:ING)?|ERROR|FATAL|PANIC)\s+(.*)$/i; +const RE_PREFIXED_LINE = /^\[([^\]]+)\]\s+(TRACE|DEBUG|INFO|WARN(?:ING)?|ERROR|FATAL|PANIC)\s*(.*)$/i; +const RE_PLAIN_LEVEL_LINE = /^\s*(TRACE|DEBUG|INFO|WARN(?:ING)?|ERROR|FATAL|PANIC)\s+(.*)$/i; function detectLevel(line) { // daed/dae logs use lvl=info / [INFO] / level=warning style - const m = line.match(/\b(DEBUG|INFO|WARN(?:ING)?|ERROR|FATAL|PANIC)\b/i); + const m = line.match(/\b(TRACE|DEBUG|INFO|WARN(?:ING)?|ERROR|FATAL|PANIC)\b/i); if (!m) return ''; const lvl = m[1].toUpperCase(); + if (lvl === 'TRACE') return 'dd-trace'; if (lvl === 'DEBUG') return 'dd-debug'; if (lvl === 'INFO') return 'dd-info'; if (lvl.startsWith('WARN')) return 'dd-warn'; return 'dd-error'; } +function matchesFilter(line, filter) { + if (!filter) + return true; + + if (filter === 'trace' || filter === 'debug' || filter === 'info' || filter === 'warn' || filter === 'error') + return detectLevel(line) === 'dd-' + filter; + + if (filter === 'node_status') + return /\balive\b/i.test(line) + || /\bgroup\b.*\bselects dialer\b/i.test(line) + || /\bgroup\s+'[^']+'\s+\[[^\]]+\]:/i.test(line); + + if (filter === 'proxy_traffic') + return line.indexOf('<->') !== -1; + + return false; +} + /* 格式化时间戳并按北京时间显示: * - daed 默认输出 UTC ISO 8601 (e.g. "2026-05-28T19:07:54Z"),转成北京时间 * - 新格式和旧 logrus 短格式无时区信息,保留原始完整日期时间 @@ -101,6 +122,7 @@ function lvlShort(level) { function lvlClass(level) { const u = (level || '').toUpperCase(); + if (u === 'TRACE') return 'dd-lvl-trace'; if (u === 'DEBUG') return 'dd-lvl-debug'; if (u === 'INFO') return 'dd-lvl-info'; if (u === 'WARN' || u === 'WARNING') return 'dd-lvl-warn'; @@ -209,11 +231,13 @@ return view.extend({ const selFilter = E('select', { 'class': 'dd-log-btn' }, [ E('option', { 'value': '' }, _('All')), + E('option', { 'value': 'trace' }, 'TRACE'), + E('option', { 'value': 'debug' }, 'DEBUG'), E('option', { 'value': 'info' }, 'INFO'), E('option', { 'value': 'warn' }, 'WARN'), E('option', { 'value': 'error' }, 'ERROR'), - E('option', { 'value': 'alive' }, _('Node Status')), - E('option', { 'value': 'my_group' }, _('Proxy Traffic')) + E('option', { 'value': 'node_status' }, _('Node Status')), + E('option', { 'value': 'proxy_traffic' }, _('Proxy Traffic')) ]); selFilter.addEventListener('change', function() { state.filter = selFilter.value; @@ -271,7 +295,7 @@ return view.extend({ function applyFilter() { const f = state.filter; pane.querySelectorAll('.dd-line').forEach(function(el) { - if (!f || el.textContent.toLowerCase().indexOf(f) !== -1) + if (matchesFilter(el.textContent, f)) el.classList.remove('dd-hidden'); else el.classList.add('dd-hidden'); @@ -285,7 +309,7 @@ return view.extend({ const ln = lines[i]; if (!ln) continue; const el = buildLine(ln); - if (state.filter && ln.toLowerCase().indexOf(state.filter) === -1) + if (!matchesFilter(ln, state.filter)) el.classList.add('dd-hidden'); frag.appendChild(el); } diff --git a/luci-app-daede/htdocs/luci-static/resources/view/daede/widgets.js b/luci-app-daede/htdocs/luci-static/resources/view/daede/widgets.js index 0686bb6c9..843fcba49 100644 --- a/luci-app-daede/htdocs/luci-static/resources/view/daede/widgets.js +++ b/luci-app-daede/htdocs/luci-static/resources/view/daede/widgets.js @@ -156,6 +156,13 @@ function renderStatusCard(ctx, listenAddr) { let busy = false; let lastError = ''; let refreshGeneration = 0; + const probeState = { + running: false, + text: '', + kind: '', + visibleUntil: 0, + hideTimer: null + }; const body = E('div', { 'id': 'dd-status-body' }, E('em', {}, _('Collecting data…'))); const card = E('div', { 'class': 'dd-card dd-status-card' }, [ E('h4', { 'class': 'dd-card-title' }, _('Service Status')), @@ -265,13 +272,28 @@ function renderStatusCard(ctx, listenAddr) { } if (state.running && be.name === 'dae') { const ckBtn = E('button', { 'class': 'cbi-button cbi-button-action' }, _('Test YouTube')); - const ckRes = E('span', { 'class': 'dd-meta', style: 'margin-left:8px;display:none' }, ''); + const probeVisible = probeState.running || probeState.visibleUntil > Date.now(); + const ckRes = E('span', { + 'class': 'dd-meta' + (probeState.kind ? ' ' + probeState.kind : ''), + style: 'margin-left:8px;' + (probeVisible ? '' : 'display:none') + }, probeState.text); + ckBtn.disabled = probeState.running; ckBtn.addEventListener('click', function(ev) { ev.preventDefault(); + if (probeState.running) + return; + if (probeState.hideTimer) { + clearTimeout(probeState.hideTimer); + probeState.hideTimer = null; + } + probeState.running = true; + probeState.text = _('Testing…'); + probeState.kind = ''; + probeState.visibleUntil = 0; ckBtn.disabled = true; ckRes.style.display = 'inline'; ckRes.classList.remove('dd-ok', 'dd-err'); - ckRes.textContent = _('Testing…'); + ckRes.textContent = probeState.text; /* actually probe YouTube through dae's transparent proxy */ fs.exec('/usr/share/luci-app-daede/proxy-check.sh', []).then(function(r) { const out = (r && r.stdout) || ''; @@ -279,20 +301,28 @@ function renderStatusCard(ctx, listenAddr) { const code = (out.match(/code=(\S+)/) || [])[1] || '000'; const ms = parseInt((out.match(/ms=(\d+)/) || [])[1] || '0'); if (ok) { - ckRes.classList.add('dd-ok'); - ckRes.textContent = _('YouTube reachable · %d ms').format(ms); + probeState.kind = 'dd-ok'; + probeState.text = _('YouTube reachable · %d ms').format(ms); } else { - ckRes.classList.add('dd-err'); - ckRes.textContent = (code === '000') + probeState.kind = 'dd-err'; + probeState.text = (code === '000') ? _('YouTube unreachable (timeout)') : _('YouTube unreachable (HTTP %s)').format(code); } }).catch(function() { - ckRes.classList.add('dd-err'); - ckRes.textContent = _('YouTube unreachable (timeout)'); + probeState.kind = 'dd-err'; + probeState.text = _('YouTube unreachable (timeout)'); }).finally(function() { - ckBtn.disabled = false; - setTimeout(function() { ckRes.style.display = 'none'; }, 8000); + probeState.running = false; + probeState.visibleUntil = Date.now() + 8000; + refresh(true); + probeState.hideTimer = setTimeout(function() { + probeState.text = ''; + probeState.kind = ''; + probeState.visibleUntil = 0; + probeState.hideTimer = null; + refresh(true); + }, 8000); }); }); actions.push(ckBtn); @@ -314,7 +344,11 @@ function renderStatusCard(ctx, listenAddr) { poll.add(refresh); refresh(); - card._ddCleanup = function() { poll.remove(refresh); }; + card._ddCleanup = function() { + poll.remove(refresh); + if (probeState.hideTimer) + clearTimeout(probeState.hideTimer); + }; return card; } diff --git a/luci-app-daede/po/templates/daede.pot b/luci-app-daede/po/templates/daede.pot index 84705320d..316379ebc 100644 --- a/luci-app-daede/po/templates/daede.pot +++ b/luci-app-daede/po/templates/daede.pot @@ -891,3 +891,9 @@ msgstr "" msgid "TTL returned to LAN DNS clients. 0 keeps dae default behavior." msgstr "" + +msgid "Log level" +msgstr "" + +msgid "Debug and trace show per-connection routing details, but increase CPU usage and log volume. Enable them only while troubleshooting." +msgstr "" diff --git a/luci-app-daede/po/zh-cn/daede.po b/luci-app-daede/po/zh-cn/daede.po index 1a95c4173..b4ec479c0 100644 --- a/luci-app-daede/po/zh-cn/daede.po +++ b/luci-app-daede/po/zh-cn/daede.po @@ -663,6 +663,12 @@ msgstr "代理流量" msgid "Logging" msgstr "日志" +msgid "Log level" +msgstr "日志级别" + +msgid "Debug and trace show per-connection routing details, but increase CPU usage and log volume. Enable them only while troubleshooting." +msgstr "Debug 和 Trace 会显示逐连接的分流详情,但会增加 CPU 占用和日志量,仅建议排障时启用。" + msgid "Save manual config" msgstr "保存手动配置" diff --git a/luci-app-daede/po/zh_Hans/daede.po b/luci-app-daede/po/zh_Hans/daede.po index 1a95c4173..b4ec479c0 100644 --- a/luci-app-daede/po/zh_Hans/daede.po +++ b/luci-app-daede/po/zh_Hans/daede.po @@ -663,6 +663,12 @@ msgstr "代理流量" msgid "Logging" msgstr "日志" +msgid "Log level" +msgstr "日志级别" + +msgid "Debug and trace show per-connection routing details, but increase CPU usage and log volume. Enable them only while troubleshooting." +msgstr "Debug 和 Trace 会显示逐连接的分流详情,但会增加 CPU 占用和日志量,仅建议排障时启用。" + msgid "Save manual config" msgstr "保存手动配置" diff --git a/luci-app-fwlive/Makefile b/luci-app-fwlive/Makefile index a8097651e..2d2be25e2 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.45 -PKG_RELEASE:=30 +PKG_RELEASE:=31 # 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/log.js b/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js index 2e3d5974e..20d5df04c 100644 --- a/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js +++ b/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js @@ -39,6 +39,7 @@ return baseclass.extend({ 'hostapd', 'wpad' ], + nonFirewallPrefixHyphenContinuation: true, firewallHints: ['fw4', 'nft', 'iptables', 'kernel', 'firewall'], actionWords: ['ACCEPT', 'ALLOW', 'PASS', 'DROP', 'REJECT', 'DENY', 'BLOCK'], rules: [ @@ -59,7 +60,8 @@ return baseclass.extend({ ] }, - TCP_FLAG_TAIL: /\b(SYN|ACK|FIN|RST|PSH|URG)(?:\s+(?:SYN|ACK|FIN|RST|PSH|URG))*\s*$/i, + TCP_FLAG_TAIL: + /\b((?:SYN|ACK|FIN|RST|PSH|URG)(?:\s+(?:SYN|ACK|FIN|RST|PSH|URG))*)(?:\s+[A-Z][A-Z0-9_]*=[^\s]+)*\s*$/i, NETFILTER_KV_GLUE: /([^\s])(?=(IN|OUT|SRC|DST|PROTO|SPT|DPT|LEN|MAC|TYPE|CODE|TTL|TOS|PREC|DF)=)/g, @@ -73,10 +75,11 @@ return baseclass.extend({ }, NON_FIREWALL_PREFIX: - /^(dnsmasq|procd|ubusd|netifd|odhcpd|logd|dropbear|uhttpd|hostapd|wpad)([^A-Za-z0-9_]|$)/i, + /^(dnsmasq|procd|ubusd|netifd|odhcpd|logd|dropbear|uhttpd|hostapd|wpad)([^A-Za-z0-9_-]|$)/i, FIREWALL_HINT: /(^|[^A-Za-z0-9_])(fw4|nft|iptables|kernel|firewall)([^A-Za-z0-9_]|$)/i, ACTION_RE: /(^|[^A-Za-z0-9_])(ACCEPT|ALLOW|PASS|DROP|REJECT|DENY|BLOCK)([^A-Za-z0-9_]|$)/i, DENY_ACTION: /(^|[^A-Za-z0-9_])(DROP|REJECT|DENY|BLOCK)([^A-Za-z0-9_]|$)/i, + DENY_ACTION_UNDERSCORE: /(?:^|[^A-Za-z0-9])(?:DROP|REJECT|DENY|BLOCK)(?:[^A-Za-z0-9]|$)/i, MAX_DATE_SECONDS: 8640000000000, normalizeNetfilterMessage: function (message) { @@ -201,7 +204,8 @@ return baseclass.extend({ const msg = this.normalizeNetfilterMessage(message || ''); const withoutKv = msg.replace(/\b[A-Z]+=[^\s]*/g, ' '); - if (this.DENY_ACTION.test(withoutKv)) return 'UNKNOWN'; + if (this.DENY_ACTION.test(withoutKv) || this.DENY_ACTION_UNDERSCORE.test(withoutKv)) + return 'UNKNOWN'; if (/^kernel:/i.test(msg.trim())) return 'UNKNOWN'; @@ -218,7 +222,7 @@ return baseclass.extend({ const m = message.match(this.TCP_FLAG_TAIL); if (!m) return ''; - return m[0].trim().toUpperCase().replace(/\s+/g, ','); + return m[1].trim().toUpperCase().replace(/\s+/g, ','); }, parseLength: function (kv) { @@ -386,21 +390,18 @@ return baseclass.extend({ }, normalizeEntry: function (entry) { - const kv = this.parseKeyValueLog(entry.msg || ''); + const message = this.normalizeNetfilterMessage(entry.msg || ''); + const kv = this.parseKeyValueLog(message); const tsUnix = this.timestampUnix(entry); const tsDisplay = this.formatTimestampDisplay(entry); const proto = (kv.PROTO || '').toUpperCase(); - const actionRaw = this.inferActionRaw( - entry.msg || '', - kv, - this.detectAction(entry.msg || '') - ); + const actionRaw = this.inferActionRaw(message, kv, this.detectAction(message)); const action = this.normalizeAction(actionRaw); const addrs = this.extractAddrs(kv); const ifs = this.extractIfaces(kv); - const flags = this.parseFlags(entry.msg || '', kv); + const flags = this.parseFlags(message, kv); const length = this.parseLength(kv); - const ruleHint = this.parseRuleHint(entry.msg || ''); + const ruleHint = this.parseRuleHint(message); const ruleLabel = this.formatRuleLabel(ruleHint); return { 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 3dbe7f869..e1d8b2104 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 @@ -1620,11 +1620,15 @@ return view.extend({ }, resolveNamesFromReply(res) { - return res && typeof res === 'object' && res.names && typeof res.names === 'object' - ? res.names - : res && typeof res === 'object' && !Array.isArray(res) - ? res - : {}; + if (!res || typeof res !== 'object' || Array.isArray(res)) return null; + if (Object.prototype.hasOwnProperty.call(res, 'error')) return null; + if (Object.prototype.hasOwnProperty.call(res, 'names')) { + return res.names && typeof res.names === 'object' && !Array.isArray(res.names) + ? res.names + : null; + } + /* Legacy expect-unwrap replies are the names map itself. */ + return res; }, async resolveHostnamesForEntries(entries) { @@ -1666,14 +1670,12 @@ return view.extend({ this.resolveLoadShed = false; this.resolveShedUntil = 0; - /* RPC-level failure (no_resolver, jshn_missing, invalid_input): the - * reply carries no per-address signal, so return without failMark — - * otherwise every address looks like "no PTR" and retries stall - * for the failure TTL. */ - if (res && typeof res === 'object' && typeof res.error === 'string' && res.error) - return; /* Full reply: names map under .names; legacy expect-unwrap was the map. */ const names = this.resolveNamesFromReply(res); + /* RPC-level failures carry no per-address signal. Do not turn numeric, + * null, or malformed replies into negative hostname cache entries. */ + if (names === null) return; + const truncated = res && typeof res === 'object' && res.truncated === true; let updated = false; for (let i = 0; i < need.length; i++) { @@ -1682,7 +1684,9 @@ return view.extend({ hostname.lruSet(this.hostnameCache, ip, names[ip]); this.hostnameFailed.delete(ip); updated = true; - } else { + } else if (Object.prototype.hasOwnProperty.call(names, ip) || !truncated) { + /* Empty-string names are completed NXDOMAIN/timeout lookups. + * Truncated replies omit unprocessed addresses entirely. */ hostname.failMark(this.hostnameFailed, ip, now); } } diff --git a/luci-app-fwlive/root/usr/libexec/fwlive-adaptive-cap.sh b/luci-app-fwlive/root/usr/libexec/fwlive-adaptive-cap.sh index 92e38ccb1..f5dcae00d 100755 --- a/luci-app-fwlive/root/usr/libexec/fwlive-adaptive-cap.sh +++ b/luci-app-fwlive/root/usr/libexec/fwlive-adaptive-cap.sh @@ -454,7 +454,8 @@ fwlive_adaptive_merge_reply() { _adapt=1 fwlive_adaptive_enabled || _adapt=0 case "$_body" in - *\}) ;; + '{}') _sep= ;; + *\}) _sep=, ;; *) printf '%s' "$_body"; return 0 ;; esac _base=${_body%\}} @@ -466,26 +467,26 @@ fwlive_adaptive_merge_reply() { esac if [ "$_adapt" = 0 ]; then if [ "$_has_msgs" = 1 ]; then - printf '%s,"adaptive":0}' "$_base" + printf '%s%s"adaptive":0}' "$_base" "$_sep" else - printf '%s,"adaptive":0,"messages_received":%s}' "$_base" "$_msgs" + printf '%s%s"adaptive":0,"messages_received":%s}' "$_base" "$_sep" "$_msgs" fi return 0 fi if [ "$_shed" = 1 ]; then if [ "$_has_msgs" = 1 ]; then - printf '%s,"adaptive":1%s,"truncated":%s,"shed":{"level":"hot","limit":%s}}' \ - "$_base" "$_effective" "$_trunc" "$_limit" + printf '%s%s"adaptive":1%s,"truncated":%s,"shed":{"level":"hot","limit":%s}}' \ + "$_base" "$_sep" "$_effective" "$_trunc" "$_limit" else - printf '%s,"adaptive":1,"messages_received":%s%s,"truncated":%s,"shed":{"level":"hot","limit":%s}}' \ - "$_base" "$_msgs" "$_effective" "$_trunc" "$_limit" + printf '%s%s"adaptive":1,"messages_received":%s%s,"truncated":%s,"shed":{"level":"hot","limit":%s}}' \ + "$_base" "$_sep" "$_msgs" "$_effective" "$_trunc" "$_limit" fi else if [ "$_has_msgs" = 1 ]; then - printf '%s,"adaptive":1%s,"truncated":%s}' "$_base" "$_effective" "$_trunc" + printf '%s%s"adaptive":1%s,"truncated":%s}' "$_base" "$_sep" "$_effective" "$_trunc" else - printf '%s,"adaptive":1,"messages_received":%s%s,"truncated":%s}' \ - "$_base" "$_msgs" "$_effective" "$_trunc" + printf '%s%s"adaptive":1,"messages_received":%s%s,"truncated":%s}' \ + "$_base" "$_sep" "$_msgs" "$_effective" "$_trunc" fi fi } diff --git a/luci-app-fwlive/root/usr/libexec/fwlive-is-firewall-event.awk b/luci-app-fwlive/root/usr/libexec/fwlive-is-firewall-event.awk index a7db204f8..c3eedf6f6 100755 --- a/luci-app-fwlive/root/usr/libexec/fwlive-is-firewall-event.awk +++ b/luci-app-fwlive/root/usr/libexec/fwlive-is-firewall-event.awk @@ -28,7 +28,7 @@ function has_hint(s, lc) { } function non_fw_prefix(s, lc) { lc = tolower(s) - return lc ~ "^(dnsmasq|procd|ubusd|netifd|odhcpd|logd|dropbear|uhttpd|hostapd|wpad)([^a-z0-9_]|$)" + return lc ~ "^(dnsmasq|procd|ubusd|netifd|odhcpd|logd|dropbear|uhttpd|hostapd|wpad)([^a-z0-9_-]|$)" } function detect_action(s, words, n, i, w, wl, lc, start, pos, before, afterc, best, bestpos) { n = split("ACCEPT ALLOW PASS DROP REJECT DENY BLOCK", words, " ") diff --git a/luci-app-fwlive/root/usr/libexec/fwlive-logging.sh b/luci-app-fwlive/root/usr/libexec/fwlive-logging.sh index 0a6ef28ce..15f7d5a55 100755 --- a/luci-app-fwlive/root/usr/libexec/fwlive-logging.sh +++ b/luci-app-fwlive/root/usr/libexec/fwlive-logging.sh @@ -445,40 +445,41 @@ restore_wan_log_baseline() { } wan_filter_log_enabled() { - log_val="$1" - [ -n "$log_val" ] || return 1 - case "$log_val" in - *[!0-9]*) return 1 ;; - esac + log_val=$(wan_filter_log_decimal "$1") || return 1 [ $((log_val & 1)) -ne 0 ] } +wan_filter_log_decimal() { + _log_val="$1" + case "$_log_val" in + ''|*[!0-9]*) return 1 ;; + esac + while [ "${_log_val#0}" != "$_log_val" ]; do + _log_val=${_log_val#0} + done + [ -n "$_log_val" ] || _log_val=0 + printf '%s' "$_log_val" +} + wan_filter_log_target_value() { - current="$1" + current=$(wan_filter_log_decimal "$1") || { + printf '1' + return 0 + } if wan_filter_log_enabled "$current"; then printf '%s' "$current" return 0 fi - case "$current" in - ''|*[!0-9]*) - printf '1' - ;; - *) - printf '%d' $((current | 1)) - ;; - esac + printf '%d' $((current | 1)) } # Clear filter-log bit 0 only. Prints remaining value, or empty when the option # should be deleted (no bits left / non-numeric / already empty). wan_filter_log_clear_value() { - current="$1" - case "$current" in - ''|*[!0-9]*) - printf '' - return 0 - ;; - esac + current=$(wan_filter_log_decimal "$1") || { + printf '' + return 0 + } cleared=$((current & ~1)) if [ "$cleared" -eq 0 ]; then printf '' @@ -677,11 +678,27 @@ restore_wan_zone_log() { logger -t fwlive "WAN log rollback skipped: firewall changes pending" 2>/dev/null || true return 1 fi + # Capture the committed value before staging our rollback. If another + # writer stages a firewall delta after this point, the post-stage guard + # below must be able to undo only our rollback staging without reverting + # the other writer's change. + committed=$(wan_zone_log_value "$zone") if [ -z "$previous" ]; then uci -q delete "firewall.${zone}.log" 2>/dev/null || true else uci -q set "firewall.${zone}.log=${previous}" 2>/dev/null || true fi + _staged=$(uci -q changes firewall 2>/dev/null || true) + _foreign=$(wan_log_foreign_staged_lines "$zone" "$_staged") + if [ -n "$_foreign" ]; then + if [ -z "$committed" ]; then + uci delete "firewall.${zone}.log" 2>/dev/null || true + else + uci set "firewall.${zone}.log=${committed}" 2>/dev/null || true + fi + logger -t fwlive "WAN log rollback skipped after stage: firewall changes staged by another writer" 2>/dev/null || true + return 1 + fi uci commit firewall 2>/dev/null || true } @@ -692,13 +709,14 @@ restore_wan_zone_log() { # TOCTOU hardening: UCI staging is global per config file, so a # non-cooperating writer (another admin's `uci set`, the LuCI firewall page) # can stage a delta AFTER the toggle's early firewall_changes_pending check. -# Staging and committing therefore live INSIDE this function — the only path -# to `uci commit firewall` in the toggle flow, so callers cannot bypass it — -# gated by: +# Staging and committing therefore live INSIDE this function, gated by: # 1. a pending re-check BEFORE our own delta exists in staging (foreign-only); # 2. a post-stage re-check AFTER our set/delete: if anything besides our # log option is staged, undo our staging (restore the pre-stage committed # value) and abort without commit — foreign staging stays intact. +# The reload-failure rollback has its own pre/post-stage guard for the same +# reason; it must not bypass this invariant merely because it is compensating +# for a failed reload. # Residual window: a writer that stages between the post-stage check and # `uci commit` can still ride along; post-commit verification detects a # mismatched log bit. Same-option races (another writer also staging @@ -977,6 +995,20 @@ run_logging_selftest() { echo 'wan_filter_log_enabled 2: expected false' >&2 return 1 fi + for leading_value in 011 010 08 0; do + if ! wan_filter_log_decimal "$leading_value" >/dev/null; then + echo "wan_filter_log_decimal $leading_value: expected decimal digits" >&2 + return 1 + fi + done + if wan_filter_log_enabled '08' || wan_filter_log_enabled '010'; then + echo 'leading-zero even values: expected false' >&2 + return 1 + fi + if ! wan_filter_log_enabled '011'; then + echo 'leading-zero odd value: expected true' >&2 + return 1 + fi got=$(wan_filter_log_target_value '') if [ "$got" != '1' ]; then @@ -995,6 +1027,16 @@ run_logging_selftest() { echo "wan_filter_log_target_value 1: expected 1 got $got" >&2 return 1 fi + got=$(wan_filter_log_target_value '08') + if [ "$got" != '9' ]; then + echo "wan_filter_log_target_value 08: expected 9 got $got" >&2 + return 1 + fi + got=$(wan_filter_log_target_value '011') + if [ "$got" != '11' ]; then + echo "wan_filter_log_target_value 011: expected 11 got $got" >&2 + return 1 + fi # Disable clears bit 0 only: log=3 -> 2; log=1 -> delete (empty). got=$(wan_filter_log_clear_value '3') @@ -1015,6 +1057,17 @@ run_logging_selftest() { return 1 fi + got=$(wan_filter_log_clear_value '010') + if [ "$got" != '10' ]; then + echo "wan_filter_log_clear_value 010: expected 10 got $got" >&2 + return 1 + fi + got=$(wan_filter_log_clear_value '0') + if [ -n "$got" ]; then + echo "wan_filter_log_clear_value 0: expected empty got $got" >&2 + return 1 + fi + # Enable/disable parity around multi-bit values. got=$(wan_filter_log_target_value '2') if [ "$got" != '3' ]; then diff --git a/luci-app-fwlive/root/usr/libexec/rpcd/fwlive b/luci-app-fwlive/root/usr/libexec/rpcd/fwlive index 03aec944f..7af34d3d8 100755 --- a/luci-app-fwlive/root/usr/libexec/rpcd/fwlive +++ b/luci-app-fwlive/root/usr/libexec/rpcd/fwlive @@ -564,7 +564,8 @@ resolve_addresses() { start=$(date +%s) # Optional JSON on $1 (rpcd may also pass argv $3 at the call site); else stdin. input=$(read_rpc_input "$1") - count=0 + lookups=0 + truncated=0 OUT='' if ! command -v jshn >/dev/null 2>&1; then @@ -597,27 +598,51 @@ resolve_addresses() { return 0 } while IFS= read -r ip; do - [ "$count" -ge "$RESOLVE_MAX" ] && break + if [ "$lookups" -ge "$RESOLVE_MAX" ]; then + truncated=1 + break + fi # Abort cleanly once the whole-call wall-clock budget is spent; # never START a lookup past the budget. now=$(date +%s) - [ $((now - start)) -ge "$RESOLVE_BUDGET" ] && break + if [ $((now - start)) -ge "$RESOLVE_BUDGET" ]; then + truncated=1 + break + fi # Missing/empty element: set -u cannot read bare $ip. if ! is_resolvable_address "${ip:-}"; then continue fi + lookups=$((lookups + 1)) # NXDOMAIN / timeout: resolve_hostname returns 1. set -e # cannot apply to this assignment. name=$(resolve_hostname "$ip") || name= if [ -n "$name" ]; then map_add "$ip" "$name" - count=$((count + 1)) + else + # Record a completed miss so the view can fail-mark NXDOMAIN + # without treating the unprocessed truncated tail as tried. + # map_add skips empty values (rules-map labels); resolve needs them. + esc_key=$(printf '%s' "$ip" | json_escape) + case "$OUT" in + *"\"$esc_key\":"*) ;; + *) + if [ -n "$OUT" ]; then + OUT="${OUT}," + fi + OUT="${OUT}\"${esc_key}\":\"\"" + ;; + esac fi done <