mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-09-10 18:34:18 +08:00
⛅ Sync 2026-08-31 22:31:57
This commit is contained in:
+7
-5
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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 LuCI’s 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
|
||||
}),
|
||||
' ',
|
||||
_('Don’t 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(_('I’ll 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(_('I’ll 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
@@ -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 "Don’t show this again"
|
||||
msgstr "Don’t 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 "I’ll configure this under Network → Firewall"
|
||||
msgstr "I’ll 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"
|
||||
@@ -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 "Don’t show this again"
|
||||
msgstr "Don’t 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 "I’ll configure this under Network → Firewall"
|
||||
msgstr "I’ll 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 "Don’t 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 "I’ll 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 ""
|
||||
Symlink
+1
@@ -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 "Don’t show this again"
|
||||
msgstr "Don’t 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 "I’ll configure this under Network → Firewall"
|
||||
msgstr "I’ll 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
@@ -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
@@ -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
|
||||
}
|
||||
Executable
+849
@@ -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" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user