Sync 2026-08-31 22:31:57

This commit is contained in:
github-actions[bot]
2026-08-31 22:31:57 +08:00
parent a2635cb70a
commit 2a19f43c9e
41 changed files with 8458 additions and 31 deletions
+7 -5
View File
@@ -10,14 +10,15 @@ AGENTFLOW_ARCH_x86_64:=amd64
AGENTFLOW_ARCH_aarch64:=arm64
AGENTFLOW_ARCH:=$(AGENTFLOW_ARCH_$(ARCH))
AGENTFLOW_HASH_x86_64:=b02c1211f3c7f288b0b25ad187ba1e74989bdde744477875a553dc9f7f7501c8
AGENTFLOW_HASH_aarch64:=938b5feca2fc942eca13b0e8c9df9ea6e7d45c0b0adba0b61f6a3da5bc2ad8d8
AGENTFLOW_HASH_x86_64:=996ccb6580043a187e83a3558c6bb2f0ff503bafbe46b7c995cff7f77629c088
AGENTFLOW_HASH_aarch64:=6d733ebc49b095bf8670a1b655a13bb8ad98c90ced19701b67d5cdd4fe9a3db4
PKG_NAME:=agentflow
PKG_VERSION:=0.3.0
PKG_RELEASE:=3
PKG_RELEASE:=4
AGENTFLOW_DOWNLOAD:=agentflow-linux-$(AGENTFLOW_ARCH)
AGENTFLOW_URL_FILE:=agentflow-linux-$(AGENTFLOW_ARCH)
AGENTFLOW_DOWNLOAD:=$(AGENTFLOW_URL_FILE)-$(AGENTFLOW_HASH_$(ARCH))
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-$(PKG_VERSION)
PKG_BUILD_PARALLEL:=1
@@ -28,7 +29,8 @@ RSTRIP:=:
include $(INCLUDE_DIR)/package.mk
define Download/agentflow
URL:=https://fw0.koolcenter.com/binary/geili/agentflow/build/
URL:=https://fw.koolcenter.com/binary/geili/agentflow/build/
URL_FILE:=$(AGENTFLOW_URL_FILE)
FILE:=$(AGENTFLOW_DOWNLOAD)
HASH:=skip
endef
+40
View File
@@ -0,0 +1,40 @@
# SPDX-License-Identifier: Apache-2.0
#
# Wire feed first (see openwrt-feed/README.md and feeds.conf.example):
# ./scripts/feeds update luci packages fwlive
# ./scripts/feeds install luci-base luci-app-fwlive
include $(TOPDIR)/rules.mk
LUCI_TITLE:=Firewall Live View
LUCI_DESCRIPTION:=Live firewall log table (nftables/fw4 and iptables LOG) with filters
LUCI_DEPENDS:=+luci-base +logd +jsonfilter
LUCI_PKGARCH:=all
PKG_VERSION:=0.1.37
PKG_RELEASE:=1
# Reproducible build: honor SOURCE_DATE_EPOCH from the build environment (set by docker-sdk.sh / CI).
ifdef SOURCE_DATE_EPOCH
export SOURCE_DATE_EPOCH
endif
PKG_LICENSE:=Apache-2.0
PKG_MAINTAINER:=Lucas Albers <lucas.b.albers@gmail.com>
include $(TOPDIR)/feeds/luci/luci.mk
define Package/luci-app-fwlive/prerm
#!/bin/sh
[ -n "$${IPKG_INSTROOT}" ] && exit 0
[ "$$1" = "remove" ] || exit 0
. /usr/libexec/fwlive-logging.sh
# Best-effort restore: always exit 0 so a transient lock/pending-staging
# failure cannot strand the package half-removed. Baseline is kept on
# failure (restore_wan_log_baseline) for admin recovery; helpers are still
# present while this prerm runs.
restore_wan_log_baseline || logger -t fwlive "WAN log baseline restore failed during uninstall"
exit 0
endef
# call BuildPackage - OpenWrt buildroot signature
+50
View File
@@ -0,0 +1,50 @@
# luci-app-fwlive
LuCI **Firewall Live View** — client-side JS view polling `ubus fwlive poll` (firewall-only log lines), with rule labels via `fwlive rules`, optional reverse DNS via `fwlive resolve`, and opt-in WAN zone logging via `enable_wan_logging` / `disable_wan_logging`.
## Package layout (OpenWrt / LuCI conventions)
| Path | Role |
|------|------|
| `Makefile` | `LUCI_TITLE`, `LUCI_DEPENDS`, includes `luci.mk` |
| `htdocs/luci-static/resources/view/status/fwlive.js` | LuCI view (`view.extend`) |
| `htdocs/luci-static/resources/fwlive/log.js` | Parser/filter module (mirror of repo `core/fwlive-log.js`) |
| `htdocs/luci-static/resources/fwlive/constants.js` | Shared view constants (`baseclass.extend` module) |
| `htdocs/luci-static/resources/fwlive/css.js` | Inline stylesheet string (`styleText` for `E('style', …)`) |
| `htdocs/luci-static/resources/fwlive/tint.js` | Row-tint paint helpers (`baseclass.extend` module) |
| `htdocs/luci-static/resources/fwlive/links.js` | Link-builder helpers (pure + filter-aware; no host) |
| `htdocs/luci-static/resources/fwlive/chips.js` | Filter-chip DOM renderer (`renderFilterChips`) |
| `htdocs/luci-static/resources/fwlive/logging.js` | Logging toolbar and empty-state DOM renderers |
| `htdocs/luci-static/resources/fwlive/table.js` | Table thead/rows DOM renderer (`renderThead`, `renderRows`) |
| `htdocs/luci-static/resources/fwlive/buffer.js` | Ring-buffer apply/merge helpers (pause ingest + resume merge) |
| `htdocs/luci-static/resources/fwlive/hostname.js` | Hostname cache LRU + failure TTL helpers |
| `root/usr/share/luci/menu.d/*.json` | Menu entry (`admin/status/fwlive`) |
| `root/usr/share/rpcd/acl.d/*.json` | ubus ACL (read + write for logging enable/disable) |
| `root/usr/libexec/rpcd/fwlive` | rpcd plugin (`rules`, `poll`, `resolve`, `logging_status`, `enable_wan_logging`, `disable_wan_logging`) |
| `root/usr/libexec/fwlive-logging.sh` | WAN zone logging helpers |
| `/etc/fwlive/wan-log-baseline` | Written on first **Enable logging**; restored on uninstall (`prerm`) |
| `root/usr/libexec/fwlive-log-filter.sh` | Server-side firewall-only filter (`isFirewallEvent` parity) |
| `root/usr/libexec/fwlive-is-firewall-event.sh` | Shared filter logic (sourced by filter + tests) |
| `po/templates/luci-app-fwlive.pot` | i18n template (English msgid scaffolding) |
No `luasrc/` — modern JS-only app.
## Dependencies
- `luci-base`, `logd`, `jsonfilter` (declared in `LUCI_DEPENDS`; `rpcd` via `luci-base`)
- Optional reverse DNS uses BusyBox `nslookup` (stock image; not a package depend)
- No hard `firewall4` dependency
- Menu depends on ACL only (no `fs` AND of `nft`+`iptables` — that hid the entry on stock fw3 and fw4)
- Runtime backend detection selects **fw4/nft** (22.03+) or **iptables LOG** (21.02 fw3); best-effort iptables when nft absent
## Maintenance
The development home is [lucas-albers-lz4/fwlive](https://github.com/lucas-albers-lz4/fwlive).
The copy in `openwrt/luci` is a snapshot.
For which tree wins, when we re-cut, generated files, and why we do not use `PKG_SOURCE`, see
[Maintenance model](../../docs/developer/upstream-openwrt.md#maintenance-model).
## Documentation
- **Users:** [`../../docs/user/installation.md`](../../docs/user/installation.md)
- **Developers:** [`../../docs/developer/README.md`](../../docs/developer/README.md)
@@ -0,0 +1,76 @@
'use strict';
'require baseclass';
/**
* Ring-buffer helpers for luci-app-fwlive poll ingest.
* LuCI modules must return baseclass.extend(...) — plain objects fail Class.isSubclass.
*
* While paused, ingest grows up to fetchLinesMax. Live mode caps at rowLimit.
* On resume, merge (do not replace) so pause-accumulated rows survive the first
* live poll — see issue #43 pause→resume data loss.
*/
function ingestCap(paused, rowLimit, fetchLinesMax) {
return paused ? fetchLinesMax : rowLimit;
}
function mergeById(entries, normalized, cap) {
if (!normalized || !normalized.length) {
if (!entries || !entries.length)
return [];
return entries.slice(-cap);
}
const byId = {};
let i;
if (entries) {
for (i = 0; i < entries.length; i++)
byId[entries[i].id] = entries[i];
}
for (i = 0; i < normalized.length; i++)
byId[normalized[i].id] = normalized[i];
const merged = Object.keys(byId).map(function(id) { return byId[id]; });
merged.sort(function(a, b) {
const ta = a.timestamp || 0;
const tb = b.timestamp || 0;
if (ta !== tb)
return ta - tb;
return (a.log_id || 0) - (b.log_id || 0);
});
return merged.slice(-cap);
}
/**
* Apply a poll batch onto the current buffer.
*
* @param {object[]} entries - current buffer (oldest-first)
* @param {object[]} normalized - newly polled rows (oldest-first)
* @param {{ paused: boolean, resumeMerge: boolean, rowLimit: number, fetchLinesMax: number }} opts
* @returns {object[]} next buffer
*/
function applyFetchedEntries(entries, normalized, opts) {
const paused = !!(opts && opts.paused);
const resumeMerge = !!(opts && opts.resumeMerge);
const rowLimit = (opts && opts.rowLimit) || 0;
const fetchLinesMax = (opts && opts.fetchLinesMax) || rowLimit;
const cap = ingestCap(paused, rowLimit, fetchLinesMax);
const merge = paused || resumeMerge;
let next;
if (merge)
next = mergeById(entries, normalized, cap);
else
next = (normalized || []).slice(-cap);
if (!paused && next.length > rowLimit)
next = next.slice(-rowLimit);
return next;
}
return baseclass.extend({
ingestCap: ingestCap,
mergeById: mergeById,
applyFetchedEntries: applyFetchedEntries
});
@@ -0,0 +1,121 @@
'use strict';
'require baseclass'; /* LuCI require() needs Class.isSubclass — plain return {} fails */
'require fwlive.log as log';
/**
* Filter-chip DOM renderer for luci-app-fwlive.
*
* renderFilterChips(host, state, callbacks) → void
* host - container element (cleared and rebuilt; element itself is kept)
* state - shallow copy: { filters, chipFields }
* callbacks - { onInvert(field, ev), onClear(field, ev), onClearAll(ev) }
*
* Chips use the labels presentation (include: "is", exclude: "not" + light ≠).
* Modules must not mutate state. host is cleared then rebuilt (idempotent replace).
*/
function chipValueNodes(field, val) {
const p = log.parseFilterValue(val);
if (!p.value)
return [ '' ];
const valueNode = p.negate
? E('span', { 'class': 'fwlive-chip-strike' }, [ p.value ])
: p.value;
if (!p.negate) {
return [
E('span', { 'class': 'fwlive-chip-polarity' }, [ _('is') ]),
' ',
log.formatFilterChipLabel(field, val)
];
}
if (field === 'q' || field === 'src' || field === 'dst')
return [
field + ': ',
E('strong', { 'class': 'fwlive-chip-not' }, [ _('not') ]),
' contains ',
valueNode
];
return [
field + ': ',
E('strong', { 'class': 'fwlive-chip-not' }, [ _('not') ]),
' ',
valueNode
];
}
function chipLeadingSym(negated) {
if (!negated)
return null;
return E('span', {
'class': 'fwlive-chip-sym fwlive-chip-sym-light',
'aria-hidden': 'true'
}, [ '≠' ]);
}
function renderFilterChips(host, state, callbacks) {
const filters = state.filters || {};
const chipFields = state.chipFields || [];
const chips = [];
for (let i = 0; i < chipFields.length; i++) {
const spec = chipFields[i];
const val = filters[spec.key];
if (!val)
continue;
const parsed = log.parseFilterValue(val);
const negated = parsed.negate;
const kids = [];
const lead = chipLeadingSym(negated);
if (lead)
kids.push(lead);
kids.push(E('span', { 'class': 'fwlive-chip-label' }, chipValueNodes(spec.label, val)));
kids.push(E('span', {
'class': 'fwlive-chip-invert-wrap',
'data-tip': negated ? _('Include instead') : _('Exclude instead')
}, [
E('button', {
'type': 'button',
'class': 'fwlive-chip-invert',
'click': function(ev) { callbacks.onInvert(spec.key, ev); }
}, [ '≠' ])
]));
kids.push(E('a', {
'href': '#',
'class': 'fwlive-chip-remove',
'title': _('Remove filter'),
'click': function(ev) { callbacks.onClear(spec.key, ev); }
}, [ '×' ]));
chips.push(E('span', {
'class': 'fwlive-chip'
+ (negated ? ' fwlive-chip-negated' : ' fwlive-chip-include')
}, kids));
}
host.className = 'fwlive-chips fwlive-chips-labels';
host.innerHTML = '';
if (!chips.length) {
host.style.display = 'none';
return;
}
host.style.display = 'flex';
for (let i = 0; i < chips.length; i++)
host.appendChild(chips[i]);
host.appendChild(E('a', {
'href': '#',
'class': 'fwlive-chip-clear',
'click': function(ev) { callbacks.onClearAll(ev); }
}, [ _('Clear all') ]));
}
return baseclass.extend({
renderFilterChips: renderFilterChips
});
@@ -0,0 +1,24 @@
'use strict';
'require baseclass';
/**
* Shared view constants for luci-app-fwlive.
* LuCI modules must return baseclass.extend(...) — plain objects fail Class.isSubclass.
*/
return baseclass.extend({
/* Keep in sync with openwrt-feed/luci-app-fwlive/Makefile PKG_VERSION. */
APP_VERSION: '0.1.37',
ROW_LIMIT_OPTIONS: [ 25, 50, 100, 250, 500, 1000, 2000 ],
DEFAULT_ROW_LIMIT: 100,
/* Row pass/deny tint (#40): classic green/red default; accessible teal/orange */
ROW_TINT_OPTIONS: [ 'off', 'classic', 'accessible' ],
DEFAULT_ROW_TINT: 'classic',
FETCH_LINES_MAX: 2000, /* ubus poll / logd ring cap (~2000 lines ≈ typical ring) */
/* DOM budget: ~250 new/updated rows painted per second on typical LuCI routers */
RENDER_CAP_PER_SEC: 250,
VIEW_MODES: [ 'simple', 'detailed' ],
COLUMN_SETS: {
simple: [ 'action', 'time', 'iface', 'flow', 'proto', 'rule' ],
detailed: [ 'time', 'action', 'rule', 'iface_in', 'iface_out', 'dir', 'proto', 'src', 'sport', 'dst', 'dport', 'flags', 'len', 'message' ]
}
});
File diff suppressed because one or more lines are too long
@@ -0,0 +1,779 @@
.fwlive-map {
max-width: none;
width: 100%;
--fwlive-pass-color: var(--success-color-high, var(--success-color, #46a546));
--fwlive-deny-color: var(--error-color-high, var(--error-color, #ca3c3c));
--fwlive-bg-medium: var(--background-color-medium, var(--white-color-low, #f9f9f9));
--fwlive-paused-color: #b45309;
}
/* Wide page in Detail only — Simple keeps LuCIs 940px column. */
#maincontent.container:has(.fwlive-map[data-view="detailed"]) {
max-width: none;
padding-left: 16px;
padding-right: 16px;
}
.fwlive-map[data-row-tint="accessible"] {
--fwlive-pass-color: #0d9488;
--fwlive-deny-color: #c2410c;
}
.fwlive-map.fwlive-watch-paused .fwlive-title-row,
.fwlive-map.fwlive-watch-paused .fwlive-watch-strip {
background: rgba(180, 83, 9, 0.08);
background: color-mix(in srgb, var(--fwlive-paused-color) 8%, transparent);
}
.fwlive-map.fwlive-watch-paused .fwlive-title-row {
margin: 0 -8px;
padding: 6px 8px 0;
border-radius: 3px 3px 0 0;
}
.fwlive-map.fwlive-watch-paused .fwlive-watch-strip {
margin: 0 -8px 10px;
padding: 8px;
border-radius: 0 0 3px 3px;
border-bottom-color: transparent;
}
.fwlive-title-row {
display: flex;
flex-wrap: wrap;
align-items: baseline;
justify-content: space-between;
gap: 8px 16px;
margin: 0 0 4px;
}
.fwlive-title-row h2 {
margin: 0;
flex: 0 1 auto;
}
.fwlive-title-status {
display: inline-flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
min-width: 0;
margin-left: auto;
}
.fwlive-watch-strip {
display: flex;
flex-wrap: wrap;
align-items: stretch;
justify-content: flex-start;
gap: 0;
min-height: 40px;
padding: 8px 0 10px;
border-bottom: 1px solid var(--border-color-medium);
margin-bottom: 10px;
}
.fwlive-watch-group {
display: flex;
align-items: center;
gap: 6px;
min-height: 28px;
padding: 0 10px;
border-left: 1px solid var(--border-color-medium);
flex-wrap: wrap;
}
.fwlive-watch-group:last-child {
padding-right: 0;
}
.fwlive-watch-group-label {
margin-right: 2px;
color: var(--text-color-medium);
font-size: 0.72em;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
white-space: nowrap;
}
.fwlive-watch-seg {
display: inline-flex;
align-items: stretch;
border: 1px solid var(--border-color-medium);
border-radius: 3px;
overflow: hidden;
}
.fwlive-watch-seg .fwlive-seg-btn {
margin: 0;
border: 0;
border-radius: 0;
border-right: 1px solid var(--border-color-medium);
background: transparent;
padding: 4px 10px;
font-size: 0.88em;
line-height: 1.25;
min-height: 28px;
}
.fwlive-watch-seg .fwlive-seg-btn:last-child {
border-right: 0;
}
.fwlive-watch-seg .fwlive-seg-btn[aria-pressed="true"] {
background: rgba(26, 111, 156, 0.12);
background: color-mix(in srgb, var(--primary-color-high) 12%, transparent);
color: var(--primary-color-high);
font-weight: 600;
}
.fwlive-watch-label { font-weight: 600; }
.fwlive-watch-paused .fwlive-watch-label { color: var(--fwlive-paused-color); }
.fwlive-map.fwlive-watch-paused .fwlive-title-status .fwlive-watch-label { color: var(--fwlive-paused-color); }
.fwlive-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--text-color-low, var(--border-color-high));
display: inline-block;
flex-shrink: 0;
}
.fwlive-dot.fwlive-dot-on {
background: var(--fwlive-pass-color);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--fwlive-pass-color) 22%, transparent);
}
.fwlive-btn-ghost {
background: transparent;
border-color: var(--border-color-medium);
}
.fwlive-btn-quiet {
background: transparent;
border-color: var(--border-color-medium);
color: var(--text-color-medium);
font-weight: 500;
}
.fwlive-log-merged {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 10px;
border: 1px solid var(--border-color-medium);
border-radius: 3px;
background: var(--background-color-medium, var(--white-color-low, var(--background-color-high)));
color: var(--text-color-high);
font-weight: 500;
font-size: 0.88em;
line-height: 1.25;
min-height: 28px;
cursor: pointer;
}
.fwlive-log-merged:hover {
background: var(--background-color-low, var(--background-color-medium));
}
.fwlive-log-merged:disabled {
opacity: 0.65;
cursor: default;
}
.fwlive-log-on-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--fwlive-pass-color);
flex-shrink: 0;
}
.fwlive-log-rate {
color: var(--text-color-medium);
font-weight: 400;
}
.fwlive-display-bar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px 14px;
margin: 0 0 12px;
padding: 8px 12px;
border: 1px solid var(--border-color-medium);
border-radius: 3px;
background: var(--background-color-high);
}
.fwlive-display-bar-label {
flex: 0 0 auto;
font-weight: 600;
font-size: 0.92em;
color: var(--text-color-high);
white-space: nowrap;
}
.fwlive-display-controls {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px 14px;
min-width: 0;
flex: 1 1 auto;
}
.fwlive-display-ctl {
display: inline-flex;
align-items: center;
gap: 6px;
margin: 0;
font-size: 0.92em;
white-space: nowrap;
}
.fwlive-display-ctl input[type="checkbox"] {
margin: 0;
flex-shrink: 0;
}
.fwlive-display-ctl select {
margin-left: 0;
}
.fwlive-display-ctl.fwlive-hidden {
display: none;
}
/* Draw our own disclosure arrow — themes that restyle summary drop the native marker. */
.fwlive-more-filters > summary,
.fwlive-help > summary {
cursor: pointer;
list-style: none;
}
.fwlive-more-filters > summary::-webkit-details-marker,
.fwlive-help > summary::-webkit-details-marker { display: none; }
.fwlive-more-filters > summary::before,
.fwlive-help > summary::before {
content: "▸ ";
color: var(--text-color-medium);
}
.fwlive-more-filters[open] > summary::before,
.fwlive-help[open] > summary::before { content: "▾ "; }
.fwlive-ctl {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 0.92em;
flex-wrap: wrap;
}
.fwlive-ctl select { margin-left: 4px; }
#fwlive-limit { width: auto; min-width: 4.5em; }
.fwlive-grid { display: grid; grid-template-columns: repeat(4, minmax(140px, 1fr)); gap: 8px; margin-bottom: 12px; }
.fwlive-status { margin: 0; color: var(--text-color-medium); font-size: 0.9em; }
.fwlive-status-paused { color: var(--warn-color-high); font-weight: 600; }
.fwlive-status-error { color: var(--warn-color-high); font-weight: 600; }
.fwlive-hint-line {
margin: 0 0 10px;
font-size: 0.85em;
color: var(--text-color-medium);
}
.fwlive-empty-title {
font-weight: 600;
margin: 0 0 6px;
color: var(--text-color-high);
}
.fwlive-find-row .fwlive-grid-core { margin-bottom: 0; }
.fwlive-proto-pair {
display: grid;
grid-template-columns: minmax(0, 1.15fr) minmax(0, 0.85fr);
gap: 6px;
align-items: stretch;
min-width: 0;
}
.fwlive-proto-pair select,
.fwlive-proto-pair input {
width: 100%;
min-width: 0;
}
@media (max-width: 520px) {
.fwlive-proto-pair { grid-template-columns: 1fr; }
}
.fwlive-scroll {
max-height: 78vh;
max-height: 78dvh;
overflow: auto;
border: 1px solid var(--border-color-medium);
border-radius: 3px;
background: var(--background-color-high);
width: 100%;
scrollbar-gutter: stable;
}
#fwlive-table { margin: 0; width: 100%; table-layout: auto; border-collapse: collapse; }
.fwlive-map[data-view="simple"] #fwlive-table { table-layout: fixed; width: 100%; }
.fwlive-map[data-view="simple"] col.fwlive-col-action { width: 5rem; }
.fwlive-map[data-view="simple"] col.fwlive-col-time { width: 5rem; }
.fwlive-map[data-view="simple"] col.fwlive-col-iface { width: 5.5rem; }
.fwlive-map[data-view="simple"] col.fwlive-col-proto { width: 4rem; }
.fwlive-map[data-view="simple"] col.fwlive-col-rule { width: 12rem; }
.fwlive-map[data-view="detailed"] .fwlive-scroll #fwlive-table {
width: max-content;
min-width: 100%;
}
.fwlive-map[data-view="detailed"] col.fwlive-col-time { width: 11rem; }
.fwlive-map[data-view="detailed"] col.fwlive-col-action { width: 4.5rem; }
.fwlive-map[data-view="detailed"] col.fwlive-col-rule { width: 8rem; }
.fwlive-map[data-view="detailed"] col.fwlive-col-iface-in,
.fwlive-map[data-view="detailed"] col.fwlive-col-iface-out { width: 3.5rem; }
.fwlive-map[data-view="detailed"] col.fwlive-col-dir { width: 3rem; }
.fwlive-map[data-view="detailed"] col.fwlive-col-proto { width: 3.25rem; }
.fwlive-map[data-view="detailed"] col.fwlive-col-src,
.fwlive-map[data-view="detailed"] col.fwlive-col-dst { width: 9rem; }
.fwlive-map[data-view="detailed"] col.fwlive-col-sport,
.fwlive-map[data-view="detailed"] col.fwlive-col-dport { width: 3.75rem; }
.fwlive-map[data-view="detailed"] col.fwlive-col-flags { width: 5rem; }
.fwlive-map[data-view="detailed"] col.fwlive-col-len { width: 3.25rem; }
.fwlive-scroll.fwlive-msg-oneline #fwlive-table {
width: max-content;
min-width: 100%;
}
.fwlive-scroll.fwlive-msg-oneline tbody td { vertical-align: middle; }
#fwlive-table thead th {
position: sticky;
top: 0;
z-index: 2;
background: var(--background-color-low);
border-bottom: 2px solid var(--border-color-high);
white-space: nowrap;
padding: 5px 8px;
font-size: 0.9em;
font-weight: 600;
vertical-align: top;
text-align: left;
}
#fwlive-table thead th.fwlive-port,
#fwlive-table thead th.fwlive-len {
text-align: right;
}
.fwlive-map[data-view="simple"] #fwlive-table thead th,
.fwlive-map[data-view="simple"] #fwlive-table tbody td {
vertical-align: top;
}
#fwlive-table thead th.fwlive-action {
font-weight: 700;
text-transform: lowercase;
}
#fwlive-table tbody td {
padding: 5px 8px;
border-bottom: 1px solid var(--border-color-low);
vertical-align: top;
font-size: 0.92em;
}
.fwlive-row-alt td {
background: #f9f9f9;
background: var(--fwlive-bg-medium);
}
.fwlive-time, .fwlive-addr, .fwlive-port, .fwlive-len {
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
white-space: nowrap;
}
.fwlive-port, .fwlive-len { text-align: right; }
.fwlive-rule, .fwlive-iface, .fwlive-proto, .fwlive-dir, .fwlive-flags { white-space: nowrap; }
.fwlive-rule { font-size: 0.88em; }
.fwlive-rule-link {
color: var(--primary-color-high);
text-decoration: none;
font-weight: 500;
}
.fwlive-rule-link:hover { text-decoration: underline; }
.fwlive-iface-badge {
display: inline-block;
padding: 1px 6px;
background: var(--background-color-low);
border: 1px solid var(--border-color-medium);
border-radius: 3px;
font-size: 0.85em;
text-decoration: none;
}
.fwlive-iface-badge:hover {
background: var(--background-color-medium);
border-color: var(--primary-color-medium);
}
.fwlive-action {
font-weight: 700;
text-transform: lowercase;
white-space: nowrap;
}
.fwlive-deny { color: var(--fwlive-deny-color); }
.fwlive-pass { color: var(--fwlive-pass-color); }
#fwlive-table td.fwlive-action.fwlive-pass,
#fwlive-table td.fwlive-action.fwlive-pass a.fwlive-filter-link {
color: var(--fwlive-pass-color);
}
#fwlive-table td.fwlive-action.fwlive-deny,
#fwlive-table td.fwlive-action.fwlive-deny a.fwlive-filter-link {
color: var(--fwlive-deny-color);
}
.fwlive-map[data-row-tint="classic"] #fwlive-table tbody tr.fwlive-row-pass td {
background: rgba(70, 165, 70, 0.12);
background: color-mix(in srgb, var(--fwlive-pass-color) 12%, transparent);
}
.fwlive-map[data-row-tint="classic"] #fwlive-table tbody tr.fwlive-row-pass.fwlive-row-alt td {
background: rgba(70, 165, 70, 0.18);
background: color-mix(in srgb, var(--fwlive-pass-color) 12%, var(--fwlive-bg-medium));
}
.fwlive-map[data-row-tint="classic"] #fwlive-table tbody tr.fwlive-row-deny td {
background: rgba(202, 60, 60, 0.12);
background: color-mix(in srgb, var(--fwlive-deny-color) 12%, transparent);
}
.fwlive-map[data-row-tint="classic"] #fwlive-table tbody tr.fwlive-row-deny.fwlive-row-alt td {
background: rgba(202, 60, 60, 0.18);
background: color-mix(in srgb, var(--fwlive-deny-color) 12%, var(--fwlive-bg-medium));
}
.fwlive-map[data-row-tint="classic"] #fwlive-table tbody tr.fwlive-row-pass:hover td {
background: rgba(70, 165, 70, 0.20);
background: color-mix(in srgb, var(--fwlive-pass-color) 20%, transparent);
}
.fwlive-map[data-row-tint="classic"] #fwlive-table tbody tr.fwlive-row-deny:hover td {
background: rgba(202, 60, 60, 0.20);
background: color-mix(in srgb, var(--fwlive-deny-color) 20%, transparent);
}
.fwlive-map[data-row-tint="classic"][data-tint-fallback="1"] #fwlive-table tbody tr.fwlive-row-pass td {
background: rgba(70, 165, 70, 0.12);
}
.fwlive-map[data-row-tint="classic"][data-tint-fallback="1"] #fwlive-table tbody tr.fwlive-row-pass.fwlive-row-alt td {
background: rgba(70, 165, 70, 0.18);
}
.fwlive-map[data-row-tint="classic"][data-tint-fallback="1"] #fwlive-table tbody tr.fwlive-row-deny td {
background: rgba(202, 60, 60, 0.12);
}
.fwlive-map[data-row-tint="classic"][data-tint-fallback="1"] #fwlive-table tbody tr.fwlive-row-deny.fwlive-row-alt td {
background: rgba(202, 60, 60, 0.18);
}
.fwlive-map[data-row-tint="classic"][data-tint-fallback="1"] #fwlive-table tbody tr.fwlive-row-pass:hover td {
background: rgba(70, 165, 70, 0.20);
}
.fwlive-map[data-row-tint="classic"][data-tint-fallback="1"] #fwlive-table tbody tr.fwlive-row-deny:hover td {
background: rgba(202, 60, 60, 0.20);
}
.fwlive-map[data-row-tint="accessible"] #fwlive-table tbody tr.fwlive-row-pass td {
background: rgba(13, 148, 136, 0.12);
background: color-mix(in srgb, var(--fwlive-pass-color) 12%, transparent);
}
.fwlive-map[data-row-tint="accessible"] #fwlive-table tbody tr.fwlive-row-pass.fwlive-row-alt td {
background: rgba(13, 148, 136, 0.18);
background: color-mix(in srgb, var(--fwlive-pass-color) 12%, var(--fwlive-bg-medium));
}
.fwlive-map[data-row-tint="accessible"] #fwlive-table tbody tr.fwlive-row-deny td {
background: rgba(194, 65, 12, 0.12);
background: color-mix(in srgb, var(--fwlive-deny-color) 12%, transparent);
}
.fwlive-map[data-row-tint="accessible"] #fwlive-table tbody tr.fwlive-row-deny.fwlive-row-alt td {
background: rgba(194, 65, 12, 0.18);
background: color-mix(in srgb, var(--fwlive-deny-color) 12%, var(--fwlive-bg-medium));
}
.fwlive-map[data-row-tint="accessible"] #fwlive-table tbody tr.fwlive-row-pass:hover td {
background: rgba(13, 148, 136, 0.20);
background: color-mix(in srgb, var(--fwlive-pass-color) 20%, transparent);
}
.fwlive-map[data-row-tint="accessible"] #fwlive-table tbody tr.fwlive-row-deny:hover td {
background: rgba(194, 65, 12, 0.20);
background: color-mix(in srgb, var(--fwlive-deny-color) 20%, transparent);
}
.fwlive-map[data-row-tint="accessible"][data-tint-fallback="1"] #fwlive-table tbody tr.fwlive-row-pass td {
background: rgba(13, 148, 136, 0.12);
}
.fwlive-map[data-row-tint="accessible"][data-tint-fallback="1"] #fwlive-table tbody tr.fwlive-row-pass.fwlive-row-alt td {
background: rgba(13, 148, 136, 0.18);
}
.fwlive-map[data-row-tint="accessible"][data-tint-fallback="1"] #fwlive-table tbody tr.fwlive-row-deny td {
background: rgba(194, 65, 12, 0.12);
}
.fwlive-map[data-row-tint="accessible"][data-tint-fallback="1"] #fwlive-table tbody tr.fwlive-row-deny.fwlive-row-alt td {
background: rgba(194, 65, 12, 0.18);
}
.fwlive-map[data-row-tint="accessible"][data-tint-fallback="1"] #fwlive-table tbody tr.fwlive-row-pass:hover td {
background: rgba(13, 148, 136, 0.20);
}
.fwlive-map[data-row-tint="accessible"][data-tint-fallback="1"] #fwlive-table tbody tr.fwlive-row-deny:hover td {
background: rgba(194, 65, 12, 0.20);
}
.fwlive-unknown { color: var(--text-color-medium); font-weight: 500; }
.fwlive-message {
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 0.85em;
color: var(--text-color-high);
}
.fwlive-scroll.fwlive-msg-wrap .fwlive-message-wrap {
display: block;
min-width: 16em;
max-width: 42em;
white-space: normal;
word-break: break-word;
word-wrap: break-word;
}
.fwlive-scroll.fwlive-msg-oneline .fwlive-message {
white-space: nowrap;
max-width: none;
min-width: 32em;
}
#fwlive-table th.fwlive-th-message,
.fwlive-scroll.fwlive-msg-oneline .fwlive-message {
width: 99%;
}
.fwlive-empty {
margin: 12px 0;
padding: 10px;
background: var(--background-color-medium);
border: 1px dashed var(--border-color-high);
}
.fwlive-empty-muted {
font-size: 0.92em;
color: var(--text-color-medium);
margin: 0 0 8px;
}
.fwlive-consent {
margin: 0 0 12px;
padding: 10px 12px;
background: var(--background-color-high);
border: 1px solid var(--border-color-medium);
border-radius: 3px;
}
.fwlive-consent-list {
margin: 0 0 10px;
padding-left: 1.25em;
}
.fwlive-consent-list li {
margin: 0 0 4px;
}
.fwlive-consent-check {
margin: 0 0 10px;
font-size: 0.92em;
}
.fwlive-consent-actions {
margin: 0;
}
.fwlive-logging-bar {
display: contents;
}
.fwlive-logging-status {
font-size: 0.92em;
color: var(--text-color-medium);
margin-right: 4px;
}
.fwlive-logging-notice {
color: var(--fwlive-pass-color);
}
.fwlive-filter-link {
color: inherit;
text-decoration: underline;
text-decoration-style: dotted;
cursor: pointer;
}
.fwlive-filter-link:hover { color: var(--primary-color-high); }
.fwlive-chips {
display: none;
flex-wrap: wrap;
align-items: center;
gap: 6px;
margin: 0 0 10px;
}
.fwlive-chip-label {
line-height: 1.3;
}
.fwlive-chip-not {
font-weight: 700;
}
.fwlive-chip {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 8px;
background: var(--background-color-low);
border: 1px solid var(--border-color-medium);
border-radius: 3px;
font-size: 0.88em;
}
.fwlive-chip-negated {
border-style: dashed;
background: var(--background-color-medium);
}
.fwlive-chip-include {
}
.fwlive-chip-polarity {
font-weight: 700;
font-size: 0.78em;
text-transform: uppercase;
letter-spacing: 0.03em;
color: var(--text-color-medium);
}
.fwlive-chip-strike {
text-decoration: line-through;
text-decoration-thickness: 1.5px;
}
.fwlive-chip-sym {
font-weight: 700;
line-height: 1;
min-width: 0.9em;
text-align: center;
}
.fwlive-chip-sym-light {
color: var(--text-color-medium);
font-size: 0.95em;
}
#fwlive-row-tint { width: auto; min-width: 9.5em; }
/* labels (default) */
.fwlive-chips-labels .fwlive-chip-include {
border-left: 3px solid var(--primary-color-medium, var(--primary-color-high, var(--border-color-high)));
}
.fwlive-chips-labels .fwlive-chip-negated .fwlive-chip-not {
color: var(--text-color-high);
}
.fwlive-chip-invert-wrap {
position: relative;
display: inline-flex;
}
.fwlive-chip-invert-wrap::before {
content: attr(data-tip);
position: absolute;
bottom: calc(100% + 6px);
left: 50%;
transform: translateX(-50%);
white-space: nowrap;
padding: 4px 8px;
font-size: 0.85em;
font-weight: normal;
color: var(--text-color-high);
background: var(--background-color-high);
border: 1px solid var(--border-color-medium);
border-radius: 3px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.12);
opacity: 0;
visibility: hidden;
pointer-events: none;
z-index: 20;
transition: opacity 0.15s ease 1.5s, visibility 0s linear 2s;
}
.fwlive-chip-invert-wrap:hover::before,
.fwlive-chip-invert-wrap:focus-within::before {
opacity: 1;
visibility: visible;
transition: opacity 0.12s ease 0.35s, visibility 0s;
}
.fwlive-chip-invert {
color: var(--text-color-medium);
background: none;
border: none;
padding: 0;
font: inherit;
font-weight: 700;
line-height: 1;
cursor: pointer;
}
.fwlive-chip-invert:hover { color: var(--primary-color-high); }
.fwlive-chip-remove {
color: var(--text-color-medium);
text-decoration: none;
font-weight: 700;
line-height: 1;
}
.fwlive-chip-remove:hover { color: var(--fwlive-deny-color); }
.fwlive-chip-clear {
font-size: 0.88em;
margin-left: 4px;
}
.fwlive-flood {
display: none;
margin: 0 0 10px;
padding: 8px 12px;
background: var(--background-color-low);
border: 1px solid var(--warn-color-high);
border-radius: 3px;
color: var(--text-color-high);
font-size: 0.92em;
}
.fwlive-tint-warn {
display: none;
color: var(--warn-color-high);
font-size: 0.78em;
font-weight: 600;
white-space: nowrap;
padding: 2px 8px;
border: 1px solid var(--warn-color-high);
border-radius: 999px;
background: color-mix(in srgb, var(--warn-color-high) 10%, transparent);
}
.fwlive-map[data-view="simple"] #fwlive-msg-group { display: none; }
@media (max-width: 850px) {
.fwlive-title-status {
flex-basis: 100%;
margin-left: 0;
}
.fwlive-watch-group {
border-left: 0;
padding-left: 0;
padding-right: 10px;
}
.fwlive-display-bar {
align-items: flex-start;
}
}
.fwlive-help-row {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 12px;
margin: 10px 0 0;
}
.fwlive-help {
margin: 0;
font-size: 0.92em;
color: var(--text-color-medium);
min-width: 0;
flex: 1;
}
.fwlive-help ul { margin: 6px 0 0 1.2em; padding: 0; }
.fwlive-help li { margin: 4px 0; }
.fwlive-build {
flex-shrink: 0;
font-size: 0.78em;
color: var(--text-color-low, var(--text-color-medium));
font-variant-numeric: tabular-nums;
user-select: all;
white-space: nowrap;
}
.fwlive-backend {
font-size: 0.65em;
font-weight: normal;
color: var(--text-color-medium);
margin-left: 8px;
vertical-align: middle;
}
.fwlive-filter-panel { margin-bottom: 12px; }
.fwlive-map[data-view="simple"] .fwlive-grid-core {
display: grid;
grid-template-columns: repeat(3, minmax(140px, 1fr));
gap: 8px;
}
.fwlive-map[data-view="simple"] .fwlive-more-filters { margin-top: 8px; }
.fwlive-map[data-view="simple"] .fwlive-grid-extra {
display: grid;
grid-template-columns: repeat(4, minmax(140px, 1fr));
gap: 8px;
margin-top: 8px;
}
.fwlive-map[data-view="detailed"] .fwlive-more-filters > summary { display: none; }
.fwlive-map[data-view="detailed"] .fwlive-filter-panel {
display: grid;
grid-template-columns: repeat(4, minmax(140px, 1fr));
gap: 8px;
}
.fwlive-map[data-view="detailed"] .fwlive-grid-core,
.fwlive-map[data-view="detailed"] .fwlive-grid-extra,
.fwlive-map[data-view="detailed"] .fwlive-more-filters { display: contents; }
.fwlive-row-clickable { cursor: pointer; }
.fwlive-row-expanded td { border-bottom-color: transparent; }
.fwlive-msg-expand td {
background: var(--background-color-low);
padding: 8px 12px 10px;
border-bottom: 1px solid var(--border-color-medium);
}
.fwlive-msg-expand-label {
font-size: 0.82em;
color: var(--text-color-medium);
margin-bottom: 4px;
font-weight: 600;
}
.fwlive-msg-expand-body {
margin: 0;
white-space: pre-wrap;
word-break: break-word;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 0.85em;
color: var(--text-color-high);
}
.fwlive-flow-arrow {
color: var(--text-color-low);
font-weight: 700;
}
.fwlive-flow-cell { white-space: nowrap; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 0.9em; }
.fwlive-map[data-view="simple"] #fwlive-table td.fwlive-action.fwlive-pass,
.fwlive-map[data-view="simple"] #fwlive-table td.fwlive-action.fwlive-pass a.fwlive-filter-link {
color: var(--fwlive-pass-color);
}
.fwlive-map[data-view="simple"] #fwlive-table td.fwlive-action.fwlive-deny,
.fwlive-map[data-view="simple"] #fwlive-table td.fwlive-action.fwlive-deny a.fwlive-filter-link {
color: var(--fwlive-deny-color);
}
.fwlive-iface-badge {
border-radius: 10px;
padding: 2px 8px;
}
@@ -0,0 +1,60 @@
'use strict';
'require baseclass';
/**
* Hostname resolve cache helpers (LRU + failure TTL).
* Pure Map helpers — safe to unit-test on host without LuCI.
*/
return baseclass.extend({
CACHE_MAX: 1000,
FAIL_TTL_MS: 60000,
FAIL_MAX: 1000,
/* Touch-on-write LRU: re-insert moves key to newest; evict oldest when over max. */
lruSet: function(map, key, value, max) {
const cap = max || this.CACHE_MAX;
if (map.has(key))
map.delete(key);
map.set(key, value);
while (map.size > cap) {
const oldest = map.keys().next().value;
map.delete(oldest);
}
return map;
},
lruGet: function(map, key) {
if (!map.has(key))
return undefined;
const value = map.get(key);
map.delete(key);
map.set(key, value);
return value;
},
failIsHot: function(failedMap, ip, nowMs, ttlMs) {
if (!failedMap || !failedMap.has(ip))
return false;
const at = failedMap.get(ip);
const ttl = ttlMs == null ? this.FAIL_TTL_MS : ttlMs;
const now = nowMs == null ? Date.now() : nowMs;
if ((now - at) >= ttl) {
failedMap.delete(ip);
return false;
}
return true;
},
failMark: function(failedMap, ip, nowMs, max) {
const cap = max || this.FAIL_MAX;
const now = nowMs == null ? Date.now() : nowMs;
if (failedMap.has(ip))
failedMap.delete(ip);
failedMap.set(ip, now);
while (failedMap.size > cap) {
const oldest = failedMap.keys().next().value;
failedMap.delete(oldest);
}
return failedMap;
}
});
@@ -0,0 +1,151 @@
'use strict';
'require baseclass'; /* LuCI require() needs Class.isSubclass — plain return {} fails */
'require fwlive.log as log';
/**
* Link-builder helpers for luci-app-fwlive.
*
* Pure helpers (no filter side-effects):
* luciUrl, firewallZonesPath, firewallZonesUrl, firewallZonesLink
*
* Filter-aware helpers (require an onFilterClick callback):
* filterLink, addrFilterLink, ruleAdminPath, ruleAdminLink, ifaceLink
*
* No host element — all functions return DOM nodes or strings.
* May require fwlive.log for formatCell.
*/
function luciUrl(path) {
if (typeof L !== 'undefined' && L.url)
return L.url(path);
return '/cgi-bin/luci/' + path;
}
function firewallZonesPath() {
return 'admin/network/firewall/zones';
}
function firewallZonesUrl() {
return luciUrl(firewallZonesPath());
}
function firewallZonesLink(label) {
return E('a', {
'href': firewallZonesUrl(),
'class': 'fwlive-filter-link'
}, [ label || _('Network → Firewall') ]);
}
/**
* @param {string} field - filter field name
* @param {string} value - filter value
* @param {string} [label] - display label (defaults to value)
* @param {function} onFilterClick - callback(field, value, ev)
*/
function filterLink(field, value, label, onFilterClick) {
if (!value)
return log.formatCell(value);
return E('a', {
'href': '#',
'class': 'fwlive-filter-link',
'title': _('Filter by %s').format(field),
'click': function(ev) { onFilterClick(field, value, ev); }
}, [ label || value ]);
}
/**
* @param {string} field - filter field ('src' or 'dst')
* @param {string} ip - IP address
* @param {boolean} showHostnames
* @param {Map|null} hostnameCache
* @param {function} onFilterClick - callback(field, value, ev)
*/
function addrFilterLink(field, ip, showHostnames, hostnameCache, onFilterClick) {
if (!ip)
return log.formatCell(ip);
const name = showHostnames && hostnameCache ? hostnameCache.get(ip) : null;
const display = name || ip;
const title = name ? ip : _('Filter by %s').format(field);
return E('a', {
'href': '#',
'class': 'fwlive-filter-link',
'title': title,
'click': function(ev) { onFilterClick(field, ip, ev); }
}, [ display ]);
}
/**
* @param {string} hint - rule hint token
* @param {string} firewallBackend - 'nft' or 'iptables'
*/
function ruleAdminPath(hint, firewallBackend) {
if (hint === 'fw4')
return 'admin/network/firewall/rules';
if (firewallBackend === 'iptables')
return 'admin/status/iptables';
return 'admin/status/nftables';
}
/**
* @param {string} hint - rule hint token
* @param {string} label - display label
* @param {string} firewallBackend - 'nft' or 'iptables'
* @param {function} onFilterClick - callback(field, value, ev)
*/
function ruleAdminLink(hint, label, firewallBackend, onFilterClick) {
if (!hint)
return log.formatCell(hint);
const path = ruleAdminPath(hint, firewallBackend);
const url = '%s#%s'.format(luciUrl(path), encodeURIComponent(hint));
const text = label || hint;
return E('a', {
'href': '#',
'class': 'fwlive-filter-link fwlive-rule-link',
'title': _('Filter logs by rule (hint: %s). Ctrl+click to open firewall settings.').format(hint),
'click': function(ev) {
if (ev && (ev.ctrlKey || ev.metaKey)) {
if (ev.preventDefault)
ev.preventDefault();
window.location = url;
return;
}
onFilterClick('q', hint, ev);
}
}, [ text ]);
}
/**
* @param {string} value - interface name
* @param {function} onFilterClick - callback(field, value, ev)
*/
function ifaceLink(value, onFilterClick) {
if (!value)
return log.formatCell(value);
return E('a', {
'href': '#',
'class': 'fwlive-filter-link fwlive-iface-badge',
'title': _('Filter by interface'),
'click': function(ev) { onFilterClick('interface', value, ev); }
}, [ value ]);
}
return baseclass.extend({
luciUrl: luciUrl,
firewallZonesPath: firewallZonesPath,
firewallZonesUrl: firewallZonesUrl,
firewallZonesLink: firewallZonesLink,
filterLink: filterLink,
addrFilterLink: addrFilterLink,
ruleAdminPath: ruleAdminPath,
ruleAdminLink: ruleAdminLink,
ifaceLink: ifaceLink
});
@@ -0,0 +1,490 @@
'use strict';
'require baseclass';
/**
* Shared classify logic mirrors core/fwlive-log.js CLASSIFY_SPEC — keep in sync
* (gen-luci-wrapper.js gates full-spec drift; ./scripts/gen-all.sh verifies).
* LuCI-only helpers live in the @fwlive-codegen:luci-preserve region.
*/
return baseclass.extend({
CLASSIFY_SPEC: {
glueKeys: ['IN', 'OUT', 'SRC', 'DST', 'PROTO', 'SPT', 'DPT', 'LEN', 'MAC', 'TYPE', 'CODE', 'TTL', 'TOS', 'PREC', 'DF'],
nonFirewallPrefixes: ['dnsmasq', 'procd', 'ubusd', 'netifd', 'odhcpd', 'logd', 'dropbear', 'uhttpd', 'hostapd', 'wpad'],
firewallHints: ['fw4', 'nft', 'iptables', 'kernel', 'firewall'],
actionWords: ['ACCEPT', 'ALLOW', 'PASS', 'DROP', 'REJECT', 'DENY', 'BLOCK'],
rules: [
{ or: [
{ and: [ { kv: ['SRC'] }, { kv: ['DST'] } ] },
{ and: [ { kvAny: ['IN', 'OUT'] }, { kvAny: ['SRC', 'DST', 'PROTO', 'SPT', 'DPT'] } ] },
{ and: [ { action: 'known' }, { kvAny: ['IN', 'OUT', 'PROTO', 'SRC', 'DST'] } ] }
]},
{ and: [ { hint: true }, { action: 'known' } ] },
{ and: [ { hint: true }, { kvAny: ['IN', 'OUT', 'SRC', 'DST', 'PROTO'] } ] }
]
},
TCP_FLAG_TAIL: /\b(SYN|ACK|FIN|RST|PSH|URG)(?:\s+(?:SYN|ACK|FIN|RST|PSH|URG))*\s*$/i,
NETFILTER_KV_GLUE: /([^\s])(?=(IN|OUT|SRC|DST|PROTO|SPT|DPT|LEN|MAC|TYPE|CODE|TTL|TOS|PREC|DF)=)/g,
wordPattern: function(words) {
const alt = words.join('|');
return new RegExp('(^|[^A-Za-z0-9_])(' + alt + ')([^A-Za-z0-9_]|$)', 'i');
},
kvHas: function(msg, key) {
return new RegExp('(^|[^A-Za-z0-9_])' + key + '=').test(msg);
},
NON_FIREWALL_PREFIX: /^(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,
normalizeNetfilterMessage: function(message) {
return (message || '').replace(this.NETFILTER_KV_GLUE, '$1 ');
},
parseKeyValueLog: function(message) {
const out = {};
const re = /\b([A-Z]+)=([^\s]+)/g;
const normalized = this.normalizeNetfilterMessage(message);
let match;
while ((match = re.exec(normalized)) !== null)
out[match[1]] = match[2];
return out;
},
detectAction: function(message) {
const m = message.match(this.ACTION_RE);
return m ? m[2].toUpperCase() : 'UNKNOWN';
},
evaluateClassifySpec: function(message, actionRaw) {
const msg = this.normalizeNetfilterMessage(message || '');
const action = actionRaw === undefined ? this.detectAction(msg) : actionRaw;
const self = this;
const has = function(key) { return self.kvHas(msg, key); };
const hasAny = function(keys) {
for (let i = 0; i < keys.length; i++) {
if (has(keys[i]))
return true;
}
return false;
};
const pred = {
kv: function(c) {
for (let i = 0; i < c.kv.length; i++) {
if (!has(c.kv[i]))
return false;
}
return true;
},
kvAny: function(c) { return hasAny(c.kvAny); },
action: function(c) { return (c.action === 'known') ? action !== 'UNKNOWN' : true; },
hint: function() { return self.FIREWALL_HINT.test(msg); }
};
const evalNode = function(node) {
if (node.and) {
for (let i = 0; i < node.and.length; i++) {
if (!evalNode(node.and[i]))
return false;
}
return true;
}
if (node.or) {
for (let i = 0; i < node.or.length; i++) {
if (evalNode(node.or[i]))
return true;
}
return false;
}
const keys = Object.keys(node);
for (let i = 0; i < keys.length; i++) {
const k = keys[i];
if (pred[k])
return pred[k](node);
}
return false;
};
for (let i = 0; i < this.CLASSIFY_SPEC.rules.length; i++) {
if (evalNode(this.CLASSIFY_SPEC.rules[i]))
return true;
}
return false;
},
normalizeAction: function(raw) {
const a = (raw || '').toUpperCase();
const words = this.CLASSIFY_SPEC.actionWords;
const pass = words.slice(0, 3); /* ACCEPT|ALLOW|PASS */
const denyClass = words.slice(3); /* DROP|REJECT|DENY|BLOCK — positional */
if (pass.indexOf(a) >= 0)
return 'pass';
if (a === words[3]) /* DROP */
return 'drop';
if (a === words[4]) /* REJECT */
return 'reject';
if (denyClass.indexOf(a) >= 0) /* DENY|BLOCK */
return 'block';
return 'unknown';
},
parseRuleHint: function(message) {
let msg = this.normalizeNetfilterMessage(message || '').trim();
msg = msg.replace(/^\[\s*[\d.]+\]\s*/, '');
if (/^fw4:\s*/i.test(msg))
return 'fw4';
const beforeKv = msg.match(/^([A-Za-z0-9_.-]+)(?::|\s+)(?=IN=|OUT=|SRC=|DST=|PROTO=)/);
if (beforeKv)
return beforeKv[1];
const colon = msg.match(/^([A-Za-z0-9_.-]+):/);
if (colon) {
const tag = colon[1].toLowerCase();
if (tag !== 'kernel' && tag !== 'iptables')
return colon[1];
}
return '';
},
formatRuleLabel: function(hint) {
if (!hint)
return '';
if (hint === 'fw4')
return 'Firewall4';
return hint.replace(/-/g, ' ');
},
inferActionRaw: function(message, kv, actionRaw) {
if (actionRaw && actionRaw !== 'UNKNOWN')
return actionRaw;
const msg = this.normalizeNetfilterMessage(message || '');
const withoutKv = msg.replace(/\b[A-Z]+=[^\s]*/g, ' ');
if (this.DENY_ACTION.test(withoutKv))
return 'UNKNOWN';
if (/^kernel:/i.test(msg.trim()))
return 'UNKNOWN';
const hasTuple = !!(kv.IN || kv.OUT) && !!(kv.SRC || kv.DST || kv.PROTO);
if (hasTuple)
return 'PASS';
return 'UNKNOWN';
},
parseFlags: function(message, kv) {
if (kv.TCPFLAGS)
return kv.TCPFLAGS;
if (kv.FLAGS)
return kv.FLAGS;
const m = message.match(this.TCP_FLAG_TAIL);
if (!m)
return '';
return m[0].trim().toUpperCase().replace(/\s+/g, ',');
},
parseLength: function(kv) {
const len = kv.LEN || kv.LENGTH || '';
if (!len)
return null;
const n = parseInt(len, 10);
return isFinite(n) ? n : null;
},
timestampUnix: function(entry) {
if (!entry || entry.time == null || entry.time === '')
return null;
if (typeof entry.time === 'string' && /^\d{4}-\d{2}-\d{2}[T ]/.test(entry.time)) {
const ms = new Date(entry.time).getTime();
if (isFinite(ms))
return Math.floor(ms / 1000);
}
const n = Number(entry.time);
if (!isFinite(n))
return null;
return n > 1e12 ? Math.floor(n / 1000) : Math.floor(n);
},
formatTimestampDisplay: function(entry) {
const unix = this.timestampUnix(entry);
if (unix == null)
return '';
return new Date(unix * 1000).toISOString();
},
/* @fwlive-codegen:luci-preserve-begin */
formatTimestampLocal: function(unix) {
if (unix == null || !isFinite(unix))
return '';
const d = new Date(unix * 1000);
const pad = function(n) { return (n < 10 ? '0' : '') + n; };
return '%d-%s-%s %s:%s:%s'.format(
d.getFullYear(),
pad(d.getMonth() + 1),
pad(d.getDate()),
pad(d.getHours()),
pad(d.getMinutes()),
pad(d.getSeconds())
);
},
formatTimestampCompact: function(unix) {
if (unix == null || !isFinite(unix))
return '';
const d = new Date(unix * 1000);
const pad = function(n) { return (n < 10 ? '0' : '') + n; };
return '%s:%s:%s'.format(pad(d.getHours()), pad(d.getMinutes()), pad(d.getSeconds()));
},
formatFlowDisplay: function(row) {
const src = row && row.src ? String(row.src) : '';
const dst = row && row.dst ? String(row.dst) : '';
const sport = row && row.sport ? String(row.sport) : '';
const dport = row && row.dport ? String(row.dport) : '';
let left = src;
let right = dst;
if (sport)
left = left ? (left + ':' + sport) : (':' + sport);
if (dport)
right = right ? (right + ':' + dport) : (':' + dport);
if (!left && !right)
return '—';
if (!right)
return left;
if (!left)
return '→ ' + right;
return left + ' → ' + right;
},
formatCell: function(value) {
if (value == null || value === '')
return '';
return String(value);
},
formatActionLabel: function(action) {
const a = (action || '').toLowerCase();
if (!a || a === 'unknown')
return '—';
return a;
},
formatMessageDisplay: function(message, layout) {
let m = this.normalizeNetfilterMessage(message || '');
m = m.replace(/^\[\s*[\d.]+\]\s*/, '');
m = m.replace(/\bMAC=[^\s]+/g, '');
m = m.replace(/\s+/g, ' ').trim();
if (layout === 'oneline')
return m;
if (m.length > 240)
return m.substring(0, 237) + '…';
return m;
},
/* @fwlive-codegen:luci-preserve-end */
isFirewallEvent: function(entry) {
const msg = this.normalizeNetfilterMessage((entry && entry.msg) || '');
if (!msg.trim())
return false;
if (this.NON_FIREWALL_PREFIX.test(msg))
return false;
return this.evaluateClassifySpec(msg);
},
makeEntryId: function(entry, tsUnix, action, src, dst, sport, dport, proto, ifaceIn, ifaceOut) {
if (entry && entry.id != null && entry.id !== '')
return 'log:' + entry.id;
return [tsUnix, action, src, dst, sport, dport, proto, ifaceIn, ifaceOut, entry.msg || ''].join('|');
},
normalizeEntry: function(entry) {
const kv = this.parseKeyValueLog(entry.msg || '');
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 action = this.normalizeAction(actionRaw);
const src = kv.SRC || '';
const dst = kv.DST || '';
const sport = kv.SPT || '';
const dport = kv.DPT || '';
const ifaceIn = kv.IN || '';
const ifaceOut = kv.OUT || '';
const iface = ifaceIn || ifaceOut || '';
const dir = ifaceIn && ifaceOut ? 'forward' : (ifaceIn ? 'in' : (ifaceOut ? 'out' : 'unknown'));
const flags = this.parseFlags(entry.msg || '', kv);
const length = this.parseLength(kv);
const ruleHint = this.parseRuleHint(entry.msg || '');
const ruleLabel = this.formatRuleLabel(ruleHint);
return {
id: this.makeEntryId(entry, tsUnix, action, src, dst, sport, dport, proto, ifaceIn, ifaceOut),
log_id: entry && entry.id != null ? Number(entry.id) : null,
timestamp: tsUnix,
timestamp_display: tsDisplay,
rule_hint: ruleHint,
rule_label: ruleLabel,
action: action,
action_raw: actionRaw,
interface: iface,
interface_in: ifaceIn,
interface_out: ifaceOut,
direction: dir,
proto: proto,
src: src,
sport: sport,
dst: dst,
dport: dport,
flags: flags,
length: length,
message: entry.msg || ''
};
},
parseFilterValue: function(val) {
const s = (val || '').trim();
if (!s)
return { negate: false, value: '' };
if (s.charAt(0) === '!')
return { negate: true, value: s.slice(1).trim() };
return { negate: false, value: s };
},
toggleFilterNegation: function(val) {
const p = this.parseFilterValue(val);
if (!p.value)
return val;
return p.negate ? p.value : '!' + p.value;
},
formatFilterChipLabel: function(field, val) {
const p = this.parseFilterValue(val);
if (!p.value)
return '';
if (p.negate) {
if (field === 'q' || field === 'src' || field === 'dst')
return '%s: not contains %s'.format(field, p.value);
return '%s: not %s'.format(field, p.value);
}
return '%s: %s'.format(field, val);
},
matchesTextField: function(haystack, spec) {
const p = this.parseFilterValue(spec);
if (!p.value)
return true;
const hit = (haystack || '').indexOf(p.value) !== -1;
return p.negate ? !hit : hit;
},
matchesExactField: function(haystack, spec) {
const p = this.parseFilterValue(spec);
if (!p.value)
return true;
const want = p.value.toUpperCase();
const got = (haystack || '').toUpperCase();
const hit = got === want;
return p.negate ? !hit : hit;
},
matchesFilter: function(row, filters) {
if (filters.q) {
const p = this.parseFilterValue(filters.q);
if (p.value) {
const keys = Object.keys(row);
const parts = [];
for (let i = 0; i < keys.length; i++)
parts.push(row[keys[i]]);
const blob = parts.join(' ').toLowerCase();
const hit = blob.indexOf(p.value.toLowerCase()) !== -1;
if (p.negate ? hit : !hit)
return false;
}
}
if (filters.action) {
const p = this.parseFilterValue(filters.action);
if (p.value) {
const want = p.value.toLowerCase();
const hit = row.action === want
|| (row.action_raw || '').toUpperCase() === p.value.toUpperCase();
if (p.negate ? hit : !hit)
return false;
}
}
if (filters.interface) {
const p = this.parseFilterValue(filters.interface);
if (p.value) {
const iface = p.value;
const hit = row.interface === iface
|| row.interface_in === iface
|| row.interface_out === iface;
if (p.negate ? hit : !hit)
return false;
}
}
if (filters.proto && !this.matchesExactField(row.proto, filters.proto))
return false;
if (filters.src && !this.matchesTextField(row.src, filters.src))
return false;
if (filters.dst && !this.matchesTextField(row.dst, filters.dst))
return false;
if (filters.sport && !this.matchesExactField(row.sport, filters.sport))
return false;
if (filters.dport && !this.matchesExactField(row.dport, filters.dport))
return false;
return true;
},
actionRowClass: function(action) {
const a = (action || '').toLowerCase();
if (a === 'drop' || a === 'reject' || a === 'block')
return 'fwlive-action fwlive-deny';
if (a === 'pass')
return 'fwlive-action fwlive-pass';
return 'fwlive-action fwlive-unknown';
}
});
@@ -0,0 +1,270 @@
'use strict';
'require baseclass';
'require fwlive.links as links';
/**
* Logging toolbar and empty-state DOM renderers for luci-app-fwlive.
*
* renderToolbar(host, state, callbacks) → void
* host - #fwlive-logging-bar strip slot (cleared and rebuilt; element kept)
* state - { loggingStatus, loggingBusy, entriesLength, loggingNotice }
* callbacks - { onEnable(), onDisable() }
*
* G Hybrid chrome: when WAN logging is on, one merged control carries status +
* rate (click disables). When off, filled Enable CTA. Blockers stay status text.
*
* renderManualTestNodes(host, state, callbacks) → void
* host - <ul> element inside #fwlive-help (cleared and rebuilt)
* state - { firewallBackend }
*
* Empty-state helpers:
* buildEmptyStateNodes(state, callbacks) → Node[]
* renderEmptyState(host, state, callbacks) → void
* state - loggingState + { showConsent }
* callbacks - { onEnable(), onDismissConsent(persist) }
*
* Modules must not mutate state. host is cleared then rebuilt (idempotent replace).
*/
const CONSENT_STORAGE_KEY = 'fwlive-logging-consent-v1';
function consentDismissedPermanent() {
try {
return localStorage.getItem(CONSENT_STORAGE_KEY) === '1';
} catch (e) {
return false;
}
}
function persistConsentDismissed() {
try {
localStorage.setItem(CONSENT_STORAGE_KEY, '1');
} catch (e) {
/* private mode / no storage */
}
}
function blockerCode(state) {
const blockers = (state.loggingStatus && state.loggingStatus.blockers) || [];
if (blockers.indexOf('no_wan_zone') >= 0)
return 'no_wan_zone';
if (blockers.indexOf('nf_log_ipv4_missing') >= 0 ||
blockers.indexOf('nf_log_ipv6_missing') >= 0)
return 'nf_log_missing';
return '';
}
function renderToolbar(host, state, callbacks) {
host.innerHTML = '';
const st = state.loggingStatus;
if (!st) {
host.style.display = 'none';
return;
}
host.style.display = 'contents';
const blocker = blockerCode(state);
if (blocker === 'no_wan_zone') {
host.appendChild(E('span', { 'class': 'fwlive-logging-status' },
[ _('WAN logging unavailable: no WAN zone') ]));
host.appendChild(links.firewallZonesLink());
return;
}
if (blocker === 'nf_log_missing') {
host.appendChild(E('span', { 'class': 'fwlive-logging-status' },
[ _('WAN logging unavailable: missing kernel log modules') ]));
return;
}
const limit = st.wan_log_limit || _('default 10/minute');
if (st.wan_log) {
const busy = !!state.loggingBusy;
const children = busy
? [ _('Disabling…') ]
: [
E('span', { 'class': 'fwlive-log-on-dot', 'aria-hidden': 'true' }, [ '' ]),
E('span', { 'class': 'fwlive-log-label' }, [ _('WAN logging on') ]),
E('span', { 'class': 'fwlive-log-rate' }, [ _('· %s').format(limit) ])
];
host.appendChild(E('button', {
'class': 'cbi-button fwlive-log-merged',
'type': 'button',
'title': _('WAN logging on (%s). Click to disable.').format(limit),
'disabled': busy ? '' : null,
'click': function() { callbacks.onDisable(); }
}, children));
return;
}
host.appendChild(E('button', {
'class': 'cbi-button cbi-button-action',
'type': 'button',
'title': _('Enable WAN zone drop/reject logging (same as Network → Firewall).'),
'disabled': state.loggingBusy ? '' : null,
'click': function() { callbacks.onEnable(); }
}, [ state.loggingBusy ? _('Enabling…') : _('Enable logging') ]));
}
function buildConsentPanel(state, callbacks) {
const dontShowId = 'fwlive-consent-dont-show';
const panel = E('div', { 'class': 'fwlive-consent', 'id': 'fwlive-consent' }, [
E('p', { 'class': 'fwlive-empty-title' }, [ _('Before you enable logging') ]),
E('ul', { 'class': 'fwlive-consent-list' }, [
E('li', {}, [
E('strong', {}, [ _('Changes:') ]),
' ',
_('sets log on the WAN firewall zone and reloads the firewall.')
]),
E('li', {}, [
E('strong', {}, [ _('Does not change:') ]),
' ',
_('allow/deny rules, LAN logging, or anything else.')
]),
E('li', {}, [
E('strong', {}, [ _('Undo:') ]),
' ',
_('turn it back off with the WAN logging on control on the watch strip.')
])
]),
E('p', { 'class': 'fwlive-consent-check' }, [
E('label', {}, [
E('input', {
'type': 'checkbox',
'id': dontShowId
}),
' ',
_('Dont show this again')
])
]),
E('p', { 'class': 'fwlive-consent-actions' }, [
E('button', {
'class': 'cbi-button cbi-button-action',
'type': 'button',
'disabled': state.loggingBusy ? '' : null,
'click': function() {
persistConsentDismissed();
callbacks.onEnable();
}
}, [ state.loggingBusy ? _('Enabling…') : _('Enable WAN drop/reject logging') ]),
' ',
E('button', {
'class': 'cbi-button',
'type': 'button',
'click': function() {
const box = document.getElementById(dontShowId);
const persist = !!(box && box.checked);
if (persist)
persistConsentDismissed();
if (callbacks.onDismissConsent)
callbacks.onDismissConsent(persist);
}
}, [ _('Not now') ]),
' ',
links.firewallZonesLink(_('Ill configure this under Network → Firewall'))
])
]);
return panel;
}
function buildEmptyStateNodes(state, callbacks) {
const nodes = [];
const st = state.loggingStatus;
const blocker = blockerCode(state);
if (state.loggingNotice) {
nodes.push(E('p', { 'class': 'fwlive-logging-notice' }, [
state.loggingNotice,
' ',
links.firewallZonesLink()
]));
}
if (blocker === 'no_wan_zone') {
nodes.push(E('p', { 'class': 'fwlive-empty-title' }, [ _('No WAN zone found') ]));
nodes.push(E('p', {}, [
_('No WAN firewall zone found in /etc/config/firewall. Configure zones under '),
links.firewallZonesLink()
]));
return nodes;
}
if (blocker === 'nf_log_missing') {
nodes.push(E('p', { 'class': 'fwlive-empty-title' }, [ _('Kernel log modules missing') ]));
nodes.push(E('p', {}, [ _('Kernel netfilter log modules are missing. Install kmod-nf-log-ipv4 and kmod-nf-log-ipv6 (or kmod-nf-log / kmod-nf-log6), then reload the firewall.') ]));
nodes.push(E('p', {}, [
E('code', {}, [ 'opkg update && opkg install kmod-nf-log-ipv4 kmod-nf-log-ipv6' ])
]));
return nodes;
}
if (st && st.wan_log) {
nodes.push(E('p', { 'class': 'fwlive-empty-title' }, [ _('Waiting for firewall events') ]));
nodes.push(E('p', {}, [ _('WAN drop/reject logging is on. Blocked inbound WAN traffic will show up here. Normal LAN browsing will not.') ]));
nodes.push(E('p', { 'class': 'fwlive-empty-muted' }, [ _('If the WAN is quiet, wait for probes or use the optional ping check in Help / the enabling-logs guide.') ]));
nodes.push(E('p', {}, links.firewallZonesLink(_('Open firewall zone settings'))));
return nodes;
}
nodes.push(E('p', { 'class': 'fwlive-empty-title' }, [ _('Logging is off on this router') ]));
nodes.push(E('p', {}, [ _('OpenWrt does not write firewall events to the log until you turn logging on. Live View only shows what the firewall already logs — it does not add allow/deny rules.') ]));
/* Consent bullets already spell out the effect — do not repeat it or the CTA. */
if (state.showConsent) {
nodes.push(buildConsentPanel(state, callbacks));
return nodes;
}
nodes.push(E('p', {}, [ _('Turns on WAN zone drop/reject logging (same as Network → Firewall → wan → Log). Rate-limited by the zone log_limit (OpenWrt default 10/minute). Normal LAN browsing is not logged.') ]));
nodes.push(E('p', { 'class': 'fwlive-empty-muted' }, [ _('Nothing changes until you click Enable.') ]));
nodes.push(E('p', {}, [
E('button', {
'class': 'cbi-button cbi-button-action',
'type': 'button',
'disabled': state.loggingBusy ? '' : null,
'click': function() {
persistConsentDismissed();
callbacks.onEnable();
}
}, [ state.loggingBusy ? _('Enabling…') : _('Enable WAN drop/reject logging') ]),
' ',
links.firewallZonesLink(_('Ill configure this under Network → Firewall'))
]));
return nodes;
}
function renderEmptyState(host, state, callbacks) {
const nodes = buildEmptyStateNodes(state, callbacks);
host.innerHTML = '';
for (let i = 0; i < nodes.length; i++)
host.appendChild(nodes[i]);
}
/**
* renderManualTestNodes — fills a <li> host element with the backend-specific
* manual test instruction. Call from addFooter() after render() has inserted
* the placeholder <li id="fwlive-manual-test">.
*/
function renderManualTestNodes(host, state, _callbacks) {
host.innerHTML = '';
if (state.firewallBackend === 'iptables') {
host.appendChild(document.createTextNode(_('Manual test (System → Terminal): ')));
host.appendChild(E('code', {}, [ 'iptables -I INPUT -p icmp --icmp-type echo-request -j LOG --log-prefix "fwlive-ping: "' ]));
host.appendChild(document.createTextNode(_(' then ping the router.')));
} else {
host.appendChild(document.createTextNode(_('Manual test (System → Terminal): ')));
host.appendChild(E('code', {}, [ 'nft insert rule inet fw4 input ip protocol icmp icmp type echo-request log prefix "fwlive-ping " accept' ]));
host.appendChild(document.createTextNode(_(' then ping the router.')));
}
}
return baseclass.extend({
CONSENT_STORAGE_KEY: CONSENT_STORAGE_KEY,
consentDismissedPermanent: consentDismissedPermanent,
persistConsentDismissed: persistConsentDismissed,
renderToolbar: renderToolbar,
buildEmptyStateNodes: buildEmptyStateNodes,
renderEmptyState: renderEmptyState,
renderManualTestNodes: renderManualTestNodes
});
@@ -0,0 +1,50 @@
'use strict';
'require baseclass'; /* LuCI require() needs Class.isSubclass — plain return {} fails */
/**
* Protocol filter pair: grouped <select> + always-visible custom text field.
* Typed custom value wins when non-empty (D Always custom).
*/
return baseclass.extend({
readProtoFilter: function() {
const custom = document.getElementById('fwlive-proto-custom');
if (custom) {
const typed = (custom.value || '').trim();
if (typed)
return typed;
}
const sel = document.getElementById('fwlive-proto');
return sel ? (sel.value || '') : '';
},
setProtoFilterValue: function(value) {
const sel = document.getElementById('fwlive-proto');
const custom = document.getElementById('fwlive-proto-custom');
if (!sel)
return false;
value = value || '';
let inMenu = (value === '');
if (!inMenu) {
for (let i = 0; i < sel.options.length; i++) {
if (sel.options[i].value === value) {
inMenu = true;
break;
}
}
}
if (inMenu) {
sel.value = value;
if (custom)
custom.value = '';
} else {
sel.value = '';
if (custom)
custom.value = value;
}
return true;
}
});
@@ -0,0 +1,233 @@
'use strict';
'require baseclass';
'require fwlive.log as log';
'require fwlive.links as links';
/**
* Table thead/rows DOM renderer for luci-app-fwlive.
*
* renderThead(host, state, callbacks) → void
* host - <table id="fwlive-table"> (colgroup + thead tr cleared/rebuilt)
* state - shallow copy: { columns: [...] }
* callbacks - {} (unused; present for API consistency)
*
* renderRows(host, state, callbacks) → void
* host - <tbody> element (cleared and rebuilt; element itself is kept)
* state - shallow copy: { rows, columns, viewMode, messageLayout,
* expandedRowId, rowTint, showHostnames,
* hostnameCache, firewallBackend }
* callbacks - { onRowClick(rowId, ev), onFilterClick(field, value, ev),
* actionRowTintClass(action) }
*
* Internals (not a second public contract): columnLabel, columnCellClass,
* flowCell, buildColumnCell.
*
* Modules must not mutate state. host contents are cleared then rebuilt
* (idempotent replace). Does not touch #fwlive-scroll or #fwlive-empty.
*/
function columnLabel(col) {
const labels = {
time: _('Time'),
action: _('Action'),
rule: _('Rule'),
iface: _('Interface'),
iface_in: _('IN'),
iface_out: _('OUT'),
dir: _('Dir'),
proto: _('Proto'),
src: _('Source'),
dst: _('Destination'),
sport: _('SPort'),
dport: _('DPort'),
flags: _('Flags'),
len: _('Len'),
flow: _('Flow'),
message: _('Message')
};
return labels[col] || col;
}
function columnCellClass(col) {
switch (col) {
case 'time': return 'fwlive-time';
case 'action': return 'fwlive-action';
case 'rule': return 'fwlive-rule';
case 'iface':
case 'iface_in':
case 'iface_out': return 'fwlive-iface';
case 'dir': return 'fwlive-dir';
case 'proto': return 'fwlive-proto';
case 'src':
case 'dst': return 'fwlive-addr';
case 'sport':
case 'dport': return 'fwlive-port';
case 'flags': return 'fwlive-flags';
case 'len': return 'fwlive-len';
case 'flow': return 'fwlive-flow-cell';
case 'message': return 'fwlive-message fwlive-th-message';
default: return '';
}
}
function flowCell(row, state, callbacks) {
const parts = [];
const onFilterClick = callbacks.onFilterClick;
const pushAddr = (addr, port, addrField, portField) => {
if (!addr && !port)
return;
if (addr)
parts.push(links.addrFilterLink(addrField, addr,
!!state.showHostnames, state.hostnameCache, onFilterClick));
if (port) {
if (addr)
parts.push(':');
parts.push(links.filterLink(portField, port, port, onFilterClick));
}
};
pushAddr(row.src, row.sport, 'src', 'sport');
if (parts.length && (row.dst || row.dport))
parts.push(E('span', { 'class': 'fwlive-flow-arrow' }, [ ' → ' ]));
pushAddr(row.dst, row.dport, 'dst', 'dport');
if (!parts.length)
return '—';
return E('span', { 'class': 'fwlive-flow' }, parts);
}
function buildColumnCell(col, row, state, callbacks) {
const onFilterClick = callbacks.onFilterClick;
const msgDisplay = log.formatMessageDisplay(row.message, state.messageLayout);
const actionCell = row.action && row.action !== 'unknown'
? links.filterLink('action', row.action, log.formatActionLabel(row.action), onFilterClick)
: log.formatActionLabel(row.action);
switch (col) {
case 'time': {
const timeAttrs = { 'class': columnCellClass(col) };
if (state.viewMode === 'simple')
timeAttrs.title = _('Click a row for the full message');
return E('td', timeAttrs,
[ state.viewMode === 'simple'
? log.formatTimestampCompact(row.timestamp)
: log.formatTimestampLocal(row.timestamp) ]);
}
case 'action':
return E('td', { 'class': log.actionRowClass(row.action) }, [ actionCell ]);
case 'rule':
return E('td', { 'class': columnCellClass(col) },
[ links.ruleAdminLink(row.rule_hint, row.rule_label, state.firewallBackend, onFilterClick) ]);
case 'iface':
return E('td', { 'class': columnCellClass(col) },
[ links.ifaceLink(row.interface_in, onFilterClick) ]);
case 'iface_in':
case 'iface_out':
return E('td', { 'class': columnCellClass(col) }, [ links.ifaceLink(
col === 'iface_in' ? row.interface_in : row.interface_out, onFilterClick) ]);
case 'dir':
return E('td', { 'class': columnCellClass(col) }, [ log.formatCell(row.direction) ]);
case 'proto':
return E('td', { 'class': columnCellClass(col) },
[ links.filterLink('proto', row.proto, null, onFilterClick) ]);
case 'src':
return E('td', { 'class': columnCellClass(col) },
[ links.addrFilterLink('src', row.src, !!state.showHostnames, state.hostnameCache, onFilterClick) ]);
case 'sport':
return E('td', { 'class': columnCellClass(col) },
[ links.filterLink('sport', row.sport, null, onFilterClick) ]);
case 'dst':
return E('td', { 'class': columnCellClass(col) },
[ links.addrFilterLink('dst', row.dst, !!state.showHostnames, state.hostnameCache, onFilterClick) ]);
case 'dport':
return E('td', { 'class': columnCellClass(col) },
[ links.filterLink('dport', row.dport, null, onFilterClick) ]);
case 'flags':
return E('td', { 'class': columnCellClass(col) }, [ log.formatCell(row.flags) ]);
case 'len':
return E('td', { 'class': columnCellClass(col) }, [ row.length != null ? String(row.length) : '' ]);
case 'flow':
return E('td', { 'class': columnCellClass(col) }, [ flowCell(row, state, callbacks) ]);
case 'message':
if (state.messageLayout === 'wrap') {
return E('td', {
'class': 'fwlive-message',
'title': msgDisplay || ''
}, E('div', { 'class': 'fwlive-message-wrap' }, [ msgDisplay || '—' ]));
}
return E('td', {
'class': 'fwlive-message',
'title': msgDisplay || ''
}, [ msgDisplay || '—' ]);
default:
return E('td', {}, [ '' ]);
}
}
function renderThead(host, state, _callbacks) {
const columns = state.columns || [];
const tr = host.querySelector('thead tr');
if (!tr)
return;
let colgroup = host.querySelector('colgroup');
if (!colgroup) {
colgroup = E('colgroup', {});
host.insertBefore(colgroup, host.firstChild);
}
colgroup.innerHTML = '';
tr.innerHTML = '';
for (let i = 0; i < columns.length; i++) {
const col = columns[i];
colgroup.appendChild(E('col', { 'class': 'fwlive-col fwlive-col-' + col.replace(/_/g, '-') }));
tr.appendChild(E('th', { 'class': columnCellClass(col) }, [ columnLabel(col) ]));
}
}
function renderRows(host, state, callbacks) {
const rows = state.rows || [];
const columns = state.columns || [];
host.innerHTML = '';
for (let i = 0; i < rows.length; i++) {
const r = rows[i];
const rowClass = [
i % 2 ? 'fwlive-row-alt' : '',
state.viewMode === 'simple' ? 'fwlive-row-clickable' : '',
state.expandedRowId === r.id ? 'fwlive-row-expanded' : '',
state.rowTint ? callbacks.actionRowTintClass(r.action) : ''
].filter(Boolean).join(' ');
const cells = [];
for (let c = 0; c < columns.length; c++)
cells.push(buildColumnCell(columns[c], r, state, callbacks));
const tr = E('tr', {
'class': rowClass,
'click': state.viewMode === 'simple'
? (ev) => callbacks.onRowClick(r.id, ev) : null
}, cells);
host.appendChild(tr);
if (state.viewMode === 'simple' && state.expandedRowId === r.id) {
host.appendChild(E('tr', { 'class': 'fwlive-msg-expand' }, [
E('td', { 'colspan': String(columns.length) }, [
E('div', { 'class': 'fwlive-msg-expand-label' }, [ _('Message') ]),
E('pre', { 'class': 'fwlive-msg-expand-body' },
[ log.formatMessageDisplay(r.message, 'wrap') || '—' ])
])
]));
}
}
}
return baseclass.extend({
renderThead: renderThead,
renderRows: renderRows
});
@@ -0,0 +1,101 @@
'use strict';
'require baseclass';
/**
* Row-tint paint helpers for luci-app-fwlive.
* LuCI modules must return baseclass.extend(...) — plain objects fail Class.isSubclass.
*
* Modes (fwlive-row-tint localStorage / data-row-tint):
* classic — green/red (default)
* accessible — teal/orange (colorblind-safe)
* off — no row background tint
*/
var PAINT_DELTA_MIN = 8;
var CLASSIC_PASS_HEX = '#46a546';
var CLASSIC_DENY_HEX = '#ca3c3c';
var ACCESSIBLE_PASS_HEX = '#0d9488';
var ACCESSIBLE_DENY_HEX = '#c2410c';
/* Back-compat aliases — classic is the default palette. */
var PASS_HEX = CLASSIC_PASS_HEX;
var DENY_HEX = CLASSIC_DENY_HEX;
function normalizeRowTint(mode) {
if (mode === 'off' || mode === 'accessible' || mode === 'classic')
return mode;
return 'classic';
}
function hexPairForMode(mode) {
if (normalizeRowTint(mode) === 'accessible')
return { pass: ACCESSIBLE_PASS_HEX, deny: ACCESSIBLE_DENY_HEX };
return { pass: CLASSIC_PASS_HEX, deny: CLASSIC_DENY_HEX };
}
function parseCssRgbChannels(value) {
if (!value)
return null;
const s = String(value).trim().toLowerCase();
if (s === 'transparent' || s === 'rgba(0, 0, 0, 0)' || s === 'rgba(0,0,0,0)')
return null;
const rgb = s.match(/rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)/i);
if (rgb)
return [ parseFloat(rgb[1]), parseFloat(rgb[2]), parseFloat(rgb[3]) ];
/* color-mix() often serializes as color(srgb r g b[/a]) with 0..1 channels. */
const modern = s.match(/color\(\s*srgb\s+([\d.]+)\s+([\d.]+)\s+([\d.]+)/i);
if (modern)
return [
parseFloat(modern[1]) * 255,
parseFloat(modern[2]) * 255,
parseFloat(modern[3]) * 255
];
return null;
}
function cssColorPaintDelta(a, b) {
const ca = parseCssRgbChannels(a);
const cb = parseCssRgbChannels(b);
/* Transparent vs opaque color is a real paint change (common off-state). */
if (!ca && !cb)
return 0;
if (!ca && cb)
return Math.abs(cb[0]) + Math.abs(cb[1]) + Math.abs(cb[2]);
if (ca && !cb)
return Math.abs(ca[0]) + Math.abs(ca[1]) + Math.abs(ca[2]);
return Math.abs(ca[0] - cb[0]) + Math.abs(ca[1] - cb[1]) + Math.abs(ca[2] - cb[2]);
}
function tintShouldEngageFallback(opts) {
const o = opts || {};
const minDelta = (typeof o.minDelta === 'number') ? o.minDelta : PAINT_DELTA_MIN;
/* Visible paint is the success criterion; token/CSS.supports are only used when
paint cannot be measured (no delta sample yet). */
if (typeof o.paintDelta === 'number')
return o.paintDelta < minDelta;
if (o.tokenResolved === false)
return true;
return false;
}
return baseclass.extend({
PAINT_DELTA_MIN: PAINT_DELTA_MIN,
PASS_HEX: PASS_HEX,
DENY_HEX: DENY_HEX,
CLASSIC_PASS_HEX: CLASSIC_PASS_HEX,
CLASSIC_DENY_HEX: CLASSIC_DENY_HEX,
ACCESSIBLE_PASS_HEX: ACCESSIBLE_PASS_HEX,
ACCESSIBLE_DENY_HEX: ACCESSIBLE_DENY_HEX,
normalizeRowTint: normalizeRowTint,
hexPairForMode: hexPairForMode,
parseCssRgbChannels: parseCssRgbChannels,
cssColorPaintDelta: cssColorPaintDelta,
tintShouldEngageFallback: tintShouldEngageFallback
});
File diff suppressed because it is too large Load Diff
+609
View File
@@ -0,0 +1,609 @@
msgid ""
msgstr ""
"Project-Id-Version: luci-app-fwlive\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-31 02:17+0000\n"
"PO-Revision-Date: 2026-07-31 02:17+0000\n"
"Last-Translator: lucas-albers-lz4\n"
"Language-Team: German\n"
"Language: de\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:884
msgid "%d matching · %d/%d stored"
msgstr "%d matching · %d/%d stored"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:887
msgid "0 matching · %d/%d stored"
msgstr "0 matching · %d/%d stored"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:250
msgid "Accessible (teal/orange)"
msgstr "Barrierefrei (teal/orange)"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:32
msgid "Action"
msgstr "Aktion"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:594
msgid "Administrator access is required to disable logging."
msgstr "Zum Deaktivieren der Protokollierung sind Administratorrechte erforderlich."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:562
msgid "Administrator access is required to enable logging."
msgstr "Zum Aktivieren der Protokollierung sind Administratorrechte erforderlich."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1050
msgid "Also seen"
msgstr "Weitere"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:547
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:583
msgid "Another change is staged for the firewall; apply or revert it first."
msgstr "Eine andere Änderung an der Firewall ist vorgemerkt; zuerst anwenden oder verwerfen."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1615
msgid "Any action"
msgstr "Beliebige Aktion"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1043
msgid "Any protocol"
msgstr "Beliebiges Protokoll"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:113
msgid "Before you enable logging"
msgstr "Before you enable logging"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:545
msgid "Cannot enable logging until kernel log modules are installed."
msgstr "Protokollierung kann erst aktiviert werden, wenn die Kernel-Log-Module installiert sind."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:116
msgid "Changes:"
msgstr "Changes:"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:249
msgid "Classic (green/red)"
msgstr "Klassisch (grün/rot)"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1599
msgid "Classic uses green/red; Accessible uses teal/orange"
msgstr "Klassisch verwendet grün/rot; Barrierefrei verwendet teal/orange"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:116
msgid "Clear all"
msgstr "Alle löschen"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1655
msgid "Click a cell to filter · ≠ on a chip to exclude · Ctrl+click a rule for firewall settings · in Simple view, click a row for the full message"
msgstr "Zelle anklicken zum Filtern · ≠ auf einem Chip zum Ausschließen · Strg+Klick auf eine Regel für Firewall-Einstellungen · in der Einfachen Ansicht Zeile anklicken für die vollständige Meldung"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1676
msgid "Click a row (Time or other non-link cells) to see the full log line (Simple view)."
msgstr "Klicken Sie auf eine Zeile (Zeit oder andere Nicht-Link-Zellen), um die vollständige Log-Zeile zu sehen (Einfache Ansicht)."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:113
msgid "Click a row for the full message"
msgstr "Zeile anklicken für die vollständige Meldung"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1677
msgid "Click an IP, action, or protocol to filter; use the Protocol menu (or ≠ on a chip) to exclude."
msgstr "Click an IP, action, or protocol to filter; use the Protocol menu (or ≠ on a chip) to exclude."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1044
msgid "Common"
msgstr "Häufig"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:902
msgid "Connection lost — retrying…"
msgstr "Connection lost — retrying…"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:585
msgid "Could not disable logging."
msgstr "Protokollierung konnte nicht deaktiviert werden."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:549
msgid "Could not enable logging."
msgstr "Protokollierung konnte nicht aktiviert werden."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1637
msgid "Custom protocol (prefix ! to exclude). Overrides the menu when set."
msgstr "Eigenes Protokoll (! zum Ausschließen). Überschreibt das Menü, wenn gesetzt."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:42
msgid "DPort"
msgstr "Ziel-Port"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:40
msgid "Destination"
msgstr "Ziel"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1648
msgid "Destination IP contains (! to exclude)"
msgstr "Ziel-IP enthält (! zum Ausschließen)"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1649
msgid "Destination port (! to exclude)"
msgstr "Ziel-Port (! zum Ausschließen)"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1540
msgid "Detail"
msgstr "Detail"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:37
msgid "Dir"
msgstr "Richtung"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:85
msgid "Disabling…"
msgstr "Disabling…"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1573
msgid "Display options"
msgstr "Anzeigeoptionen"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1673
msgid "Display options on the bar set Limit, row tint, palette, and hostnames."
msgstr "Anzeigeoptionen in der Leiste setzen Limit, Zeilenfärbung, Palette und Hostnamen."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:121
msgid "Does not change:"
msgstr "Does not change:"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:138
msgid "Dont show this again"
msgstr "Dont show this again"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:150
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:230
msgid "Enable WAN drop/reject logging"
msgstr "Enable WAN drop/reject logging"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:104
msgid "Enable WAN zone drop/reject logging (same as Network → Firewall)."
msgstr "Enable WAN zone drop/reject logging (same as Network → Firewall)."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:107
msgid "Enable logging"
msgstr "Protokollierung aktivieren"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1672
msgid "Enable logging turns on WAN zone drop/reject logging only (same as Network → Firewall). It does not add rules or log normal LAN browsing."
msgstr "Enable logging turns on WAN zone drop/reject logging only (same as Network → Firewall). It does not add rules or log normal LAN browsing."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:107
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:150
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:230
msgid "Enabling…"
msgstr "Enabling…"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1057
msgid "Exclude"
msgstr "Ausschließen"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:80
msgid "Exclude instead"
msgstr "Stattdessen ausschließen"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:53
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:71
msgid "Filter by %s"
msgstr "Filtern nach %s"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:136
msgid "Filter by interface"
msgstr "Nach Schnittstelle filtern"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:112
msgid "Filter logs by rule (hint: %s). Ctrl+click to open firewall settings."
msgstr "Logs nach Regel filtern (Hinweis: %s). Strg+Klick öffnet die Firewall-Einstellungen."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1497
#: applications/luci-app-fwlive/root/usr/share/luci/menu.d/luci-app-fwlive.json:3
msgid "Firewall Live View"
msgstr "Firewall-Live-Ansicht"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:43
msgid "Flags"
msgstr "Flags"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:45
msgid "Flow"
msgstr "Fluss"
#: applications/luci-app-fwlive/root/usr/share/rpcd/acl.d/luci-app-fwlive.json:3
msgid "Grant access to firewall live log view"
msgstr "Zugriff auf die Live-Ansicht der Firewall-Protokolle gewähren"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1669
msgid "Help"
msgstr "Hilfe"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:853
msgid "High event rate — table refresh is throttled to protect the browser. The buffer still updates; refresh will resume automatically."
msgstr "High event rate — table refresh is throttled to protect the browser. The buffer still updates; refresh will resume automatically."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1048
msgid "ICMPv6"
msgstr "ICMPv6"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:35
msgid "IN"
msgstr "EINGANG"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1680
msgid "If Row tint looks missing, the active LuCI theme may omit success/error or info/warn CSS variables; fwlive falls back to local colors (air-gapped, no data leaves the device)."
msgstr "Wenn die Zeileneinfärbung fehlt, verwendet das aktive LuCI-Theme möglicherweise keine Erfolgs/Fehler-CSS-Variablen; fwlive fällt auf lokale Farben zurück (air-gapped, keine Daten verlassen das Gerät)."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:205
msgid "If the WAN is quiet, wait for probes or use the optional ping check in Help / the enabling-logs guide."
msgstr "If the WAN is quiet, wait for probes or use the optional ping check in Help / the enabling-logs guide."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:80
msgid "Include instead"
msgstr "Stattdessen einschließen"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:34
msgid "Interface"
msgstr "Schnittstelle"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1645
msgid "Interface (prefix ! to exclude)"
msgstr "Schnittstelle (! voranstellen zum Ausschließen)"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:165
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:232
msgid "Ill configure this under Network → Firewall"
msgstr "Ill configure this under Network → Firewall"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:194
msgid "Kernel log modules missing"
msgstr "Kernel log modules missing"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:195
msgid "Kernel netfilter log modules are missing. Install kmod-nf-log-ipv4 and kmod-nf-log-ipv6 (or kmod-nf-log / kmod-nf-log6), then reload the firewall."
msgstr "Kernel-Netfilter-Log-Module fehlen. Installieren Sie kmod-nf-log-ipv4 und kmod-nf-log-ipv6 (oder kmod-nf-log / kmod-nf-log6) und laden Sie dann die Firewall neu."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:44
msgid "Len"
msgstr "Länge"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1576
msgid "Limit"
msgstr "Limit"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:210
msgid "Logging is off on this router"
msgstr "Logging is off on this router"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:252
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:256
msgid "Manual test (System → Terminal):"
msgstr "Manueller Test (System → Terminal):"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:46
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:221
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1547
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1552
msgid "Message"
msgstr "Nachricht"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1643
msgid "More filters"
msgstr "Weitere Filter"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:37
msgid "Network → Firewall"
msgstr "Network → Firewall"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:187
msgid "No WAN firewall zone found in /etc/config/firewall. Configure zones under"
msgstr "Keine WAN-Firewall-Zone in /etc/config/firewall gefunden. Konfigurieren Sie Zonen unter"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:185
msgid "No WAN zone found"
msgstr "No WAN zone found"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:163
msgid "Not now"
msgstr "Not now"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:220
msgid "Nothing changes until you click Enable."
msgstr "Nothing changes until you click Enable."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:36
msgid "OUT"
msgstr "AUSGANG"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1567
msgid "One line"
msgstr "Einzeilig"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:206
msgid "Open firewall zone settings"
msgstr "Firewall-Zonen-Einstellungen öffnen"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:211
msgid "OpenWrt does not write firewall events to the log until you turn logging on. Live View only shows what the firewall already logs — it does not add allow/deny rules."
msgstr "OpenWrt does not write firewall events to the log until you turn logging on. Live View only shows what the firewall already logs — it does not add allow/deny rules."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1595
msgid "Palette"
msgstr "Palette"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:953
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1517
msgid "Pause"
msgstr "Pause"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:951
msgid "Paused"
msgstr "Pausiert"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:38
msgid "Proto"
msgstr "Protokoll"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1631
msgid "Protocol — common values"
msgstr "Protokoll — häufige Werte"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1613
msgid "Quick search"
msgstr "Schnellsuche"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:91
msgid "Remove filter"
msgstr "Filter entfernen"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:953
msgid "Resume"
msgstr "Fortsetzen"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1588
msgid "Row tint"
msgstr "Zeileneinfärbung"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1678
msgid "Row tint shows pass/deny row backgrounds when checked. Choose Classic (green/red, default) or Accessible (teal/orange). Action text stays colored either way."
msgstr "Zeilenfärbung zeigt Pass/Deny-Hintergrundfarben, wenn aktiviert. Wählen Sie Klassisch (grün/rot, Standard) oder Barrierefrei (teal/orange). Aktionstext bleibt in beiden Fällen farbig."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1507
msgid "Row tint used a local color fallback because the active LuCI theme did not apply pass/deny backgrounds."
msgstr "Die Zeileneinfärbung verwendete einen lokalen Farbfallback, da das aktive LuCI-Theme keine Erlauben/Verweigern-Hintergründe anwendet."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:33
msgid "Rule"
msgstr "Regel"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:41
msgid "SPort"
msgstr "Quell-Port"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1607
msgid "Show hostnames"
msgstr "Hostnamen anzeigen"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1586
msgid "Show pass/deny row background colors"
msgstr "Pass/Deny-Zeilenhintergrundfarben anzeigen"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1533
msgid "Simple"
msgstr "Einfach"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:39
msgid "Source"
msgstr "Quelle"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1646
msgid "Source IP contains (! to exclude)"
msgstr "Quell-IP enthält (! zum Ausschließen)"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1647
msgid "Source port (! to exclude)"
msgstr "Quell-Port (! zum Ausschließen)"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1674
msgid "The rate shown for WAN logging is the firewall zone log_limit. OpenWrt defaults to 10/minute when no explicit limit is configured; fwlive does not impose this cap."
msgstr "Die angezeigte Rate für WAN-Protokollierung ist das Zonen-log_limit. OpenWrt-Standard ist 10/Minute ohne explizites Limit; fwlive setzt diese Grenze nicht."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1671
msgid "The table updates automatically when your firewall logs traffic. Use Pause if it moves too fast."
msgstr "Die Tabelle aktualisiert sich automatisch, wenn Ihre Firewall Verkehr protokolliert. Pause nutzen, wenn es zu schnell wird."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1508
msgid "Theme tint fallback"
msgstr "Theme-Farb-Fallback"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:31
msgid "Time"
msgstr "Zeit"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:219
msgid "Turns on WAN zone drop/reject logging (same as Network → Firewall → wan → Log). Rate-limited by the zone log_limit (OpenWrt default 10/minute). Normal LAN browsing is not logged."
msgstr "Turns on WAN zone drop/reject logging (same as Network → Firewall → wan → Log). Rate-limited by the zone log_limit (OpenWrt default 10/minute). Normal LAN browsing is not logged."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:126
msgid "Undo:"
msgstr "Undo:"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1679
msgid "Use Detail for all columns (flags, length, raw message)."
msgstr "Verwenden Sie „Detail“ für alle Spalten (Flags, Länge, rohe Nachricht)."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1521
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1525
msgid "View"
msgstr "Ansicht"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:591
msgid "WAN drop/reject logging is off."
msgstr "WAN drop/reject logging is off."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:204
msgid "WAN drop/reject logging is on. Blocked inbound WAN traffic will show up here. Normal LAN browsing will not."
msgstr "WAN drop/reject logging is on. Blocked inbound WAN traffic will show up here. Normal LAN browsing will not."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:555
msgid "WAN drop/reject logging is on. Blocked inbound traffic should appear here as it happens — not normal LAN browsing."
msgstr "WAN drop/reject logging is on. Blocked inbound traffic should appear here as it happens — not normal LAN browsing."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:557
msgid "WAN logging is already enabled."
msgstr "WAN-Protokollierung ist bereits aktiviert."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:88
msgid "WAN logging on"
msgstr "WAN-Protokollierung: EIN"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:94
msgid "WAN logging on (%s). Click to disable."
msgstr "WAN-Protokollierung: EIN (%s). Klicken zum Deaktivieren."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:77
msgid "WAN logging unavailable: missing kernel log modules"
msgstr "WAN-Protokollierung nicht verfügbar: fehlende Kernel-Log-Module"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:70
msgid "WAN logging unavailable: no WAN zone"
msgstr "WAN-Protokollierung nicht verfügbar: keine WAN-Zone"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:203
msgid "Waiting for firewall events"
msgstr "Waiting for firewall events"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:951
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1502
msgid "Watching"
msgstr "Beobachtet"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1560
msgid "Wrap"
msgstr "Umbrechen"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:123
msgid "allow/deny rules, LAN logging, or anything else."
msgstr "allow/deny rules, LAN logging, or anything else."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:786
msgid "buffer full"
msgstr "Puffer voll"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:81
msgid "default 10/minute"
msgstr "Standard 10/Minute"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:28
msgid "is"
msgstr "ist"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:781
msgid "loading buffer"
msgstr "Lade Puffer"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:881
msgid "loading…"
msgstr "loading…"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:37
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:44
msgid "not"
msgstr "nicht"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1065
msgid "not AH"
msgstr "nicht AH"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1064
msgid "not ESP"
msgstr "nicht ESP"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1063
msgid "not GRE"
msgstr "nicht GRE"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1060
msgid "not ICMP"
msgstr "nicht ICMP"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1061
msgid "not ICMPv6"
msgstr "nicht ICMPv6"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1062
msgid "not IGMP"
msgstr "nicht IGMP"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1066
msgid "not SCTP"
msgstr "nicht SCTP"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1058
msgid "not TCP"
msgstr "nicht TCP"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1059
msgid "not UDP"
msgstr "nicht UDP"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1623
msgid "not block"
msgstr "nicht blockieren"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1622
msgid "not drop"
msgstr "nicht verwerfen"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1621
msgid "not pass"
msgstr "nicht erlauben"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1624
msgid "not reject"
msgstr "nicht zurückweisen"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1625
msgid "not unknown"
msgstr "nicht unbekannt"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1636
msgid "or type…"
msgstr "oder tippen…"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:788
msgid "render paused (high rate)"
msgstr "Rendering pausiert (hohe Rate)"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:790
msgid "scroll frozen — scroll to top to follow live"
msgstr "scroll frozen — scroll to top to follow live"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:118
msgid "sets log on the WAN firewall zone and reloads the firewall."
msgstr "sets log on the WAN firewall zone and reloads the firewall."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:254
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:258
msgid "then ping the router."
msgstr "dann den Router anpingen."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:128
msgid "turn it back off with the WAN logging on control on the watch strip."
msgstr "turn it back off with the WAN logging on control on the watch strip."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:506
msgid "using fw4"
msgstr "mit fw4"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:504
msgid "using iptables"
msgstr "mit iptables"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:89
msgid "· %s"
msgstr "· %s"
+609
View File
@@ -0,0 +1,609 @@
msgid ""
msgstr ""
"Project-Id-Version: luci-app-fwlive\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-31 02:17+0000\n"
"PO-Revision-Date: 2026-07-31 02:17+0000\n"
"Last-Translator: lucas-albers-lz4\n"
"Language-Team: Russian\n"
"Language: ru\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:884
msgid "%d matching · %d/%d stored"
msgstr "%d matching · %d/%d stored"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:887
msgid "0 matching · %d/%d stored"
msgstr "0 matching · %d/%d stored"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:250
msgid "Accessible (teal/orange)"
msgstr "Доступный (бирюзовый/оранжевый)"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:32
msgid "Action"
msgstr "Действие"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:594
msgid "Administrator access is required to disable logging."
msgstr "Для отключения журналирования требуются права администратора."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:562
msgid "Administrator access is required to enable logging."
msgstr "Для включения журналирования требуются права администратора."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1050
msgid "Also seen"
msgstr "Также встречаются"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:547
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:583
msgid "Another change is staged for the firewall; apply or revert it first."
msgstr "В брандмауэре уже есть другое незафиксированное изменение; сначала примените или отмените его."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1615
msgid "Any action"
msgstr "Любое действие"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1043
msgid "Any protocol"
msgstr "Любой протокол"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:113
msgid "Before you enable logging"
msgstr "Before you enable logging"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:545
msgid "Cannot enable logging until kernel log modules are installed."
msgstr "Невозможно включить журналирование, пока не установлены модули журнала ядра."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:116
msgid "Changes:"
msgstr "Changes:"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:249
msgid "Classic (green/red)"
msgstr "Классический (зелёный/красный)"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1599
msgid "Classic uses green/red; Accessible uses teal/orange"
msgstr "Классический — зелёный/красный; доступный — бирюзовый/оранжевый"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:116
msgid "Clear all"
msgstr "Очистить всё"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1655
msgid "Click a cell to filter · ≠ on a chip to exclude · Ctrl+click a rule for firewall settings · in Simple view, click a row for the full message"
msgstr "Щёлкните ячейку для фильтра · ≠ на чипе для исключения · Ctrl+щелчок по правилу для настроек МЭ · в Простом виде щёлкните строку для полного сообщения"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1676
msgid "Click a row (Time or other non-link cells) to see the full log line (Simple view)."
msgstr "Щёлкните строку (Время или другие ячейки без ссылки), чтобы увидеть полную строку журнала (Простой вид)."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:113
msgid "Click a row for the full message"
msgstr "Щёлкните строку для полного сообщения"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1677
msgid "Click an IP, action, or protocol to filter; use the Protocol menu (or ≠ on a chip) to exclude."
msgstr "Click an IP, action, or protocol to filter; use the Protocol menu (or ≠ on a chip) to exclude."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1044
msgid "Common"
msgstr "Частые"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:902
msgid "Connection lost — retrying…"
msgstr "Connection lost — retrying…"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:585
msgid "Could not disable logging."
msgstr "Не удалось отключить журналирование."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:549
msgid "Could not enable logging."
msgstr "Не удалось включить журналирование."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1637
msgid "Custom protocol (prefix ! to exclude). Overrides the menu when set."
msgstr "Свой протокол (! для исключения). Переопределяет меню, если задан."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:42
msgid "DPort"
msgstr "Порт назн."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:40
msgid "Destination"
msgstr "Назначение"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1648
msgid "Destination IP contains (! to exclude)"
msgstr "IP назначения содержит (! для исключения)"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1649
msgid "Destination port (! to exclude)"
msgstr "Порт назначения (! для исключения)"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1540
msgid "Detail"
msgstr "Подробно"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:37
msgid "Dir"
msgstr "Напр."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:85
msgid "Disabling…"
msgstr "Disabling…"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1573
msgid "Display options"
msgstr "Параметры отображения"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1673
msgid "Display options on the bar set Limit, row tint, palette, and hostnames."
msgstr "Параметры отображения на панели задают лимит, окраску строк, палитру и имена узлов."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:121
msgid "Does not change:"
msgstr "Does not change:"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:138
msgid "Dont show this again"
msgstr "Dont show this again"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:150
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:230
msgid "Enable WAN drop/reject logging"
msgstr "Enable WAN drop/reject logging"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:104
msgid "Enable WAN zone drop/reject logging (same as Network → Firewall)."
msgstr "Enable WAN zone drop/reject logging (same as Network → Firewall)."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:107
msgid "Enable logging"
msgstr "Включить журнал"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1672
msgid "Enable logging turns on WAN zone drop/reject logging only (same as Network → Firewall). It does not add rules or log normal LAN browsing."
msgstr "Enable logging turns on WAN zone drop/reject logging only (same as Network → Firewall). It does not add rules or log normal LAN browsing."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:107
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:150
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:230
msgid "Enabling…"
msgstr "Enabling…"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1057
msgid "Exclude"
msgstr "Исключить"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:80
msgid "Exclude instead"
msgstr "Исключить вместо этого"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:53
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:71
msgid "Filter by %s"
msgstr "Фильтр по %s"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:136
msgid "Filter by interface"
msgstr "Фильтр по интерфейсу"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:112
msgid "Filter logs by rule (hint: %s). Ctrl+click to open firewall settings."
msgstr "Фильтр журналов по правилу (подсказка: %s). Ctrl+клик открывает настройки файрвола."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1497
#: applications/luci-app-fwlive/root/usr/share/luci/menu.d/luci-app-fwlive.json:3
msgid "Firewall Live View"
msgstr "Просмотр файрвола в реальном времени"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:43
msgid "Flags"
msgstr "Флаги"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:45
msgid "Flow"
msgstr "Поток"
#: applications/luci-app-fwlive/root/usr/share/rpcd/acl.d/luci-app-fwlive.json:3
msgid "Grant access to firewall live log view"
msgstr "Предоставить доступ к живому просмотру журнала межсетевого экрана"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1669
msgid "Help"
msgstr "Справка"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:853
msgid "High event rate — table refresh is throttled to protect the browser. The buffer still updates; refresh will resume automatically."
msgstr "High event rate — table refresh is throttled to protect the browser. The buffer still updates; refresh will resume automatically."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1048
msgid "ICMPv6"
msgstr "ICMPv6"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:35
msgid "IN"
msgstr "ВХОД"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1680
msgid "If Row tint looks missing, the active LuCI theme may omit success/error or info/warn CSS variables; fwlive falls back to local colors (air-gapped, no data leaves the device)."
msgstr "Если цветовая маркировка строк отсутствует, активная тема LuCI может не содержать CSS-переменные успе"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:205
msgid "If the WAN is quiet, wait for probes or use the optional ping check in Help / the enabling-logs guide."
msgstr "If the WAN is quiet, wait for probes or use the optional ping check in Help / the enabling-logs guide."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:80
msgid "Include instead"
msgstr "Включить вместо этого"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:34
msgid "Interface"
msgstr "Интерфейс"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1645
msgid "Interface (prefix ! to exclude)"
msgstr "Интерфейс (! в начале для исключения)"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:165
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:232
msgid "Ill configure this under Network → Firewall"
msgstr "Ill configure this under Network → Firewall"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:194
msgid "Kernel log modules missing"
msgstr "Kernel log modules missing"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:195
msgid "Kernel netfilter log modules are missing. Install kmod-nf-log-ipv4 and kmod-nf-log-ipv6 (or kmod-nf-log / kmod-nf-log6), then reload the firewall."
msgstr "Отсутствуют модули журнала netfilter ядра. Установите kmod-nf-log-ipv4 и kmod-nf-log-ipv6 (или kmod-nf-log / kmod-nf-log6), затем перезагрузите файрвол."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:44
msgid "Len"
msgstr "Длина"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1576
msgid "Limit"
msgstr "Лимит"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:210
msgid "Logging is off on this router"
msgstr "Logging is off on this router"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:252
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:256
msgid "Manual test (System → Terminal):"
msgstr "Ручной тест (Система → Терминал):"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:46
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:221
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1547
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1552
msgid "Message"
msgstr "Сообщение"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1643
msgid "More filters"
msgstr "Больше фильтров"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:37
msgid "Network → Firewall"
msgstr "Network → Firewall"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:187
msgid "No WAN firewall zone found in /etc/config/firewall. Configure zones under"
msgstr "Зона WAN брандмауэра не найдена в /etc/config/firewall. Настройте зоны в"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:185
msgid "No WAN zone found"
msgstr "No WAN zone found"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:163
msgid "Not now"
msgstr "Not now"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:220
msgid "Nothing changes until you click Enable."
msgstr "Nothing changes until you click Enable."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:36
msgid "OUT"
msgstr "ВЫХОД"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1567
msgid "One line"
msgstr "Одна строка"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:206
msgid "Open firewall zone settings"
msgstr "Открыть настройки зон файрвола"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:211
msgid "OpenWrt does not write firewall events to the log until you turn logging on. Live View only shows what the firewall already logs — it does not add allow/deny rules."
msgstr "OpenWrt does not write firewall events to the log until you turn logging on. Live View only shows what the firewall already logs — it does not add allow/deny rules."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1595
msgid "Palette"
msgstr "Палитра"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:953
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1517
msgid "Pause"
msgstr "Пауза"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:951
msgid "Paused"
msgstr "Пауза"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:38
msgid "Proto"
msgstr "Протокол"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1631
msgid "Protocol — common values"
msgstr "Протокол — частые значения"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1613
msgid "Quick search"
msgstr "Быстрый поиск"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:91
msgid "Remove filter"
msgstr "Удалить фильтр"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:953
msgid "Resume"
msgstr "Продолжить"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1588
msgid "Row tint"
msgstr "Цвет строк"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1678
msgid "Row tint shows pass/deny row backgrounds when checked. Choose Classic (green/red, default) or Accessible (teal/orange). Action text stays colored either way."
msgstr "Подсветка строк показывает фоны pass/deny, когда включена. Выберите классический (зелёный/красный, по умолчанию) или доступный (бирюзовый/оранжевый). Цвет текста действия сохраняется в любом случае."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1507
msgid "Row tint used a local color fallback because the active LuCI theme did not apply pass/deny backgrounds."
msgstr "Цветовая маркировка строк использовала локальный запасной цвет, так как активная тема LuCI не применяет фон пропуска/отказа."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:33
msgid "Rule"
msgstr "Правило"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:41
msgid "SPort"
msgstr "Порт ист."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1607
msgid "Show hostnames"
msgstr "Показывать имена "
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1586
msgid "Show pass/deny row background colors"
msgstr "Показывать цвета фона строк pass/deny"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1533
msgid "Simple"
msgstr "Простой"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:39
msgid "Source"
msgstr "Источник"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1646
msgid "Source IP contains (! to exclude)"
msgstr "IP источника содержит (! для исключения)"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1647
msgid "Source port (! to exclude)"
msgstr "Порт источника (! для исключения)"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1674
msgid "The rate shown for WAN logging is the firewall zone log_limit. OpenWrt defaults to 10/minute when no explicit limit is configured; fwlive does not impose this cap."
msgstr "Показанная частота WAN-журнала — log_limit зоны. По умолчанию OpenWrt: 10/мин без явного лимита; fwlive сам лимит не задаёт."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1671
msgid "The table updates automatically when your firewall logs traffic. Use Pause if it moves too fast."
msgstr "Таблица обновляется автоматически, когда файрвол пишет в журнал. Используйте Паузу, если слишком быстро."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1508
msgid "Theme tint fallback"
msgstr "Запасной цвет темы"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:31
msgid "Time"
msgstr "Время"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:219
msgid "Turns on WAN zone drop/reject logging (same as Network → Firewall → wan → Log). Rate-limited by the zone log_limit (OpenWrt default 10/minute). Normal LAN browsing is not logged."
msgstr "Turns on WAN zone drop/reject logging (same as Network → Firewall → wan → Log). Rate-limited by the zone log_limit (OpenWrt default 10/minute). Normal LAN browsing is not logged."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:126
msgid "Undo:"
msgstr "Undo:"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1679
msgid "Use Detail for all columns (flags, length, raw message)."
msgstr "Используйте «Подробно» для всех столбцов (флаги, длина, исходное сообщение)."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1521
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1525
msgid "View"
msgstr "Вид"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:591
msgid "WAN drop/reject logging is off."
msgstr "WAN drop/reject logging is off."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:204
msgid "WAN drop/reject logging is on. Blocked inbound WAN traffic will show up here. Normal LAN browsing will not."
msgstr "WAN drop/reject logging is on. Blocked inbound WAN traffic will show up here. Normal LAN browsing will not."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:555
msgid "WAN drop/reject logging is on. Blocked inbound traffic should appear here as it happens — not normal LAN browsing."
msgstr "WAN drop/reject logging is on. Blocked inbound traffic should appear here as it happens — not normal LAN browsing."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:557
msgid "WAN logging is already enabled."
msgstr "WAN-журналирование уже включено."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:88
msgid "WAN logging on"
msgstr "WAN-журнал: ВКЛ"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:94
msgid "WAN logging on (%s). Click to disable."
msgstr "WAN-журнал: ВКЛ (%s). Нажмите, чтобы отключить."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:77
msgid "WAN logging unavailable: missing kernel log modules"
msgstr "WAN-журналирование недоступно: отсутствуют модули журнала ядра"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:70
msgid "WAN logging unavailable: no WAN zone"
msgstr "WAN-журналирование недоступно: нет зоны WAN"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:203
msgid "Waiting for firewall events"
msgstr "Waiting for firewall events"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:951
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1502
msgid "Watching"
msgstr "Наблюдение"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1560
msgid "Wrap"
msgstr "Перенос"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:123
msgid "allow/deny rules, LAN logging, or anything else."
msgstr "allow/deny rules, LAN logging, or anything else."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:786
msgid "buffer full"
msgstr "буфер заполнен"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:81
msgid "default 10/minute"
msgstr "по умолчанию 10/мин"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:28
msgid "is"
msgstr "есть"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:781
msgid "loading buffer"
msgstr "загрузка буфера"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:881
msgid "loading…"
msgstr "loading…"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:37
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:44
msgid "not"
msgstr "не"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1065
msgid "not AH"
msgstr "не AH"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1064
msgid "not ESP"
msgstr "не ESP"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1063
msgid "not GRE"
msgstr "не GRE"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1060
msgid "not ICMP"
msgstr "не ICMP"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1061
msgid "not ICMPv6"
msgstr "не ICMPv6"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1062
msgid "not IGMP"
msgstr "не IGMP"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1066
msgid "not SCTP"
msgstr "не SCTP"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1058
msgid "not TCP"
msgstr "не TCP"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1059
msgid "not UDP"
msgstr "не UDP"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1623
msgid "not block"
msgstr "не блокировать"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1622
msgid "not drop"
msgstr "не отбрасывать"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1621
msgid "not pass"
msgstr "не пропускать"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1624
msgid "not reject"
msgstr "не отклонять"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1625
msgid "not unknown"
msgstr "не неизвестно"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1636
msgid "or type…"
msgstr "или введите…"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:788
msgid "render paused (high rate)"
msgstr "рендеринг приостановлен (высокая частота)"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:790
msgid "scroll frozen — scroll to top to follow live"
msgstr "scroll frozen — scroll to top to follow live"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:118
msgid "sets log on the WAN firewall zone and reloads the firewall."
msgstr "sets log on the WAN firewall zone and reloads the firewall."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:254
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:258
msgid "then ping the router."
msgstr "затем выполните ping маршрутизатора."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:128
msgid "turn it back off with the WAN logging on control on the watch strip."
msgstr "turn it back off with the WAN logging on control on the watch strip."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:506
msgid "using fw4"
msgstr "используется fw4"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:504
msgid "using iptables"
msgstr "используется iptables"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:89
msgid "· %s"
msgstr "· %s"
@@ -0,0 +1,636 @@
msgid ""
msgstr "Content-Type: text/plain; charset=UTF-8"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:892
msgid "%d matching · %d/%d stored"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:895
msgid "0 matching · %d/%d stored"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:250
msgid "Accessible (teal/orange)"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:32
msgid "Action"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:594
msgid "Administrator access is required to disable logging."
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:562
msgid "Administrator access is required to enable logging."
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1058
msgid "Also seen"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:547
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:583
msgid "Another change is staged for the firewall; apply or revert it first."
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1623
msgid "Any action"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1051
msgid "Any protocol"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:113
msgid "Before you enable logging"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:545
msgid "Cannot enable logging until kernel log modules are installed."
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:116
msgid "Changes:"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:249
msgid "Classic (green/red)"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1607
msgid "Classic uses green/red; Accessible uses teal/orange"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:116
msgid "Clear all"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1663
msgid ""
"Click a cell to filter · ≠ on a chip to exclude · Ctrl+click a rule for "
"firewall settings · in Simple view, click a row for the full message"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1684
msgid ""
"Click a row (Time or other non-link cells) to see the full log line (Simple "
"view)."
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:113
msgid "Click a row for the full message"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1685
msgid ""
"Click an IP, action, or protocol to filter; use the Protocol menu (or ≠ on a "
"chip) to exclude."
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1052
msgid "Common"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:910
msgid "Connection lost — retrying…"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:585
msgid "Could not disable logging."
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:549
msgid "Could not enable logging."
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1645
msgid "Custom protocol (prefix ! to exclude). Overrides the menu when set."
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:42
msgid "DPort"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:40
msgid "Destination"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1656
msgid "Destination IP contains (! to exclude)"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1657
msgid "Destination port (! to exclude)"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1548
msgid "Detail"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:37
msgid "Dir"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:85
msgid "Disabling…"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1581
msgid "Display options"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1681
msgid "Display options on the bar set Limit, row tint, palette, and hostnames."
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:121
msgid "Does not change:"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:138
msgid "Dont show this again"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:150
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:230
msgid "Enable WAN drop/reject logging"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:104
msgid "Enable WAN zone drop/reject logging (same as Network → Firewall)."
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:107
msgid "Enable logging"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1680
msgid ""
"Enable logging turns on WAN zone drop/reject logging only (same as Network → "
"Firewall). It does not add rules or log normal LAN browsing."
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:107
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:150
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:230
msgid "Enabling…"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1065
msgid "Exclude"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:80
msgid "Exclude instead"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:53
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:71
msgid "Filter by %s"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:136
msgid "Filter by interface"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:112
msgid "Filter logs by rule (hint: %s). Ctrl+click to open firewall settings."
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1505
#: applications/luci-app-fwlive/root/usr/share/luci/menu.d/luci-app-fwlive.json:3
msgid "Firewall Live View"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:43
msgid "Flags"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:45
msgid "Flow"
msgstr ""
#: applications/luci-app-fwlive/root/usr/share/rpcd/acl.d/luci-app-fwlive.json:3
msgid "Grant access to firewall live log view"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1677
msgid "Help"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:861
msgid ""
"High event rate — table refresh is throttled to protect the browser. The "
"buffer still updates; refresh will resume automatically."
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1056
msgid "ICMPv6"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:35
msgid "IN"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1688
msgid ""
"If Row tint looks missing, the active LuCI theme may omit success/error or "
"info/warn CSS variables; fwlive falls back to local colors (air-gapped, no "
"data leaves the device)."
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:205
msgid ""
"If the WAN is quiet, wait for probes or use the optional ping check in "
"Help / the enabling-logs guide."
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:80
msgid "Include instead"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:34
msgid "Interface"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1653
msgid "Interface (prefix ! to exclude)"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:165
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:232
msgid "Ill configure this under Network → Firewall"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:194
msgid "Kernel log modules missing"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:195
msgid ""
"Kernel netfilter log modules are missing. Install kmod-nf-log-ipv4 and kmod-"
"nf-log-ipv6 (or kmod-nf-log / kmod-nf-log6), then reload the firewall."
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:44
msgid "Len"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1584
msgid "Limit"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:210
msgid "Logging is off on this router"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:252
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:256
msgid "Manual test (System → Terminal):"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:46
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:221
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1555
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1560
msgid "Message"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1651
msgid "More filters"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:37
msgid "Network → Firewall"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:187
msgid ""
"No WAN firewall zone found in /etc/config/firewall. Configure zones under"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:185
msgid "No WAN zone found"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:163
msgid "Not now"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:220
msgid "Nothing changes until you click Enable."
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:36
msgid "OUT"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1575
msgid "One line"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:206
msgid "Open firewall zone settings"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:211
msgid ""
"OpenWrt does not write firewall events to the log until you turn logging on. "
"Live View only shows what the firewall already logs — it does not add allow/"
"deny rules."
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1603
msgid "Palette"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:961
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1525
msgid "Pause"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:959
msgid "Paused"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:38
msgid "Proto"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1639
msgid "Protocol — common values"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1621
msgid "Quick search"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:91
msgid "Remove filter"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:961
msgid "Resume"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1596
msgid "Row tint"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1686
msgid ""
"Row tint shows pass/deny row backgrounds when checked. Choose Classic (green/"
"red, default) or Accessible (teal/orange). Action text stays colored either "
"way."
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1515
msgid ""
"Row tint used a local color fallback because the active LuCI theme did not "
"apply pass/deny backgrounds."
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:33
msgid "Rule"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:41
msgid "SPort"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1615
msgid "Show hostnames"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1594
msgid "Show pass/deny row background colors"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1541
msgid "Simple"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:39
msgid "Source"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1654
msgid "Source IP contains (! to exclude)"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1655
msgid "Source port (! to exclude)"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1682
msgid ""
"The rate shown for WAN logging is the firewall zone log_limit. OpenWrt "
"defaults to 10/minute when no explicit limit is configured; fwlive does not "
"impose this cap."
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1679
msgid ""
"The table updates automatically when your firewall logs traffic. Use Pause "
"if it moves too fast."
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1516
msgid "Theme tint fallback"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:31
msgid "Time"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:219
msgid ""
"Turns on WAN zone drop/reject logging (same as Network → Firewall → wan → "
"Log). Rate-limited by the zone log_limit (OpenWrt default 10/minute). Normal "
"LAN browsing is not logged."
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:126
msgid "Undo:"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1687
msgid "Use Detail for all columns (flags, length, raw message)."
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1529
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1533
msgid "View"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:591
msgid "WAN drop/reject logging is off."
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:204
msgid ""
"WAN drop/reject logging is on. Blocked inbound WAN traffic will show up "
"here. Normal LAN browsing will not."
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:555
msgid ""
"WAN drop/reject logging is on. Blocked inbound traffic should appear here as "
"it happens — not normal LAN browsing."
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:557
msgid "WAN logging is already enabled."
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:88
msgid "WAN logging on"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:94
msgid "WAN logging on (%s). Click to disable."
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:77
msgid "WAN logging unavailable: missing kernel log modules"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:70
msgid "WAN logging unavailable: no WAN zone"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:203
msgid "Waiting for firewall events"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:959
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1510
msgid "Watching"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1568
msgid "Wrap"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:123
msgid "allow/deny rules, LAN logging, or anything else."
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:794
msgid "buffer full"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:81
msgid "default 10/minute"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:28
msgid "is"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:789
msgid "loading buffer"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:889
msgid "loading…"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:37
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:44
msgid "not"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1073
msgid "not AH"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1072
msgid "not ESP"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1071
msgid "not GRE"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1068
msgid "not ICMP"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1069
msgid "not ICMPv6"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1070
msgid "not IGMP"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1074
msgid "not SCTP"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1066
msgid "not TCP"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1067
msgid "not UDP"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1631
msgid "not block"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1630
msgid "not drop"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1629
msgid "not pass"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1632
msgid "not reject"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1633
msgid "not unknown"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1644
msgid "or type…"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:796
msgid "render paused (high rate)"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:798
msgid "scroll frozen — scroll to top to follow live"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:118
msgid "sets log on the WAN firewall zone and reloads the firewall."
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:254
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:258
msgid "then ping the router."
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:128
msgid "turn it back off with the WAN logging on control on the watch strip."
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:506
msgid "using fw4"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:504
msgid "using iptables"
msgstr ""
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:89
msgid "· %s"
msgstr ""
+1
View File
@@ -0,0 +1 @@
zh_Hans
@@ -0,0 +1,609 @@
msgid ""
msgstr ""
"Project-Id-Version: luci-app-fwlive\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-31 02:17+0000\n"
"PO-Revision-Date: 2026-07-31 02:17+0000\n"
"Last-Translator: lucas-albers-lz4\n"
"Language-Team: Chinese (Simplified)\n"
"Language: zh_Hans\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:884
msgid "%d matching · %d/%d stored"
msgstr "%d matching · %d/%d stored"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:887
msgid "0 matching · %d/%d stored"
msgstr "0 matching · %d/%d stored"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:250
msgid "Accessible (teal/orange)"
msgstr "可访问(青绿/橙色)"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:32
msgid "Action"
msgstr "动作"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:594
msgid "Administrator access is required to disable logging."
msgstr "需要管理员权限才能"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:562
msgid "Administrator access is required to enable logging."
msgstr "需要管理员权限才能开启日志记录。"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1050
msgid "Also seen"
msgstr "其他常见"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:547
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:583
msgid "Another change is staged for the firewall; apply or revert it first."
msgstr "防火墙另有未提交的更改;请先应用或还原。"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1615
msgid "Any action"
msgstr "所有动作"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1043
msgid "Any protocol"
msgstr "任意协议"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:113
msgid "Before you enable logging"
msgstr "Before you enable logging"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:545
msgid "Cannot enable logging until kernel log modules are installed."
msgstr "å"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:116
msgid "Changes:"
msgstr "Changes:"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:249
msgid "Classic (green/red)"
msgstr "经"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1599
msgid "Classic uses green/red; Accessible uses teal/orange"
msgstr "经"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:116
msgid "Clear all"
msgstr "æ"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1655
msgid "Click a cell to filter · ≠ on a chip to exclude · Ctrl+click a rule for firewall settings · in Simple view, click a row for the full message"
msgstr "点击单元格筛选 · 芯片上的 ≠ 排除 · Ctrl+点击规则打开防火墙设置 · 在简单视图中点击行查看完整消息"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1676
msgid "Click a row (Time or other non-link cells) to see the full log line (Simple view)."
msgstr "点击一行(时间或其他非链接单元格)可查看完整日志行(简单视图)。"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:113
msgid "Click a row for the full message"
msgstr "点击行可查看完整消息"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1677
msgid "Click an IP, action, or protocol to filter; use the Protocol menu (or ≠ on a chip) to exclude."
msgstr "Click an IP, action, or protocol to filter; use the Protocol menu (or ≠ on a chip) to exclude."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1044
msgid "Common"
msgstr "常用"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:902
msgid "Connection lost — retrying…"
msgstr "Connection lost — retrying…"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:585
msgid "Could not disable logging."
msgstr "无法"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:549
msgid "Could not enable logging."
msgstr "无法开启日志记录。"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1637
msgid "Custom protocol (prefix ! to exclude). Overrides the menu when set."
msgstr "自定义协议(! 排除)。有内容时覆盖菜单。"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:42
msgid "DPort"
msgstr "目标端口"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:40
msgid "Destination"
msgstr "目标地址"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1648
msgid "Destination IP contains (! to exclude)"
msgstr "目标 IP å"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1649
msgid "Destination port (! to exclude)"
msgstr "目标端口(! 表示排除)"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1540
msgid "Detail"
msgstr "详细"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:37
msgid "Dir"
msgstr "方向"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:85
msgid "Disabling…"
msgstr "Disabling…"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1573
msgid "Display options"
msgstr "显示选项"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1673
msgid "Display options on the bar set Limit, row tint, palette, and hostnames."
msgstr "栏上的显示选项可设置行数限制、行着色、调色板和主机名。"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:121
msgid "Does not change:"
msgstr "Does not change:"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:138
msgid "Dont show this again"
msgstr "Dont show this again"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:150
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:230
msgid "Enable WAN drop/reject logging"
msgstr "Enable WAN drop/reject logging"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:104
msgid "Enable WAN zone drop/reject logging (same as Network → Firewall)."
msgstr "Enable WAN zone drop/reject logging (same as Network → Firewall)."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:107
msgid "Enable logging"
msgstr "开启日志"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1672
msgid "Enable logging turns on WAN zone drop/reject logging only (same as Network → Firewall). It does not add rules or log normal LAN browsing."
msgstr "Enable logging turns on WAN zone drop/reject logging only (same as Network → Firewall). It does not add rules or log normal LAN browsing."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:107
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:150
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:230
msgid "Enabling…"
msgstr "Enabling…"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1057
msgid "Exclude"
msgstr "排除"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:80
msgid "Exclude instead"
msgstr "改为排除"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:53
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:71
msgid "Filter by %s"
msgstr "按 %s 筛选"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:136
msgid "Filter by interface"
msgstr "按接口筛选"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:112
msgid "Filter logs by rule (hint: %s). Ctrl+click to open firewall settings."
msgstr "按规则筛选日志(提示:%s)。Ctrl+点击可打开防火墙设置。"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1497
#: applications/luci-app-fwlive/root/usr/share/luci/menu.d/luci-app-fwlive.json:3
msgid "Firewall Live View"
msgstr "防火墙实时视图"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:43
msgid "Flags"
msgstr "标志"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:45
msgid "Flow"
msgstr "流"
#: applications/luci-app-fwlive/root/usr/share/rpcd/acl.d/luci-app-fwlive.json:3
msgid "Grant access to firewall live log view"
msgstr "授予防火墙实时日志视图的访问权限"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1669
msgid "Help"
msgstr "帮助"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:853
msgid "High event rate — table refresh is throttled to protect the browser. The buffer still updates; refresh will resume automatically."
msgstr "High event rate — table refresh is throttled to protect the browser. The buffer still updates; refresh will resume automatically."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1048
msgid "ICMPv6"
msgstr "ICMPv6"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:35
msgid "IN"
msgstr "IN"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1680
msgid "If Row tint looks missing, the active LuCI theme may omit success/error or info/warn CSS variables; fwlive falls back to local colors (air-gapped, no data leaves the device)."
msgstr "如果行染色看起来缺失,当前 LuCI 主题可能未提供成功/错误 CSS 变量;fwlive 会回退到本地颜色(隔离运行,数据不会离开本机)。"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:205
msgid "If the WAN is quiet, wait for probes or use the optional ping check in Help / the enabling-logs guide."
msgstr "If the WAN is quiet, wait for probes or use the optional ping check in Help / the enabling-logs guide."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:80
msgid "Include instead"
msgstr "改为å"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:34
msgid "Interface"
msgstr "接口"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1645
msgid "Interface (prefix ! to exclude)"
msgstr "接口(! 前缀表示排除)"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:165
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:232
msgid "Ill configure this under Network → Firewall"
msgstr "Ill configure this under Network → Firewall"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:194
msgid "Kernel log modules missing"
msgstr "Kernel log modules missing"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:195
msgid "Kernel netfilter log modules are missing. Install kmod-nf-log-ipv4 and kmod-nf-log-ipv6 (or kmod-nf-log / kmod-nf-log6), then reload the firewall."
msgstr "缺少å"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:44
msgid "Len"
msgstr "长度"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1576
msgid "Limit"
msgstr "限制"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:210
msgid "Logging is off on this router"
msgstr "Logging is off on this router"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:252
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:256
msgid "Manual test (System → Terminal):"
msgstr "手动测试(系统 → 终端):"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:46
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:221
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1547
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1552
msgid "Message"
msgstr "消息"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1643
msgid "More filters"
msgstr "更多筛选"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:37
msgid "Network → Firewall"
msgstr "Network → Firewall"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:187
msgid "No WAN firewall zone found in /etc/config/firewall. Configure zones under"
msgstr "在 /etc/config/firewall 中未找到 WAN 防火墙区域。请在以下位置配置区域"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:185
msgid "No WAN zone found"
msgstr "No WAN zone found"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:163
msgid "Not now"
msgstr "Not now"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:220
msgid "Nothing changes until you click Enable."
msgstr "Nothing changes until you click Enable."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:36
msgid "OUT"
msgstr "出接口"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1567
msgid "One line"
msgstr "单行"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:206
msgid "Open firewall zone settings"
msgstr "打开防火墙区域设置"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:211
msgid "OpenWrt does not write firewall events to the log until you turn logging on. Live View only shows what the firewall already logs — it does not add allow/deny rules."
msgstr "OpenWrt does not write firewall events to the log until you turn logging on. Live View only shows what the firewall already logs — it does not add allow/deny rules."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1595
msgid "Palette"
msgstr "Palette"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:953
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1517
msgid "Pause"
msgstr "暂停"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:951
msgid "Paused"
msgstr "已暂停"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:38
msgid "Proto"
msgstr "协议"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1631
msgid "Protocol — common values"
msgstr "协议 — 常用值"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1613
msgid "Quick search"
msgstr "快速搜索"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:91
msgid "Remove filter"
msgstr "移除筛选"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:953
msgid "Resume"
msgstr "ç»§ç»­"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1588
msgid "Row tint"
msgstr "行染色"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1678
msgid "Row tint shows pass/deny row backgrounds when checked. Choose Classic (green/red, default) or Accessible (teal/orange). Action text stays colored either way."
msgstr "勾选后行着色显示放行/拒绝行背景。可选经典(绿/红,默认)或无障碍(青绿/橙)。操作文本在两种模式下均保持着色。"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1507
msgid "Row tint used a local color fallback because the active LuCI theme did not apply pass/deny backgrounds."
msgstr "行染色使用了本地颜色回退,因为当前 LuCI 主题未应用放行/拒绝背景色。"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:33
msgid "Rule"
msgstr "规则"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:41
msgid "SPort"
msgstr "源端口"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1607
msgid "Show hostnames"
msgstr "显示主机名"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1586
msgid "Show pass/deny row background colors"
msgstr "显示放行/拒绝行背景色"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1533
msgid "Simple"
msgstr "简洁"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:39
msgid "Source"
msgstr "源地址"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1646
msgid "Source IP contains (! to exclude)"
msgstr "源 IP å"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1647
msgid "Source port (! to exclude)"
msgstr "源端口(! 表示排除)"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1674
msgid "The rate shown for WAN logging is the firewall zone log_limit. OpenWrt defaults to 10/minute when no explicit limit is configured; fwlive does not impose this cap."
msgstr "WAN 日志显示的速率是防火墙区域的 log_limit。未"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1671
msgid "The table updates automatically when your firewall logs traffic. Use Pause if it moves too fast."
msgstr "防火墙写"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1508
msgid "Theme tint fallback"
msgstr "主题着色回退"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:31
msgid "Time"
msgstr "时间"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:219
msgid "Turns on WAN zone drop/reject logging (same as Network → Firewall → wan → Log). Rate-limited by the zone log_limit (OpenWrt default 10/minute). Normal LAN browsing is not logged."
msgstr "Turns on WAN zone drop/reject logging (same as Network → Firewall → wan → Log). Rate-limited by the zone log_limit (OpenWrt default 10/minute). Normal LAN browsing is not logged."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:126
msgid "Undo:"
msgstr "Undo:"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1679
msgid "Use Detail for all columns (flags, length, raw message)."
msgstr "使用「详细」可查看全部列(标志、长度、原始消息)。"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1521
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1525
msgid "View"
msgstr "视图"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:591
msgid "WAN drop/reject logging is off."
msgstr "WAN drop/reject logging is off."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:204
msgid "WAN drop/reject logging is on. Blocked inbound WAN traffic will show up here. Normal LAN browsing will not."
msgstr "WAN drop/reject logging is on. Blocked inbound WAN traffic will show up here. Normal LAN browsing will not."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:555
msgid "WAN drop/reject logging is on. Blocked inbound traffic should appear here as it happens — not normal LAN browsing."
msgstr "WAN drop/reject logging is on. Blocked inbound traffic should appear here as it happens — not normal LAN browsing."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:557
msgid "WAN logging is already enabled."
msgstr "WAN 日志已处于开启状态。"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:88
msgid "WAN logging on"
msgstr "WAN 日志:开"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:94
msgid "WAN logging on (%s). Click to disable."
msgstr "WAN 日志:开(%s)。点击可"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:77
msgid "WAN logging unavailable: missing kernel log modules"
msgstr "WAN 日志不可用:缺少å"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:70
msgid "WAN logging unavailable: no WAN zone"
msgstr "WAN 日志不可用:无 WAN 区域"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:203
msgid "Waiting for firewall events"
msgstr "Waiting for firewall events"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:951
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1502
msgid "Watching"
msgstr "监视中"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1560
msgid "Wrap"
msgstr "自动换行"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:123
msgid "allow/deny rules, LAN logging, or anything else."
msgstr "allow/deny rules, LAN logging, or anything else."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:786
msgid "buffer full"
msgstr "缓冲区已满"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:81
msgid "default 10/minute"
msgstr "默认 10 条/分钟"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:28
msgid "is"
msgstr "是"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:781
msgid "loading buffer"
msgstr "正在加载缓冲区"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:881
msgid "loading…"
msgstr "loading…"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:37
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:44
msgid "not"
msgstr "非"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1065
msgid "not AH"
msgstr "非 AH"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1064
msgid "not ESP"
msgstr "非 ESP"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1063
msgid "not GRE"
msgstr "非 GRE"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1060
msgid "not ICMP"
msgstr "非 ICMP"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1061
msgid "not ICMPv6"
msgstr "非 ICMPv6"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1062
msgid "not IGMP"
msgstr "非 IGMP"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1066
msgid "not SCTP"
msgstr "非 SCTP"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1058
msgid "not TCP"
msgstr "非 TCP"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1059
msgid "not UDP"
msgstr "非 UDP"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1623
msgid "not block"
msgstr "非阻止"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1622
msgid "not drop"
msgstr "非丢弃"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1621
msgid "not pass"
msgstr "非放行"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1624
msgid "not reject"
msgstr "非拒绝"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1625
msgid "not unknown"
msgstr "非未知"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1636
msgid "or type…"
msgstr "或输入…"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:788
msgid "render paused (high rate)"
msgstr "渲染暂停(速率过高)"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:790
msgid "scroll frozen — scroll to top to follow live"
msgstr "scroll frozen — scroll to top to follow live"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:118
msgid "sets log on the WAN firewall zone and reloads the firewall."
msgstr "sets log on the WAN firewall zone and reloads the firewall."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:254
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:258
msgid "then ping the router."
msgstr "然后 ping 路由器。"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:128
msgid "turn it back off with the WAN logging on control on the watch strip."
msgstr "turn it back off with the WAN logging on control on the watch strip."
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:506
msgid "using fw4"
msgstr "使用 fw4"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:504
msgid "using iptables"
msgstr "使用 iptables"
#: applications/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:89
msgid "· %s"
msgstr "· %s"
@@ -0,0 +1,139 @@
# SPDX-License-Identifier: Apache-2.0
# Copyright 2025-2026 Lucas Albers <lucas.b.albers@gmail.com>
#
# GENERATED FILE — do not edit. Run: ./scripts/gen-all.sh
# source: core/fwlive-log.js CLASSIFY_SPEC
# Shared isFirewallEvent parity logic (shell). Sourced by fwlive-log-filter.sh and tests.
# One awk process classifies a batch (MODE=json) or one message (default).
_fwlive_run_classify() {
awk -v MODE="${1:-msg}" "$(cat <<'AWK'
function normalize(s, keys, n, i, k) {
n = split("IN OUT SRC DST PROTO SPT DPT LEN MAC TYPE CODE TTL TOS PREC DF", keys, " ")
for (i = 1; i <= n; i++) {
k = keys[i]
while (match(s, "[^[:space:]]" k "="))
s = substr(s, 1, RSTART) " " substr(s, RSTART + 1)
}
return s
}
function trim(s) {
sub(/^[[:space:]]+/, "", s)
sub(/[[:space:]]+$/, "", s)
return s
}
function has_kv(s, key) {
return s ~ "(^|[^A-Za-z0-9_])" key "="
}
function has_hint(s, lc) {
lc = tolower(s)
return lc ~ "(^|[^a-z0-9_])(fw4|nft|iptables|kernel|firewall)([^a-z0-9_]|$)"
}
function non_fw_prefix(s, lc) {
lc = tolower(s)
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, " ")
lc = tolower(s)
best = ""
bestpos = length(s) + 1
for (i = 1; i <= n; i++) {
w = words[i]
wl = tolower(w)
start = 1
while (start <= length(lc) && match(substr(lc, start), wl)) {
pos = start + RSTART - 1
before = (pos == 1) ? " " : substr(lc, pos - 1, 1)
afterc = substr(lc, pos + length(wl), 1)
if (before !~ /[a-z0-9_]/ && (afterc == "" || afterc !~ /[a-z0-9_]/)) {
if (pos < bestpos) { bestpos = pos; best = w }
break
}
start = pos + 1
}
}
return best == "" ? "UNKNOWN" : best
}
function json_unhex4(h, n, i, c, v) {
n = 0
h = tolower(h)
for (i = 1; i <= 4; i++) {
c = substr(h, i, 1)
v = index("0123456789abcdef", c)
if (v == 0) return -1
n = n * 16 + v - 1
}
return n
}
function json_get_msg(obj, s, i, c, esc, out, hex, n) {
if (!match(obj, /"msg"[[:space:]]*:[[:space:]]*"/)) return ""
s = substr(obj, RSTART + RLENGTH)
out = ""
esc = 0
for (i = 1; i <= length(s); i++) {
c = substr(s, i, 1)
if (esc) {
if (c == "n") out = out "\n"
else if (c == "t") out = out "\t"
else if (c == "r") out = out "\r"
else if (c == "b") out = out "\b"
else if (c == "f") out = out "\f"
else if (c == "u") {
hex = substr(s, i + 1, 4)
n = (length(hex) == 4) ? json_unhex4(hex) : -1
if (n >= 1 && n <= 255) out = out sprintf("%c", n)
else if (n < 0) out = out "u"
if (n >= 0) i += 4
} else out = out c
esc = 0
} else if (c == "\\") {
esc = 1
} else if (c == "\"") {
return out
} else {
out = out c
}
}
return out
}
function is_fw(s, action) {
s = trim(normalize(s))
if (s == "") return 0
if (non_fw_prefix(s)) return 0
action = detect_action(s)
if (has_kv(s, "SRC") && has_kv(s, "DST")) return 1
if ((has_kv(s, "IN") || has_kv(s, "OUT")) && (has_kv(s, "SRC") || has_kv(s, "DST") || has_kv(s, "PROTO") || has_kv(s, "SPT") || has_kv(s, "DPT"))) return 1
if (action != "UNKNOWN" && (has_kv(s, "IN") || has_kv(s, "OUT") || has_kv(s, "PROTO") || has_kv(s, "SRC") || has_kv(s, "DST"))) return 1
if (has_hint(s) && action != "UNKNOWN") return 1
if (has_hint(s) && (has_kv(s, "IN") || has_kv(s, "OUT") || has_kv(s, "SRC") || has_kv(s, "DST") || has_kv(s, "PROTO"))) return 1
return 0
}
BEGIN { if (MODE != "json") ORS = "" }
{
if (MODE == "json") {
msg = json_get_msg($0)
if (is_fw(msg)) {
if (out_n++) printf ","
printf "%s", $0
}
next
}
buf = (NR == 1) ? $0 : buf "\n" $0
}
END {
if (MODE != "json")
print is_fw(buf) ? 1 : 0
}
AWK
)"
}
is_firewall_event_msg() {
_r=$(printf '%s' "$1" | _fwlive_run_classify msg)
[ "$_r" = 1 ]
}
_fwlive_filter_json_entries() {
_fwlive_run_classify json
}
+27
View File
@@ -0,0 +1,27 @@
#!/bin/sh
# SPDX-License-Identifier: Apache-2.0
# Copyright 2025-2026 Lucas Albers <lucas.b.albers@gmail.com>
#
# Filter log.read JSON to firewall-only entries (isFirewallEvent parity).
# Log messages are treated as data (jsonfilter + awk stdin); never interpolated
# into shell command strings. Usage: ubus call log read '...' | fwlive-log-filter.sh
#
# Perf (#219): one jsonfilter for @.log[*] plus one awk classify. Process
# count is constant per poll, not O(entries).
if ! command -v jsonfilter >/dev/null 2>&1; then
command -v logger >/dev/null 2>&1 && logger -t fwlive "jsonfilter not found; cannot filter firewall logs"
printf '%s' '{"log":[],"error":"jsonfilter_missing"}'
exit 1
fi
FILTER_DIR="$(cd "$(dirname "$0")" && pwd)"
# shellcheck disable=SC1091 # classifier is a sibling file next to this script
. "$FILTER_DIR/fwlive-is-firewall-event.sh"
input="$(cat)"
[ -n "$input" ] || input='{"log":[]}'
printf '%s' '{"log":['
jsonfilter -s "$input" -e '@.log[*]' 2>/dev/null | _fwlive_filter_json_entries
printf '%s' ']}'
+712
View File
@@ -0,0 +1,712 @@
#!/bin/sh
# SPDX-License-Identifier: Apache-2.0
# Copyright 2025-2026 Lucas Albers <lucas.b.albers@gmail.com>
#
# WAN zone logging helpers for ubus fwlive (logging_status / enable / disable).
NF_LOG_IPV4='/proc/sys/net/netfilter/nf_log/2'
NF_LOG_IPV6='/proc/sys/net/netfilter/nf_log/10'
# Serialize the WAN logging read->compute->set->commit window across
# concurrent ubus write-ACL callers (#151): each toggle re-reads the current
# firewall.<zone>.log bit, computes a target, then uci set + uci commit. Two
# concurrent callers could otherwise interleave and last-commit-wins.
#
# BusyBox flock constraint: it has NO -w timeout. A stuck lock holder blocks
# any waiter until the holder exits or the device reboots. The critical
# section MUST stay SHORT (a few uci commands). Do NOT hold the lock across
# the /etc/init.d/firewall reload (can take seconds); the lock is released
# before reload, and reload-failure rollback is a best-effort UCI write
# outside the lock.
# 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}"
# RFC 8259 string escape. Lives here so prerm can source this file
# standalone (#222). rpcd sources us and must not redefine this.
json_escape() {
# Escape for JSON string content per RFC 8259 (including remaining C0 controls).
# Slurp stdin as one string. Default awk RS is newline — RS="" is
# paragraph mode and drops blank-line separators (a\n\nb → ab). A
# sentinel is appended so a leading, trailing, or lone newline is kept.
{ cat; printf '%s' '_'; } | awk '
BEGIN {
ORS = ""
for (n = 1; n < 128; n++)
ord[sprintf("%c", n)] = n
}
{
if (NR > 1) buf = buf "\n"
buf = buf $0
}
END {
if (length(buf) > 0)
buf = substr(buf, 1, length(buf) - 1)
for (i = 1; i <= length(buf); i++) {
c = substr(buf, i, 1)
if (c == "\\") printf "\\\\"
else if (c == "\"") printf "\\\""
else if (c == "\t") printf "\\t"
else if (c == "\r") printf "\\r"
else if (c == "\n") printf "\\n"
else {
o = ord[c] + 0
if (o > 0 && o < 32)
printf "\\u%04x", o
else if (o == 127)
printf "\\u007f"
else
printf "%s", c
}
}
}'
}
# Production lock dir must be root-owned with no group/other write (#204).
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 ]
}
# Acquire the exclusive logging lock on fd 9. Blocks until free; fails closed
# (return 1) only if the lock file cannot be opened or flock is unavailable.
# Create/tighten the lock to 0600 so unprivileged UIDs cannot take LOCK_EX on
# a world-readable fd (issue #167 / flock(2) allows exclusive locks on O_RDONLY).
acquire_wan_log_lock() {
# Fail closed on symlinks: chmod/chown/exec O_TRUNC follow the target as root
# (#204). Default lock lives under /etc/fwlive (root-only), not world-writable
# /var/lock. Re-check after create/tighten (TOCTOU).
lock_dir="$(dirname "$WAN_LOG_LOCK_FILE")"
[ -L "$lock_dir" ] && return 1
[ -L "$WAN_LOG_LOCK_FILE" ] && return 1
( umask 077; mkdir -p "$lock_dir" ) 2>/dev/null || return 1
[ -L "$lock_dir" ] && return 1
if [ -z "${FWLIVE_WAN_LOG_LOCK_FILE:-}" ]; then
wan_log_lock_dir_safe "$lock_dir" || return 1
fi
( umask 077; : >> "$WAN_LOG_LOCK_FILE" ) 2>/dev/null || return 1
[ -L "$WAN_LOG_LOCK_FILE" ] && return 1
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
flock 9 2>/dev/null || {
exec 9>&-
return 1
}
}
# Release the logging lock (explicit unlock, then close fd 9).
release_wan_log_lock() {
flock -u 9 2>/dev/null || true
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 (issue #168); skip
# non-zone sections that happen to share name='wan'.
_zones=$(uci -q show firewall 2>/dev/null \
| sed -n "s/^firewall\.\([^.]*\)\.name='wan'$/\1/p")
for zone in $_zones; do
[ -n "$zone" ] || continue
[ "$(uci -q get "firewall.${zone}" 2>/dev/null)" = "zone" ] || continue
printf '%s' "$zone"
return 0
done
return 0
}
firewall_changes_pending() {
pending="$(uci -q changes firewall 2>/dev/null)"
[ -n "$pending" ]
}
wan_zone_log_value() {
zone="$1"
[ -n "$zone" ] || return 1
uci -q get "firewall.${zone}.log" 2>/dev/null
}
# Match only firewall.<zone>.log deltas in `uci changes` — not log_limit.
wan_log_foreign_staged_lines() {
_zone="$1"
_staged="$2"
_ours_prefix="firewall.${_zone}.log="
_del="-firewall.${_zone}.log"
_del_sp="- firewall.${_zone}.log"
printf '%s\n' "$_staged" | awk -v p="$_ours_prefix" -v d="$_del" -v ds="$_del_sp" '
NF == 0 { next }
index($0, p) == 1 { next }
$0 == d || $0 == ds { next }
{ print }
'
}
wan_log_count_our_staged_lines() {
_zone="$1"
_staged="$2"
_ours_prefix="firewall.${_zone}.log="
_del="-firewall.${_zone}.log"
_del_sp="- firewall.${_zone}.log"
printf '%s\n' "$_staged" | awk -v p="$_ours_prefix" -v d="$_del" -v ds="$_del_sp" '
NF == 0 { next }
index($0, p) == 1 { c++; next }
$0 == d || $0 == ds { c++; next }
END { print c+0 }
'
}
wan_log_baseline_path() {
printf '%s' "$WAN_LOG_BASELINE_FILE"
}
# Snapshot firewall.<wan>.log once before the first enable changes UCI.
# Empty file means the option was unset. Skipped when baseline already exists.
maybe_snapshot_wan_log_baseline() {
zone="$1"
path="$(wan_log_baseline_path)"
[ -n "$zone" ] || return 1
[ -f "$path" ] && return 0
mkdir -p "$(dirname "$path")" 2>/dev/null || return 1
current=$(wan_zone_log_value "$zone")
printf '%s' "$current" >"$path" 2>/dev/null || return 1
return 0
}
# Restore WAN zone log from the install-time baseline (package prerm).
# No-op when baseline is missing. Returns 1 on failure; baseline file is
# kept until restore commits successfully.
#
# Hold the logging lock across the current-value read, equality cleanup,
# commit, and baseline unlink — otherwise a concurrent enable can snapshot
# the old baseline (or race the unlink) and lose the only restore value.
restore_wan_log_baseline() {
path="$(wan_log_baseline_path)"
[ -f "$path" ] || return 0
baseline=$(cat "$path" 2>/dev/null)
zone=$(find_wan_zone_section)
if [ -z "$zone" ]; then
logger -t fwlive "WAN log baseline restore skipped: no WAN zone" 2>/dev/null || true
return 1
fi
if ! acquire_wan_log_lock; then
logger -t fwlive "WAN log baseline restore skipped: lock unavailable" 2>/dev/null || true
return 1
fi
current=$(wan_zone_log_value "$zone")
if [ "${current:-}" = "${baseline:-}" ]; then
rm -f "$path"
release_wan_log_lock
return 0
fi
zone_json=$(json_null_or_string "$zone")
if firewall_changes_pending; then
release_wan_log_lock
logger -t fwlive "WAN log baseline restore skipped: firewall changes pending" 2>/dev/null || true
return 1
fi
if ! commit_wan_log_change "$zone" "$zone_json" "$baseline"; then
release_wan_log_lock
logger -t fwlive "WAN log baseline restore: commit gate failed" 2>/dev/null || true
return 1
fi
rm -f "$path"
release_wan_log_lock
reload_firewall || logger -t fwlive "WAN log baseline restored; firewall reload failed" 2>/dev/null || true
return 0
}
wan_filter_log_enabled() {
log_val="$1"
[ -n "$log_val" ] || return 1
case "$log_val" in
*[!0-9]*) return 1 ;;
esac
[ $((log_val & 1)) -ne 0 ]
}
wan_filter_log_target_value() {
current="$1"
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
}
# 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
cleared=$((current & ~1))
if [ "$cleared" -eq 0 ]; then
printf ''
else
printf '%d' "$cleared"
fi
}
read_nf_log_backend() {
path="$1"
[ -f "$path" ] || return 1
val=$(cat "$path" 2>/dev/null)
[ -n "$val" ] && [ "$val" != 'none' ]
}
check_nf_log_ipv4() {
read_nf_log_backend "$NF_LOG_IPV4"
}
check_nf_log_ipv6() {
read_nf_log_backend "$NF_LOG_IPV6"
}
logging_blockers_append() {
blocker="$1"
[ -n "$blocker" ] || return 0
esc=$(printf '%s' "$blocker" | json_escape)
if [ -n "$LOGGING_BLOCKERS" ]; then
LOGGING_BLOCKERS="${LOGGING_BLOCKERS},"
fi
LOGGING_BLOCKERS="${LOGGING_BLOCKERS}\"${esc}\""
}
collect_logging_blockers() {
zone="$1"
LOGGING_BLOCKERS=''
[ -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'
[ -n "$LOGGING_BLOCKERS" ] || return 0
return 1
}
json_null_or_string() {
val="$1"
if [ -z "$val" ]; then
printf 'null'
else
esc=$(printf '%s' "$val" | json_escape)
printf '"%s"' "$esc"
fi
}
build_logging_status_json() {
zone=$(find_wan_zone_section)
log_val=$(wan_zone_log_value "$zone")
limit_val=$( [ -n "$zone" ] && uci -q get "firewall.${zone}.log_limit" 2>/dev/null )
wan_log=false
if wan_filter_log_enabled "$log_val"; then
wan_log=true
fi
nf4=false
check_nf_log_ipv4 && nf4=true
nf6=false
check_nf_log_ipv6 && nf6=true
collect_logging_blockers "$zone"
blockers="[${LOGGING_BLOCKERS:-}]"
ready=false
if [ -n "$zone" ] && [ "$wan_log" = true ] && [ "$nf4" = true ] && [ "$nf6" = true ]; then
ready=true
fi
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,"blockers":%s}' \
"$zone_json" "$wan_log" "$limit_json" "$nf4" "$nf6" "$ready" "$blockers"
}
reload_firewall() {
if [ -x /etc/init.d/firewall ]; then
/etc/init.d/firewall reload >/dev/null 2>&1
return $?
fi
return 1
}
# Best-effort UCI rollback when firewall reload fails after commit.
restore_wan_zone_log() {
zone="$1"
previous="$2"
[ -n "$zone" ] || return 1
# Refuse to publish unrelated staged firewall deltas (issue #168).
if firewall_changes_pending; then
logger -t fwlive "WAN log rollback skipped: firewall changes pending" 2>/dev/null || true
return 1
fi
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
uci commit firewall 2>/dev/null || true
}
# Stage + commit the WAN log bit. Caller MUST hold the logging lock; this
# closes the read->compute->set->commit window so a concurrent toggle cannot
# commit between our read and our write (no lost update / no stale overwrite).
#
# TOCTOU hardening (#191): 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:
# 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.
# 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
# firewall.<wan>.log) are not distinguishable in the changes list.
# The caller-reported error is firewall_changes_pending: the LuCI view already
# maps it to the accurate "another change is staged" notice.
#
# target: value to stage; EMPTY means delete the option (bit fully cleared).
# Prints the failure JSON and returns 1 on abort or failure.
verify_wan_log_commit() {
zone="$1"
expected="$2"
readback="$(uci -q get "firewall.${zone}.log" 2>/dev/null || true)"
if [ -n "$expected" ]; then
[ "$readback" = "$expected" ] && return 0
return 1
fi
[ -z "$readback" ] && return 0
return 1
}
commit_wan_log_change() {
zone="$1"
zone_json="$2"
target="$3"
# Last-moment guard (#191): runs before OUR delta is staged, so a
# non-empty changes list here can only be a foreign writer's race.
if firewall_changes_pending; then
logger -t fwlive "WAN log toggle aborted at commit gate: firewall changes staged by another writer" 2>/dev/null || true
printf '{"ok":false,"changed":false,"wan_zone":%s,"error":"firewall_changes_pending"}' "$zone_json"
return 1
fi
# Staging is empty here — capture the committed value so a post-stage
# foreign race can undo our delta without `uci revert firewall`.
previous=$(wan_zone_log_value "$zone")
if [ -n "$target" ]; then
if ! uci set "firewall.${zone}.log=${target}"; then
printf '{"ok":false,"changed":false,"wan_zone":%s,"error":"uci_set_failed"}' "$zone_json"
return 1
fi
else
if ! uci delete "firewall.${zone}.log"; then
printf '{"ok":false,"changed":false,"wan_zone":%s,"error":"uci_delete_failed"}' "$zone_json"
return 1
fi
fi
# Post-stage guard (#191): anything besides our log option is foreign.
# Undo our staging only (set previous / delete to match committed); leave
# foreign deltas untouched and abort without commit.
_staged=$(uci -q changes firewall 2>/dev/null || true)
_foreign=$(wan_log_foreign_staged_lines "$zone" "$_staged")
if [ -n "$_foreign" ]; then
if [ -z "$previous" ]; then
uci delete "firewall.${zone}.log" 2>/dev/null || true
else
uci set "firewall.${zone}.log=${previous}" 2>/dev/null || true
fi
logger -t fwlive "WAN log toggle aborted after stage: firewall changes staged by another writer" 2>/dev/null || true
printf '{"ok":false,"changed":false,"wan_zone":%s,"error":"firewall_changes_pending"}' "$zone_json"
return 1
fi
if ! uci commit firewall; then
# Drop our staged log delta so a later toggle is not stuck on
# firewall_changes_pending from this package's own orphaned write —
# but ONLY when nothing foreign is staged: `uci revert firewall` is
# config-wide (uci has no option-level revert), and reverting would
# clobber a concurrent writer's uncommitted delta (CodeRabbit/luna
# fold, #191). With foreign staging present, leave it and warn.
_staged=$(uci -q changes firewall 2>/dev/null || true)
_total=$(printf '%s\n' "$_staged" | grep -c . 2>/dev/null || true)
_ours=$(wan_log_count_our_staged_lines "$zone" "$_staged")
if [ "${_total:-0}" -gt 0 ] && [ "${_total:-0}" -eq "${_ours:-0}" ]; then
uci -q revert firewall 2>/dev/null || true
else
logger -t fwlive "WAN log commit failed with foreign changes staged — not reverting (uci_commit_failed)" 2>/dev/null || true
fi
printf '{"ok":false,"changed":false,"wan_zone":%s,"error":"uci_commit_failed"}' "$zone_json"
return 1
fi
# Post-commit verification (#191): confirm the committed config really
# carries what we wrote (empty target = option must now be gone/empty).
# A mismatch means another writer overtook the commit: warn loudly but do
# NOT blind-revert (that would destroy unrelated committed data); the
# reload-failure rollback path still guards the ordinary failure case.
if ! verify_wan_log_commit "$zone" "$target"; then
logger -t fwlive "WAN log post-commit verify FAILED: wrote '${target:-<deleted>}', read back differs" 2>/dev/null || true
fi
return 0
}
# Firewall reload + best-effort UCI rollback on reload failure. The reload
# itself runs WITHOUT the logging lock (it can take seconds and a held lock
# would block a concurrent toggle until the holder exits — BusyBox flock has
# no -w timeout).
#
# The ROLLBACK re-acquires the lock (luna fold 2026-08-10): read->compare->
# restore is only atomic when no other writer can commit between the read and
# the restore. All writers hold the same flock, so re-acquiring it makes the
# decision-and-restore a serialized unit. The lock is held only for the few
# uci commands of the restore (short critical section), never across the
# reload. If the lock cannot be re-acquired, skip the rollback (report the
# reload failure; the next toggle self-corrects).
reload_and_report_wan_log() {
zone="$1"
previous="$2"
committed="$3"
fail_msg="$4"
success_msg="$5"
zone_json="$6"
if ! reload_firewall; then
# Re-acquire the logging lock so the rollback decision is atomic
# against concurrent toggles (no check-then-restore race).
if acquire_wan_log_lock; then
now="$(wan_zone_log_value "$zone")"
if [ "$now" = "$committed" ]; then
# Current value is still what THIS caller committed — restore
# the pre-commit value. (If a concurrent toggle committed the
# same value, the toggle is idempotent: the state intent is
# identical, so restoring previous is the correct rollback.)
if restore_wan_zone_log "$zone" "$previous"; then
logger -t fwlive "$fail_msg" 2>/dev/null || true
else
logger -t fwlive "Firewall reload failed; WAN log rollback skipped (pending changes or restore failed)" 2>/dev/null || true
fi
else
# A concurrent toggle changed the value after our commit; do
# not clobber it. Log the divergence and leave the newer value.
logger -t fwlive "Firewall reload failed; WAN log changed concurrently — rollback skipped" 2>/dev/null || true
fi
release_wan_log_lock
else
# Cannot re-acquire the lock: skip the rollback, report the
# reload failure. The next toggle self-corrects the state.
logger -t fwlive "Firewall reload failed; rollback lock unavailable — skipped" 2>/dev/null || true
fi
printf '{"ok":false,"changed":false,"wan_zone":%s,"error":"firewall_reload_failed"}' "$zone_json"
return 0
fi
logger -t fwlive "$success_msg" 2>/dev/null || true
printf '{"ok":true,"changed":true,"wan_zone":%s}' "$zone_json"
return 0
}
enable_wan_logging() {
zone=$(find_wan_zone_section)
if [ -z "$zone" ]; then
printf '{"ok":false,"changed":false,"wan_zone":null,"error":"no_wan_zone"}'
return 0
fi
zone_json=$(json_null_or_string "$zone")
if ! check_nf_log_ipv4 || ! check_nf_log_ipv6; then
printf '{"ok":false,"changed":false,"wan_zone":%s,"error":"nf_log_missing"}' "$zone_json"
return 0
fi
# Locked critical section: read->compute->stage->commit for firewall.<zone>.log.
# The log bit is re-read AFTER acquiring the lock so the target is computed
# from the latest committed value; a concurrent toggle cannot interleave.
# Staging + commit live inside commit_wan_log_change behind its last-moment
# firewall_changes_pending re-check (#191): a foreign writer racing between
# the early check above and the commit aborts the toggle instead of having
# its half-finished delta published with our log bit.
if ! acquire_wan_log_lock; then
printf '{"ok":false,"changed":false,"wan_zone":%s,"error":"lock_failed"}' "$zone_json"
return 0
fi
if firewall_changes_pending; then
release_wan_log_lock
printf '{"ok":false,"changed":false,"wan_zone":%s,"error":"firewall_changes_pending"}' "$zone_json"
return 0
fi
current=$(wan_zone_log_value "$zone")
if wan_filter_log_enabled "$current"; then
release_wan_log_lock
printf '{"ok":true,"changed":false,"wan_zone":%s}' "$zone_json"
return 0
fi
if ! maybe_snapshot_wan_log_baseline "$zone"; then
release_wan_log_lock
printf '{"ok":false,"changed":false,"wan_zone":%s,"error":"baseline_snapshot_failed"}' "$zone_json"
return 0
fi
target=$(wan_filter_log_target_value "$current")
if ! commit_wan_log_change "$zone" "$zone_json" "$target"; then
release_wan_log_lock
return 0
fi
release_wan_log_lock
reload_and_report_wan_log "$zone" "$current" "$target" \
'Firewall reload failed after enable; reverted UCI WAN log' \
'WAN zone logging enabled' \
"$zone_json"
return 0
}
disable_wan_logging() {
zone=$(find_wan_zone_section)
if [ -z "$zone" ]; then
printf '{"ok":false,"changed":false,"wan_zone":null,"error":"no_wan_zone"}'
return 0
fi
zone_json=$(json_null_or_string "$zone")
# Locked critical section: read->compute->stage->commit for firewall.<zone>.log.
# The log bit is re-read AFTER acquiring the lock so the target is computed
# from the latest committed value; a concurrent toggle cannot interleave.
# Staging + commit live inside commit_wan_log_change behind its last-moment
# firewall_changes_pending re-check (#191): a foreign writer racing between
# the early check above and the commit aborts the toggle instead of having
# its half-finished delta published with our log bit.
if ! acquire_wan_log_lock; then
printf '{"ok":false,"changed":false,"wan_zone":%s,"error":"lock_failed"}' "$zone_json"
return 0
fi
if firewall_changes_pending; then
release_wan_log_lock
printf '{"ok":false,"changed":false,"wan_zone":%s,"error":"firewall_changes_pending"}' "$zone_json"
return 0
fi
current=$(wan_zone_log_value "$zone")
if [ -z "$current" ] || ! wan_filter_log_enabled "$current"; then
release_wan_log_lock
printf '{"ok":true,"changed":false,"wan_zone":%s}' "$zone_json"
return 0
fi
target=$(wan_filter_log_clear_value "$current")
if ! commit_wan_log_change "$zone" "$zone_json" "$target"; then
release_wan_log_lock
return 0
fi
release_wan_log_lock
reload_and_report_wan_log "$zone" "$current" "$target" \
'Firewall reload failed after disable; reverted UCI WAN log' \
'WAN zone logging disabled' \
"$zone_json"
return 0
}
run_logging_selftest() {
if wan_filter_log_enabled ''; then
echo 'wan_filter_log_enabled empty: expected false' >&2
return 1
fi
if ! wan_filter_log_enabled '1'; then
echo 'wan_filter_log_enabled 1: expected true' >&2
return 1
fi
if ! wan_filter_log_enabled '3'; then
echo 'wan_filter_log_enabled 3: expected true' >&2
return 1
fi
if wan_filter_log_enabled '2'; then
echo 'wan_filter_log_enabled 2: expected false' >&2
return 1
fi
got=$(wan_filter_log_target_value '')
if [ "$got" != '1' ]; then
echo "wan_filter_log_target_value empty: expected 1 got $got" >&2
return 1
fi
got=$(wan_filter_log_target_value '2')
if [ "$got" != '3' ]; then
echo "wan_filter_log_target_value 2: expected 3 got $got" >&2
return 1
fi
got=$(wan_filter_log_target_value '1')
if [ "$got" != '1' ]; then
echo "wan_filter_log_target_value 1: expected 1 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')
if [ "$got" != '2' ]; then
echo "wan_filter_log_clear_value 3: expected 2 got $got" >&2
return 1
fi
got=$(wan_filter_log_clear_value '1')
if [ -n "$got" ]; then
echo "wan_filter_log_clear_value 1: expected empty got $got" >&2
return 1
fi
got=$(wan_filter_log_clear_value '2')
if [ "$got" != '2' ]; then
echo "wan_filter_log_clear_value 2: expected 2 got $got" >&2
return 1
fi
# Enable/disable parity around multi-bit values.
got=$(wan_filter_log_target_value '2')
if [ "$got" != '3' ]; then
echo "enable from 2: expected 3 got $got" >&2
return 1
fi
got=$(wan_filter_log_clear_value '3')
if [ "$got" != '2' ]; then
echo "disable from 3: expected 2 got $got" >&2
return 1
fi
return 0
}
+849
View File
@@ -0,0 +1,849 @@
#!/bin/sh
# SPDX-License-Identifier: Apache-2.0
# Copyright 2025-2026 Lucas Albers <lucas.b.albers@gmail.com>
#
# rpcd plugin: ubus object "fwlive"
# rules — rule hint map (no args)
# poll — firewall-only log tail; pass line count as addresses[0] (rpcd array schema)
# resolve — reverse DNS for IP addresses (BusyBox nslookup)
# logging_status — WAN zone logging readiness (no args)
# enable_wan_logging / disable_wan_logging — opt-in WAN zone log=1 (no args)
FW4_TAG='!fw4: '
LIBEXEC_DIR="$(cd "$(dirname "$0")/.." && pwd)"
FILTER_SH="$LIBEXEC_DIR/fwlive-log-filter.sh"
LOGGING_SH="$LIBEXEC_DIR/fwlive-logging.sh"
# Max reverse-DNS lookups per resolve call (~32 names keeps nslookup load bounded on small routers).
RESOLVE_MAX=32
# Per-lookup nslookup timeout (integer seconds — BusyBox `timeout` truncates
# fractional values to 0 on OpenWrt 23.05/1.36, so 0.5 would silently
# degrade to no timeout; luna fold 2026-08-10). The wall-clock budget
# below is the real whole-call bound; this per-lookup cap only prevents a
# single hung resolver from dominating the budget. LAN/local-resolver
# lookups are single-digit ms, so 1s is ample. Passed as argv to
# `timeout`, never interpolated into a shell string.
RESOLVE_TIMEOUT=1
# Wall-clock budget (seconds) for a single ubus fwlive.resolve call. The
# lookup loop checks elapsed time and aborts cleanly once this budget is
# spent, so a flood of unresolvable IPs cannot hold an rpcd worker for the
# full RESOLVE_MAX * RESOLVE_TIMEOUT worst case. Worst case total time is
# budget + one in-progress lookup (<= RESOLVE_TIMEOUT), i.e. ~6s.
RESOLVE_BUDGET=5
# Cap raw logd lines per poll (~2000 ≈ typical logd ring / ~200 KB JSON before filter).
POLL_LINES_MAX=2000
# Bound nft ruleset dumps (corrupted/hung nft must not block LuCI rules load).
NFT_TIMEOUT=5
# CLI-only iptables fixture path for __rulesmap_iptables (no arbitrary paths).
RULESMAP_IPTABLES_FILE='/tmp/rulesmap'
# json_escape lives in fwlive-logging.sh so prerm can source that file alone (#222).
# shellcheck disable=SC1090,SC1091 # LOGGING_SH resolves to sibling ../fwlive-logging.sh
. "$LOGGING_SH"
slug_key() {
echo "$1" | tr '[:upper:]' '[:lower:]' | tr ' _' '--'
}
map_add() {
key="$1"
val="$2"
[ -n "$key" ] || return 0
[ -n "$val" ] || return 0
esc_key=$(printf '%s' "$key" | json_escape)
# Global first-wins deduplication: skip if escaped key already present.
# 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).
case "$OUT" in
*"\"$esc_key\":"*) return 0 ;;
esac
esc_val=$(printf '%s' "$val" | json_escape)
if [ -n "$OUT" ]; then
OUT="${OUT},"
fi
OUT="${OUT}\"${esc_key}\":\"${esc_val}\""
}
uci_rule_names() {
uci -q show firewall 2>/dev/null | sed -n "s/^firewall\.@rule\[[0-9]*\]\.name='\(.*\)'$/\1/p"
}
is_uci_style_name() {
case "$1" in
*' '*|*'"'*|*"'"*) return 1 ;;
[!A-Za-z0-9_-]*) return 1 ;;
esac
return 0
}
normalize_log_prefix() {
printf '%s' "$1" | sed 's/[[:space:]:]*$//'
}
read_rpc_input() {
if [ -n "$1" ]; then
printf '%s' "$1"
else
cat
fi
}
map_prefix_with_label() {
prefix="$1"
label="$2"
prefix=$(normalize_log_prefix "$prefix")
[ -n "$prefix" ] || return 0
slug=$(slug_key "$prefix")
if [ -n "$label" ]; then
map_add "$prefix" "$label"
if [ "$slug" != "$prefix" ]; then
map_add "$slug" "$label"
fi
else
cosmetic=$(echo "$prefix" | tr '-' ' ')
map_add "$prefix" "$cosmetic"
if [ "$slug" != "$prefix" ]; then
map_add "$slug" "$cosmetic"
fi
fi
}
run_with_timeout() {
secs="$1"
shift
if command -v timeout >/dev/null 2>&1; then
timeout "$secs" "$@" 2>/dev/null
else
"$@" 2>/dev/null
fi
}
map_log_prefix_entry() {
prefix="$1"
comment="$2"
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
map_prefix_with_label "$prefix" "$label"
}
map_uci_rule_names() {
# Line-wise (here-doc, not an unquoted for-loop) so a name with
# spaces stays one token. Names that fail is_uci_style_name are
# skipped: parseRuleHint() cannot match them, and fragments of
# "My Rule" must not shadow a real rule named Rule (#226).
# The while-read stays in the current shell (here-doc, not a
# pipeline) so map_add mutations reach OUT.
while IFS= read -r name || [ -n "$name" ]; do
[ -n "$name" ] || continue
is_uci_style_name "$name" || continue
map_add "$name" "$name"
slug=$(slug_key "$name")
if [ "$slug" != "$name" ]; then
map_add "$slug" "$name"
fi
done <<EOF
$(uci_rule_names)
EOF
}
map_from_nft_stream() {
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.
prefix=$(echo "$line" | sed -n 's/.*log prefix "\([^"\\]*\(\\.[^"\\]*\)*\)".*/\1/p')
comment=$(echo "$line" | sed -n 's/.*comment "\([^"\\]*\(\\.[^"\\]*\)*\)".*/\1/p')
label=''
case "$comment" in
"${FW4_TAG}"*)
suffix="${comment#"${FW4_TAG}"}"
if is_uci_style_name "$suffix"; then
label="$suffix"
fi
;;
esac
if [ -n "$prefix" ]; then
map_prefix_with_label "$prefix" "$label"
elif [ -n "$label" ]; then
map_prefix_with_label "$label" "$label"
fi
done
}
map_from_iptables_save_stream() {
while IFS= read -r line; do
case "$line" in
-*) ;;
*) continue ;;
esac
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"
done
}
nft_list_ruleset() {
run_with_timeout "$NFT_TIMEOUT" nft list ruleset
}
detect_firewall_backend() {
if command -v nft >/dev/null 2>&1 && nft_list_ruleset >/dev/null; then
printf '%s' nft
return 0
fi
if command -v iptables-save >/dev/null 2>&1; then
printf '%s' iptables
return 0
fi
printf '%s' unknown
}
_fwlive_tmp_dir_ok() {
# Fail closed unless the dump dir is a real directory with the sticky
# bit. Without sticky, another uid can unlink a just-created mktemp
# file and replace it with a symlink before we reopen with `>` as
# root (same class as #204). Optional $1 is for CLI tests; production
# always uses /tmp.
dir="${1:-/tmp}"
[ -n "$dir" ] || return 1
[ -L "$dir" ] && return 1
[ -d "$dir" ] || return 1
# POSIX XCU `test -k`; BusyBox/dash implement it. shellcheck SC3065
# incorrectly treats -k as non-POSIX (and `stat -c` is not on OpenWrt).
# shellcheck disable=SC3065
[ -k "$dir" ]
}
_fwlive_mktemp() {
# Secure temp file for ruleset dumps. Uses mktemp only -- no
# predictable fallback (symlink arbitrary-write primitive when
# running as root under rpcd). Uses /tmp explicitly and does not
# honour TMPDIR. /tmp must be sticky (checked). Never rm+reuse,
# never touch/chmod into existence, template stays inside /tmp.
_fwlive_tmp_dir_ok || return 1
_mk_prefix="$1"
_mk_file=$(mktemp "/tmp/${_mk_prefix}.XXXXXX" 2>/dev/null) || return 1
[ -n "$_mk_file" ] || return 1
printf '%s' "$_mk_file"
}
build_rules_map() {
OUT=''
backend=$(detect_firewall_backend)
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"
fi
rm -f "$_tmp"
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"
fi
rm -f "$_tmp"
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"
fi
rm -f "$_tmp"
fi
fi
;;
esac
printf '{"backend":"%s","rules":{%s}}' "$backend" "$OUT"
}
rulesmap_from_iptables_file() {
file="$1"
OUT=''
[ "$file" = "$RULESMAP_IPTABLES_FILE" ] || return 1
[ -f "$file" ] || return 1
map_from_iptables_save_stream < "$file"
printf '{"backend":"iptables","rules":{%s}}' "$OUT"
}
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.
_val="$1"
case "$_val" in
''|*[!0-9]*)
printf '%s' "50"
return 0
;;
esac
_tmp=$(printf '%s' "$_val" | sed 's/^0*//')
if [ -z "$_tmp" ]; then
printf '%s' "50"
return 0
fi
if [ "${#_tmp}" -gt "${#POLL_LINES_MAX}" ]; then
printf '%s' "$POLL_LINES_MAX"
return 0
fi
if [ "$_tmp" -gt "$POLL_LINES_MAX" ]; then
printf '%s' "$POLL_LINES_MAX"
else
printf '%s' "$_tmp"
fi
}
poll_lines_from_input() {
input="$1"
lines=50
if ! command -v jshn >/dev/null 2>&1; then
printf '%s' "$lines"
return 0
fi
# Device-only helper; not present on host CI runners.
# shellcheck disable=SC1091
. /usr/share/libubox/jshn.sh
json_load "$input" 2>/dev/null || {
printf '%s' "$lines"
return 0
}
if json_select addresses 2>/dev/null; then
# json_get_var assigns into the named shell variable (jshn).
json_get_var first 1
json_select ..
# shellcheck disable=SC2154 # set by json_get_var above
case "$first" in
''|*[!0-9]*) ;;
*) lines="$first" ;;
esac
fi
lines=$(poll_clamp_lines "$lines")
printf '%s' "$lines"
}
fetch_firewall_logs() {
input="$1"
lines=$(poll_lines_from_input "$input")
# lines is numeric-clamped; stream/oneshot are fixed (not caller-controlled).
raw=$(ubus call log read "{\"lines\":$lines,\"stream\":false,\"oneshot\":true}" 2>/dev/null) \
|| raw='{"log":[]}'
# Do not swallow a filter failure into a silent empty table (#220).
# If the filter exits non-zero but still printed JSON, keep it.
# Empty success is not valid poll JSON — the shipped filter always
# prints {"log":[...]}, but a stub/regression must not emit a blank body.
if ! out=$(printf '%s' "$raw" | "$FILTER_SH"); then
if [ -z "$out" ]; then
out='{"log":[],"error":"filter_failed"}'
fi
fi
[ -n "$out" ] || out='{"log":[]}'
printf '%s' "$out"
}
poll_logs() {
# Optional JSON on $1 (rpcd may also pass argv $3 at the call site); else stdin.
input=$(read_rpc_input "$1")
fetch_firewall_logs "$input"
}
# Parse BusyBox and bind-utils nslookup reverse output. Prefer
# "<ptr> name = <host>"; fall back to "Address N: <ip> <host>".
# Trailing dots are stripped. Empty / server-only output is not a name.
parse_nslookup_name() {
# Prefer "<ptr> name = <host>". The "Address N: ip host" fallback
# must ignore the Server block (BusyBox mini nslookup prints the
# resolver via the same Address N: format first). Only accept
# Address N: after a blank line (the answer section).
printf '%s\n' "$1" | awk '
tolower($0) ~ /name[ \t]*=/ {
sub(/.*[Nn][Aa][Mm][Ee][ \t]*=[ \t]*/, "")
gsub(/[ \t].*$/, "")
gsub(/\.$/, "")
if ($0 != "") { print; exit }
}
NF == 0 { in_answer = 1; next }
in_answer && $1 == "Address" && $2 ~ /^[0-9]+:/ && NF >= 4 {
host = $4
gsub(/\.$/, "", host)
if (host != "") { print host; exit }
}
'
}
resolve_hostname() {
ip="$1"
[ -n "$ip" ] || return 1
is_resolvable_address "$ip" || return 1
command -v nslookup >/dev/null 2>&1 || return 1
out=$(run_with_timeout "$RESOLVE_TIMEOUT" nslookup "$ip")
[ -n "$out" ] || return 1
name=$(parse_nslookup_name "$out")
[ -n "$name" ] || return 1
printf '%s' "$name"
}
# Strict IPv4/IPv6 validation before nslookup (issue #190): the old char-class
# check admitted hostname-shaped tokens of hex chars + dots/colons
# (e.g. "dead.beef.cafe.baad"), letting the resolver fall back to upstream DNS
# queries chosen by an authenticated session. Validate the real address
# families here so nslookup only ever sees genuine numeric addresses.
# Embedded-IPv4 tails ("::ffff:192.0.2.1") are fully validated, not failed
# closed. awk is already a dependency of this script (json_escape).
is_resolvable_address() {
addr="$1"
[ -n "$addr" ] || return 1
# Reject anything outside the address alphabet BEFORE awk, so embedded
# newlines/whitespace (which awk would treat as record separators) can
# never let a valid first record mask trailing garbage that still reaches
# nslookup (#190, CodeRabbit/luna fold): nslookup only ever sees a single
# token. Note the check intentionally allows only hex/colon/dot.
case "$addr" in
*[!0-9a-fA-F:.]*) return 1 ;;
esac
printf '%s\n' "$addr" | awk '
function is_ipv4(s, n, o, i) {
if (s !~ /^[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*$/)
return 0
n = split(s, o, ".")
if (n != 4)
return 0
for (i = 1; i <= 4; i++)
if (o[i] + 0 > 255)
return 0
return 1
}
function is_hexgroup(g) {
return (g ~ /^[0-9a-fA-F][0-9a-fA-F]?[0-9a-fA-F]?[0-9a-fA-F]?$/)
}
function is_ipv6(s, k, head, tail, lc, v4, extra, nh, nt, h, t, i) {
if (s !~ /^[0-9a-fA-F:.]+$/)
return 0
if (s ~ /:::/)
return 0
k = index(s, "::")
if (k > 0) {
if (index(substr(s, k + 2), "::") > 0)
return 0
head = substr(s, 1, k - 1)
tail = substr(s, k + 2)
} else {
head = ""
tail = s
}
extra = 0
if (tail ~ /\./) {
lc = length(tail)
while (lc > 0 && substr(tail, lc, 1) != ":")
lc--
if (lc == 0) {
if (!is_ipv4(tail))
return 0
tail = ""
} else {
v4 = substr(tail, lc + 1)
if (!is_ipv4(v4))
return 0
tail = substr(tail, 1, lc - 1)
}
extra = 2
}
nh = (head == "" ? 0 : split(head, h, ":"))
nt = (tail == "" ? 0 : split(tail, t, ":"))
for (i = 1; i <= nh; i++)
if (!is_hexgroup(h[i]))
return 0
for (i = 1; i <= nt; i++)
if (!is_hexgroup(t[i]))
return 0
if (k > 0)
return (nh + nt + extra <= 7)
return (nh + nt + extra == 8)
}
NR == 1 {
valid = (($0 ~ /:/) ? is_ipv6($0) : is_ipv4($0))
next
}
END { exit (NR == 1 && valid) ? 0 : 1 }
'
}
resolve_addresses() {
# Missing resolver must not look like "no PTR" (#218). Check before
# starting the budget clock or reading stdin.
if ! command -v nslookup >/dev/null 2>&1; then
command -v logger >/dev/null 2>&1 && logger -t fwlive "nslookup not found; resolve disabled"
printf '{"names":{},"error":"no_resolver"}'
return 0
fi
# Wall-clock budget clock starts at the TRUE function entry (luna
# folds 2026-08-10): the budget must bound the whole call — including
# read_rpc_input (stalled stdin must not escape the budget) and json
# parsing. `date +%s` can jump under NTP sync — accepted (worst case
# the budget over- or under-runs by the jump; worker starvation is
# still prevented).
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
OUT=''
if ! command -v jshn >/dev/null 2>&1; then
printf '{"names":{}}'
return 0
fi
# shellcheck disable=SC1091
. /usr/share/libubox/jshn.sh
json_load "$input" 2>/dev/null || {
printf '{"names":{}}'
return 0
}
if json_select addresses 2>/dev/null; then
idx=1
# json_get_type / json_get_var assign into named shell variables (jshn).
# shellcheck disable=SC2154
while json_get_type atype "$idx" && [ "$atype" = string ]; do
[ "$count" -ge "$RESOLVE_MAX" ] && break
# 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
json_get_var ip "$idx"
if ! is_resolvable_address "$ip"; then
idx=$((idx + 1))
continue
fi
name=$(resolve_hostname "$ip")
if [ -n "$name" ]; then
map_add "$ip" "$name"
count=$((count + 1))
fi
idx=$((idx + 1))
done
json_select ..
fi
printf '{"names":{%s}}' "$OUT"
}
run_selftest() {
input=$(printf 'a\\b"c\nd\te')
escaped=$(printf '%s' "$input" | json_escape)
expected=$(printf 'a\\\\b\\"c\\nd\\te')
if [ "$escaped" != "$expected" ]; then
echo "json_escape: mismatch" >&2
return 1
fi
# Remaining C0 controls must become \u00XX (RFC 8259), not raw bytes.
input=$(printf 'x\001y\037z')
escaped=$(printf '%s' "$input" | json_escape)
expected='x\u0001y\u001fz'
if [ "$escaped" != "$expected" ]; then
echo "json_escape C0: expected $expected got $escaped" >&2
return 1
fi
# Blank lines are data (paragraph-mode RS="" used to drop them).
input=$(printf 'a\n\nb')
escaped=$(printf '%s' "$input" | json_escape)
expected=$(printf 'a\\n\\nb')
if [ "$escaped" != "$expected" ]; then
echo "json_escape blank line: expected $expected got $escaped" >&2
return 1
fi
if is_resolvable_address 'not an ip'; then
echo 'is_resolvable_address: expected reject for spaces' >&2
return 1
fi
# Literal metachar token (must not expand) — reject before nslookup.
# shellcheck disable=SC2016
if is_resolvable_address '$(reboot)'; then
echo 'is_resolvable_address: expected reject for metachar' >&2
return 1
fi
if is_resolvable_address 'example.com'; then
echo 'is_resolvable_address: expected reject for hostname' >&2
return 1
fi
if ! is_resolvable_address '192.0.2.1'; then
echo 'is_resolvable_address: expected accept IPv4' >&2
return 1
fi
if ! is_resolvable_address '2001:db8::1'; then
echo 'is_resolvable_address: expected accept IPv6' >&2
return 1
fi
if ! is_resolvable_address '::1'; then
echo 'is_resolvable_address: expected accept IPv6 loopback ::1' >&2
return 1
fi
# Issue #190: hostname-shaped tokens of hex chars + dots/colons passed the
# old char-class check and made the resolver fall back to upstream DNS. Each case
# below must REJECT (and would be ACCEPTED by the reverted char-class code).
if is_resolvable_address 'ab.cd.ef.01'; then
echo 'is_resolvable_address: expected reject for dotted-hex ab.cd.ef.01' >&2
return 1
fi
if is_resolvable_address 'dead.beef.cafe.baad'; then
echo 'is_resolvable_address: expected reject for dotted-hex dead.beef.cafe.baad' >&2
return 1
fi
if is_resolvable_address 'a.b.c.d.e.f'; then
echo 'is_resolvable_address: expected reject for 6-group dotted token a.b.c.d.e.f' >&2
return 1
fi
if is_resolvable_address 'aa:bb:cc:dd'; then
echo 'is_resolvable_address: expected reject for under-populated IPv6 aa:bb:cc:dd (no ::)' >&2
return 1
fi
if is_resolvable_address '123.456.1.1'; then
echo 'is_resolvable_address: expected reject for octet > 255 in 123.456.1.1' >&2
return 1
fi
# Embedded-IPv4 tail must validate, not fail closed.
if ! is_resolvable_address '::ffff:192.0.2.1'; then
echo 'is_resolvable_address: expected accept IPv6-mapped ::ffff:192.0.2.1' >&2
return 1
fi
# Multi-line input must be rejected wholesale (CodeRabbit/luna fold): awk
# reads newline-separated records, so a valid first record must not mask
# trailing garbage that would still reach nslookup.
if is_resolvable_address "$(printf '192.0.2.1\nnot-an-address')"; then
echo 'is_resolvable_address: expected reject for multi-line IPv4 token' >&2
return 1
fi
if is_resolvable_address "$(printf '2001:db8::1\nextra')"; then
echo 'is_resolvable_address: expected reject for multi-line IPv6 token' >&2
return 1
fi
# Escaped-quote capture: a log prefix containing \" must not be truncated
# at the escaped quote (nft dumps strings with backslash escapes).
got=$(printf '%s\n' 'log prefix "My \" Rule"' | sed -n 's/.*log prefix "\([^"\\]*\(\\.[^"\\]*\)*\)".*/\1/p')
if [ "$got" != 'My \" Rule' ]; then
echo "escaped-quote prefix capture: expected 'My \\\" Rule' got '$got'" >&2
return 1
fi
# Plain prefix still captures unchanged.
got=$(printf '%s\n' 'log prefix "Plain Rule"' | sed -n 's/.*log prefix "\([^"\\]*\(\\.[^"\\]*\)*\)".*/\1/p')
if [ "$got" != 'Plain Rule' ]; then
echo "plain prefix capture: expected 'Plain Rule' got '$got'" >&2
return 1
fi
# Empty prefix edge.
got=$(printf '%s\n' 'log prefix ""' | sed -n 's/.*log prefix "\([^"\\]*\(\\.[^"\\]*\)*\)".*/\1/p')
if [ "$got" != '' ]; then
echo "empty prefix capture: expected '' got '$got'" >&2
return 1
fi
got=$(poll_clamp_lines "99999999999999999999")
if [ "$got" != "$POLL_LINES_MAX" ]; then
echo "poll_clamp_lines: over-long expected $POLL_LINES_MAX got $got" >&2
return 1
fi
got=$(poll_clamp_lines "0")
if [ "$got" != "50" ]; then
echo "poll_clamp_lines: zero expected 50 got $got" >&2
return 1
fi
got=$(poll_clamp_lines "2001")
if [ "$got" != "$POLL_LINES_MAX" ]; then
echo "poll_clamp_lines: 2001 expected $POLL_LINES_MAX got $got" >&2
return 1
fi
got=$(poll_clamp_lines "500")
if [ "$got" != "500" ]; then
echo "poll_clamp_lines: expected 500 got $got" >&2
return 1
fi
if ! _fwlive_tmp_dir_ok; then
echo "_fwlive_tmp_dir_ok: /tmp must be a sticky directory" >&2
return 1
fi
got=$(parse_nslookup_name "Server: 127.0.0.1
Address: 127.0.0.1:53
8.8.8.8.in-addr.arpa name = dns.google.")
if [ "$got" != "dns.google" ]; then
echo "parse_nslookup_name: bind-style expected dns.google got '$got'" >&2
return 1
fi
got=$(parse_nslookup_name "Server: 127.0.0.1
Address: 127.0.0.1:53
Address 1: 8.8.8.8 dns.google.")
if [ "$got" != "dns.google" ]; then
echo "parse_nslookup_name: busybox Address N expected dns.google got '$got'" >&2
return 1
fi
got=$(parse_nslookup_name "Server: 192.168.1.1
Address 1: 192.168.1.1 router.lan
Name: 8.8.8.8
Address 1: 8.8.8.8 dns.google")
if [ "$got" != "dns.google" ]; then
echo "parse_nslookup_name: mini nslookup must skip resolver Address N, got '$got'" >&2
return 1
fi
got=$(parse_nslookup_name "Server: 127.0.0.1
Address: 127.0.0.1:53")
if [ -n "$got" ]; then
echo "parse_nslookup_name: server-only expected empty got '$got'" >&2
return 1
fi
run_logging_selftest || return 1
if ! command -v jshn >/dev/null 2>&1; then
echo "skip: jshn not available (poll cap via jshn not tested)" >&2
return 0
fi
got=$(poll_lines_from_input '{"addresses":["999999"]}')
if [ "$got" != "$POLL_LINES_MAX" ]; then
echo "poll_lines_from_input: expected $POLL_LINES_MAX got $got" >&2
return 1
fi
got=$(poll_lines_from_input '{"addresses":["99999999999999999999"]}')
if [ "$got" != "$POLL_LINES_MAX" ]; then
echo "poll_lines_from_input: over-long expected $POLL_LINES_MAX got $got" >&2
return 1
fi
got=$(poll_lines_from_input '{"addresses":["18446744073709551616"]}')
if [ "$got" != "$POLL_LINES_MAX" ]; then
echo "poll_lines_from_input: 18446744073709551616 expected $POLL_LINES_MAX got $got" >&2
return 1
fi
got=$(poll_lines_from_input '{"addresses":["0"]}')
if [ "$got" != "50" ]; then
echo "poll_lines_from_input: zero expected 50 got $got" >&2
return 1
fi
got=$(poll_lines_from_input '{"addresses":["2001"]}')
if [ "$got" != "$POLL_LINES_MAX" ]; then
echo "poll_lines_from_input: 2001 expected $POLL_LINES_MAX got $got" >&2
return 1
fi
got=$(poll_lines_from_input '{"addresses":["500"]}')
if [ "$got" != "500" ]; then
echo "poll_lines_from_input: expected 500 got $got" >&2
return 1
fi
got=$(poll_lines_from_input '{"addresses":["100"]}')
if [ "$got" != "100" ]; then
echo "poll_lines_from_input: expected 100 got $got" >&2
return 1
fi
return 0
}
case "$1" in
__selftest)
run_selftest
exit $?
;;
__rulesmap_iptables)
# CLI selftest only — never a ubus method. Fixed path only (no argv file read).
rulesmap_from_iptables_file "$RULESMAP_IPTABLES_FILE"
exit $?
;;
__poll_clamp)
poll_clamp_lines "$2"
exit $?
;;
__tmp_dir_ok)
_fwlive_tmp_dir_ok "$2"
exit $?
;;
__resolve_one)
if name=$(resolve_hostname "$2"); then
printf '%s\n' "$name"
exit 0
fi
exit 1
;;
__parse_nslookup)
name=$(parse_nslookup_name "$2")
if [ -n "$name" ]; then
printf '%s\n' "$name"
exit 0
fi
printf '\n'
exit 1
;;
list)
# Method names only — args objects describe call parameters.
# "backend" is a rules reply field, not a method.
echo '{"rules":{},"poll":{"addresses":[]},"resolve":{"addresses":[]},"logging_status":{},"enable_wan_logging":{},"disable_wan_logging":{}}'
;;
call)
case "$2" in
rules)
build_rules_map
;;
poll)
poll_logs "$3"
;;
resolve)
resolve_addresses "$3"
;;
logging_status)
build_logging_status_json
;;
enable_wan_logging)
enable_wan_logging
;;
disable_wan_logging)
disable_wan_logging
;;
*)
echo '{"error":"Method not found"}'
exit 1
;;
esac
;;
esac
@@ -0,0 +1,13 @@
{
"admin/status/fwlive": {
"title": "Firewall Live View",
"order": 5,
"action": {
"type": "view",
"path": "status/fwlive"
},
"depends": {
"acl": [ "luci-app-fwlive" ]
}
}
}
@@ -0,0 +1,15 @@
{
"luci-app-fwlive": {
"description": "Grant access to firewall live log view",
"read": {
"ubus": {
"fwlive": [ "rules", "poll", "resolve", "logging_status" ]
}
},
"write": {
"ubus": {
"fwlive": [ "enable_wan_logging", "disable_wan_logging" ]
}
}
}
}
+1 -1
View File
@@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall
PKG_VERSION:=26.8.26
PKG_RELEASE:=253
PKG_RELEASE:=254
PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \
@@ -151,8 +151,8 @@ end
o = s:taboption("Main", Value, "node_socks_port", translate("Node") .. " Socks " .. translate("Listen Port"))
o.default = 1070
o.placeholder = 1070
o.datatype = "range(1,65535)"
o.rmempty = false
o:depends({ node = "", ["!reverse"] = true })
--[[
if has_singbox or has_xray then
@@ -47,8 +47,8 @@ o:depends("console_auth", true)
---- Console Port
o = s:option(Value, "console_port", translate("Console Port"), translate("In the browser input routing IP plus port access, such as:192.168.1.1:1188"))
o.default = 1188
o.placeholder = 1188
o.datatype = "range(1,65535)"
o.rmempty = false
o:depends("balancing_enable", true)
o = s:option(Flag, "bind_local", translate("Haproxy Port") .. " " .. translate("Bind Local"), translate("When selected, it can only be accessed localhost."))
@@ -2,7 +2,7 @@
local map = self.map
local api = map.api
local haproxy_enable = map:get("@global_haproxy[0]", "balancing_enable") or "0"
local console_port = map:get("@global_haproxy[0]", "console_port") or ""
local console_port = map:get("@global_haproxy[0]", "console_port") or "1188"
-%>
<p id="_status"></p>
@@ -1072,7 +1072,6 @@ start_dns() {
esac
# 追加直连DNS到iptables/nftables
[ "$(config_n_get @global_haproxy[0] balancing_enable 0)" != "1" ] && IPT_APPEND_DNS=
add_default_port() {
[ -z "$1" ] && echo "" || echo "$1" | awk -F',' '{for(i=1;i<=NF;i++){if($i !~ /#/) $i=$i"#53";} print $0;}' OFS=','
}
@@ -1338,7 +1337,7 @@ start_haproxy() {
fi
local haproxy_path=$TMP_PATH/haproxy
local haproxy_conf="config.cfg"
lua $APP_PATH/haproxy.lua -path ${haproxy_path} -conf ${haproxy_conf} -dns ${LOCAL_DNS}
lua $APP_PATH/haproxy.lua -path ${haproxy_path} -conf ${haproxy_conf}
ln_run "$(first_type haproxy)" haproxy "/dev/null" -f "${haproxy_path}/${haproxy_conf}"
}
@@ -6,9 +6,7 @@ local fs = api.fs
local uci = api.uci
local sys = api.sys
local log = function(...)
api.log(...)
end
local log = api.log
function get_ip_port_from(str)
local result_port = sys.exec("echo -n " .. str .. " | sed -n 's/^.*[:#]\\([0-9]*\\)$/\\1/p'")
@@ -19,13 +17,13 @@ end
local var = api.get_args(arg)
local haproxy_path = var["-path"]
local haproxy_conf = var["-conf"]
local haproxy_dns = var["-dns"] or "119.29.29.29:53,223.5.5.5:53"
local haproxy_dns = "127.0.0.1"
local cpu_thread = sys.exec('echo -n $(cat /proc/cpuinfo | grep "processor" | wc -l)') or "1"
local health_check_type = api.uci_get_c("@global_haproxy[0]", "health_check_type") or "tcp"
local health_check_inter = api.uci_get_c("@global_haproxy[0]", "health_check_inter") or "20"
local balancingStrategy = api.uci_get_c("@global_haproxy[0]", "balancingStrategy") or "roundrobin"
local console_port = api.uci_get_c("@global_haproxy[0]", "console_port")
local console_port = api.uci_get_c("@global_haproxy[0]", "console_port") or "1188"
local bind_local = api.uci_get_c("@global_haproxy[0]", "bind_local") or "0"
local bind_address = "0.0.0.0"
if bind_local == "1" then bind_address = "127.0.0.1" end
@@ -54,7 +52,7 @@ defaults
log global
option tcplog
option dontlognull
option http-server-close
option abortonclose
#option forwardfor except 127.0.0.0/8
option redispatch
retries 2
+1 -1
View File
@@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall2
PKG_VERSION:=26.8.27
PKG_RELEASE:=105
PKG_RELEASE:=106
PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \
@@ -52,7 +52,7 @@ defaults
log global
option tcplog
option dontlognull
option http-server-close
option abortonclose
#option forwardfor except 127.0.0.0/8
option redispatch
retries 2
+3 -3
View File
@@ -15,12 +15,12 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-wwand
PKG_RELEASE:=8
PKG_RELEASE:=9
PKG_SOURCE_PROTO:=git
PKG_SOURCE_URL:=https://github.com/ddimension/luci-app-wwand.git
PKG_SOURCE_VERSION:=0a1cb01913fc2ec77c8fa5246ba7c8ee17ffd54c
PKG_SOURCE_DATE:=2026-08-30
PKG_SOURCE_VERSION:=88a711251296810ea3428d32a3da9c5b635cea0c
PKG_SOURCE_DATE:=2026-08-31
PKG_MIRROR_HASH:=skip
PKG_LICENSE:=GPL-2.0-only
+3 -3
View File
@@ -18,12 +18,12 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-proto-wwand
PKG_RELEASE:=4
PKG_RELEASE:=5
PKG_SOURCE_PROTO:=git
PKG_SOURCE_URL:=https://github.com/ddimension/luci-proto-wwand.git
PKG_SOURCE_VERSION:=4d101fd7903bdad30d6220bc155d57469495f68c
PKG_SOURCE_DATE:=2026-08-30
PKG_SOURCE_VERSION:=35f0323f5621eb06e69991f5819ff53e4ca8cc7a
PKG_SOURCE_DATE:=2026-08-31
PKG_MIRROR_HASH:=skip
PKG_LICENSE:=GPL-2.0-only
+1 -1
View File
@@ -17,7 +17,7 @@ LUCI_NAME:=luci-theme-footstrap
FOOTSTRAP_VERSION?=
ifneq ($(FOOTSTRAP_VERSION),)
PKG_VERSION:=$(FOOTSTRAP_VERSION)
PKG_RELEASE:=47
PKG_RELEASE:=48
endif
LUCI_TITLE:=Footstrap Theme
@@ -334,6 +334,27 @@ function putBack(el, was) {
const at = sc ? sc.scrollTop : window.scrollY;
if (sc) sc.scrollTop = at + drift; else window.scrollTo(0, at + drift);
_restAt = scrollTop();
/* AND WHERE THE ELEMENT ACTUALLY LANDED, which is not always `was`. The line above used to be
* the whole of it, on the reasoning this comment states the write is exactly the drift, so the
* element is back at the top it was remembered at. It is not whenever the write was CLAMPED
* SHORT, a case the line above already allows for by re-reading the offset: the page had less
* room than the drift asked for, the element stops wherever the clamp left it, and `_rest` goes
* on naming a top nothing can reach. Every later tick then measures that unreachable difference
* and spends it on the reader: measured on webkit/Overview @390 top, `_rest` claiming 93.02 for
* an element standing at 105, and the reader 12px off on the next tick (52px at normal density).
*
* Two rects on a layout the write has already forced. The section half is re-read for the same
* reason and cannot be measured by the same probe the sweep's swap returns the very nodes it
* took out, so `_rest.el` survives it and the fallback is never reached, while a real
* `dom.content()` puts NEW nodes in and lateDrift() corrects against `sec`/`secTop` instead.
*
* `_rest.at` is deliberately NOT touched: it belongs to anchorFor(), on the path where the
* engine does no anchoring, and this fault is on the other one measured, the sweep is green
* on all 18 cells of the failing axis without it. */
if (_rest) {
if (_rest.el && _rest.el.isConnected) _rest.top = _rest.el.getBoundingClientRect().top;
if (_rest.sec && _rest.sec.isConnected) _rest.secTop = _rest.sec.getBoundingClientRect().top;
}
}
function noteMotion() {
+2 -2
View File
@@ -5,8 +5,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=sing-box
PKG_VERSION:=1.13.21
PKG_RELEASE:=28
PKG_VERSION:=1.14.0
PKG_RELEASE:=29
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/SagerNet/sing-box/tar.gz/v$(PKG_VERSION)?
+3 -3
View File
@@ -43,11 +43,11 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=wwand
PKG_RELEASE:=15
PKG_RELEASE:=16
PKG_SOURCE_PROTO:=git
PKG_SOURCE_URL:=https://github.com/ddimension/wwand.git
PKG_SOURCE_VERSION:=95acd0064d8307cde67a05c5a93bf9e62254ab8d
PKG_SOURCE_VERSION:=68a3f844a310fe9d7216d8febc496856ee5e0054
PKG_SOURCE_DATE:=2026-08-31
PKG_MIRROR_HASH:=skip
@@ -94,7 +94,7 @@ WWAND_BASE_UC:=apndb.uc atcmd.uc atcmd_parse.uc atport.uc backend.uc board.uc \
modem_common.uc modem_init_qmi.uc modem_quirks.uc modeswitch.uc ncm_vendors.uc \
netlink.uc netsel_ops.uc protocol_switch.uc reconnect.uc recovery.uc regdetail.uc \
sim.uc sim_plmn.uc simops.uc sms.uc sms_pdu.uc telemetry_mbim.uc telemetry_ncm.uc \
telemetry_qmi.uc transport.uc ubus.uc
telemetry_qmi.uc transport.uc ubus.uc version.uc
WWAND_BASE_CODEC:=arfcn_bands.uc hex.uc qmux.uc tlv.uc
WWAND_BASE_SCHEMA:=cat.uc ctl.uc dms.uc dsd.uc loc.uc loc_lazy.uc merge.uc nas.uc pdc.uc rat.uc \
tmd.uc uim.uc wda.uc wds.uc wms.uc wms_lazy.uc